From 5e363f00673a9d8b88721c9dfaf367502703571e Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 5 Nov 2025 08:16:25 +0100 Subject: [PATCH 001/152] Remove problematic entities from data --- src/data/mod.rs | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/data/mod.rs b/src/data/mod.rs index d016035..6586394 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(), + name: "factory_game::express_transport_belt".to_string().into(), + 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() From e925ee923eaf1d31c1f3a0c58c67b1b3835b213f Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 5 Nov 2025 08:16:54 +0100 Subject: [PATCH 002/152] Allow picking mining_drills for hotbar --- src/frontend/action/action_state_machine.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index b494cf5..03210c0 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -1290,7 +1290,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 +1305,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 +1414,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!(), }; From fd626a5f0601cd8cf9fba83fab7f698cb3563dc2 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 5 Nov 2025 08:49:25 +0100 Subject: [PATCH 003/152] Do not rename belt for now since it breaks blueprints --- src/data/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/mod.rs b/src/data/mod.rs index 6586394..2add391 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1513,7 +1513,7 @@ impl RawDataStore { belt_infos: vec![ // TODO: For now only have one kind of transport belt since connection is still borked BeltInfo { - name: "factory_game::express_transport_belt".to_string().into(), + name: "factory_game::fast_transport_belt".to_string().into(), display_name: "Express Transport Belt".to_string(), has_underground: Some(BeltUndergroundInfo { max_distance: 9 }), has_splitter: None, From 28db90bb171c28a9b8b51c85bfc4e80536630ced Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 5 Nov 2025 09:51:10 +0100 Subject: [PATCH 004/152] Add current_tick to power_grid update --- src/power/mod.rs | 2 +- src/power/power_grid.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/power/mod.rs b/src/power/mod.rs index 0d84e66..8ffc785 100644 --- a/src/power/mod.rs +++ b/src/power/mod.rs @@ -1021,7 +1021,7 @@ impl PowerGridStorage PowerGrid, ) -> ( ResearchProgress, From 95eb141c9a42f77e36dda3fa67f7a724a36354d4 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 5 Nov 2025 09:51:21 +0100 Subject: [PATCH 005/152] Remove dbg log --- src/rendering/render_world.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 30ee5a2..d889697 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -2201,7 +2201,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) From c5697615efc535f026e3980ce77fbb2c26ec6acc Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 5 Nov 2025 09:51:34 +0100 Subject: [PATCH 006/152] Stop overlapping for solar field --- test_blueprints/solar_tile.bp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 4e377e047c32a896d4d10de2fa86306e768acb78 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 5 Nov 2025 09:52:04 +0100 Subject: [PATCH 007/152] Change beacons to scale linearly every 60 ticks (instead of a binary on/off) --- src/power/power_grid.rs | 88 +++++++++++++++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/src/power/power_grid.rs b/src/power/power_grid.rs index 438612d..63cc059 100644 --- a/src/power/power_grid.rs +++ b/src/power/power_grid.rs @@ -128,6 +128,7 @@ pub struct PowerGrid { 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 @@ -2642,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![] }; From 5e36d425d8567d3220c46f219219d50c0637f0f2 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 7 Nov 2025 09:55:40 +0100 Subject: [PATCH 008/152] Allow hand size/movetime on sushi belts, fixing a inserter bug that only happened on the first placed inserter per belt --- src/belt/mod.rs | 18 +++++----- src/belt/smart.rs | 10 +++--- src/belt/sushi.rs | 66 +++++++++++++++++++++++++---------- src/rendering/render_world.rs | 10 ++++-- 4 files changed, 70 insertions(+), 34 deletions(-) diff --git a/src/belt/mod.rs b/src/belt/mod.rs index c9f81fa..50cbe1d 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!"); }, } diff --git a/src/belt/smart.rs b/src/belt/smart.rs index 0e93852..f13321e 100644 --- a/src/belt/smart.rs +++ b/src/belt/smart.rs @@ -46,10 +46,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 +94,8 @@ pub struct BeltInserterInfo { pub outgoing: bool, pub state: InserterState, pub connection: FakeUnionStorage, + pub hand_size: ITEMCOUNTTYPE, + pub movetime: u8, } #[derive(Debug)] @@ -180,7 +178,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 +345,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; 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/rendering/render_world.rs b/src/rendering/render_world.rs index d889697..14c10d2 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -1,5 +1,4 @@ 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::blueprint::blueprint_string::BlueprintString; use crate::chest::ChestSize; @@ -909,11 +908,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 +964,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); From 4ac6f31dfd93710564fbf06f53baccf772d07d8c Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 7 Nov 2025 09:56:08 +0100 Subject: [PATCH 009/152] add default hotbar assignment --- src/frontend/action/action_state_machine.rs | 62 +++++++++++++++++++-- src/rendering/render_world.rs | 5 +- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index 03210c0..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, } } diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 14c10d2..86ac5fb 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -1,5 +1,6 @@ use crate::belt::belt::Belt; 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; @@ -2302,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])) { @@ -2335,7 +2336,7 @@ pub fn render_ui< entities: EntityPlaceOptions::Single(PlaceEntityType::MiningDrill { pos: assembler_pos, rotation: inserter_rotation, - ty: 1, + ty: 0, }), force: false, })); From 910353e6578a34931989f3a11433ec6bdf54d12d Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 7 Nov 2025 09:57:42 +0100 Subject: [PATCH 010/152] Fix failing assertion in assembler join --- src/assembler/simd.rs | 61 ++++++++----------------------------------- 1 file changed, 11 insertions(+), 50 deletions(-) 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)] From 0313293aae791ffa609ec49a1857448ba7f2a75a Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sat, 8 Nov 2025 06:39:32 +0100 Subject: [PATCH 011/152] Add Accumulator sprite --- src/rendering/mod.rs | 5 +++++ src/rendering/render_world.rs | 4 ++-- .../temp_assets/krastorio/energy-storage.png | Bin 0 -> 71532 bytes 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 src/rendering/temp_assets/krastorio/energy-storage.png 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 86ac5fb..7f57a49 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -1210,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 @@ -1707,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, 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 Date: Sat, 8 Nov 2025 10:16:18 +0100 Subject: [PATCH 012/152] Remove the concept of Storage translation, and fix issues with fluid index updates when merging power grids, allowing full merges of the gigabase test --- src/app_state.rs | 66 ++++----- src/belt/mod.rs | 12 +- src/belt/smart.rs | 19 ++- 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/liquid/mod.rs | 11 +- src/par_generation.rs | 3 +- src/power/mod.rs | 62 ++++++++- src/storage_list.rs | 92 ++++++++++++- 15 files changed, 317 insertions(+), 161 deletions(-) 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/belt/mod.rs b/src/belt/mod.rs index 50cbe1d..fcfe7de 100644 --- a/src/belt/mod.rs +++ b/src/belt/mod.rs @@ -3012,7 +3012,8 @@ impl BeltStore { belt_id.item, new_src, data_store, - ), + ) + .unwrap(), ); }, AnyBelt::Sushi(index) => { @@ -3020,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"), @@ -3047,7 +3049,8 @@ impl BeltStore { belt_id.item, new_dest, data_store, - ), + ) + .unwrap(), ); }, AnyBelt::Sushi(index) => { @@ -3055,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 f13321e..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, @@ -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/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/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 8ffc785..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( #[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] From 04a5144f993f11e964a49f24fef654a93704731f Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sat, 8 Nov 2025 10:37:09 +0100 Subject: [PATCH 013/152] Update Readme --- README.md | 50 ++++++++++++-------------------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) 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 From 444951d5a67384d5110c110bef896b4b8a70ac01 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sat, 8 Nov 2025 10:39:22 +0100 Subject: [PATCH 014/152] cargo fmt --- src/rendering/eframe_app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() From 0cc97193043361856608969227a19aa2f8f034f0 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sat, 8 Nov 2025 10:41:00 +0100 Subject: [PATCH 015/152] Bump version to 0.2.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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" From 498a9f83ef02509d44b7cac837d946f33ee49683 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sat, 8 Nov 2025 10:48:16 +0100 Subject: [PATCH 016/152] Fix errors --- src/assembler/mod.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++ src/lab.rs | 2 +- 2 files changed, 55 insertions(+), 1 deletion(-) 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/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 From 63dbaebf0f21fbeffb86f65c79e0b3308f178587 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sat, 8 Nov 2025 10:57:05 +0100 Subject: [PATCH 017/152] Add num_assemblers to bucketed assembler store --- src/assembler/bucketed.rs | 4 ++++ 1 file changed, 4 insertions(+) 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() + } } From ba23665e424de2e67521bc8490ea5ad55e97dd73 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 9 Nov 2025 16:05:50 +0100 Subject: [PATCH 018/152] Fix problem with recipe translation --- src/app_state.rs | 5 +---- src/frontend/world/tile.rs | 3 +-- src/par_generation.rs | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 2ac4173..50dc8a6 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -2335,10 +2335,7 @@ impl GameState( Storage::Assembler { grid: id.grid, index: id.assembler_index, - recipe_idx_with_this_item: data_store - .recipe_to_translated_index[&(id.recipe, item)], + recipe_idx_with_this_item: id.recipe.id, }, dest_conn, Box::new(|_weak_index: WeakIndex| {}) diff --git a/src/par_generation.rs b/src/par_generation.rs index 5634961..4a55670 100644 --- a/src/par_generation.rs +++ b/src/par_generation.rs @@ -1418,9 +1418,7 @@ fn pipe_stage( } => Storage::Assembler { grid: id.grid, index: id.assembler_index, - recipe_idx_with_this_item: data_store - .recipe_to_translated_index - [&(id.recipe, fluid.unwrap())], + recipe_idx_with_this_item: id.recipe.id, }, _ => unreachable!(), }, From 4afc6172a140264189bfd495d24d0352da1dd7b4 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 9 Nov 2025 17:25:24 +0100 Subject: [PATCH 019/152] Allow clocking to take into account the speed of the destination --- src/rendering/render_world.rs | 261 ++++++++++++++++++++++++++++++---- 1 file changed, 233 insertions(+), 28 deletions(-) diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 7f57a49..20cbeb7 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -2462,30 +2462,26 @@ pub fn render_ui< if ui.button("⚠️Auto Clock Inserters").clicked() { let inserters_without_values_set = game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter_map(|e| match e { Entity::Inserter { ty, user_movetime, direction, pos, info, .. } => { - if user_movetime.is_none() { - match info { - crate::frontend::world::tile::InserterInfo::NotAttached { .. } => None, - crate::frontend::world::tile::InserterInfo::Attached { info } => match info { - crate::frontend::world::tile::AttachedInserter::BeltStorage {id, belt_pos, .. } => { - let item = game_state_ref.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos); - let start_pos = - data_store.inserter_start_pos(*ty, *pos, *direction); - let end_pos = - data_store.inserter_end_pos(*ty, *pos, *direction); - Some((ty, pos, start_pos, end_pos, item, true)) - }, - crate::frontend::world::tile::AttachedInserter::BeltBelt { .. } => None, - crate::frontend::world::tile::AttachedInserter::StorageStorage { item, .. } => { - let start_pos = + match info { + crate::frontend::world::tile::InserterInfo::NotAttached { .. } => None, + crate::frontend::world::tile::InserterInfo::Attached { info } => match info { + crate::frontend::world::tile::AttachedInserter::BeltStorage {id, belt_pos, .. } => { + let item = game_state_ref.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos); + let start_pos = data_store.inserter_start_pos(*ty, *pos, *direction); let end_pos = data_store.inserter_end_pos(*ty, *pos, *direction); - Some((ty, pos, start_pos, end_pos, *item, false)) - }, + Some((ty, pos, start_pos, end_pos, item, true)) }, - } - } else { - None + crate::frontend::world::tile::AttachedInserter::BeltBelt { .. } => None, + crate::frontend::world::tile::AttachedInserter::StorageStorage { item, .. } => { + let start_pos = + data_store.inserter_start_pos(*ty, *pos, *direction); + let end_pos = + data_store.inserter_end_pos(*ty, *pos, *direction); + Some((ty, pos, start_pos, end_pos, *item, false)) + }, + }, } }, _ => None, @@ -2496,7 +2492,7 @@ pub fn render_ui< if let Some(e) = game_state_ref.world.get_entity_at(end_pos, data_store_ref) { match e { - Entity::Assembler {info, .. } => { + Entity::Assembler {pos: assembler_pos, ty: assembler_ty, rotation: assembler_rotation, info, .. } => { match info { AssemblerInfo::UnpoweredNoRecipe => {}, AssemblerInfo::Unpowered(_) => {}, @@ -2505,11 +2501,73 @@ pub fn render_ui< let (_, _, count_in_recipe) = data_store_ref.recipe_to_items_and_amounts[id.recipe.into_usize()].iter().find(|(dir, recipe_item, _)| *dir == ItemRecipeDir::Ing && item == *recipe_item).unwrap(); let time_per_recipe = data_store_ref.recipe_timers[usize_from(id.recipe.id)] as f32; - let AssemblerOnclickInfo { base_speed, speed_mod, .. } = game_state_ref.simulation_state.factory.power_grids.get_assembler_info(*id, data_store_ref); + let AssemblerOnclickInfo { base_speed, speed_mod, prod_mod, .. } = game_state_ref.simulation_state.factory.power_grids.get_assembler_info(*id, data_store_ref); let crafting_speed = base_speed * (1.0 + speed_mod); - let time_per_craft = time_per_recipe / crafting_speed; + let mut crafts_per_tick = crafting_speed / time_per_recipe; + + let mut outputs = data_store.recipe_to_items_and_amounts[id.recipe.into_usize()] + .iter() + .filter(|(dir, _, _)| *dir == data::ItemRecipeDir::Out).map(|(_, item ,amount_in_recipe)| (*item, *amount_in_recipe, 0.0)) + .collect_vec(); + + let inserters = game_state_ref.world.get_entities_colliding_with(Position { + x: assembler_pos.x - i32::from(data_store_ref.max_inserter_search_range), + y: assembler_pos.y - i32::from(data_store_ref.max_inserter_search_range), + }, [ + u16::from(data_store_ref.max_inserter_search_range) * 2 + data_store_ref.assembler_info[*assembler_ty as usize].size(*assembler_rotation).0, + u16::from(data_store_ref.max_inserter_search_range) * 2 + data_store_ref.assembler_info[*assembler_ty as usize].size(*assembler_rotation).1 + ].into(), data_store_ref).into_iter().filter(|e| matches!(e, Entity::Inserter { .. })).filter_map(|e| match e { + Entity::Inserter { + pos, direction, ty, user_movetime, info, .. + } => { + let start_pos = data_store_ref.inserter_start_pos(*ty, *pos, *direction); + + let item = match info { + crate::frontend::world::tile::InserterInfo::NotAttached { } => return None, + crate::frontend::world::tile::InserterInfo::Attached { info } => { + match info { + crate::frontend::world::tile::AttachedInserter::BeltStorage { id, belt_pos } => { + let item = game_state_ref.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos); + + item + }, + crate::frontend::world::tile::AttachedInserter::BeltBelt { item, .. } => *item, + crate::frontend::world::tile::AttachedInserter::StorageStorage { item, .. } => *item, + } + }, + }; + + if start_pos.contained_in(*assembler_pos, data_store.assembler_info[*assembler_ty as usize].size(*assembler_rotation)) { + let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store_ref.inserter_infos[*ty as usize].swing_time_ticks); + + let items_per_tick = f32::from(data_store_ref.inserter_infos[*ty as usize].base_hand_size) / (2.0 * f32::from(movetime) + 2.0); + + Some((item, items_per_tick)) + } else { + None + } + } + _ => unreachable!(), + }); + + for (item, amount) in inserters { + outputs.iter_mut().find(|(list_item, _, _)| *list_item == item).unwrap().2 += amount; + } - let items_needed_per_tick = *count_in_recipe as f32 / time_per_craft; + for (item, amount_in_recipe, amount_removed_by_inserters_per_tick) in outputs { + let recipes_per_tick = amount_removed_by_inserters_per_tick / (f32::from(amount_in_recipe) * (1.0 + prod_mod)); + + + if !data_store_ref.item_is_fluid[item.into_usize()] && amount_removed_by_inserters_per_tick > 0.0 { + if recipes_per_tick < crafts_per_tick { + crafts_per_tick = recipes_per_tick; + } + + } + } + + + let items_needed_per_tick = *count_in_recipe as f32 * crafts_per_tick; // FIXME: Take tech level into consideration let hand_size = data_store.inserter_infos[*ty as usize].base_hand_size as f32; @@ -2531,13 +2589,66 @@ pub fn render_ui< } }, + Entity::Chest { ty: chest_ty, pos: chest_pos, item, slot_limit } => { + let inserters = game_state_ref.world.get_entities_colliding_with(Position { + x: chest_pos.x - i32::from(data_store_ref.max_inserter_search_range), + y: chest_pos.y - i32::from(data_store_ref.max_inserter_search_range), + }, [ + u16::from(data_store_ref.max_inserter_search_range) * 3, u16::from(data_store_ref.max_inserter_search_range) * 3 + ].into(), data_store_ref).into_iter().filter(|e| matches!(e, Entity::Inserter { .. })).filter_map(|e| match e { + Entity::Inserter { + pos, direction, ty, user_movetime, info, .. + } => { + let start_pos = data_store_ref.inserter_start_pos(*ty, *pos, *direction); + + let item = match info { + crate::frontend::world::tile::InserterInfo::NotAttached { } => return None, + crate::frontend::world::tile::InserterInfo::Attached { info } => { + match info { + crate::frontend::world::tile::AttachedInserter::BeltStorage { id, belt_pos } => { + let item = game_state_ref.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos); + + item + }, + crate::frontend::world::tile::AttachedInserter::BeltBelt { item, .. } => *item, + crate::frontend::world::tile::AttachedInserter::StorageStorage { item, .. } => *item, + } + }, + }; + + if start_pos.contained_in(*chest_pos, data_store_ref.chest_tile_sizes[*chest_ty as usize]) { + let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store_ref.inserter_infos[*ty as usize].swing_time_ticks); + + let items_per_tick = f32::from(data_store_ref.inserter_infos[*ty as usize].base_hand_size) / (2.0 * f32::from(movetime)); + + Some((item, items_per_tick)) + } else { + None + } + } + _ => unreachable!(), + }); + + let outgoing_amount: f32 = inserters.map(|(_item, amount_per_tick)| amount_per_tick).sum(); + + let hand_size = data_store.inserter_infos[*ty as usize].base_hand_size as f32; + + let full_rotations_needed_per_tick = outgoing_amount / hand_size; + + let full_rotation_time_in_ticks = 1.0 / full_rotations_needed_per_tick; + + let swing_time_in_ticks = full_rotation_time_in_ticks / 2.0 - 1.0; + + goal_movetime = max(goal_movetime, swing_time_in_ticks as u16 / 10 * 10); + } + _ => {} } } if let Some(e) = game_state_ref.world.get_entity_at(start_pos, data_store_ref) { match e { - Entity::Assembler { info, .. } => { + Entity::Assembler { pos: assembler_pos, ty: assembler_ty, rotation: assembler_rotation, info, .. } => { match info { AssemblerInfo::UnpoweredNoRecipe => {}, AssemblerInfo::Unpowered(_) => {}, @@ -2548,9 +2659,68 @@ pub fn render_ui< let AssemblerOnclickInfo { base_speed, speed_mod, prod_mod, .. } = game_state_ref.simulation_state.factory.power_grids.get_assembler_info(*id, data_store_ref); let crafting_speed = base_speed * (1.0 + speed_mod); - let time_per_craft = time_per_recipe / crafting_speed; + let mut crafts_per_tick = crafting_speed / time_per_recipe; + + let mut outputs = data_store.recipe_to_items_and_amounts[id.recipe.into_usize()] + .iter() + .filter(|(dir, _, _)| *dir == data::ItemRecipeDir::Out).map(|(_, item ,amount_in_recipe)| (*item, *amount_in_recipe, 0.0)) + .collect_vec(); + + let inserters = game_state_ref.world.get_entities_colliding_with(Position { + x: assembler_pos.x - i32::from(data_store_ref.max_inserter_search_range), + y: assembler_pos.y - i32::from(data_store_ref.max_inserter_search_range), + }, [ + u16::from(data_store_ref.max_inserter_search_range) * 2 + data_store_ref.assembler_info[*assembler_ty as usize].size(*assembler_rotation).0, + u16::from(data_store_ref.max_inserter_search_range) * 2 + data_store_ref.assembler_info[*assembler_ty as usize].size(*assembler_rotation).1 + ].into(), data_store_ref).into_iter().filter(|e| matches!(e, Entity::Inserter { .. })).filter_map(|e| match e { + Entity::Inserter { + pos, direction, ty, user_movetime, info, .. + } => { + let start_pos = data_store_ref.inserter_start_pos(*ty, *pos, *direction); + + let item = match info { + crate::frontend::world::tile::InserterInfo::NotAttached { } => return None, + crate::frontend::world::tile::InserterInfo::Attached { info } => { + match info { + crate::frontend::world::tile::AttachedInserter::BeltStorage { id, belt_pos } => { + let item = game_state_ref.simulation_state.factory.belts.get_inserter_item(*id, *belt_pos); + + item + }, + crate::frontend::world::tile::AttachedInserter::BeltBelt { item, .. } => *item, + crate::frontend::world::tile::AttachedInserter::StorageStorage { item, .. } => *item, + } + }, + }; + + if start_pos.contained_in(*assembler_pos, data_store.assembler_info[*assembler_ty as usize].size(*assembler_rotation)) { + let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store_ref.inserter_infos[*ty as usize].swing_time_ticks); - let items_produced_per_tick = (*count_in_recipe as f32 * (1.0 + prod_mod)) / time_per_craft; + let items_per_tick = f32::from(data_store_ref.inserter_infos[*ty as usize].base_hand_size) / (2.0 * f32::from(movetime) + 2.0); + + Some((item, items_per_tick)) + } else { + None + } + } + _ => unreachable!(), + }); + + for (item, amount) in inserters { + outputs.iter_mut().find(|(list_item, _, _)| *list_item == item).unwrap().2 += amount; + } + + for (list_item, amount_in_recipe, amount_removed_by_inserters_per_tick) in outputs { + let recipes_per_tick = amount_removed_by_inserters_per_tick / (f32::from(amount_in_recipe) * (1.0 + prod_mod)); + + if list_item != item && !data_store_ref.item_is_fluid[item.into_usize()] && amount_removed_by_inserters_per_tick > 0.0 { + if recipes_per_tick < crafts_per_tick { + crafts_per_tick = recipes_per_tick; + } + } + } + + let items_produced_per_tick = (*count_in_recipe as f32 * (1.0 + prod_mod)) * crafts_per_tick; // FIXME: Take tech level into consideration let hand_size = data_store.inserter_infos[*ty as usize].base_hand_size as f32; @@ -2561,7 +2731,10 @@ pub fn render_ui< let swing_time_in_ticks = full_rotation_time_in_ticks / 2.0 - 1.0; - goal_movetime = max(goal_movetime, swing_time_in_ticks as u16); + goal_movetime = max(goal_movetime, swing_time_in_ticks as u16 / 10 * 10); + if goal_movetime > 10_000 { + dbg!(items_produced_per_tick); + } }, } }, @@ -2574,9 +2747,41 @@ pub fn render_ui< }); actions.extend(inserter_pos_and_time.map(|(pos, time)| { + if time > 10_000 { + dbg!(pos); + } + ActionType::OverrideInserterMovetime { pos, new_movetime: Some(time.try_into().unwrap()) } })) } + + // if ui.button("⚠️Auto Clock Inserters (SLOW!)").clicked() { + // let mut clocking_state = crate::clocking::ClockingState::default(); + + // for assembler in game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter(|e| matches!(e, Entity::Assembler { ..})) { + // clocking_state.assume_enough_ingredients(assembler, &game_state_ref.world, &game_state_ref.simulation_state, data_store_ref); + // } + + // for _ in 0..5 { + // for inserter in game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter(|e| matches!(e, Entity::Inserter { ..})) { + // clocking_state.get_clocking(inserter, &game_state_ref.world, &game_state_ref.simulation_state, data_store_ref); + // } + + // for assembler in game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter(|e| matches!(e, Entity::Assembler { ..})) { + // clocking_state.calculate_machine_slowdown(assembler, &game_state_ref.world, &game_state_ref.simulation_state, data_store_ref); + // } + // } + + // actions.extend(game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter(|e| matches!(e, Entity::Inserter { ..})).map(|inserter| { + // let pos = inserter.get_pos(); + + // let ticks = clocking_state.get_clocking(inserter, &game_state_ref.world, &game_state_ref.simulation_state, data_store_ref); + + + // ActionType::OverrideInserterMovetime { pos, new_movetime: ticks.map(|v| v.try_into().unwrap()) } + // })); + // } + if ui.button("Remove Clocking from all Inserters").clicked() { let inserters_without_values_set = game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter_map(|e| match e { Entity::Inserter { pos, info, .. } => { From 9386b638f6316e968b17d949e711b97bd8678c01 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 9 Nov 2025 17:26:58 +0100 Subject: [PATCH 020/152] cargo update --- Cargo.lock | 128 ++++++++++++++++++++++++++--------------------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2e6601..f8b7128 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,7 +247,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -414,7 +414,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -449,7 +449,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -528,7 +528,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -544,7 +544,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -683,7 +683,7 @@ checksum = "ffebfc2d28a12b262c303cb3860ee77b91bd83b1f20f0bd2a9693008e2f55a9e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -805,7 +805,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -879,9 +879,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.44" +version = "1.2.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" dependencies = [ "find-msvc-tools", "jobserver", @@ -928,9 +928,9 @@ dependencies = [ [[package]] name = "charts-rs" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64caa6454be69fcf2555a9692729d118d26ee8c3b281c355e600707c19b39109" +checksum = "46613e62ca4b6ff0b9e262adaf27dc3113aaf264accb90db048b833c6f95ad45" dependencies = [ "ahash", "arc-swap", @@ -947,12 +947,12 @@ dependencies = [ [[package]] name = "charts-rs-derive" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a22d9bb806d0d341a234556bee1481d8877a78c1a718d9a92aa4e9f542766c" +checksum = "c8d38f1088dcf6ce3487a09c49fc2d2f8759045603f24d5814357e9283260426" dependencies = [ "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1194,7 +1194,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1270,7 +1270,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1393,7 +1393,7 @@ source = "git+https://github.com/BloodStainedCrow/egui-show-info#0b1a1a7e6b2b759 dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1545,7 +1545,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1566,7 +1566,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1577,7 +1577,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1620,7 +1620,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1785,7 +1785,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1900,7 +1900,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2002,7 +2002,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2089,7 +2089,7 @@ checksum = "46b134aa084df7c3a513a1035c52f623e4b3065dfaf3d905a4f28a2e79b5bb3f" dependencies = [ "attribute-derive", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2598,7 +2598,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2894,7 +2894,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -3161,7 +3161,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -3213,7 +3213,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -3529,9 +3529,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orbclient" -version = "0.3.48" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +checksum = "247ad146e19b9437f8604c21f8652423595cf710ad108af40e77d3ae6e96b827" dependencies = [ "libredox", ] @@ -3685,7 +3685,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "unicase", ] @@ -3722,7 +3722,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -3927,7 +3927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4035,9 +4035,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -4061,7 +4061,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4430,7 +4430,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.108", + "syn 2.0.109", "unicode-ident", ] @@ -4596,7 +4596,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4631,7 +4631,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4825,7 +4825,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4922,7 +4922,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4934,7 +4934,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4969,9 +4969,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" dependencies = [ "proc-macro2", "quote", @@ -4997,7 +4997,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -5085,7 +5085,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -5096,7 +5096,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -5304,7 +5304,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -5611,7 +5611,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "wasm-bindgen-shared", ] @@ -5822,9 +5822,9 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +checksum = "009936b22a61d342859b5f0ea64681cbb35a358ab548e2a44a8cf0dac2d980b8" [[package]] name = "wgpu" @@ -6110,7 +6110,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6121,7 +6121,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6132,7 +6132,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6143,7 +6143,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6633,7 +6633,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "synstructure", ] @@ -6689,7 +6689,7 @@ checksum = "dc6821851fa840b708b4cbbaf6241868cabc85a2dc22f426361b0292bfc0b836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "zbus-lockstep", "zbus_xml", "zvariant", @@ -6704,7 +6704,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "zbus_names", "zvariant", "zvariant_utils", @@ -6752,7 +6752,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6772,7 +6772,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "synstructure", ] @@ -6806,7 +6806,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6863,7 +6863,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "zvariant_utils", ] @@ -6876,6 +6876,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.108", + "syn 2.0.109", "winnow", ] From 14018b98b46f9d71312ca092d951b5f441a2001b Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 9 Nov 2025 17:29:09 +0100 Subject: [PATCH 021/152] cargo fmt --- src/rendering/render_world.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 20cbeb7..0cb82cd 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -2561,7 +2561,7 @@ pub fn render_ui< if !data_store_ref.item_is_fluid[item.into_usize()] && amount_removed_by_inserters_per_tick > 0.0 { if recipes_per_tick < crafts_per_tick { crafts_per_tick = recipes_per_tick; - } + } } } @@ -2716,7 +2716,7 @@ pub fn render_ui< if list_item != item && !data_store_ref.item_is_fluid[item.into_usize()] && amount_removed_by_inserters_per_tick > 0.0 { if recipes_per_tick < crafts_per_tick { crafts_per_tick = recipes_per_tick; - } + } } } From c0489d6b58c425c5ca3b12db514e5b3a96b3a8a8 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 9 Nov 2025 22:09:52 +0100 Subject: [PATCH 022/152] Remove unneeded nightly features --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fa48c38..92efa22 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,8 +6,6 @@ #![feature(mixed_integer_ops_unsigned_sub)] #![feature(int_roundings)] #![feature(strict_overflow_ops)] -#![feature(thin_box)] -#![feature(ptr_metadata)] extern crate test; @@ -67,6 +65,8 @@ const TICKS_PER_SECOND_LOGIC: u64 = 60; const TICKS_PER_SECOND_RUNSPEED: u64 = 60; +pub mod clocking; + pub mod get_size; pub mod assembler; From e5946e768e12e083598ef022a55518ccba07dd2c Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 10 Nov 2025 10:42:00 +0100 Subject: [PATCH 023/152] Add autosaving --- src/frontend/action/action_state_machine.rs | 6 +- src/rendering/render_world.rs | 104 +++++++++++++++----- 2 files changed, 83 insertions(+), 27 deletions(-) diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index a699881..cfe1464 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -14,7 +14,7 @@ use log::{error, warn}; use petgraph::Directed; use crate::{ - NewWithDataStore, + NewWithDataStore, TICKS_PER_SECOND_LOGIC, app_state::SimulationState, belt::splitter::SplitterDistributionMode, blueprint::Blueprint, @@ -156,6 +156,8 @@ pub struct ActionStateMachine, pub hotbar_window_open: bool, + + pub autosave_interval: u32, } #[derive(Debug)] @@ -278,6 +280,8 @@ impl hotbar: Hotbar::new(data_store), hotbar_window_open: true, + + autosave_interval: (60 * TICKS_PER_SECOND_LOGIC) as u32, } } diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 0cb82cd..6946edf 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -14,7 +14,7 @@ use crate::get_size::RamUsage; use crate::item::{ITEMCOUNTTYPE, Indexable}; use crate::lab::{LabViewInfo, TICKS_PER_SCIENCE}; use crate::liquid::FluidSystemState; -use crate::par_generation::ParGenerateInfo; +use crate::par_generation::{ParGenerateInfo, Timer}; use crate::rendering::{BeltSide, Corner}; use crate::saving::{save_components, save_with_fork}; use crate::statistics::{NUM_DIFFERENT_TIMESCALES, TIMESCALE_NAMES}; @@ -1899,6 +1899,45 @@ pub fn render_ui< let data_store_ref = &*data_store; let mut actions = vec![]; + let current_tick = aux_data.current_tick; + + let tick = current_tick % u64::from(state_machine_ref.autosave_interval); + + if cfg!(target_os = "linux") { + if tick == 0 { + if state_machine_ref.current_fork_save_in_progress.is_none() { + let recv = save_with_fork(&*world, &*simulation_state, &*aux_data, data_store_ref); + if let Some(recv) = recv { + recv.set_nonblocking(true) + .expect("Could not set pipe to nonblocking!"); + state_machine_ref.current_fork_save_in_progress = Some(ForkSaveInfo { + recv, + current_state: 0, + }); + } else { + error!("Nonblocking save failed to start! Saving in blocking mode"); + save_components(&*world, &*simulation_state, &*aux_data, data_store_ref); + } + } else { + warn!( + "Save already in progress while trying to start autosave interval. If this was due to autosaves taking too long, consider increasing your autosave interval." + ); + } + } + } else { + // Ensure that the saving Window is on screen when the window freezes + if tick >= u64::from(state_machine_ref.autosave_interval) - 10 || tick <= 5 { + let progress = if tick > 1 && tick <= 5 { 1.0 } else { 0.0 }; + if tick == 0 { + let _timer = Timer::new("Saving"); + save_components(&*world, &*simulation_state, &*aux_data, data_store_ref); + } + Window::new("Saving...").default_open(true).show(ctx, |ui| { + ui.add(ProgressBar::new(progress).corner_radius(0.0)); + }); + } + } + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] ui.vertical_centered(|ui|{ ui.label( @@ -1982,6 +2021,19 @@ pub fn render_ui< .logarithmic(true), ); + let mut autosave_interval_minutes = state_machine_ref.autosave_interval / 60 / (TICKS_PER_SECOND_LOGIC as u32); + ui.add( + egui::Slider::new(&mut autosave_interval_minutes, 1..=100) + .integer() + .custom_formatter(|v, _range| { + let value: u32 = v as u32; + + format!("{value} min") + }) + .text("Autosave interval"), + ); + state_machine_ref.autosave_interval = autosave_interval_minutes * 60 * (TICKS_PER_SECOND_LOGIC as u32); + None }) .inner @@ -3820,31 +3872,31 @@ pub fn render_ui< }, } - egui::Area::new("Hotbar".into()) - .anchor(Align2::CENTER_BOTTOM, (0.0, 0.0)) - .show(ui.ctx(), |ui| { - egui_extras::TableBuilder::new(ui) - .columns(Column::auto().resizable(false), 10) - .body(|mut body| { - body.row(30.0, |mut row| { - for i in 0..10 { - if row - .col(|ui| { - let button_response = ui.button(format!("{i}")); - - if button_response.hovered() { - dbg!(i); - } - }) - .1 - .hovered() - { - dbg!(i); - }; - } - }); - }); - }); + // egui::Area::new("Hotbar".into()) + // .anchor(Align2::CENTER_BOTTOM, (0.0, 0.0)) + // .show(ui.ctx(), |ui| { + // egui_extras::TableBuilder::new(ui) + // .columns(Column::auto().resizable(false), 10) + // .body(|mut body| { + // body.row(30.0, |mut row| { + // for i in 0..10 { + // if row + // .col(|ui| { + // let button_response = ui.button(format!("{i}")); + + // if button_response.hovered() { + // dbg!(i); + // } + // }) + // .1 + // .hovered() + // { + // dbg!(i); + // }; + // } + // }); + // }); + // }); Window::new("Technology") .collapsible(false) From f367c5b73ab227a9b3449ac389af308542119517 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 10 Nov 2025 11:01:27 +0100 Subject: [PATCH 024/152] Fix negative chunks rendering being off by one --- src/rendering/render_world.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 6946edf..edb9b6b 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -300,9 +300,9 @@ pub fn render_world( pos_iter .map(|(x_offs, y_offs)| { let chunk_draw_offs = ( - x_offs as f32 * CHUNK_SIZE_FLOAT - camera_pos.0 % CHUNK_SIZE_FLOAT + x_offs as f32 * CHUNK_SIZE_FLOAT - camera_pos.0.rem_euclid(CHUNK_SIZE_FLOAT) + (0.5 * num_tiles_across_screen_horizontal), - y_offs as f32 * CHUNK_SIZE_FLOAT - camera_pos.1 % CHUNK_SIZE_FLOAT + y_offs as f32 * CHUNK_SIZE_FLOAT - camera_pos.1.rem_euclid(CHUNK_SIZE_FLOAT) + (0.5 * num_tiles_across_screen_vertical), ); From 5d1c063a6fbb637a97545a8f1181e4fc19195ac4 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 10 Nov 2025 11:11:01 +0100 Subject: [PATCH 025/152] fix off by one in negative chunks --- src/frontend/action/action_state_machine.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index cfe1464..140959f 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -1124,8 +1124,8 @@ impl ); Position { - x: mouse_pos.0 as i32, - y: mouse_pos.1 as i32, + x: mouse_pos.0.floor() as i32, + y: mouse_pos.1.floor() as i32, } } From ee492751c072da030a1f6053c61e789bf3f7a8df Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 10 Nov 2025 12:04:08 +0100 Subject: [PATCH 026/152] Fix another off by one error while rendering --- src/rendering/render_world.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index edb9b6b..fd5848a 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -181,8 +181,8 @@ pub fn render_world( }; let player_chunk = ( - (camera_pos.0 / CHUNK_SIZE_FLOAT) as i32, - (camera_pos.1 / CHUNK_SIZE_FLOAT) as i32, + (camera_pos.0 / CHUNK_SIZE_FLOAT).floor() as i32, + (camera_pos.1 / CHUNK_SIZE_FLOAT).floor() as i32, ); if num_tiles_across_screen_horizontal > SWITCH_TO_MAPVIEW_TILES { From cf4652387200e2ad4eac221e46695f8788e1e77d Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 10 Nov 2025 12:04:36 +0100 Subject: [PATCH 027/152] Correctly handle negative positions in the world chunk lookups --- src/frontend/world/tile.rs | 83 ++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index 01b052f..3b08ed0 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -1438,8 +1438,8 @@ impl GetGridIndex { fn get_grid_index(&self) -> (i32, i32) { ( - self.base_pos.0 / i32::from(CHUNK_SIZE), - self.base_pos.1 / i32::from(CHUNK_SIZE), + self.base_pos.0.div_floor(i32::from(CHUNK_SIZE)), + self.base_pos.1.div_floor(i32::from(CHUNK_SIZE)), ) } } @@ -1616,14 +1616,8 @@ impl World Self { let grid = BoundingBoxGrid::new_with_filled_grid( - [ - top_left.x / i32::from(CHUNK_SIZE), - top_left.y / i32::from(CHUNK_SIZE), - ], - [ - bottom_right.x / i32::from(CHUNK_SIZE), - bottom_right.y / i32::from(CHUNK_SIZE), - ], + Self::get_chunk_pos_for_tile(top_left).into(), + Self::get_chunk_pos_for_tile(bottom_right).into(), |[x, y]| Chunk { base_pos: (x * i32::from(CHUNK_SIZE), y * i32::from(CHUNK_SIZE)), floor_tiles: None, @@ -1974,8 +1968,7 @@ impl World World World World World { + let chunk_pos = Self::get_chunk_pos_for_tile(pos); self.belt_lookup .belt_id_to_chunks .entry(id) .or_default() - .insert((pos.x / i32::from(CHUNK_SIZE), pos.y / i32::from(CHUNK_SIZE))); + .insert(chunk_pos); }, AttachedInserter::BeltBelt { item, inserter } => { todo!("Set the correct belt_ids in the belt_lookup") @@ -3722,20 +3714,23 @@ impl World Option<&Chunk> { - self.chunks - .get(pos.x / i32::from(CHUNK_SIZE), pos.y / i32::from(CHUNK_SIZE)) + let (chunk_x, chunk_y) = Self::get_chunk_pos_for_tile(pos); + self.chunks.get(chunk_x, chunk_y) } fn get_chunk_for_tile_mut( &mut self, pos: Position, ) -> Option<&mut Chunk> { - self.chunks - .get_mut(pos.x / i32::from(CHUNK_SIZE), pos.y / i32::from(CHUNK_SIZE)) + let (chunk_x, chunk_y) = Self::get_chunk_pos_for_tile(pos); + self.chunks.get_mut(chunk_x, chunk_y) } - fn get_chunk_pos_for_tile(&self, pos: Position) -> (i32, i32) { - (pos.x / i32::from(CHUNK_SIZE), pos.y / i32::from(CHUNK_SIZE)) + fn get_chunk_pos_for_tile(pos: Position) -> (i32, i32) { + ( + pos.x.div_floor(i32::from(CHUNK_SIZE)), + pos.y.div_floor(i32::from(CHUNK_SIZE)), + ) } fn get_chunk_mut( @@ -3827,11 +3822,11 @@ impl World World World= 1); assert!(chunk_range_y.clone().count() >= 1); @@ -4148,10 +4143,10 @@ impl World= 1); debug_assert!(chunk_range_y.clone().count() >= 1); @@ -4421,10 +4416,10 @@ impl World= 1); debug_assert!(chunk_range_y.clone().count() >= 1); @@ -4951,10 +4946,10 @@ impl World Date: Mon, 10 Nov 2025 14:00:19 +0100 Subject: [PATCH 028/152] Don't miss autosaves with high tickrate --- src/frontend/action/action_state_machine.rs | 2 ++ src/rendering/render_world.rs | 15 +++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index 140959f..e1ae6e9 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -157,6 +157,7 @@ pub struct ActionStateMachine, pub hotbar_window_open: bool, + pub last_tick_seen_for_autosave: u32, pub autosave_interval: u32, } @@ -281,6 +282,7 @@ impl hotbar: Hotbar::new(data_store), hotbar_window_open: true, + last_tick_seen_for_autosave: 0, autosave_interval: (60 * TICKS_PER_SECOND_LOGIC) as u32, } } diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index fd5848a..e9eb436 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -1901,10 +1901,10 @@ pub fn render_ui< let current_tick = aux_data.current_tick; - let tick = current_tick % u64::from(state_machine_ref.autosave_interval); + let tick = (current_tick % u64::from(state_machine_ref.autosave_interval)) as u32; if cfg!(target_os = "linux") { - if tick == 0 { + if tick < state_machine_ref.last_tick_seen_for_autosave { if state_machine_ref.current_fork_save_in_progress.is_none() { let recv = save_with_fork(&*world, &*simulation_state, &*aux_data, data_store_ref); if let Some(recv) = recv { @@ -1926,9 +1926,9 @@ pub fn render_ui< } } else { // Ensure that the saving Window is on screen when the window freezes - if tick >= u64::from(state_machine_ref.autosave_interval) - 10 || tick <= 5 { + if tick >= state_machine_ref.autosave_interval - 10 || tick <= 5 { let progress = if tick > 1 && tick <= 5 { 1.0 } else { 0.0 }; - if tick == 0 { + if tick < state_machine_ref.last_tick_seen_for_autosave { let _timer = Timer::new("Saving"); save_components(&*world, &*simulation_state, &*aux_data, data_store_ref); } @@ -1937,6 +1937,7 @@ pub fn render_ui< }); } } + state_machine_ref.last_tick_seen_for_autosave = tick; #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] ui.vertical_centered(|ui|{ @@ -2021,7 +2022,8 @@ pub fn render_ui< .logarithmic(true), ); - let mut autosave_interval_minutes = state_machine_ref.autosave_interval / 60 / (TICKS_PER_SECOND_LOGIC as u32); + let mut autosave_interval_minutes = + state_machine_ref.autosave_interval / 60 / (TICKS_PER_SECOND_LOGIC as u32); ui.add( egui::Slider::new(&mut autosave_interval_minutes, 1..=100) .integer() @@ -2032,7 +2034,8 @@ pub fn render_ui< }) .text("Autosave interval"), ); - state_machine_ref.autosave_interval = autosave_interval_minutes * 60 * (TICKS_PER_SECOND_LOGIC as u32); + state_machine_ref.autosave_interval = + autosave_interval_minutes * 60 * (TICKS_PER_SECOND_LOGIC as u32); None }) From c0241cdf2ba03c03297f3d99173ea90f95337103 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 10 Nov 2025 14:00:40 +0100 Subject: [PATCH 029/152] Move megabase to the player starts in the middle --- src/app_state.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 50dc8a6..780353f 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -215,12 +215,17 @@ impl GameState, data_store: &DataStore, ) -> Self { + const X_OFFS: i32 = -1_800; + // TODO: Increase size to fit solar field let mut ret = GameState::new_with_world_area( - Position { x: 0, y: 0 }, Position { - x: 16_000, - y: 12_000, + x: X_OFFS, + y: -6_000, + }, + Position { + x: 16_000 + X_OFFS, + y: 6_000, }, data_store, ); @@ -239,7 +244,10 @@ impl GameState GameState GameState Date: Wed, 12 Nov 2025 11:42:39 +0100 Subject: [PATCH 030/152] Make list len check only in debug mode --- src/inserter/storage_storage_with_buckets_indirect.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/inserter/storage_storage_with_buckets_indirect.rs b/src/inserter/storage_storage_with_buckets_indirect.rs index 475a111..9840a5c 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -394,6 +394,7 @@ impl BucketedStorageStorageInserterStore { grid_size: usize, current_tick: u32, ) { + #[cfg(debug_assertions)] let old_len: usize = self.get_list_sizes().iter().sum(); assert!(self.current_tick < self.list_len()); @@ -609,7 +610,14 @@ impl BucketedStorageStorageInserterStore { self.current_tick = (self.current_tick + 1) % self.list_len(); - assert_eq!(old_len, self.get_list_sizes().iter().sum::()); + #[cfg(debug_assertions)] + { + assert_eq!( + old_len, + self.get_list_sizes().iter().sum::(), + "Updating inserters lost an inserter from the update lists" + ); + } } fn get_list_sizes(&self) -> Vec { From 9cb400110fad7c7013fce51afdaed4cb2be48637 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 12 Nov 2025 12:21:58 +0100 Subject: [PATCH 031/152] Do not have clocking module yet --- src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 92efa22..90f0259 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,8 +65,6 @@ const TICKS_PER_SECOND_LOGIC: u64 = 60; const TICKS_PER_SECOND_RUNSPEED: u64 = 60; -pub mod clocking; - pub mod get_size; pub mod assembler; From 7bb643045c0bedc93a62e4522b78f610104ae2c1 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sat, 15 Nov 2025 12:14:29 +0100 Subject: [PATCH 032/152] Switch to MaxInsertionLimit to save on random reads --- src/assembler/bucketed.rs | 11 +++--- src/assembler/mod.rs | 3 +- src/assembler/simd.rs | 9 +++-- src/chest.rs | 10 ++++-- src/mining_drill/mod.rs | 3 +- src/mining_drill/only_solo_owned.rs | 3 +- src/mining_drill/with_shared_ore.rs | 3 +- src/storage_list.rs | 56 ++++++++++++++++++++++------- 8 files changed, 73 insertions(+), 25 deletions(-) diff --git a/src/assembler/bucketed.rs b/src/assembler/bucketed.rs index a36c9ab..dc43b23 100644 --- a/src/assembler/bucketed.rs +++ b/src/assembler/bucketed.rs @@ -4,14 +4,14 @@ use std::{array, iter, u8}; use itertools::Itertools; use log::warn; +use crate::assembler::arrays; use crate::assembler::{PowerUsageInfo, TIMERTYPE}; use crate::data::{DataStore, ItemRecipeDir}; use crate::frontend::world::Position; use crate::item::{ITEMCOUNTTYPE, IdxTrait, Indexable, Recipe}; use crate::power::Watt; use crate::power::power_grid::{IndexUpdateInfo, MAX_POWER_MULT, PowerGridIdentifier}; - -use crate::assembler::arrays; +use crate::storage_list::{MaxInsertionLimit, PANIC_ON_INSERT}; use crate::WeakIdxTrait; @@ -632,14 +632,17 @@ impl ( ( - [&[ITEMCOUNTTYPE]; NUM_INGS], + [MaxInsertionLimit<'_>; NUM_INGS], [&mut [ITEMCOUNTTYPE]; NUM_INGS], ), [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], ) { ( ( - self.ings_max_insert.each_mut().map(|b| &**b), + self.ings_max_insert.each_mut().map(|b| match b.get(0) { + Some(v) => MaxInsertionLimit::Global(*v), + None => PANIC_ON_INSERT, + }), self.ings.each_mut().map(|b| &mut **b), ), self.outputs.each_mut().map(|b| &mut **b), diff --git a/src/assembler/mod.rs b/src/assembler/mod.rs index 8640c61..986e561 100644 --- a/src/assembler/mod.rs +++ b/src/assembler/mod.rs @@ -1,6 +1,7 @@ use std::{array, marker::PhantomData, simd::Simd, u8}; use crate::frontend::world::tile::ModuleTy; +use crate::storage_list::MaxInsertionLimit; use crate::{ data::DataStore, frontend::world::{Position, tile::AssemblerID}, @@ -672,7 +673,7 @@ pub trait MultiAssemblerStore< &mut self, ) -> ( ( - [&[ITEMCOUNTTYPE]; NUM_INGS], + [MaxInsertionLimit<'_>; NUM_INGS], [&mut [ITEMCOUNTTYPE]; NUM_INGS], ), [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index e108e61..70f4733 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -18,6 +18,7 @@ use crate::{ Watt, power_grid::{IndexUpdateInfo, MAX_POWER_MULT, PowerGridEntity, PowerGridIdentifier}, }, + storage_list::{MaxInsertionLimit, PANIC_ON_INSERT}, }; use itertools::{Either, Itertools}; @@ -942,14 +943,18 @@ impl ( ( - [&[ITEMCOUNTTYPE]; NUM_INGS], + [MaxInsertionLimit<'_>; NUM_INGS], [&mut [ITEMCOUNTTYPE]; NUM_INGS], ), [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], ) { ( ( - self.ings_max_insert.each_mut().map(|b| &**b), + self.ings_max_insert.each_mut().map(|b| match b.get(0) { + Some(v) => MaxInsertionLimit::Global(*v), + None => PANIC_ON_INSERT, + }), + // self.ings_max_insert.each_mut().map(|b| &**b), self.ings.each_mut().map(|b| &mut **b), ), self.outputs.each_mut().map(|b| &mut **b), diff --git a/src/chest.rs b/src/chest.rs index bf056c9..e8c4eeb 100644 --- a/src/chest.rs +++ b/src/chest.rs @@ -4,6 +4,7 @@ use std::{cmp::min, u8}; use rayon::iter::IndexedParallelIterator; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; +use crate::storage_list::MaxInsertionLimit; use crate::{ data::DataStore, item::{ITEMCOUNTTYPE, IdxTrait, Item, WeakIdxTrait, usize_from}, @@ -322,8 +323,13 @@ impl MultiChestStore { removed_items } - pub fn storage_list_slices(&mut self) -> (&[ITEMCOUNTTYPE], &mut [ITEMCOUNTTYPE]) { - (self.max_insert.as_slice(), self.inout.as_mut_slice()) + pub fn storage_list_slices<'a>( + &'a mut self, + ) -> (MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE]) { + ( + MaxInsertionLimit::PerMachine(self.max_insert.as_slice()), + self.inout.as_mut_slice(), + ) } } diff --git a/src/mining_drill/mod.rs b/src/mining_drill/mod.rs index fe4eefb..9d281b5 100644 --- a/src/mining_drill/mod.rs +++ b/src/mining_drill/mod.rs @@ -2,6 +2,7 @@ use crate::inserter::StaticID; use crate::item::ITEMCOUNTTYPE; use crate::mining_drill::only_solo_owned::PureDrillStorageOnlySoloOwned; use crate::power::power_grid::MAX_POWER_MULT; +use crate::storage_list::MaxInsertionLimit; use crate::{ data::DataStore, frontend::world::Position, @@ -360,7 +361,7 @@ impl MiningDrillStore { pub fn storages_by_item( &mut self, - ) -> impl Iterator { + ) -> impl Iterator, &mut [ITEMCOUNTTYPE])> { self.pure_solo_owned .iter_mut() .map(|store| store.get_inventories()) diff --git a/src/mining_drill/only_solo_owned.rs b/src/mining_drill/only_solo_owned.rs index 21e1a25..82aeb88 100644 --- a/src/mining_drill/only_solo_owned.rs +++ b/src/mining_drill/only_solo_owned.rs @@ -12,6 +12,7 @@ use crate::item::Indexable; use crate::item::Item; use crate::mining_drill::MiningDrillInfo; use crate::power::Joule; +use crate::storage_list::MaxInsertionLimit; use std::cmp::min; use std::mem; @@ -132,7 +133,7 @@ impl PureDrillStorageOnlySoloOwned { owned_resource_tile_list } - pub fn get_inventories(&mut self) -> (&[ITEMCOUNTTYPE], &mut [ITEMCOUNTTYPE]) { + pub fn get_inventories(&mut self) -> (MaxInsertionLimit<'_>, &mut [ITEMCOUNTTYPE]) { (ALWAYS_FULL, self.inventory.as_mut_slice()) } diff --git a/src/mining_drill/with_shared_ore.rs b/src/mining_drill/with_shared_ore.rs index 18dc320..1e5a05a 100644 --- a/src/mining_drill/with_shared_ore.rs +++ b/src/mining_drill/with_shared_ore.rs @@ -1,5 +1,6 @@ use std::{cmp::min, iter, u8}; +use crate::storage_list::MaxInsertionLimit; use crate::{ data::DataStore, item::{ITEMCOUNTTYPE, IdxTrait, Item, WeakIdxTrait}, @@ -237,7 +238,7 @@ impl PureDrillStorageWithSharedOreTiles { Joule(0) } - pub fn get_inventories(&mut self) -> (&[ITEMCOUNTTYPE], &mut [ITEMCOUNTTYPE]) { + pub fn get_inventories(&mut self) -> (MaxInsertionLimit<'_>, &mut [ITEMCOUNTTYPE]) { (ALWAYS_FULL, self.inventory.as_mut_slice()) } } diff --git a/src/storage_list.rs b/src/storage_list.rs index cfd7281..fc3f5ce 100644 --- a/src/storage_list.rs +++ b/src/storage_list.rs @@ -1,5 +1,6 @@ use core::panic; use std::iter; +use std::ops::Index; use std::u16; use itertools::Itertools; @@ -22,11 +23,36 @@ use crate::{ split_arbitrary::split_arbitrary_mut_slice, }; -// FIXME: We just yeet 10MB of RAM into the wind here :/ -pub const ALWAYS_FULL: &'static [ITEMCOUNTTYPE] = &[0; 10_000_000]; -pub const PANIC_ON_INSERT: &'static [ITEMCOUNTTYPE] = &[0; 0]; +pub const ALWAYS_FULL: MaxInsertionLimit<'static> = MaxInsertionLimit::Global(0); +pub const PANIC_ON_INSERT: MaxInsertionLimit<'static> = MaxInsertionLimit::PerMachine(&[]); -type SingleGridStorage<'a, 'b> = (&'a [ITEMCOUNTTYPE], &'b mut [ITEMCOUNTTYPE]); +#[derive(Debug)] +pub enum MaxInsertionLimit<'a> { + PerMachine(&'a [ITEMCOUNTTYPE]), + Global(ITEMCOUNTTYPE), +} + +impl<'a> Index for MaxInsertionLimit<'a> { + type Output = ITEMCOUNTTYPE; + fn index(&self, index: usize) -> &Self::Output { + // return &30; + match self { + MaxInsertionLimit::PerMachine(items) => &items[index], + MaxInsertionLimit::Global(value) => value, + } + } +} +impl<'a> MaxInsertionLimit<'a> { + fn get(&self, index: usize) -> Option<&ITEMCOUNTTYPE> { + // return Some(&30); + match self { + MaxInsertionLimit::PerMachine(items) => items.get(index), + MaxInsertionLimit::Global(value) => Some(value), + } + } +} + +type SingleGridStorage<'a, 'b> = (MaxInsertionLimit<'a>, &'b mut [ITEMCOUNTTYPE]); pub type SingleItemStorages<'a, 'b> = &'a mut [SingleGridStorage<'b, 'b>]; //[SingleGridStorage; NUM_RECIPES * NUM_GRIDS]; pub type FullStorages<'a, 'b> = Box<[SingleGridStorage<'a, 'b>]>; //[SingleGridStorage; NUM_ITEMS * NUM_RECIPES * NUM_GRIDS]; @@ -352,7 +378,7 @@ pub fn storages_by_item<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( num_power_grids, data_store, ), - (v.0, v.1, v.2.len()), + (v.0, v.1, v.3.len()), ) }) .collect(); @@ -471,7 +497,7 @@ fn all_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( Item = ( Item, Storage, - &'a [ITEMCOUNTTYPE], + MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], ), > + use<'a, 'b, ItemIdxType, RecipeIdxType> { @@ -493,9 +519,9 @@ fn all_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( pub fn static_storages_pre_sorted<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( item: Item, chest_store: &'a mut MultiChestStore, - drill_lists: (&'a [ITEMCOUNTTYPE], &'a mut [ITEMCOUNTTYPE]), + drill_lists: (MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE]), data_store: &'b DataStore, -) -> impl Iterator +) -> impl Iterator, &'a mut [ITEMCOUNTTYPE])> + use<'a, 'b, ItemIdxType, RecipeIdxType> { let grid_size = grid_size(item, data_store); let static_size = static_size(item, data_store); @@ -518,7 +544,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait Item = ( Item, Storage, - &'a [ITEMCOUNTTYPE], + MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], ), > + use<'a, 'b, ItemIdxType, RecipeIdxType> { @@ -1378,7 +1404,7 @@ fn all_lab_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( Item = ( Item, Storage, - &'a [ITEMCOUNTTYPE], + MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], ), > + use<'a, 'b, ItemIdxType, RecipeIdxType> { @@ -1391,7 +1417,11 @@ fn all_lab_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( ( item, Storage::Lab { grid, index: 0 }, - max_insert.as_slice(), + // max_insert.as_slice(), + match max_insert.get(0) { + Some(v) => MaxInsertionLimit::Global(*v), + None => PANIC_ON_INSERT, + }, science.as_mut_slice(), ) }) @@ -1406,7 +1436,7 @@ fn all_static_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( Item = ( Item, Storage, - &'a [ITEMCOUNTTYPE], + MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], ), > + use<'a, 'b, ItemIdxType, RecipeIdxType> { @@ -1475,7 +1505,7 @@ fn all_chest_storages<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( Item = ( Item, Storage, - &'a [ITEMCOUNTTYPE], + MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], ), > + use<'a, ItemIdxType, RecipeIdxType> { From a07f37b645c89e4042c967478a112b0b46c08343 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sat, 15 Nov 2025 12:14:48 +0100 Subject: [PATCH 033/152] Correctly count types --- src/assembler/bucketed.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/assembler/bucketed.rs b/src/assembler/bucketed.rs index dc43b23..c652b23 100644 --- a/src/assembler/bucketed.rs +++ b/src/assembler/bucketed.rs @@ -804,6 +804,8 @@ impl Date: Sun, 16 Nov 2025 13:33:48 +0100 Subject: [PATCH 034/152] Fix the tests --- src/inserter/belt_storage_pure_buckets.rs | 41 ++++++++--- src/inserter/storage_storage_with_buckets.rs | 71 ++++++++++++++----- .../storage_storage_with_buckets_indirect.rs | 22 ++++-- 3 files changed, 106 insertions(+), 28 deletions(-) diff --git a/src/inserter/belt_storage_pure_buckets.rs b/src/inserter/belt_storage_pure_buckets.rs index 2be1991..ffde7ab 100644 --- a/src/inserter/belt_storage_pure_buckets.rs +++ b/src/inserter/belt_storage_pure_buckets.rs @@ -1278,6 +1278,7 @@ mod test { use crate::{ belt::smart::SmartBelt, inserter::{FakeUnionStorage, belt_storage_inserter::Dir}, + storage_list::MaxInsertionLimit, }; use super::*; @@ -1324,8 +1325,14 @@ mod test { item, &mut frontend[item], &mut [ - (max_insert.as_slice(), storages_in[item].as_mut_slice()), - (max_insert.as_slice(), storages_out[item].as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in[item].as_mut_slice(), + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out[item].as_mut_slice(), + ), ], &mut belts[item], 10, @@ -1335,8 +1342,14 @@ mod test { item, &mut frontend[item], &mut [ - (max_insert.as_slice(), storages_in[item].as_mut_slice()), - (max_insert.as_slice(), storages_out[item].as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in[item].as_mut_slice(), + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out[item].as_mut_slice(), + ), ], &mut belts[item], 10, @@ -1408,8 +1421,14 @@ mod test { 0, frontend, &mut [ - (max_insert.as_slice(), storage_in.as_mut_slice()), - (max_insert.as_slice(), storage_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_in.as_mut_slice(), + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_out.as_mut_slice(), + ), ], belts, 10, @@ -1419,8 +1438,14 @@ mod test { 0, frontend, &mut [ - (max_insert.as_slice(), storage_in.as_mut_slice()), - (max_insert.as_slice(), storage_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_in.as_mut_slice(), + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_out.as_mut_slice(), + ), ], belts, 10, diff --git a/src/inserter/storage_storage_with_buckets.rs b/src/inserter/storage_storage_with_buckets.rs index 8492fbc..f3f6f02 100644 --- a/src/inserter/storage_storage_with_buckets.rs +++ b/src/inserter/storage_storage_with_buckets.rs @@ -1277,11 +1277,14 @@ mod test { use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; use test::Bencher; - use crate::inserter::{ - FakeUnionStorage, - storage_storage_with_buckets::{ - BucketedStorageStorageInserterStoreFrontend, InserterId, InserterIdentifier, + use crate::{ + inserter::{ + FakeUnionStorage, + storage_storage_with_buckets::{ + BucketedStorageStorageInserterStoreFrontend, InserterId, InserterIdentifier, + }, }, + storage_list::MaxInsertionLimit, }; use super::BucketedStorageStorageInserterStore; @@ -1307,8 +1310,14 @@ mod test { store[item].update( &mut frontend[item], &mut [ - (max_insert.as_slice(), storages_in[item].as_mut_slice()), - (max_insert.as_slice(), storages_out[item].as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in[item].as_mut_slice(), + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out[item].as_mut_slice(), + ), ], 10, current_tick, @@ -1366,8 +1375,14 @@ mod test { store.update( frontend, &mut [ - (max_insert.as_slice(), storage_in.as_mut_slice()), - (max_insert.as_slice(), storage_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_in.as_mut_slice(), + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_out.as_mut_slice(), + ), ], 10, current_tick, @@ -1404,8 +1419,14 @@ mod test { store.update( &mut frontend, &mut [ - (max_insert.as_slice(), storages_in.as_mut_slice()), - (max_insert.as_slice(), storages_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in.as_mut_slice(), + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out.as_mut_slice(), + ), ], 10, current_time, @@ -1491,8 +1512,14 @@ mod test { store.update( &mut frontend, &mut [ - (max_insert.as_slice(), storages_in.as_mut_slice()), - (max_insert.as_slice(), storages_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in.as_mut_slice(), + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out.as_mut_slice(), + ), ], 10, current_time, @@ -1528,8 +1555,14 @@ mod test { store.update( &mut frontend, &mut [ - (max_insert.as_slice(), storages_in.as_mut_slice()), - (max_insert.as_slice(), storages_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in.as_mut_slice(), + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out.as_mut_slice(), + ), ], 10, current_time, @@ -1603,8 +1636,14 @@ mod test { store.update( &mut frontend, &mut [ - (max_insert.as_slice(), storages_in.as_mut_slice()), - (max_insert.as_slice(), storages_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in.as_mut_slice(), + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out.as_mut_slice(), + ), ], 10, current_time, diff --git a/src/inserter/storage_storage_with_buckets_indirect.rs b/src/inserter/storage_storage_with_buckets_indirect.rs index 9840a5c..87cbab0 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -936,6 +936,8 @@ mod test { use rand::{random, seq::SliceRandom}; use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; + use crate::storage_list::MaxInsertionLimit; + use super::*; #[bench] @@ -957,8 +959,14 @@ mod test { store[item].update( 0, &mut [ - (max_insert.as_slice(), storages_in[item].as_mut_slice()), - (max_insert.as_slice(), storages_out[item].as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_in[item].as_mut_slice(), + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storages_out[item].as_mut_slice(), + ), ], 10, current_tick, @@ -1012,8 +1020,14 @@ mod test { store.update( 0, &mut [ - (max_insert.as_slice(), storage_in.as_mut_slice()), - (max_insert.as_slice(), storage_out.as_mut_slice()), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_in.as_mut_slice(), + ), + ( + MaxInsertionLimit::PerMachine(max_insert.as_slice()), + storage_out.as_mut_slice(), + ), ], 10, current_tick, From f3df270f08da3f0eba239341460103b5be3d1593 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 20 Nov 2025 13:39:22 +0100 Subject: [PATCH 035/152] NEw blueprint format with immediate inserter time --- src/blueprint/blueprint_string.rs | 12 ++++++-- src/blueprint/mod.rs | 30 +++++++++++-------- test_blueprints/lots_of_belts.bp | 4 +-- .../murphy/megabase_new_blueprint_format.bp | 4 +-- .../red_and_green_with_clocking.bp | 3 -- test_blueprints/red_sci.bp | 3 -- .../red_sci_with_beacons_and_belts.bp | 3 -- test_blueprints/solar_tile.bp | 4 +-- 8 files changed, 34 insertions(+), 29 deletions(-) delete mode 100644 test_blueprints/red_and_green_with_clocking.bp delete mode 100644 test_blueprints/red_sci.bp delete mode 100644 test_blueprints/red_sci_with_beacons_and_belts.bp diff --git a/src/blueprint/blueprint_string.rs b/src/blueprint/blueprint_string.rs index 76b9b50..27c6db1 100644 --- a/src/blueprint/blueprint_string.rs +++ b/src/blueprint/blueprint_string.rs @@ -1,5 +1,6 @@ use base64::engine::general_purpose::STANDARD; use flate2::Compression; +use log::error; use super::Blueprint; use super::BlueprintAction; @@ -46,7 +47,7 @@ struct BlueprintStringInternal { Option, )>, - inserters: Vec<(BaseEntity, Option)>, + inserters: Vec<(BaseEntity, Option, Option>)>, set_recipe: Vec<(Position, usize)>, movetime: Vec<(Position, Option>)>, @@ -69,6 +70,7 @@ impl TryFrom for Blueprint { let Ok(internal) = bincode::serde::decode_from_reader(dec, bincode::config::standard()) else { + error!("Blueprint failed to deserialize!"); return Err(()); }; @@ -95,6 +97,8 @@ impl TryFrom for Blueprint { ores, } = internal; + // dbg!(&movetime); + let actions = assemblers .into_iter() .map(|BaseEntity { pos, ty, rotation }| { @@ -207,12 +211,14 @@ impl TryFrom for Blueprint { )); let actions = actions.chain(inserters.into_iter().map( - |(BaseEntity { pos, ty, rotation }, filter)| { + |(BaseEntity { pos, ty, rotation }, filter, movetime)| { BlueprintAction::PlaceEntity(BlueprintPlaceEntity::Inserter { pos, ty: data_strings[ty].clone(), dir: rotation, filter: filter.map(|idx| data_strings[idx].clone()), + + movetime, }) }, )); @@ -308,6 +314,7 @@ impl From for BlueprintString { dir, filter, ty, + movetime, } => { internal.inserters.push(( BaseEntity { @@ -318,6 +325,7 @@ impl From for BlueprintString { filter.map(|item| { internal.data_strings.get_index_or_insert(item.into()) }), + movetime, )); }, super::BlueprintPlaceEntity::Belt { diff --git a/src/blueprint/mod.rs b/src/blueprint/mod.rs index 59801dc..e7fea3f 100644 --- a/src/blueprint/mod.rs +++ b/src/blueprint/mod.rs @@ -105,6 +105,8 @@ enum BlueprintPlaceEntity { /// The Item the inserter will move, must fit both the in and output side filter: Option>, + movetime: Option>, + #[serde(default = "default_inserter")] ty: Arc, }, @@ -190,13 +192,15 @@ impl BlueprintAction { dir, filter, ty, - user_movetime: _, + user_movetime, } => BlueprintPlaceEntity::Inserter { pos, dir, filter: filter .map(|item| data_store.item_names[item.into_usize()].clone()), ty: data_store.inserter_infos[ty as usize].name.clone(), + + movetime: user_movetime, }, PlaceEntityType::Belt { pos, direction, ty } => { BlueprintPlaceEntity::Belt { @@ -342,6 +346,7 @@ impl BlueprintAction { dir, filter, ty, + movetime, } => PlaceEntityType::Inserter { pos: *pos, dir: *dir, @@ -365,7 +370,7 @@ impl BlueprintAction { .position(|info| info.name == *ty) .expect("No inserter for name") as u8, - user_movetime: None, + user_movetime: *movetime, }, BlueprintPlaceEntity::Belt { pos, @@ -981,7 +986,7 @@ impl Blueprint { }, BlueprintAction::SetRecipe { pos, .. } => (1, 3, (BeltId::Pure(0), 0), *pos, 1), BlueprintAction::OverrideInserterMovetime { pos, .. } => { - (1, 5, (BeltId::Pure(0), 0), *pos, 1) + (1, 6, (BeltId::Pure(0), 0), *pos, 1) }, BlueprintAction::AddModules { pos, .. } => (1, 3, (BeltId::Pure(0), 0), *pos, 1), BlueprintAction::SetChestSlotLimit { pos, .. } => (1, 3, (BeltId::Pure(0), 0), *pos, 1), @@ -1285,18 +1290,19 @@ impl Blueprint { dir: *direction, filter: None, ty: data_store.inserter_infos[*ty as usize].name.clone(), + movetime: *user_movetime, }, )]; - if let Some(user_movetime) = *user_movetime { - ret.push(BlueprintAction::OverrideInserterMovetime { - pos: Position { - x: pos.x - base_pos.x, - y: pos.y - base_pos.y, - }, - new_movetime: Some(user_movetime), - }); - } + // if let Some(user_movetime) = *user_movetime { + // ret.push(BlueprintAction::OverrideInserterMovetime { + // pos: Position { + // x: pos.x - base_pos.x, + // y: pos.y - base_pos.y, + // }, + // new_movetime: Some(user_movetime), + // }); + // } ret }, diff --git a/test_blueprints/lots_of_belts.bp b/test_blueprints/lots_of_belts.bp index 822488a..3fded4c 100644 --- a/test_blueprints/lots_of_belts.bp +++ b/test_blueprints/lots_of_belts.bp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:728b246aec89787dc88b73dadfbed119e01261d1eb110c94cede3dbc320e4794 -size 10096 +oid sha256:01d20091aefaee493c769ff343c7bdca55cb697e4160c192c9d1ef8e484b4795 +size 240 diff --git a/test_blueprints/murphy/megabase_new_blueprint_format.bp b/test_blueprints/murphy/megabase_new_blueprint_format.bp index d4aafcd..8761a06 100644 --- a/test_blueprints/murphy/megabase_new_blueprint_format.bp +++ b/test_blueprints/murphy/megabase_new_blueprint_format.bp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b3cfe5eb02d043c0dcee88f753473e087f7ff3f7ba2c7095e0733d8345086ae -size 8355640 +oid sha256:46bed03db9d17c70b0ba0c8de6d3ce1990458e2753dc06d6d4a09871d79c16c9 +size 7395268 diff --git a/test_blueprints/red_and_green_with_clocking.bp b/test_blueprints/red_and_green_with_clocking.bp deleted file mode 100644 index 2718740..0000000 --- a/test_blueprints/red_and_green_with_clocking.bp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a08a6159c8b5f4cc40c3c6541ba6ce735a232862755fa4b58bd56e67d99df447 -size 117875 diff --git a/test_blueprints/red_sci.bp b/test_blueprints/red_sci.bp deleted file mode 100644 index 43a502a..0000000 --- a/test_blueprints/red_sci.bp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:772a709b63a46d70b3d34ba581e9f9ddf5fb168f881339dcb53e649de7c7ba0c -size 6415 diff --git a/test_blueprints/red_sci_with_beacons_and_belts.bp b/test_blueprints/red_sci_with_beacons_and_belts.bp deleted file mode 100644 index 2c3c6ab..0000000 --- a/test_blueprints/red_sci_with_beacons_and_belts.bp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0be6c5a23c94e49f69580da798630aab2664edc5ef4ace0ba2afe14a10dfb606 -size 37415 diff --git a/test_blueprints/solar_tile.bp b/test_blueprints/solar_tile.bp index 3934598..3797c45 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:9ffa0b20b177f29b19871a3b201fb430ef7892c99cb63e83248fec42604e3ec1 -size 572 +oid sha256:31bd098354202734055ba9da08dcacfa9d8b76a4fac7b14efcdc3db2be208993 +size 576 From e9e01e4c91a74b5a55fec596920b5a31f2cfa85f Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 20 Nov 2025 13:42:46 +0100 Subject: [PATCH 036/152] Add power drain --- src/assembler/mod.rs | 1 + src/assembler/simd.rs | 2 +- src/data/factorio_1_1.fgmod | 7 +++++++ src/data/factorio_1_1.rs | 4 ++++ src/data/mod.rs | 3 +++ src/power/mod.rs | 6 ++++-- src/power/power_grid.rs | 22 +++++++++++++++++----- 7 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/assembler/mod.rs b/src/assembler/mod.rs index 986e561..7627d69 100644 --- a/src/assembler/mod.rs +++ b/src/assembler/mod.rs @@ -614,6 +614,7 @@ impl Iterator for ZipArray { } } +#[derive(Debug)] pub enum PowerUsageInfo { ByType(Vec), Combined(Watt), diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index 70f4733..1227627 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -527,7 +527,7 @@ impl RawDataStore { display_name: "Assembling Machine".to_string(), tile_size: (3, 3), working_power_draw: Watt(150_000), + power_drain: Watt(2_500), fluid_connection_offsets: vec![], fluid_connection_flowthrough: vec![], base_bonus_prod: 0, @@ -381,6 +382,7 @@ pub fn get_raw_data_fn() -> RawDataStore { display_name: "Assembling Machine 2".to_string(), tile_size: (3, 3), working_power_draw: Watt(75_000), + power_drain: Watt(5_000), fluid_connection_offsets: vec![], fluid_connection_flowthrough: vec![], base_bonus_prod: 0, @@ -392,6 +394,7 @@ pub fn get_raw_data_fn() -> RawDataStore { display_name: "Assembling Machine 3".to_string(), tile_size: (3, 3), working_power_draw: Watt(375_000), + power_drain: Watt(12_500), fluid_connection_offsets: vec![], fluid_connection_flowthrough: vec![], base_bonus_prod: 0, @@ -403,6 +406,7 @@ pub fn get_raw_data_fn() -> RawDataStore { display_name: "Electric Furnace".to_string(), tile_size: (3, 3), working_power_draw: Watt(180000), + power_drain: Watt(6_000), fluid_connection_offsets: vec![], fluid_connection_flowthrough: vec![], num_module_slots: 2, diff --git a/src/data/mod.rs b/src/data/mod.rs index 2add391..69a8c01 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -65,6 +65,7 @@ struct RawAssemblingMachine { name: String, display_name: String, tile_size: (u8, u8), + power_drain: Watt, working_power_draw: Watt, fluid_connection_offsets: Vec, fluid_connection_flowthrough: Vec, @@ -329,6 +330,8 @@ pub struct AssemblerInfo { pub base_speed: u8, pub base_prod: u8, pub base_power_consumption: Watt, + + pub power_drain: Watt, } impl AssemblerInfo { diff --git a/src/power/mod.rs b/src/power/mod.rs index 1126760..9c1d31c 100644 --- a/src/power/mod.rs +++ b/src/power/mod.rs @@ -1066,7 +1066,8 @@ impl PowerGridStorage>(); - let (research_progress, production_info, times_labs_used_science, beacon_updates) = self + let (research_progress, production_info, times_labs_used_science, beacon_updates) = { + self .power_grids .par_iter_mut() .map(|grid| grid.update(&solar_production, tech_state, current_tick, data_store)) @@ -1082,7 +1083,8 @@ impl PowerGridStorage PowerGrid PowerGrid PowerGrid Date: Thu, 20 Nov 2025 13:45:31 +0100 Subject: [PATCH 037/152] Remove outdated and unmaintained examples --- src/app_state.rs | 95 ------------------------------------------------ 1 file changed, 95 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 780353f..17c97a3 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -171,44 +171,6 @@ impl GameState, - data_store: &DataStore, - ) -> Self { - let mut ret = GameState::new_with_world_area( - Position { x: 0, y: 0 }, - Position { x: 3500, y: 31000 }, - data_store, - ); - - const BP_STRING: &'static str = include_str!("../test_blueprints/red_sci.bp"); - let bp: Blueprint = ron::de::from_str(BP_STRING).unwrap(); - // let file = File::open("test_blueprints/red_sci.bp").unwrap(); - // let bp: Blueprint = ron::de::from_reader(file).unwrap(); - let bp = bp.get_reusable(false, data_store); - - puffin::set_scopes_on(false); - let y_range = (1590..3000).step_by(7); - let x_range = (1590..3000).step_by(20); - - let total = y_range.size_hint().0 * x_range.size_hint().0; - - let mut current = 0; - - for y_pos in y_range { - for x_pos in x_range.clone() { - progress.store((current as f64 / total as f64).to_bits(), Ordering::Relaxed); - current += 1; - - bp.apply(Position { x: x_pos, y: y_pos }, &mut ret, data_store); - } - } - puffin::set_scopes_on(true); - - ret - } - #[must_use] pub fn new_with_megabase( use_solar_field: bool, @@ -381,63 +343,6 @@ impl GameState, - data_store: &DataStore, - ) -> Self { - Self::new_with_beacon_red_green_production_many_grids(progress, data_store) - } - - #[must_use] - pub fn new_with_beacon_red_green_production_many_grids( - progress: Arc, - data_store: &DataStore, - ) -> Self { - let mut ret = GameState::new_with_world_area( - Position { x: 0, y: 0 }, - Position { x: 44000, y: 44000 }, - data_store, - ); - - const BP_STRING: &'static str = - include_str!("../test_blueprints/red_and_green_with_clocking.bp"); - let bp: Blueprint = ron::de::from_str(BP_STRING).unwrap(); - // let file = File::open("test_blueprints/red_and_green_with_clocking.bp").unwrap(); - // let bp: Blueprint = ron::de::from_reader(file).unwrap(); - let bp = bp.get_reusable(false, data_store); - - puffin::set_scopes_on(false); - let y_range = (0..40_000).step_by(4_000); - let x_range = (0..40_000).step_by(4_000); - - let total = y_range.size_hint().0 * x_range.size_hint().0; - - let mut current = 0; - - for y_start in y_range { - for x_start in x_range.clone() { - progress.store((current as f64 / total as f64).to_bits(), Ordering::Relaxed); - current += 1; - for y_pos in (1590..4000).step_by(40) { - for x_pos in (1590..4000).step_by(50) { - bp.apply( - Position { - x: x_start + x_pos, - y: y_start + y_pos, - }, - &mut ret, - data_store, - ); - } - } - } - } - puffin::set_scopes_on(true); - - ret - } - #[must_use] pub fn new_with_beacon_belt_production( progress: Arc, From fcccd85ac19ba13dc91c104df6bde926d1b33720 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 20 Nov 2025 13:45:55 +0100 Subject: [PATCH 038/152] Add a stage profiling scope --- src/app_state.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 17c97a3..5de277f 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -2724,7 +2724,9 @@ impl GameState GameState Date: Thu, 20 Nov 2025 13:46:23 +0100 Subject: [PATCH 039/152] Warn when trying to set inserter speed on empty tile instead of silent error --- src/app_state.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 5de277f..5645d9c 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1288,10 +1288,10 @@ impl GameState { - game_state - .world - .get_entity_at_mut(*pos, data_store) - .map(|e| match e { + let e = game_state.world.get_entity_at_mut(*pos, data_store); + + if let Some(e) = e { + match e { Entity::Inserter { ty, user_movetime, @@ -1351,7 +1351,10 @@ impl GameState { warn!("Tried to set Inserter Settings on non inserter"); }, - }); + } + } else { + warn!("Tried to set Inserter Settings on empty tile"); + } }, ActionType::PlaceEntity(place_entity_info) => { let force = place_entity_info.force; From f5ee49ced860ee6221bf8201bc0848238d6d3a6f Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 20 Nov 2025 13:47:03 +0100 Subject: [PATCH 040/152] Remove outdated examples --- src/lib.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 90f0259..8f13519 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -323,15 +323,6 @@ fn run_integrated_server( .unwrap(), StartGameInfo::Create(info) => match info { GameCreationInfo::Empty => GameState::new(&data_store), - GameCreationInfo::RedGreen => { - GameState::new_with_beacon_production(progress, &data_store) - }, - GameCreationInfo::RedGreenBelts => { - GameState::new_with_beacon_belt_production(progress, &data_store) - }, - GameCreationInfo::RedWithLabs => { - GameState::new_with_production(progress, &data_store) - }, GameCreationInfo::Megabase(use_solar_field) => { GameState::new_with_megabase(use_solar_field, progress, &data_store) }, @@ -352,6 +343,8 @@ fn run_integrated_server( }, GameCreationInfo::FromBP(path) => GameState::new_with_bp(&data_store, path), + + _ => unimplemented!(), }, }); @@ -411,7 +404,7 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { let raw_data = get_raw_data_test(); let data_store = raw_data.process(); - let progress = Default::default(); + // let progress = Default::default(); let connections: Arc>> = Arc::default(); @@ -424,10 +417,7 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { data::DataStoreOptions::ItemU8RecipeU8(data_store) => { let game_state = load(todo!("Add a console argument for the save file path")) .map(|save| save.game_state) - .unwrap_or_else(|| { - // GameState::new(&data_store) - GameState::new_with_beacon_production(progress, &data_store) - }); + .unwrap_or_else(|| GameState::new(&data_store)); let mut game = Game::new( GameInitData::DedicatedServer( From 4dc6f19a21bcd8d0304915f649b34a4c193b24d9 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 20 Nov 2025 16:28:36 +0100 Subject: [PATCH 041/152] Time usage statistics --- src/statistics/mod.rs | 1 + src/statistics/time_usage.rs | 80 ++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 src/statistics/time_usage.rs diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs index 6c40a86..54c31df 100644 --- a/src/statistics/mod.rs +++ b/src/statistics/mod.rs @@ -21,6 +21,7 @@ mod power; pub mod production; pub mod recipe; pub mod research; +pub mod time_usage; pub const NUM_DIFFERENT_TIMESCALES: usize = 5; pub const SAMPLES_FOR_SMOOTHING_BASE: usize = 600; diff --git a/src/statistics/time_usage.rs b/src/statistics/time_usage.rs new file mode 100644 index 0000000..3ac634b --- /dev/null +++ b/src/statistics/time_usage.rs @@ -0,0 +1,80 @@ +use std::{ + collections::BTreeMap, + iter, + ops::{Add, AddAssign}, + time::Duration, +}; + +use crate::item::IdxTrait; + +use super::IntoSeries; + +#[cfg(feature = "client")] +use egui_show_info_derive::ShowInfo; +#[cfg(feature = "client")] +use get_size2::GetSize; +use itertools::Itertools; + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)] +pub struct TimeUsageInfo { + full_update_time: Duration, +} + +impl Add<&TimeUsageInfo> for TimeUsageInfo { + type Output = TimeUsageInfo; + + fn add(mut self, rhs: &TimeUsageInfo) -> Self::Output { + self.full_update_time += rhs.full_update_time; + + self + } +} + +impl AddAssign<&TimeUsageInfo> for TimeUsageInfo { + fn add_assign(&mut self, rhs: &TimeUsageInfo) { + self.full_update_time += rhs.full_update_time; + } +} + +impl IntoSeries<(), ItemIdxType, RecipeIdxType> + for TimeUsageInfo +{ + fn into_series( + values: &[Self], + smoothing_window: usize, + _filter: Option bool>, + _data_store: &crate::data::DataStore, + ) -> impl Iterator { + BTreeMap::from_iter( + values + .windows(smoothing_window) + .map(|infos| { + let TimeUsageInfo { full_update_time } = + infos.iter().fold(TimeUsageInfo::default(), |a, b| a + b); + + iter::once(( + (0, "Full Update Time"), + full_update_time.as_secs_f32() * 1000.0, + )) + }) + .flatten() + .into_group_map() + .into_iter() + .map(|(k, v)| (k.0, (k.1, v))), + ) + .into_iter() + .map(move |(time_id, a)| { + ( + time_id, + ( + a.0, + a.1.into_iter() + .map(|v| v as f32 / smoothing_window as f32) + .collect(), + ) + .into(), + ) + }) + } +} From b8aa513c5ed1baa0b96068a00fdf34e5dbee2986 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 20 Nov 2025 16:52:17 +0100 Subject: [PATCH 042/152] Fix some bucketed assembler things --- src/assembler/bucketed.rs | 273 ++++++++++++++++++++++++++------------ 1 file changed, 185 insertions(+), 88 deletions(-) diff --git a/src/assembler/bucketed.rs b/src/assembler/bucketed.rs index c652b23..c5de285 100644 --- a/src/assembler/bucketed.rs +++ b/src/assembler/bucketed.rs @@ -1,5 +1,5 @@ use std::cmp::{max, min}; -use std::{array, iter, u8}; +use std::{array, iter, u8, u16}; use itertools::Itertools; use log::warn; @@ -29,6 +29,7 @@ pub struct MultiAssemblerStore< const NUM_INGS: usize, const NUM_OUTPUTS: usize, > { + current_power_tick: u32, recipe: Recipe, num_by_types: Vec, @@ -81,21 +82,16 @@ impl recipe_outs: &[ITEMCOUNTTYPE; NUM_OUTPUTS], power_subticks_per_craft: TIMERTYPE, - power_subtick_overshoot: u8, + ticks_passed: u16, ) -> (NextUpdateInfo, bool, bool, bool) { - let ticks_per_main: u16 = - power_subticks_per_craft * 20 / u16::from(data.combined_speed_mod); + let ticks_per_main: u16 = (power_subticks_per_craft as u32 * 20 + / u32::from(data.combined_speed_mod)) + .try_into() + .unwrap(); let tick_till_main_done: u16 = ticks_per_main - data.timer; - let power_subticks_passed = if data.bonus_productivity > 0 { - let ticks_per_prod: u16 = ticks_per_main * 100 / u16::from(data.bonus_productivity); - let tick_till_prod_done: u16 = ticks_per_prod - data.prod_timer; - - min(tick_till_main_done, tick_till_prod_done) + u16::from(power_subtick_overshoot) - } else { - tick_till_main_done + u16::from(power_subtick_overshoot) - }; + let power_subticks_passed = ticks_passed; let (is_idle, main_produced, prod_produced) = Self::apply_subticks( &mut ings, @@ -108,7 +104,7 @@ impl ); let next_update = if !is_idle { - Self::get_next_update_info_running(power_subticks_per_craft, data) + Self::get_next_update_info_running(ticks_per_main, data) } else { // We are waiting for ingredients or space // Check again next tick @@ -129,8 +125,10 @@ impl power_subticks_per_craft: TIMERTYPE, num_subticks: TIMERTYPE, ) -> (bool, bool, bool) { - let ticks_per_main: u16 = - power_subticks_per_craft * 20 / u16::from(data.combined_speed_mod); + let ticks_per_main: u16 = (power_subticks_per_craft as u32 * 20 + / u32::from(data.combined_speed_mod)) + .try_into() + .unwrap(); if ings .into_iter() @@ -145,10 +143,13 @@ impl data.prod_timer += num_subticks; let (main_done, prod_done) = ( - (data.timer >= ticks_per_main).then_some(data.timer - ticks_per_main), + data.timer.checked_sub(ticks_per_main), if data.bonus_productivity > 0 { - let ticks_per_prod: u16 = ticks_per_main * 100 / u16::from(data.bonus_productivity); - (data.prod_timer >= ticks_per_prod).then_some(data.prod_timer - ticks_per_prod) + let ticks_per_prod: u16 = (u32::from(ticks_per_main) * 100 + / u32::from(data.bonus_productivity)) + .try_into() + .unwrap(); + data.prod_timer.checked_sub(ticks_per_prod) } else { None }, @@ -163,8 +164,8 @@ impl for (ing, recipe) in ings.into_iter().zip(recipe_ings.iter()) { debug_assert!(**ing >= *recipe); } - data.timer += num_subticks; - data.prod_timer += num_subticks; + // data.timer += num_subticks; + // data.prod_timer += num_subticks; (true, false, false) }, (None, Some(prod_overshoot)) => { @@ -175,13 +176,14 @@ impl } data.prod_timer = prod_overshoot; - data.timer += num_subticks; + // data.timer += num_subticks; (true, false, true) } else { // No space (The last tick, the timer will not increase) - data.timer += num_subticks - prod_overshoot - 1; - data.prod_timer += num_subticks - prod_overshoot - 1; + // data.timer += num_subticks - prod_overshoot - 1; + // data.prod_timer += num_subticks - prod_overshoot - 1; + data.prod_timer -= prod_overshoot + 1; // Since prod_timer is right below ticks_per_prod we will check again next tick by get_next_update_info_running @@ -202,13 +204,15 @@ impl } data.timer = main_overshoot; - data.prod_timer += num_subticks; + // data.prod_timer += num_subticks; (true, true, false) } else { // No space (The last tick, the timer will not increase) - data.timer += num_subticks - main_overshoot - 1; - data.prod_timer = num_subticks - main_overshoot - 1; + // data.timer += num_subticks - main_overshoot - 1; + // data.prod_timer = num_subticks - main_overshoot - 1; + data.timer -= main_overshoot + 1; + data.prod_timer -= main_overshoot + 1; // Since prod_timer is right below ticks_per_prod we will check again next tick by get_next_update_info_running @@ -217,38 +221,57 @@ impl } }, (Some(main_overshoot), Some(prod_overshoot)) => { - let first_overshoot = max(main_overshoot, prod_overshoot); - - let (idle, main_done, prod_done) = Self::apply_subticks( - ings, - outs, - data, - recipe_ings, - recipe_outs, - power_subticks_per_craft, - num_subticks - first_overshoot, - ); - - let rest = min(main_overshoot, prod_overshoot); + let main_is_first_done = main_overshoot < prod_overshoot; - let (second_idle, second_main_done, second_prod_done) = Self::apply_subticks( - ings, - outs, - data, - recipe_ings, - recipe_outs, - power_subticks_per_craft, - rest, - ); + if check_space(outs) { + for (out, recipe) in outs.into_iter().zip(recipe_outs) { + **out += *recipe; + } + if main_is_first_done { + for (ing, recipe) in ings.into_iter().zip(recipe_ings.iter()) { + debug_assert!(**ing >= *recipe); + **ing -= *recipe; + } + } + if check_space(outs) { + for (out, recipe) in outs.into_iter().zip(recipe_outs) { + **out += *recipe; + } + if !main_is_first_done { + for (ing, recipe) in ings.into_iter().zip(recipe_ings.iter()) { + debug_assert!(**ing >= *recipe); + **ing -= *recipe; + } + } + + data.timer = main_overshoot; + data.prod_timer = prod_overshoot; + + (true, true, true) + } else { + if main_is_first_done { + data.timer = main_overshoot; + data.prod_timer -= (prod_overshoot - main_overshoot) + 1; + } else { + data.prod_timer = prod_overshoot; + data.timer -= (main_overshoot - prod_overshoot) + 1; + } + + (false, main_is_first_done, !main_is_first_done) + } + } else { + let overshoot = max(main_overshoot, prod_overshoot); + data.timer -= overshoot + 1; + data.prod_timer -= overshoot + 1; - debug_assert!(!(main_done && second_main_done)); - debug_assert!(!(prod_done && second_prod_done)); + (false, false, false) + } - ( - !idle || !second_idle, - main_done && second_main_done, - prod_done && second_prod_done, - ) + // ( + // !idle || !second_idle, + // main_done && second_main_done, + // prod_done && second_prod_done, + // ) }, }; @@ -259,12 +282,14 @@ impl power_subticks_per_main: u16, data: &AssemblerDataStruct, ) -> NextUpdateInfo { - let tick_till_main_done: u16 = power_subticks_per_main - data.timer; + let tick_till_main_done: u16 = power_subticks_per_main.checked_sub(data.timer).unwrap(); let when = if data.bonus_productivity > 0 { - let subticks_per_prod: u16 = - power_subticks_per_main * 100 / u16::from(data.bonus_productivity); - let tick_till_prod_done: u16 = subticks_per_prod - data.prod_timer; + let subticks_per_prod: u16 = (u32::from(power_subticks_per_main) * 100 + / u32::from(data.bonus_productivity)) + .try_into() + .unwrap(); + let tick_till_prod_done: u16 = subticks_per_prod.checked_sub(data.prod_timer).unwrap(); min(tick_till_main_done, tick_till_prod_done) } else { @@ -272,6 +297,7 @@ impl }; NextUpdateInfo { when } + // NextUpdateInfo { when: 1 } } } @@ -286,6 +312,8 @@ struct AssemblerDataStruct { bonus_productivity: u8, combined_speed_mod: u8, ty: u8, + + last_update_time: u32, } #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] @@ -319,12 +347,13 @@ impl 0 { + if let Ok(ticks_per_prod) = u16::try_from( + ticks_per_main as u32 * 100 + / u32::from(self.hot_data[index as usize].bonus_productivity), + ) { + f32::from(self.hot_data[index as usize].prod_timer + ticks_passed) + / f32::from(ticks_per_prod) + } else { + // TODO: This is bad, since productivity no longer works here + 0.0 + } + } else { + 0.0 + }; + super::AssemblerOnclickInfo { inputs: self .ings @@ -393,10 +457,9 @@ impl(), - "Some Assembler are not being updated!" - ); - } + // #[cfg(debug_assertions)] + // { + // assert!( + // self.buckets + // .waiting_for_update + // .iter() + // .flat_map(|v| v.iter()) + // .all_unique(), + // "Some Assembler in multiple update buckets" + // ); + // assert_eq!( + // self.hot_data.len(), + // self.buckets + // .waiting_for_update + // .iter() + // .map(|v| v.len()) + // .sum::(), + // "Some Assembler are not being updated!" + // ); + // } let (ing_idx, out_idx) = recipe_lookup[recipe.into_usize()]; @@ -553,7 +624,9 @@ impl 0 { + // dbg!(&power_used); + } ( PowerUsageInfo::ByType( @@ -729,6 +823,7 @@ impl Date: Thu, 20 Nov 2025 16:52:58 +0100 Subject: [PATCH 043/152] Fix power for increased usage with drain --- src/app_state.rs | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 5645d9c..0f7d8d9 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -186,7 +186,7 @@ impl GameState GameState GameState GameState GameState Date: Fri, 21 Nov 2025 20:26:42 +0100 Subject: [PATCH 044/152] Add waitlist support in simd assemblers --- src/assembler/simd.rs | 90 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index 1227627..9cc04a8 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -13,6 +13,7 @@ use crate::{ MASKTYPE, SIMDTYPE, data::{AssemblerInfo, DataStore, ItemRecipeDir}, frontend::world::Position, + inserter::FakeUnionStorage, item::{ITEMCOUNTTYPE, IdxTrait, Indexable, Recipe, WeakIdxTrait}, power::{ Watt, @@ -21,6 +22,7 @@ use crate::{ storage_list::{MaxInsertionLimit, PANIC_ON_INSERT}, }; use itertools::{Either, Itertools}; +use static_assertions::const_assert; use super::{AssemblerOnclickInfo, PowerUsageInfo, Simdtype, TIMERTYPE, arrays}; @@ -29,9 +31,32 @@ use egui_show_info_derive::ShowInfo; #[cfg(feature = "client")] use get_size2::GetSize; +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[repr(align(64))] +struct InserterWaitList { + inserters: [Option; 3], +} + +const_assert!(std::mem::size_of::() <= 64); + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] +struct Inserter { + item: u8, + self_is_source: bool, + // Ideally we would track the hand here so we avoid having to reinsert them each time the assembler produces anything + // This does mean we can only fit 3 Inserters per cacheline :/ + // This is fixed by the item arena optimization + current_hand: ITEMCOUNTTYPE, + max_hand: ITEMCOUNTTYPE, + movetime: u16, + index: u32, + other: FakeUnionStorage, +} + // FIXME: We store the same slice length n times! // TODO: Don´t clump update data and data for adding/removing assemblers together! - // FIXME: Using Boxed slices here is probably the main contributor to the time usage for building large power grids, since this means reallocation whenever we add assemblers! #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -76,6 +101,11 @@ pub struct MultiAssemblerStore< timers: Box<[TIMERTYPE]>, prod_timers: Box<[TIMERTYPE]>, + // FIXME: For me to be able to add inserters to waitlists during parallel/per item updates, + // We would need a list of waitlists per ING and OUTPUT item + // This is a pretty big commitment in terms of memory + waitlists: Box<[InserterWaitList]>, + holes: Vec, positions: Box<[Position]>, types: Box<[u8]>, @@ -363,6 +393,25 @@ impl .as_array(), ); } + + // let has_produced = timer_mask | prod_timer_mask; + + // for (i, has_produced) in has_produced.to_array().into_iter().enumerate() { + // if has_produced { + // let final_idx = index + i; + + // self.waitlists[final_idx] + // .inserters + // .iter_mut() + // .for_each(|v| { + // let v = v.take(); + + // if let Some(v) = v { + // todo!() + // } + // }); + // } + // } } if timer_mask.any() { for i in 0..NUM_INGS { @@ -373,6 +422,29 @@ impl timer_mask.select(ings - our_ings, ings).cast().as_array(), ); } + + // let has_consumed = timer_mask; + // for (i, has_consumed) in has_consumed.to_array().into_iter().enumerate() { + // if has_consumed { + // let final_idx = index + i; + + // self.waitlists[final_idx] + // .inserters + // .iter_mut() + // .for_each(|v| { + // let v = v.take(); + + // if let Some(v) = v { + // todo!() + // // FIXME: Here we technically need to insert the inserter back into its storage. + // // But since different recipes/power grids are updated in parallel this will not work :/ + // // So we either collect into a temporary collection, which does not sound ideal, or ensure only a single + // // assembler update with any individual item is active at the same time. + // // That also does not sound great + // } + // }); + // } + // } } times_ings_used += (timer_mask.to_int().reduce_sum() * -1) as u32; num_finished_crafts += ((timer_mask.to_int().reduce_sum() @@ -459,6 +531,8 @@ impl Date: Fri, 21 Nov 2025 20:27:49 +0100 Subject: [PATCH 045/152] Stop crash when at zero samples in debug mode --- src/rendering/render_world.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index e9eb436..0b4a849 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -4214,9 +4214,13 @@ pub fn render_ui< .product::(); let ticks_total = min( ticks_per_sample * NUM_SAMPLES_AT_INTERVALS[time_scale], - (aux_data.statistics.production.num_samples_pushed - - ticks_per_sample - + 1) + (aux_data + .statistics + .production + .num_samples_pushed + .checked_sub(ticks_per_sample) + .map(|v| v + 1) + .unwrap_or(0)) .next_multiple_of(ticks_per_sample), ); From eb8bab95ce371090d672ef1bf169a1b36157de56 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 21 Nov 2025 20:28:00 +0100 Subject: [PATCH 046/152] Drain --- src/data/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/data/mod.rs b/src/data/mod.rs index 69a8c01..81b8e7a 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1592,6 +1592,8 @@ impl RawDataStore { base_prod: m.base_bonus_prod, base_power_consumption: m.working_power_draw, + power_drain: m.power_drain, + fluid_connections: m .fluid_connection_offsets .iter() From 32a49abc52f90b0c0f86ecc9068fa1fdabd7cd89 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 21 Nov 2025 20:28:11 +0100 Subject: [PATCH 047/152] Update shell --- codium.nix | 3 ++- shell.nix | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/codium.nix b/codium.nix index 99b1a4d..f6ad7e0 100644 --- a/codium.nix +++ b/codium.nix @@ -1,12 +1,13 @@ let nix-vscode-extensions.url = "github:nix-community/nix-vscode-extensions"; - pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/1750f3c1c89488e2ffdd47cab9d05454dddfb734.tar.gz")) { }; + pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60.tar.gz")) { }; addr2linePkg = pkgs.callPackage ./addr2line-rs/default.nix {}; in pkgs.mkShell { buildInputs = [ ] ++ (with pkgs; [ bacon + openssl (vscode-with-extensions.override { vscode = vscodium; diff --git a/shell.nix b/shell.nix index 6bcf3da..8d16ff1 100644 --- a/shell.nix +++ b/shell.nix @@ -1,5 +1,5 @@ let - pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/929116e316068c7318c54eb4d827f7d9756d5e9c.tar.gz")) { overlays = [ ]; }; + pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60.tar.gz")) { overlays = [ ]; }; buildInputs = [ ] ++ (with pkgs; [ rustup From acb566cf6d6f31bb972726b2c3c536c20a5b9f9d Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 21 Nov 2025 20:28:22 +0100 Subject: [PATCH 048/152] proptest --- proptest-regressions/inserter/mod.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/proptest-regressions/inserter/mod.txt b/proptest-regressions/inserter/mod.txt index 12d51b2..f1f3c08 100644 --- a/proptest-regressions/inserter/mod.txt +++ b/proptest-regressions/inserter/mod.txt @@ -6,3 +6,4 @@ # everyone who runs the test benefits from these saved cases. cc a27a3dc53d7764e25eaa4974387a987dd428e75859b930d82490208fccf4a1a1 # shrinks to (item, num_grids, storage) = (Item { id: 0 }, 1, Lab { grid: 0, index: 0 }) cc da0b9b95353896ab4b3c74e271842d68b773f18aa829b4f174a035584bd104e6 # shrinks to (item, num_grids, storage) = (Item { id: 0 }, 1, Static { index: 0, static_id: Chest }) +cc 4bfd9265944575f3527bc20200aad3fb6292511435a7723bc421b76e052de433 # shrinks to (item, _num_grids, storage) = (Item { id: 0 }, 1, Assembler { grid: 0, recipe_idx_with_this_item: 1, index: 0 }) From 13f90972addbd92d8edec0e24dec8f77d57856e6 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 23 Nov 2025 19:14:49 +0100 Subject: [PATCH 049/152] Add waitlist support to assemblers --- src/app_state.rs | 194 +++++- src/assembler/bucketed.rs | 55 +- src/assembler/mod.rs | 259 +++++++- src/assembler/simd.rs | 578 +++++++++++++++--- src/chest.rs | 206 ++++++- src/data/mod.rs | 18 +- src/frontend/world/tile.rs | 37 +- src/inserter/belt_storage_inserter.rs | 4 +- .../belt_storage_inserter_non_const_gen.rs | 4 +- src/inserter/belt_storage_pure_buckets.rs | 4 +- src/inserter/mod.rs | 18 +- src/inserter/storage_storage_inserter.rs | 4 +- src/inserter/storage_storage_with_buckets.rs | 4 +- .../storage_storage_with_buckets_indirect.rs | 247 +++++--- src/item.rs | 2 +- src/lib.rs | 1 + src/liquid/mod.rs | 4 +- src/power/mod.rs | 96 ++- src/power/power_grid.rs | 397 ++++++------ src/storage_list.rs | 211 +++++-- test_blueprints/lots_of_belts.bp | 4 +- 21 files changed, 1857 insertions(+), 490 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 0f7d8d9..b544154 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -8,7 +8,9 @@ use crate::frontend::action::place_entity::PlaceEntityInfo; use crate::frontend::world::tile::ModuleSlots; use crate::frontend::world::tile::ModuleTy; use crate::inserter::InserterStateInfo; +use crate::inserter::WaitlistSearchSide; use crate::inserter::storage_storage_with_buckets_indirect::BucketedStorageStorageInserterStore; +use crate::inserter::storage_storage_with_buckets_indirect::InserterBucketData; use crate::inserter::storage_storage_with_buckets_indirect::InserterIdentifier; use crate::item::ITEMCOUNTTYPE; use crate::join_many::join; @@ -698,13 +700,20 @@ impl StorageStorageInserterStore { item: Item, movetime: u16, id: InserterIdentifier, - ) { + ) -> Result<(), WaitlistSearchSide> { let inserter = self.inserters[item.into_usize()] .get_mut(&movetime) .unwrap() .0 .remove_inserter(id); - // TODO: Handle what happens with the items + + match inserter { + Ok(_) => { + // TODO: Handle what happens with the items + Ok(()) + }, + Err(side) => Err(side), + } } #[must_use] @@ -714,25 +723,43 @@ impl StorageStorageInserterStore { old_movetime: u16, new_movetime: u16, id: InserterIdentifier, - ) -> InserterIdentifier { + ) -> Result< + InserterIdentifier, + ( + WaitlistSearchSide, + impl FnMut(FakeUnionStorage, FakeUnionStorage, ITEMCOUNTTYPE) -> InserterIdentifier, + ), + > { // FIXME: This does not preserve the inserter state at all! - let inserter = self.inserters[item.into_usize()] + let inner_id = match self.inserters[item.into_usize()] .get_mut(&old_movetime) .unwrap() .0 - .remove_inserter(id); - - let inner_id: InserterIdentifier = self.inserters[item.into_usize()] - .entry(new_movetime) - .or_insert_with(|| (BucketedStorageStorageInserterStore::new(new_movetime),)) - .0 - .add_inserter( - inserter.storage_id_in, - inserter.storage_id_out, - inserter.max_hand_size, - ); + .remove_inserter(id) + { + Ok(inserter) => self.inserters[item.into_usize()] + .entry(new_movetime) + .or_insert_with(|| (BucketedStorageStorageInserterStore::new(new_movetime),)) + .0 + .add_inserter( + inserter.storage_id_in, + inserter.storage_id_out, + inserter.max_hand_size, + ), + Err(wait_list_side) => { + return Err((wait_list_side, move |source, dest, max_hand_size| { + self.inserters[item.into_usize()] + .entry(new_movetime) + .or_insert_with( + || (BucketedStorageStorageInserterStore::new(new_movetime),), + ) + .0 + .add_inserter(source, dest, max_hand_size) + })); + }, + }; - inner_id + Ok(inner_id) } #[profiling::function] @@ -1296,6 +1323,8 @@ impl GameState { match info { @@ -1330,7 +1359,7 @@ impl GameState GameState new_id, + Err((search_side, mut handle_fn)) => { + match search_side { + WaitlistSearchSide::Source => { + let start_pos = data_store + .inserter_start_pos( + *ty, + *inserter_pos, + *direction, + ); + + let item = *item; + let inserter = *inserter; + match game_state + .world + .get_entity_at( + start_pos, data_store, + ) + .unwrap() + { + Entity::Assembler { + info: + AssemblerInfo::Powered { + id, + .. + }, + .. + } => { + let removed = game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores.remove_wait_list_inserter(*id, item, inserter.id, data_store); + handle_fn( + removed.storage_id_in, + removed.storage_id_out, + removed.max_hand.into(), + ) + }, + + e => unreachable!("{e:?}"), + } + }, + WaitlistSearchSide::Dest => { + let end_pos = data_store + .inserter_end_pos( + *ty, + *inserter_pos, + *direction, + ); + + let item = *item; + let inserter = *inserter; + match game_state + .world + .get_entity_at( + end_pos, data_store, + ) + .unwrap() + { + Entity::Assembler { + info: + AssemblerInfo::Powered { + id, + .. + }, + .. + } => { + let removed = game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores.remove_wait_list_inserter(*id, item, inserter.id, data_store); + handle_fn( + removed.storage_id_in, + removed.storage_id_out, + removed.max_hand.into(), + ) + }, + + e => unreachable!("{e:?}"), + } + }, + } + }, + }; + let Some(Entity::Inserter { + user_movetime, + info: + InserterInfo::Attached { + info: + AttachedInserter::StorageStorage { + inserter, + .. + }, + }, + .. + }) = game_state + .world + .get_entity_at_mut(*pos, data_store) + else { + unreachable!(); + }; + *user_movetime = + Some(new_movetime.try_into().unwrap()); *inserter = new_id; } }, }, } - *user_movetime = *new_movetime; }, _ => { warn!("Tried to set Inserter Settings on non inserter"); @@ -2727,7 +2852,7 @@ impl GameState GameState( recipe: Recipe, + _grid: PowerGridIdentifier, data_store: &DataStore, ) -> Self { let ticks_per_craft = data_store.recipe_timers[recipe.into_usize()]; @@ -367,6 +369,14 @@ impl( + &mut self, + new_grid_id: PowerGridIdentifier, + data_store: &DataStore, + ) { + // Do nothing + } + fn get_recipe(&self) -> Recipe { self.recipe } @@ -589,7 +599,12 @@ impl, - ) -> (PowerUsageInfo, u32, u32) + ) -> ( + PowerUsageInfo, + u32, + u32, + impl Iterator>, + ) where RecipeIdxType: IdxTrait, { @@ -719,6 +734,7 @@ impl; NUM_INGS], [&mut [ITEMCOUNTTYPE]; NUM_INGS], + [&mut [InserterWaitList]; NUM_INGS], ), - [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], - ) { ( - ( - self.ings_max_insert.each_mut().map(|b| match b.get(0) { - Some(v) => MaxInsertionLimit::Global(*v), - None => PANIC_ON_INSERT, - }), - self.ings.each_mut().map(|b| &mut **b), - ), - self.outputs.each_mut().map(|b| &mut **b), - ) + [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], + [&mut [InserterWaitList]; NUM_OUTPUTS], + ), + ) { + // ( + // ( + // self.ings_max_insert.each_mut().map(|b| match b.get(0) { + // Some(v) => MaxInsertionLimit::Global(*v), + // None => PANIC_ON_INSERT, + // }), + // self.ings.each_mut().map(|b| &mut **b), + // ), + // self.outputs.each_mut().map(|b| &mut **b), + // ) + todo!() } fn modify_modifiers( @@ -984,4 +1005,14 @@ impl usize { self.hot_data.len() - self.holes.len() } + + fn remove_wait_list_inserter( + &mut self, + index: u32, + item: crate::item::Item, + id: crate::inserter::storage_storage_with_buckets_indirect::InserterId, + data_store: &DataStore, + ) -> super::simd::InserterReinsertionInfo { + unreachable!() + } } diff --git a/src/assembler/mod.rs b/src/assembler/mod.rs index 7627d69..dc139d4 100644 --- a/src/assembler/mod.rs +++ b/src/assembler/mod.rs @@ -1,6 +1,8 @@ use std::{array, marker::PhantomData, simd::Simd, u8}; +use crate::assembler::simd::{Inserter, InserterReinsertionInfo, InserterWaitList}; use crate::frontend::world::tile::ModuleTy; +use crate::inserter::storage_storage_with_buckets_indirect::InserterId; use crate::storage_list::MaxInsertionLimit; use crate::{ data::DataStore, @@ -91,69 +93,72 @@ impl< > { #[must_use] - pub fn new(data_store: &DataStore) -> Self { + pub fn new( + grid_id: PowerGridIdentifier, + data_store: &DataStore, + ) -> Self { let assemblers_0_1 = data_store .ing_out_num_to_recipe .get(&(0, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_1_1 = data_store .ing_out_num_to_recipe .get(&(1, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_2_1 = data_store .ing_out_num_to_recipe .get(&(2, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_2_2 = data_store .ing_out_num_to_recipe .get(&(2, 2)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_2_3 = data_store .ing_out_num_to_recipe .get(&(2, 3)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_3_1 = data_store .ing_out_num_to_recipe .get(&(3, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_4_1 = data_store .ing_out_num_to_recipe .get(&(4, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_5_1 = data_store .ing_out_num_to_recipe .get(&(5, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); let assemblers_6_1 = data_store .ing_out_num_to_recipe .get(&(6, 1)) .unwrap() .iter() - .map(|r| MultiAssemblerStore::new(*r, data_store)) + .map(|r| MultiAssemblerStore::new(*r, grid_id, data_store)) .collect(); Self { @@ -283,6 +288,54 @@ impl< ) } + pub fn set_grid( + &mut self, + grid_id: PowerGridIdentifier, + data_store: &DataStore, + ) { + 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; + + for ass in assemblers_0_1 { + ass.set_grid_id(grid_id, data_store); + } + + for ass in assemblers_1_1 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_2_1 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_2_2 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_2_3 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_3_1 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_4_1 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_5_1 { + ass.set_grid_id(grid_id, data_store); + } + for ass in assemblers_6_1 { + ass.set_grid_id(grid_id, data_store); + } + } + pub fn get_info( &self, assembler_id: AssemblerID, @@ -507,6 +560,165 @@ impl< } } + pub fn remove_wait_list_inserter( + &mut self, + assembler_id: AssemblerID, + item: Item, + inserter_id: InserterId, + data_store: &DataStore, + ) -> simd::InserterReinsertionInfo { + let recipe_id = assembler_id.recipe.id.into(); + + match ( + data_store.recipe_num_ing_lookup[recipe_id], + data_store.recipe_num_out_lookup[recipe_id], + ) { + (0, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_0_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_0_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter( + assembler_id.assembler_index, + item, + inserter_id, + data_store, + ) + }, + (1, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_1_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_1_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter( + assembler_id.assembler_index, + item, + inserter_id, + data_store, + ) + }, + (2, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_2_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_2_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter( + assembler_id.assembler_index, + item, + inserter_id, + data_store, + ) + }, + + (2, 2) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_2_2[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_2_2[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter( + assembler_id.assembler_index, + item, + inserter_id, + data_store, + ) + }, + + (2, 3) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_2_3[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_2_3[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter( + assembler_id.assembler_index, + item, + inserter_id, + data_store, + ) + }, + + (3, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_3_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_3_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter( + assembler_id.assembler_index, + item, + inserter_id, + data_store, + ) + }, + + (4, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_4_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_4_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter( + assembler_id.assembler_index, + item, + inserter_id, + data_store, + ) + }, + + (5, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_5_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_5_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter( + assembler_id.assembler_index, + item, + inserter_id, + data_store, + ) + }, + + (6, 1) => { + assert_eq!( + assembler_id.recipe, + self.assemblers_6_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .get_recipe() + ); + + self.assemblers_6_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] + .remove_wait_list_inserter( + assembler_id.assembler_index, + item, + inserter_id, + data_store, + ) + }, + + _ => unreachable!(), + } + } + pub fn num_assemblers(&self) -> usize { let Self { assemblers_0_1, @@ -637,8 +849,14 @@ pub trait MultiAssemblerStore< { fn new( recipe: Recipe, + grid: PowerGridIdentifier, data_store: &DataStore, ) -> Self; + fn set_grid_id( + &mut self, + new_grid_id: PowerGridIdentifier, + data_store: &DataStore, + ); fn get_recipe(&self) -> Recipe; fn get_info( &self, @@ -666,7 +884,12 @@ pub trait MultiAssemblerStore< recipe_maximums: &[[ITEMCOUNTTYPE; NUM_OUTPUTS]], times: &[TIMERTYPE], data_store: &DataStore, - ) -> (PowerUsageInfo, u32, u32) + ) -> ( + PowerUsageInfo, + u32, + u32, + impl Iterator>, + ) where RecipeIdxType: IdxTrait; @@ -676,8 +899,12 @@ pub trait MultiAssemblerStore< ( [MaxInsertionLimit<'_>; NUM_INGS], [&mut [ITEMCOUNTTYPE]; NUM_INGS], + [&mut [InserterWaitList]; NUM_INGS], + ), + ( + [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], + [&mut [InserterWaitList]; NUM_OUTPUTS], ), - [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], ); fn modify_modifiers( @@ -855,6 +1082,14 @@ pub trait MultiAssemblerStore< } fn num_assemblers(&self) -> usize; + + fn remove_wait_list_inserter( + &mut self, + index: u32, + item: Item, + id: InserterId, + data_store: &DataStore, + ) -> simd::InserterReinsertionInfo; } pub mod arrays { diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index 9cc04a8..fde1ab4 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -1,5 +1,8 @@ use std::{ - array, i32, + array, + cmp::min, + i32, + num::NonZero, ops::{Add, Sub}, simd::{ Simd, @@ -7,14 +10,15 @@ use std::{ num::{SimdInt, SimdUint}, }, u8, + vec::Drain, }; use crate::{ MASKTYPE, SIMDTYPE, data::{AssemblerInfo, DataStore, ItemRecipeDir}, frontend::world::Position, - inserter::FakeUnionStorage, - item::{ITEMCOUNTTYPE, IdxTrait, Indexable, Recipe, WeakIdxTrait}, + inserter::{FakeUnionStorage, Storage, storage_storage_with_buckets_indirect::InserterId}, + item::{ITEMCOUNTTYPE, IdxTrait, Indexable, Item, Recipe, WeakIdxTrait}, power::{ Watt, power_grid::{IndexUpdateInfo, MAX_POWER_MULT, PowerGridEntity, PowerGridIdentifier}, @@ -34,25 +38,47 @@ use get_size2::GetSize; #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[repr(align(64))] -struct InserterWaitList { - inserters: [Option; 3], +pub struct InserterWaitList { + pub inserters: [Option; 4], } const_assert!(std::mem::size_of::() <= 64); #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] -#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] -struct Inserter { - item: u8, - self_is_source: bool, +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Inserter { + // item: u8, + // self_is_source: bool, // Ideally we would track the hand here so we avoid having to reinsert them each time the assembler produces anything // This does mean we can only fit 3 Inserters per cacheline :/ // This is fixed by the item arena optimization - current_hand: ITEMCOUNTTYPE, - max_hand: ITEMCOUNTTYPE, - movetime: u16, - index: u32, - other: FakeUnionStorage, + pub current_hand: ITEMCOUNTTYPE, + pub max_hand: NonZero, + pub movetime: u16, + pub(crate) index: InserterId, + pub other: FakeUnionStorage, +} + +#[derive(Debug)] +pub struct InserterReinsertionInfo { + pub movetime: u16, + pub item: Item, + pub current_hand: ITEMCOUNTTYPE, + pub max_hand: ITEMCOUNTTYPE, + pub(crate) index: InserterId, + pub storage_id_in: FakeUnionStorage, + pub storage_id_out: FakeUnionStorage, +} + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone)] +struct InternalInserterReinsertionInfo { + pub movetime: u16, + pub item: u8, + pub max_hand: ITEMCOUNTTYPE, + pub(crate) index: InserterId, + pub self_index: u32, + pub other: FakeUnionStorage, } // FIXME: We store the same slice length n times! @@ -104,12 +130,22 @@ pub struct MultiAssemblerStore< // FIXME: For me to be able to add inserters to waitlists during parallel/per item updates, // We would need a list of waitlists per ING and OUTPUT item // This is a pretty big commitment in terms of memory - waitlists: Box<[InserterWaitList]>, + #[serde(with = "arrays")] + waitlists_ings: [Box<[InserterWaitList]>; NUM_INGS], + #[serde(with = "arrays")] + waitlists_outputs: [Box<[InserterWaitList]>; NUM_OUTPUTS], holes: Vec, positions: Box<[Position]>, types: Box<[u8]>, len: usize, + + #[serde(skip)] + inserter_waitlist_output_vec: Vec, + #[serde(with = "arrays")] + self_fake_union_ing: [FakeUnionStorage; NUM_INGS], + #[serde(with = "arrays")] + self_fake_union_out: [FakeUnionStorage; NUM_OUTPUTS], } // TODO: Maybe also add a defragmentation routine to mend the ineffeciencies left by deconstruction large amounts of assemblers @@ -232,6 +268,10 @@ impl times: &[TIMERTYPE], power_list: &[AssemblerInfo], ) -> (Watt, u32, u32) { + // FIXME: This could technically not be enough if enough items are produced in a single tick + self.inserter_waitlist_output_vec + .reserve((self.len * 4).saturating_sub(self.inserter_waitlist_output_vec.len())); + let (ing_idx, out_idx) = recipe_lookup[self.recipe.id.into()]; let our_ings: &[ITEMCOUNTTYPE; NUM_INGS] = &recipe_ings[ing_idx]; @@ -381,70 +421,138 @@ impl *timer_arr = new_timer.to_array(); *prod_timer_arr = new_prod_timer.to_array(); if timer_mask.any() || prod_timer_mask.any() { - for i in 0..NUM_OUTPUTS { - let our_outputs = SIMDTYPE::splat(our_outputs[i].into()); - let outputs: Simd = - Simd::::from_slice(&self.outputs[i][index..]); - let outputs = outputs.cast(); - let int_output = timer_mask.select(outputs + our_outputs, outputs); - self.outputs[i][index..(index + 16)].copy_from_slice( - (prod_timer_mask.select(int_output + our_outputs, int_output)) - .cast() - .as_array(), - ); - } - - // let has_produced = timer_mask | prod_timer_mask; - - // for (i, has_produced) in has_produced.to_array().into_iter().enumerate() { - // if has_produced { - // let final_idx = index + i; - - // self.waitlists[final_idx] - // .inserters - // .iter_mut() - // .for_each(|v| { - // let v = v.take(); - - // if let Some(v) = v { - // todo!() - // } - // }); - // } + // for i in 0..NUM_OUTPUTS { + // let our_outputs = SIMDTYPE::splat(our_outputs[i].into()); + // let outputs: Simd = + // Simd::::from_slice(&self.outputs[i][index..]); + // let outputs = outputs.cast(); + // let int_output = timer_mask.select(outputs + our_outputs, outputs); + // self.outputs[i][index..(index + 16)].copy_from_slice( + // (prod_timer_mask.select(int_output + our_outputs, int_output)) + // .cast() + // .as_array(), + // ); // } + + let has_produced = timer_mask | prod_timer_mask; + + for (i, has_produced) in has_produced.to_array().into_iter().enumerate() { + if has_produced { + let final_idx = index + i; + let mut items = our_outputs.clone(); + + for (item, (out, items_to_distribute)) in self + .waitlists_outputs + .iter_mut() + .zip(&mut items) + .enumerate() + { + for ins in &mut out[final_idx].inserters { + if let Some(v) = ins { + let amount_taken_by_this_inserter = min( + *items_to_distribute, + ITEMCOUNTTYPE::from(v.max_hand) - v.current_hand, + ); + if v.current_hand + amount_taken_by_this_inserter + == ITEMCOUNTTYPE::from(v.max_hand) + { + let ins = ins.take().unwrap(); + let Ok(()) = + self.inserter_waitlist_output_vec.push_within_capacity( + InternalInserterReinsertionInfo { + movetime: ins.movetime, + item: (NUM_INGS + item) as u8, + max_hand: ins.max_hand.into(), + index: ins.index, + self_index: final_idx as u32, + other: ins.other, + }, + ) + else { + panic!( + "Not enough space in inserter readdition vec. Capacity is {}", + self.inserter_waitlist_output_vec.capacity() + ); + }; + } else { + v.current_hand += amount_taken_by_this_inserter; + } + *items_to_distribute -= amount_taken_by_this_inserter; + + // TODO: Check if this is good or bad + if *items_to_distribute == 0 { + break; + } + } + } + + if *items_to_distribute > 0 { + self.outputs[item][final_idx] += *items_to_distribute; + } + } + } + } } if timer_mask.any() { - for i in 0..NUM_INGS { - let ings: Simd = Simd::::from_slice(&self.ings[i][index..]); - let ings = ings.cast(); - let our_ings = SIMDTYPE::splat(our_ings[i].into()); - self.ings[i][index..(index + 16)].copy_from_slice( - timer_mask.select(ings - our_ings, ings).cast().as_array(), - ); - } - - // let has_consumed = timer_mask; - // for (i, has_consumed) in has_consumed.to_array().into_iter().enumerate() { - // if has_consumed { - // let final_idx = index + i; - - // self.waitlists[final_idx] - // .inserters - // .iter_mut() - // .for_each(|v| { - // let v = v.take(); - - // if let Some(v) = v { - // todo!() - // // FIXME: Here we technically need to insert the inserter back into its storage. - // // But since different recipes/power grids are updated in parallel this will not work :/ - // // So we either collect into a temporary collection, which does not sound ideal, or ensure only a single - // // assembler update with any individual item is active at the same time. - // // That also does not sound great - // } - // }); - // } + // for i in 0..NUM_INGS { + // let ings: Simd = Simd::::from_slice(&self.ings[i][index..]); + // let ings = ings.cast(); + // let our_ings = SIMDTYPE::splat(our_ings[i].into()); + // self.ings[i][index..(index + 16)].copy_from_slice( + // timer_mask.select(ings - our_ings, ings).cast().as_array(), + // ); // } + + let has_consumed = timer_mask; + for (i, has_consumed) in has_consumed.to_array().into_iter().enumerate() { + if has_consumed { + let final_idx = index + i; + let mut items = our_ings.clone(); + + for (item, (ing, items_to_drain)) in + self.waitlists_ings.iter_mut().zip(&mut items).enumerate() + { + for ins in &mut ing[final_idx].inserters { + if let Some(v) = ins { + let amount_taken_by_this_inserter = + min(*items_to_drain, v.current_hand); + if v.current_hand - amount_taken_by_this_inserter == 0 { + let ins = ins.take().unwrap(); + let Ok(()) = + self.inserter_waitlist_output_vec.push_within_capacity( + InternalInserterReinsertionInfo { + movetime: ins.movetime, + item: item as u8, + max_hand: ins.max_hand.into(), + index: ins.index, + self_index: final_idx as u32, + other: ins.other, + }, + ) + else { + panic!( + "Not enough space in inserter readdition vec. Capacity is {}.", + self.inserter_waitlist_output_vec.capacity() + ); + }; + } else { + v.current_hand -= amount_taken_by_this_inserter; + } + *items_to_drain -= amount_taken_by_this_inserter; + + // TODO: Check if this is good or bad + if *items_to_drain == 0 { + break; + } + } + } + + if *items_to_drain > 0 { + self.ings[item][final_idx] -= *items_to_drain; + } + } + } + } } times_ings_used += (timer_mask.to_int().reduce_sum() * -1) as u32; num_finished_crafts += ((timer_mask.to_int().reduce_sum() @@ -507,7 +615,8 @@ impl( recipe: Recipe, - _data_store: &DataStore, + grid: PowerGridIdentifier, + data_store: &DataStore, ) -> Self { Self { recipe, @@ -531,15 +640,92 @@ impl( + &mut self, + new_grid_id: PowerGridIdentifier, + data_store: &DataStore, + ) { + self.self_fake_union_ing = { + array::from_fn(|index| { + let item = data_store.recipe_item_index_to_item[self.recipe.into_usize()][index]; + + FakeUnionStorage::from_storage_with_statics_at_zero( + item, + Storage::Assembler { + grid: new_grid_id, + recipe_idx_with_this_item: self.recipe.id, + index: 0, + }, + data_store, + ) + .unwrap() + }) + }; + self.self_fake_union_out = { + array::from_fn(|index| { + let item = data_store.recipe_item_index_to_item[self.recipe.into_usize()] + [index + NUM_INGS]; + + FakeUnionStorage::from_storage_with_statics_at_zero( + item, + Storage::Assembler { + grid: new_grid_id, + recipe_idx_with_this_item: self.recipe.id, + index: 0, + }, + data_store, + ) + .unwrap() + }) + }; + } + fn modify_modifiers( &mut self, index: u32, @@ -883,8 +1069,18 @@ impl other.inserter_waitlist_output_vec.capacity() + { + self.inserter_waitlist_output_vec + } else { + other.inserter_waitlist_output_vec + }, + + self_fake_union_ing: { + array::from_fn(|index| { + let item = + data_store.recipe_item_index_to_item[self.recipe.into_usize()][index]; + + FakeUnionStorage::from_storage_with_statics_at_zero( + item, + Storage::Assembler { + grid: new_grid_id, + recipe_idx_with_this_item: self.recipe.id, + index: 0, + }, + data_store, + ) + .unwrap() + }) + }, + self_fake_union_out: { + array::from_fn(|index| { + let item = data_store.recipe_item_index_to_item[self.recipe.into_usize()] + [index + NUM_INGS]; + + FakeUnionStorage::from_storage_with_statics_at_zero( + item, + Storage::Assembler { + grid: new_grid_id, + recipe_idx_with_this_item: self.recipe.id, + index: 0, + }, + data_store, + ) + .unwrap() + }) + }, }; #[cfg(debug_assertions)] @@ -1001,7 +1241,12 @@ impl, - ) -> (PowerUsageInfo, u32, u32) + ) -> ( + PowerUsageInfo, + u32, + u32, + impl Iterator>, + ) where RecipeIdxType: IdxTrait, { @@ -1015,7 +1260,54 @@ impl; NUM_INGS], [&mut [ITEMCOUNTTYPE]; NUM_INGS], + [&mut [InserterWaitList]; NUM_INGS], + ), + ( + [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], + [&mut [InserterWaitList]; NUM_OUTPUTS], ), - [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], ) { ( ( @@ -1035,8 +1331,12 @@ impl usize { self.len - self.holes.len() } + + fn remove_wait_list_inserter( + &mut self, + index: u32, + item: Item, + id: InserterId, + data_store: &DataStore, + ) -> self::InserterReinsertionInfo { + let item_index = data_store.recipe_item_index_to_item[self.recipe.into_usize()] + .iter() + .position(|recipe_item| recipe_item == &item) + .unwrap(); + + if item_index < NUM_INGS { + let v = self.waitlists_ings[item_index][index as usize] + .inserters + .iter_mut() + .filter(|v| v.is_some()) + .find(|ins| ins.as_ref().unwrap().index == id) + .unwrap(); + + let ins = v.take().unwrap(); + + InserterReinsertionInfo { + movetime: ins.movetime, + item: item, + current_hand: ins.current_hand, + max_hand: ins.max_hand.into(), + index: ins.index, + // This is an ingredient inserter + storage_id_in: ins.other, + // This is an ingredient inserter + storage_id_out: FakeUnionStorage { + index: index, + grid_or_static_flag: self.self_fake_union_ing[item_index].grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_ing[item_index] + .recipe_idx_with_this_item, + }, + } + } else { + let item_index = item_index - NUM_INGS; + let v = self.waitlists_outputs[item_index][index as usize] + .inserters + .iter_mut() + .filter(|v| v.is_some()) + .find(|ins| ins.as_ref().unwrap().index == id) + .unwrap(); + + let ins = v.take().unwrap(); + + InserterReinsertionInfo { + movetime: ins.movetime, + item: item, + current_hand: ins.current_hand, + max_hand: ins.max_hand.into(), + index: ins.index, + // This is an output inserter + storage_id_in: FakeUnionStorage { + index: index, + grid_or_static_flag: self.self_fake_union_out[item_index].grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_out[item_index] + .recipe_idx_with_this_item, + }, + // This is an output inserter + storage_id_out: ins.other, + } + } + } } #[cfg(test)] @@ -1414,7 +1806,7 @@ mod test { const NUM_ASSEMBLERS: usize = 30_000_000; let mut assemblers: Vec> = (0..NUM_RECIPES as u8) - .map(|_| MultiAssemblerStore::new(Recipe { id: 11 }, &DATA_STORE)) + .map(|_| MultiAssemblerStore::new(Recipe { id: 11 }, 0, &DATA_STORE)) .collect_vec(); let items: Vec> = vec![ diff --git a/src/chest.rs b/src/chest.rs index e8c4eeb..fc96b84 100644 --- a/src/chest.rs +++ b/src/chest.rs @@ -1,10 +1,15 @@ use std::cmp::max; +use std::num::NonZero; use std::{cmp::min, u8}; use rayon::iter::IndexedParallelIterator; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; +use static_assertions::const_assert; -use crate::storage_list::MaxInsertionLimit; +use crate::assembler::simd::{Inserter, InserterReinsertionInfo, InserterWaitList}; +use crate::inserter::FakeUnionStorage; +use crate::inserter::storage_storage_with_buckets_indirect::InserterId; +use crate::storage_list::{InserterWaitLists, MaxInsertionLimit}; use crate::{ data::DataStore, item::{ITEMCOUNTTYPE, IdxTrait, Item, WeakIdxTrait, usize_from}, @@ -21,6 +26,37 @@ const CHEST_GOAL_AMOUNT: ITEMCOUNTTYPE = ITEMCOUNTTYPE::MAX / 2; pub type ChestSize = u32; pub type SignedChestSize = i32; +// #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +// #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +// #[repr(align(64))] +// pub struct InserterWaitList { +// pub inserters: [Option; 3], +// } + +// const_assert!(std::mem::size_of::() <= 64); + +// #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +// #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +// pub struct Inserter { +// // item: u8, +// self_is_source: bool, +// // Ideally we would track the hand here so we avoid having to reinsert them each time the assembler produces anything +// // This does mean we can only fit 3 Inserters per cacheline :/ +// // This is fixed by the item arena optimization +// pub current_hand: ITEMCOUNTTYPE, +// pub max_hand: NonZero, +// pub movetime: u16, +// pub(crate) index: InserterId, +// pub other: FakeUnionStorage, +// } + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone)] +struct InternalInserterReinsertionInfo { + inserter: Inserter, + self_is_source: bool, +} + #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct FullChestStore { @@ -32,17 +68,42 @@ impl FullChestStore { pub fn update( &mut self, data_store: &DataStore, - ) { + ) -> impl IndexedParallelIterator + + ParallelIterator< + Item = impl Iterator>, + > { self.stores .par_iter_mut() .enumerate() - .for_each(|(item_id, store)| { + .map(|(item_id, store)| { + let item = store.item; profiling::scope!( "Chest Update", format!("Item: {}", data_store.item_display_names[item_id]).as_str() ); - store.update_simd() - }); + store.update_simd().map( + move |InternalInserterReinsertionInfo { + inserter, + self_is_source, + }| InserterReinsertionInfo { + movetime: inserter.movetime, + item, + current_hand: inserter.current_hand, + max_hand: inserter.max_hand.into(), + index: inserter.index, + storage_id_in: if self_is_source { + todo!() + } else { + inserter.other + }, + storage_id_out: if self_is_source { + inserter.other + } else { + todo!() + }, + }, + ) + }) } } @@ -57,6 +118,11 @@ pub struct MultiChestStore { max_items: Vec, holes: Vec, + wait_list: Vec, + + #[serde(skip)] + inserter_reinsertion_vec: Vec, + num_large_chests: usize, } @@ -69,8 +135,13 @@ impl MultiChestStore { storage: vec![], max_insert: vec![], max_items: vec![], + + wait_list: vec![], + holes: vec![], + inserter_reinsertion_vec: vec![], + num_large_chests: 0, } } @@ -96,6 +167,8 @@ impl MultiChestStore { self.max_insert[hole] = max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX); self.max_items[hole] = max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX)); + self.wait_list[hole] = InserterWaitList::default(); + hole.try_into().unwrap() } else { self.inout.push(0); @@ -105,6 +178,7 @@ impl MultiChestStore { self.max_items .push(max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX))); + self.wait_list.push(InserterWaitList::default()); (self.inout.len() - 1).try_into().unwrap() } } @@ -120,6 +194,8 @@ impl MultiChestStore { self.max_insert[hole] = max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX); self.max_items[hole] = max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX)); + self.wait_list[hole] = InserterWaitList::default(); + hole.try_into().unwrap() } else { self.inout.push(0); @@ -129,6 +205,8 @@ impl MultiChestStore { self.max_items .push(max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX))); + self.wait_list.push(InserterWaitList::default()); + (self.inout.len() - 1).try_into().unwrap() } } @@ -144,6 +222,7 @@ impl MultiChestStore { self.inout[index] = 0; self.storage[index] = 0; self.max_items[index] = 0; + self.wait_list[index] = InserterWaitList::default(); items } @@ -180,7 +259,9 @@ impl MultiChestStore { } } - pub fn update_simd(&mut self) { + fn update_simd(&mut self) -> impl Iterator { + self.inserter_reinsertion_vec + .reserve((self.inout.len() * 4).saturating_sub(self.inserter_reinsertion_vec.len())); #[cfg(debug_assertions)] { if self.num_large_chests == 0 { @@ -189,32 +270,90 @@ impl MultiChestStore { } // TODO: Splitting this into large chests and small chests would be better since now a single large chest will make all small chests in the world expensive - if self.num_large_chests > 0 { - for (inout, (storage, max_items)) in self - .inout - .iter_mut() - .zip(self.storage.iter_mut().zip(self.max_items.iter().copied())) - { - let to_move = inout.abs_diff(CHEST_GOAL_AMOUNT); - - let switch = ChestSize::from(*inout >= CHEST_GOAL_AMOUNT); - - let moved: SignedChestSize = (switch as SignedChestSize - + (1 - switch as SignedChestSize) * -1) - * (min( - ChestSize::from(to_move), - (max_items - *storage) * switch + (1 - switch) * *storage, - ) as SignedChestSize); - - *inout = (ChestSize::from(*inout)).wrapping_sub_signed(moved) as u8; - *storage = (*storage).wrapping_add_signed(moved) as ChestSize; + // With wait lists we cannot avoid updating all chests (even small chests) + // if self.num_large_chests > 0 { + for ((inout, (storage, max_items)), wait_list) in self + .inout + .iter_mut() + .zip(self.storage.iter_mut().zip(self.max_items.iter().copied())) + .zip(self.wait_list.iter_mut()) + { + // let was_full = *storage == max_items; + // let was_empty = *storage == 0; + let to_move = inout.abs_diff(CHEST_GOAL_AMOUNT); - debug_assert!(*storage <= max_items); - } - } else { - // Since all chests have a max_items of 0, we will never need/want to move items out of the inout - // So we can just return + let switch = ChestSize::from(*inout >= CHEST_GOAL_AMOUNT); + + // if was_empty || was_full { + // for ins in wait_list.inserters.iter_mut() { + // if let Some(inserter) = ins { + // if was_empty && *inout > 0 { + // // There can only be outgoing inserters in the waitlist if we are empty + // // FIXME: This could be wrong IFF a chest inout is filled then emptied in a single tick! + // let taken_by_this_inserter = min( + // ITEMCOUNTTYPE::from(inserter.max_hand) - inserter.current_hand, + // *inout, + // ); + + // *inout -= taken_by_this_inserter; + // inserter.current_hand += taken_by_this_inserter; + + // if inserter.current_hand == ITEMCOUNTTYPE::from(inserter.max_hand) { + // let removed = ins.take().unwrap(); + // self.inserter_reinsertion_vec + // .push_within_capacity(InternalInserterReinsertionInfo { + // inserter: removed, + // self_is_source: true, + // }) + // .unwrap(); + // } + // } else if was_full + // && *inout < max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX) + // { + // // The chest was full + + // // There can only be incoming inserters in the waitlist if we are full + // // FIXME: This could be wrong IFF a chest inout is filled then emptied in a single tick! + // let taken_by_this_inserter = min( + // ITEMCOUNTTYPE::from(inserter.max_hand) - inserter.current_hand, + // max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX) - *inout, + // ); + + // *inout += taken_by_this_inserter; + // inserter.current_hand += taken_by_this_inserter; + + // if inserter.current_hand == ITEMCOUNTTYPE::from(inserter.max_hand) { + // let removed = ins.take().unwrap(); + // self.inserter_reinsertion_vec + // .push_within_capacity(InternalInserterReinsertionInfo { + // inserter: removed, + // self_is_source: false, + // }) + // .unwrap(); + // } + // } + // } + // } + // } + + let mut moved: SignedChestSize = (switch as SignedChestSize + + (1 - switch as SignedChestSize) * -1) + * (min( + ChestSize::from(to_move), + (max_items - *storage) * switch + (1 - switch) * *storage, + ) as SignedChestSize); + + *inout = (ChestSize::from(*inout)).wrapping_sub_signed(moved) as u8; + *storage = (*storage).wrapping_add_signed(moved) as ChestSize; + + debug_assert!(*storage <= max_items); } + // } else { + // // Since all chests have a max_items of 0, we will never need/want to move items out of the inout + // // So we can just return + // } + + self.inserter_reinsertion_vec.drain(..) } pub fn add_items_to_chest( @@ -325,10 +464,15 @@ impl MultiChestStore { pub fn storage_list_slices<'a>( &'a mut self, - ) -> (MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE]) { + ) -> ( + MaxInsertionLimit<'a>, + &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, + ) { ( MaxInsertionLimit::PerMachine(self.max_insert.as_slice()), self.inout.as_mut_slice(), + InserterWaitLists::None, // InserterWaitLists::PerMachine(self.wait_list.as_mut_slice()), ) } } diff --git a/src/data/mod.rs b/src/data/mod.rs index 81b8e7a..6d48f9f 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -578,6 +578,7 @@ pub struct DataStore { pub technology_costs: Vec<(u64, Box<[ITEMCOUNTTYPE]>)>, pub belt_infos: Vec, pub mining_drill_info: Vec, + pub recipe_item_index_to_item: Box<[Box<[Item]>]>, } #[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] @@ -735,7 +736,9 @@ pub struct PowerPoleData { pub connection_range: u8, } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, serde::Deserialize, serde::Serialize)] +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Deserialize, serde::Serialize, +)] pub enum ItemRecipeDir { Ing, Out, @@ -1556,6 +1559,19 @@ impl RawDataStore { }, ], + recipe_item_index_to_item: recipe_to_items + .iter() + .map(|recipe| { + let items = recipe + .clone() + .into_iter() + .sorted_by_key(|p| p.0) + .map(|p| p.1); + + items.collect() + }) + .collect(), + recipe_allowed_assembling_machines: self .recipes .iter() diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index 3b08ed0..28791d6 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -1340,10 +1340,29 @@ fn removal_of_possible_inserter_connection {}, + Err(side) => match side { + crate::inserter::WaitlistSearchSide::Source => { + if start_pos.contained_in(pos, size) { + // No need to remove it. The storage which this inserter was waiting on was removed anywy + } else { + todo!() + } + }, + crate::inserter::WaitlistSearchSide::Dest => { + if end_pos.contained_in(pos, size) { + // No need to remove it. The storage which this inserter was waiting on was removed anywy + } else { + todo!() + } + }, + }, + }; *info = InserterInfo::NotAttached {}; }, @@ -1386,10 +1405,20 @@ fn removal_of_possible_inserter_connection {}, + Err(side) => match side { + crate::inserter::WaitlistSearchSide::Source => { + // No need to remove it. The mining_drill which this inserter was waiting on was removed anywy + }, + crate::inserter::WaitlistSearchSide::Dest => { + todo!() + }, + }, + }; }, } *internal_inserter = InternalInserterInfo::NotAttached {}; diff --git a/src/inserter/belt_storage_inserter.rs b/src/inserter/belt_storage_inserter.rs index 27a67a9..7aabb0e 100644 --- a/src/inserter/belt_storage_inserter.rs +++ b/src/inserter/belt_storage_inserter.rs @@ -82,7 +82,7 @@ impl BeltStorageInserter<{ Dir::BeltToStorage }> { } }, InserterState::WaitingForSpaceInDestination(count) => { - let (max_insert, old) = + let (max_insert, old, _) = index_fake_union(todo!(), storages, self.storage_id, grid_size); let to_insert = min(count, *max_insert - *old); @@ -135,7 +135,7 @@ impl BeltStorageInserter<{ Dir::StorageToBelt }> { match self.state { InserterState::WaitingForSourceItems(count) => { - let (_max_insert, old) = + 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 8a67da5..9d3bd2d 100644 --- a/src/inserter/belt_storage_inserter_non_const_gen.rs +++ b/src/inserter/belt_storage_inserter_non_const_gen.rs @@ -110,7 +110,7 @@ impl BeltStorageInserterDyn { } }, DynInserterState::BSWaitingForSpaceInDestination(count) => { - let (max_insert, old) = + let (max_insert, old, _) = index_fake_union(item_id, storages, self.storage_id, grid_size); let to_insert = min(count, *max_insert - *old); @@ -146,7 +146,7 @@ impl BeltStorageInserterDyn { false }, DynInserterState::SBWaitingForSourceItems(count) => { - let (_max_insert, old) = + 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 ffde7ab..4126620 100644 --- a/src/inserter/belt_storage_pure_buckets.rs +++ b/src/inserter/belt_storage_pure_buckets.rs @@ -634,7 +634,7 @@ impl BucketedStorageStorageInserterStore { } }, Dir::StorageToBelt => { - let (_max_insert, old) = + 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); @@ -676,7 +676,7 @@ impl BucketedStorageStorageInserterStore { ) -> bool { match DIR { Dir::BeltToStorage => { - let (max_insert, old) = + 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); diff --git a/src/inserter/mod.rs b/src/inserter/mod.rs index c19b6ec..ff06cfa 100644 --- a/src/inserter/mod.rs +++ b/src/inserter/mod.rs @@ -55,6 +55,19 @@ enum SushiInserterState { EmptyAndMovingBack(u8), } +#[derive(Debug)] +pub struct WaitlistSearchInfo { + side_to_search: WaitlistSearchSide, + source_id: FakeUnionStorage, + dest_id: FakeUnionStorage, +} + +#[derive(Debug)] +pub enum WaitlistSearchSide { + Source, + Dest, +} + // TODO: We still need to store the timer and hand fullness. For the timer, 5 bits should suffice // I don't think we need to store the recipe_id in 8 bits, since 32 recipes should be max for any resource (so 5 bits) // Current Plan: htttttrrrrrsssssssssssssssssssss @@ -179,7 +192,10 @@ impl FakeUnionStorage { } } - pub fn from_storage_with_statics_at_zero( + pub fn from_storage_with_statics_at_zero< + ItemIdxType: WeakIdxTrait, + RecipeIdxType: WeakIdxTrait, + >( item: Item, storage: Storage, data_store: &DataStore, diff --git a/src/inserter/storage_storage_inserter.rs b/src/inserter/storage_storage_inserter.rs index 8eb259c..f129515 100644 --- a/src/inserter/storage_storage_inserter.rs +++ b/src/inserter/storage_storage_inserter.rs @@ -54,7 +54,7 @@ impl StorageStorageInserter { match self.state { InserterState::WaitingForSourceItems(count) => { - let (_max_insert, old) = + 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); @@ -71,7 +71,7 @@ impl StorageStorageInserter { } }, InserterState::WaitingForSpaceInDestination(count) => { - let (max_insert, old) = + 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 f3f6f02..3b64068 100644 --- a/src/inserter/storage_storage_with_buckets.rs +++ b/src/inserter/storage_storage_with_buckets.rs @@ -653,7 +653,7 @@ impl BucketedStorageStorageInserterStore { _current_tick: u32, _movetime: u16, ) -> bool { - let (_max_insert, old) = + 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); @@ -687,7 +687,7 @@ impl BucketedStorageStorageInserterStore { _current_tick: u32, _movetime: u16, ) -> bool { - let (max_insert, old) = + 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 87cbab0..29eb534 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -5,8 +5,11 @@ use super::{ FakeUnionStorage, InserterStateInfo, storage_storage_with_buckets::LargeInserterState, }; use crate::{ + assembler::simd::{Inserter as WaitListInserter, InserterReinsertionInfo, InserterWaitList}, + inserter::WaitlistSearchSide, item::ITEMCOUNTTYPE, join_many::join, + power::power_grid::PowerGrid, storage_list::{SingleItemStorages, index_fake_union}, }; use std::cmp::min; @@ -56,23 +59,23 @@ pub enum ImplicitState { #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] pub struct InserterIdentifier { - id: InserterId, + pub id: InserterId, } #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] -struct InserterId { +pub(crate) struct InserterId { index: u32, } #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] -struct InserterBucketData { +pub struct InserterBucketData { pub storage_id_in: FakeUnionStorage, pub storage_id_out: FakeUnionStorage, - index: InserterId, - current_hand: ITEMCOUNTTYPE, - max_hand_size: ITEMCOUNTTYPE, + pub index: InserterId, + pub current_hand: ITEMCOUNTTYPE, + pub max_hand_size: ITEMCOUNTTYPE, } #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] @@ -95,6 +98,11 @@ const PLACEHOLDER: InserterState = InserterState { state: ImplicitState::WaitingForSourceItems(0), }; +struct UpdateResult { + extract: bool, + reinsert: bool, +} + impl BucketedStorageStorageInserterStore { pub fn new(movetime: u16) -> Self { Self { @@ -149,71 +157,93 @@ impl BucketedStorageStorageInserterStore { self.movetime as usize + 1 } - pub fn remove_inserter(&mut self, id: InserterIdentifier) -> Inserter { + pub fn remove_inserter( + &mut self, + id: InserterIdentifier, + ) -> Result { let state = self.inserters[id.id.index as usize]; - let ret = |storage_id_in, storage_id_out, max_hand_size| Inserter { - storage_id_in, - storage_id_out, - last_update_time: state.last_update_time, - max_hand_size, - state: state.state, + let ret = |storage_id_in, storage_id_out, max_hand_size| { + Ok(Inserter { + storage_id_in, + storage_id_out, + last_update_time: state.last_update_time, + max_hand_size, + state: state.state, + }) }; - self.inserters[id.id.index as usize] = PLACEHOLDER; - - if let Some(pos) = self - .waiting_for_item - .iter() - .position(|ins| id.id == ins.index) - { - let removed = self.waiting_for_item.remove(pos); + let hand = self.inserters[id.id.index as usize].state; - return ret( - removed.storage_id_in, - removed.storage_id_out, - removed.max_hand_size, - ); - } - - if let Some(pos) = self - .waiting_for_space_in_destination - .iter() - .position(|ins| id.id == ins.index) - { - let removed = self.waiting_for_space_in_destination.remove(pos); - - return ret( - removed.storage_id_in, - removed.storage_id_out, - removed.max_hand_size, - ); - } + self.inserters[id.id.index as usize] = PLACEHOLDER; - for full in &mut self.full_and_moving_out { - if let Some(pos) = full.iter().position(|ins| id.id == ins.index) { - let removed = full.remove(pos); + if let ImplicitState::WaitingForSourceItems(_) = hand { + if let Some(pos) = self + .waiting_for_item + .iter() + .position(|ins| id.id == ins.index) + { + let removed = self.waiting_for_item.remove(pos); return ret( removed.storage_id_in, removed.storage_id_out, removed.max_hand_size, ); + } else { + return Err(WaitlistSearchSide::Source); } } - for empty in &mut self.empty_and_moving_back { - if let Some(pos) = empty.iter().position(|ins| id.id == ins.index) { - let removed = empty.remove(pos); + if let ImplicitState::WaitingForSpaceInDestination(_) = hand { + if let Some(pos) = self + .waiting_for_space_in_destination + .iter() + .position(|ins| id.id == ins.index) + { + let removed = self.waiting_for_space_in_destination.remove(pos); return ret( removed.storage_id_in, removed.storage_id_out, removed.max_hand_size, ); + } else { + return Err(WaitlistSearchSide::Dest); } } + if let ImplicitState::FullAndMovingOut = hand { + for full in &mut self.full_and_moving_out { + if let Some(pos) = full.iter().position(|ins| id.id == ins.index) { + let removed = full.remove(pos); + + return ret( + removed.storage_id_in, + removed.storage_id_out, + removed.max_hand_size, + ); + } + } + unreachable!() + } + + if let ImplicitState::EmptyAndMovingBack = hand { + for empty in &mut self.empty_and_moving_back { + if let Some(pos) = empty.iter().position(|ins| id.id == ins.index) { + let removed = empty.remove(pos); + + return ret( + removed.storage_id_in, + removed.storage_id_out, + removed.max_hand_size, + ); + } + } + unreachable!() + } + + // TODO: Use match statemen unreachable!() } @@ -279,11 +309,12 @@ impl BucketedStorageStorageInserterStore { storages: SingleItemStorages, grid_size: usize, current_tick: u32, - _movetime: u16, - ) -> bool { + movetime: u16, + ) -> UpdateResult { let storage_id = bucket_data.storage_id_in; - let (_max_insert, old) = index_fake_union(item_id, storages, storage_id, grid_size); + let (_max_insert, old, wait_list) = + index_fake_union(item_id, storages, storage_id, grid_size); let old_val = *old; let max_hand_size = bucket_data.max_hand_size; @@ -298,15 +329,42 @@ impl BucketedStorageStorageInserterStore { inserter.state = ImplicitState::WaitingForSourceItems(bucket_data.current_hand); } - let extract = bucket_data.current_hand == max_hand_size; - - if extract { + if bucket_data.current_hand == max_hand_size { inserter.state = ImplicitState::FullAndMovingOut; // Only use the lower 2 bytes inserter.last_update_time = current_tick as u16; - } + UpdateResult { + extract: true, + reinsert: true, + } + } else { + if let Some(wait_list) = wait_list { + if let Some(pos) = wait_list.inserters.iter_mut().find(|slot| slot.is_none()) { + *pos = Some(WaitListInserter { + current_hand: bucket_data.current_hand, + max_hand: bucket_data.max_hand_size.try_into().unwrap(), + movetime: movetime, + index: bucket_data.index, + other: bucket_data.storage_id_out, + }); - extract + UpdateResult { + extract: true, + reinsert: false, + } + } else { + UpdateResult { + extract: false, + reinsert: false, + } + } + } else { + UpdateResult { + extract: false, + reinsert: false, + } + } + } } fn handle_waiting_for_space_ins( @@ -317,11 +375,12 @@ impl BucketedStorageStorageInserterStore { storages: SingleItemStorages, grid_size: usize, current_tick: u32, - _movetime: u16, - ) -> bool { + movetime: u16, + ) -> UpdateResult { let storage_id = bucket_data.storage_id_out; - let (max_insert, old) = index_fake_union(item_id, storages, storage_id, grid_size); + let (max_insert, old, wait_list) = + index_fake_union(item_id, storages, storage_id, grid_size); let old_val = *old; let max_insert = *max_insert; @@ -335,15 +394,42 @@ impl BucketedStorageStorageInserterStore { inserter.state = ImplicitState::WaitingForSpaceInDestination(bucket_data.current_hand); } - let extract = bucket_data.current_hand == 0; - - if extract { + if bucket_data.current_hand == 0 { inserter.state = ImplicitState::EmptyAndMovingBack; // Only use the lower 2 bytes inserter.last_update_time = current_tick as u16; - } + UpdateResult { + extract: true, + reinsert: true, + } + } else { + if let Some(wait_list) = wait_list { + if let Some(pos) = wait_list.inserters.iter_mut().find(|slot| slot.is_none()) { + *pos = Some(WaitListInserter { + current_hand: bucket_data.current_hand, + max_hand: bucket_data.max_hand_size.try_into().unwrap(), + movetime: movetime, + index: bucket_data.index, + other: bucket_data.storage_id_in, + }); - extract + UpdateResult { + extract: true, + reinsert: false, + } + } else { + UpdateResult { + extract: false, + reinsert: false, + } + } + } else { + UpdateResult { + extract: false, + reinsert: false, + } + } + } } pub fn get_load_info(&self) -> (usize, usize, usize, usize) { @@ -467,10 +553,11 @@ impl BucketedStorageStorageInserterStore { current_tick, self.movetime, ) + .extract }); self.full_and_moving_out[(self.current_tick + usize::from(self.movetime)) % len] - .extend(now_moving); + .extend(now_moving.filter(|ins| ins.current_hand == ins.max_hand_size)); } // { @@ -541,10 +628,11 @@ impl BucketedStorageStorageInserterStore { current_tick, self.movetime, ) + .extract }); self.empty_and_moving_back[(self.current_tick + usize::from(self.movetime)) % len] - .extend(now_moving_back); + .extend(now_moving_back.filter(|ins| ins.current_hand == 0)); } { @@ -610,14 +698,15 @@ impl BucketedStorageStorageInserterStore { self.current_tick = (self.current_tick + 1) % self.list_len(); - #[cfg(debug_assertions)] - { - assert_eq!( - old_len, - self.get_list_sizes().iter().sum::(), - "Updating inserters lost an inserter from the update lists" - ); - } + // TODO: This does not hold with waitlists. As such this should only be active if waitlists are disabled + // #[cfg(debug_assertions)] + // { + // assert_eq!( + // old_len, + // self.get_list_sizes().iter().sum::(), + // "Updating inserters lost an inserter from the update lists" + // ); + // } } fn get_list_sizes(&self) -> Vec { @@ -921,6 +1010,18 @@ impl BucketedStorageStorageInserterStore { unreachable!() } + + pub fn reinsert_empty(&mut self, inserter: InserterBucketData) { + self.empty_and_moving_back + [(self.current_tick + usize::from(self.movetime)) % self.empty_and_moving_back.len()] + .push(inserter); + } + + pub fn reinsert_full(&mut self, inserter: InserterBucketData) { + self.full_and_moving_out + [(self.current_tick + usize::from(self.movetime)) % self.full_and_moving_out.len()] + .push(inserter); + } } #[cfg(test)] diff --git a/src/item.rs b/src/item.rs index de37a11..0de4a60 100644 --- a/src/item.rs +++ b/src/item.rs @@ -50,7 +50,7 @@ pub trait WeakIdxTrait: { } -pub fn usize_from(t: T) -> usize { +pub fn usize_from(t: T) -> usize { Into::::into(t) } diff --git a/src/lib.rs b/src/lib.rs index 8f13519..7231be6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ #![feature(mixed_integer_ops_unsigned_sub)] #![feature(int_roundings)] #![feature(strict_overflow_ops)] +#![feature(vec_push_within_capacity)] extern crate test; diff --git a/src/liquid/mod.rs b/src/liquid/mod.rs index 3a4cc58..beb8ab5 100644 --- a/src/liquid/mod.rs +++ b/src/liquid/mod.rs @@ -1571,7 +1571,7 @@ pub fn update_fluid_system( if hot_data.current_fluid_level == 0 { break; } - let (max, data) = index_fake_union(item_id, 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( @@ -1597,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(item_id, 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/power/mod.rs b/src/power/mod.rs index 9c1d31c..64645a7 100644 --- a/src/power/mod.rs +++ b/src/power/mod.rs @@ -1,4 +1,8 @@ -use crate::frontend::world::tile::ModuleSlots; +use crate::inserter::storage_storage_with_buckets_indirect::InserterBucketData; +use crate::item::Indexable; +use crate::{ + app_state::StorageStorageInserterStore, frontend::world::tile::ModuleSlots, join_many::join, +}; use itertools::Itertools; use log::{error, warn}; use power_grid::{ @@ -213,7 +217,10 @@ impl PowerGridStorage PowerGridStorage PowerGridStorage PowerGridStorage PowerGridStorage, ) -> (ResearchProgress, RecipeTickInfo, Option) { { @@ -1067,23 +1079,63 @@ impl PowerGridStorage>(); let (research_progress, production_info, times_labs_used_science, beacon_updates) = { - self - .power_grids - .par_iter_mut() - .map(|grid| grid.update(&solar_production, tech_state, current_tick, data_store)) - .reduce( - || (0, RecipeTickInfo::new(data_store), 0, vec![]), - |(acc_progress, infos, times_labs_used_science, mut old_updates), - (rhs_progress, info, new_times_labs_used_science, new_updates)| { - old_updates.extend(new_updates); - ( - acc_progress + rhs_progress, - infos + &info, - times_labs_used_science + new_times_labs_used_science, - old_updates, - ) - }, - ) + let lists = { + profiling::scope!("Update Grids"); + + self.power_grids + .par_iter_mut() + .map(|grid| { + grid.update(&solar_production, tech_state, current_tick, data_store) + }) + .collect_vec_list() + }; + + { + profiling::scope!("Fold lists"); + lists.into_iter().flatten().fold( + (0, RecipeTickInfo::new(data_store), 0, vec![]), + |(acc_progress, infos, times_labs_used_science, mut old_updates), + ( + rhs_progress, + info, + new_times_labs_used_science, + new_updates, + reinsertions, + )| { + join!( + || { + old_updates.extend(new_updates); + }, + || { + for inserter in reinsertions { + let store = inserter_store.inserters + [inserter.item.into_usize()] + .get_mut(&inserter.movetime) + .unwrap(); + let ins = InserterBucketData { + storage_id_in: inserter.storage_id_in, + storage_id_out: inserter.storage_id_out, + index: inserter.index, + current_hand: inserter.current_hand, + max_hand_size: inserter.max_hand, + }; + if inserter.current_hand == 0 { + store.0.reinsert_empty(ins); + } else { + store.0.reinsert_empty(ins); + } + } + } + ); + ( + acc_progress + rhs_progress, + infos + &info, + times_labs_used_science + new_times_labs_used_science, + old_updates, + ) + }, + ) + } }; { diff --git a/src/power/power_grid.rs b/src/power/power_grid.rs index 621e854..b0e8edd 100644 --- a/src/power/power_grid.rs +++ b/src/power/power_grid.rs @@ -1,3 +1,5 @@ +use crate::assembler::simd::Inserter; +use crate::assembler::simd::InserterReinsertionInfo; use crate::frontend::world::tile::ModuleSlots; use crate::frontend::world::tile::ModuleTy; use crate::{ @@ -183,11 +185,14 @@ pub struct PowerPoleUpdateInfo { impl PowerGrid { #[must_use] - pub fn new_trusted(data_store: &DataStore) -> Self { + pub fn new_trusted( + future_grid_id: PowerGridIdentifier, + data_store: &DataStore, + ) -> Self { let network = Network::trusted_new_empty(); Self { - stores: FullAssemblerStore::new(data_store), + stores: FullAssemblerStore::new(future_grid_id, data_store), lab_stores: MultiLabStore::new(&data_store.science_bottle_items), grid_graph: network, steam_power_producers: SteamPowerProducerStore { @@ -228,13 +233,14 @@ impl PowerGrid, first_pole_pos: Position, ) -> Self { let network = Network::new((), first_pole_pos); Self { - stores: FullAssemblerStore::new(data_store), + stores: FullAssemblerStore::new(future_grid_id, data_store), lab_stores: MultiLabStore::new(&data_store.science_bottle_items), grid_graph: network, steam_power_producers: SteamPowerProducerStore { @@ -274,11 +280,14 @@ impl PowerGrid) -> Self { + pub fn new_placeholder( + future_grid_id: PowerGridIdentifier, + data_store: &DataStore, + ) -> Self { let network = Network::new((), Position { x: 0, y: 0 }); Self { - stores: FullAssemblerStore::new(data_store), + stores: FullAssemblerStore::new(future_grid_id, data_store), lab_stores: MultiLabStore::new(&data_store.science_bottle_items), grid_graph: network, steam_power_producers: SteamPowerProducerStore { @@ -318,16 +327,25 @@ impl PowerGrid)>, data_store: &DataStore, ) -> Self { Self { grid_graph: graph, // TODO: If adding a power pole has addition side effect besides changing the graph, this is not correct - ..Self::new(data_store, Position { x: 0, y: 0 }) + ..Self::new(future_grid_id, data_store, Position { x: 0, y: 0 }) } } + pub fn set_grid_id( + &mut self, + grid_id: PowerGridIdentifier, + data_store: &DataStore, + ) { + self.stores.set_grid(grid_id, data_store); + } + #[must_use] #[profiling::function] pub fn join( @@ -721,7 +739,7 @@ impl PowerGrid = - Self::new_from_graph(network, data_store); + Self::new_from_graph(PowerGridIdentifier::MAX, network, data_store); let storage_updates: Vec<_> = self .move_connected_entities(&mut new_network, data_store) @@ -2288,35 +2306,52 @@ impl PowerGrid, - ) -> (Watt, Vec) { - iter.map(|(power_used, times_ings_used, crafts_finished)| { - ( - power_used, - SingleRecipeTickInfo { - full_crafts: times_ings_used as u64, - prod_crafts: crafts_finished.checked_sub(times_ings_used).expect( - "More ingredients used than crafts finished?!? Negative productivity?", - ) as u64, + iter: impl ParallelIterator< + Item = ( + Watt, + u32, + u32, + impl Iterator>, + ), + >, + ) -> ( + Watt, + Vec, + Vec>, + ) { + iter.collect_vec_list() + .into_iter() + .flatten() + .map( + |(power_used, times_ings_used, crafts_finished, reinsertion)| { + ( + power_used, + SingleRecipeTickInfo { + full_crafts: times_ings_used as u64, + prod_crafts: crafts_finished.checked_sub(times_ings_used).expect( + "More ingredients used than crafts finished?!? Negative productivity?", + ) as u64, + }, + reinsertion, + ) + }, + ) + // .fold_with( + // (Watt(0), vec![]), + // |(acc_power, mut infos), (rhs_power, info)| { + // infos.push(info); + // (acc_power + rhs_power, infos) + // }, + // ) + .fold( + (Watt(0), vec![], vec![]), + |(acc_power, mut infos, mut reinsertions), (rhs_power, info, reinsertion)| { + infos.push(info); + reinsertions.extend(reinsertion); + + (acc_power + rhs_power, infos, reinsertions) }, ) - }) - .fold_with( - (Watt(0), vec![]), - |(acc_power, mut infos), (rhs_power, info)| { - infos.push(info); - - (acc_power + rhs_power, infos) - }, - ) - .reduce( - || (Watt(0), vec![]), - |(acc_power, mut infos), (rhs_power, info)| { - infos.extend_from_slice(&info); - - (acc_power + rhs_power, infos) - }, - ) } #[profiling::function] @@ -2331,23 +2366,33 @@ impl PowerGrid, (i16, i16, i16))>, + itertools::Either< + impl Iterator>, + std::iter::Empty>, + >, ) { if self.is_placeholder { - return (0, RecipeTickInfo::new(data_store), 0, vec![]); + return ( + 0, + RecipeTickInfo::new(data_store), + 0, + vec![], + itertools::Either::Right(std::iter::empty()), + ); } let active_recipes = tech_state.get_active_recipes(); let ( - (power_used_0_1, infos_0_1), - (power_used_1_1, infos_1_1), - (power_used_2_1, infos_2_1), - (power_used_2_2, infos_2_2), - (power_used_2_3, infos_2_3), - (power_used_3_1, infos_3_1), - (power_used_4_1, infos_4_1), - (power_used_5_1, infos_5_1), - (power_used_6_1, infos_6_1), + (power_used_0_1, infos_0_1, reinsertions_0_1), + (power_used_1_1, infos_1_1, reinsertions_1_1), + (power_used_2_1, infos_2_1, reinsertions_2_1), + (power_used_2_2, infos_2_2, reinsertions_2_2), + (power_used_2_3, infos_2_3, reinsertions_2_3), + (power_used_3_1, infos_3_1, reinsertions_3_1), + (power_used_4_1, infos_4_1, reinsertions_4_1), + (power_used_5_1, infos_5_1, reinsertions_5_1), + (power_used_6_1, infos_6_1, reinsertions_6_1), (lab_power_used, times_labs_used_science, tech_progress), ) = join!( || { @@ -2361,21 +2406,21 @@ impl PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid PowerGrid MaxInsertionLimit<'a> { } } -type SingleGridStorage<'a, 'b> = (MaxInsertionLimit<'a>, &'b mut [ITEMCOUNTTYPE]); +#[derive(Debug)] +pub enum InserterWaitLists<'a> { + PerMachine(&'a mut [InserterWaitList]), + None, +} + +impl<'a> Index for InserterWaitLists<'a> { + type Output = InserterWaitList; + fn index(&self, index: usize) -> &Self::Output { + match self { + InserterWaitLists::PerMachine(items) => &items[index], + InserterWaitLists::None => panic!("No list"), + } + } +} +impl<'a> IndexMut for InserterWaitLists<'a> { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + self.get_mut(index).unwrap() + } +} +impl<'a> InserterWaitLists<'a> { + fn get_mut(&mut self, index: usize) -> Option<&mut InserterWaitList> { + match self { + InserterWaitLists::PerMachine(items) => items.get_mut(index), + InserterWaitLists::None => None, + } + } +} + +type SingleGridStorage<'a, 'b> = ( + MaxInsertionLimit<'a>, + &'b mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, +); pub type SingleItemStorages<'a, 'b> = &'a mut [SingleGridStorage<'b, 'b>]; //[SingleGridStorage; NUM_RECIPES * NUM_GRIDS]; pub type FullStorages<'a, 'b> = Box<[SingleGridStorage<'a, 'b>]>; //[SingleGridStorage; NUM_ITEMS * NUM_RECIPES * NUM_GRIDS]; -fn num_labs( +fn num_labs( item: Item, data_store: &DataStore, ) -> usize { usize::from(data_store.item_is_science[usize_from(item.id)]) } -pub fn num_recipes( +pub fn num_recipes( item: Item, data_store: &DataStore, ) -> usize { @@ -75,7 +109,7 @@ pub fn num_recipes( num_recipes } -pub fn static_size( +pub fn static_size( _item: Item, _data_store: &DataStore, ) -> usize { @@ -90,7 +124,7 @@ pub fn static_size( size } -pub fn grid_size( +pub fn grid_size( item: Item, data_store: &DataStore, ) -> usize { @@ -118,7 +152,11 @@ pub fn index<'a, 'b, RecipeIdxType: IdxTrait>( num_recipes: usize, grid_size: usize, static_size: usize, -) -> (&'a ITEMCOUNTTYPE, &'a mut ITEMCOUNTTYPE) { +) -> ( + &'a ITEMCOUNTTYPE, + &'a mut ITEMCOUNTTYPE, + Option<&'a mut InserterWaitList>, +) { let first_grid_offs_in_grids = static_size.div_ceil(grid_size); match storage_id { @@ -137,6 +175,7 @@ pub fn index<'a, 'b, RecipeIdxType: IdxTrait>( ( &outer.0[usize::try_from(index).unwrap()], &mut outer.1[usize::try_from(index).unwrap()], + outer.2.get_mut(usize::try_from(index).unwrap()), ) }, Storage::Lab { grid, index } => { @@ -145,6 +184,7 @@ pub fn index<'a, 'b, RecipeIdxType: IdxTrait>( ( &outer.0[usize::try_from(index).unwrap()], &mut outer.1[usize::try_from(index).unwrap()], + outer.2.get_mut(usize::try_from(index).unwrap()), ) }, Storage::Static { static_id, index } => { @@ -153,6 +193,7 @@ pub fn index<'a, 'b, RecipeIdxType: IdxTrait>( ( &outer.0[usize::try_from(index).unwrap()], &mut outer.1[usize::try_from(index).unwrap()], + outer.2.get_mut(usize::try_from(index).unwrap()), ) }, } @@ -164,7 +205,11 @@ pub fn index_fake_union<'a, 'b>( slice: SingleItemStorages<'a, 'b>, storage_id: FakeUnionStorage, grid_size: usize, -) -> (&'a ITEMCOUNTTYPE, &'a mut ITEMCOUNTTYPE) { +) -> ( + &'a ITEMCOUNTTYPE, + &'a mut ITEMCOUNTTYPE, + Option<&'a mut InserterWaitList>, +) { let (outer, inner) = storage_id.into_inner_and_outer_indices_with_statics_at_zero(grid_size); let len = slice.len(); @@ -260,7 +305,8 @@ pub fn index_fake_union<'a, 'b>( ); } }; - (max_insert, items) + let wait_list = subslice.2.get_mut(inner); + (max_insert, items, wait_list) } #[profiling::function] @@ -470,13 +516,18 @@ pub fn storages_by_item<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( static_storages_pre_sorted( item, chest_store, - mining_drill_lists, + ( + mining_drill_lists.0, + mining_drill_lists.1, + InserterWaitLists::None, + ), data_store, ) - .chain( - grid.into_iter() - .map(|(_item, _storage, max_insert, data)| (max_insert, data)), - ) + .chain(grid.into_iter().map( + |(_item, _storage, max_insert, data, wait_list)| { + (max_insert, data, wait_list) + }, + )) }, ) .collect() @@ -499,6 +550,7 @@ fn all_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( Storage, MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, ), > + use<'a, 'b, ItemIdxType, RecipeIdxType> { let all_storages = grids @@ -519,10 +571,19 @@ fn all_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( pub fn static_storages_pre_sorted<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( item: Item, chest_store: &'a mut MultiChestStore, - drill_lists: (MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE]), + drill_lists: ( + MaxInsertionLimit<'a>, + &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, + ), data_store: &'b DataStore, -) -> impl Iterator, &'a mut [ITEMCOUNTTYPE])> -+ use<'a, 'b, ItemIdxType, RecipeIdxType> { +) -> impl Iterator< + Item = ( + MaxInsertionLimit<'a>, + &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, + ), +> + use<'a, 'b, ItemIdxType, RecipeIdxType> { let grid_size = grid_size(item, data_store); let static_size = static_size(item, data_store); @@ -530,9 +591,13 @@ pub fn static_storages_pre_sorted<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: let first_grid_offs = grid_size * first_grid_offs_in_grids; - iter::once(chest_store.storage_list_slices()) + let (chest_max, chest_data, wait_lists) = chest_store.storage_list_slices(); + + iter::once((chest_max, chest_data, wait_lists)) .chain(iter::once(drill_lists)) - .chain(iter::repeat_with(|| (ALWAYS_FULL, [].as_mut_slice()))) + .chain(iter::repeat_with(|| { + (ALWAYS_FULL, [].as_mut_slice(), InserterWaitLists::None) + })) .take(first_grid_offs) } @@ -546,6 +611,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait Storage, MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, ), > + use<'a, 'b, ItemIdxType, RecipeIdxType> { let i = assembler_store @@ -566,7 +632,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait .copied() .unwrap(); - let (([], []), [outputs]) = multi.get_all_mut(); + let (([], [], []), ([outputs], [output_wait])) = multi.get_all_mut(); ( item, @@ -580,6 +646,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + output_wait, ) }) .chain( @@ -614,7 +681,8 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait .copied() .unwrap(); - let (([ings_max_insert], [ings]), [outputs]) = multi.get_all_mut(); + let (([ings_max_insert], [ings], [ings_wait]), ([outputs], [outputs_wait])) = + multi.get_all_mut(); [ ( @@ -629,6 +697,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings_max_insert, ings, + ings_wait, ), ( item_out, @@ -642,6 +711,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + outputs_wait, ), ] }), @@ -678,7 +748,10 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait .copied() .unwrap(); - let (([ings0_max, ings1_max], [ings0, ings1]), [outputs]) = multi.get_all_mut(); + let ( + ([ings0_max, ings1_max], [ings0, ings1], [ings0_wait, ings1_wait]), + ([outputs], [outputs_wait]), + ) = multi.get_all_mut(); [ ( @@ -693,6 +766,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -706,6 +780,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_out, @@ -719,6 +794,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + outputs_wait, ), ] }), @@ -755,8 +831,10 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait let item_out0 = *items_out.next().unwrap(); let item_out1 = *items_out.next().unwrap(); - let (([ings0_max, ings1_max], [ings0, ings1]), [outputs0, outputs1]) = - multi.get_all_mut(); + let ( + ([ings0_max, ings1_max], [ings0, ings1], [ings0_wait, ings1_wait]), + ([outputs0, outputs1], [outputs0_wait, outputs1_wait]), + ) = multi.get_all_mut(); [ ( @@ -771,6 +849,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -784,6 +863,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_out0, @@ -797,6 +877,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs0, + outputs0_wait, ), ( item_out1, @@ -810,6 +891,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs1, + outputs1_wait, ), ] }), @@ -847,8 +929,13 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait let item_out1 = *items_out.next().unwrap(); let item_out2 = *items_out.next().unwrap(); - let (([ings0_max, ings1_max], [ings0, ings1]), [outputs0, outputs1, outputs2]) = - multi.get_all_mut(); + let ( + ([ings0_max, ings1_max], [ings0, ings1], [ings0_wait, ings1_wait]), + ( + [outputs0, outputs1, outputs2], + [outputs0_wait, outputs1_wait, outputs2_wait], + ), + ) = multi.get_all_mut(); [ ( @@ -863,6 +950,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -876,6 +964,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_out0, @@ -889,6 +978,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs0, + outputs0_wait, ), ( item_out1, @@ -902,6 +992,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs1, + outputs1_wait, ), ( item_out2, @@ -915,6 +1006,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs2, + outputs2_wait, ), ] }), @@ -952,8 +1044,14 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait .copied() .unwrap(); - let (([ings0_max, ings1_max, ings2_max], [ings0, ings1, ings2]), [outputs]) = - multi.get_all_mut(); + let ( + ( + [ings0_max, ings1_max, ings2_max], + [ings0, ings1, ings2], + [ings0_wait, ings1_wait, ings2_wait], + ), + ([outputs], [outputs_wait]), + ) = multi.get_all_mut(); [ ( @@ -968,6 +1066,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -981,6 +1080,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_in2, @@ -994,6 +1094,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings2_max, ings2, + ings2_wait, ), ( item_out, @@ -1007,6 +1108,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + outputs_wait, ), ] }), @@ -1049,8 +1151,9 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait ( [ings0_max, ings1_max, ings2_max, ings3_max], [ings0, ings1, ings2, ings3], + [ings0_wait, ings1_wait, ings2_wait, ings3_wait], ), - [outputs], + ([outputs], [outputs_wait]), ) = multi.get_all_mut(); [ @@ -1066,6 +1169,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -1079,6 +1183,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_in2, @@ -1092,6 +1197,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings2_max, ings2, + ings2_wait, ), ( item_in3, @@ -1105,6 +1211,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings3_max, ings3, + ings3_wait, ), ( item_out, @@ -1118,6 +1225,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + outputs_wait, ), ] }), @@ -1161,8 +1269,9 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait ( [ings0_max, ings1_max, ings2_max, ings3_max, ings4_max], [ings0, ings1, ings2, ings3, ings4], + [ings0_wait, ings1_wait, ings2_wait, ings3_wait, ings4_wait], ), - [outputs], + ([outputs], [outputs_wait]), ) = multi.get_all_mut(); [ @@ -1178,6 +1287,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -1191,6 +1301,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_in2, @@ -1204,6 +1315,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings2_max, ings2, + ings2_wait, ), ( item_in3, @@ -1217,6 +1329,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings3_max, ings3, + ings3_wait, ), ( item_in4, @@ -1230,6 +1343,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings4_max, ings4, + ings4_wait, ), ( item_out, @@ -1243,6 +1357,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + outputs_wait, ), ] }), @@ -1294,8 +1409,16 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait ings5_max, ], [ings0, ings1, ings2, ings3, ings4, ings5], + [ + ings0_wait, + ings1_wait, + ings2_wait, + ings3_wait, + ings4_wait, + ings5_wait, + ], ), - [outputs], + ([outputs], [outputs_wait]), ) = multi.get_all_mut(); [ @@ -1311,6 +1434,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings0_max, ings0, + ings0_wait, ), ( item_in1, @@ -1324,6 +1448,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings1_max, ings1, + ings1_wait, ), ( item_in2, @@ -1337,6 +1462,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings2_max, ings2, + ings2_wait, ), ( item_in3, @@ -1350,6 +1476,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings3_max, ings3, + ings3_wait, ), ( item_in4, @@ -1363,6 +1490,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings4_max, ings4, + ings4_wait, ), ( item_in5, @@ -1376,6 +1504,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ings5_max, ings5, + ings5_wait, ), ( item_out, @@ -1389,10 +1518,12 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait }, ALWAYS_FULL, outputs, + outputs_wait, ), ] }), - ); + ) + .map(|(a, b, c, d, e)| (a, b, c, d, InserterWaitLists::PerMachine(e))); i } @@ -1406,6 +1537,7 @@ fn all_lab_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( Storage, MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, ), > + use<'a, 'b, ItemIdxType, RecipeIdxType> { lab_store @@ -1423,6 +1555,7 @@ fn all_lab_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( None => PANIC_ON_INSERT, }, science.as_mut_slice(), + InserterWaitLists::None, ) }) } @@ -1438,6 +1571,7 @@ fn all_static_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( Storage, MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, ), > + use<'a, 'b, ItemIdxType, RecipeIdxType> { (0..data_store.item_display_names.len()) @@ -1458,7 +1592,7 @@ fn all_static_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( assert!(first_grid_offs >= static_size); assert!(first_grid_offs % grid_size == 0); - let (max_insert, data) = chest.storage_list_slices(); + let (max_insert, data, wait_lists) = chest.storage_list_slices(); std::iter::repeat(item) .zip( @@ -1469,6 +1603,7 @@ fn all_static_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( }, max_insert, data, + wait_lists, )) .chain(iter::once(( Storage::Static { @@ -1477,6 +1612,7 @@ fn all_static_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( }, drill_lists.0, drill_lists.1, + InserterWaitLists::None, ))) .chain( std::iter::repeat_with(|| (PANIC_ON_INSERT, [].as_mut_slice())) @@ -1489,12 +1625,13 @@ fn all_static_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( }, max, data, + InserterWaitLists::None, ) }), ) .take(first_grid_offs), ) - .map(|(item, (a, b, c))| (item, a, b, c)) + .map(|(a, (b, c, d, e))| (a, b, c, d, e)) }) } @@ -1507,6 +1644,7 @@ fn all_chest_storages<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( Storage, MaxInsertionLimit<'a>, &'a mut [ITEMCOUNTTYPE], + InserterWaitLists<'a>, ), > + use<'a, ItemIdxType, RecipeIdxType> { chest_store @@ -1518,7 +1656,7 @@ fn all_chest_storages<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( id: id.try_into().unwrap(), }; - let (max_insert, data) = multi.storage_list_slices(); + let (max_insert, data, wait_lists) = multi.storage_list_slices(); ( item, @@ -1528,6 +1666,7 @@ fn all_chest_storages<'a, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait>( }, max_insert, data, + wait_lists, ) }) } diff --git a/test_blueprints/lots_of_belts.bp b/test_blueprints/lots_of_belts.bp index 3fded4c..51d7dcc 100644 --- a/test_blueprints/lots_of_belts.bp +++ b/test_blueprints/lots_of_belts.bp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01d20091aefaee493c769ff343c7bdca55cb697e4160c192c9d1ef8e484b4795 -size 240 +oid sha256:194a098488a8aacc304ff8654922ca6cf814926b29d04705ec9424ad34f593fa +size 236 From 0970d46808e7514e67e267fc52855d7878b5a0b8 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 23 Nov 2025 23:57:23 +0100 Subject: [PATCH 050/152] Add chest waitlists --- src/app_state.rs | 1 + src/assembler/simd.rs | 5 +- src/chest.rs | 136 ++++++++++-------- .../storage_storage_with_buckets_indirect.rs | 2 + 4 files changed, 81 insertions(+), 63 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index b544154..a98d6f3 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1005,6 +1005,7 @@ impl Factory= 120 { belt.update(sushi_splitters); } diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index fde1ab4..a0210fb 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -39,7 +39,7 @@ use get_size2::GetSize; #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[repr(align(64))] pub struct InserterWaitList { - pub inserters: [Option; 4], + pub inserters: [Option; 3], } const_assert!(std::mem::size_of::() <= 64); @@ -48,7 +48,8 @@ const_assert!(std::mem::size_of::() <= 64); #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Inserter { // item: u8, - // self_is_source: bool, + // TODO: This is not needed for assemblers, just for chests. + pub self_is_source: bool, // Ideally we would track the hand here so we avoid having to reinsert them each time the assembler produces anything // This does mean we can only fit 3 Inserters per cacheline :/ // This is fixed by the item arena optimization diff --git a/src/chest.rs b/src/chest.rs index fc96b84..2aa4b3b 100644 --- a/src/chest.rs +++ b/src/chest.rs @@ -2,13 +2,13 @@ use std::cmp::max; use std::num::NonZero; use std::{cmp::min, u8}; -use rayon::iter::IndexedParallelIterator; +use rayon::iter::{IndexedParallelIterator, IntoParallelIterator}; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; use static_assertions::const_assert; use crate::assembler::simd::{Inserter, InserterReinsertionInfo, InserterWaitList}; -use crate::inserter::FakeUnionStorage; use crate::inserter::storage_storage_with_buckets_indirect::InserterId; +use crate::inserter::{FakeUnionStorage, StaticID}; use crate::storage_list::{InserterWaitLists, MaxInsertionLimit}; use crate::{ data::DataStore, @@ -55,6 +55,7 @@ pub type SignedChestSize = i32; struct InternalInserterReinsertionInfo { inserter: Inserter, self_is_source: bool, + self_index: u32, } #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] @@ -85,6 +86,7 @@ impl FullChestStore { move |InternalInserterReinsertionInfo { inserter, self_is_source, + self_index, }| InserterReinsertionInfo { movetime: inserter.movetime, item, @@ -92,18 +94,28 @@ impl FullChestStore { max_hand: inserter.max_hand.into(), index: inserter.index, storage_id_in: if self_is_source { - todo!() + FakeUnionStorage { + index: self_index, + grid_or_static_flag: 0, + recipe_idx_with_this_item: StaticID::Chest as u16, + } } else { inserter.other }, storage_id_out: if self_is_source { inserter.other } else { - todo!() + FakeUnionStorage { + index: self_index, + grid_or_static_flag: 0, + recipe_idx_with_this_item: StaticID::Chest as u16, + } }, }, ) }) + .collect::>() + .into_par_iter() } } @@ -112,6 +124,7 @@ impl FullChestStore { pub struct MultiChestStore { item: Item, max_insert: Vec, + pub last_inout: Vec, pub inout: Vec, storage: Vec, // TODO: Any way to not have to store this a billion times? @@ -132,6 +145,7 @@ impl MultiChestStore { Self { item, inout: vec![], + last_inout: vec![], storage: vec![], max_insert: vec![], max_items: vec![], @@ -163,6 +177,7 @@ impl MultiChestStore { if let Some(hole) = self.holes.pop() { self.inout[hole] = 0; + self.last_inout[hole] = 0; self.storage[hole] = 0; self.max_insert[hole] = max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX); @@ -172,6 +187,7 @@ impl MultiChestStore { hole.try_into().unwrap() } else { self.inout.push(0); + self.last_inout.push(0); self.storage.push(0); self.max_insert .push(max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX)); @@ -190,6 +206,7 @@ impl MultiChestStore { if let Some(hole) = self.holes.pop() { self.inout[hole] = 0; + self.last_inout[hole] = 0; self.storage[hole] = 0; self.max_insert[hole] = max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX); @@ -199,6 +216,7 @@ impl MultiChestStore { hole.try_into().unwrap() } else { self.inout.push(0); + self.last_inout.push(0); self.storage.push(0); self.max_insert .push(max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX)); @@ -220,6 +238,7 @@ impl MultiChestStore { } let items = ChestSize::from(self.inout[index]) + self.storage[index]; self.inout[index] = 0; + self.last_inout[index] = 0; self.storage[index] = 0; self.max_items[index] = 0; self.wait_list[index] = InserterWaitList::default(); @@ -272,71 +291,65 @@ impl MultiChestStore { // TODO: Splitting this into large chests and small chests would be better since now a single large chest will make all small chests in the world expensive // With wait lists we cannot avoid updating all chests (even small chests) // if self.num_large_chests > 0 { - for ((inout, (storage, max_items)), wait_list) in self + for (index, ((((inout, last_inout), (storage, max_items)), wait_list), max_insert)) in self .inout .iter_mut() + .zip(self.last_inout.iter_mut()) .zip(self.storage.iter_mut().zip(self.max_items.iter().copied())) .zip(self.wait_list.iter_mut()) + .zip(self.max_insert.iter()) + .enumerate() { - // let was_full = *storage == max_items; - // let was_empty = *storage == 0; + let is_full = *inout == *max_insert; + let is_empty = *inout == 0; + let was_full = *last_inout == *max_insert; + let was_empty = *last_inout == 0; + *last_inout = *inout; + let to_move = inout.abs_diff(CHEST_GOAL_AMOUNT); let switch = ChestSize::from(*inout >= CHEST_GOAL_AMOUNT); - // if was_empty || was_full { - // for ins in wait_list.inserters.iter_mut() { - // if let Some(inserter) = ins { - // if was_empty && *inout > 0 { - // // There can only be outgoing inserters in the waitlist if we are empty - // // FIXME: This could be wrong IFF a chest inout is filled then emptied in a single tick! - // let taken_by_this_inserter = min( - // ITEMCOUNTTYPE::from(inserter.max_hand) - inserter.current_hand, - // *inout, - // ); - - // *inout -= taken_by_this_inserter; - // inserter.current_hand += taken_by_this_inserter; - - // if inserter.current_hand == ITEMCOUNTTYPE::from(inserter.max_hand) { - // let removed = ins.take().unwrap(); - // self.inserter_reinsertion_vec - // .push_within_capacity(InternalInserterReinsertionInfo { - // inserter: removed, - // self_is_source: true, - // }) - // .unwrap(); - // } - // } else if was_full - // && *inout < max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX) - // { - // // The chest was full - - // // There can only be incoming inserters in the waitlist if we are full - // // FIXME: This could be wrong IFF a chest inout is filled then emptied in a single tick! - // let taken_by_this_inserter = min( - // ITEMCOUNTTYPE::from(inserter.max_hand) - inserter.current_hand, - // max_items.try_into().unwrap_or(ITEMCOUNTTYPE::MAX) - *inout, - // ); - - // *inout += taken_by_this_inserter; - // inserter.current_hand += taken_by_this_inserter; - - // if inserter.current_hand == ITEMCOUNTTYPE::from(inserter.max_hand) { - // let removed = ins.take().unwrap(); - // self.inserter_reinsertion_vec - // .push_within_capacity(InternalInserterReinsertionInfo { - // inserter: removed, - // self_is_source: false, - // }) - // .unwrap(); - // } - // } - // } - // } - // } - - let mut moved: SignedChestSize = (switch as SignedChestSize + if (was_full && !is_full) || (was_empty && !is_empty) { + for ins in wait_list.inserters.iter_mut() { + if let Some(inserter) = ins { + if inserter.self_is_source && !is_empty { + let taken_by_this_inserter = min( + ITEMCOUNTTYPE::from(inserter.max_hand) - inserter.current_hand, + *inout, + ); + + *inout -= taken_by_this_inserter; + inserter.current_hand += taken_by_this_inserter; + } else if !inserter.self_is_source && !is_full { + let taken_by_this_inserter = min( + inserter.current_hand, + *max_insert - *inout, + ); + + *inout += taken_by_this_inserter; + inserter.current_hand -= taken_by_this_inserter; + } + + if (inserter.current_hand == ITEMCOUNTTYPE::from(inserter.max_hand) + && inserter.self_is_source) + || (inserter.current_hand == 0 && !inserter.self_is_source) + { + let removed = ins.take().unwrap(); + let is_source = removed.self_is_source; + self.inserter_reinsertion_vec + .push_within_capacity(InternalInserterReinsertionInfo { + inserter: removed, + self_is_source: is_source, + self_index: index as u32, + }) + .unwrap(); + } + } + } + } + + let moved: SignedChestSize = (switch as SignedChestSize + (1 - switch as SignedChestSize) * -1) * (min( ChestSize::from(to_move), @@ -472,7 +485,8 @@ impl MultiChestStore { ( MaxInsertionLimit::PerMachine(self.max_insert.as_slice()), self.inout.as_mut_slice(), - InserterWaitLists::None, // InserterWaitLists::PerMachine(self.wait_list.as_mut_slice()), + // InserterWaitLists::None, + InserterWaitLists::PerMachine(self.wait_list.as_mut_slice()), ) } } diff --git a/src/inserter/storage_storage_with_buckets_indirect.rs b/src/inserter/storage_storage_with_buckets_indirect.rs index 29eb534..a6dc6eb 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -341,6 +341,7 @@ impl BucketedStorageStorageInserterStore { if let Some(wait_list) = wait_list { if let Some(pos) = wait_list.inserters.iter_mut().find(|slot| slot.is_none()) { *pos = Some(WaitListInserter { + self_is_source: true, current_hand: bucket_data.current_hand, max_hand: bucket_data.max_hand_size.try_into().unwrap(), movetime: movetime, @@ -406,6 +407,7 @@ impl BucketedStorageStorageInserterStore { if let Some(wait_list) = wait_list { if let Some(pos) = wait_list.inserters.iter_mut().find(|slot| slot.is_none()) { *pos = Some(WaitListInserter { + self_is_source: false, current_hand: bucket_data.current_hand, max_hand: bucket_data.max_hand_size.try_into().unwrap(), movetime: movetime, From e76ebc9886c53b420445009903db5a25e5f57d35 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 26 Nov 2025 00:07:04 +0100 Subject: [PATCH 051/152] Fix warning --- src/inserter/storage_storage_with_buckets_indirect.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inserter/storage_storage_with_buckets_indirect.rs b/src/inserter/storage_storage_with_buckets_indirect.rs index a6dc6eb..7536238 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -64,7 +64,7 @@ pub struct InserterIdentifier { #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] -pub(crate) struct InserterId { +pub struct InserterId { index: u32, } From a075fae2a9cd7d77e58d33aa095fd9d14eb44fd6 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 26 Nov 2025 00:07:24 +0100 Subject: [PATCH 052/152] Add potential struct for waitlists with beltstorage inserters --- src/assembler/simd.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index a0210fb..a3c2145 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -60,6 +60,34 @@ pub struct Inserter { pub other: FakeUnionStorage, } +const_assert!(std::mem::size_of::>() <= 20); +const_assert!(std::mem::size_of::() <= 20); + +pub struct InserterWithBelts { + current_hand: ITEMCOUNTTYPE, + max_hand: NonZero, + + rest: InserterWithBeltsEnum, +} + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum InserterWithBeltsEnum { + StorageStorage { + // TODO: This is not needed for assemblers, just for chests. + self_is_source: bool, + movetime: NonZero, + index: InserterId, + + other: FakeUnionStorage, + }, + BeltStorage { + movetime: NonZero, + belt_id: u32, + belt_pos: u16, + }, +} + #[derive(Debug)] pub struct InserterReinsertionInfo { pub movetime: u16, From bcf24d34774f183eefc58a89a390fc2f510d6135 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 26 Nov 2025 00:07:33 +0100 Subject: [PATCH 053/152] Add inserter info debug option --- src/rendering/render_world.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 0b4a849..50024d0 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -2896,6 +2896,37 @@ pub fn render_ui< } } + CollapsingHeader::new("Inserter Counts").show(ui, |ui| { + let mut storage_storage = 0; + let mut belt_storage = 0; + let mut belt_belt = 0; + + + for inserter_info in game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()).filter_map(|e| match e { + Entity::Inserter { + info: crate::frontend::world::tile::InserterInfo::Attached { + info + }, + .. + } => { + Some(info) + }, + + _ => None, + }) { + match inserter_info { + crate::frontend::world::tile::AttachedInserter::StorageStorage { ..} => storage_storage += 1, + crate::frontend::world::tile::AttachedInserter::BeltStorage { ..} => belt_storage += 1, + crate::frontend::world::tile::AttachedInserter::BeltBelt { ..} => belt_belt += 1, + } + } + + ui.label(&format!("StorageStorage: {}", storage_storage)); + ui.label(&format!("BeltStorage: {}", belt_storage)); + ui.label(&format!("BeltBelt: {}", belt_belt)); + + }); + CollapsingHeader::new("Lab analysis").show(ui, |ui| { let mut items = vec![0u32; data_store.item_names.len()]; From 5291cbb03cf9be248a8637bd156c376f8ae02cb9 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 26 Nov 2025 11:34:59 +0100 Subject: [PATCH 054/152] Switch to index based inserters from offset based inserters --- src/belt/smart.rs | 465 +++++------- src/belt/sushi.rs | 714 +++++++----------- .../belt_storage_inserter_non_const_gen.rs | 7 +- 3 files changed, 478 insertions(+), 708 deletions(-) diff --git a/src/belt/smart.rs b/src/belt/smart.rs index 850106c..83a9a6e 100644 --- a/src/belt/smart.rs +++ b/src/belt/smart.rs @@ -84,6 +84,19 @@ pub struct EmptyBelt { pub len: u16, } +type TEST = (BeltStorageInserterDyn, u8, ITEMCOUNTTYPE); + +// TODO: Idea: +// Have Belt inserters only be in belts as waitlist. +// when their hand is full, move them into a "store" of some kind +// where they are lazily waited on for their movetime. Once that is up, their outputs they either directly interact with the storage, +// or are put into the respective waitlist on the storage (i.e. assembler) +// Since the belts only interact with the "stores" 1.. lists and not the zeroth list (which interacts with the assemblers) +// We should be able to +// 1. Do the belt update in parallel with the belt_storage_store +// 2. Stop the belt updates from needing the storage lists, making them parallelizable with assembler updates +// This should ~double the amount of parallel processing we can do, +// and should prevent us from having to wait on the (always going to be) slow belt update for starting on assemblers (significantly improving the parallelism there also) #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct InserterStoreDyn { @@ -320,18 +333,17 @@ impl SmartBelt { let mut pos = 0; for inserter in self.inserters.inserters.iter_mut() { - pos += inserter.0.offset; + pos = inserter.0.belt_pos; if pos == belt_pos { inserter.0.storage_id = new; return; } else if pos > belt_pos { - unreachable!( - "Tried to set_inserter_storage_id with position {belt_pos}, which does not contain an inserter. {:?}", - self.inserters - ); } - pos += 1; } + unreachable!( + "Tried to set_inserter_storage_id with position {belt_pos}, which does not contain an inserter. {:?}", + self.inserters + ); } #[must_use] @@ -339,7 +351,7 @@ impl SmartBelt { let mut pos = 0; for inserter in self.inserters.inserters.iter() { - pos += inserter.0.offset; + pos = inserter.0.belt_pos; if pos == belt_pos { let (dir, state) = inserter.0.state.into(); return Some(BeltInserterInfo { @@ -349,29 +361,24 @@ impl SmartBelt { movetime: inserter.1, hand_size: inserter.2, }); - } else if pos > belt_pos { - return None; } - pos += 1; } None } pub(super) fn change_inserter_movetime(&mut self, belt_pos: BeltLenType, new_movetime: u16) { - let mut pos = 0; - - for (inserter, movetime, _hand_size) in self.inserters.inserters.iter_mut() { - pos += inserter.offset; - if pos == belt_pos { - *movetime = new_movetime.try_into().unwrap_or(u8::MAX); - return; - } else if pos > belt_pos { - break; - } - pos += 1; - } - unreachable!("The belt did not have an inserter at position specified to change movetime") + let Some(inserter) = self + .inserters + .inserters + .iter_mut() + .find(|ins| ins.0.belt_pos == belt_pos) + else { + unreachable!( + "The belt did not have an inserter at position specified to change movetime" + ) + }; + inserter.1 = new_movetime.try_into().unwrap_or(u8::MAX); } pub fn remove_inserter(&mut self, pos: BeltLenType) -> Result { @@ -379,38 +386,24 @@ impl SmartBelt { return Err(()); } - let mut pos_after_last_inserter = 0; - let mut i = 0; - - for offset in self.inserters.inserters.iter().map(|i| i.0.offset) { - let next_inserter_pos = pos_after_last_inserter + offset; - - match next_inserter_pos.cmp(&pos) { - std::cmp::Ordering::Greater => panic!( - "The belt did not have an inserter at position specified to remove inserter from" - ), // This is the index to insert at - std::cmp::Ordering::Equal => break, - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } + match self + .inserters + .inserters + .iter() + .position(|ins| ins.0.belt_pos == pos) + { + Some(idx) => { + let mut removed = None; + take_mut::take(&mut self.inserters.inserters, |ins| { + let mut ins = ins.into_vec(); + removed = Some(ins.remove(idx)); + ins.into_boxed_slice() + }); - let mut old_inserter = None; - take_mut::take(&mut self.inserters.inserters, |ins| { - let mut ins = ins.into_vec(); - old_inserter = Some(ins.remove(i)); - ins.into_boxed_slice() - }); - let old_inserter = old_inserter.unwrap(); - // The offset after i (which has now shifted left to i) - if let Some(next_offs) = self.inserters.inserters.get_mut(i) { - next_offs.0.offset += old_inserter.0.offset + 1 + Ok(removed.unwrap().0.storage_id) + }, + None => Err(()), } - - Ok(old_inserter.0.storage_id) } // FIXME: This is horrendously slow. it breaks my tests since they are compiled without optimizations!!! @@ -437,48 +430,16 @@ impl SmartBelt { return Err(InserterAdditionError::ItemMismatch); } - let mut pos_after_last_inserter = 0; - let mut i = 0; - - for offset in self.inserters.inserters.iter().map(|i| i.0.offset) { - let next_inserter_pos = pos_after_last_inserter + offset; - - match next_inserter_pos.cmp(&index) { - std::cmp::Ordering::Greater => break, // This is the index to insert at - std::cmp::Ordering::Equal => return Err(InserterAdditionError::SpaceOccupied), - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } - - // Insert at i - let new_inserter_offset = index - pos_after_last_inserter; take_mut::take(&mut self.inserters.inserters, |ins| { let mut ins = ins.into_vec(); - ins.insert( - i, - ( - BeltStorageInserterDyn::new( - Dir::BeltToStorage, - new_inserter_offset, - storage_id, - ), - movetime.try_into().unwrap_or(u8::MAX), - hand_size, - ), - ); + ins.push(( + BeltStorageInserterDyn::new(Dir::BeltToStorage, index, storage_id), + movetime.try_into().unwrap_or(u8::MAX), + hand_size, + )); ins.into_boxed_slice() }); - let next = self.inserters.inserters.get_mut(i + 1); - - if let Some(next_ins) = next { - next_ins.0.offset -= new_inserter_offset + 1; - } - Ok(()) } @@ -500,54 +461,21 @@ impl SmartBelt { self.locs.len() ); - let mut pos_after_last_inserter = 0; - let mut i = 0; - - for offset in self.inserters.inserters.iter().map(|i| i.0.offset) { - let next_inserter_pos = pos_after_last_inserter + offset; - - match next_inserter_pos.cmp(&index) { - std::cmp::Ordering::Greater => break, // This is the index to insert at - std::cmp::Ordering::Equal => return Err(InserterAdditionError::SpaceOccupied), - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } - // We only only return an item mismatch if we know the space is free, so we do not transition to sushi, // And then fail anyway if filter != self.item { return Err(InserterAdditionError::ItemMismatch); } - - // Insert at i - let new_inserter_offset = index - pos_after_last_inserter; take_mut::take(&mut self.inserters.inserters, |ins| { let mut ins = ins.into_vec(); - ins.insert( - i, - ( - BeltStorageInserterDyn::new( - Dir::StorageToBelt, - new_inserter_offset, - storage_id, - ), - movetime.try_into().unwrap_or(u8::MAX), - hand_size, - ), - ); + ins.push(( + BeltStorageInserterDyn::new(Dir::StorageToBelt, index, storage_id), + movetime.try_into().unwrap_or(u8::MAX), + hand_size, + )); ins.into_boxed_slice() }); - let next = self.inserters.inserters.get_mut(i + 1); - - if let Some(next_ins) = next { - next_ins.0.offset -= new_inserter_offset + 1; - } - Ok(()) } @@ -586,7 +514,6 @@ impl SmartBelt { if self.get_len() == 0 { return; } - let mut i = 0; let old_first_free = match self.first_free_index { FreeIndex::FreeIndex(idx) => idx, @@ -625,18 +552,18 @@ impl SmartBelt { // i += 1; // } - let mut first_free_changed = false; + let mut new_first_free = old_first_free; + for (ins, movetime, hand_size) in self.inserters.inserters.iter_mut() { - i += ins.offset; // Taken from VecDeque::wrap_index - let logical_index = usize::from(self.zero_index) + usize::from(i); + let logical_index = usize::from(self.zero_index) + usize::from(ins.belt_pos); let loc_idx = if logical_index >= self.locs.len() { logical_index - self.locs.len() } else { logical_index }; - if i < old_first_free { + if ins.belt_pos < old_first_free { // We KNOW this position is filled debug_assert!(self.locs[loc_idx]); let mut loc = true; @@ -651,9 +578,9 @@ impl SmartBelt { if !loc { self.locs.set(loc_idx, false); - if !first_free_changed { - self.first_free_index = FreeIndex::FreeIndex(i); - first_free_changed = true; + if ins.belt_pos <= new_first_free { + self.first_free_index = FreeIndex::FreeIndex(ins.belt_pos); + new_first_free = ins.belt_pos; } } } else { @@ -670,10 +597,9 @@ impl SmartBelt { if changed { // the inserter changed something. - if !first_free_changed && i == old_first_free && *loc { + if ins.belt_pos == new_first_free && *loc { // This was the old first free pos - self.first_free_index = - FreeIndex::OldFreeIndex(BeltLenType::try_from(i).unwrap()); + self.first_free_index = FreeIndex::OldFreeIndex(ins.belt_pos); } // if !first_free_changed && i == old_first_free && !*loc { // // This is the new first_free_pos @@ -683,8 +609,6 @@ impl SmartBelt { // } } } - - i += 1; } } @@ -746,25 +670,22 @@ impl SmartBelt { .iter() .map(|ins| match ins.0.state.into() { (Dir::StorageToBelt, InserterState::WaitingForSpaceInDestination(_)) => { - (ins.0.offset, true) + (ins.0.belt_pos, true) }, (Dir::BeltToStorage, InserterState::WaitingForSourceItems(_)) => { - (ins.0.offset, true) + (ins.0.belt_pos, true) }, - _ => (ins.0.offset, false), - }) - .fold((0u16, 0usize), |(old_idx, old_count), (offs, needs)| { - let our_idx = old_idx + offs; - ( - our_idx + 1, - old_count - + usize::from( - needs && (old_idx == 0 || (old_idx - 1) / 8 / 64 != our_idx / 8 / 64), - ), - ) + _ => (ins.0.belt_pos, false), }) - .1; + .fold(0usize, |old_count, (belt_pos, needs)| { + // let our_idx = belt_pos; + // (old_count + // + usize::from( + // needs && (old_idx == 0 || (old_idx - 1) / 8 / 64 != our_idx / 8 / 64), + // ),) + todo!() + }); let cache_lines_from_storage_lookup = self .inserters @@ -1008,24 +929,19 @@ impl SmartBelt { let num_front_inserters = front_inserters.inserters.len(); let _num_back_inserters = back_inserters.inserters.len(); - let free_spots_before_last_inserter_front: u16 = - front_inserters.inserters.iter().map(|i| i.0.offset).sum(); - let length_after_last_inserter = TryInto::::try_into(front_len) - .expect("Belt should be max u16::MAX long") - - free_spots_before_last_inserter_front - - TryInto::::try_into(num_front_inserters) - .expect("Belt should be max u16::MAX long"); - - if let Some(ins) = back_inserters.inserters.get_mut(0) { - ins.0.offset += length_after_last_inserter; - } - let mut new_inserters = front_inserters; take_mut::take(&mut new_inserters.inserters, |ins| { let mut ins = ins.into_vec(); let mut other = vec![].into_boxed_slice(); mem::swap(&mut other, &mut back_inserters.inserters); - ins.extend(other.into_vec().drain(..)); + ins.extend(other.into_vec().drain(..).map(|mut back_ins| { + back_ins.0.belt_pos = back_ins + .0 + .belt_pos + .checked_add(front_len.try_into().unwrap()) + .unwrap(); + back_ins + })); ins.into_boxed_slice() }); @@ -1120,11 +1036,11 @@ impl SmartBelt { }; if side == Side::FRONT { - if !inserters.inserters.is_empty() { - inserters.inserters[0].0.offset = inserters.inserters[0] + for ins in &mut inserters.inserters { + ins.0.belt_pos = ins .0 - .offset - .checked_add(front_extension_amount) + .belt_pos + .checked_add(len) .expect("Max length of belt (u16::MAX) reached"); } } @@ -1154,102 +1070,103 @@ impl SmartBelt { } pub fn break_belt_at(&mut self, belt_pos_to_break_at: u16) -> Option { - // TODO: Is this correct - if self.is_circular { - assert!(self.input_splitter.is_none()); - assert!(self.output_splitter.is_none()); - self.is_circular = false; - self.first_free_index = FreeIndex::OldFreeIndex(0); - self.zero_index = belt_pos_to_break_at; - // FIXME: This will teleport items - return None; - } + todo!() + // // TODO: Is this correct + // if self.is_circular { + // assert!(self.input_splitter.is_none()); + // assert!(self.output_splitter.is_none()); + // self.is_circular = false; + // self.first_free_index = FreeIndex::OldFreeIndex(0); + // self.zero_index = belt_pos_to_break_at; + // // FIXME: This will teleport items + // return None; + // } - if belt_pos_to_break_at == 0 || belt_pos_to_break_at == self.get_len() { - return None; - } + // if belt_pos_to_break_at == 0 || belt_pos_to_break_at == self.get_len() { + // return None; + // } - let mut new_locs = None; - take_mut::take(&mut self.locs, |locs| { - let mut locs_vec = BitBox::from(locs).into_bitvec(); + // let mut new_locs = None; + // take_mut::take(&mut self.locs, |locs| { + // let mut locs_vec = BitBox::from(locs).into_bitvec(); - let len = locs_vec.len(); + // let len = locs_vec.len(); - locs_vec.rotate_left(usize::from(self.zero_index) % len); + // locs_vec.rotate_left(usize::from(self.zero_index) % len); - new_locs = Some( - locs_vec - .split_off(belt_pos_to_break_at.into()) - .into_boxed_bitslice(), - ); + // new_locs = Some( + // locs_vec + // .split_off(belt_pos_to_break_at.into()) + // .into_boxed_bitslice(), + // ); - locs_vec.into_boxed_bitslice().into() - }); + // locs_vec.into_boxed_bitslice().into() + // }); - self.zero_index = 0; - self.first_free_index = FreeIndex::OldFreeIndex(0); + // self.zero_index = 0; + // self.first_free_index = FreeIndex::OldFreeIndex(0); - let new_locs = new_locs.unwrap(); + // let new_locs = new_locs.unwrap(); - let mut offsets = self - .inserters - .inserters - .iter() - .map(|i| i.0.offset) - .enumerate(); + // let mut offsets = self + // .inserters + // .inserters + // .iter() + // .map(|i| i.0.offset) + // .enumerate(); - let mut current_pos = 0; + // let mut current_pos = 0; - let (split_at_inserters, new_offs) = loop { - let Some((i, next_offset)) = offsets.next() else { - break (self.inserters.inserters.len(), 0); - }; + // let (split_at_inserters, new_offs) = loop { + // let Some((i, next_offset)) = offsets.next() else { + // break (self.inserters.inserters.len(), 0); + // }; - // Skip next_offset spots - current_pos += next_offset; + // // Skip next_offset spots + // current_pos += next_offset; - if current_pos >= belt_pos_to_break_at { - break (i, current_pos - belt_pos_to_break_at); - } + // if current_pos >= belt_pos_to_break_at { + // break (i, current_pos - belt_pos_to_break_at); + // } - // The spot, the inserter corresponding to this offset is placed - current_pos += 1; - }; + // // The spot, the inserter corresponding to this offset is placed + // current_pos += 1; + // }; - let mut new_inserters = None; - take_mut::take(&mut self.inserters.inserters, |ins| { - let mut ins = ins.into_vec(); - new_inserters = Some(ins.split_off(split_at_inserters).into_boxed_slice()); - ins.into_boxed_slice() - }); - let mut new_inserters = new_inserters.unwrap(); + // let mut new_inserters = None; + // take_mut::take(&mut self.inserters.inserters, |ins| { + // let mut ins = ins.into_vec(); + // new_inserters = Some(ins.split_off(split_at_inserters).into_boxed_slice()); + // ins.into_boxed_slice() + // }); + // let mut new_inserters = new_inserters.unwrap(); - if let Some(ins) = new_inserters.get_mut(0) { - ins.0.offset = new_offs; - } + // if let Some(ins) = new_inserters.get_mut(0) { + // ins.0.offset = new_offs; + // } - // Since we split off the back portion, it will own our input splitter if we have one - let input_splitter = self.input_splitter.take(); + // // Since we split off the back portion, it will own our input splitter if we have one + // let input_splitter = self.input_splitter.take(); - let new_belt = Self { - ty: self.ty, + // let new_belt = Self { + // ty: self.ty, - is_circular: false, - first_free_index: FreeIndex::OldFreeIndex(0), - zero_index: 0, - locs: new_locs.into(), - inserters: InserterStoreDyn { - inserters: new_inserters, - }, - item: self.item, + // is_circular: false, + // first_free_index: FreeIndex::OldFreeIndex(0), + // zero_index: 0, + // locs: new_locs.into(), + // inserters: InserterStoreDyn { + // inserters: new_inserters, + // }, + // item: self.item, - last_moving_spot: self.last_moving_spot.saturating_sub(belt_pos_to_break_at), + // last_moving_spot: self.last_moving_spot.saturating_sub(belt_pos_to_break_at), - input_splitter, - output_splitter: None, - }; + // input_splitter, + // output_splitter: None, + // }; - Some(new_belt) + // Some(new_belt) } } @@ -1479,16 +1396,16 @@ impl Belt for SmartBelt { return (vec![], self.get_len()); } - let before_inserter_positions = (0..self.inserters.inserters.len()) - .map(|i| { - let offsets: u16 = self.inserters.inserters[..=i] - .iter() - .map(|i| i.0.offset) - .sum(); - let occupied_spaces = i; - let pos = offsets as usize + occupied_spaces; - pos - }) + let kept_range = match side { + Side::FRONT => amount..self.get_len(), + Side::BACK => 0..(self.get_len().checked_sub(amount).unwrap()), + }; + + let before_inserter_positions = self + .inserters + .inserters + .iter() + .map(|ins| ins.0.belt_pos) .collect_vec(); assert!(!self.is_circular); @@ -1511,24 +1428,11 @@ impl Belt for SmartBelt { locs.into_boxed_bitslice().into() }); - let kept_range = match side { - Side::FRONT => amount..(self.get_len() + amount), - Side::BACK => 0..self.get_len(), - }; - - let mut pos_after_last_inserter = 0; - let mut pos_after_last_removed_inserter = 0; - take_mut::take(&mut self.inserters.inserters, |inserters| { let mut inserters = inserters.into_vec(); - // FIXME: This is awful, but it should work inserters.retain(|inserter| { - let next_inserter_pos = pos_after_last_inserter + inserter.0.offset; - pos_after_last_inserter = next_inserter_pos + 1; - - if !kept_range.contains(&next_inserter_pos) { - pos_after_last_removed_inserter = pos_after_last_inserter; + if !kept_range.contains(&inserter.0.belt_pos) { false } else { true @@ -1539,23 +1443,18 @@ impl Belt for SmartBelt { }); if side == Side::FRONT { - if let Some(ins) = self.inserters.inserters.first_mut() { - ins.0.offset -= amount - pos_after_last_removed_inserter; + for ins in &mut self.inserters.inserters { + ins.0.belt_pos = ins.0.belt_pos.checked_sub(amount).unwrap(); } } self.first_free_index = FreeIndex::OldFreeIndex(0); - let after_inserter_positions = (0..self.inserters.inserters.len()) - .map(|i| { - let offsets: u16 = self.inserters.inserters[..=i] - .iter() - .map(|i| i.0.offset) - .sum(); - let occupied_spaces = i; - let pos = offsets as usize + occupied_spaces; - pos - }) + let after_inserter_positions = self + .inserters + .inserters + .iter() + .map(|ins| ins.0.belt_pos) .collect_vec(); match side { @@ -1566,7 +1465,7 @@ impl Belt for SmartBelt { .zip(after_inserter_positions.iter().rev()) { assert_eq!( - *before - amount as usize, + *before - amount, *after, "before: {:?}\n after: {:?}", before_inserter_positions, diff --git a/src/belt/sushi.rs b/src/belt/sushi.rs index 85b8c0f..90a7fb9 100644 --- a/src/belt/sushi.rs +++ b/src/belt/sushi.rs @@ -90,54 +90,17 @@ impl SushiBelt { self.locs.len() ); - let mut pos_after_last_inserter = 0; - let mut i = 0; - - 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) { - std::cmp::Ordering::Greater => break, // This is the index to insert at - std::cmp::Ordering::Equal => return Err(SpaceOccupiedError), - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } - - // Insert at i - let new_inserter_offset = pos - pos_after_last_inserter; take_mut::take(&mut self.inserters.inserters, |ins| { let mut ins = ins.into_vec(); - ins.insert( - i, - ( - BeltStorageInserterDyn::new( - Dir::StorageToBelt, - new_inserter_offset, - storage_id, - ), - filter, - movetime.try_into().unwrap_or(u8::MAX), - hand_size, - ), - ); + ins.push(( + BeltStorageInserterDyn::new(Dir::StorageToBelt, pos, storage_id), + filter, + movetime.try_into().unwrap_or(u8::MAX), + hand_size, + )); ins.into_boxed_slice() }); - let next = self.inserters.inserters.get_mut(i + 1); - - if let Some((next_ins, _item, _movetime, _hand_size)) = next { - next_ins.offset -= new_inserter_offset + 1; - } - Ok(()) } @@ -155,80 +118,36 @@ impl SushiBelt { self.locs.len() ); - let mut pos_after_last_inserter = 0; - let mut i = 0; - - 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) { - std::cmp::Ordering::Greater => break, // This is the index to insert at - std::cmp::Ordering::Equal => return Err(SpaceOccupiedError), - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } - - // Insert at i - let new_inserter_offset = pos - pos_after_last_inserter; take_mut::take(&mut self.inserters.inserters, |ins| { let mut ins = ins.into_vec(); - ins.insert( - i, - ( - BeltStorageInserterDyn::new( - Dir::BeltToStorage, - new_inserter_offset, - storage_id, - ), - filter, - movetime.try_into().unwrap_or(u8::MAX), - hand_size, - ), - ); + ins.push(( + BeltStorageInserterDyn::new(Dir::BeltToStorage, pos, storage_id), + filter, + movetime.try_into().unwrap_or(u8::MAX), + hand_size, + )); ins.into_boxed_slice() }); - let next = self.inserters.inserters.get_mut(i + 1); - - if let Some((next_ins, _item, _movetime, _hand_size)) = next { - next_ins.offset -= new_inserter_offset + 1; - } - Ok(()) } #[must_use] pub fn get_inserter_info_at(&self, belt_pos: u16) -> Option { - let mut pos = 0; - - for (inserter, _item, movetime, hand_size) in self.inserters.inserters.iter() { - pos += inserter.offset; - if pos == belt_pos { - let (dir, state) = inserter.state.into(); - return Some(BeltInserterInfo { + self.inserters + .inserters + .iter() + .find(|ins| ins.0.belt_pos == belt_pos) + .map(|ins| { + let (dir, state) = ins.0.state.into(); + BeltInserterInfo { outgoing: dir == Dir::BeltToStorage, state, - connection: inserter.storage_id, - - hand_size: *hand_size, - movetime: *movetime, - }); - } else if pos > belt_pos { - return None; - } - pos += 1; - } - - None + connection: ins.0.storage_id, + hand_size: ins.3, + movetime: ins.2, + } + }) } pub fn get_inserter_item(&self, belt_pos: u16) -> Item { @@ -238,46 +157,24 @@ impl SushiBelt { self.locs.len() ); - let mut pos_after_last_inserter = 0; - let mut i = 0; - - for offset in self - .inserters + 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) { - std::cmp::Ordering::Greater => panic!( - "The belt did not have an inserter at position specified to remove inserter from" - ), // This is the index to insert at - std::cmp::Ordering::Equal => break, - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } - - self.inserters.inserters[i].1 + .find(|ins| ins.0.belt_pos == belt_pos) + .map(|ins| ins.1) + .expect("No inserter at pos") } pub fn set_inserter_storage_id(&mut self, belt_pos: u16, new: FakeUnionStorage) { - let mut pos = 0; - - for (inserter, _item, _movetime, _hand_size) in self.inserters.inserters.iter_mut() { - pos += inserter.offset; - if pos == belt_pos { - inserter.storage_id = new; - return; - } else if pos >= belt_pos { - unreachable!() - } - pos += 1; - } + let Some(ins) = self + .inserters + .inserters + .iter_mut() + .find(|ins| ins.0.belt_pos == belt_pos) + else { + unreachable!() + }; + ins.0.storage_id = new; } pub fn remove_inserter(&mut self, pos: BeltLenType) { @@ -287,29 +184,12 @@ impl SushiBelt { self.locs.len() ); - let mut pos_after_last_inserter = 0; - let mut i = 0; - - for offset in self + let i = 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) { - std::cmp::Ordering::Greater => panic!( - "The belt did not have an inserter at position specified to remove inserter from" - ), // This is the index to insert at - std::cmp::Ordering::Equal => break, - - std::cmp::Ordering::Less => { - pos_after_last_inserter = next_inserter_pos + 1; - i += 1; - }, - } - } + .position(|ins| ins.0.belt_pos == pos) + .unwrap(); let mut removed = None; take_mut::take(&mut self.inserters.inserters, |ins| { @@ -318,8 +198,6 @@ impl SushiBelt { ins.into_boxed_slice() }); let removed = removed.unwrap(); - // The offset after i (which has now shifted left to i) - self.inserters.inserters[i].0.offset += removed.0.offset + 1; } pub(super) fn check_sushi( @@ -477,98 +355,99 @@ impl SushiBelt { } pub fn break_belt_at(&mut self, belt_pos_to_break_at: u16) -> Option { - // TODO: Is this correct - if self.is_circular { - self.is_circular = false; - self.first_free_index = FreeIndex::OldFreeIndex(0); - self.zero_index = belt_pos_to_break_at; - return None; - } - - if belt_pos_to_break_at == 0 || belt_pos_to_break_at == self.get_len() { - return None; - } - - let mut new_locs = None; - take_mut::take(&mut self.locs, |locs| { - let mut locs_vec = locs.into_vec(); - - let len = locs_vec.len(); - - locs_vec.rotate_left(usize::from(self.zero_index) % len); - - new_locs = Some( - locs_vec - .split_off(belt_pos_to_break_at.into()) - .into_boxed_slice(), - ); - - locs_vec.into_boxed_slice() - }); - - self.zero_index = 0; - self.first_free_index = FreeIndex::OldFreeIndex(0); - - let new_locs = new_locs.unwrap(); - - let mut offsets = self - .inserters - .inserters - .iter() - .map(|(i, _item, _movetime, _hand_size)| i.offset) - .enumerate(); - - let mut current_pos = 0; - - let (split_at_inserters, new_offs) = loop { - let Some((i, next_offset)) = offsets.next() else { - break (self.inserters.inserters.len(), 0); - }; - - // Skip next_offset spots - current_pos += next_offset; - - if current_pos >= belt_pos_to_break_at { - break (i, current_pos - belt_pos_to_break_at); - } - - // The spot, the inserter corresponding to this offset is placed - current_pos += 1; - }; - - let mut new_inserters = None; - take_mut::take(&mut self.inserters.inserters, |ins| { - let mut ins = ins.into_vec(); - new_inserters = Some(ins.split_off(split_at_inserters).into_boxed_slice()); - ins.into_boxed_slice() - }); - let mut new_inserters = new_inserters.unwrap(); - - if let Some(new_ins) = new_inserters.get_mut(0) { - new_ins.0.offset = new_offs; - } - - // Since self will end up as the front half, any inputting splitter will end up at the back belt - let input_splitter = self.input_splitter.take(); - - let new_belt = Self { - ty: self.ty, - - is_circular: false, - first_free_index: FreeIndex::OldFreeIndex(0), - zero_index: 0, - locs: new_locs, - inserters: SushiInserterStoreDyn { - inserters: new_inserters, - }, - - last_moving_spot: self.last_moving_spot.saturating_sub(belt_pos_to_break_at), - - input_splitter, - output_splitter: None, - }; - - Some(new_belt) + todo!() + // // TODO: Is this correct + // if self.is_circular { + // self.is_circular = false; + // self.first_free_index = FreeIndex::OldFreeIndex(0); + // self.zero_index = belt_pos_to_break_at; + // return None; + // } + + // if belt_pos_to_break_at == 0 || belt_pos_to_break_at == self.get_len() { + // return None; + // } + + // let mut new_locs = None; + // take_mut::take(&mut self.locs, |locs| { + // let mut locs_vec = locs.into_vec(); + + // let len = locs_vec.len(); + + // locs_vec.rotate_left(usize::from(self.zero_index) % len); + + // new_locs = Some( + // locs_vec + // .split_off(belt_pos_to_break_at.into()) + // .into_boxed_slice(), + // ); + + // locs_vec.into_boxed_slice() + // }); + + // self.zero_index = 0; + // self.first_free_index = FreeIndex::OldFreeIndex(0); + + // let new_locs = new_locs.unwrap(); + + // let mut offsets = self + // .inserters + // .inserters + // .iter() + // .map(|(i, _item, _movetime, _hand_size)| i.offset) + // .enumerate(); + + // let mut current_pos = 0; + + // let (split_at_inserters, new_offs) = loop { + // let Some((i, next_offset)) = offsets.next() else { + // break (self.inserters.inserters.len(), 0); + // }; + + // // Skip next_offset spots + // current_pos += next_offset; + + // if current_pos >= belt_pos_to_break_at { + // break (i, current_pos - belt_pos_to_break_at); + // } + + // // The spot, the inserter corresponding to this offset is placed + // current_pos += 1; + // }; + + // let mut new_inserters = None; + // take_mut::take(&mut self.inserters.inserters, |ins| { + // let mut ins = ins.into_vec(); + // new_inserters = Some(ins.split_off(split_at_inserters).into_boxed_slice()); + // ins.into_boxed_slice() + // }); + // let mut new_inserters = new_inserters.unwrap(); + + // if let Some(new_ins) = new_inserters.get_mut(0) { + // new_ins.0.offset = new_offs; + // } + + // // Since self will end up as the front half, any inputting splitter will end up at the back belt + // let input_splitter = self.input_splitter.take(); + + // let new_belt = Self { + // ty: self.ty, + + // is_circular: false, + // first_free_index: FreeIndex::OldFreeIndex(0), + // zero_index: 0, + // locs: new_locs, + // inserters: SushiInserterStoreDyn { + // inserters: new_inserters, + // }, + + // last_moving_spot: self.last_moving_spot.saturating_sub(belt_pos_to_break_at), + + // input_splitter, + // output_splitter: None, + // }; + + // Some(new_belt) } pub fn make_circular(&mut self) { @@ -630,30 +509,19 @@ impl SushiBelt { // Important, first_free_index must ALWAYS be used using mod len let back_zero_index = usize::from(back_zero_index) % back_locs.len(); - let num_front_inserters = front_inserters.inserters.len(); - let _num_back_inserters = back_inserters.inserters.len(); - - let free_spots_before_last_inserter_front: u16 = front_inserters - .inserters - .iter() - .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") - - free_spots_before_last_inserter_front - - TryInto::::try_into(num_front_inserters) - .expect("Belt should be max u16::MAX long"); - - if let Some((i, _item, _movetime, _hand_size)) = back_inserters.inserters.get_mut(0) { - i.offset += length_after_last_inserter; - } - let mut new_inserters = front_inserters; take_mut::take(&mut new_inserters.inserters, |ins| { let mut ins = ins.into_vec(); let mut other = vec![].into_boxed_slice(); mem::swap(&mut other, &mut back_inserters.inserters); - ins.extend(other.into_vec().drain(..)); + ins.extend(other.into_vec().drain(..).map(|mut back_ins| { + back_ins.0.belt_pos = back_ins + .0 + .belt_pos + .checked_add(front_len.try_into().unwrap()) + .unwrap(); + back_ins + })); ins.into_boxed_slice() }); @@ -1025,157 +893,159 @@ impl Belt for SushiBelt { amount: BeltLenType, side: Side, ) -> (Vec<(Item, u32)>, BeltLenType) { - if amount == 0 { - return (vec![], self.get_len()); - } - - assert!(!self.is_circular); - assert!(amount <= self.get_len()); - - self.locs - .rotate_left(self.zero_index as usize % self.locs.len()); - self.zero_index = 0; - let mut item_counts = HashMap::default(); - take_mut::take(&mut self.locs, |locs| { - let mut locs = locs.into_vec(); - - let removed_items = match side { - Side::FRONT => locs.drain(..(amount as usize)), - Side::BACK => locs.drain((locs.len() - (amount as usize))..), - }; - - item_counts = removed_items.flatten().counts(); - - locs.into_boxed_slice() - }); - - let kept_range = match side { - Side::FRONT => amount..(self.get_len() + amount), - Side::BACK => 0..self.get_len(), - }; - - let mut pos_after_last_inserter = 0; - let mut pos_after_last_removed_inserter = 0; - - take_mut::take(&mut self.inserters.inserters, |inserters| { - let mut inserters = inserters.into_vec(); - - // FIXME: This is awful, but it should work - inserters.retain(|inserter| { - let next_inserter_pos = pos_after_last_inserter + inserter.0.offset; - pos_after_last_inserter = next_inserter_pos + 1; - - if !kept_range.contains(&next_inserter_pos) { - pos_after_last_removed_inserter = pos_after_last_inserter; - false - } else { - true - } - }); - - inserters.into_boxed_slice() - }); - - if side == Side::FRONT { - if let Some((i, _item, _movetime, _hand_size)) = self.inserters.inserters.first_mut() { - i.offset -= amount - pos_after_last_removed_inserter; - } - } - - self.first_free_index = FreeIndex::OldFreeIndex(0); - - ( - item_counts - .into_iter() - .sorted_by_key(|(k, _)| *k) - .map(|(k, v)| (k, v.try_into().unwrap())) - .collect_vec(), - self.get_len(), - ) + todo!() + // if amount == 0 { + // return (vec![], self.get_len()); + // } + + // assert!(!self.is_circular); + // assert!(amount <= self.get_len()); + + // self.locs + // .rotate_left(self.zero_index as usize % self.locs.len()); + // self.zero_index = 0; + // let mut item_counts = HashMap::default(); + // take_mut::take(&mut self.locs, |locs| { + // let mut locs = locs.into_vec(); + + // let removed_items = match side { + // Side::FRONT => locs.drain(..(amount as usize)), + // Side::BACK => locs.drain((locs.len() - (amount as usize))..), + // }; + + // item_counts = removed_items.flatten().counts(); + + // locs.into_boxed_slice() + // }); + + // let kept_range = match side { + // Side::FRONT => amount..(self.get_len() + amount), + // Side::BACK => 0..self.get_len(), + // }; + + // let mut pos_after_last_inserter = 0; + // let mut pos_after_last_removed_inserter = 0; + + // take_mut::take(&mut self.inserters.inserters, |inserters| { + // let mut inserters = inserters.into_vec(); + + // // FIXME: This is awful, but it should work + // inserters.retain(|inserter| { + // let next_inserter_pos = pos_after_last_inserter + inserter.0.offset; + // pos_after_last_inserter = next_inserter_pos + 1; + + // if !kept_range.contains(&next_inserter_pos) { + // pos_after_last_removed_inserter = pos_after_last_inserter; + // false + // } else { + // true + // } + // }); + + // inserters.into_boxed_slice() + // }); + + // if side == Side::FRONT { + // if let Some((i, _item, _movetime, _hand_size)) = self.inserters.inserters.first_mut() { + // i.offset -= amount - pos_after_last_removed_inserter; + // } + // } + + // self.first_free_index = FreeIndex::OldFreeIndex(0); + + // ( + // item_counts + // .into_iter() + // .sorted_by_key(|(k, _)| *k) + // .map(|(k, v)| (k, v.try_into().unwrap())) + // .collect_vec(), + // self.get_len(), + // ) } fn add_length(&mut self, amount: BeltLenType, side: super::smart::Side) -> BeltLenType { - let len = self.get_len(); - - take_mut::take(self, |slf| { - let Self { - ty, - - is_circular: _, - first_free_index, - zero_index, - locs, - mut inserters, - - last_moving_spot, - - input_splitter, - output_splitter, - } = slf; - - match side { - Side::FRONT => assert!(output_splitter.is_none()), - Side::BACK => assert!(input_splitter.is_none()), - } - - // Important, first_free_index must ALWAYS be used using mod len - let zero_index = zero_index % len; - - let mut locs = locs.into_vec(); - - let old_len = locs.len(); - - let (new_empty, new_zero, front_extension_amount) = match side { - Side::FRONT => { - locs.splice( - usize::from(zero_index)..usize::from(zero_index), - repeat(None).take(usize::from(amount)), - ); - (FreeIndex::FreeIndex(0), zero_index, amount) - }, - Side::BACK => { - locs.splice( - ((usize::from(zero_index) + (old_len - 1)) % old_len) - ..((usize::from(zero_index) + (old_len - 1)) % old_len), - repeat(None).take(usize::from(amount)), - ); - ( - match first_free_index { - FreeIndex::FreeIndex(idx) => FreeIndex::FreeIndex(idx), - FreeIndex::OldFreeIndex(idx) => FreeIndex::OldFreeIndex(idx), - }, - zero_index + amount, - 0, - ) - }, - }; - - if side == Side::FRONT { - if !inserters.inserters.is_empty() { - inserters.inserters[0].0.offset = inserters.inserters[0] - .0 - .offset - .checked_add(front_extension_amount) - .expect("Max length of belt (u16::MAX) reached"); - } - } - - Self { - ty, - - is_circular: false, - first_free_index: new_empty, - zero_index: new_zero, - locs: locs.into_boxed_slice(), - inserters, - - last_moving_spot, - - input_splitter, - output_splitter, - } - }); - - len + amount + todo!() + // let len = self.get_len(); + + // take_mut::take(self, |slf| { + // let Self { + // ty, + + // is_circular: _, + // first_free_index, + // zero_index, + // locs, + // mut inserters, + + // last_moving_spot, + + // input_splitter, + // output_splitter, + // } = slf; + + // match side { + // Side::FRONT => assert!(output_splitter.is_none()), + // Side::BACK => assert!(input_splitter.is_none()), + // } + + // // Important, first_free_index must ALWAYS be used using mod len + // let zero_index = zero_index % len; + + // let mut locs = locs.into_vec(); + + // let old_len = locs.len(); + + // let (new_empty, new_zero, front_extension_amount) = match side { + // Side::FRONT => { + // locs.splice( + // usize::from(zero_index)..usize::from(zero_index), + // repeat(None).take(usize::from(amount)), + // ); + // (FreeIndex::FreeIndex(0), zero_index, amount) + // }, + // Side::BACK => { + // locs.splice( + // ((usize::from(zero_index) + (old_len - 1)) % old_len) + // ..((usize::from(zero_index) + (old_len - 1)) % old_len), + // repeat(None).take(usize::from(amount)), + // ); + // ( + // match first_free_index { + // FreeIndex::FreeIndex(idx) => FreeIndex::FreeIndex(idx), + // FreeIndex::OldFreeIndex(idx) => FreeIndex::OldFreeIndex(idx), + // }, + // zero_index + amount, + // 0, + // ) + // }, + // }; + + // if side == Side::FRONT { + // if !inserters.inserters.is_empty() { + // inserters.inserters[0].0.offset = inserters.inserters[0] + // .0 + // .offset + // .checked_add(front_extension_amount) + // .expect("Max length of belt (u16::MAX) reached"); + // } + // } + + // Self { + // ty, + + // is_circular: false, + // first_free_index: new_empty, + // zero_index: new_zero, + // locs: locs.into_boxed_slice(), + // inserters, + + // last_moving_spot, + + // input_splitter, + // output_splitter, + // } + // }); + + // len + amount } } diff --git a/src/inserter/belt_storage_inserter_non_const_gen.rs b/src/inserter/belt_storage_inserter_non_const_gen.rs index 9d3bd2d..0c923b3 100644 --- a/src/inserter/belt_storage_inserter_non_const_gen.rs +++ b/src/inserter/belt_storage_inserter_non_const_gen.rs @@ -4,6 +4,7 @@ use std::{ }; use crate::{ + belt::belt::BeltLenType, item::ITEMCOUNTTYPE, storage_list::{SingleItemStorages, index_fake_union}, }; @@ -65,16 +66,16 @@ impl From for (Dir, InserterState) { #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct BeltStorageInserterDyn { - pub offset: u16, + pub belt_pos: u16, pub storage_id: FakeUnionStorage, pub state: DynInserterState, } impl BeltStorageInserterDyn { #[must_use] - pub const fn new(dir: Dir, offset: u16, id: FakeUnionStorage) -> Self { + pub const fn new(dir: Dir, belt_pos: BeltLenType, id: FakeUnionStorage) -> Self { Self { - offset, + belt_pos, storage_id: id, state: match dir { Dir::BeltToStorage => DynInserterState::BSWaitingForSourceItems(0), From d4e8e8b1363f7d20922385dd0e3e8110fe0f9689 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 27 Nov 2025 00:48:08 +0100 Subject: [PATCH 055/152] Add belt_storage wait lists --- src/app_state.rs | 414 +++++++++++------- src/assembler/simd.rs | 297 +++++++++---- src/belt/mod.rs | 3 +- src/belt/smart.rs | 316 ++++++------- src/belt/sushi.rs | 32 +- src/chest.rs | 97 ++-- src/data/mod.rs | 5 +- src/frontend/world/tile.rs | 15 +- src/inserter/belt_storage_movement_list.rs | 256 +++++++++++ src/inserter/mod.rs | 1 + .../storage_storage_with_buckets_indirect.rs | 81 ++-- src/power/mod.rs | 97 +++- src/rendering/render_world.rs | 30 +- 13 files changed, 1143 insertions(+), 501 deletions(-) create mode 100644 src/inserter/belt_storage_movement_list.rs diff --git a/src/app_state.rs b/src/app_state.rs index a98d6f3..119f123 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,3 +1,4 @@ +use crate::assembler::simd::Conn; use crate::belt::BeltTileId; use crate::belt::belt::Belt; use crate::blueprint::blueprint_string::BlueprintString; @@ -9,6 +10,9 @@ use crate::frontend::world::tile::ModuleSlots; use crate::frontend::world::tile::ModuleTy; use crate::inserter::InserterStateInfo; use crate::inserter::WaitlistSearchSide; +use crate::inserter::belt_storage_inserter; +use crate::inserter::belt_storage_movement_list::BeltStorageInserterInMovement; +use crate::inserter::belt_storage_movement_list::List; use crate::inserter::storage_storage_with_buckets_indirect::BucketedStorageStorageInserterStore; use crate::inserter::storage_storage_with_buckets_indirect::InserterBucketData; use crate::inserter::storage_storage_with_buckets_indirect::InserterIdentifier; @@ -574,13 +578,30 @@ pub struct Factory { pub power_grids: PowerGridStorage, pub belts: BeltStore, pub storage_storage_inserters: StorageStorageInserterStore, - pub belt_storage_inserters: Box<[BTreeMap< - u16, - ( - crate::inserter::belt_storage_pure_buckets::BucketedStorageStorageInserterStoreFrontend, - crate::inserter::belt_storage_pure_buckets::BucketedStorageStorageInserterStore, - ), - >]>, + pub belt_storage_inserters: Box< + [( + ( + List< + { belt_storage_inserter::Dir::BeltToStorage }, + { belt_storage_inserter::Dir::BeltToStorage }, + >, + List< + { belt_storage_inserter::Dir::StorageToBelt }, + { belt_storage_inserter::Dir::BeltToStorage }, + >, + ), + ( + List< + { belt_storage_inserter::Dir::BeltToStorage }, + { belt_storage_inserter::Dir::StorageToBelt }, + >, + List< + { belt_storage_inserter::Dir::StorageToBelt }, + { belt_storage_inserter::Dir::StorageToBelt }, + >, + ), + )], + >, pub chests: FullChestStore, pub fluid_store: FluidSystemStore, @@ -593,7 +614,7 @@ pub struct Factory { #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct StorageStorageInserterStore { - pub inserters: Box<[BTreeMap]>, + pub inserters: Box<[BTreeMap, (BucketedStorageStorageInserterStore,)>]>, } impl StorageStorageInserterStore { @@ -661,7 +682,7 @@ impl StorageStorageInserterStore { pub fn add_ins( &mut self, item: Item, - movetime: u16, + movetime: NonZero, start: Storage, dest: Storage, hand_size: ITEMCOUNTTYPE, @@ -684,7 +705,7 @@ impl StorageStorageInserterStore { pub fn get_inserter( &self, item: Item, - movetime: u16, + movetime: NonZero, id: InserterIdentifier, current_tick: u32, ) -> InserterStateInfo { @@ -698,7 +719,7 @@ impl StorageStorageInserterStore { pub fn remove_ins( &mut self, item: Item, - movetime: u16, + movetime: NonZero, id: InserterIdentifier, ) -> Result<(), WaitlistSearchSide> { let inserter = self.inserters[item.into_usize()] @@ -720,8 +741,8 @@ impl StorageStorageInserterStore { pub fn change_movetime( &mut self, item: Item, - old_movetime: u16, - new_movetime: u16, + old_movetime: NonZero, + new_movetime: NonZero, id: InserterIdentifier, ) -> Result< InserterIdentifier, @@ -766,7 +787,7 @@ impl StorageStorageInserterStore { pub fn update_inserter_src( &mut self, item: Item, - movetime: u16, + movetime: NonZero, id: InserterIdentifier, new_src: Storage, data_store: &DataStore, @@ -786,7 +807,7 @@ impl StorageStorageInserterStore { pub fn update_inserter_dest( &mut self, item: Item, - movetime: u16, + movetime: NonZero, id: InserterIdentifier, new_dest: Storage, data_store: &DataStore, @@ -806,7 +827,7 @@ impl StorageStorageInserterStore { pub fn update_inserter_src_if_equal( &mut self, item: Item, - movetime: u16, + movetime: NonZero, id: InserterIdentifier, old_src: FakeUnionStorage, new_src: Storage, @@ -828,7 +849,7 @@ impl StorageStorageInserterStore { pub fn update_inserter_dest_if_equal( &mut self, item: Item, - movetime: u16, + movetime: NonZero, id: InserterIdentifier, old_dest: FakeUnionStorage, new_dest: Storage, @@ -859,7 +880,7 @@ impl Factory Factory Factory Factory= 120 { - belt.update(sushi_splitters); - } - belt.update_inserters(item_storages, grid_size); - } - } + let (belt_to_storage_flow, storage_to_belt_flow) = belt_storage_inserter_lists; - { - profiling::scope!("Update PurePure Inserters"); - for ( - ins, - ((source, source_pos), (dest, dest_pos), cooldown, filter), - ) in pure_to_pure_inserters.iter_mut().flatten() - { - let [mut source_loc, mut dest_loc] = if *source == *dest { - assert_ne!( - source_pos, dest_pos, - "An inserter cannot take and drop off on the same tile" - ); - // We are taking and placing onto the same belt - let belt = &mut belt_store.belts[*source]; - - belt.get_two([(*source_pos).into(), (*dest_pos).into()]).map(|v| *v) - } else { - let [inp, out] = belt_store - .belts - .get_disjoint_mut([*source, *dest]) - .unwrap(); - - [*inp.get(*source_pos), *out.get(*dest_pos)] - }; - - if *cooldown == 0 { - ins.update_instant(&mut source_loc,&mut dest_loc); - } else { - ins.update( - &mut source_loc, - &mut dest_loc, - *cooldown, - // FIXME: - 1, - (), - |_| { - filter - .map(|filter_item| filter_item == item) - .unwrap_or(true) - }, - ); - } + let (outgoing_belt_to_storage, incoming_belt_to_storage) = belt_to_storage_flow; + let (incoming_storage_to_belt, outgoing_storage_to_belt) = storage_to_belt_flow; - { - profiling::scope!("Update update_first_free_pos"); - if !source_loc { - let _: Option<_> = belt_store.belts[*source] - .remove_item(*source_pos); - } + let (belt_storage_exit_outgoing, mut belt_storage_reinsertion_outgoing) = outgoing_belt_to_storage.tick(); + let (belt_storage_exit_incoming, mut belt_storage_reinsertion_incoming) = incoming_belt_to_storage.tick(); - if dest_loc { - let _ = belt_store.belts[*dest] - .try_insert_item(*dest_pos, item); + let (storage_belt_exit_outgoing, mut storage_belt_reinsertion_outgoing) = outgoing_storage_to_belt.tick(); + let (storage_belt_exit_incoming, mut storage_belt_reinsertion_incoming) = incoming_storage_to_belt.tick(); + + + join!( + || { + profiling::scope!( + "Pure Belt Update", + format!("Item: {}", data_store.item_display_names[item_id]).as_str() + ); + { + profiling::scope!("storage_belt_exit.update"); + belt_storage_exit_incoming.update(&mut belt_storage_reinsertion_outgoing, belt_store.belts.as_mut_slice()); + storage_belt_exit_outgoing.update(&mut storage_belt_reinsertion_incoming, belt_store.belts.as_mut_slice()); + } + { + profiling::scope!( + "Update Belts", + format!("Count: {}", belt_store.belts.len()) + ); + for (self_index, (belt, ty)) in + belt_store.belts.iter_mut().zip(&belt_store.belt_ty).enumerate() + { + // TODO: Avoid last minute decision making + // If I have a list per type, I can avoid loading belts if they are not updated + if update_timers[usize::from(*ty)] >= 120 { + belt.update(sushi_splitters); + belt.update_inserters(self_index.try_into().unwrap(), &mut belt_storage_reinsertion_outgoing, &mut storage_belt_reinsertion_incoming); + } } } - } - } - } - { - - let item = Item { - id: item_id.try_into().unwrap(), - }; + { + profiling::scope!("Update PurePure Inserters"); + for ( + ins, + ((source, source_pos), (dest, dest_pos), cooldown, filter), + ) in pure_to_pure_inserters.iter_mut().flatten() + { + let [mut source_loc, mut dest_loc] = if *source == *dest { + assert_ne!( + source_pos, dest_pos, + "An inserter cannot take and drop off on the same tile" + ); + // We are taking and placing onto the same belt + let belt = &mut belt_store.belts[*source]; + + belt.get_two([(*source_pos).into(), (*dest_pos).into()]).map(|v| *v) + } else { + let [inp, out] = belt_store + .belts + .get_disjoint_mut([*source, *dest]) + .unwrap(); + + [*inp.get(*source_pos), *out.get(*dest_pos)] + }; + + if *cooldown == 0 { + ins.update_instant(&mut source_loc,&mut dest_loc); + } else { + ins.update( + &mut source_loc, + &mut dest_loc, + *cooldown, + // FIXME: + 1, + (), + |_| { + filter + .map(|filter_item| filter_item == item) + .unwrap_or(true) + }, + ); + } - let grid_size = grid_size(item, data_store); - let num_recipes = num_recipes(item, data_store); + { + profiling::scope!("Update update_first_free_pos"); + if !source_loc { + let _: Option<_> = belt_store.belts[*source] + .remove_item(*source_pos); + } - if data_store.item_is_fluid[item_id] { - profiling::scope!( - "FluidSystem Update", - format!("Item: {}", data_store.item_display_names[item_id]) - .as_str() - ); - for fluid_system in fluid_store { - // FIXME: Switch to holes - if let Some(fluid_system) = fluid_system { - update_fluid_system(item_id, &mut fluid_system.hot_data, item_storages, grid_size); + if dest_loc { + let _ = belt_store.belts[*dest] + .try_insert_item(*dest_pos, item); + } + } + } + } + }, + || { + { + profiling::scope!("belt_storage_exit.update"); + belt_storage_exit_outgoing.update(item_id, grid_size, &mut belt_storage_reinsertion_incoming, item_storages); + storage_belt_exit_incoming.update(item_id, grid_size, &mut storage_belt_reinsertion_outgoing, item_storages); + } + { + if data_store.item_is_fluid[item_id] { + profiling::scope!( + "FluidSystem Update", + format!("Item: {}", data_store.item_display_names[item_id]) + .as_str() + ); + for fluid_system in fluid_store { + // FIXME: Switch to holes + if let Some(fluid_system) = fluid_system { + update_fluid_system(item_id, &mut fluid_system.hot_data, item_storages, grid_size); + } + } + } else { + profiling::scope!( + "StorageStorage Inserter Update", + format!("Item: {}", data_store.item_display_names[item_id]) + .as_str() + ); + for (ins_store,) in storage_storage_inserter_stores.values_mut() + { + profiling::scope!( + "StorageStorage Inserter Update", + format!("Movetime: {}", ins_store.movetime).as_str() + ); + ins_store.update( + item_id, + item_storages, + grid_size, + current_tick, + ); + } + } } } - } else { - profiling::scope!( - "StorageStorage Inserter Update", - format!("Item: {}", data_store.item_display_names[item_id]) - .as_str() - ); - for (ins_store,) in storage_storage_inserter_stores.values_mut() - { - profiling::scope!( - "StorageStorage Inserter Update", - format!("Movetime: {}", ins_store.movetime).as_str() - ); - ins_store.update( - item_id, - item_storages, - grid_size, - current_tick, - ); - } - } + ); } let pure_update_time = pure_update_start.elapsed(); @@ -1347,11 +1387,10 @@ impl GameState todo!(), AttachedInserter::StorageStorage { item, inserter } => { - let old_movetime = - user_movetime.map(|v| v.into()).unwrap_or( - data_store.inserter_infos[*ty as usize] - .swing_time_ticks, - ); + let old_movetime = user_movetime.unwrap_or( + data_store.inserter_infos[*ty as usize] + .swing_time_ticks, + ); let new_movetime = new_movetime.map(|v| v.into()).unwrap_or( @@ -1399,9 +1438,17 @@ impl GameState { let removed = game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores.remove_wait_list_inserter(*id, item, inserter.id, data_store); + let Conn::Storage { + index, + storage_id_in, + storage_id_out, + } = removed.conn + else { + unreachable!() + }; handle_fn( - removed.storage_id_in, - removed.storage_id_out, + storage_id_in, + storage_id_out, removed.max_hand.into(), ) }, @@ -1435,9 +1482,17 @@ impl GameState { let removed = game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores.remove_wait_list_inserter(*id, item, inserter.id, data_store); + let Conn::Storage { + index, + storage_id_in, + storage_id_out, + } = removed.conn + else { + unreachable!() + }; handle_fn( - removed.storage_id_in, - removed.storage_id_out, + storage_id_in, + storage_id_out, removed.max_hand.into(), ) }, @@ -2868,6 +2923,7 @@ impl GameState GameState { + let store = store.get_mut(&inserter.movetime).unwrap(); + let ins = InserterBucketData { + storage_id_in, + storage_id_out, + index, + current_hand: inserter.current_hand, + max_hand_size: inserter.max_hand, + }; + if inserter.current_hand == 0 { + store.0.reinsert_empty(ins); + } else { + store.0.reinsert_empty(ins); + } + }, + crate::assembler::simd::Conn::Belt { + belt_id, + belt_pos, + self_storage, + self_is_source, + } => { + let ((_a, belt_storage), (_c, storage_belt)) = belt_storage; + + if self_is_source { + storage_belt.reinsert( + inserter.movetime, + BeltStorageInserterInMovement { + current_hand: inserter.max_hand, + movetime: inserter.movetime.try_into().unwrap(), + storage: self_storage, + belt: belt_id, + belt_pos, + max_hand_size: inserter.max_hand, + }, + ); + } else { + belt_storage.reinsert( + inserter.movetime, + BeltStorageInserterInMovement { + current_hand: 0, + movetime: inserter.movetime.try_into().unwrap(), + storage: self_storage, + belt: belt_id, + belt_pos, + max_hand_size: inserter.max_hand, + }, + ); + } + }, } } }); diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index a3c2145..b441fa0 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -1,3 +1,4 @@ +use crate::belt::belt::BeltLenType; use std::{ array, cmp::min, @@ -39,7 +40,7 @@ use get_size2::GetSize; #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[repr(align(64))] pub struct InserterWaitList { - pub inserters: [Option; 3], + pub inserters: [Option; 3], } const_assert!(std::mem::size_of::() <= 64); @@ -63,26 +64,28 @@ pub struct Inserter { const_assert!(std::mem::size_of::>() <= 20); const_assert!(std::mem::size_of::() <= 20); -pub struct InserterWithBelts { - current_hand: ITEMCOUNTTYPE, - max_hand: NonZero, +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct InserterWithBelts { + pub(crate) current_hand: ITEMCOUNTTYPE, + pub(crate) max_hand: ITEMCOUNTTYPE, - rest: InserterWithBeltsEnum, + pub(crate) rest: InserterWithBeltsEnum, + pub(crate) movetime: NonZero, } #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub enum InserterWithBeltsEnum { +pub(crate) enum InserterWithBeltsEnum { StorageStorage { // TODO: This is not needed for assemblers, just for chests. self_is_source: bool, - movetime: NonZero, index: InserterId, other: FakeUnionStorage, }, BeltStorage { - movetime: NonZero, + self_is_source: bool, belt_id: u32, belt_pos: u16, }, @@ -90,24 +93,52 @@ pub enum InserterWithBeltsEnum { #[derive(Debug)] pub struct InserterReinsertionInfo { - pub movetime: u16, + pub movetime: NonZero, pub item: Item, pub current_hand: ITEMCOUNTTYPE, pub max_hand: ITEMCOUNTTYPE, - pub(crate) index: InserterId, - pub storage_id_in: FakeUnionStorage, - pub storage_id_out: FakeUnionStorage, + + pub(crate) conn: Conn, +} + +#[derive(Debug)] +pub enum Conn { + Storage { + index: InserterId, + storage_id_in: FakeUnionStorage, + storage_id_out: FakeUnionStorage, + }, + Belt { + belt_id: u32, + belt_pos: BeltLenType, + self_storage: FakeUnionStorage, + self_is_source: bool, + }, } #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone)] struct InternalInserterReinsertionInfo { - pub movetime: u16, + pub movetime: NonZero, pub item: u8, pub max_hand: ITEMCOUNTTYPE, - pub(crate) index: InserterId, pub self_index: u32, - pub other: FakeUnionStorage, + + pub(crate) rest: Rest, +} + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone)] +enum Rest { + Storage { + index: InserterId, + other: FakeUnionStorage, + }, + Belt { + belt_id: u32, + belt_pos: BeltLenType, + self_is_source: bool, + }, } // FIXME: We store the same slice length n times! @@ -298,8 +329,10 @@ impl power_list: &[AssemblerInfo], ) -> (Watt, u32, u32) { // FIXME: This could technically not be enough if enough items are produced in a single tick - self.inserter_waitlist_output_vec - .reserve((self.len * 4).saturating_sub(self.inserter_waitlist_output_vec.len())); + self.inserter_waitlist_output_vec.reserve( + (self.len * 4 * (NUM_INGS + NUM_OUTPUTS)) + .saturating_sub(self.inserter_waitlist_output_vec.len()), + ); let (ing_idx, out_idx) = recipe_lookup[self.recipe.id.into()]; @@ -489,12 +522,26 @@ impl let Ok(()) = self.inserter_waitlist_output_vec.push_within_capacity( InternalInserterReinsertionInfo { - movetime: ins.movetime, + movetime: ins.movetime.into(), item: (NUM_INGS + item) as u8, max_hand: ins.max_hand.into(), - index: ins.index, self_index: final_idx as u32, - other: ins.other, + rest: match ins.rest { + InserterWithBeltsEnum::StorageStorage { + self_is_source: _, + index, + other, + } => Rest::Storage { index, other }, + InserterWithBeltsEnum::BeltStorage { + belt_id, + belt_pos, + self_is_source, + } => Rest::Belt { + belt_id, + belt_pos, + self_is_source, + }, + }, }, ) else { @@ -550,12 +597,26 @@ impl let Ok(()) = self.inserter_waitlist_output_vec.push_within_capacity( InternalInserterReinsertionInfo { - movetime: ins.movetime, + movetime: ins.movetime.into(), item: item as u8, max_hand: ins.max_hand.into(), - index: ins.index, self_index: final_idx as u32, - other: ins.other, + rest: match ins.rest { + InserterWithBeltsEnum::StorageStorage { + self_is_source: _, + index, + other, + } => Rest::Storage { index, other }, + InserterWithBeltsEnum::BeltStorage { + belt_id, + belt_pos, + self_is_source, + } => Rest::Belt { + belt_id, + belt_pos, + self_is_source, + }, + }, }, ) else { @@ -1307,33 +1368,69 @@ impl Conn::Storage { + index, + storage_id_in: if (internal.item as usize) < NUM_INGS { + // This is an ingredient inserter + other + } else { + FakeUnionStorage { + index: internal.self_index, + grid_or_static_flag: self.self_fake_union_out + [internal.item as usize - NUM_INGS] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_out + [internal.item as usize - NUM_INGS] + .recipe_idx_with_this_item, + } + }, + storage_id_out: if (internal.item as usize) < NUM_INGS { + // This is an ingredient inserter + FakeUnionStorage { + index: internal.self_index, + grid_or_static_flag: self.self_fake_union_ing + [internal.item as usize] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_ing + [internal.item as usize] + .recipe_idx_with_this_item, + } + } else { + other + }, + }, + Rest::Belt { + belt_id, + belt_pos, + self_is_source, + } => Conn::Belt { + belt_id, + belt_pos, + self_is_source, + self_storage: if (internal.item as usize) < NUM_INGS { + FakeUnionStorage { + index: internal.self_index, + grid_or_static_flag: self.self_fake_union_ing + [internal.item as usize] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_ing + [internal.item as usize] + .recipe_idx_with_this_item, + } + } else { + FakeUnionStorage { + index: internal.self_index, + grid_or_static_flag: self.self_fake_union_out + [internal.item as usize - NUM_INGS] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_out + [internal.item as usize - NUM_INGS] + .recipe_idx_with_this_item, + } + }, + }, }, }), ) @@ -1752,7 +1849,7 @@ impl( &mut self, - index: u32, + self_index: u32, item: Item, id: InserterId, data_store: &DataStore, @@ -1763,57 +1860,107 @@ impl index == id, + InserterWithBeltsEnum::BeltStorage { .. } => false, + }) .unwrap(); let ins = v.take().unwrap(); InserterReinsertionInfo { - movetime: ins.movetime, + movetime: ins.movetime.into(), item: item, current_hand: ins.current_hand, max_hand: ins.max_hand.into(), - index: ins.index, - // This is an ingredient inserter - storage_id_in: ins.other, - // This is an ingredient inserter - storage_id_out: FakeUnionStorage { - index: index, - grid_or_static_flag: self.self_fake_union_ing[item_index].grid_or_static_flag, - recipe_idx_with_this_item: self.self_fake_union_ing[item_index] - .recipe_idx_with_this_item, + + conn: match ins.rest { + InserterWithBeltsEnum::StorageStorage { index, other, .. } => Conn::Storage { + index, + // This is an ingredient inserter + storage_id_in: other, + // This is an ingredient inserter + storage_id_out: FakeUnionStorage { + index: self_index, + grid_or_static_flag: self.self_fake_union_ing[item_index] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_ing[item_index] + .recipe_idx_with_this_item, + }, + }, + InserterWithBeltsEnum::BeltStorage { + self_is_source, + belt_id, + belt_pos, + } => Conn::Belt { + self_is_source, + belt_id, + belt_pos, + self_storage: FakeUnionStorage { + index: self_index, + grid_or_static_flag: self.self_fake_union_ing[item_index] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_ing[item_index] + .recipe_idx_with_this_item, + }, + }, }, } } else { let item_index = item_index - NUM_INGS; - let v = self.waitlists_outputs[item_index][index as usize] + let v = self.waitlists_outputs[item_index][self_index as usize] .inserters .iter_mut() .filter(|v| v.is_some()) - .find(|ins| ins.as_ref().unwrap().index == id) + .find(|ins| match ins.as_ref().unwrap().rest { + InserterWithBeltsEnum::StorageStorage { index, .. } => index == id, + InserterWithBeltsEnum::BeltStorage { .. } => false, + }) .unwrap(); let ins = v.take().unwrap(); InserterReinsertionInfo { - movetime: ins.movetime, + movetime: ins.movetime.into(), item: item, current_hand: ins.current_hand, max_hand: ins.max_hand.into(), - index: ins.index, - // This is an output inserter - storage_id_in: FakeUnionStorage { - index: index, - grid_or_static_flag: self.self_fake_union_out[item_index].grid_or_static_flag, - recipe_idx_with_this_item: self.self_fake_union_out[item_index] - .recipe_idx_with_this_item, + + conn: match ins.rest { + InserterWithBeltsEnum::StorageStorage { index, other, .. } => Conn::Storage { + index, + // This is an output inserter + storage_id_in: FakeUnionStorage { + index: self_index, + grid_or_static_flag: self.self_fake_union_out[item_index] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_out[item_index] + .recipe_idx_with_this_item, + }, + // This is an output inserter + storage_id_out: other, + }, + InserterWithBeltsEnum::BeltStorage { + self_is_source, + belt_id, + belt_pos, + } => Conn::Belt { + self_is_source, + belt_id, + belt_pos, + self_storage: FakeUnionStorage { + index: self_index, + grid_or_static_flag: self.self_fake_union_ing[item_index] + .grid_or_static_flag, + recipe_idx_with_this_item: self.self_fake_union_ing[item_index] + .recipe_idx_with_this_item, + }, + }, }, - // This is an output inserter - storage_id_out: ins.other, } } } diff --git a/src/belt/mod.rs b/src/belt/mod.rs index fcfe7de..c8ffe24 100644 --- a/src/belt/mod.rs +++ b/src/belt/mod.rs @@ -17,6 +17,7 @@ use crate::get_size::{Mutex, StableGraph}; use crate::item::ITEMCOUNTTYPE; use crate::par_generation::BeltKind; use petgraph::stable_graph::DefaultIx; +use std::num::NonZero; use std::ops::RangeInclusive; use std::{ cell::UnsafeCell, @@ -3185,7 +3186,7 @@ impl BeltStore { &mut self, id: BeltTileId, pos: BeltLenType, - new_movetime: u16, + new_movetime: NonZero, ) { match id { BeltTileId::AnyBelt(index, _) => match &mut self.any_belts[index as usize] { diff --git a/src/belt/smart.rs b/src/belt/smart.rs index 83a9a6e..7450f9f 100644 --- a/src/belt/smart.rs +++ b/src/belt/smart.rs @@ -1,18 +1,21 @@ use std::{ iter::repeat, + num::NonZero, ops::{Deref, DerefMut}, sync::atomic::AtomicUsize, u8, }; -use crate::item::Indexable; +use crate::inserter::{ + belt_storage_inserter_non_const_gen::DynInserterState, + belt_storage_movement_list::{BeltStorageInserterInMovement, ReinsertionLists}, +}; use crate::{ inserter::{ InserterState, belt_storage_inserter::Dir, belt_storage_inserter_non_const_gen::BeltStorageInserterDyn, }, item::{ITEMCOUNTTYPE, IdxTrait, Item, WeakIdxTrait}, - storage_list::SingleItemStorages, }; use bitvec::{ access::BitSafeUsize, @@ -50,7 +53,8 @@ pub static NUM_BELT_LOCS_SEARCHED: AtomicUsize = AtomicUsize::new(0); #[allow(clippy::module_name_repetitions)] #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -#[repr(align(64))] +// FIXME: Make sure a smart belt fits in a cacheline +// #[repr(align(64))] pub struct SmartBelt { pub(super) ty: u8, @@ -59,7 +63,7 @@ pub struct SmartBelt { /// Important, zero_index must ALWAYS be used using mod len pub(super) zero_index: BeltLenType, pub(super) locs: crate::get_size::BitBox, - pub(super) inserters: InserterStoreDyn, + pub inserters: InserterStoreDyn, pub(super) item: Item, @@ -69,7 +73,8 @@ pub struct SmartBelt { pub(super) output_splitter: Option<(SplitterID, SplitterSide)>, } -const_assert! {std::mem::size_of::>() <= 64} +// FIXME: +// const_assert! {std::mem::size_of::>() <= 64} #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)] @@ -84,7 +89,16 @@ pub struct EmptyBelt { pub len: u16, } -type TEST = (BeltStorageInserterDyn, u8, ITEMCOUNTTYPE); +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct InserterExtractedWhenMoving { + pub(crate) storage: FakeUnionStorage, + pub(crate) belt_pos: BeltLenType, + pub(crate) movetime: NonZero, + pub(crate) outgoing: bool, + pub(crate) max_hand_size: ITEMCOUNTTYPE, + pub(crate) current_hand: ITEMCOUNTTYPE, +} // TODO: Idea: // Have Belt inserters only be in belts as waitlist. @@ -100,7 +114,7 @@ type TEST = (BeltStorageInserterDyn, u8, ITEMCOUNTTYPE); #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct InserterStoreDyn { - pub(super) inserters: Box<[(BeltStorageInserterDyn, u8, ITEMCOUNTTYPE)]>, + pub inserters: Vec, } #[derive(Debug)] @@ -130,9 +144,7 @@ impl SmartBelt { first_free_index: FreeIndex::FreeIndex(0), zero_index: 0, locs: bitbox![0; len.into()].into(), - inserters: InserterStoreDyn { - inserters: vec![].into_boxed_slice(), - }, + inserters: InserterStoreDyn { inserters: vec![] }, item, @@ -192,7 +204,22 @@ impl SmartBelt { inserters: SushiInserterStoreDyn { inserters: inserters .into_iter() - .map(|(inserter, movetime, hand_size)| (inserter, item, movetime, hand_size)) + .map(|ins| { + ( + BeltStorageInserterDyn { + belt_pos: ins.belt_pos, + storage_id: ins.storage, + state: if ins.outgoing { + DynInserterState::BSWaitingForSourceItems(ins.current_hand) + } else { + DynInserterState::SBWaitingForSourceItems(ins.current_hand) + }, + }, + item, + ins.movetime, + ins.max_hand_size, + ) + }) .collect(), }, @@ -323,8 +350,8 @@ impl SmartBelt { pub fn change_inserter_storage_id(&mut self, old: FakeUnionStorage, new: FakeUnionStorage) { for inserter in &mut self.inserters.inserters { - if inserter.0.storage_id == old { - inserter.0.storage_id = new; + if inserter.storage == old { + inserter.storage = new; } } } @@ -333,11 +360,10 @@ impl SmartBelt { let mut pos = 0; for inserter in self.inserters.inserters.iter_mut() { - pos = inserter.0.belt_pos; + pos = inserter.belt_pos; if pos == belt_pos { - inserter.0.storage_id = new; + inserter.storage = new; return; - } else if pos > belt_pos { } } unreachable!( @@ -351,15 +377,18 @@ impl SmartBelt { let mut pos = 0; for inserter in self.inserters.inserters.iter() { - pos = inserter.0.belt_pos; + pos = inserter.belt_pos; if pos == belt_pos { - let (dir, state) = inserter.0.state.into(); return Some(BeltInserterInfo { - outgoing: dir == Dir::BeltToStorage, - state, - connection: inserter.0.storage_id, - movetime: inserter.1, - hand_size: inserter.2, + outgoing: inserter.outgoing, + state: if inserter.outgoing { + InserterState::WaitingForSourceItems(inserter.current_hand) + } else { + InserterState::WaitingForSpaceInDestination(inserter.current_hand) + }, + connection: inserter.storage, + movetime: inserter.movetime.into(), + hand_size: inserter.max_hand_size, }); } } @@ -367,18 +396,26 @@ impl SmartBelt { None } - pub(super) fn change_inserter_movetime(&mut self, belt_pos: BeltLenType, new_movetime: u16) { + pub(super) fn change_inserter_movetime( + &mut self, + belt_pos: BeltLenType, + new_movetime: NonZero, + ) { let Some(inserter) = self .inserters .inserters .iter_mut() - .find(|ins| ins.0.belt_pos == belt_pos) + .find(|ins| ins.belt_pos == belt_pos) else { unreachable!( "The belt did not have an inserter at position specified to change movetime" ) }; - inserter.1 = new_movetime.try_into().unwrap_or(u8::MAX); + inserter.movetime = u16::from(new_movetime) + .try_into() + .unwrap_or(u8::MAX) + .try_into() + .unwrap(); } pub fn remove_inserter(&mut self, pos: BeltLenType) -> Result { @@ -390,17 +427,12 @@ impl SmartBelt { .inserters .inserters .iter() - .position(|ins| ins.0.belt_pos == pos) + .position(|ins| ins.belt_pos == pos) { Some(idx) => { - let mut removed = None; - take_mut::take(&mut self.inserters.inserters, |ins| { - let mut ins = ins.into_vec(); - removed = Some(ins.remove(idx)); - ins.into_boxed_slice() - }); + let removed = self.inserters.inserters.remove(idx); - Ok(removed.unwrap().0.storage_id) + Ok(removed.storage) }, None => Err(()), } @@ -430,14 +462,13 @@ impl SmartBelt { return Err(InserterAdditionError::ItemMismatch); } - take_mut::take(&mut self.inserters.inserters, |ins| { - let mut ins = ins.into_vec(); - ins.push(( - BeltStorageInserterDyn::new(Dir::BeltToStorage, index, storage_id), - movetime.try_into().unwrap_or(u8::MAX), - hand_size, - )); - ins.into_boxed_slice() + self.inserters.inserters.push(InserterExtractedWhenMoving { + storage: storage_id, + belt_pos: index, + movetime: movetime.try_into().unwrap_or(u8::MAX).try_into().unwrap(), + outgoing: true, + max_hand_size: hand_size, + current_hand: 0, }); Ok(()) @@ -466,14 +497,13 @@ impl SmartBelt { if filter != self.item { return Err(InserterAdditionError::ItemMismatch); } - take_mut::take(&mut self.inserters.inserters, |ins| { - let mut ins = ins.into_vec(); - ins.push(( - BeltStorageInserterDyn::new(Dir::StorageToBelt, index, storage_id), - movetime.try_into().unwrap_or(u8::MAX), - hand_size, - )); - ins.into_boxed_slice() + self.inserters.inserters.push(InserterExtractedWhenMoving { + storage: storage_id, + belt_pos: index, + movetime: movetime.try_into().unwrap_or(u8::MAX).try_into().unwrap(), + outgoing: false, + max_hand_size: hand_size, + current_hand: 0, }); Ok(()) @@ -508,8 +538,17 @@ impl SmartBelt { pub fn update_inserters<'a, 'b>( &mut self, - storages: SingleItemStorages<'a, 'b>, - grid_size: usize, + self_index: u32, + reinsertion_outgoing: &mut ReinsertionLists< + '_, + { Dir::BeltToStorage }, + { Dir::BeltToStorage }, + >, + reinsertion_incoming: &mut ReinsertionLists< + '_, + { Dir::BeltToStorage }, + { Dir::StorageToBelt }, + >, ) { if self.get_len() == 0 { return; @@ -520,41 +559,14 @@ impl SmartBelt { FreeIndex::OldFreeIndex(idx) => idx, }; - // for ins in self.inserters.inserters.iter_mut() { - // i += usize::from(ins.offset); - // let idx = (i + usize::from(self.zero_index)) % self.locs.len(); - // let loc = self.locs.get_mut(idx); - - // match loc { - // Some(mut loc) => { - // let changed = - // ins.update(loc.as_mut(), storages, MOVETIME, HAND_SIZE, grid_size); - - // if changed { - // // the inserter changed something. - // if !*loc && i < usize::from(first_possible_free_pos) { - // // This is the new first free pos. - // first_possible_free_pos = BeltLenType::try_from(i).unwrap(); - // self.first_free_index = - // FreeIndex::FreeIndex(BeltLenType::try_from(i).unwrap()); - // } else if *loc && i == usize::from(first_possible_free_pos) { - // // This was the old first free pos - // self.first_free_index = - // FreeIndex::OldFreeIndex(BeltLenType::try_from(i).unwrap()); - // } - // } - // }, - // None => unreachable!( - // "Adding the offsets of the inserters is bigger than the length of the belt." - // ), - // } - - // i += 1; - // } - let mut new_first_free = old_first_free; - for (ins, movetime, hand_size) in self.inserters.inserters.iter_mut() { + let extracted = self.inserters.inserters.extract_if(.., |ins| { + // FIXME: This should not be needed, if we did not incorrectly insert inserters always in the belt + if ins.current_hand == 0 && !ins.outgoing { + return true; + } + // Taken from VecDeque::wrap_index let logical_index = usize::from(self.zero_index) + usize::from(ins.belt_pos); let loc_idx = if logical_index >= self.locs.len() { @@ -566,49 +578,63 @@ impl SmartBelt { if ins.belt_pos < old_first_free { // We KNOW this position is filled debug_assert!(self.locs[loc_idx]); - let mut loc = true; - let _changed = ins.update( - self.item.into_usize(), - &mut loc, - storages, - *movetime, - *hand_size, - grid_size, - ); - - if !loc { + if ins.outgoing { + ins.current_hand += 1; self.locs.set(loc_idx, false); if ins.belt_pos <= new_first_free { self.first_free_index = FreeIndex::FreeIndex(ins.belt_pos); new_first_free = ins.belt_pos; } + + if ins.current_hand == ins.max_hand_size { + true + } else { + false + } + } else { + false } } else { let mut loc = self.locs.get_mut(loc_idx).unwrap(); - let changed = ins.update( - self.item.into_usize(), - loc.as_mut(), - storages, - *movetime, - *hand_size, - grid_size, - ); + if ins.outgoing && *loc { + *loc = false; + ins.current_hand += 1; + if ins.current_hand == ins.max_hand_size { + true + } else { + false + } + } else if !ins.outgoing && !*loc { + *loc = true; + ins.current_hand -= 1; - if changed { - // the inserter changed something. if ins.belt_pos == new_first_free && *loc { // This was the old first free pos self.first_free_index = FreeIndex::OldFreeIndex(ins.belt_pos); } - // if !first_free_changed && i == old_first_free && !*loc { - // // This is the new first_free_pos - // self.first_free_index = - // FreeIndex::FreeIndex(BeltLenType::try_from(i).unwrap()); - // first_free_changed = true; - // } + + if ins.current_hand == 0 { true } else { false } + } else { + false } } + }); + + for ins in extracted { + let in_movement = BeltStorageInserterInMovement { + movetime: ins.movetime, + storage: ins.storage, + belt: self_index, + belt_pos: ins.belt_pos, + max_hand_size: ins.max_hand_size, + current_hand: ins.current_hand, + }; + if ins.outgoing { + reinsertion_outgoing.reinsert(ins.movetime.into(), in_movement); + } else { + reinsertion_incoming.reinsert(ins.movetime.into(), in_movement); + } } } @@ -668,15 +694,11 @@ impl SmartBelt { .inserters .inserters .iter() - .map(|ins| match ins.0.state.into() { - (Dir::StorageToBelt, InserterState::WaitingForSpaceInDestination(_)) => { - (ins.0.belt_pos, true) - }, - (Dir::BeltToStorage, InserterState::WaitingForSourceItems(_)) => { - (ins.0.belt_pos, true) - }, + .map(|ins| match (ins.outgoing,) { + (false,) => (ins.belt_pos, true), + (true,) => (ins.belt_pos, true), - _ => (ins.0.belt_pos, false), + _ => (ins.belt_pos, false), }) .fold(0usize, |old_count, (belt_pos, needs)| { // let our_idx = belt_pos; @@ -691,13 +713,9 @@ impl SmartBelt { .inserters .inserters .iter() - .filter_map(|ins| match ins.0.state.into() { - (Dir::StorageToBelt, InserterState::WaitingForSpaceInDestination(_)) => { - Some(ins.0.storage_id) - }, - (Dir::BeltToStorage, InserterState::WaitingForSourceItems(_)) => { - Some(ins.0.storage_id) - }, + .filter_map(|ins| match (ins.outgoing,) { + (false,) => Some(ins.storage), + (true,) => Some(ins.storage), _ => None, }) @@ -930,20 +948,15 @@ impl SmartBelt { let _num_back_inserters = back_inserters.inserters.len(); let mut new_inserters = front_inserters; - take_mut::take(&mut new_inserters.inserters, |ins| { - let mut ins = ins.into_vec(); - let mut other = vec![].into_boxed_slice(); - mem::swap(&mut other, &mut back_inserters.inserters); - ins.extend(other.into_vec().drain(..).map(|mut back_ins| { - back_ins.0.belt_pos = back_ins - .0 + new_inserters + .inserters + .extend(back_inserters.inserters.drain(..).map(|mut back_ins| { + back_ins.belt_pos = back_ins .belt_pos .checked_add(front_len.try_into().unwrap()) .unwrap(); back_ins })); - ins.into_boxed_slice() - }); let mut front_locs_vec = BitBox::from(front_locs).into_bitvec(); @@ -1037,8 +1050,7 @@ impl SmartBelt { if side == Side::FRONT { for ins in &mut inserters.inserters { - ins.0.belt_pos = ins - .0 + ins.belt_pos = ins .belt_pos .checked_add(len) .expect("Max length of belt (u16::MAX) reached"); @@ -1217,9 +1229,7 @@ impl EmptyBelt { first_free_index: FreeIndex::FreeIndex(0), zero_index: 0, locs: bitbox![0; self.len as usize].into(), - inserters: InserterStoreDyn { - inserters: vec![].into_boxed_slice(), - }, + inserters: InserterStoreDyn { inserters: vec![] }, item, last_moving_spot: 0, input_splitter: self.input_splitter, @@ -1405,7 +1415,7 @@ impl Belt for SmartBelt { .inserters .inserters .iter() - .map(|ins| ins.0.belt_pos) + .map(|ins| ins.belt_pos) .collect_vec(); assert!(!self.is_circular); @@ -1428,23 +1438,17 @@ impl Belt for SmartBelt { locs.into_boxed_bitslice().into() }); - take_mut::take(&mut self.inserters.inserters, |inserters| { - let mut inserters = inserters.into_vec(); - - inserters.retain(|inserter| { - if !kept_range.contains(&inserter.0.belt_pos) { - false - } else { - true - } - }); - - inserters.into_boxed_slice() + self.inserters.inserters.retain(|inserter| { + if !kept_range.contains(&inserter.belt_pos) { + false + } else { + true + } }); if side == Side::FRONT { for ins in &mut self.inserters.inserters { - ins.0.belt_pos = ins.0.belt_pos.checked_sub(amount).unwrap(); + ins.belt_pos = ins.belt_pos.checked_sub(amount).unwrap(); } } @@ -1454,7 +1458,7 @@ impl Belt for SmartBelt { .inserters .inserters .iter() - .map(|ins| ins.0.belt_pos) + .map(|ins| ins.belt_pos) .collect_vec(); match side { diff --git a/src/belt/sushi.rs b/src/belt/sushi.rs index 90a7fb9..16041ff 100644 --- a/src/belt/sushi.rs +++ b/src/belt/sushi.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::num::NonZero; use std::{iter::repeat, mem}; use itertools::Itertools; @@ -8,6 +9,7 @@ use egui_show_info_derive::ShowInfo; #[cfg(feature = "client")] use get_size2::GetSize; +use crate::belt::smart::InserterExtractedWhenMoving; use crate::inserter::belt_storage_inserter::Dir; use crate::item::ITEMCOUNTTYPE; use crate::{ @@ -47,7 +49,14 @@ 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, u8, ITEMCOUNTTYPE)]>, + pub(super) inserters: Box< + [( + BeltStorageInserterDyn, + Item, + NonZero, + ITEMCOUNTTYPE, + )], + >, } #[derive(Debug, PartialEq, Eq)] @@ -95,7 +104,7 @@ impl SushiBelt { ins.push(( BeltStorageInserterDyn::new(Dir::StorageToBelt, pos, storage_id), filter, - movetime.try_into().unwrap_or(u8::MAX), + movetime.try_into().unwrap_or(u8::MAX).try_into().unwrap(), hand_size, )); ins.into_boxed_slice() @@ -123,7 +132,7 @@ impl SushiBelt { ins.push(( BeltStorageInserterDyn::new(Dir::BeltToStorage, pos, storage_id), filter, - movetime.try_into().unwrap_or(u8::MAX), + movetime.try_into().unwrap_or(u8::MAX).try_into().unwrap(), hand_size, )); ins.into_boxed_slice() @@ -145,7 +154,7 @@ impl SushiBelt { state, connection: ins.0.storage_id, hand_size: ins.3, - movetime: ins.2, + movetime: ins.2.into(), } }) } @@ -308,7 +317,20 @@ impl SushiBelt { // if item != inserter_item { // error!("We need to handle inserters which will never work again in smart belts!!!!!!!"); // } - (ins, movetime, hand_size) + let (dir, state) = ins.state.into(); + InserterExtractedWhenMoving { + storage: ins.storage_id, + belt_pos: ins.belt_pos, + movetime, + outgoing: dir == Dir::BeltToStorage, + max_hand_size: hand_size, + current_hand: match state { + crate::inserter::InserterState::WaitingForSourceItems(hand) => hand, + crate::inserter::InserterState::WaitingForSpaceInDestination(hand) => hand, + crate::inserter::InserterState::FullAndMovingOut(_) => hand_size, + crate::inserter::InserterState::EmptyAndMovingBack(_) => 0, + }, + } }).collect(), }, item, diff --git a/src/chest.rs b/src/chest.rs index 2aa4b3b..c4b74e3 100644 --- a/src/chest.rs +++ b/src/chest.rs @@ -6,7 +6,9 @@ use rayon::iter::{IndexedParallelIterator, IntoParallelIterator}; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; use static_assertions::const_assert; -use crate::assembler::simd::{Inserter, InserterReinsertionInfo, InserterWaitList}; +use crate::assembler::simd::{ + Inserter, InserterReinsertionInfo, InserterWaitList, InserterWithBelts, +}; use crate::inserter::storage_storage_with_buckets_indirect::InserterId; use crate::inserter::{FakeUnionStorage, StaticID}; use crate::storage_list::{InserterWaitLists, MaxInsertionLimit}; @@ -53,8 +55,7 @@ pub type SignedChestSize = i32; #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone)] struct InternalInserterReinsertionInfo { - inserter: Inserter, - self_is_source: bool, + inserter: InserterWithBelts, self_index: u32, } @@ -85,31 +86,53 @@ impl FullChestStore { store.update_simd().map( move |InternalInserterReinsertionInfo { inserter, - self_is_source, self_index, }| InserterReinsertionInfo { - movetime: inserter.movetime, + movetime: inserter.movetime.into(), item, current_hand: inserter.current_hand, max_hand: inserter.max_hand.into(), - index: inserter.index, - storage_id_in: if self_is_source { - FakeUnionStorage { - index: self_index, - grid_or_static_flag: 0, - recipe_idx_with_this_item: StaticID::Chest as u16, - } - } else { - inserter.other - }, - storage_id_out: if self_is_source { - inserter.other - } else { - FakeUnionStorage { - index: self_index, - grid_or_static_flag: 0, - recipe_idx_with_this_item: StaticID::Chest as u16, - } + + conn: match inserter.rest { + crate::assembler::simd::InserterWithBeltsEnum::StorageStorage { + self_is_source, + index, + other, + } => crate::assembler::simd::Conn::Storage { + index, + storage_id_in: if self_is_source { + FakeUnionStorage { + index: self_index, + grid_or_static_flag: 0, + recipe_idx_with_this_item: StaticID::Chest as u16, + } + } else { + other + }, + storage_id_out: if self_is_source { + other + } else { + FakeUnionStorage { + index: self_index, + grid_or_static_flag: 0, + recipe_idx_with_this_item: StaticID::Chest as u16, + } + }, + }, + crate::assembler::simd::InserterWithBeltsEnum::BeltStorage { + belt_id, + belt_pos, + self_is_source, + } => crate::assembler::simd::Conn::Belt { + belt_id, + belt_pos, + self_is_source, + self_storage: FakeUnionStorage { + index: self_index, + grid_or_static_flag: 0, + recipe_idx_with_this_item: StaticID::Chest as u16, + }, + }, }, }, ) @@ -313,7 +336,18 @@ impl MultiChestStore { if (was_full && !is_full) || (was_empty && !is_empty) { for ins in wait_list.inserters.iter_mut() { if let Some(inserter) = ins { - if inserter.self_is_source && !is_empty { + let self_is_source = match inserter.rest { + crate::assembler::simd::InserterWithBeltsEnum::StorageStorage { + self_is_source, + .. + } => self_is_source, + crate::assembler::simd::InserterWithBeltsEnum::BeltStorage { + self_is_source, + .. + } => self_is_source, + }; + + if self_is_source && !is_empty { let taken_by_this_inserter = min( ITEMCOUNTTYPE::from(inserter.max_hand) - inserter.current_hand, *inout, @@ -321,26 +355,23 @@ impl MultiChestStore { *inout -= taken_by_this_inserter; inserter.current_hand += taken_by_this_inserter; - } else if !inserter.self_is_source && !is_full { - let taken_by_this_inserter = min( - inserter.current_hand, - *max_insert - *inout, - ); + } else if !self_is_source && !is_full { + let taken_by_this_inserter = + min(inserter.current_hand, *max_insert - *inout); *inout += taken_by_this_inserter; inserter.current_hand -= taken_by_this_inserter; } if (inserter.current_hand == ITEMCOUNTTYPE::from(inserter.max_hand) - && inserter.self_is_source) - || (inserter.current_hand == 0 && !inserter.self_is_source) + && self_is_source) + || (inserter.current_hand == 0 && !self_is_source) { let removed = ins.take().unwrap(); - let is_source = removed.self_is_source; + let is_source = self_is_source; self.inserter_reinsertion_vec .push_within_capacity(InternalInserterReinsertionInfo { inserter: removed, - self_is_source: is_source, self_index: index as u32, }) .unwrap(); diff --git a/src/data/mod.rs b/src/data/mod.rs index 6d48f9f..6c6eeb5 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -25,6 +25,7 @@ use crate::{ item::{ITEMCOUNTTYPE, IdxTrait, Item, Recipe, WeakIdxTrait}, power::{Joule, Watt}, }; +use std::num::NonZero; type ItemString = String; type RecipeString = String; @@ -162,7 +163,7 @@ struct RawFluidFlowthrough { struct RawInserter { name: String, display_name: String, - time_per_trip: TIMERTYPE, + time_per_trip: NonZero, handsize: ITEMCOUNTTYPE, tile_size: [u8; 2], @@ -476,7 +477,7 @@ pub struct InserterInfo { pub display_name: String, pub size: [u8; 2], - pub swing_time_ticks: u16, + pub swing_time_ticks: NonZero, /// pre any increases by technology pub base_hand_size: ITEMCOUNTTYPE, diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index 28791d6..8acbb38 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -813,7 +813,7 @@ fn instantiate_mining_drill_internal_inserter {}, Err(side) => match side { crate::inserter::WaitlistSearchSide::Source => { @@ -2596,9 +2596,8 @@ impl World World, - movetime: u16, + movetime: NonZero, hand_size: crate::item::ITEMCOUNTTYPE, known_filter: Option>, @@ -3206,7 +3205,7 @@ impl World, filter: Item, - movetime: u16, + movetime: NonZero, hand_size: crate::item::ITEMCOUNTTYPE, start: InserterConnection, @@ -3322,7 +3321,7 @@ impl World, + pub(crate) storage: FakeUnionStorage, + pub(crate) belt: u32, + pub(crate) belt_pos: BeltLenType, + + pub(crate) current_hand: ITEMCOUNTTYPE, + pub(crate) max_hand_size: ITEMCOUNTTYPE, +} + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct List { + zero_index: usize, + lists: Box<[Vec]>, +} + +impl Default for List { + fn default() -> Self { + Self { + zero_index: 0, + lists: vec![vec![]; u8::MAX as usize + 2].into_boxed_slice(), + } + } +} + +pub struct FinishedMovingLists<'a, const SWING_DIR: Dir, const ITEM_FLOW_DIR: Dir> { + list: &'a mut Vec, +} + +pub struct ReinsertionLists<'a, const SWING_DIR: Dir, const ITEM_FLOW_DIR: Dir> { + first: &'a mut [Vec], + second: &'a mut [Vec], +} + +impl<'a, const SWING_DIR: Dir, const ITEM_FLOW_DIR: Dir> + ReinsertionLists<'a, SWING_DIR, ITEM_FLOW_DIR> +{ + pub fn reinsert(&mut self, movetime: NonZero, ins: BeltStorageInserterInMovement) { + // TODO: Maybe it is worth it to rotate the lists to avoid this branch? + if (u16::from(movetime) as usize) < self.first.len() { + self.first + .get_mut(u16::from(movetime) as usize) + .expect(&format!("movetime: {:?}", movetime)) + .push(ins); + } else { + self.second + .get_mut((u16::from(movetime) as usize) - self.first.len()) + .expect(&format!("movetime: {:?}", movetime)) + .push(ins); + } + } +} + +impl List { + pub fn tick( + &mut self, + ) -> ( + FinishedMovingLists<'_, SWING_DIR, ITEM_FLOW_DIR>, + ReinsertionLists<'_, SWING_DIR, ITEM_FLOW_DIR>, + ) { + self.zero_index += 1; + self.zero_index %= self.lists.len(); + let (second, rest) = self.lists.split_at_mut(self.zero_index); + + let ([finished], first) = rest.split_at_mut(1) else { + unreachable!() + }; + + ( + FinishedMovingLists { list: finished }, + ReinsertionLists { first, second }, + ) + } + + pub fn reinsert(&mut self, movetime: NonZero, ins: BeltStorageInserterInMovement) { + let index = (usize::from(u16::from(movetime)) + self.zero_index) % self.lists.len(); + self.lists[index].push(ins); + } +} + +impl<'a> FinishedMovingLists<'a, { Dir::BeltToStorage }, { Dir::BeltToStorage }> { + pub fn update( + self, + item_id: usize, + grid_size: usize, + reinsertion_list: &mut ReinsertionLists<'_, { Dir::StorageToBelt }, { Dir::BeltToStorage }>, + storages: SingleItemStorages, + ) { + self.list.retain_mut(|inserter| { + let (max_insert, data, wait_list) = + index_fake_union(item_id, storages, inserter.storage, grid_size); + + let items_moved = min(inserter.current_hand, *max_insert - *data); + + inserter.current_hand -= items_moved; + *data += items_moved; + + if inserter.current_hand == 0 { + reinsertion_list.reinsert(inserter.movetime.into(), *inserter); + false + } else { + if let Some(wait_list) = wait_list { + if let Some(empty) = wait_list.inserters.iter_mut().find(|v| v.is_none()) { + *empty = Some(InserterWithBelts { + current_hand: inserter.current_hand, + max_hand: inserter.max_hand_size.into(), + rest: InserterWithBeltsEnum::BeltStorage { + self_is_source: false, + belt_id: inserter.belt, + belt_pos: inserter.belt_pos, + }, + movetime: inserter.movetime.into(), + }); + false + } else { + true + } + } else { + true + } + } + }); + } +} + +impl<'a> FinishedMovingLists<'a, { Dir::BeltToStorage }, { Dir::StorageToBelt }> { + pub fn update( + self, + item_id: usize, + grid_size: usize, + reinsertion_list: &mut ReinsertionLists<'_, { Dir::StorageToBelt }, { Dir::StorageToBelt }>, + storages: SingleItemStorages, + ) { + self.list.retain_mut(|inserter| { + let (max_insert, data, wait_list) = + index_fake_union(item_id, storages, inserter.storage, grid_size); + + let items_moved = min(inserter.max_hand_size - inserter.current_hand, *data); + + inserter.current_hand += items_moved; + *data -= items_moved; + + if inserter.current_hand == inserter.max_hand_size { + reinsertion_list.reinsert(inserter.movetime.into(), *inserter); + false + } else { + if let Some(wait_list) = wait_list { + if let Some(empty) = wait_list.inserters.iter_mut().find(|v| v.is_none()) { + *empty = Some(InserterWithBelts { + current_hand: inserter.current_hand, + max_hand: inserter.max_hand_size.into(), + rest: InserterWithBeltsEnum::BeltStorage { + self_is_source: true, + belt_id: inserter.belt, + belt_pos: inserter.belt_pos, + }, + movetime: inserter.movetime.into(), + }); + false + } else { + true + } + } else { + true + } + } + }); + } +} + +impl<'a> FinishedMovingLists<'a, { Dir::StorageToBelt }, { Dir::StorageToBelt }> { + pub fn update( + self, + reinsertion_list: &mut ReinsertionLists<'_, { Dir::BeltToStorage }, { Dir::StorageToBelt }>, + belts: &mut [SmartBelt], + ) { + self.list.retain(|inserter| { + let belt = &mut belts[inserter.belt as usize]; + + let mut current_hand = inserter.max_hand_size; + + if belt.try_insert_correct_item(inserter.belt_pos).is_ok() { + current_hand -= 1; + } + + if current_hand == 0 { + reinsertion_list.reinsert(inserter.movetime.into(), *inserter); + false + } else { + belt.inserters.inserters.push(InserterExtractedWhenMoving { + storage: inserter.storage, + belt_pos: inserter.belt_pos, + movetime: inserter.movetime, + outgoing: false, + max_hand_size: inserter.max_hand_size, + current_hand, + }); + false + } + }); + } +} + +impl<'a> FinishedMovingLists<'a, { Dir::StorageToBelt }, { Dir::BeltToStorage }> { + pub fn update( + self, + reinsertion_list: &mut ReinsertionLists<'_, { Dir::BeltToStorage }, { Dir::BeltToStorage }>, + belts: &mut [SmartBelt], + ) { + self.list.retain(|inserter| { + let belt = &mut belts[inserter.belt as usize]; + + let mut current_hand = 0; + + if belt.remove_item(inserter.belt_pos).is_some() { + current_hand += 1; + } + + if current_hand == inserter.max_hand_size { + reinsertion_list.reinsert(inserter.movetime.into(), *inserter); + false + } else { + belt.inserters.inserters.push(InserterExtractedWhenMoving { + storage: inserter.storage, + belt_pos: inserter.belt_pos, + movetime: inserter.movetime, + outgoing: true, + max_hand_size: inserter.max_hand_size, + current_hand, + }); + false + } + }); + } +} diff --git a/src/inserter/mod.rs b/src/inserter/mod.rs index ff06cfa..a6f8fdd 100644 --- a/src/inserter/mod.rs +++ b/src/inserter/mod.rs @@ -29,6 +29,7 @@ pub mod belt_storage_pure_buckets; pub mod storage_storage_inserter; pub mod storage_storage_with_buckets; // pub mod storage_storage_with_buckets_compressed; +pub mod belt_storage_movement_list; pub mod storage_storage_with_buckets_indirect; // mod bucket_bit_compressed; diff --git a/src/inserter/storage_storage_with_buckets_indirect.rs b/src/inserter/storage_storage_with_buckets_indirect.rs index 7536238..1d5efe6 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -5,7 +5,9 @@ use super::{ FakeUnionStorage, InserterStateInfo, storage_storage_with_buckets::LargeInserterState, }; use crate::{ - assembler::simd::{Inserter as WaitListInserter, InserterReinsertionInfo, InserterWaitList}, + assembler::simd::{ + InserterReinsertionInfo, InserterWaitList, InserterWithBelts as WaitListInserter, + }, inserter::WaitlistSearchSide, item::ITEMCOUNTTYPE, join_many::join, @@ -14,6 +16,8 @@ use crate::{ }; use std::cmp::min; +use std::num::NonZero; + #[cfg(feature = "client")] use egui_show_info_derive::ShowInfo; #[cfg(feature = "client")] @@ -81,7 +85,7 @@ pub struct InserterBucketData { #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct BucketedStorageStorageInserterStore { - pub movetime: u16, + pub movetime: NonZero, holes: Vec, inserters: Vec, @@ -104,16 +108,17 @@ struct UpdateResult { } impl BucketedStorageStorageInserterStore { - pub fn new(movetime: u16) -> Self { + pub fn new(movetime: NonZero) -> Self { Self { movetime, holes: vec![], inserters: vec![], waiting_for_item: vec![], - full_and_moving_out: vec![vec![]; movetime as usize + 1].into_boxed_slice(), + full_and_moving_out: vec![vec![]; u16::from(movetime) as usize + 1].into_boxed_slice(), waiting_for_space_in_destination: vec![], - empty_and_moving_back: vec![vec![]; movetime as usize + 1].into_boxed_slice(), + empty_and_moving_back: vec![vec![]; u16::from(movetime) as usize + 1] + .into_boxed_slice(), current_tick: 0, } } @@ -154,7 +159,7 @@ impl BucketedStorageStorageInserterStore { } fn list_len(&self) -> usize { - self.movetime as usize + 1 + u16::from(self.movetime) as usize + 1 } pub fn remove_inserter( @@ -309,7 +314,7 @@ impl BucketedStorageStorageInserterStore { storages: SingleItemStorages, grid_size: usize, current_tick: u32, - movetime: u16, + movetime: std::num::NonZero, ) -> UpdateResult { let storage_id = bucket_data.storage_id_in; @@ -341,12 +346,15 @@ impl BucketedStorageStorageInserterStore { if let Some(wait_list) = wait_list { if let Some(pos) = wait_list.inserters.iter_mut().find(|slot| slot.is_none()) { *pos = Some(WaitListInserter { - self_is_source: true, current_hand: bucket_data.current_hand, max_hand: bucket_data.max_hand_size.try_into().unwrap(), movetime: movetime, - index: bucket_data.index, - other: bucket_data.storage_id_out, + + rest: crate::assembler::simd::InserterWithBeltsEnum::StorageStorage { + self_is_source: true, + index: bucket_data.index, + other: bucket_data.storage_id_out, + }, }); UpdateResult { @@ -376,7 +384,7 @@ impl BucketedStorageStorageInserterStore { storages: SingleItemStorages, grid_size: usize, current_tick: u32, - movetime: u16, + movetime: std::num::NonZero, ) -> UpdateResult { let storage_id = bucket_data.storage_id_out; @@ -407,12 +415,15 @@ impl BucketedStorageStorageInserterStore { if let Some(wait_list) = wait_list { if let Some(pos) = wait_list.inserters.iter_mut().find(|slot| slot.is_none()) { *pos = Some(WaitListInserter { - self_is_source: false, current_hand: bucket_data.current_hand, max_hand: bucket_data.max_hand_size.try_into().unwrap(), movetime: movetime, - index: bucket_data.index, - other: bucket_data.storage_id_in, + + rest: crate::assembler::simd::InserterWithBeltsEnum::StorageStorage { + self_is_source: false, + index: bucket_data.index, + other: bucket_data.storage_id_in, + }, }); UpdateResult { @@ -558,7 +569,8 @@ impl BucketedStorageStorageInserterStore { .extract }); - self.full_and_moving_out[(self.current_tick + usize::from(self.movetime)) % len] + self.full_and_moving_out + [(self.current_tick + usize::from(u16::from(self.movetime))) % len] .extend(now_moving.filter(|ins| ins.current_hand == ins.max_hand_size)); } @@ -633,7 +645,8 @@ impl BucketedStorageStorageInserterStore { .extract }); - self.empty_and_moving_back[(self.current_tick + usize::from(self.movetime)) % len] + self.empty_and_moving_back + [(self.current_tick + usize::from(u16::from(self.movetime))) % len] .extend(now_moving_back.filter(|ins| ins.current_hand == 0)); } @@ -761,25 +774,27 @@ impl BucketedStorageStorageInserterStore { first_tick_value_with_this_lower_part(ins.last_update_time, current_tick), ); - u16::try_from(u32::from(self.movetime).strict_sub(time_passed)).expect( - &format!( + u16::try_from(u32::from(u16::from(self.movetime)).strict_sub(time_passed)) + .expect(&format!( "Inserter has been moving for more than u16::MAX ticks: {}", - u32::from(self.movetime).strict_sub(current_tick.strict_sub( - first_tick_value_with_this_lower_part( + u32::from(u16::from(self.movetime)).strict_sub( + current_tick.strict_sub(first_tick_value_with_this_lower_part( ins.last_update_time, current_tick - ) - )) - ), - ) + )) + ) + )) }), ImplicitState::EmptyAndMovingBack => LargeInserterState::EmptyAndMovingBack( - u16::try_from(u32::from(self.movetime).strict_sub(current_tick.strict_sub( - first_tick_value_with_this_lower_part(ins.last_update_time, current_tick), - ))) + u16::try_from(u32::from(u16::from(self.movetime)).strict_sub( + current_tick.strict_sub(first_tick_value_with_this_lower_part( + ins.last_update_time, + current_tick, + )), + )) .expect(&format!( "Inserter has been moving for more than u16::MAX ticks: {}", - u32::from(self.movetime).strict_sub(current_tick.strict_sub( + u32::from(u16::from(self.movetime)).strict_sub(current_tick.strict_sub( first_tick_value_with_this_lower_part( ins.last_update_time, current_tick @@ -1014,21 +1029,21 @@ impl BucketedStorageStorageInserterStore { } pub fn reinsert_empty(&mut self, inserter: InserterBucketData) { - self.empty_and_moving_back - [(self.current_tick + usize::from(self.movetime)) % self.empty_and_moving_back.len()] + self.empty_and_moving_back[(self.current_tick + usize::from(u16::from(self.movetime))) + % self.empty_and_moving_back.len()] .push(inserter); } pub fn reinsert_full(&mut self, inserter: InserterBucketData) { - self.full_and_moving_out - [(self.current_tick + usize::from(self.movetime)) % self.full_and_moving_out.len()] + self.full_and_moving_out[(self.current_tick + usize::from(u16::from(self.movetime))) + % self.full_and_moving_out.len()] .push(inserter); } } #[cfg(test)] mod test { - const MOVETIME: u16 = 120; + const MOVETIME: NonZero = NonZero::new(120).unwrap(); const NUM_INSERTERS: usize = 20_000_000; const NUM_ITEMS: usize = 5; diff --git a/src/power/mod.rs b/src/power/mod.rs index 64645a7..f1f8735 100644 --- a/src/power/mod.rs +++ b/src/power/mod.rs @@ -1,3 +1,7 @@ +use crate::inserter::belt_storage_inserter::Dir; +use crate::inserter::belt_storage_movement_list::{ + BeltStorageInserterInMovement, List, ReinsertionLists, +}; use crate::inserter::storage_storage_with_buckets_indirect::InserterBucketData; use crate::item::Indexable; use crate::{ @@ -1057,6 +1061,18 @@ impl PowerGridStorage, + List<{ Dir::StorageToBelt }, { Dir::BeltToStorage }>, + ), + ( + List<{ Dir::BeltToStorage }, { Dir::StorageToBelt }>, + List<{ Dir::StorageToBelt }, { Dir::StorageToBelt }>, + ), + )], + data_store: &DataStore, ) -> (ResearchProgress, RecipeTickInfo, Option) { { @@ -1108,25 +1124,76 @@ impl PowerGridStorage { + let store = inserter_store.inserters + [inserter.item.into_usize()] + .get_mut(&inserter.movetime) + .unwrap(); + let ins = InserterBucketData { + storage_id_in: storage_id_in, + storage_id_out: storage_id_out, + index: index, + current_hand: inserter.current_hand, + max_hand_size: inserter.max_hand, + }; + if inserter.current_hand == 0 { + store.0.reinsert_empty(ins); + } else { + store.0.reinsert_empty(ins); + } + }, + crate::assembler::simd::Conn::Belt { + belt_id, + belt_pos, + self_is_source, + self_storage, + } => { + let ((_a, belt_storage), (_c, storage_belt)) = + &mut belt_storage_reinsertion_list + [inserter.item.into_usize()]; + + if self_is_source { + storage_belt.reinsert( + inserter.movetime, + BeltStorageInserterInMovement { + current_hand: inserter.max_hand, + movetime: inserter + .movetime + .try_into() + .unwrap(), + storage: self_storage, + belt: belt_id, + belt_pos, + max_hand_size: inserter.max_hand, + }, + ); + } else { + belt_storage.reinsert( + inserter.movetime, + BeltStorageInserterInMovement { + current_hand: 0, + movetime: inserter + .movetime + .try_into() + .unwrap(), + storage: self_storage, + belt: belt_id, + belt_pos, + max_hand_size: inserter.max_hand, + }, + ); + } + }, } } } ); + ( acc_progress + rhs_progress, infos + &info, diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 50024d0..b60d7f0 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -66,6 +66,7 @@ use rayon::iter::{IntoParallelIterator, ParallelIterator}; use std::cmp::max; use std::fs::File; use std::io::{Read, Write}; +use std::num::NonZero; use std::sync::LazyLock; use std::{ cmp::{Ordering, min}, @@ -906,7 +907,7 @@ pub fn render_world( let start_pos = data_store.inserter_start_pos(*ty, *pos, *direction); let end_pos = data_store.inserter_end_pos(*ty, *pos, *direction); - let movetime: u16 = user_movetime.map(|v| v.into()).unwrap_or(data_store.inserter_infos[*ty as usize].swing_time_ticks).into(); + let movetime = 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 Some(state) = game_state.simulation_state.factory.belts.get_inserter_info_at(*id, *belt_pos) else { @@ -972,8 +973,8 @@ pub fn render_world( let (position, items): (f32, ITEMCOUNTTYPE) = match state.state { crate::inserter::storage_storage_with_buckets::LargeInserterState::WaitingForSourceItems(count) => (0.0, count), crate::inserter::storage_storage_with_buckets::LargeInserterState::WaitingForSpaceInDestination(count) => (1.0, count), - crate::inserter::storage_storage_with_buckets::LargeInserterState::FullAndMovingOut(timer) => (1.0 - (timer as f32 / movetime as f32), hand_size), - crate::inserter::storage_storage_with_buckets::LargeInserterState::EmptyAndMovingBack(timer) => (timer as f32 / movetime as f32, 0), + crate::inserter::storage_storage_with_buckets::LargeInserterState::FullAndMovingOut(timer) => (1.0 - (timer as f32 / u16::from(movetime) as f32), hand_size), + crate::inserter::storage_storage_with_buckets::LargeInserterState::EmptyAndMovingBack(timer) => (timer as f32 / u16::from(movetime) as f32, 0), }; assert!(position >= 0.0); @@ -2595,7 +2596,7 @@ pub fn render_ui< if start_pos.contained_in(*assembler_pos, data_store.assembler_info[*assembler_ty as usize].size(*assembler_rotation)) { let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store_ref.inserter_infos[*ty as usize].swing_time_ticks); - let items_per_tick = f32::from(data_store_ref.inserter_infos[*ty as usize].base_hand_size) / (2.0 * f32::from(movetime) + 2.0); + let items_per_tick = f32::from(data_store_ref.inserter_infos[*ty as usize].base_hand_size) / (2.0 * f32::from(u16::from(movetime)) + 2.0); Some((item, items_per_tick)) } else { @@ -2639,7 +2640,7 @@ pub fn render_ui< let swing_time_in_ticks = full_rotation_time_in_ticks / 2.0 - 1.0; - goal_movetime = max(goal_movetime, swing_time_in_ticks as u16 / 10 * 10); + goal_movetime = max(goal_movetime, (swing_time_in_ticks as u16 / 10 * 10).try_into().unwrap()); }, } }, @@ -2674,7 +2675,7 @@ pub fn render_ui< if start_pos.contained_in(*chest_pos, data_store_ref.chest_tile_sizes[*chest_ty as usize]) { let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store_ref.inserter_infos[*ty as usize].swing_time_ticks); - let items_per_tick = f32::from(data_store_ref.inserter_infos[*ty as usize].base_hand_size) / (2.0 * f32::from(movetime)); + let items_per_tick = f32::from(data_store_ref.inserter_infos[*ty as usize].base_hand_size) / (2.0 * f32::from(u16::from(movetime))); Some((item, items_per_tick)) } else { @@ -2694,7 +2695,7 @@ pub fn render_ui< let swing_time_in_ticks = full_rotation_time_in_ticks / 2.0 - 1.0; - goal_movetime = max(goal_movetime, swing_time_in_ticks as u16 / 10 * 10); + goal_movetime = max(goal_movetime, (swing_time_in_ticks as u16 / 10 * 10).try_into().unwrap()); } _ => {} @@ -2751,7 +2752,7 @@ pub fn render_ui< if start_pos.contained_in(*assembler_pos, data_store.assembler_info[*assembler_ty as usize].size(*assembler_rotation)) { let movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store_ref.inserter_infos[*ty as usize].swing_time_ticks); - let items_per_tick = f32::from(data_store_ref.inserter_infos[*ty as usize].base_hand_size) / (2.0 * f32::from(movetime) + 2.0); + let items_per_tick = f32::from(data_store_ref.inserter_infos[*ty as usize].base_hand_size) / (2.0 * f32::from(u16::from(movetime)) + 2.0); Some((item, items_per_tick)) } else { @@ -2786,10 +2787,7 @@ pub fn render_ui< let swing_time_in_ticks = full_rotation_time_in_ticks / 2.0 - 1.0; - goal_movetime = max(goal_movetime, swing_time_in_ticks as u16 / 10 * 10); - if goal_movetime > 10_000 { - dbg!(items_produced_per_tick); - } + goal_movetime = max(goal_movetime, (swing_time_in_ticks as u16 / 10 * 10).try_into().unwrap()); }, } }, @@ -2802,10 +2800,6 @@ pub fn render_ui< }); actions.extend(inserter_pos_and_time.map(|(pos, time)| { - if time > 10_000 { - dbg!(pos); - } - ActionType::OverrideInserterMovetime { pos, new_movetime: Some(time.try_into().unwrap()) } })) } @@ -3040,7 +3034,7 @@ pub fn render_ui< .storage_storage_inserters .inserters .iter() - .flat_map(|tree: &std::collections::BTreeMap| tree.values()) + .flat_map(|tree| tree.values()) .map(|(store, )| store.get_load_info()) .map(|(_, _, num_storage_cachelines, num_struct_cachelines)| { num_storage_cachelines + num_struct_cachelines @@ -3702,7 +3696,7 @@ pub fn render_ui< if movetime_overridden { let mut movetime = user_movetime.map(|v| v.into()).unwrap_or(data_store.inserter_infos[*ty as usize].swing_time_ticks); - ui.add(egui::Slider::new(&mut movetime, (data_store.inserter_infos[*ty as usize].swing_time_ticks)..=u16::MAX).text("Ticks per half swing")); + ui.add(egui::Slider::new(&mut movetime, (data_store.inserter_infos[*ty as usize].swing_time_ticks)..=NonZero::::MAX).text("Ticks per half swing")); if *user_movetime != Some(movetime.try_into().unwrap()) { actions.push(ActionType::OverrideInserterMovetime { pos: *pos, new_movetime: Some(movetime.try_into().unwrap()) }); From 70e4af2172cd6ba418c7e2d4a8e55e4f7dbbb381 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 3 Dec 2025 21:32:54 +0100 Subject: [PATCH 056/152] Allow world generation to fall back to btreemap and correctly handle assembler wait lists more efficiently --- Cargo.toml | 1 + src/app_state.rs | 499 ++++++++++++------ src/assembler/bucketed.rs | 8 +- src/assembler/mod.rs | 4 +- src/assembler/simd.rs | 303 ++++++++--- src/belt/mod.rs | 4 - src/chest.rs | 19 +- src/data/factorio_1_1.fgmod | 16 +- .../world/sparse_grid/bounding_box_grid.rs | 296 ++++++----- src/frontend/world/sparse_grid/dynamic.rs | 243 +++++++++ src/frontend/world/sparse_grid/map_grid.rs | 186 +++++++ src/frontend/world/sparse_grid/mod.rs | 120 ++--- src/frontend/world/tile.rs | 30 +- src/inserter/belt_storage_movement_list.rs | 24 +- .../storage_storage_with_buckets_indirect.rs | 20 +- src/lib.rs | 11 +- src/power/mod.rs | 27 +- src/rendering/eframe_app.rs | 25 +- src/rendering/map_view.rs | 98 +++- src/rendering/render_world.rs | 2 + src/storage_list.rs | 47 +- test_blueprints/iron_generation_test.bp | 3 + tested_ideas/assembler_waitlist.md | 22 + tested_ideas/belt_inserter_list.md | 6 + 24 files changed, 1465 insertions(+), 549 deletions(-) create mode 100644 src/frontend/world/sparse_grid/dynamic.rs create mode 100644 src/frontend/world/sparse_grid/map_grid.rs create mode 100644 test_blueprints/iron_generation_test.bp create mode 100644 tested_ideas/assembler_waitlist.md create mode 100644 tested_ideas/belt_inserter_list.md diff --git a/Cargo.toml b/Cargo.toml index 37f5add..f922fae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ hex = "0.4.3" postcard = { version = "1.1.1", features = ["use-std"] } charts-rs = { version = "0.3.20", features = ["resvg"] } strum = { version = "0.27.1", features = ["derive"] } +# explicitly disable atomic feature, so that bitvecs do not use atomic instructions. very important for performance! bitvec = { version = "1.0.1", features = ["alloc", "serde", "std"], default-features = false } bimap = { version = "0.6.3", features = ["serde", "std"], default-features = false } eframe = { version = "0.31.1", features = ["accesskit", "default_fonts", "wayland", "web_screen_reader", "x11", "wgpu"], optional = true, default-features = false } diff --git a/src/app_state.rs b/src/app_state.rs index 119f123..455dc0b 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -6,6 +6,7 @@ use crate::chest::ChestSize; use crate::data::AllowedFluidDirection; use crate::frontend::action::belt_placement::FakeGameState; use crate::frontend::action::place_entity::PlaceEntityInfo; +use crate::frontend::world::tile::CHUNK_SIZE; use crate::frontend::world::tile::ModuleSlots; use crate::frontend::world::tile::ModuleTy; use crate::inserter::InserterStateInfo; @@ -406,7 +407,7 @@ impl GameState GameState, + data_store: &DataStore, + ) -> Self { + const CHUNK_THICKNESS: i32 = 150; + + // TODO: Correct size + let ret = GameState::new_with_world_area( + Position { + x: -1_000_000, + y: -1_000_000, + }, + Position { + x: 1_000_000, + y: (-1_000_000 + CHUNK_THICKNESS * 16), + }, + data_store, + ); + + let left = (-1_000_000..=(-1_000_000 + CHUNK_THICKNESS * 16)) + .step_by(usize::from(CHUNK_SIZE)) + .cartesian_product((-1_000_000..=1_000_000).step_by(usize::from(CHUNK_SIZE))) + .map(|(x, y)| (x / i32::from(CHUNK_SIZE), y / i32::from(CHUNK_SIZE))); + ret.world.lock().add_chunks(left); + + let right = ((1_000_000 - CHUNK_THICKNESS * 16)..=1_000_000) + .step_by(usize::from(CHUNK_SIZE)) + .cartesian_product((-1_000_000..=1_000_000).step_by(usize::from(CHUNK_SIZE))) + .map(|(x, y)| (x / i32::from(CHUNK_SIZE), y / i32::from(CHUNK_SIZE))); + ret.world.lock().add_chunks(right); + + let bottom = ((1_000_000 - CHUNK_THICKNESS * 16)..=1_000_000) + .step_by(usize::from(CHUNK_SIZE)) + .cartesian_product((-1_000_000..=1_000_000).step_by(usize::from(CHUNK_SIZE))) + .map(|(y, x)| (x / i32::from(CHUNK_SIZE), y / i32::from(CHUNK_SIZE))); + ret.world.lock().add_chunks(bottom); + + ret + } + pub fn add_solar_field( &mut self, pos: Position, @@ -983,9 +1025,15 @@ impl Factory Factory Factory= 120 { - belt.update(sushi_splitters); - belt.update_inserters(self_index.try_into().unwrap(), &mut belt_storage_reinsertion_outgoing, &mut storage_belt_reinsertion_incoming); - } - } - } - - { - profiling::scope!("Update PurePure Inserters"); - for ( - ins, - ((source, source_pos), (dest, dest_pos), cooldown, filter), - ) in pure_to_pure_inserters.iter_mut().flatten() - { - let [mut source_loc, mut dest_loc] = if *source == *dest { - assert_ne!( - source_pos, dest_pos, - "An inserter cannot take and drop off on the same tile" - ); - // We are taking and placing onto the same belt - let belt = &mut belt_store.belts[*source]; - - belt.get_two([(*source_pos).into(), (*dest_pos).into()]).map(|v| *v) - } else { - let [inp, out] = belt_store - .belts - .get_disjoint_mut([*source, *dest]) - .unwrap(); - - [*inp.get(*source_pos), *out.get(*dest_pos)] - }; - - if *cooldown == 0 { - ins.update_instant(&mut source_loc,&mut dest_loc); - } else { - ins.update( - &mut source_loc, - &mut dest_loc, - *cooldown, - // FIXME: - 1, - (), - |_| { - filter - .map(|filter_item| filter_item == item) - .unwrap_or(true) - }, + for fluid_system in fluid_store { + // FIXME: Switch to holes + if let Some(fluid_system) = fluid_system { + update_fluid_system( + item_id, + &mut fluid_system.hot_data, + item_storages, + grid_size, ); } - - { - profiling::scope!("Update update_first_free_pos"); - if !source_loc { - let _: Option<_> = belt_store.belts[*source] - .remove_item(*source_pos); - } - - if dest_loc { - let _ = belt_store.belts[*dest] - .try_insert_item(*dest_pos, item); - } - } } - } - }, - || { - { - profiling::scope!("belt_storage_exit.update"); - belt_storage_exit_outgoing.update(item_id, grid_size, &mut belt_storage_reinsertion_incoming, item_storages); - storage_belt_exit_incoming.update(item_id, grid_size, &mut storage_belt_reinsertion_outgoing, item_storages); - } - { - if data_store.item_is_fluid[item_id] { - profiling::scope!( - "FluidSystem Update", - format!("Item: {}", data_store.item_display_names[item_id]) - .as_str() - ); - for fluid_system in fluid_store { - // FIXME: Switch to holes - if let Some(fluid_system) = fluid_system { - update_fluid_system(item_id, &mut fluid_system.hot_data, item_storages, grid_size); - } - } - } else { + } else { + profiling::scope!( + "StorageStorage Inserter Update", + format!( + "Item: {}", + data_store.item_display_names[item_id] + ) + .as_str() + ); + for (ins_store,) in + storage_storage_inserter_stores.values_mut() + { profiling::scope!( "StorageStorage Inserter Update", - format!("Item: {}", data_store.item_display_names[item_id]) + format!("Movetime: {}", ins_store.movetime) .as_str() ); - for (ins_store,) in storage_storage_inserter_stores.values_mut() - { - profiling::scope!( - "StorageStorage Inserter Update", - format!("Movetime: {}", ins_store.movetime).as_str() - ); - ins_store.update( - item_id, - item_storages, - grid_size, - current_tick, - ); - } + ins_store.update( + item_id, + item_storages, + grid_size, + current_tick, + ); } } } - ); + } } let pure_update_time = pure_update_start.elapsed(); @@ -2908,27 +2914,208 @@ impl GameState, Vec<_>) = simulation_state + .factory + .belt_storage_inserters + .iter_mut() + .map(|belt_storage_inserter_lists| { + let (belt_to_storage_flow, storage_to_belt_flow) = belt_storage_inserter_lists; + + let (outgoing_belt_to_storage, incoming_belt_to_storage) = belt_to_storage_flow; + let (incoming_storage_to_belt, outgoing_storage_to_belt) = storage_to_belt_flow; + + let (belt_storage_exit_outgoing, belt_storage_reinsertion_outgoing) = + outgoing_belt_to_storage.get(); + let (belt_storage_exit_incoming, belt_storage_reinsertion_incoming) = + incoming_belt_to_storage.get(); + + let (storage_belt_exit_outgoing, storage_belt_reinsertion_outgoing) = + outgoing_storage_to_belt.get(); + let (storage_belt_exit_incoming, storage_belt_reinsertion_incoming) = + incoming_storage_to_belt.get(); + + ( + ( + belt_storage_exit_incoming, + belt_storage_reinsertion_outgoing, + storage_belt_exit_outgoing, + storage_belt_reinsertion_incoming, + ), + ( + belt_storage_exit_outgoing, + belt_storage_reinsertion_incoming, + storage_belt_reinsertion_outgoing, + storage_belt_exit_incoming, + ), + ) + }) + .unzip(); + + let mut ret = None; + + { + // profiling::scope!("Power Grid, Chest, Mining Drill Stage"); + + rayon::scope(|scope| { + simulation_state + .factory + .belts + .inner + .smart_belts + .iter_mut() + .zip( + simulation_state + .factory + .belts + .inner + .belt_belt_inserters + .pure_to_pure_inserters + .iter_mut(), + ) + .zip(belt_stuff) + .zip(simulation_state.factory.item_times.iter_mut()) + .enumerate() + // Queue long running items first, so we do not end up waiting for a long running item at the end, which we could have started early on + // This alone is ~10% faster on a test game I have + .sorted_unstable_by_key(|&(_, (_, &mut avg_time))| -(avg_time * 1000.0) as i32) + .for_each( + |( + item_id, + (((belt_store, pure_to_pure_inserters), belt_stuff), avg_time), + )| scope.spawn(move |_| { + profiling::scope!( + "Pure Belt Update", + format!("Item: {}", data_store.item_display_names[item_id]).as_str() + ); + + let item = Item { + id: item_id.try_into().unwrap(), + }; + + let (belt_storage_exit_incoming, mut belt_storage_reinsertion_outgoing, storage_belt_exit_outgoing, mut storage_belt_reinsertion_incoming) = belt_stuff; + + + { + profiling::scope!("storage_belt_exit.update"); + belt_storage_exit_incoming.update( + &mut belt_storage_reinsertion_outgoing, + belt_store.belts.as_mut_slice(), + ); + storage_belt_exit_outgoing.update( + &mut storage_belt_reinsertion_incoming, + belt_store.belts.as_mut_slice(), + ); + } + { + profiling::scope!( + "Update Belts", + format!("Count: {}", belt_store.belts.len()) + ); + for (self_index, (belt, ty)) in belt_store + .belts + .iter_mut() + .zip(&belt_store.belt_ty) + .enumerate() + { + // TODO: Avoid last minute decision making + // If I have a list per type, I can avoid loading belts if they are not updated + if update_timers[usize::from(*ty)] >= 120 { + belt.update(sushi_splitters); + belt.update_inserters( + self_index.try_into().unwrap(), + &mut belt_storage_reinsertion_outgoing, + &mut storage_belt_reinsertion_incoming, + ); + } + } + } + + { + profiling::scope!("Update PurePure Inserters"); + for (ins, ((source, source_pos), (dest, dest_pos), cooldown, filter)) in + pure_to_pure_inserters.iter_mut().flatten() + { + let [mut source_loc, mut dest_loc] = if *source == *dest { + assert_ne!( + source_pos, dest_pos, + "An inserter cannot take and drop off on the same tile" + ); + // We are taking and placing onto the same belt + let belt = &mut belt_store.belts[*source]; + + belt.get_two([(*source_pos).into(), (*dest_pos).into()]) + .map(|v| *v) + } else { + let [inp, out] = + belt_store.belts.get_disjoint_mut([*source, *dest]).unwrap(); + + [*inp.get(*source_pos), *out.get(*dest_pos)] + }; + + if *cooldown == 0 { + ins.update_instant(&mut source_loc, &mut dest_loc); + } else { + ins.update( + &mut source_loc, + &mut dest_loc, + *cooldown, + // FIXME: + 1, + (), + |_| { + filter + .map(|filter_item| filter_item == item) + .unwrap_or(true) + }, + ); + } + + { + profiling::scope!("Update update_first_free_pos"); + if !source_loc { + let _: Option<_> = + belt_store.belts[*source].remove_item(*source_pos); + } + + if dest_loc { + let _ = + belt_store.belts[*dest].try_insert_item(*dest_pos, item); + } + } + } + } + }) + ); + + scope.spawn(|_| { + ret = Some(join!( + || simulation_state.factory.chests.update(data_store), + || { + simulation_state.factory.ore_store.update( + &simulation_state.tech_state.mining_productivity_by_item, + data_store, + ) + }, + || { + simulation_state.factory.power_grids.update( + &simulation_state.tech_state, + aux_data.current_tick as u32, + &mut simulation_state.factory.storage_storage_inserters, + assembler_stuff.as_mut_slice(), + data_store, + ) + } + )); + }); + }) }; + + let (reinsertions, mining_production_by_item, (tech_progress, recipe_tick_info, lab_info)) = + ret.unwrap(); + { profiling::scope!("Chest inserter waitlist reinsertion"); reinsertions @@ -3035,6 +3222,16 @@ impl GameState ( ( [MaxInsertionLimit<'_>; NUM_INGS], - [&mut [ITEMCOUNTTYPE]; NUM_INGS], - [&mut [InserterWaitList]; NUM_INGS], + [&mut [u8]; NUM_INGS], + [(&mut [InserterWaitList], &mut [u8]); NUM_INGS], ), ( - [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], - [&mut [InserterWaitList]; NUM_OUTPUTS], + [&mut [u8]; NUM_OUTPUTS], + [(&mut [InserterWaitList], &mut [u8]); NUM_OUTPUTS], ), ) { // ( diff --git a/src/assembler/mod.rs b/src/assembler/mod.rs index dc139d4..3718ac1 100644 --- a/src/assembler/mod.rs +++ b/src/assembler/mod.rs @@ -899,11 +899,11 @@ pub trait MultiAssemblerStore< ( [MaxInsertionLimit<'_>; NUM_INGS], [&mut [ITEMCOUNTTYPE]; NUM_INGS], - [&mut [InserterWaitList]; NUM_INGS], + [(&mut [InserterWaitList], &mut [ITEMCOUNTTYPE]); NUM_INGS], ), ( [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], - [&mut [InserterWaitList]; NUM_OUTPUTS], + [(&mut [InserterWaitList], &mut [ITEMCOUNTTYPE]); NUM_OUTPUTS], ), ); diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index b441fa0..3657b7e 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -7,7 +7,7 @@ use std::{ ops::{Add, Sub}, simd::{ Simd, - cmp::SimdPartialOrd, + cmp::{SimdPartialEq, SimdPartialOrd}, num::{SimdInt, SimdUint}, }, u8, @@ -36,11 +36,13 @@ use egui_show_info_derive::ShowInfo; #[cfg(feature = "client")] use get_size2::GetSize; +const WAITLIST_LEN: usize = 3; + #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[repr(align(64))] pub struct InserterWaitList { - pub inserters: [Option; 3], + pub(crate) inserters: [Option; WAITLIST_LEN], } const_assert!(std::mem::size_of::() <= 64); @@ -51,7 +53,7 @@ pub struct Inserter { // item: u8, // TODO: This is not needed for assemblers, just for chests. pub self_is_source: bool, - // Ideally we would track the hand here so we avoid having to reinsert them each time the assembler produces anything + // We track the hand here so we avoid having to reinsert them each time the assembler produces anything // This does mean we can only fit 3 Inserters per cacheline :/ // This is fixed by the item arena optimization pub current_hand: ITEMCOUNTTYPE, @@ -193,8 +195,11 @@ pub struct MultiAssemblerStore< #[serde(with = "arrays")] waitlists_ings: [Box<[InserterWaitList]>; NUM_INGS], #[serde(with = "arrays")] + waitlists_ings_needed: [Box<[ITEMCOUNTTYPE]>; NUM_INGS], + #[serde(with = "arrays")] waitlists_outputs: [Box<[InserterWaitList]>; NUM_OUTPUTS], - + #[serde(with = "arrays")] + waitlists_outputs_needed: [Box<[ITEMCOUNTTYPE]>; NUM_OUTPUTS], holes: Vec, positions: Box<[Position]>, types: Box<[u8]>, @@ -389,6 +394,50 @@ impl )) .enumerate() { + // let timer = SIMDTYPE::from_array(*timer_arr); + // let stopped_empty = dbg!(timer).simd_eq(SIMDTYPE::splat(0)); + // let stopped_full = timer.simd_eq(SIMDTYPE::splat(TIMERTYPE::MAX)); + + // let speed_mod = Simd::::from_array(*speed_mod); + // let increase: SIMDTYPE = (power_level_recipe_increase.cast::() + // * speed_mod.cast::() + // / Simd::::splat(20)) + // .cast(); + // let new_timer_assuming_free_run = timer + increase; + // let timer_mask_assuming_free_run: MASKTYPE = new_timer_assuming_free_run.simd_lt(timer); + // let progress = (new_timer_assuming_free_run.sub(timer)).cast::(); + // let prod_timer = SIMDTYPE::from_array(*prod_timer_arr); + // let bonus_prod = Simd::::from_array(*bonus_prod); + // let new_prod_timer_assuming_free_run: SIMDTYPE = prod_timer.add(SimdUint::cast::( + // progress * SimdUint::cast::(bonus_prod) / Simd::::splat(100), + // )); + // let prod_timer_mask: MASKTYPE = if bonus_prod + // .simd_gt(Simd::::splat(0)) + // .any() + // { + // let prod_timer = SIMDTYPE::from_array(*prod_timer_arr); + // // This needs be calculated in u32 to prevent overflows in intermediate values + // let progress = (new_timer_assuming_free_run.sub(timer)).cast::(); + // let new_prod_timer: SIMDTYPE = prod_timer.add(SimdUint::cast::( + // progress * SimdUint::cast::(bonus_prod) / Simd::::splat(100), + // )); + + // *prod_timer_arr = new_prod_timer.to_array(); + + // new_prod_timer.simd_lt(prod_timer) + // } else { + // MASKTYPE::splat(false) + // }; + // if !stopped_empty.any() + // && !stopped_full.any() + // && !timer_mask_assuming_free_run.any() + // && !prod_timer_mask.any() + // { + // *timer_arr = new_timer_assuming_free_run.to_array(); + // *prod_timer_arr = new_prod_timer_assuming_free_run.to_array(); + // continue; + // } + let index = index * 16; // ~~Remove the items from the ings at the start of the crafting process~~ // We will do this as part of the frontend ui! @@ -439,10 +488,13 @@ impl let new_timer = space_mask.select(new_timer_output_space, new_timer_output_full); + if timer == new_timer { + continue; + } + // Power calculation // We use power if any work was done - let uses_power = - ing_mask & (space_mask | timer.simd_lt(SIMDTYPE::splat(TIMERTYPE::MAX))); + let uses_power = timer.simd_ne(new_timer); if self.single_type.is_some() { power_const_type += u32::from( uses_power @@ -460,28 +512,30 @@ impl ); } - if timer == new_timer { - continue; - } - let timer_mask: MASKTYPE = new_timer.simd_lt(timer); - // if we have enough items for another craft keep the wrapped value (This improves the accuracy), else clamp it to 0 let new_timer = (!timer_mask | ing_mask_for_two_crafts).select(new_timer, SIMDTYPE::splat(0)); - - let prod_timer = SIMDTYPE::from_array(*prod_timer_arr); + *timer_arr = new_timer.to_array(); let bonus_prod = Simd::::from_array(*bonus_prod); - // This needs be calculated in u32 to prevent overflows in intermediate values - let progress = (new_timer.sub(timer)).cast::(); - let new_prod_timer: SIMDTYPE = prod_timer.add(SimdUint::cast::( - progress * SimdUint::cast::(bonus_prod) / Simd::::splat(100), - )); + let prod_timer_mask: MASKTYPE = if bonus_prod + .simd_gt(Simd::::splat(0)) + .any() + { + let prod_timer = SIMDTYPE::from_array(*prod_timer_arr); + // This needs be calculated in u32 to prevent overflows in intermediate values + let progress = (new_timer.sub(timer)).cast::(); + let new_prod_timer: SIMDTYPE = prod_timer.add(SimdUint::cast::( + progress * SimdUint::cast::(bonus_prod) / Simd::::splat(100), + )); - let prod_timer_mask: MASKTYPE = new_prod_timer.simd_lt(prod_timer); + *prod_timer_arr = new_prod_timer.to_array(); + + new_prod_timer.simd_lt(prod_timer) + } else { + MASKTYPE::splat(false) + }; - *timer_arr = new_timer.to_array(); - *prod_timer_arr = new_prod_timer.to_array(); if timer_mask.any() || prod_timer_mask.any() { // for i in 0..NUM_OUTPUTS { // let our_outputs = SIMDTYPE::splat(our_outputs[i].into()); @@ -498,6 +552,7 @@ impl let has_produced = timer_mask | prod_timer_mask; + // FIXME: We are missing production if main and prod tick are at the same time! for (i, has_produced) in has_produced.to_array().into_iter().enumerate() { if has_produced { let final_idx = index + i; @@ -509,18 +564,31 @@ impl .zip(&mut items) .enumerate() { - for ins in &mut out[final_idx].inserters { - if let Some(v) = ins { - let amount_taken_by_this_inserter = min( - *items_to_distribute, - ITEMCOUNTTYPE::from(v.max_hand) - v.current_hand, - ); - if v.current_hand + amount_taken_by_this_inserter - == ITEMCOUNTTYPE::from(v.max_hand) - { - let ins = ins.take().unwrap(); - let Ok(()) = - self.inserter_waitlist_output_vec.push_within_capacity( + if *items_to_distribute + self.outputs[item][final_idx] + >= min( + self.waitlists_outputs_needed[item][final_idx], + our_maximums[item], + ) + { + *items_to_distribute += self.outputs[item][final_idx]; + self.outputs[item][final_idx] = 0; + for idx in 0..WAITLIST_LEN { + let ins = &mut out[final_idx].inserters[idx]; + if let Some(v) = ins { + let amount_taken_by_this_inserter = min( + *items_to_distribute, + ITEMCOUNTTYPE::from(v.max_hand) - v.current_hand, + ); + if v.current_hand + amount_taken_by_this_inserter + == ITEMCOUNTTYPE::from(v.max_hand) + { + let ins = ins.take().unwrap(); + for move_left_idx in idx..WAITLIST_LEN { + out[final_idx].inserters[move_left_idx] = out[final_idx].inserters.get_mut(move_left_idx + 1).map(|v| v.take()).unwrap_or(None); + } + let Ok(()) = self + .inserter_waitlist_output_vec + .push_within_capacity( InternalInserterReinsertionInfo { movetime: ins.movetime.into(), item: (NUM_INGS + item) as u8, @@ -543,21 +611,24 @@ impl }, }, }, - ) - else { - panic!( - "Not enough space in inserter readdition vec. Capacity is {}", - self.inserter_waitlist_output_vec.capacity() - ); - }; - } else { - v.current_hand += amount_taken_by_this_inserter; - } - *items_to_distribute -= amount_taken_by_this_inserter; - - // TODO: Check if this is good or bad - if *items_to_distribute == 0 { - break; + ) else { + panic!( + "Not enough space in inserter readdition vec. Capacity is {}", + self.inserter_waitlist_output_vec.capacity() + ); + }; + self.waitlists_outputs_needed[item][final_idx] = out + [final_idx] + .inserters[0].as_ref().map(|ins| ins.max_hand - ins.current_hand).unwrap_or(ITEMCOUNTTYPE::MAX); + } else { + v.current_hand += amount_taken_by_this_inserter; + } + *items_to_distribute -= amount_taken_by_this_inserter; + + // TODO: Check if this is good or bad + if *items_to_distribute == 0 { + break; + } } } } @@ -588,14 +659,28 @@ impl for (item, (ing, items_to_drain)) in self.waitlists_ings.iter_mut().zip(&mut items).enumerate() { - for ins in &mut ing[final_idx].inserters { - if let Some(v) = ins { - let amount_taken_by_this_inserter = + if *items_to_drain + + (self.ings_max_insert[item][final_idx] + - self.ings[item][final_idx]) + >= self.waitlists_ings_needed[item][final_idx] + { + *items_to_drain += (self.ings_max_insert[item][final_idx] + - self.ings[item][final_idx]); + self.ings[item][final_idx] = self.ings_max_insert[item][final_idx]; + + for idx in 0..WAITLIST_LEN { + let ins = &mut ing[final_idx].inserters[idx]; + if let Some(v) = ins { + let amount_taken_by_this_inserter = min(*items_to_drain, v.current_hand); - if v.current_hand - amount_taken_by_this_inserter == 0 { - let ins = ins.take().unwrap(); - let Ok(()) = - self.inserter_waitlist_output_vec.push_within_capacity( + if v.current_hand - amount_taken_by_this_inserter == 0 { + let ins = ins.take().unwrap(); + for move_left_idx in idx..WAITLIST_LEN { + ing[final_idx].inserters[move_left_idx] = ing[final_idx].inserters.get_mut(move_left_idx + 1).map(|v| v.take()).unwrap_or(None); + } + let Ok(()) = self + .inserter_waitlist_output_vec + .push_within_capacity( InternalInserterReinsertionInfo { movetime: ins.movetime.into(), item: item as u8, @@ -618,21 +703,24 @@ impl }, }, }, - ) - else { - panic!( - "Not enough space in inserter readdition vec. Capacity is {}.", - self.inserter_waitlist_output_vec.capacity() - ); - }; - } else { - v.current_hand -= amount_taken_by_this_inserter; - } - *items_to_drain -= amount_taken_by_this_inserter; - - // TODO: Check if this is good or bad - if *items_to_drain == 0 { - break; + ) else { + panic!( + "Not enough space in inserter readdition vec. Capacity is {}.", + self.inserter_waitlist_output_vec.capacity() + ); + }; + self.waitlists_ings_needed[item][final_idx] = ing + [final_idx] + .inserters[0].as_ref().map(|ins| ins.current_hand).unwrap_or(ITEMCOUNTTYPE::MAX); + } else { + v.current_hand -= amount_taken_by_this_inserter; + } + *items_to_drain -= amount_taken_by_this_inserter; + + // TODO: Check if this is good or bad + if *items_to_drain == 0 { + break; + } } } } @@ -731,8 +819,9 @@ impl| v.into_vec()); + for (new, other) in new_waitlists_outputs_needed + .iter_mut() + .zip(other.waitlists_outputs_needed) + { + new.extend(other); + } + let updates = IntoIterator::into_iter(other.positions) .take(other.len) .zip(other.types) @@ -1261,7 +1368,9 @@ impl; NUM_INGS], [&mut [ITEMCOUNTTYPE]; NUM_INGS], - [&mut [InserterWaitList]; NUM_INGS], + [(&mut [InserterWaitList], &mut [ITEMCOUNTTYPE]); NUM_INGS], ), ( [&mut [ITEMCOUNTTYPE]; NUM_OUTPUTS], - [&mut [InserterWaitList]; NUM_OUTPUTS], + [(&mut [InserterWaitList], &mut [ITEMCOUNTTYPE]); NUM_OUTPUTS], ), ) { ( @@ -1457,11 +1566,23 @@ impl BeltStore { // TODO: Update inserters! } } - - for current_timer in self.inner.belt_update_timers.iter_mut() { - *current_timer %= 120; - } } pub fn get_splitter_belt_ids( diff --git a/src/chest.rs b/src/chest.rs index c4b74e3..34833ee 100644 --- a/src/chest.rs +++ b/src/chest.rs @@ -155,6 +155,7 @@ pub struct MultiChestStore { holes: Vec, wait_list: Vec, + wait_list_min: Vec, #[serde(skip)] inserter_reinsertion_vec: Vec, @@ -174,6 +175,7 @@ impl MultiChestStore { max_items: vec![], wait_list: vec![], + wait_list_min: vec![], holes: vec![], @@ -206,6 +208,7 @@ impl MultiChestStore { self.max_items[hole] = max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX)); self.wait_list[hole] = InserterWaitList::default(); + self.wait_list_min[hole] = ITEMCOUNTTYPE::MAX; hole.try_into().unwrap() } else { @@ -218,6 +221,7 @@ impl MultiChestStore { self.max_items .push(max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX))); self.wait_list.push(InserterWaitList::default()); + self.wait_list_min.push(ITEMCOUNTTYPE::MAX); (self.inout.len() - 1).try_into().unwrap() } } @@ -235,6 +239,7 @@ impl MultiChestStore { self.max_items[hole] = max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX)); self.wait_list[hole] = InserterWaitList::default(); + self.wait_list_min[hole] = ITEMCOUNTTYPE::MAX; hole.try_into().unwrap() } else { @@ -247,6 +252,7 @@ impl MultiChestStore { self.max_items .push(max_items.saturating_sub(ChestSize::from(ITEMCOUNTTYPE::MAX))); self.wait_list.push(InserterWaitList::default()); + self.wait_list_min.push(ITEMCOUNTTYPE::MAX); (self.inout.len() - 1).try_into().unwrap() } @@ -265,6 +271,7 @@ impl MultiChestStore { self.storage[index] = 0; self.max_items[index] = 0; self.wait_list[index] = InserterWaitList::default(); + self.wait_list_min[index] = ITEMCOUNTTYPE::MAX; items } @@ -314,12 +321,15 @@ impl MultiChestStore { // TODO: Splitting this into large chests and small chests would be better since now a single large chest will make all small chests in the world expensive // With wait lists we cannot avoid updating all chests (even small chests) // if self.num_large_chests > 0 { - for (index, ((((inout, last_inout), (storage, max_items)), wait_list), max_insert)) in self + for ( + index, + ((((inout, last_inout), (storage, max_items)), (wait_list, wait_list_min)), max_insert), + ) in self .inout .iter_mut() .zip(self.last_inout.iter_mut()) .zip(self.storage.iter_mut().zip(self.max_items.iter().copied())) - .zip(self.wait_list.iter_mut()) + .zip(self.wait_list.iter_mut().zip(self.wait_list_min.iter_mut())) .zip(self.max_insert.iter()) .enumerate() { @@ -517,7 +527,10 @@ impl MultiChestStore { MaxInsertionLimit::PerMachine(self.max_insert.as_slice()), self.inout.as_mut_slice(), // InserterWaitLists::None, - InserterWaitLists::PerMachine(self.wait_list.as_mut_slice()), + InserterWaitLists::PerMachine( + self.wait_list.as_mut_slice(), + self.wait_list_min.as_mut_slice(), + ), ) } } diff --git a/src/data/factorio_1_1.fgmod b/src/data/factorio_1_1.fgmod index c32e7ac..494cf78 100644 --- a/src/data/factorio_1_1.fgmod +++ b/src/data/factorio_1_1.fgmod @@ -12,10 +12,10 @@ output: [ ( item: "factory_game::iron_ore", - amount: 1, + amount: 50, ), ], - time_to_craft: 1, + time_to_craft: 100, is_intermediate: false, ), ( @@ -30,10 +30,10 @@ output: [ ( item: "factory_game::copper_ore", - amount: 1, + amount: 50, ), ], - time_to_craft: 1, + time_to_craft: 100, is_intermediate: false, ), ( @@ -48,10 +48,10 @@ output: [ ( item: "factory_game::coal", - amount: 1, + amount: 50, ), ], - time_to_craft: 1, + time_to_craft: 100, is_intermediate: false, ), ( @@ -66,10 +66,10 @@ output: [ ( item: "factory_game::stone", - amount: 1, + amount: 50, ), ], - time_to_craft: 1, + time_to_craft: 100, is_intermediate: false, ), ( diff --git a/src/frontend/world/sparse_grid/bounding_box_grid.rs b/src/frontend/world/sparse_grid/bounding_box_grid.rs index be0edfa..ce9ac58 100644 --- a/src/frontend/world/sparse_grid/bounding_box_grid.rs +++ b/src/frontend/world/sparse_grid/bounding_box_grid.rs @@ -1,3 +1,4 @@ +use crate::frontend::world::sparse_grid::SparseGrid; use crate::saving::{save_at, save_at_fork}; use super::GetGridIndex; @@ -7,6 +8,7 @@ use rayon::slice::ParallelSlice; use std::cmp::{max, min}; use std::fmt::Debug; use std::fs::create_dir_all; +use std::mem; use std::ops::RangeInclusive; use std::path::PathBuf; @@ -27,57 +29,82 @@ pub struct BoundingBoxGrid { // FIXME: currently only for i32 to allow getting the number of slots type I = i32; -impl> BoundingBoxGrid { - pub fn new() -> Self { +impl + 'static> SparseGrid for BoundingBoxGrid { + fn new() -> Self { Self { extent: None, values: vec![], } } - pub fn new_with_filled_grid(top_left: [I; 2], bottom_right: [I; 2], generation_fn: F) -> Self + fn get_extent(&self) -> Option<[RangeInclusive; 2]> { + self.extent.map(|v| v.map(|[start, end]| start..=end)) + } + + fn get_default(&mut self, x: I, y: I) -> &T where - F: Fn([I; 2]) -> T + Sync, - T: Send, + T: Default, { - let extent = Some([ - [top_left[0], bottom_right[0]], - [top_left[1], bottom_right[1]], - ]); + todo!() + } - let width = usize::try_from(bottom_right[0] - top_left[0]) - .expect("Check bounding box argument order") - + 1; + fn insert(&mut self, x: I, y: I, value: T) -> Option { + self.include_in_extent(x, y); - let height = usize::try_from(bottom_right[1] - top_left[1]) - .expect("Check bounding box argument order") - + 1; + let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); + debug_assert!(self.values[index].is_none()); + let mut old = Some(value); + mem::swap(&mut old, &mut self.values[index]); + old + } - let mut values = Vec::with_capacity(width * height); + fn insert_deduplicate(&mut self, x: I, y: I, value: T) -> Option + where + T: PartialEq + Default, + { + todo!() + } - (0..(width * height)) - .into_par_iter() - .map(|v| (v % width, v / width)) - .map(|(x_offs, y_offs)| { - ( - top_left[0] - .checked_add_unsigned(x_offs.try_into().unwrap()) - .unwrap(), - top_left[1] - .checked_add_unsigned(y_offs.try_into().unwrap()) - .unwrap(), - ) - }) - .map(|(x, y)| Some(generation_fn([x, y]))) - .collect_into_vec(&mut values); + fn get(&self, x: I, y: I) -> Option<&T> { + if let Some(extent) = &self.extent { + if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { + return None; + } + } - Self { extent, values } + let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); + + self.values[index].as_ref() + } + + fn get_mut(&mut self, x: I, y: I) -> Option<&mut T> { + if let Some(extent) = &self.extent { + if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { + return None; + } + } + + let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); + + self.values[index].as_mut() + } + + fn occupied_entries(&self) -> impl Iterator { + self.values + .iter() + .filter_map(|v| v.as_ref().map(|v| (v.get_grid_index(), v))) + } + + fn occupied_entries_mut(&mut self) -> impl Iterator { + self.values + .iter_mut() + .filter_map(|v| v.as_mut().map(|v| (v.get_grid_index(), v))) } // TODO: Do I want to save None values? - pub fn save_fork(&self, base_path: PathBuf) + fn save_single_thread(&self, base_path: PathBuf) where - T: Sync + serde::Serialize, + T: serde::Serialize, { create_dir_all(&base_path).expect("Failed to create world dir"); @@ -94,7 +121,7 @@ impl> BoundingBoxGrid { } // TODO: Do I want to save None values? - pub fn par_save(&self, base_path: PathBuf) + fn par_save(&self, base_path: PathBuf) where T: Sync + serde::Serialize, { @@ -112,7 +139,7 @@ impl> BoundingBoxGrid { // todo!("Serialize the rest") } - pub fn par_load(base_path: PathBuf) -> Self + fn par_load(base_path: PathBuf) -> Self where for<'a> T: Send + serde::Deserialize<'a>, { @@ -161,6 +188,88 @@ impl> BoundingBoxGrid { Self { extent, values } } + fn insert_many( + &mut self, + positions: impl IntoIterator + Clone, + values: impl IntoIterator, + ) { + let x_min = positions + .clone() + .into_iter() + .map(|(x, _y)| x) + .min() + .unwrap(); + let x_max = positions + .clone() + .into_iter() + .map(|(x, _y)| x) + .max() + .unwrap(); + + let y_min = positions + .clone() + .into_iter() + .map(|(_x, y)| y) + .min() + .unwrap(); + let y_max = positions + .clone() + .into_iter() + .map(|(_x, y)| y) + .max() + .unwrap(); + + self.include_in_extent(x_min, y_min); + self.include_in_extent(x_max, y_max); + + for (value, (x, y)) in values.into_iter().zip(positions) { + let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); + debug_assert!(self.values[index].is_none()); + self.values[index] = Some(value); + } + } +} + +impl> BoundingBoxGrid { + pub fn new_with_filled_grid(top_left: [I; 2], bottom_right: [I; 2], generation_fn: F) -> Self + where + F: Fn([I; 2]) -> T + Sync, + T: Send, + { + let extent = Some([ + [top_left[0], bottom_right[0]], + [top_left[1], bottom_right[1]], + ]); + + let width = usize::try_from(bottom_right[0] - top_left[0]) + .expect("Check bounding box argument order") + + 1; + + let height = usize::try_from(bottom_right[1] - top_left[1]) + .expect("Check bounding box argument order") + + 1; + + let mut values = Vec::with_capacity(width * height); + + (0..(width * height)) + .into_par_iter() + .map(|v| (v % width, v / width)) + .map(|(x_offs, y_offs)| { + ( + top_left[0] + .checked_add_unsigned(x_offs.try_into().unwrap()) + .unwrap(), + top_left[1] + .checked_add_unsigned(y_offs.try_into().unwrap()) + .unwrap(), + ) + }) + .map(|(x, y)| Some(generation_fn([x, y]))) + .collect_into_vec(&mut values); + + Self { extent, values } + } + fn include_in_extent(&mut self, x: I, y: I) { if let Some(extent) = &mut self.extent { let [x_range, y_range] = extent; @@ -215,105 +324,30 @@ impl> BoundingBoxGrid { } } - pub fn get_extent(&self) -> Option<[RangeInclusive; 2]> { - self.extent.map(|v| v.map(|[start, end]| start..=end)) - } - - pub fn insert(&mut self, x: I, y: I, value: T) { - self.include_in_extent(x, y); - - let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); - debug_assert!(self.values[index].is_none()); - self.values[index] = Some(value); - } - - pub fn insert_many( - &mut self, - positions: impl IntoIterator + Clone, - values: impl IntoIterator + Clone, - ) where - T: PartialEq + Debug, - { - let x_min = positions - .clone() - .into_iter() - .map(|(x, _y)| x) - .min() - .unwrap(); - let x_max = positions - .clone() - .into_iter() - .map(|(x, _y)| x) - .max() - .unwrap(); - - let y_min = positions - .clone() - .into_iter() - .map(|(_x, y)| y) - .min() - .unwrap(); - let y_max = positions - .clone() - .into_iter() - .map(|(_x, y)| y) - .max() - .unwrap(); - - self.include_in_extent(x_min, y_min); - self.include_in_extent(x_max, y_max); - - #[cfg(debug_assertions)] - { - assert!( - positions - .clone() - .into_iter() - .zip(values.clone()) - .all(|(pos, v)| pos == v.get_grid_index()) - ); + pub(super) fn get_extent_after_insertion( + &self, + positions: impl IntoIterator, + ) -> [RangeInclusive; 2] { + let mut positions = positions.into_iter(); + let [x, y] = positions.next().unwrap(); + let extent = self.extent.unwrap_or([[x, x], [y, y]]); + let [mut x_range, mut y_range] = extent; + + for [x, y] in positions { + x_range[0] = min(x_range[0], x); + x_range[1] = max(x_range[1], x); + + y_range[0] = min(y_range[0], y); + y_range[1] = max(y_range[1], y); } - for (value, (x, y)) in values.into_iter().zip(positions) { - let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); - debug_assert!(self.values[index].is_none()); - self.values[index] = Some(value); - } + [x_range[0]..=x_range[1], y_range[0]..=y_range[1]] } - pub fn get(&self, x: I, y: I) -> Option<&T> { - if let Some(extent) = &self.extent { - if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { - return None; - } - } - - let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); - - self.values[index].as_ref() - } - - pub fn get_mut(&mut self, x: I, y: I) -> Option<&mut T> { - if let Some(extent) = &self.extent { - if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { - return None; - } - } - - let index = Self::calculate_index(self.extent.as_ref().unwrap(), [x, y]); - - self.values[index].as_mut() - } - - pub fn occupied_entries(&self) -> impl Iterator { - self.values - .iter() - .filter_map(|v| v.as_ref().map(|v| (v.get_grid_index(), v))) - } - - pub fn occupied_entries_mut(&mut self) -> impl Iterator { + pub(super) fn into_iter(self) -> impl Iterator { self.values - .iter_mut() - .filter_map(|v| v.as_mut().map(|v| (v.get_grid_index(), v))) + .into_iter() + .flatten() + .map(|chunk| (chunk.get_grid_index(), chunk)) } } diff --git a/src/frontend/world/sparse_grid/dynamic.rs b/src/frontend/world/sparse_grid/dynamic.rs new file mode 100644 index 0000000..4315d33 --- /dev/null +++ b/src/frontend/world/sparse_grid/dynamic.rs @@ -0,0 +1,243 @@ +use std::{fs::create_dir_all, iter}; + +use itertools::Either; + +use crate::{ + frontend::world::sparse_grid::{ + GetGridIndex, SparseGrid, bounding_box_grid::BoundingBoxGrid, map_grid::BtreeMapGrid, + }, + saving::{load_at, save_at, save_at_fork}, +}; + +#[cfg(feature = "client")] +use egui_show_info_derive::ShowInfo; +#[cfg(feature = "client")] +use get_size2::GetSize; + +// If more than every 20th chunk is ininhabitat, switch to map +// TODO: Find a good value for this +const SWITCH_RATIO: usize = 20; + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub(crate) struct DynamicGrid { + pub(crate) num_chunks: usize, + store: Backing, +} + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +enum Backing { + BoundingBoxGrid(BoundingBoxGrid), + Map(BtreeMapGrid), +} + +type I = i32; +impl + 'static> SparseGrid for DynamicGrid { + fn new() -> Self { + Self { + num_chunks: 0, + store: Backing::BoundingBoxGrid(BoundingBoxGrid::new()), + } + } + + fn get_extent(&self) -> Option<[std::ops::RangeInclusive; 2]> { + match &self.store { + Backing::BoundingBoxGrid(grid) => grid.get_extent(), + Backing::Map(grid) => grid.get_extent(), + } + } + + fn get_default(&mut self, x: I, y: I) -> &T + where + T: Default, + { + unimplemented!() + } + + fn get(&self, x: I, y: I) -> Option<&T> { + match &self.store { + Backing::BoundingBoxGrid(grid) => grid.get(x, y), + Backing::Map(grid) => grid.get(x, y), + } + } + + fn get_mut(&mut self, x: I, y: I) -> Option<&mut T> { + match &mut self.store { + Backing::BoundingBoxGrid(grid) => grid.get_mut(x, y), + Backing::Map(grid) => grid.get_mut(x, y), + } + } + + fn insert(&mut self, x: I, y: I, value: T) -> Option { + self.num_chunks += 1; + match &mut self.store { + Backing::BoundingBoxGrid(grid) => { + let [x_ext, y_ext] = grid.get_extent_after_insertion(iter::once([x, y])); + + let new_size = x_ext.count() * y_ext.count(); + + if self.num_chunks * SWITCH_RATIO < new_size { + log::error!("SWITCHING TO SLOW WORLD REPRESENTATION TO SAVE RAM!"); + let mut ret = None; + take_mut::take(&mut self.store, |store| { + let Backing::BoundingBoxGrid(grid) = store else { + unreachable!() + }; + let mut new_grid = BtreeMapGrid::new_with_values( + grid.get_extent().unwrap(), + grid.into_iter(), + ); + ret = Some(new_grid.insert(x, y, value)); + Backing::Map(new_grid) + }); + + ret.unwrap() + } else { + grid.insert(x, y, value) + } + }, + Backing::Map(grid) => grid.insert(x, y, value), + } + } + + fn insert_many( + &mut self, + positions: impl IntoIterator + Clone, + values: impl IntoIterator, + ) { + self.num_chunks += positions.clone().into_iter().count(); + match &mut self.store { + Backing::BoundingBoxGrid(grid) => { + let [x_ext, y_ext] = grid + .get_extent_after_insertion(positions.clone().into_iter().map(|v| v.into())); + + let new_size = x_ext.count() * y_ext.count(); + + if self.num_chunks * SWITCH_RATIO < new_size { + log::error!("SWITCHING TO SLOW WORLD REPRESENTATION TO SAVE RAM!"); + take_mut::take(&mut self.store, |store| { + let Backing::BoundingBoxGrid(grid) = store else { + unreachable!() + }; + let mut new_grid = BtreeMapGrid::new_with_values( + grid.get_extent().unwrap(), + grid.into_iter(), + ); + new_grid.insert_many(positions, values); + Backing::Map(new_grid) + }); + } else { + grid.insert_many(positions, values) + } + }, + Backing::Map(grid) => grid.insert_many(positions, values), + } + } + + fn insert_deduplicate(&mut self, x: I, y: I, value: T) -> Option + where + T: PartialEq + Default, + { + unimplemented!() + } + + fn occupied_entries(&self) -> impl Iterator { + match &self.store { + Backing::BoundingBoxGrid(grid) => Either::Left(grid.occupied_entries()), + Backing::Map(grid) => Either::Right(grid.occupied_entries()), + } + } + + fn occupied_entries_mut(&mut self) -> impl Iterator { + match &mut self.store { + Backing::BoundingBoxGrid(grid) => Either::Left(grid.occupied_entries_mut()), + Backing::Map(grid) => Either::Right(grid.occupied_entries_mut()), + } + } + + fn save_single_thread(&self, base_path: std::path::PathBuf) + where + T: serde::Serialize, + I: serde::Serialize, + { + create_dir_all(&base_path).expect("Failed to create world dir"); + + let is_map = match &self.store { + Backing::BoundingBoxGrid(_) => false, + Backing::Map(_) => true, + }; + + save_at_fork(&is_map, base_path.join("is_map")); + + match &self.store { + Backing::BoundingBoxGrid(grid) => { + grid.save_single_thread(base_path); + }, + Backing::Map(grid) => { + grid.save_single_thread(base_path); + }, + } + } + + fn par_save(&self, base_path: std::path::PathBuf) + where + T: Send + Sync + serde::Serialize, + I: Send + Sync + serde::Serialize, + { + create_dir_all(&base_path).expect("Failed to create world dir"); + + let is_map = match &self.store { + Backing::BoundingBoxGrid(_) => false, + Backing::Map(_) => true, + }; + + save_at(&is_map, base_path.join("is_map")); + + match &self.store { + Backing::BoundingBoxGrid(grid) => { + grid.par_save(base_path); + }, + Backing::Map(grid) => { + grid.par_save(base_path); + }, + } + } + + fn par_load(base_path: std::path::PathBuf) -> Self + where + for<'a> T: Send + serde::Deserialize<'a>, + for<'a> I: Send + serde::Deserialize<'a>, + { + let is_map = load_at::(base_path.join("is_map")); + + let store = match is_map { + true => Backing::Map(BtreeMapGrid::par_load(base_path)), + false => Backing::BoundingBoxGrid(BoundingBoxGrid::par_load(base_path)), + }; + + let count = match &store { + Backing::BoundingBoxGrid(grid) => grid.occupied_entries().count(), + Backing::Map(grid) => grid.occupied_entries().count(), + }; + + Self { + num_chunks: count, + store, + } + } +} + +impl + 'static> DynamicGrid { + pub fn new_with_filled_grid(top_left: [I; 2], bottom_right: [I; 2], generation_fn: F) -> Self + where + F: Fn([I; 2]) -> T + Sync, + T: Send, + { + let store = BoundingBoxGrid::new_with_filled_grid(top_left, bottom_right, generation_fn); + Self { + num_chunks: (store.occupied_entries().count()), + store: Backing::BoundingBoxGrid(store), + } + } +} diff --git a/src/frontend/world/sparse_grid/map_grid.rs b/src/frontend/world/sparse_grid/map_grid.rs new file mode 100644 index 0000000..91f3741 --- /dev/null +++ b/src/frontend/world/sparse_grid/map_grid.rs @@ -0,0 +1,186 @@ +#[cfg(feature = "client")] +use egui_show_info_derive::ShowInfo; +#[cfg(feature = "client")] +use get_size2::GetSize; +use itertools::Itertools; +use rayon::iter::IntoParallelIterator; +use rayon::iter::ParallelIterator; +use std::cmp::{max, min}; +use std::collections::BTreeMap; +use std::fs::File; +use std::fs::create_dir_all; +use std::hash::Hash; +use std::ops::RangeInclusive; + +use crate::frontend::world::sparse_grid::{FILE_CHUNK_SIZE, SparseGrid}; +use crate::saving::load_at; +use crate::saving::save_at_fork; + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BtreeMapGrid { + extent: Option<[[I; 2]; 2]>, + values: BTreeMap<(I, I), T>, +} + +impl SparseGrid for BtreeMapGrid { + fn new() -> Self { + Self { + extent: None, + values: Default::default(), + } + } + + fn get_extent(&self) -> Option<[RangeInclusive; 2]> { + self.extent.map(|v| v.map(|[start, end]| start..=end)) + } + + fn get_default(&mut self, x: I, y: I) -> &T + where + T: Default, + { + self.include_in_extent(x, y); + self.values.entry((x, y)).or_default() + } + + fn get(&self, x: I, y: I) -> Option<&T> { + // if let Some(extent) = &self.extent { + // if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { + // return None; + // } + // } + + self.values.get(&(x, y)) + } + + fn get_mut(&mut self, x: I, y: I) -> Option<&mut T> { + self.values.get_mut(&(x, y)) + } + + fn insert(&mut self, x: I, y: I, value: T) -> Option { + self.include_in_extent(x, y); + self.values.insert((x, y), value) + } + + fn insert_deduplicate(&mut self, x: I, y: I, value: T) -> Option + where + T: PartialEq + Default, + { + self.include_in_extent(x, y); + if value == T::default() { + self.values.remove(&(x, y)) + } else { + self.values.insert((x, y), value) + } + } + + fn occupied_entries(&self) -> impl Iterator { + self.values + .iter() + // .sorted_by_key(|(k, _)| **k) + .map(|(a, b)| (*a, b)) + } + + fn occupied_entries_mut(&mut self) -> impl Iterator { + self.values + .iter_mut() + // .sorted_by_key(|(k, _)| **k) + .map(|(a, b)| (*a, b)) + } + + fn save_single_thread(&self, base_path: std::path::PathBuf) + where + T: serde::Serialize, + I: serde::Serialize, + { + create_dir_all(&base_path).expect("Failed to create world dir"); + + self.values + .iter() + .chunks(FILE_CHUNK_SIZE) + .into_iter() + .enumerate() + .for_each(|(i, chunks)| { + save_at_fork(&chunks.collect_vec(), base_path.join(format!("chunk-{i}"))); + }); + } + + fn par_save(&self, base_path: std::path::PathBuf) + where + T: Send + Sync + serde::Serialize, + I: Send + Sync + serde::Serialize, + { + // FIXME: This is single threaded + self.values + .iter() + .chunks(FILE_CHUNK_SIZE) + .into_iter() + .enumerate() + .for_each(|(i, chunks)| { + save_at_fork(&chunks.collect_vec(), base_path.join(format!("chunk-{i}"))); + }); + } + + fn par_load(base_path: std::path::PathBuf) -> Self + where + for<'a> T: Send + serde::Deserialize<'a>, + for<'a> I: Send + serde::Deserialize<'a>, + { + let values: BTreeMap<_, _> = (0..) + .map(|chunk_id| base_path.join(format!("chunk-{chunk_id}"))) + .take_while(|path| { + // FIXME: Use another function + File::open(path).is_ok() + }) + .collect_vec() + .into_par_iter() + .map(|file_path| load_at::>(file_path)) + .flatten() + .collect(); + + let top_left = values + .iter() + .map(|value| *value.0) + .reduce(|a, b| (min(a.0, b.0), min(a.1, b.1))); + + let bottom_right = values + .iter() + .map(|value| *value.0) + .reduce(|a, b| (max(a.0, b.0), max(a.1, b.1))); + + let extent = match (top_left, bottom_right) { + (None, None) => None, + (Some((min_x, min_y)), Some((max_x, max_y))) => Some([[min_x, max_x], [min_y, max_y]]), + + _ => unreachable!(), + }; + + Self { extent, values } + } +} + +impl BtreeMapGrid { + pub(super) fn new_with_values( + extent: [RangeInclusive; 2], + values: impl IntoIterator, + ) -> Self { + Self { + extent: Some(extent.map(|extent| [*extent.start(), *extent.end()])), + values: BTreeMap::from_iter(values), + } + } + + fn include_in_extent(&mut self, x: I, y: I) { + if let Some(extent) = &mut self.extent { + let [x_range, y_range] = extent; + + x_range[0] = min(x_range[0], x); + x_range[1] = max(x_range[1], x); + + y_range[0] = min(y_range[0], y); + y_range[1] = max(y_range[1], y); + } else { + self.extent = Some([[x, x], [y, y]]); + } + } +} diff --git a/src/frontend/world/sparse_grid/mod.rs b/src/frontend/world/sparse_grid/mod.rs index 0b13cae..aecd340 100644 --- a/src/frontend/world/sparse_grid/mod.rs +++ b/src/frontend/world/sparse_grid/mod.rs @@ -1,103 +1,55 @@ -#[cfg(feature = "client")] -use egui_show_info_derive::ShowInfo; -#[cfg(feature = "client")] -use get_size2::GetSize; -use std::cmp::{max, min}; -use std::collections::BTreeMap; -use std::hash::Hash; -use std::ops::RangeInclusive; +use std::{ops::RangeInclusive, path::PathBuf}; pub mod bounding_box_grid; // pub mod perfect_grid; +pub mod dynamic; +pub mod map_grid; -#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct SparseGrid { - extent: Option<[[I; 2]; 2]>, - values: BTreeMap<(I, I), T>, -} +const FILE_CHUNK_SIZE: usize = 100_000; -impl SparseGrid { - pub fn new() -> Self +pub trait SparseGrid { + fn new() -> Self; + fn get_extent(&self) -> Option<[RangeInclusive; 2]>; + fn get_default(&mut self, x: I, y: I) -> &T where - I: Default, - { - Self { - extent: None, - values: Default::default(), - } - } + T: Default; - fn include_in_extent(&mut self, x: I, y: I) { - if let Some(extent) = &mut self.extent { - let [x_range, y_range] = extent; + fn get(&self, x: I, y: I) -> Option<&T>; + fn get_mut(&mut self, x: I, y: I) -> Option<&mut T>; + fn insert(&mut self, x: I, y: I, value: T) -> Option; - x_range[0] = min(x_range[0], x); - x_range[1] = max(x_range[1], x); - - y_range[0] = min(y_range[0], y); - y_range[1] = max(y_range[1], y); - } else { - self.extent = Some([[x, x], [y, y]]); - } - } - - pub fn get_extent(&self) -> Option<[RangeInclusive; 2]> { - self.extent.map(|v| v.map(|[start, end]| start..=end)) - } - - pub fn get_default(&mut self, x: I, y: I) -> &T + fn insert_deduplicate(&mut self, x: I, y: I, value: T) -> Option where - T: Default, - { - self.include_in_extent(x, y); - self.values.entry((x, y)).or_default() - } + T: PartialEq + Default; - pub fn get(&self, x: I, y: I) -> Option<&T> { - if let Some(extent) = &self.extent { - if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { - return None; - } - } + fn occupied_entries(&self) -> impl Iterator; + fn occupied_entries_mut(&mut self) -> impl Iterator; - self.values.get(&(x, y)) - } - - pub fn get_mut(&mut self, x: I, y: I) -> Option<&mut T> { - self.values.get_mut(&(x, y)) - } + fn save_single_thread(&self, base_path: PathBuf) + where + T: serde::Serialize, + I: serde::Serialize; - pub fn insert(&mut self, x: I, y: I, value: T) -> Option { - self.include_in_extent(x, y); - self.values.insert((x, y), value) - } + // TODO: Do I want to save None values? + fn par_save(&self, base_path: PathBuf) + where + T: Send + Sync + serde::Serialize, + I: Send + Sync + serde::Serialize; - pub fn insert_deduplicate(&mut self, x: I, y: I, value: T) -> Option + fn par_load(base_path: PathBuf) -> Self where - T: PartialEq + Default, - { - self.include_in_extent(x, y); - if value == T::default() { - self.values.remove(&(x, y)) - } else { - self.values.insert((x, y), value) + for<'a> T: Send + serde::Deserialize<'a>, + for<'a> I: Send + serde::Deserialize<'a>; + + fn insert_many( + &mut self, + positions: impl IntoIterator + Clone, + values: impl IntoIterator, + ) { + for (v, pos) in values.into_iter().zip(positions) { + self.insert(pos.0, pos.1, v); } } - - pub fn occupied_entries(&self) -> impl Iterator { - self.values - .iter() - // .sorted_by_key(|(k, _)| **k) - .map(|(a, b)| (*a, b)) - } - - pub fn occupied_entries_mut(&mut self) -> impl Iterator { - self.values - .iter_mut() - // .sorted_by_key(|(k, _)| **k) - .map(|(a, b)| (*a, b)) - } } pub trait GetGridIndex { diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index 8acbb38..c1976fa 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -1,3 +1,5 @@ +use crate::frontend::world::sparse_grid::SparseGrid; +use crate::frontend::world::sparse_grid::dynamic::DynamicGrid; use crate::frontend::world::tile::belt_placement::expected_belt_state; use crate::mining_drill; use crate::mining_drill::AddMinerError; @@ -106,7 +108,7 @@ const_assert!(CHUNK_SIZE * CHUNK_SIZE - 1 <= u8::MAX as u16); #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct Chunk { +pub struct Chunk { base_pos: (i32, i32), pub floor_tiles: Option>, chunk_tile_to_entity_into: Option>, @@ -173,7 +175,7 @@ pub struct World { // TODO: I don´t think I want FP pub players: Vec, - chunks: BoundingBoxGrid>, + pub chunks: DynamicGrid>, belt_lookup: BeltIdLookup, belt_recieving_input_directions: HashMap>, @@ -1536,7 +1538,7 @@ impl World World World Self { - let grid = BoundingBoxGrid::new_with_filled_grid( + let grid = DynamicGrid::new_with_filled_grid( Self::get_chunk_pos_for_tile(top_left).into(), Self::get_chunk_pos_for_tile(bottom_right).into(), |[x, y]| Chunk { @@ -1686,6 +1688,18 @@ impl World + Clone) { + self.chunks.insert_many( + chunks.clone(), + chunks.into_iter().map(|pos| Chunk { + base_pos: (pos.0 * i32::from(CHUNK_SIZE), pos.1 * i32::from(CHUNK_SIZE)), + floor_tiles: None, + chunk_tile_to_entity_into: None, + entities: vec![], + }), + ) + } + pub fn get_ore_type_at_pos(&self, pos: Position) -> Option> { self.ore_lookup .ore_lookup @@ -4084,9 +4098,11 @@ impl World } impl List { - pub fn tick( + pub fn tick(&mut self) { + self.zero_index += 1; + self.zero_index %= self.lists.len(); + } + + pub fn get( &mut self, ) -> ( FinishedMovingLists<'_, SWING_DIR, ITEM_FLOW_DIR>, ReinsertionLists<'_, SWING_DIR, ITEM_FLOW_DIR>, ) { - self.zero_index += 1; - self.zero_index %= self.lists.len(); let (second, rest) = self.lists.split_at_mut(self.zero_index); let ([finished], first) = rest.split_at_mut(1) else { @@ -120,8 +123,11 @@ impl<'a> FinishedMovingLists<'a, { Dir::BeltToStorage }, { Dir::BeltToStorage }> reinsertion_list.reinsert(inserter.movetime.into(), *inserter); false } else { - if let Some(wait_list) = wait_list { - if let Some(empty) = wait_list.inserters.iter_mut().find(|v| v.is_none()) { + if let Some((wait_list, wait_list_needed)) = wait_list { + if let Some((pos, empty)) = wait_list.inserters.iter_mut().enumerate().find(|(_i, v)| v.is_none()) { + if pos == 0 { + *wait_list_needed = inserter.current_hand; + } *empty = Some(InserterWithBelts { current_hand: inserter.current_hand, max_hand: inserter.max_hand_size.into(), @@ -165,8 +171,12 @@ impl<'a> FinishedMovingLists<'a, { Dir::BeltToStorage }, { Dir::StorageToBelt }> reinsertion_list.reinsert(inserter.movetime.into(), *inserter); false } else { - if let Some(wait_list) = wait_list { - if let Some(empty) = wait_list.inserters.iter_mut().find(|v| v.is_none()) { + if let Some((wait_list, wait_list_needed)) = wait_list { + if let Some((pos, empty)) = wait_list.inserters.iter_mut().enumerate().find(|(_i, v)| v.is_none()) { + if pos == 0 { + *wait_list_needed = inserter.max_hand_size - inserter.current_hand; + + } *empty = Some(InserterWithBelts { current_hand: inserter.current_hand, max_hand: inserter.max_hand_size.into(), diff --git a/src/inserter/storage_storage_with_buckets_indirect.rs b/src/inserter/storage_storage_with_buckets_indirect.rs index 1d5efe6..f046427 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -343,9 +343,13 @@ impl BucketedStorageStorageInserterStore { reinsert: true, } } else { - if let Some(wait_list) = wait_list { - if let Some(pos) = wait_list.inserters.iter_mut().find(|slot| slot.is_none()) { - *pos = Some(WaitListInserter { + if let Some((wait_list, wait_list_needed)) = wait_list { + if let Some((pos, empty)) = wait_list.inserters.iter_mut().enumerate().find(|(_i, v)| v.is_none()) { + if pos == 0 { + *wait_list_needed = bucket_data.max_hand_size - bucket_data.current_hand; + } + + *empty = Some(WaitListInserter { current_hand: bucket_data.current_hand, max_hand: bucket_data.max_hand_size.try_into().unwrap(), movetime: movetime, @@ -412,9 +416,13 @@ impl BucketedStorageStorageInserterStore { reinsert: true, } } else { - if let Some(wait_list) = wait_list { - if let Some(pos) = wait_list.inserters.iter_mut().find(|slot| slot.is_none()) { - *pos = Some(WaitListInserter { + if let Some((wait_list, wait_list_needed)) = wait_list { + if let Some((pos, empty)) = wait_list.inserters.iter_mut().enumerate().find(|(_i, v)| v.is_none()) { + if pos == 0 { + *wait_list_needed = bucket_data.current_hand; + } + + *empty = Some(WaitListInserter { current_hand: bucket_data.current_hand, max_hand: bucket_data.max_hand_size.try_into().unwrap(), movetime: movetime, diff --git a/src/lib.rs b/src/lib.rs index 7231be6..f258234 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -271,6 +271,8 @@ enum GameCreationInfo { SolarField(Watt, Position), + TrainRide, + FromBP(PathBuf), } @@ -290,7 +292,7 @@ fn run_integrated_server( let local_addr = "127.0.0.1:57267"; let cancel: Arc = Default::default(); - // accept_continously(local_addr, connections.clone(), cancel.clone()).unwrap(); + accept_continously(local_addr, connections.clone(), cancel.clone()).unwrap(); match data_store { data::DataStoreOptions::ItemU8RecipeU8(data_store) => { @@ -342,6 +344,9 @@ fn run_integrated_server( GameCreationInfo::LotsOfBelts => { GameState::new_with_lots_of_belts(progress, &data_store) }, + GameCreationInfo::TrainRide => { + GameState::new_with_world_train_ride(progress, &data_store) + }, GameCreationInfo::FromBP(path) => GameState::new_with_bp(&data_store, path), @@ -469,9 +474,7 @@ fn run_client(remote_addr: SocketAddr) -> (LoadedGame, Arc, Sender PowerGridStorage, - List<{ Dir::StorageToBelt }, { Dir::BeltToStorage }>, - ), - ( - List<{ Dir::BeltToStorage }, { Dir::StorageToBelt }>, - List<{ Dir::StorageToBelt }, { Dir::StorageToBelt }>, - ), + FinishedMovingLists<'_, { Dir::BeltToStorage }, { Dir::BeltToStorage }>, + ReinsertionLists<'_, { Dir::StorageToBelt }, { Dir::BeltToStorage }>, + ReinsertionLists<'_, { Dir::StorageToBelt }, { Dir::StorageToBelt }>, + FinishedMovingLists<'_, { Dir::BeltToStorage }, { Dir::StorageToBelt }>, )], data_store: &DataStore, @@ -1153,12 +1150,16 @@ impl PowerGridStorage { - let ((_a, belt_storage), (_c, storage_belt)) = - &mut belt_storage_reinsertion_list - [inserter.item.into_usize()]; + let ( + belt_storage_exit_outgoing, + belt_storage_reinsertion_incoming, + storage_belt_reinsertion_outgoing, + storage_belt_exit_incoming, + ) = &mut belt_storage_reinsertion_list + [inserter.item.into_usize()]; if self_is_source { - storage_belt.reinsert( + storage_belt_reinsertion_outgoing.reinsert( inserter.movetime, BeltStorageInserterInMovement { current_hand: inserter.max_hand, @@ -1173,7 +1174,7 @@ impl PowerGridStorage= real_tile_x_end); + assert!(tile_y_end >= real_tile_y_end); + let size = [ + (((map_tile_size) * pixel_to_tile - (tile_x_end - real_tile_x_end) as u32) + / pixel_to_tile) as usize, + (((map_tile_size) * pixel_to_tile - (tile_y_end - real_tile_y_end) as u32) + / pixel_to_tile) as usize, + ]; + + assert!(size[0] > 0); + assert!(size[1] > 0); + + renderer.create_runtime_texture_if_missing(tile_texture_id, size, || { + let ret = collect_colors( + world, + [tile_x as u32, tile_y as u32], + size.map(|v| v as u32), + map_tile_size, + pixel_to_tile, + data_store, + ); + + assert_eq!(Borrow::<[u8]>::borrow(&ret).len(), size[0] * size[1] * 4); + + ret + }); if let Some(allowed_time) = allowed_time { if start_time.elapsed() > allowed_time { @@ -174,6 +194,7 @@ static DEDUP_MAP: LazyLock>> = LazyLock::new(|| { fn collect_colors( world: &World, [tile_x, tile_y]: [u32; 2], + [size_x, size_y]: [u32; 2], map_tile_size: u32, pixel_to_tile: u32, data_store: &crate::data::DataStore, @@ -187,20 +208,20 @@ fn collect_colors( as i32, }, Position { - x: (i32::try_from((tile_x + 1) * map_tile_size * pixel_to_tile).unwrap() + x: (i32::try_from((tile_x * map_tile_size + size_x) * pixel_to_tile).unwrap() - 1_000_000) as i32, - y: (i32::try_from((tile_y + 1) * map_tile_size * pixel_to_tile).unwrap() + y: (i32::try_from((tile_y * map_tile_size + size_y) * pixel_to_tile).unwrap() - 1_000_000) as i32, }, ) .unwrap_or(true) { ColorResult::Generated(bytemuck::cast_vec( - ((tile_y * map_tile_size)..((tile_y + 1) * map_tile_size)) + ((tile_y * map_tile_size)..(tile_y * map_tile_size + size_y)) .into_par_iter() .flat_map_iter(|y_pos| { std::iter::repeat(y_pos) - .zip((tile_x * map_tile_size)..((tile_x + 1) * map_tile_size)) + .zip((tile_x * map_tile_size)..(tile_x * map_tile_size + size_x)) }) .map(|(y_pos, x_pos)| { let x_pos_world = @@ -208,6 +229,9 @@ fn collect_colors( let y_pos_world = (i32::try_from(y_pos * pixel_to_tile).unwrap() - 1_000_000) as i32; + assert!(x_pos_world <= 1_000_000); + assert!(y_pos_world <= 1_000_000); + let color = world.get_entity_color( Position { x: x_pos_world, @@ -221,12 +245,12 @@ fn collect_colors( .collect(), )) } else { - match DEDUP_MAP.get(&(map_tile_size * map_tile_size)) { + match DEDUP_MAP.get(&(size_x * size_y)) { Some(cached_alloc) => ColorResult::Const(&cached_alloc), None => ColorResult::Generated(bytemuck::cast_vec(vec![ Color32::BLACK; - map_tile_size as usize - * map_tile_size as usize + size_x as usize + * size_y as usize ])), } }; @@ -384,14 +408,30 @@ pub fn render_map_view( ); if renderer.has_runtime_texture(texture_id) { - map_layer.draw_runtime_texture( - texture_id, - DrawInstance { - position: [tile_draw_offs.0, tile_draw_offs.1], - size: [(map_tile_size * pixel_to_tile) as f32; 2], - animation_frame: 0, - }, - ); + let tile_x_end = + ((tile_x + 1) * map_tile_size as i32 * pixel_to_tile as i32) - 1_000_000; + let real_tile_x_end = min(tile_x_end, 1_000_000); + let tile_y_end = + ((tile_y + 1) * map_tile_size as i32 * pixel_to_tile as i32) - 1_000_000; + let real_tile_y_end = min(tile_y_end, 1_000_000); + assert!(tile_x_end >= real_tile_x_end); + assert!(tile_y_end >= real_tile_y_end); + let size = [ + ((map_tile_size) * pixel_to_tile - (tile_x_end - real_tile_x_end) as u32) + as f32, + ((map_tile_size) * pixel_to_tile - (tile_y_end - real_tile_y_end) as u32) + as f32, + ]; + if size[0] > 0.0 && size[1] > 0.0 { + map_layer.draw_runtime_texture( + texture_id, + DrawInstance { + position: [tile_draw_offs.0, tile_draw_offs.1], + size, + animation_frame: 0, + }, + ); + } } // map_layer.draw_sprite( // &Sprite::new(Texture::default()), diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index b60d7f0..6e1c2ad 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -3203,6 +3203,8 @@ pub fn render_ui< game_state_ref.simulation_state.factory.belts )); }); + + ui.label(&format!("Number of generated chunks: {}", game_state_ref.world.chunks.num_chunks)); }); Window::new("UPS").default_open(true).show(ctx, |ui| { diff --git a/src/storage_list.rs b/src/storage_list.rs index c23d5c9..5952ce1 100644 --- a/src/storage_list.rs +++ b/src/storage_list.rs @@ -55,28 +55,35 @@ impl<'a> MaxInsertionLimit<'a> { #[derive(Debug)] pub enum InserterWaitLists<'a> { - PerMachine(&'a mut [InserterWaitList]), + PerMachine(&'a mut [InserterWaitList], &'a mut [ITEMCOUNTTYPE]), None, } -impl<'a> Index for InserterWaitLists<'a> { - type Output = InserterWaitList; - fn index(&self, index: usize) -> &Self::Output { - match self { - InserterWaitLists::PerMachine(items) => &items[index], - InserterWaitLists::None => panic!("No list"), - } - } -} -impl<'a> IndexMut for InserterWaitLists<'a> { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - self.get_mut(index).unwrap() - } -} +// impl<'a> Index for InserterWaitLists<'a> { +// type Output = (&'a mut InserterWaitList, &'a mut ITEMCOUNTTYPE); +// fn index<'b>(&'b self, index: usize) -> &'b Self::Output { +// match self { +// InserterWaitLists::PerMachine(items, min_needed) => { +// &(&mut items[index], &mut min_needed[index]) +// }, +// InserterWaitLists::None => panic!("No list"), +// } +// } +// } +// impl<'a> IndexMut for InserterWaitLists<'a> { +// fn index_mut(&mut self, index: usize) -> &mut Self::Output { +// &mut self.get_mut(index).unwrap() +// } +// } impl<'a> InserterWaitLists<'a> { - fn get_mut(&mut self, index: usize) -> Option<&mut InserterWaitList> { + fn get_mut(&mut self, index: usize) -> Option<(&mut InserterWaitList, &mut ITEMCOUNTTYPE)> { match self { - InserterWaitLists::PerMachine(items) => items.get_mut(index), + InserterWaitLists::PerMachine(items, min_needed) => { + match (items.get_mut(index), min_needed.get_mut(index)) { + (Some(a), Some(b)) => Some((a, b)), + _ => None, + } + }, InserterWaitLists::None => None, } } @@ -155,7 +162,7 @@ pub fn index<'a, 'b, RecipeIdxType: IdxTrait>( ) -> ( &'a ITEMCOUNTTYPE, &'a mut ITEMCOUNTTYPE, - Option<&'a mut InserterWaitList>, + Option<(&'a mut InserterWaitList, &'a mut ITEMCOUNTTYPE)>, ) { let first_grid_offs_in_grids = static_size.div_ceil(grid_size); @@ -208,7 +215,7 @@ pub fn index_fake_union<'a, 'b>( ) -> ( &'a ITEMCOUNTTYPE, &'a mut ITEMCOUNTTYPE, - Option<&'a mut InserterWaitList>, + Option<(&'a mut InserterWaitList, &'a mut ITEMCOUNTTYPE)>, ) { let (outer, inner) = storage_id.into_inner_and_outer_indices_with_statics_at_zero(grid_size); @@ -1523,7 +1530,7 @@ fn all_assembler_storages<'a, 'b, ItemIdxType: IdxTrait, RecipeIdxType: IdxTrait ] }), ) - .map(|(a, b, c, d, e)| (a, b, c, d, InserterWaitLists::PerMachine(e))); + .map(|(a, b, c, d, e)| (a, b, c, d, InserterWaitLists::PerMachine(e.0, e.1))); i } diff --git a/test_blueprints/iron_generation_test.bp b/test_blueprints/iron_generation_test.bp new file mode 100644 index 0000000..5459903 --- /dev/null +++ b/test_blueprints/iron_generation_test.bp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:159caefe3769289f547fadd71bcf57bc4caf6a580b690375e5f6eba564d4a585 +size 140 diff --git a/tested_ideas/assembler_waitlist.md b/tested_ideas/assembler_waitlist.md new file mode 100644 index 0000000..175a43a --- /dev/null +++ b/tested_ideas/assembler_waitlist.md @@ -0,0 +1,22 @@ +Ensure the assembler waitlist is only read if it produced anything. + +easiest to test by making the "generate" recipes slow and duplicating lots of them and observing the patterns in lag spikes. + +Issue: +Checking the waitlist is a very significant part of the runtime of the assembler update. +Figure out a way to improve this. +Maybe a bitvec to store if there are any inserters in the list? This would be set whenever an inserter enters the waitlist (adding an additional cache line read + write), but would allow skipping the waitlist check if said bit is not set. +Depending on the cache hit rate of this bitset, it could be beneficial or not, since checking this bit is a cacheline (theoretically) on its own. +In practice this seems unlikely, since a cacheline fits 64 * 8 = 512 flags. +The higher the crafting speed/(lower the recipe craft time) the more important this optimization becomes, and also the more likely the bitvec is to be already cached, reducing random access costs. + +This is of course not beneficial if each assembler always has (at least one) inserter per item in the waitlist whenever it finished a craft. + + +If I implement this, one thing I should test is, not using a bitvec, but instead just a bool slice, since it should not require reading the memory when setting the flag. + +This flag would be (potentially) reset, whenever the assembler crafts and subsequently checks the waitlist. Since we already checked the waitlist, checking if it is empty, should not incur any additional memory bandwidth costs. + + +Another thing, that could theoretically help is reducing the size of an inserter waitlist to 32 bytes (maybe by reducing the capacity to 2), and evaluating if this is an improvement or not. +My guess is it is unlikely, since the additional spinning inserters will kill all improvements we could get. \ No newline at end of file diff --git a/tested_ideas/belt_inserter_list.md b/tested_ideas/belt_inserter_list.md new file mode 100644 index 0000000..8075c7b --- /dev/null +++ b/tested_ideas/belt_inserter_list.md @@ -0,0 +1,6 @@ +One quirk I noticed while profiling is, I got no samples for the branch where the inserter was outgoing +and we are at less than old_first_free. + + +While that might be normal (due to this situation immediately stopping itself from happening next tick), +investigate to make sure. \ No newline at end of file From 658ff81b99cb9a8c968cb11dced4cb9a6a10ccc4 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 3 Dec 2025 21:39:03 +0100 Subject: [PATCH 057/152] Revert the debug tile coloring --- src/frontend/world/tile.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index c1976fa..ecd597f 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -4098,11 +4098,9 @@ impl World Date: Thu, 4 Dec 2025 16:45:24 +0100 Subject: [PATCH 058/152] Do not panic if forking fails --- src/saving/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/saving/mod.rs b/src/saving/mod.rs index f274d35..0d34da5 100644 --- a/src/saving/mod.rs +++ b/src/saving/mod.rs @@ -322,9 +322,16 @@ pub fn save_with_fork( let (send, recv) = interprocess::unnamed_pipe::pipe().expect("Failed to create unnamed pipe"); match fork::fork() { - Ok(fork::Fork::Parent(child_pid)) => return Some(recv), + Ok(fork::Fork::Parent(child_pid)) => { + log::info!("Started saving fork with pid {}", child_pid); + return Some(recv); + }, Ok(fork::Fork::Child) => {}, - Err(e) => panic!("Failed to fork!"), + Err(e) => { + log::error!("Saving with fork failed: Unable to create fork: {}", e); + save_components(world, simulation_state, aux_data, data_store); + return None; + }, } save_components_fork_safe(world, simulation_state, aux_data, data_store, send); From ac5afc4254b7be7087d046a5da6c8bcc99161a31 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 16 Dec 2025 22:34:17 +0100 Subject: [PATCH 059/152] Add assembler craft tracking as an optional feature --- Cargo.toml | 1 + src/assembler/bucketed.rs | 4 +++ src/assembler/mod.rs | 3 +++ src/assembler/simd.rs | 50 ++++++++++++++++++++++++++++++++--- src/rendering/render_world.rs | 9 +++++++ 5 files changed, 63 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f922fae..065d457 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -139,3 +139,4 @@ dhat-heap = [] profiler = ["profiling/profile-with-puffin"] client = [ "dep:eframe", "dep:egui", "dep:egui_extras", "dep:egui_plot", "dep:puffin_egui", "dep:egui_graphs", "dep:egui-show-info", "dep:egui-show-info-derive", "dep:tilelib", "dep:image", "dep:rfd", "dep:get-size2"] logging = ["simple_logger"] +assembler-craft-tracking = [] \ No newline at end of file diff --git a/src/assembler/bucketed.rs b/src/assembler/bucketed.rs index 3699464..463f6aa 100644 --- a/src/assembler/bucketed.rs +++ b/src/assembler/bucketed.rs @@ -482,6 +482,10 @@ impl { pub prod_mod: f32, pub power_consumption_mod: f32, pub base_power_consumption: Watt, + + #[cfg(feature = "assembler-craft-tracking")] + pub times_craft_finished: u32, } impl< diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index 3657b7e..f5472f6 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -201,6 +201,10 @@ pub struct MultiAssemblerStore< #[serde(with = "arrays")] waitlists_outputs_needed: [Box<[ITEMCOUNTTYPE]>; NUM_OUTPUTS], holes: Vec, + + #[cfg(feature = "assembler-craft-tracking")] + number_of_crafts_finished: Vec, + positions: Box<[Position]>, types: Box<[u8]>, len: usize, @@ -550,12 +554,21 @@ impl // ); // } - let has_produced = timer_mask | prod_timer_mask; - // FIXME: We are missing production if main and prod tick are at the same time! - for (i, has_produced) in has_produced.to_array().into_iter().enumerate() { - if has_produced { + for (i, (has_produced_base, has_produced_prod)) in timer_mask + .to_array() + .into_iter() + .zip(prod_timer_mask.to_array().into_iter()) + .enumerate() + { + if has_produced_base || has_produced_prod { let final_idx = index + i; + #[cfg(feature = "assembler-craft-tracking")] + { + self.number_of_crafts_finished[final_idx] += + u32::from(has_produced_base) + u32::from(has_produced_prod); + } + let mut items = our_outputs.clone(); for (item, (out, items_to_distribute)) in self @@ -862,6 +875,9 @@ impl( prod_mod: _, power_consumption_mod: _, base_power_consumption: _, + + .. } = game_state .simulation_state .factory @@ -3417,6 +3419,9 @@ pub fn render_ui< prod_mod, power_consumption_mod, base_power_consumption, + + #[cfg(feature = "assembler-craft-tracking")] + times_craft_finished } = game_state_ref .simulation_state .factory @@ -3475,6 +3480,10 @@ pub fn render_ui< ui.label(format!("Productivity: {:.1}%", prod_mod * 100.0)); ui.label(format!("Max Consumption: {}({:+.0}%)", Watt((base_power_consumption.0 as f64 * (1.0 + power_consumption_mod as f64)) as u64), power_consumption_mod * 100.0)); + + #[cfg(feature = "assembler-craft-tracking")] + ui.label(format!("Crafts finished: {}", times_craft_finished)); + } } }, From 24e567f96bbc815dedf9ca0630040da49aff0ef1 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 17 Dec 2025 18:30:24 +0100 Subject: [PATCH 060/152] Update RAM bandwidth estimate with waitlist optimization --- src/rendering/eframe_app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index 62c0082..0d5adc6 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -485,7 +485,7 @@ impl eframe::App for App { ui.add(Slider::new(gigabase_size, 1..=1_000).logarithmic(true).update_while_editing(true).text("Number of base copies to build")); let single_base_size = 15.4 / 40.0; - let single_base_usage = 40.0 / 40.0; + let single_base_usage = 40.0 / 60.0; ui.label(&format!("Est. Memory Usage: ~{:.1}GB", single_base_size * f64::from(*gigabase_size))); ui.label(&format!("Est. Memory Bandwidth for 60 UPS: ~{:.1}GB/s", single_base_usage * f64::from(*gigabase_size))); From af7e42485c28e0f4c8ee31d1063befdc31172971 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 17 Dec 2025 18:49:35 +0100 Subject: [PATCH 061/152] Add debug stat gathering and improve waitlists --- Cargo.toml | 1 + src/app_state.rs | 45 +++-- src/assembler/simd.rs | 32 +++- src/belt/smart.rs | 166 ++++++++++++++---- src/belt/sushi.rs | 4 +- src/inserter/belt_storage_movement_list.rs | 21 ++- .../storage_storage_with_buckets_indirect.rs | 14 +- src/rendering/render_world.rs | 40 ++++- 8 files changed, 255 insertions(+), 68 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 065d457..1ceda71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -139,4 +139,5 @@ dhat-heap = [] profiler = ["profiling/profile-with-puffin"] client = [ "dep:eframe", "dep:egui", "dep:egui_extras", "dep:egui_plot", "dep:puffin_egui", "dep:egui_graphs", "dep:egui-show-info", "dep:egui-show-info-derive", "dep:tilelib", "dep:image", "dep:rfd", "dep:get-size2"] logging = ["simple_logger"] +debug-stat-gathering = [] assembler-craft-tracking = [] \ No newline at end of file diff --git a/src/app_state.rs b/src/app_state.rs index 455dc0b..9b1bb80 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -76,6 +76,7 @@ use itertools::Itertools; use log::error; use log::{info, trace, warn}; use petgraph::graph::NodeIndex; +use rayon::iter::IntoParallelRefIterator; use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; use std::collections::BTreeMap; use std::io::BufReader; @@ -3014,21 +3015,39 @@ impl GameState= 120 { + .par_iter_mut() + .zip(belt_store.belt_ty.par_iter()) + .enumerate().filter_map(|(self_index, (belt, ty))| { + // Only update belts, which have moved according to their timer + // This is what makes some types of belts different speed from others + (update_timers[usize::from(*ty)] >= 120).then_some((self_index, belt)) + }).map(|(self_index, belt)| { + // Update a belt belt.update(sushi_splitters); - belt.update_inserters( - self_index.try_into().unwrap(), - &mut belt_storage_reinsertion_outgoing, - &mut storage_belt_reinsertion_incoming, - ); + belt.update_inserters_lazy().into_iter().flatten().zip(iter::repeat(self_index)) + }).fold(|| vec![], |mut v, reinsertions| { + v.extend(reinsertions); + v + }).collect_vec_list().into_iter().flatten().flatten(); + + // Do the reinsertion sequentially + for (ins, self_index) in reinsertion { + let self_index = self_index as u32; + let in_movement = BeltStorageInserterInMovement { + movetime: ins.movetime, + storage: ins.storage, + belt: self_index, + belt_pos: ins.belt_pos, + max_hand_size: ins.max_hand_size, + current_hand: ins.current_hand, + }; + if ins.outgoing { + belt_storage_reinsertion_outgoing.reinsert(ins.movetime.into(), in_movement); + } else { + storage_belt_reinsertion_incoming.reinsert(ins.movetime.into(), in_movement); } } } diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index f5472f6..f5efd5b 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -597,7 +597,12 @@ impl { let ins = ins.take().unwrap(); for move_left_idx in idx..WAITLIST_LEN { - out[final_idx].inserters[move_left_idx] = out[final_idx].inserters.get_mut(move_left_idx + 1).map(|v| v.take()).unwrap_or(None); + out[final_idx].inserters[move_left_idx] = out + [final_idx] + .inserters + .get_mut(move_left_idx + 1) + .map(|v| v.take()) + .unwrap_or(None); } let Ok(()) = self .inserter_waitlist_output_vec @@ -630,9 +635,11 @@ impl self.inserter_waitlist_output_vec.capacity() ); }; - self.waitlists_outputs_needed[item][final_idx] = out - [final_idx] - .inserters[0].as_ref().map(|ins| ins.max_hand - ins.current_hand).unwrap_or(ITEMCOUNTTYPE::MAX); + self.waitlists_outputs_needed[item][final_idx] = + out[final_idx].inserters[0] + .as_ref() + .map(|ins| ins.max_hand - ins.current_hand) + .unwrap_or(ITEMCOUNTTYPE::MAX); } else { v.current_hand += amount_taken_by_this_inserter; } @@ -685,11 +692,16 @@ impl let ins = &mut ing[final_idx].inserters[idx]; if let Some(v) = ins { let amount_taken_by_this_inserter = - min(*items_to_drain, v.current_hand); + min(*items_to_drain, v.current_hand); if v.current_hand - amount_taken_by_this_inserter == 0 { let ins = ins.take().unwrap(); for move_left_idx in idx..WAITLIST_LEN { - ing[final_idx].inserters[move_left_idx] = ing[final_idx].inserters.get_mut(move_left_idx + 1).map(|v| v.take()).unwrap_or(None); + ing[final_idx].inserters[move_left_idx] = ing + [final_idx] + .inserters + .get_mut(move_left_idx + 1) + .map(|v| v.take()) + .unwrap_or(None); } let Ok(()) = self .inserter_waitlist_output_vec @@ -722,9 +734,11 @@ impl self.inserter_waitlist_output_vec.capacity() ); }; - self.waitlists_ings_needed[item][final_idx] = ing - [final_idx] - .inserters[0].as_ref().map(|ins| ins.current_hand).unwrap_or(ITEMCOUNTTYPE::MAX); + self.waitlists_ings_needed[item][final_idx] = + ing[final_idx].inserters[0] + .as_ref() + .map(|ins| ins.current_hand) + .unwrap_or(ITEMCOUNTTYPE::MAX); } else { v.current_hand -= amount_taken_by_this_inserter; } diff --git a/src/belt/smart.rs b/src/belt/smart.rs index 7450f9f..43a46d2 100644 --- a/src/belt/smart.rs +++ b/src/belt/smart.rs @@ -1,5 +1,5 @@ use std::{ - iter::repeat, + iter::{self, repeat}, num::NonZero, ops::{Deref, DerefMut}, sync::atomic::AtomicUsize, @@ -43,13 +43,28 @@ use egui_show_info_derive::ShowInfo; #[cfg(feature = "client")] use get_size2::GetSize; -// #[cfg(debug_assertions)] +#[cfg(feature = "debug-stat-gathering")] pub static NUM_BELT_UPDATES: AtomicUsize = AtomicUsize::new(0); -// #[cfg(debug_assertions)] +#[cfg(feature = "debug-stat-gathering")] pub static NUM_BELT_FREE_CACHE_HITS: AtomicUsize = AtomicUsize::new(0); -// #[cfg(debug_assertions)] +#[cfg(feature = "debug-stat-gathering")] pub static NUM_BELT_LOCS_SEARCHED: AtomicUsize = AtomicUsize::new(0); +#[cfg(feature = "debug-stat-gathering")] +pub static NUM_BELT_INSERTER_UPDATES: AtomicUsize = AtomicUsize::new(0); +#[cfg(feature = "debug-stat-gathering")] +pub static TIMES_ALL_INCOMING_EARLY_RETURN: AtomicUsize = AtomicUsize::new(0); + +#[cfg(feature = "debug-stat-gathering")] +pub static NUM_INSERTER_LOADS_WAITING_FOR_ITEMS: AtomicUsize = AtomicUsize::new(0); +#[cfg(feature = "debug-stat-gathering")] +pub static NUM_INSERTER_LOADS_WAITING_FOR_SPACE: AtomicUsize = AtomicUsize::new(0); +#[cfg(feature = "debug-stat-gathering")] +pub static NUM_INSERTER_LOADS_WAITING_FOR_SPACE_IN_GUARANTEED_FULL: AtomicUsize = + AtomicUsize::new(0); +#[cfg(feature = "debug-stat-gathering")] +pub static TIMES_INSERTERS_EXTRACTED: AtomicUsize = AtomicUsize::new(0); + #[allow(clippy::module_name_repetitions)] #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] @@ -71,6 +86,8 @@ pub struct SmartBelt { pub(super) input_splitter: Option<(SplitterID, SplitterSide)>, pub(super) output_splitter: Option<(SplitterID, SplitterSide)>, + + pub(crate) latest_inserter_pos_if_all_incoming: Option>, } // FIXME: @@ -152,6 +169,8 @@ impl SmartBelt { input_splitter: None, output_splitter: None, + + latest_inserter_pos_if_all_incoming: None, } } @@ -189,6 +208,8 @@ impl SmartBelt { input_splitter, output_splitter, + + latest_inserter_pos_if_all_incoming: earliest_inserter_pos_if_all_incoming, } = self; SushiBelt { @@ -457,6 +478,7 @@ impl SmartBelt { "Bounds check {index} >= {}", self.locs.len() ); + self.latest_inserter_pos_if_all_incoming = None; if filter != self.item { return Err(InserterAdditionError::ItemMismatch); @@ -491,6 +513,7 @@ impl SmartBelt { "Bounds check {index} >= {}", self.locs.len() ); + self.latest_inserter_pos_if_all_incoming = None; // We only only return an item mismatch if we know the space is free, so we do not transition to sushi, // And then fail anyway @@ -550,39 +573,106 @@ impl SmartBelt { { Dir::StorageToBelt }, >, ) { - if self.get_len() == 0 { + let Some(extracted) = self.update_inserters_lazy() else { return; + }; + + for ins in extracted { + let in_movement = BeltStorageInserterInMovement { + movetime: ins.movetime, + storage: ins.storage, + belt: self_index, + belt_pos: ins.belt_pos, + max_hand_size: ins.max_hand_size, + current_hand: ins.current_hand, + }; + if ins.outgoing { + reinsertion_outgoing.reinsert(ins.movetime.into(), in_movement); + } else { + reinsertion_incoming.reinsert(ins.movetime.into(), in_movement); + } } + } + + pub fn update_inserters_lazy( + &mut self, + ) -> Option + Send> { + if self.get_len() == 0 { + return None; + } + #[cfg(feature = "debug-stat-gathering")] + NUM_BELT_INSERTER_UPDATES.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let old_first_free = match self.first_free_index { FreeIndex::FreeIndex(idx) => idx, FreeIndex::OldFreeIndex(idx) => idx, }; + if let Some(lastest_pos) = self.latest_inserter_pos_if_all_incoming { + // All incoming inserters are incoming and their positions are <= lastest_pos + + if BeltLenType::from(lastest_pos) < old_first_free { + // All inserters are incoming AND all inserters are trying to put onto positions, which are fully filled + // Thus, nothing will change + #[cfg(feature = "debug-stat-gathering")] + if !self.inserters.inserters.is_empty() { + TIMES_ALL_INCOMING_EARLY_RETURN + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + + return None; + } + } + let mut new_first_free = old_first_free; - let extracted = self.inserters.inserters.extract_if(.., |ins| { + let mut min_pos = Some(NonZero::new(1).unwrap()); + + let zero_index = &mut self.zero_index; + let first_free_index = &mut self.first_free_index; + let locs = &mut self.locs; + let latest_inserter_pos_if_all_incoming = &mut self.latest_inserter_pos_if_all_incoming; + + let extracted = self.inserters.inserters.extract_if(.., move |ins| { // FIXME: This should not be needed, if we did not incorrectly insert inserters always in the belt if ins.current_hand == 0 && !ins.outgoing { return true; } // Taken from VecDeque::wrap_index - let logical_index = usize::from(self.zero_index) + usize::from(ins.belt_pos); - let loc_idx = if logical_index >= self.locs.len() { - logical_index - self.locs.len() + let logical_index = usize::from(*zero_index) + usize::from(ins.belt_pos); + let loc_idx = if logical_index >= locs.len() { + logical_index - locs.len() } else { logical_index }; - if ins.belt_pos < old_first_free { + #[cfg(feature = "debug-stat-gathering")] + if ins.outgoing { + NUM_INSERTER_LOADS_WAITING_FOR_ITEMS + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } else { + NUM_INSERTER_LOADS_WAITING_FOR_SPACE + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + + if ins.outgoing { + min_pos = None; + } else { + if let Some(min_pos) = &mut min_pos { + *min_pos = std::cmp::max(*min_pos, NonZero::new(ins.belt_pos).expect("Currently inserters at belt_pos 0 are unsupported, and should never be generated")); + } + } + *latest_inserter_pos_if_all_incoming = min_pos; + + let extract = if ins.belt_pos < old_first_free { // We KNOW this position is filled - debug_assert!(self.locs[loc_idx]); + debug_assert!(locs[loc_idx]); if ins.outgoing { ins.current_hand += 1; - self.locs.set(loc_idx, false); + locs.set(loc_idx, false); if ins.belt_pos <= new_first_free { - self.first_free_index = FreeIndex::FreeIndex(ins.belt_pos); + *first_free_index = FreeIndex::FreeIndex(ins.belt_pos); new_first_free = ins.belt_pos; } @@ -592,10 +682,13 @@ impl SmartBelt { false } } else { + #[cfg(feature = "debug-stat-gathering")] + NUM_INSERTER_LOADS_WAITING_FOR_SPACE_IN_GUARANTEED_FULL + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); false } } else { - let mut loc = self.locs.get_mut(loc_idx).unwrap(); + let mut loc = locs.get_mut(loc_idx).unwrap(); if ins.outgoing && *loc { *loc = false; @@ -611,31 +704,24 @@ impl SmartBelt { if ins.belt_pos == new_first_free && *loc { // This was the old first free pos - self.first_free_index = FreeIndex::OldFreeIndex(ins.belt_pos); + *first_free_index = FreeIndex::OldFreeIndex(ins.belt_pos); } if ins.current_hand == 0 { true } else { false } } else { false } + }; + + #[cfg(feature = "debug-stat-gathering")] + if extract { + TIMES_INSERTERS_EXTRACTED.fetch_add(1, std::sync::atomic::Ordering::Relaxed); } + + extract }); - for ins in extracted { - let in_movement = BeltStorageInserterInMovement { - movetime: ins.movetime, - storage: ins.storage, - belt: self_index, - belt_pos: ins.belt_pos, - max_hand_size: ins.max_hand_size, - current_hand: ins.current_hand, - }; - if ins.outgoing { - reinsertion_outgoing.reinsert(ins.movetime.into(), in_movement); - } else { - reinsertion_incoming.reinsert(ins.movetime.into(), in_movement); - } - } + Some(extracted) } fn remove_first_free_pos_maybe(&mut self, now_filled_pos: BeltLenType) { @@ -909,6 +995,8 @@ impl SmartBelt { input_splitter: None, output_splitter, + + latest_inserter_pos_if_all_incoming: _, } = front else { unreachable!() @@ -930,6 +1018,8 @@ impl SmartBelt { input_splitter, output_splitter: None, + + latest_inserter_pos_if_all_incoming: _, } = back else { unreachable!() @@ -983,6 +1073,9 @@ impl SmartBelt { input_splitter, output_splitter, + + // Since this is just an optimization, and will be rechecked on next update, None is fine + latest_inserter_pos_if_all_incoming: None, } } @@ -1007,6 +1100,8 @@ impl SmartBelt { input_splitter, output_splitter, + + latest_inserter_pos_if_all_incoming: earliest_inserter_pos_if_all_incoming, } = self; match side { @@ -1074,6 +1169,9 @@ impl SmartBelt { input_splitter, output_splitter, + + // Since this is just an optimization, and will be rechecked on next update, None is fine + latest_inserter_pos_if_all_incoming: None, }; new.find_and_update_real_first_free_index(); @@ -1234,6 +1332,8 @@ impl EmptyBelt { last_moving_spot: 0, input_splitter: self.input_splitter, output_splitter: self.output_splitter, + + latest_inserter_pos_if_all_incoming: None, } } @@ -1588,7 +1688,7 @@ impl Belt for SmartBelt { }, } - #[cfg(debug_assertions)] + #[cfg(feature = "debug-stat-gathering")] { NUM_BELT_UPDATES.fetch_add(1, std::sync::atomic::Ordering::Relaxed); match self.first_free_index { @@ -1611,7 +1711,7 @@ impl Belt for SmartBelt { let Some(first_free_index_real) = first_free_index_real else { // All slots are full - #[cfg(debug_assertions)] + #[cfg(feature = "debug-stat-gathering")] { NUM_BELT_LOCS_SEARCHED.fetch_add( (len - old_free) as usize, @@ -1621,7 +1721,7 @@ impl Belt for SmartBelt { return; }; - #[cfg(debug_assertions)] + #[cfg(feature = "debug-stat-gathering")] { NUM_BELT_LOCS_SEARCHED.fetch_add( (first_free_index_real - old_free) as usize, diff --git a/src/belt/sushi.rs b/src/belt/sushi.rs index 16041ff..0d3eb4b 100644 --- a/src/belt/sushi.rs +++ b/src/belt/sushi.rs @@ -338,7 +338,9 @@ impl SushiBelt { last_moving_spot, input_splitter, - output_splitter + output_splitter, + + latest_inserter_pos_if_all_incoming: None, } } diff --git a/src/inserter/belt_storage_movement_list.rs b/src/inserter/belt_storage_movement_list.rs index a9f6dba..bb68272 100644 --- a/src/inserter/belt_storage_movement_list.rs +++ b/src/inserter/belt_storage_movement_list.rs @@ -124,7 +124,12 @@ impl<'a> FinishedMovingLists<'a, { Dir::BeltToStorage }, { Dir::BeltToStorage }> false } else { if let Some((wait_list, wait_list_needed)) = wait_list { - if let Some((pos, empty)) = wait_list.inserters.iter_mut().enumerate().find(|(_i, v)| v.is_none()) { + if let Some((pos, empty)) = wait_list + .inserters + .iter_mut() + .enumerate() + .find(|(_i, v)| v.is_none()) + { if pos == 0 { *wait_list_needed = inserter.current_hand; } @@ -172,10 +177,14 @@ impl<'a> FinishedMovingLists<'a, { Dir::BeltToStorage }, { Dir::StorageToBelt }> false } else { if let Some((wait_list, wait_list_needed)) = wait_list { - if let Some((pos, empty)) = wait_list.inserters.iter_mut().enumerate().find(|(_i, v)| v.is_none()) { + if let Some((pos, empty)) = wait_list + .inserters + .iter_mut() + .enumerate() + .find(|(_i, v)| v.is_none()) + { if pos == 0 { *wait_list_needed = inserter.max_hand_size - inserter.current_hand; - } *empty = Some(InserterWithBelts { current_hand: inserter.current_hand, @@ -226,6 +235,10 @@ impl<'a> FinishedMovingLists<'a, { Dir::StorageToBelt }, { Dir::StorageToBelt }> max_hand_size: inserter.max_hand_size, current_hand, }); + // This is an incoming inserter + if let Some(latest) = &mut belt.latest_inserter_pos_if_all_incoming { + *latest = std::cmp::max(*latest, NonZero::new(inserter.belt_pos).expect("Currently inserters at belt_pos 0 are unsupported, and should never be generated")) + } false } }); @@ -259,6 +272,8 @@ impl<'a> FinishedMovingLists<'a, { Dir::StorageToBelt }, { Dir::BeltToStorage }> max_hand_size: inserter.max_hand_size, current_hand, }); + // This is an outgoing inserter + belt.latest_inserter_pos_if_all_incoming = None; false } }); diff --git a/src/inserter/storage_storage_with_buckets_indirect.rs b/src/inserter/storage_storage_with_buckets_indirect.rs index f046427..a22aa94 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -344,7 +344,12 @@ impl BucketedStorageStorageInserterStore { } } else { if let Some((wait_list, wait_list_needed)) = wait_list { - if let Some((pos, empty)) = wait_list.inserters.iter_mut().enumerate().find(|(_i, v)| v.is_none()) { + if let Some((pos, empty)) = wait_list + .inserters + .iter_mut() + .enumerate() + .find(|(_i, v)| v.is_none()) + { if pos == 0 { *wait_list_needed = bucket_data.max_hand_size - bucket_data.current_hand; } @@ -417,7 +422,12 @@ impl BucketedStorageStorageInserterStore { } } else { if let Some((wait_list, wait_list_needed)) = wait_list { - if let Some((pos, empty)) = wait_list.inserters.iter_mut().enumerate().find(|(_i, v)| v.is_none()) { + if let Some((pos, empty)) = wait_list + .inserters + .iter_mut() + .enumerate() + .find(|(_i, v)| v.is_none()) + { if pos == 0 { *wait_list_needed = bucket_data.current_hand; } diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index c863275..5ad090c 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -1,6 +1,13 @@ use crate::belt::belt::Belt; -use crate::belt::smart::{NUM_BELT_FREE_CACHE_HITS, NUM_BELT_UPDATES}; -use crate::belt::smart::{NUM_BELT_LOCS_SEARCHED, SmartBelt}; +#[cfg(feature = "debug-stat-gathering")] +use crate::belt::smart::{ + NUM_BELT_FREE_CACHE_HITS, NUM_BELT_INSERTER_UPDATES, NUM_BELT_LOCS_SEARCHED, NUM_BELT_UPDATES, + NUM_INSERTER_LOADS_WAITING_FOR_ITEMS, NUM_INSERTER_LOADS_WAITING_FOR_SPACE, + NUM_INSERTER_LOADS_WAITING_FOR_SPACE_IN_GUARANTEED_FULL, TIMES_ALL_INCOMING_EARLY_RETURN, + TIMES_INSERTERS_EXTRACTED, +}; + +use crate::belt::smart::SmartBelt; use crate::blueprint::blueprint_string::BlueprintString; use crate::chest::ChestSize; use crate::frontend::action::action_state_machine::ForkSaveInfo; @@ -2873,11 +2880,30 @@ pub fn render_ui< 'P' }))); - let num_locs_searched = NUM_BELT_LOCS_SEARCHED.load(std::sync::atomic::Ordering::Relaxed); - let num_cache_hits = NUM_BELT_FREE_CACHE_HITS.load(std::sync::atomic::Ordering::Relaxed); - let num_updates = NUM_BELT_UPDATES.load(std::sync::atomic::Ordering::Relaxed); - ui.label(&format!("BeltUpdates: {}, BeltCacheHits: {}, Cache ratio: {:.2}%", num_updates, num_cache_hits,num_cache_hits as f64 / num_updates as f64 * 100.0 )); - ui.label(&format!("BeltLocsSearched: {}, LocsPerUpdate: {:.2}", num_locs_searched, num_locs_searched as f64 / num_updates as f64 )); + + #[cfg(feature = "debug-stat-gathering")] + CollapsingHeader::new("Gathered Debug Stats").show(ui, |ui| { + let num_locs_searched = NUM_BELT_LOCS_SEARCHED.load(std::sync::atomic::Ordering::Relaxed); + let num_cache_hits = NUM_BELT_FREE_CACHE_HITS.load(std::sync::atomic::Ordering::Relaxed); + let num_updates = NUM_BELT_UPDATES.load(std::sync::atomic::Ordering::Relaxed); + ui.label(&format!("BeltUpdates: {}, BeltCacheHits: {}, Cache ratio: {:.2}%", num_updates, num_cache_hits,num_cache_hits as f64 / num_updates as f64 * 100.0 )); + ui.label(&format!("BeltLocsSearched: {}, LocsPerUpdate: {:.2}", num_locs_searched, num_locs_searched as f64 / num_updates as f64 )); + + + let inserter_update_calls = NUM_BELT_INSERTER_UPDATES.load(std::sync::atomic::Ordering::Relaxed); + let inserter_update_skips = TIMES_ALL_INCOMING_EARLY_RETURN.load(std::sync::atomic::Ordering::Relaxed); + let inserter_loads_waiting_for_item = NUM_INSERTER_LOADS_WAITING_FOR_ITEMS.load(std::sync::atomic::Ordering::Relaxed); + let inserter_loads_waiting_for_space = NUM_INSERTER_LOADS_WAITING_FOR_SPACE.load(std::sync::atomic::Ordering::Relaxed); + let inserter_loads_waiting_for_space_waster = NUM_INSERTER_LOADS_WAITING_FOR_SPACE_IN_GUARANTEED_FULL.load(std::sync::atomic::Ordering::Relaxed); + let inserter_extractions = TIMES_INSERTERS_EXTRACTED.load(std::sync::atomic::Ordering::Relaxed); + ui.label(&format!("Belts updated: {}, percentage skipped: {:.2}", inserter_update_calls, (inserter_update_skips) as f64 / inserter_update_calls as f64)); + ui.label(&format!("Total inserter loads: {}, Avg per belt: {:.2}", inserter_loads_waiting_for_item + inserter_loads_waiting_for_space, (inserter_loads_waiting_for_item + inserter_loads_waiting_for_space) as f64 / inserter_update_calls as f64)); + ui.label(&format!("Loads waiting for item: {}, {:.2}", inserter_loads_waiting_for_item, inserter_loads_waiting_for_item as f64 / (inserter_loads_waiting_for_item + inserter_loads_waiting_for_space) as f64 )); + ui.label(&format!("Loads waiting for space: {}, {:.2}", inserter_loads_waiting_for_space, inserter_loads_waiting_for_space as f64 / (inserter_loads_waiting_for_item + inserter_loads_waiting_for_space) as f64 )); + ui.label(&format!("Loads waiting for space, while space is guaranteed filled: {}, {:.2}", inserter_loads_waiting_for_space_waster, inserter_loads_waiting_for_space_waster as f64 / (inserter_loads_waiting_for_item + inserter_loads_waiting_for_space) as f64 )); + ui.label(&format!("Extractions: {}, Avg ticks before extraction: {:.2}", inserter_extractions, (inserter_loads_waiting_for_item + inserter_loads_waiting_for_space) as f64 / inserter_extractions as f64 )); + }); + if ui.button("Remove Infinity Batteries").clicked() { for entity in game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()) { From d7cd4c5ae0a6e3ed23d510226f81ec6bad11814f Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 4 Jan 2026 03:08:03 +0100 Subject: [PATCH 062/152] Add Save Game management --- Cargo.lock | 54 +++- Cargo.toml | 6 +- src/app_state.rs | 46 ++- src/lib.rs | 108 ++++--- src/par_generation.rs | 3 +- src/rendering/eframe_app.rs | 476 ++++++++++++++++++++++++------- src/rendering/render_world.rs | 48 +++- src/saving/loading.rs | 47 +++ src/saving/mod.rs | 109 ++++++- src/saving/save_file_settings.rs | 32 +++ src/scenario.rs | 87 ++++++ 11 files changed, 847 insertions(+), 169 deletions(-) create mode 100644 src/saving/loading.rs create mode 100644 src/saving/save_file_settings.rs create mode 100644 src/scenario.rs diff --git a/Cargo.lock b/Cargo.lock index f8b7128..7085c09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -962,7 +962,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", "windows-link 0.2.1", ] @@ -1702,6 +1705,7 @@ dependencies = [ "built 0.8.0", "bytemuck", "charts-rs", + "chrono", "dhat", "directories", "ecolor", @@ -1729,6 +1733,7 @@ dependencies = [ "memoffset", "mimalloc", "noise", + "open", "parking_lot 0.12.5", "petgraph 0.8.2", "postcard", @@ -1736,7 +1741,8 @@ dependencies = [ "proptest", "puffin", "puffin_egui", - "rand 0.8.5", + "rand 0.9.2", + "rand_xoshiro", "rayon", "recycle_vec", "rfd", @@ -1755,6 +1761,7 @@ dependencies = [ "take_mut", "thin-dst", "tilelib", + "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-timer", @@ -2620,6 +2627,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "itertools" version = "0.10.5" @@ -3521,6 +3547,17 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -3626,6 +3663,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -4153,6 +4196,15 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.3", +] + [[package]] name = "range-alloc" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index 1ceda71..bc3a6f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ eframe = { version = "0.31.1", features = ["accesskit", "default_fonts", "waylan wgpu = { version = "25.0.2", features = ["webgl"] } egui_extras = { version = "0.31.1", optional = true } egui_plot = { version = "0.32.1", optional = true } -rand = "0.8.5" +rand = "0.9.0" bitcode = { version = "0.6.6", features = ["serde"] } egui = { version = "0.31.1", features = ["bytemuck", "serde"], optional = true } flate2 = { version = "1.1.1", features = ["zlib-rs"] } @@ -75,6 +75,10 @@ fixed-buffer = "1.0.2" base64 = "0.22.1" mimalloc = { version = "0.1.48", features = ["v3"] } rustc-hash = "2.1.1" +chrono = { version = "0.4.42", features = ["serde"] } +rand_xoshiro = "0.7.0" +open = "5.3.3" +url = "2.5.7" [build-dependencies] built = {version = "0.8", features= ["git2", "chrono"]} diff --git a/src/app_state.rs b/src/app_state.rs index 9b1bb80..f1692d7 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -29,6 +29,8 @@ use crate::par_generation::ParGenerateInfo; use crate::par_generation::par_generate; use crate::power::Watt; #[cfg(feature = "client")] +use crate::saving::loading::SaveFileList; +#[cfg(feature = "client")] use crate::{Input, LoadedGame}; use crate::{ belt::{BeltBeltInserterInfo, BeltStore}, @@ -102,6 +104,8 @@ use crate::get_size::Mutex; #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct AuxillaryData { + pub game_name: String, + pub current_tick: u64, pub statistics: GenStatistics, @@ -115,9 +119,11 @@ pub struct AuxillaryData { } impl AuxillaryData { pub fn new( + name: String, data_store: &DataStore, ) -> Self { AuxillaryData { + game_name: name, current_tick: 0, statistics: GenStatistics::new(data_store), update_times: Timeline::new(false, data_store), @@ -159,15 +165,16 @@ impl<'a> AddAssign<&'a UpdateTime> for UpdateTime { impl GameState { #[must_use] - pub fn new(data_store: &DataStore) -> Self { + pub fn new(name: String, data_store: &DataStore) -> Self { Self { world: Mutex::new(World::default()), simulation_state: Mutex::new(SimulationState::new(data_store)), - aux_data: Mutex::new(AuxillaryData::new(data_store)), + aux_data: Mutex::new(AuxillaryData::new(name, data_store)), } } fn new_with_world_area( + name: String, top_left: Position, bottom_right: Position, data_store: &DataStore, @@ -175,12 +182,13 @@ impl GameState, data_store: &DataStore, @@ -189,6 +197,7 @@ impl GameState GameState, data_store: &DataStore, @@ -330,6 +340,7 @@ impl GameState GameState, data_store: &DataStore, ) -> Self { let mut ret = GameState::new_with_world_area( + name, Position { x: 0, y: 0 }, Position { x: 10000, y: 20000 }, data_store, @@ -396,10 +409,12 @@ impl GameState, data_store: &DataStore, ) -> Self { let mut ret = GameState::new_with_world_area( + name, Position { x: 0, y: 0 }, Position { x: 1_000, @@ -439,6 +454,7 @@ impl GameState, @@ -447,6 +463,7 @@ impl GameState Self { // TODO: Correct size let mut ret = GameState::new_with_world_area( + name, Position { x: 0, y: 0 }, Position { x: 60000, y: 60000 }, data_store, @@ -459,6 +476,7 @@ impl GameState, data_store: &DataStore, ) -> Self { @@ -466,6 +484,7 @@ impl GameState GameState, data_store: &DataStore, ) -> Self { - let mut ret = GameState::new(data_store); + let mut ret = GameState::new(name, data_store); let red = File::open("test_blueprints/eight_beacon_red_sci_with_storage.bp").unwrap(); let red: Blueprint = ron::de::from_reader(red).unwrap(); @@ -573,10 +593,12 @@ impl GameState, bp_path: impl AsRef, ) -> Self { let mut ret = GameState::new_with_world_area( + name, Position { x: 0, y: 0 }, Position { x: 32000, @@ -1181,9 +1203,15 @@ impl Factory, + }, + LoadSaveMenu { + save_files: SaveFileList, + }, + NewGameMenu { + new_game_name: String, gigabase_size: u16, }, Ingame, @@ -3610,7 +3638,7 @@ mod tests { #[test] fn test_random_blueprint_does_not_crash(base_pos in random_position(), blueprint in random_blueprint_strategy::(0..1_000, &DATA_STORE)) { - let mut game_state = GameState::new(&DATA_STORE); + let mut game_state = GameState::new("Test Game".to_string(), &DATA_STORE); blueprint.apply(false, base_pos, &mut game_state, &DATA_STORE); @@ -3619,7 +3647,7 @@ mod tests { #[test] fn test_random_blueprint_does_not_crash_after(base_pos in random_position(), blueprint in random_blueprint_strategy::(0..100, &DATA_STORE), time in 0usize..10) { - let mut game_state = GameState::new(&DATA_STORE); + let mut game_state = GameState::new("Test Game".to_string(), &DATA_STORE); blueprint.apply(false, base_pos, &mut game_state, &DATA_STORE); @@ -3649,7 +3677,7 @@ mod tests { // .. // }))); - let mut game_state = GameState::new(&DATA_STORE); + let mut game_state = GameState::new("Test Game".to_string(), &DATA_STORE); Blueprint { actions: actions.into_iter().map(|a| BlueprintAction::from_with_datastore(&a, &*DATA_STORE)).collect() }.apply(false, Position { x: 0, y: 0 }, &mut game_state, &DATA_STORE); @@ -3729,7 +3757,7 @@ mod tests { #[bench] fn bench_single_inserter(b: &mut Bencher) { - let mut game_state = GameState::new(&DATA_STORE); + let mut game_state = GameState::new("Test Game".to_string(), &DATA_STORE); let mut rep = Replay::new(&game_state, None, (*DATA_STORE).clone()); diff --git a/src/lib.rs b/src/lib.rs index f258234..7215c16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ pub mod built_info { #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] use eframe::web_sys; +#[cfg(feature = "client")] use std::{ borrow::Borrow, net::{SocketAddr, TcpStream}, @@ -77,6 +78,8 @@ pub mod mining_drill; pub mod power; pub mod research; +pub mod scenario; + mod shopping_list_arena; // This is an experiment. Before I can use it, I need to run it through a miri gauntlet @@ -253,7 +256,10 @@ pub fn main() { enum StartGameInfo { Load(PathBuf), LoadReadable(PathBuf), - Create(GameCreationInfo), + Create { + name: String, + info: GameCreationInfo, + }, } enum GameCreationInfo { @@ -280,6 +286,9 @@ enum GameCreationInfo { fn run_integrated_server( progress: Arc, start_game_info: StartGameInfo, + + // FIXME: This type is wrong + listen_addr: Option<&'static str>, ) -> (LoadedGame, Arc, Sender) { // TODO: Do mod loading here let raw_data = get_raw_data_test(); @@ -289,10 +298,10 @@ fn run_integrated_server( let connections: Arc>> = Arc::default(); - let local_addr = "127.0.0.1:57267"; let cancel: Arc = Default::default(); - - accept_continously(local_addr, connections.clone(), cancel.clone()).unwrap(); + if let Some(listen_addr) = listen_addr { + accept_continously(listen_addr, connections.clone(), cancel.clone()).unwrap(); + } match data_store { data::DataStoreOptions::ItemU8RecipeU8(data_store) => { @@ -307,11 +316,10 @@ fn run_integrated_server( let game_state = Arc::new(match start_game_info { StartGameInfo::Load(path) => load(path) .map(|sg| { - if sg.checksum != data_store.checksum { - // Try reconciliation - // todo!("Checksum mismatch, try to merge old and new mod state") - } else { - } + assert_eq!( + sg.checksum, data_store.checksum, + "A savegame can only be loaded with the EXACT same mods!" + ); sg.game_state }) .unwrap(), @@ -324,33 +332,47 @@ fn run_integrated_server( sg.game_state }) .unwrap(), - StartGameInfo::Create(info) => match info { - GameCreationInfo::Empty => GameState::new(&data_store), - GameCreationInfo::Megabase(use_solar_field) => { - GameState::new_with_megabase(use_solar_field, progress, &data_store) - }, - GameCreationInfo::Gigabase(count) => { - GameState::new_with_gigabase(count, progress, &data_store) - }, - GameCreationInfo::SolarField(wattage, base_pos) => { - GameState::new_with_tons_of_solar( - wattage, - base_pos, - None, - progress, - &data_store, - ) - }, - GameCreationInfo::LotsOfBelts => { - GameState::new_with_lots_of_belts(progress, &data_store) - }, - GameCreationInfo::TrainRide => { - GameState::new_with_world_train_ride(progress, &data_store) - }, - - GameCreationInfo::FromBP(path) => GameState::new_with_bp(&data_store, path), - - _ => unimplemented!(), + StartGameInfo::Create { name, info } => { + assert!( + name.is_ascii(), + "For now only ASCII game names are allowed, since they are used as the filename" + ); + match info { + GameCreationInfo::Empty => GameState::new(name, &data_store), + GameCreationInfo::Megabase(use_solar_field) => { + GameState::new_with_megabase( + name, + use_solar_field, + progress, + &data_store, + ) + }, + GameCreationInfo::Gigabase(count) => { + GameState::new_with_gigabase(name, count, progress, &data_store) + }, + GameCreationInfo::SolarField(wattage, base_pos) => { + GameState::new_with_tons_of_solar( + name, + wattage, + base_pos, + None, + progress, + &data_store, + ) + }, + GameCreationInfo::LotsOfBelts => { + GameState::new_with_lots_of_belts(name, progress, &data_store) + }, + GameCreationInfo::TrainRide => { + GameState::new_with_world_train_ride(name, progress, &data_store) + }, + + GameCreationInfo::FromBP(path) => { + GameState::new_with_bp(name, &data_store, path) + }, + + _ => unimplemented!(), + } }, }); @@ -369,7 +391,9 @@ fn run_integrated_server( // This is a little hack. Our connection accept thread is stuck waiting for connections and will only exit if anything connects. // So we just connect to ourselves :) // See https://stackoverflow.com/questions/56692961/how-do-i-gracefully-exit-tcplistener-incoming - let _ = TcpStream::connect(local_addr); + if let Some(local_addr) = listen_addr { + let _ = TcpStream::connect(local_addr); + } }), }, &data_store, @@ -423,7 +447,7 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { data::DataStoreOptions::ItemU8RecipeU8(data_store) => { let game_state = load(todo!("Add a console argument for the save file path")) .map(|save| save.game_state) - .unwrap_or_else(|| GameState::new(&data_store)); + .unwrap_or_else(|| GameState::new("Server Save".to_string(), &data_store)); let mut game = Game::new( GameInitData::DedicatedServer( @@ -474,7 +498,7 @@ fn run_client(remote_addr: SocketAddr) -> (LoadedGame, Arc, Sender ParGenerateInfo( + name: String, world_size: BoundingBox, generation_info: ParGenerateInfo, positions: Vec, @@ -837,7 +838,7 @@ pub fn par_generate( GameState { world: Mutex::new(world), simulation_state: Mutex::new(sim_state), - aux_data: Mutex::new(AuxillaryData::new(data_store)), + aux_data: Mutex::new(AuxillaryData::new(name, data_store)), } } diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index 0d5adc6..fc8ea48 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -1,4 +1,5 @@ use std::{ + fs::remove_dir_all, net::ToSocketAddrs, sync::{ Arc, @@ -9,12 +10,15 @@ use std::{ time::Duration, }; +use chrono::Local; +use egui_extras::{Column, TableBuilder}; +use url::Url; use wasm_timer::Instant; use directories::ProjectDirs; use parking_lot::Mutex; -use crate::{GameCreationInfo, run_client}; +use crate::{GameCreationInfo, run_client, saving::loading::SaveFileList}; use crate::{StartGameInfo, frontend::world::Position}; use crate::{rendering::render_world::EscapeMenuOptions, run_integrated_server}; use eframe::{ @@ -22,7 +26,8 @@ use eframe::{ egui_wgpu::{self, CallbackTrait}, }; use egui::{ - Align2, Color32, CursorIcon, Grid, Modal, ProgressBar, RichText, Slider, TextEdit, Window, + Align2, Button, Color32, CursorIcon, Grid, Modal, ProgressBar, RichText, Slider, TextBuffer, + TextEdit, Window, }; use log::{error, warn}; use tilelib::types::RawRenderer; @@ -45,7 +50,7 @@ use crate::saving::save; pub struct App { raw_renderer: Option, - pub state: AppState, + pub(crate) state: AppState, pub currently_loaded_game: Option, last_rendered_update: u64, @@ -70,10 +75,7 @@ impl App { ) }), input_sender: None, - state: AppState::MainMenu { - in_ip_box: None, - gigabase_size: 40, - }, + state: AppState::MainMenu { in_ip_box: None }, texture_atlas: atlas, currently_loaded_game: None, last_rendered_update: 0, @@ -98,6 +100,8 @@ impl eframe::App for App { match &self.state { AppState::MainMenu { .. } => {}, + AppState::LoadSaveMenu { .. } => {}, + AppState::NewGameMenu { .. } => {}, AppState::Loading { .. } => { ctx.request_repaint_after(Duration::from_secs_f32(1.0 / 60.0)); }, @@ -126,6 +130,159 @@ impl eframe::App for App { self.update_ingame(ctx, frame); }, + AppState::LoadSaveMenu { save_files } => { + let mut new_state = None; + egui::CentralPanel::default().show(ctx, |ui| { + if ui.button("Back to Main Menu").clicked() { + new_state = Some(AppState::MainMenu { in_ip_box: None }); + } + + if ui.button("Open Save File Folder").clicked() { + let uri = Url::from_file_path( + ProjectDirs::from("de", "aschhoff", "factory_game") + .expect("No Home path found") + .data_dir(), + ) + .expect("Could not generate URI"); + open::that(uri.as_str()).expect("Failed to Open Folder"); + } + + let mut dirty = false; + + TableBuilder::new(ui) + .columns(Column::remainder(), 5) + .header(1.0, |mut header| { + header.col(|ui| { + ui.label("Save File Name"); + }); + header.col(|ui| { + ui.label("Saved At"); + }); + header.col(|ui| { + ui.label("Playtime"); + }); + header.col(|ui| { + ui.label(""); + }); + }) + .body(|ui| { + ui.rows(1.0, save_files.save_files.len(), |mut row| { + let idx = row.index(); + let file = &save_files.save_files[idx]; + + match file { + Ok(file) => { + row.col(|ui| { + ui.label(&file.stored.name); + }); + row.col(|ui| { + ui.label(format!( + "{}", + file.stored + .saved_at + .with_timezone(&Local) + .format("%v %R") + )); + }); + row.col(|ui| { + let dur = + chrono::Duration::from_std(file.stored.playtime) + .unwrap(); + ui.label(format!( + "{:02}:{:02}:{:02}", + dur.num_hours(), + dur.num_minutes() % 60, + dur.num_seconds() % 60, + )); + }); + row.col(|ui| { + if ui + .add_enabled( + true, + Button::new( + RichText::new("Delete").color(Color32::RED), + ), + ) + .clicked() + { + remove_dir_all(&file.path) + .expect("Unable to delete save game"); + dirty = true; + } + }); + row.col(|ui| { + if ui.add_enabled(true, Button::new("Load")).clicked() { + let path = file.path.clone(); + let progress = + Arc::new(AtomicU64::new(0f64.to_bits())); + let (send, recv) = channel(); + + let progress_send = progress.clone(); + thread::spawn(move || { + send.send(run_integrated_server( + progress_send, + StartGameInfo::Load(path), + None, + )) + .expect("Channel send failed"); + }); + new_state = Some(AppState::Loading { + start_time: Instant::now(), + progress, + game_state_receiver: recv, + }); + } + }); + }, + Err((p, e)) => { + row.col(|ui| { + ui.label("Save File corrupt").on_hover_text(&format!( + "Path: {}, Error: {e:?}", + p.display() + )); + }); + row.col(|ui| { + ui.label("Save File corrupt").on_hover_text(&format!( + "Path: {}, Error: {e:?}", + p.display() + )); + }); + row.col(|ui| { + ui.label("Save File corrupt").on_hover_text(&format!( + "Path: {}, Error: {e:?}", + p.display() + )); + }); + row.col(|ui| { + ui.add_enabled(false, Button::new("Load")); + }); + row.col(|ui| { + if ui.add_enabled(true, Button::new("Delete")).clicked() + { + remove_dir_all(p) + .expect("Unable to delete save game"); + dirty = true; + } + }); + }, + } + }); + }); + + if dirty { + *save_files = SaveFileList::generate_from_save_folder( + ProjectDirs::from("de", "aschhoff", "factory_game") + .expect("No Home path found") + .data_dir(), + ) + } + }); + // Borrow checker issue + if let Some(new_state) = new_state { + self.state = new_state; + } + }, + AppState::Loading { start_time, progress, @@ -198,10 +355,7 @@ impl eframe::App for App { } }, - AppState::MainMenu { - in_ip_box, - gigabase_size, - } => { + AppState::MainMenu { in_ip_box } => { Window::new("Version") .default_pos(Align2::RIGHT_TOP.pos_in_rect(&ctx.screen_rect())) .show(ctx, |ui| { @@ -282,48 +436,28 @@ impl eframe::App for App { } } - let gigabase_size = *gigabase_size; - CentralPanel::default().show(ctx, |ui| { #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] - ui.vertical_centered(|ui|{ - ui.label( - egui::RichText::new("Detected running in a browser(WASM). Performance might be significantly degraded, and/or features might not work correctly. Support is on a best effort basis.") - .heading() - .color(egui::Color32::RED), - ); - ui.label( - egui::RichText::new("For the best experience run on native.") - .heading() - .color(egui::Color32::RED), - ); - }); + ui.vertical_centered(|ui|{ + ui.label( + egui::RichText::new("Detected running in a browser(WASM). Performance might be significantly degraded, and/or features might not work correctly. Support is on a best effort basis.") + .heading() + .color(egui::Color32::RED), + ); + ui.label( + egui::RichText::new("For the best experience run on native.") + .heading() + .color(egui::Color32::RED), + ); + }); if ui.button("Load").clicked() { - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] - if let Some(path) = rfd::FileDialog::new() - .set_directory( + self.state = AppState::LoadSaveMenu { + save_files: SaveFileList::generate_from_save_folder( ProjectDirs::from("de", "aschhoff", "factory_game") .expect("No Home path found") .data_dir(), - ) - .pick_folder() - { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Load(path), - )).expect("Channel send failed"); - }); - - self.state = AppState::Loading { - start_time: Instant::now(),progress, - game_state_receiver: recv, - }; + ), } } // else if ui.button("Load Debug Save").clicked() { @@ -352,14 +486,48 @@ impl eframe::App for App { // game_state_receiver: recv, // }; // } - // } - else if ui.add_enabled(cfg!(not(target_arch = "wasm32")), egui::Button::new("Connect over network")).on_disabled_hover_text("Disabled on WASM").clicked() { + // } + else if ui + .button( + "Create New Game" + ) + .clicked() + { + self.state = AppState::NewGameMenu { gigabase_size: 40, new_game_name: "My World".to_string()}; + } + else if ui + .add_enabled( + cfg!(not(target_arch = "wasm32")), + egui::Button::new("Connect over network"), + ) + .on_disabled_hover_text("Disabled on WASM") + .clicked() + { let AppState::MainMenu { in_ip_box, .. } = &mut self.state else { unreachable!() }; assert!(in_ip_box.is_none()); *in_ip_box = Some((String::new(), false)); - } else if ui.button("Empty World").clicked() { + } + }); + }, + + AppState::NewGameMenu { + gigabase_size, + new_game_name, + } => { + let gigabase_size = *gigabase_size; + let mut new_game_name = new_game_name.take(); + + CentralPanel::default().show(ctx, |ui| { + if ui.button("Back to Main Menu").clicked() { + self.state = AppState::MainMenu { in_ip_box: None }; + return; + } + + ui.add(TextEdit::singleline(&mut new_game_name).char_limit(100)); + + if ui.button("Empty World").clicked() { let progress = Arc::new(AtomicU64::new(0f64.to_bits())); let (send, recv) = channel(); @@ -368,18 +536,28 @@ impl eframe::App for App { thread::spawn(move || { send.send(run_integrated_server( progress_send, - StartGameInfo::Create(GameCreationInfo::Empty), - )).expect("Channel send failed"); + StartGameInfo::Create { + name: new_game_name, + info: GameCreationInfo::Empty, + }, + None, + )) + .expect("Channel send failed"); }); #[cfg(target_arch = "wasm32")] send.send(run_integrated_server( progress_send, - StartGameInfo::Create(GameCreationInfo::Empty), + StartGameInfo::Create { + name: new_game_name, + info: GameCreationInfo::Empty, + }, + None, )); self.state = AppState::Loading { - start_time: Instant::now(),progress, + start_time: Instant::now(), + progress, game_state_receiver: recv, }; } else if ui.button("Lots of Belts").clicked() { @@ -391,21 +569,31 @@ impl eframe::App for App { thread::spawn(move || { send.send(run_integrated_server( progress_send, - StartGameInfo::Create(GameCreationInfo::LotsOfBelts), - )).expect("Channel send failed"); + StartGameInfo::Create { + name: new_game_name, + info: GameCreationInfo::LotsOfBelts, + }, + None, + )) + .expect("Channel send failed"); }); #[cfg(target_arch = "wasm32")] send.send(run_integrated_server( progress_send, - StartGameInfo::Create(GameCreationInfo::LotsOfBelts), + StartGameInfo::Create { + name: new_game_name, + info: GameCreationInfo::LotsOfBelts, + }, + None, )); self.state = AppState::Loading { - start_time: Instant::now(),progress, + start_time: Instant::now(), + progress, game_state_receiver: recv, }; - } else if ui.button("Train Ride around the world").clicked() { + } else if ui.button("Train Ride around the world").clicked() { let progress = Arc::new(AtomicU64::new(0f64.to_bits())); let (send, recv) = channel(); @@ -414,21 +602,31 @@ impl eframe::App for App { thread::spawn(move || { send.send(run_integrated_server( progress_send, - StartGameInfo::Create(GameCreationInfo::TrainRide), - )).expect("Channel send failed"); + StartGameInfo::Create { + info: GameCreationInfo::TrainRide, + name: new_game_name, + }, + None, + )) + .expect("Channel send failed"); }); #[cfg(target_arch = "wasm32")] send.send(run_integrated_server( progress_send, - StartGameInfo::Create(GameCreationInfo::TrainRide), + StartGameInfo::Create { + info: GameCreationInfo::TrainRide, + name: new_game_name, + }, + None, )); self.state = AppState::Loading { - start_time: Instant::now(),progress, + start_time: Instant::now(), + progress, game_state_receiver: recv, }; - } else if ui.button("Megabase").clicked() { + } else if ui.button("Megabase").clicked() { let progress = Arc::new(AtomicU64::new(0f64.to_bits())); let (send, recv) = channel(); @@ -437,18 +635,28 @@ impl eframe::App for App { thread::spawn(move || { send.send(run_integrated_server( progress_send, - StartGameInfo::Create(GameCreationInfo::Megabase(true)), - )).expect("Channel send failed"); + StartGameInfo::Create { + info: GameCreationInfo::Megabase(true), + name: new_game_name, + }, + None, + )) + .expect("Channel send failed"); }); #[cfg(target_arch = "wasm32")] send.send(run_integrated_server( progress_send, - StartGameInfo::Create(GameCreationInfo::Megabase(true)), + StartGameInfo::Create { + info: GameCreationInfo::Megabase(true), + name: new_game_name, + }, + None, )); self.state = AppState::Loading { - start_time: Instant::now(),progress, + start_time: Instant::now(), + progress, game_state_receiver: recv, }; } else if ui.button("Megabase with Infinity Battery").clicked() { @@ -460,40 +668,61 @@ impl eframe::App for App { thread::spawn(move || { send.send(run_integrated_server( progress_send, - StartGameInfo::Create(GameCreationInfo::Megabase(false)), - )).expect("Channel send failed"); + StartGameInfo::Create { + info: GameCreationInfo::Megabase(false), + name: new_game_name, + }, + None, + )) + .expect("Channel send failed"); }); #[cfg(target_arch = "wasm32")] send.send(run_integrated_server( progress_send, - StartGameInfo::Create(GameCreationInfo::Megabase(false)), + StartGameInfo::Create { + info: GameCreationInfo::Megabase(false), + name: new_game_name, + }, + None, )); self.state = AppState::Loading { - start_time: Instant::now(),progress, + start_time: Instant::now(), + progress, game_state_receiver: recv, }; } else if { - let v = ui.horizontal( |ui| { + let v = ui.horizontal(|ui| { let ret = ui.button("Gigabase").clicked(); - let AppState::MainMenu { gigabase_size, .. } = &mut self.state else { + let AppState::NewGameMenu { gigabase_size, .. } = &mut self.state + else { unreachable!() }; - ui.add(Slider::new(gigabase_size, 1..=1_000).logarithmic(true).update_while_editing(true).text("Number of base copies to build")); + ui.add( + Slider::new(gigabase_size, 1..=1_000) + .logarithmic(true) + .update_while_editing(true) + .text("Number of base copies to build"), + ); - let single_base_size = 15.4 / 40.0; - let single_base_usage = 40.0 / 60.0; + let single_base_size = 15.4 / 40.0; + let single_base_usage = 40.0 / 60.0; - ui.label(&format!("Est. Memory Usage: ~{:.1}GB", single_base_size * f64::from(*gigabase_size))); - ui.label(&format!("Est. Memory Bandwidth for 60 UPS: ~{:.1}GB/s", single_base_usage * f64::from(*gigabase_size))); + ui.label(&format!( + "Est. Memory Usage: ~{:.1}GB", + single_base_size * f64::from(*gigabase_size) + )); + ui.label(&format!( + "Est. Memory Bandwidth for 60 UPS: ~{:.1}GB/s", + single_base_usage * f64::from(*gigabase_size) + )); ret }); - v.inner } { let progress = Arc::new(AtomicU64::new(0f64.to_bits())); @@ -503,12 +732,18 @@ impl eframe::App for App { thread::spawn(move || { send.send(run_integrated_server( progress_send, - StartGameInfo::Create(GameCreationInfo::Gigabase(gigabase_size)), - )).expect("Channel send failed"); + StartGameInfo::Create { + info: GameCreationInfo::Gigabase(gigabase_size), + name: new_game_name, + }, + None, + )) + .expect("Channel send failed"); }); self.state = AppState::Loading { - start_time: Instant::now(),progress, + start_time: Instant::now(), + progress, game_state_receiver: recv, }; } else if ui.button("Solar Field").clicked() { @@ -519,15 +754,21 @@ impl eframe::App for App { thread::spawn(move || { send.send(run_integrated_server( progress_send, - StartGameInfo::Create(GameCreationInfo::SolarField( - crate::power::Watt(1_000), - Position { x: 1600, y: 1600 }, - )), - )).expect("Channel send failed"); + StartGameInfo::Create { + info: GameCreationInfo::SolarField( + crate::power::Watt(1_000), + Position { x: 1600, y: 1600 }, + ), + name: new_game_name, + }, + None, + )) + .expect("Channel send failed"); }); self.state = AppState::Loading { - start_time: Instant::now(),progress, + start_time: Instant::now(), + progress, game_state_receiver: recv, }; } else if ui.button("With bp file").clicked() { @@ -540,15 +781,28 @@ impl eframe::App for App { thread::spawn(move || { send.send(run_integrated_server( progress_send, - StartGameInfo::Create(GameCreationInfo::FromBP(path)), - )).expect("Channel send failed"); + StartGameInfo::Create { + info: GameCreationInfo::FromBP(path), + name: new_game_name, + }, + None, + )) + .expect("Channel send failed"); }); self.state = AppState::Loading { - start_time: Instant::now(),progress, + start_time: Instant::now(), + progress, game_state_receiver: recv, }; } + } else { + if let AppState::NewGameMenu { + new_game_name: ngn, .. + } = &mut self.state + { + *ngn = new_game_name; + } } }); }, @@ -558,10 +812,30 @@ impl eframe::App for App { fn on_exit(&mut self) { if let Some(state) = &self.currently_loaded_game { match &state.state { - LoadedGame::ItemU8RecipeU8(state) => save(&state.state, &state.data_store.lock()), - LoadedGame::ItemU8RecipeU16(state) => save(&state.state, &state.data_store.lock()), - LoadedGame::ItemU16RecipeU8(state) => save(&state.state, &state.data_store.lock()), - LoadedGame::ItemU16RecipeU16(state) => save(&state.state, &state.data_store.lock()), + LoadedGame::ItemU8RecipeU8(state) => save( + "Last Exit", + Some("last_exit.save"), + &state.state, + &state.data_store.lock(), + ), + LoadedGame::ItemU8RecipeU16(state) => save( + "Last Exit", + Some("last_exit.save"), + &state.state, + &state.data_store.lock(), + ), + LoadedGame::ItemU16RecipeU8(state) => save( + "Last Exit", + Some("last_exit.save"), + &state.state, + &state.data_store.lock(), + ), + LoadedGame::ItemU16RecipeU16(state) => save( + "Last Exit", + Some("last_exit.save"), + &state.state, + &state.data_store.lock(), + ), } } } @@ -691,10 +965,14 @@ impl App { }, Err(escape) => match escape { EscapeMenuOptions::BackToMainMenu => { - self.state = AppState::MainMenu { - in_ip_box: None, - gigabase_size: 40, - }; + save( + "Last Exit", + Some("last_exit.save"), + &loaded_game_sized.state, + &loaded_game_sized.data_store.lock(), + ); + + self.state = AppState::MainMenu { in_ip_box: None }; self.last_rendered_update = 0; self.input_sender = None; diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 5ad090c..36650e1 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -1916,7 +1916,14 @@ pub fn render_ui< if cfg!(target_os = "linux") { if tick < state_machine_ref.last_tick_seen_for_autosave { if state_machine_ref.current_fork_save_in_progress.is_none() { - let recv = save_with_fork(&*world, &*simulation_state, &*aux_data, data_store_ref); + let recv = save_with_fork( + &aux_data.game_name, + None, + &*world, + &*simulation_state, + &*aux_data, + data_store_ref, + ); if let Some(recv) = recv { recv.set_nonblocking(true) .expect("Could not set pipe to nonblocking!"); @@ -1926,7 +1933,14 @@ pub fn render_ui< }); } else { error!("Nonblocking save failed to start! Saving in blocking mode"); - save_components(&*world, &*simulation_state, &*aux_data, data_store_ref); + save_components( + &aux_data.game_name, + None, + &*world, + &*simulation_state, + &*aux_data, + data_store_ref, + ); } } else { warn!( @@ -1940,7 +1954,14 @@ pub fn render_ui< let progress = if tick > 1 && tick <= 5 { 1.0 } else { 0.0 }; if tick < state_machine_ref.last_tick_seen_for_autosave { let _timer = Timer::new("Saving"); - save_components(&*world, &*simulation_state, &*aux_data, data_store_ref); + save_components( + &aux_data.game_name, + None, + &*world, + &*simulation_state, + &*aux_data, + data_store_ref, + ); } Window::new("Saving...").default_open(true).show(ctx, |ui| { ui.add(ProgressBar::new(progress).corner_radius(0.0)); @@ -1995,7 +2016,14 @@ pub fn render_ui< .show(ctx, |ui| { ui.heading("Paused"); if ui.button("Save").clicked() { - save_components(&*world, &*simulation_state, &*aux_data, data_store_ref); + save_components( + &aux_data.game_name, + Some(&format!("{}.save", &aux_data.game_name)), + &*world, + &*simulation_state, + &*aux_data, + data_store_ref, + ); } if ui @@ -2011,8 +2039,14 @@ pub fn render_ui< }) .clicked() { - let recv = - save_with_fork(&*world, &*simulation_state, &*aux_data, data_store_ref); + let recv = save_with_fork( + &aux_data.game_name, + Some(&format!("{}.save", &aux_data.game_name)), + &*world, + &*simulation_state, + &*aux_data, + data_store_ref, + ); if let Some(recv) = recv { recv.set_nonblocking(true) .expect("Could not set pipe to nonblocking!"); @@ -3506,7 +3540,7 @@ pub fn render_ui< ui.label(format!("Productivity: {:.1}%", prod_mod * 100.0)); ui.label(format!("Max Consumption: {}({:+.0}%)", Watt((base_power_consumption.0 as f64 * (1.0 + power_consumption_mod as f64)) as u64), power_consumption_mod * 100.0)); - + #[cfg(feature = "assembler-craft-tracking")] ui.label(format!("Crafts finished: {}", times_craft_finished)); diff --git a/src/saving/loading.rs b/src/saving/loading.rs new file mode 100644 index 0000000..8f2e377 --- /dev/null +++ b/src/saving/loading.rs @@ -0,0 +1,47 @@ +use std::path::{Path, PathBuf}; + +use crate::saving::{ + LoadError, + save_file_settings::{SaveFileInfo, StoredSaveFileInfo}, + try_load_at, +}; + +#[derive(Debug)] +pub(crate) struct SaveFileList { + pub(crate) save_files: Vec>, +} + +#[derive(Debug)] +pub(crate) enum SaveFileError { + CouldNotOpenSaveFile(std::io::Error), + LoadError(LoadError), +} + +impl SaveFileList { + pub(crate) fn generate_from_save_folder(save_folder: &Path) -> Self { + let folder = std::fs::read_dir(save_folder).expect("Could not read save folder"); + + let mut saves: Vec<_> = folder + .map(|save_folder| { + let save = save_folder.unwrap(); + let info_path = save.path().join("save_file_info"); + let info: Result = try_load_at(info_path); + + info.map_err(|err| (save.path(), SaveFileError::LoadError(err))) + .map(|stored| SaveFileInfo { + path: save.path(), + stored, + }) + }) + .collect(); + + saves.sort_by(|a, b| match (a, b) { + (Ok(a), Ok(b)) => a.stored.saved_at.cmp(&b.stored.saved_at).reverse(), + (Ok(_), Err(_)) => std::cmp::Ordering::Greater, + (Err(_), Ok(_)) => std::cmp::Ordering::Less, + (Err((a, _)), Err((b, _))) => a.cmp(b), + }); + + SaveFileList { save_files: saves } + } +} diff --git a/src/saving/mod.rs b/src/saving/mod.rs index 0d34da5..90b1dcf 100644 --- a/src/saving/mod.rs +++ b/src/saving/mod.rs @@ -4,6 +4,7 @@ use std::{ io::{Read, Write}, marker::PhantomData, path::PathBuf, + time::Duration, }; use bitcode::Encode; @@ -20,8 +21,12 @@ use crate::{ item::IdxTrait, join_many::join, par_generation::Timer, + saving::save_file_settings::StoredSaveFileInfo, }; +pub mod loading; +mod save_file_settings; + #[derive(Debug, Encode, serde::Deserialize, serde::Serialize)] pub struct SaveGame< ItemIdxType: IdxTrait, @@ -63,23 +68,38 @@ pub fn save_at_fork(value: &V, path: PathBuf) { } pub fn load_at serde::Deserialize<'a>>(path: PathBuf) -> V { + try_load_at(path).expect("Failed to load file") +} + +#[derive(Debug)] +pub(crate) enum LoadError { + CouldNotOpenFile(std::io::Error), + DeserializationFailed(bincode::error::DecodeError), +} + +pub fn try_load_at serde::Deserialize<'a>>(path: PathBuf) -> Result { profiling::scope!("Load at", format!("path: {}", path.display())); let file = { profiling::scope!("Open file"); - File::open(&path).expect(&format!("could not open file {:?}", &path)) + File::open(&path).map_err(|err| LoadError::CouldNotOpenFile(err))? }; let mut buf_reader = BufReader::new(file); - { + let loaded = { profiling::scope!("Decompressing and deserializing"); bincode::serde::decode_from_std_read(&mut buf_reader, bincode::config::standard()) - .expect("Deserialization failed") - } + .map_err(|err| LoadError::DeserializationFailed(err))? + }; + + Ok(loaded) } /// # Panics /// If File system stuff fails pub fn save_components( + name: &str, + save_name: Option<&str>, + world: &World, simulation_state: &SimulationState, aux_data: &AuxillaryData, @@ -113,7 +133,24 @@ pub fn save_components( // } let temp_file_dir = dir.data_dir().join("tmp.save"); - let save_file_dir = dir.data_dir().join("save.save"); + let save_file_dir = if let Some(name) = save_name { + dir.data_dir().join(&name) + } else { + dir.data_dir().join("autosave.save") + }; + + let info = StoredSaveFileInfo { + name: if save_name.is_some() { + name.to_string() + } else { + format!("[Autosave] {}", name) + }, + saved_at: chrono::offset::Utc::now(), + playtime: Duration::from_secs(1) / 60 * aux_data.current_tick as u32, + is_autosave: save_name.is_none(), + includes_replay: false, + preview: None, + }; create_dir_all(&temp_file_dir).expect("Could not create temp dir"); @@ -136,6 +173,9 @@ pub fn save_components( } = simulation_state; join!( + || { + save_at(&info, temp_file_dir.join("save_file_info")); + }, || { save_at(&checksum, temp_file_dir.join("checksum")); }, @@ -208,6 +248,9 @@ pub fn save_components( /// # Panics /// If File system stuff fails pub fn save_components_fork_safe( + name: &str, + save_name: Option<&str>, + world: &World, simulation_state: &SimulationState, aux_data: &AuxillaryData, @@ -219,8 +262,27 @@ pub fn save_components_fork_safe create_dir_all(dir.data_dir()).expect("Could not create data dir"); + // FIXME: Allocation and chrono is prob illegal after a fork + let info = StoredSaveFileInfo { + name: if save_name.is_some() { + name.to_string() + } else { + format!("[Autosave] {}", name) + }, + saved_at: chrono::offset::Utc::now(), + playtime: Duration::from_secs(1) / 60 * aux_data.current_tick as u32, + + is_autosave: save_name.is_none(), + includes_replay: false, + preview: None, + }; + let temp_file_dir = dir.data_dir().join("tmp.save"); - let save_file_dir = dir.data_dir().join("save.save"); + let save_file_dir = if let Some(name) = save_name { + dir.data_dir().join(&name) + } else { + dir.data_dir().join("autosave.save") + }; create_dir_all(&temp_file_dir).expect("Could not create temp dir"); @@ -240,6 +302,7 @@ pub fn save_components_fork_safe }, } = simulation_state; + save_at_fork(&info, temp_file_dir.join("save_file_info")); send.write(&[0]).expect("Write to pipe failed"); // send.flush().expect("Flushing pipe failed"); save_at_fork(checksum, temp_file_dir.join("checksum")); @@ -312,6 +375,9 @@ pub fn save_components_fork_safe /// # Panics /// If File system stuff fails pub fn save_with_fork( + name: &str, + save_name: Option<&str>, + world: &World, simulation_state: &SimulationState, aux_data: &AuxillaryData, @@ -329,12 +395,27 @@ pub fn save_with_fork( Ok(fork::Fork::Child) => {}, Err(e) => { log::error!("Saving with fork failed: Unable to create fork: {}", e); - save_components(world, simulation_state, aux_data, data_store); + save_components( + name, + save_name, + world, + simulation_state, + aux_data, + data_store, + ); return None; }, } - save_components_fork_safe(world, simulation_state, aux_data, data_store, send); + save_components_fork_safe( + name, + save_name, + world, + simulation_state, + aux_data, + data_store, + send, + ); unsafe { libc::_exit(0) } } @@ -349,6 +430,9 @@ pub fn save_with_fork( /// # Panics /// If File system stuff fails pub fn save( + name: &str, + save_name: Option<&str>, + game_state: &GameState, data_store: &DataStore, ) { @@ -372,7 +456,14 @@ pub fn save( &*aux_data.lock() }; - save_components(world, simulation_state, aux_data, data_store); + save_components( + name, + save_name, + world, + simulation_state, + aux_data, + data_store, + ); } /// # Panics diff --git a/src/saving/save_file_settings.rs b/src/saving/save_file_settings.rs new file mode 100644 index 0000000..e2eb0ec --- /dev/null +++ b/src/saving/save_file_settings.rs @@ -0,0 +1,32 @@ +use std::{path::PathBuf, time::Duration}; + +use chrono::Utc; + +use crate::scenario::ScenarioInfo; + +#[derive(Debug, Clone)] +pub(crate) struct SaveFileInfo { + pub(crate) path: PathBuf, + pub(crate) stored: StoredSaveFileInfo, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct StoredSaveFileInfo { + pub(crate) name: String, + + pub(crate) saved_at: chrono::DateTime, + pub(crate) playtime: Duration, + pub(super) is_autosave: bool, + + pub(super) includes_replay: bool, + + // scenario: ScenarioInfo, + + // TODO: Do I want a preview? + pub(super) preview: Option<()>, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct SaveFileSettings { + save_location: PathBuf, +} diff --git a/src/scenario.rs b/src/scenario.rs new file mode 100644 index 0000000..9f08908 --- /dev/null +++ b/src/scenario.rs @@ -0,0 +1,87 @@ +use std::ops::RangeInclusive; + +use rand::Rng; +use rand_xoshiro::rand_core::SeedableRng; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct ScenarioInfo { + // The extent of the world. + // Used for ribbon worlds + world_x_range: RangeInclusive, + world_y_range: RangeInclusive, + + // The player will spawn in a random position in this rect + player_spawn_area: [RangeInclusive; 2], + + pre_placed_structures: ScenarioStructureList, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct Seed([u8; 16]); + +impl ScenarioInfo { + fn instantiate( + &self, + seed: Seed, + pre_placed_structure_count: Option, + ) -> ScenarioInfoInstantiation<'_> { + let mut random = rand_xoshiro::Xoroshiro128Plus::from_seed(seed.0); + + let x = random.random_range(self.player_spawn_area[0].clone()); + let y = random.random_range(self.player_spawn_area[1].clone()); + + ScenarioInfoInstantiation { + seed, + + world_x_range: self.world_x_range.clone(), + world_y_range: self.world_y_range.clone(), + player_spawn_area: [x, y], + + pre_placed_structure_count, + pre_placed_structures: &self.pre_placed_structures, + } + } +} + +#[derive(Debug, Clone, serde::Serialize)] +pub(crate) struct ScenarioInfoInstantiation<'a> { + seed: Seed, + + world_x_range: RangeInclusive, + world_y_range: RangeInclusive, + + player_spawn_area: [i32; 2], + + pre_placed_structure_count: Option, + pre_placed_structures: &'a ScenarioStructureList, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct ScenarioStructureList { + structures: Vec, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) enum ScenarioStructureKind { + Oneshot { + structure: ScenarioStructureDescription, + }, + Tiled { + structure: ScenarioStructureDescription, + tiling_info: (), + + variable_count: Option>, + }, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct ScenarioStructureDescription {} + +pub(crate) const EMPTY_LAB_WORLD: ScenarioInfo = ScenarioInfo { + world_x_range: -1_000_000..=1_000_000, + world_y_range: -1_000_000..=1_000_000, + + player_spawn_area: [0..=0, 0..=0], + + pre_placed_structures: ScenarioStructureList { structures: vec![] }, +}; From f027cf647513f52b274d0d1924c3a3de24287f4f Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 4 Jan 2026 03:15:33 +0100 Subject: [PATCH 063/152] cargo update --- Cargo.lock | 679 +++++++++++++++++++++----------------------- rust-toolchain.toml | 2 +- 2 files changed, 332 insertions(+), 349 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7085c09..e09d0d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,6 +150,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + [[package]] name = "aligned-vec" version = "0.6.4" @@ -235,9 +244,12 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] [[package]] name = "arg_enum_proc_macro" @@ -247,7 +259,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -268,6 +280,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "ash" version = "0.38.0+1.3.281" @@ -361,16 +382,16 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.2", + "rustix 1.1.3", "slab", "windows-sys 0.61.2", ] [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener", "event-listener-strategy", @@ -403,7 +424,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 1.1.2", + "rustix 1.1.3", ] [[package]] @@ -414,7 +435,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -429,7 +450,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.2", + "rustix 1.1.3", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -449,7 +470,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -528,7 +549,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -544,7 +565,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -553,6 +574,26 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.17", + "v_frame", + "y4m", +] + [[package]] name = "av1-grain" version = "0.2.5" @@ -664,9 +705,9 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitcode" -version = "0.6.7" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648bd963d2e5d465377acecfb4b827f9f553b6bc97a8f61715779e9ed9e52b74" +checksum = "0a6ed1b54d8dc333e7be604d00fa9262f4635485ffea923647b6521a5fff045d" dependencies = [ "arrayvec", "bitcode_derive", @@ -677,13 +718,13 @@ dependencies = [ [[package]] name = "bitcode_derive" -version = "0.6.7" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffebfc2d28a12b262c303cb3860ee77b91bd83b1f20f0bd2a9693008e2f55a9e" +checksum = "238b90427dfad9da4a9abd60f3ec1cdee6b80454bde49ed37f1781dd8e9dc7f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -703,9 +744,12 @@ dependencies = [ [[package]] name = "bitstream-io" -version = "2.6.0" +version = "4.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] [[package]] name = "bitvec" @@ -766,12 +810,6 @@ dependencies = [ "piper", ] -[[package]] -name = "built" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" - [[package]] name = "built" version = "0.8.0" @@ -784,9 +822,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" @@ -805,7 +843,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -822,9 +860,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "calloop" @@ -848,7 +886,7 @@ checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" dependencies = [ "bitflags 2.10.0", "polling", - "rustix 1.1.2", + "rustix 1.1.3", "slab", "tracing", ] @@ -872,16 +910,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" dependencies = [ "calloop 0.14.3", - "rustix 1.1.2", + "rustix 1.1.3", "wayland-backend", "wayland-client", ] [[package]] name = "cc" -version = "1.2.45" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "jobserver", @@ -895,16 +933,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -952,7 +980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d38f1088dcf6ce3487a09c49fc2d2f8759045603f24d5814357e9283260426" dependencies = [ "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -1094,6 +1122,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "core_maths" version = "0.1.1" @@ -1160,9 +1197,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -1197,7 +1234,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -1273,7 +1310,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -1396,7 +1433,7 @@ source = "git+https://github.com/BloodStainedCrow/egui-show-info#0b1a1a7e6b2b759 dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -1526,9 +1563,9 @@ checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enum-map" @@ -1548,7 +1585,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -1569,7 +1606,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -1580,7 +1617,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -1623,7 +1660,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -1680,9 +1717,9 @@ dependencies = [ [[package]] name = "exr" -version = "1.73.0" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" dependencies = [ "bit_field", "half", @@ -1702,7 +1739,7 @@ dependencies = [ "bincode 2.0.1", "bitcode", "bitvec", - "built 0.8.0", + "built", "bytemuck", "charts-rs", "chrono", @@ -1792,7 +1829,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -1806,9 +1843,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "fixed-buffer" @@ -1907,7 +1944,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -2009,7 +2046,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -2080,9 +2117,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -2090,20 +2127,20 @@ dependencies = [ [[package]] name = "get-size-derive2" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46b134aa084df7c3a513a1035c52f623e4b3065dfaf3d905a4f28a2e79b5bb3f" +checksum = "ab21d7bd2c625f2064f04ce54bcb88bc57c45724cde45cba326d784e22d3f71a" dependencies = [ "attribute-derive", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] name = "get-size2" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d51c9f2e956a517619ad9e7eaebc7a573f9c49b38152e12eade750f89156f9" +checksum = "879272b0de109e2b67b39fcfe3d25fdbba96ac07e44a254f5a0b4d7ff55340cb" dependencies = [ "get-size-derive2", ] @@ -2114,7 +2151,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.2", + "rustix 1.1.3", "windows-link 0.2.1", ] @@ -2147,9 +2184,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.13.3" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" dependencies = [ "color_quant", "weezl", @@ -2163,9 +2200,9 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "git2" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" +checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" dependencies = [ "bitflags 2.10.0", "libc", @@ -2361,9 +2398,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heapless" @@ -2475,9 +2512,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -2489,9 +2526,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -2531,9 +2568,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.8" +version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" dependencies = [ "bytemuck", "byteorder-lite", @@ -2549,8 +2586,8 @@ dependencies = [ "rayon", "rgb", "tiff", - "zune-core", - "zune-jpeg", + "zune-core 0.5.0", + "zune-jpeg 0.5.7", ] [[package]] @@ -2577,12 +2614,12 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "rayon", "serde", "serde_core", @@ -2605,7 +2642,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -2655,15 +2692,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -2675,9 +2703,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jni" @@ -2713,9 +2741,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -2763,9 +2791,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libfuzzer-sys" @@ -2779,9 +2807,9 @@ dependencies = [ [[package]] name = "libgit2-sys" -version = "0.18.2+1.9.1" +version = "0.18.3+1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" dependencies = [ "cc", "libc", @@ -2817,29 +2845,29 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall 0.5.18", + "redox_syscall 0.7.0", ] [[package]] name = "libz-rs-sys" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" dependencies = [ "zlib-rs", ] [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "libc", @@ -2883,9 +2911,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loop9" @@ -2920,7 +2948,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -3028,9 +3056,9 @@ checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" [[package]] name = "moxcms" -version = "0.7.9" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" dependencies = [ "num-traits", "pxfm", @@ -3187,7 +3215,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -3239,7 +3267,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -3566,10 +3594,11 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orbclient" -version = "0.3.49" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "247ad146e19b9437f8604c21f8652423595cf710ad108af40e77d3ae6e96b827" +checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" dependencies = [ + "libc", "libredox", ] @@ -3663,6 +3692,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pathdiff" version = "0.2.3" @@ -3728,7 +3763,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", "unicase", ] @@ -3765,7 +3800,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -3833,7 +3868,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -3845,9 +3880,9 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "postcard" @@ -3898,7 +3933,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.7", + "toml_edit", ] [[package]] @@ -3946,9 +3981,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] @@ -3970,7 +4005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -3984,7 +4019,7 @@ dependencies = [ "bitflags 2.10.0", "num-traits", "rand 0.9.2", - "rand_chacha 0.9.0", + "rand_chacha", "rand_xorshift 0.4.0", "regex-syntax", "rusty-fork", @@ -4029,9 +4064,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.25" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" dependencies = [ "num-traits", ] @@ -4069,9 +4104,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.37.5" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", ] @@ -4104,7 +4139,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -4125,8 +4160,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "libc", - "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -4136,20 +4169,10 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.3", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_chacha" version = "0.9.0" @@ -4165,9 +4188,6 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] [[package]] name = "rand_core" @@ -4213,19 +4233,21 @@ checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" [[package]] name = "rav1e" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" dependencies = [ + "aligned-vec", "arbitrary", "arg_enum_proc_macro", "arrayvec", + "av-scenechange", "av1-grain", "bitstream-io", - "built 0.7.7", + "built", "cfg-if", "interpolate_name", - "itertools 0.12.1", + "itertools 0.14.0", "libc", "libfuzzer-sys", "log", @@ -4234,23 +4256,21 @@ dependencies = [ "noop_proc_macro", "num-derive", "num-traits", - "once_cell", "paste", "profiling", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand 0.9.2", + "rand_chacha", "simd_helpers", - "system-deps", - "thiserror 1.0.69", + "thiserror 2.0.17", "v_frame", "wasm-bindgen", ] [[package]] name = "ravif" -version = "0.11.20" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" dependencies = [ "avif-serialize", "imgref", @@ -4326,6 +4346,15 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -4482,7 +4511,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.109", + "syn 2.0.113", "unicode-ident", ] @@ -4528,9 +4557,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags 2.10.0", "errno", @@ -4575,12 +4604,6 @@ dependencies = [ "unicode-script", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "same-file" version = "1.0.6" @@ -4648,20 +4671,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -4683,16 +4706,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", + "syn 2.0.113", ] [[package]] @@ -4714,18 +4728,19 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simd_helpers" @@ -4771,9 +4786,9 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "slotmap" -version = "1.0.7" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" dependencies = [ "version_check", ] @@ -4825,7 +4840,7 @@ dependencies = [ "libc", "log", "memmap2", - "rustix 1.1.2", + "rustix 1.1.3", "thiserror 2.0.17", "wayland-backend", "wayland-client", @@ -4877,7 +4892,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -4974,7 +4989,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -4986,7 +5001,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -5021,9 +5036,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.109" +version = "2.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" +checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" dependencies = [ "proc-macro2", "quote", @@ -5049,20 +5064,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", + "syn 2.0.113", ] [[package]] @@ -5077,22 +5079,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -5137,7 +5133,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -5148,7 +5144,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -5168,7 +5164,7 @@ dependencies = [ "half", "quick-error 2.0.1", "weezl", - "zune-jpeg", + "zune-jpeg 0.4.21", ] [[package]] @@ -5272,75 +5268,41 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", - "serde", - "serde_spanned", - "toml_datetime 0.6.11", - "winnow", -] - -[[package]] -name = "toml_edit" -version = "0.23.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" -dependencies = [ - "indexmap", - "toml_datetime 0.7.3", + "toml_datetime", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -5350,20 +5312,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -5453,9 +5415,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-script" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" [[package]] name = "unicode-segmentation" @@ -5534,12 +5496,12 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -5566,12 +5528,6 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab68b56840f69efb0fefbe3ab6661499217ffdc58e2eef7c3f6f69835386322" -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - [[package]] name = "version_check" version = "0.9.5" @@ -5620,9 +5576,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -5633,9 +5589,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -5646,9 +5602,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5656,22 +5612,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -5693,13 +5649,13 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" dependencies = [ "cc", "downcast-rs", - "rustix 1.1.2", + "rustix 1.1.3", "scoped-tls", "smallvec", "wayland-sys", @@ -5707,12 +5663,12 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.11" +version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" dependencies = [ "bitflags 2.10.0", - "rustix 1.1.2", + "rustix 1.1.3", "wayland-backend", "wayland-scanner", ] @@ -5730,20 +5686,20 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.31.11" +version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" +checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078" dependencies = [ - "rustix 1.1.2", + "rustix 1.1.3", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.32.9" +version = "0.32.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" dependencies = [ "bitflags 2.10.0", "wayland-backend", @@ -5766,9 +5722,9 @@ dependencies = [ [[package]] name = "wayland-protocols-misc" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfe33d551eb8bffd03ff067a8b44bb963919157841a99957151299a6307d19c" +checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e" dependencies = [ "bitflags 2.10.0", "wayland-backend", @@ -5779,9 +5735,9 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" +checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" dependencies = [ "bitflags 2.10.0", "wayland-backend", @@ -5792,9 +5748,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" +checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" dependencies = [ "bitflags 2.10.0", "wayland-backend", @@ -5805,20 +5761,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" dependencies = [ "proc-macro2", - "quick-xml 0.37.5", + "quick-xml 0.38.4", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" dependencies = [ "dlib", "log", @@ -5828,9 +5784,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -5874,9 +5830,9 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009936b22a61d342859b5f0ea64681cbb35a358ab548e2a44a8cf0dac2d980b8" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "wgpu" @@ -6162,7 +6118,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -6173,7 +6129,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -6184,7 +6140,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -6195,7 +6151,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -6569,9 +6525,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -6619,7 +6575,7 @@ dependencies = [ "libc", "libloading", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "x11rb-protocol", ] @@ -6666,6 +6622,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yoke" version = "0.8.1" @@ -6685,7 +6647,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", "synstructure", ] @@ -6725,9 +6687,9 @@ dependencies = [ [[package]] name = "zbus-lockstep" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e96e38ded30eeab90b6ba88cb888d70aef4e7489b6cd212c5e5b5ec38045b6" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" dependencies = [ "zbus_xml", "zvariant", @@ -6735,13 +6697,13 @@ dependencies = [ [[package]] name = "zbus-lockstep-macros" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6821851fa840b708b4cbbaf6241868cabc85a2dc22f426361b0292bfc0b836" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", "zbus-lockstep", "zbus_xml", "zvariant", @@ -6756,7 +6718,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", "zbus_names", "zvariant", "zvariant_utils", @@ -6789,22 +6751,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] @@ -6824,7 +6786,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", "synstructure", ] @@ -6858,14 +6820,20 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", ] [[package]] name = "zlib-rs" -version = "0.5.2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" + +[[package]] +name = "zmij" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" +checksum = "4ee2a72b10d087f75fb2e1c2c7343e308fe6970527c22a41caf8372e165ff5c1" [[package]] name = "zune-core" @@ -6873,6 +6841,12 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +[[package]] +name = "zune-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" + [[package]] name = "zune-inflate" version = "0.2.54" @@ -6888,7 +6862,16 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ - "zune-core", + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d915729b0e7d5fe35c2f294c5dc10b30207cc637920e5b59077bfa3da63f28" +dependencies = [ + "zune-core 0.5.0", ] [[package]] @@ -6915,7 +6898,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.109", + "syn 2.0.113", "zvariant_utils", ] @@ -6928,6 +6911,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.109", + "syn 2.0.113", "winnow", ] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9fe5ede..ed45212 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "nightly-2025-05-27" +channel = "nightly-2025-12-28" components = ["clippy", "rustfmt"] From 4f787244bd6c3e0f2fc112f3471e4c7a0976bb2a Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 4 Jan 2026 04:36:12 +0100 Subject: [PATCH 064/152] Update egui --- Cargo.lock | 148 +++++++++++++++++++++--------------- Cargo.toml | 23 ++---- src/assembler/simd.rs | 4 +- src/rendering/eframe_app.rs | 10 ++- src/research.rs | 2 + 5 files changed, 106 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e09d0d2..93fafdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1351,19 +1351,21 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] name = "ecolor" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bdf37f8d5bd9aa7f753573fdda9cf7343afa73dd28d7bfe9593bd9798fc07e" dependencies = [ "bytemuck", "color-hex", - "emath 0.31.1 (git+https://github.com/BloodStainedCrow/egui?rev=4e11a02)", + "emath", "serde", ] [[package]] name = "eframe" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14d1c15e7bd136b309bd3487e6ffe5f668b354cd9768636a836dd738ac90eb0b" dependencies = [ "ahash", "bytemuck", @@ -1389,7 +1391,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "web-time 1.1.0", + "web-time", "wgpu", "winapi", "windows-sys 0.59.0", @@ -1398,26 +1400,28 @@ dependencies = [ [[package]] name = "egui" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5d0306cd61ca75e29682926d71f2390160247f135965242e904a636f51c0dc" dependencies = [ "accesskit", "ahash", "bitflags 2.10.0", - "emath 0.31.1 (git+https://github.com/BloodStainedCrow/egui?rev=4e11a02)", + "emath", "epaint", "log", "nohash-hasher", "profiling", "ron 0.10.1", "serde", + "smallvec", "unicode-segmentation", ] [[package]] name = "egui-show-info" version = "0.1.0" -source = "git+https://github.com/BloodStainedCrow/egui-show-info#0b1a1a7e6b2b75935b7bb7710de4fc4900a35fa9" +source = "git+https://github.com/BloodStainedCrow/egui-show-info#2f1c3f454e4577ff72e0c7eb21cafe4850c60ac2" dependencies = [ "bimap", "egui", @@ -1429,7 +1433,7 @@ dependencies = [ [[package]] name = "egui-show-info-derive" version = "0.1.0" -source = "git+https://github.com/BloodStainedCrow/egui-show-info#0b1a1a7e6b2b75935b7bb7710de4fc4900a35fa9" +source = "git+https://github.com/BloodStainedCrow/egui-show-info#2f1c3f454e4577ff72e0c7eb21cafe4850c60ac2" dependencies = [ "proc-macro2", "quote", @@ -1438,8 +1442,9 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c12eca13293f8eba27a32aaaa1c765bfbf31acd43e8d30d5881dcbe5e99ca0c7" dependencies = [ "ahash", "bytemuck", @@ -1450,15 +1455,16 @@ dependencies = [ "profiling", "thiserror 1.0.69", "type-map", - "web-time 1.1.0", + "web-time", "wgpu", "winit", ] [[package]] name = "egui-winit" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f95d0a91f9cb0dc2e732d49c2d521ac8948e1f0b758f306fb7b14d6f5db3927f" dependencies = [ "accesskit_winit", "ahash", @@ -1469,16 +1475,16 @@ dependencies = [ "profiling", "raw-window-handle", "smithay-clipboard", - "web-time 1.1.0", + "web-time", "webbrowser", "winit", ] [[package]] name = "egui_extras" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624659a2e972a46f4d5f646557906c55f1cd5a0836eddbe610fdf1afba1b4226" +checksum = "dddbceddf39805fc6c62b1f7f9c05e23590b40844dc9ed89c6dc6dbc886e3e3b" dependencies = [ "ahash", "egui", @@ -1491,8 +1497,9 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7037813341727937f9e22f78d912f3e29bc3c46e2f40a9e82bb51cbf5e4cfb" dependencies = [ "ahash", "bytemuck", @@ -1508,10 +1515,12 @@ dependencies = [ [[package]] name = "egui_graphs" -version = "0.25.1" -source = "git+https://github.com/BloodStainedCrow/egui_graphs?branch=tree_layout#32aeb647d0ebd65ff0105274397ce4c4258d9e87" +version = "0.28.0" +source = "git+https://github.com/BloodStainedCrow/egui_graphs?branch=tree_layout#5e4e3191d6d17e264660d78b6f47363d16bd325b" dependencies = [ "egui", + "getrandom 0.2.16", + "instant", "petgraph 0.8.2", "rand 0.9.2", "serde", @@ -1519,13 +1528,13 @@ dependencies = [ [[package]] name = "egui_plot" -version = "0.32.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14ae092b46ea532f6c69d3e71036fb3b688fd00fd09c2a1e43d17051a8ae43e6" +checksum = "524318041a8ea90c81c738e8985f8ad9e3f9bed636b03c2ff37b218113ed5121" dependencies = [ "ahash", "egui", - "emath 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", + "emath", ] [[package]] @@ -1536,14 +1545,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "emath" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" - -[[package]] -name = "emath" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +checksum = "45fd7bc25f769a3c198fe1cf183124bf4de3bd62ef7b4f1eaf6b08711a3af8db" dependencies = [ "bytemuck", "serde", @@ -1622,14 +1626,15 @@ dependencies = [ [[package]] name = "epaint" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63adcea970b7a13094fe97a36ab9307c35a750f9e24bf00bb7ef3de573e0fddb" dependencies = [ "ab_glyph", "ahash", "bytemuck", "ecolor", - "emath 0.31.1 (git+https://github.com/BloodStainedCrow/egui?rev=4e11a02)", + "emath", "epaint_default_fonts", "log", "nohash-hasher", @@ -1640,8 +1645,9 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.31.1" -source = "git+https://github.com/BloodStainedCrow/egui?rev=4e11a02#4e11a02615078f509d9acc474ae13a66b411eaf7" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1537accc50c9cab5a272c39300bdd0dd5dca210f6e5e8d70be048df9596e7ca2" [[package]] name = "equator" @@ -1776,7 +1782,7 @@ dependencies = [ "postcard", "profiling", "proptest", - "puffin", + "puffin 0.19.1 (git+https://github.com/EmbarkStudios/puffin)", "puffin_egui", "rand 0.9.2", "rand_xoshiro", @@ -2632,6 +2638,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -2915,6 +2924,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "log-once" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d8a05e3879b317b1b6dbf353e5bba7062bedcc59815267bb23eaa0c576cebf0" +dependencies = [ + "log", +] + [[package]] name = "loop9" version = "0.1.5" @@ -3995,7 +4013,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" dependencies = [ "profiling-procmacros", - "puffin", + "puffin 0.19.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -4029,37 +4047,51 @@ dependencies = [ [[package]] name = "puffin" -version = "0.19.2" -source = "git+https://github.com/BloodStainedCrow/puffin#420f296797ecf2acdc7156bf65e2b9a2d8e06c8d" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9dae7b05c02ec1a6bc9bcf20d8bc64a7dcbf57934107902a872014899b741f" dependencies = [ "anyhow", - "bincode 1.3.3", "byteorder", "cfg-if", "itertools 0.10.5", + "once_cell", + "parking_lot 0.12.5", +] + +[[package]] +name = "puffin" +version = "0.19.1" +source = "git+https://github.com/EmbarkStudios/puffin#c5276b9d5264af37a9c9fb2655990a3a0b720a0b" +dependencies = [ + "anyhow", + "bincode 1.3.3", + "byteorder", + "cfg-if", + "itertools 0.14.0", "js-sys", "lz4_flex", - "once_cell", "parking_lot 0.12.5", "serde", - "web-time 0.2.4", + "web-time", ] [[package]] name = "puffin_egui" -version = "0.29.1" -source = "git+https://github.com/BloodStainedCrow/puffin#420f296797ecf2acdc7156bf65e2b9a2d8e06c8d" +version = "0.29.0" +source = "git+https://github.com/EmbarkStudios/puffin#c5276b9d5264af37a9c9fb2655990a3a0b720a0b" dependencies = [ "egui", "egui_extras", "indexmap", + "log", + "log-once", "natord", - "once_cell", "parking_lot 0.12.5", - "puffin", + "puffin 0.19.1 (git+https://github.com/EmbarkStudios/puffin)", "time", "vec1", - "web-time 0.2.4", + "web-time", ] [[package]] @@ -5169,8 +5201,8 @@ dependencies = [ [[package]] name = "tilelib" -version = "0.1.0" -source = "git+https://github.com/BloodStainedCrow/tilelib.git#ddacebc889c872eb4146604e0f4cde1caf2c20d5" +version = "0.2.0" +source = "git+https://github.com/BloodStainedCrow/tilelib.git#a79c22ac7dd51dacb7c5e80e4fcd8ed3448a5f05" dependencies = [ "bytemuck", "egui", @@ -5792,16 +5824,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "web-time" version = "1.1.0" @@ -6516,7 +6538,7 @@ dependencies = [ "wayland-protocols", "wayland-protocols-plasma", "web-sys", - "web-time 1.1.0", + "web-time", "windows-sys 0.52.0", "x11-dl", "x11rb", diff --git a/Cargo.toml b/Cargo.toml index bc3a6f8..fc55c15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,34 +31,33 @@ strum = { version = "0.27.1", features = ["derive"] } # explicitly disable atomic feature, so that bitvecs do not use atomic instructions. very important for performance! bitvec = { version = "1.0.1", features = ["alloc", "serde", "std"], default-features = false } bimap = { version = "0.6.3", features = ["serde", "std"], default-features = false } -eframe = { version = "0.31.1", features = ["accesskit", "default_fonts", "wayland", "web_screen_reader", "x11", "wgpu"], optional = true, default-features = false } +eframe = { version = "0.32", features = ["accesskit", "default_fonts", "wayland", "web_screen_reader", "x11", "wgpu"], optional = true, default-features = false } wgpu = { version = "25.0.2", features = ["webgl"] } -egui_extras = { version = "0.31.1", optional = true } -egui_plot = { version = "0.32.1", optional = true } +egui_extras = { version = "0.32", optional = true } +egui_plot = { version = "0.33", optional = true } rand = "0.9.0" bitcode = { version = "0.6.6", features = ["serde"] } -egui = { version = "0.31.1", features = ["bytemuck", "serde"], optional = true } +egui = { version = "0.32", features = ["bytemuck", "serde"], optional = true } flate2 = { version = "1.1.1", features = ["zlib-rs"] } rstest = "0.25.0" parking_lot = { version = "0.12.3", features = ["serde", "deadlock_detection"] } profiling = { version = "1.0.16" } -puffin_egui = { version = "0.29", optional = true } -puffin = { version = "0.19", features = ["web"] } +puffin_egui = { git = "https://github.com/EmbarkStudios/puffin", optional = true } +puffin = { git = "https://github.com/EmbarkStudios/puffin", features = ["web"] } dhat = "0.3.3" noise = { version = "0.9.0", features = ["std"] } rfd = { version = "0.15.3", optional = true } -egui_graphs = { version = "0.25.1", optional = true } +egui_graphs = { version = "0.28", optional = true } serde_path_to_error = "0.1.17" get-size2 = { version = "0.7.1", features = ["derive"], optional = true } egui-show-info = { git = "https://github.com/BloodStainedCrow/egui-show-info", features = ["petgraph", "parking_lot", "enum-map", "bimap"], optional = true } egui-show-info-derive = { git = "https://github.com/BloodStainedCrow/egui-show-info", optional = true } bytemuck = "1.23.1" -# ph = "0.9.6" memoffset = "0.9.1" smallvec = { version = "1.15.1", features = ["serde"] } -ecolor = { version = "0.31.1", features = ["color-hex"] } +ecolor = { version = "0.32", features = ["color-hex"] } getrandom = { version = "0.3.3", features = ["wasm_js"] } getrandom_old = { version = "0.2.16", features = ["js"], package = "getrandom" } wasm-bindgen = "0.2.104" @@ -88,12 +87,6 @@ winit = "0.30.12" proptest = "1.4.0" [patch.crates-io] -puffin_egui = { git = "https://github.com/BloodStainedCrow/puffin" } -puffin = { git = "https://github.com/BloodStainedCrow/puffin" } -egui = { git = "https://github.com/BloodStainedCrow/egui", rev = "4e11a02" } -eframe = { git = "https://github.com/BloodStainedCrow/egui", rev = "4e11a02" } -ecolor = { git = "https://github.com/BloodStainedCrow/egui", rev = "4e11a02" } -egui-wgpu = { git = "https://github.com/BloodStainedCrow/egui", rev = "4e11a02" } egui_graphs = { git = "https://github.com/BloodStainedCrow/egui_graphs", branch = "tree_layout" } petgraph = { git = "https://github.com/BloodStainedCrow/petgraph", branch = "stable_graph_node_weights_mut_indexed" } diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index f5efd5b..c20f8f1 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -604,7 +604,7 @@ impl .map(|v| v.take()) .unwrap_or(None); } - let Ok(()) = self + let Ok(_) = self .inserter_waitlist_output_vec .push_within_capacity( InternalInserterReinsertionInfo { @@ -703,7 +703,7 @@ impl .map(|v| v.take()) .unwrap_or(None); } - let Ok(()) = self + let Ok(_) = self .inserter_waitlist_output_vec .push_within_capacity( InternalInserterReinsertionInfo { diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index fc8ea48..d8db4ae 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -12,6 +12,7 @@ use std::{ use chrono::Local; use egui_extras::{Column, TableBuilder}; +use egui_graphs::LayoutStateTree; use url::Url; use wasm_timer::Instant; @@ -351,6 +352,14 @@ impl eframe::App for App { state: new_state, tick: current_tick, }); + // FIXME: This is needed to prevent the tech tree from collapsing? + // TODO: Make an issue to investigae why this is needed + Window::new("FIXME").show(ctx, |ui| { + egui_graphs::reset_layout::( + ui, + Some("Tech Tree".to_string()), + ); + }); self.state = AppState::Ingame; } }, @@ -704,7 +713,6 @@ impl eframe::App for App { ui.add( Slider::new(gigabase_size, 1..=1_000) .logarithmic(true) - .update_while_editing(true) .text("Number of base copies to build"), ); diff --git a/src/research.rs b/src/research.rs index 3ee004e..7907190 100644 --- a/src/research.rs +++ b/src/research.rs @@ -247,6 +247,7 @@ impl TechState { data_store: &DataStore, ) -> Graph, (), Directed, u16, DefaultNodeShape, DefaultEdgeShape> { + // TODO: This seems to be called every frame??? egui_graphs::to_graph_custom::<_, _, _, _, DefaultNodeShape, DefaultEdgeShape>( &data_store.technology_tree, |node| { @@ -565,6 +566,7 @@ impl TechState { let mut view = GraphView::<_, _, _, _, _, _, LayoutStateTree, LayoutTree>::new(render_graph) + .with_id(Some("Tech Tree".to_string())) .with_navigations( &SettingsNavigation::new() .with_fit_to_screen_enabled(false) From e7ef2def904c00092ff1e0e9bdedf71490fec0c6 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 4 Jan 2026 16:42:12 +0100 Subject: [PATCH 065/152] Reenable profiling --- Cargo.lock | 37 +++++++------------------------------ Cargo.toml | 2 ++ 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93fafdf..3ac4d31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1770,7 +1770,7 @@ dependencies = [ "hex", "image", "interprocess", - "itertools 0.14.0", + "itertools", "libc", "log", "memoffset", @@ -1782,7 +1782,7 @@ dependencies = [ "postcard", "profiling", "proptest", - "puffin 0.19.1 (git+https://github.com/EmbarkStudios/puffin)", + "puffin", "puffin_egui", "rand 0.9.2", "rand_xoshiro", @@ -2692,15 +2692,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -4013,7 +4004,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" dependencies = [ "profiling-procmacros", - "puffin 0.19.1 (registry+https://github.com/rust-lang/crates.io-index)", + "puffin", ] [[package]] @@ -4045,20 +4036,6 @@ dependencies = [ "unarray", ] -[[package]] -name = "puffin" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9dae7b05c02ec1a6bc9bcf20d8bc64a7dcbf57934107902a872014899b741f" -dependencies = [ - "anyhow", - "byteorder", - "cfg-if", - "itertools 0.10.5", - "once_cell", - "parking_lot 0.12.5", -] - [[package]] name = "puffin" version = "0.19.1" @@ -4068,7 +4045,7 @@ dependencies = [ "bincode 1.3.3", "byteorder", "cfg-if", - "itertools 0.14.0", + "itertools", "js-sys", "lz4_flex", "parking_lot 0.12.5", @@ -4088,7 +4065,7 @@ dependencies = [ "log-once", "natord", "parking_lot 0.12.5", - "puffin 0.19.1 (git+https://github.com/EmbarkStudios/puffin)", + "puffin", "time", "vec1", "web-time", @@ -4279,7 +4256,7 @@ dependencies = [ "built", "cfg-if", "interpolate_name", - "itertools 0.14.0", + "itertools", "libc", "libfuzzer-sys", "log", @@ -5208,7 +5185,7 @@ dependencies = [ "egui", "egui-wgpu", "image", - "itertools 0.14.0", + "itertools", "log", "pollster", "spin_sleep_util", diff --git a/Cargo.toml b/Cargo.toml index fc55c15..6405e66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,8 @@ winit = "0.30.12" proptest = "1.4.0" [patch.crates-io] +puffin_egui = { git = "https://github.com/EmbarkStudios/puffin", optional = true } +puffin = { git = "https://github.com/EmbarkStudios/puffin", features = ["web"] } egui_graphs = { git = "https://github.com/BloodStainedCrow/egui_graphs", branch = "tree_layout" } petgraph = { git = "https://github.com/BloodStainedCrow/petgraph", branch = "stable_graph_node_weights_mut_indexed" } From ac86630167ba094dbe4f99995424f3d7dea9f9ff Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 4 Jan 2026 18:52:41 +0100 Subject: [PATCH 066/152] Correctly switch to linear beacons --- src/app_state.rs | 22 +- src/inserter/belt_storage_pure_buckets.rs | 10 +- src/inserter/storage_storage_with_buckets.rs | 14 +- .../storage_storage_with_buckets_indirect.rs | 6 +- src/power/mod.rs | 193 +++++++++++++----- src/power/power_grid.rs | 105 ++++------ 6 files changed, 222 insertions(+), 128 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index f1692d7..7a6ebe7 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -3661,7 +3661,7 @@ mod tests { } #[test] - fn test_beacons_always_effect(actions in beacon_test_val().prop_shuffle()) { + fn test_beacons_always_effect(actions in beacon_test_val().prop_shuffle(), ticks in 60usize..120) { prop_assume!(actions.iter().position(|a| matches!(a, ActionType::PlaceEntity(PlaceEntityInfo { force: false, entities: EntityPlaceOptions::Single(PlaceEntityType::Assembler { @@ -3681,7 +3681,8 @@ mod tests { Blueprint { actions: actions.into_iter().map(|a| BlueprintAction::from_with_datastore(&a, &*DATA_STORE)).collect() }.apply(false, Position { x: 0, y: 0 }, &mut game_state, &DATA_STORE); - for _ in 0usize..10 { + // Currently Beacons are only able to update every 60 ticks + for _ in 0usize..ticks { GameState::update( &mut *game_state.simulation_state.lock(), &mut *game_state.aux_data.lock(), @@ -3690,20 +3691,25 @@ mod tests { } let world = game_state.world.lock(); - let Some(Entity::Assembler { info: AssemblerInfo::Powered { id, .. }, .. }) = world.get_entity_at(Position { x: 1600, y: 1600 }, &DATA_STORE) else { + let Some(Entity::Assembler { info: AssemblerInfo::Powered { id, .. }, modules, .. }) = world.get_entity_at(Position { x: 1600, y: 1600 }, &DATA_STORE) else { unreachable!("{:?}", game_state.world.lock().get_entity_at(Position { x: 1600, y: 1600 }, &DATA_STORE)); }; + let modules = &world.module_slot_dedup_table[*modules as usize]; + let id = *id; - std::mem::drop(world); + // std::mem::drop(world); - prop_assume!(game_state.simulation_state.lock().factory.power_grids.power_grids[usize::from(id.grid)].last_power_mult == MAX_POWER_MULT); + prop_assert!(game_state.simulation_state.lock().factory.power_grids.power_grids[usize::from(id.grid)].last_power_mult == MAX_POWER_MULT); let info = game_state.simulation_state.lock().factory.power_grids.power_grids[usize::from(id.grid)].get_assembler_info(id, &DATA_STORE); - prop_assert!((info.power_consumption_mod - 0.7).abs() < 1.0e-6, "power_consumption_mod: {:?}", info.power_consumption_mod); + let module_power_effect = modules.iter().flatten().map(|module| DATA_STORE.module_info[*module as usize].power_mod as f32 / 20.0).sum::(); + let module_speed_effect = modules.iter().flatten().map(|module| DATA_STORE.module_info[*module as usize].speed_mod as f32 / 20.0).sum::(); + let module_prod_effect = modules.iter().flatten().map(|module| DATA_STORE.module_info[*module as usize].prod_mod as f32 / 100.0).sum::(); + prop_assert!((info.power_consumption_mod - (0.7 + module_power_effect)).abs() < 1.0e-6, "power_consumption_mod: {:?}, wanted: {}", info.power_consumption_mod, (0.7 + module_power_effect)); prop_assert!((info.base_speed - 1.25).abs() < 1.0e-6, "base_speed: {:?}", info.base_speed); - prop_assert!((info.prod_mod - 0.0).abs() < 1.0e-6, "prod_mod: {:?}", info.prod_mod); - prop_assert!((info.speed_mod - (0.5)).abs() < 1.0e-6, "speed_mod: {:?}", info.speed_mod); + prop_assert!((info.prod_mod - (0.0 + module_prod_effect)).abs() < 1.0e-6, "prod_mod: {:?}", info.prod_mod); + prop_assert!((info.speed_mod - (0.5 + module_speed_effect)).abs() < 1.0e-6, "speed_mod: {:?}, wanted: {}", info.speed_mod, (0.5 + module_speed_effect)); prop_assert_eq!(info.base_power_consumption, Watt(375_000), "base_power_consumption: {:?}", info.base_power_consumption); } diff --git a/src/inserter/belt_storage_pure_buckets.rs b/src/inserter/belt_storage_pure_buckets.rs index 4126620..9f8804c 100644 --- a/src/inserter/belt_storage_pure_buckets.rs +++ b/src/inserter/belt_storage_pure_buckets.rs @@ -1278,7 +1278,7 @@ mod test { use crate::{ belt::smart::SmartBelt, inserter::{FakeUnionStorage, belt_storage_inserter::Dir}, - storage_list::MaxInsertionLimit, + storage_list::{InserterWaitLists, MaxInsertionLimit}, }; use super::*; @@ -1328,10 +1328,12 @@ mod test { ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_in[item].as_mut_slice(), + InserterWaitLists::None, ), ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_out[item].as_mut_slice(), + InserterWaitLists::None, ), ], &mut belts[item], @@ -1345,10 +1347,12 @@ mod test { ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_in[item].as_mut_slice(), + InserterWaitLists::None, ), ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_out[item].as_mut_slice(), + InserterWaitLists::None, ), ], &mut belts[item], @@ -1424,10 +1428,12 @@ mod test { ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storage_in.as_mut_slice(), + InserterWaitLists::None, ), ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storage_out.as_mut_slice(), + InserterWaitLists::None, ), ], belts, @@ -1441,10 +1447,12 @@ mod test { ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storage_in.as_mut_slice(), + InserterWaitLists::None, ), ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storage_out.as_mut_slice(), + InserterWaitLists::None, ), ], belts, diff --git a/src/inserter/storage_storage_with_buckets.rs b/src/inserter/storage_storage_with_buckets.rs index 3b64068..2a79a4a 100644 --- a/src/inserter/storage_storage_with_buckets.rs +++ b/src/inserter/storage_storage_with_buckets.rs @@ -1284,7 +1284,7 @@ mod test { BucketedStorageStorageInserterStoreFrontend, InserterId, InserterIdentifier, }, }, - storage_list::MaxInsertionLimit, + storage_list::{InserterWaitLists, MaxInsertionLimit}, }; use super::BucketedStorageStorageInserterStore; @@ -1313,10 +1313,12 @@ mod test { ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_in[item].as_mut_slice(), + InserterWaitLists::None, ), ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_out[item].as_mut_slice(), + InserterWaitLists::None, ), ], 10, @@ -1378,10 +1380,12 @@ mod test { ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storage_in.as_mut_slice(), + InserterWaitLists::None, ), ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storage_out.as_mut_slice(), + InserterWaitLists::None, ), ], 10, @@ -1422,10 +1426,12 @@ mod test { ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_in.as_mut_slice(), + InserterWaitLists::None, ), ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_out.as_mut_slice(), + InserterWaitLists::None, ), ], 10, @@ -1515,10 +1521,12 @@ mod test { ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_in.as_mut_slice(), + InserterWaitLists::None, ), ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_out.as_mut_slice(), + InserterWaitLists::None, ), ], 10, @@ -1558,10 +1566,12 @@ mod test { ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_in.as_mut_slice(), + InserterWaitLists::None, ), ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_out.as_mut_slice(), + InserterWaitLists::None, ), ], 10, @@ -1639,10 +1649,12 @@ mod test { ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_in.as_mut_slice(), + InserterWaitLists::None, ), ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_out.as_mut_slice(), + InserterWaitLists::None, ), ], 10, diff --git a/src/inserter/storage_storage_with_buckets_indirect.rs b/src/inserter/storage_storage_with_buckets_indirect.rs index a22aa94..2118614 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -1072,7 +1072,7 @@ mod test { use rand::{random, seq::SliceRandom}; use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; - use crate::storage_list::MaxInsertionLimit; + use crate::storage_list::{InserterWaitLists, MaxInsertionLimit}; use super::*; @@ -1098,10 +1098,12 @@ mod test { ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_in[item].as_mut_slice(), + InserterWaitLists::None, ), ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storages_out[item].as_mut_slice(), + InserterWaitLists::None, ), ], 10, @@ -1159,10 +1161,12 @@ mod test { ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storage_in.as_mut_slice(), + InserterWaitLists::None, ), ( MaxInsertionLimit::PerMachine(max_insert.as_slice()), storage_out.as_mut_slice(), + InserterWaitLists::None, ), ], 10, diff --git a/src/power/mod.rs b/src/power/mod.rs index e2d6d1a..e563aeb 100644 --- a/src/power/mod.rs +++ b/src/power/mod.rs @@ -5,6 +5,7 @@ use crate::inserter::belt_storage_movement_list::{ }; use crate::inserter::storage_storage_with_buckets_indirect::InserterBucketData; use crate::item::Indexable; +use crate::power::power_grid::MAX_POWER_MULT; use crate::{ app_state::StorageStorageInserterStore, frontend::world::tile::ModuleSlots, join_many::join, }; @@ -301,7 +302,7 @@ impl PowerGridStorage, - _data_store: &DataStore, + data_store: &DataStore, ) -> PowerGridIdentifier { // TODO: This is O(N). Is that a problem? let hole_idx = self.power_grids.iter().position(|grid| grid.is_placeholder); @@ -322,6 +323,8 @@ impl PowerGridStorage PowerGridStorage= MIN_BEACON_POWER_MULT, - self.power_grids[usize::from(removed_id)].last_power_mult >= MIN_BEACON_POWER_MULT, + new_power_mult + == self.power_grids[usize::from(removed_id)].power_mult_at_last_beacon_update, + new_power_mult + == self.power_grids[usize::from(kept_id)].power_mult_at_last_beacon_update, ) { (true, true) => vec![], - (true, false) => { - // Enable the beacons - self.power_grids[usize::from(removed_id)] - .beacon_affected_entities - .iter() - .map(|(k, v)| (*k, (v.0, v.1, 0))) - .collect() - }, - (false, true) => { - // Disable the beacons - self.power_grids[usize::from(removed_id)] - .beacon_affected_entities - .iter() - .map(|(k, v)| (*k, (-v.0, -v.1, -0))) - .collect() - }, - (false, false) => vec![], + (true, false) => self.power_grids[usize::from(removed_id)] + .beacon_affected_entities + .iter() + .map(|(k, v)| { + let old_effect = calculate_beacon_effect( + self.power_grids[usize::from(removed_id)].power_mult_at_last_beacon_update, + (*v).into(), + ); + let new_effect = calculate_beacon_effect(new_power_mult, (*v).into()); + + ( + *k, + old_effect + .into_iter() + .zip(new_effect) + .map(|(old, new)| new - old) + .collect_array() + .unwrap(), + ) + }) + .collect(), + (false, true) => self.power_grids[usize::from(kept_id)] + .beacon_affected_entities + .iter() + .map(|(k, v)| { + let old_effect = calculate_beacon_effect( + self.power_grids[usize::from(kept_id)].power_mult_at_last_beacon_update, + (*v).into(), + ); + let new_effect = calculate_beacon_effect(new_power_mult, (*v).into()); + + ( + *k, + old_effect + .into_iter() + .zip(new_effect) + .map(|(old, new)| new - old) + .collect_array() + .unwrap(), + ) + }) + .collect(), + (false, false) => self.power_grids[usize::from(removed_id)] + .beacon_affected_entities + .iter() + .map(|(k, v)| { + let old_effect = calculate_beacon_effect( + self.power_grids[usize::from(removed_id)].power_mult_at_last_beacon_update, + (*v).into(), + ); + let new_effect = calculate_beacon_effect(new_power_mult, (*v).into()); + + ( + *k, + old_effect + .into_iter() + .zip(new_effect) + .map(|(old, new)| new - old) + .collect_array() + .unwrap(), + ) + }) + .chain( + self.power_grids[usize::from(kept_id)] + .beacon_affected_entities + .iter() + .map(|(k, v)| { + let old_effect = calculate_beacon_effect( + self.power_grids[usize::from(kept_id)] + .power_mult_at_last_beacon_update, + (*v).into(), + ); + let new_effect = calculate_beacon_effect(new_power_mult, (*v).into()); + + ( + *k, + old_effect + .into_iter() + .zip(new_effect) + .map(|(old, new)| new - old) + .collect_array() + .unwrap(), + ) + }), + ) + .collect(), }; { @@ -775,8 +849,11 @@ impl PowerGridStorage { - self.power_grids[usize::from(id.grid)] - .change_assembler_module_modifiers(id, update.1, data_store); + self.power_grids[usize::from(id.grid)].change_assembler_module_modifiers( + id, + update.1.into(), + data_store, + ); }, power_grid::BeaconAffectedEntity::Lab { grid, index } => { // TODO: @@ -1254,26 +1331,17 @@ impl PowerGridStorage= MIN_BEACON_POWER_MULT - { - // Add the full beacon effect since we are powered - ( + let effect = calculate_beacon_effect( + self.power_grids[usize::from(grid)].power_mult_at_last_beacon_update, + [ effect.0 * data_store.beacon_info[usize::from(ty)].effectiveness.0 as i16 / data_store.beacon_info[usize::from(ty)].effectiveness.1 as i16, effect.1 * data_store.beacon_info[usize::from(ty)].effectiveness.0 as i16 / data_store.beacon_info[usize::from(ty)].effectiveness.1 as i16, effect.2 * data_store.beacon_info[usize::from(ty)].effectiveness.0 as i16 / data_store.beacon_info[usize::from(ty)].effectiveness.1 as i16, - ) - } else { - // Not enough power, only add the power_consumption modifier - ( - 0, - 0, - effect.2 * data_store.beacon_info[usize::from(ty)].effectiveness.0 as i16 - / data_store.beacon_info[usize::from(ty)].effectiveness.1 as i16, - ) - }; + ], + ); let affected_entities: Vec> = affected_entities.into_iter().collect(); @@ -1281,13 +1349,16 @@ impl PowerGridStorage { - self.power_grids[usize::from(id.grid)] - .change_assembler_module_modifiers(*id, effect, data_store); + self.power_grids[usize::from(id.grid)].change_assembler_module_modifiers( + *id, + effect.into(), + data_store, + ); }, BeaconAffectedEntity::Lab { grid, index } => { self.power_grids[usize::from(*grid)].change_lab_module_modifiers( (*index).try_into().unwrap(), - effect, + effect.into(), data_store, ); }, @@ -1437,16 +1508,11 @@ impl PowerGridStorage= MIN_BEACON_POWER_MULT - { - // Add the full beacon effect since we are powered - raw_effect - } else { - // Not enough power, only add the power_consumption modifier - (0, 0, raw_effect.2) - }; + let effect = calculate_beacon_effect( + self.power_grids[usize::from(self.pole_pos_to_grid_id[&beacon_pole_pos])] + .power_mult_at_last_beacon_update, + raw_effect.into(), + ); let effect_sum = self.power_grids[usize::from(self.pole_pos_to_grid_id[&beacon_pole_pos])] .beacon_affected_entities @@ -1459,13 +1525,16 @@ impl PowerGridStorage { - self.power_grids[usize::from(id.grid)] - .change_assembler_module_modifiers(id, effect, data_store); + self.power_grids[usize::from(id.grid)].change_assembler_module_modifiers( + id, + effect.into(), + data_store, + ); }, BeaconAffectedEntity::Lab { grid, index } => { self.power_grids[usize::from(grid)].change_lab_module_modifiers( index.try_into().unwrap(), - effect, + effect.into(), data_store, ); }, @@ -1476,3 +1545,23 @@ impl PowerGridStorage [i16; 3] { + linear_scaling_effect(power_mult, raw_effect) +} + +#[allow(unused)] +fn switching_effect(power_mult: u8, raw_effect: [i16; 3]) -> [i16; 3] { + if power_mult >= MIN_BEACON_POWER_MULT { + raw_effect + } else { + [0, 0, raw_effect[2]] + } +} + +fn linear_scaling_effect(power_mult: u8, raw_effect: [i16; 3]) -> [i16; 3] { + assert!(power_mult <= MAX_POWER_MULT); + raw_effect + .map(|e| i32::from(e) * i32::from(power_mult) / i32::from(MAX_POWER_MULT)) + .map(|v| v.try_into().unwrap()) +} diff --git a/src/power/power_grid.rs b/src/power/power_grid.rs index b0e8edd..814f369 100644 --- a/src/power/power_grid.rs +++ b/src/power/power_grid.rs @@ -2,6 +2,7 @@ use crate::assembler::simd::Inserter; use crate::assembler::simd::InserterReinsertionInfo; use crate::frontend::world::tile::ModuleSlots; use crate::frontend::world::tile::ModuleTy; +use crate::power::calculate_beacon_effect; use crate::{ assembler::{MultiAssemblerStore, simd::MultiAssemblerStore as MultiAssemblerStoreStruct}, join_many::join, @@ -739,7 +740,8 @@ impl PowerGrid = - Self::new_from_graph(PowerGridIdentifier::MAX, network, data_store); + // FIXME: Is an Id 0 fine here? + Self::new_from_graph(0, network, data_store); let storage_updates: Vec<_> = self .move_connected_entities(&mut new_network, data_store) @@ -868,13 +870,12 @@ impl PowerGrid= MIN_BEACON_POWER_MULT { - raw_effect - } else { - (0, 0, raw_effect.2) - }; + let effect = calculate_beacon_effect( + self.power_mult_at_last_beacon_update, + raw_effect.into(), + ); - if effect.0 > 0 || effect.1 > 0 || effect.2 > 0 { + if effect[0] > 0 || effect[1] > 0 || effect[2] > 0 { let removed_beacon_affected_entities = self .beacon_affected_entity_map .remove(&(pole_pos, weak_idx)) @@ -882,7 +883,7 @@ impl PowerGrid 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() - // } 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"); + let beacon_updates: Vec<(BeaconAffectedEntity<_>, _)> = if current_tick % 60 == 0 { + let updates = if next_power_mult == self.power_mult_at_last_beacon_update { + vec![] + } else { 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(); + .filter_map(|(k, v)| { + let old_effect = calculate_beacon_effect( + self.power_mult_at_last_beacon_update, + (*v).into(), + ); + let new_effect = calculate_beacon_effect(next_power_mult, (*v).into()); - (entity, change.into()) + if old_effect == new_effect { + None + } else { + Some(( + *k, + ( + new_effect[0] - old_effect[0], + new_effect[1] - old_effect[1], + new_effect[2] - old_effect[2], + ), + )) + } }) .collect() - } else { - vec![] }; self.power_mult_at_last_beacon_update = next_power_mult; - - ret + updates } else { vec![] }; @@ -2925,7 +2901,7 @@ impl PowerGrid PowerGrid PowerGrid= MIN_BEACON_POWER_MULT { - (-effect.0, -effect.1, -effect.2) - } else { - (-0, -0, -effect.2) - }; + let old_effect = + calculate_beacon_effect(self.power_mult_at_last_beacon_update, raw_effect.into()); + + let now_removed_effect = (-old_effect[0], -old_effect[1], -old_effect[2]); #[cfg(debug_assertions)] { From edac0bd8e6474ccaa755b2f3ab4fe784b0e062f7 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 4 Jan 2026 22:12:43 +0100 Subject: [PATCH 067/152] Add onclick info to accumulators and solar panels --- Cargo.toml | 2 +- src/data/mod.rs | 14 ++++++++++++++ src/power/power_grid.rs | 2 +- src/rendering/render_world.rs | 33 ++++++++++++++++++++++++++++----- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6405e66..e7ee33c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,7 +88,7 @@ proptest = "1.4.0" [patch.crates-io] puffin_egui = { git = "https://github.com/EmbarkStudios/puffin", optional = true } -puffin = { git = "https://github.com/EmbarkStudios/puffin", features = ["web"] } +puffin = { git = "https://github.com/EmbarkStudios/puffin" } egui_graphs = { git = "https://github.com/BloodStainedCrow/egui_graphs", branch = "tree_layout" } petgraph = { git = "https://github.com/BloodStainedCrow/petgraph", branch = "stable_graph_node_weights_mut_indexed" } diff --git a/src/data/mod.rs b/src/data/mod.rs index 6c6eeb5..171825b 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -441,6 +441,20 @@ impl SolarPanelOutputFunction { }, } } + + pub(crate) fn max(&self) -> Watt { + match self { + SolarPanelOutputFunction::Constant(watt) => *watt, + SolarPanelOutputFunction::Segmented(power_generation_segments) => { + power_generation_segments + .iter() + .map(|segment| segment.end_power) + .max() + .unwrap() + }, + SolarPanelOutputFunction::Lookup(watts) => *watts.iter().max().unwrap(), + } + } } #[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] diff --git a/src/power/power_grid.rs b/src/power/power_grid.rs index 814f369..f15c491 100644 --- a/src/power/power_grid.rs +++ b/src/power/power_grid.rs @@ -2174,7 +2174,7 @@ impl PowerGrid { - // TODO + Entity::SolarPanel { ty, .. } => { + ui.label(format!("{}", &data_store.solar_panel_info[*ty as usize].display_name)); + + let current = data_store.solar_panel_info[*ty as usize].power_output.get_at_time(aux_data.current_tick as u32); + let max = data_store.solar_panel_info[*ty as usize].power_output.max(); + + let perc = current.0 as f32 / max.0 as f32; + + ui.add(ProgressBar::new(perc).text(format!("{}/{}", current, max)).corner_radius(0.0)); }, - Entity::Accumulator { .. } => { - // TODO + Entity::Accumulator { ty, pole_position, .. } => { + ui.label(format!("{}", &data_store.accumulator_info[*ty as usize].display_name)); + + let max_charge = data_store.accumulator_info[*ty as usize].max_charge; + let charge = if let Some(pole_pos) = pole_position { + let grid = game_state_ref.simulation_state.factory.power_grids.pole_pos_to_grid_id[&pole_pos.0]; + let grid = &game_state_ref.simulation_state.factory.power_grids.power_grids[grid as usize]; + let charge = grid.main_accumulator_charge[*ty as usize] / grid.main_accumulator_count[*ty as usize]; + + charge + } else { + // FIXME: A unconnected accumulator can still have charge + Joule(0) + }; + + let perc = charge.0 as f32 / max_charge.0 as f32; + + ui.add(ProgressBar::new(perc).text(format!("{}/{}", charge, max_charge)).corner_radius(0.0)); }, - Entity::Beacon { .. } => { + Entity::Beacon { ty, modules, .. } => { // TODO }, Entity::FluidTank { ty, pos, rotation } => { From 21dd859f5411692cc6a27cea7a2c171076471b9a Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 4 Jan 2026 22:24:34 +0100 Subject: [PATCH 068/152] Clean up unused imports --- src/app_state.rs | 14 ++++++-------- src/assembler/bucketed.rs | 4 ++-- src/assembler/mod.rs | 2 +- src/assembler/simd.rs | 1 - src/belt/smart.rs | 5 +---- src/belt/sushi.rs | 3 +-- src/chest.rs | 7 +------ src/data/mod.rs | 2 +- src/frontend/action/action_state_machine.rs | 1 - src/frontend/action/belt_placement.rs | 2 +- src/frontend/world/tile.rs | 6 ++---- src/inserter/mod.rs | 2 +- .../storage_storage_with_buckets_indirect.rs | 5 +---- src/mining_drill/only_solo_owned.rs | 1 - src/power/mod.rs | 2 +- src/power/power_grid.rs | 1 - src/rendering/render_world.rs | 3 +-- src/saving/save_file_settings.rs | 2 -- src/statistics/research.rs | 2 +- src/storage_list.rs | 2 +- 20 files changed, 22 insertions(+), 45 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 7a6ebe7..2a08c33 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -61,9 +61,7 @@ use crate::{ statistics::{ GenStatistics, Timeline, consumption::ConsumptionInfo, production::ProductionInfo, }, - storage_list::{ - SingleItemStorages, full_to_by_item, grid_size, num_recipes, sizes, storages_by_item, - }, + storage_list::{SingleItemStorages, full_to_by_item, grid_size, sizes, storages_by_item}, }; use crate::{ item::Indexable, @@ -76,7 +74,7 @@ use flate2::bufread::ZlibDecoder; use get_size2::GetSize; use itertools::Itertools; use log::error; -use log::{info, trace, warn}; +use log::{info, warn}; use petgraph::graph::NodeIndex; use rayon::iter::IntoParallelRefIterator; use rayon::iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; @@ -1102,20 +1100,20 @@ impl Factory for Position { #[cfg(test)] mod test { - use proptest::{prop_assert, prop_assert_eq, proptest}; + use proptest::proptest; // use crate::{ // DATA_STORE, diff --git a/src/inserter/mod.rs b/src/inserter/mod.rs index a6f8fdd..88ff284 100644 --- a/src/inserter/mod.rs +++ b/src/inserter/mod.rs @@ -1,6 +1,6 @@ use std::{marker::PhantomData, u16}; -use crate::item::{ITEMCOUNTTYPE, Indexable}; +use crate::item::ITEMCOUNTTYPE; use crate::{ data::DataStore, item::{IdxTrait, Item, Recipe, WeakIdxTrait}, diff --git a/src/inserter/storage_storage_with_buckets_indirect.rs b/src/inserter/storage_storage_with_buckets_indirect.rs index 2118614..591ffda 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -5,13 +5,10 @@ use super::{ FakeUnionStorage, InserterStateInfo, storage_storage_with_buckets::LargeInserterState, }; use crate::{ - assembler::simd::{ - InserterReinsertionInfo, InserterWaitList, InserterWithBelts as WaitListInserter, - }, + assembler::simd::InserterWithBelts as WaitListInserter, inserter::WaitlistSearchSide, item::ITEMCOUNTTYPE, join_many::join, - power::power_grid::PowerGrid, storage_list::{SingleItemStorages, index_fake_union}, }; use std::cmp::min; diff --git a/src/mining_drill/only_solo_owned.rs b/src/mining_drill/only_solo_owned.rs index 82aeb88..b19c655 100644 --- a/src/mining_drill/only_solo_owned.rs +++ b/src/mining_drill/only_solo_owned.rs @@ -8,7 +8,6 @@ use crate::WeakIdxTrait; use crate::data::DataStore; use crate::item::ITEMCOUNTTYPE; use crate::item::IdxTrait; -use crate::item::Indexable; use crate::item::Item; use crate::mining_drill::MiningDrillInfo; use crate::power::Joule; diff --git a/src/power/mod.rs b/src/power/mod.rs index e563aeb..6664a27 100644 --- a/src/power/mod.rs +++ b/src/power/mod.rs @@ -1,7 +1,7 @@ use crate::inserter::belt_storage_inserter::Dir; use crate::inserter::belt_storage_movement_list::FinishedMovingLists; use crate::inserter::belt_storage_movement_list::{ - BeltStorageInserterInMovement, List, ReinsertionLists, + BeltStorageInserterInMovement, ReinsertionLists, }; use crate::inserter::storage_storage_with_buckets_indirect::InserterBucketData; use crate::item::Indexable; diff --git a/src/power/power_grid.rs b/src/power/power_grid.rs index f15c491..cec772c 100644 --- a/src/power/power_grid.rs +++ b/src/power/power_grid.rs @@ -1,4 +1,3 @@ -use crate::assembler::simd::Inserter; use crate::assembler::simd::InserterReinsertionInfo; use crate::frontend::world::tile::ModuleSlots; use crate::frontend::world::tile::ModuleTy; diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 2ed01cc..d2cd839 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -56,9 +56,8 @@ use eframe::egui::{ self, Align2, Color32, ComboBox, Context, CornerRadius, Label, Layout, ProgressBar, Stroke, Ui, Window, }; -use egui::{Button, CollapsingHeader, Modal, Rect, RichText, ScrollArea, Sense, Slider}; +use egui::{Button, CollapsingHeader, Modal, RichText, ScrollArea, Sense, Slider}; use egui_extras::{Column, TableBuilder}; -use egui_graphs::Graph; use egui_plot::{AxisHints, GridMark, Line, Plot, PlotPoints}; use egui_show_info::ShowInfo; use flate2::Compression; diff --git a/src/saving/save_file_settings.rs b/src/saving/save_file_settings.rs index e2eb0ec..0234f76 100644 --- a/src/saving/save_file_settings.rs +++ b/src/saving/save_file_settings.rs @@ -2,8 +2,6 @@ use std::{path::PathBuf, time::Duration}; use chrono::Utc; -use crate::scenario::ScenarioInfo; - #[derive(Debug, Clone)] pub(crate) struct SaveFileInfo { pub(crate) path: PathBuf, diff --git a/src/statistics/research.rs b/src/statistics/research.rs index 2a8149c..c2fe254 100644 --- a/src/statistics/research.rs +++ b/src/statistics/research.rs @@ -1,6 +1,6 @@ use std::iter; -use crate::{item::IdxTrait, research::ResearchProgress}; +use crate::item::IdxTrait; use super::IntoSeries; diff --git a/src/storage_list.rs b/src/storage_list.rs index 5952ce1..abcc72a 100644 --- a/src/storage_list.rs +++ b/src/storage_list.rs @@ -1,6 +1,6 @@ use core::panic; use std::iter; -use std::ops::{Index, IndexMut}; +use std::ops::Index; use std::u16; use itertools::Itertools; From a5d26a6d52b254af815073b344a3e8b543ca1de0 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 4 Jan 2026 22:26:07 +0100 Subject: [PATCH 069/152] Remove old feature flags --- src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7215c16..d05724f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,9 +3,7 @@ #![feature(adt_const_params)] #![feature(array_try_map)] #![feature(never_type)] -#![feature(mixed_integer_ops_unsigned_sub)] #![feature(int_roundings)] -#![feature(strict_overflow_ops)] #![feature(vec_push_within_capacity)] extern crate test; From 6fd7cf83d02a7b34c1b807b23991fed39d2166ce Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 4 Jan 2026 23:03:29 +0100 Subject: [PATCH 070/152] Reduce warnings --- src/app_state.rs | 21 +- src/assembler/bucketed.rs | 14 +- src/assembler/simd.rs | 8 +- src/belt/mod.rs | 7 +- src/belt/smart.rs | 9 +- src/blueprint/blueprint_string.rs | 4 +- src/blueprint/mod.rs | 14 +- src/chest.rs | 1 - src/data/mod.rs | 7 +- src/frontend/action/belt_placement.rs | 357 +++++++++--------- .../world/sparse_grid/bounding_box_grid.rs | 13 +- src/frontend/world/sparse_grid/dynamic.rs | 9 +- src/frontend/world/sparse_grid/map_grid.rs | 13 +- src/frontend/world/sparse_grid/mod.rs | 4 - src/frontend/world/tile.rs | 72 ++-- src/inserter/belt_storage_movement_list.rs | 2 +- src/inserter/belt_storage_pure_buckets.rs | 5 +- src/inserter/storage_storage_with_buckets.rs | 8 +- ...storage_storage_with_buckets_compressed.rs | 8 +- .../storage_storage_with_buckets_indirect.rs | 6 +- src/liquid/mod.rs | 40 +- src/par_generation.rs | 4 +- src/power/mod.rs | 12 +- src/power/power_grid.rs | 8 +- src/rendering/eframe_app.rs | 6 +- src/rendering/render_world.rs | 36 +- src/research.rs | 5 +- src/saving/mod.rs | 2 +- 28 files changed, 316 insertions(+), 379 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 2a08c33..7a8bd0a 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1365,10 +1365,7 @@ impl GameState { if let Some((item, index)) = item { let removed_items = @@ -1418,7 +1415,7 @@ impl GameState todo!(), + AttachedInserter::BeltBelt { .. } => todo!(), AttachedInserter::StorageStorage { item, inserter } => { let old_movetime = user_movetime.unwrap_or( data_store.inserter_infos[*ty as usize] @@ -1472,9 +1469,9 @@ impl GameState { let removed = game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores.remove_wait_list_inserter(*id, item, inserter.id, data_store); let Conn::Storage { - index, storage_id_in, storage_id_out, + .. } = removed.conn else { unreachable!() @@ -1896,7 +1893,7 @@ impl GameState Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, crate::power::power_grid::PowerGridEntity::Lab { index, .. } => Storage::Lab { grid: storage_update.new_grid, index }, - crate::power::power_grid::PowerGridEntity::LazyPowerProducer { item, index } => todo!(), + crate::power::power_grid::PowerGridEntity::LazyPowerProducer { .. } => todo!(), crate::power::power_grid::PowerGridEntity::SolarPanel { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), @@ -1911,13 +1908,13 @@ impl GameState { + Entity::FluidTank { pos, .. } => { 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.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!(), + 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 { .. } => todo!(), crate::power::power_grid::PowerGridEntity::SolarPanel { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), @@ -1929,7 +1926,7 @@ impl GameState Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, crate::power::power_grid::PowerGridEntity::Lab { ty, index } => Storage::Lab { grid: storage_update.new_grid, index }, - crate::power::power_grid::PowerGridEntity::LazyPowerProducer { item, index } => todo!(), + crate::power::power_grid::PowerGridEntity::LazyPowerProducer { .. } => todo!(), crate::power::power_grid::PowerGridEntity::SolarPanel { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), diff --git a/src/assembler/bucketed.rs b/src/assembler/bucketed.rs index 9a6a076..09e134d 100644 --- a/src/assembler/bucketed.rs +++ b/src/assembler/bucketed.rs @@ -371,8 +371,8 @@ impl( &mut self, - new_grid_id: PowerGridIdentifier, - data_store: &DataStore, + _new_grid_id: PowerGridIdentifier, + _data_store: &DataStore, ) { // Do nothing } @@ -600,7 +600,7 @@ impl, ) -> ( @@ -1012,10 +1012,10 @@ impl( &mut self, - index: u32, - item: crate::item::Item, - id: crate::inserter::storage_storage_with_buckets_indirect::InserterId, - data_store: &DataStore, + _index: u32, + _item: crate::item::Item, + _id: crate::inserter::storage_storage_with_buckets_indirect::InserterId, + _data_store: &DataStore, ) -> super::simd::InserterReinsertionInfo { unreachable!() } diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index cf7a78a..4b96329 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -381,7 +381,7 @@ impl let (timers, overlap) = self.timers.as_chunks_mut::<{ SIMDTYPE::LEN }>(); // FIXME: - // assert!(overlap.is_empty()); + assert!(overlap.is_empty()); for ( index, @@ -683,8 +683,8 @@ impl - self.ings[item][final_idx]) >= self.waitlists_ings_needed[item][final_idx] { - *items_to_drain += (self.ings_max_insert[item][final_idx] - - self.ings[item][final_idx]); + *items_to_drain += self.ings_max_insert[item][final_idx] + - self.ings[item][final_idx]; self.ings[item][final_idx] = self.ings_max_insert[item][final_idx]; for idx in 0..WAITLIST_LEN { @@ -2194,7 +2194,7 @@ mod test { .collect_vec(); let items: Vec> = vec![ - rayon::iter::repeat(rand::thread_rng().gen_range(250..=255)) + rayon::iter::repeat(rand::rng().random_range(250..=255)) .flat_map_iter(|v| std::iter::repeat_n(v, 10)) .take_any(NUM_ASSEMBLERS) .collect(); diff --git a/src/belt/mod.rs b/src/belt/mod.rs index 005aeed..2ae47ba 100644 --- a/src/belt/mod.rs +++ b/src/belt/mod.rs @@ -2079,7 +2079,6 @@ impl BeltStore { ) { if dedup.contains(&tile_id) { todo!(); - return; } let (inserter_item_sources, items_on_belt): (Vec<_>, Vec<_>) = match tile_id { @@ -3107,8 +3106,8 @@ impl BeltStore { .len() - 1) as u32 }, - (AnyBelt::Smart(source_belt_id), AnyBelt::Sushi(dest_index)) => todo!(), - (AnyBelt::Sushi(source_index), AnyBelt::Smart(dest_belt_id)) => todo!(), + (AnyBelt::Smart(_source_belt_id), AnyBelt::Sushi(_dest_index)) => todo!(), + (AnyBelt::Sushi(_source_index), AnyBelt::Smart(_dest_belt_id)) => todo!(), (AnyBelt::Sushi(source_index), AnyBelt::Sushi(dest_index)) => { self.inner .belt_belt_inserters @@ -3190,7 +3189,7 @@ impl BeltStore { let smart_belt = self.inner.get_smart_mut(*smart_belt_id); smart_belt.change_inserter_movetime(pos, new_movetime); }, - AnyBelt::Sushi(sushi_belt_id) => { + AnyBelt::Sushi(_sushi_belt_id) => { todo!() }, AnyBelt::Empty(_) => unimplemented!("Empty belt cannot have inserters"), diff --git a/src/belt/smart.rs b/src/belt/smart.rs index 9874aec..9096421 100644 --- a/src/belt/smart.rs +++ b/src/belt/smart.rs @@ -1031,9 +1031,6 @@ impl SmartBelt { // Important, first_free_index must ALWAYS be used using mod len let back_zero_index = usize::from(back_zero_index) % back_locs.len(); - let num_front_inserters = front_inserters.inserters.len(); - let _num_back_inserters = back_inserters.inserters.len(); - let mut new_inserters = front_inserters; new_inserters .inserters @@ -1115,7 +1112,7 @@ impl SmartBelt { let old_len = locs.len(); - let (new_empty, new_zero, front_extension_amount) = match side { + let (new_empty, new_zero, _front_extension_amount) = match side { Side::FRONT => { locs.splice( usize::from(zero_index)..usize::from(zero_index), @@ -1454,7 +1451,7 @@ impl Belt for EmptyBelt { self.len } - fn add_length(&mut self, amount: BeltLenType, side: Side) -> BeltLenType { + fn add_length(&mut self, amount: BeltLenType, _side: Side) -> BeltLenType { self.len += amount; self.len } @@ -1695,7 +1692,7 @@ impl Belt for SmartBelt { FreeIndex::OldFreeIndex(_) => {}, } } - let (old_free, need_to_check) = match self.first_free_index { + let (_old_free, need_to_check) = match self.first_free_index { FreeIndex::FreeIndex(idx) => (idx, false), FreeIndex::OldFreeIndex(idx) => (idx, true), }; diff --git a/src/blueprint/blueprint_string.rs b/src/blueprint/blueprint_string.rs index 27c6db1..966a0fe 100644 --- a/src/blueprint/blueprint_string.rs +++ b/src/blueprint/blueprint_string.rs @@ -332,7 +332,7 @@ impl From for BlueprintString { pos, direction, ty, - copied_belt_info, + copied_belt_info: _, } => { internal.belts.push(BaseEntity { pos, @@ -345,7 +345,7 @@ impl From for BlueprintString { direction, ty, underground_dir, - copied_belt_info, + copied_belt_info: _, } => { internal.underground_belts.push(( BaseEntity { diff --git a/src/blueprint/mod.rs b/src/blueprint/mod.rs index e7fea3f..e27dee6 100644 --- a/src/blueprint/mod.rs +++ b/src/blueprint/mod.rs @@ -1117,8 +1117,8 @@ impl Blueprint { ty, pos, rotation, - drill_id, - internal_inserter, + drill_id: _, + internal_inserter: _, } => { vec![BlueprintAction::PlaceEntity( BlueprintPlaceEntity::MiningDrill { @@ -1281,7 +1281,7 @@ impl Blueprint { ty, .. } => { - let mut ret = vec![BlueprintAction::PlaceEntity( + let ret = vec![BlueprintAction::PlaceEntity( BlueprintPlaceEntity::Inserter { pos: Position { x: pos.x - base_pos.x, @@ -1315,13 +1315,7 @@ impl Blueprint { ty: data_store.chest_names[*ty as usize].clone(), })] }, - crate::frontend::world::tile::Entity::Roboport { - ty, - pos, - power_grid, - network, - id, - } => todo!(), + crate::frontend::world::tile::Entity::Roboport { .. } => todo!(), crate::frontend::world::tile::Entity::SolarPanel { pos, ty, .. } => { vec![BlueprintAction::PlaceEntity( BlueprintPlaceEntity::SolarPanel { diff --git a/src/chest.rs b/src/chest.rs index f74a2ff..baf007b 100644 --- a/src/chest.rs +++ b/src/chest.rs @@ -373,7 +373,6 @@ impl MultiChestStore { || (inserter.current_hand == 0 && !self_is_source) { let removed = ins.take().unwrap(); - let is_source = self_is_source; self.inserter_reinsertion_vec .push_within_capacity(InternalInserterReinsertionInfo { inserter: removed, diff --git a/src/data/mod.rs b/src/data/mod.rs index 79835ac..75a2609 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1422,11 +1422,8 @@ impl RawDataStore { unit_increase_per_level: units_per_level, } }, - InfiniteCostScaling::Quadradic { units_per_level } => todo!(), - InfiniteCostScaling::Exponential { - units_per_level, - exponential_base, - } => todo!(), + InfiniteCostScaling::Quadradic { .. } => todo!(), + InfiniteCostScaling::Exponential { .. } => todo!(), }, level_counter_offset: infinite_info.display_level_offset, } diff --git a/src/frontend/action/belt_placement.rs b/src/frontend/action/belt_placement.rs index 5a08cef..998ad66 100644 --- a/src/frontend/action/belt_placement.rs +++ b/src/frontend/action/belt_placement.rs @@ -1877,242 +1877,241 @@ pub fn expected_belt_state( } // #[cfg(test)] -#[cfg(testTODO)] -mod test { - use proptest::prelude::{Just, Strategy}; - use proptest::{prop_assert, prop_assume, proptest}; - - use crate::DATA_STORE; - use crate::app_state::GameState; - use crate::blueprint::Blueprint; - use crate::frontend::action::ActionType; - use crate::frontend::action::set_recipe::SetRecipeInfo; - use crate::frontend::world::Position; - use crate::frontend::world::tile::{AssemblerInfo, Dir, Entity, InserterInfo, PlaceEntityType}; - use crate::item::Recipe; - - fn chest_onto_belt() -> impl Strategy>> { - Just(vec![ - place(PlaceEntityType::Assembler { - pos: Position { x: 0, y: 3 }, - ty: 0, - rotation: Dir::North, - }), - ActionType::SetRecipe(SetRecipeInfo { - pos: Position { x: 0, y: 3 }, - recipe: Recipe { id: 0 }, - }), - place(PlaceEntityType::Inserter { - pos: Position { x: 2, y: 2 }, - dir: crate::frontend::world::tile::Dir::North, - filter: None, - ty: 0, - user_movetime: None, - }), - place(PlaceEntityType::Belt { - pos: Position { x: 2, y: 1 }, - direction: crate::frontend::world::tile::Dir::East, - ty: 0, - }), - place(PlaceEntityType::PowerPole { - pos: Position { x: 0, y: 2 }, - ty: 0, - }), - place(PlaceEntityType::PowerPole { - pos: Position { x: 5, y: 0 }, - ty: 0, - }), - place(PlaceEntityType::SolarPanel { - pos: Position { x: 6, y: 0 }, - ty: 0, - }), - ]) - } +// mod test { +// use proptest::prelude::{Just, Strategy}; +// use proptest::{prop_assert, prop_assume, proptest}; + +// use crate::DATA_STORE; +// use crate::app_state::GameState; +// use crate::blueprint::Blueprint; +// use crate::frontend::action::ActionType; +// use crate::frontend::action::set_recipe::SetRecipeInfo; +// use crate::frontend::world::Position; +// use crate::frontend::world::tile::{AssemblerInfo, Dir, Entity, InserterInfo, PlaceEntityType}; +// use crate::item::Recipe; + +// fn chest_onto_belt() -> impl Strategy>> { +// Just(vec![ +// place(PlaceEntityType::Assembler { +// pos: Position { x: 0, y: 3 }, +// ty: 0, +// rotation: Dir::North, +// }), +// ActionType::SetRecipe(SetRecipeInfo { +// pos: Position { x: 0, y: 3 }, +// recipe: Recipe { id: 0 }, +// }), +// place(PlaceEntityType::Inserter { +// pos: Position { x: 2, y: 2 }, +// dir: crate::frontend::world::tile::Dir::North, +// filter: None, +// ty: 0, +// user_movetime: None, +// }), +// place(PlaceEntityType::Belt { +// pos: Position { x: 2, y: 1 }, +// direction: crate::frontend::world::tile::Dir::East, +// ty: 0, +// }), +// place(PlaceEntityType::PowerPole { +// pos: Position { x: 0, y: 2 }, +// ty: 0, +// }), +// place(PlaceEntityType::PowerPole { +// pos: Position { x: 5, y: 0 }, +// ty: 0, +// }), +// place(PlaceEntityType::SolarPanel { +// pos: Position { x: 6, y: 0 }, +// ty: 0, +// }), +// ]) +// } + +// fn belts_into_sideload() -> impl Strategy>> { +// Just(vec![ +// place(PlaceEntityType::Belt { +// pos: Position { x: 3, y: 1 }, +// direction: crate::frontend::world::tile::Dir::East, +// ty: 0, +// }), +// place(PlaceEntityType::Belt { +// pos: Position { x: 4, y: 0 }, +// direction: crate::frontend::world::tile::Dir::South, +// ty: 0, +// }), +// place(PlaceEntityType::Belt { +// pos: Position { x: 4, y: 1 }, +// direction: crate::frontend::world::tile::Dir::South, +// ty: 0, +// }), +// place(PlaceEntityType::Belt { +// pos: Position { x: 4, y: 2 }, +// direction: crate::frontend::world::tile::Dir::South, +// ty: 0, +// }), +// ]) +// } + +// fn sideload_items() -> impl Strategy>> { +// (chest_onto_belt(), belts_into_sideload()).prop_map(|(mut a, b)| { +// a.extend(b.into_iter()); +// a +// }) +// } + +// fn place(ty: PlaceEntityType) -> ActionType { +// ActionType::PlaceEntity(crate::frontend::action::place_entity::PlaceEntityInfo { +// entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single(ty), +// force: false, +// }) +// } - fn belts_into_sideload() -> impl Strategy>> { - Just(vec![ - place(PlaceEntityType::Belt { - pos: Position { x: 3, y: 1 }, - direction: crate::frontend::world::tile::Dir::East, - ty: 0, - }), - place(PlaceEntityType::Belt { - pos: Position { x: 4, y: 0 }, - direction: crate::frontend::world::tile::Dir::South, - ty: 0, - }), - place(PlaceEntityType::Belt { - pos: Position { x: 4, y: 1 }, - direction: crate::frontend::world::tile::Dir::South, - ty: 0, - }), - place(PlaceEntityType::Belt { - pos: Position { x: 4, y: 2 }, - direction: crate::frontend::world::tile::Dir::South, - ty: 0, - }), - ]) - } +// proptest! { - fn sideload_items() -> impl Strategy>> { - (chest_onto_belt(), belts_into_sideload()).prop_map(|(mut a, b)| { - a.extend(b.into_iter()); - a - }) - } +// #[test] +// fn inserter_always_attaches(actions in chest_onto_belt().prop_shuffle()) { +// let mut state = GameState::new( &DATA_STORE); - fn place(ty: PlaceEntityType) -> ActionType { - ActionType::PlaceEntity(crate::frontend::action::place_entity::PlaceEntityInfo { - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single(ty), - force: false, - }) - } +// let bp = Blueprint { actions }; - proptest! { +// bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - #[test] - fn inserter_always_attaches(actions in chest_onto_belt().prop_shuffle()) { - let mut state = GameState::new( &DATA_STORE); +// let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); - let bp = Blueprint { actions }; +// let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); - bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); +// prop_assert!(assembler_powered); - let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); +// let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); - let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); +// prop_assume!(assembler_working, "{:?}", ent); - prop_assert!(assembler_powered); +// let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); - let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); +// let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); - prop_assume!(assembler_working, "{:?}", ent); +// prop_assert!(inserter_attached, "{:?}", ent); +// } - let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); +// #[test] +// fn inserter_always_attaches_full_bp(actions in sideload_items().prop_shuffle()) { +// let mut state = GameState::new(&DATA_STORE); - let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); +// let bp = Blueprint { actions }; - prop_assert!(inserter_attached, "{:?}", ent); - } +// bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - #[test] - fn inserter_always_attaches_full_bp(actions in sideload_items().prop_shuffle()) { - let mut state = GameState::new(&DATA_STORE); +// let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); - let bp = Blueprint { actions }; +// let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); - bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); +// prop_assert!(assembler_powered); - let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); +// let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); - let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); +// prop_assume!(assembler_working, "{:?}", ent); - prop_assert!(assembler_powered); +// let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); - let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); +// let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); - prop_assume!(assembler_working, "{:?}", ent); +// prop_assert!(inserter_attached, "{:?}", ent); +// } - let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); +// #[test] +// fn sideload_empty_does_not_crash(actions in belts_into_sideload().prop_shuffle()) { +// let mut state = GameState::new(&DATA_STORE); - let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); +// let bp = Blueprint { actions }; - prop_assert!(inserter_attached, "{:?}", ent); - } +// bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); +// } - #[test] - fn sideload_empty_does_not_crash(actions in belts_into_sideload().prop_shuffle()) { - let mut state = GameState::new(&DATA_STORE); +// #[test] +// fn sideload_with_items_at_source_does_not_crash(actions in sideload_items().prop_shuffle()) { +// let mut state = GameState::new(&DATA_STORE); - let bp = Blueprint { actions }; +// let bp = Blueprint { actions }; - bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - } +// bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); +// } - #[test] - fn sideload_with_items_at_source_does_not_crash(actions in sideload_items().prop_shuffle()) { - let mut state = GameState::new(&DATA_STORE); +// #[test] +// fn sideload_with_items_at_source_items_reach_the_intersection(actions in chest_onto_belt().prop_shuffle()) { +// let mut state = GameState::new( &DATA_STORE); - let bp = Blueprint { actions }; +// let bp = Blueprint { actions }; - bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - } +// bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - #[test] - fn sideload_with_items_at_source_items_reach_the_intersection(actions in chest_onto_belt().prop_shuffle()) { - let mut state = GameState::new( &DATA_STORE); +// let assembler = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); - let bp = Blueprint { actions }; +// let assembler_working = matches!(assembler, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); - bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); +// prop_assume!(assembler_working, "{:?}", assembler); - let assembler = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); +// let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); - let assembler_working = matches!(assembler, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); +// let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); - prop_assume!(assembler_working, "{:?}", assembler); +// prop_assume!(inserter_attached, "{:?}", ent); - let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); +// for _ in 0..200 { +// state.update(&DATA_STORE); +// } - let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); +// let Some(Entity::Belt { id, .. }) = state.world.get_entity_at(Position { x: 1602, y: 1601 }, &DATA_STORE) else { +// unreachable!() +// }; - prop_assume!(inserter_attached, "{:?}", ent); +// let items_at_intersection = state.simulation_state.factory.belts.get_item_iter(*id).into_iter().next().expect(&format!("{:?}", state.simulation_state.factory.belts.get_item_iter(*id).into_iter().collect::>())).is_some(); - for _ in 0..200 { - state.update(&DATA_STORE); - } +// prop_assert!(state.statistics.production.total.unwrap().items_produced.iter().copied().sum::() > 0); - let Some(Entity::Belt { id, .. }) = state.world.get_entity_at(Position { x: 1602, y: 1601 }, &DATA_STORE) else { - unreachable!() - }; +// prop_assert!(items_at_intersection, "{:?}, \n{:?}", state.simulation_state.factory.belts, state.simulation_state.factory.belts.get_item_iter(*id).into_iter().collect::>()); +// } - let items_at_intersection = state.simulation_state.factory.belts.get_item_iter(*id).into_iter().next().expect(&format!("{:?}", state.simulation_state.factory.belts.get_item_iter(*id).into_iter().collect::>())).is_some(); +// #[test] +// fn sideload_with_items_at_source_items_actually_reach(actions in sideload_items().prop_shuffle()) { +// let mut state = GameState::new(&DATA_STORE); - prop_assert!(state.statistics.production.total.unwrap().items_produced.iter().copied().sum::() > 0); +// let bp = Blueprint { actions }; - prop_assert!(items_at_intersection, "{:?}, \n{:?}", state.simulation_state.factory.belts, state.simulation_state.factory.belts.get_item_iter(*id).into_iter().collect::>()); - } - - #[test] - fn sideload_with_items_at_source_items_actually_reach(actions in sideload_items().prop_shuffle()) { - let mut state = GameState::new(&DATA_STORE); - - let bp = Blueprint { actions }; +// bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); +// let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); - let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); +// let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); - let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); +// prop_assume!(assembler_powered); - prop_assume!(assembler_powered); +// let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); - let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); +// prop_assume!(assembler_working, "{:?}", ent); - prop_assume!(assembler_working, "{:?}", ent); +// let inserter_attached = matches!(state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(), Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); - let inserter_attached = matches!(state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(), Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); +// prop_assume!(inserter_attached); - prop_assume!(inserter_attached); +// for _ in 0..2000 { +// state.update(&DATA_STORE); +// } - for _ in 0..2000 { - state.update(&DATA_STORE); - } - - let Some(Entity::Belt { id: id_going_right, .. }) = state.world.get_entity_at(Position { x: 1602, y: 1601 }, &DATA_STORE) else { - unreachable!() - }; +// let Some(Entity::Belt { id: id_going_right, .. }) = state.world.get_entity_at(Position { x: 1602, y: 1601 }, &DATA_STORE) else { +// unreachable!() +// }; - let Some(Entity::Belt { id: id_going_down, .. }) = state.world.get_entity_at(Position { x: 1604, y: 1602 }, &DATA_STORE) else { - unreachable!() - }; +// let Some(Entity::Belt { id: id_going_down, .. }) = state.world.get_entity_at(Position { x: 1604, y: 1602 }, &DATA_STORE) else { +// unreachable!() +// }; - let produced = state.statistics.production.total.unwrap().items_produced.iter().copied().sum::(); +// let produced = state.statistics.production.total.unwrap().items_produced.iter().copied().sum::(); - prop_assume!(produced > 0, "{:?}", produced); +// prop_assume!(produced > 0, "{:?}", produced); - prop_assert!(dbg!(state.simulation_state.factory.belts.get_item_iter(*id_going_down).into_iter().next().unwrap()).is_some(),"down: {:?}\n, right:{:?}", state.simulation_state.factory.belts.get_item_iter(*id_going_down).into_iter().collect::>(), state.simulation_state.factory.belts.get_item_iter(*id_going_right).into_iter().collect::>()); - } +// prop_assert!(dbg!(state.simulation_state.factory.belts.get_item_iter(*id_going_down).into_iter().next().unwrap()).is_some(),"down: {:?}\n, right:{:?}", state.simulation_state.factory.belts.get_item_iter(*id_going_down).into_iter().collect::>(), state.simulation_state.factory.belts.get_item_iter(*id_going_right).into_iter().collect::>()); +// } - } -} +// } +// } diff --git a/src/frontend/world/sparse_grid/bounding_box_grid.rs b/src/frontend/world/sparse_grid/bounding_box_grid.rs index ce9ac58..6e391c5 100644 --- a/src/frontend/world/sparse_grid/bounding_box_grid.rs +++ b/src/frontend/world/sparse_grid/bounding_box_grid.rs @@ -45,7 +45,11 @@ impl + 'static> SparseGrid for BoundingBoxGrid { where T: Default, { - todo!() + if self.get(x, y).is_none() { + let old = self.insert(x, y, T::default()); + assert!(old.is_none()); + } + self.get(x, y).unwrap() } fn insert(&mut self, x: I, y: I, value: T) -> Option { @@ -58,13 +62,6 @@ impl + 'static> SparseGrid for BoundingBoxGrid { old } - fn insert_deduplicate(&mut self, x: I, y: I, value: T) -> Option - where - T: PartialEq + Default, - { - todo!() - } - fn get(&self, x: I, y: I) -> Option<&T> { if let Some(extent) = &self.extent { if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { diff --git a/src/frontend/world/sparse_grid/dynamic.rs b/src/frontend/world/sparse_grid/dynamic.rs index 4315d33..2b74b68 100644 --- a/src/frontend/world/sparse_grid/dynamic.rs +++ b/src/frontend/world/sparse_grid/dynamic.rs @@ -48,7 +48,7 @@ impl + 'static> SparseGrid for DynamicGrid { } } - fn get_default(&mut self, x: I, y: I) -> &T + fn get_default(&mut self, _x: I, _y: I) -> &T where T: Default, { @@ -135,13 +135,6 @@ impl + 'static> SparseGrid for DynamicGrid { } } - fn insert_deduplicate(&mut self, x: I, y: I, value: T) -> Option - where - T: PartialEq + Default, - { - unimplemented!() - } - fn occupied_entries(&self) -> impl Iterator { match &self.store { Backing::BoundingBoxGrid(grid) => Either::Left(grid.occupied_entries()), diff --git a/src/frontend/world/sparse_grid/map_grid.rs b/src/frontend/world/sparse_grid/map_grid.rs index 91f3741..6d8a3e4 100644 --- a/src/frontend/world/sparse_grid/map_grid.rs +++ b/src/frontend/world/sparse_grid/map_grid.rs @@ -44,6 +44,7 @@ impl SparseGrid for Btr } fn get(&self, x: I, y: I) -> Option<&T> { + // TODO: Why is this commented out? // if let Some(extent) = &self.extent { // if x < extent[0][0] || x > extent[0][1] || y < extent[1][0] || y > extent[1][1] { // return None; @@ -62,18 +63,6 @@ impl SparseGrid for Btr self.values.insert((x, y), value) } - fn insert_deduplicate(&mut self, x: I, y: I, value: T) -> Option - where - T: PartialEq + Default, - { - self.include_in_extent(x, y); - if value == T::default() { - self.values.remove(&(x, y)) - } else { - self.values.insert((x, y), value) - } - } - fn occupied_entries(&self) -> impl Iterator { self.values .iter() diff --git a/src/frontend/world/sparse_grid/mod.rs b/src/frontend/world/sparse_grid/mod.rs index aecd340..93f4bef 100644 --- a/src/frontend/world/sparse_grid/mod.rs +++ b/src/frontend/world/sparse_grid/mod.rs @@ -18,10 +18,6 @@ pub trait SparseGrid { fn get_mut(&mut self, x: I, y: I) -> Option<&mut T>; fn insert(&mut self, x: I, y: I, value: T) -> Option; - fn insert_deduplicate(&mut self, x: I, y: I, value: T) -> Option - where - T: PartialEq + Default; - fn occupied_entries(&self) -> impl Iterator; fn occupied_entries_mut(&mut self) -> impl Iterator; diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index 0d82f28..f4c9695 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -3414,10 +3414,10 @@ impl World match attached_inserter { - AttachedInserter::BeltStorage { id, belt_pos } => { + AttachedInserter::BeltStorage { .. } => { // TODO }, - AttachedInserter::BeltBelt { item, inserter } => { + AttachedInserter::BeltBelt { .. } => { // TODO }, AttachedInserter::StorageStorage { .. } => todo!(), @@ -3428,13 +3428,7 @@ impl World todo!(), + Entity::MiningDrill { .. } => todo!(), Entity::Belt { .. } | Entity::Underground { .. } | Entity::Splitter { .. } @@ -3493,7 +3487,7 @@ impl World true, InserterInfo::Attached { info, .. } => match info { AttachedInserter::BeltStorage { id, .. } => *id != old_id, - AttachedInserter::BeltBelt { item, inserter } => { + AttachedInserter::BeltBelt { .. } => { // TODO: true }, @@ -3577,7 +3571,7 @@ impl World todo!(), + AttachedInserter::BeltBelt { .. } => todo!(), AttachedInserter::StorageStorage { .. } => {}, }, }, @@ -3705,7 +3699,7 @@ impl World todo!(), + AttachedInserter::BeltBelt { .. } => todo!(), AttachedInserter::StorageStorage { .. } => {}, }, }, @@ -3740,10 +3734,10 @@ impl World { + AttachedInserter::BeltBelt { .. } => { // TODO: }, - AttachedInserter::StorageStorage { item, inserter } => { + AttachedInserter::StorageStorage { .. } => { // TODO: }, } @@ -4857,7 +4851,7 @@ impl World todo!(), + Entity::Splitter { .. } => todo!(), Entity::Chest { ty, pos, item, .. } => { if let Some((item, index)) = item { @@ -4872,13 +4866,7 @@ impl World todo!(), + Entity::Roboport { .. } => todo!(), Entity::Inserter { info: InserterInfo::NotAttached { .. }, @@ -4901,23 +4889,23 @@ impl World { - sim_state.factory.storage_storage_inserters.remove_ins( - *item, - user_movetime - .map(|v| v.into()) - .unwrap_or(data_store.inserter_infos[*ty as usize].swing_time_ticks) - .into(), - *inserter, - ); + sim_state + .factory + .storage_storage_inserters + .remove_ins( + *item, + user_movetime + .map(|v| v.into()) + .unwrap_or( + data_store.inserter_infos[*ty as usize].swing_time_ticks, + ) + .into(), + *inserter, + ) + .expect("Failed Removing Inserter"); }, }, - Entity::MiningDrill { - ty, - pos, - rotation, - drill_id, - internal_inserter, - } => todo!(), + Entity::MiningDrill { .. } => todo!(), } let chunk = self.get_chunk_for_tile_mut(e_pos).unwrap(); @@ -5098,7 +5086,7 @@ impl Chunk, + _data_store: &DataStore, ) -> bool { if let Some(arr) = &self.chunk_tile_to_entity_into { let x_in_chunk = pos.x.rem_euclid(i32::from(CHUNK_SIZE)) as usize; @@ -5258,10 +5246,10 @@ impl GetSize for ModuleSlots { impl, Info: EguiDisplayable> ShowInfo for ModuleSlots { fn show_fields>( &self, - extractor: &mut E, - ui: &mut egui::Ui, - path: String, - cache: &mut C, + _extractor: &mut E, + _ui: &mut egui::Ui, + _path: String, + _cache: &mut C, ) { } } diff --git a/src/inserter/belt_storage_movement_list.rs b/src/inserter/belt_storage_movement_list.rs index bb68272..a4d9ac2 100644 --- a/src/inserter/belt_storage_movement_list.rs +++ b/src/inserter/belt_storage_movement_list.rs @@ -164,7 +164,7 @@ impl<'a> FinishedMovingLists<'a, { Dir::BeltToStorage }, { Dir::StorageToBelt }> storages: SingleItemStorages, ) { self.list.retain_mut(|inserter| { - let (max_insert, data, wait_list) = + let (_max_insert, data, wait_list) = index_fake_union(item_id, storages, inserter.storage, grid_size); let items_moved = min(inserter.max_hand_size - inserter.current_hand, *data); diff --git a/src/inserter/belt_storage_pure_buckets.rs b/src/inserter/belt_storage_pure_buckets.rs index 9f8804c..a7d2cea 100644 --- a/src/inserter/belt_storage_pure_buckets.rs +++ b/src/inserter/belt_storage_pure_buckets.rs @@ -246,6 +246,7 @@ impl BucketedStorageStorageInserterStoreFrontend { }); } + #[allow(non_snake_case)] for (i, _) in sizes.into_iter().enumerate().sorted_by_key(|v| v.0) { let MOVING_OUT_END: usize = store.list_len(); let WATING_FOR_SPACE: usize = MOVING_OUT_END; @@ -1317,8 +1318,8 @@ mod test { let mut belt_ids = (0..(NUM_INSERTERS as u32)) .map(|v| v % NUM_BELTS as u32) .collect_vec(); - values.shuffle(&mut rand::thread_rng()); - belt_ids.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); + belt_ids.shuffle(&mut rand::rng()); for (storage, belt) in values.into_iter().zip(belt_ids) { if random::() < 1 { store[item].0.update( diff --git a/src/inserter/storage_storage_with_buckets.rs b/src/inserter/storage_storage_with_buckets.rs index 2a79a4a..d71f82d 100644 --- a/src/inserter/storage_storage_with_buckets.rs +++ b/src/inserter/storage_storage_with_buckets.rs @@ -1304,7 +1304,7 @@ mod test { for item in 0..NUM_ITEMS { let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); for i in values { if random::() < 1 { store[item].update( @@ -1414,7 +1414,7 @@ mod test { let mut storages_out = vec![0u8; NUM_INSERTERS]; let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); let mut current_time: u32 = 0; @@ -1497,7 +1497,7 @@ mod test { for v in &mut storages_out { *v = 0u8; } - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); to_find = (0..(NUM_VISIBLE as u32)) .into_iter() .map(|i| InserterIdentifier { @@ -1554,7 +1554,7 @@ mod test { let mut storages_out = vec![0u8; NUM_INSERTERS]; let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); let mut current_time: u32 = 0; diff --git a/src/inserter/storage_storage_with_buckets_compressed.rs b/src/inserter/storage_storage_with_buckets_compressed.rs index a58d6d6..d32df7f 100644 --- a/src/inserter/storage_storage_with_buckets_compressed.rs +++ b/src/inserter/storage_storage_with_buckets_compressed.rs @@ -1229,7 +1229,7 @@ mod test { for item in 0..NUM_ITEMS { let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); for i in values { if random::() < 1 { store[item].update( @@ -1324,7 +1324,7 @@ mod test { let mut storages_out = vec![0u8; NUM_INSERTERS]; let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); let mut current_time: u32 = 0; @@ -1399,7 +1399,7 @@ mod test { for v in &mut storages_out { *v = 0u8; } - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); to_find = (0..(NUM_VISIBLE as u32)) .into_iter() .map(|i| InserterIdentifier { @@ -1448,7 +1448,7 @@ mod test { let mut storages_out = vec![0u8; NUM_INSERTERS]; let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); let mut current_time: u32 = 0; diff --git a/src/inserter/storage_storage_with_buckets_indirect.rs b/src/inserter/storage_storage_with_buckets_indirect.rs index 591ffda..89447f8 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -508,8 +508,8 @@ impl BucketedStorageStorageInserterStore { grid_size: usize, current_tick: u32, ) { - #[cfg(debug_assertions)] - let old_len: usize = self.get_list_sizes().iter().sum(); + // #[cfg(debug_assertions)] + // let old_len: usize = self.get_list_sizes().iter().sum(); assert!(self.current_tick < self.list_len()); @@ -1086,7 +1086,7 @@ mod test { for item in 0..NUM_ITEMS { let mut values = (0..(NUM_INSERTERS as u32)).collect_vec(); - values.shuffle(&mut rand::thread_rng()); + values.shuffle(&mut rand::rng()); for i in values { if random::() < 1 { store[item].update( diff --git a/src/liquid/mod.rs b/src/liquid/mod.rs index beb8ab5..c135597 100644 --- a/src/liquid/mod.rs +++ b/src/liquid/mod.rs @@ -23,8 +23,6 @@ use get_size2::GetSize; pub mod connection_logic; -const FLUID_INSERTER_MOVETIME: u16 = 1; - #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)] pub struct FluidSystemId { @@ -1074,7 +1072,7 @@ impl FluidSystem { fn add_output( &mut self, - fluid: Item, + _fluid: Item, source_pipe_position: Position, dest: FakeUnionStorage, _dest_pos: Position, @@ -1099,7 +1097,7 @@ impl FluidSystem { fn add_input( &mut self, - fluid: Item, + _fluid: Item, dest_pipe_position: Position, source: FakeUnionStorage, _source_pos: Position, @@ -1315,10 +1313,10 @@ impl FluidSystem { for conn in other.graph.weak_components_mut() { match conn { - FluidSystemEntity::OutgoingPump { inserter_id } => { + FluidSystemEntity::OutgoingPump { .. } => { todo!() }, - FluidSystemEntity::IncomingPump { inserter_id } => { + FluidSystemEntity::IncomingPump { .. } => { todo!() }, FluidSystemEntity::Input { inserter_id, .. } => { @@ -1382,18 +1380,18 @@ impl FluidSystem { let mut fluid_distribution = old_fluid_level; for (weak_index, connection_to_remove) in connections_to_remove.into_iter().collect_vec() { - let fluid = old_fluid.expect("If we have any connections we MUST have a fluid set"); + let _fluid = old_fluid.expect("If we have any connections we MUST have a fluid set"); match connection_to_remove { - FluidSystemEntity::OutgoingPump { inserter_id } => { + FluidSystemEntity::OutgoingPump { .. } => { todo!() }, - FluidSystemEntity::IncomingPump { inserter_id } => { + FluidSystemEntity::IncomingPump { .. } => { todo!() }, - FluidSystemEntity::Input { inserter_id, .. } => { + FluidSystemEntity::Input { .. } => { self.remove_input(fluid_box_position, weak_index); }, - FluidSystemEntity::Output { inserter_id, .. } => { + FluidSystemEntity::Output { .. } => { self.remove_output(fluid_box_position, weak_index); }, } @@ -1411,19 +1409,19 @@ impl FluidSystem { match new_system.state { FluidSystemState::NoFluid => {}, - FluidSystemState::HasFluid { fluid } => { + FluidSystemState::HasFluid { fluid: _ } => { for connection in new_system.graph.weak_components_mut() { match connection { - FluidSystemEntity::OutgoingPump { inserter_id } => { + FluidSystemEntity::OutgoingPump { .. } => { todo!() }, - FluidSystemEntity::IncomingPump { inserter_id } => { + FluidSystemEntity::IncomingPump { .. } => { todo!() }, - FluidSystemEntity::Input { inserter_id, .. } => { + FluidSystemEntity::Input { .. } => { todo!() }, - FluidSystemEntity::Output { inserter_id, .. } => { + FluidSystemEntity::Output { .. } => { todo!() }, } @@ -1499,16 +1497,16 @@ impl FluidSystem { FluidSystemState::HasFluid { fluid } => { for connection in new_system.graph.weak_components_mut() { match connection { - FluidSystemEntity::OutgoingPump { inserter_id } => { + FluidSystemEntity::OutgoingPump { .. } => { todo!() }, - FluidSystemEntity::IncomingPump { inserter_id } => { + FluidSystemEntity::IncomingPump { .. } => { todo!() }, - FluidSystemEntity::Input { inserter_id, .. } => { + FluidSystemEntity::Input { .. } => { todo!() }, - FluidSystemEntity::Output { inserter_id, .. } => { + FluidSystemEntity::Output { .. } => { todo!() }, } @@ -1535,7 +1533,7 @@ impl FluidSystem { match self.state { FluidSystemState::NoFluid => {}, - FluidSystemState::HasFluid { fluid } => { + FluidSystemState::HasFluid { fluid: _ } => { assert!(fluid_left_for_us <= self.hot_data.storage_capacity.try_into().unwrap()); self.hot_data.current_fluid_level = fluid_left_for_us; }, diff --git a/src/par_generation.rs b/src/par_generation.rs index 94b9914..ba6ba59 100644 --- a/src/par_generation.rs +++ b/src/par_generation.rs @@ -774,14 +774,14 @@ pub fn par_generate( data_store, ); - let mut chest_store = chest_stage( + let chest_store = chest_stage( &mut world, generation_info.chest_actions, positions.iter().copied(), data_store, ); - let mut storage_storage_store = StorageStorageInserterStore::new(data_store); + let storage_storage_store = StorageStorageInserterStore::new(data_store); let fluid_store = pipe_stage( &mut world, generation_info.pipe_actions.fluid_networks, diff --git a/src/power/mod.rs b/src/power/mod.rs index 6664a27..3199bbb 100644 --- a/src/power/mod.rs +++ b/src/power/mod.rs @@ -542,7 +542,7 @@ impl PowerGridStorage todo!(), + power_grid::BeaconAffectedEntity::Lab { .. } => todo!(), } } } @@ -643,7 +643,7 @@ impl PowerGridStorage { self.power_grids[id.grid as usize].is_assembler_id_a_hole(*id, data_store) }, - BeaconAffectedEntity::Lab { grid, index } => { + BeaconAffectedEntity::Lab { .. } => { // TODO: Check that this is not a lab hole false }, @@ -715,7 +715,7 @@ impl PowerGridStorage { self.power_grids[id.grid as usize].is_assembler_id_a_hole(*id, data_store) }, - BeaconAffectedEntity::Lab { grid, index } => { + BeaconAffectedEntity::Lab { .. } => { // TODO: Check that this is not a lab hole false }, @@ -855,7 +855,7 @@ impl PowerGridStorage { + power_grid::BeaconAffectedEntity::Lab { .. } => { // TODO: error!("Ignoring Beacon affect on lab"); }, @@ -1228,10 +1228,10 @@ impl PowerGridStorage { let ( - belt_storage_exit_outgoing, + _belt_storage_exit_outgoing, belt_storage_reinsertion_incoming, storage_belt_reinsertion_outgoing, - storage_belt_exit_incoming, + _storage_belt_exit_incoming, ) = &mut belt_storage_reinsertion_list [inserter.item.into_usize()]; diff --git a/src/power/power_grid.rs b/src/power/power_grid.rs index cec772c..c1a14f3 100644 --- a/src/power/power_grid.rs +++ b/src/power/power_grid.rs @@ -848,7 +848,7 @@ impl PowerGrid { let residual_items = self.lab_stores.remove_lab(*index); }, - PowerGridEntity::LazyPowerProducer { item, index } => { + PowerGridEntity::LazyPowerProducer { .. } => { todo!("Remove LazyPowerProducer (Steam Engine)") }, PowerGridEntity::SolarPanel { ty } => { @@ -955,7 +955,7 @@ impl PowerGrid { + PowerGridEntity::LazyPowerProducer { .. } => { todo!("Move LazyPowerProducer (Steam Engine)") }, PowerGridEntity::SolarPanel { ty } => { @@ -2251,7 +2251,7 @@ impl PowerGrid(info: &LazyPowerMachineInfo) -> Self { + fn new(_info: &LazyPowerMachineInfo) -> Self { Self { ingredient: vec![], stored_power: vec![], diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index d8db4ae..ad3e8fe 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -989,13 +989,13 @@ impl App { }, } }, - LoadedGame::ItemU8RecipeU16(loaded_game_sized) => { + LoadedGame::ItemU8RecipeU16(_loaded_game_sized) => { todo!("Handle bigger item/recipe counts") }, - LoadedGame::ItemU16RecipeU8(loaded_game_sized) => { + LoadedGame::ItemU16RecipeU8(_loaded_game_sized) => { todo!("Handle bigger item/recipe counts") }, - LoadedGame::ItemU16RecipeU16(loaded_game_sized) => { + LoadedGame::ItemU16RecipeU16(_loaded_game_sized) => { todo!("Handle bigger item/recipe counts") }, }; diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index d2cd839..e201c7b 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -204,16 +204,16 @@ pub fn render_world( state_machine.current_mouse_pos, ); - // bp.draw( - // ( - // x as f32 + num_tiles_across_screen_horizontal / 2.0, - // y as f32 + num_tiles_across_screen_vertical / 2.0, - // ), - // camera_pos, - // &mut entity_overlay_layer, - // texture_atlas, - // data_store, - // ); + bp.draw( + ( + x as f32 + num_tiles_across_screen_horizontal / 2.0, + y as f32 + num_tiles_across_screen_vertical / 2.0, + ), + camera_pos, + &mut entity_overlay_layer, + texture_atlas, + data_store, + ); } mem::drop(state_machine); @@ -647,7 +647,7 @@ pub fn render_world( state, } => { let mut source_dir = None; - let (sprite, corner) = { + let (sprite, _corner) = { match state { BeltState::Straight => { (&texture_atlas.belt[*direction], None::) @@ -970,7 +970,7 @@ pub fn render_world( item_pos[1] += stack_offset; } }, - crate::frontend::world::tile::AttachedInserter::BeltBelt { item, inserter } => { + crate::frontend::world::tile::AttachedInserter::BeltBelt { .. } => { // TODO: }, crate::frontend::world::tile::AttachedInserter::StorageStorage { item, inserter } => { @@ -2203,10 +2203,7 @@ pub fn render_ui< data::RepeatableCostScaling::Linear { unit_increase_per_level, } => unit_increase_per_level * u64::from(times_this_tech_was_finished), - data::RepeatableCostScaling::Exponential { - unit_multiplier_per_level_nom, - unit_multiplier_per_level_denom, - } => todo!(), + data::RepeatableCostScaling::Exponential { .. } => todo!(), }; tech_cost_units += tech_cost_increase; @@ -2279,7 +2276,6 @@ pub fn render_ui< .include_y(0.0) .set_margin_fraction([0.0, 0.05].into()) .x_grid_spacer(|grid_input| { - let min_step = grid_input.base_step_size; (0..NUM_X_AXIS_TICKS[SCALE]) .map(|v| GridMark { value: v as f64 / (NUM_X_AXIS_TICKS[SCALE] as f64) @@ -2365,14 +2361,14 @@ pub fn render_ui< Window::new("DEBUG USE WITH CARE") .default_open(false) .show(ctx, |ui| { - if ui.button("⚠️DEFRAGMENT GAMESTATE").clicked() { - // TODO: + // TODO: + // if ui.button("⚠️DEFRAGMENT GAMESTATE").clicked() { // let mut new_state = game_state_ref.clone(); // mem::swap(&mut new_state, &mut *game_state_ref); // mem::drop(new_state); - } + // } if ui.button("Switch from generation assemblers to miners (inserter_transfer)").clicked() { for entity in game_state_ref.world.get_chunks().flat_map(|chunk| chunk.get_entities()) { diff --git a/src/research.rs b/src/research.rs index 7907190..de15a0c 100644 --- a/src/research.rs +++ b/src/research.rs @@ -141,10 +141,7 @@ impl TechState { data::RepeatableCostScaling::Linear { unit_increase_per_level, } => unit_increase_per_level * u64::from(times_this_tech_was_finished), - data::RepeatableCostScaling::Exponential { - unit_multiplier_per_level_nom, - unit_multiplier_per_level_denom, - } => todo!(), + data::RepeatableCostScaling::Exponential { .. } => todo!(), }; tech_cost_units += tech_cost_increase; diff --git a/src/saving/mod.rs b/src/saving/mod.rs index 90b1dcf..ab6a47d 100644 --- a/src/saving/mod.rs +++ b/src/saving/mod.rs @@ -57,7 +57,7 @@ pub fn save_at(value: &V, path: PathBuf) { } pub fn save_at_fork(value: &V, path: PathBuf) { - let mut file = { File::create(path).expect("could not create file") }; + let file = { File::create(path).expect("could not create file") }; // FIXME: It is technically not okay to allocate here. let mut buf_writer = BufWriter::new(file); From d4bd29169c962d0249bb50f1b6734b64f60e1439 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 5 Jan 2026 01:55:59 +0100 Subject: [PATCH 071/152] Allow indexing without item_id --- src/inserter/belt_storage_inserter.rs | 4 +- .../belt_storage_inserter_non_const_gen.rs | 4 +- src/inserter/belt_storage_movement_list.rs | 4 +- src/inserter/belt_storage_pure_buckets.rs | 4 +- src/inserter/storage_storage_inserter.rs | 4 +- src/inserter/storage_storage_with_buckets.rs | 5 +- .../storage_storage_with_buckets_indirect.rs | 4 +- src/liquid/mod.rs | 4 +- src/storage_list.rs | 142 ++++++++++-------- 9 files changed, 100 insertions(+), 75 deletions(-) diff --git a/src/inserter/belt_storage_inserter.rs b/src/inserter/belt_storage_inserter.rs index 7aabb0e..e6a5458 100644 --- a/src/inserter/belt_storage_inserter.rs +++ b/src/inserter/belt_storage_inserter.rs @@ -83,7 +83,7 @@ impl BeltStorageInserter<{ Dir::BeltToStorage }> { }, InserterState::WaitingForSpaceInDestination(count) => { let (max_insert, old, _) = - index_fake_union(todo!(), storages, self.storage_id, grid_size); + index_fake_union(None, storages, self.storage_id, grid_size); let to_insert = min(count, *max_insert - *old); if to_insert > 0 { @@ -136,7 +136,7 @@ impl BeltStorageInserter<{ Dir::StorageToBelt }> { match self.state { InserterState::WaitingForSourceItems(count) => { let (_max_insert, old, _) = - index_fake_union(todo!(), storages, self.storage_id, grid_size); + index_fake_union(None, 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 0c923b3..a744789 100644 --- a/src/inserter/belt_storage_inserter_non_const_gen.rs +++ b/src/inserter/belt_storage_inserter_non_const_gen.rs @@ -112,7 +112,7 @@ impl BeltStorageInserterDyn { }, DynInserterState::BSWaitingForSpaceInDestination(count) => { let (max_insert, old, _) = - index_fake_union(item_id, storages, self.storage_id, grid_size); + index_fake_union(Some(item_id), storages, self.storage_id, grid_size); let to_insert = min(count, *max_insert - *old); if to_insert > 0 { @@ -148,7 +148,7 @@ impl BeltStorageInserterDyn { }, DynInserterState::SBWaitingForSourceItems(count) => { let (_max_insert, old, _) = - index_fake_union(item_id, storages, self.storage_id, grid_size); + index_fake_union(Some(item_id), storages, self.storage_id, grid_size); let to_extract = min(max_hand_size - count, *old); diff --git a/src/inserter/belt_storage_movement_list.rs b/src/inserter/belt_storage_movement_list.rs index a4d9ac2..22e007c 100644 --- a/src/inserter/belt_storage_movement_list.rs +++ b/src/inserter/belt_storage_movement_list.rs @@ -112,7 +112,7 @@ impl<'a> FinishedMovingLists<'a, { Dir::BeltToStorage }, { Dir::BeltToStorage }> ) { self.list.retain_mut(|inserter| { let (max_insert, data, wait_list) = - index_fake_union(item_id, storages, inserter.storage, grid_size); + index_fake_union(Some(item_id), storages, inserter.storage, grid_size); let items_moved = min(inserter.current_hand, *max_insert - *data); @@ -165,7 +165,7 @@ impl<'a> FinishedMovingLists<'a, { Dir::BeltToStorage }, { Dir::StorageToBelt }> ) { self.list.retain_mut(|inserter| { let (_max_insert, data, wait_list) = - index_fake_union(item_id, storages, inserter.storage, grid_size); + index_fake_union(Some(item_id), storages, inserter.storage, grid_size); let items_moved = min(inserter.max_hand_size - inserter.current_hand, *data); diff --git a/src/inserter/belt_storage_pure_buckets.rs b/src/inserter/belt_storage_pure_buckets.rs index a7d2cea..f839ed3 100644 --- a/src/inserter/belt_storage_pure_buckets.rs +++ b/src/inserter/belt_storage_pure_buckets.rs @@ -636,7 +636,7 @@ impl BucketedStorageStorageInserterStore { }, Dir::StorageToBelt => { let (_max_insert, old, _) = - index_fake_union(item_id, storages, inserter.storage_id, grid_size); + index_fake_union(Some(item_id), storages, inserter.storage_id, grid_size); let to_extract = min(inserter.max_hand_size - inserter.current_hand, *old); @@ -678,7 +678,7 @@ impl BucketedStorageStorageInserterStore { match DIR { Dir::BeltToStorage => { let (max_insert, old, _) = - index_fake_union(item_id, storages, inserter.storage_id, grid_size); + index_fake_union(Some(item_id), storages, inserter.storage_id, grid_size); let to_insert = min(inserter.current_hand, *max_insert - *old); diff --git a/src/inserter/storage_storage_inserter.rs b/src/inserter/storage_storage_inserter.rs index f129515..8f48ef0 100644 --- a/src/inserter/storage_storage_inserter.rs +++ b/src/inserter/storage_storage_inserter.rs @@ -55,7 +55,7 @@ impl StorageStorageInserter { match self.state { InserterState::WaitingForSourceItems(count) => { let (_max_insert, old, _) = - index_fake_union(item_id, storages, self.storage_id_in, grid_size); + index_fake_union(Some(item_id), storages, self.storage_id_in, grid_size); let to_extract = min(max_hand_size - count, *old); @@ -72,7 +72,7 @@ impl StorageStorageInserter { }, InserterState::WaitingForSpaceInDestination(count) => { let (max_insert, old, _) = - index_fake_union(item_id, storages, self.storage_id_out, grid_size); + index_fake_union(Some(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 d71f82d..dc7e0e0 100644 --- a/src/inserter/storage_storage_with_buckets.rs +++ b/src/inserter/storage_storage_with_buckets.rs @@ -249,6 +249,7 @@ impl BucketedStorageStorageInserterStoreFrontend { ); } + #[allow(non_snake_case)] for (i, _) in sizes.into_iter().enumerate().sorted_by_key(|v| v.0) { let MOVING_OUT_END: usize = store.list_len(); let WATING_FOR_SPACE: usize = MOVING_OUT_END + 1; @@ -654,7 +655,7 @@ impl BucketedStorageStorageInserterStore { _movetime: u16, ) -> bool { let (_max_insert, old, _) = - index_fake_union(todo!(), storages, inserter.storage_id_in, grid_size); + index_fake_union(None, storages, inserter.storage_id_in, grid_size); let to_extract = min(inserter.max_hand_size - inserter.current_hand, *old); @@ -688,7 +689,7 @@ impl BucketedStorageStorageInserterStore { _movetime: u16, ) -> bool { let (max_insert, old, _) = - index_fake_union(todo!(), storages, inserter.storage_id_out, grid_size); + index_fake_union(None, 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 89447f8..b9653ff 100644 --- a/src/inserter/storage_storage_with_buckets_indirect.rs +++ b/src/inserter/storage_storage_with_buckets_indirect.rs @@ -316,7 +316,7 @@ impl BucketedStorageStorageInserterStore { let storage_id = bucket_data.storage_id_in; let (_max_insert, old, wait_list) = - index_fake_union(item_id, storages, storage_id, grid_size); + index_fake_union(Some(item_id), storages, storage_id, grid_size); let old_val = *old; let max_hand_size = bucket_data.max_hand_size; @@ -395,7 +395,7 @@ impl BucketedStorageStorageInserterStore { let storage_id = bucket_data.storage_id_out; let (max_insert, old, wait_list) = - index_fake_union(item_id, storages, storage_id, grid_size); + index_fake_union(Some(item_id), storages, storage_id, grid_size); let old_val = *old; let max_insert = *max_insert; diff --git a/src/liquid/mod.rs b/src/liquid/mod.rs index c135597..564869b 100644 --- a/src/liquid/mod.rs +++ b/src/liquid/mod.rs @@ -1569,7 +1569,7 @@ pub fn update_fluid_system( if hot_data.current_fluid_level == 0 { break; } - let (max, data, _) = index_fake_union(item_id, storages, outgoing_conn, grid_size); + let (max, data, _) = index_fake_union(Some(item_id), storages, outgoing_conn, grid_size); let amount_wanted = *max - *data; let amount_extracted = min( @@ -1595,7 +1595,7 @@ pub fn update_fluid_system( if hot_data.current_fluid_level == hot_data.storage_capacity { break; } - let (_max, data, _) = index_fake_union(item_id, storages, incoming_conn, grid_size); + let (_max, data, _) = index_fake_union(Some(item_id), storages, incoming_conn, grid_size); let amount_wanted = *data; let amount_extracted = min( diff --git a/src/storage_list.rs b/src/storage_list.rs index abcc72a..734e570 100644 --- a/src/storage_list.rs +++ b/src/storage_list.rs @@ -208,7 +208,7 @@ pub fn index<'a, 'b, RecipeIdxType: IdxTrait>( #[inline(always)] pub fn index_fake_union<'a, 'b>( - item_id: usize, + item_id: Option, slice: SingleItemStorages<'a, 'b>, storage_id: FakeUnionStorage, grid_size: usize, @@ -227,88 +227,112 @@ pub fn index_fake_union<'a, 'b>( }; 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; + if let Some(item_id) = item_id { + 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; - 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 !is_static { + let grid_id = storage_id.grid_or_static_flag as usize - static_size; - 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 + 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 { - // We are a lab + let static_id = + StaticID::try_from(storage_id.recipe_idx_with_this_item as u8).unwrap(); + panic!( - "Failed FakeUnion Index for item {} for Lab in grid {}, with index {}", - &DATA_STORE.item_names[item_id], grid_id, index + "Failed FakeUnion Index for item {} for Static {:?}, with index {}", + &DATA_STORE.item_names[item_id], static_id, index ); } } else { - let static_id = StaticID::try_from(storage_id.recipe_idx_with_this_item as u8).unwrap(); + let grid = storage_id.grid_or_static_flag; + let rec = storage_id.recipe_idx_with_this_item; + let index = storage_id.index; panic!( - "Failed FakeUnion Index for item {} for Static {:?}, with index {}", - &DATA_STORE.item_names[item_id], static_id, index + "Failed FakeUnion Index in grid_or_static_flag {}, with recipe_idx_with_this_item {} and index {}", + grid, rec, 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; + if let Some(item_id) = item_id { + 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; - 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 !is_static { + let grid_id = storage_id.grid_or_static_flag as usize - static_size; - 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 + 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 { - // We are a lab + let static_id = + StaticID::try_from(storage_id.recipe_idx_with_this_item as u8).unwrap(); + panic!( - "Failed FakeUnion Index for item {} for Lab in grid {}, with index {}", - &DATA_STORE.item_names[item_id], grid_id, index + "Failed FakeUnion Index for item {} for Static {:?}, with index {}", + &DATA_STORE.item_names[item_id], static_id, index ); } } else { - let static_id = StaticID::try_from(storage_id.recipe_idx_with_this_item as u8).unwrap(); + let grid = storage_id.grid_or_static_flag; + let rec = storage_id.recipe_idx_with_this_item; + let index = storage_id.index; panic!( - "Failed FakeUnion Index for item {} for Static {:?}, with index {}", - &DATA_STORE.item_names[item_id], static_id, index + "Failed FakeUnion Index in grid_or_static_flag {}, with recipe_idx_with_this_item {} and index {}", + grid, rec, index ); } }; From ee24703552cfbe1b879604ebe37469ceec9b88ec Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 5 Jan 2026 03:22:52 +0100 Subject: [PATCH 072/152] Remove unneded dep --- Cargo.lock | 4 ---- Cargo.toml | 1 - src/belt/smart.rs | 2 +- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ac4d31..21c3f29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1796,7 +1796,6 @@ dependencies = [ "serde_path_to_error", "sha2", "simple_logger", - "smallvec", "spin_sleep_util", "stable-vec", "static_assertions", @@ -4807,9 +4806,6 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] [[package]] name = "smithay-client-toolkit" diff --git a/Cargo.toml b/Cargo.toml index e7ee33c..e9e9a6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,6 @@ egui-show-info = { git = "https://github.com/BloodStainedCrow/egui-show-info", f egui-show-info-derive = { git = "https://github.com/BloodStainedCrow/egui-show-info", optional = true } bytemuck = "1.23.1" memoffset = "0.9.1" -smallvec = { version = "1.15.1", features = ["serde"] } ecolor = { version = "0.32", features = ["color-hex"] } getrandom = { version = "0.3.3", features = ["wasm_js"] } diff --git a/src/belt/smart.rs b/src/belt/smart.rs index 9096421..9a79bdf 100644 --- a/src/belt/smart.rs +++ b/src/belt/smart.rs @@ -1692,7 +1692,7 @@ impl Belt for SmartBelt { FreeIndex::OldFreeIndex(_) => {}, } } - let (_old_free, need_to_check) = match self.first_free_index { + let (old_free, need_to_check) = match self.first_free_index { FreeIndex::FreeIndex(idx) => (idx, false), FreeIndex::OldFreeIndex(idx) => (idx, true), }; From f80c656e3e08b15b02dfbc791c247e17b8ab3c2e Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 5 Jan 2026 17:40:46 +0100 Subject: [PATCH 073/152] Create SmallCapVec and disable because it is slower --- src/app_state.rs | 8 +- src/belt/smart.rs | 325 ++++++++++++--------- src/belt/sushi.rs | 2 +- src/inserter/belt_storage_movement_list.rs | 33 ++- src/lib.rs | 1 + src/main.rs | 1 + src/temp_vec.rs | 245 ++++++++++++++++ 7 files changed, 452 insertions(+), 163 deletions(-) create mode 100644 src/temp_vec.rs diff --git a/src/app_state.rs b/src/app_state.rs index 7a8bd0a..8f4c12b 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -3039,6 +3039,8 @@ impl GameState GameState SmartBelt { first_free_index: FreeIndex::FreeIndex(0), zero_index: 0, locs: bitbox![0; len.into()].into(), - inserters: InserterStoreDyn { inserters: vec![] }, + inserters: InserterStoreDyn { + inserters: VecHolder::new(), + }, item, @@ -221,6 +227,7 @@ impl SmartBelt { zero_index, inserters: SushiInserterStoreDyn { inserters: inserters + .into_vec() .into_iter() .map(|ins| { ( @@ -367,51 +374,58 @@ impl SmartBelt { } pub fn change_inserter_storage_id(&mut self, old: FakeUnionStorage, new: FakeUnionStorage) { - for inserter in &mut self.inserters.inserters { - if inserter.storage == old { - inserter.storage = new; + self.inserters.inserters.access_mut(|v| { + for inserter in v { + if inserter.storage == old { + inserter.storage = new; + } } - } + }); } pub fn set_inserter_storage_id(&mut self, belt_pos: u16, new: FakeUnionStorage) { let mut pos = 0; - for inserter in self.inserters.inserters.iter_mut() { - pos = inserter.belt_pos; - if pos == belt_pos { - inserter.storage = new; - return; + let Ok(()) = self.inserters.inserters.access_mut(|v| { + for inserter in v { + pos = inserter.belt_pos; + if pos == belt_pos { + inserter.storage = new; + return Ok(()); + } } - } - unreachable!( - "Tried to set_inserter_storage_id with position {belt_pos}, which does not contain an inserter. {:?}", - self.inserters - ); + Err(()) + }) else { + unreachable!( + "Tried to set_inserter_storage_id with position {belt_pos}, which does not contain an inserter. {:?}", + self.inserters + ); + }; } #[must_use] pub fn get_inserter_info_at(&self, belt_pos: u16) -> Option { let mut pos = 0; - for inserter in self.inserters.inserters.iter() { - pos = inserter.belt_pos; - if pos == belt_pos { - return Some(BeltInserterInfo { - outgoing: inserter.outgoing, - state: if inserter.outgoing { - InserterState::WaitingForSourceItems(inserter.current_hand) - } else { - InserterState::WaitingForSpaceInDestination(inserter.current_hand) - }, - connection: inserter.storage, - movetime: inserter.movetime.into(), - hand_size: inserter.max_hand_size, - }); + self.inserters.inserters.access(|v| { + for inserter in v { + pos = inserter.belt_pos; + if pos == belt_pos { + return Some(BeltInserterInfo { + outgoing: inserter.outgoing, + state: if inserter.outgoing { + InserterState::WaitingForSourceItems(inserter.current_hand) + } else { + InserterState::WaitingForSpaceInDestination(inserter.current_hand) + }, + connection: inserter.storage, + movetime: inserter.movetime.into(), + hand_size: inserter.max_hand_size, + }); + } } - } - - None + None + }) } pub(super) fn change_inserter_movetime( @@ -448,7 +462,7 @@ impl SmartBelt { .position(|ins| ins.belt_pos == pos) { Some(idx) => { - let removed = self.inserters.inserters.remove(idx); + let removed = self.inserters.inserters.access_mut(|v| v.remove(idx)); Ok(removed.storage) }, @@ -481,13 +495,15 @@ impl SmartBelt { return Err(InserterAdditionError::ItemMismatch); } - self.inserters.inserters.push(InserterExtractedWhenMoving { - storage: storage_id, - belt_pos: index, - movetime: movetime.try_into().unwrap_or(u8::MAX).try_into().unwrap(), - outgoing: true, - max_hand_size: hand_size, - current_hand: 0, + self.inserters.inserters.access_mut(|v| { + v.push(InserterExtractedWhenMoving { + storage: storage_id, + belt_pos: index, + movetime: movetime.try_into().unwrap_or(u8::MAX).try_into().unwrap(), + outgoing: true, + max_hand_size: hand_size, + current_hand: 0, + }); }); Ok(()) @@ -517,13 +533,15 @@ impl SmartBelt { if filter != self.item { return Err(InserterAdditionError::ItemMismatch); } - self.inserters.inserters.push(InserterExtractedWhenMoving { - storage: storage_id, - belt_pos: index, - movetime: movetime.try_into().unwrap_or(u8::MAX).try_into().unwrap(), - outgoing: false, - max_hand_size: hand_size, - current_hand: 0, + self.inserters.inserters.access_mut(|v| { + v.push(InserterExtractedWhenMoving { + storage: storage_id, + belt_pos: index, + movetime: movetime.try_into().unwrap_or(u8::MAX).try_into().unwrap(), + outgoing: false, + max_hand_size: hand_size, + current_hand: 0, + }); }); Ok(()) @@ -630,92 +648,97 @@ impl SmartBelt { let locs = &mut self.locs; let latest_inserter_pos_if_all_incoming = &mut self.latest_inserter_pos_if_all_incoming; - let extracted = self.inserters.inserters.extract_if(.., move |ins| { - // FIXME: This should not be needed, if we did not incorrectly insert inserters always in the belt - if ins.current_hand == 0 && !ins.outgoing { - return true; - } - - // Taken from VecDeque::wrap_index - let logical_index = usize::from(*zero_index) + usize::from(ins.belt_pos); - let loc_idx = if logical_index >= locs.len() { - logical_index - locs.len() - } else { - logical_index - }; + let extracted = self.inserters.inserters.access_mut(|v| { + v.extract_if(.., move |ins| { + // FIXME: This should not be needed, if we did not incorrectly insert inserters always in the belt + if ins.current_hand == 0 && !ins.outgoing { + return true; + } - #[cfg(feature = "debug-stat-gathering")] - if ins.outgoing { - NUM_INSERTER_LOADS_WAITING_FOR_ITEMS - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); - } else { - NUM_INSERTER_LOADS_WAITING_FOR_SPACE - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); - } + // Taken from VecDeque::wrap_index + let logical_index = usize::from(*zero_index) + usize::from(ins.belt_pos); + let loc_idx = if logical_index >= locs.len() { + logical_index - locs.len() + } else { + logical_index + }; - if ins.outgoing { - min_pos = None; - } else { - if let Some(min_pos) = &mut min_pos { - *min_pos = std::cmp::max(*min_pos, NonZero::new(ins.belt_pos).expect("Currently inserters at belt_pos 0 are unsupported, and should never be generated")); + #[cfg(feature = "debug-stat-gathering")] + if ins.outgoing { + NUM_INSERTER_LOADS_WAITING_FOR_ITEMS + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } else { + NUM_INSERTER_LOADS_WAITING_FOR_SPACE + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); } - } - *latest_inserter_pos_if_all_incoming = min_pos; - let extract = if ins.belt_pos < old_first_free { - // We KNOW this position is filled - debug_assert!(locs[loc_idx]); if ins.outgoing { - ins.current_hand += 1; - locs.set(loc_idx, false); - if ins.belt_pos <= new_first_free { - *first_free_index = FreeIndex::FreeIndex(ins.belt_pos); - new_first_free = ins.belt_pos; + min_pos = None; + } else { + if let Some(min_pos) = &mut min_pos { + *min_pos = std::cmp::max(*min_pos, NonZero::new(ins.belt_pos).expect("Currently inserters at belt_pos 0 are unsupported, and should never be generated")); } - - if ins.current_hand == ins.max_hand_size { - true + } + *latest_inserter_pos_if_all_incoming = min_pos; + + let extract = if ins.belt_pos < old_first_free { + // We KNOW this position is filled + debug_assert!(locs[loc_idx]); + if ins.outgoing { + ins.current_hand += 1; + locs.set(loc_idx, false); + if ins.belt_pos <= new_first_free { + *first_free_index = FreeIndex::FreeIndex(ins.belt_pos); + new_first_free = ins.belt_pos; + } + + if ins.current_hand == ins.max_hand_size { + true + } else { + false + } } else { + #[cfg(feature = "debug-stat-gathering")] + NUM_INSERTER_LOADS_WAITING_FOR_SPACE_IN_GUARANTEED_FULL + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); false } } else { - #[cfg(feature = "debug-stat-gathering")] - NUM_INSERTER_LOADS_WAITING_FOR_SPACE_IN_GUARANTEED_FULL - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); - false - } - } else { - let mut loc = locs.get_mut(loc_idx).unwrap(); - - if ins.outgoing && *loc { - *loc = false; - ins.current_hand += 1; - if ins.current_hand == ins.max_hand_size { - true + let mut loc = locs.get_mut(loc_idx).unwrap(); + + if ins.outgoing && *loc { + *loc = false; + ins.current_hand += 1; + if ins.current_hand == ins.max_hand_size { + true + } else { + false + } + } else if !ins.outgoing && !*loc { + *loc = true; + ins.current_hand -= 1; + + if ins.belt_pos == new_first_free && *loc { + // This was the old first free pos + *first_free_index = FreeIndex::OldFreeIndex(ins.belt_pos); + } + + if ins.current_hand == 0 { true } else { false } } else { false } - } else if !ins.outgoing && !*loc { - *loc = true; - ins.current_hand -= 1; - - if ins.belt_pos == new_first_free && *loc { - // This was the old first free pos - *first_free_index = FreeIndex::OldFreeIndex(ins.belt_pos); - } + }; - if ins.current_hand == 0 { true } else { false } - } else { - false + #[cfg(feature = "debug-stat-gathering")] + if extract { + TIMES_INSERTERS_EXTRACTED.fetch_add(1, std::sync::atomic::Ordering::Relaxed); } - }; - - #[cfg(feature = "debug-stat-gathering")] - if extract { - TIMES_INSERTERS_EXTRACTED.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - } - extract + extract + }) + // TODO: When using a SmallCapVec, the we cannot lazily return the extract_if iterator, since the vec no longer exists after the closure returns + // This could be fixed using a custom extract_if implementation, but that is very hairy unsafe code I am unwilling to commit to for now + .collect_vec() }); Some(extracted) @@ -1032,15 +1055,17 @@ impl SmartBelt { let back_zero_index = usize::from(back_zero_index) % back_locs.len(); let mut new_inserters = front_inserters; - new_inserters - .inserters - .extend(back_inserters.inserters.drain(..).map(|mut back_ins| { - back_ins.belt_pos = back_ins - .belt_pos - .checked_add(front_len.try_into().unwrap()) - .unwrap(); - back_ins - })); + new_inserters.inserters.access_mut(|new| { + back_inserters.inserters.access_mut(|back| { + new.extend(back.drain(..).map(|mut back_ins| { + back_ins.belt_pos = back_ins + .belt_pos + .checked_add(front_len.try_into().unwrap()) + .unwrap(); + back_ins + })); + }); + }); let mut front_locs_vec = BitBox::from(front_locs).into_bitvec(); @@ -1138,12 +1163,14 @@ impl SmartBelt { }; if side == Side::FRONT { - for ins in &mut inserters.inserters { - ins.belt_pos = ins - .belt_pos - .checked_add(len) - .expect("Max length of belt (u16::MAX) reached"); - } + inserters.inserters.access_mut(|v| { + for ins in v { + ins.belt_pos = ins + .belt_pos + .checked_add(len) + .expect("Max length of belt (u16::MAX) reached"); + } + }); } let mut new = Self { @@ -1321,7 +1348,9 @@ impl EmptyBelt { first_free_index: FreeIndex::FreeIndex(0), zero_index: 0, locs: bitbox![0; self.len as usize].into(), - inserters: InserterStoreDyn { inserters: vec![] }, + inserters: InserterStoreDyn { + inserters: VecHolder::new(), + }, item, last_moving_spot: 0, input_splitter: self.input_splitter, @@ -1532,18 +1561,22 @@ impl Belt for SmartBelt { locs.into_boxed_bitslice().into() }); - self.inserters.inserters.retain(|inserter| { - if !kept_range.contains(&inserter.belt_pos) { - false - } else { - true - } + self.inserters.inserters.access_mut(|v| { + v.retain(|inserter| { + if !kept_range.contains(&inserter.belt_pos) { + false + } else { + true + } + }); }); if side == Side::FRONT { - for ins in &mut self.inserters.inserters { - ins.belt_pos = ins.belt_pos.checked_sub(amount).unwrap(); - } + self.inserters.inserters.access_mut(|v| { + for ins in v { + ins.belt_pos = ins.belt_pos.checked_sub(amount).unwrap(); + } + }); } self.first_free_index = FreeIndex::OldFreeIndex(0); diff --git a/src/belt/sushi.rs b/src/belt/sushi.rs index 0edc7ed..f8b6252 100644 --- a/src/belt/sushi.rs +++ b/src/belt/sushi.rs @@ -330,7 +330,7 @@ impl SushiBelt { crate::inserter::InserterState::EmptyAndMovingBack(_) => 0, }, } - }).collect(), + }).collect::>().try_into().unwrap(), }, item, diff --git a/src/inserter/belt_storage_movement_list.rs b/src/inserter/belt_storage_movement_list.rs index 22e007c..b293644 100644 --- a/src/inserter/belt_storage_movement_list.rs +++ b/src/inserter/belt_storage_movement_list.rs @@ -9,6 +9,7 @@ use crate::{ inserter::{FakeUnionStorage, belt_storage_inserter::Dir}, item::{ITEMCOUNTTYPE, IdxTrait}, storage_list::{SingleItemStorages, index_fake_union}, + temp_vec::VecHolder, }; #[cfg(feature = "client")] @@ -227,13 +228,15 @@ impl<'a> FinishedMovingLists<'a, { Dir::StorageToBelt }, { Dir::StorageToBelt }> reinsertion_list.reinsert(inserter.movetime.into(), *inserter); false } else { - belt.inserters.inserters.push(InserterExtractedWhenMoving { - storage: inserter.storage, - belt_pos: inserter.belt_pos, - movetime: inserter.movetime, - outgoing: false, - max_hand_size: inserter.max_hand_size, - current_hand, + belt.inserters.inserters.access_mut(|v| { + v.push(InserterExtractedWhenMoving { + storage: inserter.storage, + belt_pos: inserter.belt_pos, + movetime: inserter.movetime, + outgoing: false, + max_hand_size: inserter.max_hand_size, + current_hand, + }); }); // This is an incoming inserter if let Some(latest) = &mut belt.latest_inserter_pos_if_all_incoming { @@ -264,13 +267,15 @@ impl<'a> FinishedMovingLists<'a, { Dir::StorageToBelt }, { Dir::BeltToStorage }> reinsertion_list.reinsert(inserter.movetime.into(), *inserter); false } else { - belt.inserters.inserters.push(InserterExtractedWhenMoving { - storage: inserter.storage, - belt_pos: inserter.belt_pos, - movetime: inserter.movetime, - outgoing: true, - max_hand_size: inserter.max_hand_size, - current_hand, + belt.inserters.inserters.access_mut(|v| { + v.push(InserterExtractedWhenMoving { + storage: inserter.storage, + belt_pos: inserter.belt_pos, + movetime: inserter.movetime, + outgoing: true, + max_hand_size: inserter.max_hand_size, + current_hand, + }); }); // This is an outgoing inserter belt.latest_inserter_pos_if_all_incoming = None; diff --git a/src/lib.rs b/src/lib.rs index d05724f..691624a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,6 +79,7 @@ pub mod research; pub mod scenario; mod shopping_list_arena; +mod temp_vec; // This is an experiment. Before I can use it, I need to run it through a miri gauntlet // mod small_box_slice; diff --git a/src/main.rs b/src/main.rs index 7bca002..4975187 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ static ALLOC: dhat::Alloc = dhat::Alloc; #[cfg(not(feature = "dhat-heap"))] +#[cfg(not(debug_assertions))] #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; diff --git a/src/temp_vec.rs b/src/temp_vec.rs new file mode 100644 index 0000000..cbfc088 --- /dev/null +++ b/src/temp_vec.rs @@ -0,0 +1,245 @@ +use std::mem::ManuallyDrop; + +use egui_show_info::{EguiDisplayable, InfoExtractor, ShowInfo}; +use get_size2::GetSize; + +pub(crate) struct SmallCapVec { + ptr: *mut T, + len: u32, + capacity: u32, +} + +impl std::fmt::Debug for SmallCapVec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.access(|v| v.fmt(f)) + } +} + +impl Clone for SmallCapVec { + fn clone(&self) -> Self { + self.access(|v| v.clone()).try_into().unwrap() + } +} + +unsafe impl Send for SmallCapVec {} +// Send might be overkill here but I dont need it so better safe than sorry +unsafe impl Sync for SmallCapVec {} + +impl Drop for SmallCapVec { + fn drop(&mut self) { + let dropped_vec = + unsafe { Vec::from_raw_parts(self.ptr, self.len as usize, self.capacity as usize) }; + std::mem::drop(dropped_vec); + } +} + +impl VecHolder for SmallCapVec { + fn new() -> Self { + let (ptr, len, capacity) = Vec::new().into_raw_parts(); + + assert!( + u16::try_from(capacity).is_ok(), + "SmallCapVec capacity too large at creation" + ); + + Self { + ptr, + len: len.try_into().unwrap(), + capacity: capacity.try_into().unwrap(), + } + } + + #[inline(always)] + fn access(&self, action: impl FnOnce(&Vec) -> R) -> R { + // Wrapping the Vec in ManuallyDrop is needed to avoid a double drop if the user closure panics. + let temp_vec = ManuallyDrop::new(unsafe { + Vec::from_raw_parts(self.ptr, self.len as usize, self.capacity as usize) + }); + + let ret = action(&temp_vec); + + assert!( + u16::try_from(temp_vec.capacity()).is_ok(), + "SmallCapVec grew too large" + ); + + let (ptr, len, capacity) = ManuallyDrop::into_inner(temp_vec).into_raw_parts(); + assert!(ptr == self.ptr, "pointer changed with shared ref?"); + assert!(len == self.len as usize, "len changed with shared ref?"); + assert!( + capacity == self.capacity as usize, + "capacity changed with shared ref?" + ); + + ret + } + + #[inline(always)] + fn access_mut(&mut self, action: impl FnOnce(&mut Vec) -> R) -> R { + // Wrapping the Vec in ManuallyDrop is needed to avoid a double drop if the user closure panics. + let mut temp_vec = ManuallyDrop::new(unsafe { + Vec::from_raw_parts(self.ptr, self.len as usize, self.capacity as usize) + }); + + let ret = action(&mut temp_vec); + + assert!( + u16::try_from(temp_vec.capacity()).is_ok(), + "SmallCapVec grew too large" + ); + + let (ptr, len, capacity) = ManuallyDrop::into_inner(temp_vec).into_raw_parts(); + self.ptr = ptr; + self.len = len.try_into().unwrap(); + self.capacity = capacity.try_into().unwrap(); + + ret + } + + fn into_vec(self) -> Vec { + let Self { ptr, len, capacity } = self; + + unsafe { Vec::from_raw_parts(ptr, len.try_into().unwrap(), capacity.try_into().unwrap()) } + } +} + +impl std::ops::Deref for SmallCapVec { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + unsafe { core::slice::from_raw_parts(self.ptr, self.len as usize) } + } +} + +impl std::ops::DerefMut for SmallCapVec { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { core::slice::from_raw_parts_mut(self.ptr, self.len as usize) } + } +} + +impl TryFrom> for SmallCapVec { + type Error = >::Error; + + fn try_from(value: Vec) -> Result { + let (ptr, len, capacity) = value.into_raw_parts(); + + Ok(Self { + ptr, + len: len.try_into()?, + capacity: capacity.try_into()?, + }) + } +} + +impl VecHolder for Vec { + fn new() -> Self { + Vec::new() + } + + fn access(&self, action: impl FnOnce(&Vec) -> R) -> R { + action(self) + } + + fn access_mut(&mut self, action: impl FnOnce(&mut Vec) -> R) -> R { + action(self) + } + + fn into_vec(self) -> Vec { + self + } +} + +impl GetSize for SmallCapVec { + fn get_heap_size(&self) -> usize { + self.access(|v| v.get_heap_size()) + } +} + +impl, E: InfoExtractor, Info: EguiDisplayable> ShowInfo + for SmallCapVec +where + E: InfoExtractor, Info>, +{ + fn show_fields>( + &self, + extractor: &mut E, + ui: &mut egui::Ui, + path: String, + cache: &mut C, + ) { + // TODO: + } + + fn show_info>( + &self, + extractor: &mut E, + ui: &mut egui::Ui, + path: &str, + cache: &mut C, + ) { + let mut path = String::from(path); + path.push_str(std::any::type_name::()); + + let our_info = if let Some(cached) = cache.get(&path) { + cached + } else { + let new = extractor.extract_info(self); + cache.put(path.clone(), &new); + new + }; + + egui::CollapsingHeader::new(std::any::type_name::()) + .id_salt(&path) + .show(ui, |ui| { + our_info.show(ui); + self.show_fields(extractor, ui, path, cache); + }); + } +} +impl serde::Serialize for SmallCapVec { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.access(|v| v.serialize(serializer)) + } +} +impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for SmallCapVec { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let v = Vec::::deserialize(deserializer)?; + + // FIXME: + Ok(v.try_into().expect("SmallCapVec too long")) + } +} + +pub(crate) trait VecHolder { + fn new() -> Self; + fn access(&self, action: impl FnOnce(&Vec) -> R) -> R; + fn access_mut(&mut self, action: impl FnOnce(&mut Vec) -> R) -> R; + fn into_vec(self) -> Vec; +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[should_panic] + fn panic_during_access() { + let mut small = SmallCapVec::::new(); + + small.access_mut(|v| { + v.push(100); + }); + + small.access(|v| { + assert!(v.is_empty()); + }); + + std::mem::drop(small); + } +} From 4914c27f61a5745ba82114c45144c04db92cc4b0 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 6 Jan 2026 02:53:41 +0100 Subject: [PATCH 074/152] Switch to flake based nix distribution --- codium.nix | 29 --------------- flake.lock | 66 ++++++++++++++++++++++++++++++++++ flake.nix | 70 +++++++++++++++++++++++++++++++++++++ shell.nix | 28 --------------- src/rendering/eframe_app.rs | 5 +-- 5 files changed, 139 insertions(+), 59 deletions(-) delete mode 100644 codium.nix create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 shell.nix diff --git a/codium.nix b/codium.nix deleted file mode 100644 index f6ad7e0..0000000 --- a/codium.nix +++ /dev/null @@ -1,29 +0,0 @@ -let - nix-vscode-extensions.url = "github:nix-community/nix-vscode-extensions"; - pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60.tar.gz")) { }; - addr2linePkg = pkgs.callPackage ./addr2line-rs/default.nix {}; -in -pkgs.mkShell { - buildInputs = [ - ] ++ (with pkgs; [ - bacon - openssl - - (vscode-with-extensions.override { - vscode = vscodium; - vscodeExtensions = with vscode-extensions; [ - rust-lang.rust-analyzer - vadimcn.vscode-lldb - gruntfuggly.todo-tree - a5huynh.vscode-ron - ]; - }) - - addr2linePkg - ]); - RUST_BACKTRACE = 1; - - shellHook = '' - export PATH=${addr2linePkg}/bin:$PATH - ''; -} \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..eaf4e9e --- /dev/null +++ b/flake.lock @@ -0,0 +1,66 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1767596244, + "narHash": "sha256-P4NRZUjYbeuzv4hGrXxfdg0QpdGVoeNn0CMmzIyr398=", + "owner": "nix-community", + "repo": "fenix", + "rev": "eedfb5a27900e82ec0390acc83d4d226ce86e714", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1767379071, + "narHash": "sha256-EgE0pxsrW9jp9YFMkHL9JMXxcqi/OoumPJYwf+Okucw=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "fb7944c166a3b630f177938e478f0378e64ce108", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "fenix": "fenix", + "nixpkgs": "nixpkgs" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1767551763, + "narHash": "sha256-lcA/e3++3aZQSj6xCsBi2VpYyC3Q+oO/oukgfHiL+Ts=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "6a1246b69ca761480b9278df019f717b549cface", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d419b93 --- /dev/null +++ b/flake.nix @@ -0,0 +1,70 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, fenix }: let + pkgs = nixpkgs.legacyPackages."x86_64-linux"; + fenixLib = fenix.packages."x86_64-linux"; + + rustToolchain = fenixLib.fromToolchainFile { + file = ./rust-toolchain.toml; + sha256 = "sha256-dXoddWaPL6UtPscTpxMUMBDL83jFtqeDtmH/+bXBs3E="; + }; + + neededPackages = with pkgs; [ + wayland + xorg.libX11 + xorg.libXcursor + xorg.libXrandr + xorg.libXi + libxkbcommon + + openssl + + vulkan-headers vulkan-loader + ]; + in { + devShells."x86_64-linux".codium = pkgs.mkShell { + buildInputs = with pkgs; [ + rustToolchain + + perf + bacon + + (vscode-with-extensions.override { + vscode = vscodium; + vscodeExtensions = with vscode-extensions; [ + rust-lang.rust-analyzer + vadimcn.vscode-lldb + gruntfuggly.todo-tree + a5huynh.vscode-ron + ]; + }) + ] ++ neededPackages; + LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${builtins.toString (pkgs.lib.makeLibraryPath neededPackages)}"; + # env.RUST_SRC_PATH = "${rustToolchain.rust-src}"; + }; + + packages."x86_64-linux".default = (pkgs.makeRustPlatform { + cargo = rustToolchain; + rustc = rustToolchain; + }).buildRustPackage { + name = "factory"; + src = ./.; + buildInputs = neededPackages; + nativeBuildInputs = [ pkgs.pkg-config pkgs.makeWrapper ]; + cargoHash = "sha256-Wgk3H7hw9yeMt8juVuLVBX+Z8JnNxD1+e9LY0CTQr0E="; + # cargoLock.lockFile = ./Cargo.lock; + doCheck = false; + + postInstall = '' + wrapProgram "$out/bin/factory" --prefix LD_LIBRARY_PATH : "${builtins.toString (pkgs.lib.makeLibraryPath neededPackages)}" + ''; + }; + }; +} diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 8d16ff1..0000000 --- a/shell.nix +++ /dev/null @@ -1,28 +0,0 @@ -let - pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60.tar.gz")) { overlays = [ ]; }; - buildInputs = [ - ] ++ (with pkgs; [ - rustup - - pkg-config - - # perf for cargo-flamegraph - linuxPackages_latest.perf - - wayland - xorg.libX11 - xorg.libXcursor - xorg.libXrandr - xorg.libXi - libxkbcommon - - openssl - - vulkan-headers vulkan-loader - ]); -in -pkgs.mkShell { - inherit buildInputs; - RUST_BACKTRACE = 1; - LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${builtins.toString (pkgs.lib.makeLibraryPath buildInputs)}"; -} diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index ad3e8fe..bf9de9e 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -188,7 +188,7 @@ impl eframe::App for App { row.col(|ui| { let dur = chrono::Duration::from_std(file.stored.playtime) - .unwrap(); + .expect("Could not transform playtime to chrono duration"); ui.label(format!( "{:02}:{:02}:{:02}", dur.num_hours(), @@ -373,7 +373,8 @@ impl eframe::App for App { if crate::built_info::GIT_HEAD_REF == Some("refs/head/master") { ui.label(crate::built_info::PKG_VERSION); } else { - ui.label(crate::built_info::GIT_VERSION.unwrap()); + let version = crate::built_info::GIT_VERSION.unwrap_or("Could not get git version"); + ui.label(version); } ui.end_row(); From 3dd765f08b68ef6889cb1934e87b1ff21bbc3980 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 6 Jan 2026 03:02:54 +0100 Subject: [PATCH 075/152] Do not panic if the build script did not run --- src/rendering/eframe_app.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index bf9de9e..e2cb2ff 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -186,9 +186,12 @@ impl eframe::App for App { )); }); row.col(|ui| { - let dur = - chrono::Duration::from_std(file.stored.playtime) - .expect("Could not transform playtime to chrono duration"); + let dur = chrono::Duration::from_std( + file.stored.playtime, + ) + .expect( + "Could not transform playtime to chrono duration", + ); ui.label(format!( "{:02}:{:02}:{:02}", dur.num_hours(), @@ -373,7 +376,8 @@ impl eframe::App for App { if crate::built_info::GIT_HEAD_REF == Some("refs/head/master") { ui.label(crate::built_info::PKG_VERSION); } else { - let version = crate::built_info::GIT_VERSION.unwrap_or("Could not get git version"); + let version = crate::built_info::GIT_VERSION + .unwrap_or("Could not get git version"); ui.label(version); } ui.end_row(); From 4c93e1a4ad434da1cf6e36f723352e00e0a886b9 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 6 Jan 2026 04:11:44 +0100 Subject: [PATCH 076/152] Fix terminal in codium --- flake.lock | 8 ++++---- flake.nix | 9 ++++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index eaf4e9e..185d316 100644 --- a/flake.lock +++ b/flake.lock @@ -23,16 +23,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1767379071, - "narHash": "sha256-EgE0pxsrW9jp9YFMkHL9JMXxcqi/OoumPJYwf+Okucw=", + "lastModified": 1762943920, + "narHash": "sha256-ITeH8GBpQTw9457ICZBddQEBjlXMmilML067q0e6vqY=", "owner": "nixos", "repo": "nixpkgs", - "rev": "fb7944c166a3b630f177938e478f0378e64ce108", + "rev": "91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixos-unstable", + "ref": "91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index d419b93..770a7d0 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,6 @@ { inputs = { - nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + nixpkgs.url = "github:nixos/nixpkgs?ref=91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60"; fenix = { url = "github:nix-community/fenix"; inputs.nixpkgs.follows = "nixpkgs"; @@ -31,6 +31,7 @@ in { devShells."x86_64-linux".codium = pkgs.mkShell { buildInputs = with pkgs; [ + bashInteractive rustToolchain perf @@ -47,6 +48,12 @@ }) ] ++ neededPackages; LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${builtins.toString (pkgs.lib.makeLibraryPath neededPackages)}"; + + shellHook = '' + export SHELL="${pkgs.bashInteractive}/bin/bash" + ''; + + # env.RUST_SRC_PATH = "${rustToolchain.rust-src}"; }; From afe0b07caf23bfcfaac317bceafa49c1331fd11e Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 6 Jan 2026 06:13:55 +0100 Subject: [PATCH 077/152] Normalize scroll speed no matter if lines or units are used --- src/frontend/action/action_state_machine.rs | 2 +- src/frontend/input.rs | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index 54859bf..361d5ef 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -623,7 +623,7 @@ impl let Position {x: mouse_x, y: mouse_y} = Self::player_mouse_to_tile(self.zoom_level, self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos); match delta { (_, y) => { - self.zoom_level -= y as f32 / 10.0 * self.mouse_wheel_sensitivity; + self.zoom_level -= y as f32 * 4.0 * self.mouse_wheel_sensitivity; }, } if let Some(view_center) = &mut self.map_view_info { diff --git a/src/frontend/input.rs b/src/frontend/input.rs index b763d2c..85bd02e 100644 --- a/src/frontend/input.rs +++ b/src/frontend/input.rs @@ -72,10 +72,24 @@ impl TryFrom for Input { modifiers: _modifiers, } => match unit { eframe::egui::MouseWheelUnit::Point => { + dbg!(delta); Ok(Input::MouseScoll((delta.x as f64, delta.y as f64))) }, eframe::egui::MouseWheelUnit::Line => { - Ok(Input::MouseScoll((delta.x as f64, delta.y as f64))) + dbg!(delta); + // TODO: This is hardcoded to the default of egui + // See InputOptions::default and https://github.com/emilk/egui/issues/461 + let units_per_line = if cfg!(all(target_arch = "wasm32", target_os = "unknown")) + { + 8.0 + } else { + 40.0 + }; + + Ok(Input::MouseScoll(( + delta.x as f64 / units_per_line, + delta.y as f64 / units_per_line, + ))) }, eframe::egui::MouseWheelUnit::Page => Err(()), }, From b540979111a8cf581f1a185f114a6d1423d1f35d Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 6 Jan 2026 06:14:15 +0100 Subject: [PATCH 078/152] Add ActionStateMachine::new_from_gamestate --- src/frontend/action/action_state_machine.rs | 59 ++++++++++++++++++++- src/lib.rs | 28 +++++----- 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index 361d5ef..c5cd318 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -233,7 +233,7 @@ impl ActionStateMachine { #[must_use] - pub fn new( + fn new( my_player_id: PLAYERID, local_player_pos: (f32, f32), data_store: &DataStore, @@ -286,6 +286,63 @@ impl } } + #[must_use] + pub fn new_from_gamestate( + my_player_id: PLAYERID, + world: &World, + sim_state: &SimulationState, + data_store: &DataStore, + ) -> Self { + let player_pos = world.players[my_player_id as usize].pos; + + Self { + my_player_id, + local_player_pos: player_pos, + + tech_tree_render: None, + + statistics_panel_open: false, + technology_panel_open: true, + statistics_panel: StatisticsPanel::default(), + statistics_panel_locked_scale: false, + production_filters: vec![true; data_store.item_display_names.len()], + consumption_filters: vec![true; data_store.item_display_names.len()], + + current_mouse_pos: (0.0, 0.0), + current_held_keys: HashSet::new(), + state: ActionStateMachineState::Idle, + + zoom_level: 1.0, + map_view_info: None, + + escape_menu_open: false, + debug_view_options: DebugViewOptions { + highlight_sushi_belts: false, + sushi_belt_len_threshhold: 1, + + sushi_finder_view_lock: None, + }, + + copy_info: None, + + show_graph_dot_output: false, + + recipe: PhantomData, + + get_size_cache: HashMap::new(), + + mouse_wheel_sensitivity: 1.0, + + current_fork_save_in_progress: None, + + hotbar: Hotbar::new(data_store), + hotbar_window_open: true, + + last_tick_seen_for_autosave: 0, + autosave_interval: (60 * TICKS_PER_SECOND_LOGIC) as u32, + } + } + #[allow(clippy::too_many_lines)] pub fn handle_inputs<'a, 'b, 'c, 'd, 'e, I: IntoIterator + 'b>( &'a mut self, diff --git a/src/lib.rs b/src/lib.rs index 691624a..b8e63a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -305,12 +305,6 @@ fn run_integrated_server( match data_store { data::DataStoreOptions::ItemU8RecipeU8(data_store) => { let (send, recv) = channel(); - let state_machine: Arc>> = - Arc::new(Mutex::new(ActionStateMachine::new( - 0, - (100.0 * CHUNK_SIZE_FLOAT, 100.0 * CHUNK_SIZE_FLOAT), - &data_store, - ))); let game_state = Arc::new(match start_game_info { StartGameInfo::Load(path) => load(path) @@ -375,6 +369,14 @@ fn run_integrated_server( }, }); + let state_machine: Arc>> = + Arc::new(Mutex::new(ActionStateMachine::new_from_gamestate( + 0, + &*game_state.world.lock(), + &*game_state.simulation_state.lock(), + &data_store, + ))); + let (ui_sender, ui_recv) = channel(); let mut game = Game::new( @@ -488,18 +490,20 @@ fn run_client(remote_addr: SocketAddr) -> (LoadedGame, Arc, Sender { let (send, recv) = channel(); - let state_machine: Arc>> = - Arc::new(Mutex::new(ActionStateMachine::new( - 1, - (100.0 * CHUNK_SIZE_FLOAT, 100.0 * CHUNK_SIZE_FLOAT), - &data_store, - ))); let game_state = Arc::new( // FIXME: When running in client mode, we should download the gamestate from the server instead of loading it from disk GameState::new("FIXME".to_string(), &data_store), ); + let state_machine: Arc>> = + Arc::new(Mutex::new(ActionStateMachine::new_from_gamestate( + 1, + &*game_state.world.lock(), + &*game_state.simulation_state.lock(), + &data_store, + ))); + let (ui_sender, ui_recv) = channel(); let mut game = Game::new( From 0b70a76e63e1179dc5081205a764cf71c33af249 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 6 Jan 2026 06:15:43 +0100 Subject: [PATCH 079/152] Remove dbg! printing --- src/frontend/input.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/frontend/input.rs b/src/frontend/input.rs index 85bd02e..0b2580c 100644 --- a/src/frontend/input.rs +++ b/src/frontend/input.rs @@ -72,11 +72,9 @@ impl TryFrom for Input { modifiers: _modifiers, } => match unit { eframe::egui::MouseWheelUnit::Point => { - dbg!(delta); Ok(Input::MouseScoll((delta.x as f64, delta.y as f64))) }, eframe::egui::MouseWheelUnit::Line => { - dbg!(delta); // TODO: This is hardcoded to the default of egui // See InputOptions::default and https://github.com/emilk/egui/issues/461 let units_per_line = if cfg!(all(target_arch = "wasm32", target_os = "unknown")) From aeca48453e4a5a60cfbd180dd1c638b40b4a44a7 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 6 Jan 2026 06:37:04 +0100 Subject: [PATCH 080/152] Update map view when placing ore using the editor --- src/app_state.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app_state.rs b/src/app_state.rs index 8f4c12b..95c3c81 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -2858,6 +2858,11 @@ impl GameState { game_state.world.ore_lookup.add_ore(*pos, *ore, *amount); + game_state + .world + .map_updates + .get_or_insert_default() + .push(*pos); }, } From 06830a206974c5e55052daaea6e179fa3518a6a7 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 6 Jan 2026 07:00:37 +0100 Subject: [PATCH 081/152] Fix dedicated server compile --- Cargo.toml | 2 +- flake.nix | 34 +++++++++++++++++++--------------- result | 1 + src/data/mod.rs | 2 +- src/item.rs | 1 + src/lib.rs | 3 +-- src/temp_vec.rs | 4 ++++ 7 files changed, 28 insertions(+), 19 deletions(-) create mode 120000 result diff --git a/Cargo.toml b/Cargo.toml index e9e9a6a..d212f84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ image = { version = "0.25.5", optional = true } log = "0.4.25" simple_logger = {version = "5.0.0", optional = true } rayon = "1.10.0" -serde = { version = "1.0.217", features = ["derive"], default-features = false } +serde = { version = "1.0.217", features = ["derive", "rc"] } directories = "6.0.0" ron = "0.8.1" take_mut = "0.2.2" diff --git a/flake.nix b/flake.nix index 770a7d0..efd1132 100644 --- a/flake.nix +++ b/flake.nix @@ -28,6 +28,23 @@ vulkan-headers vulkan-loader ]; + + client_package = (pkgs.makeRustPlatform { + cargo = rustToolchain; + rustc = rustToolchain; + }).buildRustPackage { + name = "factory"; + src = ./.; + buildInputs = neededPackages; + nativeBuildInputs = [ pkgs.pkg-config pkgs.makeWrapper ]; + cargoHash = "sha256-Wgk3H7hw9yeMt8juVuLVBX+Z8JnNxD1+e9LY0CTQr0E="; + # cargoLock.lockFile = ./Cargo.lock; + doCheck = false; + + postInstall = '' + wrapProgram "$out/bin/factory" --prefix LD_LIBRARY_PATH : "${builtins.toString (pkgs.lib.makeLibraryPath neededPackages)}" + ''; + }; in { devShells."x86_64-linux".codium = pkgs.mkShell { buildInputs = with pkgs; [ @@ -57,21 +74,8 @@ # env.RUST_SRC_PATH = "${rustToolchain.rust-src}"; }; - packages."x86_64-linux".default = (pkgs.makeRustPlatform { - cargo = rustToolchain; - rustc = rustToolchain; - }).buildRustPackage { - name = "factory"; - src = ./.; - buildInputs = neededPackages; - nativeBuildInputs = [ pkgs.pkg-config pkgs.makeWrapper ]; - cargoHash = "sha256-Wgk3H7hw9yeMt8juVuLVBX+Z8JnNxD1+e9LY0CTQr0E="; - # cargoLock.lockFile = ./Cargo.lock; - doCheck = false; + packages."x86_64-linux".default = client_package; - postInstall = '' - wrapProgram "$out/bin/factory" --prefix LD_LIBRARY_PATH : "${builtins.toString (pkgs.lib.makeLibraryPath neededPackages)}" - ''; - }; + packages."x86_64-linux".dedicated_server = client_package.overrideAttrs ( oldAttrs: { cargoBuildFlags = [ "--no-default-features" ]; }); }; } diff --git a/result b/result new file mode 120000 index 0000000..9671db7 --- /dev/null +++ b/result @@ -0,0 +1 @@ +/nix/store/llg8qb1mscj1ym4sfs6js5bihsnixnsw-factory \ No newline at end of file diff --git a/src/data/mod.rs b/src/data/mod.rs index 75a2609..650d93c 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -742,7 +742,7 @@ impl FluidConnection { } } -#[derive(Debug, Clone, serde::Serialize, serde:: Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PowerPoleData { pub name: Arc, pub display_name: String, diff --git a/src/item.rs b/src/item.rs index 0de4a60..db18e7b 100644 --- a/src/item.rs +++ b/src/item.rs @@ -47,6 +47,7 @@ pub trait WeakIdxTrait: + Hash + Ord + 'static + + Debug { } diff --git a/src/lib.rs b/src/lib.rs index b8e63a3..07293a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,6 @@ pub mod built_info { #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] use eframe::web_sys; -#[cfg(feature = "client")] use std::{ borrow::Borrow, net::{SocketAddr, TcpStream}, @@ -193,7 +192,7 @@ pub fn main() -> Result<(), ()> { #[cfg(not(feature = "client"))] { - info!("Running Dedicated server!"); + log::info!("Running Dedicated server!"); // let dir = ProjectDirs::from("de", "aschhoff", "factory_game").expect("No Home path found"); // let save_file_dir = dir.data_dir().join("save.save"); // run_dedicated_server(StartGameInfo::Load(save_file_dir)); diff --git a/src/temp_vec.rs b/src/temp_vec.rs index cbfc088..5b0ef59 100644 --- a/src/temp_vec.rs +++ b/src/temp_vec.rs @@ -1,6 +1,8 @@ use std::mem::ManuallyDrop; +#[cfg(feature = "client")] use egui_show_info::{EguiDisplayable, InfoExtractor, ShowInfo}; +#[cfg(feature = "client")] use get_size2::GetSize; pub(crate) struct SmallCapVec { @@ -149,12 +151,14 @@ impl VecHolder for Vec { } } +#[cfg(feature = "client")] impl GetSize for SmallCapVec { fn get_heap_size(&self) -> usize { self.access(|v| v.get_heap_size()) } } +#[cfg(feature = "client")] impl, E: InfoExtractor, Info: EguiDisplayable> ShowInfo for SmallCapVec where From 0c2feacff43dab5933405b5c8eeb9d876c6fe605 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 11 Jan 2026 10:15:19 +0100 Subject: [PATCH 082/152] Fix web compile and add new Create game ui --- Cargo.lock | 31 ++ Cargo.toml | 16 +- flake.nix | 2 +- index.html | 137 +++++++++ result | 1 - src/app_state.rs | 60 +--- src/blueprint/mod.rs | 20 ++ src/example_worlds/mod.rs | 273 +++++++++++++++++ src/frontend/action/action_state_machine.rs | 5 + src/frontend/input.rs | 3 +- src/lib.rs | 228 +++++++++----- src/main.rs | 18 +- src/multiplayer/bad_tcp.rs | 8 + src/multiplayer/mod.rs | 1 + src/multiplayer/plumbing.rs | 17 +- src/multiplayer/server.rs | 30 +- src/rendering/eframe_app.rs | 323 +++----------------- src/rendering/render_world.rs | 63 ++-- src/saving/mod.rs | 29 +- 19 files changed, 792 insertions(+), 473 deletions(-) create mode 100644 index.html delete mode 120000 result create mode 100644 src/example_worlds/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 21c3f29..a59bb79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -262,6 +262,16 @@ dependencies = [ "syn 2.0.113", ] +[[package]] +name = "args" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b7432c65177b8d5c032d56e020dd8d407e939468479fc8c300e2d93e6d970b" +dependencies = [ + "getopts", + "log", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -1072,6 +1082,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1740,6 +1760,7 @@ dependencies = [ name = "factory" version = "0.2.1" dependencies = [ + "args", "base64 0.22.1", "bimap", "bincode 2.0.1", @@ -1749,6 +1770,7 @@ dependencies = [ "bytemuck", "charts-rs", "chrono", + "console_error_panic_hook", "dhat", "directories", "ecolor", @@ -2160,6 +2182,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.16" diff --git a/Cargo.toml b/Cargo.toml index d212f84..d18ae75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,17 +66,22 @@ bincode = { version = "2.0.1", features = ["serde"] } thin-dst = "1.1.0" stable-vec = "0.4.1" recycle_vec = "1.1.2" -fork = "0.3.0" -libc = { version = "0.2.177", default-features = false } -interprocess = "2.2.3" fixed-buffer = "1.0.2" base64 = "0.22.1" mimalloc = { version = "0.1.48", features = ["v3"] } rustc-hash = "2.1.1" chrono = { version = "0.4.42", features = ["serde"] } rand_xoshiro = "0.7.0" -open = "5.3.3" url = "2.5.7" +args = "2.2.0" +console_error_panic_hook = "0.1.7" + +# These are all the dependencies which do not work on wasm +[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] +libc = { version = "0.2.177", default-features = false } +interprocess = { version = "2.2.3" } +fork = "0.3.0" +open = "5.3.3" [build-dependencies] built = {version = "0.8", features= ["git2", "chrono"]} @@ -105,7 +110,6 @@ match_same_arms = { level = "deny", priority = -1 } redundant_closure_for_method_calls = { level = "allow", priority = 1 } suboptimal_flops = { level = "allow", priority = 1 } module_name_repetitions = { level = "allow", priority = 1 } -# lto = true [profile.release-with-debug] inherits = "release" @@ -138,4 +142,4 @@ profiler = ["profiling/profile-with-puffin"] client = [ "dep:eframe", "dep:egui", "dep:egui_extras", "dep:egui_plot", "dep:puffin_egui", "dep:egui_graphs", "dep:egui-show-info", "dep:egui-show-info-derive", "dep:tilelib", "dep:image", "dep:rfd", "dep:get-size2"] logging = ["simple_logger"] debug-stat-gathering = [] -assembler-craft-tracking = [] \ No newline at end of file +assembler-craft-tracking = [] diff --git a/flake.nix b/flake.nix index efd1132..79546bd 100644 --- a/flake.nix +++ b/flake.nix @@ -37,7 +37,7 @@ src = ./.; buildInputs = neededPackages; nativeBuildInputs = [ pkgs.pkg-config pkgs.makeWrapper ]; - cargoHash = "sha256-Wgk3H7hw9yeMt8juVuLVBX+Z8JnNxD1+e9LY0CTQr0E="; + cargoHash = "sha256-TV+y1lHUmBJFBlgv2KBjQEXDLVqHNkgZsIXN2P3972k="; # cargoLock.lockFile = ./Cargo.lock; doCheck = false; diff --git a/index.html b/index.html new file mode 100644 index 0000000..03dca5c --- /dev/null +++ b/index.html @@ -0,0 +1,137 @@ + + + + + + + + + + FactoryGame + + + + + + + + + + + + + + + + + + + +
+ +

+ Loading… +

+
+
+ + + + + + + + + diff --git a/result b/result deleted file mode 120000 index 9671db7..0000000 --- a/result +++ /dev/null @@ -1 +0,0 @@ -/nix/store/llg8qb1mscj1ym4sfs6js5bihsnixnsw-factory \ No newline at end of file diff --git a/src/app_state.rs b/src/app_state.rs index 95c3c81..c5f6c44 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -9,6 +9,7 @@ use crate::frontend::action::place_entity::PlaceEntityInfo; use crate::frontend::world::tile::CHUNK_SIZE; use crate::frontend::world::tile::ModuleSlots; use crate::frontend::world::tile::ModuleTy; +use crate::get_const_string; use crate::inserter::InserterStateInfo; use crate::inserter::WaitlistSearchSide; use crate::inserter::belt_storage_inserter; @@ -110,6 +111,7 @@ pub struct AuxillaryData { pub update_round_trip_times: Timeline, pub update_times: Timeline, + #[get_size(ignore)] #[serde(skip)] last_update_time: Option, @@ -206,10 +208,7 @@ impl GameState GameState, - data_store: &DataStore, - ) -> Self { - let mut ret = GameState::new_with_world_area( - name, - Position { x: 0, y: 0 }, - Position { x: 10000, y: 20000 }, - data_store, - ); - - let file = File::open("test_blueprints/red_sci_with_beacons_and_belts.bp").unwrap(); - let bp: Blueprint = ron::de::from_reader(file).unwrap(); - let bp = bp.get_reusable(false, data_store); - - let y_range = (0..20_000).step_by(6_000); - - let total = y_range.size_hint().0; - - let mut current = 0; - - puffin::set_scopes_on(false); - for y_start in y_range { - progress.store((current as f64 / total as f64).to_bits(), Ordering::Relaxed); - current += 1; - for y_pos in (1590..6000).step_by(10) { - for x_pos in (1590..4000).step_by(60) { - bp.apply( - Position { - x: x_pos, - y: y_start + y_pos, - }, - &mut ret, - data_store, - ); - } - } - } - puffin::set_scopes_on(true); - - ret - } - #[must_use] pub fn new_with_lots_of_belts( name: String, @@ -421,9 +375,7 @@ impl GameState GameState, data_store: &DataStore, ) { - let mut file = File::open("test_blueprints/solar_tile.bp").unwrap(); - let mut s = String::new(); - file.read_to_string(&mut s).unwrap(); + let s = get_const_string!("test_blueprints/solar_tile.bp"); let bp: BlueprintString = BlueprintString(s); let mut bp: Blueprint = Blueprint::try_from(bp).unwrap(); bp.optimize(); diff --git a/src/blueprint/mod.rs b/src/blueprint/mod.rs index e27dee6..de64342 100644 --- a/src/blueprint/mod.rs +++ b/src/blueprint/mod.rs @@ -1798,3 +1798,23 @@ pub(crate) mod test { }) } } + +#[macro_export] +macro_rules! get_const_string { + ($path:literal) => {{ + #[cfg(target_arch = "wasm32")] + { + include_str!(concat!("../", $path)).to_string() + } + + #[cfg(not(target_arch = "wasm32"))] + { + let mut s = String::new(); + let mut file = File::open($path).expect(&format!("Failed to open bp file {}", $path)); + + file.read_to_string(&mut s) + .expect(&format!("Failed reading from bp file {}", $path)); + s + } + }}; +} diff --git a/src/example_worlds/mod.rs b/src/example_worlds/mod.rs new file mode 100644 index 0000000..7670126 --- /dev/null +++ b/src/example_worlds/mod.rs @@ -0,0 +1,273 @@ +use std::{ + ops::RangeInclusive, + sync::{LazyLock, atomic::AtomicU64}, +}; + +use egui::Button; + +use crate::{app_state::GameState, data::DataStore, frontend::world::Position, power::Watt}; + +pub(crate) struct WorldValueStore { + name_field: String, + worlds: Vec>, +} + +impl Default for WorldValueStore { + fn default() -> Self { + let v = WORLDS + .iter() + .map(|world| { + world + .values + .iter() + .map(|value| value.default.clone()) + .collect() + }) + .collect(); + + WorldValueStore { + name_field: "New World".to_string(), + worlds: v, + } + } +} + +pub(crate) fn list_example_worlds( + values: &mut WorldValueStore, + ui: &mut egui::Ui, +) -> Option, &DataStore) -> GameState + 'static> +{ + ui.horizontal(|ui| { + ui.label("World Name:"); + ui.text_edit_singleline(&mut values.name_field); + }); + + for (world, world_values) in WORLDS.iter().zip(values.worlds.iter_mut()) { + let v = ui.horizontal(|ui| { + ui.label(world.name); + ui.label(world.description); + + for (desc, value) in world.values.iter().zip(world_values.iter_mut()) { + ui.vertical(|ui| { + ui.label(desc.name); + match &desc.kind { + ValueKind::Range { allowed, log } => { + let ValueValue::Range(value) = value else { + unreachable!(); + }; + + ui.add( + egui::Slider::new(value, allowed.clone()) + .logarithmic(*log) + .text(desc.name), + ); + }, + ValueKind::Toggle {} => { + let ValueValue::Toggle(value) = value else { + unreachable!(); + }; + + ui.checkbox(value, ()); + }, + } + }); + } + + let allowed = if cfg!(target_arch = "wasm32") { + (world.allowed_on_wasm)(world_values) == AllowedOnWasm::True + } else { + true + }; + + let disabled_str = if cfg!(target_arch = "wasm32") { + match (world.allowed_on_wasm)(world_values) { + AllowedOnWasm::True => "", + AllowedOnWasm::False(s) => s.unwrap_or("Not available on WASM"), + } + } else { + "" + }; + + if ui + .add_enabled(allowed, Button::new("Create")) + .on_disabled_hover_text(disabled_str) + .clicked() + { + let world_values = world_values.clone(); + let name = values.name_field.clone(); + let fun = world.creation_fn; + Some(move |progress, data_store: &'_ DataStore| { + (fun)(name, progress, &world_values, data_store) + }) + } else { + None + } + }); + + if let Some(v) = v.inner { + return Some(v); + } + + ui.separator(); + } + + None +} + +struct ExampleWorld { + name: &'static str, + description: &'static str, + values: Vec, + + // TODO: I might want to change this to depend on the values + allowed_on_wasm: fn(&[ValueValue]) -> AllowedOnWasm, + creation_fn: fn( + String, + std::sync::Arc, + &[ValueValue], + &DataStore, + ) -> GameState, +} + +#[derive(Debug, PartialEq)] +enum AllowedOnWasm { + True, + False(Option<&'static str>), +} + +struct WorldValue { + name: &'static str, + kind: ValueKind, + default: ValueValue, +} + +enum ValueKind { + Range { + allowed: RangeInclusive, + log: bool, + }, + Toggle {}, +} + +// FIXME: Naming??? +#[derive(Clone)] +enum ValueValue { + Range(usize), + Toggle(bool), +} + +const WORLDS: LazyLock<[ExampleWorld; 5]> = LazyLock::new(|| { + [ + ExampleWorld { + name: "Empty World", + description: "Completely Empty", + values: vec![], + + allowed_on_wasm: |_| AllowedOnWasm::True, + + creation_fn: |name, _, _, data_store| GameState::new(name, data_store), + }, + ExampleWorld { + name: "Megabase", + description: "A world consisting of a 40k SPM Megabase designed by Smurphy", + values: vec![WorldValue { + name: "Generate Solar Field", + kind: ValueKind::Toggle {}, + default: ValueValue::Toggle(false), + }], + + allowed_on_wasm: |_| AllowedOnWasm::True, + + creation_fn: |name, progress, values, data_store| { + let [ValueValue::Toggle(use_solar_field)] = values else { + unreachable!(); + }; + + GameState::new_with_megabase(name, *use_solar_field, progress, data_store) + }, + }, + ExampleWorld { + name: "Gigabase", + description: "A world consisting of multiple copies of a 40k SPM Megabase", + values: vec![WorldValue { + name: "Megabase Count", + kind: ValueKind::Range { + allowed: 0..=1000, + log: true, + }, + default: ValueValue::Range(40), + }], + + allowed_on_wasm: |_| { + AllowedOnWasm::False(Some( + "WASM does not support enough memory to run a gigabase", + )) + }, + + creation_fn: |name, progress, values, data_store| { + let [ValueValue::Range(count)] = values else { + unreachable!(); + }; + + GameState::new_with_gigabase( + name, + (*count).try_into().unwrap(), + progress, + data_store, + ) + }, + }, + ExampleWorld { + name: "Trip around the world", + description: "A small ring around the world", + values: vec![], + allowed_on_wasm: |_| AllowedOnWasm::True, + creation_fn: |name, progress, _values, data_store| { + GameState::new_with_world_train_ride(name, progress, data_store) + }, + }, + ExampleWorld { + name: "Solar Field", + description: "A world containing a Solar Field", + values: vec![WorldValue { + name: "Panel Count", + kind: ValueKind::Range { + allowed: 1..=1_000_000_000, + log: true, + }, + default: ValueValue::Range(1_000_000), + }], + allowed_on_wasm: |values| { + let [ValueValue::Range(count)] = values else { + unreachable!(); + }; + + // Testing said ~100 bytes per solar panel + let expected_size = *count * 100 + 1_000_000_000; + + // Wasm only has 4GB of RAM + if expected_size > 4_000_000_000 { + AllowedOnWasm::False(Some( + "Generated World Size would exceed maximum memory on WASM, consider reducing panel count", + )) + } else { + AllowedOnWasm::True + } + }, + creation_fn: |name, progress, values, data_store| { + let [ValueValue::Range(count)] = values else { + unreachable!(); + }; + + GameState::new_with_tons_of_solar( + name, + Watt(42_000) * (*count) as u64, + Position { x: 1_600, y: 1_600 }, + None, + progress, + data_store, + ) + }, + }, + ] +}); diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index c5cd318..f0a9e76 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -151,6 +151,8 @@ pub struct ActionStateMachine, pub hotbar: Hotbar, @@ -160,6 +162,7 @@ pub struct ActionStateMachine mouse_wheel_sensitivity: 1.0, + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] current_fork_save_in_progress: None, hotbar: Hotbar::new(data_store), @@ -333,6 +337,7 @@ impl mouse_wheel_sensitivity: 1.0, + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] current_fork_save_in_progress: None, hotbar: Hotbar::new(data_store), diff --git a/src/frontend/input.rs b/src/frontend/input.rs index 0b2580c..743d6a9 100644 --- a/src/frontend/input.rs +++ b/src/frontend/input.rs @@ -79,7 +79,8 @@ impl TryFrom for Input { // See InputOptions::default and https://github.com/emilk/egui/issues/461 let units_per_line = if cfg!(all(target_arch = "wasm32", target_os = "unknown")) { - 8.0 + // FIXME: Why does it seem like I need to use 40 here, if egui uses 8 on wasm???? + 40.0 } else { 40.0 }; diff --git a/src/lib.rs b/src/lib.rs index 07293a5..77718e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ extern crate test; +// FIXME: I believe this does not work in nix packages, since build scripts do not work, in order to keep the build reproducable // See https://docs.rs/built/latest/built/ pub mod built_info { // The file has been placed there by the build script. @@ -52,9 +53,6 @@ use rendering::{ window::{LoadedGame, LoadedGameSized}, }; -#[cfg(not(feature = "client"))] -use directories::ProjectDirs; - use saving::{load, load_readable}; use std::path::PathBuf; @@ -77,6 +75,8 @@ pub mod research; pub mod scenario; +mod example_worlds; + mod shopping_list_arena; mod temp_vec; @@ -153,7 +153,7 @@ impl NewWithDataStore for T { } #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] -pub fn main() -> Result<(), ()> { +pub fn main(input: &Vec) -> Result<(), args::ArgsError> { // use ron::ser::PrettyConfig; // let raw = crate::data::factorio_1_1::get_raw_data_fn(); @@ -170,6 +170,7 @@ pub fn main() -> Result<(), ()> { .env() .init() .unwrap(); + log::info!("Welcome to main on native"); #[cfg(feature = "client")] { @@ -192,25 +193,56 @@ pub fn main() -> Result<(), ()> { #[cfg(not(feature = "client"))] { - log::info!("Running Dedicated server!"); - // let dir = ProjectDirs::from("de", "aschhoff", "factory_game").expect("No Home path found"); - // let save_file_dir = dir.data_dir().join("save.save"); - // run_dedicated_server(StartGameInfo::Load(save_file_dir)); - run_dedicated_server(StartGameInfo::Load( - "/home/tim/.local/share/factory_game/save.save" - .try_into() - .unwrap(), - )); + use crate::saving::save_folder; + + let mut args = args::Args::new("factory", "FactoryGame dedicated server"); + + args.flag("h", "help", "Print the usage menu"); + args.flag("c", "create", "Create a new world"); + args.flag( + "o", + "override", + "Allows overriding an existing world when creating world", + ); + + args.parse(input)?; + + let help = args.value_of("help")?; + if help { + args.full_usage(); + return Ok(()); + } + + let create = args.value_of("create")?; + let overwrite = args.value_of("override")?; + + log::info!("Running Dedicated server"); + let save_path = save_folder().join("dedicated_server_save.save"); + log::info!("Loading Save game from {:?}", &save_path); + + let start_game = if create { + StartGameInfo::Create { + name: "dedicated_server_save.save".to_string(), + info: GameCreationInfo::Empty, + allow_overwrite: overwrite, + } + } else { + StartGameInfo::Load(save_path) + }; + run_dedicated_server(start_game); } } #[cfg(target_arch = "wasm32")] -pub fn main() { +pub fn main(_input: &Vec) -> Result<(), args::ArgsError> { + console_error_panic_hook::set_once(); + puffin::set_scopes_on(true); use eframe::wasm_bindgen::JsCast as _; // Redirect `log` message to `console.log` and friends: - eframe::WebLogger::init(log::LevelFilter::Error).ok(); + eframe::WebLogger::init(log::LevelFilter::Warn).ok(); + log::info!("Welcome to main on wasm"); let web_options = eframe::WebOptions::default(); @@ -249,6 +281,8 @@ pub fn main() { } } }); + + Ok(()) } enum StartGameInfo { @@ -257,6 +291,7 @@ enum StartGameInfo { Create { name: String, info: GameCreationInfo, + allow_overwrite: bool, }, } @@ -280,10 +315,84 @@ enum GameCreationInfo { FromBP(PathBuf), } +// match start_game_info { +// StartGameInfo::Load(path) => load(path) +// .map(|sg| { +// assert_eq!( +// sg.checksum, data_store.checksum, +// "A savegame can only be loaded with the EXACT same mods!" +// ); +// sg.game_state +// }) +// .unwrap(), +// StartGameInfo::LoadReadable(path) => load_readable(path) +// .map(|sg| { +// assert_eq!( +// sg.checksum, data_store.checksum, +// "A savegame can only be loaded with the EXACT same mods!" +// ); +// sg.game_state +// }) +// .unwrap(), +// StartGameInfo::Create { +// name, +// info, +// allow_overwrite, +// } => { +// assert!( +// name.is_ascii(), +// "For now only ASCII game names are allowed, since they are used as the filename" +// ); + +// if !allow_overwrite { +// log::error!( +// "Currently allow_overwrite is ignored and overwriting is always allowed!!!!" +// ); +// } + +// match info { +// GameCreationInfo::Empty => GameState::new(name, &data_store), +// GameCreationInfo::Megabase(use_solar_field) => { +// GameState::new_with_megabase( +// name, +// use_solar_field, +// progress, +// &data_store, +// ) +// }, +// GameCreationInfo::Gigabase(count) => { +// GameState::new_with_gigabase(name, count, progress, &data_store) +// }, +// GameCreationInfo::SolarField(wattage, base_pos) => { +// GameState::new_with_tons_of_solar( +// name, +// wattage, +// base_pos, +// None, +// progress, +// &data_store, +// ) +// }, +// GameCreationInfo::LotsOfBelts => { +// GameState::new_with_lots_of_belts(name, progress, &data_store) +// }, +// GameCreationInfo::TrainRide => { +// GameState::new_with_world_train_ride(name, progress, &data_store) +// }, + +// GameCreationInfo::FromBP(path) => { +// GameState::new_with_bp(name, &data_store, path) +// }, + +// _ => unimplemented!(), +// } +// }, +// } + #[cfg(feature = "client")] fn run_integrated_server( progress: Arc, - start_game_info: StartGameInfo, + game_creation_fn: impl FnOnce(Arc, &DataStore) -> GameState, // FIXME: This type is wrong listen_addr: Option<&'static str>, @@ -305,68 +414,7 @@ fn run_integrated_server( data::DataStoreOptions::ItemU8RecipeU8(data_store) => { let (send, recv) = channel(); - let game_state = Arc::new(match start_game_info { - StartGameInfo::Load(path) => load(path) - .map(|sg| { - assert_eq!( - sg.checksum, data_store.checksum, - "A savegame can only be loaded with the EXACT same mods!" - ); - sg.game_state - }) - .unwrap(), - StartGameInfo::LoadReadable(path) => load_readable(path) - .map(|sg| { - assert_eq!( - sg.checksum, data_store.checksum, - "A savegame can only be loaded with the EXACT same mods!" - ); - sg.game_state - }) - .unwrap(), - StartGameInfo::Create { name, info } => { - assert!( - name.is_ascii(), - "For now only ASCII game names are allowed, since they are used as the filename" - ); - match info { - GameCreationInfo::Empty => GameState::new(name, &data_store), - GameCreationInfo::Megabase(use_solar_field) => { - GameState::new_with_megabase( - name, - use_solar_field, - progress, - &data_store, - ) - }, - GameCreationInfo::Gigabase(count) => { - GameState::new_with_gigabase(name, count, progress, &data_store) - }, - GameCreationInfo::SolarField(wattage, base_pos) => { - GameState::new_with_tons_of_solar( - name, - wattage, - base_pos, - None, - progress, - &data_store, - ) - }, - GameCreationInfo::LotsOfBelts => { - GameState::new_with_lots_of_belts(name, progress, &data_store) - }, - GameCreationInfo::TrainRide => { - GameState::new_with_world_train_ride(name, progress, &data_store) - }, - - GameCreationInfo::FromBP(path) => { - GameState::new_with_bp(name, &data_store, path) - }, - - _ => unimplemented!(), - } - }, - }); + let game_state = Arc::new(game_creation_fn(progress, &data_store)); let state_machine: Arc>> = Arc::new(Mutex::new(ActionStateMachine::new_from_gamestate( @@ -438,16 +486,32 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { let connections: Arc>> = Arc::default(); - let local_addr = "127.0.0.1:8080"; + let local_addr = "127.0.0.1:42069"; let cancel: Arc = Default::default(); + log::warn!("Hosting on {}", &local_addr); accept_continously(local_addr, connections.clone(), cancel.clone()).unwrap(); match data_store { data::DataStoreOptions::ItemU8RecipeU8(data_store) => { - let game_state = load(todo!("Add a console argument for the save file path")) - .map(|save| save.game_state) - .unwrap_or_else(|| GameState::new("Server Save".to_string(), &data_store)); + let game_state = match start_game_info { + StartGameInfo::Load(path_buf) => load(path_buf) + .map(|save| save.game_state) + .expect("Could not load game"), + StartGameInfo::LoadReadable(path_buf) => unimplemented!(), + StartGameInfo::Create { + name, + info, + allow_overwrite, + } => { + if !allow_overwrite { + log::error!( + "Currently allow_overwrite is ignored and overwriting is always allowed!!!!" + ); + } + GameState::new(name, &data_store) + }, + }; let mut game = Game::new( GameInitData::DedicatedServer( diff --git a/src/main.rs b/src/main.rs index 4975187..bedce7b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,12 @@ +use std::env; + #[cfg(feature = "dhat-heap")] #[global_allocator] static ALLOC: dhat::Alloc = dhat::Alloc; #[cfg(not(feature = "dhat-heap"))] #[cfg(not(debug_assertions))] +#[cfg(not(target_arch = "wasm32"))] #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; @@ -15,5 +18,18 @@ fn main() -> Result<(), ()> { #[cfg(feature = "dhat-heap")] let _profiler = dhat::Profiler::new_heap(); - factory::main() + let args: Vec = env::args().collect(); + + match factory::main(&args) { + Ok(()) => { + log::info!("Exiting"); + Ok(()) + }, + Err(e) => { + println!("{e}"); + Err(()) + }, + }; + + Ok(()) } diff --git a/src/multiplayer/bad_tcp.rs b/src/multiplayer/bad_tcp.rs index 8b13789..19d1cda 100644 --- a/src/multiplayer/bad_tcp.rs +++ b/src/multiplayer/bad_tcp.rs @@ -1 +1,9 @@ +use std::net::TcpStream; +enum ClientState { + PreConnection(TcpStream), +} + +enum ServerState { + PreConnection(TcpStream), +} diff --git a/src/multiplayer/mod.rs b/src/multiplayer/mod.rs index 223eaaa..d371cf2 100644 --- a/src/multiplayer/mod.rs +++ b/src/multiplayer/mod.rs @@ -30,6 +30,7 @@ use crate::{ mod plumbing; // mod protocol; +mod bad_tcp; mod server; pub mod connection_reciever_tcp; diff --git a/src/multiplayer/plumbing.rs b/src/multiplayer/plumbing.rs index 414c220..cb0b202 100644 --- a/src/multiplayer/plumbing.rs +++ b/src/multiplayer/plumbing.rs @@ -7,7 +7,7 @@ use std::{ time::{Duration, Instant}, }; -use crate::app_state::SimulationState; +use crate::{app_state::SimulationState, frontend::world::Position}; use log::error; use parking_lot::Mutex; @@ -71,11 +71,14 @@ impl ActionSource = state_machine - .handle_inputs(&self.local_input, world, sim_state, data_store) + .handle_inputs(self.local_input.try_iter(), world, sim_state, data_store) .into_iter() .collect(); + local_actions.extend(state_machine.once_per_update_actions(world, data_store)); + local_actions.extend(self.ui_actions.try_iter()); + mem::drop(state_machine); for action in &local_actions { @@ -85,9 +88,7 @@ impl ActionSource, _) = postcard::from_io((&self.server_connection, &mut buffer)).unwrap(); - recieved_actions - .into_iter() - .chain(self.ui_actions.try_iter()) + recieved_actions.into_iter() } } @@ -163,8 +164,6 @@ impl ActionSource let actions: Vec<_> = actions.into_iter().collect(); // Send the actions to the clients for conn in self.client_connections.lock().iter() { - for action in &actions { - postcard::to_io(&action, conn).expect("tcp send failed"); - } + postcard::to_io(&actions, conn).expect("tcp send failed"); } } } diff --git a/src/multiplayer/server.rs b/src/multiplayer/server.rs index 848e29e..8777f76 100644 --- a/src/multiplayer/server.rs +++ b/src/multiplayer/server.rs @@ -1,5 +1,7 @@ use std::{borrow::Borrow, fs::File, io::Write, marker::PhantomData}; +#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] +use crate::saving::save_with_fork; use crate::{ app_state::{GameState, SimulationState}, data::DataStore, @@ -71,16 +73,32 @@ impl< data_store: &DataStore, ) { let mut simulation_state = game_state.simulation_state.lock(); + let mut world = game_state.world.lock(); { profiling::scope!("GameState Update"); - GameState::update( - &mut *simulation_state, - &mut *game_state.aux_data.lock(), - data_store, - ); + let aux_data = &mut *game_state.aux_data.lock(); + + // TODO: Autosave interval + if aux_data.current_tick % (60 * 60 * 5) == 0 { + // Autosave + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + { + profiling::scope!("Autosave"); + // TODO: Handle overlapping saves + let _ = save_with_fork( + "dedicated_server_save", + Some("dedicated_server_save.save"), + &world, + &simulation_state, + &aux_data, + data_store, + ); + } + } + + GameState::update(&mut *simulation_state, aux_data, data_store); } - let mut world = game_state.world.lock(); let current_tick = game_state.aux_data.lock().current_tick; let actions_iter = { diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index e2cb2ff..bf5db00 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -16,10 +16,12 @@ use egui_graphs::LayoutStateTree; use url::Url; use wasm_timer::Instant; -use directories::ProjectDirs; use parking_lot::Mutex; -use crate::{GameCreationInfo, run_client, saving::loading::SaveFileList}; +use crate::{ + GameCreationInfo, example_worlds, run_client, + saving::{load, loading::SaveFileList, save_folder}, +}; use crate::{StartGameInfo, frontend::world::Position}; use crate::{rendering::render_world::EscapeMenuOptions, run_integrated_server}; use eframe::{ @@ -56,6 +58,8 @@ pub struct App { last_rendered_update: u64, + world_creation_state: example_worlds::WorldValueStore, + pub input_sender: Option>, texture_atlas: Arc, @@ -77,6 +81,7 @@ impl App { }), input_sender: None, state: AppState::MainMenu { in_ip_box: None }, + world_creation_state: Default::default(), texture_atlas: atlas, currently_loaded_game: None, last_rendered_update: 0, @@ -138,14 +143,19 @@ impl eframe::App for App { new_state = Some(AppState::MainMenu { in_ip_box: None }); } - if ui.button("Open Save File Folder").clicked() { - let uri = Url::from_file_path( - ProjectDirs::from("de", "aschhoff", "factory_game") - .expect("No Home path found") - .data_dir(), + if ui + .add_enabled( + cfg!(not(all(target_arch = "wasm32", target_os = "unknown"))), + Button::new("Open Save File Folder"), ) - .expect("Could not generate URI"); - open::that(uri.as_str()).expect("Failed to Open Folder"); + .clicked() + { + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + { + let uri = + Url::from_file_path(save_folder()).expect("Could not generate URI"); + open::that(uri.as_str()).expect("Failed to Open Folder"); + } } let mut dirty = false; @@ -215,7 +225,7 @@ impl eframe::App for App { } }); row.col(|ui| { - if ui.add_enabled(true, Button::new("Load")).clicked() { + if ui.add_enabled(true, Button::new("Load")).on_disabled_hover_text("Currently WASM does not support saving or loading").clicked() { let path = file.path.clone(); let progress = Arc::new(AtomicU64::new(0f64.to_bits())); @@ -225,7 +235,16 @@ impl eframe::App for App { thread::spawn(move || { send.send(run_integrated_server( progress_send, - StartGameInfo::Load(path), + |progress, data_store| { + load(path).map(|sg| { + assert_eq!( + sg.checksum, data_store.checksum, + "A savegame can only be loaded with the EXACT same mods!" + ); + sg.game_state + }) + .unwrap() + }, None, )) .expect("Channel send failed"); @@ -274,11 +293,7 @@ impl eframe::App for App { }); if dirty { - *save_files = SaveFileList::generate_from_save_folder( - ProjectDirs::from("de", "aschhoff", "factory_game") - .expect("No Home path found") - .data_dir(), - ) + *save_files = SaveFileList::generate_from_save_folder(&save_folder()) } }); // Borrow checker issue @@ -465,12 +480,10 @@ impl eframe::App for App { ); }); - if ui.button("Load").clicked() { + if ui.add_enabled(cfg!(not(all(target_arch = "wasm32", target_os = "unknown"))), Button::new("Load")).on_disabled_hover_text("Saving/Loading not yet supported when running the browser").clicked() { self.state = AppState::LoadSaveMenu { save_files: SaveFileList::generate_from_save_folder( - ProjectDirs::from("de", "aschhoff", "factory_game") - .expect("No Home path found") - .data_dir(), + &save_folder() ), } } @@ -478,9 +491,7 @@ impl eframe::App for App { // #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] // if let Some(path) = rfd::FileDialog::new() // .set_directory( - // ProjectDirs::from("de", "aschhoff", "factory_game") - // .expect("No Home path found") - // .data_dir(), + // save_folder(), // ) // .pick_file() // { @@ -530,18 +541,15 @@ impl eframe::App for App { gigabase_size, new_game_name, } => { - let gigabase_size = *gigabase_size; - let mut new_game_name = new_game_name.take(); - CentralPanel::default().show(ctx, |ui| { if ui.button("Back to Main Menu").clicked() { self.state = AppState::MainMenu { in_ip_box: None }; return; } + let ret = + example_worlds::list_example_worlds(&mut self.world_creation_state, ui); - ui.add(TextEdit::singleline(&mut new_game_name).char_limit(100)); - - if ui.button("Empty World").clicked() { + if let Some(creation_fn) = ret { let progress = Arc::new(AtomicU64::new(0f64.to_bits())); let (send, recv) = channel(); @@ -549,11 +557,8 @@ impl eframe::App for App { #[cfg(not(target_arch = "wasm32"))] thread::spawn(move || { send.send(run_integrated_server( - progress_send, - StartGameInfo::Create { - name: new_game_name, - info: GameCreationInfo::Empty, - }, + progress_send.clone(), + creation_fn, None, )) .expect("Channel send failed"); @@ -565,6 +570,7 @@ impl eframe::App for App { StartGameInfo::Create { name: new_game_name, info: GameCreationInfo::Empty, + allow_overwrite: true, }, None, )); @@ -574,248 +580,6 @@ impl eframe::App for App { progress, game_state_receiver: recv, }; - } else if ui.button("Lots of Belts").clicked() { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - #[cfg(not(target_arch = "wasm32"))] - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create { - name: new_game_name, - info: GameCreationInfo::LotsOfBelts, - }, - None, - )) - .expect("Channel send failed"); - }); - - #[cfg(target_arch = "wasm32")] - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create { - name: new_game_name, - info: GameCreationInfo::LotsOfBelts, - }, - None, - )); - - self.state = AppState::Loading { - start_time: Instant::now(), - progress, - game_state_receiver: recv, - }; - } else if ui.button("Train Ride around the world").clicked() { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - #[cfg(not(target_arch = "wasm32"))] - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create { - info: GameCreationInfo::TrainRide, - name: new_game_name, - }, - None, - )) - .expect("Channel send failed"); - }); - - #[cfg(target_arch = "wasm32")] - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create { - info: GameCreationInfo::TrainRide, - name: new_game_name, - }, - None, - )); - - self.state = AppState::Loading { - start_time: Instant::now(), - progress, - game_state_receiver: recv, - }; - } else if ui.button("Megabase").clicked() { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - #[cfg(not(target_arch = "wasm32"))] - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create { - info: GameCreationInfo::Megabase(true), - name: new_game_name, - }, - None, - )) - .expect("Channel send failed"); - }); - - #[cfg(target_arch = "wasm32")] - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create { - info: GameCreationInfo::Megabase(true), - name: new_game_name, - }, - None, - )); - - self.state = AppState::Loading { - start_time: Instant::now(), - progress, - game_state_receiver: recv, - }; - } else if ui.button("Megabase with Infinity Battery").clicked() { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - #[cfg(not(target_arch = "wasm32"))] - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create { - info: GameCreationInfo::Megabase(false), - name: new_game_name, - }, - None, - )) - .expect("Channel send failed"); - }); - - #[cfg(target_arch = "wasm32")] - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create { - info: GameCreationInfo::Megabase(false), - name: new_game_name, - }, - None, - )); - - self.state = AppState::Loading { - start_time: Instant::now(), - progress, - game_state_receiver: recv, - }; - } else if { - let v = ui.horizontal(|ui| { - let ret = ui.button("Gigabase").clicked(); - - let AppState::NewGameMenu { gigabase_size, .. } = &mut self.state - else { - unreachable!() - }; - - ui.add( - Slider::new(gigabase_size, 1..=1_000) - .logarithmic(true) - .text("Number of base copies to build"), - ); - - let single_base_size = 15.4 / 40.0; - let single_base_usage = 40.0 / 60.0; - - ui.label(&format!( - "Est. Memory Usage: ~{:.1}GB", - single_base_size * f64::from(*gigabase_size) - )); - ui.label(&format!( - "Est. Memory Bandwidth for 60 UPS: ~{:.1}GB/s", - single_base_usage * f64::from(*gigabase_size) - )); - - ret - }); - - v.inner - } { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create { - info: GameCreationInfo::Gigabase(gigabase_size), - name: new_game_name, - }, - None, - )) - .expect("Channel send failed"); - }); - - self.state = AppState::Loading { - start_time: Instant::now(), - progress, - game_state_receiver: recv, - }; - } else if ui.button("Solar Field").clicked() { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create { - info: GameCreationInfo::SolarField( - crate::power::Watt(1_000), - Position { x: 1600, y: 1600 }, - ), - name: new_game_name, - }, - None, - )) - .expect("Channel send failed"); - }); - - self.state = AppState::Loading { - start_time: Instant::now(), - progress, - game_state_receiver: recv, - }; - } else if ui.button("With bp file").clicked() { - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] - if let Some(path) = rfd::FileDialog::new().pick_file() { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); - let (send, recv) = channel(); - - let progress_send = progress.clone(); - thread::spawn(move || { - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create { - info: GameCreationInfo::FromBP(path), - name: new_game_name, - }, - None, - )) - .expect("Channel send failed"); - }); - - self.state = AppState::Loading { - start_time: Instant::now(), - progress, - game_state_receiver: recv, - }; - } - } else { - if let AppState::NewGameMenu { - new_game_name: ngn, .. - } = &mut self.state - { - *ngn = new_game_name; - } } }); }, @@ -858,6 +622,9 @@ impl App { fn update_ingame(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) { let size = ctx.available_rect(); + #[cfg(target_arch = "wasm32")] + let mut render_action_vec = vec![]; + CentralPanel::default().show(ctx, |ui| { if ui.ui_contains_pointer() { ctx.set_cursor_icon(CursorIcon::Default); @@ -1087,10 +854,10 @@ impl App { let mut sim_state_lock = state.state.simulation_state.lock(); let sim_state = &mut *sim_state_lock; - let mut actions: Vec> = state + let mut actions: Vec> = state .state_machine .lock() - .handle_inputs(inputs, world, data_store) + .handle_inputs(inputs, world, sim_state, data_store) .collect(); actions.extend( diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index e201c7b..67361ec 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -10,6 +10,7 @@ use crate::belt::smart::{ use crate::belt::smart::SmartBelt; use crate::blueprint::blueprint_string::BlueprintString; use crate::chest::ChestSize; +#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] use crate::frontend::action::action_state_machine::ForkSaveInfo; use crate::frontend::action::place_entity::EntityPlaceOptions; use crate::frontend::action::place_entity::PlaceEntityInfo; @@ -23,7 +24,9 @@ use crate::lab::{LabViewInfo, TICKS_PER_SCIENCE}; use crate::liquid::FluidSystemState; use crate::par_generation::{ParGenerateInfo, Timer}; use crate::rendering::{BeltSide, Corner}; -use crate::saving::{save_components, save_with_fork}; +use crate::saving::save_components; +#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] +use crate::saving::save_with_fork; use crate::statistics::{NUM_DIFFERENT_TIMESCALES, TIMESCALE_NAMES}; use crate::{ TICKS_PER_SECOND_LOGIC, @@ -62,6 +65,8 @@ use egui_plot::{AxisHints, GridMark, Line, Plot, PlotPoints}; use egui_show_info::ShowInfo; use flate2::Compression; use flate2::write::ZlibEncoder; + +#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] use interprocess::os::unix::unnamed_pipe::UnnamedPipeExt; use itertools::Itertools; use log::error; @@ -1912,6 +1917,7 @@ pub fn render_ui< let tick = (current_tick % u64::from(state_machine_ref.autosave_interval)) as u32; + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] if cfg!(target_os = "linux") { if tick < state_machine_ref.last_tick_seen_for_autosave { if state_machine_ref.current_fork_save_in_progress.is_none() { @@ -1983,6 +1989,7 @@ pub fn render_ui< ); }); + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] if let Some(recv) = &mut state_machine_ref.current_fork_save_in_progress { const NUM_STATES: u8 = 12; @@ -2025,12 +2032,21 @@ pub fn render_ui< ); } - if ui - .add_enabled( + let enabled = { + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + { + false + } + + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + { cfg!(target_os = "linux") - && state_machine_ref.current_fork_save_in_progress.is_none(), - Button::new("Save with fork"), - ) + && state_machine_ref.current_fork_save_in_progress.is_none() + } + }; + + if ui + .add_enabled(enabled, Button::new("Save with fork")) .on_disabled_hover_text(if !cfg!(target_os = "linux") { "Only available on Linux" } else { @@ -2038,21 +2054,24 @@ pub fn render_ui< }) .clicked() { - let recv = save_with_fork( - &aux_data.game_name, - Some(&format!("{}.save", &aux_data.game_name)), - &*world, - &*simulation_state, - &*aux_data, - data_store_ref, - ); - if let Some(recv) = recv { - recv.set_nonblocking(true) - .expect("Could not set pipe to nonblocking!"); - state_machine_ref.current_fork_save_in_progress = Some(ForkSaveInfo { - recv, - current_state: 0, - }); + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + { + let recv = save_with_fork( + &aux_data.game_name, + Some(&format!("{}.save", &aux_data.game_name)), + &*world, + &*simulation_state, + &*aux_data, + data_store_ref, + ); + if let Some(recv) = recv { + recv.set_nonblocking(true) + .expect("Could not set pipe to nonblocking!"); + state_machine_ref.current_fork_save_in_progress = Some(ForkSaveInfo { + recv, + current_state: 0, + }); + } } } if ui.button("Main Menu").clicked() { @@ -3904,7 +3923,7 @@ pub fn render_ui< }, Entity::Accumulator { ty, pole_position, .. } => { ui.label(format!("{}", &data_store.accumulator_info[*ty as usize].display_name)); - + let max_charge = data_store.accumulator_info[*ty as usize].max_charge; let charge = if let Some(pole_pos) = pole_position { let grid = game_state_ref.simulation_state.factory.power_grids.pole_pos_to_grid_id[&pole_pos.0]; diff --git a/src/saving/mod.rs b/src/saving/mod.rs index ab6a47d..b3afa51 100644 --- a/src/saving/mod.rs +++ b/src/saving/mod.rs @@ -27,6 +27,13 @@ use crate::{ pub mod loading; mod save_file_settings; +pub(crate) fn save_folder() -> PathBuf { + ProjectDirs::from("de", "aschhoff", "factory_game") + .expect("No Home path found") + .data_dir() + .join("saves") +} + #[derive(Debug, Encode, serde::Deserialize, serde::Serialize)] pub struct SaveGame< ItemIdxType: IdxTrait, @@ -106,9 +113,9 @@ pub fn save_components( data_store: &DataStore, ) { let checksum = data_store.checksum.clone(); - let dir = ProjectDirs::from("de", "aschhoff", "factory_game").expect("No Home path found"); + let save_dir = save_folder(); - create_dir_all(dir.data_dir()).expect("Could not create data dir"); + create_dir_all(&save_dir).expect("Could not create save dir"); // if let Ok(s) = env::var("FACTORY_SAVE_READABLE") { // if s == "true" { @@ -132,11 +139,11 @@ pub fn save_components( // } // } - let temp_file_dir = dir.data_dir().join("tmp.save"); + let temp_file_dir = save_dir.join("tmp.save"); let save_file_dir = if let Some(name) = save_name { - dir.data_dir().join(&name) + save_dir.join(&name) } else { - dir.data_dir().join("autosave.save") + save_dir.join("autosave.save") }; let info = StoredSaveFileInfo { @@ -247,6 +254,7 @@ pub fn save_components( /// # Panics /// If File system stuff fails +#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] pub fn save_components_fork_safe( name: &str, save_name: Option<&str>, @@ -258,9 +266,9 @@ pub fn save_components_fork_safe mut send: interprocess::unnamed_pipe::Sender, ) { let checksum = &data_store.checksum; - let dir = ProjectDirs::from("de", "aschhoff", "factory_game").expect("No Home path found"); + let save_dir = save_folder(); - create_dir_all(dir.data_dir()).expect("Could not create data dir"); + create_dir_all(&save_dir).expect("Could not create data dir"); // FIXME: Allocation and chrono is prob illegal after a fork let info = StoredSaveFileInfo { @@ -277,11 +285,11 @@ pub fn save_components_fork_safe preview: None, }; - let temp_file_dir = dir.data_dir().join("tmp.save"); + let temp_file_dir = save_dir.join("tmp.save"); let save_file_dir = if let Some(name) = save_name { - dir.data_dir().join(&name) + save_dir.join(&name) } else { - dir.data_dir().join("autosave.save") + save_dir.join("autosave.save") }; create_dir_all(&temp_file_dir).expect("Could not create temp dir"); @@ -374,6 +382,7 @@ pub fn save_components_fork_safe /// # Panics /// If File system stuff fails +#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] pub fn save_with_fork( name: &str, save_name: Option<&str>, From 814294813eb8004a34a50250488e4ee0fb0a32f3 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 12 Jan 2026 04:45:21 +0100 Subject: [PATCH 083/152] Fully support dedicated server based multiplayer --- Cargo.lock | 298 ++++++++------------- Cargo.toml | 3 +- flake.nix | 18 +- src/app_state.rs | 9 +- src/blueprint/mod.rs | 1 + src/example_worlds/mod.rs | 11 +- src/frontend/action/mod.rs | 8 + src/frontend/world/tile.rs | 4 +- src/lib.rs | 160 +++-------- src/multiplayer/connection_reciever_tcp.rs | 9 +- src/multiplayer/mod.rs | 75 +++++- src/multiplayer/plumbing.rs | 128 +++++++-- src/multiplayer/server.rs | 23 +- src/rendering/eframe_app.rs | 35 ++- src/rendering/render_world.rs | 2 +- src/saving/mod.rs | 13 + 16 files changed, 443 insertions(+), 354 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a59bb79..9ae31ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -308,28 +308,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "ashpd" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" -dependencies = [ - "async-fs", - "async-net", - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.9.2", - "raw-window-handle", - "serde", - "serde_repr", - "url", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "zbus", -] - [[package]] name = "async-broadcast" version = "0.7.2" @@ -368,17 +346,6 @@ dependencies = [ "slab", ] -[[package]] -name = "async-fs" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - [[package]] name = "async-io" version = "2.6.0" @@ -408,17 +375,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-net" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - [[package]] name = "async-process" version = "2.5.0" @@ -445,7 +401,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -480,7 +436,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -559,7 +515,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -575,7 +531,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -734,7 +690,7 @@ checksum = "238b90427dfad9da4a9abd60f3ec1cdee6b80454bde49ed37f1781dd8e9dc7f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -853,7 +809,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -927,9 +883,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.51" +version = "1.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ "find-msvc-tools", "jobserver", @@ -990,7 +946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d38f1088dcf6ce3487a09c49fc2d2f8759045603f24d5814357e9283260426" dependencies = [ "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -1254,7 +1210,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -1330,7 +1286,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -1457,7 +1413,7 @@ source = "git+https://github.com/BloodStainedCrow/egui-show-info#2f1c3f454e4577f dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -1539,7 +1495,7 @@ version = "0.28.0" source = "git+https://github.com/BloodStainedCrow/egui_graphs?branch=tree_layout#5e4e3191d6d17e264660d78b6f47363d16bd325b" dependencies = [ "egui", - "getrandom 0.2.16", + "getrandom 0.2.17", "instant", "petgraph 0.8.2", "rand 0.9.2", @@ -1609,7 +1565,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -1630,7 +1586,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -1641,7 +1597,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -1686,7 +1642,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -1787,13 +1743,14 @@ dependencies = [ "fork", "genawaiter", "get-size2", - "getrandom 0.2.16", + "getrandom 0.2.17", "getrandom 0.3.4", "hex", "image", "interprocess", "itertools", "libc", + "lockfile", "log", "memoffset", "mimalloc", @@ -1856,7 +1813,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -1870,9 +1827,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" [[package]] name = "fixed-buffer" @@ -1971,7 +1928,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -2073,7 +2030,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -2160,7 +2117,7 @@ checksum = "ab21d7bd2c625f2064f04ce54bcb88bc57c45724cde45cba326d784e22d3f71a" dependencies = [ "attribute-derive", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -2193,9 +2150,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -2260,9 +2217,9 @@ dependencies = [ [[package]] name = "glam" -version = "0.30.9" +version = "0.30.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd47b05dddf0005d850e5644cae7f2b14ac3df487979dbfff3b56f20b1a6ae46" +checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" [[package]] name = "glob" @@ -2650,9 +2607,9 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -2681,7 +2638,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -2821,9 +2778,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.179" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libfuzzer-sys" @@ -2939,6 +2896,12 @@ dependencies = [ "serde", ] +[[package]] +name = "lockfile" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be1cf190319c74ba3e45923624626ae2e43fe42ad7e60ff38ded81044c37630" + [[package]] name = "log" version = "0.4.29" @@ -2987,7 +2950,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -3179,19 +3142,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "no-std-compat" version = "0.2.0" @@ -3254,7 +3204,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -3306,7 +3256,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -3802,7 +3752,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", "unicase", ] @@ -3839,7 +3789,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -4020,9 +3970,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] @@ -4044,7 +3994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -4131,16 +4081,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quick-xml" version = "0.38.4" @@ -4148,13 +4088,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", + "serde", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -4178,7 +4119,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -4400,7 +4341,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", "thiserror 2.0.17", ] @@ -4462,26 +4403,29 @@ dependencies = [ [[package]] name = "rfd" -version = "0.15.4" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +checksum = "069d6129dede311430d0dcf1ded88a7affc7a342c2d8e6336043d43ed14dac17" dependencies = [ - "ashpd", "block2 0.6.2", "dispatch2", "js-sys", + "libc", "log", "objc2 0.6.3", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", + "percent-encoding", "pollster", "raw-window-handle", - "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4550,7 +4494,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.113", + "syn 2.0.114", "unicode-ident", ] @@ -4710,14 +4654,14 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] name = "serde_json" -version = "1.0.148" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", @@ -4745,7 +4689,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -4928,7 +4872,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -5025,7 +4969,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -5037,7 +4981,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -5072,9 +5016,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.113" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -5100,7 +5044,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -5169,7 +5113,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -5180,7 +5124,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -5354,7 +5298,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -5415,9 +5359,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-bidi" @@ -5481,9 +5425,9 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -5491,12 +5435,6 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "usvg" version = "0.45.1" @@ -5655,7 +5593,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -5802,7 +5740,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" dependencies = [ "proc-macro2", - "quick-xml 0.38.4", + "quick-xml", "quote", ] @@ -6144,7 +6082,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -6155,7 +6093,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -6166,7 +6104,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -6177,7 +6115,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -6673,15 +6611,15 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", "synstructure", ] [[package]] name = "zbus" -version = "5.12.0" +version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +checksum = "17f79257df967b6779afa536788657777a0001f5b42524fcaf5038d4344df40b" dependencies = [ "async-broadcast", "async-executor", @@ -6697,8 +6635,9 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix", + "libc", "ordered-stream", + "rustix 1.1.3", "serde", "serde_repr", "tracing", @@ -6729,7 +6668,7 @@ checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", "zbus-lockstep", "zbus_xml", "zvariant", @@ -6737,14 +6676,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.12.0" +version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +checksum = "aad23e2d2f91cae771c7af7a630a49e755f1eb74f8a46e9f6d5f7a146edf5a37" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", "zbus_names", "zvariant", "zvariant_utils", @@ -6752,47 +6691,45 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", "winnow", "zvariant", ] [[package]] name = "zbus_xml" -version = "5.0.2" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589e9a02bfafb9754bb2340a9e3b38f389772684c63d9637e76b1870377bec29" +checksum = "441a0064125265655bccc3a6af6bef56814d9277ac83fce48b1cd7e160b80eac" dependencies = [ - "quick-xml 0.36.2", + "quick-xml", "serde", - "static_assertions", "zbus_names", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -6812,7 +6749,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", "synstructure", ] @@ -6846,7 +6783,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", ] [[package]] @@ -6857,9 +6794,9 @@ checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" [[package]] name = "zmij" -version = "1.0.9" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee2a72b10d087f75fb2e1c2c7343e308fe6970527c22a41caf8372e165ff5c1" +checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec" [[package]] name = "zune-core" @@ -6902,14 +6839,13 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.8.0" +version = "5.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +checksum = "326aaed414f04fe839777b4c443d4e94c74e7b3621093bd9c5e649ac8aa96543" dependencies = [ "endi", "enumflags2", "serde", - "url", "winnow", "zvariant_derive", "zvariant_utils", @@ -6917,26 +6853,26 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.8.0" +version = "5.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +checksum = "ba44e1f8f4da9e6e2d25d2a60b116ef8b9d0be174a7685e55bb12a99866279a7" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.113", + "syn 2.0.114", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.113", + "syn 2.0.114", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index d18ae75..8226415 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ puffin_egui = { git = "https://github.com/EmbarkStudios/puffin", optional = true puffin = { git = "https://github.com/EmbarkStudios/puffin", features = ["web"] } dhat = "0.3.3" noise = { version = "0.9.0", features = ["std"] } -rfd = { version = "0.15.3", optional = true } +rfd = { version = "0.17", optional = true } egui_graphs = { version = "0.28", optional = true } serde_path_to_error = "0.1.17" get-size2 = { version = "0.7.1", features = ["derive"], optional = true } @@ -75,6 +75,7 @@ rand_xoshiro = "0.7.0" url = "2.5.7" args = "2.2.0" console_error_panic_hook = "0.1.7" +lockfile = "0.4.0" # These are all the dependencies which do not work on wasm [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] diff --git a/flake.nix b/flake.nix index 79546bd..5860119 100644 --- a/flake.nix +++ b/flake.nix @@ -5,6 +5,8 @@ url = "github:nix-community/fenix"; inputs.nixpkgs.follows = "nixpkgs"; }; + + # crane.url = "github:ipetkov/crane"; }; outputs = { self, nixpkgs, fenix }: let @@ -37,7 +39,7 @@ src = ./.; buildInputs = neededPackages; nativeBuildInputs = [ pkgs.pkg-config pkgs.makeWrapper ]; - cargoHash = "sha256-TV+y1lHUmBJFBlgv2KBjQEXDLVqHNkgZsIXN2P3972k="; + cargoHash = "sha256-AQ6UxUS0DGK+v695XvqX81r/5pGfCsQdzLP70kAOcME="; # cargoLock.lockFile = ./Cargo.lock; doCheck = false; @@ -76,6 +78,18 @@ packages."x86_64-linux".default = client_package; - packages."x86_64-linux".dedicated_server = client_package.overrideAttrs ( oldAttrs: { cargoBuildFlags = [ "--no-default-features" ]; }); + "wasm" = client_package.overrideAttrs ( oldAttrs: { + CARGO_BUILD_TARGET = "wasm32-wasi"; + nativeBuildInputs = oldAttrs.nativeBuildInputs ++ [ pkgs.wabt ]; + postInstall = '' + mkdir -p $out/lib + ls -lR $out + wasm-strip $out/bin/factory -o $out/lib/factory.wasm + rm -rf $out/bin + wasm-validate $out/lib/factory.wasm + ''; + }); + + packages."x86_64-linux".dedicated_server = client_package.overrideAttrs ( oldAttrs: { cargoBuildFlags = [ "--no-default-features" "-F logging" ]; }); }; } diff --git a/src/app_state.rs b/src/app_state.rs index c5f6c44..87a4db8 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -9,6 +9,7 @@ use crate::frontend::action::place_entity::PlaceEntityInfo; use crate::frontend::world::tile::CHUNK_SIZE; use crate::frontend::world::tile::ModuleSlots; use crate::frontend::world::tile::ModuleTy; +use crate::frontend::world::tile::PlayerInfo; use crate::get_const_string; use crate::inserter::InserterStateInfo; use crate::inserter::WaitlistSearchSide; @@ -111,7 +112,8 @@ pub struct AuxillaryData { pub update_round_trip_times: Timeline, pub update_times: Timeline, - #[get_size(ignore)] + + #[cfg_attr(feature = "client", get_size(ignore))] #[serde(skip)] last_update_time: Option, @@ -1221,6 +1223,11 @@ impl GameState { + log::info!("Player joined"); + game_state.world.players.push(PlayerInfo::default()); + }, + ActionType::SetActiveResearch { tech } => { game_state.simulation_state.tech_state.current_technology = *tech; }, diff --git a/src/blueprint/mod.rs b/src/blueprint/mod.rs index de64342..0e71496 100644 --- a/src/blueprint/mod.rs +++ b/src/blueprint/mod.rs @@ -175,6 +175,7 @@ impl BlueprintAction { data_store: &DataStore, ) -> Self { match action { + ActionType::SpawnPlayer { .. } => unreachable!(), ActionType::PlaceFloorTile(_) => unimplemented!(), ActionType::PlaceEntity(place_entity_info) => { match place_entity_info.entities.clone() { diff --git a/src/example_worlds/mod.rs b/src/example_worlds/mod.rs index 7670126..85cd879 100644 --- a/src/example_worlds/mod.rs +++ b/src/example_worlds/mod.rs @@ -3,8 +3,6 @@ use std::{ sync::{LazyLock, atomic::AtomicU64}, }; -use egui::Button; - use crate::{app_state::GameState, data::DataStore, frontend::world::Position, power::Watt}; pub(crate) struct WorldValueStore { @@ -32,6 +30,7 @@ impl Default for WorldValueStore { } } +#[cfg(feature = "client")] pub(crate) fn list_example_worlds( values: &mut WorldValueStore, ui: &mut egui::Ui, @@ -89,7 +88,7 @@ pub(crate) fn list_example_worlds( }; if ui - .add_enabled(allowed, Button::new("Create")) + .add_enabled(allowed, egui::Button::new("Create")) .on_disabled_hover_text(disabled_str) .clicked() { @@ -243,10 +242,10 @@ const WORLDS: LazyLock<[ExampleWorld; 5]> = LazyLock::new(|| { }; // Testing said ~100 bytes per solar panel - let expected_size = *count * 100 + 1_000_000_000; + let expected_size = (*count) as u64 * 100 + 1_000_000_000; - // Wasm only has 4GB of RAM - if expected_size > 4_000_000_000 { + // Wasm only has 4GB of RAM so limit to ~3GB + if expected_size > 3_000_000_000 { AllowedOnWasm::False(Some( "Generated World Size would exceed maximum memory on WASM, consider reducing panel count", )) diff --git a/src/frontend/action/mod.rs b/src/frontend/action/mod.rs index 4c6a000..3a0eeeb 100644 --- a/src/frontend/action/mod.rs +++ b/src/frontend/action/mod.rs @@ -72,6 +72,9 @@ pub enum ActionType { }, Ping(Position), + + // TODO: Does this need args? + SpawnPlayer {}, } impl ActionType { @@ -109,6 +112,7 @@ impl ActionType None, ActionType::PlaceOre { pos, .. } => Some(*pos), ActionType::Ping(position) => Some(*position), + ActionType::SpawnPlayer { .. } => None, } } @@ -176,6 +180,8 @@ impl ActionType None, ActionType::PlaceOre { .. } => None, ActionType::Ping(_) => None, + + ActionType::SpawnPlayer { .. } => None, } } @@ -200,6 +206,8 @@ impl ActionType None, ActionType::PlaceOre { pos, .. } => Some([1, 1]), ActionType::Ping(_) => None, + + ActionType::SpawnPlayer { .. } => None, }) } } diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index f4c9695..01bfb30 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -4266,7 +4266,7 @@ impl World { @@ -4355,7 +4355,7 @@ impl World { if pos.contained_in(e.get_pos(), e.get_entity_size(data_store)) { diff --git a/src/lib.rs b/src/lib.rs index 77718e5..b006687 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -315,80 +315,6 @@ enum GameCreationInfo { FromBP(PathBuf), } -// match start_game_info { -// StartGameInfo::Load(path) => load(path) -// .map(|sg| { -// assert_eq!( -// sg.checksum, data_store.checksum, -// "A savegame can only be loaded with the EXACT same mods!" -// ); -// sg.game_state -// }) -// .unwrap(), -// StartGameInfo::LoadReadable(path) => load_readable(path) -// .map(|sg| { -// assert_eq!( -// sg.checksum, data_store.checksum, -// "A savegame can only be loaded with the EXACT same mods!" -// ); -// sg.game_state -// }) -// .unwrap(), -// StartGameInfo::Create { -// name, -// info, -// allow_overwrite, -// } => { -// assert!( -// name.is_ascii(), -// "For now only ASCII game names are allowed, since they are used as the filename" -// ); - -// if !allow_overwrite { -// log::error!( -// "Currently allow_overwrite is ignored and overwriting is always allowed!!!!" -// ); -// } - -// match info { -// GameCreationInfo::Empty => GameState::new(name, &data_store), -// GameCreationInfo::Megabase(use_solar_field) => { -// GameState::new_with_megabase( -// name, -// use_solar_field, -// progress, -// &data_store, -// ) -// }, -// GameCreationInfo::Gigabase(count) => { -// GameState::new_with_gigabase(name, count, progress, &data_store) -// }, -// GameCreationInfo::SolarField(wattage, base_pos) => { -// GameState::new_with_tons_of_solar( -// name, -// wattage, -// base_pos, -// None, -// progress, -// &data_store, -// ) -// }, -// GameCreationInfo::LotsOfBelts => { -// GameState::new_with_lots_of_belts(name, progress, &data_store) -// }, -// GameCreationInfo::TrainRide => { -// GameState::new_with_world_train_ride(name, progress, &data_store) -// }, - -// GameCreationInfo::FromBP(path) => { -// GameState::new_with_bp(name, &data_store, path) -// }, - -// _ => unimplemented!(), -// } -// }, -// } - #[cfg(feature = "client")] fn run_integrated_server( progress: Arc, @@ -403,11 +329,10 @@ fn run_integrated_server( let tick_counter: Arc = Arc::new(AtomicU64::new(0)); - let connections: Arc>> = Arc::default(); - + let (new_conn_send, new_conn_recv) = channel(); let cancel: Arc = Default::default(); if let Some(listen_addr) = listen_addr { - accept_continously(listen_addr, connections.clone(), cancel.clone()).unwrap(); + accept_continously(listen_addr, new_conn_send, cancel.clone()).unwrap(); } match data_store { @@ -430,7 +355,10 @@ fn run_integrated_server( GameInitData::IntegratedServer { game_state: game_state.clone(), tick_counter: tick_counter.clone(), - info: ServerInfo { connections }, + info: ServerInfo { + connections: vec![], + new_connection_recv: new_conn_recv, + }, action_state_machine: state_machine.clone(), inputs: recv, ui_actions: ui_recv, @@ -484,13 +412,14 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { // let progress = Default::default(); - let connections: Arc>> = Arc::default(); - let local_addr = "127.0.0.1:42069"; let cancel: Arc = Default::default(); log::warn!("Hosting on {}", &local_addr); - accept_continously(local_addr, connections.clone(), cancel.clone()).unwrap(); + + let (new_conn_send, new_conn_recv) = channel(); + + accept_continously(local_addr, new_conn_send, cancel.clone()).unwrap(); match data_store { data::DataStoreOptions::ItemU8RecipeU8(data_store) => { @@ -516,7 +445,10 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { let mut game = Game::new( GameInitData::DedicatedServer( game_state, - ServerInfo { connections }, + ServerInfo { + connections: vec![], + new_connection_recv: new_conn_recv, + }, Box::new(move || { cancel.store(true, Ordering::Relaxed); // This is a little hack. Our connection accept thread is stuck waiting for connections and will only exit if anything connects. @@ -543,7 +475,10 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { } #[cfg(feature = "client")] -fn run_client(remote_addr: SocketAddr) -> (LoadedGame, Arc, Sender) { +fn run_client( + remote_addr: SocketAddr, + game_state_sender: Sender<(LoadedGame, Arc, Sender)>, +) { // TODO: Do mod loading here let raw_data = get_raw_data_test(); let data_store = raw_data.process(); @@ -554,55 +489,44 @@ fn run_client(remote_addr: SocketAddr) -> (LoadedGame, Arc, Sender { let (send, recv) = channel(); - let game_state = Arc::new( - // FIXME: When running in client mode, we should download the gamestate from the server instead of loading it from disk - GameState::new("FIXME".to_string(), &data_store), - ); - - let state_machine: Arc>> = - Arc::new(Mutex::new(ActionStateMachine::new_from_gamestate( - 1, - &*game_state.world.lock(), - &*game_state.simulation_state.lock(), - &data_store, - ))); - let (ui_sender, ui_recv) = channel(); + let stop = Arc::new(AtomicBool::new(false)); + let m_stop = stop.clone(); + let m_data_store = data_store.clone(); + let data_store = Arc::new(Mutex::new(data_store)); + let m_tick_counter = tick_counter.clone(); let mut game = Game::new( GameInitData::Client { - game_state: game_state.clone(), - action_state_machine: state_machine.clone(), + game_state_start_fun: Box::new(move |game_state, state_machine| { + log::info!("GameState Recieved Successfully"); + + game_state_sender + .send(( + LoadedGame::ItemU8RecipeU8(LoadedGameSized { + state: game_state, + state_machine, + data_store, + ui_action_sender: ui_sender, + stop_update_thread: stop, + }), + tick_counter, + send, + )) + .unwrap(); + }), inputs: recv, - tick_counter: tick_counter.clone(), + tick_counter: m_tick_counter, info: ClientConnectionInfo { addr: remote_addr }, ui_actions: ui_recv, }, - &data_store, + &m_data_store, ) .expect("Could not start Game"); - let stop = Arc::new(AtomicBool::new(false)); - - let m_data_store = data_store.clone(); - let m_stop = stop.clone(); thread::spawn(move || { game.run(m_stop, &m_data_store); }); - - let data_store = Arc::new(Mutex::new(data_store)); - return ( - LoadedGame::ItemU8RecipeU8(LoadedGameSized { - state: game_state, - state_machine, - data_store, - ui_action_sender: ui_sender, - - stop_update_thread: stop, - }), - tick_counter, - send, - ); }, _ => todo!(), } diff --git a/src/multiplayer/connection_reciever_tcp.rs b/src/multiplayer/connection_reciever_tcp.rs index 1496483..398920c 100644 --- a/src/multiplayer/connection_reciever_tcp.rs +++ b/src/multiplayer/connection_reciever_tcp.rs @@ -1,6 +1,6 @@ use std::{ net::{TcpListener, TcpStream, ToSocketAddrs}, - sync::{Arc, atomic::AtomicBool}, + sync::{Arc, atomic::AtomicBool, mpsc::Sender}, thread, }; @@ -10,7 +10,7 @@ pub type ConnectionList = Arc>>; pub fn accept_continously( local_addr: impl ToSocketAddrs, - connections: ConnectionList, + new_connection: Sender, cancel: Arc, ) -> Result<(), std::io::Error> { let listener = TcpListener::bind(local_addr)?; @@ -18,12 +18,15 @@ pub fn accept_continously( for conn in listener.incoming() { match conn { Ok(conn) => { + log::info!("Got new connection"); if cancel.load(std::sync::atomic::Ordering::SeqCst) { return; } else { conn.set_nonblocking(true) .expect("Setting connectiong to nonblocking failed"); - connections.lock().push(conn); + new_connection + .send(conn) + .expect("Sending via channel failed"); } }, Err(err) => match err.kind() { diff --git a/src/multiplayer/mod.rs b/src/multiplayer/mod.rs index d371cf2..2d0e692 100644 --- a/src/multiplayer/mod.rs +++ b/src/multiplayer/mod.rs @@ -21,7 +21,7 @@ use server::{ActionSource, GameStateUpdateHandler, HandledActionConsumer}; use crate::frontend::action::action_state_machine::ActionStateMachine; use crate::{ TICKS_PER_SECOND_RUNSPEED, - app_state::{GameState, SimulationState}, + app_state::{AuxillaryData, GameState, SimulationState}, data::DataStore, frontend::{action::ActionType, input::Input, world::tile::World}, item::{IdxTrait, WeakIdxTrait}, @@ -80,14 +80,19 @@ pub struct ClientConnectionInfo { } pub struct ServerInfo { - pub connections: Arc>>, + pub connections: Vec, + pub new_connection_recv: Receiver, } pub enum GameInitData { #[cfg(feature = "client")] Client { - game_state: Arc>, - action_state_machine: Arc>>, + game_state_start_fun: Box< + dyn FnOnce( + Arc>, + Arc>>, + ), + >, inputs: Receiver, ui_actions: Receiver>, tick_counter: Arc, @@ -116,6 +121,11 @@ pub enum ExitReason { ConnectionDropped, } +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct PlayerIDInformation { + player_id: u16, +} + impl Game { pub fn new( init: GameInitData, @@ -124,14 +134,54 @@ impl Game { let stream = std::net::TcpStream::connect(info.addr)?; + + let mut buffer = vec![0; 1_000_000]; + let decoder = flate2::read::ZlibDecoder::new(stream); + + log::info!("Get player_id"); + let (PlayerIDInformation { player_id }, (decoder, _)) = + postcard::from_io((decoder, &mut buffer)) + .expect("Could not recieve PlayerIDInformation from server!"); + log::info!("Get simulation_state"); + let (simulation_state, (decoder, _)): ( + crate::get_size::Mutex>, + _, + ) = postcard::from_io((decoder, &mut buffer)) + .expect("Could not recieve Game State from server!"); + log::info!("Get world"); + let (world, (decoder, _)): (crate::get_size::Mutex>, _) = + postcard::from_io((decoder, &mut buffer)) + .expect("Could not recieve Game State from server!"); + log::info!("Get aux_data"); + let (aux_data, (decoder, _)): (crate::get_size::Mutex, _) = + postcard::from_io((decoder, &mut buffer)) + .expect("Could not recieve Game State from server!"); + + let game_state = Arc::new(GameState { + world, + simulation_state, + aux_data, + }); + + let action_state_machine = + Arc::new(Mutex::new(ActionStateMachine::new_from_gamestate( + player_id, + &*game_state.world.lock(), + &*game_state.simulation_state.lock(), + data_store, + ))); + + let stream = decoder.into_inner(); + + game_state_start_fun(game_state.clone(), action_state_machine.clone()); + Ok(Self::Client( game_state, GameStateUpdateHandler::new(Client { @@ -202,7 +252,17 @@ impl Game return e, } - if !env::var("ZOOM").is_ok() { + let is_client = { + #[cfg(feature = "client")] + { + matches!(self, Game::Client(_, _, _)) + } + #[cfg(not(feature = "client"))] + { + false + } + }; + if !env::var("ZOOM").is_ok() && !is_client { profiling::scope!("Wait"); update_interval.tick(); } @@ -261,6 +321,7 @@ impl _current_tick: u64, _: &World, _: &SimulationState, + _: &AuxillaryData, _: &DataStore, ) -> impl Iterator> + use<'a, ItemIdxType, RecipeIdxType> { diff --git a/src/multiplayer/plumbing.rs b/src/multiplayer/plumbing.rs index cb0b202..42c5ee0 100644 --- a/src/multiplayer/plumbing.rs +++ b/src/multiplayer/plumbing.rs @@ -7,7 +7,12 @@ use std::{ time::{Duration, Instant}, }; -use crate::{app_state::SimulationState, frontend::world::Position}; +use crate::{ + app_state::{AuxillaryData, SimulationState}, + frontend::{action::belt_placement::FakeGameState, world::Position}, + multiplayer::PlayerIDInformation, +}; +use flate2::Compression; use log::error; use parking_lot::Mutex; @@ -34,7 +39,9 @@ pub(crate) struct Client } pub(crate) struct Server { - client_connections: ConnectionList, + client_connections: Mutex>, + new_connections: Receiver, + accepted_connections: Mutex>, item: PhantomData, recipe: PhantomData, @@ -43,7 +50,10 @@ pub(crate) struct Server impl Server { pub fn new(info: ServerInfo) -> Self { Self { - client_connections: info.connections, + client_connections: Mutex::new(info.connections), + new_connections: info.new_connection_recv, + accepted_connections: Mutex::new(vec![]), + item: PhantomData, recipe: PhantomData, } @@ -54,11 +64,12 @@ impl Server ActionSource for Client { - fn get<'a, 'b, 'c, 'd>( + fn get<'a, 'b, 'c, 'd, 'e>( &'a self, _current_tick: u64, world: &'b World, sim_state: &'d SimulationState, + aux_data: &'e AuxillaryData, data_store: &'c DataStore, ) -> impl Iterator> + use<'a, 'b, 'c, 'd, ItemIdxType, RecipeIdxType> { @@ -68,6 +79,8 @@ impl ActionSource = state_machine @@ -84,9 +97,22 @@ impl ActionSource, _) = - postcard::from_io((&self.server_connection, &mut buffer)).unwrap(); + // FIXME: This sucks + let mut buffer: Vec = vec![0; 1_000_000]; + + let flavor = + postcard::de_flavors::io::io::IOReader::new(&self.server_connection, &mut buffer); + + let mut deserializer = postcard::Deserializer::from_flavor(flavor); + + let recieved_actions: Vec<_> = match serde_path_to_error::deserialize(&mut deserializer) { + Ok(actions) => actions, + Err(err) => { + let path = err.path().to_string(); + log::warn!("Failed to recieve actions. Path \"{}\" failed!", path); + vec![] + }, + }; recieved_actions.into_iter() } @@ -111,11 +137,13 @@ impl ActionSource, - _sim_state: &SimulationState, + world: &World, + sim_state: &SimulationState, + aux_data: &AuxillaryData, _data_store: &DataStore, ) -> impl Iterator> + use { + log::trace!("Server::Get"); const RECV_BUFFER_LEN: usize = 10_000; let start = Instant::now(); // This is the Server, it will just keep on chugging along and never block @@ -171,7 +199,68 @@ impl ActionSource ) { let actions: Vec<_> = actions.into_iter().collect(); // Send the actions to the clients - for conn in self.client_connections.lock().iter() { - postcard::to_io(&actions, conn).expect("tcp send failed"); - } + self.client_connections.lock().retain(|conn| { + let keep = postcard::to_io(&actions, conn).is_ok(); + + // let keep = serde_json::to_writer(conn, &actions).is_ok(); + + if !keep { + log::warn!("Dropping Connection"); + } + + keep + }); } } @@ -203,11 +300,12 @@ pub(crate) struct IntegratedServer ActionSource for IntegratedServer { - fn get<'a, 'b, 'c, 'd>( + fn get<'a, 'b, 'c, 'd, 'e>( &'a self, current_tick: u64, world: &'b World, sim_state: &'d SimulationState, + aux_data: &'e AuxillaryData, data_store: &'c DataStore, ) -> impl Iterator> + use<'a, 'b, 'c, 'd, ItemIdxType, RecipeIdxType> { @@ -234,7 +332,7 @@ impl ActionSource Duration::from_millis(10) { diff --git a/src/multiplayer/server.rs b/src/multiplayer/server.rs index 8777f76..829c326 100644 --- a/src/multiplayer/server.rs +++ b/src/multiplayer/server.rs @@ -3,7 +3,7 @@ use std::{borrow::Borrow, fs::File, io::Write, marker::PhantomData}; #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] use crate::saving::save_with_fork; use crate::{ - app_state::{GameState, SimulationState}, + app_state::{AuxillaryData, GameState, SimulationState}, data::DataStore, frontend::{action::ActionType, world::tile::World}, item::{IdxTrait, WeakIdxTrait}, @@ -23,11 +23,12 @@ where } pub(super) trait ActionSource { - fn get<'a, 'b, 'c, 'd>( + fn get<'a, 'b, 'c, 'd, 'e>( &'a self, current_tick: u64, world: &'b World, sim_state: &'d SimulationState, + aux_data: &'e AuxillaryData, data_store: &'c DataStore, ) -> impl Iterator> + use<'a, 'b, 'c, 'd, Self, ItemIdxType, RecipeIdxType>; @@ -72,14 +73,16 @@ impl< replay: Option<&mut Replay>, data_store: &DataStore, ) { + log::trace!("Start Update"); + let mut simulation_state = game_state.simulation_state.lock(); let mut world = game_state.world.lock(); + let aux_data = &mut *game_state.aux_data.lock(); { profiling::scope!("GameState Update"); - let aux_data = &mut *game_state.aux_data.lock(); // TODO: Autosave interval - if aux_data.current_tick % (60 * 60 * 5) == 0 { + if aux_data.current_tick % (60 * 60 * 1) == 0 { // Autosave #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] { @@ -98,14 +101,20 @@ impl< GameState::update(&mut *simulation_state, aux_data, data_store); } + log::trace!("Post Autosave"); - let current_tick = game_state.aux_data.lock().current_tick; + let current_tick = aux_data.current_tick; let actions_iter = { profiling::scope!("Get Actions"); - self.action_interface - .get(current_tick, &world, &simulation_state, data_store) + self.action_interface.get( + current_tick, + &world, + &simulation_state, + &aux_data, + data_store, + ) }; let actions: Vec<_> = actions_iter.into_iter().collect(); diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index bf5db00..3095b9b 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -453,7 +453,7 @@ impl eframe::App for App { let progress = Arc::new(AtomicU64::new(0f64.to_bits())); let (send, recv) = channel(); - send.send(run_client(ip)).expect("Channel send failed"); + run_client(ip, send); self.state = AppState::Loading { start_time: Instant::now(), @@ -542,6 +542,29 @@ impl eframe::App for App { new_game_name, } => { CentralPanel::default().show(ctx, |ui| { + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + ui.vertical_centered(|ui|{ + ui.label( + egui::RichText::new("Detected running in a browser(WASM). Performance might be significantly degraded, and/or features might not work correctly. Support is on a best effort basis.") + .heading() + .color(egui::Color32::RED), + ); + ui.label( + egui::RichText::new("For the best experience run on native.") + .heading() + .color(egui::Color32::RED), + ); + }); + + #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] + ui.vertical_centered(|ui|{ + ui.label( + egui::RichText::new("Creating a game on WASM might freeze the browser tab for some time. Do not worry if after clicking \'Create\' the tab freezes and your browser warns that \"This page is slowing down your browser\"") + .heading() + .color(egui::Color32::YELLOW), + ); + }); + if ui.button("Back to Main Menu").clicked() { self.state = AppState::MainMenu { in_ip_box: None }; return; @@ -565,15 +588,7 @@ impl eframe::App for App { }); #[cfg(target_arch = "wasm32")] - send.send(run_integrated_server( - progress_send, - StartGameInfo::Create { - name: new_game_name, - info: GameCreationInfo::Empty, - allow_overwrite: true, - }, - None, - )); + send.send(run_integrated_server(progress_send, creation_fn, None)); self.state = AppState::Loading { start_time: Instant::now(), diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 67361ec..8220a64 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -1471,7 +1471,7 @@ pub fn render_world( animation_frame: 0, }, ); - info!( + log::trace!( "Rendering other player {} at {:?}", player_id, [player.pos.0 - camera_pos.0, player.pos.1 - camera_pos.1,] diff --git a/src/saving/mod.rs b/src/saving/mod.rs index b3afa51..966f2e5 100644 --- a/src/saving/mod.rs +++ b/src/saving/mod.rs @@ -268,6 +268,15 @@ pub fn save_components_fork_safe let checksum = &data_store.checksum; let save_dir = save_folder(); + // FIXME: This could lock forever + let lockfile = loop { + if let Ok(lockfile) = + lockfile::Lockfile::create_with_parents(save_dir.join("save_in_progress")) + { + break lockfile; + } + }; + create_dir_all(&save_dir).expect("Could not create data dir"); // FIXME: Allocation and chrono is prob illegal after a fork @@ -292,6 +301,8 @@ pub fn save_components_fork_safe save_dir.join("autosave.save") }; + assert_ne!(temp_file_dir, save_dir); + create_dir_all(&temp_file_dir).expect("Could not create temp dir"); { @@ -378,6 +389,8 @@ pub fn save_components_fork_safe // Remove old save if it exists let _ = std::fs::remove_dir_all(&save_file_dir); std::fs::rename(temp_file_dir, save_file_dir).expect("Could not rename tmp save dir!"); + + lockfile.release().expect("Failed to remove lockfile"); } /// # Panics From 359578119c08a5cc730b2a42408acfd6bedd3842 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 12 Jan 2026 05:54:13 +0100 Subject: [PATCH 084/152] Fix cargo hash --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 5860119..6cf8790 100644 --- a/flake.nix +++ b/flake.nix @@ -39,7 +39,7 @@ src = ./.; buildInputs = neededPackages; nativeBuildInputs = [ pkgs.pkg-config pkgs.makeWrapper ]; - cargoHash = "sha256-AQ6UxUS0DGK+v695XvqX81r/5pGfCsQdzLP70kAOcME="; + cargoHash = "sha256-/iACDjmjwgN4pB+2FawgACaToMejl4DIOIrGOWDGnPI="; # cargoLock.lockFile = ./Cargo.lock; doCheck = false; From 18485d8da15212c44a8254d9c415846e1e59831a Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 12 Jan 2026 06:08:46 +0100 Subject: [PATCH 085/152] Switch to listen on all adresses --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index b006687..f31b2b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -412,7 +412,7 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { // let progress = Default::default(); - let local_addr = "127.0.0.1:42069"; + let local_addr = "0.0.0.0:42069"; let cancel: Arc = Default::default(); log::warn!("Hosting on {}", &local_addr); From a5c09143e6d0c2e24c1388ef63e5441dfa69576c Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 12 Jan 2026 06:15:48 +0100 Subject: [PATCH 086/152] log error when connection fails --- src/multiplayer/plumbing.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/multiplayer/plumbing.rs b/src/multiplayer/plumbing.rs index 42c5ee0..6fba5bb 100644 --- a/src/multiplayer/plumbing.rs +++ b/src/multiplayer/plumbing.rs @@ -217,24 +217,24 @@ impl ActionSource Date: Mon, 12 Jan 2026 06:29:48 +0100 Subject: [PATCH 087/152] Allow blocking durin player connect --- src/multiplayer/plumbing.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/multiplayer/plumbing.rs b/src/multiplayer/plumbing.rs index 6fba5bb..2d5f22e 100644 --- a/src/multiplayer/plumbing.rs +++ b/src/multiplayer/plumbing.rs @@ -214,6 +214,10 @@ impl ActionSource ActionSource Date: Mon, 12 Jan 2026 06:56:09 +0100 Subject: [PATCH 088/152] Flush the TCP Stream on the server --- src/multiplayer/plumbing.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/multiplayer/plumbing.rs b/src/multiplayer/plumbing.rs index 2d5f22e..7d332cf 100644 --- a/src/multiplayer/plumbing.rs +++ b/src/multiplayer/plumbing.rs @@ -1,5 +1,5 @@ use std::{ - io::Read, + io::{Read, Write}, marker::PhantomData, mem, net::TcpStream, @@ -216,6 +216,7 @@ impl ActionSource ActionSource ) { let actions: Vec<_> = actions.into_iter().collect(); // Send the actions to the clients - self.client_connections.lock().retain(|conn| { - let keep = postcard::to_io(&actions, conn).is_ok(); + self.client_connections.lock().retain(|mut conn| { + let mut keep = postcard::to_io(&actions, conn).is_ok(); + keep &= conn.flush().is_ok(); // let keep = serde_json::to_writer(conn, &actions).is_ok(); From 8a7aa1e7a318fd0ebc3c779a103df1e50a73f1ef Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 12 Jan 2026 07:33:00 +0100 Subject: [PATCH 089/152] Drop connection on error instead of panicking --- src/multiplayer/plumbing.rs | 90 +++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/src/multiplayer/plumbing.rs b/src/multiplayer/plumbing.rs index 7d332cf..bd3f788 100644 --- a/src/multiplayer/plumbing.rs +++ b/src/multiplayer/plumbing.rs @@ -79,8 +79,6 @@ impl ActionSource = state_machine @@ -153,48 +151,53 @@ impl ActionSource Duration::from_millis(10) { error!("buffer {:?}", start.elapsed()); } - let recieved_actions: Vec> = self - .client_connections - .lock() - .iter() - .flat_map(|mut conn| { - let start = Instant::now(); - let mut ret = vec![]; - - match conn.peek(&mut buffer) { - Ok(len) => { - let mut written_buffer = &buffer[0..len]; - - loop { - if let Ok((v, rest)) = postcard::take_from_bytes(written_buffer) { - let consumed_len = written_buffer.len() - rest.len(); - written_buffer = rest; - std::io::copy( - &mut std::io::Read::by_ref(&mut conn).take(consumed_len as u64), - &mut std::io::sink(), - ) - .expect("Discarding used bytes failed"); - ret.push(v); - } else { - if written_buffer.len() == RECV_BUFFER_LEN { - error!("RECV_BUFFER_LEN exhausted!"); - } - break; + let mut recieved_actions: Vec> = vec![]; + + self.client_connections.lock().retain(|mut conn| { + let start = Instant::now(); + let mut ret = vec![]; + + let keep = match conn.peek(&mut buffer) { + Ok(len) => { + let mut written_buffer = &buffer[0..len]; + + loop { + if let Ok((v, rest)) = postcard::take_from_bytes(written_buffer) { + let consumed_len = written_buffer.len() - rest.len(); + written_buffer = rest; + std::io::copy( + &mut std::io::Read::by_ref(&mut conn).take(consumed_len as u64), + &mut std::io::sink(), + ) + .expect("Discarding used bytes failed"); + ret.push(v); + } else { + if written_buffer.len() == RECV_BUFFER_LEN { + error!("RECV_BUFFER_LEN exhausted!"); } + break; } + } + + true + }, + Err(e) => match e.kind() { + std::io::ErrorKind::WouldBlock => { + // No data to read + true }, - Err(e) => match e.kind() { - std::io::ErrorKind::WouldBlock => { - // No data to read - }, - e => todo!("{:?}", e), + err => { + log::warn!("Dropping Connection: {:?}", err); + false }, - } + }, + }; - ret - }) - .collect(); + recieved_actions.extend(ret); + + keep + }); if start.elapsed() > Duration::from_millis(10) { error!("recieved_actions {:?}", start.elapsed()); } @@ -290,15 +293,16 @@ impl let actions: Vec<_> = actions.into_iter().collect(); // Send the actions to the clients self.client_connections.lock().retain(|mut conn| { - let mut keep = postcard::to_io(&actions, conn).is_ok(); + let mut keep = if let Err(err) = postcard::to_io(&actions, conn) { + log::warn!("Dropping Connection: {:?}", err); + false + } else { + true + }; keep &= conn.flush().is_ok(); // let keep = serde_json::to_writer(conn, &actions).is_ok(); - if !keep { - log::warn!("Dropping Connection"); - } - keep }); } From b7fd94e1f84814ee079da42fbd4b626f0409fd96 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 12 Jan 2026 23:08:25 +0100 Subject: [PATCH 090/152] Add the ability to queue research --- src/app_state.rs | 33 ++++++++++++++++--- src/blueprint/mod.rs | 3 +- src/frontend/action/mod.rs | 17 +++++++--- src/power/mod.rs | 12 ++++--- src/power/power_grid.rs | 4 +-- src/rendering/render_world.rs | 3 +- src/research.rs | 62 ++++++++++++++++++++++++++++------- 7 files changed, 104 insertions(+), 30 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 87a4db8..6cc82fb 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1228,14 +1228,37 @@ impl GameState { - game_state.simulation_state.tech_state.current_technology = *tech; + ActionType::AddResearchToQueue { tech } => { + // TODO: Do not allow adding techs which are done? + if !game_state + .simulation_state + .tech_state + .research_queue + .contains(tech) + { + game_state + .simulation_state + .tech_state + .research_queue + .push(*tech); + + // TODO: Do I want a max length for the queue? + }; + }, + ActionType::RemoveResearchFromQueue { tech } => { + game_state + .simulation_state + .tech_state + .research_queue + .retain(|queued_tech| queued_tech != tech); }, ActionType::CheatUnlockTechnology { tech } => { - if game_state.simulation_state.tech_state.current_technology == Some(*tech) { - game_state.simulation_state.tech_state.current_technology = None; - } + game_state + .simulation_state + .tech_state + .research_queue + .retain(|queued_tech| queued_tech != tech); game_state .simulation_state .tech_state diff --git a/src/blueprint/mod.rs b/src/blueprint/mod.rs index 0e71496..22b4c5f 100644 --- a/src/blueprint/mod.rs +++ b/src/blueprint/mod.rs @@ -315,7 +315,8 @@ impl BlueprintAction { amount: *amount, }, ActionType::Remove(_) => unimplemented!(), - ActionType::SetActiveResearch { .. } => unimplemented!(), + ActionType::AddResearchToQueue { .. } => unimplemented!(), + ActionType::RemoveResearchFromQueue { .. } => unimplemented!(), ActionType::CheatUnlockTechnology { .. } => unimplemented!(), ActionType::CheatRelockTechnology { .. } => unimplemented!(), ActionType::Ping(_) => unimplemented!(), diff --git a/src/frontend/action/mod.rs b/src/frontend/action/mod.rs index 3a0eeeb..767a9f5 100644 --- a/src/frontend/action/mod.rs +++ b/src/frontend/action/mod.rs @@ -53,8 +53,12 @@ pub enum ActionType { Remove(Position), - SetActiveResearch { - tech: Option, + AddResearchToQueue { + tech: Technology, + }, + + RemoveResearchFromQueue { + tech: Technology, }, CheatUnlockTechnology { @@ -107,7 +111,8 @@ impl ActionType Some(*pos), ActionType::SetChestSlotLimit { pos, .. } => Some(*pos), ActionType::Remove(position) => Some(*position), - ActionType::SetActiveResearch { .. } => None, + ActionType::AddResearchToQueue { .. } => None, + ActionType::RemoveResearchFromQueue { .. } => None, ActionType::CheatUnlockTechnology { .. } => None, ActionType::CheatRelockTechnology { .. } => None, ActionType::PlaceOre { pos, .. } => Some(*pos), @@ -175,7 +180,8 @@ impl ActionType None, ActionType::SetChestSlotLimit { .. } => None, ActionType::Remove(_) => None, - ActionType::SetActiveResearch { .. } => None, + ActionType::AddResearchToQueue { .. } => None, + ActionType::RemoveResearchFromQueue { .. } => None, ActionType::CheatUnlockTechnology { .. } => None, ActionType::CheatRelockTechnology { .. } => None, ActionType::PlaceOre { .. } => None, @@ -201,7 +207,8 @@ impl ActionType Some([1, 1]), ActionType::SetChestSlotLimit { .. } => Some([1, 1]), ActionType::Remove(_) => Some([1, 1]), - ActionType::SetActiveResearch { .. } => None, + ActionType::AddResearchToQueue { .. } => None, + ActionType::RemoveResearchFromQueue { .. } => None, ActionType::CheatUnlockTechnology { .. } => None, ActionType::CheatRelockTechnology { .. } => None, ActionType::PlaceOre { pos, .. } => Some([1, 1]), diff --git a/src/power/mod.rs b/src/power/mod.rs index 3199bbb..30dcc97 100644 --- a/src/power/mod.rs +++ b/src/power/mod.rs @@ -1302,10 +1302,14 @@ impl PowerGridStorage PowerGrid, + pub research_queue: Vec, + // Map from technologies to how many times they were completed pub finished_technologies: HashMap, pub in_progress_technologies: HashMap, @@ -93,7 +94,7 @@ impl TechState { .collect(); Self { - current_technology: None, + research_queue: vec![], finished_technologies, recipe_active, @@ -120,7 +121,7 @@ impl TechState { return; } - if let Some(current) = &self.current_technology { + if let Some(current) = self.research_queue.first() { let (tech_cost_units, tech_cost_items) = &data_store.technology_costs[current.id as usize]; let mut tech_cost_units = *tech_cost_units; @@ -212,7 +213,7 @@ impl TechState { if is_repeating { // Just keep researching the same tech (just one level higher) } else { - self.current_technology = None; + self.research_queue.remove(0); } // Since we only check if a tech is finished at the end of each update, it is possible we produced more science progress in this tick, than was required. @@ -329,6 +330,10 @@ impl TechState { data_store: &DataStore, ) -> impl Iterator> + use { + use itertools::Itertools; + + debug_assert!(self.research_queue.iter().all_unique()); + { profiling::scope!("Update Tech Tree colors"); for tech in 0..data_store.technology_costs.len() { @@ -395,7 +400,7 @@ impl TechState { SidePanel::new(egui::panel::Side::Left, "Current Technology Info Sidepanel").show_inside( ui, |ui| { - if let Some(tech) = &self.current_technology { + if let Some(tech) = self.research_queue.first() { let is_repeating = data_store .technology_tree .node_weight(NodeIndex::new(tech.id.into())) @@ -463,10 +468,44 @@ impl TechState { ); if ui.button("Cancel").clicked() { - ret.push(ActionType::SetActiveResearch { tech: None }); + ret.push(ActionType::RemoveResearchFromQueue { tech: *tech }); } else if ui.button("[CHEAT] Unlock Technology").clicked() { ret.push(ActionType::CheatUnlockTechnology { tech: *tech }); } + + ui.separator(); + + use egui_extras::{Column, TableBuilder}; + + TableBuilder::new(ui) + .column(Column::remainder()) + .column(Column::auto()) + .id_salt("Research Queue") + .body(|body| { + body.rows(1.0, self.research_queue.len() - 1, |mut row| { + let idx = row.index(); + + let tech = &self.research_queue[idx + 1]; + + row.col(|ui| { + ui.label( + &data_store + .technology_tree + .node_weight(NodeIndex::from(tech.id)) + .unwrap() + .name, + ); + }); + + row.col(|ui| { + if ui.button("X").clicked() { + ret.push(ActionType::RemoveResearchFromQueue { + tech: *tech, + }); + } + }); + }); + }); } }, ); @@ -529,10 +568,9 @@ impl TechState { .unwrap_or(0) > 0 }); - - let is_currently_researching = Some(Technology { + let is_currently_researching = self.research_queue.contains(&Technology { id: selected_node.index().try_into().unwrap(), - }) == self.current_technology; + }); if ui .add_enabled( @@ -541,10 +579,10 @@ impl TechState { ) .clicked() { - ret.push(ActionType::SetActiveResearch { - tech: Some(Technology { + ret.push(ActionType::AddResearchToQueue { + tech: Technology { id: selected_node.index().try_into().unwrap(), - }), + }, }); } From 20e49b307b1e6b32e1542f86f7c7a11ee2d1ca64 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 13 Jan 2026 03:09:16 +0100 Subject: [PATCH 091/152] Have example worlds research techs needed --- src/example_worlds/mod.rs | 77 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/src/example_worlds/mod.rs b/src/example_worlds/mod.rs index 85cd879..fcfa299 100644 --- a/src/example_worlds/mod.rs +++ b/src/example_worlds/mod.rs @@ -1,9 +1,16 @@ use std::{ + iter, ops::RangeInclusive, sync::{LazyLock, atomic::AtomicU64}, }; -use crate::{app_state::GameState, data::DataStore, frontend::world::Position, power::Watt}; +use crate::{ + app_state::GameState, + data::DataStore, + frontend::{action::ActionType, world::Position}, + power::Watt, + research::Technology, +}; pub(crate) struct WorldValueStore { name_field: String, @@ -182,7 +189,38 @@ const WORLDS: LazyLock<[ExampleWorld; 5]> = LazyLock::new(|| { unreachable!(); }; - GameState::new_with_megabase(name, *use_solar_field, progress, data_store) + let gs = GameState::new_with_megabase(name, *use_solar_field, progress, data_store); + + let techs = 0..data_store.technology_costs.len(); + + let cheat_unlocks = techs.map(|id| ActionType::CheatUnlockTechnology { + tech: crate::research::Technology { + id: id.try_into().unwrap(), + }, + }); + + let mining_prod_id = data_store + .technology_tree + .node_indices() + .find_map(|node| { + let tech = data_store.technology_tree.node_weight(node).unwrap(); + + (tech.name == "Mining Productivity").then_some(node.index()) + }) + .unwrap(); + + GameState::apply_actions( + &mut *gs.simulation_state.lock(), + &mut *gs.world.lock(), + cheat_unlocks.chain(iter::once(ActionType::AddResearchToQueue { + tech: Technology { + id: mining_prod_id.try_into().unwrap(), + }, + })), + data_store, + ); + + gs }, }, ExampleWorld { @@ -208,12 +246,43 @@ const WORLDS: LazyLock<[ExampleWorld; 5]> = LazyLock::new(|| { unreachable!(); }; - GameState::new_with_gigabase( + let gs = GameState::new_with_gigabase( name, (*count).try_into().unwrap(), progress, data_store, - ) + ); + + let techs = 0..data_store.technology_costs.len(); + + let cheat_unlocks = techs.map(|id| ActionType::CheatUnlockTechnology { + tech: crate::research::Technology { + id: id.try_into().unwrap(), + }, + }); + + let mining_prod_id = data_store + .technology_tree + .node_indices() + .find_map(|node| { + let tech = data_store.technology_tree.node_weight(node).unwrap(); + + (tech.name == "Mining Productivity").then_some(node.index()) + }) + .unwrap(); + + GameState::apply_actions( + &mut *gs.simulation_state.lock(), + &mut *gs.world.lock(), + cheat_unlocks.chain(iter::once(ActionType::AddResearchToQueue { + tech: Technology { + id: mining_prod_id.try_into().unwrap(), + }, + })), + data_store, + ); + + gs }, }, ExampleWorld { From 16efc085bcca8961e645fe5506c6bdd13c074021 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 13 Jan 2026 03:44:49 +0100 Subject: [PATCH 092/152] Add more statistics time scales --- src/statistics/mod.rs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs index 54c31df..3d26e8b 100644 --- a/src/statistics/mod.rs +++ b/src/statistics/mod.rs @@ -23,7 +23,7 @@ pub mod recipe; pub mod research; pub mod time_usage; -pub const NUM_DIFFERENT_TIMESCALES: usize = 5; +pub const NUM_DIFFERENT_TIMESCALES: usize = 8; pub const SAMPLES_FOR_SMOOTHING_BASE: usize = 600; const SAMPLES_FOR_SMOOTHING: [usize; NUM_DIFFERENT_TIMESCALES] = { @@ -41,7 +41,8 @@ const SAMPLES_FOR_SMOOTHING: [usize; NUM_DIFFERENT_TIMESCALES] = { } b }; -pub const NUM_SAMPLES_AT_INTERVALS: [usize; NUM_DIFFERENT_TIMESCALES] = [600, 60, 60, 60, 50]; +pub const NUM_SAMPLES_AT_INTERVALS: [usize; NUM_DIFFERENT_TIMESCALES] = + [600, 60, 60, 60, 60, 50, 50, 50]; pub const NUM_SAMPLES_AT_INTERVALS_STORED: [usize; NUM_DIFFERENT_TIMESCALES] = { let mut b = [0; NUM_DIFFERENT_TIMESCALES]; let mut i = 0; @@ -51,18 +52,29 @@ pub const NUM_SAMPLES_AT_INTERVALS_STORED: [usize; NUM_DIFFERENT_TIMESCALES] = { } b }; -pub const NUM_X_AXIS_TICKS: [usize; NUM_DIFFERENT_TIMESCALES] = [10, 6, 6, 10, 10]; -pub const RELATIVE_INTERVAL_MULTS: [usize; NUM_DIFFERENT_TIMESCALES] = [1, 60, 60, 10, 5]; - -pub const TIMESCALE_NAMES: [&'static str; NUM_DIFFERENT_TIMESCALES] = - ["10 seconds", "1 minute", "1 hour", "10 hours", "50 hours"]; +pub const NUM_X_AXIS_TICKS: [usize; NUM_DIFFERENT_TIMESCALES] = [10, 6, 10, 6, 10, 10, 10, 10]; +pub const RELATIVE_INTERVAL_MULTS: [usize; NUM_DIFFERENT_TIMESCALES] = [1, 60, 10, 6, 10, 5, 5, 4]; + +pub const TIMESCALE_NAMES: [&'static str; NUM_DIFFERENT_TIMESCALES] = [ + "10 seconds", + "1 minute", + "10 minute", + "1 hour", + "10 hours", + "50 hours", + "250 hours", + "1000 hours", +]; pub const TIMESCALE_LEGEND: [fn(f64) -> String; NUM_DIFFERENT_TIMESCALES] = [ |t| format!("{:.0}s", t / 60.0), |t| format!("{:.0}s", t), + |t| format!("{:.0}m", t / 6.0), |t| format!("{:.0}m", t), - |t| format!("{:.0}m", t * 10.0), + |t| format!("{:.0}h", t / 60.0), |t| format!("{:.0}h", t), + |t| format!("{:.0}h", t * 5.0), + |t| format!("{:.0}h", t * 20.0), ]; #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] From 6c111697511a57b1ce590c0dc8a59538df4ae690 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 13 Jan 2026 06:18:37 +0100 Subject: [PATCH 093/152] Tip Window --- src/frontend/action/action_state_machine.rs | 44 +++--- src/rendering/render_world.rs | 156 ++++++++++++-------- 2 files changed, 118 insertions(+), 82 deletions(-) diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index f0a9e76..9c0d3fe 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -8,6 +8,7 @@ use std::{ }; use egui_graphs::{DefaultEdgeShape, DefaultNodeShape, Graph}; +use enum_map::EnumMap; use itertools::Itertools; use log::{error, warn}; use petgraph::Directed; @@ -112,8 +113,6 @@ pub struct ActionStateMachine, @@ -133,8 +132,6 @@ pub struct ActionStateMachine, pub state: ActionStateMachineState, - pub escape_menu_open: bool, - pub debug_view_options: DebugViewOptions, pub zoom_level: f32, @@ -156,10 +153,20 @@ pub struct ActionStateMachine, pub hotbar: Hotbar, - pub hotbar_window_open: bool, pub last_tick_seen_for_autosave: u32, pub autosave_interval: u32, + + pub open_windows: EnumMap, +} + +#[derive(Debug, enum_map::Enum, PartialEq)] +pub(crate) enum Window { + Tip, + Technology, + Statistics, + Hotbar, + Escape, } #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] @@ -241,14 +248,14 @@ impl local_player_pos: (f32, f32), data_store: &DataStore, ) -> Self { + let open_windows = EnumMap::from_fn(|win| win == Window::Tip); + Self { my_player_id, local_player_pos, tech_tree_render: None, - statistics_panel_open: false, - technology_panel_open: true, statistics_panel: StatisticsPanel::default(), statistics_panel_locked_scale: false, production_filters: vec![true; data_store.item_display_names.len()], @@ -261,7 +268,6 @@ impl zoom_level: 1.0, map_view_info: None, - escape_menu_open: false, debug_view_options: DebugViewOptions { highlight_sushi_belts: false, sushi_belt_len_threshhold: 1, @@ -283,10 +289,11 @@ impl current_fork_save_in_progress: None, hotbar: Hotbar::new(data_store), - hotbar_window_open: true, last_tick_seen_for_autosave: 0, autosave_interval: (60 * TICKS_PER_SECOND_LOGIC) as u32, + + open_windows, } } @@ -298,6 +305,7 @@ impl data_store: &DataStore, ) -> Self { let player_pos = world.players[my_player_id as usize].pos; + let open_windows = EnumMap::from_fn(|win| win == Window::Tip); Self { my_player_id, @@ -305,8 +313,6 @@ impl tech_tree_render: None, - statistics_panel_open: false, - technology_panel_open: true, statistics_panel: StatisticsPanel::default(), statistics_panel_locked_scale: false, production_filters: vec![true; data_store.item_display_names.len()], @@ -319,7 +325,6 @@ impl zoom_level: 1.0, map_view_info: None, - escape_menu_open: false, debug_view_options: DebugViewOptions { highlight_sushi_belts: false, sushi_belt_len_threshhold: 1, @@ -341,10 +346,11 @@ impl current_fork_save_in_progress: None, hotbar: Hotbar::new(data_store), - hotbar_window_open: true, last_tick_seen_for_autosave: 0, autosave_interval: (60 * TICKS_PER_SECOND_LOGIC) as u32, + + open_windows, } } @@ -358,7 +364,7 @@ impl ) -> impl Iterator> + use<'a, 'b, 'c, 'd, 'e, ItemIdxType, RecipeIdxType, I> { input.into_iter().map(|input| { - if self.escape_menu_open && input != Input::KeyPress(Key::Esc) { + if self.open_windows[Window::Escape] && input != Input::KeyPress(Key::Esc) { match input { Input::KeyPress(key) => { self.current_held_keys.insert(key); @@ -930,7 +936,7 @@ impl }, (_, Key::E) => { - self.hotbar_window_open = !self.hotbar_window_open; + self.open_windows[Window::Hotbar] = !self.open_windows[Window::Hotbar]; vec![] }, @@ -1117,12 +1123,12 @@ impl }, (_, Key::P) => { - self.statistics_panel_open = !self.statistics_panel_open; + self.open_windows[Window::Statistics] = !self.open_windows[Window::Statistics]; vec![] }, (_, Key::T) => { - self.technology_panel_open = !self.technology_panel_open; + self.open_windows[Window::Technology] = !self.open_windows[Window::Technology]; vec![] }, @@ -1149,7 +1155,7 @@ impl }, (_, Key::Esc) => { - self.escape_menu_open = !self.escape_menu_open; + self.open_windows[Window::Escape] = !self.open_windows[Window::Escape]; vec![] }, @@ -1157,7 +1163,7 @@ impl }; // Do not send any actions if we are in the escape menu - if self.escape_menu_open { + if self.open_windows[Window::Escape] { vec![].into_iter() } else { ret.into_iter() diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 11df423..aaece2c 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -10,6 +10,7 @@ use crate::belt::smart::{ use crate::belt::smart::SmartBelt; use crate::blueprint::blueprint_string::BlueprintString; use crate::chest::ChestSize; +use crate::frontend::action::action_state_machine; #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] use crate::frontend::action::action_state_machine::ForkSaveInfo; use crate::frontend::action::place_entity::EntityPlaceOptions; @@ -2017,7 +2018,7 @@ pub fn render_ui< } } - if state_machine_ref.escape_menu_open { + if state_machine_ref.open_windows[action_state_machine::Window::Escape] { if let Some(escape_action) = Modal::new("Pause Window".into()) .show(ctx, |ui| { ui.heading("Paused"); @@ -2074,6 +2075,9 @@ pub fn render_ui< } } } + if ui.button("Reopen Tip Window").clicked() { + state_machine_ref.open_windows[action_state_machine::Window::Tip] = true; + } if ui.button("Main Menu").clicked() { return Some(EscapeMenuOptions::BackToMainMenu); } @@ -2132,13 +2136,35 @@ pub fn render_ui< }); // TODO: Make this conditional - let mut open = state_machine_ref.hotbar_window_open; + let mut open = state_machine_ref.open_windows[action_state_machine::Window::Hotbar]; Window::new("Customize Hotbar") .open(&mut open) .show(ctx, |ui| { state_machine_ref.hotbar_window(ui, data_store_ref); }); - state_machine_ref.hotbar_window_open = open; + state_machine_ref.open_windows[action_state_machine::Window::Hotbar] = open; + + let mut open = state_machine_ref.open_windows[action_state_machine::Window::Tip]; + Window::new("Tip").open(&mut open).show(ctx, |ui| { + ui.label("Use Mining Drills to mine Resources, Smelt them in Furnaces and Assemble them into Science Packs. These can then be used in Laboratories to research new technologies."); + ui.label("Inserters can pull items out of machines, drop them onto Conveyor Belts, or load them back into different machines."); + ui.label("Machines require power to run. Power Poles supply nearby machines and extract power from nearby power sources."); + + ui.separator(); + + ui.label("Use [WASD] to move around."); + ui.label("Press [E] to open the hotbar customization menu."); + ui.label("Press [P] to open your production statistics."); + ui.label("Press [T] to open the technology tree."); + ui.label("Press [M] to switch to map view."); + ui.label("Use the [Scroll Wheel] to zoom in or out."); + ui.label("Use [Q] to pipette the entity under your cursor."); + ui.label("Use [Left Click] to place a held entity or Blueprint or inspect an entity."); + ui.label("Hold [Right Click] to deconstruct an entity."); + ui.label("Press [Ctrl + C] and start dragging to copy an area into a Blueprint."); + }); + + state_machine_ref.open_windows[action_state_machine::Window::Tip] = open; Window::new("Size") .fixed_size(egui::vec2(1920f32, 1080f32)) @@ -2169,26 +2195,6 @@ pub fn render_ui< }); }); - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] - Window::new("Import BP") - .default_open(false) - .show(ctx, |ui| { - if ui.button("Import").clicked() { - if let Some(path) = rfd::FileDialog::new().pick_file() { - if let Ok(mut file) = File::open(path) { - let mut bp_string = BlueprintString(String::new()); - file.read_to_string(&mut bp_string.0) - .expect("Failed to read from file"); - - if let Ok(bp) = bp_string.try_into() { - state_machine_ref.state = - ActionStateMachineState::Holding(HeldObject::Blueprint(bp)); - } - } - } - } - }); - let tech_response = Window::new("Tech") .anchor(Align2::RIGHT_TOP, [0.0, 0.0]) .title_bar(false) @@ -2375,7 +2381,7 @@ pub fn render_ui< } if tech_response.clicked() { - state_machine_ref.technology_panel_open = true; + state_machine_ref.open_windows[action_state_machine::Window::Technology] = true; } Window::new("DEBUG USE WITH CARE") @@ -3345,46 +3351,70 @@ pub fn render_ui< }); }); - Window::new("BP").default_open(false).show(ctx, |ui| { - let bp = if let ActionStateMachineState::Holding(HeldObject::Blueprint(bp)) = - &state_machine_ref.state - { - Some(bp) - } else { - None - }; + Window::new("Blueprint") + .default_open(false) + .show(ctx, |ui| { + let is_wasm = cfg!(target_arch = "wasm32"); - if ui - .add_enabled(bp.is_some(), Button::new("Copy Blueprint String")) - .clicked() - { - let s: BlueprintString = bp.cloned().unwrap().into(); - ctx.copy_text(s.0); - } + if ui + .add_enabled(!is_wasm, Button::new("Import")) + .on_disabled_hover_text("Disabled on WASM for now") + .clicked() + { + #[cfg(not(target_arch = "wasm32"))] + if let Some(path) = rfd::FileDialog::new().pick_file() { + if let Ok(mut file) = File::open(path) { + let mut bp_string = BlueprintString(String::new()); + file.read_to_string(&mut bp_string.0) + .expect("Failed to read from file"); - if ui - .add_enabled(bp.is_some(), Button::new("Write Blueprint String to file")) - .clicked() - { - let s: BlueprintString = bp.cloned().unwrap().into(); - let mut file = File::create("saved.bp").unwrap(); - file.write(s.0.as_bytes()).unwrap(); - } + if let Ok(bp) = bp_string.try_into() { + state_machine_ref.state = + ActionStateMachineState::Holding(HeldObject::Blueprint(bp)); + } + } + } + } - if ui - .add_enabled( - bp.is_some(), - Button::new("Write Blueprint binary data to file"), - ) - .clicked() - { - let v: Vec = bitcode::serialize(bp.unwrap()).unwrap(); - let file = File::create("saved_binary.bp").unwrap(); - let mut encoder = ZlibEncoder::new(file, Compression::best()); - encoder.write_all(&v).unwrap(); - encoder.finish().unwrap(); - } - }); + let bp = if let ActionStateMachineState::Holding(HeldObject::Blueprint(bp)) = + &state_machine_ref.state + { + Some(bp) + } else { + None + }; + + if ui + .add_enabled(bp.is_some(), Button::new("Copy Blueprint String")) + .clicked() + { + let s: BlueprintString = bp.cloned().unwrap().into(); + ctx.copy_text(s.0); + } + + if ui + .add_enabled(bp.is_some(), Button::new("Write Blueprint String to file")) + .clicked() + { + let s: BlueprintString = bp.cloned().unwrap().into(); + let mut file = File::create("saved.bp").unwrap(); + file.write(s.0.as_bytes()).unwrap(); + } + + if ui + .add_enabled( + bp.is_some(), + Button::new("Write Blueprint binary data to file"), + ) + .clicked() + { + let v: Vec = bitcode::serialize(bp.unwrap()).unwrap(); + let file = File::create("saved_binary.bp").unwrap(); + let mut encoder = ZlibEncoder::new(file, Compression::best()); + encoder.write_all(&v).unwrap(); + encoder.finish().unwrap(); + } + }); Window::new("RawData").default_open(false).show(ctx, |ui| { let raw = get_raw_data_test(); @@ -4037,7 +4067,7 @@ pub fn render_ui< Window::new("Technology") .collapsible(false) - .open(&mut state_machine_ref.technology_panel_open) + .open(&mut state_machine_ref.open_windows[action_state_machine::Window::Technology]) .show(ctx, |ui| { let research_actions = game_state_ref .simulation_state @@ -4058,7 +4088,7 @@ pub fn render_ui< Window::new("Statistics") .collapsible(false) - .open(&mut state_machine_ref.statistics_panel_open) + .open(&mut state_machine_ref.open_windows[action_state_machine::Window::Statistics]) .show(ctx, |ui| { let time_scale = match &mut state_machine_ref.statistics_panel { StatisticsPanel::Items(timescale) => timescale, From 41eec64d5daa368e1300dc40828b87b13425d4b8 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 14 Jan 2026 15:38:36 +0100 Subject: [PATCH 094/152] Disable saves on wasm --- src/rendering/eframe_app.rs | 1 + src/rendering/render_world.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index 3095b9b..c2ad8d8 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -760,6 +760,7 @@ impl App { }, Err(escape) => match escape { EscapeMenuOptions::BackToMainMenu => { + #[cfg(not(target_arch = "wasm32"))] save( "Last Exit", Some("last_exit.save"), diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index aaece2c..eb8e7c2 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -2022,7 +2022,14 @@ pub fn render_ui< if let Some(escape_action) = Modal::new("Pause Window".into()) .show(ctx, |ui| { ui.heading("Paused"); - if ui.button("Save").clicked() { + + let in_wasm = cfg!(target_arch = "wasm32"); + + if ui + .add_enabled(!in_wasm, Button::new("Save")) + .on_disabled_hover_text("I currently do not support saving in the Browser yet") + .clicked() + { save_components( &aux_data.game_name, Some(&format!("{}.save", &aux_data.game_name)), From d5ea1ce721f1984ca05b42b402a3475bb0ee1a32 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 3 Feb 2026 21:18:22 +0100 Subject: [PATCH 095/152] Fix saving issues and use std::File::lock for lockfile --- .gitignore | 3 +- .vscode/extensions.json | 7 - Cargo.lock | 7 - Cargo.toml | 1 - flake.lock | 12 +- src/assembler/simd.rs | 30 ++-- src/belt/mod.rs | 165 +++++++++++++++++- src/data/factorio_1_1.fgmod | 2 +- .../world/sparse_grid/bounding_box_grid.rs | 75 ++++---- src/frontend/world/tile.rs | 71 ++++++-- src/lib.rs | 2 + src/lockfile/mod.rs | 57 ++++++ src/multiplayer/mod.rs | 32 +++- src/multiplayer/server.rs | 20 +-- src/power/power_grid.rs | 2 +- src/rendering/eframe_app.rs | 56 +++--- src/research.rs | 3 +- src/saving/loading.rs | 19 +- src/saving/mod.rs | 33 ++-- 19 files changed, 444 insertions(+), 153 deletions(-) delete mode 100644 .vscode/extensions.json create mode 100644 src/lockfile/mod.rs diff --git a/.gitignore b/.gitignore index 8c907ba..e03cb19 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ dhat-heap.json samply.json vtune-results profile.json.gz -dist \ No newline at end of file +dist +result \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index abac346..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "gruntfuggly.todo-tree", - "eamodio.gitlens", - "spencerwmiles.vscode-task-buttons" - ] -} diff --git a/Cargo.lock b/Cargo.lock index 9ae31ee..687b4d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1750,7 +1750,6 @@ dependencies = [ "interprocess", "itertools", "libc", - "lockfile", "log", "memoffset", "mimalloc", @@ -2896,12 +2895,6 @@ dependencies = [ "serde", ] -[[package]] -name = "lockfile" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be1cf190319c74ba3e45923624626ae2e43fe42ad7e60ff38ded81044c37630" - [[package]] name = "log" version = "0.4.29" diff --git a/Cargo.toml b/Cargo.toml index 8226415..0f9d9b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,6 @@ rand_xoshiro = "0.7.0" url = "2.5.7" args = "2.2.0" console_error_panic_hook = "0.1.7" -lockfile = "0.4.0" # These are all the dependencies which do not work on wasm [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] diff --git a/flake.lock b/flake.lock index 185d316..ba94eac 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1767596244, - "narHash": "sha256-P4NRZUjYbeuzv4hGrXxfdg0QpdGVoeNn0CMmzIyr398=", + "lastModified": 1770102568, + "narHash": "sha256-VYwA9FmakKJ3zLfAd7bdj9xIB9PzfISLoYh6eZl+EuQ=", "owner": "nix-community", "repo": "fenix", - "rev": "eedfb5a27900e82ec0390acc83d4d226ce86e714", + "rev": "592daa37b5a3175c61541329b64d6c1972303bc1", "type": "github" }, "original": { @@ -46,11 +46,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1767551763, - "narHash": "sha256-lcA/e3++3aZQSj6xCsBi2VpYyC3Q+oO/oukgfHiL+Ts=", + "lastModified": 1770026591, + "narHash": "sha256-VZlloygYDmozJwbZZkCSNpiPhNdOW/AA0b6LmNBZ3xU=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "6a1246b69ca761480b9278df019f717b549cface", + "rev": "74eca73f3b0a41b80228b8e499c7547cc8b2effa", "type": "github" }, "original": { diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index 4b96329..4b717ad 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -337,10 +337,10 @@ impl power_list: &[AssemblerInfo], ) -> (Watt, u32, u32) { // FIXME: This could technically not be enough if enough items are produced in a single tick - self.inserter_waitlist_output_vec.reserve( - (self.len * 4 * (NUM_INGS + NUM_OUTPUTS)) - .saturating_sub(self.inserter_waitlist_output_vec.len()), - ); + // self.inserter_waitlist_output_vec.reserve( + // (self.len * 4 * (NUM_INGS + NUM_OUTPUTS)) + // .saturating_sub(self.inserter_waitlist_output_vec.len()), + // ); let (ing_idx, out_idx) = recipe_lookup[self.recipe.id.into()]; @@ -603,10 +603,9 @@ impl .map(|v| v.take()) .unwrap_or(None); } - let Ok(_) = self - .inserter_waitlist_output_vec - .push_within_capacity( - InternalInserterReinsertionInfo { + let () = + self.inserter_waitlist_output_vec + .push(InternalInserterReinsertionInfo { movetime: ins.movetime.into(), item: (NUM_INGS + item) as u8, max_hand: ins.max_hand.into(), @@ -627,8 +626,8 @@ impl self_is_source, }, }, - }, - ) else { + }) + else { panic!( "Not enough space in inserter readdition vec. Capacity is {}", self.inserter_waitlist_output_vec.capacity() @@ -702,10 +701,9 @@ impl .map(|v| v.take()) .unwrap_or(None); } - let Ok(_) = self - .inserter_waitlist_output_vec - .push_within_capacity( - InternalInserterReinsertionInfo { + let () = + self.inserter_waitlist_output_vec + .push(InternalInserterReinsertionInfo { movetime: ins.movetime.into(), item: item as u8, max_hand: ins.max_hand.into(), @@ -726,8 +724,8 @@ impl self_is_source, }, }, - }, - ) else { + }) + else { panic!( "Not enough space in inserter readdition vec. Capacity is {}.", self.inserter_waitlist_output_vec.capacity() diff --git a/src/belt/mod.rs b/src/belt/mod.rs index 2ae47ba..438304b 100644 --- a/src/belt/mod.rs +++ b/src/belt/mod.rs @@ -19,6 +19,7 @@ use crate::par_generation::BeltKind; use petgraph::stable_graph::DefaultIx; use std::num::NonZero; use std::ops::RangeInclusive; +use std::u32; use std::{ cell::UnsafeCell, collections::{HashMap, HashSet}, @@ -3799,8 +3800,168 @@ impl BeltStore { /// Remove the Splitter from the update list and its connections to belts. /// Does NOT remove any length of belt originally associated with a splitter world entity! - pub fn remove_splitter(&mut self, tile_id: SplitterTileId) { - todo!() + pub fn remove_splitter(&mut self, tile_id: SplitterTileId) -> Vec<(Item, u32)> { + match tile_id { + SplitterTileId::Any(idx) => { + self.any_splitter_holes.push(idx); + + let removed_items = match self.any_splitters[idx as usize] { + AnySplitter::Pure(item, idx) => { + let id = SplitterID { index: idx as u32 }; + + let removed_items = self.inner.smart_belts[item.into_usize()] + .belts_mut() + .flat_map(|belt| { + let removed_items_front = + if let Some((splitter, _)) = belt.input_splitter { + if splitter == id { + belt.input_splitter = None; + } + + let (removed_items, _new_len) = + belt.remove_length(SPLITTER_BELT_LEN, Side::BACK); + Some(removed_items) + } else { + None + }; + + let removed_items_back = + if let Some((splitter, _)) = belt.output_splitter { + if splitter == id { + belt.output_splitter = None; + } + + let (removed_items, _new_len) = + belt.remove_length(SPLITTER_BELT_LEN, Side::FRONT); + Some(removed_items) + } else { + None + }; + + removed_items_front.into_iter().chain(removed_items_back) + }) + .collect_vec(); + + assert_eq!(removed_items.len(), 4); + + removed_items.into_iter().flatten().collect_vec() + }, + AnySplitter::Sushi(id) => { + let mut removed_items = self + .inner + .sushi_belts + .iter_mut() + .flat_map(|belt| { + let removed_items_front = + if let Some((splitter, _)) = belt.input_splitter { + if splitter == id { + belt.input_splitter = None; + } + + let (removed_items, _new_len) = + belt.remove_length(SPLITTER_BELT_LEN, Side::BACK); + Some(removed_items) + } else { + None + }; + + let removed_items_back = + if let Some((splitter, _)) = belt.output_splitter { + if splitter == id { + belt.output_splitter = None; + } + + let (removed_items, _new_len) = + belt.remove_length(SPLITTER_BELT_LEN, Side::FRONT); + Some(removed_items) + } else { + None + }; + + removed_items_front.into_iter().chain(removed_items_back) + }) + .collect_vec(); + + removed_items.extend(self.inner.empty_belts.iter_mut().flat_map(|belt| { + let removed_items_front = + if let Some((splitter, _)) = belt.input_splitter { + if splitter == id { + belt.input_splitter = None; + } + + let (removed_items, _new_len) = + belt.remove_length(SPLITTER_BELT_LEN, Side::BACK); + Some(removed_items) + } else { + None + }; + + let removed_items_back = + if let Some((splitter, _)) = belt.output_splitter { + if splitter == id { + belt.output_splitter = None; + } + + let (removed_items, _new_len) = + belt.remove_length(SPLITTER_BELT_LEN, Side::FRONT); + Some(removed_items) + } else { + None + }; + + removed_items_front.into_iter().chain(removed_items_back) + })); + + if removed_items.len() < 4 { + removed_items.extend( + self.inner + .smart_belts + .iter_mut() + .flat_map(|store| store.belts_mut()) + .flat_map(|belt| { + let removed_items_front = + if let Some((splitter, _)) = belt.input_splitter { + if splitter == id { + belt.input_splitter = None; + } + + let (removed_items, _new_len) = belt + .remove_length(SPLITTER_BELT_LEN, Side::BACK); + Some(removed_items) + } else { + None + }; + + let removed_items_back = + if let Some((splitter, _)) = belt.output_splitter { + if splitter == id { + belt.output_splitter = None; + } + + let (removed_items, _new_len) = belt + .remove_length(SPLITTER_BELT_LEN, Side::FRONT); + Some(removed_items) + } else { + None + }; + + removed_items_front.into_iter().chain(removed_items_back) + }), + ); + } + + assert_eq!(removed_items.len(), 4); + + removed_items.into_iter().flatten().collect_vec() + }, + }; + + self.any_splitters[idx as usize] = + AnySplitter::Sushi(SplitterID { index: u32::MAX }); + + removed_items + }, + } } pub fn get_len(&self, id: BeltTileId) -> u16 { diff --git a/src/data/factorio_1_1.fgmod b/src/data/factorio_1_1.fgmod index 494cf78..5007dbd 100644 --- a/src/data/factorio_1_1.fgmod +++ b/src/data/factorio_1_1.fgmod @@ -3364,7 +3364,7 @@ solar_panels: [ ( name: "factory_game::infinity_battery", - display_name: "Infinity Battery", + display_name: "Infinite Power Source", tile_size: (2, 2), output: Constant((1000000000000)), ), diff --git a/src/frontend/world/sparse_grid/bounding_box_grid.rs b/src/frontend/world/sparse_grid/bounding_box_grid.rs index 6e391c5..90a15b5 100644 --- a/src/frontend/world/sparse_grid/bounding_box_grid.rs +++ b/src/frontend/world/sparse_grid/bounding_box_grid.rs @@ -87,15 +87,17 @@ impl + 'static> SparseGrid for BoundingBoxGrid { } fn occupied_entries(&self) -> impl Iterator { - self.values - .iter() - .filter_map(|v| v.as_ref().map(|v| (v.get_grid_index(), v))) + self.values.iter().enumerate().filter_map(|(idx, v)| { + let [x, y] = Self::calculate_pos(&self.extent.unwrap(), idx); + v.as_ref().map(|v| ((x, y), v)) + }) } fn occupied_entries_mut(&mut self) -> impl Iterator { - self.values - .iter_mut() - .filter_map(|v| v.as_mut().map(|v| (v.get_grid_index(), v))) + self.values.iter_mut().enumerate().filter_map(|(idx, v)| { + let [x, y] = Self::calculate_pos(&self.extent.unwrap(), idx); + v.as_mut().map(|v| ((x, y), v)) + }) } // TODO: Do I want to save None values? @@ -103,7 +105,9 @@ impl + 'static> SparseGrid for BoundingBoxGrid { where T: serde::Serialize, { - create_dir_all(&base_path).expect("Failed to create world dir"); + create_dir_all(&base_path).expect("Failed to create chunk dir"); + + save_at_fork(&self.extent, base_path.join("extent")); // TODO: Choose a chunk size self.values @@ -122,7 +126,9 @@ impl + 'static> SparseGrid for BoundingBoxGrid { where T: Sync + serde::Serialize, { - create_dir_all(&base_path).expect("Failed to create world dir"); + create_dir_all(&base_path).expect("Failed to create chunk dir"); + + save_at(&self.extent, base_path.join("extent")); // TODO: Choose a chunk size self.values @@ -140,6 +146,8 @@ impl + 'static> SparseGrid for BoundingBoxGrid { where for<'a> T: Send + serde::Deserialize<'a>, { + let extent = load_at(base_path.join("extent")); + let values: Vec<_> = (0..) .map(|chunk_id| base_path.join(format!("chunk-{chunk_id}"))) .take_while(|path| { @@ -152,35 +160,17 @@ impl + 'static> SparseGrid for BoundingBoxGrid { .flatten() .collect(); - let top_left = values - .iter() - .flatten() - .map(|value| value.get_grid_index()) - .reduce(|a, b| (min(a.0, b.0), min(a.1, b.1))); - - let bottom_right = values - .iter() - .flatten() - .map(|value| value.get_grid_index()) - .reduce(|a, b| (max(a.0, b.0), max(a.1, b.1))); - - let extent = match (top_left, bottom_right) { - (None, None) => None, - (Some((min_x, min_y)), Some((max_x, max_y))) => Some([[min_x, max_x], [min_y, max_y]]), - - _ => unreachable!(), - }; - - for (index, chunk) in values - .iter() - .enumerate() - .filter_map(|(index, chunk)| chunk.as_ref().map(|chunk| (index, chunk))) - { - assert_eq!( - Self::calculate_index(&extent.unwrap(), chunk.get_grid_index().into()), - index - ); - } + // TODO: This assertion requires storing the pos + // for (index, chunk) in values + // .iter() + // .enumerate() + // .filter_map(|(index, chunk)| chunk.as_ref().map(|chunk| (index, chunk))) + // { + // assert_eq!( + // Self::calculate_index(&extent.unwrap(), chunk.get_grid_index().into()), + // index + // ); + // } Self { extent, values } } @@ -295,6 +285,17 @@ impl> BoundingBoxGrid { height_offs as usize * width as usize + width_offs as usize } + fn calculate_pos(extent: &[[I; 2]; 2], idx: usize) -> [I; 2] { + let width = extent[0][1] - extent[0][0]; + let x_offs = idx as i32 % width; + let y_offs = idx as i32 / width; + + let x = extent[0][0] + x_offs; + let y = extent[1][0] + y_offs; + + [x, y] + } + fn reorder_for_new_extent(&mut self, new_point: [I; 2]) { let [x, y] = new_point; diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index 01bfb30..f27caae 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -24,6 +24,7 @@ use std::{ num::NonZero, ops::{Add, ControlFlow, Range}, }; +use strum::IntoEnumIterator; use crate::frontend::world::sparse_grid::GetGridIndex; @@ -107,7 +108,7 @@ const_assert!(CHUNK_SIZE * CHUNK_SIZE - 1 <= u8::MAX as u16); #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)] pub struct Chunk { - base_pos: (i32, i32), + // base_pos: (i32, i32), pub floor_tiles: Option>, chunk_tile_to_entity_into: Option>, entities: Vec>, @@ -1325,7 +1326,7 @@ fn removal_of_possible_inserter_connection { - sim_state.factory.belts.remove_inserter(*id, *belt_pos); + let () = sim_state.factory.belts.remove_inserter(*id, *belt_pos); *info = InserterInfo::NotAttached {}; }, @@ -1466,10 +1467,11 @@ impl GetGridIndex for Chunk { fn get_grid_index(&self) -> (i32, i32) { - ( - self.base_pos.0.div_floor(i32::from(CHUNK_SIZE)), - self.base_pos.1.div_floor(i32::from(CHUNK_SIZE)), - ) + // ( + // self.base_pos.0.div_floor(i32::from(CHUNK_SIZE)), + // self.base_pos.1.div_floor(i32::from(CHUNK_SIZE)), + // ) + unimplemented!() } } @@ -1648,7 +1650,7 @@ impl World World World, ) -> bool { let bb_top_left = (pos.x, pos.y); @@ -4131,11 +4134,11 @@ impl World World todo!(), + Entity::Splitter { + pos, direction, id, .. + } => { + let removed_items = sim_state.factory.belts.remove_splitter(*id); + // TODO: Handle these removed items + + // Handle the shortening of the belt due to the removal of the splitter len + // Front + for side in SplitterSide::iter() { + let side_offs = match side { + SplitterSide::Left => (0, 0), + SplitterSide::Right => direction.turn_right().into_offset(), + }; + let pos = *pos + *direction; + + let pos = Position { + x: pos.x + i32::from(side_offs.0), + y: pos.y + i32::from(side_offs.1), + }; + + // FIXME: SIDELOADING MUST BE HANDLED, AND TURNING + if let Some(Entity::Belt { id, .. }) = self.get_entity_at(pos, data_store) { + } + } + // BACK + for side in SplitterSide::iter() { + let side_offs = match side { + SplitterSide::Left => (0, 0), + SplitterSide::Right => direction.turn_right().into_offset(), + }; + let pos = *pos + (direction.reverse()); + + let pos = Position { + x: pos.x + i32::from(side_offs.0), + y: pos.y + i32::from(side_offs.1), + }; + + // FIXME: SIDELOADING MUST BE HANDLED, AND TURNING + if let Some(Entity::Belt { id, .. }) = self.get_entity_at(pos, data_store) { + self.modify_belt_pos(*id, true, SPLITTER_BELT_LEN); + } + } + }, Entity::Chest { ty, pos, item, .. } => { if let Some((item, index)) = item { diff --git a/src/lib.rs b/src/lib.rs index f31b2b0..6ac6741 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -119,6 +119,8 @@ pub mod liquid; mod par_generation; +mod lockfile; + impl WeakIdxTrait for u8 {} impl WeakIdxTrait for u16 {} impl IdxTrait for u8 {} diff --git a/src/lockfile/mod.rs b/src/lockfile/mod.rs new file mode 100644 index 0000000..c27b9f0 --- /dev/null +++ b/src/lockfile/mod.rs @@ -0,0 +1,57 @@ +use std::{fs::File, path::Path}; + +#[derive(Debug)] +pub(crate) struct LockfileUnique { + file: std::fs::File, +} + +impl LockfileUnique { + /* + Blocks until the lockfile was locked exclusively + */ + pub fn create_blocking(path: impl AsRef) -> std::io::Result { + let file = File::create(path)?; + file.lock()?; + + Ok(Self { file }) + } + + pub fn release(self) -> std::io::Result<()> { + self.file.unlock()?; + + Ok(()) + } +} + +impl Drop for LockfileUnique { + fn drop(&mut self) { + if let Err(e) = self.file.unlock() { + log::error!("Failed unlocking unique lockfile: {e:?}"); + } + } +} + +#[derive(Debug)] +pub(crate) struct LockfileShared { + file: std::fs::File, +} + +impl LockfileShared { + /* + Blocks until the lockfile was locked shared + */ + pub fn create_blocking(path: impl AsRef) -> std::io::Result { + let file = File::create(path)?; + file.lock_shared()?; + + Ok(Self { file }) + } +} + +impl Drop for LockfileShared { + fn drop(&mut self) { + if let Err(e) = self.file.unlock() { + log::error!("Failed unlocking shared lockfile: {e:?}"); + } + } +} diff --git a/src/multiplayer/mod.rs b/src/multiplayer/mod.rs index 2d0e692..01f885b 100644 --- a/src/multiplayer/mod.rs +++ b/src/multiplayer/mod.rs @@ -290,7 +290,37 @@ impl Game game_state_update_handler.update(game_state, Some(replay), data_store), + ) => { + { + profiling::scope!("GameState dedicated server autosave"); + let simulation_state = game_state.simulation_state.lock(); + let world = game_state.world.lock(); + let aux_data = &mut *game_state.aux_data.lock(); + + // TODO: Autosave interval + if aux_data.current_tick % (60 * 60 * 1) == 0 { + // Autosave + #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + { + use crate::saving::save_with_fork; + + profiling::scope!("Autosave"); + // TODO: Handle overlapping saves + let _ = save_with_fork( + "dedicated_server_save", + Some("dedicated_server_save.save"), + &world, + &simulation_state, + &aux_data, + data_store, + ); + } + } + } + log::trace!("Post Autosave"); + + game_state_update_handler.update(game_state, Some(replay), data_store); + }, #[cfg(feature = "client")] Game::IntegratedServer( game_state, diff --git a/src/multiplayer/server.rs b/src/multiplayer/server.rs index 829c326..a2ec238 100644 --- a/src/multiplayer/server.rs +++ b/src/multiplayer/server.rs @@ -81,27 +81,9 @@ impl< { profiling::scope!("GameState Update"); - // TODO: Autosave interval - if aux_data.current_tick % (60 * 60 * 1) == 0 { - // Autosave - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] - { - profiling::scope!("Autosave"); - // TODO: Handle overlapping saves - let _ = save_with_fork( - "dedicated_server_save", - Some("dedicated_server_save.save"), - &world, - &simulation_state, - &aux_data, - data_store, - ); - } - } - GameState::update(&mut *simulation_state, aux_data, data_store); } - log::trace!("Post Autosave"); + log::trace!("Post Update"); let current_tick = aux_data.current_tick; diff --git a/src/power/power_grid.rs b/src/power/power_grid.rs index fd338d0..54afd6c 100644 --- a/src/power/power_grid.rs +++ b/src/power/power_grid.rs @@ -884,7 +884,7 @@ impl PowerGrid save( - "Last Exit", - Some("last_exit.save"), - &state.state, - &state.data_store.lock(), - ), - LoadedGame::ItemU8RecipeU16(state) => save( - "Last Exit", - Some("last_exit.save"), - &state.state, - &state.data_store.lock(), - ), - LoadedGame::ItemU16RecipeU8(state) => save( - "Last Exit", - Some("last_exit.save"), - &state.state, - &state.data_store.lock(), - ), - LoadedGame::ItemU16RecipeU16(state) => save( - "Last Exit", - Some("last_exit.save"), - &state.state, - &state.data_store.lock(), - ), + LoadedGame::ItemU8RecipeU8(state) => { + save( + "Last Exit", + Some("last_exit.save"), + &state.state, + &state.data_store.lock(), + ); + }, + LoadedGame::ItemU8RecipeU16(state) => { + save( + "Last Exit", + Some("last_exit.save"), + &state.state, + &state.data_store.lock(), + ); + }, + LoadedGame::ItemU16RecipeU8(state) => { + save( + "Last Exit", + Some("last_exit.save"), + &state.state, + &state.data_store.lock(), + ); + }, + LoadedGame::ItemU16RecipeU16(state) => { + save( + "Last Exit", + Some("last_exit.save"), + &state.state, + &state.data_store.lock(), + ); + }, } } } diff --git a/src/research.rs b/src/research.rs index bb90225..7033088 100644 --- a/src/research.rs +++ b/src/research.rs @@ -210,7 +210,8 @@ impl TechState { { self.recipe_active[recipe.into_usize()] = true; } - if is_repeating { + // TODO: Do not autorepeat always + if is_repeating && self.research_queue.len() == 1 { // Just keep researching the same tech (just one level higher) } else { self.research_queue.remove(0); diff --git a/src/saving/loading.rs b/src/saving/loading.rs index 8f2e377..54e83bf 100644 --- a/src/saving/loading.rs +++ b/src/saving/loading.rs @@ -22,16 +22,23 @@ impl SaveFileList { let folder = std::fs::read_dir(save_folder).expect("Could not read save folder"); let mut saves: Vec<_> = folder - .map(|save_folder| { + .filter_map(|save_folder| { let save = save_folder.unwrap(); + + if save.file_name() == "save_in_progress.lockfile" { + return None; + } + let info_path = save.path().join("save_file_info"); let info: Result = try_load_at(info_path); - info.map_err(|err| (save.path(), SaveFileError::LoadError(err))) - .map(|stored| SaveFileInfo { - path: save.path(), - stored, - }) + Some( + info.map_err(|err| (save.path(), SaveFileError::LoadError(err))) + .map(|stored| SaveFileInfo { + path: save.path(), + stored, + }), + ) }) .collect(); diff --git a/src/saving/mod.rs b/src/saving/mod.rs index 966f2e5..2b3f830 100644 --- a/src/saving/mod.rs +++ b/src/saving/mod.rs @@ -64,7 +64,12 @@ pub fn save_at(value: &V, path: PathBuf) { } pub fn save_at_fork(value: &V, path: PathBuf) { - let file = { File::create(path).expect("could not create file") }; + let file = { + File::create(&path).expect(&format!( + "could not create file, tried to create {}", + path.display() + )) + }; // FIXME: It is technically not okay to allocate here. let mut buf_writer = BufWriter::new(file); @@ -115,6 +120,11 @@ pub fn save_components( let checksum = data_store.checksum.clone(); let save_dir = save_folder(); + let lockfile = crate::lockfile::LockfileUnique::create_blocking( + save_dir.join("save_in_progress.lockfile"), + ) + .expect("Locking lockfile failed"); + create_dir_all(&save_dir).expect("Could not create save dir"); // if let Ok(s) = env::var("FACTORY_SAVE_READABLE") { @@ -160,6 +170,7 @@ pub fn save_components( }; create_dir_all(&temp_file_dir).expect("Could not create temp dir"); + dbg!(&temp_file_dir); // FIXME: What to do, if the size of the Save in memory + on disk exceeds RAM? @@ -249,7 +260,12 @@ pub fn save_components( // Remove old save if it exists let _ = std::fs::remove_dir_all(&save_file_dir); - std::fs::rename(temp_file_dir, save_file_dir).expect("Could not rename tmp save dir!"); + std::fs::rename(&temp_file_dir, save_file_dir).expect(&format!( + "Could not rename tmp save dir: {}", + temp_file_dir.display() + )); + + lockfile.release().expect("Failed to remove lockfile"); } /// # Panics @@ -268,14 +284,10 @@ pub fn save_components_fork_safe let checksum = &data_store.checksum; let save_dir = save_folder(); - // FIXME: This could lock forever - let lockfile = loop { - if let Ok(lockfile) = - lockfile::Lockfile::create_with_parents(save_dir.join("save_in_progress")) - { - break lockfile; - } - }; + let lockfile = crate::lockfile::LockfileUnique::create_blocking( + save_dir.join("save_in_progress.lockfile"), + ) + .expect("Locking lockfile failed"); create_dir_all(&save_dir).expect("Could not create data dir"); @@ -304,6 +316,7 @@ pub fn save_components_fork_safe assert_ne!(temp_file_dir, save_dir); create_dir_all(&temp_file_dir).expect("Could not create temp dir"); + dbg!(&temp_file_dir); { let SimulationState { From 347c3a5bd8c951599e609a13d0d6993e781035f0 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 3 Feb 2026 23:47:53 +0100 Subject: [PATCH 096/152] Fix typo --- src/frontend/world/sparse_grid/dynamic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/world/sparse_grid/dynamic.rs b/src/frontend/world/sparse_grid/dynamic.rs index 2b74b68..a2d7ef8 100644 --- a/src/frontend/world/sparse_grid/dynamic.rs +++ b/src/frontend/world/sparse_grid/dynamic.rs @@ -14,7 +14,7 @@ use egui_show_info_derive::ShowInfo; #[cfg(feature = "client")] use get_size2::GetSize; -// If more than every 20th chunk is ininhabitat, switch to map +// If more than every 20th chunk is inhabited, switch to map // TODO: Find a good value for this const SWITCH_RATIO: usize = 20; From 65f7318fd821be9dd2dfb07d727cc39f1323d477 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 3 Feb 2026 23:48:11 +0100 Subject: [PATCH 097/152] Start bucket consolidation refactor --- src/bucket_store/const_reinsertion.rs | 93 ++++++++++++++ src/bucket_store/mod.rs | 21 ++++ .../storage_storage_inserter_store/mod.rs | 115 ++++++++++++++++++ src/lib.rs | 1 + 4 files changed, 230 insertions(+) create mode 100644 src/bucket_store/const_reinsertion.rs create mode 100644 src/bucket_store/mod.rs create mode 100644 src/bucket_store/storage_storage_inserter_store/mod.rs diff --git a/src/bucket_store/const_reinsertion.rs b/src/bucket_store/const_reinsertion.rs new file mode 100644 index 0000000..d091ffe --- /dev/null +++ b/src/bucket_store/const_reinsertion.rs @@ -0,0 +1,93 @@ +pub(crate) struct ConstReinsertionBucketStore< + const STATE_COUNT: usize, + Reinsertion: super::Reinsertion, +> { + pub reinsertion_time: u32, + + states: [State; STATE_COUNT], +} + +struct State { + moving_time_counts: Box<[u32]>, + moving_values: Vec, + ticking_values: Vec, +} + +impl + ConstReinsertionBucketStore +{ + pub fn new(reinsertion_time: u32) -> Self { + assert_eq!(Reinsertion::STATE_COUNT, STATE_COUNT); + assert!(Reinsertion::STATE_COUNT > 0); + assert!(reinsertion_time > 0); + Self { + reinsertion_time, + states: std::array::from_fn(|_| State { + moving_time_counts: vec![0; reinsertion_time as usize].into_boxed_slice(), + moving_values: vec![], + ticking_values: vec![], + }), + } + } + + pub fn update<'a>(&mut self, world_state: &mut Reinsertion::WorldState<'a>) { + for state in &mut self.states { + // Done moving + let (ticking, (moving, moving_time_counts)) = { + ( + &mut state.ticking_values, + (&mut state.moving_values, &mut state.moving_time_counts), + ) + }; + + let amount_done_moving = moving_time_counts[0]; + let extract_range = 0..(amount_done_moving as usize); + + let removed_moving = moving.drain(extract_range); + let added_ticking = removed_moving.map(|moving| Reinsertion::moving_to_ticking(moving)); + + ticking.extend(added_ticking); + + moving_time_counts.rotate_left(1); + assert_eq!( + amount_done_moving, + moving_time_counts[self.reinsertion_time as usize - 1] + ); + moving_time_counts[self.reinsertion_time as usize - 1] = 0; + } + + for state in 0..STATE_COUNT { + let next_state = (state + 1) % STATE_COUNT; + + // Start Moving + let (ticking, (moving, moving_time_counts)) = if state == next_state { + assert!(STATE_COUNT == 1); + let state = &mut self.states[state]; + ( + &mut state.ticking_values, + (&mut state.moving_values, &mut state.moving_time_counts), + ) + } else { + assert!(STATE_COUNT > 1); + let [this, next] = self.states.get_disjoint_mut([state, next_state]).unwrap(); + + ( + &mut this.ticking_values, + (&mut next.moving_values, &mut next.moving_time_counts), + ) + }; + + let extracted_ticking = + ticking.extract_if(.., |ticking| Reinsertion::tick(state, ticking, world_state)); + + let reinserted_moving = + extracted_ticking.filter_map(|ticking| Reinsertion::ticking_to_moving(ticking)); + + let moving_old_len = moving.len(); + moving.extend(reinserted_moving); + let added = moving.len() - moving_old_len; + + moving_time_counts[self.reinsertion_time as usize - 1] += added as u32; + } + } +} diff --git a/src/bucket_store/mod.rs b/src/bucket_store/mod.rs new file mode 100644 index 0000000..158f8f5 --- /dev/null +++ b/src/bucket_store/mod.rs @@ -0,0 +1,21 @@ +pub(crate) mod const_reinsertion; + +mod storage_storage_inserter_store; + +pub(crate) trait Reinsertion { + const STATE_COUNT: usize; + + type MovingValue: Copy; + type TickingValue: Copy; + + type WorldState<'a>; + + fn tick<'a>( + state: usize, + value: &mut Self::TickingValue, + world_state: &mut Self::WorldState<'a>, + ) -> bool; + fn ticking_to_moving(value: Self::TickingValue) -> Option; + + fn moving_to_ticking(value: Self::MovingValue) -> Self::TickingValue; +} diff --git a/src/bucket_store/storage_storage_inserter_store/mod.rs b/src/bucket_store/storage_storage_inserter_store/mod.rs new file mode 100644 index 0000000..33acff3 --- /dev/null +++ b/src/bucket_store/storage_storage_inserter_store/mod.rs @@ -0,0 +1,115 @@ +use std::num::NonZero; + +use enum_map::Enum; + +use crate::{bucket_store::Reinsertion, item::ITEMCOUNTTYPE}; + +struct StorageStorageInserterStore {} + +struct Info { + last_ticked: u16, + state_at_last_tick: ImplicitState, +} + +enum ImplicitState { + WaitingForSourceItems(ITEMCOUNTTYPE), + WaitingForSpaceInDestination(ITEMCOUNTTYPE), +} + +struct StorageStorageInserterReinsertion; + +#[derive(Debug, Enum)] +enum State { + PickingUpItems, + DroppingOffItems, +} + +#[derive(Debug, Clone, Copy)] +struct MovingInserter { + id: u32, + todo: !, +} + +#[derive(Debug, Clone, Copy)] +struct TickingInserter { + id: u32, + hand: ITEMCOUNTTYPE, + todo: !, +} + +struct WorldState<'a> { + max_hand_size: ITEMCOUNTTYPE, + + current_tick: u32, + infos: &'a mut [Info], +} + +impl Reinsertion for StorageStorageInserterReinsertion { + const STATE_COUNT: usize = State::LENGTH; + + type MovingValue = MovingInserter; + + type TickingValue = TickingInserter; + + type WorldState<'a> = WorldState<'a>; + + #[inline] + fn tick<'a>( + state: usize, + value: &mut Self::TickingValue, + world_state: &mut Self::WorldState<'a>, + ) -> bool { + let WorldState { + current_tick, + infos, + max_hand_size, + } = world_state; + + infos[value.id as usize].last_ticked = *current_tick as u16; + + match State::from_usize(state) { + State::PickingUpItems => { + let amount_picked_up: ITEMCOUNTTYPE = todo!("Pick up actually"); + + if amount_picked_up > 0 { + value.hand += amount_picked_up; + infos[value.id as usize].state_at_last_tick = + ImplicitState::WaitingForSourceItems(value.hand); + if value.hand == *max_hand_size { + todo!("Try to put it in waitlist"); + true + } else { + false + } + } else { + false + } + }, + State::DroppingOffItems => { + let amount_dropped_off: ITEMCOUNTTYPE = todo!("Drop off actually"); + + if amount_dropped_off > 0 { + value.hand -= amount_dropped_off; + infos[value.id as usize].state_at_last_tick = + ImplicitState::WaitingForSpaceInDestination(value.hand); + if value.hand == 0 { + todo!("Try to put it in waitlist"); + true + } else { + false + } + } else { + false + } + }, + } + } + + fn ticking_to_moving(value: Self::TickingValue) -> Option { + todo!() + } + + fn moving_to_ticking(value: Self::MovingValue) -> Self::TickingValue { + todo!() + } +} diff --git a/src/lib.rs b/src/lib.rs index 6ac6741..4a56968 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -119,6 +119,7 @@ pub mod liquid; mod par_generation; +mod bucket_store; mod lockfile; impl WeakIdxTrait for u8 {} From 332e999200c33b9559daef90a38c067373ae47fc Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 3 Feb 2026 23:59:43 +0100 Subject: [PATCH 098/152] Update cargo hash --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 6cf8790..947abf7 100644 --- a/flake.nix +++ b/flake.nix @@ -39,7 +39,7 @@ src = ./.; buildInputs = neededPackages; nativeBuildInputs = [ pkgs.pkg-config pkgs.makeWrapper ]; - cargoHash = "sha256-/iACDjmjwgN4pB+2FawgACaToMejl4DIOIrGOWDGnPI="; + cargoHash = "sha256-83+1Y486PUHM9+uyFw+yJ9bNMlMbN/fc8cYRzKmDdb8="; # cargoLock.lockFile = ./Cargo.lock; doCheck = false; From 14666c5691bf6228554f3f8663fd2807cf332d06 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 4 Feb 2026 03:18:09 +0100 Subject: [PATCH 099/152] Fix render lag when connected as client --- src/multiplayer/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multiplayer/mod.rs b/src/multiplayer/mod.rs index 01f885b..2127081 100644 --- a/src/multiplayer/mod.rs +++ b/src/multiplayer/mod.rs @@ -262,7 +262,7 @@ impl Game Date: Wed, 4 Feb 2026 03:18:25 +0100 Subject: [PATCH 100/152] Correctly print help message for dedicated server --- src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 4a56968..abd4abc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -198,6 +198,7 @@ pub fn main(input: &Vec) -> Result<(), args::ArgsError> { { use crate::saving::save_folder; + log::info!("This is the dedicated server"); let mut args = args::Args::new("factory", "FactoryGame dedicated server"); args.flag("h", "help", "Print the usage menu"); @@ -209,10 +210,11 @@ pub fn main(input: &Vec) -> Result<(), args::ArgsError> { ); args.parse(input)?; + log::trace!("Parsed input"); let help = args.value_of("help")?; if help { - args.full_usage(); + println!("{}", args.full_usage()); return Ok(()); } From cd5038e02cf8e662ccc62dfbca412480f6ddde23 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 4 Feb 2026 19:58:01 +0100 Subject: [PATCH 101/152] Correctly remove storage_storage inserters in waitlists --- src/app_state.rs | 7 ++-- src/assembler/bucketed.rs | 2 +- src/assembler/mod.rs | 67 ++++++------------------------------ src/assembler/simd.rs | 38 +++++++++++++++++---- src/chest.rs | 51 ++++++++++++++++++++++++++++ src/frontend/world/tile.rs | 69 ++++++++++++++++++++++++++++++-------- src/power/power_grid.rs | 12 +++++++ 7 files changed, 166 insertions(+), 80 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 6cc82fb..c909ec8 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1447,7 +1447,9 @@ impl GameState { - let removed = game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores.remove_wait_list_inserter(*id, item, inserter.id, data_store); + let removed = + game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores + .remove_wait_list_inserter(*id, item, crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id: inserter.id }, data_store); let Conn::Storage { storage_id_in, storage_id_out, @@ -1491,7 +1493,8 @@ impl GameState { - let removed = game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores.remove_wait_list_inserter(*id, item, inserter.id, data_store); + let removed = game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores + .remove_wait_list_inserter(*id, item, crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id: inserter.id }, data_store); let Conn::Storage { index, storage_id_in, diff --git a/src/assembler/bucketed.rs b/src/assembler/bucketed.rs index 09e134d..b089a7b 100644 --- a/src/assembler/bucketed.rs +++ b/src/assembler/bucketed.rs @@ -1014,7 +1014,7 @@ impl, - _id: crate::inserter::storage_storage_with_buckets_indirect::InserterId, + _info: crate::chest::WaitingInserterRemovalInfo, _data_store: &DataStore, ) -> super::simd::InserterReinsertionInfo { unreachable!() diff --git a/src/assembler/mod.rs b/src/assembler/mod.rs index cd36896..319cc7b 100644 --- a/src/assembler/mod.rs +++ b/src/assembler/mod.rs @@ -567,7 +567,7 @@ impl< &mut self, assembler_id: AssemblerID, item: Item, - inserter_id: InserterId, + info: crate::chest::WaitingInserterRemovalInfo, data_store: &DataStore, ) -> simd::InserterReinsertionInfo { let recipe_id = assembler_id.recipe.id.into(); @@ -584,12 +584,7 @@ impl< ); self.assemblers_0_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] - .remove_wait_list_inserter( - assembler_id.assembler_index, - item, - inserter_id, - data_store, - ) + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) }, (1, 1) => { assert_eq!( @@ -599,12 +594,7 @@ impl< ); self.assemblers_1_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] - .remove_wait_list_inserter( - assembler_id.assembler_index, - item, - inserter_id, - data_store, - ) + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) }, (2, 1) => { assert_eq!( @@ -614,12 +604,7 @@ impl< ); self.assemblers_2_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] - .remove_wait_list_inserter( - assembler_id.assembler_index, - item, - inserter_id, - data_store, - ) + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) }, (2, 2) => { @@ -630,12 +615,7 @@ impl< ); self.assemblers_2_2[data_store.recipe_to_ing_out_combo_idx[recipe_id]] - .remove_wait_list_inserter( - assembler_id.assembler_index, - item, - inserter_id, - data_store, - ) + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) }, (2, 3) => { @@ -646,12 +626,7 @@ impl< ); self.assemblers_2_3[data_store.recipe_to_ing_out_combo_idx[recipe_id]] - .remove_wait_list_inserter( - assembler_id.assembler_index, - item, - inserter_id, - data_store, - ) + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) }, (3, 1) => { @@ -662,12 +637,7 @@ impl< ); self.assemblers_3_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] - .remove_wait_list_inserter( - assembler_id.assembler_index, - item, - inserter_id, - data_store, - ) + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) }, (4, 1) => { @@ -678,12 +648,7 @@ impl< ); self.assemblers_4_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] - .remove_wait_list_inserter( - assembler_id.assembler_index, - item, - inserter_id, - data_store, - ) + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) }, (5, 1) => { @@ -694,12 +659,7 @@ impl< ); self.assemblers_5_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] - .remove_wait_list_inserter( - assembler_id.assembler_index, - item, - inserter_id, - data_store, - ) + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) }, (6, 1) => { @@ -710,12 +670,7 @@ impl< ); self.assemblers_6_1[data_store.recipe_to_ing_out_combo_idx[recipe_id]] - .remove_wait_list_inserter( - assembler_id.assembler_index, - item, - inserter_id, - data_store, - ) + .remove_wait_list_inserter(assembler_id.assembler_index, item, info, data_store) }, _ => unreachable!(), @@ -1090,7 +1045,7 @@ pub trait MultiAssemblerStore< &mut self, index: u32, item: Item, - id: InserterId, + info: crate::chest::WaitingInserterRemovalInfo, data_store: &DataStore, ) -> simd::InserterReinsertionInfo; } diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index 4b717ad..484969e 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -2057,7 +2057,7 @@ impl, - id: InserterId, + info: crate::chest::WaitingInserterRemovalInfo, data_store: &DataStore, ) -> self::InserterReinsertionInfo { let item_index = data_store.recipe_item_index_to_item[self.recipe.into_usize()] @@ -2070,9 +2070,21 @@ impl index == id, - InserterWithBeltsEnum::BeltStorage { .. } => false, + .find(|ins| match (&ins.as_ref().unwrap().rest, info) { + ( + InserterWithBeltsEnum::StorageStorage { index, .. }, + crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id }, + ) => inserter_id == *index, + ( + InserterWithBeltsEnum::BeltStorage { + belt_id: ins_belt_id, + belt_pos: ins_belt_pos, + .. + }, + crate::chest::WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }, + ) => *ins_belt_id == belt_id && *ins_belt_pos == belt_pos, + + _ => false, }) .unwrap(); @@ -2122,9 +2134,21 @@ impl index == id, - InserterWithBeltsEnum::BeltStorage { .. } => false, + .find(|ins| match (&ins.as_ref().unwrap().rest, info) { + ( + InserterWithBeltsEnum::StorageStorage { index, .. }, + crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id }, + ) => inserter_id == *index, + ( + InserterWithBeltsEnum::BeltStorage { + belt_id: ins_belt_id, + belt_pos: ins_belt_pos, + .. + }, + crate::chest::WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }, + ) => *ins_belt_id == belt_id && *ins_belt_pos == belt_pos, + + _ => false, }) .unwrap(); diff --git a/src/chest.rs b/src/chest.rs index baf007b..082aab0 100644 --- a/src/chest.rs +++ b/src/chest.rs @@ -5,6 +5,8 @@ use rayon::iter::{IndexedParallelIterator, IntoParallelIterator}; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; use crate::assembler::simd::{InserterReinsertionInfo, InserterWaitList, InserterWithBelts}; +use crate::belt::belt::BeltLenType; +use crate::inserter::storage_storage_with_buckets_indirect::InserterId; use crate::inserter::{FakeUnionStorage, StaticID}; use crate::storage_list::{InserterWaitLists, MaxInsertionLimit}; use crate::{ @@ -158,6 +160,12 @@ pub struct MultiChestStore { num_large_chests: usize, } +#[derive(Debug, Clone, Copy)] +pub(crate) enum WaitingInserterRemovalInfo { + StorageStorage { inserter_id: InserterId }, + BeltStorage { belt_id: u32, belt_pos: BeltLenType }, +} + impl MultiChestStore { #[must_use] pub fn new(item: Item) -> Self { @@ -221,6 +229,49 @@ impl MultiChestStore { } } + pub(crate) fn remove_inserter_from_waitlist( + &mut self, + id: u32, + inserter_info: WaitingInserterRemovalInfo, + ) { + for wait_slot in &mut self.wait_list[id as usize].inserters { + if let Some(inserter) = wait_slot { + match (&inserter_info, &inserter.rest) { + ( + WaitingInserterRemovalInfo::StorageStorage { inserter_id }, + crate::assembler::simd::InserterWithBeltsEnum::StorageStorage { + self_is_source, + index, + other, + }, + ) => { + if *index == *inserter_id { + let removed = wait_slot.take().unwrap(); + // TODO: What do I want to return + return; + } + }, + ( + WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }, + crate::assembler::simd::InserterWithBeltsEnum::BeltStorage { + self_is_source, + belt_id: ins_belt_id, + belt_pos: ins_belt_pos, + }, + ) => { + if *belt_id == *ins_belt_id && *ins_belt_pos == *belt_pos { + let removed = wait_slot.take().unwrap(); + // TODO: What do I want to return + return; + } + }, + + _ => {}, + } + } + } + } + pub fn add_custom_chest(&mut self, max_items: ChestSize) -> u32 { if max_items > 255 { self.num_large_chests += 1; diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index f27caae..6f2751b 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -4919,6 +4919,8 @@ impl World {}, Entity::Inserter { ty, + pos, + direction, user_movetime, info: InserterInfo::Attached { @@ -4934,20 +4936,59 @@ impl World { - sim_state - .factory - .storage_storage_inserters - .remove_ins( - *item, - user_movetime - .map(|v| v.into()) - .unwrap_or( - data_store.inserter_infos[*ty as usize].swing_time_ticks, - ) - .into(), - *inserter, - ) - .expect("Failed Removing Inserter"); + match sim_state.factory.storage_storage_inserters.remove_ins( + *item, + user_movetime + .map(|v| v.into()) + .unwrap_or(data_store.inserter_infos[*ty as usize].swing_time_ticks) + .into(), + *inserter, + ) { + Ok(()) => { + log::trace!( + "Successfully removed storage storage inserter from sim_state.factory.storage_storage_inserters" + ); + }, + Err(side) => { + log::trace!( + "Failed removing storage storage inserter from sim_state.factory.storage_storage_inserters, need to look at {:?}", + side + ); + + let search_pos = match side { + crate::inserter::WaitlistSearchSide::Source => { + data_store.inserter_start_pos(*ty, *pos, *direction) + }, + crate::inserter::WaitlistSearchSide::Dest => { + data_store.inserter_end_pos(*ty, *pos, *direction) + }, + }; + + match self.get_entity_at(search_pos, data_store).expect("The inserter is supposed to be in the waitlist of this entity so it should exist") { + Entity::Assembler { info, .. } => { + let AssemblerInfo::Powered { id, pole_position, .. } = info else { + unreachable!("If an inserter is attached, the assembler must be powered with a recipe") + }; + sim_state.factory.power_grids.power_grids[sim_state.factory.power_grids.pole_pos_to_grid_id[pole_position] as usize] + .remove_waiting_inserter(*id, *item, crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id: inserter.id }, data_store); + }, + Entity::Chest { item: chest_item, .. } => { + let (chest_item, chest_id) = chest_item.expect("If a chest has an inserter it must havbe an item"); + assert_eq!(chest_item, *item); + sim_state.factory.chests.stores[item.into_usize()].remove_inserter_from_waitlist(chest_id, crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id: inserter.id }); + }, + Entity::Roboport { .. } => todo!(), + Entity::Lab { .. } => unreachable!("Currently Labs do not have a waitlist implementation"), + Entity::MiningDrill { .. } => unreachable!("Currently Drills do not have a waitlist implementation"), + + e => unreachable!("A storage_storage inserter cannot attach to a {e:?}") + } + + log::trace!( + "Successfully removed storage storage inserter from waitlist" + ); + }, + } }, }, Entity::MiningDrill { .. } => todo!(), diff --git a/src/power/power_grid.rs b/src/power/power_grid.rs index 54afd6c..eb48e63 100644 --- a/src/power/power_grid.rs +++ b/src/power/power_grid.rs @@ -543,6 +543,18 @@ impl PowerGrid, + inserter_item: Item, + info: crate::chest::WaitingInserterRemovalInfo, + data_store: &DataStore, + ) { + // FIXME: Do I want to return something here? + self.stores + .remove_wait_list_inserter(assembler_id, inserter_item, info, data_store); + } + pub fn add_solar_panel( &mut self, panel_position: Position, From 7375e9129cfff5083b977342b19902cfce4c151e Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 4 Feb 2026 22:21:14 +0100 Subject: [PATCH 102/152] Allow removal of belt_storage inserters in movement/waitlists --- src/app_state.rs | 299 ++++++++++----------- src/assembler/simd.rs | 11 +- src/belt/mod.rs | 16 +- src/belt/smart.rs | 2 +- src/frontend/world/tile.rs | 109 +++++++- src/inserter/belt_storage_movement_list.rs | 22 ++ 6 files changed, 294 insertions(+), 165 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index c909ec8..d2f8ea3 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1377,170 +1377,165 @@ impl GameState { - match info { - InserterInfo::NotAttached {} => {}, - InserterInfo::Attached { info } => match info { - AttachedInserter::BeltStorage { id, belt_pos } => { - game_state - .simulation_state - .factory - .belts - .change_inserter_movetime( - *id, - *belt_pos, - new_movetime.map(|v| v.into()).unwrap_or( - data_store.inserter_infos[*ty as usize] - .swing_time_ticks, - ), - ); - }, - AttachedInserter::BeltBelt { .. } => todo!(), - AttachedInserter::StorageStorage { item, inserter } => { - let old_movetime = user_movetime.unwrap_or( - data_store.inserter_infos[*ty as usize] - .swing_time_ticks, - ); - - let new_movetime = + } => match info { + InserterInfo::NotAttached {} => {}, + InserterInfo::Attached { info } => match info { + AttachedInserter::BeltStorage { id, belt_pos } => { + game_state + .simulation_state + .factory + .belts + .change_inserter_movetime( + *id, + *belt_pos, new_movetime.map(|v| v.into()).unwrap_or( data_store.inserter_infos[*ty as usize] .swing_time_ticks, - ); + ), + ); + }, + AttachedInserter::BeltBelt { .. } => todo!(), + AttachedInserter::StorageStorage { item, inserter } => { + let old_movetime = user_movetime.unwrap_or( + data_store.inserter_infos[*ty as usize] + .swing_time_ticks, + ); - if old_movetime != new_movetime { - let new_id = match game_state - .simulation_state - .factory - .storage_storage_inserters - .change_movetime( - *item, - old_movetime.into(), - new_movetime.into(), - *inserter, - ) { - Ok(new_id) => new_id, - Err((search_side, mut handle_fn)) => { - match search_side { - WaitlistSearchSide::Source => { - let start_pos = data_store - .inserter_start_pos( - *ty, - *inserter_pos, - *direction, - ); - - let item = *item; - let inserter = *inserter; - match game_state - .world - .get_entity_at( - start_pos, data_store, - ) - .unwrap() - { - Entity::Assembler { - info: - AssemblerInfo::Powered { - id, - .. - }, - .. - } => { - let removed = + let new_movetime = + new_movetime.map(|v| v.into()).unwrap_or( + data_store.inserter_infos[*ty as usize] + .swing_time_ticks, + ); + + if old_movetime != new_movetime { + let new_id = match game_state + .simulation_state + .factory + .storage_storage_inserters + .change_movetime( + *item, + old_movetime.into(), + new_movetime.into(), + *inserter, + ) { + Ok(new_id) => new_id, + Err((search_side, mut handle_fn)) => { + match search_side { + WaitlistSearchSide::Source => { + let start_pos = data_store + .inserter_start_pos( + *ty, + *inserter_pos, + *direction, + ); + + let item = *item; + let inserter = *inserter; + match game_state + .world + .get_entity_at( + start_pos, data_store, + ) + .unwrap() + { + Entity::Assembler { + info: + AssemblerInfo::Powered { + id, + .. + }, + .. + } => { + let removed = game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores .remove_wait_list_inserter(*id, item, crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id: inserter.id }, data_store); - let Conn::Storage { - storage_id_in, - storage_id_out, - .. - } = removed.conn - else { - unreachable!() - }; - handle_fn( - storage_id_in, - storage_id_out, - removed.max_hand.into(), - ) - }, - - e => unreachable!("{e:?}"), - } - }, - WaitlistSearchSide::Dest => { - let end_pos = data_store - .inserter_end_pos( - *ty, - *inserter_pos, - *direction, - ); - - let item = *item; - let inserter = *inserter; - match game_state - .world - .get_entity_at( - end_pos, data_store, - ) - .unwrap() - { - Entity::Assembler { - info: - AssemblerInfo::Powered { - id, - .. - }, + let Conn::Storage { + storage_id_in, + storage_id_out, .. - } => { - let removed = game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores - .remove_wait_list_inserter(*id, item, crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id: inserter.id }, data_store); - let Conn::Storage { - index, - storage_id_in, - storage_id_out, - } = removed.conn - else { - unreachable!() - }; - handle_fn( - storage_id_in, - storage_id_out, - removed.max_hand.into(), - ) - }, - - e => unreachable!("{e:?}"), - } - }, - } - }, - }; + } = removed.conn + else { + unreachable!() + }; + handle_fn( + storage_id_in, + storage_id_out, + removed.max_hand.into(), + ) + }, - let Some(Entity::Inserter { - user_movetime, - info: - InserterInfo::Attached { - info: - AttachedInserter::StorageStorage { - inserter, + e => unreachable!("{e:?}"), + } + }, + WaitlistSearchSide::Dest => { + let end_pos = data_store + .inserter_end_pos( + *ty, + *inserter_pos, + *direction, + ); + + let item = *item; + let inserter = *inserter; + match game_state + .world + .get_entity_at(end_pos, data_store) + .unwrap() + { + Entity::Assembler { + info: + AssemblerInfo::Powered { + id, + .. + }, .. + } => { + let removed = game_state.simulation_state.factory.power_grids.power_grids[id.grid as usize].stores + .remove_wait_list_inserter(*id, item, crate::chest::WaitingInserterRemovalInfo::StorageStorage { inserter_id: inserter.id }, data_store); + let Conn::Storage { + index, + storage_id_in, + storage_id_out, + } = removed.conn + else { + unreachable!() + }; + handle_fn( + storage_id_in, + storage_id_out, + removed.max_hand.into(), + ) }, + + e => unreachable!("{e:?}"), + } }, - .. - }) = game_state - .world - .get_entity_at_mut(*pos, data_store) - else { - unreachable!(); - }; - *user_movetime = - Some(new_movetime.try_into().unwrap()); - *inserter = new_id; - } - }, + } + }, + }; + + let Some(Entity::Inserter { + user_movetime, + info: + InserterInfo::Attached { + info: + AttachedInserter::StorageStorage { + inserter, + .. + }, + }, + .. + }) = game_state + .world + .get_entity_at_mut(*pos, data_store) + else { + unreachable!(); + }; + *user_movetime = Some(new_movetime.try_into().unwrap()); + *inserter = new_id; + } }, - } + }, }, _ => { warn!("Tried to set Inserter Settings on non inserter"); diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index 484969e..bd4c0e4 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -2066,7 +2066,8 @@ impl BeltStore { todo!() } - pub fn remove_inserter(&mut self, id: BeltTileId, pos: BeltLenType) { + pub fn remove_inserter( + &mut self, + id: BeltTileId, + pos: BeltLenType, + ) -> Result<(), (u32, BeltLenType)> { match id { BeltTileId::AnyBelt(index, _) => match &mut self.any_belts[index as usize] { AnyBelt::Smart(smart_belt_id) => { let smart_belt = self.inner.get_smart_mut(*smart_belt_id); - smart_belt - .remove_inserter(pos) - .expect("Failed to remove inserter from smart belt"); + match smart_belt.remove_inserter(pos) { + Ok(FakeUnionStorage { .. }) => Ok(()), + Err(()) => Err((smart_belt_id.index as u32, pos)), + } }, AnyBelt::Sushi(sushi_belt_id) => { let sushi_belt = &mut self.inner.sushi_belts[*sushi_belt_id]; @@ -3172,10 +3177,11 @@ impl BeltStore { info!("Unable to convert belt {id:?} to pure belt"); }, } + Ok(()) }, AnyBelt::Empty(_) => unimplemented!("Empty belt cannot have inserters"), }, - }; + } } pub fn change_inserter_movetime( diff --git a/src/belt/smart.rs b/src/belt/smart.rs index d93d5b7..a074f59 100644 --- a/src/belt/smart.rs +++ b/src/belt/smart.rs @@ -452,7 +452,7 @@ impl SmartBelt { pub fn remove_inserter(&mut self, pos: BeltLenType) -> Result { if usize::from(pos) >= self.locs.len() { - return Err(()); + unreachable!("Len out of range"); } match self diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index 6f2751b..9352771 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -1326,7 +1326,10 @@ fn removal_of_possible_inserter_connection { - let () = sim_state.factory.belts.remove_inserter(*id, *belt_pos); + match sim_state.factory.belts.remove_inserter(*id, *belt_pos) { + Ok(()) => {}, + Err((belt_id, belt_pos)) => todo!(), + } *info = InserterInfo::NotAttached {}; }, @@ -4929,8 +4932,108 @@ impl World match attached_inserter { - AttachedInserter::BeltStorage { id, belt_pos } => { - sim_state.factory.belts.remove_inserter(*id, *belt_pos); + AttachedInserter::BeltStorage { id, belt_pos } => 'remove: { + match sim_state.factory.belts.remove_inserter(*id, *belt_pos) { + Ok(()) => { + log::trace!( + "Successfully removed belt storage inserter from sim_state.factory.belts" + ); + }, + Err((belt_id, belt_pos)) => { + let item = sim_state + .factory + .belts + .get_pure_item(*id) + .expect("Sushi belts do not currently use waitlists"); + let start_pos = + data_store.inserter_start_pos(*ty, *pos, *direction); + + let (belt_to_storage, storage_to_belt) = &mut sim_state + .factory + .belt_storage_inserters[item.into_usize()]; + + let start_at_belt = match self.get_entity_at(start_pos, data_store).expect("Since this inserter is attached, there should be an entity there") { + Entity::Belt { .. } => {true}, + Entity::Underground { ..} => {true}, + Entity::Splitter { .. } => {true}, + _ => false + }; + + if start_at_belt { + if belt_to_storage.0.remove_inserter(belt_id, belt_pos).is_ok() + { + break 'remove; + } + if belt_to_storage.1.remove_inserter(belt_id, belt_pos).is_ok() + { + break 'remove; + } + } else { + if storage_to_belt.0.remove_inserter(belt_id, belt_pos).is_ok() + { + break 'remove; + } + if storage_to_belt.1.remove_inserter(belt_id, belt_pos).is_ok() + { + break 'remove; + } + } + + let removed_stuff = match self.get_entity_at(start_pos, data_store).expect("Since this inserter is attached, there should be an entity there") { + Entity::Assembler { info, .. } => { + let AssemblerInfo::Powered { id, pole_position, .. } = info else { + unreachable!("If an inserter is attached, the assembler must be powered with a recipe") + }; + sim_state.factory.power_grids.power_grids[sim_state.factory.power_grids.pole_pos_to_grid_id[pole_position] as usize] + .remove_waiting_inserter(*id, item, crate::chest::WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }, data_store); + true + }, + Entity::Belt { .. } => {false}, + Entity::Underground { ..} => {false}, + Entity::Splitter { .. } => {false}, + Entity::Chest { item: chest_item, .. } => { + let (chest_item, chest_id) = chest_item.expect("If a chest has an inserter it must havbe an item"); + assert_eq!(chest_item, item); + sim_state.factory.chests.stores[item.into_usize()] + .remove_inserter_from_waitlist(chest_id, crate::chest::WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }); + true + }, + Entity::Roboport { .. } => todo!(), + Entity::Lab { .. } => unreachable!("Labs currently do not have waitlists"), + Entity::MiningDrill { .. } => unreachable!("Drills currently do not have waitlists"), + + _ => unreachable!() + }; + + if !removed_stuff { + let end_pos = + data_store.inserter_end_pos(*ty, *pos, *direction); + match self.get_entity_at(end_pos, data_store).expect("Since this inserter is attached, there should be an entity there") { + Entity::Assembler { info, .. } => { + let AssemblerInfo::Powered { id, pole_position, .. } = info else { + unreachable!("If an inserter is attached, the assembler must be powered with a recipe") + }; + sim_state.factory.power_grids.power_grids[sim_state.factory.power_grids.pole_pos_to_grid_id[pole_position] as usize] + .remove_waiting_inserter(*id, item, crate::chest::WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }, data_store); + }, + Entity::Belt { .. } => unreachable!("There should be a non belt on start or end"), + Entity::Underground { .. } => unreachable!("There should be a non belt on start or end"), + Entity::Splitter { .. } => unreachable!("There should be a non belt on start or end"), + Entity::Chest { item: chest_item, .. } => { + let (chest_item, chest_id) = chest_item.expect("If a chest has an inserter it must havbe an item"); + assert_eq!(chest_item, item); + sim_state.factory.chests.stores[item.into_usize()] + .remove_inserter_from_waitlist(chest_id, crate::chest::WaitingInserterRemovalInfo::BeltStorage { belt_id, belt_pos }); + }, + Entity::Roboport { .. } => todo!(), + Entity::Lab { .. } => unreachable!("Labs currently do not have waitlists"), + Entity::MiningDrill { .. } => unreachable!("Drills currently do not have waitlists"), + + _ => unreachable!() + } + } + }, + } }, AttachedInserter::BeltBelt { inserter, .. } => { sim_state.factory.belts.remove_belt_belt_inserter(*inserter); diff --git a/src/inserter/belt_storage_movement_list.rs b/src/inserter/belt_storage_movement_list.rs index b293644..8e72e73 100644 --- a/src/inserter/belt_storage_movement_list.rs +++ b/src/inserter/belt_storage_movement_list.rs @@ -101,6 +101,28 @@ impl List Result<(), ()> { + let index = self + .lists + .iter() + .enumerate() + .find_map(|(list_index, list)| { + list.iter() + .position(|moving_inserter| { + moving_inserter.belt == belt_id && moving_inserter.belt_pos == belt_pos + }) + .map(|v| (list_index, v)) + }); + + if let Some((list_index, index)) = index { + // TODO: Swap remove might do weird things to the priority but it should be fine + let v = self.lists[list_index].swap_remove(index); + Ok(()) + } else { + Err(()) + } + } } impl<'a> FinishedMovingLists<'a, { Dir::BeltToStorage }, { Dir::BeltToStorage }> { From 4d37e57491f7dea202db6220db32754e1eacc171 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 4 Feb 2026 22:35:50 +0100 Subject: [PATCH 103/152] Create map textures preemptively, so that the user will never see the map view loading --- src/rendering/render_world.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index eb8e7c2..d619d50 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -255,13 +255,20 @@ pub fn render_world( }, num_tiles_across_screen_horizontal as u32, num_tiles_across_screen_vertical as u32, - // Only allow incremental map_view building for the last view level - map_view::MIN_WIDTH - .iter() - .all(|&v| v < num_tiles_across_screen_horizontal as u32) - .then_some(Duration::from_millis(15)), - // Some(Duration::from_millis(15)), - // None, + None, + data_store, + ); + } + + { + profiling::scope!("Create Map Textures Preemptively"); + create_map_textures_if_needed( + &world, + renderer, + Position { x: 0, y: 0 }, + 2_000_000, + 2_000_000, + Some(Duration::from_millis(1)), data_store, ); } @@ -1895,6 +1902,7 @@ pub enum EscapeMenuOptions { BackToMainMenu, } +#[profiling::function] pub fn render_ui< ItemIdxType: IdxTrait + ShowInfo, RecipeIdxType: IdxTrait + ShowInfo, From 9b0da5eeb69f81db26e7f2c8fb49b46c17744bfe Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 5 Feb 2026 07:55:23 +0100 Subject: [PATCH 104/152] only require wasm to disable native features --- Cargo.toml | 2 +- src/frontend/action/action_state_machine.rs | 8 +- src/lib.rs | 4 +- src/multiplayer/mod.rs | 2 +- src/multiplayer/server.rs | 2 +- src/rendering/eframe_app.rs | 269 ++++++++++---------- src/rendering/render_world.rs | 14 +- src/saving/mod.rs | 4 +- 8 files changed, 156 insertions(+), 149 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0f9d9b8..4cc3081 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ args = "2.2.0" console_error_panic_hook = "0.1.7" # These are all the dependencies which do not work on wasm -[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] libc = { version = "0.2.177", default-features = false } interprocess = { version = "2.2.3" } fork = "0.3.0" diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index 9c0d3fe..b5570e7 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -149,7 +149,7 @@ pub struct ActionStateMachine, pub hotbar: Hotbar, @@ -169,7 +169,7 @@ pub(crate) enum Window { Escape, } -#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] +#[cfg(not(target_arch = "wasm32"))] #[derive(Debug)] pub struct ForkSaveInfo { pub recv: interprocess::unnamed_pipe::Recver, @@ -285,7 +285,7 @@ impl mouse_wheel_sensitivity: 1.0, - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + #[cfg(not(target_arch = "wasm32"))] current_fork_save_in_progress: None, hotbar: Hotbar::new(data_store), @@ -342,7 +342,7 @@ impl mouse_wheel_sensitivity: 1.0, - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + #[cfg(not(target_arch = "wasm32"))] current_fork_save_in_progress: None, hotbar: Hotbar::new(data_store), diff --git a/src/lib.rs b/src/lib.rs index abd4abc..9f0d326 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,7 @@ use parking_lot::Mutex; use app_state::GameState; use data::{DataStore, factorio_1_1::get_raw_data_test}; #[cfg(feature = "client")] -#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] +#[cfg(not(target_arch = "wasm32"))] use eframe::NativeOptions; use frontend::world::{Position, tile::CHUNK_SIZE_FLOAT}; #[cfg(feature = "client")] @@ -155,7 +155,7 @@ impl NewWithDataStore for T { } } -#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] +#[cfg(not(target_arch = "wasm32"))] pub fn main(input: &Vec) -> Result<(), args::ArgsError> { // use ron::ser::PrettyConfig; diff --git a/src/multiplayer/mod.rs b/src/multiplayer/mod.rs index 2127081..37a85e8 100644 --- a/src/multiplayer/mod.rs +++ b/src/multiplayer/mod.rs @@ -300,7 +300,7 @@ impl Game { - if wants_keyboard { - continue; - } - }, - Event::PointerMoved(_) - | Event::MouseMoved(_) - | Event::PointerButton { .. } - | Event::PointerGone - | Event::Zoom(_) - | Event::Touch { .. } - | Event::MouseWheel { .. } => { - if wants_pointer { - continue; - } - }, - _ => {}, - } - let input = if let Event::PointerMoved(dest) = event { - let pos_normalized = [dest.x / size.width(), dest.y / size.height()]; - - let ar = size.width() / size.height(); - - if pos_normalized[0] < 0.0 - || pos_normalized[0] > 1.0 - || pos_normalized[1] < 0.0 - || pos_normalized[1] > 1.0 - { - continue; + { + profiling::scope!("Inputs"); + #[cfg(not(target_arch = "wasm32"))] + ui.input(|input_state| { + for event in &input_state.events { + match event { + Event::Copy + | Event::Cut + | Event::Paste(_) + | Event::Text(_) + | Event::Key { .. } => { + if wants_keyboard { + continue; + } + }, + Event::PointerMoved(_) + | Event::MouseMoved(_) + | Event::PointerButton { .. } + | Event::PointerGone + | Event::Zoom(_) + | Event::Touch { .. } + | Event::MouseWheel { .. } => { + if wants_pointer { + continue; + } + }, + _ => {}, } + let input = if let Event::PointerMoved(dest) = event { + let pos_normalized = + [dest.x / size.width(), dest.y / size.height()]; - Ok(Input::MouseMove( - pos_normalized[0] - 0.5, - (pos_normalized[1] - 0.5) / ar, - )) - } else { - event.clone().try_into() - }; + let ar = size.width() / size.height(); - if let Ok(input) = input { - if self.input_sender.as_mut().unwrap().send(input).is_err() { - #[cfg(not(test))] - panic!("Could not send input"); - #[allow(unreachable_code)] + if pos_normalized[0] < 0.0 + || pos_normalized[0] > 1.0 + || pos_normalized[1] < 0.0 + || pos_normalized[1] > 1.0 { - error!("Could not send input"); + continue; } - } - } - } - }); - - match &game.state { - LoadedGame::ItemU8RecipeU8(loaded_game_sized) => { - let cb = Callback { - raw_renderer: self - .raw_renderer - .clone() - .expect("Tried to Load a game without a renderer ready"), - texture_atlas: self.texture_atlas.clone(), - state_machine: loaded_game_sized.state_machine.clone(), - game_state: loaded_game_sized.state.clone(), - data_store: loaded_game_sized.data_store.clone(), - }; - painter.add(Shape::Callback(egui_wgpu::Callback::new_paint_callback( - size, cb, - ))); - - let simulation_state = loaded_game_sized.state.simulation_state.lock(); - let world = loaded_game_sized.state.world.lock(); - let aux_data = loaded_game_sized.state.aux_data.lock(); - let state_machine = loaded_game_sized.state_machine.lock(); - let data_store = loaded_game_sized.data_store.lock(); - - let tick = game.tick.load(std::sync::atomic::Ordering::Relaxed); - self.last_rendered_update = tick; + Ok(Input::MouseMove( + pos_normalized[0] - 0.5, + (pos_normalized[1] - 0.5) / ar, + )) + } else { + event.clone().try_into() + }; - match render_ui( - ctx, - ui, - state_machine, - simulation_state, - world, - aux_data, - data_store, - ) { - Ok(render_actions) => { - for action in render_actions { - #[cfg(not(target_arch = "wasm32"))] - loaded_game_sized - .ui_action_sender - .send(action) - .expect("Ui action channel died"); - - #[cfg(target_arch = "wasm32")] - render_action_vec.push(action); + if let Ok(input) = input { + if self.input_sender.as_mut().unwrap().send(input).is_err() { + #[cfg(not(test))] + panic!("Could not send input"); + #[allow(unreachable_code)] + { + error!("Could not send input"); + } } - }, - Err(escape) => match escape { - EscapeMenuOptions::BackToMainMenu => { - #[cfg(not(target_arch = "wasm32"))] - save( - "Last Exit", - Some("last_exit.save"), - &loaded_game_sized.state, - &loaded_game_sized.data_store.lock(), - ); - - self.state = AppState::MainMenu { in_ip_box: None }; - self.last_rendered_update = 0; - self.input_sender = None; - - self.currently_loaded_game = None; - }, - }, + } } - }, - LoadedGame::ItemU8RecipeU16(_loaded_game_sized) => { - todo!("Handle bigger item/recipe counts") - }, - LoadedGame::ItemU16RecipeU8(_loaded_game_sized) => { - todo!("Handle bigger item/recipe counts") - }, - LoadedGame::ItemU16RecipeU16(_loaded_game_sized) => { - todo!("Handle bigger item/recipe counts") - }, - }; + }); + } + + { + profiling::scope!("Render UI"); + match &game.state { + LoadedGame::ItemU8RecipeU8(loaded_game_sized) => { + let cb = Callback { + raw_renderer: self + .raw_renderer + .clone() + .expect("Tried to Load a game without a renderer ready"), + texture_atlas: self.texture_atlas.clone(), + state_machine: loaded_game_sized.state_machine.clone(), + game_state: loaded_game_sized.state.clone(), + data_store: loaded_game_sized.data_store.clone(), + }; + painter.add(Shape::Callback(egui_wgpu::Callback::new_paint_callback( + size, cb, + ))); + + let simulation_state = loaded_game_sized.state.simulation_state.lock(); + let world = loaded_game_sized.state.world.lock(); + let aux_data = loaded_game_sized.state.aux_data.lock(); + let state_machine = loaded_game_sized.state_machine.lock(); + let data_store = loaded_game_sized.data_store.lock(); + + let tick = game.tick.load(std::sync::atomic::Ordering::Relaxed); + + self.last_rendered_update = tick; + + match render_ui( + ctx, + ui, + state_machine, + simulation_state, + world, + aux_data, + data_store, + ) { + Ok(render_actions) => { + for action in render_actions { + #[cfg(not(target_arch = "wasm32"))] + loaded_game_sized + .ui_action_sender + .send(action) + .expect("Ui action channel died"); + + #[cfg(target_arch = "wasm32")] + render_action_vec.push(action); + } + }, + Err(escape) => match escape { + EscapeMenuOptions::BackToMainMenu => { + #[cfg(not(target_arch = "wasm32"))] + save( + "Last Exit", + Some("last_exit.save"), + &loaded_game_sized.state, + &loaded_game_sized.data_store.lock(), + ); + + self.state = AppState::MainMenu { in_ip_box: None }; + self.last_rendered_update = 0; + self.input_sender = None; + + self.currently_loaded_game = None; + }, + }, + } + }, + LoadedGame::ItemU8RecipeU16(_loaded_game_sized) => { + todo!("Handle bigger item/recipe counts") + }, + LoadedGame::ItemU16RecipeU8(_loaded_game_sized) => { + todo!("Handle bigger item/recipe counts") + }, + LoadedGame::ItemU16RecipeU16(_loaded_game_sized) => { + todo!("Handle bigger item/recipe counts") + }, + }; + } } else { warn!("No Game loaded!"); } diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index d619d50..ebabb86 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -11,7 +11,7 @@ use crate::belt::smart::SmartBelt; use crate::blueprint::blueprint_string::BlueprintString; use crate::chest::ChestSize; use crate::frontend::action::action_state_machine; -#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] +#[cfg(not(target_arch = "wasm32"))] use crate::frontend::action::action_state_machine::ForkSaveInfo; use crate::frontend::action::place_entity::EntityPlaceOptions; use crate::frontend::action::place_entity::PlaceEntityInfo; @@ -26,7 +26,7 @@ use crate::liquid::FluidSystemState; use crate::par_generation::{ParGenerateInfo, Timer}; use crate::rendering::{BeltSide, Corner}; use crate::saving::save_components; -#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] +#[cfg(not(target_arch = "wasm32"))] use crate::saving::save_with_fork; use crate::statistics::{NUM_DIFFERENT_TIMESCALES, TIMESCALE_NAMES}; use crate::{ @@ -67,7 +67,7 @@ use egui_show_info::ShowInfo; use flate2::Compression; use flate2::write::ZlibEncoder; -#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] +#[cfg(not(target_arch = "wasm32"))] use interprocess::os::unix::unnamed_pipe::UnnamedPipeExt; use itertools::Itertools; use log::error; @@ -1926,7 +1926,7 @@ pub fn render_ui< let tick = (current_tick % u64::from(state_machine_ref.autosave_interval)) as u32; - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + #[cfg(not(target_arch = "wasm32"))] if cfg!(target_os = "linux") { if tick < state_machine_ref.last_tick_seen_for_autosave { if state_machine_ref.current_fork_save_in_progress.is_none() { @@ -1998,7 +1998,7 @@ pub fn render_ui< ); }); - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + #[cfg(not(target_arch = "wasm32"))] if let Some(recv) = &mut state_machine_ref.current_fork_save_in_progress { const NUM_STATES: u8 = 12; @@ -2054,7 +2054,7 @@ pub fn render_ui< false } - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + #[cfg(not(target_arch = "wasm32"))] { cfg!(target_os = "linux") && state_machine_ref.current_fork_save_in_progress.is_none() @@ -2070,7 +2070,7 @@ pub fn render_ui< }) .clicked() { - #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] + #[cfg(not(target_arch = "wasm32"))] { let recv = save_with_fork( &aux_data.game_name, diff --git a/src/saving/mod.rs b/src/saving/mod.rs index 2b3f830..9ceb0ec 100644 --- a/src/saving/mod.rs +++ b/src/saving/mod.rs @@ -270,7 +270,7 @@ pub fn save_components( /// # Panics /// If File system stuff fails -#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] +#[cfg(not(target_arch = "wasm32"))] pub fn save_components_fork_safe( name: &str, save_name: Option<&str>, @@ -408,7 +408,7 @@ pub fn save_components_fork_safe /// # Panics /// If File system stuff fails -#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] +#[cfg(not(target_arch = "wasm32"))] pub fn save_with_fork( name: &str, save_name: Option<&str>, From 704c7954822265fe7a21050249a999431489a77d Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 5 Feb 2026 08:03:01 +0100 Subject: [PATCH 105/152] Use crane for wasm package and add trunk --- flake.lock | 28 ++++++++++++++++----- flake.nix | 72 +++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/flake.lock b/flake.lock index ba94eac..e8998d6 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,20 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1770169865, + "narHash": "sha256-iPiy13xzDQ9GjpOez+NNIjh/qjl7i4RDf9dF2x5mF9I=", + "owner": "ipetkov", + "repo": "crane", + "rev": "8254ccf3b5b5131890ee073776f2e61c6d1e55d4", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "fenix": { "inputs": { "nixpkgs": [ @@ -8,11 +23,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1770102568, - "narHash": "sha256-VYwA9FmakKJ3zLfAd7bdj9xIB9PzfISLoYh6eZl+EuQ=", + "lastModified": 1770188896, + "narHash": "sha256-ZBpEh6aTvdoZvIM8sojnr43FPyegq/sjUkNWtF4kDO8=", "owner": "nix-community", "repo": "fenix", - "rev": "592daa37b5a3175c61541329b64d6c1972303bc1", + "rev": "6dbe5750c68a55e7cb3f8b62883c4dbfeecf14d5", "type": "github" }, "original": { @@ -39,6 +54,7 @@ }, "root": { "inputs": { + "crane": "crane", "fenix": "fenix", "nixpkgs": "nixpkgs" } @@ -46,11 +62,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1770026591, - "narHash": "sha256-VZlloygYDmozJwbZZkCSNpiPhNdOW/AA0b6LmNBZ3xU=", + "lastModified": 1770092556, + "narHash": "sha256-DcUKN1nzz7LhyZGJMhSBhY5zrl8/GjJJ9SNqqTd77OQ=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "74eca73f3b0a41b80228b8e499c7547cc8b2effa", + "rev": "a84d92ff213e30fb00d7b812e07c4f67e99dcd29", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 947abf7..a4e7fd5 100644 --- a/flake.nix +++ b/flake.nix @@ -5,19 +5,29 @@ url = "github:nix-community/fenix"; inputs.nixpkgs.follows = "nixpkgs"; }; - - # crane.url = "github:ipetkov/crane"; + crane.url = "github:ipetkov/crane"; }; - outputs = { self, nixpkgs, fenix }: let + outputs = { self, nixpkgs, fenix, crane }: let + inherit (nixpkgs) lib; pkgs = nixpkgs.legacyPackages."x86_64-linux"; fenixLib = fenix.packages."x86_64-linux"; + toolchain_sha = "sha256-dXoddWaPL6UtPscTpxMUMBDL83jFtqeDtmH/+bXBs3E="; + rustToolchain = fenixLib.fromToolchainFile { file = ./rust-toolchain.toml; - sha256 = "sha256-dXoddWaPL6UtPscTpxMUMBDL83jFtqeDtmH/+bXBs3E="; + sha256 = toolchain_sha; }; + wasmToolchain = fenixLib.combine [ + (fenixLib.targets.wasm32-unknown-unknown.fromToolchainFile { + file = ./rust-toolchain.toml; + sha256 = toolchain_sha; + }) + rustToolchain + ]; + neededPackages = with pkgs; [ wayland xorg.libX11 @@ -31,12 +41,18 @@ vulkan-headers vulkan-loader ]; - client_package = (pkgs.makeRustPlatform { - cargo = rustToolchain; - rustc = rustToolchain; - }).buildRustPackage { + client_package_for_target = { + target, toolchain + }: ((crane.mkLib nixpkgs.legacyPackages.${pkgs.system}).overrideToolchain toolchain).buildPackage { name = "factory"; + CARGO_BUILD_TARGET = target; + meta = { + homepage = "https://www.github.com/BloodStainedCrow/FactoryGame/"; + maintainers = with lib.maintainers; [ BloodStainedCrow ]; + mainProgram = "factory"; + }; src = ./.; + buildInputs = neededPackages; nativeBuildInputs = [ pkgs.pkg-config pkgs.makeWrapper ]; cargoHash = "sha256-83+1Y486PUHM9+uyFw+yJ9bNMlMbN/fc8cYRzKmDdb8="; @@ -47,6 +63,8 @@ wrapProgram "$out/bin/factory" --prefix LD_LIBRARY_PATH : "${builtins.toString (pkgs.lib.makeLibraryPath neededPackages)}" ''; }; + + client_package = client_package_for_target { target = "x86_64-unknown-linux-gnu"; toolchain = rustToolchain; }; in { devShells."x86_64-linux".codium = pkgs.mkShell { buildInputs = with pkgs; [ @@ -77,19 +95,45 @@ }; packages."x86_64-linux".default = client_package; + packages."x86_64-linux".dedicated_server = client_package.overrideAttrs ( oldAttrs: { cargoBuildFlags = [ "--no-default-features" "-F logging" ]; }); - "wasm" = client_package.overrideAttrs ( oldAttrs: { - CARGO_BUILD_TARGET = "wasm32-wasi"; - nativeBuildInputs = oldAttrs.nativeBuildInputs ++ [ pkgs.wabt ]; + "wasm" = (client_package_for_target { target = "wasm32-unknown-unknown"; toolchain = wasmToolchain; }).overrideAttrs ( oldAttrs: { + nativeBuildInputs = oldAttrs.nativeBuildInputs ++ [ pkgs.wabt pkgs.binaryen ]; postInstall = '' mkdir -p $out/lib ls -lR $out - wasm-strip $out/bin/factory -o $out/lib/factory.wasm - rm -rf $out/bin + wasm-strip $out/bin/factory.wasm -o $out/lib/factory.wasm + # TODO: Use wasm-opt + # wasm-opt $out/lib/factory.wasm -o $out/lib/factory.wasm -O4 + rm -r $out/bin wasm-validate $out/lib/factory.wasm ''; + + # We cannot run tests on a wasm binary + doCheck = false; }); - packages."x86_64-linux".dedicated_server = client_package.overrideAttrs ( oldAttrs: { cargoBuildFlags = [ "--no-default-features" "-F logging" ]; }); + "trunk" = ((crane.mkLib nixpkgs.legacyPackages.${pkgs.system}).overrideToolchain wasmToolchain).buildTrunkPackage { + nativeBuildInputs = [ pkgs.lld ]; + CARGO_BUILD_TARGET = "wasm32-unknown-unknown"; + src = ./.; + + wasm-bindgen-cli = pkgs.buildWasmBindgenCli rec { + src = pkgs.fetchCrate { + pname = "wasm-bindgen-cli"; + version = "0.2.106"; + hash = "sha256-M6WuGl7EruNopHZbqBpucu4RWz44/MSdv6f0zkYw+44="; + # hash = lib.fakeHash; + }; + + cargoDeps = pkgs.rustPlatform.fetchCargoVendor { + inherit src; + inherit (src) pname version; + hash = "sha256-ElDatyOwdKwHg3bNH/1pcxKI7LXkhsotlDPQjiLHBwA="; + # hash = lib.fakeHash; + }; + }; + }; + }; } From a733d3f199ac7f93c8604c3c2b29509b13a000f4 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 5 Feb 2026 22:13:24 +0100 Subject: [PATCH 106/152] Correctly give back the blueprint string in case of error --- src/blueprint/blueprint_string.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/blueprint/blueprint_string.rs b/src/blueprint/blueprint_string.rs index 966a0fe..06f7ebf 100644 --- a/src/blueprint/blueprint_string.rs +++ b/src/blueprint/blueprint_string.rs @@ -60,8 +60,13 @@ struct BlueprintStringInternal { modules: Vec<(Position, usize)>, } +#[derive(Debug)] +pub enum BlueprintImportError { + BlueprintStringInvalid(BlueprintString), +} + impl TryFrom for Blueprint { - type Error = (); + type Error = BlueprintImportError; fn try_from(value: BlueprintString) -> Result { let raw_str = value.0; @@ -71,7 +76,9 @@ impl TryFrom for Blueprint { let Ok(internal) = bincode::serde::decode_from_reader(dec, bincode::config::standard()) else { error!("Blueprint failed to deserialize!"); - return Err(()); + return Err(BlueprintImportError::BlueprintStringInvalid( + BlueprintString(raw_str), + )); }; let BlueprintStringInternal { From 3f29901218c6b79a8e60a8b5d881e0c129290ec9 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 5 Feb 2026 22:40:27 +0100 Subject: [PATCH 107/152] Tell the flake this needs lfs to build correctly --- flake.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flake.nix b/flake.nix index a4e7fd5..c781f74 100644 --- a/flake.nix +++ b/flake.nix @@ -66,6 +66,9 @@ client_package = client_package_for_target { target = "x86_64-unknown-linux-gnu"; toolchain = rustToolchain; }; in { + inputs.self.lfs = true; + + devShells."x86_64-linux".codium = pkgs.mkShell { buildInputs = with pkgs; [ bashInteractive From e3b48bce61045e210bedce4f25fa5486fd69864e Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 5 Feb 2026 22:55:00 +0100 Subject: [PATCH 108/152] Fix the flake self lfs requirement --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index c781f74..02dad5d 100644 --- a/flake.nix +++ b/flake.nix @@ -8,6 +8,7 @@ crane.url = "github:ipetkov/crane"; }; + inputs.self.lfs = true; outputs = { self, nixpkgs, fenix, crane }: let inherit (nixpkgs) lib; pkgs = nixpkgs.legacyPackages."x86_64-linux"; @@ -66,7 +67,6 @@ client_package = client_package_for_target { target = "x86_64-unknown-linux-gnu"; toolchain = rustToolchain; }; in { - inputs.self.lfs = true; devShells."x86_64-linux".codium = pkgs.mkShell { From 3fc4ba5f36ad1dee94794805424376845e4b535d Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 6 Feb 2026 06:46:22 +0100 Subject: [PATCH 109/152] Remove unneeded deps --- Cargo.lock | 308 ++++++++++++++++++++++------------------------------- Cargo.toml | 12 +-- 2 files changed, 134 insertions(+), 186 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 687b4d4..34b590a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,9 +212,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arbitrary" @@ -244,9 +244,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +checksum = "9ded5f9a03ac8f24d1b8a25101ee812cd32cdc8c50a4c50237de2c4915850e73" dependencies = [ "rustversion", ] @@ -555,7 +555,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror 2.0.17", + "thiserror 2.0.18", "v_frame", "y4m", ] @@ -794,9 +794,9 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] @@ -826,9 +826,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "calloop" @@ -883,9 +883,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "jobserver", @@ -951,9 +951,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", @@ -978,7 +978,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1012,11 +1012,11 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colored" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1403,7 +1403,7 @@ dependencies = [ "egui", "enum-map", "parking_lot 0.12.5", - "petgraph 0.8.2", + "petgraph", ] [[package]] @@ -1497,7 +1497,7 @@ dependencies = [ "egui", "getrandom 0.2.17", "instant", - "petgraph 0.8.2", + "petgraph", "rand 0.9.2", "serde", ] @@ -1669,9 +1669,9 @@ checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "euclid" -version = "0.22.11" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" dependencies = [ "num-traits", ] @@ -1738,12 +1738,10 @@ dependencies = [ "egui_graphs", "egui_plot", "enum-map", - "fixed-buffer", "flate2", "fork", "genawaiter", "get-size2", - "getrandom 0.2.17", "getrandom 0.3.4", "hex", "image", @@ -1756,7 +1754,7 @@ dependencies = [ "noise", "open", "parking_lot 0.12.5", - "petgraph 0.8.2", + "petgraph", "postcard", "profiling", "proptest", @@ -1775,7 +1773,6 @@ dependencies = [ "sha2", "simple_logger", "spin_sleep_util", - "stable-vec", "static_assertions", "strum 0.27.2", "take_mut", @@ -1826,21 +1823,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" - -[[package]] -name = "fixed-buffer" -version = "1.0.2" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e394ec858f7f07a580a2e63c6da5c4632cd7503da034d6a545c75516a2df91" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -1850,13 +1835,13 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "libz-rs-sys", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -2110,9 +2095,9 @@ dependencies = [ [[package]] name = "get-size-derive2" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab21d7bd2c625f2064f04ce54bcb88bc57c45724cde45cba326d784e22d3f71a" +checksum = "f2b6d1e2f75c16bfbcd0f95d84f99858a6e2f885c2287d1f5c3a96e8444a34b4" dependencies = [ "attribute-derive", "quote", @@ -2121,9 +2106,9 @@ dependencies = [ [[package]] name = "get-size2" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879272b0de109e2b67b39fcfe3d25fdbba96ac07e44a254f5a0b4d7ff55340cb" +checksum = "49cf31a6d70300cf81461098f7797571362387ef4bf85d32ac47eaa59b3a5a1a" dependencies = [ "get-size-derive2", ] @@ -2192,9 +2177,9 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "git2" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" +checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ "bitflags 2.10.0", "libc", @@ -2216,9 +2201,9 @@ dependencies = [ [[package]] name = "glam" -version = "0.30.10" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" +checksum = "74a4d85559e2637d3d839438b5b3d75c31e655276f9544d72475c36b92fabbed" [[package]] name = "glob" @@ -2434,9 +2419,9 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2578,8 +2563,8 @@ dependencies = [ "rayon", "rgb", "tiff", - "zune-core 0.5.0", - "zune-jpeg 0.5.7", + "zune-core 0.5.1", + "zune-jpeg 0.5.12", ] [[package]] @@ -2648,9 +2633,9 @@ checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" [[package]] name = "interprocess" -version = "2.2.3" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +checksum = "7b00d05442c2106c75b7410f820b152f61ec0edc7befcb9b381b673a20314753" dependencies = [ "doctest-file", "libc", @@ -2727,9 +2712,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -2815,9 +2800,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" @@ -2840,15 +2825,6 @@ dependencies = [ "redox_syscall 0.7.0", ] -[[package]] -name = "libz-rs-sys" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" -dependencies = [ - "zlib-rs", -] - [[package]] name = "libz-sys" version = "1.1.23" @@ -3080,7 +3056,7 @@ dependencies = [ "rustc-hash 1.1.0", "spirv", "strum 0.26.3", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-ident", ] @@ -3135,12 +3111,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "no-std-compat" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df270209a7f04d62459240d890ecb792714d5db12c92937823574a09930276b4" - [[package]] name = "nohash-hasher" version = "0.2.0" @@ -3659,10 +3629,8 @@ version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "backtrace", "cfg-if", "libc", - "petgraph 0.6.5", "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", @@ -3692,22 +3660,12 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset 0.4.2", - "indexmap", -] - [[package]] name = "petgraph" version = "0.8.2" source = "git+https://github.com/BloodStainedCrow/petgraph?branch=stable_graph_node_weights_mut_indexed#a2146326d17db154ac872a4d029fc1a4e546cc84" dependencies = [ - "fixedbitset 0.5.7", + "fixedbitset", "hashbrown 0.15.5", "indexmap", "rayon", @@ -3862,9 +3820,9 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "postcard" @@ -3963,9 +3921,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3992,9 +3950,9 @@ dependencies = [ [[package]] name = "proptest" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", @@ -4086,9 +4044,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -4143,7 +4101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4153,7 +4111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4164,9 +4122,9 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -4186,7 +4144,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4195,7 +4153,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4234,7 +4192,7 @@ dependencies = [ "rand 0.9.2", "rand_chacha", "simd_helpers", - "thiserror 2.0.17", + "thiserror 2.0.18", "v_frame", "wasm-bindgen", ] @@ -4336,14 +4294,14 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -4353,9 +4311,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -4364,9 +4322,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "relative-path" @@ -4396,9 +4354,9 @@ dependencies = [ [[package]] name = "rfd" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069d6129dede311430d0dcf1ded88a7affc7a342c2d8e6336043d43ed14dac17" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" dependencies = [ "block2 0.6.2", "dispatch2", @@ -4493,9 +4451,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" @@ -4750,15 +4708,15 @@ dependencies = [ [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slotmap" @@ -4814,7 +4772,7 @@ dependencies = [ "log", "memmap2", "rustix 1.1.3", - "thiserror 2.0.17", + "thiserror 2.0.18", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -4904,15 +4862,6 @@ dependencies = [ "bitflags 2.10.0", ] -[[package]] -name = "stable-vec" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1dff32a2ce087283bec878419027cebd888760d8760b2941ad0843531dc9ec8" -dependencies = [ - "no-std-compat", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -5091,11 +5040,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -5111,9 +5060,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -5159,9 +5108,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", "itoa", @@ -5169,22 +5118,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" dependencies = [ "num-conv", "time-core", @@ -5463,9 +5412,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "js-sys", "serde_core", @@ -5543,9 +5492,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -5556,11 +5505,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -5569,9 +5519,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5579,9 +5529,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -5592,9 +5542,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -5751,9 +5701,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -5842,7 +5792,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", "wgpu-core-deps-wasm", @@ -5926,7 +5876,7 @@ dependencies = [ "raw-window-handle", "renderdoc-sys", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "wasm-bindgen", "web-sys", "wgpu-types", @@ -5944,7 +5894,7 @@ dependencies = [ "bytemuck", "js-sys", "log", - "thiserror 2.0.17", + "thiserror 2.0.18", "web-sys", ] @@ -6610,9 +6560,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.13.1" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f79257df967b6779afa536788657777a0001f5b42524fcaf5038d4344df40b" +checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" dependencies = [ "async-broadcast", "async-executor", @@ -6669,9 +6619,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.13.1" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aad23e2d2f91cae771c7af7a630a49e755f1eb74f8a46e9f6d5f7a146edf5a37" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -6707,18 +6657,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", @@ -6781,15 +6731,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.5" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" +checksum = "a7948af682ccbc3342b6e9420e8c51c1fe5d7bf7756002b4a3c6cabfe96a7e3c" [[package]] name = "zmij" -version = "1.0.13" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" [[package]] name = "zune-core" @@ -6799,9 +6749,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-core" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" [[package]] name = "zune-inflate" @@ -6823,18 +6773,18 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.7" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d915729b0e7d5fe35c2f294c5dc10b30207cc637920e5b59077bfa3da63f28" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" dependencies = [ - "zune-core 0.5.0", + "zune-core 0.5.1", ] [[package]] name = "zvariant" -version = "5.9.1" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "326aaed414f04fe839777b4c443d4e94c74e7b3621093bd9c5e649ac8aa96543" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" dependencies = [ "endi", "enumflags2", @@ -6846,9 +6796,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.9.1" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba44e1f8f4da9e6e2d25d2a60b116ef8b9d0be174a7685e55bb12a99866279a7" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 4cc3081..b66735d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ petgraph = { version = "0.8.2", features = ["rayon", "serde", "serde-1", "serde_ sha2 = "0.10.8" hex = "0.4.3" postcard = { version = "1.1.1", features = ["use-std"] } +# Remove this. I no longer use this as the charts are ingame, and it just makes the code awful charts-rs = { version = "0.3.20", features = ["resvg"] } strum = { version = "0.27.1", features = ["derive"] } # explicitly disable atomic feature, so that bitvecs do not use atomic instructions. very important for performance! @@ -39,12 +40,11 @@ rand = "0.9.0" bitcode = { version = "0.6.6", features = ["serde"] } egui = { version = "0.32", features = ["bytemuck", "serde"], optional = true } flate2 = { version = "1.1.1", features = ["zlib-rs"] } -rstest = "0.25.0" -parking_lot = { version = "0.12.3", features = ["serde", "deadlock_detection"] } +parking_lot = { version = "0.12.3", features = ["serde"] } profiling = { version = "1.0.16" } puffin_egui = { git = "https://github.com/EmbarkStudios/puffin", optional = true } puffin = { git = "https://github.com/EmbarkStudios/puffin", features = ["web"] } -dhat = "0.3.3" +dhat = {version = "0.3.3", optional = true } noise = { version = "0.9.0", features = ["std"] } rfd = { version = "0.17", optional = true } egui_graphs = { version = "0.28", optional = true } @@ -58,15 +58,12 @@ memoffset = "0.9.1" ecolor = { version = "0.32", features = ["color-hex"] } getrandom = { version = "0.3.3", features = ["wasm_js"] } -getrandom_old = { version = "0.2.16", features = ["js"], package = "getrandom" } wasm-bindgen = "0.2.104" wasm-bindgen-futures = "0.4.54" wasm-timer = "0.2.5" bincode = { version = "2.0.1", features = ["serde"] } thin-dst = "1.1.0" -stable-vec = "0.4.1" recycle_vec = "1.1.2" -fixed-buffer = "1.0.2" base64 = "0.22.1" mimalloc = { version = "0.1.48", features = ["v3"] } rustc-hash = "2.1.1" @@ -89,6 +86,7 @@ built = {version = "0.8", features= ["git2", "chrono"]} [dev-dependencies] winit = "0.30.12" proptest = "1.4.0" +rstest = "0.25.0" [patch.crates-io] puffin_egui = { git = "https://github.com/EmbarkStudios/puffin", optional = true } @@ -137,7 +135,7 @@ default = ["profiler", "graphics", "client", "logging"] # Use Krastorio2 graphics. Since I have not properly added licensing information, I currently do not push them, therefore this feature is broken graphics = [] # dhat-rs memory profiling (https://docs.rs/dhat/latest/dhat/) -dhat-heap = [] +dhat-heap = [ "dhat" ] profiler = ["profiling/profile-with-puffin"] client = [ "dep:eframe", "dep:egui", "dep:egui_extras", "dep:egui_plot", "dep:puffin_egui", "dep:egui_graphs", "dep:egui-show-info", "dep:egui-show-info-derive", "dep:tilelib", "dep:image", "dep:rfd", "dep:get-size2"] logging = ["simple_logger"] From e5e685da43c6fbcc8ed2c48d688181bd68778dc3 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 6 Feb 2026 19:28:07 +0100 Subject: [PATCH 110/152] Setup flake to correctly pass built info via env vars --- flake.lock | 12 ++++++------ flake.nix | 8 ++++++++ src/rendering/eframe_app.rs | 8 ++++---- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index e8998d6..d98e035 100644 --- a/flake.lock +++ b/flake.lock @@ -23,11 +23,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1770188896, - "narHash": "sha256-ZBpEh6aTvdoZvIM8sojnr43FPyegq/sjUkNWtF4kDO8=", + "lastModified": 1770275419, + "narHash": "sha256-g2wfAevB/IFF6Y1C74TbhRERlUVFVGQsgGp/lLR4lQM=", "owner": "nix-community", "repo": "fenix", - "rev": "6dbe5750c68a55e7cb3f8b62883c4dbfeecf14d5", + "rev": "aa3fbaab2bdc73c5c1e25a124c272dde295bc957", "type": "github" }, "original": { @@ -62,11 +62,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1770092556, - "narHash": "sha256-DcUKN1nzz7LhyZGJMhSBhY5zrl8/GjJJ9SNqqTd77OQ=", + "lastModified": 1770200365, + "narHash": "sha256-Z3V5v8tSwZ3l4COVSt0b6Av0wZwTUf7Qj0SQ2/Z5RX0=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "a84d92ff213e30fb00d7b812e07c4f67e99dcd29", + "rev": "1433910d1ffaff2c7a5fb7ba701f82ea578a99e3", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 02dad5d..c1a34c3 100644 --- a/flake.nix +++ b/flake.nix @@ -46,6 +46,14 @@ target, toolchain }: ((crane.mkLib nixpkgs.legacyPackages.${pkgs.system}).overrideToolchain toolchain).buildPackage { name = "factory"; + + # info for built + # BUILT_OVERRIDE_factory_GIT_DIRTY = if self.revDirty then "true" else "false"; + BUILT_OVERRIDE_factory_GIT_HEAD_REF = self.ref or null; + BUILT_OVERRIDE_factory_GIT_COMMIT_HASH = self.rev or null; + BUILT_OVERRIDE_factory_GIT_COMMIT_HASH_SHORT = self.revShort or null; + SOURCE_DATE_EPOCH = self.lastModified; + CARGO_BUILD_TARGET = target; meta = { homepage = "https://www.github.com/BloodStainedCrow/FactoryGame/"; diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index 361ff39..102371d 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -392,15 +392,15 @@ impl eframe::App for App { ui.label(crate::built_info::PKG_VERSION); } else { let version = crate::built_info::GIT_VERSION - .unwrap_or("Could not get git version"); + .unwrap_or(crate::built_info::GIT_COMMIT_HASH_SHORT.unwrap_or("Could not get git version")); ui.label(version); } ui.end_row(); // TODO: This does not work because of nixos :/ - // ui.label("Built at:"); - // ui.label(crate::built_info::BUILT_TIME_UTC); - // ui.end_row(); + ui.label("Built at:"); + ui.label(crate::built_info::BUILT_TIME_UTC); + ui.end_row(); }) }); From 99bc7ac8745bcbea0b3e330d6639aef343f8ed54 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 6 Feb 2026 20:24:23 +0100 Subject: [PATCH 111/152] Correct wasm-bindgen-cli version --- flake.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index c1a34c3..ead7522 100644 --- a/flake.nix +++ b/flake.nix @@ -132,15 +132,15 @@ wasm-bindgen-cli = pkgs.buildWasmBindgenCli rec { src = pkgs.fetchCrate { pname = "wasm-bindgen-cli"; - version = "0.2.106"; - hash = "sha256-M6WuGl7EruNopHZbqBpucu4RWz44/MSdv6f0zkYw+44="; + version = "0.2.108"; + hash = "sha256-UsuxILm1G6PkmVw0I/JF12CRltAfCJQFOaT4hFwvR8E="; # hash = lib.fakeHash; }; cargoDeps = pkgs.rustPlatform.fetchCargoVendor { inherit src; inherit (src) pname version; - hash = "sha256-ElDatyOwdKwHg3bNH/1pcxKI7LXkhsotlDPQjiLHBwA="; + hash = "sha256-iqQiWbsKlLBiJFeqIYiXo3cqxGLSjNM8SOWXGM9u43E="; # hash = lib.fakeHash; }; }; From 00c27425ce101b7cbebf82df377a05bcf60ae14b Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sat, 7 Feb 2026 22:53:39 +0100 Subject: [PATCH 112/152] Correctly use built info for all builds --- flake.nix | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/flake.nix b/flake.nix index ead7522..daaf9f3 100644 --- a/flake.nix +++ b/flake.nix @@ -42,17 +42,21 @@ vulkan-headers vulkan-loader ]; - client_package_for_target = { - target, toolchain - }: ((crane.mkLib nixpkgs.legacyPackages.${pkgs.system}).overrideToolchain toolchain).buildPackage { - name = "factory"; - + built_overrides = { # info for built # BUILT_OVERRIDE_factory_GIT_DIRTY = if self.revDirty then "true" else "false"; BUILT_OVERRIDE_factory_GIT_HEAD_REF = self.ref or null; BUILT_OVERRIDE_factory_GIT_COMMIT_HASH = self.rev or null; BUILT_OVERRIDE_factory_GIT_COMMIT_HASH_SHORT = self.revShort or null; SOURCE_DATE_EPOCH = self.lastModified; + }; + + client_package_for_target = { + target, toolchain + }: ((crane.mkLib nixpkgs.legacyPackages.${pkgs.system}).overrideToolchain toolchain).buildPackage ({ + name = "factory"; + + BUILT_OVERRIDE_factory_GIT_COMMIT_HASH_SHORT = "BLUB"; CARGO_BUILD_TARGET = target; meta = { @@ -71,7 +75,7 @@ postInstall = '' wrapProgram "$out/bin/factory" --prefix LD_LIBRARY_PATH : "${builtins.toString (pkgs.lib.makeLibraryPath neededPackages)}" ''; - }; + } // built_overrides); client_package = client_package_for_target { target = "x86_64-unknown-linux-gnu"; toolchain = rustToolchain; }; in { @@ -124,7 +128,7 @@ doCheck = false; }); - "trunk" = ((crane.mkLib nixpkgs.legacyPackages.${pkgs.system}).overrideToolchain wasmToolchain).buildTrunkPackage { + "trunk" = ((crane.mkLib nixpkgs.legacyPackages.${pkgs.system}).overrideToolchain wasmToolchain).buildTrunkPackage ({ nativeBuildInputs = [ pkgs.lld ]; CARGO_BUILD_TARGET = "wasm32-unknown-unknown"; src = ./.; @@ -144,7 +148,7 @@ # hash = lib.fakeHash; }; }; - }; + } // built_overrides); }; } From 473b925d35ceadb0c9989eac2806479cfe6e4669 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 8 Feb 2026 02:11:44 +0100 Subject: [PATCH 113/152] Add some onclick info to beacons --- src/rendering/render_world.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index ebabb86..deeebc0 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -3987,7 +3987,23 @@ pub fn render_ui< ui.add(ProgressBar::new(perc).text(format!("{}/{}", charge, max_charge)).corner_radius(0.0)); }, Entity::Beacon { ty, modules, .. } => { - // TODO + ui.label(&data_store.beacon_info[*ty as usize].display_name); + + // Render module slots + let modules = &game_state_ref.world.module_slot_dedup_table[*modules as usize]; + TableBuilder::new(ui).id_salt("Module Slots").columns(Column::auto(), modules.len()).body(|mut body| { + body.row(1.0, |mut row| { + for module in modules.iter() { + row.col(|ui| { + if let Some(module_id) = module { + ui.label(&data_store_ref.module_info[*module_id as usize].display_name); + } else { + ui.label("Empty Module Slot"); + } + }); + } + }); + }); }, Entity::FluidTank { ty, pos, rotation } => { let id = game_state_ref.simulation_state.factory.fluid_store.fluid_box_pos_to_network_id[pos]; From fbe513af6f4af1935438d8df16a8f37586a257e5 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 8 Feb 2026 06:52:29 +0100 Subject: [PATCH 114/152] remove dependence on charts_rs --- Cargo.lock | 348 +--------------------------------- Cargo.toml | 4 +- src/statistics/consumption.rs | 2 +- src/statistics/mod.rs | 42 ++-- src/statistics/power.rs | 3 +- src/statistics/production.rs | 2 +- src/statistics/research.rs | 4 +- src/statistics/time_usage.rs | 4 +- 8 files changed, 32 insertions(+), 377 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34b590a..01186b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,12 +168,6 @@ dependencies = [ "equator", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android-activity" version = "0.6.0" @@ -242,15 +236,6 @@ dependencies = [ "x11rb", ] -[[package]] -name = "arc-swap" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ded5f9a03ac8f24d1b8a25101ee812cd32cdc8c50a4c50237de2c4915850e73" -dependencies = [ - "rustversion", -] - [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -920,35 +905,6 @@ dependencies = [ "libc", ] -[[package]] -name = "charts-rs" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46613e62ca4b6ff0b9e262adaf27dc3113aaf264accb90db048b833c6f95ad45" -dependencies = [ - "ahash", - "arc-swap", - "charts-rs-derive", - "fontdue", - "once_cell", - "regex", - "resvg", - "serde", - "serde_json", - "snafu", - "substring", -] - -[[package]] -name = "charts-rs-derive" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d38f1088dcf6ce3487a09c49fc2d2f8759045603f24d5814357e9283260426" -dependencies = [ - "quote", - "syn 2.0.114", -] - [[package]] name = "chrono" version = "0.4.43" @@ -1107,15 +1063,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "core_maths" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" -dependencies = [ - "libm", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -1187,12 +1134,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" -[[package]] -name = "data-url" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" - [[package]] name = "deranged" version = "0.5.5" @@ -1667,15 +1608,6 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" -[[package]] -name = "euclid" -version = "0.22.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" -dependencies = [ - "num-traits", -] - [[package]] name = "event-listener" version = "5.4.1" @@ -1724,7 +1656,6 @@ dependencies = [ "bitvec", "built", "bytemuck", - "charts-rs", "chrono", "console_error_panic_hook", "dhat", @@ -1844,12 +1775,6 @@ dependencies = [ "zlib-rs", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - [[package]] name = "fnv" version = "1.0.7" @@ -1862,38 +1787,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "fontconfig-parser" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" -dependencies = [ - "roxmltree", -] - -[[package]] -name = "fontdb" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" -dependencies = [ - "fontconfig-parser", - "log", - "slotmap", - "tinyvec", - "ttf-parser 0.25.1", -] - -[[package]] -name = "fontdue" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b" -dependencies = [ - "hashbrown 0.15.5", - "ttf-parser 0.21.1", -] - [[package]] name = "foreign-types" version = "0.5.0" @@ -2367,8 +2260,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash", "rayon", ] @@ -2557,7 +2448,7 @@ dependencies = [ "image-webp", "moxcms", "num-traits", - "png 0.18.0", + "png", "qoi", "ravif", "rayon", @@ -2577,12 +2468,6 @@ dependencies = [ "quick-error 2.0.1", ] -[[package]] -name = "imagesize" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" - [[package]] name = "imgref" version = "1.12.0" @@ -2633,9 +2518,9 @@ checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" [[package]] name = "interprocess" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b00d05442c2106c75b7410f820b152f61ec0edc7befcb9b381b673a20314753" +checksum = "53bf2b0e0785c5394a7392f66d7c4fb9c653633c29b27a932280da3cb344c66a" dependencies = [ "doctest-file", "libc", @@ -2737,17 +2622,6 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" -[[package]] -name = "kurbo" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" -dependencies = [ - "arrayvec", - "euclid", - "smallvec", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -2945,9 +2819,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" @@ -3579,7 +3453,7 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" dependencies = [ - "ttf-parser 0.25.1", + "ttf-parser", ] [[package]] @@ -3717,12 +3591,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - [[package]] name = "pin-project" version = "1.1.10" @@ -3772,19 +3640,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - [[package]] name = "png" version = "0.18.0" @@ -4338,20 +4193,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" -[[package]] -name = "resvg" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" -dependencies = [ - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", -] - [[package]] name = "rfd" version = "0.17.2" @@ -4384,9 +4225,6 @@ name = "rgb" version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" -dependencies = [ - "bytemuck", -] [[package]] name = "ron" @@ -4413,12 +4251,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - [[package]] name = "rstest" version = "0.25.0" @@ -4520,24 +4352,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "rustybuzz" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" -dependencies = [ - "bitflags 2.10.0", - "bytemuck", - "core_maths", - "log", - "smallvec", - "ttf-parser 0.25.1", - "unicode-bidi-mirroring", - "unicode-ccc", - "unicode-properties", - "unicode-script", -] - [[package]] name = "same-file" version = "1.0.6" @@ -4697,15 +4511,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "simplecss" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" -dependencies = [ - "log", -] - [[package]] name = "siphasher" version = "1.0.2" @@ -4805,27 +4610,6 @@ dependencies = [ "serde", ] -[[package]] -name = "snafu" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" -dependencies = [ - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "spin" version = "0.9.8" @@ -4879,9 +4663,6 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] [[package]] name = "strum" @@ -4926,25 +4707,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "substring" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" -dependencies = [ - "autocfg", -] - -[[package]] -name = "svgtypes" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" -dependencies = [ - "kurbo", - "siphasher", -] - [[package]] name = "syn" version = "1.0.109" @@ -5150,7 +4912,6 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "png 0.17.16", "tiny-skia-path", ] @@ -5175,21 +4936,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -5252,20 +4998,11 @@ dependencies = [ "once_cell", ] -[[package]] -name = "ttf-parser" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" - [[package]] name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -dependencies = [ - "core_maths", -] [[package]] name = "type-map" @@ -5305,54 +5042,18 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-bidi-mirroring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" - -[[package]] -name = "unicode-ccc" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" - [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - -[[package]] -name = "unicode-script" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" - [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unicode-vo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" - [[package]] name = "unicode-width" version = "0.2.2" @@ -5377,33 +5078,6 @@ dependencies = [ "serde", ] -[[package]] -name = "usvg" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" -dependencies = [ - "base64 0.22.1", - "data-url", - "flate2", - "fontdb", - "imagesize", - "kurbo", - "log", - "pico-args", - "roxmltree", - "rustybuzz", - "simplecss", - "siphasher", - "strict-num", - "svgtypes", - "tiny-skia-path", - "unicode-bidi", - "unicode-script", - "unicode-vo", - "xmlwriter", -] - [[package]] name = "utf8_iter" version = "1.0.4" @@ -5721,9 +5395,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" +checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" dependencies = [ "core-foundation 0.10.1", "jni", @@ -6523,12 +6197,6 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - [[package]] name = "y4m" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index b66735d..a94c8e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,8 +26,6 @@ petgraph = { version = "0.8.2", features = ["rayon", "serde", "serde-1", "serde_ sha2 = "0.10.8" hex = "0.4.3" postcard = { version = "1.1.1", features = ["use-std"] } -# Remove this. I no longer use this as the charts are ingame, and it just makes the code awful -charts-rs = { version = "0.3.20", features = ["resvg"] } strum = { version = "0.27.1", features = ["derive"] } # explicitly disable atomic feature, so that bitvecs do not use atomic instructions. very important for performance! bitvec = { version = "1.0.1", features = ["alloc", "serde", "std"], default-features = false } @@ -132,7 +130,7 @@ incremental = true [features] default = ["profiler", "graphics", "client", "logging"] -# Use Krastorio2 graphics. Since I have not properly added licensing information, I currently do not push them, therefore this feature is broken +# Use Krastorio2 graphics. graphics = [] # dhat-rs memory profiling (https://docs.rs/dhat/latest/dhat/) dhat-heap = [ "dhat" ] diff --git a/src/statistics/consumption.rs b/src/statistics/consumption.rs index 4a3c7e2..cff019f 100644 --- a/src/statistics/consumption.rs +++ b/src/statistics/consumption.rs @@ -4,13 +4,13 @@ use std::{ ops::{Add, AddAssign}, }; -use charts_rs::Series; use itertools::Itertools; use crate::{ NewWithDataStore, data::DataStore, item::{IdxTrait, Indexable, Item}, + statistics::Series, }; use crate::research::LabTickInfo; diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs index 3d26e8b..cf27e5c 100644 --- a/src/statistics/mod.rs +++ b/src/statistics/mod.rs @@ -1,15 +1,9 @@ use std::{array, ops::AddAssign}; -use charts_rs::{LineChart, Series}; use consumption::ConsumptionInfo; use production::ProductionInfo; -use crate::{ - NewWithDataStore, - data::DataStore, - item::{IdxTrait, Item}, - research::ResearchProgress, -}; +use crate::{NewWithDataStore, data::DataStore, item::IdxTrait, research::ResearchProgress}; #[cfg(feature = "client")] use egui_show_info_derive::ShowInfo; @@ -77,6 +71,21 @@ pub const TIMESCALE_LEGEND: [fn(f64) -> String; NUM_DIFFERENT_TIMESCALES] = [ |t| format!("{:.0}h", t * 20.0), ]; +#[derive(Debug)] +pub(crate) struct Series { + pub name: String, + pub data: Vec, +} + +impl From<(&str, Vec)> for Series { + fn from((name, data): (&str, Vec)) -> Self { + Self { + name: name.into(), + data: data, + } + } +} + #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct GenStatistics { @@ -105,25 +114,6 @@ impl GenStatistics { self.consumption.append_single_set_of_samples(samples.1); self.research.append_single_set_of_samples(samples.2 as u64); } - - pub fn get_chart( - &self, - timescale: usize, - data_store: &DataStore, - filter: Option) -> bool>, - ) -> LineChart { - let prod_values: Vec = self - .production - .get_series(timescale, data_store, filter) - .into_iter() - .map(|v| v.1) - .collect(); - - LineChart::new( - prod_values, - vec![".".to_string(); NUM_SAMPLES_AT_INTERVALS[timescale]], - ) - } } pub trait IntoSeries: Sized { diff --git a/src/statistics/power.rs b/src/statistics/power.rs index 154869b..e8b7037 100644 --- a/src/statistics/power.rs +++ b/src/statistics/power.rs @@ -1,10 +1,9 @@ use std::iter; -use charts_rs::Series; - use crate::{ data::DataStore, item::{IdxTrait, Item}, + statistics::Series, }; use super::IntoSeries; diff --git a/src/statistics/production.rs b/src/statistics/production.rs index 6f4fcb0..08c9689 100644 --- a/src/statistics/production.rs +++ b/src/statistics/production.rs @@ -4,13 +4,13 @@ use std::{ ops::{Add, AddAssign}, }; -use charts_rs::Series; use itertools::Itertools; use crate::{ NewWithDataStore, data::DataStore, item::{IdxTrait, Item}, + statistics::Series, }; use super::{IntoSeries, recipe::RecipeTickInfo}; diff --git a/src/statistics/research.rs b/src/statistics/research.rs index c2fe254..fa23f49 100644 --- a/src/statistics/research.rs +++ b/src/statistics/research.rs @@ -1,6 +1,6 @@ use std::iter; -use crate::item::IdxTrait; +use crate::{item::IdxTrait, statistics::Series}; use super::IntoSeries; @@ -14,7 +14,7 @@ impl IntoSeries<(), ItemIdxType, smoothing_window: usize, filter: Option bool>, _data_store: &crate::data::DataStore, - ) -> impl Iterator { + ) -> impl Iterator { iter::once(( 0, ( diff --git a/src/statistics/time_usage.rs b/src/statistics/time_usage.rs index 3ac634b..320bcd8 100644 --- a/src/statistics/time_usage.rs +++ b/src/statistics/time_usage.rs @@ -5,7 +5,7 @@ use std::{ time::Duration, }; -use crate::item::IdxTrait; +use crate::{item::IdxTrait, statistics::Series}; use super::IntoSeries; @@ -45,7 +45,7 @@ impl IntoSeries<(), ItemIdxType, smoothing_window: usize, _filter: Option bool>, _data_store: &crate::data::DataStore, - ) -> impl Iterator { + ) -> impl Iterator { BTreeMap::from_iter( values .windows(smoothing_window) From b649e1ce23a07b585e4f509d34a97d9c03522701 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Sun, 8 Feb 2026 06:52:33 +0100 Subject: [PATCH 115/152] Only pin codium to old version to fix terminal open hotkey bug --- flake.lock | 37 +++++++++++++++++++++++++++---------- flake.nix | 10 ++++++---- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/flake.lock b/flake.lock index d98e035..7d36536 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1770169865, - "narHash": "sha256-iPiy13xzDQ9GjpOez+NNIjh/qjl7i4RDf9dF2x5mF9I=", + "lastModified": 1770419512, + "narHash": "sha256-o8Vcdz6B6bkiGUYkZqFwH3Pv1JwZyXht3dMtS7RchIo=", "owner": "ipetkov", "repo": "crane", - "rev": "8254ccf3b5b5131890ee073776f2e61c6d1e55d4", + "rev": "2510f2cbc3ccd237f700bb213756a8f35c32d8d7", "type": "github" }, "original": { @@ -23,11 +23,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1770275419, - "narHash": "sha256-g2wfAevB/IFF6Y1C74TbhRERlUVFVGQsgGp/lLR4lQM=", + "lastModified": 1770447430, + "narHash": "sha256-smrRbWhvJF6BATB6pXbD8Cp04HRrVcYQkXqOhUF81nk=", "owner": "nix-community", "repo": "fenix", - "rev": "aa3fbaab2bdc73c5c1e25a124c272dde295bc957", + "rev": "e1b28f6ca0d1722edceec1f2f3501558988d1aed", "type": "github" }, "original": { @@ -37,6 +37,22 @@ } }, "nixpkgs": { + "locked": { + "lastModified": 1770197578, + "narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-codium": { "locked": { "lastModified": 1762943920, "narHash": "sha256-ITeH8GBpQTw9457ICZBddQEBjlXMmilML067q0e6vqY=", @@ -56,17 +72,18 @@ "inputs": { "crane": "crane", "fenix": "fenix", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "nixpkgs-codium": "nixpkgs-codium" } }, "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1770200365, - "narHash": "sha256-Z3V5v8tSwZ3l4COVSt0b6Av0wZwTUf7Qj0SQ2/Z5RX0=", + "lastModified": 1770290336, + "narHash": "sha256-rJ79U68ZLjCSg1Qq+63aBXi//W7blaKiYq9NnfeTboA=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "1433910d1ffaff2c7a5fb7ba701f82ea578a99e3", + "rev": "d2a00da09293267e5be2efb216698762929d7140", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index daaf9f3..df42f1e 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,7 @@ { inputs = { - nixpkgs.url = "github:nixos/nixpkgs?ref=91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60"; + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-codium.url = "github:nixos/nixpkgs?ref=91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60"; fenix = { url = "github:nix-community/fenix"; inputs.nixpkgs.follows = "nixpkgs"; @@ -9,9 +10,10 @@ }; inputs.self.lfs = true; - outputs = { self, nixpkgs, fenix, crane }: let + outputs = { self, nixpkgs, nixpkgs-codium, fenix, crane }: let inherit (nixpkgs) lib; pkgs = nixpkgs.legacyPackages."x86_64-linux"; + pkgs-codium = nixpkgs-codium.legacyPackages."x86_64-linux"; fenixLib = fenix.packages."x86_64-linux"; toolchain_sha = "sha256-dXoddWaPL6UtPscTpxMUMBDL83jFtqeDtmH/+bXBs3E="; @@ -90,8 +92,8 @@ bacon (vscode-with-extensions.override { - vscode = vscodium; - vscodeExtensions = with vscode-extensions; [ + vscode = pkgs-codium.vscodium; + vscodeExtensions = with pkgs-codium.vscode-extensions; [ rust-lang.rust-analyzer vadimcn.vscode-lldb gruntfuggly.todo-tree From 1a8a9ae0c5a874b08c8584e2716495056723bfb0 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Wed, 11 Feb 2026 04:34:50 +0100 Subject: [PATCH 116/152] cargo update --- Cargo.lock | 216 ++++++++++++++++++++++++++++++++++++++++++++++------- Cargo.toml | 1 - 2 files changed, 191 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01186b0..ad24555 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2052,6 +2052,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "gif" version = "0.14.1" @@ -2413,6 +2426,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -2628,6 +2647,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lebe" version = "0.5.3" @@ -2636,15 +2661,15 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libfuzzer-sys" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" dependencies = [ "arbitrary", "cc", @@ -3029,9 +3054,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-derive" @@ -3722,6 +3747,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.114", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -4765,12 +4800,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix 1.1.3", "windows-sys 0.61.2", @@ -4870,9 +4905,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -4887,15 +4922,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -4959,9 +4994,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1" dependencies = [ "winnow", ] @@ -5044,9 +5079,9 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-segmentation" @@ -5060,6 +5095,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unty" version = "0.0.4" @@ -5157,9 +5198,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen", ] @@ -5223,6 +5273,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-timer" version = "0.2.5" @@ -5238,6 +5310,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "wayland-backend" version = "0.3.12" @@ -6115,9 +6199,91 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.114", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -6405,9 +6571,9 @@ checksum = "a7948af682ccbc3342b6e9420e8c51c1fe5d7bf7756002b4a3c6cabfe96a7e3c" [[package]] name = "zmij" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index a94c8e1..468aa1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ name = "factory" version = "0.2.1" edition = "2024" -rust-version = "1.85" build = "build.rs" From fe7ad9e7b3739810c91f5463d9bd63f2378d0278 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 06:08:34 +0100 Subject: [PATCH 117/152] Do logging at lower levels --- src/app_state.rs | 4 ++-- src/frontend/world/tile.rs | 6 +++--- src/par_generation.rs | 28 ++++++++++++++-------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index d2f8ea3..a3dbf39 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -325,7 +325,7 @@ impl GameState GameState( } }, Err(e) => { - info!( + log::trace!( "try_instantiate_inserter failed at {:?}, with {e:?}", new_instantiate_pos ); @@ -3515,7 +3515,7 @@ impl World 150 { warn!("Having to check a lot of chunks: {}", old_chunks.len()); } @@ -3624,7 +3624,7 @@ impl World 150 { warn!("Having to check a lot of chunks: {}", num_chunks); } diff --git a/src/par_generation.rs b/src/par_generation.rs index ba6ba59..e3e5542 100644 --- a/src/par_generation.rs +++ b/src/par_generation.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use itertools::{Itertools, assert_equal}; -use log::warn; +use log::info; use crate::frontend::world::tile::BeltState; use crate::inserter::FakeUnionStorage; @@ -725,7 +725,7 @@ pub fn par_generate( data_store: &DataStore, ) -> GameState { let _timer = Timer::new("par_generate"); - warn!("par_generate"); + info!("par_generate"); let mut world = World::new_with_area(world_size.top_left, world_size.bottom_right); world.set_module_combinations_trusted(generation_info.module_combinations.clone()); @@ -803,7 +803,7 @@ pub fn par_generate( { let _timer = Timer::new("belt_placement_stage"); - warn!("belt_placement_stage"); + info!("belt_placement_stage"); for ent in belt_entities { world.add_belt_entity_trusted(ent, data_store); } @@ -813,7 +813,7 @@ pub fn par_generate( { let _timer = Timer::new("misc_stage"); - warn!("misc_stage"); + info!("misc_stage"); for base_pos in positions.iter().copied() { GameState::apply_actions( &mut sim_state, @@ -857,7 +857,7 @@ fn power_pole_stage( data_store: &DataStore, ) -> PowerGridStorage { let _timer = Timer::new("power_pole_stage"); - warn!("power_pole_stage"); + info!("power_pole_stage"); let TrustedPowerPoleStageInfo { num_grids, poles } = pole_pole_stage_info; let mut store = PowerGridStorage::new(); @@ -921,7 +921,7 @@ fn belt_stage( * base_positions.len(); let _timer = Timer::new(format!("Placed {} belts", belt_count)); - warn!("belt_stage"); + info!("belt_stage"); let mut store = BeltStore::new(data_store); let mut ret = vec![]; @@ -993,7 +993,7 @@ fn assembler_stage( let _timer = Timer::new(format!("Placed {} assemblers", count)); - warn!("assembler_stage"); + info!("assembler_stage"); let mut ret = vec![]; for (i, &base_pos) in base_positions.into_iter().enumerate() { @@ -1088,7 +1088,7 @@ fn lab_stage( let count: usize = lab_actions.len() * base_positions.len(); let _timer = Timer::new(format!("Placed {} labs", count)); - warn!("lab_stage"); + info!("lab_stage"); for (i, &base_pos) in base_positions.into_iter().enumerate() { for action in lab_actions.iter().copied() { let TrustedLabPlacement { @@ -1152,7 +1152,7 @@ fn beacon_stage( let _timer = Timer::new(format!("Placed {} beacons", count)); let _timer = Timer::new("beacon_stage"); - warn!("beacon_stage"); + info!("beacon_stage"); for (i, &base_pos) in base_positions.into_iter().enumerate() { for action in beacon_actions.iter().copied() { let TrustedBeaconPlacement { @@ -1260,7 +1260,7 @@ fn inserter_stage( let _timer = Timer::new(format!("Placed {} inserters", count)); let _timer = Timer::new("inserter_stage"); - warn!("inserter_stage"); + info!("inserter_stage"); inserter_actions.sort_by_key(|a| a.get_pos()); for action in inserter_actions { for &base_pos in base_positions { @@ -1310,7 +1310,7 @@ fn chest_stage( data_store: &DataStore, ) -> FullChestStore { let _timer = Timer::new("chest_stage"); - warn!("chest_stage"); + info!("chest_stage"); let mut store = FullChestStore { stores: (0..data_store.item_display_names.len()) .map(|id| Item { @@ -1371,7 +1371,7 @@ fn pipe_stage( data_store: &DataStore, ) -> FluidSystemStore { let _timer = Timer::new("pipe_stage"); - warn!("pipe_stage"); + info!("pipe_stage"); let mut store: FluidSystemStore = FluidSystemStore::new(data_store); for base_pos in base_positions { @@ -1457,7 +1457,7 @@ fn pipe_stage( // data_store: &DataStore, // ) { // let _timer = Timer::new("pipe_stage"); -// warn!("pipe_stage"); +// info!("pipe_stage"); // pipe_actions.sort_by_key(|a| a.get_pos()); // for base_pos in base_positions { // for mut action in pipe_actions.iter().cloned() { @@ -1496,6 +1496,6 @@ impl> Timer { impl> Drop for Timer { fn drop(&mut self) { let dur = self.start.elapsed(); - warn!("[{}]: {:?}", self.text.borrow(), dur); + info!("[{}]: {:?}", self.text.borrow(), dur); } } From bc503d8c1732c6231b9d803ae8716c322780a8dd Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 06:53:30 +0100 Subject: [PATCH 118/152] Remove the need to allocate a new BFS for every traversal --- Cargo.lock | 1 + Cargo.toml | 1 + src/belt/mod.rs | 9 ++++++- src/frontend/world/tile.rs | 32 +++++++++++++++++----- src/get_size.rs | 55 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ad24555..5baa04a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1669,6 +1669,7 @@ dependencies = [ "egui_graphs", "egui_plot", "enum-map", + "fixedbitset", "flate2", "fork", "genawaiter", diff --git a/Cargo.toml b/Cargo.toml index 468aa1b..81327d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ rand_xoshiro = "0.7.0" url = "2.5.7" args = "2.2.0" console_error_panic_hook = "0.1.7" +fixedbitset = "0.5.7" # These are all the dependencies which do not work on wasm [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/src/belt/mod.rs b/src/belt/mod.rs index e780baa..18dd222 100644 --- a/src/belt/mod.rs +++ b/src/belt/mod.rs @@ -1,5 +1,6 @@ #[cfg(feature = "client")] use egui_show_info_derive::ShowInfo; +use fixedbitset::FixedBitSet; #[cfg(feature = "client")] use get_size2::GetSize; @@ -13,7 +14,7 @@ pub mod splitter; mod sushi; use crate::belt::smart::EmptyBelt; -use crate::get_size::{Mutex, StableGraph}; +use crate::get_size::{self, Mutex, StableGraph}; use crate::item::ITEMCOUNTTYPE; use crate::par_generation::BeltKind; use petgraph::stable_graph::DefaultIx; @@ -119,6 +120,8 @@ pub struct BeltStore { pub belt_graph: StableGraph, BeltGraphConnection, Directed, DefaultIx>, + #[serde(skip)] + pub belt_graph_bfs: get_size::Bfs, pub belt_graph_lookup: HashMap, NodeIndex>, } @@ -1525,6 +1528,7 @@ impl BeltStore { any_splitter_holes: vec![], belt_graph: StableGraph::default(), + belt_graph_bfs: get_size::Bfs::default(), belt_graph_lookup: HashMap::default(), } } @@ -2049,6 +2053,9 @@ impl BeltStore { }, Err(None) => { // The belt is empty, nothing to do + + // Since we are an empty belt, nothing to propagate + return; }, Err(Some(_)) => { // This needs to be sushi diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index cf32679..e6c4f0e 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -9,6 +9,7 @@ use egui::Color32; #[cfg(feature = "client")] use egui_show_info::{EguiDisplayable, InfoExtractor, ShowInfo}; use log::error; +use petgraph::visit::VisitMap; use rayon::iter::IndexedParallelIterator; use rayon::iter::ParallelIterator; use rayon::prelude::ParallelSliceMut; @@ -461,17 +462,36 @@ fn try_instantiating_inserters_for_belt_cascade { + pub(crate) bfs: petgraph::visit::Bfs, +} + +impl Default for Bfs { + fn default() -> Self { + Self { + bfs: petgraph::visit::Bfs::default(), + } + } +} + +impl std::fmt::Debug for Bfs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Bfs").field("bfs", &"TODO").finish() + } +} + +#[cfg(feature = "client")] +impl GetSize for Bfs { + fn get_heap_size(&self) -> usize { + 0 + } +} + +#[cfg(feature = "client")] +impl> ShowInfo + for Bfs +{ + fn show_fields>( + &self, + _extractor: &mut Extractor, + _ui: &mut egui::Ui, + _path: String, + _cache: &mut C, + ) { + } +} + +impl Deref for Bfs { + type Target = petgraph::visit::Bfs; + + fn deref(&self) -> &Self::Target { + &self.bfs + } +} + +impl DerefMut for Bfs { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.bfs + } +} + impl From for BitBox { fn from(value: bitvec::prelude::BitBox) -> Self { Self { bitbox: value } From 784dfbb013b617f5c327f95544b798cc71b9fbaa Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 07:49:13 +0100 Subject: [PATCH 119/152] Only grow the bfs --- flake.nix | 1 + src/frontend/world/tile.rs | 88 +++++++++++++++++++------------------- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/flake.nix b/flake.nix index df42f1e..7a2fa2d 100644 --- a/flake.nix +++ b/flake.nix @@ -89,6 +89,7 @@ rustToolchain perf + samply bacon (vscode-with-extensions.override { diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index e6c4f0e..b16ca49 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -9,6 +9,7 @@ use egui::Color32; #[cfg(feature = "client")] use egui_show_info::{EguiDisplayable, InfoExtractor, ShowInfo}; use log::error; +use petgraph::visit::NodeIndexable; use petgraph::visit::VisitMap; use rayon::iter::IndexedParallelIterator; use rayon::iter::ParallelIterator; @@ -462,21 +463,13 @@ fn try_instantiating_inserters_for_belt_cascade World chunk.can_fit(pos, size, data_store), - // None => false, - // }, - // ) + let chunk_range_x = (pos.x.div_floor(i32::from(CHUNK_SIZE))) + ..=((pos.x + i32::from(size.0) - 1).div_floor(i32::from(CHUNK_SIZE))); + let chunk_range_y = (pos.y.div_floor(i32::from(CHUNK_SIZE))) + ..=((pos.y + i32::from(size.1) - 1).div_floor(i32::from(CHUNK_SIZE))); + + chunk_range_x + .cartesian_product(chunk_range_y) + .all( + |(chunk_x, chunk_y)| match self.get_chunk(chunk_x, chunk_y) { + Some(chunk) => chunk.can_fit( + pos, + size, + Position { + x: chunk_x * i32::from(CHUNK_SIZE), + y: chunk_y * i32::from(CHUNK_SIZE), + }, + data_store, + ), + None => false, + }, + ) // !self.any_entity_colliding_with(pos, size, data_store) - self.get_entities_colliding_with(pos, size, data_store) - .into_iter() - .next() - .is_none() + // self.get_entities_colliding_with(pos, size, data_store) + // .into_iter() + // .next() + // .is_none() } pub fn get_power_poles_which_could_connect_to_pole_at<'a, 'b>( @@ -5295,23 +5296,24 @@ impl Chunk, ) -> bool { if let Some(arr) = &self.chunk_tile_to_entity_into { - let x_in_chunk = pos.x.rem_euclid(i32::from(CHUNK_SIZE)) as usize; - let y_in_chunk = pos.y.rem_euclid(i32::from(CHUNK_SIZE)) as usize; - for x in x_in_chunk - ..min( - x_in_chunk + usize::from(size.0), - usize::from(CHUNK_SIZE) - 1, - ) - { - for y in y_in_chunk - ..min( - y_in_chunk + usize::from(size.1), - usize::from(CHUNK_SIZE) - 1, - ) - { + let x_start = max(pos.x - chunk_top_left.x, 0) as usize; + let y_start = max(pos.y - chunk_top_left.y, 0) as usize; + + let x_end = min( + (pos.x + size.0 as i32) - chunk_top_left.x, + CHUNK_SIZE as i32, + ) as usize; + let y_end = min( + (pos.y + size.1 as i32) - chunk_top_left.y, + CHUNK_SIZE as i32, + ) as usize; + + for x in x_start..x_end { + for y in y_start..y_end { match arr_val_to_state(self.entities.len(), arr[x][y]) { ChunkTileState::Index(_) => return false, ChunkTileState::Empty => {}, From 46b9c39f31fdf91e6f28a2b55249f51778797561 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 08:54:51 +0100 Subject: [PATCH 120/152] Reduce log::info spam --- src/frontend/world/tile.rs | 56 +++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index b16ca49..bb9a06c 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -3475,9 +3475,11 @@ impl World= {} {:?} to {:?}", - belt_pos_earliest, old_id, new_id + belt_pos_earliest, + old_id, + new_id ); if belt_pos_earliest != 0 { if let Some(waiting) = self.to_instantiate_by_belt.get(&old_id) { @@ -3621,7 +3623,7 @@ impl World, data_store: &DataStore, ) { - info!("Change belt_id {:?} to {:?}", old_id, new_id); + log::debug!("Change belt_id {:?} to {:?}", old_id, new_id); if let Some(waiting) = self.to_instantiate_by_belt.remove(&old_id) { self.to_instantiate_by_belt .entry(new_id) @@ -5270,12 +5272,26 @@ impl Chunk, ) -> Option<&Entity> { - self.entities.iter().find(|e| { - let e_pos = e.get_pos(); - let e_size = e.get_entity_size(data_store); + let x = usize::try_from(pos.x.rem_euclid(i32::from(CHUNK_SIZE))).unwrap(); + let y = usize::try_from(pos.y.rem_euclid(i32::from(CHUNK_SIZE))).unwrap(); - pos.contained_in(e_pos, (e_size.0.into(), e_size.1.into())) - }) + if let Some(arr) = &self.chunk_tile_to_entity_into { + match arr_val_to_state(self.entities.len(), arr[x][y]) { + ChunkTileState::Index(idx) => return Some(&self.entities[idx]), + ChunkTileState::Empty => return None, + ChunkTileState::OtherChunk => return None, + ChunkTileState::EmptyOrOtherChunk => return None, + } + } else { + None + } + + // self.entities.iter().find(|e| { + // let e_pos = e.get_pos(); + // let e_size = e.get_entity_size(data_store); + + // pos.contained_in(e_pos, (e_size.0.into(), e_size.1.into())) + // }) } pub fn get_entity_at_mut( @@ -5283,12 +5299,26 @@ impl Chunk, ) -> Option<&mut Entity> { - self.entities.iter_mut().find(|e| { - let e_pos = e.get_pos(); - let e_size = e.get_entity_size(data_store); + let x = usize::try_from(pos.x.rem_euclid(i32::from(CHUNK_SIZE))).unwrap(); + let y = usize::try_from(pos.y.rem_euclid(i32::from(CHUNK_SIZE))).unwrap(); - pos.contained_in(e_pos, (e_size.0.into(), e_size.1.into())) - }) + if let Some(arr) = &self.chunk_tile_to_entity_into { + match arr_val_to_state(self.entities.len(), arr[x][y]) { + ChunkTileState::Index(idx) => return Some(&mut self.entities[idx]), + ChunkTileState::Empty => return None, + ChunkTileState::OtherChunk => return None, + ChunkTileState::EmptyOrOtherChunk => return None, + } + } else { + None + } + + // self.entities.iter_mut().find(|e| { + // let e_pos = e.get_pos(); + // let e_size = e.get_entity_size(data_store); + + // pos.contained_in(e_pos, (e_size.0.into(), e_size.1.into())) + // }) } #[must_use] From 4beec2b83084f184f71538cc7f021b9d2eaf20e5 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 08:55:37 +0100 Subject: [PATCH 121/152] Significantly increase speed of propagating sushi by lazily computing values --- src/belt/mod.rs | 66 ++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/src/belt/mod.rs b/src/belt/mod.rs index 18dd222..ee93e76 100644 --- a/src/belt/mod.rs +++ b/src/belt/mod.rs @@ -2089,39 +2089,45 @@ impl BeltStore { todo!(); } - let (inserter_item_sources, items_on_belt): (Vec<_>, Vec<_>) = match tile_id { + if let Some(_pre_calc) = done.get(&tile_id) { + return; + } + + let mut items_on_belt_or_from_inserters: Vec<_> = match tile_id { BeltTileId::AnyBelt(index, _) => match self.any_belts[index as usize] { AnyBelt::Smart(belt_id) => { let belt = self.inner.get_smart(belt_id); - let items_all_empty = belt.items().all(|loc| loc.is_none()); - match (belt.inserters.inserters.is_empty(), items_all_empty) { - (true, true) => (vec![], vec![]), - (true, false) => (vec![], vec![belt_id.item]), - (false, true) => (vec![belt_id.item], vec![]), - (false, false) => (vec![belt_id.item], vec![belt_id.item]), + if belt.inserters.inserters.is_empty() { + let items_all_empty = belt.items().all(|loc| loc.is_none()); + + if items_all_empty { + vec![] + } else { + vec![belt_id.item] + } + } else { + vec![belt_id.item] } }, AnyBelt::Sushi(sushi_idx) => { let belt = self.inner.get_sushi(sushi_idx); - ( - belt.inserters - .inserters - .iter() - .filter_map(|(ins, item, _movetime, _hand_size)| { - let (dir, _state) = ins.state.into(); - (dir == Dir::StorageToBelt).then_some(*item) - }) - .dedup() - .collect(), - belt.items().into_iter().flatten().dedup().collect(), - ) + belt.inserters + .inserters + .iter() + .filter_map(|(ins, item, _movetime, _hand_size)| { + let (dir, _state) = ins.state.into(); + (dir == Dir::StorageToBelt).then_some(*item) + }) + .chain(belt.items().into_iter().flatten()) + .dedup() + .collect() }, - AnyBelt::Empty(_empty_idx) => (vec![], vec![]), + AnyBelt::Empty(_empty_idx) => vec![], }, }; - let incoming_belts: Vec<_> = self + let incoming_belts = self .belt_graph .edges_directed( *self.belt_graph_lookup[&tile_id], @@ -2132,10 +2138,9 @@ impl BeltStore { self.belt_graph.node_weight(edge.source()).unwrap(), *edge.weight(), ) - }) - .collect(); + }); - let incoming_belt_items: Vec<_> = incoming_belts + let incoming_belt_items = incoming_belts .into_iter() .flat_map(|(belt, connection)| match connection { BeltGraphConnection::Sideload { dest_belt_pos: _ } @@ -2156,17 +2161,10 @@ impl BeltStore { filter, } => once(filter).collect(), }) - .dedup() - .collect(); + .dedup(); + items_on_belt_or_from_inserters.extend(incoming_belt_items); - done.insert( - tile_id, - items_on_belt - .into_iter() - .chain(inserter_item_sources) - .chain(incoming_belt_items) - .collect(), - ); + done.insert(tile_id, items_on_belt_or_from_inserters); } #[profiling::function] From 13559717010f2e2a1fc9aca006fc94f117939bde Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 11:26:48 +0100 Subject: [PATCH 122/152] Switch user progress reporting to ProgressInfo --- src/app_state.rs | 37 ++++++----- src/example_worlds/mod.rs | 22 +++---- src/lib.rs | 8 ++- src/par_generation.rs | 92 +++++++++++++++++++++++++-- src/progress_info.rs | 122 ++++++++++++++++++++++++++++++++++++ src/rendering/eframe_app.rs | 44 ++++++++----- 6 files changed, 274 insertions(+), 51 deletions(-) create mode 100644 src/progress_info.rs diff --git a/src/app_state.rs b/src/app_state.rs index a3dbf39..cdd3b88 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -30,6 +30,7 @@ use crate::par_generation::BoundingBox; use crate::par_generation::ParGenerateInfo; use crate::par_generation::par_generate; use crate::power::Watt; +use crate::progress_info::ProgressInfo; #[cfg(feature = "client")] use crate::saving::loading::SaveFileList; #[cfg(feature = "client")] @@ -192,7 +193,7 @@ impl GameState, + progress: ProgressInfo, data_store: &DataStore, ) -> Self { const X_OFFS: i32 = -1_800; @@ -221,6 +222,7 @@ impl GameState GameState GameState GameState GameState, + progress: ProgressInfo, data_store: &DataStore, ) -> Self { let width = count.isqrt(); @@ -353,6 +355,7 @@ impl GameState GameState, - progress: Arc, + progress: ProgressInfo, data_store: &DataStore, ) -> Self { // TODO: Correct size @@ -429,7 +432,7 @@ impl GameState, + progress: ProgressInfo, data_store: &DataStore, ) -> Self { const CHUNK_THICKNESS: i32 = 150; @@ -474,7 +477,7 @@ impl GameState, - progress: Arc, + progress: ProgressInfo, data_store: &DataStore, ) { let s = get_const_string!("test_blueprints/solar_tile.bp"); @@ -495,15 +498,20 @@ impl GameState GameState, + progress: ProgressInfo, game_state_receiver: Receiver<(LoadedGame, Arc, Sender)>, + current_message: String, }, } diff --git a/src/example_worlds/mod.rs b/src/example_worlds/mod.rs index fcfa299..b9019ab 100644 --- a/src/example_worlds/mod.rs +++ b/src/example_worlds/mod.rs @@ -1,9 +1,7 @@ -use std::{ - iter, - ops::RangeInclusive, - sync::{LazyLock, atomic::AtomicU64}, -}; +use std::{iter, ops::RangeInclusive, sync::LazyLock}; +#[cfg(feature = "client")] +use crate::progress_info::ProgressInfo; use crate::{ app_state::GameState, data::DataStore, @@ -41,8 +39,7 @@ impl Default for WorldValueStore { pub(crate) fn list_example_worlds( values: &mut WorldValueStore, ui: &mut egui::Ui, -) -> Option, &DataStore) -> GameState + 'static> -{ +) -> Option) -> GameState + 'static> { ui.horizontal(|ui| { ui.label("World Name:"); ui.text_edit_singleline(&mut values.name_field); @@ -127,12 +124,7 @@ struct ExampleWorld { // TODO: I might want to change this to depend on the values allowed_on_wasm: fn(&[ValueValue]) -> AllowedOnWasm, - creation_fn: fn( - String, - std::sync::Arc, - &[ValueValue], - &DataStore, - ) -> GameState, + creation_fn: fn(String, ProgressInfo, &[ValueValue], &DataStore) -> GameState, } #[derive(Debug, PartialEq)] @@ -175,7 +167,7 @@ const WORLDS: LazyLock<[ExampleWorld; 5]> = LazyLock::new(|| { }, ExampleWorld { name: "Megabase", - description: "A world consisting of a 40k SPM Megabase designed by Smurphy", + description: "A world consisting of a single 40k SPM Megabase designed by Smurphy", values: vec![WorldValue { name: "Generate Solar Field", kind: ValueKind::Toggle {}, @@ -237,7 +229,7 @@ const WORLDS: LazyLock<[ExampleWorld; 5]> = LazyLock::new(|| { allowed_on_wasm: |_| { AllowedOnWasm::False(Some( - "WASM does not support enough memory to run a gigabase", + "WASM does not support enough memory to run a gigabase, consider switching to native", )) }, diff --git a/src/lib.rs b/src/lib.rs index 9f0d326..e34facd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,8 @@ use saving::{load, load_readable}; use std::path::PathBuf; use crate::item::Indexable; +#[cfg(feature = "client")] +use crate::progress_info::ProgressInfo; const TICKS_PER_SECOND_LOGIC: u64 = 60; @@ -71,10 +73,12 @@ pub mod item; pub mod lab; pub mod mining_drill; pub mod power; +pub mod progress_info; pub mod research; pub mod scenario; +#[cfg(feature = "client")] mod example_worlds; mod shopping_list_arena; @@ -322,8 +326,8 @@ enum GameCreationInfo { #[cfg(feature = "client")] fn run_integrated_server( - progress: Arc, - game_creation_fn: impl FnOnce(Arc, &DataStore) -> GameState, + progress: ProgressInfo, + game_creation_fn: impl FnOnce(ProgressInfo, &DataStore) -> GameState, // FIXME: This type is wrong listen_addr: Option<&'static str>, diff --git a/src/par_generation.rs b/src/par_generation.rs index e3e5542..2e5c3c6 100644 --- a/src/par_generation.rs +++ b/src/par_generation.rs @@ -5,6 +5,7 @@ use log::info; use crate::frontend::world::tile::BeltState; use crate::inserter::FakeUnionStorage; +use crate::progress_info::ProgressInfo; use crate::{ DataStore, GameState, Position, WeakIdxTrait, app_state::{AuxillaryData, Factory, SimulationState, StorageStorageInserterStore}, @@ -716,29 +717,47 @@ impl ParGenerateInfo( name: String, world_size: BoundingBox, generation_info: ParGenerateInfo, positions: Vec, + + progress: ProgressInfo, + data_store: &DataStore, ) -> GameState { let _timer = Timer::new("par_generate"); info!("par_generate"); + progress.push_stage(1.0 / NUM_STAGES as f64, Some("Generate Chunks".to_string())); let mut world = World::new_with_area(world_size.top_left, world_size.bottom_right); + progress.pop_stage(); world.set_module_combinations_trusted(generation_info.module_combinations.clone()); let num_grids = generation_info.power_pole_actions.num_grids; + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some(format!( + "Placing {} Power Poles", + generation_info.power_pole_actions.poles.len() * positions.len() + )), + ); let mut grid_store = power_pole_stage( generation_info.power_pole_actions, &mut world, positions.iter().copied(), data_store, ); + progress.pop_stage(); + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some("Generating Assembler and belt layouts".to_string()), + ); let (assembler_entities, (belt_store, belt_entities)) = join!( || assembler_stage( &generation_info.module_combinations, @@ -750,11 +769,21 @@ pub fn par_generate( ), || belt_stage(generation_info.belt_actions, &positions, data_store,) ); - + progress.pop_stage(); + + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some(format!( + "Placing {} Assembler Entities", + assembler_entities.len() + )), + ); for ent in assembler_entities { world.add_entity_trusted(ent, data_store); } + progress.pop_stage(); + progress.push_stage(1.0 / NUM_STAGES as f64, Some("Placing Labs".to_string())); lab_stage( &mut world, &generation_info.module_combinations, @@ -764,15 +793,25 @@ pub fn par_generate( &positions, data_store, ); - + progress.pop_stage(); + + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some(format!( + "Placing {} Beacons", + generation_info.beacon_actions.len() * positions.len() + )), + ); beacon_stage( &mut world, &mut grid_store, generation_info.beacon_actions, num_grids, &positions, + progress.clone(), data_store, ); + progress.pop_stage(); let chest_store = chest_stage( &mut world, @@ -781,6 +820,19 @@ pub fn par_generate( data_store, ); + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some(format!( + "Placing {} Pipes", + generation_info + .pipe_actions + .fluid_networks + .iter() + .map(|network| network.tanks.len()) + .sum::() + * positions.len() + )), + ); let storage_storage_store = StorageStorageInserterStore::new(data_store); let fluid_store = pipe_stage( &mut world, @@ -788,6 +840,7 @@ pub fn par_generate( positions.iter().copied(), data_store, ); + progress.pop_stage(); let mut sim_state = SimulationState { factory: Factory { @@ -801,6 +854,10 @@ pub fn par_generate( ..SimulationState::new(data_store) }; + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some("Placing Belt Entities".to_string()), + ); { let _timer = Timer::new("belt_placement_stage"); info!("belt_placement_stage"); @@ -808,12 +865,18 @@ pub fn par_generate( world.add_belt_entity_trusted(ent, data_store); } } + progress.pop_stage(); // splitter_stage(); + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some("Placing Splitters".to_string()), + ); { let _timer = Timer::new("misc_stage"); info!("misc_stage"); + let num_positions = positions.len(); for base_pos in positions.iter().copied() { GameState::apply_actions( &mut sim_state, @@ -824,16 +887,27 @@ pub fn par_generate( .map(|action| ReusableBlueprint::set_base_pos(action, base_pos)), data_store, ); + progress.add_progress(1.0 / num_positions as f64); } } - + progress.pop_stage(); + + progress.push_stage( + 1.0 / NUM_STAGES as f64, + Some(format!( + "Placing {} Inserters", + generation_info.inserter_actions.len() * positions.len() + )), + ); inserter_stage( &mut world, &mut sim_state, generation_info.inserter_actions, &positions, + progress.clone(), data_store, ); + progress.pop_stage(); GameState { world: Mutex::new(world), @@ -1146,6 +1220,7 @@ fn beacon_stage( beacon_actions: Vec, num_grids: usize, base_positions: &[Position], + progress: ProgressInfo, data_store: &DataStore, ) { let count: usize = beacon_actions.len() * base_positions.len(); @@ -1153,7 +1228,9 @@ fn beacon_stage( let _timer = Timer::new(format!("Placed {} beacons", count)); let _timer = Timer::new("beacon_stage"); info!("beacon_stage"); + let num_positions = base_positions.len(); for (i, &base_pos) in base_positions.into_iter().enumerate() { + progress.add_progress(1.0 / num_positions as f64); for action in beacon_actions.iter().copied() { let TrustedBeaconPlacement { ty, @@ -1254,6 +1331,9 @@ fn inserter_stage( sim_state: &mut SimulationState, mut inserter_actions: Vec>, base_positions: &[Position], + + progress: ProgressInfo, + data_store: &DataStore, ) { let count: usize = inserter_actions.len() * base_positions.len(); @@ -1262,7 +1342,11 @@ fn inserter_stage( let _timer = Timer::new("inserter_stage"); info!("inserter_stage"); inserter_actions.sort_by_key(|a| a.get_pos()); - for action in inserter_actions { + let action_count = inserter_actions.len(); + for (i, action) in inserter_actions.into_iter().enumerate() { + if i % 1000 == 999 { + progress.add_progress(1.0 / (action_count / 1000) as f64); + } for &base_pos in base_positions { match action { ActionType::PlaceEntity(PlaceEntityInfo { diff --git a/src/progress_info.rs b/src/progress_info.rs new file mode 100644 index 0000000..82f4027 --- /dev/null +++ b/src/progress_info.rs @@ -0,0 +1,122 @@ +use std::sync::{ + Arc, + atomic::{AtomicBool, AtomicU64, Ordering}, +}; + +use parking_lot::Mutex; + +#[derive(Debug)] +pub struct ProgressInfo { + inner: Arc, +} + +#[derive(Debug)] +struct Inner { + // NOTE(Tim): This u64 is interpreted as a f64 + progress: AtomicU64, + stages: Mutex>, + message_changed: AtomicBool, +} + +#[derive(Debug)] +struct ProgressStage { + message: Option, + begin: f64, + multiplier: f64, +} + +impl Clone for ProgressInfo { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl ProgressInfo { + pub fn new() -> Self { + let zero: f64 = 0.0; + let zero: AtomicU64 = AtomicU64::new(zero.to_bits()); + + Self { + inner: Arc::new(Inner { + progress: zero, + stages: Mutex::new(vec![ProgressStage { + message: None, + begin: 0.0, + multiplier: 1.0, + }]), + message_changed: AtomicBool::new(false), + }), + } + } + + pub fn get_progress(&self) -> f64 { + f64::from_bits(self.inner.progress.load(Ordering::Relaxed)) + } + + pub fn get_message(&self) -> Option { + if self.inner.message_changed.load(Ordering::SeqCst) { + self.inner + .stages + .lock() + .iter() + .rev() + .find_map(|stage| stage.message.as_ref()) + .cloned() + } else { + None + } + } + + pub fn set_progress(&self, progress: f64) { + let binding = self.inner.stages.lock(); + let stage = binding.last().unwrap(); + + self.inner.progress.store( + (stage.begin + progress * stage.multiplier).to_bits(), + Ordering::Relaxed, + ); + } + + // FIXME: This is technically a race condition here. But it is prob fine + pub fn add_progress(&self, amount: f64) { + let binding = self.inner.stages.lock(); + let stage = binding.last().unwrap(); + + let current = self.get_progress(); + + self.inner.progress.store( + (current + amount * stage.multiplier).to_bits(), + Ordering::Relaxed, + ); + } + + // FIXME: This is technically a race condition here. But it is prob fine + pub fn push_stage(&self, perc_of_current_stage: f64, message: Option) { + let mut stages = self.inner.stages.lock(); + let multiplier = + stages.iter().map(|stage| stage.multiplier).product::() * perc_of_current_stage; + + let begin = self.get_progress(); + if message.is_some() { + self.inner.message_changed.store(true, Ordering::SeqCst); + } + stages.push(ProgressStage { + message, + begin, + multiplier, + }); + } + + // FIXME: This is technically a race condition here. But it is prob fine + pub fn pop_stage(&self) { + // When a stage is popped it must be done + self.set_progress(1.0); + let mut stages = self.inner.stages.lock(); + let stage = stages.pop().expect("Popped more stages than were pushed"); + if stage.message.is_some() { + self.inner.message_changed.store(true, Ordering::SeqCst); + } + } +} diff --git a/src/rendering/eframe_app.rs b/src/rendering/eframe_app.rs index 102371d..a01d168 100644 --- a/src/rendering/eframe_app.rs +++ b/src/rendering/eframe_app.rs @@ -3,7 +3,6 @@ use std::{ net::ToSocketAddrs, sync::{ Arc, - atomic::{AtomicU64, Ordering}, mpsc::{Sender, channel}, }, thread, @@ -19,18 +18,18 @@ use wasm_timer::Instant; use parking_lot::Mutex; use crate::{ - GameCreationInfo, example_worlds, run_client, + example_worlds, + progress_info::ProgressInfo, + run_client, saving::{load, loading::SaveFileList, save_folder}, }; -use crate::{StartGameInfo, frontend::world::Position}; use crate::{rendering::render_world::EscapeMenuOptions, run_integrated_server}; use eframe::{ egui::{CentralPanel, Event, PaintCallbackInfo, Shape}, egui_wgpu::{self, CallbackTrait}, }; use egui::{ - Align2, Button, Color32, CursorIcon, Grid, Modal, ProgressBar, RichText, Slider, TextBuffer, - TextEdit, Window, + Align2, Button, Color32, CursorIcon, Grid, Modal, ProgressBar, RichText, TextEdit, Window, }; use log::{error, warn}; use tilelib::types::RawRenderer; @@ -228,7 +227,7 @@ impl eframe::App for App { if ui.add_enabled(true, Button::new("Load")).on_disabled_hover_text("Currently WASM does not support saving or loading").clicked() { let path = file.path.clone(); let progress = - Arc::new(AtomicU64::new(0f64.to_bits())); + ProgressInfo::new(); let (send, recv) = channel(); let progress_send = progress.clone(); @@ -253,6 +252,7 @@ impl eframe::App for App { start_time: Instant::now(), progress, game_state_receiver: recv, + current_message: "Loading savegame".to_string(), }); } }); @@ -306,8 +306,12 @@ impl eframe::App for App { start_time, progress, game_state_receiver, + current_message, } => { - let progress = f64::from_bits(progress.load(Ordering::Relaxed)); + if let Some(new_message) = progress.get_message() { + *current_message = new_message; + } + let progress = progress.get_progress(); CentralPanel::default().show(ctx, |ui| { #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] @@ -328,7 +332,7 @@ impl eframe::App for App { .default_pos((0.5, 0.5)) .show(ctx, |ui| { let mul: f64 = 1.0 / progress; - let text = if mul.is_infinite() { + let time_text = if mul.is_infinite() { format!("Calculating Remaining Time...") } else { if mul >= 1.0 { @@ -341,15 +345,21 @@ impl eframe::App for App { .div_ceil(60) ) } else { - error!("mul out of range 1.0..: {}", mul); - format!("Calculating Remaining Time...") + if mul < 0.99 { + error!("mul out of range 1.0..: {}", mul); + format!("Calculating Remaining Time...") + } else { + // Lets assume this is just floating point rounding crap + format!("Est Remaining 1 min") + } } }; ui.add( ProgressBar::new(progress as f32) .corner_radius(0) - .text(text), + .text(current_message.as_str()), ); + ui.label(time_text); if mul.is_finite() { ui.label(format!( "Est Full Time: {:?} min", @@ -391,8 +401,10 @@ impl eframe::App for App { if crate::built_info::GIT_HEAD_REF == Some("refs/head/master") { ui.label(crate::built_info::PKG_VERSION); } else { - let version = crate::built_info::GIT_VERSION - .unwrap_or(crate::built_info::GIT_COMMIT_HASH_SHORT.unwrap_or("Could not get git version")); + let version = crate::built_info::GIT_VERSION.unwrap_or( + crate::built_info::GIT_COMMIT_HASH_SHORT + .unwrap_or("Could not get git version"), + ); ui.label(version); } ui.end_row(); @@ -450,7 +462,7 @@ impl eframe::App for App { }) .inner { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); + let progress = ProgressInfo::new(); let (send, recv) = channel(); run_client(ip, send); @@ -459,6 +471,7 @@ impl eframe::App for App { start_time: Instant::now(), progress, game_state_receiver: recv, + current_message: "Fetching gamestate from server".to_string(), }; return; @@ -573,7 +586,7 @@ impl eframe::App for App { example_worlds::list_example_worlds(&mut self.world_creation_state, ui); if let Some(creation_fn) = ret { - let progress = Arc::new(AtomicU64::new(0f64.to_bits())); + let progress = ProgressInfo::new(); let (send, recv) = channel(); let progress_send = progress.clone(); @@ -594,6 +607,7 @@ impl eframe::App for App { start_time: Instant::now(), progress, game_state_receiver: recv, + current_message: "Creating world".to_string() }; } }); From 981e9c64e392568b033a6ee4ef99a093e3d2c96a Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 11:27:05 +0100 Subject: [PATCH 123/152] Remove unneeded power_grid_lookup --- src/frontend/world/tile.rs | 171 +------------------------------------ 1 file changed, 3 insertions(+), 168 deletions(-) diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index bb9a06c..c9fce3e 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -180,7 +180,6 @@ pub struct World { belt_lookup: BeltIdLookup, belt_recieving_input_directions: HashMap>, - power_grid_lookup: PowerGridConnectedDevicesLookup, pub ore_lookup: OreLookup, @@ -257,12 +256,6 @@ enum WorldUpdate { NewEntity { pos: Position }, } -#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -struct PowerGridConnectedDevicesLookup { - grid_to_chunks: BTreeMap>, -} - #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] struct BeltIdLookup { @@ -1538,7 +1531,6 @@ impl World World World World World World World World World { - if let Some((pole_pos, _, _)) = pole_position { - let grid = sim_state.factory.power_grids.pole_pos_to_grid_id[&pole_pos]; - self.power_grid_lookup - .grid_to_chunks - .entry(grid) - .or_default() - .insert(chunk_pos); - } - cascading_updates.push(new_lab_cascade(pos, data_store)); }, - Entity::SolarPanel { pole_position, .. } => { - if let Some((pole_pos, _)) = pole_position { - let grid = sim_state.factory.power_grids.pole_pos_to_grid_id[&pole_pos]; - self.power_grid_lookup - .grid_to_chunks - .entry(grid) - .or_default() - .insert(chunk_pos); - } - }, - Entity::Accumulator { pole_position, .. } => { - if let Some((pole_pos, _)) = pole_position { - let grid = sim_state.factory.power_grids.pole_pos_to_grid_id[&pole_pos]; - self.power_grid_lookup - .grid_to_chunks - .entry(grid) - .or_default() - .insert(chunk_pos); - } - }, + Entity::SolarPanel { pole_position, .. } => {}, + Entity::Accumulator { pole_position, .. } => {}, Entity::Assembler { info, .. } => match info { AssemblerInfo::UnpoweredNoRecipe | AssemblerInfo::Unpowered(_) => {}, - AssemblerInfo::PoweredNoRecipe(pole_position) => { - let grid = sim_state.factory.power_grids.pole_pos_to_grid_id[&pole_position]; - self.power_grid_lookup - .grid_to_chunks - .entry(grid) - .or_default() - .insert(chunk_pos); - }, + AssemblerInfo::PoweredNoRecipe(pole_position) => {}, AssemblerInfo::Powered { id: AssemblerID { grid, .. }, pole_position, .. } => { - let lookup_grid = - sim_state.factory.power_grids.pole_pos_to_grid_id[&pole_position]; - assert_eq!(grid, lookup_grid); - self.power_grid_lookup - .grid_to_chunks - .entry(grid) - .or_default() - .insert(chunk_pos); - cascading_updates.push(newly_working_assembler(pos, data_store)); }, }, Entity::PowerPole { pos: pole_pos, .. } => { - let grid = sim_state.factory.power_grids.pole_pos_to_grid_id[&pole_pos]; - self.power_grid_lookup - .grid_to_chunks - .entry(grid) - .or_default() - .insert(chunk_pos); - // Handle Entities that are newly powered cascading_updates.push(new_power_pole(pole_pos, data_store)); }, @@ -3366,104 +3299,6 @@ impl World, - old_id: PowerGridIdentifier, - new_id: PowerGridIdentifier, - ) { - let old_chunks = self.power_grid_lookup.grid_to_chunks.remove(&old_id); - - for chunk_pos in old_chunks.iter().flatten() { - let chunk = self - .chunks - .get_mut(chunk_pos.0, chunk_pos.1) - .expect("Ungenerated chunk in belt map!"); - - for entity in &mut chunk.entities { - match entity { - Entity::SolarPanel { - pole_position: None, - .. - } => {}, - Entity::SolarPanel { - pole_position: Some(_), - .. - } => todo!(), - Entity::Accumulator { - pole_position: None, - .. - } => {}, - Entity::Accumulator { - pole_position: Some(_), - .. - } => todo!(), - Entity::Lab { - pole_position: None, - .. - } => {}, - Entity::Lab { - pole_position: Some(_), - .. - } => todo!(), - Entity::Assembler { info, .. } => match info { - AssemblerInfo::UnpoweredNoRecipe | AssemblerInfo::Unpowered(_) => {}, - AssemblerInfo::PoweredNoRecipe(_) => {}, - AssemblerInfo::Powered { - id: - AssemblerID { - grid: grid_in_id, .. - }, - pole_position, - .. - } => { - let grid = - sim_state.factory.power_grids.pole_pos_to_grid_id[pole_position]; - assert_eq!(grid, new_id); - if *grid_in_id == old_id { - *grid_in_id = new_id; - } - }, - }, - Entity::PowerPole { .. } => {}, - Entity::Inserter { info, .. } => match info { - InserterInfo::NotAttached { .. } => {}, - InserterInfo::Attached { - info: attached_inserter, - .. - } => match attached_inserter { - AttachedInserter::BeltStorage { .. } => { - // TODO - }, - AttachedInserter::BeltBelt { .. } => { - // TODO - }, - AttachedInserter::StorageStorage { .. } => todo!(), - }, - }, - Entity::Roboport { power_grid, .. } => { - if *power_grid == Some(old_id) { - *power_grid = Some(new_id); - } - }, - Entity::MiningDrill { .. } => todo!(), - Entity::Belt { .. } - | Entity::Underground { .. } - | Entity::Splitter { .. } - | Entity::Chest { .. } - | Entity::Beacon { .. } - | Entity::FluidTank { .. } => {}, - } - } - } - - self.power_grid_lookup - .grid_to_chunks - .entry(new_id) - .or_default() - .extend(old_chunks.into_iter().flatten()); - } - pub fn update_belt_id_after( &mut self, sim_state: &mut SimulationState, From 26fe68b95813eb9317918e1c09ceceb9534fa840 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 13:16:08 +0100 Subject: [PATCH 124/152] Render the effects of selected researches --- src/research.rs | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/research.rs b/src/research.rs index 7033088..6a4fd9c 100644 --- a/src/research.rs +++ b/src/research.rs @@ -525,10 +525,16 @@ impl TechState { } if !render_graph.selected_nodes().is_empty() { + use crate::data::TechnologyEffect; + let [selected_node] = render_graph.selected_nodes() else { unreachable!("We only allow selecting a single node!"); }; + let selected_tech = Technology { + id: selected_node.index().try_into().unwrap(), + }; + ui.label( &data_store .technology_tree @@ -549,9 +555,7 @@ impl TechState { false } else { self.finished_technologies - .get(&Technology { - id: selected_node.index().try_into().unwrap(), - }) + .get(&selected_tech) .copied() .unwrap_or(0) > 0 @@ -569,9 +573,7 @@ impl TechState { .unwrap_or(0) > 0 }); - let is_currently_researching = self.research_queue.contains(&Technology { - id: selected_node.index().try_into().unwrap(), - }); + let is_currently_researching = self.research_queue.contains(&selected_tech); if ui .add_enabled( @@ -581,21 +583,32 @@ impl TechState { .clicked() { ret.push(ActionType::AddResearchToQueue { - tech: Technology { - id: selected_node.index().try_into().unwrap(), - }, + tech: selected_tech, }); } if is_done { if ui.button("[CHEAT] Undo Technology").clicked() { ret.push(ActionType::CheatRelockTechnology { - tech: Technology { - id: selected_node.index().try_into().unwrap(), - }, + tech: selected_tech, }); } } + + // Render Tech Effect + let TechnologyEffect { unlocked_recipes } = &data_store + .technology_tree + .node_weight(NodeIndex::from(selected_tech.id)) + .unwrap() + .effect; + + // Unlocked Recipes + if !unlocked_recipes.is_empty() { + ui.label("Unlocks Recipes:"); + for recipe in unlocked_recipes { + ui.label(&data_store.recipe_display_names[recipe.into_usize()]); + } + } } }, ); From e978fb92f0a630c8eb2ff6fbb1a0b87d9d0fc5c6 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 13:33:28 +0100 Subject: [PATCH 125/152] Adjust map view settings to have lower VRAM usage and better quality --- src/rendering/map_view.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rendering/map_view.rs b/src/rendering/map_view.rs index ebe0262..7fc546d 100644 --- a/src/rendering/map_view.rs +++ b/src/rendering/map_view.rs @@ -25,9 +25,9 @@ pub struct MapViewUpdate { // FIXME: It seems we are rendering one map tile to much in the positive directions -const NUM_MAP_TILE_SIZES: usize = 4; +const NUM_MAP_TILE_SIZES: usize = 5; // TODO: Figure out a good tilesize. 1024 seems to work fine, but is larger or smaller better? -const TILE_SIZE_PIXELS: [u32; NUM_MAP_TILE_SIZES] = [1024, 1024, 1024, 4000]; +const TILE_SIZE_PIXELS: [u32; NUM_MAP_TILE_SIZES] = [1024, 1024, 1024, 1024, 4000]; // TODO: Since array::map is not const, we hack it like this const NUM_TILES_PER_AXIS: [u32; NUM_MAP_TILE_SIZES] = { let mut b = [0; NUM_MAP_TILE_SIZES]; @@ -38,8 +38,8 @@ const NUM_TILES_PER_AXIS: [u32; NUM_MAP_TILE_SIZES] = { } b }; -const TILE_PIXEL_TO_WORLD_TILE: [u32; NUM_MAP_TILE_SIZES] = [1, 4, 16, 64]; -pub const MIN_WIDTH: [u32; NUM_MAP_TILE_SIZES] = [0, 5_000, 10_000, 50_000]; +const TILE_PIXEL_TO_WORLD_TILE: [u32; NUM_MAP_TILE_SIZES] = [1, 4, 16, 64, 256]; +pub const MIN_WIDTH: [u32; NUM_MAP_TILE_SIZES] = [0, 10_000, 40_000, 100_000, 300_000]; #[profiling::function] pub fn create_map_textures_if_needed( From 083ad1447480cb5d77d1c2d95edb0b765edf7115 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 13:46:55 +0100 Subject: [PATCH 126/152] Fix train ride example --- src/app_state.rs | 4 +++- .../world/sparse_grid/bounding_box_grid.rs | 22 +++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index cdd3b88..7fa6fe2 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -435,7 +435,9 @@ impl GameState, ) -> Self { - const CHUNK_THICKNESS: i32 = 150; + // This is the maximum thickness the player can generate while at the edge of the world + // Factorio uses 20 chunks, but their chunks are twice as large in each dimension + const CHUNK_THICKNESS: i32 = 40; // TODO: Correct size let ret = GameState::new_with_world_area( diff --git a/src/frontend/world/sparse_grid/bounding_box_grid.rs b/src/frontend/world/sparse_grid/bounding_box_grid.rs index 90a15b5..67dcbc6 100644 --- a/src/frontend/world/sparse_grid/bounding_box_grid.rs +++ b/src/frontend/world/sparse_grid/bounding_box_grid.rs @@ -299,6 +299,8 @@ impl> BoundingBoxGrid { fn reorder_for_new_extent(&mut self, new_point: [I; 2]) { let [x, y] = new_point; + let old_extent = self.extent.unwrap_or([[x, x], [y, y]]); + let extent = self.extent.get_or_insert([[x, x], [y, y]]); let [x_range, y_range] = extent; @@ -315,8 +317,8 @@ impl> BoundingBoxGrid { self.values.resize_with(new_size, || None); - for old_val in values { - let pos = old_val.get_grid_index(); + for (idx, old_val) in values.into_iter().enumerate() { + let pos = Self::calculate_pos(&old_extent, idx); let index = Self::calculate_index(extent, pos.into()); self.values[index] = Some(old_val); } @@ -345,7 +347,19 @@ impl> BoundingBoxGrid { pub(super) fn into_iter(self) -> impl Iterator { self.values .into_iter() - .flatten() - .map(|chunk| (chunk.get_grid_index(), chunk)) + .enumerate() + .filter_map(|(idx, v)| v.map(|v| (idx, v))) + .map(move |(idx, chunk)| { + ( + Self::calculate_pos( + &self + .extent + .expect("Since we have any chunks, we must have an extent"), + idx, + ) + .into(), + chunk, + ) + }) } } From ee3293e5dd583f33681828e7250cbad3b72feba6 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 13:47:04 +0100 Subject: [PATCH 127/152] Fix some data --- src/data/factorio_1_1.fgmod | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/data/factorio_1_1.fgmod b/src/data/factorio_1_1.fgmod index 5007dbd..b35ba53 100644 --- a/src/data/factorio_1_1.fgmod +++ b/src/data/factorio_1_1.fgmod @@ -628,7 +628,7 @@ ), ( name: "factory_game::light_oil_cracking", - display_name: "Heavy Oil Cracking", + display_name: "Light Oil Cracking", possible_machines: [ "factory_game::chemical_plant", ], @@ -2268,9 +2268,6 @@ RecipeUnlock("factory_game::raw_oil_generation"), RecipeUnlock("factory_game::water_generation"), RecipeUnlock("factory_game::stone_generation"), - - // TODO: - RecipeUnlock("factory_game::solid_fuel_from_light_oil"), ], ), ( @@ -2949,6 +2946,7 @@ RecipeUnlock("factory_game::advanced_oil_processing"), RecipeUnlock("factory_game::heavy_oil_cracking"), RecipeUnlock("factory_game::light_oil_cracking"), + RecipeUnlock("factory_game::solid_fuel_from_light_oil"), ], ), ( From 000e87a960209323d32fd23434e007a012c763ae Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 14:13:35 +0100 Subject: [PATCH 128/152] Add get_entities_in_chunks_colliding_with allowing queries to skip overlap tests if the query is filtered afterward anyway --- src/frontend/world/tile.rs | 30 +++++++++++++++++++++--------- src/rendering/render_world.rs | 10 +++++----- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index c9fce3e..da5fc2f 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -928,7 +928,7 @@ fn new_lab_cascade( 2 * data_store.max_beacon_range.1 as u16 + size.1, ); - for entity in world.get_entities_colliding_with( + for entity in world.get_entities_in_chunks_colliding_with( beacon_search_start_pos, beacon_search_size, data_store, @@ -1241,7 +1241,7 @@ fn newly_working_assembler( 2 * data_store.max_beacon_range.1 as u16 + size.1, ); - for entity in world.get_entities_colliding_with( + for entity in world.get_entities_in_chunks_colliding_with( beacon_search_start_pos, beacon_search_size, data_store, @@ -3635,7 +3635,8 @@ impl World, ) -> Option { - self.get_entities_colliding_with( + // We can use *_in_chunks_* here, since we do a overlaps test later anyway. This way we do not have to check twice + self.get_entities_in_chunks_colliding_with( Position { x: entity_pos.x - i32::from(data_store.max_power_search_range), y: entity_pos.y - i32::from(data_store.max_power_search_range), @@ -4026,6 +4027,23 @@ impl World, ) -> impl IntoIterator, IntoIter: Clone> + + use<'a, 'b, ItemIdxType, RecipeIdxType> { + self.get_entities_in_chunks_colliding_with(pos, size, data_store) + .into_iter() + .filter(move |e| { + let e_pos = e.get_pos(); + let e_size = e.get_entity_size(data_store); + + pos.overlap(size, e_pos, (e_size.0.into(), e_size.1.into())) + }) + } + + pub fn get_entities_in_chunks_colliding_with<'a, 'b>( + &'a self, + pos: Position, + size: (u16, u16), + data_store: &'b DataStore, + ) -> impl IntoIterator, IntoIter: Clone> + use<'a, 'b, ItemIdxType, RecipeIdxType> { let max_size = data_store.max_entity_size; @@ -4045,12 +4063,6 @@ impl World { - let inserters = game_state_ref.world.get_entities_colliding_with(Position { + let inserters = game_state_ref.world.get_entities_in_chunks_colliding_with(Position { x: chest_pos.x - i32::from(data_store_ref.max_inserter_search_range), y: chest_pos.y - i32::from(data_store_ref.max_inserter_search_range), }, [ @@ -2801,7 +2801,7 @@ pub fn render_ui< .filter(|(dir, _, _)| *dir == data::ItemRecipeDir::Out).map(|(_, item ,amount_in_recipe)| (*item, *amount_in_recipe, 0.0)) .collect_vec(); - let inserters = game_state_ref.world.get_entities_colliding_with(Position { + let inserters = game_state_ref.world.get_entities_in_chunks_colliding_with(Position { x: assembler_pos.x - i32::from(data_store_ref.max_inserter_search_range), y: assembler_pos.y - i32::from(data_store_ref.max_inserter_search_range), }, [ From 33f6405c3ae441e1a5506506543270dffb127d1b Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 14:31:51 +0100 Subject: [PATCH 129/152] Remove the need for an unecessary allocation --- src/app_state.rs | 12 ++++++------ src/power/mod.rs | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 7fa6fe2..396cf34 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1735,7 +1735,7 @@ impl GameState = game_state + let connection_candidates = game_state .world .get_power_poles_which_could_connect_to_pole_at( pole_pos, @@ -1745,14 +1745,14 @@ impl GameState { // Handle storage updates for storage_update in storage_updates { diff --git a/src/power/mod.rs b/src/power/mod.rs index 30dcc97..1e3af8d 100644 --- a/src/power/mod.rs +++ b/src/power/mod.rs @@ -341,6 +341,8 @@ impl PowerGridStorage, ) -> Option< impl IntoIterator> + // Asserting this is static means it does not capture the input lifetime of the connected_pole iter + + 'static + use, > { #[cfg(debug_assertions)] From 2bed72c5417a395c512969d9244acf1bf7330e8b Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 14:57:11 +0100 Subject: [PATCH 130/152] Correctly handle differing connection ranges for poles, and significantly improve speed of finding connecting poles --- src/frontend/world/tile.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index da5fc2f..ae25f95 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -5062,19 +5062,37 @@ impl World impl IntoIterator, IntoIter: Clone> + Clone + use<'a, 'b, ItemIdxType, RecipeIdxType> { - self.get_entities_colliding_with( + self.get_entities_in_chunks_colliding_with( Position { x: pole_pos.x - i32::from(connection_range), y: pole_pos.y - i32::from(connection_range), }, ( - 2 * connection_range as u16 + pole_size.0 as u16, - 2 * connection_range as u16 + pole_size.1 as u16, + 2 * connection_range as u16 + pole_size.0, + 2 * connection_range as u16 + pole_size.1, ), data_store, ) .into_iter() - .filter(|e| matches!(e, Entity::PowerPole { .. })) + .filter(move |e| match e { + Entity::PowerPole { ty, pos } => { + let other_range = data_store.power_pole_data[*ty as usize].connection_range; + let other_size = data_store.power_pole_data[*ty as usize].size; + + pole_pos.overlap( + pole_size, + Position { + x: pos.x - other_range as i32, + y: pos.y - other_range as i32, + }, + ( + 2 * other_range as u16 + other_size.0, + 2 * other_range as u16 + other_size.1, + ), + ) + }, + _ => false, + }) } fn get_power_pole_range( From 7c292fc5df80123c609648c32b4e52b229c183c3 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Fri, 13 Feb 2026 20:47:19 +0100 Subject: [PATCH 131/152] Fix non connecting underground crash --- src/frontend/action/belt_placement.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/frontend/action/belt_placement.rs b/src/frontend/action/belt_placement.rs index 998ad66..8c2ab49 100644 --- a/src/frontend/action/belt_placement.rs +++ b/src/frontend/action/belt_placement.rs @@ -395,11 +395,9 @@ pub fn handle_underground_removal Date: Sat, 14 Feb 2026 05:56:42 +0100 Subject: [PATCH 132/152] New replay system and fixed visual replay tests --- Cargo.lock | 187 ++------ Cargo.toml | 4 +- crash_replays/001.rep.ron | 1 + crash_replays/002.rep.ron | 1 + crash_replays/003.rep.ron | 1 + crash_replays/004.rep.ron | 1 + crash_replays/dummy.rep | 0 src/app_state.rs | 298 ++++++------ src/belt/mod.rs | 5 +- src/blueprint/mod.rs | 18 - src/example_worlds/mod.rs | 80 +++- src/frontend/action/belt_placement.rs | 2 + src/lib.rs | 95 ++-- src/main.rs | 2 +- src/multiplayer/mod.rs | 20 +- src/multiplayer/server.rs | 18 +- src/par_generation.rs | 4 +- src/power/power_grid.rs | 1 + src/rendering/eframe_app.rs | 20 +- src/rendering/render_world.rs | 10 +- src/replays/mod.rs | 319 +++++-------- src/replays/replay_action.rs | 656 ++++++++++++++++++++++++++ src/saving/mod.rs | 1 + tests/visual_replay_tests.rs | 171 +++++++ tests/visual_test.rs | 158 ------- 25 files changed, 1312 insertions(+), 761 deletions(-) create mode 100644 crash_replays/001.rep.ron create mode 100644 crash_replays/002.rep.ron create mode 100644 crash_replays/003.rep.ron create mode 100644 crash_replays/004.rep.ron delete mode 100644 crash_replays/dummy.rep create mode 100644 src/replays/replay_action.rs create mode 100644 tests/visual_replay_tests.rs delete mode 100644 tests/visual_test.rs diff --git a/Cargo.lock b/Cargo.lock index 5baa04a..7fed985 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,7 +244,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -386,7 +386,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -421,7 +421,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -500,7 +500,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -516,7 +516,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn 2.0.114", + "syn", ] [[package]] @@ -675,7 +675,7 @@ checksum = "238b90427dfad9da4a9abd60f3ec1cdee6b80454bde49ed37f1781dd8e9dc7f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -794,7 +794,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -1151,7 +1151,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -1227,7 +1227,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -1354,7 +1354,7 @@ source = "git+https://github.com/BloodStainedCrow/egui-show-info#2f1c3f454e4577f dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -1506,7 +1506,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -1527,7 +1527,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -1538,7 +1538,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -1583,7 +1583,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -1672,7 +1672,6 @@ dependencies = [ "fixedbitset", "flate2", "fork", - "genawaiter", "get-size2", "getrandom 0.3.4", "hex", @@ -1741,7 +1740,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -1806,7 +1805,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -1908,7 +1907,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -1947,36 +1946,6 @@ dependencies = [ "slab", ] -[[package]] -name = "genawaiter" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" -dependencies = [ - "genawaiter-macro", - "genawaiter-proc-macro", - "proc-macro-hack", -] - -[[package]] -name = "genawaiter-macro" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" - -[[package]] -name = "genawaiter-proc-macro" -version = "0.99.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738" -dependencies = [ - "proc-macro-error", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1995,7 +1964,7 @@ checksum = "f2b6d1e2f75c16bfbcd0f95d84f99858a6e2f885c2287d1f5c3a96e8444a34b4" dependencies = [ "attribute-derive", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -2527,7 +2496,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -2819,7 +2788,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -3067,7 +3036,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -3119,7 +3088,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -3603,7 +3572,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.114", + "syn", "unicase", ] @@ -3634,7 +3603,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -3755,7 +3724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn", ] [[package]] @@ -3767,38 +3736,6 @@ dependencies = [ "toml_edit", ] -[[package]] -name = "proc-macro-error" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "syn-mid", - "version_check", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro-utils" version = "0.10.0" @@ -3836,7 +3773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -3961,7 +3898,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -4313,7 +4250,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.114", + "syn", "unicode-ident", ] @@ -4455,7 +4392,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -4490,7 +4427,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -4728,7 +4665,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.114", + "syn", ] [[package]] @@ -4740,18 +4677,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "syn", ] [[package]] @@ -4765,17 +4691,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "syn-mid" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -4784,7 +4699,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -4853,7 +4768,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -4864,7 +4779,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -5022,7 +4937,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -5261,7 +5176,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn", "wasm-bindgen-shared", ] @@ -5784,7 +5699,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -5795,7 +5710,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -5806,7 +5721,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -5817,7 +5732,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -6228,7 +6143,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.114", + "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -6244,7 +6159,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", + "syn", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -6389,7 +6304,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", "synstructure", ] @@ -6446,7 +6361,7 @@ checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", "zbus-lockstep", "zbus_xml", "zvariant", @@ -6461,7 +6376,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn", "zbus_names", "zvariant", "zvariant_utils", @@ -6507,7 +6422,7 @@ checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -6527,7 +6442,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", "synstructure", ] @@ -6561,7 +6476,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn", ] [[package]] @@ -6638,7 +6553,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn", "zvariant_utils", ] @@ -6651,6 +6566,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.114", + "syn", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 81327d3..77bb9fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ ron = "0.8.1" take_mut = "0.2.2" static_assertions = "1.1.0" itertools = "0.14.0" -genawaiter = "0.99.1" petgraph = { version = "0.8.2", features = ["rayon", "serde", "serde-1", "serde_derive"] } sha2 = "0.10.8" hex = "0.4.3" @@ -129,7 +128,7 @@ incremental = true # codegen-units = 1 [features] -default = ["profiler", "graphics", "client", "logging"] +default = ["profiler", "graphics", "client", "logging", "replay"] # Use Krastorio2 graphics. graphics = [] # dhat-rs memory profiling (https://docs.rs/dhat/latest/dhat/) @@ -139,3 +138,4 @@ client = [ "dep:eframe", "dep:egui", "dep:egui_extras", "dep:egui_plot", "dep:pu logging = ["simple_logger"] debug-stat-gathering = [] assembler-craft-tracking = [] +replay = [] \ No newline at end of file diff --git a/crash_replays/001.rep.ron b/crash_replays/001.rep.ron new file mode 100644 index 0000000..eb24d74 --- /dev/null +++ b/crash_replays/001.rep.ron @@ -0,0 +1 @@ +(program_info:(game_version:"2bed72c",git_dirty:true,mod_sha:"CAB095682E37B9E01477714A53608B1A73646A70C4E1560D8C437464F8AF7628",mod_list:[]),generation_info:(example_idx:0,example_settings:[]),actions:[(timestamp:77,action:Position(player:"0",pos:(1600.0,1599.5834))),(timestamp:78,action:Position(player:"0",pos:(1600.0,1599.1667))),(timestamp:79,action:Position(player:"0",pos:(1600.0,1598.7501))),(timestamp:80,action:Position(player:"0",pos:(1600.0,1598.3335))),(timestamp:81,action:Position(player:"0",pos:(1600.0,1597.9169))),(timestamp:82,action:Position(player:"0",pos:(1600.0,1597.5002))),(timestamp:83,action:Position(player:"0",pos:(1600.0,1597.0836))),(timestamp:84,action:Position(player:"0",pos:(1600.0,1596.667))),(timestamp:85,action:Position(player:"0",pos:(1600.0,1596.2504))),(timestamp:86,action:Position(player:"0",pos:(1600.0,1595.8337))),(timestamp:87,action:Position(player:"0",pos:(1600.0,1595.4171))),(timestamp:88,action:Position(player:"0",pos:(1600.0,1595.0005))),(timestamp:89,action:Position(player:"0",pos:(1600.0,1594.5839))),(timestamp:90,action:Position(player:"0",pos:(1600.0,1594.1672))),(timestamp:91,action:Position(player:"0",pos:(1600.4166,1593.7506))),(timestamp:92,action:Position(player:"0",pos:(1600.8333,1593.334))),(timestamp:93,action:Position(player:"0",pos:(1601.2499,1592.9174))),(timestamp:94,action:Position(player:"0",pos:(1601.6665,1592.5007))),(timestamp:95,action:Position(player:"0",pos:(1602.0831,1592.0841))),(timestamp:96,action:Position(player:"0",pos:(1602.4998,1591.6675))),(timestamp:97,action:Position(player:"0",pos:(1602.9164,1591.2509))),(timestamp:98,action:Position(player:"0",pos:(1603.333,1590.8342))),(timestamp:99,action:Position(player:"0",pos:(1603.7496,1590.4176))),(timestamp:100,action:Position(player:"0",pos:(1604.1663,1590.001))),(timestamp:101,action:Position(player:"0",pos:(1604.5829,1590.001))),(timestamp:102,action:Position(player:"0",pos:(1604.9995,1590.001))),(timestamp:109,action:Position(player:"0",pos:(1604.9995,1590.4176))),(timestamp:110,action:Position(player:"0",pos:(1604.9995,1590.8342))),(timestamp:111,action:Position(player:"0",pos:(1604.9995,1591.2509))),(timestamp:112,action:Position(player:"0",pos:(1604.5829,1591.6675))),(timestamp:113,action:Position(player:"0",pos:(1604.1663,1592.0841))),(timestamp:114,action:Position(player:"0",pos:(1603.7496,1592.5007))),(timestamp:115,action:Position(player:"0",pos:(1603.333,1592.9174))),(timestamp:116,action:Position(player:"0",pos:(1602.9164,1593.334))),(timestamp:117,action:Position(player:"0",pos:(1602.4998,1593.7506))),(timestamp:118,action:Position(player:"0",pos:(1602.0831,1594.1672))),(timestamp:119,action:Position(player:"0",pos:(1601.6665,1594.5839))),(timestamp:120,action:Position(player:"0",pos:(1601.2499,1595.0005))),(timestamp:121,action:Position(player:"0",pos:(1600.8333,1595.4171))),(timestamp:122,action:Position(player:"0",pos:(1600.4166,1595.8337))),(timestamp:123,action:Position(player:"0",pos:(1600.0,1596.2504))),(timestamp:124,action:Position(player:"0",pos:(1599.5834,1596.667))),(timestamp:125,action:Position(player:"0",pos:(1599.1667,1597.0836))),(timestamp:126,action:Position(player:"0",pos:(1598.7501,1597.5002))),(timestamp:127,action:Position(player:"0",pos:(1598.3335,1597.9169))),(timestamp:128,action:Position(player:"0",pos:(1597.9169,1598.3335))),(timestamp:129,action:Position(player:"0",pos:(1597.5002,1598.7501))),(timestamp:130,action:Position(player:"0",pos:(1597.0836,1598.7501))),(timestamp:131,action:Position(player:"0",pos:(1596.667,1598.7501))),(timestamp:132,action:Position(player:"0",pos:(1596.2504,1598.7501))),(timestamp:133,action:Position(player:"0",pos:(1595.8337,1598.7501))),(timestamp:134,action:Position(player:"0",pos:(1595.4171,1598.7501))),(timestamp:135,action:Position(player:"0",pos:(1595.0005,1598.7501))),(timestamp:164,action:Position(player:"0",pos:(1595.0005,1598.3335))),(timestamp:165,action:Position(player:"0",pos:(1595.0005,1597.9169))),(timestamp:166,action:Position(player:"0",pos:(1595.0005,1597.5002))),(timestamp:167,action:Position(player:"0",pos:(1595.0005,1597.0836))),(timestamp:168,action:Position(player:"0",pos:(1595.0005,1596.667))),(timestamp:169,action:Position(player:"0",pos:(1595.0005,1596.2504))),(timestamp:170,action:Position(player:"0",pos:(1595.0005,1595.8337))),(timestamp:171,action:Position(player:"0",pos:(1595.0005,1595.4171))),(timestamp:172,action:Position(player:"0",pos:(1595.4171,1595.0005))),(timestamp:173,action:Position(player:"0",pos:(1595.8337,1594.5839))),(timestamp:174,action:Position(player:"0",pos:(1596.2504,1594.1672))),(timestamp:175,action:Position(player:"0",pos:(1596.667,1593.7506))),(timestamp:176,action:Position(player:"0",pos:(1597.0836,1593.334))),(timestamp:177,action:Position(player:"0",pos:(1597.5002,1592.9174))),(timestamp:178,action:Position(player:"0",pos:(1597.9169,1592.5007))),(timestamp:179,action:Position(player:"0",pos:(1598.3335,1592.0841))),(timestamp:180,action:Position(player:"0",pos:(1598.7501,1591.6675))),(timestamp:181,action:Position(player:"0",pos:(1599.1667,1591.2509))),(timestamp:182,action:Position(player:"0",pos:(1599.5834,1590.8342))),(timestamp:183,action:Position(player:"0",pos:(1600.0,1590.4176))),(timestamp:184,action:Position(player:"0",pos:(1600.4166,1590.001))),(timestamp:185,action:Position(player:"0",pos:(1600.8333,1589.5844))),(timestamp:186,action:Position(player:"0",pos:(1601.2499,1589.1677))),(timestamp:187,action:Position(player:"0",pos:(1601.6665,1588.7511))),(timestamp:188,action:Position(player:"0",pos:(1602.0831,1588.3345))),(timestamp:189,action:Position(player:"0",pos:(1602.4998,1587.9178))),(timestamp:190,action:Position(player:"0",pos:(1602.9164,1587.5012))),(timestamp:191,action:Position(player:"0",pos:(1603.333,1587.0846))),(timestamp:192,action:Position(player:"0",pos:(1603.7496,1586.668))),(timestamp:193,action:Position(player:"0",pos:(1604.1663,1586.2513))),(timestamp:194,action:Position(player:"0",pos:(1604.5829,1585.8347))),(timestamp:195,action:Position(player:"0",pos:(1604.9995,1585.4181))),(timestamp:196,action:Position(player:"0",pos:(1605.4161,1585.0015))),(timestamp:197,action:Position(player:"0",pos:(1605.8328,1584.5848))),(timestamp:198,action:Position(player:"0",pos:(1606.2494,1584.1682))),(timestamp:199,action:Position(player:"0",pos:(1606.666,1583.7516))),(timestamp:200,action:Position(player:"0",pos:(1607.0826,1583.335))),(timestamp:201,action:Position(player:"0",pos:(1607.4993,1582.9183))),(timestamp:202,action:Position(player:"0",pos:(1607.9159,1582.5017))),(timestamp:203,action:Position(player:"0",pos:(1608.3325,1582.0851))),(timestamp:204,action:Position(player:"0",pos:(1608.7491,1581.6685))),(timestamp:205,action:Position(player:"0",pos:(1609.1658,1581.2518))),(timestamp:206,action:Position(player:"0",pos:(1609.1658,1580.8352))),(timestamp:207,action:Position(player:"0",pos:(1608.7491,1580.8352))),(timestamp:208,action:Position(player:"0",pos:(1608.3325,1580.8352))),(timestamp:209,action:Position(player:"0",pos:(1607.9159,1580.8352))),(timestamp:210,action:Position(player:"0",pos:(1607.4993,1580.8352))),(timestamp:211,action:Position(player:"0",pos:(1607.0826,1580.8352))),(timestamp:212,action:Position(player:"0",pos:(1606.666,1580.8352))),(timestamp:213,action:Position(player:"0",pos:(1606.2494,1580.8352))),(timestamp:214,action:PlaceEntity(force:false,info:(pos:(x:1603,y:1578),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:214,action:Position(player:"0",pos:(1605.8328,1580.8352))),(timestamp:215,action:Position(player:"0",pos:(1605.4161,1580.8352))),(timestamp:216,action:Position(player:"0",pos:(1604.9995,1580.8352))),(timestamp:217,action:Position(player:"0",pos:(1604.5829,1580.8352))),(timestamp:218,action:Position(player:"0",pos:(1604.1663,1580.8352))),(timestamp:219,action:Position(player:"0",pos:(1603.7496,1580.8352))),(timestamp:220,action:Position(player:"0",pos:(1603.333,1580.8352))),(timestamp:221,action:Position(player:"0",pos:(1602.9164,1580.8352))),(timestamp:222,action:Position(player:"0",pos:(1602.4998,1580.8352))),(timestamp:223,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1581),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:223,action:Position(player:"0",pos:(1602.0831,1580.8352))),(timestamp:224,action:Position(player:"0",pos:(1601.6665,1580.8352))),(timestamp:225,action:Position(player:"0",pos:(1601.2499,1580.8352))),(timestamp:226,action:Position(player:"0",pos:(1600.8333,1580.8352))),(timestamp:227,action:Position(player:"0",pos:(1600.4166,1580.8352))),(timestamp:228,action:Position(player:"0",pos:(1600.0,1580.8352))),(timestamp:229,action:Position(player:"0",pos:(1599.5834,1580.8352))),(timestamp:230,action:Position(player:"0",pos:(1599.1667,1580.8352))),(timestamp:231,action:Position(player:"0",pos:(1598.7501,1580.8352))),(timestamp:232,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1582),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:232,action:Position(player:"0",pos:(1598.3335,1580.8352))),(timestamp:240,action:PlaceEntity(force:false,info:(pos:(x:1602,y:1580),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:251,action:PlaceEntity(force:false,info:(pos:(x:1601,y:1577),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:396,action:SetRecipe(pos:(x:1598,y:1581),recipe:"factory_game::iron_ore_generation")),(timestamp:445,action:Position(player:"0",pos:(1598.7501,1580.8352))),(timestamp:446,action:Position(player:"0",pos:(1599.1667,1580.8352))),(timestamp:447,action:Position(player:"0",pos:(1599.5834,1580.8352))),(timestamp:448,action:Position(player:"0",pos:(1600.0,1580.8352))),(timestamp:449,action:Position(player:"0",pos:(1600.4166,1580.8352))),(timestamp:450,action:Position(player:"0",pos:(1600.8333,1580.8352))),(timestamp:451,action:Position(player:"0",pos:(1601.2499,1580.8352))),(timestamp:452,action:Position(player:"0",pos:(1601.6665,1580.8352))),(timestamp:453,action:Position(player:"0",pos:(1602.0831,1580.8352))),(timestamp:454,action:Position(player:"0",pos:(1602.4998,1580.8352))),(timestamp:455,action:Position(player:"0",pos:(1602.9164,1580.8352))),(timestamp:456,action:Position(player:"0",pos:(1603.333,1580.8352))),(timestamp:576,action:SetRecipe(pos:(x:1603,y:1578),recipe:"factory_game::iron_ore_generation")),(timestamp:625,action:PlaceEntity(force:false,info:(pos:(x:1603,y:1582),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:667,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1582),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:697,action:PlaceEntity(force:false,info:(pos:(x:1601,y:1583),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:788,action:PlaceEntity(force:false,info:(pos:(x:1603,y:1584),ty:"factory_game::infinity_battery",rotation:North,kind:SolarPanel()))),(timestamp:901,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1580),ty:"factory_game::inserter",rotation:North,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:1039,action:PlaceEntity(force:false,info:(pos:(x:1602,y:1579),ty:"factory_game::inserter",rotation:West,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:1110,action:PlaceEntity(force:false,info:(pos:(x:1601,y:1579),ty:"factory_game::fast_transport_belt",rotation:West,kind:Belt()))),(timestamp:1120,action:PlaceEntity(force:false,info:(pos:(x:1601,y:1579),ty:"factory_game::fast_transport_belt",rotation:West,kind:Belt()))),(timestamp:1130,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1579),ty:"factory_game::fast_transport_belt",rotation:West,kind:Belt()))),(timestamp:1138,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1579),ty:"factory_game::fast_transport_belt",rotation:West,kind:Belt())))],current_timestep:1139,end_timestep:None) \ No newline at end of file diff --git a/crash_replays/002.rep.ron b/crash_replays/002.rep.ron new file mode 100644 index 0000000..702e5e3 --- /dev/null +++ b/crash_replays/002.rep.ron @@ -0,0 +1 @@ +(program_info:(game_version:"2bed72c",git_dirty:true,mod_sha:"CAB095682E37B9E01477714A53608B1A73646A70C4E1560D8C437464F8AF7628",mod_list:[]),generation_info:(example_idx:0,example_settings:[]),actions:[(timestamp:36,action:Position(player:"0",pos:(1599.5834,1600.0))),(timestamp:37,action:Position(player:"0",pos:(1599.1667,1600.0))),(timestamp:38,action:Position(player:"0",pos:(1598.7501,1600.0))),(timestamp:39,action:Position(player:"0",pos:(1598.3335,1600.0))),(timestamp:40,action:Position(player:"0",pos:(1597.9169,1600.0))),(timestamp:156,action:PlaceEntity(force:false,info:(pos:(x:1595,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:164,action:PlaceEntity(force:false,info:(pos:(x:1596,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:174,action:PlaceEntity(force:false,info:(pos:(x:1596,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:183,action:PlaceEntity(force:false,info:(pos:(x:1597,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:192,action:PlaceEntity(force:false,info:(pos:(x:1597,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:200,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:210,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:219,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:228,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:240,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:326,action:Remove(pos:(x:1597,y:1597))),(timestamp:396,action:PlaceEntity(force:false,info:(pos:(x:1597,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Belt()))),(timestamp:487,action:PlaceEntity(force:false,info:(pos:(x:1595,y:1598),ty:"factory_game::inserter",rotation:North,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:533,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1598),ty:"factory_game::inserter",rotation:South,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:574,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1599),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:780,action:SetRecipe(pos:(x:1600,y:1599),recipe:"factory_game::iron_ore_generation")),(timestamp:844,action:PlaceEntity(force:false,info:(pos:(x:1594,y:1599),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:938,action:SetRecipe(pos:(x:1594,y:1599),recipe:"factory_game::iron_ore_generation")),(timestamp:1000,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1600),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:1230,action:Remove(pos:(x:1595,y:1598))),(timestamp:1407,action:PlaceEntity(force:false,info:(pos:(x:1595,y:1598),ty:"factory_game::inserter",rotation:North,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:1430,action:Position(player:"0",pos:(1598.3335,1600.0))),(timestamp:1431,action:Position(player:"0",pos:(1598.7501,1600.0))),(timestamp:1432,action:Position(player:"0",pos:(1599.1667,1600.0))),(timestamp:1433,action:Position(player:"0",pos:(1599.5834,1600.0))),(timestamp:1434,action:Position(player:"0",pos:(1600.0,1600.0))),(timestamp:1435,action:Position(player:"0",pos:(1600.4166,1600.0))),(timestamp:1436,action:Position(player:"0",pos:(1600.8333,1600.0))),(timestamp:1437,action:Position(player:"0",pos:(1601.2499,1600.0))),(timestamp:1438,action:Position(player:"0",pos:(1601.6665,1600.0))),(timestamp:1532,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1602),ty:"factory_game::infinity_battery",rotation:North,kind:SolarPanel()))),(timestamp:1572,action:Position(player:"0",pos:(1601.2499,1600.0))),(timestamp:1573,action:Position(player:"0",pos:(1600.8333,1600.0))),(timestamp:1574,action:Position(player:"0",pos:(1600.4166,1600.0))),(timestamp:1575,action:Position(player:"0",pos:(1600.0,1600.0))),(timestamp:1576,action:Position(player:"0",pos:(1599.5834,1600.0))),(timestamp:1577,action:Position(player:"0",pos:(1599.1667,1600.0))),(timestamp:1578,action:Position(player:"0",pos:(1598.7501,1600.0))),(timestamp:1579,action:Position(player:"0",pos:(1598.3335,1600.0))),(timestamp:1580,action:Position(player:"0",pos:(1597.9169,1600.0))),(timestamp:1888,action:Remove(pos:(x:1600,y:1598))),(timestamp:1968,action:Remove(pos:(x:1595,y:1598))),(timestamp:2026,action:PlaceEntity(force:false,info:(pos:(x:1595,y:1598),ty:"factory_game::inserter",rotation:North,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:2067,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1598),ty:"factory_game::inserter",rotation:South,kind:Inserter(filter:None,user_movetime:None)))),(timestamp:2209,action:Remove(pos:(x:1598,y:1602))),(timestamp:2339,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1602),ty:"factory_game::infinity_battery",rotation:North,kind:SolarPanel()))),(timestamp:2379,action:Position(player:"0",pos:(1597.9169,1600.4166))),(timestamp:2380,action:Position(player:"0",pos:(1597.9169,1600.8333))),(timestamp:2381,action:Position(player:"0",pos:(1597.9169,1601.2499))),(timestamp:2382,action:Position(player:"0",pos:(1597.9169,1601.6665))),(timestamp:2383,action:Position(player:"0",pos:(1597.9169,1602.0831))),(timestamp:2384,action:Position(player:"0",pos:(1597.9169,1602.4998))),(timestamp:2385,action:Position(player:"0",pos:(1597.9169,1602.9164))),(timestamp:2386,action:Position(player:"0",pos:(1597.9169,1603.333))),(timestamp:2387,action:Position(player:"0",pos:(1597.9169,1603.7496))),(timestamp:2388,action:Position(player:"0",pos:(1597.9169,1604.1663))),(timestamp:2389,action:Position(player:"0",pos:(1597.9169,1604.5829))),(timestamp:2390,action:Position(player:"0",pos:(1597.9169,1604.9995))),(timestamp:2391,action:Position(player:"0",pos:(1597.9169,1605.4161))),(timestamp:2392,action:Position(player:"0",pos:(1597.9169,1605.8328))),(timestamp:2393,action:Position(player:"0",pos:(1597.5002,1606.2494))),(timestamp:2394,action:Position(player:"0",pos:(1597.0836,1606.666))),(timestamp:2395,action:Position(player:"0",pos:(1597.0836,1607.0826))),(timestamp:2396,action:Position(player:"0",pos:(1597.0836,1607.4993))),(timestamp:2397,action:Position(player:"0",pos:(1597.0836,1607.9159))),(timestamp:2398,action:Position(player:"0",pos:(1597.0836,1608.3325))),(timestamp:2399,action:Position(player:"0",pos:(1597.0836,1608.7491))),(timestamp:2400,action:Position(player:"0",pos:(1597.0836,1609.1658))),(timestamp:2401,action:Position(player:"0",pos:(1597.0836,1609.5824))),(timestamp:2402,action:Position(player:"0",pos:(1597.0836,1609.999))),(timestamp:2403,action:Position(player:"0",pos:(1597.0836,1610.4156))),(timestamp:2404,action:Position(player:"0",pos:(1597.0836,1610.8323))),(timestamp:2406,action:Position(player:"0",pos:(1597.0836,1611.2489))),(timestamp:2407,action:Position(player:"0",pos:(1597.0836,1611.6655))),(timestamp:2423,action:PlaceEntity(force:false,info:(pos:(x:1591,y:1617),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2434,action:PlaceEntity(force:false,info:(pos:(x:1592,y:1621),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2444,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1622),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2453,action:PlaceEntity(force:false,info:(pos:(x:1602,y:1621),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2462,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1619),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2471,action:PlaceEntity(force:false,info:(pos:(x:1595,y:1618),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2481,action:PlaceEntity(force:false,info:(pos:(x:1593,y:1619),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2490,action:PlaceEntity(force:false,info:(pos:(x:1596,y:1621),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2499,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1620),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2506,action:Position(player:"0",pos:(1597.0836,1611.2489))),(timestamp:2507,action:Position(player:"0",pos:(1597.0836,1610.8323))),(timestamp:2508,action:Position(player:"0",pos:(1597.0836,1610.4156))),(timestamp:2509,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1617),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2509,action:Position(player:"0",pos:(1597.0836,1609.999))),(timestamp:2510,action:Position(player:"0",pos:(1597.0836,1609.5824))),(timestamp:2511,action:Position(player:"0",pos:(1597.0836,1609.1658))),(timestamp:2512,action:Position(player:"0",pos:(1597.0836,1608.7491))),(timestamp:2513,action:Position(player:"0",pos:(1597.0836,1608.3325))),(timestamp:2519,action:PlaceEntity(force:false,info:(pos:(x:1597,y:1616),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2530,action:PlaceEntity(force:false,info:(pos:(x:1596,y:1616),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2539,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1614),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2549,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1611),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2559,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1610),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:2577,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1606),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole())))],current_timestep:2578,end_timestep:None) \ No newline at end of file diff --git a/crash_replays/003.rep.ron b/crash_replays/003.rep.ron new file mode 100644 index 0000000..cc3c476 --- /dev/null +++ b/crash_replays/003.rep.ron @@ -0,0 +1 @@ +(program_info:(game_version:"2bed72c",git_dirty:true,mod_sha:"CAB095682E37B9E01477714A53608B1A73646A70C4E1560D8C437464F8AF7628",mod_list:[]),generation_info:(example_idx:0,example_settings:[]),actions:[(timestamp:139,action:PlaceEntity(force:false,info:(pos:(x:1597,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:152,action:Position(player:"0",pos:(1600.4166,1600.0))),(timestamp:153,action:Position(player:"0",pos:(1600.8333,1600.0))),(timestamp:154,action:Position(player:"0",pos:(1601.2499,1600.0))),(timestamp:155,action:Position(player:"0",pos:(1601.6665,1600.0))),(timestamp:156,action:Position(player:"0",pos:(1602.0831,1600.0))),(timestamp:157,action:Position(player:"0",pos:(1602.4998,1600.0))),(timestamp:158,action:Position(player:"0",pos:(1602.9164,1600.0))),(timestamp:159,action:Position(player:"0",pos:(1603.333,1600.0))),(timestamp:183,action:Position(player:"0",pos:(1603.7496,1600.0))),(timestamp:184,action:Position(player:"0",pos:(1604.1663,1600.0))),(timestamp:185,action:Position(player:"0",pos:(1604.5829,1600.0))),(timestamp:232,action:PlaceEntity(force:false,info:(pos:(x:1602,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:279,action:PlaceEntity(force:false,info:(pos:(x:1607,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:337,action:PlaceEntity(force:false,info:(pos:(x:1612,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:367,action:Position(player:"0",pos:(1604.1663,1600.0))),(timestamp:368,action:Position(player:"0",pos:(1603.7496,1600.0))),(timestamp:369,action:Position(player:"0",pos:(1603.333,1600.0))),(timestamp:370,action:Position(player:"0",pos:(1602.9164,1600.0))),(timestamp:371,action:Position(player:"0",pos:(1602.4998,1600.0))),(timestamp:372,action:Position(player:"0",pos:(1602.0831,1600.0))),(timestamp:373,action:Position(player:"0",pos:(1601.6665,1600.0))),(timestamp:474,action:Remove(pos:(x:1607,y:1598))),(timestamp:560,action:Remove(pos:(x:1602,y:1598))),(timestamp:819,action:PlaceEntity(force:false,info:(pos:(x:1596,y:1600),ty:"factory_game::infinity_battery",rotation:North,kind:SolarPanel()))),(timestamp:839,action:Position(player:"0",pos:(1602.0831,1600.0))),(timestamp:840,action:Position(player:"0",pos:(1602.4998,1600.0))),(timestamp:841,action:Position(player:"0",pos:(1602.9164,1600.0))),(timestamp:842,action:Position(player:"0",pos:(1603.333,1600.0))),(timestamp:843,action:Position(player:"0",pos:(1603.7496,1600.0))),(timestamp:844,action:Position(player:"0",pos:(1604.1663,1600.0))),(timestamp:845,action:Position(player:"0",pos:(1604.5829,1600.0))),(timestamp:846,action:Position(player:"0",pos:(1604.9995,1600.0))),(timestamp:847,action:Position(player:"0",pos:(1605.4161,1600.0))),(timestamp:848,action:Position(player:"0",pos:(1605.8328,1600.0))),(timestamp:871,action:PlaceEntity(force:false,info:(pos:(x:1612,y:1600),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:994,action:SetRecipe(pos:(x:1612,y:1600),recipe:"factory_game::iron_ore_generation")),(timestamp:1105,action:PlaceEntity(force:false,info:(pos:(x:1607,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:1178,action:PlaceEntity(force:false,info:(pos:(x:1602,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:1293,action:Position(player:"0",pos:(1605.4161,1600.0))),(timestamp:1294,action:Position(player:"0",pos:(1604.9995,1600.0))),(timestamp:1295,action:Position(player:"0",pos:(1604.5829,1600.0))),(timestamp:1296,action:Position(player:"0",pos:(1604.1663,1600.0))),(timestamp:1297,action:Position(player:"0",pos:(1603.7496,1600.0))),(timestamp:1298,action:Position(player:"0",pos:(1603.333,1600.0))),(timestamp:1354,action:Remove(pos:(x:1602,y:1598))),(timestamp:1429,action:Remove(pos:(x:1607,y:1598))),(timestamp:1480,action:Position(player:"0",pos:(1603.7496,1600.0))),(timestamp:1481,action:Position(player:"0",pos:(1604.1663,1600.0))),(timestamp:1482,action:Position(player:"0",pos:(1604.5829,1600.0))),(timestamp:1483,action:Position(player:"0",pos:(1604.9995,1600.0))),(timestamp:1484,action:Position(player:"0",pos:(1605.4161,1600.0))),(timestamp:1485,action:Position(player:"0",pos:(1605.8328,1600.0))),(timestamp:1486,action:Position(player:"0",pos:(1606.2494,1600.0))),(timestamp:1487,action:Position(player:"0",pos:(1606.666,1600.0))),(timestamp:1502,action:Position(player:"0",pos:(1606.666,1599.5834))),(timestamp:1503,action:Position(player:"0",pos:(1606.666,1599.1667))),(timestamp:1504,action:Position(player:"0",pos:(1606.666,1598.7501))),(timestamp:1505,action:Position(player:"0",pos:(1606.666,1598.3335))),(timestamp:1506,action:Position(player:"0",pos:(1606.666,1597.9169))),(timestamp:1507,action:Position(player:"0",pos:(1606.666,1597.5002))),(timestamp:1508,action:Position(player:"0",pos:(1606.666,1597.0836))),(timestamp:1509,action:Position(player:"0",pos:(1606.666,1596.667))),(timestamp:1510,action:Position(player:"0",pos:(1606.666,1596.2504))),(timestamp:1511,action:Position(player:"0",pos:(1606.666,1595.8337))),(timestamp:1512,action:Position(player:"0",pos:(1606.666,1595.4171))),(timestamp:1513,action:Position(player:"0",pos:(1606.666,1595.0005))),(timestamp:1607,action:Position(player:"0",pos:(1606.666,1594.5839))),(timestamp:1608,action:Position(player:"0",pos:(1606.666,1594.1672))),(timestamp:1609,action:Position(player:"0",pos:(1606.666,1593.7506))),(timestamp:1610,action:Position(player:"0",pos:(1606.666,1593.334))),(timestamp:1611,action:Position(player:"0",pos:(1606.666,1592.9174))),(timestamp:1612,action:Position(player:"0",pos:(1606.666,1592.5007))),(timestamp:1613,action:Position(player:"0",pos:(1606.666,1592.0841))),(timestamp:1614,action:Position(player:"0",pos:(1606.666,1591.6675))),(timestamp:1681,action:Position(player:"0",pos:(1606.2494,1591.6675))),(timestamp:1682,action:Position(player:"0",pos:(1605.8328,1591.6675))),(timestamp:1683,action:Position(player:"0",pos:(1605.4161,1591.6675))),(timestamp:1684,action:Position(player:"0",pos:(1604.9995,1591.6675))),(timestamp:1685,action:Position(player:"0",pos:(1604.5829,1591.6675))),(timestamp:1686,action:Position(player:"0",pos:(1604.1663,1591.6675))),(timestamp:1687,action:Position(player:"0",pos:(1603.7496,1591.6675))),(timestamp:1688,action:Position(player:"0",pos:(1603.333,1591.6675))),(timestamp:1689,action:Position(player:"0",pos:(1602.9164,1591.6675))),(timestamp:1690,action:Position(player:"0",pos:(1602.4998,1591.6675))),(timestamp:1691,action:Position(player:"0",pos:(1602.0831,1591.6675))),(timestamp:1692,action:Position(player:"0",pos:(1601.6665,1591.6675))),(timestamp:1871,action:PlaceEntity(force:false,info:(pos:(x:1607,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:1921,action:PlaceEntity(force:false,info:(pos:(x:1602,y:1598),ty:"factory_game::small_power_pole",rotation:North,kind:PowerPole()))),(timestamp:1938,action:Position(player:"0",pos:(1601.6665,1592.0841))),(timestamp:1939,action:Position(player:"0",pos:(1601.6665,1592.5007))),(timestamp:1940,action:Position(player:"0",pos:(1601.6665,1592.9174))),(timestamp:1941,action:Position(player:"0",pos:(1601.6665,1593.334))),(timestamp:1942,action:Position(player:"0",pos:(1601.6665,1593.7506))),(timestamp:1943,action:Position(player:"0",pos:(1601.6665,1594.1672))),(timestamp:1944,action:Position(player:"0",pos:(1601.6665,1594.5839))),(timestamp:1945,action:Position(player:"0",pos:(1601.6665,1595.0005))),(timestamp:1946,action:Position(player:"0",pos:(1601.6665,1595.4171))),(timestamp:1978,action:Position(player:"0",pos:(1602.0831,1595.4171))),(timestamp:1979,action:Position(player:"0",pos:(1602.4998,1595.4171))),(timestamp:1980,action:Position(player:"0",pos:(1602.9164,1595.4171))),(timestamp:1981,action:Position(player:"0",pos:(1603.333,1595.4171))),(timestamp:1982,action:Position(player:"0",pos:(1603.7496,1595.4171))),(timestamp:1983,action:Position(player:"0",pos:(1604.1663,1595.4171))),(timestamp:1984,action:Position(player:"0",pos:(1604.5829,1595.4171))),(timestamp:1985,action:Position(player:"0",pos:(1604.9995,1595.4171))),(timestamp:1986,action:Position(player:"0",pos:(1605.4161,1595.4171))),(timestamp:1987,action:Position(player:"0",pos:(1605.8328,1595.4171))),(timestamp:1988,action:Position(player:"0",pos:(1606.2494,1595.4171))),(timestamp:1989,action:Position(player:"0",pos:(1606.666,1595.4171))),(timestamp:1990,action:Position(player:"0",pos:(1607.0826,1595.4171))),(timestamp:1991,action:Position(player:"0",pos:(1607.4993,1595.4171))),(timestamp:1992,action:Position(player:"0",pos:(1607.9159,1595.4171))),(timestamp:1993,action:Position(player:"0",pos:(1608.3325,1595.4171))),(timestamp:1994,action:Position(player:"0",pos:(1608.7491,1595.4171))),(timestamp:1995,action:Position(player:"0",pos:(1609.1658,1595.4171))),(timestamp:1996,action:Position(player:"0",pos:(1609.5824,1595.4171))),(timestamp:1997,action:Position(player:"0",pos:(1609.999,1595.4171))),(timestamp:1998,action:Position(player:"0",pos:(1610.4156,1595.4171))),(timestamp:1999,action:Position(player:"0",pos:(1610.8323,1595.4171))),(timestamp:2000,action:Position(player:"0",pos:(1611.2489,1595.4171))),(timestamp:2001,action:Position(player:"0",pos:(1611.6655,1595.4171))),(timestamp:2002,action:Position(player:"0",pos:(1612.0822,1595.4171))),(timestamp:2003,action:Position(player:"0",pos:(1612.4988,1595.4171))),(timestamp:2004,action:Position(player:"0",pos:(1612.9154,1595.4171))),(timestamp:2005,action:Position(player:"0",pos:(1613.332,1595.4171))),(timestamp:2006,action:Position(player:"0",pos:(1613.7487,1595.4171))),(timestamp:2070,action:Position(player:"0",pos:(1613.7487,1595.0005))),(timestamp:2071,action:Position(player:"0",pos:(1613.7487,1594.5839))),(timestamp:2072,action:Position(player:"0",pos:(1613.332,1594.1672))),(timestamp:2073,action:Position(player:"0",pos:(1612.9154,1593.7506))),(timestamp:2074,action:Position(player:"0",pos:(1612.4988,1593.334))),(timestamp:2075,action:Position(player:"0",pos:(1612.0822,1592.9174))),(timestamp:2076,action:Position(player:"0",pos:(1611.6655,1592.5007))),(timestamp:2077,action:Position(player:"0",pos:(1611.2489,1592.0841))),(timestamp:2078,action:Position(player:"0",pos:(1610.8323,1592.0841))),(timestamp:2079,action:Position(player:"0",pos:(1610.4156,1592.0841))),(timestamp:2080,action:Position(player:"0",pos:(1609.999,1592.0841))),(timestamp:2081,action:Position(player:"0",pos:(1609.5824,1592.0841))),(timestamp:2082,action:Position(player:"0",pos:(1609.1658,1592.0841))),(timestamp:2083,action:Position(player:"0",pos:(1608.7491,1592.0841))),(timestamp:2084,action:Position(player:"0",pos:(1608.3325,1592.0841))),(timestamp:2085,action:Position(player:"0",pos:(1607.9159,1592.0841))),(timestamp:2086,action:Position(player:"0",pos:(1607.4993,1592.0841))),(timestamp:2087,action:Position(player:"0",pos:(1607.0826,1592.0841))),(timestamp:2088,action:Position(player:"0",pos:(1606.666,1592.0841))),(timestamp:2089,action:Position(player:"0",pos:(1606.2494,1592.0841))),(timestamp:2090,action:Position(player:"0",pos:(1605.8328,1592.0841))),(timestamp:2091,action:Position(player:"0",pos:(1605.4161,1592.0841))),(timestamp:2092,action:Position(player:"0",pos:(1604.9995,1592.0841))),(timestamp:2093,action:Position(player:"0",pos:(1604.5829,1592.0841))),(timestamp:2094,action:Position(player:"0",pos:(1604.1663,1592.0841))),(timestamp:2095,action:Position(player:"0",pos:(1603.7496,1592.0841))),(timestamp:2096,action:Position(player:"0",pos:(1603.333,1592.0841))),(timestamp:2097,action:Position(player:"0",pos:(1602.9164,1592.0841))),(timestamp:2098,action:Position(player:"0",pos:(1602.4998,1592.0841))),(timestamp:2099,action:Position(player:"0",pos:(1602.0831,1592.0841))),(timestamp:2100,action:Position(player:"0",pos:(1601.6665,1592.0841))),(timestamp:2101,action:Position(player:"0",pos:(1601.2499,1592.0841))),(timestamp:2102,action:Position(player:"0",pos:(1600.8333,1592.0841))),(timestamp:2103,action:Position(player:"0",pos:(1600.4166,1592.0841))),(timestamp:2104,action:Position(player:"0",pos:(1600.0,1592.0841))),(timestamp:2124,action:Position(player:"0",pos:(1600.4166,1592.0841))),(timestamp:2125,action:Position(player:"0",pos:(1600.8333,1592.0841))),(timestamp:2126,action:Position(player:"0",pos:(1601.2499,1592.0841))),(timestamp:2127,action:Position(player:"0",pos:(1601.6665,1592.0841))),(timestamp:2128,action:Position(player:"0",pos:(1602.0831,1592.0841))),(timestamp:2129,action:Position(player:"0",pos:(1602.4998,1592.0841))),(timestamp:2130,action:Position(player:"0",pos:(1602.9164,1592.0841))),(timestamp:2131,action:Position(player:"0",pos:(1603.333,1592.0841))),(timestamp:2132,action:Position(player:"0",pos:(1603.7496,1592.0841))),(timestamp:2133,action:Position(player:"0",pos:(1604.1663,1592.0841))),(timestamp:2134,action:Position(player:"0",pos:(1604.5829,1592.0841))),(timestamp:2135,action:Position(player:"0",pos:(1604.9995,1592.0841))),(timestamp:2136,action:Position(player:"0",pos:(1605.4161,1592.0841))),(timestamp:2137,action:Position(player:"0",pos:(1605.8328,1592.0841))),(timestamp:2138,action:Position(player:"0",pos:(1606.2494,1592.0841))),(timestamp:2139,action:Position(player:"0",pos:(1606.666,1592.0841))),(timestamp:2140,action:Position(player:"0",pos:(1607.0826,1592.0841))),(timestamp:2141,action:Position(player:"0",pos:(1607.4993,1592.0841))),(timestamp:2142,action:Position(player:"0",pos:(1607.9159,1592.0841))),(timestamp:2143,action:Position(player:"0",pos:(1608.3325,1592.0841))),(timestamp:2144,action:Position(player:"0",pos:(1608.7491,1592.0841))),(timestamp:2145,action:Position(player:"0",pos:(1609.1658,1592.0841))),(timestamp:2146,action:Position(player:"0",pos:(1609.5824,1592.0841))),(timestamp:2147,action:Position(player:"0",pos:(1609.999,1592.0841))),(timestamp:2148,action:Position(player:"0",pos:(1610.4156,1592.0841))),(timestamp:2149,action:Position(player:"0",pos:(1610.8323,1592.0841))),(timestamp:2150,action:Position(player:"0",pos:(1611.2489,1592.0841))),(timestamp:2151,action:Position(player:"0",pos:(1611.6655,1592.0841))),(timestamp:2152,action:Position(player:"0",pos:(1612.0822,1592.0841))),(timestamp:2153,action:Position(player:"0",pos:(1612.4988,1592.0841))),(timestamp:2154,action:Position(player:"0",pos:(1612.9154,1592.0841))),(timestamp:2155,action:Position(player:"0",pos:(1613.332,1592.0841))),(timestamp:2156,action:Position(player:"0",pos:(1613.7487,1592.0841))),(timestamp:2157,action:Position(player:"0",pos:(1614.1653,1592.0841))),(timestamp:2158,action:Position(player:"0",pos:(1614.5819,1592.0841))),(timestamp:2159,action:Position(player:"0",pos:(1614.9985,1592.0841))),(timestamp:2160,action:Position(player:"0",pos:(1615.4152,1592.0841))),(timestamp:2161,action:Position(player:"0",pos:(1615.8318,1592.0841))),(timestamp:2162,action:Position(player:"0",pos:(1616.2484,1592.0841))),(timestamp:2163,action:Position(player:"0",pos:(1616.665,1592.0841))),(timestamp:2164,action:Position(player:"0",pos:(1617.0817,1592.0841))),(timestamp:2165,action:Position(player:"0",pos:(1617.4983,1592.0841))),(timestamp:2166,action:Position(player:"0",pos:(1617.9149,1592.0841))),(timestamp:2167,action:Position(player:"0",pos:(1618.3315,1592.0841))),(timestamp:2168,action:Position(player:"0",pos:(1618.7482,1592.0841))),(timestamp:2169,action:Position(player:"0",pos:(1619.1648,1592.0841))),(timestamp:2170,action:Position(player:"0",pos:(1619.5814,1592.0841))),(timestamp:2171,action:Position(player:"0",pos:(1619.998,1592.0841))),(timestamp:2172,action:Position(player:"0",pos:(1620.4147,1592.0841))),(timestamp:2215,action:Position(player:"0",pos:(1619.998,1592.5007))),(timestamp:2216,action:Position(player:"0",pos:(1619.5814,1592.9174))),(timestamp:2217,action:Position(player:"0",pos:(1619.1648,1593.334))),(timestamp:2218,action:Position(player:"0",pos:(1618.7482,1593.7506))),(timestamp:2219,action:Position(player:"0",pos:(1618.3315,1594.1672))),(timestamp:2220,action:Position(player:"0",pos:(1617.9149,1594.5839))),(timestamp:2221,action:Position(player:"0",pos:(1617.4983,1595.0005))),(timestamp:2222,action:Position(player:"0",pos:(1617.0817,1595.4171))),(timestamp:2223,action:Position(player:"0",pos:(1616.665,1595.8337))),(timestamp:2224,action:Position(player:"0",pos:(1616.2484,1596.2504))),(timestamp:2225,action:Position(player:"0",pos:(1615.8318,1596.667))),(timestamp:2226,action:Position(player:"0",pos:(1615.4152,1597.0836))),(timestamp:2227,action:Position(player:"0",pos:(1614.9985,1597.5002))),(timestamp:2228,action:Position(player:"0",pos:(1614.5819,1597.9169))),(timestamp:2229,action:Position(player:"0",pos:(1614.1653,1598.3335))),(timestamp:2230,action:Position(player:"0",pos:(1613.7487,1598.7501))),(timestamp:2231,action:Position(player:"0",pos:(1613.332,1599.1667))),(timestamp:2232,action:Position(player:"0",pos:(1612.9154,1599.5834))),(timestamp:2233,action:Position(player:"0",pos:(1612.4988,1600.0))),(timestamp:2234,action:Position(player:"0",pos:(1612.0822,1600.4166))),(timestamp:2235,action:Position(player:"0",pos:(1611.6655,1600.8333))),(timestamp:2236,action:Position(player:"0",pos:(1611.2489,1600.8333))),(timestamp:2237,action:Position(player:"0",pos:(1610.8323,1600.8333))),(timestamp:2238,action:Position(player:"0",pos:(1610.4156,1600.8333))),(timestamp:2239,action:Position(player:"0",pos:(1609.999,1600.8333))),(timestamp:2240,action:Position(player:"0",pos:(1609.5824,1600.8333))),(timestamp:2241,action:Position(player:"0",pos:(1609.1658,1600.8333))),(timestamp:2242,action:Position(player:"0",pos:(1608.7491,1600.8333))),(timestamp:2243,action:Position(player:"0",pos:(1608.3325,1600.8333))),(timestamp:2244,action:Position(player:"0",pos:(1607.9159,1600.8333))),(timestamp:2245,action:Position(player:"0",pos:(1607.4993,1600.8333))),(timestamp:2246,action:Position(player:"0",pos:(1607.0826,1600.8333))),(timestamp:2247,action:Position(player:"0",pos:(1606.666,1600.8333))),(timestamp:2248,action:Position(player:"0",pos:(1606.2494,1600.8333))),(timestamp:2249,action:Position(player:"0",pos:(1605.8328,1600.8333))),(timestamp:2250,action:Position(player:"0",pos:(1605.4161,1600.8333))),(timestamp:2251,action:Position(player:"0",pos:(1604.9995,1600.8333))),(timestamp:2252,action:Position(player:"0",pos:(1604.5829,1600.8333))),(timestamp:2253,action:Position(player:"0",pos:(1604.1663,1600.8333))),(timestamp:2254,action:Position(player:"0",pos:(1603.7496,1600.8333))),(timestamp:2255,action:Position(player:"0",pos:(1603.333,1600.8333))),(timestamp:2256,action:Position(player:"0",pos:(1602.9164,1600.8333))),(timestamp:2257,action:Position(player:"0",pos:(1602.4998,1600.8333))),(timestamp:2258,action:Position(player:"0",pos:(1602.0831,1600.8333))),(timestamp:2259,action:Position(player:"0",pos:(1601.6665,1600.8333))),(timestamp:2312,action:Position(player:"0",pos:(1601.6665,1600.4166))),(timestamp:2313,action:Position(player:"0",pos:(1601.6665,1600.0))),(timestamp:2314,action:Position(player:"0",pos:(1601.6665,1599.5834))),(timestamp:2315,action:Position(player:"0",pos:(1601.6665,1599.1667))),(timestamp:2316,action:Position(player:"0",pos:(1601.6665,1598.7501))),(timestamp:2317,action:Position(player:"0",pos:(1601.6665,1598.3335))),(timestamp:2318,action:Position(player:"0",pos:(1601.6665,1597.9169))),(timestamp:2319,action:Position(player:"0",pos:(1601.6665,1597.5002))),(timestamp:2320,action:Position(player:"0",pos:(1601.6665,1597.0836))),(timestamp:2321,action:Position(player:"0",pos:(1601.6665,1596.667))),(timestamp:2322,action:Position(player:"0",pos:(1601.6665,1596.2504))),(timestamp:2323,action:Position(player:"0",pos:(1601.6665,1595.8337))),(timestamp:2324,action:Position(player:"0",pos:(1601.6665,1595.4171))),(timestamp:2325,action:Position(player:"0",pos:(1601.6665,1595.0005))),(timestamp:2326,action:Position(player:"0",pos:(1601.6665,1594.5839))),(timestamp:2327,action:Position(player:"0",pos:(1601.6665,1594.1672))),(timestamp:2328,action:Position(player:"0",pos:(1601.6665,1593.7506))),(timestamp:2357,action:Position(player:"0",pos:(1601.6665,1594.1672))),(timestamp:2358,action:Position(player:"0",pos:(1601.6665,1594.5839))),(timestamp:2359,action:Position(player:"0",pos:(1601.6665,1595.0005))),(timestamp:2360,action:Position(player:"0",pos:(1601.6665,1595.4171))),(timestamp:2361,action:Position(player:"0",pos:(1601.6665,1595.8337))),(timestamp:2362,action:Position(player:"0",pos:(1601.6665,1596.2504))),(timestamp:2363,action:Position(player:"0",pos:(1601.6665,1596.667))),(timestamp:2364,action:Position(player:"0",pos:(1601.6665,1597.0836))),(timestamp:2365,action:Position(player:"0",pos:(1601.6665,1597.5002))),(timestamp:2366,action:Position(player:"0",pos:(1601.6665,1597.9169))),(timestamp:2367,action:Position(player:"0",pos:(1601.6665,1598.3335))),(timestamp:2368,action:Position(player:"0",pos:(1601.6665,1598.7501))),(timestamp:2369,action:Position(player:"0",pos:(1601.6665,1599.1667))),(timestamp:2370,action:Position(player:"0",pos:(1601.6665,1599.5834))),(timestamp:2371,action:Position(player:"0",pos:(1601.6665,1600.0))),(timestamp:2372,action:Position(player:"0",pos:(1601.6665,1600.4166))),(timestamp:2373,action:Position(player:"0",pos:(1601.6665,1600.8333))),(timestamp:2374,action:Position(player:"0",pos:(1601.6665,1601.2499))),(timestamp:2429,action:Remove(pos:(x:1597,y:1600))),(timestamp:2452,action:Position(player:"0",pos:(1602.0831,1601.2499))),(timestamp:2453,action:Position(player:"0",pos:(1602.4998,1601.2499))),(timestamp:2454,action:Position(player:"0",pos:(1602.9164,1601.2499))),(timestamp:2455,action:Position(player:"0",pos:(1603.333,1601.2499))),(timestamp:2456,action:Position(player:"0",pos:(1603.7496,1601.2499))),(timestamp:2457,action:Position(player:"0",pos:(1604.1663,1601.2499))),(timestamp:2458,action:Position(player:"0",pos:(1604.5829,1601.2499))),(timestamp:2459,action:Position(player:"0",pos:(1604.9995,1601.2499))),(timestamp:2460,action:Position(player:"0",pos:(1605.4161,1601.2499))),(timestamp:2461,action:Position(player:"0",pos:(1605.8328,1601.2499))),(timestamp:2462,action:Position(player:"0",pos:(1606.2494,1601.2499))),(timestamp:2463,action:Position(player:"0",pos:(1606.666,1601.2499))),(timestamp:2464,action:Position(player:"0",pos:(1607.0826,1601.2499))),(timestamp:2465,action:Position(player:"0",pos:(1607.4993,1601.2499))),(timestamp:2466,action:Position(player:"0",pos:(1607.9159,1601.2499))),(timestamp:2467,action:Position(player:"0",pos:(1608.3325,1601.2499))),(timestamp:2564,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1600),ty:"factory_game::infinity_battery",rotation:North,kind:SolarPanel()))),(timestamp:2591,action:Position(player:"0",pos:(1607.9159,1601.2499))),(timestamp:2592,action:Position(player:"0",pos:(1607.4993,1601.2499))),(timestamp:2593,action:Position(player:"0",pos:(1607.0826,1601.2499))),(timestamp:2594,action:Position(player:"0",pos:(1606.666,1601.2499))),(timestamp:2595,action:Position(player:"0",pos:(1606.2494,1601.2499))),(timestamp:2596,action:Position(player:"0",pos:(1605.8328,1601.2499))),(timestamp:2597,action:Position(player:"0",pos:(1605.4161,1601.2499))),(timestamp:2598,action:Position(player:"0",pos:(1604.9995,1601.2499))),(timestamp:2599,action:Position(player:"0",pos:(1604.5829,1601.2499))),(timestamp:2600,action:Position(player:"0",pos:(1604.1663,1601.2499))),(timestamp:2601,action:Position(player:"0",pos:(1603.7496,1601.2499))),(timestamp:2602,action:Position(player:"0",pos:(1603.333,1601.2499))),(timestamp:2603,action:Position(player:"0",pos:(1602.9164,1601.2499))),(timestamp:2604,action:Position(player:"0",pos:(1602.4998,1601.2499))),(timestamp:2605,action:Position(player:"0",pos:(1602.0831,1601.2499))),(timestamp:2606,action:Position(player:"0",pos:(1601.6665,1601.2499))),(timestamp:2607,action:Position(player:"0",pos:(1601.2499,1601.2499))),(timestamp:2608,action:Position(player:"0",pos:(1600.8333,1601.2499))),(timestamp:2609,action:Position(player:"0",pos:(1600.4166,1601.2499))),(timestamp:2610,action:Position(player:"0",pos:(1600.0,1601.2499))),(timestamp:2611,action:Position(player:"0",pos:(1599.5834,1601.2499))),(timestamp:2612,action:Position(player:"0",pos:(1599.1667,1601.2499))),(timestamp:2613,action:Position(player:"0",pos:(1598.7501,1601.2499))),(timestamp:2614,action:Position(player:"0",pos:(1598.3335,1601.2499))),(timestamp:2615,action:Position(player:"0",pos:(1597.9169,1601.2499))),(timestamp:2616,action:Position(player:"0",pos:(1597.5002,1601.2499))),(timestamp:2617,action:Position(player:"0",pos:(1597.0836,1601.2499))),(timestamp:2618,action:Position(player:"0",pos:(1596.667,1601.2499))),(timestamp:2619,action:Position(player:"0",pos:(1596.2504,1601.2499))),(timestamp:2620,action:Position(player:"0",pos:(1595.8337,1601.2499))),(timestamp:2621,action:Position(player:"0",pos:(1595.4171,1601.2499))),(timestamp:2622,action:Position(player:"0",pos:(1595.0005,1601.2499))),(timestamp:2623,action:Position(player:"0",pos:(1594.5839,1601.2499))),(timestamp:2624,action:Position(player:"0",pos:(1594.1672,1601.2499))),(timestamp:2625,action:Position(player:"0",pos:(1593.7506,1601.2499))),(timestamp:2626,action:Position(player:"0",pos:(1593.334,1601.2499))),(timestamp:2627,action:Position(player:"0",pos:(1592.9174,1601.2499))),(timestamp:2628,action:Position(player:"0",pos:(1592.5007,1601.2499))),(timestamp:2629,action:Position(player:"0",pos:(1592.0841,1601.2499))),(timestamp:2630,action:Position(player:"0",pos:(1591.6675,1601.2499))),(timestamp:2631,action:Position(player:"0",pos:(1591.2509,1601.2499))),(timestamp:2632,action:Position(player:"0",pos:(1590.8342,1601.2499))),(timestamp:2636,action:Position(player:"0",pos:(1591.2509,1601.2499))),(timestamp:2637,action:Position(player:"0",pos:(1591.6675,1601.2499))),(timestamp:2638,action:Position(player:"0",pos:(1592.0841,1601.2499))),(timestamp:2639,action:Position(player:"0",pos:(1592.5007,1601.2499))),(timestamp:2640,action:Position(player:"0",pos:(1592.9174,1601.2499))),(timestamp:2641,action:Position(player:"0",pos:(1593.334,1601.2499))),(timestamp:2642,action:Position(player:"0",pos:(1593.7506,1601.2499))),(timestamp:2643,action:Position(player:"0",pos:(1594.1672,1601.2499))),(timestamp:2644,action:Position(player:"0",pos:(1594.5839,1601.2499))),(timestamp:2645,action:Position(player:"0",pos:(1595.0005,1601.2499))),(timestamp:2646,action:Position(player:"0",pos:(1595.4171,1601.2499))),(timestamp:2647,action:Position(player:"0",pos:(1595.8337,1601.2499))),(timestamp:2648,action:Position(player:"0",pos:(1596.2504,1601.2499))),(timestamp:2649,action:Position(player:"0",pos:(1596.667,1601.2499))),(timestamp:2650,action:Position(player:"0",pos:(1597.0836,1601.2499))),(timestamp:2651,action:Position(player:"0",pos:(1597.5002,1601.2499))),(timestamp:2652,action:Position(player:"0",pos:(1597.9169,1601.2499))),(timestamp:2653,action:Position(player:"0",pos:(1598.3335,1601.2499))),(timestamp:2654,action:Position(player:"0",pos:(1598.7501,1601.2499))),(timestamp:2655,action:Position(player:"0",pos:(1599.1667,1601.2499))),(timestamp:2656,action:Position(player:"0",pos:(1599.5834,1601.2499))),(timestamp:2657,action:Position(player:"0",pos:(1600.0,1601.2499))),(timestamp:2658,action:Position(player:"0",pos:(1600.4166,1601.2499))),(timestamp:2659,action:Position(player:"0",pos:(1600.8333,1601.2499))),(timestamp:2660,action:Position(player:"0",pos:(1601.2499,1601.2499))),(timestamp:2661,action:Position(player:"0",pos:(1601.6665,1601.2499))),(timestamp:2662,action:Position(player:"0",pos:(1602.0831,1601.2499))),(timestamp:2663,action:Position(player:"0",pos:(1602.4998,1601.2499))),(timestamp:2719,action:Position(player:"0",pos:(1602.9164,1601.2499))),(timestamp:2720,action:Position(player:"0",pos:(1603.333,1601.2499))),(timestamp:2721,action:Position(player:"0",pos:(1603.7496,1601.2499))),(timestamp:2722,action:Position(player:"0",pos:(1604.1663,1601.2499))),(timestamp:2723,action:Position(player:"0",pos:(1604.5829,1600.8333))),(timestamp:2724,action:Position(player:"0",pos:(1604.9995,1600.4166))),(timestamp:2725,action:Position(player:"0",pos:(1605.4161,1600.0))),(timestamp:2726,action:Position(player:"0",pos:(1605.8328,1599.5834))),(timestamp:2727,action:Position(player:"0",pos:(1606.2494,1599.1667))),(timestamp:2728,action:Position(player:"0",pos:(1606.666,1598.7501))),(timestamp:2729,action:Position(player:"0",pos:(1607.0826,1598.3335))),(timestamp:2730,action:Position(player:"0",pos:(1607.4993,1597.9169))),(timestamp:2731,action:Position(player:"0",pos:(1607.9159,1597.5002))),(timestamp:2732,action:Position(player:"0",pos:(1608.3325,1597.0836))),(timestamp:2733,action:Position(player:"0",pos:(1608.3325,1596.667))),(timestamp:2734,action:Position(player:"0",pos:(1608.3325,1596.2504))),(timestamp:2735,action:Position(player:"0",pos:(1608.3325,1595.8337))),(timestamp:2776,action:Position(player:"0",pos:(1608.3325,1596.2504))),(timestamp:2777,action:Position(player:"0",pos:(1608.3325,1596.667))),(timestamp:2778,action:Position(player:"0",pos:(1608.3325,1597.0836))),(timestamp:2779,action:Position(player:"0",pos:(1608.3325,1597.5002))),(timestamp:2780,action:Position(player:"0",pos:(1608.3325,1597.9169))),(timestamp:2781,action:Position(player:"0",pos:(1608.3325,1598.3335))),(timestamp:2850,action:PlaceEntity(force:false,info:(pos:(x:1605,y:1595),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Entrance)))),(timestamp:2902,action:PlaceEntity(force:false,info:(pos:(x:1610,y:1595),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Exit)))),(timestamp:3159,action:PlaceEntity(force:false,info:(pos:(x:1607,y:1595),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Entrance)))),(timestamp:3248,action:Position(player:"0",pos:(1608.3325,1597.9169))),(timestamp:3249,action:Position(player:"0",pos:(1608.3325,1597.5002))),(timestamp:3250,action:Position(player:"0",pos:(1608.3325,1597.0836))),(timestamp:3251,action:Position(player:"0",pos:(1608.3325,1596.667))),(timestamp:3252,action:Position(player:"0",pos:(1608.3325,1596.2504))),(timestamp:3253,action:Position(player:"0",pos:(1608.3325,1595.8337))),(timestamp:3254,action:Position(player:"0",pos:(1608.3325,1595.4171))),(timestamp:3255,action:Position(player:"0",pos:(1608.3325,1595.0005))),(timestamp:3256,action:Position(player:"0",pos:(1608.3325,1594.5839))),(timestamp:3257,action:Position(player:"0",pos:(1608.3325,1594.1672))),(timestamp:3258,action:Position(player:"0",pos:(1608.3325,1593.7506))),(timestamp:3259,action:Position(player:"0",pos:(1608.3325,1593.334))),(timestamp:3260,action:Position(player:"0",pos:(1608.3325,1592.9174))),(timestamp:3261,action:Position(player:"0",pos:(1608.3325,1592.5007))),(timestamp:3262,action:Position(player:"0",pos:(1608.3325,1592.0841))),(timestamp:3263,action:Position(player:"0",pos:(1608.3325,1591.6675))),(timestamp:3264,action:Position(player:"0",pos:(1608.3325,1591.2509))),(timestamp:3265,action:Position(player:"0",pos:(1608.3325,1590.8342))),(timestamp:3913,action:Position(player:"0",pos:(1608.3325,1590.4176))),(timestamp:3914,action:Position(player:"0",pos:(1608.3325,1590.001))),(timestamp:3915,action:Position(player:"0",pos:(1608.3325,1589.5844))),(timestamp:3916,action:Position(player:"0",pos:(1608.3325,1589.1677))),(timestamp:3917,action:Position(player:"0",pos:(1608.3325,1588.7511))),(timestamp:3918,action:Position(player:"0",pos:(1608.3325,1588.3345))),(timestamp:3919,action:Position(player:"0",pos:(1608.3325,1587.9178))),(timestamp:3920,action:Position(player:"0",pos:(1608.3325,1587.5012))),(timestamp:3921,action:Position(player:"0",pos:(1608.3325,1587.0846))),(timestamp:3922,action:Position(player:"0",pos:(1608.3325,1586.668))),(timestamp:3923,action:Position(player:"0",pos:(1608.3325,1586.2513))),(timestamp:3924,action:Position(player:"0",pos:(1608.3325,1585.8347))),(timestamp:3925,action:Position(player:"0",pos:(1608.3325,1585.4181))),(timestamp:3926,action:Position(player:"0",pos:(1608.3325,1585.0015))),(timestamp:3937,action:Position(player:"0",pos:(1608.3325,1585.4181))),(timestamp:3938,action:Position(player:"0",pos:(1608.3325,1585.8347))),(timestamp:3939,action:Position(player:"0",pos:(1608.3325,1586.2513))),(timestamp:3940,action:Position(player:"0",pos:(1608.3325,1586.668))),(timestamp:3941,action:Position(player:"0",pos:(1608.3325,1587.0846))),(timestamp:3942,action:Position(player:"0",pos:(1608.3325,1587.5012))),(timestamp:3943,action:Position(player:"0",pos:(1608.3325,1587.9178))),(timestamp:3944,action:Position(player:"0",pos:(1608.3325,1588.3345))),(timestamp:3945,action:Position(player:"0",pos:(1608.3325,1588.7511))),(timestamp:3946,action:Position(player:"0",pos:(1608.3325,1589.1677))),(timestamp:3947,action:Position(player:"0",pos:(1608.3325,1589.5844))),(timestamp:3948,action:Position(player:"0",pos:(1608.3325,1590.001))),(timestamp:3949,action:Position(player:"0",pos:(1608.3325,1590.4176))),(timestamp:3950,action:Position(player:"0",pos:(1608.3325,1590.8342))),(timestamp:3951,action:Position(player:"0",pos:(1608.3325,1591.2509))),(timestamp:3952,action:Position(player:"0",pos:(1608.3325,1591.6675))),(timestamp:3953,action:Position(player:"0",pos:(1608.3325,1592.0841))),(timestamp:3954,action:Position(player:"0",pos:(1608.3325,1592.5007))),(timestamp:3955,action:Position(player:"0",pos:(1608.3325,1592.9174))),(timestamp:3956,action:Position(player:"0",pos:(1608.3325,1593.334))),(timestamp:3957,action:Position(player:"0",pos:(1608.3325,1593.7506))),(timestamp:3958,action:Position(player:"0",pos:(1608.3325,1594.1672))),(timestamp:3959,action:Position(player:"0",pos:(1608.3325,1594.5839))),(timestamp:3960,action:Position(player:"0",pos:(1608.3325,1595.0005))),(timestamp:3961,action:Position(player:"0",pos:(1608.3325,1595.4171))),(timestamp:3962,action:Position(player:"0",pos:(1608.3325,1595.8337))),(timestamp:3963,action:Position(player:"0",pos:(1608.3325,1596.2504))),(timestamp:3964,action:Position(player:"0",pos:(1608.3325,1596.667))),(timestamp:3965,action:Position(player:"0",pos:(1608.3325,1597.0836))),(timestamp:3966,action:Position(player:"0",pos:(1608.3325,1597.5002))),(timestamp:3967,action:Position(player:"0",pos:(1608.3325,1597.9169))),(timestamp:3968,action:Position(player:"0",pos:(1608.3325,1598.3335))),(timestamp:3969,action:Position(player:"0",pos:(1608.3325,1598.7501))),(timestamp:3970,action:Position(player:"0",pos:(1608.3325,1599.1667))),(timestamp:3971,action:Position(player:"0",pos:(1608.3325,1599.5834))),(timestamp:3972,action:Position(player:"0",pos:(1608.3325,1600.0))),(timestamp:3973,action:Position(player:"0",pos:(1608.3325,1600.4166))),(timestamp:3974,action:Position(player:"0",pos:(1608.3325,1600.8333))),(timestamp:3975,action:Position(player:"0",pos:(1608.3325,1601.2499))),(timestamp:3976,action:Position(player:"0",pos:(1608.3325,1601.6665))),(timestamp:3977,action:Position(player:"0",pos:(1608.3325,1602.0831))),(timestamp:3978,action:Position(player:"0",pos:(1608.3325,1602.4998))),(timestamp:3979,action:Position(player:"0",pos:(1608.3325,1602.9164))),(timestamp:3980,action:Position(player:"0",pos:(1608.3325,1603.333))),(timestamp:3981,action:Position(player:"0",pos:(1608.3325,1603.7496))),(timestamp:3996,action:Position(player:"0",pos:(1608.3325,1603.333))),(timestamp:3997,action:Position(player:"0",pos:(1608.3325,1602.9164))),(timestamp:3998,action:Position(player:"0",pos:(1608.3325,1602.4998))),(timestamp:3999,action:Position(player:"0",pos:(1608.3325,1602.0831))),(timestamp:4000,action:Position(player:"0",pos:(1608.3325,1601.6665))),(timestamp:4001,action:Position(player:"0",pos:(1608.3325,1601.2499))),(timestamp:4002,action:Position(player:"0",pos:(1608.3325,1600.8333))),(timestamp:4003,action:Position(player:"0",pos:(1608.3325,1600.4166))),(timestamp:4004,action:Position(player:"0",pos:(1608.3325,1600.0))),(timestamp:4005,action:Position(player:"0",pos:(1608.3325,1599.5834))),(timestamp:4006,action:Position(player:"0",pos:(1608.3325,1599.1667))),(timestamp:4007,action:Position(player:"0",pos:(1608.3325,1598.7501))),(timestamp:4008,action:Position(player:"0",pos:(1608.3325,1598.3335))),(timestamp:4009,action:Position(player:"0",pos:(1608.3325,1597.9169))),(timestamp:4010,action:Position(player:"0",pos:(1608.3325,1597.5002))),(timestamp:4011,action:Position(player:"0",pos:(1608.3325,1597.0836))),(timestamp:4012,action:Position(player:"0",pos:(1608.3325,1596.667))),(timestamp:4013,action:Position(player:"0",pos:(1608.3325,1596.2504))),(timestamp:4014,action:Position(player:"0",pos:(1608.3325,1595.8337))),(timestamp:4015,action:Position(player:"0",pos:(1608.3325,1595.4171))),(timestamp:4016,action:Position(player:"0",pos:(1608.3325,1595.0005))),(timestamp:4017,action:Position(player:"0",pos:(1608.3325,1594.5839))),(timestamp:4018,action:Position(player:"0",pos:(1608.3325,1594.1672))),(timestamp:4019,action:Position(player:"0",pos:(1608.3325,1593.7506))),(timestamp:4020,action:Position(player:"0",pos:(1608.3325,1593.334))),(timestamp:4021,action:Position(player:"0",pos:(1608.3325,1592.9174))),(timestamp:4047,action:Position(player:"0",pos:(1608.3325,1593.334))),(timestamp:4048,action:Position(player:"0",pos:(1608.3325,1593.7506))),(timestamp:4049,action:Position(player:"0",pos:(1608.3325,1594.1672))),(timestamp:4050,action:Position(player:"0",pos:(1608.3325,1594.5839))),(timestamp:4051,action:Position(player:"0",pos:(1608.3325,1595.0005))),(timestamp:4052,action:Position(player:"0",pos:(1608.3325,1595.4171))),(timestamp:4053,action:Position(player:"0",pos:(1608.3325,1595.8337))),(timestamp:4054,action:Position(player:"0",pos:(1608.3325,1596.2504))),(timestamp:4055,action:Position(player:"0",pos:(1608.3325,1596.667))),(timestamp:4056,action:Position(player:"0",pos:(1608.3325,1597.0836))),(timestamp:4057,action:Position(player:"0",pos:(1608.3325,1597.5002))),(timestamp:4058,action:Position(player:"0",pos:(1608.3325,1597.9169))),(timestamp:4059,action:Position(player:"0",pos:(1608.3325,1598.3335))),(timestamp:4060,action:Position(player:"0",pos:(1608.3325,1598.7501))),(timestamp:4061,action:Position(player:"0",pos:(1608.3325,1599.1667))),(timestamp:4062,action:Position(player:"0",pos:(1608.3325,1599.5834))),(timestamp:4063,action:Position(player:"0",pos:(1608.3325,1600.0))),(timestamp:4064,action:Position(player:"0",pos:(1608.3325,1600.4166))),(timestamp:4065,action:Position(player:"0",pos:(1608.3325,1600.8333))),(timestamp:4066,action:Position(player:"0",pos:(1608.3325,1601.2499))),(timestamp:4086,action:Position(player:"0",pos:(1608.3325,1600.8333))),(timestamp:4087,action:Position(player:"0",pos:(1608.3325,1600.4166))),(timestamp:4088,action:Position(player:"0",pos:(1608.3325,1600.0))),(timestamp:4089,action:Position(player:"0",pos:(1608.3325,1599.5834))),(timestamp:4090,action:Position(player:"0",pos:(1608.3325,1599.1667))),(timestamp:4091,action:Position(player:"0",pos:(1608.3325,1598.7501))),(timestamp:4092,action:Position(player:"0",pos:(1608.3325,1598.3335))),(timestamp:4158,action:Remove(pos:(x:1605,y:1595)))],current_timestep:4159,end_timestep:None) \ No newline at end of file diff --git a/crash_replays/004.rep.ron b/crash_replays/004.rep.ron new file mode 100644 index 0000000..90d4b22 --- /dev/null +++ b/crash_replays/004.rep.ron @@ -0,0 +1 @@ +(program_info:(game_version:"7c292fc",git_dirty:true,mod_sha:"CAB095682E37B9E01477714A53608B1A73646A70C4E1560D8C437464F8AF7628",mod_list:[]),generation_info:(example_idx:0,example_settings:[]),actions:[(timestamp:107,action:PlaceEntity(force:false,info:(pos:(x:1598,y:1598),ty:"factory_game::assembler1",rotation:North,kind:Assembler()))),(timestamp:198,action:Position(player:"0",pos:(1600.4166,1599.5834))),(timestamp:199,action:Position(player:"0",pos:(1600.8333,1599.1667))),(timestamp:200,action:Position(player:"0",pos:(1601.2499,1598.7501))),(timestamp:201,action:Position(player:"0",pos:(1601.6665,1598.3335))),(timestamp:202,action:Position(player:"0",pos:(1602.0831,1597.9169))),(timestamp:203,action:Position(player:"0",pos:(1602.4998,1597.5002))),(timestamp:204,action:Position(player:"0",pos:(1602.9164,1597.0836))),(timestamp:387,action:SetRecipe(pos:(x:1598,y:1598),recipe:"factory_game::iron_ore_generation")),(timestamp:514,action:Position(player:"0",pos:(1602.9164,1597.5002))),(timestamp:515,action:Position(player:"0",pos:(1602.9164,1597.9169))),(timestamp:516,action:Position(player:"0",pos:(1602.9164,1598.3335))),(timestamp:517,action:Position(player:"0",pos:(1602.9164,1598.7501))),(timestamp:518,action:Position(player:"0",pos:(1602.9164,1599.1667))),(timestamp:519,action:Position(player:"0",pos:(1602.9164,1599.5834))),(timestamp:520,action:Position(player:"0",pos:(1602.9164,1600.0))),(timestamp:521,action:Position(player:"0",pos:(1602.9164,1600.4166))),(timestamp:522,action:Position(player:"0",pos:(1602.9164,1600.8333))),(timestamp:523,action:Position(player:"0",pos:(1602.9164,1601.2499))),(timestamp:524,action:Position(player:"0",pos:(1602.9164,1601.6665))),(timestamp:525,action:Position(player:"0",pos:(1602.9164,1602.0831))),(timestamp:526,action:Position(player:"0",pos:(1602.9164,1602.4998))),(timestamp:569,action:Remove(pos:(x:1599,y:1599))),(timestamp:593,action:Position(player:"0",pos:(1602.9164,1602.0831))),(timestamp:594,action:Position(player:"0",pos:(1602.9164,1601.6665))),(timestamp:595,action:Position(player:"0",pos:(1602.9164,1601.2499))),(timestamp:596,action:Position(player:"0",pos:(1602.9164,1600.8333))),(timestamp:597,action:Position(player:"0",pos:(1602.9164,1600.4166))),(timestamp:598,action:Position(player:"0",pos:(1602.9164,1600.0))),(timestamp:919,action:PlaceEntity(force:false,info:(pos:(x:1599,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Entrance)))),(timestamp:989,action:PlaceEntity(force:false,info:(pos:(x:1604,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Exit)))),(timestamp:1033,action:PlaceEntity(force:false,info:(pos:(x:1600,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Entrance)))),(timestamp:1057,action:PlaceEntity(force:false,info:(pos:(x:1603,y:1597),ty:"factory_game::fast_transport_belt",rotation:East,kind:Underground(underground_dir:Exit)))),(timestamp:1125,action:Remove(pos:(x:1600,y:1597))),(timestamp:1184,action:Remove(pos:(x:1603,y:1597)))],current_timestep:1185,end_timestep:None) \ No newline at end of file diff --git a/crash_replays/dummy.rep b/crash_replays/dummy.rep deleted file mode 100644 index e69de29..0000000 diff --git a/src/app_state.rs b/src/app_state.rs index 396cf34..0468140 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -31,6 +31,8 @@ use crate::par_generation::ParGenerateInfo; use crate::par_generation::par_generate; use crate::power::Watt; use crate::progress_info::ProgressInfo; +use crate::replays::GenerationInformation; +use crate::replays::ProgramInformation; #[cfg(feature = "client")] use crate::saving::loading::SaveFileList; #[cfg(feature = "client")] @@ -106,6 +108,7 @@ use crate::get_size::Mutex; #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct AuxillaryData { pub game_name: String, + pub gen_info: (ProgramInformation, GenerationInformation), pub current_tick: u64, @@ -123,10 +126,12 @@ pub struct AuxillaryData { impl AuxillaryData { pub fn new( name: String, + gen_info: GenerationInformation, data_store: &DataStore, ) -> Self { AuxillaryData { game_name: name, + gen_info: (ProgramInformation::new(data_store), gen_info), current_tick: 0, statistics: GenStatistics::new(data_store), update_times: Timeline::new(false, data_store), @@ -168,16 +173,21 @@ impl<'a> AddAssign<&'a UpdateTime> for UpdateTime { impl GameState { #[must_use] - pub fn new(name: String, data_store: &DataStore) -> Self { + pub fn new( + name: String, + gen_info: GenerationInformation, + data_store: &DataStore, + ) -> Self { Self { world: Mutex::new(World::default()), simulation_state: Mutex::new(SimulationState::new(data_store)), - aux_data: Mutex::new(AuxillaryData::new(name, data_store)), + aux_data: Mutex::new(AuxillaryData::new(name, gen_info, data_store)), } } fn new_with_world_area( name: String, + gen_info: GenerationInformation, top_left: Position, bottom_right: Position, data_store: &DataStore, @@ -185,13 +195,14 @@ impl GameState, @@ -201,6 +212,7 @@ impl GameState GameState GameState, @@ -342,6 +358,7 @@ impl GameState GameState, data_store: &DataStore, ) -> Self { let mut ret = GameState::new_with_world_area( name, + gen_info, Position { x: 0, y: 0 }, Position { x: 1_000, @@ -410,6 +429,7 @@ impl GameState, @@ -419,6 +439,7 @@ impl GameState GameState, ) -> Self { @@ -442,6 +464,7 @@ impl GameState GameState, data_store: &DataStore, ) -> Self { - let mut ret = GameState::new(name, data_store); + let mut ret = GameState::new(name, gen_info, data_store); let red = File::open("test_blueprints/eight_beacon_red_sci_with_storage.bp").unwrap(); let red: Blueprint = ron::de::from_reader(red).unwrap(); @@ -553,11 +577,13 @@ impl GameState, bp_path: impl AsRef, ) -> Self { let mut ret = GameState::new_with_world_area( name, + gen_info, Position { x: 0, y: 0 }, Position { x: 32000, @@ -1162,7 +1188,8 @@ impl Factory, }, @@ -3534,6 +3561,7 @@ mod tests { use crate::blueprint::BlueprintAction; + use crate::replays::GenerationInformation; use crate::{ DATA_STORE, app_state::GameState, @@ -3629,7 +3657,7 @@ mod tests { #[test] fn test_random_blueprint_does_not_crash(base_pos in random_position(), blueprint in random_blueprint_strategy::(0..1_000, &DATA_STORE)) { - let mut game_state = GameState::new("Test Game".to_string(), &DATA_STORE); + let mut game_state = GameState::new("Test Game".to_string(), GenerationInformation::default(), &DATA_STORE); blueprint.apply(false, base_pos, &mut game_state, &DATA_STORE); @@ -3638,7 +3666,7 @@ mod tests { #[test] fn test_random_blueprint_does_not_crash_after(base_pos in random_position(), blueprint in random_blueprint_strategy::(0..100, &DATA_STORE), time in 0usize..10) { - let mut game_state = GameState::new("Test Game".to_string(), &DATA_STORE); + let mut game_state = GameState::new("Test Game".to_string(), GenerationInformation::default(), &DATA_STORE); blueprint.apply(false, base_pos, &mut game_state, &DATA_STORE); @@ -3668,7 +3696,7 @@ mod tests { // .. // }))); - let mut game_state = GameState::new("Test Game".to_string(), &DATA_STORE); + let mut game_state = GameState::new("Test Game".to_string(), GenerationInformation::default(), &DATA_STORE); Blueprint { actions: actions.into_iter().map(|a| BlueprintAction::from_with_datastore(&a, &*DATA_STORE)).collect() }.apply(false, Position { x: 0, y: 0 }, &mut game_state, &DATA_STORE); @@ -3752,132 +3780,132 @@ mod tests { // } } - #[bench] - fn bench_single_inserter(b: &mut Bencher) { - let mut game_state = GameState::new("Test Game".to_string(), &DATA_STORE); - - let mut rep = Replay::new(&game_state, None, (*DATA_STORE).clone()); - - rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { - force: false, - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( - crate::frontend::world::tile::PlaceEntityType::PowerPole { - pos: Position { x: 0, y: 5 }, - ty: 0, - }, - ), - })]); - - rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { - force: false, - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( - crate::frontend::world::tile::PlaceEntityType::SolarPanel { - pos: Position { x: 0, y: 2 }, - ty: 0, - }, - ), - })]); - - rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { - force: false, - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( - crate::frontend::world::tile::PlaceEntityType::Assembler { - pos: Position { x: 0, y: 6 }, - ty: 0, - rotation: Dir::North, - }, - ), - })]); - - rep.append_actions(vec![ActionType::SetRecipe( - crate::frontend::action::set_recipe::SetRecipeInfo { - pos: Position { x: 0, y: 6 }, - recipe: Recipe { id: 0 }, - }, - )]); - - rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { - force: false, - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( - crate::frontend::world::tile::PlaceEntityType::Belt { - pos: Position { x: 1, y: 4 }, - direction: crate::frontend::world::tile::Dir::East, - ty: 0, - }, - ), - })]); - - rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { - force: false, - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( - crate::frontend::world::tile::PlaceEntityType::Belt { - pos: Position { x: 2, y: 4 }, - direction: crate::frontend::world::tile::Dir::East, - ty: 0, - }, - ), - })]); - - rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { - force: false, - entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( - crate::frontend::world::tile::PlaceEntityType::Inserter { - ty: 0, - pos: Position { x: 1, y: 5 }, - dir: crate::frontend::world::tile::Dir::North, - filter: None, - user_movetime: None, - }, - ), - })]); - - let blueprint = Blueprint::from_replay(&rep); - - blueprint.apply( - false, - Position { x: 1600, y: 1600 }, - &mut game_state, - &DATA_STORE, - ); - - dbg!( - &game_state - .world - .lock() - .get_chunk_for_tile(Position { x: 1600, y: 1600 }) - ); - - dbg!(game_state.aux_data.lock().current_tick); - - for _ in 0..600 { - GameState::update( - &mut *game_state.simulation_state.lock(), - &mut *game_state.aux_data.lock(), - &DATA_STORE, - ); - } + // #[bench] + // fn bench_single_inserter(b: &mut Bencher) { + // let mut game_state = GameState::new("Test Game".to_string(), &DATA_STORE); + + // let mut rep = Replay::new(&game_state, None, (*DATA_STORE).clone()); + + // rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { + // force: false, + // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( + // crate::frontend::world::tile::PlaceEntityType::PowerPole { + // pos: Position { x: 0, y: 5 }, + // ty: 0, + // }, + // ), + // })]); + + // rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { + // force: false, + // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( + // crate::frontend::world::tile::PlaceEntityType::SolarPanel { + // pos: Position { x: 0, y: 2 }, + // ty: 0, + // }, + // ), + // })]); + + // rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { + // force: false, + // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( + // crate::frontend::world::tile::PlaceEntityType::Assembler { + // pos: Position { x: 0, y: 6 }, + // ty: 0, + // rotation: Dir::North, + // }, + // ), + // })]); + + // rep.append_actions(vec![ActionType::SetRecipe( + // crate::frontend::action::set_recipe::SetRecipeInfo { + // pos: Position { x: 0, y: 6 }, + // recipe: Recipe { id: 0 }, + // }, + // )]); + + // rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { + // force: false, + // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( + // crate::frontend::world::tile::PlaceEntityType::Belt { + // pos: Position { x: 1, y: 4 }, + // direction: crate::frontend::world::tile::Dir::East, + // ty: 0, + // }, + // ), + // })]); + + // rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { + // force: false, + // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( + // crate::frontend::world::tile::PlaceEntityType::Belt { + // pos: Position { x: 2, y: 4 }, + // direction: crate::frontend::world::tile::Dir::East, + // ty: 0, + // }, + // ), + // })]); + + // rep.append_actions(vec![ActionType::PlaceEntity(PlaceEntityInfo { + // force: false, + // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single( + // crate::frontend::world::tile::PlaceEntityType::Inserter { + // ty: 0, + // pos: Position { x: 1, y: 5 }, + // dir: crate::frontend::world::tile::Dir::North, + // filter: None, + // user_movetime: None, + // }, + // ), + // })]); + + // let blueprint = Blueprint::from_replay(&rep); + + // blueprint.apply( + // false, + // Position { x: 1600, y: 1600 }, + // &mut game_state, + // &DATA_STORE, + // ); - b.iter(|| { - GameState::update( - &mut *game_state.simulation_state.lock(), - &mut *game_state.aux_data.lock(), - &DATA_STORE, - ); - }); + // dbg!( + // &game_state + // .world + // .lock() + // .get_chunk_for_tile(Position { x: 1600, y: 1600 }) + // ); - dbg!(game_state.aux_data.lock().current_tick); - - assert!( - game_state - .aux_data - .lock() - .statistics - .production - .total - .as_ref() - .unwrap() - .items_produced[0] - > 0 - ); - } + // dbg!(game_state.aux_data.lock().current_tick); + + // for _ in 0..600 { + // GameState::update( + // &mut *game_state.simulation_state.lock(), + // &mut *game_state.aux_data.lock(), + // &DATA_STORE, + // ); + // } + + // b.iter(|| { + // GameState::update( + // &mut *game_state.simulation_state.lock(), + // &mut *game_state.aux_data.lock(), + // &DATA_STORE, + // ); + // }); + + // dbg!(game_state.aux_data.lock().current_tick); + + // assert!( + // game_state + // .aux_data + // .lock() + // .statistics + // .production + // .total + // .as_ref() + // .unwrap() + // .items_produced[0] + // > 0 + // ); + // } } diff --git a/src/belt/mod.rs b/src/belt/mod.rs index ee93e76..6b89738 100644 --- a/src/belt/mod.rs +++ b/src/belt/mod.rs @@ -2097,7 +2097,10 @@ impl BeltStore { BeltTileId::AnyBelt(index, _) => match self.any_belts[index as usize] { AnyBelt::Smart(belt_id) => { let belt = self.inner.get_smart(belt_id); - if belt.inserters.inserters.is_empty() { + // FIXME: This gives incorrect results when using extracted inserters + // if belt.inserters.inserters.is_empty() { + // FIXME: To "fix" this, I will assume each belt has an incoming inserter + if false { let items_all_empty = belt.items().all(|loc| loc.is_none()); if items_all_empty { diff --git a/src/blueprint/mod.rs b/src/blueprint/mod.rs index 22b4c5f..c003c7b 100644 --- a/src/blueprint/mod.rs +++ b/src/blueprint/mod.rs @@ -1061,24 +1061,6 @@ impl Blueprint { ); } - pub fn from_replay< - ItemIdxType: IdxTrait, - RecipeIdxType: IdxTrait, - DS: Borrow>, - >( - replay: &Replay, - ) -> Self { - Self { - actions: replay - .actions - .iter() - .map(|ra| { - BlueprintAction::from_with_datastore(&ra.action, replay.data_store.borrow()) - }) - .collect(), - } - } - pub fn from_area( world: &World, sim_state: &SimulationState, diff --git a/src/example_worlds/mod.rs b/src/example_worlds/mod.rs index b9019ab..7910fca 100644 --- a/src/example_worlds/mod.rs +++ b/src/example_worlds/mod.rs @@ -1,3 +1,8 @@ +#[cfg(feature = "client")] +use egui_show_info_derive::ShowInfo; +#[cfg(feature = "client")] +use get_size2::GetSize; + use std::{iter, ops::RangeInclusive, sync::LazyLock}; #[cfg(feature = "client")] @@ -7,6 +12,7 @@ use crate::{ data::DataStore, frontend::{action::ActionType, world::Position}, power::Watt, + replays::GenerationInformation, research::Technology, }; @@ -45,7 +51,7 @@ pub(crate) fn list_example_worlds( ui.text_edit_singleline(&mut values.name_field); }); - for (world, world_values) in WORLDS.iter().zip(values.worlds.iter_mut()) { + for (idx, (world, world_values)) in WORLDS.iter().zip(values.worlds.iter_mut()).enumerate() { let v = ui.horizontal(|ui| { ui.label(world.name); ui.label(world.description); @@ -100,7 +106,15 @@ pub(crate) fn list_example_worlds( let name = values.name_field.clone(); let fun = world.creation_fn; Some(move |progress, data_store: &'_ DataStore| { - (fun)(name, progress, &world_values, data_store) + (fun)( + name, + progress, + GenerationInformation { + example_idx: idx, + example_settings: world_values, + }, + data_store, + ) }) } else { None @@ -117,6 +131,28 @@ pub(crate) fn list_example_worlds( None } +pub(crate) fn get_builder( + name: String, + idx: usize, + values: Vec, +) -> impl FnOnce(ProgressInfo, &DataStore) -> GameState + 'static { + let fun = WORLDS + .get(idx) + .expect("Example World index out of bounds") + .creation_fn; + move |progress, data_store: &'_ DataStore| { + (fun)( + name, + progress, + GenerationInformation { + example_idx: idx, + example_settings: values, + }, + data_store, + ) + } +} + struct ExampleWorld { name: &'static str, description: &'static str, @@ -124,7 +160,8 @@ struct ExampleWorld { // TODO: I might want to change this to depend on the values allowed_on_wasm: fn(&[ValueValue]) -> AllowedOnWasm, - creation_fn: fn(String, ProgressInfo, &[ValueValue], &DataStore) -> GameState, + creation_fn: + fn(String, ProgressInfo, GenerationInformation, &DataStore) -> GameState, } #[derive(Debug, PartialEq)] @@ -148,8 +185,9 @@ enum ValueKind { } // FIXME: Naming??? -#[derive(Clone)] -enum ValueValue { +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) enum ValueValue { Range(usize), Toggle(bool), } @@ -163,7 +201,7 @@ const WORLDS: LazyLock<[ExampleWorld; 5]> = LazyLock::new(|| { allowed_on_wasm: |_| AllowedOnWasm::True, - creation_fn: |name, _, _, data_store| GameState::new(name, data_store), + creation_fn: |name, _, gen_info, data_store| GameState::new(name, gen_info, data_store), }, ExampleWorld { name: "Megabase", @@ -176,12 +214,18 @@ const WORLDS: LazyLock<[ExampleWorld; 5]> = LazyLock::new(|| { allowed_on_wasm: |_| AllowedOnWasm::True, - creation_fn: |name, progress, values, data_store| { - let [ValueValue::Toggle(use_solar_field)] = values else { + creation_fn: |name, progress, gen_info, data_store| { + let &[ValueValue::Toggle(use_solar_field)] = &gen_info.example_settings[..] else { unreachable!(); }; - let gs = GameState::new_with_megabase(name, *use_solar_field, progress, data_store); + let gs = GameState::new_with_megabase( + name, + gen_info, + use_solar_field, + progress, + data_store, + ); let techs = 0..data_store.technology_costs.len(); @@ -233,14 +277,15 @@ const WORLDS: LazyLock<[ExampleWorld; 5]> = LazyLock::new(|| { )) }, - creation_fn: |name, progress, values, data_store| { - let [ValueValue::Range(count)] = values else { + creation_fn: |name, progress, gen_info, data_store| { + let &[ValueValue::Range(count)] = &gen_info.example_settings[..] else { unreachable!(); }; let gs = GameState::new_with_gigabase( name, - (*count).try_into().unwrap(), + gen_info, + count.try_into().unwrap(), progress, data_store, ); @@ -282,8 +327,8 @@ const WORLDS: LazyLock<[ExampleWorld; 5]> = LazyLock::new(|| { description: "A small ring around the world", values: vec![], allowed_on_wasm: |_| AllowedOnWasm::True, - creation_fn: |name, progress, _values, data_store| { - GameState::new_with_world_train_ride(name, progress, data_store) + creation_fn: |name, progress, gen_info, data_store| { + GameState::new_with_world_train_ride(name, gen_info, progress, data_store) }, }, ExampleWorld { @@ -314,14 +359,15 @@ const WORLDS: LazyLock<[ExampleWorld; 5]> = LazyLock::new(|| { AllowedOnWasm::True } }, - creation_fn: |name, progress, values, data_store| { - let [ValueValue::Range(count)] = values else { + creation_fn: |name, progress, gen_info, data_store| { + let &[ValueValue::Range(count)] = &gen_info.example_settings[..] else { unreachable!(); }; GameState::new_with_tons_of_solar( name, - Watt(42_000) * (*count) as u64, + gen_info, + Watt(42_000) * count as u64, Position { x: 1_600, y: 1_600 }, None, progress, diff --git a/src/frontend/action/belt_placement.rs b/src/frontend/action/belt_placement.rs index 8c2ab49..2341d12 100644 --- a/src/frontend/action/belt_placement.rs +++ b/src/frontend/action/belt_placement.rs @@ -396,6 +396,8 @@ pub fn handle_underground_removal NewWithDataStore for T { } } +fn get_version() -> &'static str { + if crate::built_info::GIT_HEAD_REF == Some("refs/head/master") { + crate::built_info::PKG_VERSION + } else { + let version = crate::built_info::GIT_VERSION.unwrap_or( + crate::built_info::GIT_COMMIT_HASH_SHORT.unwrap_or("Could not get git version"), + ); + version + } +} + #[cfg(not(target_arch = "wasm32"))] pub fn main(input: &Vec) -> Result<(), args::ArgsError> { // use ron::ser::PrettyConfig; @@ -299,6 +311,7 @@ enum StartGameInfo { LoadReadable(PathBuf), Create { name: String, + gen_info: GenerationInformation, info: GameCreationInfo, allow_overwrite: bool, }, @@ -348,7 +361,8 @@ fn run_integrated_server( data::DataStoreOptions::ItemU8RecipeU8(data_store) => { let (send, recv) = channel(); - let game_state = Arc::new(game_creation_fn(progress, &data_store)); + let game_state = game_creation_fn(progress, &data_store); + let game_state = Arc::new(game_state); let state_machine: Arc>> = Arc::new(Mutex::new(ActionStateMachine::new_from_gamestate( @@ -419,7 +433,7 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { let raw_data = get_raw_data_test(); let data_store = raw_data.process(); - // let progress = Default::default(); + // let progress = ProgressInfo::new(); let local_addr = "0.0.0.0:42069"; let cancel: Arc = Default::default(); @@ -439,6 +453,7 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { StartGameInfo::LoadReadable(path_buf) => unimplemented!(), StartGameInfo::Create { name, + gen_info, info, allow_overwrite, } => { @@ -447,7 +462,7 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { "Currently allow_overwrite is ignored and overwriting is always allowed!!!!" ); } - GameState::new(name, &data_store) + GameState::new(name, gen_info, &data_store) }, }; @@ -588,59 +603,59 @@ mod tests { replays::{Replay, run_till_finished}, }; - #[bench] - fn clone_empty_simulation(b: &mut Bencher) { - let data_store = get_raw_data_test().process().assume_simple(); + // #[bench] + // fn clone_empty_simulation(b: &mut Bencher) { + // let data_store = get_raw_data_test().process().assume_simple(); - let game_state = GameState::new("Test World".to_string(), &data_store); + // let game_state = GameState::new("Test World".to_string(), &data_store); - let replay = Replay::new(&game_state, None, Rc::new(data_store)); + // let replay = Replay::new(&game_state, None, Rc::new(data_store)); - b.iter(|| replay.clone()); - } + // b.iter(|| replay.clone()); + // } - #[bench] - fn empty_simulation(b: &mut Bencher) { - // 1 hour - const NUM_TICKS: u64 = TICKS_PER_SECOND_LOGIC * 60 * 60; + // #[bench] + // fn empty_simulation(b: &mut Bencher) { + // // 1 hour + // const NUM_TICKS: u64 = TICKS_PER_SECOND_LOGIC * 60 * 60; - let data_store = get_raw_data_test().process().assume_simple(); + // let data_store = get_raw_data_test().process().assume_simple(); - let game_state = GameState::new("Test World".to_string(), &data_store); + // let game_state = GameState::new("Test World".to_string(), &data_store); - let mut replay = Replay::new(&game_state, None, Rc::new(data_store)); + // let mut replay = Replay::new(&game_state, None, Rc::new(data_store)); - for _ in 0..NUM_TICKS { - replay.tick(); - } + // for _ in 0..NUM_TICKS { + // replay.tick(); + // } - replay.finish(); + // replay.finish(); - b.iter(|| black_box(replay.clone().run().with(run_till_finished))); - } + // b.iter(|| black_box(replay.clone().run().with(run_till_finished))); + // } - #[bench] - fn noop_actions_simulation(b: &mut Bencher) { - // 1 hour - const NUM_TICKS: u64 = TICKS_PER_SECOND_LOGIC * 60 * 60; + // #[bench] + // fn noop_actions_simulation(b: &mut Bencher) { + // // 1 hour + // const NUM_TICKS: u64 = TICKS_PER_SECOND_LOGIC * 60 * 60; - let data_store = get_raw_data_test().process().assume_simple(); + // let data_store = get_raw_data_test().process().assume_simple(); - let game_state = GameState::new("Test World".to_string(), &data_store); + // let game_state = GameState::new("Test World".to_string(), &data_store); - let mut replay = Replay::new(&game_state, None, Rc::new(data_store)); + // let mut replay = Replay::new(&game_state, None, Rc::new(data_store)); - for _ in 0..NUM_TICKS { - replay.append_actions( - iter::repeat(ActionType::Ping(Position { x: 100, y: 100 })).take(5), - ); - replay.tick(); - } + // for _ in 0..NUM_TICKS { + // replay.append_actions( + // iter::repeat(ActionType::Ping(Position { x: 100, y: 100 })).take(5), + // ); + // replay.tick(); + // } - replay.finish(); + // replay.finish(); - b.iter(|| replay.clone().run().with(run_till_finished)); - } + // b.iter(|| replay.clone().run().with(run_till_finished)); + // } // #[rstest] // fn crashing_replays(#[files("crash_replays/*.rep")] path: PathBuf) { diff --git a/src/main.rs b/src/main.rs index bedce7b..ac83994 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,7 @@ fn main() -> Result<(), ()> { Ok(()) }, Err(e) => { - println!("{e}"); + eprintln!("{e}"); Err(()) }, }; diff --git a/src/multiplayer/mod.rs b/src/multiplayer/mod.rs index 37a85e8..bb96342 100644 --- a/src/multiplayer/mod.rs +++ b/src/multiplayer/mod.rs @@ -44,7 +44,7 @@ pub(crate) enum Game { ), DedicatedServer( GameState, - Replay>, + Replay, GameStateUpdateHandler>, Box, ), @@ -52,7 +52,7 @@ pub(crate) enum Game { #[cfg(feature = "client")] IntegratedServer( Arc>, - Replay>, + Replay, GameStateUpdateHandler< ItemIdxType, RecipeIdxType, @@ -194,10 +194,7 @@ impl Game { - #[cfg(debug_assertions)] - let replay = Replay::new(&game_state, None, data_store.clone()); - #[cfg(not(debug_assertions))] - let replay = Replay::new_dummy(data_store.clone()); + let replay = Replay::new(game_state.aux_data.lock().gen_info.1.clone(), data_store); Ok(Self::DedicatedServer( game_state, replay, @@ -215,10 +212,7 @@ impl Game { - #[cfg(debug_assertions)] - let replay = Replay::new(&game_state, None, data_store.clone()); - #[cfg(not(debug_assertions))] - let replay = Replay::new_dummy(data_store.clone()); + let replay = Replay::new(game_state.aux_data.lock().gen_info.1.clone(), data_store); Ok(Self::IntegratedServer( game_state, replay, @@ -278,11 +272,7 @@ impl Game { - game_state_update_handler.update::<&DataStore>( - &game_state, - None, - data_store, - ); + game_state_update_handler.update(&game_state, None, data_store); tick_counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed); }, Game::DedicatedServer( diff --git a/src/multiplayer/server.rs b/src/multiplayer/server.rs index 0b3d1ec..963cf9a 100644 --- a/src/multiplayer/server.rs +++ b/src/multiplayer/server.rs @@ -1,7 +1,5 @@ -use std::{borrow::Borrow, fs::File, io::Write, marker::PhantomData}; +use std::{fs::File, io::Write, marker::PhantomData}; -#[cfg(not(target_arch = "wasm32"))] -use crate::saving::save_with_fork; use crate::{ app_state::{AuxillaryData, GameState, SimulationState}, data::DataStore, @@ -67,10 +65,10 @@ impl< } } - pub fn update> + serde::Serialize>( + pub fn update( &mut self, game_state: &GameState, - replay: Option<&mut Replay>, + replay: Option<&mut Replay>, data_store: &DataStore, ) { log::trace!("Start Update"); @@ -101,19 +99,21 @@ impl< let actions: Vec<_> = actions_iter.into_iter().collect(); + #[cfg(feature = "replay")] { profiling::scope!("Update Replay"); if let Some(replay) = replay { - replay.append_actions(actions.iter().cloned()); + replay.append_actions(actions.iter().cloned(), data_store); replay.tick(); - #[cfg(debug_assertions)] { + use ron::ser::PrettyConfig; + profiling::scope!("Serialize Replay to disk"); // If we are in debug mode, save the replay to a file let mut file = File::create("./last_replay.rep").expect("Could not open file"); - let ser = bitcode::serialize(replay).unwrap(); - file.write_all(ser.as_slice()) + let ser = ron::ser::to_string_pretty(replay, PrettyConfig::default()).unwrap(); + file.write_all(ser.as_bytes()) .expect("Could not write to file"); } } diff --git a/src/par_generation.rs b/src/par_generation.rs index 2e5c3c6..ac10a55 100644 --- a/src/par_generation.rs +++ b/src/par_generation.rs @@ -6,6 +6,7 @@ use log::info; use crate::frontend::world::tile::BeltState; use crate::inserter::FakeUnionStorage; use crate::progress_info::ProgressInfo; +use crate::replays::GenerationInformation; use crate::{ DataStore, GameState, Position, WeakIdxTrait, app_state::{AuxillaryData, Factory, SimulationState, StorageStorageInserterStore}, @@ -721,6 +722,7 @@ const NUM_STAGES: u16 = 10; /// Its not very parallel for now, but it does use the fact that we know the generation order to skip a lot of searches pub fn par_generate( name: String, + gen_info: GenerationInformation, world_size: BoundingBox, generation_info: ParGenerateInfo, positions: Vec, @@ -912,7 +914,7 @@ pub fn par_generate( GameState { world: Mutex::new(world), simulation_state: Mutex::new(sim_state), - aux_data: Mutex::new(AuxillaryData::new(name, data_store)), + aux_data: Mutex::new(AuxillaryData::new(name, gen_info, data_store)), } } diff --git a/src/power/power_grid.rs b/src/power/power_grid.rs index eb48e63..1d70bd3 100644 --- a/src/power/power_grid.rs +++ b/src/power/power_grid.rs @@ -2867,6 +2867,7 @@ impl PowerGrid, - pub(crate) state: AppState, + pub state: AppState, pub currently_loaded_game: Option, last_rendered_update: u64, @@ -398,15 +398,7 @@ impl eframe::App for App { .show(ctx, |ui| { Grid::new("version_grid").num_columns(2).show(ui, |ui| { ui.label("Version:"); - if crate::built_info::GIT_HEAD_REF == Some("refs/head/master") { - ui.label(crate::built_info::PKG_VERSION); - } else { - let version = crate::built_info::GIT_VERSION.unwrap_or( - crate::built_info::GIT_COMMIT_HASH_SHORT - .unwrap_or("Could not get git version"), - ); - ui.label(version); - } + ui.label(get_version()); ui.end_row(); // TODO: This does not work because of nixos :/ @@ -755,11 +747,17 @@ impl App { size, cb, ))); + // dbg!("pre simulation_state"); let simulation_state = loaded_game_sized.state.simulation_state.lock(); + // dbg!("pre world"); let world = loaded_game_sized.state.world.lock(); + // dbg!("pre aux_data"); let aux_data = loaded_game_sized.state.aux_data.lock(); + // dbg!("pre state_machine"); let state_machine = loaded_game_sized.state_machine.lock(); + // dbg!("pre data_store"); let data_store = loaded_game_sized.data_store.lock(); + // dbg!("post data_store"); let tick = game.tick.load(std::sync::atomic::Ordering::Relaxed); diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 5cbecbf..5094061 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -2000,8 +2000,6 @@ pub fn render_ui< #[cfg(not(target_arch = "wasm32"))] if let Some(recv) = &mut state_machine_ref.current_fork_save_in_progress { - const NUM_STATES: u8 = 12; - let mut v = [0]; if let Err(e) = recv.recv.read_exact(&mut v) { if e.kind() != std::io::ErrorKind::WouldBlock { @@ -2011,7 +2009,7 @@ pub fn render_ui< } else { if let Some(current_state) = v.last() { recv.current_state = *current_state; - if recv.current_state == NUM_STATES { + if recv.current_state == crate::saving::FORK_SAVE_STAGES as u8 { state_machine_ref.current_fork_save_in_progress = None; } } @@ -2019,8 +2017,10 @@ pub fn render_ui< if let Some(recv) = &state_machine_ref.current_fork_save_in_progress { Window::new("Saving...").default_open(true).show(ctx, |ui| { ui.add( - ProgressBar::new(recv.current_state as f32 / NUM_STATES as f32) - .corner_radius(0.0), + ProgressBar::new( + recv.current_state as f32 / crate::saving::FORK_SAVE_STAGES as f32, + ) + .corner_radius(0.0), ); }); } diff --git a/src/replays/mod.rs b/src/replays/mod.rs index 0b1b03f..8eecd5c 100644 --- a/src/replays/mod.rs +++ b/src/replays/mod.rs @@ -1,90 +1,77 @@ -use std::borrow::Borrow; -use std::future::Future; -use std::ops::ControlFlow; +#[cfg(feature = "client")] +use egui_show_info_derive::ShowInfo; +#[cfg(feature = "client")] +use get_size2::GetSize; use std::sync::Arc; -use parking_lot::Mutex; -use std::mem; +use itertools::Itertools; +use log::warn; -use std::path::PathBuf; +mod replay_action; -use genawaiter::GeneratorState::Complete; -use genawaiter::GeneratorState::Yielded; -use genawaiter::rc::{Gen, r#gen}; -use itertools::Itertools; +use crate::example_worlds::{self, ValueValue}; +use crate::progress_info::ProgressInfo; +use crate::replays::replay_action::{ReplayAction, ReplayActionError}; +use crate::{app_state::GameState, data::DataStore, frontend::action::ActionType, item::IdxTrait}; +use crate::{built_info, get_version}; + +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct ProgramInformation { + // The git rev when this was recorded + game_version: String, + git_dirty: bool, + mod_sha: String, + mod_list: Vec<()>, +} +impl ProgramInformation { + pub(crate) fn new(data_store: &DataStore) -> Self { + Self { + game_version: get_version().to_string(), + git_dirty: built_info::GIT_DIRTY.unwrap_or(false), + mod_sha: data_store.checksum.clone(), + mod_list: vec![], + } + } +} -use crate::{ - app_state::GameState, - data::DataStore, - frontend::action::ActionType, - item::{IdxTrait, WeakIdxTrait}, -}; +#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct GenerationInformation { + // The example world (and settings) which were used + pub example_idx: usize, + pub example_settings: Vec, +} -// TODO: Keyframe support #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct Replay< - ItemIdxType: WeakIdxTrait, - RecipeIdxType: WeakIdxTrait, - DataStor: Borrow>, -> { - /// Compressed binary representation of the starting GameState - starting_state: Box<[u8]>, - pub actions: Vec>, +pub struct Replay { + program_info: ProgramInformation, + generation_info: GenerationInformation, - pub data_store: DataStor, + actions: Vec, current_timestep: u64, - end_timestep: Option, - - storage_location: Option, - - is_dummy: bool, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct ReplayAction { +struct ReplayTimedAction { timestamp: u64, - pub action: ActionType, + action: ReplayAction, } -impl< - ItemIdxType: IdxTrait, - RecipeIdxType: IdxTrait, - DataStor: Borrow>, -> Replay -{ - pub fn new( - game_state: &GameState, - storage_location: Option, - data_store: DataStor, +impl Replay { + pub(crate) fn new( + generation_info: GenerationInformation, + data_store: &DataStore, ) -> Self { - let game_state_bytes = bitcode::serialize(game_state).unwrap(); Self { - starting_state: game_state_bytes.into_boxed_slice(), + program_info: ProgramInformation::new(data_store), + generation_info, actions: vec![], - data_store, current_timestep: 0, end_timestep: None, - - storage_location, - - is_dummy: false, - } - } - - pub fn new_dummy(data_store: DataStor) -> Self { - Self { - starting_state: Box::new([]), - actions: vec![], - data_store, - current_timestep: 0, - end_timestep: None, - - storage_location: None, - - is_dummy: true, } } @@ -95,17 +82,15 @@ impl< .expect("Replay running longer than u64::MAX ticks!"); } - pub fn append_actions( + pub fn append_actions( &mut self, actions: impl IntoIterator>, + data_store: &DataStore, ) { - if self.is_dummy { - return; - } self.actions - .extend(actions.into_iter().map(|a| ReplayAction { + .extend(actions.into_iter().map(|a| ReplayTimedAction { timestamp: self.current_timestep, - action: a, + action: ReplayAction::from_action(a, data_store), })); } @@ -115,165 +100,75 @@ impl< pub fn run( self, - ) -> ReplayViewer< - (GameState, DataStor), - impl Future, DataStor)>, - > { - assert!(!self.is_dummy); - ReplayViewer { - generator: r#gen!({ - let data_store = self.data_store; - - let mut actions = self.actions.into_iter().peekable(); - let mut current_timestep = 0; - - let mut game_state: GameState = - bitcode::deserialize(&*self.starting_state).unwrap(); - - // Free up the memory, so we do not store two copies of the GameState - mem::drop(self.starting_state); - - loop { - let this_ticks_actions = actions - .by_ref() - .peeking_take_while(|a| a.timestamp == current_timestep) - .map(|ra| ra.action); - - // FIXME: - // game_state.apply_actions(this_ticks_actions, data_store.borrow()); - - GameState::update( - &mut *game_state.simulation_state.lock(), - &mut *game_state.aux_data.lock(), - data_store.borrow(), - ); - - // let game_state_opt: Option> = - // yield_!(game_state); - - // game_state = game_state_opt.unwrap(); - - if Some(current_timestep) == self.end_timestep { - break; - } else { - current_timestep += 1; - } - } - - (game_state, data_store) - }), - } - } - - pub fn run_with( - self, - game_state_out: Arc>>, - on_tick: impl Fn(), - ) { - dbg!(&self.end_timestep); - - let data_store = self.data_store; - + game_state: Arc>, + mut on_tick: impl FnMut(&GameState), + data_store: &DataStore, + ) -> Result>, ReplayActionError> { let mut actions = self.actions.into_iter().peekable(); let mut current_timestep = 0; - let game_state: GameState = - bitcode::deserialize(&*self.starting_state).unwrap(); + if data_store.checksum != self.program_info.mod_sha { + warn!("Mod SHA mismatch between replay recording and playback. Sync issues may appear"); + } - // Free up the memory, so we do not store two copies of the GameState - mem::drop(self.starting_state); + if self.program_info.game_version != get_version() && !cfg!(test) { + warn!( + "Game version mismatch between replay recording and playback. Sync issues may appear" + ); + } - *(game_state_out.lock()) = game_state; + let GameState { + world, + simulation_state, + aux_data, + } = &*game_state; + + let new_game_state: GameState = example_worlds::get_builder( + "REPLAY_WORLD".to_string(), + self.generation_info.example_idx, + self.generation_info.example_settings, + )(ProgressInfo::new(), data_store); + + { + *simulation_state.lock() = new_game_state.simulation_state.mutex.into_inner(); + *world.lock() = new_game_state.world.mutex.into_inner(); + *aux_data.lock() = new_game_state.aux_data.mutex.into_inner(); + } loop { let this_ticks_actions: Vec<_> = actions .by_ref() .peeking_take_while(|a| a.timestamp == current_timestep) - .map(|ra| ra.action) - .collect(); - - let game_state = game_state_out.lock(); - - // FIXME: - // GameState::apply_actions( - // game_state.simulation_state, - // game_state.world, - // this_ticks_actions, - // data_store.borrow(), - // ); - - GameState::update( - &mut *game_state.simulation_state.lock(), - &mut *game_state.aux_data.lock(), - data_store.borrow(), - ); - - on_tick(); - - // let game_state_opt: Option> = - // yield_!(game_state); + .map(|ra| ra.action.to_action(data_store)) + .try_collect()?; + + { + let mut sim_state = game_state.simulation_state.lock(); + let mut world = game_state.world.lock(); + + GameState::apply_actions( + &mut *sim_state, + &mut *world, + this_ticks_actions, + data_store, + ); + + GameState::update( + &mut *sim_state, + &mut *game_state.aux_data.lock(), + data_store, + ); + } - // game_state = game_state_opt.unwrap(); + on_tick(&game_state); if Some(current_timestep) == self.end_timestep { - break; + break Ok(game_state); } else { current_timestep += 1; } } } - - // fn save(&self) -> Result<(), ()> - // where - // DataStor: serde::Serialize, - // { - // match &self.storage_location { - // Some(path) => { - // // Ensure the folder exists - // create_dir_all(path).unwrap(); - - // let - - // let start = Instant::now(); - // // If we are in debug mode, save the replay to a file - // let mut file = File::create(path).expect("Could not open file"); - // let ser = bitcode::serialize(self).unwrap(); - // dbg!(start.elapsed()); - // file.write_all(ser.as_slice()) - // .expect("Could not write to file"); - // dbg!(start.elapsed()); - // Ok(()) - // }, - // None => Err(()), - // } - // } } -pub struct ReplayViewer> { - generator: Gen, F>, -} - -impl> ReplayViewer { - pub fn with(mut self, mut every_step: impl FnMut(&V) -> ControlFlow<(), ()>) -> V { - let mut gs = self.generator.resume_with(None); - - while let Yielded(v) = gs { - match every_step(&v) { - ControlFlow::Continue(()) => {}, - ControlFlow::Break(()) => return v, - } - - gs = self.generator.resume_with(Some(v)); - } - - let Complete(v) = gs else { unreachable!() }; - - let _ = every_step(&v); - - v - } -} - -pub fn run_till_finished(_: &V) -> ControlFlow<(), ()> { - ControlFlow::Continue(()) -} +pub fn run_till_finished(_: &GameState) {} diff --git a/src/replays/replay_action.rs b/src/replays/replay_action.rs new file mode 100644 index 0000000..a4b9bd1 --- /dev/null +++ b/src/replays/replay_action.rs @@ -0,0 +1,656 @@ +use std::num::NonZero; + +use petgraph::graph::NodeIndex; + +use crate::Position; +use crate::belt::splitter::SplitterDistributionMode; +use crate::data::DataStore; +use crate::frontend::action::place_entity::{EntityPlaceOptions, PlaceEntityInfo}; +use crate::frontend::action::set_recipe::SetRecipeInfo; +use crate::frontend::world::tile::Dir; +use crate::frontend::world::tile::PlaceEntityType; +use crate::frontend::world::tile::UndergroundDir; +use crate::item::{Indexable, Item, Recipe}; +use crate::{frontend::action::ActionType, item::IdxTrait}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct ReplayPlaceEntity { + pos: Position, + ty: String, + rotation: Dir, + + kind: ReplayPlaceEntityKind, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +enum ReplayPlaceEntityKind { + Assembler {}, + Inserter { + /// The Item the inserter will move, must fit both the in and output side + filter: Option, + + user_movetime: Option>, + }, + Belt {}, + Underground { + underground_dir: UndergroundDir, + }, + PowerPole {}, + Splitter { + in_mode: Option, + out_mode: Option, + }, + Chest {}, + SolarPanel {}, + Accumulator {}, + Lab {}, + Beacon {}, + FluidTank {}, + MiningDrill {}, +} +impl ReplayPlaceEntity { + fn from_entity_place( + ty: &PlaceEntityType
, + data_store: &DataStore, + ) -> Self { + use ReplayPlaceEntityKind::*; + match ty.clone() { + PlaceEntityType::Assembler { pos, ty, rotation } => Self { + pos, + ty: data_store.assembler_info[ty as usize].name.to_string(), + rotation, + kind: Assembler {}, + }, + PlaceEntityType::Inserter { + ty, + pos, + dir, + filter, + user_movetime, + } => Self { + pos, + ty: data_store.inserter_infos[ty as usize].name.to_string(), + rotation: dir, + kind: Inserter { + filter: filter.map(|item| data_store.item_names[item.into_usize()].to_string()), + user_movetime, + }, + }, + PlaceEntityType::Belt { pos, direction, ty } => Self { + pos, + ty: data_store.belt_infos[ty as usize].name.to_string(), + rotation: direction, + kind: Belt {}, + }, + PlaceEntityType::Underground { + pos, + direction, + ty, + underground_dir, + } => Self { + pos, + ty: data_store.belt_infos[ty as usize].name.to_string(), + rotation: direction, + kind: Underground { underground_dir }, + }, + PlaceEntityType::PowerPole { pos, ty } => Self { + pos, + ty: data_store.power_pole_data[ty as usize].name.to_string(), + rotation: Dir::default(), + kind: PowerPole {}, + }, + PlaceEntityType::Splitter { + pos, + direction, + ty, + in_mode, + out_mode, + } => Self { + pos, + ty: data_store.belt_infos[ty as usize].name.to_string(), + rotation: direction, + kind: Splitter { in_mode, out_mode }, + }, + PlaceEntityType::Chest { pos, ty } => Self { + pos, + ty: data_store.chest_names[ty as usize].to_string(), + rotation: Dir::default(), + kind: Chest {}, + }, + PlaceEntityType::SolarPanel { pos, ty } => Self { + pos, + ty: data_store.solar_panel_info[ty as usize].name.to_string(), + rotation: Dir::default(), + kind: SolarPanel {}, + }, + PlaceEntityType::Accumulator { pos, ty } => Self { + pos, + ty: data_store.accumulator_info[ty as usize].name.to_string(), + rotation: Dir::default(), + kind: Accumulator {}, + }, + PlaceEntityType::Lab { pos, ty } => Self { + pos, + ty: data_store.lab_info[ty as usize].name.to_string(), + rotation: Dir::default(), + kind: Lab {}, + }, + PlaceEntityType::Beacon { ty, pos } => Self { + pos, + ty: data_store.beacon_info[ty as usize].name.to_string(), + rotation: Dir::default(), + kind: Beacon {}, + }, + PlaceEntityType::FluidTank { ty, pos, rotation } => Self { + pos, + ty: data_store.fluid_tank_infos[ty as usize].name.to_string(), + rotation, + kind: FluidTank {}, + }, + PlaceEntityType::MiningDrill { ty, pos, rotation } => Self { + pos, + ty: data_store.mining_drill_info[ty as usize].name.to_string(), + rotation, + kind: MiningDrill {}, + }, + } + } + + fn to_entity_place( + self, + data_store: &DataStore, + ) -> Result, ReplayActionError> { + use PlaceEntityType::*; + let Self { + pos, + ty, + rotation, + kind, + } = self; + let ty = match kind { + ReplayPlaceEntityKind::Assembler {} => Assembler { + pos, + ty: data_store + .assembler_info + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Assembler {}", + ty + )))?, + rotation, + }, + ReplayPlaceEntityKind::Inserter { + filter, + user_movetime, + } => Inserter { + ty: data_store + .inserter_infos + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Inserter {}", + ty + )))?, + pos, + dir: rotation, + filter: filter + .map(|filter| { + data_store + .item_names + .iter() + .position(|info| **info == *ty) + .map(|v| Item::from(A::try_from(v).unwrap())) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Item {}", + filter + ))) + }) + .transpose()?, + user_movetime, + }, + ReplayPlaceEntityKind::Belt {} => Belt { + pos, + direction: rotation, + ty: data_store + .belt_infos + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Belt {}", + ty + )))?, + }, + ReplayPlaceEntityKind::Underground { underground_dir } => Underground { + pos, + direction: rotation, + ty: data_store + .belt_infos + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Belt {}", + ty + )))?, + underground_dir, + }, + ReplayPlaceEntityKind::PowerPole {} => PowerPole { + pos, + ty: data_store + .power_pole_data + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Power Pole {}", + ty + )))?, + }, + ReplayPlaceEntityKind::Splitter { in_mode, out_mode } => Splitter { + pos, + direction: rotation, + ty: data_store + .belt_infos + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Belt {}", + ty + )))?, + in_mode, + out_mode, + }, + ReplayPlaceEntityKind::Chest {} => Chest { + pos, + ty: data_store + .chest_names + .iter() + .position(|info| **info == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Chest {}", + ty + )))?, + }, + ReplayPlaceEntityKind::SolarPanel {} => SolarPanel { + pos, + ty: data_store + .solar_panel_info + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Solar Panel {}", + ty + )))?, + }, + ReplayPlaceEntityKind::Accumulator {} => Accumulator { + pos, + ty: data_store + .accumulator_info + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Accumulator {}", + ty + )))?, + }, + ReplayPlaceEntityKind::Lab {} => Lab { + pos, + ty: data_store + .lab_info + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Lab {}", + ty + )))?, + }, + ReplayPlaceEntityKind::Beacon {} => Beacon { + ty: data_store + .beacon_info + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Beacon {}", + ty + )))?, + pos, + }, + ReplayPlaceEntityKind::FluidTank {} => FluidTank { + ty: data_store + .fluid_tank_infos + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing FluidTank {}", + ty + )))?, + pos, + rotation, + }, + ReplayPlaceEntityKind::MiningDrill {} => MiningDrill { + ty: data_store + .mining_drill_info + .iter() + .position(|info| *info.name == *ty) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing Mining Drill {}", + ty + )))?, + pos, + rotation, + }, + }; + + Ok(ty) + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(super) enum ReplayAction { + PlaceFloorTile { + pos: Position, + ty: String, + }, + PlaceEntity { + force: bool, + info: ReplayPlaceEntity, + }, + + SetRecipe { + pos: Position, + recipe: String, + }, + + OverrideInserterMovetime { + pos: Position, + new_movetime: Option>, + }, + + Position { + player: String, + pos: (f32, f32), + }, + + AddModules { + pos: Position, + modules: Vec, + }, + RemoveModules { + pos: Position, + indices: Vec, + }, + + SetChestSlotLimit { + pos: Position, + num_slots: u8, + }, + + Remove { + pos: Position, + }, + + AddResearchToQueue { + tech: String, + }, + + RemoveResearchFromQueue { + tech: String, + }, + + CheatUnlockTechnology { + tech: String, + }, + + CheatRelockTechnology { + tech: String, + }, + + PlaceOre { + pos: Position, + ore: String, + amount: u32, + }, + + SpawnPlayer {}, +} + +impl ReplayAction { + pub(super) fn from_action( + action: ActionType, + data_store: &DataStore, + ) -> Self { + match action { + ActionType::PlaceFloorTile(_) => todo!(), + ActionType::PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(ty), + force, + }) => Self::PlaceEntity { + force, + info: ReplayPlaceEntity::from_entity_place(&ty, data_store), + }, + ActionType::SetRecipe(set_recipe_info) => Self::SetRecipe { + pos: set_recipe_info.pos, + recipe: data_store.recipe_names[set_recipe_info.recipe.into_usize()].to_string(), + }, + ActionType::OverrideInserterMovetime { pos, new_movetime } => { + Self::OverrideInserterMovetime { pos, new_movetime } + }, + ActionType::Position(id, pos) => Self::Position { + player: format!("{id}"), + pos, + }, + ActionType::AddModules { pos, modules } => Self::AddModules { + pos, + modules: modules + .into_iter() + .map(|module| data_store.module_info[module as usize].name.to_string()) + .collect(), + }, + ActionType::RemoveModules { pos, indices } => Self::RemoveModules { pos, indices }, + ActionType::SetChestSlotLimit { pos, num_slots } => { + Self::SetChestSlotLimit { pos, num_slots } + }, + ActionType::Remove(position) => Self::Remove { pos: position }, + ActionType::AddResearchToQueue { tech } => Self::AddResearchToQueue { + tech: data_store + .technology_tree + .node_weight(NodeIndex::new(tech.id.into())) + .unwrap() + .name + .clone(), + }, + ActionType::RemoveResearchFromQueue { tech } => Self::RemoveResearchFromQueue { + tech: data_store + .technology_tree + .node_weight(NodeIndex::new(tech.id.into())) + .unwrap() + .name + .clone(), + }, + ActionType::CheatUnlockTechnology { tech } => Self::CheatUnlockTechnology { + tech: data_store + .technology_tree + .node_weight(NodeIndex::new(tech.id.into())) + .unwrap() + .name + .clone(), + }, + ActionType::CheatRelockTechnology { tech } => Self::CheatRelockTechnology { + tech: data_store + .technology_tree + .node_weight(NodeIndex::new(tech.id.into())) + .unwrap() + .name + .clone(), + }, + ActionType::PlaceOre { pos, ore, amount } => Self::PlaceOre { + pos, + ore: data_store.item_names[ore.into_usize()].to_string(), + amount, + }, + ActionType::Ping(position) => todo!(), + ActionType::SpawnPlayer {} => Self::SpawnPlayer {}, + } + } + + pub(super) fn to_action( + self, + data_store: &DataStore, + ) -> Result, ReplayActionError> { + use ActionType::*; + let action = match self { + ReplayAction::PlaceFloorTile { pos, ty } => todo!(), + ReplayAction::PlaceEntity { force, info } => PlaceEntity(PlaceEntityInfo { + entities: EntityPlaceOptions::Single(info.to_entity_place(data_store)?), + force, + }), + ReplayAction::SetRecipe { pos, recipe } => SetRecipe(SetRecipeInfo { + pos, + recipe: Recipe::from( + B::try_from( + data_store + .recipe_names + .iter() + .position(|name| **name == *recipe) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing recipe {}", + recipe + )))?, + ) + .unwrap(), + ), + }), + ReplayAction::OverrideInserterMovetime { pos, new_movetime } => { + OverrideInserterMovetime { pos, new_movetime } + }, + ReplayAction::Position { player, pos } => Position(player.parse().unwrap(), pos), + ReplayAction::AddModules { pos, modules } => AddModules { + pos, + modules: modules + .into_iter() + .map(|mod_name| { + data_store + .module_info + .iter() + .position(|modinfo| *modinfo.name == *mod_name) + .map(|v| v.try_into().unwrap()) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing module {}", + mod_name + ))) + }) + .try_collect()?, + }, + ReplayAction::RemoveModules { pos, indices } => RemoveModules { pos, indices }, + ReplayAction::SetChestSlotLimit { pos, num_slots } => { + SetChestSlotLimit { pos, num_slots } + }, + ReplayAction::Remove { pos } => Remove(pos), + ReplayAction::AddResearchToQueue { tech } => AddResearchToQueue { + tech: crate::research::Technology { + id: data_store + .technology_tree + .node_indices() + .find(|index| { + data_store.technology_tree.node_weight(*index).unwrap().name == tech + }) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing technology {}", + tech + )))? + .index() + .try_into() + .unwrap(), + }, + }, + ReplayAction::RemoveResearchFromQueue { tech } => RemoveResearchFromQueue { + tech: crate::research::Technology { + id: data_store + .technology_tree + .node_indices() + .find(|index| { + data_store.technology_tree.node_weight(*index).unwrap().name == tech + }) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing technology {}", + tech + )))? + .index() + .try_into() + .unwrap(), + }, + }, + ReplayAction::CheatUnlockTechnology { tech } => CheatUnlockTechnology { + tech: crate::research::Technology { + id: data_store + .technology_tree + .node_indices() + .find(|index| { + data_store.technology_tree.node_weight(*index).unwrap().name == tech + }) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing technology {}", + tech + )))? + .index() + .try_into() + .unwrap(), + }, + }, + ReplayAction::CheatRelockTechnology { tech } => CheatRelockTechnology { + tech: crate::research::Technology { + id: data_store + .technology_tree + .node_indices() + .find(|index| { + data_store.technology_tree.node_weight(*index).unwrap().name == tech + }) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing technology {}", + tech + )))? + .index() + .try_into() + .unwrap(), + }, + }, + ReplayAction::PlaceOre { pos, ore, amount } => PlaceOre { + pos, + ore: Item::from( + A::try_from( + data_store + .item_names + .iter() + .position(|name| **name == *ore) + .ok_or(ReplayActionError::MissingDatastoreEntry(format!( + "Missing item {}", + ore + )))?, + ) + .unwrap(), + ), + amount, + }, + ReplayAction::SpawnPlayer {} => SpawnPlayer {}, + }; + + Ok(action) + } +} + +#[derive(Debug)] +pub enum ReplayActionError { + MissingDatastoreEntry(String), +} diff --git a/src/saving/mod.rs b/src/saving/mod.rs index 9ceb0ec..96b5dc5 100644 --- a/src/saving/mod.rs +++ b/src/saving/mod.rs @@ -268,6 +268,7 @@ pub fn save_components( lockfile.release().expect("Failed to remove lockfile"); } +pub const FORK_SAVE_STAGES: usize = 13; /// # Panics /// If File system stuff fails #[cfg(not(target_arch = "wasm32"))] diff --git a/tests/visual_replay_tests.rs b/tests/visual_replay_tests.rs new file mode 100644 index 0000000..f04cf29 --- /dev/null +++ b/tests/visual_replay_tests.rs @@ -0,0 +1,171 @@ +use eframe::EventLoopBuilderHook; +use factory::app_state::AppState; +use factory::rendering::eframe_app; +use factory::rendering::window::LoadedGameSized; + +use factory::DATA_STORE; +use factory::replays::GenerationInformation; +use parking_lot::Mutex; +use rstest::fixture; +use rstest::rstest; +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::atomic::AtomicU64; +use std::sync::mpsc::channel; +use std::thread::sleep; +use std::thread::spawn; +use std::time::Duration; +use winit::platform::wayland::EventLoopBuilderExtWayland; + +use factory::app_state::GameState; +use factory::frontend::action::action_state_machine::ActionStateMachine; +use factory::rendering::window::LoadedGame; +use factory::rendering::window::LoadedGameInfo; +use factory::replays::Replay; + +use egui::Context; + +#[fixture] +#[once] +fn start_ui() -> (Mutex, Arc>) { + simple_logger::SimpleLogger::new() + .with_level(log::LevelFilter::Error) + .env() + .init() + .unwrap(); + let (ctx_send, ctx_recv) = channel(); + + let gs = Arc::new(GameState::new( + "TEST_GAMESTATE".to_string(), + GenerationInformation::default(), + &DATA_STORE, + )); + + let gs_move = gs.clone(); + spawn(move || { + let (send_input, recv) = channel(); + let sm = { + let sim_state = gs_move.simulation_state.lock(); + Arc::new(Mutex::new(ActionStateMachine::new_from_gamestate( + 0, + &*gs_move.world.lock(), + &*sim_state, + &DATA_STORE, + ))) + }; + + let sm_move = sm.clone(); + let gs_move_move = gs_move.clone(); + spawn(move || { + loop { + { + let sim_state = gs_move_move.simulation_state.lock(); + let world = gs_move_move.world.lock(); + for _action in sm_move.lock().handle_inputs( + recv.try_iter(), + &*world, + &*sim_state, + &DATA_STORE, + ) { + // dbg!(_action); + } + for _action in sm_move.lock().once_per_update_actions(&*world, &DATA_STORE) { + // dbg!(_action); + } + } + sleep(Duration::from_millis(16)); + } + }); + + let event_loop_builder: Option = + Some(Box::new(|event_loop_builder| { + event_loop_builder.with_any_thread(true); + })); + let native_options: eframe::NativeOptions = eframe::NativeOptions { + event_loop_builder, + + ..Default::default() + }; + + eprintln!("eframe::run_native"); + eframe::run_native( + format!("FactoryGame Test Runner").as_str(), + native_options, + Box::new(move |cc| { + let mut app = eframe_app::App::new(cc); + + ctx_send.send(cc.egui_ctx.clone()).unwrap(); + + let (send, _recv) = channel(); + + app.currently_loaded_game = Some(LoadedGameInfo { + state: LoadedGame::ItemU8RecipeU8(LoadedGameSized { + state: gs_move, + state_machine: sm, + // FIXME: This test was the only reason this was in a mutex and now I no longer use it + data_store: Arc::new(Mutex::new(DATA_STORE.clone())), + ui_action_sender: send, + stop_update_thread: Default::default(), + }), + tick: Arc::new(AtomicU64::new(0)), + }); + + app.input_sender = Some(send_input); + app.state = AppState::Ingame; + + Ok(Box::new(app)) + }), + ) + .expect("failed to run app"); + eprintln!("eframe::run_native returned??"); + }); + + let ctx_lock = Mutex::new(ctx_recv.recv().unwrap()); + + // FIXME: When the last test finishes we SEGV?!? + (ctx_lock, gs) +} + +#[rstest] +fn crashing_replays_visual( + #[files("crash_replays/*.rep.ron")] path: PathBuf, + start_ui: &(Mutex, Arc>), +) { + use std::{fs::File, io::Read}; + + let _im_running = start_ui.0.lock(); + eprintln!("{path:?}"); + + // Keep running for 30 seconds + const RUNTIME_AFTER_PRESUMED_CRASH: u64 = 30 * 60; + + let mut file = File::open(&path).unwrap(); + + let mut v = Vec::with_capacity(file.metadata().unwrap().len() as usize); + + file.read_to_end(&mut v).unwrap(); + let str = String::try_from(v).expect("File content not UTF-8"); + + let mut replay: Replay = ron::de::from_str(&str).expect( + format!("Test replay {path:?} did not deserialize, consider removing it.").as_str(), + ); + replay.finish(); + + let game_state = replay + .run( + start_ui.1.clone(), + |_current_gs| { + // sleep(Duration::from_millis(1)); + }, + &DATA_STORE, + ) + .expect("Replay ran into an error"); + + for _ in 0..RUNTIME_AFTER_PRESUMED_CRASH { + GameState::update( + &mut *game_state.simulation_state.lock(), + &mut *game_state.aux_data.lock(), + &DATA_STORE, + ); + } +} diff --git a/tests/visual_test.rs b/tests/visual_test.rs deleted file mode 100644 index 95ce01e..0000000 --- a/tests/visual_test.rs +++ /dev/null @@ -1,158 +0,0 @@ -// use eframe::EventLoopBuilderHook; -// use factory::app_state::AppState; -// use factory::rendering::eframe_app; -// use factory::rendering::window::LoadedGameSized; - -// use factory::DATA_STORE; -// use parking_lot::Mutex; -// use rstest::fixture; -// use rstest::rstest; -// use std::path::PathBuf; -// use std::sync::Arc; -// use std::sync::atomic::AtomicU64; -// use std::sync::mpsc::channel; -// use std::thread::sleep; -// use std::thread::spawn; -// use std::time::Duration; -// use winit::platform::wayland::EventLoopBuilderExtWayland; - -// use factory::app_state::GameState; -// use factory::data::DataStore; -// use factory::frontend::action::action_state_machine::ActionStateMachine; -// use factory::rendering::window::LoadedGame; -// use factory::rendering::window::LoadedGameInfo; -// use factory::replays::Replay; - -// use egui::Context; - -// #[fixture] -// #[once] -// fn start_ui() -> ( -// Mutex, -// Arc>>, -// Arc>>, -// ) { -// let (ctx_send, ctx_recv) = channel(); - -// let ds = Arc::new(Mutex::new(DATA_STORE.clone())); -// let gs = Arc::new(Mutex::new(GameState::new(&DATA_STORE))); - -// let gs_move = gs.clone(); -// let ds_move = ds.clone(); -// spawn(move || { -// let (send, recv) = channel(); -// let sm = Arc::new(Mutex::new(ActionStateMachine::new( -// 0, -// (1600.0, 1600.0), -// &DATA_STORE, -// ))); - -// let sm_move = sm.clone(); -// let gs_move_move = gs_move.clone(); -// let ds_move_move = ds_move.clone(); -// spawn(move || { -// loop { -// { -// let gs = gs_move_move.lock(); -// for action in -// sm_move -// .lock() -// .handle_inputs(&recv, &gs.world, &ds_move_move.lock()) -// { -// dbg!(action); -// } -// } -// sleep(Duration::from_millis(16)); -// } -// }); - -// let event_loop_builder: Option = -// Some(Box::new(|event_loop_builder| { -// event_loop_builder.with_any_thread(true); -// })); -// let native_options: eframe::NativeOptions = eframe::NativeOptions { -// event_loop_builder, - -// ..Default::default() -// }; - -// eframe::run_native( -// format!("FactoryGame Test Runner").as_str(), -// native_options, -// Box::new(move |cc| { -// let mut app = eframe_app::App::new(cc); - -// ctx_send.send(cc.egui_ctx.clone()).unwrap(); - -// let (send, _recv) = channel(); - -// app.currently_loaded_game = Some(LoadedGameInfo { -// state: LoadedGame::ItemU8RecipeU8(LoadedGameSized { -// state: gs_move, -// state_machine: sm, -// data_store: ds_move, -// ui_action_sender: send, -// stop_update_thread: Default::default(), -// }), -// tick: Arc::new(AtomicU64::new(0)), -// }); - -// let (send, _recv) = channel(); - -// app.input_sender = Some(send); -// app.state = AppState::Ingame; - -// Ok(Box::new(app)) -// }), -// ) -// .expect("failed to run app"); -// }); - -// let ctx_lock = Mutex::new(ctx_recv.recv().unwrap()); - -// // FIXME: When the last test finishes we SEGV?!? -// (ctx_lock, ds, gs) -// } - -// #[rstest] -// fn crashing_replays_visual( -// #[files("crash_replays/*.rep")] path: PathBuf, -// start_ui: &( -// Mutex, -// Arc>>, -// Arc>>, -// ), -// ) { -// use std::{fs::File, io::Read}; - -// let _im_running = start_ui.0.lock(); -// let gs = start_ui.2.clone(); - -// // Keep running for 30 seconds -// const RUNTIME_AFTER_PRESUMED_CRASH: u64 = 30 * 60; - -// let mut file = File::open(&path).unwrap(); - -// let mut v = Vec::with_capacity(file.metadata().unwrap().len() as usize); - -// file.read_to_end(&mut v).unwrap(); - -// // TODO: For non u8 IdxTypes this will fail -// let mut replay: Replay> = bitcode::deserialize(v.as_slice()).expect( -// format!("Test replay {path:?} did not deserialize, consider removing it.").as_str(), -// ); -// replay.finish(); - -// *start_ui.1.lock() = replay.data_store.clone(); - -// let gs_move = gs.clone(); -// let ds_move = start_ui.1.clone(); - -// replay.run_with(gs_move.clone(), || { -// sleep(Duration::from_millis(1)); -// }); - -// for _ in 0..RUNTIME_AFTER_PRESUMED_CRASH { -// gs_move.lock().update(&ds_move.lock()); -// } -// } From e7192b6d9af28374b67dee2a3aed3ad5189da231 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 16 Feb 2026 17:35:25 +0100 Subject: [PATCH 133/152] Update held item when moving mouse --- src/frontend/action/action_state_machine.rs | 145 ++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index b5570e7..99d6553 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -663,6 +663,151 @@ impl }, Input::MouseMove(x, y) => { self.current_mouse_pos = (x, y); + + match &mut self.state { + ActionStateMachineState::CtrlCPressed + | ActionStateMachineState::CopyDragInProgress { start_pos: _ } => {}, + ActionStateMachineState::DelPressed + | ActionStateMachineState::DeleteDragInProgress { start_pos: _ } => {}, + + ActionStateMachineState::Idle | ActionStateMachineState::Viewing(_) => {}, + ActionStateMachineState::Holding(held_object) => match held_object { + HeldObject::Blueprint(_) => {}, + + HeldObject::Tile(_floor_tile) => {}, + HeldObject::Entity(place_entity_type) => match place_entity_type { + PlaceEntityType::Assembler { + pos: position, + ty: _, + rotation: _, + .. + } => { + *position = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Inserter { + pos, + dir: _, + filter: _, + ty: _, + .. + } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Belt { + pos, + ty: _, + direction: _, + } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Underground { + pos, + ty: _, + direction: _, + underground_dir: _, + } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::PowerPole { pos, ty: _ } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Splitter { + pos, + direction: _, + ty: _, + in_mode: _, + out_mode: _, + } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Chest { pos, .. } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::SolarPanel { pos, ty: _ } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Accumulator { pos, ty: _ } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Lab { pos, ty: _, .. } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::Beacon { ty: _, pos, .. } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::FluidTank { + ty: _, + pos, + rotation: _, + } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + PlaceEntityType::MiningDrill { + ty: _, + pos, + rotation: _, + } => { + *pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); + }, + }, + HeldObject::OrePlacement { .. } => {}, + }, + ActionStateMachineState::Deconstructing(position, timer) => { + //todo!("Check if we are still over the same thing") + }, + } vec![] }, From 81540784894620977137d184d53bd569eb90cf37 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 16 Feb 2026 18:10:50 +0100 Subject: [PATCH 134/152] add test harness --- Cargo.lock | 10 + Cargo.toml | 3 +- .../test_world_harness/mod.txt | 7 + src/lib.rs | 3 + src/test_world_harness/assert.rs | 27 ++ src/test_world_harness/mod.rs | 313 ++++++++++++++++++ 6 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 proptest-regressions/test_world_harness/mod.txt create mode 100644 src/test_world_harness/assert.rs create mode 100644 src/test_world_harness/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 7fed985..24a60c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1004,6 +1004,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1658,6 +1667,7 @@ dependencies = [ "bytemuck", "chrono", "console_error_panic_hook", + "convert_case", "dhat", "directories", "ecolor", diff --git a/Cargo.toml b/Cargo.toml index 77bb9fe..6d62264 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ url = "2.5.7" args = "2.2.0" console_error_panic_hook = "0.1.7" fixedbitset = "0.5.7" +convert_case = "0.11.0" # These are all the dependencies which do not work on wasm [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -138,4 +139,4 @@ client = [ "dep:eframe", "dep:egui", "dep:egui_extras", "dep:egui_plot", "dep:pu logging = ["simple_logger"] debug-stat-gathering = [] assembler-craft-tracking = [] -replay = [] \ No newline at end of file +replay = [] diff --git a/proptest-regressions/test_world_harness/mod.txt b/proptest-regressions/test_world_harness/mod.txt new file mode 100644 index 0000000..3fc0d91 --- /dev/null +++ b/proptest-regressions/test_world_harness/mod.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 7beba775c95a401c87549909d3f4975ebab4cca640f413eccaa0801c9aaa77e1 # shrinks to pos = Position { x: 1600, y: 1600 }, zoom_level = 0.0, camera_pos = (0.0, 25.080559) diff --git a/src/lib.rs b/src/lib.rs index f424e3b..de6c050 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,6 +79,9 @@ pub mod research; pub mod scenario; +#[cfg(test)] +pub mod test_world_harness; + #[cfg(feature = "client")] mod example_worlds; diff --git a/src/test_world_harness/assert.rs b/src/test_world_harness/assert.rs new file mode 100644 index 0000000..c784e60 --- /dev/null +++ b/src/test_world_harness/assert.rs @@ -0,0 +1,27 @@ +use crate::{frontend::world::Position, item::IdxTrait}; + +impl super::Test { + pub fn assert_tile_empty(&self, pos: (i32, i32)) { + assert_eq!( + self.game_state + .world + .lock() + .get_entity_at(Position { x: pos.0, y: pos.1 }, &self.data_store), + None, + "{pos:?} was not empty" + ) + } + + pub fn assert_tile_occupied(&self, pos: (i32, i32)) { + assert!( + matches!( + self.game_state + .world + .lock() + .get_entity_at(Position { x: pos.0, y: pos.1 }, &self.data_store), + Some(_), + ), + "{pos:?} was not occupied" + ) + } +} diff --git a/src/test_world_harness/mod.rs b/src/test_world_harness/mod.rs new file mode 100644 index 0000000..1c1cb93 --- /dev/null +++ b/src/test_world_harness/mod.rs @@ -0,0 +1,313 @@ +use std::iter; + +use itertools::Itertools; + +use crate::{ + DATA_STORE, + app_state::GameState, + data::DataStore, + frontend::{ + action::{ + ActionType, + action_state_machine::{ + ActionStateMachine, ActionStateMachineState, HeldObject, WIDTH_PER_LEVEL, + }, + }, + input, + world::tile::Dir, + }, + item::IdxTrait, + replays::GenerationInformation, + test_world_harness::test::player_mouse_to_tile, +}; + +mod assert; + +pub struct Test { + data_store: DataStore, + game_state: GameState, + state_machine: ActionStateMachine, + + action_queue: Vec>, +} + +impl Default for Test { + fn default() -> Self { + let data_store = DATA_STORE.clone(); + let game_state = GameState::new( + "TEST_GAMESTATE".to_string(), + GenerationInformation::default(), + &data_store, + ); + let state_machine = ActionStateMachine::new_from_gamestate( + 0, + &*game_state.world.lock(), + &*game_state.simulation_state.lock(), + &data_store, + ); + Self { + data_store, + game_state, + state_machine, + + action_queue: vec![], + } + } +} + +impl Test { + pub fn clear_hand(&mut self) { + assert!( + matches!( + self.state_machine.state, + ActionStateMachineState::Holding(_) + ), + "Expected to be holding something when calling clear_hand: {:?}", + self.state_machine.state + ); + + self.state_machine.state = ActionStateMachineState::Idle; + } + + // FIXME: This should not exist and is a compat hazard + pub fn hold_bad(&mut self, held: HeldObject) { + self.state_machine.state = ActionStateMachineState::Holding(held); + } + + pub fn hold(&mut self, item: &str) { + let item = ident(item); + let item = self + .data_store + .item_names + .iter() + .enumerate() + .filter_map(|(i, item_name)| item_name.contains(&item).then_some(i)) + .exactly_one() + .expect(&format!("Could not find exclusive match for {}", item)); + + todo!() + } + + pub fn rotate_holding(&mut self, goal_dir: Dir) { + assert!( + matches!( + self.state_machine.state, + ActionStateMachineState::Holding(_) + ), + "Expected to be holding something when calling rotate_holding: {:?}", + self.state_machine.state + ); + + match &mut self.state_machine.state { + ActionStateMachineState::Holding(held_object) => match held_object { + crate::frontend::action::action_state_machine::HeldObject::Tile(_) => { + unreachable!("Tiles cannot be rotated") + }, + crate::frontend::action::action_state_machine::HeldObject::Entity( + place_entity_type, + ) => match place_entity_type { + crate::frontend::world::tile::PlaceEntityType::Assembler { + rotation, .. + } => *rotation = goal_dir, + crate::frontend::world::tile::PlaceEntityType::Inserter { dir, .. } => { + *dir = goal_dir + }, + crate::frontend::world::tile::PlaceEntityType::Belt { direction, .. } => { + *direction = goal_dir + }, + crate::frontend::world::tile::PlaceEntityType::Underground { + direction, + .. + } => *direction = goal_dir, + crate::frontend::world::tile::PlaceEntityType::PowerPole { .. } => todo!(), + crate::frontend::world::tile::PlaceEntityType::Splitter { + direction, .. + } => *direction = goal_dir, + crate::frontend::world::tile::PlaceEntityType::Chest { .. } => todo!(), + crate::frontend::world::tile::PlaceEntityType::SolarPanel { .. } => todo!(), + crate::frontend::world::tile::PlaceEntityType::Accumulator { .. } => todo!(), + crate::frontend::world::tile::PlaceEntityType::Lab { .. } => todo!(), + crate::frontend::world::tile::PlaceEntityType::Beacon { .. } => todo!(), + crate::frontend::world::tile::PlaceEntityType::FluidTank { + rotation, .. + } => *rotation = goal_dir, + crate::frontend::world::tile::PlaceEntityType::MiningDrill { + rotation, .. + } => *rotation = goal_dir, + }, + crate::frontend::action::action_state_machine::HeldObject::OrePlacement { + .. + } => unreachable!("Ore cannot be rotated"), + crate::frontend::action::action_state_machine::HeldObject::Blueprint(_) => { + todo!("Blueprints cannot be rotated") + }, + }, + + _ => unreachable!(), + } + } + + pub fn place(&mut self, pos: (i32, i32)) { + self.click(pos); + self.tick(); + } + + pub fn click(&mut self, pos: (i32, i32)) { + self.left_mouse_up(); + self.mouse_to(pos); + self.left_mouse_down(); + self.left_mouse_up(); + } + + fn left_mouse_down(&mut self) { + self.handle_input(input::Input::LeftClickPressed { + shift: false, + ctrl: false, + }); + } + + fn left_mouse_up(&mut self) { + self.handle_input(input::Input::LeftClickReleased); + } + + pub fn mouse_to(&mut self, pos: (i32, i32)) { + let screen_pos = tile_to_screen( + self.state_machine.zoom_level, + self.state_machine.local_player_pos, + pos, + ) + .expect("TODO: Position is offscreen"); + + self.handle_input(input::Input::MouseMove(screen_pos.0, screen_pos.1)); + } + + fn right_click(&mut self, pos: (i32, i32)) { + self.mouse_to(pos); + self.right_mouse_down(); + self.right_mouse_up(); + } + + pub fn right_mouse_down(&mut self) { + self.handle_input(input::Input::RightClickPressed { shift: false }); + } + + fn right_mouse_up(&mut self) { + self.handle_input(input::Input::RightClickReleased); + } + + fn press(&mut self, key: input::Key) { + self.handle_input(input::Input::KeyPress(key)); + } + fn release(&mut self, key: input::Key) { + self.handle_input(input::Input::KeyRelease(key)); + } + + fn type_key(&mut self, key: input::Key) { + self.press(key); + self.release(key); + } + + fn handle_input(&mut self, input: input::Input) { + let actions = self + .state_machine + .handle_inputs( + iter::once(input), + &*self.game_state.world.lock(), + &*self.game_state.simulation_state.lock(), + &self.data_store, + ) + .collect::>(); + + self.action_queue.extend(actions); + } + + pub fn tick(&mut self) { + self.tick_many(1); + } + + pub fn tick_many(&mut self, count: usize) { + for _ in 0..count { + self.action_queue.extend( + self.state_machine + .once_per_update_actions(&*self.game_state.world.lock(), &self.data_store), + ); + + GameState::apply_actions( + &mut *self.game_state.simulation_state.lock(), + &mut *self.game_state.world.lock(), + self.action_queue.drain(..), + &self.data_store, + ); + + GameState::update( + &mut *self.game_state.simulation_state.lock(), + &mut *self.game_state.aux_data.lock(), + &self.data_store, + ); + } + } +} + +impl Drop for Test { + fn drop(&mut self) { + self.tick_many(20); + } +} + +fn tile_to_screen(zoom_level: f32, camera_pos: (f32, f32), pos: (i32, i32)) -> Option<(f32, f32)> { + let middle = (pos.0 as f32 + 0.5, pos.1 as f32 + 0.5); + + let camera_space = (middle.0 - camera_pos.0, middle.1 - camera_pos.1); + + let mouse_pos = ( + camera_space.0 / 1.5f32.powf(zoom_level) / WIDTH_PER_LEVEL as f32, + camera_space.1 / 1.5f32.powf(zoom_level) / WIDTH_PER_LEVEL as f32, + ); + + Some(mouse_pos) +} + +fn ident(s: &str) -> String { + use convert_case::Casing; + s.to_case(convert_case::Case::Snake) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + blueprint::test::random_position, frontend::action::action_state_machine::WIDTH_PER_LEVEL, + }; + use proptest::*; + + pub fn player_mouse_to_tile( + zoom_level: f32, + camera_pos: (f32, f32), + mouse_pos: (f32, f32), + ) -> (i32, i32) { + let mouse_pos = ( + ((mouse_pos.0) * (WIDTH_PER_LEVEL as f32)) + .mul_add(1.5f32.powf(zoom_level), camera_pos.0), + ((mouse_pos.1) * (WIDTH_PER_LEVEL as f32)) + .mul_add(1.5f32.powf(zoom_level), camera_pos.1), + ); + + (mouse_pos.0.floor() as i32, mouse_pos.1.floor() as i32) + } + + proptest! { + + #[test] + fn mouse_transform_identity(pos in random_position(), zoom_level in 0.0f32..10.0, camera_pos in (-100.0f32..=100.0, -100.0f32..=100.0)) { + let mouse_pos = tile_to_screen(zoom_level, camera_pos, (pos.x, pos.y)); + + prop_assume!(mouse_pos.is_some()); + let mouse_pos = mouse_pos.expect("Assumed"); + + let pos_res = player_mouse_to_tile(zoom_level, camera_pos, mouse_pos); + + prop_assert_eq!((pos.x, pos.y), pos_res); + } + + } +} From 27ba74289f9d13b0a207f342d5b257c85d5d96a5 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Mon, 16 Feb 2026 19:37:00 +0100 Subject: [PATCH 135/152] Show accumulator count in power grid info --- src/rendering/render_world.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 5094061..323932c 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -3665,7 +3665,7 @@ pub fn render_ui< }); TableBuilder::new(ui).columns(Column::auto(), 2).body(|body| { - body.rows(1.0, pg.num_assemblers_of_type.len() + pg.num_solar_panels_of_type.len() + pg.num_beacons_of_type.len()+ pg.num_labs_of_type.len(), |mut row| { + body.rows(1.0, pg.num_assemblers_of_type.len() + pg.num_solar_panels_of_type.len() + pg.num_beacons_of_type.len()+ pg.num_labs_of_type.len()+ pg.main_accumulator_count.len(), |mut row| { let i = row.index(); if i < pg.num_assemblers_of_type.len() { @@ -3688,8 +3688,15 @@ pub fn render_ui< }); row.col(|ui| {ui.add(Label::new(format!("{}", pg.num_beacons_of_type[i])).extend());}); - } else { + } else if i < pg.num_assemblers_of_type.len() + pg.num_solar_panels_of_type.len() + pg.num_beacons_of_type.len() + pg.main_accumulator_count.len() { let i = i - (pg.num_assemblers_of_type.len() + pg.num_solar_panels_of_type.len() + pg.num_beacons_of_type.len()); + row.col(|ui| { + ui.add(Label::new(&data_store_ref.accumulator_info[i].display_name).extend()); + + }); + row.col(|ui| {ui.add(Label::new(format!("{}", pg.main_accumulator_count[i])).extend());}); + } else { + let i = i - (pg.num_assemblers_of_type.len() + pg.num_solar_panels_of_type.len() + pg.num_beacons_of_type.len() + pg.main_accumulator_count.len()); row.col(|ui| { ui.add(Label::new(&data_store_ref.lab_info[i].display_name).extend()); From 76dbccac802b8b8eeb3b8b881877b3589260aa74 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 17 Feb 2026 11:49:05 +0100 Subject: [PATCH 136/152] Use moduleDedupIndex type --- src/app_state.rs | 72 ++++++++++++++++++++++++------------------- src/blueprint/mod.rs | 11 ++++++- src/par_generation.rs | 8 ++--- 3 files changed, 55 insertions(+), 36 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 0468140..a186ca2 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,12 +1,15 @@ use crate::assembler::simd::Conn; use crate::belt::BeltTileId; use crate::belt::belt::Belt; +use crate::blueprint::BlueprintAction; +use crate::blueprint::BlueprintPlaceEntity; use crate::blueprint::blueprint_string::BlueprintString; use crate::chest::ChestSize; use crate::data::AllowedFluidDirection; use crate::frontend::action::belt_placement::FakeGameState; use crate::frontend::action::place_entity::PlaceEntityInfo; use crate::frontend::world::tile::CHUNK_SIZE; +use crate::frontend::world::tile::ModuleSlotDedupIndex; use crate::frontend::world::tile::ModuleSlots; use crate::frontend::world::tile::ModuleTy; use crate::frontend::world::tile::PlayerInfo; @@ -508,9 +511,13 @@ impl GameState GameState GameState GameState GameState GameState GameState GameState GameState, @@ -936,6 +936,14 @@ impl Blueprint { self.actions.len() } + pub fn extract_if(&mut self, filter: impl Fn(&BlueprintAction) -> bool) -> Self { + let extracted = self.actions.extract_if(.., |action| (filter)(action)); + + Self { + actions: extracted.collect(), + } + } + pub fn optimize(&mut self) { info!("Optimizing Blueprint"); self.actions.par_sort_unstable_by_key(|v| match v { @@ -982,6 +990,7 @@ impl Blueprint { BlueprintPlaceEntity::FluidTank { pos, .. } => { (1, 2, (BeltId::Pure(0), 0), *pos, 0) }, + BlueprintPlaceEntity::MiningDrill { pos, .. } => { (1, 3, (BeltId::Pure(0), 0), *pos, 0) }, diff --git a/src/par_generation.rs b/src/par_generation.rs index ac10a55..0387d72 100644 --- a/src/par_generation.rs +++ b/src/par_generation.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use itertools::{Itertools, assert_equal}; use log::info; -use crate::frontend::world::tile::BeltState; +use crate::frontend::world::tile::{BeltState, ModuleSlotDedupIndex}; use crate::inserter::FakeUnionStorage; use crate::progress_info::ProgressInfo; use crate::replays::GenerationInformation; @@ -1123,7 +1123,7 @@ fn assembler_stage( let ent = Entity::Assembler { ty, pos, - modules: modules as u32, + modules: modules as ModuleSlotDedupIndex, info: match info { Some((pole_pos, id, weak_idx)) => { crate::frontend::world::tile::AssemblerInfo::Powered { @@ -1207,7 +1207,7 @@ fn lab_stage( let ent = Entity::Lab { pos, ty, - modules: modules as u32, + modules: modules as ModuleSlotDedupIndex, pole_position: pole_pos, }; @@ -1319,7 +1319,7 @@ fn beacon_stage( let ent = Entity::Beacon { pos, ty, - modules: modules as u32, + modules: modules as ModuleSlotDedupIndex, pole_position: pole_pos, }; From 48bf199d6a09f6e67ecd1db18086592342aad6fa Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 17 Feb 2026 11:49:18 +0100 Subject: [PATCH 137/152] Fix accumulators not being found by new power poles --- src/frontend/world/tile.rs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index ae25f95..01109db 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -1060,6 +1060,17 @@ fn new_power_pole( *pole_position = Some((pole_pos, weak_index)); }, + Entity::Accumulator { + pos, + ty, + pole_position: pole_position @ None, + } => { + let weak_index = sim_state.factory.power_grids.power_grids + [usize::from(grid_id)] + .add_accumulator(*pos, *ty, pole_pos, data_store); + + *pole_position = Some((pole_pos, weak_index)); + }, Entity::Lab { pos, ty, @@ -1850,7 +1861,7 @@ impl World World World World unreachable!("Called change recipe on non assembler: {e:?}"), @@ -4059,6 +4070,13 @@ impl World= 1); debug_assert!(chunk_range_y.clone().count() >= 1); + // dbg!( + // chunk_range_x + // .clone() + // .cartesian_product(chunk_range_y.clone()) + // .count() + // ); + chunk_range_x .cartesian_product(chunk_range_y) .filter_map(|(chunk_x, chunk_y)| self.chunks.get(chunk_x, chunk_y)) @@ -5319,7 +5337,7 @@ pub type ModuleTy = u8; #[derive(Debug, Clone, PartialEq)] pub struct ModuleSlots(pub thin_dst::ThinBox<(), Option>); -pub type ModuleSlotDedupIndex = u32; +pub type ModuleSlotDedupIndex = u16; impl serde::Serialize for ModuleSlots { fn serialize(&self, serializer: S) -> Result From a340fb8a196dc3c85318481fed703bd62dc65e2c Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 17 Feb 2026 11:49:33 +0100 Subject: [PATCH 138/152] Add test for broken underground crash --- src/frontend/action/belt_placement.rs | 434 +++++++++++++++----------- src/frontend/world/mod.rs | 12 +- src/power/power_grid.rs | 1 - 3 files changed, 265 insertions(+), 182 deletions(-) diff --git a/src/frontend/action/belt_placement.rs b/src/frontend/action/belt_placement.rs index 2341d12..eda8dc3 100644 --- a/src/frontend/action/belt_placement.rs +++ b/src/frontend/action/belt_placement.rs @@ -397,7 +397,7 @@ pub fn handle_underground_removal impl Strategy>> { -// Just(vec![ -// place(PlaceEntityType::Assembler { -// pos: Position { x: 0, y: 3 }, -// ty: 0, -// rotation: Dir::North, -// }), -// ActionType::SetRecipe(SetRecipeInfo { -// pos: Position { x: 0, y: 3 }, -// recipe: Recipe { id: 0 }, -// }), -// place(PlaceEntityType::Inserter { -// pos: Position { x: 2, y: 2 }, -// dir: crate::frontend::world::tile::Dir::North, -// filter: None, -// ty: 0, -// user_movetime: None, -// }), -// place(PlaceEntityType::Belt { -// pos: Position { x: 2, y: 1 }, -// direction: crate::frontend::world::tile::Dir::East, -// ty: 0, -// }), -// place(PlaceEntityType::PowerPole { -// pos: Position { x: 0, y: 2 }, -// ty: 0, -// }), -// place(PlaceEntityType::PowerPole { -// pos: Position { x: 5, y: 0 }, -// ty: 0, -// }), -// place(PlaceEntityType::SolarPanel { -// pos: Position { x: 6, y: 0 }, -// ty: 0, -// }), -// ]) -// } - -// fn belts_into_sideload() -> impl Strategy>> { -// Just(vec![ -// place(PlaceEntityType::Belt { -// pos: Position { x: 3, y: 1 }, -// direction: crate::frontend::world::tile::Dir::East, -// ty: 0, -// }), -// place(PlaceEntityType::Belt { -// pos: Position { x: 4, y: 0 }, -// direction: crate::frontend::world::tile::Dir::South, -// ty: 0, -// }), -// place(PlaceEntityType::Belt { -// pos: Position { x: 4, y: 1 }, -// direction: crate::frontend::world::tile::Dir::South, -// ty: 0, -// }), -// place(PlaceEntityType::Belt { -// pos: Position { x: 4, y: 2 }, -// direction: crate::frontend::world::tile::Dir::South, -// ty: 0, -// }), -// ]) -// } - -// fn sideload_items() -> impl Strategy>> { -// (chest_onto_belt(), belts_into_sideload()).prop_map(|(mut a, b)| { -// a.extend(b.into_iter()); -// a -// }) -// } - -// fn place(ty: PlaceEntityType) -> ActionType { -// ActionType::PlaceEntity(crate::frontend::action::place_entity::PlaceEntityInfo { -// entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single(ty), -// force: false, -// }) -// } +#[cfg(test)] +mod test { + use crate::{ + frontend::{ + action::action_state_machine::{ActionStateMachineState, HeldObject}, + world::{ + Position, + tile::{Dir, PlaceEntityType, UndergroundDir}, + }, + }, + test_world_harness::*, + }; + + #[test] + fn underground_blocking_crash() { + let mut test = Test::default(); + + test.hold_bad(HeldObject::Entity(PlaceEntityType::Underground { + pos: Position::default(), + direction: Dir::default(), + ty: 0, + underground_dir: UndergroundDir::Entrance, + })); + test.rotate_holding(Dir::East); + // Place outer pair of undergrounds + test.place((0, 0)); + test.place((5, 0)); + + // // Place inner pair of undergrounds + test.place((2, 0)); + test.place((3, 0)); + + test.clear_hand(); + test.assert_tile_occupied((2, 0)); + + // Remove left of inner pair of undergrounds + test.mouse_to((2, 0)); + test.right_mouse_down(); + test.tick_many(35); + + test.assert_tile_occupied((0, 0)); + test.assert_tile_occupied((5, 0)); + test.assert_tile_empty((2, 0)); + test.assert_tile_occupied((3, 0)); + + // Remove right of inner pair of undergrounds + test.mouse_to((3, 0)); + test.right_mouse_down(); + test.tick_many(35); + + test.assert_tile_occupied((0, 0)); + test.assert_tile_occupied((5, 0)); + test.assert_tile_empty((2, 0)); + test.assert_tile_empty((3, 0)); + } + + #[test] + fn place_underground() { + let mut test = Test::default(); -// proptest! { + test.hold_bad(HeldObject::Entity(PlaceEntityType::Underground { + pos: Position::default(), + direction: Dir::default(), + ty: 0, + underground_dir: UndergroundDir::Entrance, + })); + test.rotate_holding(Dir::East); + test.place((0, 0)); + test.place((5, 0)); -// #[test] -// fn inserter_always_attaches(actions in chest_onto_belt().prop_shuffle()) { -// let mut state = GameState::new( &DATA_STORE); + test.clear_hand(); -// let bp = Blueprint { actions }; + test.assert_tile_occupied((0, 0)); + test.assert_tile_occupied((5, 0)); + } + + // use proptest::prelude::{Just, Strategy}; + // use proptest::{prop_assert, prop_assume, proptest}; + + // use crate::DATA_STORE; + // use crate::app_state::GameState; + // use crate::blueprint::Blueprint; + // use crate::frontend::action::ActionType; + // use crate::frontend::action::set_recipe::SetRecipeInfo; + // use crate::frontend::world::Position; + // use crate::frontend::world::tile::{AssemblerInfo, Dir, Entity, InserterInfo, PlaceEntityType}; + // use crate::item::Recipe; + + // fn chest_onto_belt() -> impl Strategy>> { + // Just(vec![ + // place(PlaceEntityType::Assembler { + // pos: Position { x: 0, y: 3 }, + // ty: 0, + // rotation: Dir::North, + // }), + // ActionType::SetRecipe(SetRecipeInfo { + // pos: Position { x: 0, y: 3 }, + // recipe: Recipe { id: 0 }, + // }), + // place(PlaceEntityType::Inserter { + // pos: Position { x: 2, y: 2 }, + // dir: crate::frontend::world::tile::Dir::North, + // filter: None, + // ty: 0, + // user_movetime: None, + // }), + // place(PlaceEntityType::Belt { + // pos: Position { x: 2, y: 1 }, + // direction: crate::frontend::world::tile::Dir::East, + // ty: 0, + // }), + // place(PlaceEntityType::PowerPole { + // pos: Position { x: 0, y: 2 }, + // ty: 0, + // }), + // place(PlaceEntityType::PowerPole { + // pos: Position { x: 5, y: 0 }, + // ty: 0, + // }), + // place(PlaceEntityType::SolarPanel { + // pos: Position { x: 6, y: 0 }, + // ty: 0, + // }), + // ]) + // } + + // fn belts_into_sideload() -> impl Strategy>> { + // Just(vec![ + // place(PlaceEntityType::Belt { + // pos: Position { x: 3, y: 1 }, + // direction: crate::frontend::world::tile::Dir::East, + // ty: 0, + // }), + // place(PlaceEntityType::Belt { + // pos: Position { x: 4, y: 0 }, + // direction: crate::frontend::world::tile::Dir::South, + // ty: 0, + // }), + // place(PlaceEntityType::Belt { + // pos: Position { x: 4, y: 1 }, + // direction: crate::frontend::world::tile::Dir::South, + // ty: 0, + // }), + // place(PlaceEntityType::Belt { + // pos: Position { x: 4, y: 2 }, + // direction: crate::frontend::world::tile::Dir::South, + // ty: 0, + // }), + // ]) + // } + + // fn sideload_items() -> impl Strategy>> { + // (chest_onto_belt(), belts_into_sideload()).prop_map(|(mut a, b)| { + // a.extend(b.into_iter()); + // a + // }) + // } + + // fn place(ty: PlaceEntityType) -> ActionType { + // ActionType::PlaceEntity(crate::frontend::action::place_entity::PlaceEntityInfo { + // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single(ty), + // force: false, + // }) + // } + + // proptest! { -// bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); + // #[test] + // fn inserter_always_attaches(actions in chest_onto_belt().prop_shuffle()) { + // let mut state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); -// let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); + // let bp = Blueprint { actions }; -// let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); + // bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); -// prop_assert!(assembler_powered); + // let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); -// let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); + // let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); -// prop_assume!(assembler_working, "{:?}", ent); + // prop_assert!(assembler_powered); -// let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); + // let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); -// let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); + // prop_assume!(assembler_working, "{:?}", ent); -// prop_assert!(inserter_attached, "{:?}", ent); -// } + // let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); -// #[test] -// fn inserter_always_attaches_full_bp(actions in sideload_items().prop_shuffle()) { -// let mut state = GameState::new(&DATA_STORE); + // let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); -// let bp = Blueprint { actions }; + // prop_assert!(inserter_attached, "{:?}", ent); + // } -// bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); + // #[test] + // fn inserter_always_attaches_full_bp(actions in sideload_items().prop_shuffle()) { + // let mut state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); -// let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); + // let bp = Blueprint { actions }; -// let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); + // bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); -// prop_assert!(assembler_powered); + // let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); -// let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); + // let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); -// prop_assume!(assembler_working, "{:?}", ent); + // prop_assert!(assembler_powered); -// let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); + // let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); -// let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); + // prop_assume!(assembler_working, "{:?}", ent); -// prop_assert!(inserter_attached, "{:?}", ent); -// } + // let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); -// #[test] -// fn sideload_empty_does_not_crash(actions in belts_into_sideload().prop_shuffle()) { -// let mut state = GameState::new(&DATA_STORE); + // let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); -// let bp = Blueprint { actions }; + // prop_assert!(inserter_attached, "{:?}", ent); + // } -// bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); -// } + // #[test] + // fn sideload_empty_does_not_crash(actions in belts_into_sideload().prop_shuffle()) { + // let mut state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); -// #[test] -// fn sideload_with_items_at_source_does_not_crash(actions in sideload_items().prop_shuffle()) { -// let mut state = GameState::new(&DATA_STORE); + // let bp = Blueprint { actions }; -// let bp = Blueprint { actions }; + // bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); + // } -// bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); -// } + // #[test] + // fn sideload_with_items_at_source_does_not_crash(actions in sideload_items().prop_shuffle()) { + // let mut state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); -// #[test] -// fn sideload_with_items_at_source_items_reach_the_intersection(actions in chest_onto_belt().prop_shuffle()) { -// let mut state = GameState::new( &DATA_STORE); + // let bp = Blueprint { actions }; -// let bp = Blueprint { actions }; + // bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); + // } -// bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); + // #[test] + // fn sideload_with_items_at_source_items_reach_the_intersection(actions in chest_onto_belt().prop_shuffle()) { + // let mut state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); -// let assembler = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); + // let bp = Blueprint { actions }; -// let assembler_working = matches!(assembler, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); + // bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); -// prop_assume!(assembler_working, "{:?}", assembler); + // let assembler = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); -// let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); + // let assembler_working = matches!(assembler, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); -// let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); + // prop_assume!(assembler_working, "{:?}", assembler); -// prop_assume!(inserter_attached, "{:?}", ent); + // let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); -// for _ in 0..200 { -// state.update(&DATA_STORE); -// } + // let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); -// let Some(Entity::Belt { id, .. }) = state.world.get_entity_at(Position { x: 1602, y: 1601 }, &DATA_STORE) else { -// unreachable!() -// }; + // prop_assume!(inserter_attached, "{:?}", ent); -// let items_at_intersection = state.simulation_state.factory.belts.get_item_iter(*id).into_iter().next().expect(&format!("{:?}", state.simulation_state.factory.belts.get_item_iter(*id).into_iter().collect::>())).is_some(); + // for _ in 0..200 { + // state.update(&DATA_STORE); + // } -// prop_assert!(state.statistics.production.total.unwrap().items_produced.iter().copied().sum::() > 0); + // let Some(Entity::Belt { id, .. }) = state.world.get_entity_at(Position { x: 1602, y: 1601 }, &DATA_STORE) else { + // unreachable!() + // }; -// prop_assert!(items_at_intersection, "{:?}, \n{:?}", state.simulation_state.factory.belts, state.simulation_state.factory.belts.get_item_iter(*id).into_iter().collect::>()); -// } + // let items_at_intersection = state.simulation_state.factory.belts.get_item_iter(*id).into_iter().next().expect(&format!("{:?}", state.simulation_state.factory.belts.get_item_iter(*id).into_iter().collect::>())).is_some(); -// #[test] -// fn sideload_with_items_at_source_items_actually_reach(actions in sideload_items().prop_shuffle()) { -// let mut state = GameState::new(&DATA_STORE); + // prop_assert!(state.statistics.production.total.unwrap().items_produced.iter().copied().sum::() > 0); -// let bp = Blueprint { actions }; + // prop_assert!(items_at_intersection, "{:?}, \n{:?}", state.simulation_state.factory.belts, state.simulation_state.factory.belts.get_item_iter(*id).into_iter().collect::>()); + // } -// bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); + // #[test] + // fn sideload_with_items_at_source_items_actually_reach(actions in sideload_items().prop_shuffle()) { + // let mut state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); -// let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); + // let bp = Blueprint { actions }; -// let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); + // bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); -// prop_assume!(assembler_powered); + // let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); -// let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); + // let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); -// prop_assume!(assembler_working, "{:?}", ent); + // prop_assume!(assembler_powered); -// let inserter_attached = matches!(state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(), Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); + // let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); -// prop_assume!(inserter_attached); + // prop_assume!(assembler_working, "{:?}", ent); -// for _ in 0..2000 { -// state.update(&DATA_STORE); -// } + // let inserter_attached = matches!(state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(), Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); -// let Some(Entity::Belt { id: id_going_right, .. }) = state.world.get_entity_at(Position { x: 1602, y: 1601 }, &DATA_STORE) else { -// unreachable!() -// }; + // prop_assume!(inserter_attached); -// let Some(Entity::Belt { id: id_going_down, .. }) = state.world.get_entity_at(Position { x: 1604, y: 1602 }, &DATA_STORE) else { -// unreachable!() -// }; + // for _ in 0..2000 { + // state.update(&DATA_STORE); + // } -// let produced = state.statistics.production.total.unwrap().items_produced.iter().copied().sum::(); + // let Some(Entity::Belt { id: id_going_right, .. }) = state.world.get_entity_at(Position { x: 1602, y: 1601 }, &DATA_STORE) else { + // unreachable!() + // }; -// prop_assume!(produced > 0, "{:?}", produced); + // let Some(Entity::Belt { id: id_going_down, .. }) = state.world.get_entity_at(Position { x: 1604, y: 1602 }, &DATA_STORE) else { + // unreachable!() + // }; -// prop_assert!(dbg!(state.simulation_state.factory.belts.get_item_iter(*id_going_down).into_iter().next().unwrap()).is_some(),"down: {:?}\n, right:{:?}", state.simulation_state.factory.belts.get_item_iter(*id_going_down).into_iter().collect::>(), state.simulation_state.factory.belts.get_item_iter(*id_going_right).into_iter().collect::>()); -// } + // let produced = state.statistics.production.total.unwrap().items_produced.iter().copied().sum::(); -// } -// } + // prop_assume!(produced > 0, "{:?}", produced); + + // prop_assert!(dbg!(state.simulation_state.factory.belts.get_item_iter(*id_going_down).into_iter().next().unwrap()).is_some(),"down: {:?}\n, right:{:?}", state.simulation_state.factory.belts.get_item_iter(*id_going_down).into_iter().collect::>(), state.simulation_state.factory.belts.get_item_iter(*id_going_right).into_iter().collect::>()); + // } + + // } +} diff --git a/src/frontend/world/mod.rs b/src/frontend/world/mod.rs index 00e9f8e..4760ce3 100644 --- a/src/frontend/world/mod.rs +++ b/src/frontend/world/mod.rs @@ -9,7 +9,17 @@ pub mod tile; // TODO: Do not use usize for anything that might go to another machine, where it could be different size! #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive( - Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, + Debug, + Clone, + Copy, + Default, + serde::Serialize, + serde::Deserialize, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, )] pub struct Position { pub x: i32, diff --git a/src/power/power_grid.rs b/src/power/power_grid.rs index 1d70bd3..8940548 100644 --- a/src/power/power_grid.rs +++ b/src/power/power_grid.rs @@ -115,7 +115,6 @@ pub struct PowerGrid { Network)>, steam_power_producers: SteamPowerProducerStore, - // TODO: Currently there can only be a single type of solar panel and accumulator pub num_solar_panels_of_type: Box<[u64]>, pub main_accumulator_count: Box<[u64]>, pub main_accumulator_charge: Box<[Joule]>, From d00cecf398bd9ee33d31c89e919c7e140f050ba8 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 17 Feb 2026 19:22:23 +0100 Subject: [PATCH 139/152] Add debug option --- src/rendering/render_world.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 323932c..6d9409b 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -2949,6 +2949,15 @@ pub fn render_ui< } else { 'P' }))); + if ui.button("Find additional Grid").clicked() { + let id = game_state_ref.simulation_state.factory.power_grids.power_grids.iter().enumerate().filter(|(_id, grid)| !grid.is_placeholder).nth(1); + + if let Some((id, _grid)) = id { + let pole_pos = game_state_ref.simulation_state.factory.power_grids.pole_pos_to_grid_id.iter().find(|(k, v)| **v as usize == id).unwrap().0; + + state_machine_ref.local_player_pos = (pole_pos.x as f32, pole_pos.y as f32); + } + } #[cfg(feature = "debug-stat-gathering")] From b2b6c4347173097adb14878359e072c989b9c46a Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 17 Feb 2026 19:22:46 +0100 Subject: [PATCH 140/152] Improve lag of copy paste entire megabase --- src/blueprint/mod.rs | 2 +- src/frontend/action/action_state_machine.rs | 7 +++++-- src/multiplayer/server.rs | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/blueprint/mod.rs b/src/blueprint/mod.rs index 0d2feab..ba4ab7e 100644 --- a/src/blueprint/mod.rs +++ b/src/blueprint/mod.rs @@ -946,7 +946,7 @@ impl Blueprint { pub fn optimize(&mut self) { info!("Optimizing Blueprint"); - self.actions.par_sort_unstable_by_key(|v| match v { + self.actions.par_sort_by_key(|v| match v { BlueprintAction::PlaceEntity(e) => match e { BlueprintPlaceEntity::Assembler { pos, .. } => { (1, 3, (BeltId::Pure(0), 0), *pos, 0) diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index 99d6553..46382d9 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -471,7 +471,7 @@ impl match held_object { HeldObject::Blueprint(bp) => { - bp.get_reusable(force, data_store).actions_with_base_pos(Self::player_mouse_to_tile( + bp.get_reusable(force, data_store).optimize().actions_with_base_pos(Self::player_mouse_to_tile( self.zoom_level, self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos, @@ -635,7 +635,10 @@ impl let x_range = min(start_pos.x, end_pos.x)..(max(start_pos.x, end_pos.x) + 1); let y_range = min(start_pos.y, end_pos.y)..(max(start_pos.y, end_pos.y) + 1); - self.state = ActionStateMachineState::Holding(HeldObject::Blueprint(Blueprint::from_area(world, sim_state, [x_range, y_range], data_store))); + let mut bp = Blueprint::from_area(world, sim_state, [x_range, y_range], data_store); + bp.optimize(); + + self.state = ActionStateMachineState::Holding(HeldObject::Blueprint(bp)); vec![] }, ActionStateMachineState::DeleteDragInProgress { start_pos } => { diff --git a/src/multiplayer/server.rs b/src/multiplayer/server.rs index 963cf9a..f8a21ce 100644 --- a/src/multiplayer/server.rs +++ b/src/multiplayer/server.rs @@ -106,6 +106,7 @@ impl< replay.append_actions(actions.iter().cloned(), data_store); replay.tick(); + #[cfg(debug_assertions)] { use ron::ser::PrettyConfig; From 83c19ad0c0cf03c905b52ad27a15b25b44a4b7ae Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Tue, 17 Feb 2026 21:18:57 +0100 Subject: [PATCH 141/152] Add initial Datapedia draft --- src/data/mod.rs | 1 + src/data/pedia/mod.rs | 214 ++++++++++++++++++++ src/frontend/action/action_state_machine.rs | 13 +- src/rendering/render_world.rs | 7 +- 4 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 src/data/pedia/mod.rs diff --git a/src/data/mod.rs b/src/data/mod.rs index 650d93c..412587b 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -17,6 +17,7 @@ use sha2::{Digest, Sha256}; use strum::IntoEnumIterator; pub mod factorio_1_1; +pub mod pedia; use crate::{ assembler::TIMERTYPE, diff --git a/src/data/pedia/mod.rs b/src/data/pedia/mod.rs new file mode 100644 index 0000000..0c9dfd2 --- /dev/null +++ b/src/data/pedia/mod.rs @@ -0,0 +1,214 @@ +use egui::{ScrollArea, Window}; + +use crate::{ + TICKS_PER_SECOND_LOGIC, + data::{DataStore, ItemRecipeDir}, + item::{IdxTrait, Indexable, Item, Recipe, WeakIdxTrait}, +}; + +#[derive(Debug)] +pub(crate) struct Pedia { + open_page: OpenPage, + + entry: Option>, +} + +#[derive(Debug)] +struct OpenPage { + catagory: String, +} + +#[derive(Debug)] +enum OpenEntry { + Item { item: Item }, + Recipe { recipe: Recipe }, +} + +impl Pedia { + pub(crate) fn new(_data_store: &DataStore) -> Self { + Self { + open_page: OpenPage { + catagory: "TODO".to_string(), + }, + entry: Some(OpenEntry::Item { + item: Item { id: 10.into() }, + }), + } + } + + pub(crate) fn show_window( + &mut self, + open: &mut bool, + ctx: &egui::Context, + data_store: &DataStore, + ) { + Window::new("Datapedia").open(open).show(ctx, |ui| { + ui.columns_const(|[_grid, entry_ui]| { + // Grid + + // Entry + match &mut self.entry { + Some(entry) => Self::show_entry(entry, entry_ui, data_store), + None => {}, + } + }); + }); + } + + fn show_entry( + entry: &mut OpenEntry, + ui: &mut egui::Ui, + data_store: &DataStore, + ) { + ScrollArea::vertical().show(ui, |ui| { + match entry { + OpenEntry::Item { item: item_item } => { + let item = item_item.into_usize(); + ui.heading(&data_store.item_display_names[item]); + if !data_store.item_is_fluid[item] { + ui.label(&format!( + "Stack Size: {}", + data_store.item_stack_sizes[item] + )); + } + // TODO: How do I want to find the main recipe? + let main_recipe = data_store + .recipe_names + .iter() + .position(|recipe_name| **recipe_name == *data_store.item_names[item]); + + if let Some(main_recipe) = main_recipe { + let ingredients = data_store.recipe_to_items_and_amounts[main_recipe] + .iter() + .filter(|(dir, _item, _count)| *dir == ItemRecipeDir::Ing); + let results = data_store.recipe_to_items_and_amounts[main_recipe] + .iter() + .filter(|(dir, _item, _count)| *dir == ItemRecipeDir::Out); + + let crafting_time = data_store.recipe_timers[main_recipe] as f32 + / (TICKS_PER_SECOND_LOGIC as f32); + + ui.label("Ingredients:"); + for (_, ing, count) in ingredients { + if ui + .label(&format!( + "{count}x: {}", + data_store.item_display_names[ing.into_usize()] + )) + .clicked() + { + *item_item = *ing; + } + } + ui.label(&format!("{:.2}s Crafting time", crafting_time)); + ui.label("Results:"); + for (_, result, count) in results { + if ui + .label(&format!( + "{count}x: {}", + data_store.item_display_names[result.into_usize()] + )) + .clicked() + { + *item_item = *result; + } + } + + let made_in = &data_store.recipe_allowed_assembling_machines[main_recipe]; + + ui.label("Made In:"); + for machine in made_in { + let name = &data_store.assembler_info[*machine as usize].display_name; + + if ui.label(name).clicked() { + todo!() + } + } + + ui.separator(); + } + + let mut alternative_recipes = data_store + .recipe_names + .iter() + .enumerate() + .filter(|(_id, recipe_name)| ***recipe_name != *data_store.item_names[item]) + .filter(|(id, _name)| { + data_store.recipe_to_items[*id] + .contains(&(ItemRecipeDir::Out, *item_item)) + }) + .peekable(); + + if alternative_recipes.peek().is_some() { + ui.label("Alternative Recipes:"); + for (recipe_id, name) in alternative_recipes { + if ui + .label(&data_store.recipe_display_names[recipe_id]) + .clicked() + { + if let Some(item) = data_store + .item_names + .iter() + .position(|item_name| item_name == name) + { + // Show the item + *item_item = Item { + id: ItemIdxType::try_from(item).unwrap(), + }; + return; + } else { + *entry = OpenEntry::Recipe { + recipe: Recipe { + id: RecipeIdxType::try_from(recipe_id).unwrap(), + }, + }; + return; + } + } + } + } + + let mut used_in = data_store + .recipe_names + .iter() + .enumerate() + .filter(|(id, _name)| { + data_store.recipe_to_items[*id] + .contains(&(ItemRecipeDir::Ing, *item_item)) + }) + .peekable(); + + if used_in.peek().is_some() { + ui.label("Used In:"); + for (recipe_id, name) in used_in { + if ui + .label(&data_store.recipe_display_names[recipe_id]) + .clicked() + { + if let Some(item) = data_store + .item_names + .iter() + .position(|item_name| item_name == name) + { + // Show the item + *item_item = Item { + id: ItemIdxType::try_from(item).unwrap(), + }; + return; + } else { + *entry = OpenEntry::Recipe { + recipe: Recipe { + id: RecipeIdxType::try_from(recipe_id).unwrap(), + }, + }; + return; + } + } + } + } + }, + OpenEntry::Recipe { recipe } => todo!(), + } + }); + } +} diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index 46382d9..544a946 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -18,7 +18,7 @@ use crate::{ app_state::SimulationState, belt::splitter::SplitterDistributionMode, blueprint::Blueprint, - data::{self, DataStore}, + data::{self, DataStore, pedia::Pedia}, frontend::{ action::{ place_entity::{EntityPlaceOptions, PlaceEntityInfo}, @@ -158,6 +158,8 @@ pub struct ActionStateMachine, + + pub datapedia: Pedia, } #[derive(Debug, enum_map::Enum, PartialEq)] @@ -167,6 +169,7 @@ pub(crate) enum Window { Statistics, Hotbar, Escape, + Datapedia, } #[cfg(not(target_arch = "wasm32"))] @@ -248,7 +251,8 @@ impl local_player_pos: (f32, f32), data_store: &DataStore, ) -> Self { - let open_windows = EnumMap::from_fn(|win| win == Window::Tip); + //FIXME: REMOVE Datapedia + let open_windows = EnumMap::from_fn(|win| win == Window::Tip || win == Window::Datapedia); Self { my_player_id, @@ -294,6 +298,7 @@ impl autosave_interval: (60 * TICKS_PER_SECOND_LOGIC) as u32, open_windows, + datapedia: Pedia::new(data_store), } } @@ -305,7 +310,8 @@ impl data_store: &DataStore, ) -> Self { let player_pos = world.players[my_player_id as usize].pos; - let open_windows = EnumMap::from_fn(|win| win == Window::Tip); + //FIXME: REMOVE Datapedia + let open_windows = EnumMap::from_fn(|win| win == Window::Tip || win == Window::Datapedia); Self { my_player_id, @@ -351,6 +357,7 @@ impl autosave_interval: (60 * TICKS_PER_SECOND_LOGIC) as u32, open_windows, + datapedia: Pedia::new(data_store), } } diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 6d9409b..88618dc 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -2178,9 +2178,14 @@ pub fn render_ui< ui.label("Hold [Right Click] to deconstruct an entity."); ui.label("Press [Ctrl + C] and start dragging to copy an area into a Blueprint."); }); - state_machine_ref.open_windows[action_state_machine::Window::Tip] = open; + let mut open = state_machine_ref.open_windows[action_state_machine::Window::Datapedia]; + state_machine_ref + .datapedia + .show_window(&mut open, ctx, data_store_ref); + state_machine_ref.open_windows[action_state_machine::Window::Datapedia] = open; + Window::new("Size") .fixed_size(egui::vec2(1920f32, 1080f32)) .default_open(false) From 81f56d3acfa40db7aa9820ce9ca20102acfa9946 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 19 Feb 2026 09:50:36 +0100 Subject: [PATCH 142/152] Disable workflow --- .github/workflows/clippy.yml | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 .github/workflows/clippy.yml diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml deleted file mode 100644 index 4349927..0000000 --- a/.github/workflows/clippy.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Clippy check - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - -jobs: - clippy: - name: Clippy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - override: true - - run: rustup component add clippy - - uses: actions-rs/cargo@v1 - with: - command: clippy - toolchain: nightly - # For now I will allow unused here - args: -- -D warnings -A unused From cde4b33645c96271c5cd97e59c8314847860127d Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 19 Feb 2026 10:26:19 +0100 Subject: [PATCH 143/152] Clean up warnings --- src/app_state.rs | 35 +-- src/assembler/bucketed.rs | 2 - src/assembler/mod.rs | 1 - src/assembler/simd.rs | 6 +- src/belt/mod.rs | 129 +---------- src/belt/smart.rs | 35 ++- src/belt/sushi.rs | 9 +- src/blueprint/mod.rs | 9 +- src/chest.rs | 2 +- src/data/mod.rs | 2 +- src/data/pedia/mod.rs | 16 +- src/frontend/action/action_state_machine.rs | 50 +++- src/frontend/action/belt_placement.rs | 239 +------------------- src/frontend/world/tile.rs | 168 +++++--------- src/get_size.rs | 7 + src/lib.rs | 176 +++++--------- src/main.rs | 4 +- src/multiplayer/mod.rs | 20 +- src/multiplayer/plumbing.rs | 21 +- src/rendering/render_world.rs | 29 +-- src/replays/mod.rs | 4 +- src/research.rs | 4 +- src/saving/loading.rs | 2 +- src/test_world_harness/mod.rs | 3 +- 24 files changed, 272 insertions(+), 701 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index a186ca2..c9537b2 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -111,7 +111,7 @@ use crate::get_size::Mutex; #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct AuxillaryData { pub game_name: String, - pub gen_info: (ProgramInformation, GenerationInformation), + pub(crate) gen_info: (ProgramInformation, GenerationInformation), pub current_tick: u64, @@ -148,7 +148,7 @@ impl AuxillaryData { } #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct GameState { pub world: Mutex>, pub simulation_state: Mutex>, @@ -460,6 +460,7 @@ impl GameState, ) -> Self { + progress.push_stage(1.0, Some("Generating Chunks".to_string())); // This is the maximum thickness the player can generate while at the edge of the world // Factorio uses 20 chunks, but their chunks are twice as large in each dimension const CHUNK_THICKNESS: i32 = 40; @@ -497,6 +498,8 @@ impl GameState Factory Factory GameState GameState Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, crate::power::power_grid::PowerGridEntity::Lab { index, .. } => Storage::Lab { grid: storage_update.new_grid, index }, - crate::power::power_grid::PowerGridEntity::LazyPowerProducer { item, index } => todo!(), + crate::power::power_grid::PowerGridEntity::LazyPowerProducer { .. } => todo!(), crate::power::power_grid::PowerGridEntity::SolarPanel { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), @@ -1877,7 +1882,7 @@ impl GameState Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, crate::power::power_grid::PowerGridEntity::Lab { index, .. } => Storage::Lab { grid: storage_update.new_grid, index }, - crate::power::power_grid::PowerGridEntity::LazyPowerProducer { item, index } => todo!(), + crate::power::power_grid::PowerGridEntity::LazyPowerProducer { .. } => todo!(), crate::power::power_grid::PowerGridEntity::SolarPanel { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), @@ -1901,7 +1906,7 @@ impl GameState Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, crate::power::power_grid::PowerGridEntity::Lab { index, .. } => Storage::Lab { grid: storage_update.new_grid, index }, - crate::power::power_grid::PowerGridEntity::LazyPowerProducer { item, index } => todo!(), + crate::power::power_grid::PowerGridEntity::LazyPowerProducer { .. } => todo!(), crate::power::power_grid::PowerGridEntity::SolarPanel { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Beacon { .. } => unreachable!(), @@ -1946,8 +1951,8 @@ impl GameState Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, - crate::power::power_grid::PowerGridEntity::Lab { ty, index } => Storage::Lab { grid: storage_update.new_grid, index }, + crate::power::power_grid::PowerGridEntity::Assembler { ty: _, recipe, index } => Storage::Assembler { grid: storage_update.new_grid, recipe_idx_with_this_item: recipe.id, index }, + crate::power::power_grid::PowerGridEntity::Lab { ty: _, index } => Storage::Lab { grid: storage_update.new_grid, index }, crate::power::power_grid::PowerGridEntity::LazyPowerProducer { .. } => todo!(), crate::power::power_grid::PowerGridEntity::SolarPanel { .. } => unreachable!(), crate::power::power_grid::PowerGridEntity::Accumulator { .. } => unreachable!(), @@ -2784,8 +2789,7 @@ impl GameState { - // TODO: - // todo!(); + // TODO: Currently I insert modules into beacons on placement instead of doing this. }, _ => { warn!( @@ -3037,7 +3041,7 @@ impl GameState GameState impl Strategy>> { Just(vec![ ActionType::PlaceEntity(PlaceEntityInfo { diff --git a/src/assembler/bucketed.rs b/src/assembler/bucketed.rs index b089a7b..a710611 100644 --- a/src/assembler/bucketed.rs +++ b/src/assembler/bucketed.rs @@ -90,8 +90,6 @@ impl .try_into() .unwrap(); - let tick_till_main_done: u16 = ticks_per_main - data.timer; - let power_subticks_passed = ticks_passed; let (is_idle, main_produced, prod_produced) = Self::apply_subticks( diff --git a/src/assembler/mod.rs b/src/assembler/mod.rs index 319cc7b..5626cbd 100644 --- a/src/assembler/mod.rs +++ b/src/assembler/mod.rs @@ -2,7 +2,6 @@ use std::{array, marker::PhantomData, simd::Simd, u8}; use crate::assembler::simd::{InserterReinsertionInfo, InserterWaitList}; use crate::frontend::world::tile::ModuleTy; -use crate::inserter::storage_storage_with_buckets_indirect::InserterId; use crate::storage_list::MaxInsertionLimit; use crate::{ data::DataStore, diff --git a/src/assembler/simd.rs b/src/assembler/simd.rs index bd4c0e4..461bc24 100644 --- a/src/assembler/simd.rs +++ b/src/assembler/simd.rs @@ -2212,7 +2212,7 @@ mod test { #[bench] fn bench_multithreaded_assembler_update(bencher: &mut Bencher) { const NUM_RECIPES: usize = 12; - const NUM_ASSEMBLERS: usize = 30_000_000; + const NUM_ASSEMBLERS: usize = 1_000_000; let mut assemblers: Vec> = (0..NUM_RECIPES as u8) .map(|_| MultiAssemblerStore::new(Recipe { id: 11 }, 0, &DATA_STORE)) @@ -2287,9 +2287,5 @@ mod test { }); } }); - - dbg!(i); - dbg!(num_produced); - dbg!(assemblers[11].get_info(0, &DATA_STORE)); } } diff --git a/src/belt/mod.rs b/src/belt/mod.rs index 6b89738..ebb06d3 100644 --- a/src/belt/mod.rs +++ b/src/belt/mod.rs @@ -62,7 +62,7 @@ use smart::{BeltInserterInfo, InserterAdditionError, Side, SmartBelt, SpaceOccup use splitter::{ PureSplitter, SPLITTER_BELT_LEN, SplitterDistributionMode, SplitterSide, SushiSplitter, }; -use sushi::{SushiBelt, SushiInfo}; +use sushi::SushiBelt; #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, PartialEq, Clone, Copy, serde::Deserialize, serde::Serialize)] @@ -274,15 +274,6 @@ struct SplitterStore { impl SplitterStore {} -#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -enum AnyBeltBeltInserter { - PurePure(usize), - PureSushi(usize), - SushiPure(usize), - SushiSushi(usize), -} - #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct BeltBeltInserterStore { @@ -368,8 +359,8 @@ impl InnerBeltStore { fn get_pure_splitter_belt_ids<'a>( &'a self, - item: Item, - id: usize, + _item: Item, + _id: usize, ) -> [[AnyBelt; 2]; 2] { info!("Had to search for splitter belt ids!"); todo!() @@ -2265,7 +2256,7 @@ impl BeltStore { #[profiling::function] pub fn sushi_belt_update<'a, 'b, 'c, RecipeIdxType: IdxTrait>( &mut self, - storages_by_item: impl IndexedParallelIterator>, + _storages_by_item: impl IndexedParallelIterator>, _data_store: &DataStore, ) where 'b: 'a, @@ -2301,7 +2292,7 @@ impl BeltStore { { profiling::scope!("SushiBelt Inserter Update"); - for sushi_belt in &mut self.inner.sushi_belts { + for _sushi_belt in &mut self.inner.sushi_belts { // TODO: Update inserters! } } @@ -2354,7 +2345,7 @@ impl BeltStore { ) -> [[Option>; 2]; 2] { let items = match splitter_id { SplitterTileId::Any(index) => match self.any_splitters[index as usize] { - AnySplitter::Pure(item, id) => todo!(), + AnySplitter::Pure(_item, _id) => todo!(), AnySplitter::Sushi(id) => [ self.inner.get_sushi_splitter_inputs(id), self.inner.get_sushi_splitter_outputs(id), @@ -2835,112 +2826,10 @@ impl BeltStore { ret } - pub fn remove_sideloading_inserter(&mut self, index: usize) { + pub fn remove_sideloading_inserter(&mut self, _index: usize) { todo!() } - fn get_belt_belt_inserter_sushi_lists<'a>( - &'a self, - id: BeltTileId, - ) -> ( - impl IntoIterator> + Clone + use<'a, ItemIdxType>, - impl IntoIterator> + Clone + use<'a, ItemIdxType>, - ) { - // FIXME: Consider splitters!!!! - ( - self.inner - .belt_belt_inserters - .pure_to_pure_inserters - .iter() - .enumerate() - .map(|(i, v)| { - ( - v, - Item:: { - id: i.try_into().unwrap(), - }, - ) - }) - .map(move |(v, item)| { - v.iter().flatten().filter_map(move |ins| { - // (ins.1 .1 .0 == id).then_some(SushiInfo::Pure(Some(item))) - // FIXME: - None - }) - }) - .flatten() - .chain({ - let idx = self.belt_graph_lookup[&id]; - - let edges = self - .belt_graph - .edges_directed(*idx, petgraph::Direction::Incoming); - - edges.map(|edge| match edge.weight() { - BeltGraphConnection::BeltBeltInserter { - source_belt_pos: _, - dest_belt_pos: _, - filter, - } - | BeltGraphConnection::Connected { - filter: Some(filter), - } => SushiInfo::Pure(Some(*filter)), - BeltGraphConnection::Sideload { dest_belt_pos: _ } - | BeltGraphConnection::Connected { filter: None } => { - match self.belt_graph.node_weight(edge.source()).unwrap() { - BeltTileId::AnyBelt(index, _) => match self.any_belts - [*index as usize] - { - AnyBelt::Smart(belt_id) => SushiInfo::Pure(Some(belt_id.item)), - AnyBelt::Sushi(_) => SushiInfo::Sushi, - AnyBelt::Empty(_) => SushiInfo::Sushi, - }, - } - }, - }) - }), - self.inner - .belt_belt_inserters - .pure_to_pure_inserters - .iter() - .enumerate() - .map(|(i, v)| { - ( - v, - Item:: { - id: i.try_into().unwrap(), - }, - ) - }) - .map(move |(v, item)| { - v.iter().filter_map(move |ins| { - // (ins.1.source.0 == id).then_some(SushiInfo::Pure(Some(item))) - // FIXME: - None - }) - }) - .flatten(), // .chain( - // self.inner.belt_belt_inserters - // .sideload_inserters - // .iter() - // .filter_map(move |ins| { - // if ins.1.source.0 == id { - // match ins.1.dest.0 { - // BeltTileId::AnyBelt(index, _) => match &self.any_belts[index] { - // AnyBelt::Smart(_) => { - // unreachable!("Here a sushi belt is sideloading onto a smart belt. this MUST be impossible") - // }, - // AnyBelt::Sushi(_) => Some(SushiInfo::Sushi), - // }, - // } - // } else { - // None - // } - // }), - // ), - ) - } - pub fn get_inserter_info_at( &self, belt: BeltTileId, @@ -3155,7 +3044,7 @@ impl BeltStore { } } - pub fn remove_belt_belt_inserter(&mut self, inserter_index: u32) { + pub fn remove_belt_belt_inserter(&mut self, _inserter_index: u32) { todo!() } @@ -3521,7 +3410,7 @@ impl BeltStore { self.merge_belts(front_tile_id, back_tile_id, data_store) }, - [AnyBelt::Empty(empty_idx), AnyBelt::Sushi(sushi_idx)] => { + [AnyBelt::Empty(empty_idx), AnyBelt::Sushi(_sushi_idx)] => { let new_id = self.inner.instantiate_empty_into_sushi(*empty_idx); self.any_belts[front as usize] = AnyBelt::Sushi(new_id); diff --git a/src/belt/smart.rs b/src/belt/smart.rs index a074f59..f5e41c2 100644 --- a/src/belt/smart.rs +++ b/src/belt/smart.rs @@ -5,6 +5,10 @@ use std::{ u8, }; +use crate::inserter::{ + belt_storage_inserter_non_const_gen::DynInserterState, + belt_storage_movement_list::{BeltStorageInserterInMovement, ReinsertionLists}, +}; use crate::{ inserter::{ InserterState, belt_storage_inserter::Dir, @@ -13,13 +17,6 @@ use crate::{ item::{ITEMCOUNTTYPE, IdxTrait, Item, WeakIdxTrait}, temp_vec::VecHolder, }; -use crate::{ - inserter::{ - belt_storage_inserter_non_const_gen::DynInserterState, - belt_storage_movement_list::{BeltStorageInserterInMovement, ReinsertionLists}, - }, - temp_vec::SmallCapVec, -}; use bitvec::{ access::BitSafeUsize, bitbox, @@ -212,7 +209,7 @@ impl SmartBelt { input_splitter, output_splitter, - latest_inserter_pos_if_all_incoming: earliest_inserter_pos_if_all_incoming, + latest_inserter_pos_if_all_incoming: _earliest_inserter_pos_if_all_incoming, } = self; SushiBelt { @@ -776,7 +773,7 @@ impl SmartBelt { pub fn get_update_size(&self) -> (usize, usize, usize, usize, usize) { let free_index_search_indices = match self.first_free_index { - FreeIndex::FreeIndex(idx) => vec![], + FreeIndex::FreeIndex(_idx) => vec![], FreeIndex::OldFreeIndex(idx) => self .items() .skip(usize::from(idx)) @@ -796,6 +793,7 @@ impl SmartBelt { * std::mem::size_of::()) .div_ceil(64); + let mut old_idx = 0; let cache_lines_from_inserter_belt_lookup = self .inserters .inserters @@ -807,12 +805,13 @@ impl SmartBelt { _ => (ins.belt_pos, false), }) .fold(0usize, |old_count, (belt_pos, needs)| { - // let our_idx = belt_pos; - // (old_count - // + usize::from( - // needs && (old_idx == 0 || (old_idx - 1) / 8 / 64 != our_idx / 8 / 64), - // ),) - todo!() + let our_idx = belt_pos; + let new_count = old_count + + usize::from( + needs && (old_idx == 0 || (old_idx - 1) / 8 / 64 != our_idx / 8 / 64), + ); + old_idx = our_idx; + new_count }); let cache_lines_from_storage_lookup = self @@ -1120,7 +1119,7 @@ impl SmartBelt { input_splitter, output_splitter, - latest_inserter_pos_if_all_incoming: earliest_inserter_pos_if_all_incoming, + latest_inserter_pos_if_all_incoming: _earliest_inserter_pos_if_all_incoming, } = self; match side { @@ -1200,7 +1199,7 @@ impl SmartBelt { new } - pub fn break_belt_at(&mut self, belt_pos_to_break_at: u16) -> Option { + pub fn break_belt_at(&mut self, _belt_pos_to_break_at: u16) -> Option { todo!() // // TODO: Is this correct // if self.is_circular { @@ -1725,7 +1724,7 @@ impl Belt for SmartBelt { FreeIndex::OldFreeIndex(_) => {}, } } - let (old_free, need_to_check) = match self.first_free_index { + let (_old_free, need_to_check) = match self.first_free_index { FreeIndex::FreeIndex(idx) => (idx, false), FreeIndex::OldFreeIndex(idx) => (idx, true), }; diff --git a/src/belt/sushi.rs b/src/belt/sushi.rs index f8b6252..255dbb0 100644 --- a/src/belt/sushi.rs +++ b/src/belt/sushi.rs @@ -206,6 +206,7 @@ impl SushiBelt { ins.into_boxed_slice() }); let removed = removed.unwrap(); + todo!("{:?}", removed); } pub(super) fn check_sushi( @@ -377,7 +378,7 @@ impl SushiBelt { self.output_splitter.take() } - pub fn break_belt_at(&mut self, belt_pos_to_break_at: u16) -> Option { + pub fn break_belt_at(&mut self, _belt_pos_to_break_at: u16) -> Option { todo!() // // TODO: Is this correct // if self.is_circular { @@ -913,8 +914,8 @@ impl Belt for SushiBelt { fn remove_length( &mut self, - amount: BeltLenType, - side: Side, + _amount: BeltLenType, + _side: Side, ) -> (Vec<(Item, u32)>, BeltLenType) { todo!() // if amount == 0 { @@ -986,7 +987,7 @@ impl Belt for SushiBelt { // ) } - fn add_length(&mut self, amount: BeltLenType, side: super::smart::Side) -> BeltLenType { + fn add_length(&mut self, _amount: BeltLenType, _side: super::smart::Side) -> BeltLenType { todo!() // let len = self.get_len(); diff --git a/src/blueprint/mod.rs b/src/blueprint/mod.rs index ba4ab7e..b6a9bfd 100644 --- a/src/blueprint/mod.rs +++ b/src/blueprint/mod.rs @@ -32,7 +32,6 @@ use crate::{ }, }, item::{IdxTrait, Item, Recipe}, - replays::Replay, }; pub mod blueprint_string; @@ -40,7 +39,7 @@ pub mod blueprint_string; // For now blueprint will just be a list of actions #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Blueprint { - pub actions: Vec, + pub(crate) actions: Vec, } #[derive(Debug, Clone)] @@ -49,7 +48,7 @@ pub struct ReusableBlueprint Arc { #[derive( Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize, )] -enum BeltId { +pub(crate) enum BeltId { Sushi(usize), Pure(usize), } @@ -936,7 +935,7 @@ impl Blueprint { self.actions.len() } - pub fn extract_if(&mut self, filter: impl Fn(&BlueprintAction) -> bool) -> Self { + pub(crate) fn extract_if(&mut self, filter: impl Fn(&BlueprintAction) -> bool) -> Self { let extracted = self.actions.extract_if(.., |action| (filter)(action)); Self { diff --git a/src/chest.rs b/src/chest.rs index 082aab0..eb196a0 100644 --- a/src/chest.rs +++ b/src/chest.rs @@ -161,7 +161,7 @@ pub struct MultiChestStore { } #[derive(Debug, Clone, Copy)] -pub(crate) enum WaitingInserterRemovalInfo { +pub enum WaitingInserterRemovalInfo { StorageStorage { inserter_id: InserterId }, BeltStorage { belt_id: u32, belt_pos: BeltLenType }, } diff --git a/src/data/mod.rs b/src/data/mod.rs index 412587b..6295487 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -738,7 +738,7 @@ pub struct FluidConnection { } impl FluidConnection { - pub fn with_fluid_tank_rotation(self, rotation: Dir) -> Self { + pub fn with_fluid_tank_rotation(self, _rotation: Dir) -> Self { todo!() } } diff --git a/src/data/pedia/mod.rs b/src/data/pedia/mod.rs index 0c9dfd2..3b70fe1 100644 --- a/src/data/pedia/mod.rs +++ b/src/data/pedia/mod.rs @@ -45,6 +45,12 @@ impl Pedia self.entry = Some(new_entry), + None => {}, + } // Entry match &mut self.entry { @@ -55,6 +61,14 @@ impl Pedia, + ) -> Option> { + // TODO: + None + } + fn show_entry( entry: &mut OpenEntry, ui: &mut egui::Ui, @@ -207,7 +221,7 @@ impl Pedia todo!(), + OpenEntry::Recipe { recipe } => todo!("Show entry for recipe {recipe:?}"), } }); } diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index 544a946..ce06a10 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -298,7 +298,7 @@ impl autosave_interval: (60 * TICKS_PER_SECOND_LOGIC) as u32, open_windows, - datapedia: Pedia::new(data_store), + datapedia: Pedia::new(data_store), } } @@ -357,7 +357,7 @@ impl autosave_interval: (60 * TICKS_PER_SECOND_LOGIC) as u32, open_windows, - datapedia: Pedia::new(data_store), + datapedia: Pedia::new(data_store), } } @@ -673,7 +673,7 @@ impl }, Input::MouseMove(x, y) => { self.current_mouse_pos = (x, y); - + match &mut self.state { ActionStateMachineState::CtrlCPressed | ActionStateMachineState::CopyDragInProgress { start_pos: _ } => {}, @@ -814,8 +814,24 @@ impl }, HeldObject::OrePlacement { .. } => {}, }, - ActionStateMachineState::Deconstructing(position, timer) => { - //todo!("Check if we are still over the same thing") + ActionStateMachineState::Deconstructing(position, _timer) => { + if let Some(e) = world.get_entity_at(*position, data_store) { + let e_pos = e.get_pos(); + let e_size = e.get_entity_size(data_store); + let mouse_pos = Self::player_mouse_to_tile(self.zoom_level, self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos); + + if mouse_pos.contained_in(e_pos, e_size) { + // We are still deconstructing. Continue + } else { + // The mouse is no longer over the entity + self.state = ActionStateMachineState::Idle; + } + + } else { + // The entity is gone + self.state = ActionStateMachineState::Idle; + } + }, } @@ -1015,7 +1031,7 @@ impl }, )); }, - Some(Entity::Roboport { ty, .. }) => todo!(), + Some(Entity::Roboport { .. }) => todo!(), Some(Entity::SolarPanel { pos: _, ty, @@ -1500,8 +1516,23 @@ impl }, HeldObject::OrePlacement { .. } => {}, }, - ActionStateMachineState::Deconstructing(position, timer) => { - //todo!("Check if we are still over the same thing") + ActionStateMachineState::Deconstructing(position, _timer) => { + if let Some(e) = world.get_entity_at(*position, data_store) { + let e_pos = e.get_pos(); + let e_size = e.get_entity_size(data_store); + let mouse_pos = Self::player_mouse_to_tile(self.zoom_level, self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos); + + if mouse_pos.contained_in(e_pos, e_size) { + // We are still deconstructing. Continue + } else { + // The mouse is no longer over the entity + self.state = ActionStateMachineState::Idle; + } + + } else { + // The entity is gone + self.state = ActionStateMachineState::Idle; + } }, } @@ -1784,14 +1815,11 @@ impl PlaceEntityType::MiningDrill { ty, .. } => Cow::Borrowed( &*data_store.mining_drill_info[ty as usize].display_name, ), - - _ => unreachable!(), }, _ => unreachable!(), }) .unwrap_or(Cow::Borrowed("None")); - let slot_id = egui::Id::new(("hotbar_slot", slot_idx)); let response = ui.label(&format!("{}", text)); if let Some(new) = response.dnd_release_payload::>() { diff --git a/src/frontend/action/belt_placement.rs b/src/frontend/action/belt_placement.rs index eda8dc3..287b57a 100644 --- a/src/frontend/action/belt_placement.rs +++ b/src/frontend/action/belt_placement.rs @@ -1880,7 +1880,7 @@ pub fn expected_belt_state( mod test { use crate::{ frontend::{ - action::action_state_machine::{ActionStateMachineState, HeldObject}, + action::action_state_machine::HeldObject, world::{ Position, tile::{Dir, PlaceEntityType, UndergroundDir}, @@ -1951,241 +1951,4 @@ mod test { test.assert_tile_occupied((0, 0)); test.assert_tile_occupied((5, 0)); } - - // use proptest::prelude::{Just, Strategy}; - // use proptest::{prop_assert, prop_assume, proptest}; - - // use crate::DATA_STORE; - // use crate::app_state::GameState; - // use crate::blueprint::Blueprint; - // use crate::frontend::action::ActionType; - // use crate::frontend::action::set_recipe::SetRecipeInfo; - // use crate::frontend::world::Position; - // use crate::frontend::world::tile::{AssemblerInfo, Dir, Entity, InserterInfo, PlaceEntityType}; - // use crate::item::Recipe; - - // fn chest_onto_belt() -> impl Strategy>> { - // Just(vec![ - // place(PlaceEntityType::Assembler { - // pos: Position { x: 0, y: 3 }, - // ty: 0, - // rotation: Dir::North, - // }), - // ActionType::SetRecipe(SetRecipeInfo { - // pos: Position { x: 0, y: 3 }, - // recipe: Recipe { id: 0 }, - // }), - // place(PlaceEntityType::Inserter { - // pos: Position { x: 2, y: 2 }, - // dir: crate::frontend::world::tile::Dir::North, - // filter: None, - // ty: 0, - // user_movetime: None, - // }), - // place(PlaceEntityType::Belt { - // pos: Position { x: 2, y: 1 }, - // direction: crate::frontend::world::tile::Dir::East, - // ty: 0, - // }), - // place(PlaceEntityType::PowerPole { - // pos: Position { x: 0, y: 2 }, - // ty: 0, - // }), - // place(PlaceEntityType::PowerPole { - // pos: Position { x: 5, y: 0 }, - // ty: 0, - // }), - // place(PlaceEntityType::SolarPanel { - // pos: Position { x: 6, y: 0 }, - // ty: 0, - // }), - // ]) - // } - - // fn belts_into_sideload() -> impl Strategy>> { - // Just(vec![ - // place(PlaceEntityType::Belt { - // pos: Position { x: 3, y: 1 }, - // direction: crate::frontend::world::tile::Dir::East, - // ty: 0, - // }), - // place(PlaceEntityType::Belt { - // pos: Position { x: 4, y: 0 }, - // direction: crate::frontend::world::tile::Dir::South, - // ty: 0, - // }), - // place(PlaceEntityType::Belt { - // pos: Position { x: 4, y: 1 }, - // direction: crate::frontend::world::tile::Dir::South, - // ty: 0, - // }), - // place(PlaceEntityType::Belt { - // pos: Position { x: 4, y: 2 }, - // direction: crate::frontend::world::tile::Dir::South, - // ty: 0, - // }), - // ]) - // } - - // fn sideload_items() -> impl Strategy>> { - // (chest_onto_belt(), belts_into_sideload()).prop_map(|(mut a, b)| { - // a.extend(b.into_iter()); - // a - // }) - // } - - // fn place(ty: PlaceEntityType) -> ActionType { - // ActionType::PlaceEntity(crate::frontend::action::place_entity::PlaceEntityInfo { - // entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single(ty), - // force: false, - // }) - // } - - // proptest! { - - // #[test] - // fn inserter_always_attaches(actions in chest_onto_belt().prop_shuffle()) { - // let mut state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); - - // let bp = Blueprint { actions }; - - // bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - - // let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); - - // let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); - - // prop_assert!(assembler_powered); - - // let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); - - // prop_assume!(assembler_working, "{:?}", ent); - - // let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); - - // let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); - - // prop_assert!(inserter_attached, "{:?}", ent); - // } - - // #[test] - // fn inserter_always_attaches_full_bp(actions in sideload_items().prop_shuffle()) { - // let mut state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); - - // let bp = Blueprint { actions }; - - // bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - - // let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); - - // let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); - - // prop_assert!(assembler_powered); - - // let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); - - // prop_assume!(assembler_working, "{:?}", ent); - - // let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); - - // let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); - - // prop_assert!(inserter_attached, "{:?}", ent); - // } - - // #[test] - // fn sideload_empty_does_not_crash(actions in belts_into_sideload().prop_shuffle()) { - // let mut state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); - - // let bp = Blueprint { actions }; - - // bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - // } - - // #[test] - // fn sideload_with_items_at_source_does_not_crash(actions in sideload_items().prop_shuffle()) { - // let mut state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); - - // let bp = Blueprint { actions }; - - // bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - // } - - // #[test] - // fn sideload_with_items_at_source_items_reach_the_intersection(actions in chest_onto_belt().prop_shuffle()) { - // let mut state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); - - // let bp = Blueprint { actions }; - - // bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - - // let assembler = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); - - // let assembler_working = matches!(assembler, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); - - // prop_assume!(assembler_working, "{:?}", assembler); - - // let ent = state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(); - - // let inserter_attached = matches!(ent, Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); - - // prop_assume!(inserter_attached, "{:?}", ent); - - // for _ in 0..200 { - // state.update(&DATA_STORE); - // } - - // let Some(Entity::Belt { id, .. }) = state.world.get_entity_at(Position { x: 1602, y: 1601 }, &DATA_STORE) else { - // unreachable!() - // }; - - // let items_at_intersection = state.simulation_state.factory.belts.get_item_iter(*id).into_iter().next().expect(&format!("{:?}", state.simulation_state.factory.belts.get_item_iter(*id).into_iter().collect::>())).is_some(); - - // prop_assert!(state.statistics.production.total.unwrap().items_produced.iter().copied().sum::() > 0); - - // prop_assert!(items_at_intersection, "{:?}, \n{:?}", state.simulation_state.factory.belts, state.simulation_state.factory.belts.get_item_iter(*id).into_iter().collect::>()); - // } - - // #[test] - // fn sideload_with_items_at_source_items_actually_reach(actions in sideload_items().prop_shuffle()) { - // let mut state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); - - // let bp = Blueprint { actions }; - - // bp.apply(false, Position { x: 1600, y: 1600 }, &mut state, &DATA_STORE); - - // let ent = state.world.get_entity_at(Position { x: 1600, y: 1603 }, &DATA_STORE).unwrap(); - - // let assembler_powered = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. } | AssemblerInfo::PoweredNoRecipe { .. }, .. }); - - // prop_assume!(assembler_powered); - - // let assembler_working = matches!(ent, Entity::Assembler { info: AssemblerInfo::Powered { .. }, .. }); - - // prop_assume!(assembler_working, "{:?}", ent); - - // let inserter_attached = matches!(state.world.get_entity_at(Position { x: 1602, y: 1602 }, &DATA_STORE).unwrap(), Entity::Inserter { info: InserterInfo::Attached { .. }, .. }); - - // prop_assume!(inserter_attached); - - // for _ in 0..2000 { - // state.update(&DATA_STORE); - // } - - // let Some(Entity::Belt { id: id_going_right, .. }) = state.world.get_entity_at(Position { x: 1602, y: 1601 }, &DATA_STORE) else { - // unreachable!() - // }; - - // let Some(Entity::Belt { id: id_going_down, .. }) = state.world.get_entity_at(Position { x: 1604, y: 1602 }, &DATA_STORE) else { - // unreachable!() - // }; - - // let produced = state.statistics.production.total.unwrap().items_produced.iter().copied().sum::(); - - // prop_assume!(produced > 0, "{:?}", produced); - - // prop_assert!(dbg!(state.simulation_state.factory.belts.get_item_iter(*id_going_down).into_iter().next().unwrap()).is_some(),"down: {:?}\n, right:{:?}", state.simulation_state.factory.belts.get_item_iter(*id_going_down).into_iter().collect::>(), state.simulation_state.factory.belts.get_item_iter(*id_going_right).into_iter().collect::>()); - // } - - // } } diff --git a/src/frontend/world/tile.rs b/src/frontend/world/tile.rs index 01109db..94527b2 100644 --- a/src/frontend/world/tile.rs +++ b/src/frontend/world/tile.rs @@ -78,7 +78,6 @@ use serde::Serializer; use std::iter; use noise::Seedable; -use petgraph::prelude::Bfs; use super::Position; use crate::liquid::FluidSystemId; @@ -129,10 +128,6 @@ enum FloorOre { }, } -fn is_default(val: &T) -> bool { - *val == T::default() -} - #[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct PlayerInfo { @@ -176,7 +171,7 @@ pub struct World { // TODO: I don´t think I want FP pub players: Vec, - pub chunks: DynamicGrid>, + pub(crate) chunks: DynamicGrid>, belt_lookup: BeltIdLookup, belt_recieving_input_directions: HashMap>, @@ -263,11 +258,9 @@ struct BeltIdLookup { } #[derive(Debug, Clone, Copy)] -pub struct ChunkMissingError; - -#[derive(Debug, Clone, Copy)] -enum AddEntityError { - ChunkMissingError(ChunkMissingError), +pub(crate) enum AddEntityError { + ChunkMissingError {}, + CannotFitError {}, } struct CascadingUpdate { @@ -285,7 +278,7 @@ fn try_attaching_fluids( new_assembler_pos: Position, ) -> CascadingUpdate { CascadingUpdate { - update: Box::new(move |world, sim_state, updates, data_store| { + update: Box::new(move |world, sim_state, _updates, data_store| { profiling::scope!("try_attaching_fluids"); let Some( e @ Entity::Assembler { @@ -454,7 +447,7 @@ fn try_instantiating_inserters_for_belt_cascade, ) -> CascadingUpdate { CascadingUpdate { - update: Box::new(move |_world, sim_state, updates, data_store| { + update: Box::new(move |_world, sim_state, updates, _data_store| { profiling::scope!("try_instantiating_inserters_for_belt_cascade"); let reachable = &mut *sim_state.factory.belts.belt_graph_bfs; @@ -877,9 +870,7 @@ fn instantiate_mining_drill_internal_inserter { unreachable!("We are bypassing this check"); }, - Err(InstantiateInserterError::PleaseSpecifyFilter { - belts_which_could_help, - }) => { + Err(InstantiateInserterError::PleaseSpecifyFilter { .. }) => { unreachable!("We are specifying a filter"); }, Err(e) => { @@ -1042,13 +1033,7 @@ fn new_power_pole( } => { *info = AssemblerInfo::PoweredNoRecipe(pole_pos); }, - Entity::Roboport { - ty, - pos, - power_grid, - network, - id, - } => todo!(), + Entity::Roboport { .. } => todo!(), Entity::SolarPanel { pos, ty, @@ -1345,7 +1330,7 @@ fn removal_of_possible_inserter_connection { match sim_state.factory.belts.remove_inserter(*id, *belt_pos) { Ok(()) => {}, - Err((belt_id, belt_pos)) => todo!(), + Err((_belt_id, _belt_pos)) => todo!(), } *info = InserterInfo::NotAttached {}; @@ -1395,7 +1380,7 @@ fn removal_of_possible_inserter_connection { match internal_inserter { @@ -1417,7 +1402,7 @@ fn removal_of_possible_inserter_connection World World + Clone) { self.chunks.insert_many( chunks.clone(), - chunks.into_iter().map(|pos| Chunk { + chunks.into_iter().map(|_pos| Chunk { // base_pos: (pos.0 * i32::from(CHUNK_SIZE), pos.1 * i32::from(CHUNK_SIZE)), floor_tiles: None, chunk_tile_to_entity_into: None, @@ -1732,8 +1714,8 @@ impl World Option<(Item, u32)> { None // let v = noise.get([ @@ -2216,14 +2198,14 @@ impl World, sim_state: &mut SimulationState, data_store: &DataStore, - ) -> Result<(), ()> { + ) -> Result<(), AddEntityError> { if !self.can_fit( entity.get_pos(), entity.get_entity_size(data_store), data_store, ) { warn!("Tried to place entity where it does not fit"); - return Err(()); + return Err(AddEntityError::CannotFitError {}); } profiling::scope!("add_entity {}", entity.get_type_name()); @@ -2234,27 +2216,21 @@ impl World { + Entity::Lab { pos, .. } => { cascading_updates.push(new_lab_cascade(pos, data_store)); }, - Entity::SolarPanel { pole_position, .. } => {}, - Entity::Accumulator { pole_position, .. } => {}, + Entity::SolarPanel { .. } => {}, + Entity::Accumulator { .. } => {}, Entity::Assembler { info, .. } => match info { AssemblerInfo::UnpoweredNoRecipe | AssemblerInfo::Unpowered(_) => {}, - AssemblerInfo::PoweredNoRecipe(pole_position) => {}, - AssemblerInfo::Powered { - id: AssemblerID { grid, .. }, - pole_position, - .. - } => { + AssemblerInfo::PoweredNoRecipe(_) => {}, + AssemblerInfo::Powered { .. } => { cascading_updates.push(newly_working_assembler(pos, data_store)); }, }, @@ -2999,11 +2975,11 @@ impl World InserterInstantiationNewOptions::Belts(vec![start_belt_id, dest_belt_id]), ( - InserterConnection::Belt(start_belt_id, start_belt_pos), + InserterConnection::Belt(start_belt_id, _start_belt_pos), InserterConnection::Storage(dest_storage_untranslated), ) => { let dest_storage_untranslated = match dest_storage_untranslated { @@ -5153,7 +5129,7 @@ impl Chunk, + _data_store: &DataStore, ) -> Option<&Entity> { let x = usize::try_from(pos.x.rem_euclid(i32::from(CHUNK_SIZE))).unwrap(); let y = usize::try_from(pos.y.rem_euclid(i32::from(CHUNK_SIZE))).unwrap(); @@ -5180,7 +5156,7 @@ impl Chunk, + _data_store: &DataStore, ) -> Option<&mut Entity> { let x = usize::try_from(pos.x.rem_euclid(i32::from(CHUNK_SIZE))).unwrap(); let y = usize::try_from(pos.y.rem_euclid(i32::from(CHUNK_SIZE))).unwrap(); @@ -5326,13 +5302,6 @@ pub enum UndergroundDir { Exit, } -#[cfg_attr(feature = "client", derive(ShowInfo), derive(GetSize))] -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] -struct PipeConnection { - pipe_pos: Position, - connection_weak_index: WeakIndex, -} - pub type ModuleTy = u8; #[derive(Debug, Clone, PartialEq)] pub struct ModuleSlots(pub thin_dst::ThinBox<(), Option>); @@ -5584,7 +5553,7 @@ impl Entity) -> Color32 { + pub fn get_map_color(&self, _data_store: &DataStore) -> Color32 { match self { Self::Assembler { .. } => hex_color!("#0086c9"), Self::PowerPole { .. } => hex_color!("#eeee29"), @@ -5724,12 +5693,6 @@ pub enum PlaceEntityType { }, } -impl PlaceEntityType { - pub fn order_for_optimization(&self, other: &Self) -> std::cmp::Ordering { - todo!() - } -} - impl PlaceEntityType { pub fn cares_about_power(&self) -> bool { match self { @@ -5862,55 +5825,46 @@ impl Add for Position { #[cfg(test)] mod test { - use proptest::proptest; - - // use crate::{ - // DATA_STORE, - // app_state::GameState, - // blueprint::{ - // Blueprint, - // test::{random_entity_to_place, random_position}, - // }, - // frontend::{ - // action::{ActionType, place_entity::PlaceEntityInfo}, - // world::Position, - // }, - // replays::Replay, - // }; + use proptest::{prop_assert, proptest}; - proptest! { - - // #[test] - // fn test_get_entity(position in random_position(), ent in random_entity_to_place(&DATA_STORE)) { - // let mut state = GameState::new(&DATA_STORE); - - // let mut rep = Replay::new(&state, None, &*DATA_STORE); + use crate::{ + DATA_STORE, + app_state::GameState, + blueprint::test::{random_entity_to_place, random_position}, + frontend::{ + action::{ActionType, place_entity::PlaceEntityInfo}, + world::Position, + }, + replays::GenerationInformation, + }; - // rep.append_actions([ActionType::PlaceEntity(PlaceEntityInfo { force: false, entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single(ent) })]); + proptest! { - // let bp = Blueprint::from_replay(&rep); + #[test] + fn test_get_entity(position in random_position(), ent in random_entity_to_place(&DATA_STORE)) { + let state = GameState::new("TEST_GAMESTATE".to_string(), GenerationInformation::default(), &DATA_STORE); - // bp.apply(false, position, &mut state, &DATA_STORE); + GameState::apply_actions(&mut *state.simulation_state.lock(), &mut *state.world.lock(), [ActionType::PlaceEntity(PlaceEntityInfo { force: false, entities: crate::frontend::action::place_entity::EntityPlaceOptions::Single(ent) })], &DATA_STORE); - // let mut e_pos = None; - // let mut e_size = None; - // state.world.get_entities_colliding_with(position, (100, 100), &DATA_STORE).into_iter().for_each(|v| { - // e_pos = Some(v.get_pos()); - // e_size = Some(v.get_entity_size(&DATA_STORE)); - // }); + let mut e_pos = None; + let mut e_size = None; + state.world.lock().get_entities_colliding_with(position, (100, 100), &DATA_STORE).into_iter().for_each(|v| { + e_pos = Some(v.get_pos()); + e_size = Some(v.get_entity_size(&DATA_STORE)); + }); - // prop_assert!(e_pos.is_some()); - // prop_assert!(e_size.is_some()); + prop_assert!(e_pos.is_some()); + prop_assert!(e_size.is_some()); - // let e_pos = e_pos.unwrap(); - // let e_size = e_size.unwrap(); + let e_pos = e_pos.unwrap(); + let e_size = e_size.unwrap(); - // for x_pos in e_pos.x..(e_pos.x + (e_size.0 as i32)) { - // for y_pos in e_pos.y..(e_pos.y + (e_size.1 as i32)) { - // prop_assert_eq!(state.world.get_entities_colliding_with(Position { x: x_pos, y: y_pos }, (1, 1), &DATA_STORE).into_iter().count(), 1, "test_pos = {:?}, world + {:?}", Position {x: x_pos, y: y_pos}, state.world.get_chunk_for_tile(position)); - // } - // } - // } + for x_pos in e_pos.x..(e_pos.x + (e_size.0 as i32)) { + for y_pos in e_pos.y..(e_pos.y + (e_size.1 as i32)) { + prop_assert!(state.world.lock().get_entity_at(Position { x: x_pos, y: y_pos }, &DATA_STORE).is_some(), "test_pos = {:?}, world + {:?}", Position {x: x_pos, y: y_pos}, state.world.lock().get_chunk_for_tile(position)); + } + } + } } } diff --git a/src/get_size.rs b/src/get_size.rs index af3e20f..6da7134 100644 --- a/src/get_size.rs +++ b/src/get_size.rs @@ -331,6 +331,13 @@ impl Mutex { } } +// NOTE (Tim): This impl is not ideal since if it is derived, it might not respect the locking order of things +impl Clone for Mutex { + fn clone(&self) -> Self { + Self::new(self.lock().clone()) + } +} + impl Deref for Mutex { type Target = parking_lot::Mutex; fn deref(&self) -> &Self::Target { diff --git a/src/lib.rs b/src/lib.rs index de6c050..d235a93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,10 +6,11 @@ #![feature(int_roundings)] #![feature(vec_push_within_capacity)] #![feature(iterator_try_collect)] +// the vec recycle crate will collide with Vec::recycle at some point. Once that happens I want to switch over to std anyway +#![allow(unstable_name_collisions)] extern crate test; -// FIXME: I believe this does not work in nix packages, since build scripts do not work, in order to keep the build reproducable // See https://docs.rs/built/latest/built/ pub mod built_info { // The file has been placed there by the build script. @@ -39,7 +40,7 @@ use data::{DataStore, factorio_1_1::get_raw_data_test}; #[cfg(feature = "client")] #[cfg(not(target_arch = "wasm32"))] use eframe::NativeOptions; -use frontend::world::{Position, tile::CHUNK_SIZE_FLOAT}; +use frontend::world::Position; #[cfg(feature = "client")] use frontend::{action::action_state_machine::ActionStateMachine, input::Input}; use item::{IdxTrait, WeakIdxTrait}; @@ -54,7 +55,7 @@ use rendering::{ window::{LoadedGame, LoadedGameSized}, }; -use saving::{load, load_readable}; +use saving::load; use std::path::PathBuf; use crate::item::Indexable; @@ -77,7 +78,8 @@ pub mod power; pub mod progress_info; pub mod research; -pub mod scenario; +// For future modding capabilities +// pub mod scenario; #[cfg(test)] pub mod test_world_harness; @@ -127,7 +129,8 @@ pub mod liquid; mod par_generation; -mod bucket_store; +// In progress +// mod bucket_store; mod lockfile; impl WeakIdxTrait for u8 {} @@ -175,6 +178,7 @@ fn get_version() -> &'static str { } #[cfg(not(target_arch = "wasm32"))] +#[allow(unused)] pub fn main(input: &Vec) -> Result<(), args::ArgsError> { // use ron::ser::PrettyConfig; @@ -453,11 +457,11 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { StartGameInfo::Load(path_buf) => load(path_buf) .map(|save| save.game_state) .expect("Could not load game"), - StartGameInfo::LoadReadable(path_buf) => unimplemented!(), + StartGameInfo::LoadReadable(_path_buf) => unimplemented!(), StartGameInfo::Create { name, gen_info, - info, + info: _info, allow_overwrite, } => { if !allow_overwrite { @@ -492,8 +496,6 @@ fn run_dedicated_server(start_game_info: StartGameInfo) -> ! { let data_store = Arc::new(data_store); match game.run(stop, &data_store) { - multiplayer::ExitReason::UserQuit => exit(0), - multiplayer::ExitReason::ConnectionDropped => exit(1), multiplayer::ExitReason::LoopStopped => exit(0), } }, @@ -594,125 +596,65 @@ pub fn simple( #[cfg(test)] mod tests { - use std::{iter, rc::Rc}; + use std::sync::Arc; - use test::{Bencher, black_box}; + use test::Bencher; use crate::{ - TICKS_PER_SECOND_LOGIC, - app_state::GameState, - data::factorio_1_1::get_raw_data_test, - frontend::{action::ActionType, world::Position}, - replays::{Replay, run_till_finished}, + app_state::GameState, data::factorio_1_1::get_raw_data_test, progress_info::ProgressInfo, + replays::GenerationInformation, }; - // #[bench] - // fn clone_empty_simulation(b: &mut Bencher) { - // let data_store = get_raw_data_test().process().assume_simple(); + #[bench] + fn clone_empty_simulation(b: &mut Bencher) { + let data_store = get_raw_data_test().process().assume_simple(); - // let game_state = GameState::new("Test World".to_string(), &data_store); - - // let replay = Replay::new(&game_state, None, Rc::new(data_store)); - - // b.iter(|| replay.clone()); - // } - - // #[bench] - // fn empty_simulation(b: &mut Bencher) { - // // 1 hour - // const NUM_TICKS: u64 = TICKS_PER_SECOND_LOGIC * 60 * 60; - - // let data_store = get_raw_data_test().process().assume_simple(); - - // let game_state = GameState::new("Test World".to_string(), &data_store); - - // let mut replay = Replay::new(&game_state, None, Rc::new(data_store)); - - // for _ in 0..NUM_TICKS { - // replay.tick(); - // } - - // replay.finish(); - - // b.iter(|| black_box(replay.clone().run().with(run_till_finished))); - // } - - // #[bench] - // fn noop_actions_simulation(b: &mut Bencher) { - // // 1 hour - // const NUM_TICKS: u64 = TICKS_PER_SECOND_LOGIC * 60 * 60; - - // let data_store = get_raw_data_test().process().assume_simple(); - - // let game_state = GameState::new("Test World".to_string(), &data_store); - - // let mut replay = Replay::new(&game_state, None, Rc::new(data_store)); - - // for _ in 0..NUM_TICKS { - // replay.append_actions( - // iter::repeat(ActionType::Ping(Position { x: 100, y: 100 })).take(5), - // ); - // replay.tick(); - // } - - // replay.finish(); - - // b.iter(|| replay.clone().run().with(run_till_finished)); - // } - - // #[rstest] - // fn crashing_replays(#[files("crash_replays/*.rep")] path: PathBuf) { - // use std::io::Read; - - // // Keep running for 30 seconds - // const RUNTIME_AFTER_PRESUMED_CRASH: u64 = 30 * 60; - - // let mut file = File::open(&path).unwrap(); - - // let mut v = Vec::with_capacity(file.metadata().unwrap().len() as usize); - - // file.read_to_end(&mut v).unwrap(); - - // // TODO: For non u8 IdxTypes this will fail - // let mut replay: Replay> = bitcode::deserialize(v.as_slice()) - // .expect( - // format!("Test replay {path:?} did not deserialize, consider removing it.").as_str(), - // ); - - // replay.finish(); - - // let running_replay = replay.run(); - - // let (mut game_state_before_crash, data_store) = running_replay.with(run_till_finished); + let game_state = GameState::new( + "Test World".to_string(), + GenerationInformation::default(), + &data_store, + ); - // for _ in 0..RUNTIME_AFTER_PRESUMED_CRASH { - // game_state_before_crash.update(&data_store); - // } - // } + b.iter(|| game_state.clone()); + } - // #[bench] - // fn bench_huge_red_green_sci(b: &mut Bencher) { - // let game_state = GameState::new_with_beacon_red_green_production_many_grids( - // Default::default(), - // &DATA_STORE, - // ); + #[bench] + fn empty_simulation(b: &mut Bencher) { + let data_store = get_raw_data_test().process().assume_simple(); - // let mut game_state = game_state.clone(); + let game_state = Arc::new(GameState::new( + "Test World".to_string(), + GenerationInformation::default(), + &data_store, + )); - // b.iter(|| { - // game_state.update(&DATA_STORE); - // }) - // } + b.iter(|| { + GameState::update( + &mut *game_state.simulation_state.lock(), + &mut *game_state.aux_data.lock(), + &data_store, + ) + }); + } - // #[bench] - // fn bench_12_beacon_red(b: &mut Bencher) { - // let game_state = - // GameState::new_with_beacon_belt_production(Default::default(), &DATA_STORE); + #[bench] + fn bench_megabase(b: &mut Bencher) { + let data_store = get_raw_data_test().process().assume_simple(); - // let mut game_state = game_state.clone(); + let game_state = GameState::new_with_megabase( + "Test World".to_string(), + GenerationInformation::default(), + false, + ProgressInfo::new(), + &data_store, + ); - // b.iter(|| { - // game_state.update(&DATA_STORE); - // }) - // } + b.iter(|| { + GameState::update( + &mut *game_state.simulation_state.lock(), + &mut *game_state.aux_data.lock(), + &data_store, + ); + }) + } } diff --git a/src/main.rs b/src/main.rs index ac83994..8ef4b03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,5 @@ fn main() -> Result<(), ()> { eprintln!("{e}"); Err(()) }, - }; - - Ok(()) + } } diff --git a/src/multiplayer/mod.rs b/src/multiplayer/mod.rs index bb96342..e345dd6 100644 --- a/src/multiplayer/mod.rs +++ b/src/multiplayer/mod.rs @@ -117,8 +117,6 @@ pub enum GameInitData { pub enum ExitReason { LoopStopped, - UserQuit, - ConnectionDropped, } #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -256,7 +254,7 @@ impl Game Game ActionSource for Receiver> { - fn get<'a>( + fn get<'a, 'b, 'c, 'd, 'e>( &'a self, - _current_tick: u64, - _: &World, - _: &SimulationState, - _: &AuxillaryData, - _: &DataStore, - ) -> impl Iterator> + use<'a, ItemIdxType, RecipeIdxType> - { + current_tick: u64, + world: &'b World, + sim_state: &'d SimulationState, + aux_data: &'e AuxillaryData, + data_store: &'c DataStore, + ) -> impl Iterator> + + use<'a, 'b, 'c, 'd, ItemIdxType, RecipeIdxType> { self.try_iter() } } diff --git a/src/multiplayer/plumbing.rs b/src/multiplayer/plumbing.rs index bd3f788..cb84aef 100644 --- a/src/multiplayer/plumbing.rs +++ b/src/multiplayer/plumbing.rs @@ -9,7 +9,6 @@ use std::{ use crate::{ app_state::{AuxillaryData, SimulationState}, - frontend::{action::belt_placement::FakeGameState, world::Position}, multiplayer::PlayerIDInformation, }; use flate2::Compression; @@ -26,7 +25,6 @@ use crate::{ use super::{ ServerInfo, - connection_reciever_tcp::ConnectionList, server::{ActionSource, HandledActionConsumer}, }; @@ -69,7 +67,7 @@ impl ActionSource, sim_state: &'d SimulationState, - aux_data: &'e AuxillaryData, + _aux_data: &'e AuxillaryData, data_store: &'c DataStore, ) -> impl Iterator> + use<'a, 'b, 'c, 'd, ItemIdxType, RecipeIdxType> { @@ -132,15 +130,15 @@ impl impl ActionSource for Server { - fn get( - &self, + fn get<'a, 'b, 'c, 'd, 'e>( + &'a self, _current_tick: u64, - world: &World, - sim_state: &SimulationState, - aux_data: &AuxillaryData, - _data_store: &DataStore, - ) -> impl Iterator> + use - { + world: &'b World, + sim_state: &'d SimulationState, + aux_data: &'e AuxillaryData, + _data_store: &'c DataStore, + ) -> impl Iterator> + + use<'a, 'b, 'c, 'd, ItemIdxType, RecipeIdxType> { log::trace!("Server::Get"); const RECV_BUFFER_LEN: usize = 10_000; let start = Instant::now(); @@ -154,7 +152,6 @@ impl ActionSource> = vec![]; self.client_connections.lock().retain(|mut conn| { - let start = Instant::now(); let mut ret = vec![]; let keep = match conn.peek(&mut buffer) { diff --git a/src/rendering/render_world.rs b/src/rendering/render_world.rs index 88618dc..8b18304 100644 --- a/src/rendering/render_world.rs +++ b/src/rendering/render_world.rs @@ -71,7 +71,7 @@ use flate2::write::ZlibEncoder; use interprocess::os::unix::unnamed_pipe::UnnamedPipeExt; use itertools::Itertools; use log::error; -use log::{info, trace, warn}; +use log::{trace, warn}; use parking_lot::MutexGuard; use petgraph::dot::Dot; use rayon::iter::{IntoParallelIterator, ParallelIterator}; @@ -1564,7 +1564,7 @@ pub fn render_world( ); }, - crate::frontend::action::action_state_machine::HeldObject::Tile(floor_tile) => { + crate::frontend::action::action_state_machine::HeldObject::Tile(_floor_tile) => { // TODO }, crate::frontend::action::action_state_machine::HeldObject::Entity( @@ -1591,11 +1591,7 @@ pub fn render_world( ); }, crate::frontend::world::tile::PlaceEntityType::Inserter { - pos, - dir, - filter, - ty, - user_movetime, + pos, dir, .. } => { // FIXME: Respect ty while rendering let size: [u16; 2] = [1, 1]; @@ -1695,14 +1691,7 @@ pub fn render_world( }, ); }, - crate::frontend::world::tile::PlaceEntityType::Splitter { - pos, - direction, - in_mode, - out_mode, - - ty, - } => { + crate::frontend::world::tile::PlaceEntityType::Splitter { .. } => { // TODO: }, crate::frontend::world::tile::PlaceEntityType::Chest { pos, ty } => { @@ -1846,8 +1835,8 @@ pub fn render_world( }, }, crate::frontend::action::action_state_machine::HeldObject::OrePlacement { - ore, - amount, + ore: _, + amount: _, } => { // TODO: }, @@ -3495,7 +3484,7 @@ pub fn render_ui< // TODO: Once a dropdown with a low number of items is shown, all future dropdowns get cropped to that count :/ ComboBox::new(format!("Recipe list {}", *ty), "Recipes").selected_text(goal_recipe.map(|recipe| data_store_ref.recipe_display_names[usize_from(recipe.id)].as_str()).unwrap_or("Choose a recipe!")).show_ui(ui, |ui| { - data_store_ref.recipe_display_names.iter().enumerate().filter(|(i, recipe_name)| { + data_store_ref.recipe_display_names.iter().enumerate().filter(|(i, _recipe_name)| { (aux_data.settings.show_unresearched_recipes || game_state_ref.simulation_state.tech_state.get_active_recipes()[*i]) && data_store_ref.recipe_allowed_assembling_machines[*i].contains(ty) }).for_each(|(i, recipe_name)| { @@ -3825,9 +3814,7 @@ pub fn render_ui< // TODO: }, - crate::frontend::world::tile::AttachedInserter::BeltBelt { - item, - inserter, + crate::frontend::world::tile::AttachedInserter::BeltBelt { .. } => { ui.label("BeltBelt"); // TODO: diff --git a/src/replays/mod.rs b/src/replays/mod.rs index 8eecd5c..48b207a 100644 --- a/src/replays/mod.rs +++ b/src/replays/mod.rs @@ -40,8 +40,8 @@ impl ProgramInformation { #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] pub struct GenerationInformation { // The example world (and settings) which were used - pub example_idx: usize, - pub example_settings: Vec, + pub(crate) example_idx: usize, + pub(crate) example_settings: Vec, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/src/research.rs b/src/research.rs index 6a4fd9c..d7842d7 100644 --- a/src/research.rs +++ b/src/research.rs @@ -455,8 +455,8 @@ impl TechState { unit_increase_per_level, } => unit_increase_per_level * u64::from(times_this_tech_was_finished), data::RepeatableCostScaling::Exponential { - unit_multiplier_per_level_nom, - unit_multiplier_per_level_denom, + unit_multiplier_per_level_nom: _, + unit_multiplier_per_level_denom: _, } => todo!(), }; diff --git a/src/saving/loading.rs b/src/saving/loading.rs index 54e83bf..7e2654b 100644 --- a/src/saving/loading.rs +++ b/src/saving/loading.rs @@ -7,7 +7,7 @@ use crate::saving::{ }; #[derive(Debug)] -pub(crate) struct SaveFileList { +pub struct SaveFileList { pub(crate) save_files: Vec>, } diff --git a/src/test_world_harness/mod.rs b/src/test_world_harness/mod.rs index 1c1cb93..9da7f1d 100644 --- a/src/test_world_harness/mod.rs +++ b/src/test_world_harness/mod.rs @@ -18,7 +18,6 @@ use crate::{ }, item::IdxTrait, replays::GenerationInformation, - test_world_harness::test::player_mouse_to_tile, }; mod assert; @@ -85,7 +84,7 @@ impl Test Date: Thu, 19 Feb 2026 10:26:25 +0100 Subject: [PATCH 144/152] cargo update --- Cargo.lock | 227 ++++++++++++++++++++++++++--------------------------- 1 file changed, 113 insertions(+), 114 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24a60c9..2d745ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,7 +175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.10.0", + "bitflags 2.11.0", "cc", "cesu8", "jni", @@ -319,9 +319,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", @@ -561,9 +561,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" dependencies = [ "arrayvec", ] @@ -686,9 +686,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ "serde_core", ] @@ -773,9 +773,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" [[package]] name = "bytemuck" @@ -821,7 +821,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "log", "polling", "rustix 0.38.44", @@ -831,11 +831,11 @@ dependencies = [ [[package]] name = "calloop" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "polling", "rustix 1.1.3", "slab", @@ -860,7 +860,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" dependencies = [ - "calloop 0.14.3", + "calloop 0.14.4", "rustix 1.1.3", "wayland-backend", "wayland-client", @@ -868,9 +868,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -1145,9 +1145,9 @@ checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" dependencies = [ "powerfmt", ] @@ -1222,7 +1222,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.6.2", "libc", "objc2 0.6.3", @@ -1332,7 +1332,7 @@ checksum = "5d5d0306cd61ca75e29682926d71f2390160247f135965242e904a636f51c0dc" dependencies = [ "accesskit", "ahash", - "bitflags 2.10.0", + "bitflags 2.11.0", "emath", "epaint", "log", @@ -1850,9 +1850,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1865,9 +1865,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1875,15 +1875,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1892,9 +1892,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1911,9 +1911,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1922,15 +1922,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -1940,9 +1940,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1952,7 +1952,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -2067,7 +2066,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "libgit2-sys", "log", @@ -2087,9 +2086,9 @@ dependencies = [ [[package]] name = "glam" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a4d85559e2637d3d839438b5b3d75c31e655276f9544d72475c36b92fabbed" +checksum = "34627c5158214743a374170fed714833fdf4e4b0cbcc1ea98417866a4c5d4441" [[package]] name = "glob" @@ -2115,7 +2114,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg_aliases", "cgl", "dispatch2", @@ -2181,7 +2180,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "gpu-alloc-types", ] @@ -2191,7 +2190,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2212,7 +2211,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -2223,7 +2222,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2641,9 +2640,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.181" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libfuzzer-sys" @@ -2699,9 +2698,9 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", - "redox_syscall 0.7.0", + "redox_syscall 0.7.1", ] [[package]] @@ -2830,9 +2829,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -2852,7 +2851,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block", "core-graphics-types", "foreign-types", @@ -2922,7 +2921,7 @@ checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg_aliases", "codespan-reporting", "half", @@ -2951,7 +2950,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "jni-sys", "log", "ndk-sys 0.6.0+11769913", @@ -3150,7 +3149,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -3166,7 +3165,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.6.2", "objc2 0.6.3", "objc2-core-foundation", @@ -3180,7 +3179,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -3204,7 +3203,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3216,7 +3215,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", "objc2 0.6.3", ] @@ -3227,7 +3226,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", "objc2 0.6.3", "objc2-core-foundation", @@ -3270,7 +3269,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "dispatch", "libc", @@ -3283,7 +3282,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2 0.6.3", "objc2-core-foundation", ] @@ -3294,7 +3293,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2 0.6.3", "objc2-core-foundation", ] @@ -3317,7 +3316,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3329,7 +3328,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3352,7 +3351,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", @@ -3384,7 +3383,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -3647,11 +3646,11 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "png" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crc32fast", "fdeflate", "flate2", @@ -3794,7 +3793,7 @@ checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.10.0", + "bitflags 2.11.0", "num-traits", "rand 0.9.2", "rand_chacha", @@ -4112,16 +4111,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "redox_syscall" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -4216,7 +4215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.10.0", + "bitflags 2.11.0", "serde", "serde_derive", ] @@ -4228,7 +4227,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f" dependencies = [ "base64 0.22.1", - "bitflags 2.10.0", + "bitflags 2.11.0", "serde", "serde_derive", "unicode-ident", @@ -4297,7 +4296,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4310,7 +4309,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -4484,9 +4483,9 @@ dependencies = [ [[package]] name = "simple_logger" -version = "5.1.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291bee647ce7310b0ea721bfd7e0525517b4468eb7c7e15eb8bd774343179702" +checksum = "c7038d0e96661bf9ce647e1a6f6ef6d6f3663f66d9bf741abf14ba4876071c17" dependencies = [ "colored", "log", @@ -4527,7 +4526,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "calloop 0.13.0", "calloop-wayland-source 0.3.0", "cursor-icon", @@ -4552,8 +4551,8 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ - "bitflags 2.10.0", - "calloop 0.14.3", + "bitflags 2.11.0", + "calloop 0.14.4", "calloop-wayland-source 0.4.1", "cursor-icon", "libc", @@ -4626,7 +4625,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -4692,9 +4691,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", @@ -4920,9 +4919,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.7+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] @@ -5005,9 +5004,9 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -5053,9 +5052,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ "js-sys", "serde_core", @@ -5242,7 +5241,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -5268,7 +5267,7 @@ version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "rustix 1.1.3", "wayland-backend", "wayland-scanner", @@ -5280,7 +5279,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cursor-icon", "wayland-backend", ] @@ -5302,7 +5301,7 @@ version = "0.32.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -5314,7 +5313,7 @@ version = "20250721.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5327,7 +5326,7 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5340,7 +5339,7 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5353,7 +5352,7 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5432,7 +5431,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" dependencies = [ "arrayvec", - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg_aliases", "document-features", "hashbrown 0.15.5", @@ -5462,7 +5461,7 @@ dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg_aliases", "document-features", "hashbrown 0.15.5", @@ -5531,7 +5530,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.10.0", + "bitflags 2.11.0", "block", "bytemuck", "cfg-if", @@ -5574,7 +5573,7 @@ version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aa49460c2a8ee8edba3fca54325540d904dd85b2e086ada762767e17d06e8bc" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytemuck", "js-sys", "log", @@ -6071,7 +6070,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.10.0", + "bitflags 2.11.0", "block2 0.5.1", "bytemuck", "calloop 0.13.0", @@ -6181,7 +6180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.0", "indexmap", "log", "serde", @@ -6270,7 +6269,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dlib", "log", "once_cell", @@ -6491,15 +6490,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7948af682ccbc3342b6e9420e8c51c1fe5d7bf7756002b4a3c6cabfe96a7e3c" +checksum = "c745c48e1007337ed136dc99df34128b9faa6ed542d80a1c673cf55a6d7236c8" [[package]] name = "zmij" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zune-core" From fefdc68ee6715a570e66586a43ed53b702eeca04 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 19 Feb 2026 10:32:22 +0100 Subject: [PATCH 145/152] Specify nightly version in actions --- .github/workflows/check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 78bc369..a9a6ef8 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -14,9 +14,9 @@ jobs: - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly + toolchain: nightly-2025-12-28 override: true - uses: actions-rs/cargo@v1 with: command: check - toolchain: nightly + toolchain: nightly-2025-12-28 From 0961348c5f3240f934fd29e6fa90d294c20011bc Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 19 Feb 2026 10:32:53 +0100 Subject: [PATCH 146/152] Fix cargo fmt error due to rustfmt bug --- src/frontend/action/action_state_machine.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/frontend/action/action_state_machine.rs b/src/frontend/action/action_state_machine.rs index ce06a10..654b4f3 100644 --- a/src/frontend/action/action_state_machine.rs +++ b/src/frontend/action/action_state_machine.rs @@ -826,7 +826,7 @@ impl // The mouse is no longer over the entity self.state = ActionStateMachineState::Idle; } - + } else { // The entity is gone self.state = ActionStateMachineState::Idle; @@ -1520,7 +1520,11 @@ impl if let Some(e) = world.get_entity_at(*position, data_store) { let e_pos = e.get_pos(); let e_size = e.get_entity_size(data_store); - let mouse_pos = Self::player_mouse_to_tile(self.zoom_level, self.map_view_info.unwrap_or(self.local_player_pos), self.current_mouse_pos); + let mouse_pos = Self::player_mouse_to_tile( + self.zoom_level, + self.map_view_info.unwrap_or(self.local_player_pos), + self.current_mouse_pos, + ); if mouse_pos.contained_in(e_pos, e_size) { // We are still deconstructing. Continue @@ -1528,7 +1532,6 @@ impl // The mouse is no longer over the entity self.state = ActionStateMachineState::Idle; } - } else { // The entity is gone self.state = ActionStateMachineState::Idle; From 7dbcfeaad7d79a4633fafe539504df88e8e0ec93 Mon Sep 17 00:00:00 2001 From: Tim Aschhoff Date: Thu, 19 Feb 2026 14:49:12 +0100 Subject: [PATCH 147/152] Update Readme --- README.md | 21 ++++++++++++++++++--- giga.png | Bin 0 -> 243500 bytes mega.png | Bin 0 -> 91322 bytes red_chips.png | Bin 0 -> 1010664 bytes steel.png | Bin 0 -> 329603 bytes 5 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 giga.png create mode 100644 mega.png create mode 100644 red_chips.png create mode 100644 steel.png diff --git a/README.md b/README.md index b70f16a..4896902 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,28 @@ Another goal that emerged along the way, was learning about the way modern CPUs # Why did you start? 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?". +I hope that the next person with this thought can find this project, and build on the ideas I have had and continue to push the performance. # 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. +Most logic for the basic building blocks of factories are 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 60 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. + +### Megabase (40k SPM) with Solar +![A 40k SPM megabase with its solar array](mega.png) +### Gigabase (60x Megabase, 2.4M SPM) +![A 2.4M SPM Gigabase comprised of 60 megabases running at 60UPS](giga.png) + +### Machines Producing Stuff +![Furnaces smelting steel](steel.png) +![Production of advanced circuits using direct insertion and belts](red_chips.png) # 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. +If you only want to try it out, a web based build is available on [my website](https://aschhoff.de/projects). The performance of running in the browser is not amazing (mainly due to the browser being limited to a single thread), but it should still be able to run a 40k SPM Megabase without issue. +WASM being limited to 4GB of RAM also limits the size of the factories. + +For the best experience, I advise using the native build. +It should run on Linux, Windows, MacOS as WASM (Though only Linux and WASM are tested regularly). Assuming you have [rust and cargo](https://rust-lang.org), just `cargo run --release`. On NixOS the included `flake.nix` contains the program packaged for nix, or a devshell with my VSCodium based development environment. + # 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/giga.png b/giga.png new file mode 100644 index 0000000000000000000000000000000000000000..dcb6fcd21ed976eb907e99ed76a6baefb8571f8c GIT binary patch literal 243500 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ᨡW(WbmTe#26 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%0AV2sHtRal%&989JC}A=kAvW{%R#Axi1MLp3DSU1O zTU5QpXww_}%$tiE8`r{ygu9OQx7LQ6hk{=a)dUmS}_kl;8}k+?>B6E*ou(Y{ky}l*QY1_Dx4( z@YNh275`g|IgQ@CjgB-%H5&?j*Iu>YVKL~DH8f%-^g{Y)%Cp<)s(A5{}ZOOsyue4$5v$y_yz`LJb&RaNak)He(~{xVb#g6%;fi~j)?J``el z=~4nn382{U*JCsq)&D17LMD&zC?i5>mQZkjdsb)a+(P*@Jr*=^Zx_I#KMP z4bd}*-{@D`Zo@6f81>Z_fDK|OWKnSfYY{LIm=*4XVFWa~)P3x>$=qWg!}+>WvFn#v zozdA7&W~3}rP8u4tZS8GeJ{YXDmY?P;rqy>l;Q#2jl1&cShHm(KWz&O@w52(RBG+t z!}z$eMb;Pe(g4)uS~xTRpfpTFJu#UDik)+T*F`S2?vW=Pmt?TiV>));l&v@|Y1x z4u$cZ6^h!X2H0XG2@n85(j1T{>=r`)W8XeKc}cVnFNO3K0o(h#5|oB8wweMSlniwJ z>xCfwhMg6p-0MYkR2OHarv+QIEG>6xrs!00Nn?LdoSQnJw_`+AiG@rd8a*s7748}U z6NoP%nJd;JsPmz5=_08h=S4^=NIHmqARg!ot9p2#@k3Hp| zBfotJ_+}>52-G5(Elk2rvf^p(3#m|*t6dtdx!n7ISnhJtv3*VE{`90@u1FdD;nZEU z#M2w8A)IpEsw(bUtL$!DoFNa6KNY)o8sIs|ee{&XsEl%tm0C#|D*1vFlPKh1tWikT z?1hM|GOP~_yLXSi=miJDUxe9>U%p9BfgT&q#|KamK_^RJ5stZ&b#+_o&LUF) zs1)?uQ*|bs%K@u<{i9ML=9{YKKK#ECOt_R+b$oyYVda);QW06-o1E22q#a_Cl9G?> z?sE9Nd{$uAw7t_7Zu)?}Un-{eO#ni!UcLHIg1rFpiALEM#;dZ82Mq8Y;-f?OR~RX1M|Bgiqp z20;I-D6gdHB&qjsE+T;(O+WEX5zq`-|BW&wyWT%g+4|1s;1^ZJz5~2`97&oXMzMJl zbKEx@=es#hV78roQfGCTryCIOfJITUN-1L5 z(=|@5lUjinLI3jb&`;OtpFgE|>ODl_8M2|IL-;4LJmTdSzjGLZPy$V1%nu_>L%~@> za;1UpvbwNuOGyF@3T_g8iUMR|*cPg4d$mDI!yQ}~$cbTp6}e*tfdsI^?Tc$fz>+W% zXtWSu!PAEjbbf103PtWExDASdgFGVa;oT>JLb#-G1bAccVwG`^t`ipQ(I=nQ@xj$} zO5hC%u+UQBMs0&GVX$omNkVrVhPt~C^M9ft2_K**pO;+d?Mue2a-q=IYe_sx2W|6U zEz~p7vhEwvMBS(@MDGwFz`7g@P3 zsE<)6NesJmcXyBf{OR!LMl9Xi3oHct%|!J{Vp~adJb{q&o*1Xj>*yFFA~s@sC0$ws>G3NH}uxa!t-G>++F@!<+24IA0Hk= zm}Nuz{k-n!=PzE!U%ZIn0pg&MR0QM1GcnF|85&%)_;51OQ99}7(DdGdaF;m5jkPzI z>?T@EVr?q%Th$9NfEYd~2ILLb^k+Mb@uD!zS)c8RvFQyM$_V;&wbXK->lTstY`dXI z2tl?V$@j*i@(!^HP!zwWxFeq@{`jfk7B-HvA11z-r*6jNVXTWDONN1F%YOm6D~((K z25$j5ArbI49bdkzw34wRQE|kHe6hoB3b(p<&MHKSxeQ%MY+(K^UOSWJ7g&0g-U#S} z*46v3@;+Q>n`ujbIxJIJU9*~|MRS(k3;&?lXR)CrI=DTvXV!V=(u1gzch%Y`*er%IG|BdqJe^NQOHdUf*<=9Ws|~>)1b>C$wlGT1K*m$k3$>W zAReeXeIFx!@X0d$RI%ISKdEhxS96os@gYqDY4!Pxi0NAa`$chq`*Pp0t)q-Vh2WY+i3pcXt zIt!lZS=T!jOzyI=8NWT$92tXpg0+wdKO1|c7^X9_?z-_=)I2LJLs#~H>rac=Ea>!# zv(d7S^6~9F-7?OlqnTeFM-~lq7%%M|Xn*`XgEk3NhUmQM&xT`U;Vyj%+eEo z4nas;S_WEDA_U6ZF>n?ag+a4-i2f7=+HN1Ix(F5sG=`5~_J3 zy+u+1#C*5M+>x&D@1It{jYLu+T~)11!O2Vt7dU$`%Ngcb5JoV03f@~|X`PL@f(E3A z-8-uE;;k674;+!CFB7R@ZM%;i4Acyfx2!H&r@dTw7ei**R^eSj|8h9UV&?gRnuD-h z=szri|BuK;8)M8MQL)816hm_)uBE&FG=nTgvh^V}4&y275KM#r1U>vs2@flk$#^5^$i5zu#2^M-d6=D8bHxa|HZc|G_+BKY)ZnuvnVH zEotNRhELitNU^?+7v8xso;jN4XH2H{qN1YZxEM(T4G9=5H~*BV2qJ_zf&$f7W=dQ2 zD;&uvo1xKx(!yH%#fS^#c6hkL;#WrtaCnS%%==<@lEa2RbQLQr(THLgw-?qJn7iLQ zyUTfx@=+hpv$ymN@82XU%8F`w7Vvl#-6DdyC*b9ba?+)1ek-^>5C<9TJ)74AR66$K z64Bc`iZb9Ml{#dzR9ZFPQn-k$fVUSg6{j1E1sw$H5E787+^gyh9(bi~<#rV=QZi^JrzBZhTUu=Q?oHWNaL!@LLnPV3 zmcN?&2d{&Sk861*r6y{g%|>|Tp$$^&`BIgZ=?s17<)3t>>g8O{x2V`3opQTYdS<|* zw8C>(GoPl#`jzBoty$eSQ)8g!H6wfn!`mO!8ro+cV~_$>}_>Vc%_;p9H*Df)Z_*3FaW&#O*u zwT+{x$q?SPiyT~$RNN^pb)_T?AI&VPw>$}0q+a}N`+7r7@W1+&!+zi3vcdi$e&tjB z%oOoGd&pJ;Ttx;rffXZsCpIgTt{UghufQgTRo3YDWIt(}XeiL+@Hae0_7U3UgRHzV zT2Gia{znV2B3QZPW!=*=+tZ~AqS}R575ZV0<+l_w@J#NSqL3dBjLW9j~ism$oe-7+8nKa)D^V4J2O}fj^4=l~Hyxvf^c*H( zzkx>rQsY&?H%YjReEcC@-I%VQMoYOyJI0U}B~#&mVJu{v%wM{I3tki~APR*zRsa)K zph+Z(QP7}l!qN$$H{>I2T2m5HvQIRJL1TkY`6JB=oV*4?NB_+*Ll2KvsWwtD@`cB& zL+}46eQ=eoDVE%*^60hw$cX=rG1CeOaj`YRUxmvTmLfb`o2Vi56&~x8RTdq@WOxMV zLvp+}|Kt^FkI%Xtvd2j&XlTaK)RpVbubj`3-&u*Sfn*FPp8!(|rWI3U6ySX!pi7%+ z9Ged!fDxTfQexm)SQ94ZK6jN+dz4o>{*n;L5mf`p9l=ob%@t3r4X?khr`qX*2($)Bv=!8!-Ii?m=qFC*3}s_?KvA0HnqaGrs!H})jf zyX&ou0eOYip2yq81>umG;Kg&N*$rw&^l@MjS+KJ`7A906_6hB~+q3;4P4kO59X+;` zUB7agFf|an*_eyQNr8nz%w;HYU2gxHnYoD$5v!%di9kNruhaNRBsNr1%g=#`1MVSP zkyL6po3AJ!dip}$fc4A+#l+<^>3AQ2b<0W9Rl)s8RAcT*Io0kGqQJ5bx&j+94Z zF2`6Fw@pxV)*tQEmZ{i-meiZKviZ!*Us0GUp00~pee#3Tx9l&)B}#l>s&j1Y#RSH!sHZgl#wQ_v`nJ7Bb#%7dWH1jKN`fRyB) zE)P(Y?|ewxJYtMpNd`ZiKmVXna1>Q2?qZUj(5}qR#zq78BsLVX0xzQXLpfA*BLHCP$&)8ALG`WswvvSfO!;8PH?y}F zKm<(8VVja!lzrD@rFY>MdO7TPr-X@>5*LEw8{7zrHpSE6=eJJoBQ znEI9L(0qlHlAoBQaT7ca2~eYOf6cB(kS#t~3C5^9(`TL8ey=1j1naSriLuDc9In_} zsw=P-e>?#6J@CR8b8L-HP7kg|FNjte7!T{9^y>g_6y^e~br=9gcv&)i0gByD_g}g| zPkOL@%HTRk15oqH8Xr7Q4A=IN&kJs2`GcD(I!In0%rA%pi(H*od61h!cj&|BiM%OD zS=PTf0;ML(GXZswQuf1#V9uEbJ2=(G zs@C6w_z<1|Wk-uB&yM?_Z*$9a9fkA?Z6`-_39u~e|!E6r`_;SqB zi4EG(H@r4L5vU#etMgA-*|=0}py8%yCQ%}85tA4-%UURG5gE&Z^)Ek~mi%;sXL@F|gX#I1 z11~OL16x?()%vDtzAj4MWn0+MCxS7}iblr;*0M~6e|%$KsK6>mEz~Xkpv&f!9wa2h zZ_4zSdh7c2qhKbZBRT7(Nv^K7myjQIVAv|3&!eI5)bHLCfZJdeAjc#0I(L?EgNLEb zKvQ~W$iJj{vojvj2aMT*K%q3g7iTV%Sd&k$3a&KfJLtL$*DmU@@h{}>Ay5UC@c#&V z4{)yczkmFLqC_GUG71?{p@`B#NJ2{rWu~%;?5L28$VeHH#wj}?Gbn4T z;J-kS+@8aji@u1T4^SM~!-J;Bc;SH4f;Sz?u-D=Z8hgqpt!)d^$-tD&i&#tvqDDN; z|DYx6NdSN_z!51wj%6~=6X+x+N6Iy%m{VHVO;#R5bK$V7k%Z%2%Ag8(?+7pwI4<8W zQD8zmYYaq&oYm-!bs+b8l=5M3*&a~eNvjmP3{A!TOimhj%^mAyNi@#+5YjsFZNj`% zO4~GN+Re)$;_coumW{u407;Yg?M4?F#>b1w^UH1p4ISXF4eDL}@O@!4s9k}ecb zJ{i=CeUByt)xh0g&rd5+T^>1Bt8BQ8UhB!WqEOiZ~oRr1GD$?z8`wV^42miY(dI8 zI7f0or31rUZLxI;aSI@&=2dgHP(7cobPT(NzCguI{`hstgNiVptF+`eamCi7v$n#D8{Hh zKpMfF!qp96*9=S{k~(D8avKQ+dYIYHpB%qCVitMKxxc=}=bATUq=+oXQ9gA2{>q(BlsD_QXdfv{6Z1;kJsOkC42)rCXqEqFD2=F7c$jC|2 zYHC7n)Yl(nDO&!qyp();4v9TTPE$mIgM;r_4>%{HMnc3(5!BB%A~v=dZgxkR{}vWx z<)@N=%>k0eXrhz^FUXl|W=lt}4pm&#RFbSbn3fwRKIZ76bmIHLFIZXM`RtE-v;?SX!+W z^{Vy4iW@@- z_Ya7E+SgaMnX&phhtC3vfoMLnr-sV~PMD&XpqsN1x!+IRfk&y*bYu&UUL*Wy>E;^5st#f?D~c}vwO+X?H%eoKZQd!b)&%{Ck0~W z$qbRtpAT=}33CEIQ|x50@)XKH0;^!zf((14l#@dcMO#lZN9#j+6l^C>EbI}CJkZVi z1BxzwFF?CsMZ8BgWR} z(^bI}JsX7BZJTR2kF~vz92vi-ydrXlvC)lb*X@piAN!;L)$O6a8=32LpLX8+aV+u0 zfe+S^-c0}Do-#jlC_>Fk7QR}d7&Fc5=_C!(0TssE3yEDc8EDTD(T1>PC7r7$L^qPj zA24oU)Jg7N3Q7L0HU~GJUAJkpX-a2tC+F<@U7`_k>`WIkWL;oAfLR2|?6DQEuw4)t ziUbTxSKExsFiiohyOkk;|Gs0#j<`3H060;&LM(9W_Uq&MaToueEe?p%2}O)sMM3a8C{%*AA$(8A=De-_)BDHafGvJ^myUl_3xbbg{&_4G|d|% zvE+AE(u8uW-b`1wJ1jn_H6&_gzEGJ`$G<17LspGur@Wuy{8yn zTSY%6Y7*@q3132Pn^RJelx6e|i^kRuq>^&=2wfvMlh5k(Wk>FB3Mrb zk$=VD25*yuYTUypo3Oj0a$b4@SVw!*gQYNwc8??`Z}p92T)UPR=IP66j+`dPVpWZX z@>Y5Fs9^zP$kWL8tA2OIYaXq-%3>sV@W_e;W?Q_@z)OMmPwcGj-h_L^(pO|~#M;i9 z!NNstn%kV8*Cx>%O4t6-XYC1@=2Wa|2RUYPWLfN_^GuSgPHeae?TK~PG(08oJXJ>_ zHE3J;=?!>yn7&Aw7f{~csC@yB3WwHIbm%9bD~9!usH;$75&bzJDKbdl85lrZmApYQ zNGn4s7S3s#z;ZE$4u@Vwl#Y-RGUD{I5xOObQD%@R{TTraoqlLK5O6X%9jbvpC^#ua z2>x1*M*5$qpCH09peFwlb4o_L)80N}H3GwJ9DXo1CybQXYHEY3kHSSUFa*6gJZ}7D z*y|`o7?DngKtbZZOm3v1U@)whX@e-hXVOuDHhi`N3hy>1?%rScKV(b+jTj<; zuI>3_9afhAFL_f=7Eu8mvE&chK?`kc*gtK*a6)bYD_S1i4A zx-q&~Y5u1D)xYNGIOuD;Uf^d5dJoZ4)qYDzuL6}5##e)$C;^xQxXufm52XN~2$`3I zSyCH7JT|49KJ3cq<=St*MNDiRjE3k_o1i2tmH~SYh96ayhu$*mdjBYo`aG=Am#&pS z{td})kM=_&O9H0QmX3{$6`)bwJJLiLb`6csfwnJHMRCief(opR+P?`ptiE?J=fY)U zH8)`lHBb!V;9-=bB+RL8__uZk#Rf_Mub=JjtoguX5!gNs8w)ktjlc*zG;k8UUoKwM z*4`XsvftuAYN3W96z-qVP67lZQ$Ps9`a)Q5Wa+tv01KaPX2!7c_E17%{_lEod`OGS{Y{Hv873cAh}Pw!y7ov5vo0A5f&*uNp2WjZ2qD})6 zN_tYW_UsJ+9*d__F?S4Ot>9=wHaKm;w~$=O#?kZ>P!%=LmTlWS8+T#-0)!*3CX$^3 zssmW9Jb%xj*H%G%IB%NLOqdAEgTZAGngGb=M1DCFw`>7qMh?urkqmfHFD2eN9)w(B zu;vM2vPsxz6KoND(YuWeg248Hr1M^Q4$RjRCOH)Be@t?7`Tv;YQ0f!KFr0EK&ma7p z`uS7gH*6-agWf4}4bEOlfv;1~Q$47P?&hMPv~jHhT7qIhDS-0`G(9LKgEnA}FFzh z>Vg&|JWJ%(U`cOA5gZ&4j}$~X(FCZSZXF#7K5vOrcHE)=c9(wV&$KQjnuWn1yOfw| z>5ch6@0be?hC#L1Ov(_21da&M?a^J1ajsoZjV$|OTXm$OWkGzTk%a%MS$B*g>me|aYkQ7C&6dn*qODv*q<%OH4|p-%uB!*}37 z)KBq-2@GMr76bu27Jn|Ua-za47UDB52-*}iP;ysyzlm#A>a1|~!* z=_Kv6=1N=YmgL`}AGM3Os2c9R2S`S8!9Yje8pgEYO_1sexPmZ-KvV|G5OmLw7RkXh zN_0Peq(%D{GLSUeg@zB;7f3Rpojbj7uHt)$3!m#mP=N8;%#L+4KiSXW{S*P8wO9JMzI?ro$0BcG@%?Vu zRotro_}I|g>{sw_ifbJ94%LsRn^{I9cgyflO{DPphmfkHXd^uWB5lGYjOlUx;A3mLXPDc_wuA`ndE7kHc`=IF{E zY2=53iiD}+wxLd}2E!|!!OW*Kd?Doup!M(9Sf zKIq~}XkH5}ZoUu&$t;8&0Hgh;36`y3BUDX%&ZssQ{uwF~0Y%jSB+B(b_;*E8QmzN8n5* z$PTTrZHtH^h@5uCLk*O;a~=4_&orKSJVJD7-1gNW<#?_;VW=S)|4@(-+fPt5Jlz`* ze?~s**+^O;Df;6Jrof9}#zSZnk=-oWj4(dhcQbB%$!KAYXF!8-Y5ALw_? zhtR5acZu4s;r%-SCiM`yE zXG!fB51h&~83=H_6RkZ}T}|U>oI5()X2NW>>aLL6#vqyUpZ0?2m~hgNk$8Z}y5R4J z!iv(cSNhQMLR!S{G#S>4K81fvYpiF^I6%csKt9x}cq_R*qH-wb(D)+Qg+!Xd(^m4R z2^=NN`Q%Rij~E8z_!#hCF#8^RHKDB_VU@ryajpWmBq>_pa8SM~LOG5j4~Q&K7o@2% z)}4F#W!wn3Jz(X1A5+SZ67cMqg4GE8>J0l$D*6y!i=;<3cgaixSb(yo`FMGWe;YEC z$q73zSZn^5E(X;q2oV^*wuaWBO9t_U6$DMr39KNSFZB=sYT1jss0d4&#oh)57$hkK4hB9rg=V8Cu^Cu~@PG#88k zS}78Q??3`45wd@F4S-OM=p`USM^U4MSQDgZQ7E}QUCIYI{9Xj+x=jF6HL|t1gh5svp<+tL+{o!@TJ)a{LSSx%d zb2Gfs*iqmk=UpEA75V)&X}i`EIAbA#2Qd#w5YN62#VZJpIVbb$}jTZ^Dm1ir((-{qQ) zh?|mjqsz-Res}IPi_tRloPFzlPv{KuR0DhDcIu}NmZqKe^77M%(|#$r+_cH-JgzOG zsWI-#rI9+CzTvgOS*vHfe{I>mDpBZeW#Apv!FhS73>o$~#hu-Qtbr-(y* zhi#+3!dj*A@dFyKi>Pu}Tb~_xW=PjnXE*OdA9H6iw_hjALasRCp}%&1X+W;kp6@qC zv(u;d1)M@DPiiXo@>_Fj^+nF6t~P@iVig5qk}?cp9Q&~8lHT&tBF^k^Btsmd-3*fp zVx34xGD9(m)yEM?#B?ZHNr3as<@rv&OpS^ZAi*RF5u1g%_sup;8H1J+ecw}VKk2W7Tvd`-oIX%xps z;gDElBBmmZ1^B>)vpUAsXBsIl=!{9}FV6dGc2*C`i}3dJ|D2qfA}~Yw4W5E}Nr=x& zU|TvS_Y)f(iud)phDqu;B;fX^@|4rR!H9d6fSz1SWHL6UA0L47n{tN>tF-qdK;|ehO?oAwe*sP!8HWJl$tQ$H~`{ zbbJs{i*g5(kiS$EgfP*5GIfn{<;N!hhy$L4yPTiO%aIB%ho%UTuZU|QN*18J&25=s zrTc6h!<@)P0~d}Q%5uDMlIa4;INF_Dx5buj*L@+2Bb9kIr?-b1YNq)aNe%{u?O$i; z+MdxTopx(d&qil)CgZ4KT(2YFAi|vi1vHyu>=lWrY-?{PNuoskfu)5*Eb`?nP-l=o zoV_ZSFAE@v)EaI1!pzO(y(GG%E(0?LFjj~MFad-ygFjo-O*es@JbrW5PvVcNa1FS9 z#Pgw7RdWW@OO%Z0zhSy(u^|{tNl8f;ruEP)RlNrRtdnE!-{w#TLs84yt7x!4yHBcM zCYhENn_8qa)!w~(Nt^&=CEGfZiI4W;MQ&Wt_>9J((z0q<%eC(gN*9vAN2DZ=dQpOT zVpQ-Jqp7b$gZH z;}H?i3M3;=b5O8@L#u3ECBpL~Nq`?dH6nNh1cu594G;;vhtdQ29o=8Q`k;8QITZkJ zZTOk%Z~a!G@<^TV<6M2YI`Wv@2cZzA4R{}LN5wvW?x`TpPD%5#0r`I3!|1d$s``L=uI#KIM&fnZZ6FU9@Daz``haT?S8ifhgF_( zgd2xagGj%|8Djl*?!>r8R6>O|@y)Mb0ygDf%A=@SlMHTt6HoezgwLisD*HT=3j)dE!kW#ijGo2Z3yKc8oB2 zu^JRMtxMDbGyQmr7qNy2n z;(WCi2a7Q4Y02@@($cN${Iv7_&{jWw{Md|)V8ZCIKC6~3US2Y%OND++Xy>7B1Myva zS=ngt%in)1xbb1vgPwWF#?l$8zsA<0Z9+1{F{gZaMEov`sv~O@n~j-8$)1Pp`IGx+ zyG>tKZ0u~JF>~ab*S<=lY49UrA?SL0aX_Mr4-FTO8g7$wli%t^h_oL)U`lgGV`JGX ziIw4U2eo#s`t&SqWFGMjC|(%8jUQoePq>gyj}Kf*B*f=~0hzZ94<;T7(M7@mBr}Mt zfSPZw9woZLWY)(4E}b10CuGgz6aB|2?zkJQSXa?=j%!HbExVm;rYAH7aO1xC07ns59KiY@;ElH+L8hHFfXp_9YW+B zdL}bBDOURaVy~8U!mN$>_c74e*-jjmAI2O7q!y;$3waF3&h~A5Igyg4F)*6(VEyKJ zImc3+m!D|$@)S2EJM$aSu#MZLHEQCErs`TJi^6p?7FT?@QDYIqtLQMEyF2BLRh#8|Tt9YjF& z*v!n}5C`(9Zrr$`R((!W>HIg2NwBP*UP z4-7g3VY?&OnokuLt2@A?1hnvj)ZYopv3~rx^C8;tiGMk+$q-@k^i7|?(#GMa=asGI z6HPV}3IXTe2@h|RiT_M7xm=f~)4TSpbcmzXTsGC<0Q2?w>}FFDNz1)@pLXxg(Nj%o zJxlVD(V60gUx}v=VveL}78uDQxkvQ1w>G6!6<%HNKVhm;L zj$vbo`uU$DHsN)}7pk(XKQ(F$JhPRH3YGbKWuYW1i?x}WZ6NxZfAh*KolEQs(`!H1 zo`2I^Q|LJy;NQ+z$)VrO_s+sSVt*?B=I82W&tuwO%0{wvkHBz5VoX?As3Cd=++@>h zgGsy~G9EN>Q6ON4M3fP$7r<}zi2%pcjRj|feV-%EF*G!k@Y9L{6;0i+8WV$%<9no1 zq8JON??wtHnFk3W%+__QP!bgi^RjYqTuZZg2{v|d*5#m)#G|Snc?AC7*>&Ukb<7Un zL*OV5S_EiOekYJn6^o(Tjg+GNlSXoQi#z@l>QSE5cxwo~eJ!UFtD#e-mt7iGJ&S?a zALjzTae&cOI|O(0IxPNENOEX2#L^-I;%X($*xt$vFr7gwkGv}~2HLyP{D*#2qU^~% zH6osJsVkW%Y_vEG*ZDmt;=p20XNVUMVzkbv-2Aet&GXo{>g2bk!i|>8`jnkQ0psW1 z#fvm1D*U>8X77Ml(QwAL%>~}+-Y0vt@L2c5cnJ-i7}{normqaM1k11m}gxC(HbfvOwNXo;oFG@f32C z`;cmqU~wc$pFUcDF#@$Ai4*)&8{$AlZHRP^FXd0xf?2)maf(qZBQ@Kro*fLHW_D=< z&1&aGl!*uDM|R^rI4s1;Ml5$2SEm-X($Ii)p-y07FQ=1rH89dH&Lo1HE7I07U#k@D!%^O#(fC;Y1chjzs zj69dQy4UmoOpkyBLE-^t^k84j0v)6I%x@9WY?Zylvbv@)%jV&iC8~l zoY<#Ne@U4Flxu(B8rA7&apwuzYhSMHk(X$o(cz1IUi-O22FphVVI$1-lJY@upb8_g`NCVn-( zPNn);qKd(S^$VSG{{wxCnRT>Z#5X0kHowu$vS_$s5}j#Pr`aEV3A7&jqBP5OWt#QE zAx!H*MS{tD=ftr#C@gH?n=rH$`X7D{-@dFAqahR^VHWK}de3KxIUYMQmDgsK?zj|j zaX2y}GmtHG=|OS6W#=4MiSx|i)q{6pE^O%56QO@kH$2@sEZdxsgQgsZEda|abUT?1sGj}xOx9ZP^j&JDMwAFCn{Abxc zSOxW%0Z2pnJ!t6L< zHgE}3V&dX%=MIcJD66PI5Y2`f4D0$90;iyS>IP@g$LTCVx)Bu3gv`Ev9gyUmgf$dc z1;tROS2uO!~UWa{cp0$ot^-T2w--#&T^yD!_-z{ zG(Ehf08KAd_$dMW1tQ?|sAbK*tquc%gJ#jaX0R7T4X%U0mm=;DJr z(h+e{X{OR2niL*owyFe3SY}quNz>RiCz!U+wq4bSWjEsSNFeW?uv7fojAk>um z79O2cJcQ~H4EFDx^6*o+HO@HL47xFmLLRbJD3*xJv*4`~M6Fi9{7BrMEH@ME2G4%L z(V3Ada(0O?TnaM0EQ3ycP(bASVa!{f(6Tb_GH_UKQ!Yf_87+vK`@-vXUMO|;d%Xn_8b{ywtf#8yg0+=uH6z%UO7WSVb z0Z`+DtB509wH360)#+?dlK|*Jd2dsS5jo^Ag*}ZV%w2MuTZsj82k1X^#3M-&7du00 zG2+N}?AMejL@@z{?Jhjx48_e#kg^f><7fLO{jJdz*dFIuj&N#nbIQz6^BtnOsBuw$RwdlKe;H?GOA-XyPSG#6oY z(6XM&-qCP{+dx*c5x0k;;8flBtx-Q(6(-$JDsI{%dUFKbG|3p5I0&5#$g|n+IX{$# z1`fMYot}N8AyfiG-bY3@e9CA@R~ufwituV+K(Lb$^CyWiL@!IjafpIgUKV!I0SQrm zcaWX{r$`XR=x92JrI76lzY|S?i%1Qz3fRm*+he^ZJjoV)Ed&tpoKMI!O0=*aogp{J z>D7ydf}CZFaa$Xoj7g3M0*#UZ zUf7nJ^dpG^fll`BpH(OHVc2LdJ{jU+2Zv0uPAD%{QEA?&!jvrXotp6cJsNbV;j}Hk)z?mE)(0^3LK~9h zgbJG6!<_DAz19rYL}P;Z4}^%ggM!(=M-ge3Si_ibJ($u4vC%*9#bH3i3cR&35{R%* z%uO4?@PoqM@z0HoCblD)k9PML$3R$&-G->3BaogThxC}*9$UOOxNa-51ZpB>ZWBlx z?;PYP1SMO|{{Ru}*Y`z*5-PQC<%$)kd`b4GdpQlHGjq_E5&thfJwU{(QAI=H9HIEr zLoX^z=S-Q0oHc#MbdZ*)!2I29Sd38kaJG~DX#5((Qa^SpTU+AIJl2XC-XM0br8jCh z5{1G<<J8j($c?xEU;{lYs{Hg|B|ww0}y zezSMvXm41u_4g7rNA~AJ3J*S^bSW+z>zz9Kc|z&*v&g~TUW2{eY7lix!ZG@kj7<<^TUU4U6 zODR`sdE--?$Dz8o_}j$tx5@Ydd(KJD6 zZT4N;+38lzAbiGn4S^r^W=2uY5!WRCuc^|tskynlxe=B|&G|OYN!LmzgU8OtnKl(0 zsJ6w@-<(top%6}J$xiia37vC78_I6^{U)1A_D%=(s^?p-uo$s^>>ZtD)}WO|W9b)` zbTKon$h_0r?$BI5v;WP+TAhqzOHJCD@vAYU0WS+D2GUxHY@38ALHS`%p5hJ>DOkGJ z{O&cH0CMV~7=sw>ZLzDDNahnzZa7kKwytL0e{)GTtNbhKOW;f-xudS`6dAIF0UtoG z`e>EFdk|{|237!q!5$m~02trN1a_WCP{vuufBq!?4p94SgpczD)O z(25eN6>4zs|4dktusv|Pf7Q>Gi#=>U2xY2M%DD(S3U!5&I z=+RH6^cNmxpZ`;#-#hIiir`Z>&K&UO~v=L-?wN zg$2l2d{T;l>tP))d*$%aEj1bwj&q&?UivCcF-g(xKUAK09G`Kohg&#uJv3y5EBRtsWF2rl3+VT`N2vYXD}e`8b< z%YPg!1b)S$HQEU}DE&cqZ!czS6$I(KaZ^yp1~yKY+8g1Vp>f{Z1qB&+Y;szcGcJKR z#8e%#Q0+7r1gSkJ_yeiF%fTmiMD}9DDbTFGHe?RQ?mTT0=6airoLtl zN)&B2-{$Q)T4A?e_r;n~Q2+>PS))-Dul^xwr&ULE?BZ=A~djs|UbE zlKI8YzO(<OAU^Q{y?f#8qU!Iq z15P->G8~^6iDn_Gj!p)~OmuUxsCI$%5N!<-9-+iR-sn-cb>sjdgXd6jMNL`bAlskf zEG;Tp`8wt>d@;5EVtmIx?Rrn4;3QMJ?w?hyXUAx&@DuqJc%(i_b3{K6V*7}lKVk+1 z9diyuT}d(}myc073oa8|3u#KQ$%+V_c4h#{U=znd3U55#`(oRGqGE%C>HVanB&xEy zYw1W3PH8>(xk)r^@9h8s%aOtXRrYzUjt~`3iTy+Y40qQ`eE9I({r!E$Z?1#u zO6SH68}t!hMC1Xe@1hSl6B&0;p>)Wi$qyY9$E_^}TK+x+s=;|q#`R*^!I6$E3!B`- z2nJHQhZWDWHq2PpuDKC!f>k%;Qw%k z5v!Ap1vdLZKqQ&7At*U=ioWY>bK9D2Q0Vec(x18oAdo1Cld4mmnPA`Z<8=D8&{`XC*71gbiR>-Amce*8e%AuySVE#+ zFzyU+d5+gF9=BPK$><%GdcIJqt?zW5A(u0-jOyCmVE)L40oDQz?bFs5Tu+mHLTY^z zUH5-J`0L5?98ZopJd*`zmzj}sikFu&A7b35r04h&ay z&dki%a$kF#d477oRhN6aWWI>YOMgsQFI@qXa@_rcMV(92dli(F;>fr%cppaLvW5;) z5l$Gqs07(f!aQs72HyVSeQSin6Bhtbq?Ix+YOj2(azHxK9uEI{wqSu1*!%o`++vO& z`1!*pdK8tEY=1dAIT23=T2@W<+>wG%wUkj;P%GG#%o+Cq+DhcN`H1A z@|@62ab`EO9SY0OO7|Y z9Gt%X6sj-BRaC)AwZl`Ky1Rjx8?Z%TVG~(9kV2H9da%cnQ4kP{2cYo7EW}$sBT$<- za$&F~_*~c!imR#^muJfkjkvwQvbXB^V4D1gi@oi%u-5J%jD%7=Jskdum}<>f82E8x z5L?FAlR|OPU`+Q&Nv*xwx%P!%lk2Ad;ok_Z^xA!KE5AL$Ogq}=5Cpgjvo8?91?3`a zEaY6^8GeNShIYg|x%|Zo0sI5dd0MO|RMvMig!=A?Qwi`XU`qle@P6ZB zI>cm8-yfBl{{A6u8{9`IdvhJeZ$%ou+Kp2-@t451ZRfu}CizWn=#F4KF{(TpuNj+= z;CF`4UZE4;ON}tMp~n#AKagh>^E~pY-zIXO9ld+GP=V78>}x~N^Y7di_fbwSJr48C zc%OiZyM$o73U=Z`?IbYd0Vd^8)cLu1(_(y7GMXjJbd3J$MI~+}H?xm(WNrvKeQA9>d zS_7>=IXlXP79^Z6US=QQ2zk(xB$)%;bXTzY!6O)0fi#}G_&foj;dZ}WGiduu>l?@M zsBoYM5Vm889Pzc_N9*o}8|suo;R9r21<=_cdv!2RCTzo{^XkJlreBjeA7K4N;Z^Z( z;mqUX9$4s`FAdswHF!Fz^E{XBi>>3XT6=Q_1UvV(=p6DiSo3M~Q{1m4vk{w70ks_Q z)?lbGA(bGb2|sb)8`2vXw*&qT5&l6%!#+wlSB%dsIHL!Ip$l>eQebZ1ehW5Ah*D&3 zVnu_6>-N)cPKM}6E|!4RcvTpeMCQ1SLubVYWg$sSL1eMyP%dPrB!CR-8reH);$)1M zf%4mg2JD`WL!x$!Pilz70%H5S)hI!>4FZL1ecf~QPa^;?kMts7dYo6%*#ro;xARZb z7(~%er0#%n0IT#xLLzEil(wTxoX1q?gXUjlh}}!Axx{qwYSRit@&|5p6nN8No=`hl zKI0$$HdH&Oc_O@j{+M>?VX2s?Pzo3oO2Q@eiqr%;#xrM>yEA8w2Nc#Y&rOfD-VApb zx@xlRu#VfO?)-p{H>R@9qo5g}ezW{|%^9PE8GWaOM4hKcx^qp~yn@H`9q!(xJlDC^`YesgsTG1iTwN(lYAlenjK~IVW4F)e=+lmwIZdEV;)=-qQTHQ zp+AR=0{;MZ^JT@;9lBp&{{HEUkiTR|XYn`(Q<75Y;!T?ZO>yhPzN@zNkq6sf*W&oN zA7E)w=az>KeeGY}*)6;ZQPic<@@s52ZacgmpdpRHUj~eR;KszR#q@ z8B=-8`>$Ut(Jt>lR-}C3s1?1`x_e;Gb#a@RaG}w{{PD0X7b!D`I?Z4YjYpXgy^PX< zL4lG#eJn>jSQ_51K+w0C<-umVM+J5Z)s zf1<)08GFTtmgm%wY#m?OE04Q zLViGwL+UOxSYf!5fQRn z>}Qom%rshs`6@@xoqMROI|)eFs?=y6i4*>Dre+7uu-%KT)@cNab%#yTr0TIJ3NP=x zRm~cgaVk1)TDoEqXs7(+8?iZ`T%nH0juAS2ivj~iyM)?xT~5WR$s*gIjR_^E?z0%y zQ8G?*7446B@~QeHxVExYCiguC4l{T)uZ&D@dC-YYG=}O*=VH|H#`gWDugllhdPT-o zdw@yVlgmIV$bV6;;@dVMP6p_flw}Aa*#MP6BqsYz=MI`jaILT{e@Z!{e4RhJN8{B) zd#0~0odVxj4zuevy>|G%o%V~QW&G~8wzfYd&VYVm>9umqq4z+U>R0hP;19N^xM{tB zYRY!mK_PhwfoS1_x6cbEGYMzLJQuHAU~_Jp$jh^?*ge2&cm7+Tam0Po;~8_DUVN5M z^QQcMW_O;IGX5!taAoMq;?{fkLRH4`p_5$eq-cC}a|P>E{KxzoV#c#K zE*{u3h)aCv#&T1auAh%Qy{(#V*uB_aDa{gR&T~w2N?vCC5JOw=T28`L+UU}`Gs?`H zbDWiJQ%YW#Jc+8POd9+Pk zVqHhdIKE|9_O(1K9FOg=YZnEkqX2Rc8;5g~8qkc8?!q9RL7tCH4!>?cluJEsNOOpi1Z0}BSP?t{b6uX+_DaoM9tv`bGV%dUJd0QDRmJM2+-j2j0?bXTt5YRF<1G1 z-(OC+FmQF|#lP%cCm%nMkn=%U&Oi;M_uDj`%T`}$Mf{un9q!iOLeHNfA1r8;T(F1@{plsruaFpdnmy%G*edRz% zDnOVu2<0M^vus2G+MtAoz7(?gQs{`4Yv19=89>*vZq*7>0i$0&76H{9#Q7xn5#0Kj zGk0*O1fa(DhED=A6<3;5CVmZqHeE+|wVXltM>7Oe;Ax;?PWh_#62rzA0 z00e!!@sRIbP7dGx{hNW*aE;Slk)KdK12vv$)2V@H!EGL9LWx6Ix z`^GzDzm+@Z-ICK^MmQTZzEOy* zI04DgnmB%tS8<^j>e9oxllPA?$QZ^&apMw^V#!IBNr_Cp-$)kOz?#$Y=@9gBI6MF!1 z2=P@JWEK>v8?g1PwR$}l6l|gp%djhdkJL@o+WLCqG^=ueEWiJ$=QeFQdU1U~UJDSuURO1ZHqBw-@uAJ_JF$eFqy2q>5hs>$9s6APp1= z6Vi5fIg!7;ZdD3~=TWwxw#h@Zk^bp?5V58AdG4b(MV0%OyJE;IFaKPER-sH%sUYuq;aTmeD zfTVg32rCyEpzk~WQZ3Ma|ssl^R|QqavJ+Y4F2vCp2Z zzyikikVzm2%MB1zkJdvA4$1K!OD~AuZP_BQQoLWB-VC!INx@&hxk*ApJHJk*mj(Ly zw47hPZg^zVm{yj4@(IIG>ZJ*`^j>#nn zda28s!Y2;x3(O4MvE7i#UTvT^{2<-A1 zGDF94P66HKdXS9->O_-({yC_(lA=k5;^Kkf^KLO4{`&O@WGw#CU2GGT_nV2zSK6j; z6`9ishC#%V`8d%ax3-#|%iosZp#kh_&+D}r+!amFKt%)nA_Oy`q7k{p;sd6-OlB8& zVyIeeU0MI5jQ?*?2U_-_sb5t6D`{!vAj0fL=U7UDND;Jv_65*mq^u(g#0v0pMY=x! z^0f=%#?Sk{)-YFSJ^eTR-94gxobMuX43YII-Kh^tVj&b@B)1vP0@8=PO~f#INc|Dl zra6H%r4+3p#P3wpk-t)vsSAk;%5DZ2>Np_=PyN4f9WpRua;%WEp_QA0;?xhR5J)9F zUu7rF_TbQxUZ^|QxKqU2dLxI^xUy4*x*r^Ba=AOcL$KXQi}Pt&?2gLf@X>PP8r%A8 zs@!n_u3I|vp4*U6M$!`?VHr!lxwr-*sat2ZyAF?|n)0%?|Ios@7{lk+t%VNWNajZN=w9>OgPY~t^0>7gFFLaY}&PhXJp zzZ;TzZ|^`8~lzr-rsFFs6e4Wr|oQht`p z2q;Y}{n`eLj$AUI5`;ZqM}`X<0!X#bTG1M(Aw&WYpa*m+G2XOk6B%OweI^70UdRM| z`0!!-voX*g#90n^5C~uV{8(_ihKoFmveaE!q*7nHTcYz&a}R#`7@ zDH>dl-q4zm3|LgB;QLV4kRWE1UIUd?I{9;LW`!IZF?_VV$?TB*$PHa4VgpM)Wk%8on6S+Ax?34D_+()Kvsq$lhDTnZ!^gNdq|Cm670&J9wdF!Q6RQv^H+rE znPfr`no@7ICqZDN1;eO5P+d=Q4-xP;@{zzo@Oo5}OR(iap9{fc6sXN9hj0GXLpedt3r(FgW!`*XFx0&7Cc)L)q z@pk8g2d79QJ402tkP0Q5p{Myp&WG{Y^$$DW3^NWYhl~5Zl1Ot^m$S7z)G(`ga;s-& z$F9zg4#(9MP8F1dsirj!m|8Dpcj`)cQit29lN3g%X||M!S_%+a10q6ThJ>w(zlOd3`E6*G8INT zV!X%Fa0zpsxiH9|oRFXZS_w6co*0|7z7b9O`2f4F0;>4b<0>j7f&ohe$RwFsEr|(I zK_zn>{$37xkM5i9yp-H%YqNDNe`iM%O`^BNG%{I2;-+dT?;l7z0`j>kxe_7pchz2?@3D9HcO@qMdw#aBaW^6 zZ^yE)x65?5YCRF=INxMQ)4!Qh#ZLe9U&-b)kDhbd?{w>`kg7S*G+W+izoxwDfqA?h zy-1nP^lwv+Lvllv9HUQ2EN_r9WZ86Q%uv^@pny6dfp|nfPn=7jxA9bRou=r)d28jn z$Uqwm$>DI&WOm)>&O6P@+|D&R20312qiQMcs|&Ok))0M zgJLMC1EZq@AYj6g5|jbbkHC0nmmo^@MtP1}Z;omLH7_sLFB_{DgCzecMXG9-$BR30 z7Jw=;(9lhToR>s|;YUXDG-O;qf!>RpiMaJuqh;C2oTGf#d6e$BEOD=UgADc1XTv!{ zvApiTBM)NbK+I}_1gIDNH>ayXlDDXxzf9r(VLG;jniN(}@4roAc=>;vicyHKe;ONG zXpv*g?AIM)X=!%Bs=wF(ek@!>B$fr`N7>yGFu_NCH|me~d?@i9D)68Il=nZWU)Vil zu8$&B@4pCXwnE=~_fnvOgzjKHWEFtb`QE3JuVI^k-+%`hII=@Q6rGZY-iNh>NVEBl_Gz7S=!v*wgKAz@G_-}U$R z#Fz>Uvq7;Mh#6Wq;+eA5M-#2zRda2Z-z^|jkR;AL`-#uuCiUJ(Y0J#=v|T^^O?a+8 z32`qS+sgKi!;I%L<28U~cR3A=cF#P>O{Hrp8l6chm9%G=l++|(-f)jy>#dD?u?#_g zZ`9OQAICk6M?)qvP{g)5PEAf0qlzNFMDl{) zr2OOdOc=$wey;yRqR&xR*&zzTQ{_3wna1#8Vs(wk_9ANZ?y+ONBrGLS_7}MwRT-t_;NLc4C~49A`;DB%>5$M0`NSg4W;(%(E}v=k0sgX zqV)<6$RVnlS~6)y<2zbV z_>E`0q596TIPJE2n|;ppcFN{x^HFh?SZNh!=bW>3wzi~UX>YHRY54nZa6eQOsFDQs z+OYVPY)z(u)D}k)MKyMOnho_82_C`T4l)EvgJlbeU(#7)2|$(b%_HG&GeUPbdp%I* zRNadUQ;n2n28>6ompx~){vpkamv#`JA|L_en0>2ga{KD_0@9H;@5-SE1*N`XY&u&2 zaSTA~A}1v|7-4%paor!H;cUpqeI2%5a2tGtjMF1OspHj~toUqQ4aC?ITCl_rr-got zmI8ee-_|BfMI!|Yz7Eg|Fxt^m-eedZgj*%Its&3ooAkl=(U$!6mk@$(niDe67e}f? z<$&u2XN=f=UN8Lw?xHt1s&dRT$~fx&2J29q<1jEaGs85*yey^lT^oK-ta+@;wegWb zdc+ovAs@~rOA`nCKJp0f%GLjC)nd|{(Zp8GXeD}MA^5fk_kykeLL1!5le61|@z+6< zgUsfI0RBI;_wG|)UgW-nB!~ev1)S-TDOrC=W(KFcj*9i)@J`6kU`$n|!cRcZQD6on z6j;{q8Ph;j1r=;D&QzI2gtEY{^pWu=z*b!0DWJyo?Ab$9`+YmFSz2Z`U*=v*ML`b^ z`Vhr@bg%CXekIWtWr3;WcJnZy3Nx(&*&lRA0gaCyU z4-23!BbV@VQ2Z+F0_lvQx1{yIVrHg*YMtPv&dxqb=kuH-VIlmkP|(M`{oyA(2Vu&K z%}fm`O>J#{5KMta2a@&Wi~NMl1w2;FRH8wp1qjIyhXi!JQjOr%4w^MR;rVH@%2VPE zs0T9IJ}J6XyO5{4aQpUW#WHRCYx5U(KXmNLlGqh1(s+VBf&@&oXWIe$jqF9Q0+Ru* z2tZU63cUBf4f@1hiKyq+)=#Sw9WKca2=Ng!2hhpez{eiwMEq%Q9A?M1&boX#$3hw$ z(v-k=1daD^ejQvk`wzI5Ni9tPMQo1q>v3~yuvB6fU8&X0c!Y`^!^pVTRYVwIQ! zP_P2j#Qr1^T~N*l1P4`wqjB(t60`9Ho*(I9a21F!mEb8FpakF_3_taXB-o*|v^X1k z;^h~D#!yJ3rdDLAZ7zy_2zr>}_ZTvH<9PNVg}Cnd3oCBRId*12uz_>rShU=~w6wzT zaFfZS4~HH@Nki=rXG(NIMJsq@9o$iIFv~~bS0VZzE&Ju?rsz=1O+F5Uq}NN~mjP9L zAWqf>c6@uRbZXuJ!GJ^&)&Ui^H8qUC1bVhvs)j|=(i7)35DE1nY92>LCF{m z1vAU|_lo@qFZV*rKp1v~Gx7DL569(C_?U(UMY(t)BMi;|e~o>2IM(g|_eDub%D9Wt zLPAKCTvkg)MulW0n=&%8LNsJ#B%@>{sg#jX_R8LAD2YPI7G;Dyue17&=X*SVJjd}n ze)spjD|B6->pb7@*W4G^5FUCs;aKVKIC`>DqJ`>rWhH3abwoOhb5AyYHP9dwavA7d z+qeYjqiZ-=DY*565%&3Zu&*-)s!h_2g$5v|XF@p0Dl03w4T{2keNH;o30vTS$Au>~ zHE+-Tskg!(4@aSQ<9ZI~XTGT;)XQ{34w zA$=W}92r<{p=F2V$YkZF)s*s51fySI_WTMR?lKEl*=Q-}b5!JahndiaZ!2hFm)(z8 z$4Hw?Gs|)${KX85L?YY@{I}o7<2uRiGr=UnXwtjNJUFk~hdLFI_DJ)wQPjHL1qH}c zvYKTHBm#{9_z+aSHts5!J&~+QT+!I2uHn-}BHMwbBf1es)qD8vflyUZ%*NsCW+$OYkK?oXerNaI|4|{-NV=9?TKMbO?(Th zG`VeH?&3DHjtA-E@Kp0Y@lOvN^?LF0CDE0`?v03T$p`AH=!8WAEtN`*DSCj<^udH1 zCQ;{#yr63{M*b(h5m&9|`v(8WIYM@qxiNQJrt1LCfo8WlY5Do{DKgu*Pl$kJbPu$g z`C7~yJ!}Mmblq|TQ`7s(V*-MwE(+Cu9B`=T<;(~#4=%qT$htN9jcS*hM%=loFd<=p zlQ4kTa<1|*W)e=(6aczZwQzSB$|fwMFcCmO7wQ1c45KM3Q%I~AYy6}emoleB+Ls*` zQll!RkNDW$LE|jec4*5}_aH=T;hf8zT92C))=v!QA38|W2!7mCneVi##Z24s9Jj-4 zVo;Ovm$EkyBz3y5OFTR~q+Tzld^mC&Yr>n%C%NvNIWJS$<8sgIg_x=LubM7!g(ET` z3rGS=hNMrlcXb&R*aXS<T2mc`& z(-$jNjF)?PF51j*UbJ6hvs!AqyF~ap@BV?qTxJe-XZA1Ex+LDR*8bDRqnT1`bGGj< z`=YxTBzf*mOVn=|ilNu%01rzSx{L1MHP)}tyzUYm8!OXx=x%NpzjHC(g6SSnGh?eJ z4&O-sQS*`Lxds#MpK`KR87FlHI(b&NI{wo9(5>Yb87My1nzZ@&6GUQ zvquj>OX)n;8HA)X(*KHFs1}VKRNzAoVlIapmu=>c6@vn4MOv0hq5D5S!;5m~baotn z`fPM&d8+ch7ah&KRjY(DzxzkrJ=;u^jsLo0#i;5;X(?XiO`x&df94WiVkd6Fn|~h5@(9+z%?P*Eij&R0$MZ zU>8&7rZNa>C5tm_7i^f@G*27qHtO=@hs+Ee#a6$nb>n6lA=`DU48OZ+)RR{>$|JzH zdY!vp)m>nS%}W5WB2fm$OnE5c2=g@vfdk=FY3O6#ILoZHz zM%cPYMKqo^uI)HP5xRsk30)&{Sk7U19Y<<|+SD+REC^gpOih;}H>DIhG8*rirQX<> z;4~%XFXX52*a-FB3J<@*BL(h?1T3NjKsOb=!u%W?R<^69YAvCFf}3q<+6u`&V0`>g zAo@s=YiM)NH`3IK`xkrxuGyX=2x)P#_2);!jK<_*sEiAfl+h2Jjsb_Tu#;Er=Ado?+~T z1b{?qA^SPn(Gk=#+0FNF*$;e<;#hMPOFTvoXsPV02G&fCDLeUxuVZ%~a?W-N$KM&L zB$)H>VhDrA_kI(~*zi;OfS;(TsmW~}fy#S$lvNPJGHMZlX%Qt#rQ2~Ji&vY}khF&T zf_yAI=cZI=Rtny#Qdvr|r`xNRkn}dDpevViL)sl7?v9ZQ3lhTIRy4B-ymg#=9d7pj zmgJC+r?e1&9-@!1A9%G5i$e8-rkm(a+|TD>oCqTc(OQF&q?GENEAj!C1|7E{9s%fo zWvK|hqJjU1n9&e^i1CRDUJYPbhRD*_Mr8XyPC>zjl3zb}zaRT2)SzZvNHnWrw@&X8w`*VzW#o$P!d=xuC)nY93`0tjE(O#j%CAkOd7U{F)cN9 z+v=b!^Tuw@9>kS`3_6IiNx zyn5}c9nWNw3caCYz#ps~%{O}&+MD0|%@tDw$ak_);HrVd*5-G5I)d5cT2uD~w+a;; zB~+Z}i_FvYPF6PGHX7OUgA9dmL|XH03E z1{c4b&MAYOTNi2@YxNElzE`W%;#J5D1mlcdE7+lk7*Lq*9MOT64O~~Ly2Af8j z6=*0>Ps9Is7+MJ&OUthr`5h*{S<>-HUJiVb>Gm?5b+zUeNs~DyMKqU4Q3#X zfUj<~N`v~BB=33kL&AiU1hMM3U*7_@jhDg>oz=Hhcu8*)37W@wjo0(DUFRF0*a@yM zgy14A0fCBkY@?Awen#v~lA`R4zYA=X`SX^C!^mJ#fMB{Y1B8FfOyJpPcP2^s@qJ`d(;s&ZmPc3y!nh??+d`5I zcR9r{@v0a*Bd`QycXWS$jW~;tw-9l-1DCg_ieEqFjfX_9}7$ z@xP!ghXuTVg+R9S)P34ky{hLMV36Ud9Q3Z@uE*aS1W;5iFHouVcu#Qe;7$;Ts2lwh zaCpm_EeK*G>9oS#8qDH}KVeFi=*1#p;#n`k2z@YcCYx@HrrW!&IR3&{BIyr6C;q0o zQ`g}QBiJo|VBAS}vH_=q!P!!cDBaLdX1v!pe90d{lpxIT2eDC5PhxWf%}VXh#w?0S z18?5OqcOQujeQn%*XcJn6Kpuf@l&Uy@wDsEP&HhKfaz3=QIUr@?jBrLK)kXFY;j*V zxOJKBUJrdmjgwDAe^1X9M3Rti2CXmN3)uIGW88ufTns)>G=|SVlpv23{0pJLklzQF zQV%{z#EOMW=ogI1OGNZuWVyd1kT9T5h%g9-2KfWYtk5%H&((~&edF!{9_t95Xsz59 z$+2CLk(Y*DN*C((XbCG!nV1b~#_jbHB~C60XuWS&3t@60F{oaPzZ`dMLbki%{zELs z`NI3xA=ehyGvTDArDLSoaO9GrDnpU?xt5Zd0ZSTIo3^gdhiIvKqM5~Nhk};Y!GpA* zONh`F^{j19)U0akDMYy4fFi5}59O#SLO&C^Ejbt9waOf5Uk`j4#djN4imn~{6pZ^| z)&uWg@I-(azVVx1DH$&M__7cK)SRp3HEXfl?QBNd=lcb;17Y{^S4auUbb3slN4%HB zA3=aH1_rKOErR|)K{3$uyP(>#T57ED3Xo7DB9OTd{sv$M)5$dbOJ#;nLoIf%_b)6h z-4E!Aan&jsk@#|qLYL$dFM(|#X1aLURgl!-%QsQc)`*XseC+1tK`#a~0mE|}5jU6_ ztc?+t5SDnt)U}Qs^Twq}pI$h@AT2HJw?~3f`0M8o#B@12Rw;#uJYjZ=oFgXs0$ZBN zXDEh^m(B{VKKqHqy>+y;bt>&e?=IHuE3JB?o?Sk7@5@C2n<&jE4gPob_J$k+ctPfn z)v_}b?0T>ia%8E3Bc_x>KtN)5Hkx0;f<$*>F{ToSCC>bL{l_6M!nbozxL+I?fH(1A5^dB_JIm z<5I&WmIYJ@NS#-Hel_SdJp~!Py}doUK~*4v zi;u6ys2!!o_$3rr2Ju%rNR!}yahp4;pL#}`H^B>tSuy6m5Y(OXsho`uadZP?AgJnn z^#P4~uj5qQ_7JTQF^F;OnOSdHIO;(YeTHw6$JPg@KIuqAiU;XP1ZnSUdUh2YMJQ?l z{ei9DVinDPs3UN!0nSqWf{#wOs2hGOk)=QkbQOUlc+T*#w;3&fVl7*DhRz**y}iWm3E>BFQ4Rr81-SxQeoemFBF#t7%OfGDa$sBo zPq2Nc6>qY3dou)QAZl!f%QI4@QZp=U#+dZYqu$?fT=tW$cFjrsZAwatT0K#tK|R&F zv>&ePxlU(k<(Tg=HxbnpSQGE-rl%*U&UX}b#RSA6p$-v8=n$yVJ@JK6H+3qR_QlVc zna>b+#01$ruIfve&q(N$gKn4^5E>Re4B;`aZRE4ooR0*rmza{$@SqT{FfMXDn$>vK zNJq^d&BDZurMJV z%(ze(2W7t6NLpTVTmq_V3!1= zOn)cU5x)&YodTAgSs~nS$8~hhh;7~!qF|a?q6{t%Pc<@Q?_>Sqh9OZzLK{mDD-OWd#u^|;FS#-WLF z?TM+1ZRmpQ#gy)FAw8@27HXG8YbS#nI=YH z*mWMkFiQKip@+c-fb_&6R~^_r)84&9Gh&qS4_4`+u4^dcgHO`C!S9nL7#kesYuubr z{6OFG!mURwN`mAkv{(+Cb7iGXPES`uWm6NhYt9RSN@Bqskw09(K(Ge6KS&7%AfKss5k&&zaq^}fTfn3A zd1z>5s#d*{*0+(IPjEGdejcL`ITw*i&&{=K8;}}r?&^s-Jd#TL@4Y>1KmPdu6o+6k z4<)IGF3f^!%nCizwtB}t%<2LWXEkR*7Kl6UgiH(GH9UsUw8P7O;3s5S@{n3%)|4pm zF)KN7;skET%Lfy@(XEnOrCXmfzp(L#jILKCap^hNE6fSSMunJtUQ7;5bbo_HPcY!! zScksQctOfB13iUJx7O5eUlCkyrIx#)GGKM-1r-cI?oAVXHbTI0MSEV2n2ukZjlZvg zR3xYyNyjJ_E?ggf&7UtUcaxPJJ0kLWmW5P1Uf7xNSguZU8{&;YX7#5e!mW((1iwVZ zrQYN#I-&4@yc_TiI0!ARGRTRXtH2BZoZhB2^%&n(NK>(>#$MQJIp%ZOtV6Dg#fdmBfja^;3>I9E^TofrRg^nNGakiVb`%r2Q<@Spd{$Kyn0tnH8rhRHs%|vp= zxVgDwdI*boxf`iw8{vxp-r{{Qp%iC(^|LOWWw+=Wbm$NZZGq9W5REgyDrV5l1W&Iw ztWjM!WRFTY)oE-&s5zEy($|oXa7lxdDqy{cmc%1V%4+o4Q0ZmhKC&m8GLCr*iM2Ua z9QCcUb8{j5=dvX+U*+km3I9A-_-z>}n*m2nF?aFB>j;h7dek|W0>vir_|iYd0RG1@ zIj@4)rk#h)F1y&>-TfPWAl7a7@KZw?Y!farHN{$=4^O6=_f*T2jDpkD)E#I(p>MkJ zT9$|B>e7=g%fPim$|@7aWiJg>eY$e9;YGvDU?n;V0u3*i=LO$Plel@tzxR`EAudPY zqY^1D-}SVO7fJ^OgMVK9@Jm}>*|4X<D=ECnOC3bf6#vKx#C$~+_Hdzfksv7koW-L<5@a6hYlSg@)=CZ zE$`mZV_L(U@o?&6M(YTi%pzXDJTP=X5|xM(URxx%Kkt1@25X+ysXN|2CwT7p=oP|l zy5`K>&F0<|Pshz%@f8hM$I5n6^qoo!OH;BdOpvV zcy*u(cwU3Oo9YodgT723k8_A; z94aoijU=%@^}Q7rNIVDPBGZ*W7V$M@!S4%iUX88(rqfXhi7uJpbjc+e(#MtKbB%pl3bOMts0_(TEf*2gt@ejp&!fP1tOGK>Z)OzQ+CK zmmO?%S>38~y0p06O`tx>*nb2uLx2`DT6ggC+bdI? zF)mM(uip1Q9B%ijF3;Y5zrL;Z^@I?+2ahk1;sXi_zU9{MNrXbbRP^n#O;|h$Ac0d7 z3Sx;l0I4hSB8K3W1l4Ni5hv-)#;h5t=yv>Rj_uJ{$=>mq#hH*rJMHP$NB4E%8Uk!6 zZ(;GU+{wyKhF<>DcHEqH5Xup=%EO1g->G2znx1jmb)Nl{Z@b&d31v`1L)E@R^ zBZA_xdNSv;)mC3eBzz`@pIWg$^{jC+_e5$KD?(d7x(7Y$-D23;jsa=+VDa8o12JSf zMZ22g1XqAWa@GTGEaVt0!JIf&ILSn8R0}_y2oQ}e7qyx?Sv3R{A!&#A?$JUjM{4C2 zHqs-VHP*uIfQ+mPL5hN^Tjy3RI_fL4CfYEHO2#_;%Rr=pQ?ZK6tF4NZ8^upy(BK3s^#!Hp>p5UkvXLA!Qzg zg)thS)+B*}RaSX+<~O}v`v@u&^p$e=haXJXa%ogY2S|}a-p3>d1f0&e-gy=e`5l%^ z$2tSA1*gn-?Hzn=l2^(;JFJY&&6Q{}SV93`4($OJPeDqSx_3~^dt}k%bYA#9%Tgwl zgOINCek9t6{Z(2*0u4575To7N7fB2!rMr)Du;oyK3Liw@tW`sYUzkPurz*#rGMk$$ zdpA4q#x3b{)2+djCOR-_+ekh1kAJvDm9Ej( z-d;LDfq#}Q2u)}y7;_9R&YF?Do#7{Wc~PdxCLE2c+B%m+*P3Q#EHzt|!LaoY#zi0#l1ogC74`$J70h7G@m=~o z{$cQOPNjjYdh$A0UnE|46!jt9>hom$$Mutttu9;UF!)}^u+TlIsE4$01O_tVQoA!L zY74fS$N%yk1ir+qa|FQCbzRbG$m>cO1wse=AV6C4Wp~#Gu+?3Cg>*bt$JxVWc$Fg4 z8bLSRj#{S_etop+LqwiawSuCe$SoV)j9ME9|Co*57Y~S<_v!MDndg5JaL9Jk;7i*g zBeM|zHWy7}5eOE{^GM=Ogt!2zePjocswwD-{N0>ksU~}dS|AB>LH+^(5wOc(D8M?? zQ+G`dhLF9Tu1jFg$7U+Mf8e|%F;n9H+tzAA7#aJuBSF61231x<+30HrTj@}TJ<@RZ z`fq6HrJ)>`U(Lt}K)=Gakp+R|RL@ zycbTF?Ao)BH3f`BqsYjD?k*I(g15oughu)slBI?35W;Lfwx`wI7(gBJPi>~wm!_y> zH;kX{2r)a|qxAfs$#Oh%zQ?w4;bXd8*vZkh<6CG@d-}897^&pcC6T!d0# zMFxB0!B=rV_Nm^7B4M~}l(EymV7#S8P8IyF^}hgDPp?Zew~oe{ipNp3bIw}ScU(%M zJ^HMl!ZIOGeSbJ`&$;ztq1_DMFBCOixwS1a%2g=imRZ>0Bz2h|uE5YyG*|u;kh*<{ zujnl5tDq%N%heyEw}An~tUhwhocF`TU0dODBwV^s4gok}Gw4bn2p(gB_>-9-=KdgR5gRN1EKL70g{6AFIqC65W)f z0tx}Zafifu2D3ph97xZ9k@0Hd_58)zIE`w$#aoLDf`<~1@vd6$+w4RzCCooNO|=yM z(9k>AqHHEd$$LOhxVg`|xw3VHA+@^gx=^o&%h}bk71g#UcxaDyAIc1nQ|5K*blYrV zz6rxS2>BhmqEx{B#s3x_@cThI3LKjJQuvDPmUq^8>3q4e*k7$B{nb%6j@lct(lb=i zUPs)5QgNhdy}OxkuYHVsJ-g55v-cXzlskq71%LIj1`i)i5|T@hOrDIp>l#D^RBjh6 zC6_Yzyx?N-2N^5fRt^N(OOgG_BI;?)&i@CCG4K5lyuEh+mLu0@ZauLLNwAEaPr`u{ zVj_`$BIQt9*uJqRiP?b8aQab#&f}%m#^c6WwGeCh+`5exI?X?_S+pJ2kqoYi!-Lhl64I{pcgTw*iE*esCbe40r%@tB)Q@0| z{({z&2BpWOkPaRw=#uejK!QN}EPn6M_*Z;-;kzd?OGIa*Mrf;2D&oej;yeKKdl%;f zh$7-z)=gbTfe&IUl=~nVS?8Y|`=toj&(PA+&oPdUp56r6V8lf}Hl_e5;hJybH##vf zF@iMV8$#Z97c0;kohc$LfS6Y;9%cp_!=~lwP=9M{*VKO91aTX7Xh2CGc%w{O0qGLh z3f~=c7D1RBwNs290kmNd`LLeVUyhxfy%d+C#E|&^(AjF!wLRY;tOuF{Uvu*rB&jlz zR3tFZPoKUV$o~sH$XMXU#bQLH$G}R|avbS|gQ9A9m--9#3S|1-Y&qq3(iZke!g6 z?9aCpuObtmd?L)mqCrZH&cwrh0AT@q+T3>^B+dEVqSpPZB*y`R~MIbxu z8)9IY;j4z>0Z6HmjFta#3TBHJ@MM!7Z;*o+x*UHRvtQHbL%Jt&XgJYo>a8=I3-S~X z7JdW%59?wy=V>?&aEwpL@bFZOE@AiSW`H4R9DpIohjfFt7Xl230vPeBfnWsg8(=r( zh9aM%&lPX2);f6-&Gh~rrwd&(I%H|GSmY904DIZ0B=RlwCVnV1gCc)R?VS=23ov$q zQ-DY#B4(79U5CI7h~np{iyIhv-OM1q1ho5H#{@;4U_i_8ohXJ4PSG(j)~_FGJ$V)i zG=o4nAZMcU|8SyTdfW0JXF5T;`CKaG?>>l0i3m|vtiYTLb?Q2_qF?&I_d%@(>4lOsa4igaq{tc0!WD-OyPrUs#wgAuffrBWX>kbbI z3?kNW${AUzSeZI&In-8cVJj-GHzIc?l4; zk*V(bJp$y>Ub!54?cy&;HMoB&B6 zK>&h$!~yT7>=#_yo{Ad_cwNk#8J3DXec3??S7%vN2}@!nB@a3bUeuvhU4K|WeG&cg zFkKlIJy3=(NQQ9#`HRh~~V;s&? zKRQ*Y=h*al)FPFYD_-=O8vx6X!X^MPYietu&hzkw1`m-KIN!QZT-?ivvXyVpHUv$Y zzuyFJi%Hcr+{AeOyE&m+f{>0xqubvyWqqJ+a1P5G6kV$B8QLncd&R}!jdJJ~46cND zg5;P2QUJz1$zcv*l8l?18?Lkc@LPjM#~W7dX@>=kTYsjZ=kmK!Jqvhk=;oa4{T_dJm!e1ZN(p?M)p`mN}^^{Jl6zbkBd0cA3Jd?`-7d zm!b8!-afsZ$l@X0!?ZweXpn;^fE+A3Io5ou>m`Ap0aXpPhoH*{tCKvM$~O1Uo=j42 zOL0^dOs|LO!e$eXaE8jf<_FpsT8x%F=9~Gm8YYH~Wh0*7-WNmLI_jNj?J+Dqq^R6x z-Fo)1%Hlk5C!omSXu9r7i8uZgu*i!%0zb8ooC z$Estt-tf8K^@ffY{I4GlS2jq$+wokRBjr_a5c7;&=yYg%<_5jG;06;j1^eKrH*O}~ z#wXN&NuUvQIj$h^V=eSE$^fEx{=>?JS z_(I9uyWWmnO*J*%@`)MIWrD~#fSwdW&KWRT66=n*kB|xmh~l9hAQoh} zk-zjw0TV&=GCq1RF!J&7Z8^u)<_vHQN6Z2Ap!zp%gVM#Uel5%9UVLY@lCq__0dkLr z9~{#U0ntnf+nZ#|p~#GnANy_~_TQ7@4UJnkOA`{dLa28`lqe)m6|BX*iAM?sw(ag1 z5SEcJ!af6&j^J(l{Iz{|#Mfha1fp0~>^T=lebNJc#ZLUNm^`ArrvO|KdK6zSyk+iCih=@o> z)Bb%#N~iDBYaWh47pM+sQO&QHQn2>{|ua%Vu-kT`1i zGzbQT-G6)8P0YDW*Nn!iHk>oWnj?4SDOh?k$AdAf!tftRx`{E6&{3>A^8X?Giqjw) zVJARS$^6JW+%b;o$k*ruL>+p7$28)kt^I|z)fs^W*44YhnWxuG0xRLc980@ z;8W4~OF`j6Y@@XX(1niUW&SOuB^EJU66E{V*NeIodpjnJGMC=x%hZmFWM}|TYn~Jr zU7dLPhV+@yio~X9-EHdnS9|-;h(@7?!e*l5kGgW^UCA%HQqC`L(W?Z3Q5l{&+%zjV z>z8@|bG?#tNbV2OP1Kc0fFPD=gk#o3x8ZYH(8B;Bi@tD<+l+CwG>8kLeXI`Nn3Zwu zzKS4dp^u+wOEK4wcmw0hkS{ z8qMuf;e!7}L#U{zcG0*@c+isNMg(}P>%gsVov;CVrq>FV4$>5 zA;rNfKukg!j~sI%|NMW6}=9MP9l-e zY)$nHro)|BSc6UqC=Tr(9dp0qWF^9m1gVut>MH;d!&aoWn3&p#*CX}~&d3QyRvZKf zzX|5F+TmG028Jxg!mwU)$l0Xmjq6(dIBzpiJ4?^6yzBb=efA9&@;9%>Qc`g+GzoJr zK28*CligTcaP~%D#2!KlcradfC*3k2i*Qx=Mh=dFWB!m+hDJpAAX~&)rJkX#LKI?A z04TvHUZReP)W{aB`-u&KsC{8jMeR}F8*po^m05f6IRK!%)UbH}5G*a#s`0Y1tO_;x zv06{)kN~n`MaBR`JwRd3$COYo&MznL2L(Q&+}ztAQ}o8@@i3AkI3#FeXn6&oYnaCno1G7yIyzhZTAhMi7BjKdXrHzT8^x99v`;VczXlWibYzWQJPX8z#E z42ANK5a|f$X%^9UN>7UE60pxO@6GOup^H$%5r&S_Nk__ z@~0VRue$A2{}dy2;MrM`JMI=(bCF~n0J7RnW$#NIdj%ZaG?UAqy)d)>n9GdDaVN>I zyDJw+$qUF$rlHiuQ9RWDt16U%tL8E-sr)7fk0Ou6h3}6VcOhR7!%u&1r(TZ9DYjmR zJk=Cog*y8`ylurz&{Go=4i7^hgK`}#>Wg*hEZdP@NK5KL8TUkD074WB*vX&|!5yc8 zSzvHcy2q@%V-fL7M94r=Y$#c8J0bCh>>IB%QZMlu6RHXnu_DjCBUA<-WBNfj{Z*F1 zy@9r^9@Eu5$#zkqEw6f8)ypb*>Fu{qg`2bX+BPDg%AIM;*0EaG2f!x1IOR14v2?D6 zCz)80@hai!Hn=dbjpg;rsFb?a)#52XIIP}&{aV%!qMP)wBb{b=SwtG7E~piQ!`~j4 zgYUOHe5`2ACZv*T!!BjP=5-JOT{DuXE5-}H2KylI@k`qv_Jg8=XxJfOCm#$TM2m|ool%3L4!gX{@8RA+1sHpokE58rgfhzRi&sSEDLnhaK)axfJPLFHY(m1@SvvpWP$jFnTmlOQj z7JMvFd-5g7e@3MMOGv;8?^f`;@Ji}EKeladvK06Q7R#>h-=;R*hYAkMxAfV9KkdMT zD+cf^fr}Y$Fcu+7)^!i~m|({$Q`%yf9t$fdAZNV)m^URHux~uOkaR5W&{{!lk+f&H z6|LFM;f}{Ml1*)$pwj~^*Cq!116MAxT9jt4AVdZqo%Z29)&*F()ufz5?%<7uy<_0b z`|2=jA0zf)uA6uvwc{#|+~38@&lcdAD;=G+b8ja%3(M|Orjti9cD^%+f(ec2hT6?o zZ`oeYym~ccyIbdIgz9Y1H~_sckLOHZ87Ls^;ac$lSP6mHcJb?CG=|E`%A`#Yb!#`a zJRdG^0c%gXYcE&pO{4HOi11l(Dm;06^Yct(wf3?H4^@P*zhG+MIJp6$7a++fcwPyb z7_!(yRzqKR9sxT58!d6tqV@~2cs2%qr$OAk-%zvi!IXJp>PD+=Y)ET_MFOll)Q&;Q z)FXIKu3<8SY9-`JP*G7If=9L(wPoopfR|9|Os z_U>($c_+B}0rDZRjfLXD#5fG*iv)n@t|4MOX!wbnwN()!IOp~~;^HiaOaCSK`8WO5 zJZ^;%*#*{t__OIc$jEQAg8#g#@HwQ}Xcb{1vIzt=G{#|^Bw8aRMKEvMRPYWv7ClN4 zxkiZX1m79h3G|%q`bYLDR0sd#ew#cllJpAWUTa^kSxy)YS6q`j=XV?tpS|wH@%b0~ zWoNUa$0ovRWA^sWnhVwoDdQlivMD=|pu&U02KuhA0|Oi?*zl62c--10VZ47%;QoADYhgLs^UEbUI2SUIKeaF z5nlfE+^1rDv%4@xv93IElS*Hdey`AvJWmo5@6A^5oHCE95qQNlDSMG6XXjYY3jYp^ zon0&S%y;%=NPWqS2h|l-a&+Q1bv7}7aO^E~9+QFCg;C~o9K{v5fsq+@FjZ@xlNYWD z%#Zg1@Pbo)%6c}iKIR6~`A?F%KW-O%3dQO`-yUJ}@9|$LvtGEd?;{TD{o&Ro|D4Ql z+9jUXb`Z$JbEl|iqp-W$yoZcZ*|s}&Y9=F;fUlW8e{s6xgV4habHW-1CR(@tfi(VO z-vlP4SL3L=kQ9T7j52{OoX97M%@Bm@jKvzL{gqThU!@k5Az&;3egoc9o4fw^= z<0%kskWdJG%7_w_f+*>~JQ&0p#KWm$)j9#yks=QGgU-&+JDh`EyZaiJ?0>hRx6(A* zN$S5rj5XeF8-Y=*gxwQxHJ_j$k5#2Zhb+S8(KrK)r0e^ca~-;AB}DCrShVj^p`i!9 zB)>h|BwKcq*e;vY9`*}DUWu{fxX>atWhLPOfx}#%57x^Wvizfc?@Rv?H*q5qwgP<{5N<*AX^lJrJ-SAsc z;(p2Hljp5BML$~g2o;7!90$-h45bD3k!%d>{=-0BDk&*ZUHD63!M6zt3okD0aYz~< zhD4+^E(xhFhoMw48d>=szh?VMx-GPkSjvsEivL&=n?v)kq(aTXwRi72&>`?u;!mNU z_p-ile~n(zg&&VbVtfQf`FCi9c*8xCpt-i zrop0vh33W&i#*#9PAV;cF(4HmzH_j!7@y=AT#kzg%)FE)4@g6n!}nlrL0Bb|39oEb zocq{=f1>q;s?t=H+gOuKv$jW@2{ivc0r0Ad?&2q^aLDVh1I*gfei&^X1L@UY^7sAeazuXTXvZ3MD6= zZ9=A?lSh>>Ctx}3;XP@ZwM4`Wg0F6jK>`i?IrFT6hQ=WRVspWR^jemme+7j^(5Uxy zbL}FZNIoG_@Y)hWoMjtOKW3`Y;0bArw7;yX_>-VZe0@#b#56 zs5NNPt<$J*VBtBE@R$*K_z@nb6mDlB+K)~oBpM5(ecIvS4DbhEByjxyhgsVw`px7$bpjlN`DGr(jJ0y1U$aNQShR zCL-T2S6okE?Ws%ke_8uuapS2ur4IyCf3)xfUOi~P`i+^_Ac8fqyc*^->1J4nup?yo z3g8+>M#lGi3&Z?0LaYT8?>}X&u-jpj!iUGU=^HqLC}Y$oHoY*{JpJr!glfkhZtC(M zstP^c?IWb}6O>aX|1?lh;Vs=~S{l^79|S`iMu5Q{XKLS%I7{+M(X5eyTC8 z9Xq#U%1V&GHXr>U61b}=U=Kaw2DjEZ5({(Om=^*3_<_ObApA<++FB6C<6ukyBt3=% z^iG#7uASal6C-$K;~r1~)Ur0uhHjvQj7{G5_iI}O_a&F#JIKpsdw zIwYo;V&-{5{$_+)25#3#t~Dh+$GWK(&%Wc=%6s&BfS70TT09)|J!rqJbqE}j30mZE z(qhYoIH3vz0kxV0MxoNJis3BE=luNrK}7dzZ%O;N&FnMh&Rg|1bwJvYvHDOD%oc`5 z(?$$Pz*z0r1g1V5=ksEV(aC8Y@~dpg_XLiJH^DZm6(7RJ{5|Z9_-4;T*KOL=)VBT4 zURNu&8%5K_rp1gG9J?Mj3`23sBbh$&r=oYKqkCC z=6ATfutJgUxxf#+5)!vPny?qZ&5I6MH=JuOt8d|@!=)uAdPg}UF&}3hFkajy-56l8 zsQSXl_T+Z(A{?R_xFp?F#3od*N#iX@&DHZWd^8bD|B&?E-a(m#ypRY z1X-d!@JRCu2pj}t4nBp5X<_u{f?WcDHimP6@TMDqb67sp#kXzJWoGch9Eh5$5i3rez#2;bOxI@blo8Zs7N?%PJsa)R^a=%=Z2sUH;TiS@ ze6Li@6>+3YBv3g#fV`mdWxLoJF@8Jm6~?dt7=}P*xMyL{#&yDea#1uY@OB(g(jY#A z3BLtF4>3|MO#|X-0YZp7_^k)ma0XZiWnry6wdUg9D3uv#hlslXBC9azM-mJS=T=CW zgGdF|Q#jif%~ReB>dkM*Iv-J;_fjEbM?=pV@+{GM#_UsJDLh*;Ke6&=SQshALx&u= z^65_Z)!x*e$Lh%yP|8C6xHVK!VyF#NP%flm@=~=PUjTpnxLPj6llL8;-_dbNTb-(=`t_t8Qd85;n5PpvC4e)Y*#NT!gj-vNsa}vi zX2#_+LH`iVX7B>Ab8==UU-$Fd4_2scm_%I66Wh^yMhfYQ1OF+rEzQOWqL?eULbQA1`a_s;f1}$_(maJN zxry!;*jFqT_$yZ8Mqk6hu>$)3*RK(XLW@r=g+jbXe`H4B{J-?~x9cl{d&M1p{Nzc6 zrUNM4!{CUzj=T6f02O-Xc0~z3j@2;pBeQ(9Z2Y;bca*+8X@7)Bc?WW=r!lOdY;GN~ zgrF{leHn%&(gK=J)1Y(&qgRcS$p6qzPus+XNA?Kfrb9A`-GwHSD+C4^JW!1K=b$P? z=^yoV0t(mT(cIX$E&eV1tZ)vNdMqpgFICc~);qU)!V=@>0TkvS2soThf$}|%l+bMA zV!ZG)t;G_#{UH#$UTW?)L3bE@!m<>@zb>Qdz!3nTwF`>4;n0$))By|{868JO^MV5d zxBSOBM9JKqqWx_#$E7;|TE%fr?`G@WEb9+&G0^#_cX14tU!dK2SIgh@!8cmgP)onu z%4?^DT^8+ga^GD%z1OH}x5Bq*YX5v-oAuP_w0N{CXGryv$xOkyAupx1>WKWFNOX zZuDg1k)LN;bM&Ex;M(ijf@^Oz+YU|HNmSTL1in4{&%(vU$G>>PYCG~F6*`{&&LmHt zS7wcL$gyXog3wC?P zWmlf4YEmea(sw6QwYcjum5;f$H+iHN<+YE<$MqlGDKTlzHQ!&C8JH=U`1Wi^hanw) zvxjPq*qY{q?!+GREVs8#Gl83r#;+(0yvHYOHDvYdL;0mN@)f7mlQr+bY@X8|)M2~M zEzWE7!s6Fr?{mVvD3!#PTSm{VGv`|!t5+@`+{d|Z z$BrEbp&DwsJ9g|Q?%1(Q=EptYH$|W1_`pBA5f`BPKZ1w z?lwE@TwI-P#1K~QHa0GZTdp26yJ}Rxo6a!bbkW`BriZ<&%L#pZXPX^rt|z3^QLls`iV%PtsJClq0`!NkLVni^#$D*!A;` z$2bBY?UwEzNFo%T%LM;jiNc`|FI{~RcA?@@iD%HomNHg#^v}U}pJglFJ@@YWbEBLi z$=PX_?%{hcDejxWS&OGwpO!NvzgtXrnQycw~Q(zOlveuwuK7o+xm;ATwu3+I#^mULA5ndQ=?UHT=69tc@2cO z;e)%aya$8gDq@Wnj6KBO6`ER*5eJb?IrbzeM%83)2a<#sm}givEAei9)Z8y%hu}X8 z=E*C}sH<5|_xn{o>i!~HVkINNx6s{FxtQ&!o>iGalJ-Zgg(;IBmukEBc($&-Gpz9# zm{0Ov!^hJJKWu$e0CU^AUPWFS+1jz^dzWB`)i2owJxUs%PQ;GmqZ3 zbu96-zvR>Kz zlRI9_fQ$d-_klP+8YZJYtzWJZqu4wXDV+Zzno^c&3im|dg!%ZuN)@V_6j5@mw>4Lr zDyp2R(SnN%)SRx0{Y=U)oGQ0jXO0VOKFv`$5lJG3otg398ru6Gns5SdJXY?FyoPZR>NiH&v&x=ulr9T}&h( zx*>Pg8==h~(3dRXo1$cwl6o#BUqxj8x;wX4LY93Bf||9omDKLkT^vbpAFBGb)^8UQ z=j*tU<{D|h9Tpy!_dqkrzPlk8X2s~v|0&7a=|_X?SR*9WB|gaei~M}mtpS^!a?b*^ z5mLUzlbTFa+RlHzOP1|>o*q+JDrQU*`J&Q(Am=38TkW+#8Y&>u?3ZqGz5m)XVqZD) zxj4p%PlPg~&JcGLDhw?Ym@hz&y2ehnD$UgI-kMghjxvvx-c`3=O{I@{lzzOPUbQ#d zM(eGoJ=u0)EjL=;XX}l3>fWZFPV1;2hoPRfkZuf-bOjBw@OHe?enRuU4HvAE=uIZ72Ey0{qH zAQ*x6^-2-1;}&dowx2?-nydeqWr|eY%>;Tqfi-Ehk75Ce3w7rL-BXF{~SlE3D!vWJw9`A zxMlL)rTU(j!-C|F6;VDvR;S}3HKYuJBWYz4ck!XuWG50A0pwYu<(PK(v5~cCOBX(XElqi^tbAi8)i6)?{$7L;qb>X znVhfqP7LY9>S$1}u9n&wL_c&SDx;{#_BkW%fq-y8{i`tqdxnU*21c?9xf^AS_~85P4-Du+x1K#IFkZD9fC$1laE&|oCdK>5Kj41ZMA z8nNeUSJ$4Xtr>ZF9bCrf{8~kDuv!)&Xs&gUvV%|lx13CT@gO%Uay-h{_b8p2k~Yj)LV zVerCTkSNsJuKmgx%A@c&LeAC~PY-YJp{K`UBr8{aPACzt=o*L_G$8x>@|vmCwT^IH zVV0Id4Yu8#bdv6a_UDbPSsZp)g%@1X63=!1YT?y}cPJAV?M=#?dQQ*N)ZQA>Q!Zhm z$$iY!3#F8@*zAYCia(RbCXYvP3dl;8L>#6oT2;iSTP^tQe9$kiRgkbKW2~>YD;2F% zE|otQGedB6$YHo!%p{S-J^N9qdil*bZ0ch3*xK3Ys-b$E!s%+;rj@Pl*#Xk5w$$vv zd2v;q?gnt(YU#`FZP*OVJKS|&-bjIEk9055Nt7ts>4plhJ4*9A1)OL`HiP=9;nNjLyimAy;!99AP`MxM-vl-~ViEhOEgKRYzaJ z85TX@B*A`;jVLx+60M+%UN40S6PX#4`MAZQ0|3&|ckdRS1@lAM@~udtc&pP5M(R&i zWNyc(Q@?`$$p5kB_i*pxUgT7*fpbzZ-TTAVWhsJi5%06{HjpDx^Mn9qP-y#ODC(9d zeq<9$e`rj8iN}44tJI_;zfZcsFEz?}NZhrWR@|JD+VE;JekmgE+PHtTEg7P9c59RM z>}-)Yz1x2=_gUsnL3^tO%Z{`1D-m*11R9hYgB=L~Yg^2noVn7Y0DzswLNb>?#1vhd zV_FC#CqeHvVx#EbD`E?BTDQgba>Mk1$Ep11gm`t8umUSc?_x(IZZ>w5Ja2eZkNewh zIQs(P$U;>eo*j7l@z~Kj$o|Vv4ux}R5nyDeph=&Noof0Vxj;}-R~x3S%(#mjY{NH} zIeY=nbxG1qrOzEX0{fR8o*UnrB>E%)ur^p(Eu8GFT=}FfeYR?Ans&r&Z(k7Zl~iuX zpD2_eyn*cyeRo|6e!avVb(=Ox6YTg6fSESQLBgiHTf>5tOBQGW6DFB=UyhkPlqd{J zcp>kx#u3Ftk-R0#8H@tEIP4$r*GngydTYfiB0N#3qaa(AXYqbq?qsT8K|G)-=oRKo3&^|d&__gC(ULhvUL|lnD;{ur({{gA3 zfxQRNP3(F070f603TMGTBEzpBP2Nx}ar`u5y~K%ebLD*`G-n&6X}`t7>dN+_BjPUe`8AzYRK+3;>VM|ZyY?hswC8LW z`YAm@GIx1Bmtmgo_dl?x*c(IXcG;&m4`;IA)@7atK=&i-&|27P)pOx+vJq4jZuUPz zBnUaQKx4t3M^(9l^Ms&L1CJ=0lt-hFvL~^nXqBUAh@&*pic|L(eZUS}r@Z;4g_S%I zQw(O~=Fb*p8INwKJq3QENp0TV*s;~%jk3}^1!BEU|A2*0B0qjNkT=jwn9<>1mR;6a zx^>33OKRg6k`8;Uv+nQZ06T~l$GyjG`>P18@xuv$6)&A+v|O}B)`lyQsgaOj+PEFT zJ5opA(}oInFi1h0389}5>QRj2lSN~RS46k2`)R5W!1N<(S(OV~{7X^D{vv?FM-2yG z4}}n-)LbsBR^G8cQf93Eves@-8f7r^%` zIzge(=9;C$Tgr-6$N+E4#@G2E0Sb^Cst6R7S@NmWl!&T6?+H-?K@7--@;Dlb8vy1Aj+i|Uim|UQniw= z(tHxKeh*_^QtJRNLZVs6|?T5;6^GbD%%Rlur>2u&pO1eH;SRN>T1QX^O_{$e_r$lKfiwL7=BPz z-cVAv-;HfideAYrlG{A7qhmP3UW^xnF6N#_-MC`V1`tU3s?SxV)0jh@amjLa+u=HF zIg+o`Ey1#qLWRC!MuT<8HQ+>oWhWL zo40{hOx$De0p9R7Rw^DFUAdh{Qc$L?2M+CyivlgCYidj z7LqoaNWig?kzTq;C)A$g{IiHy23g>N->#5F2~_BdYpe1s*@qRNiTNjFp~dPeQCGYh z{9+>(b|T`ed`Sh0=HprbD-_OgRRWHUGz5sU(Ea_E$)iLrCa0Je5nEM z;{^x%t)VJ zxfyEps!EhDmrRMUHDgX*Hm;$tZtI>8qgvR$PSK54E{qzuV;k!;8;_Iigi27@=CAW> zz8%lJJX-lt2#d;gU!T$sB*;;YpwMUq)k(@!F@yTA@29r`a}{FBmU>Z3ivvL_nw^h9 zToZib?)}B^<3JTV5}yN+Z&}y2F^tQ3uiMY6?&N*3lUoXNrz0N11{G?w&Pe4)sqkhg zUDISV;hvA+kuxj`OCv*qHJNZLL!GKa4fHRoFPD9W&tTrSnEP36lx8T5h;|a@l?R#M z97^aKwlNOcQ`-+?uK6DP&)y1EKP!NBhN_!BiLA%AmAs7R7u43x``B$}8N#_I9!8;L z-Se>2JZZC1f_4rZ+)X|>r!{yg3Tn{3iBOwT?6@S-^cA}Cqm_Geg((mV@5`@`joGt?}=94Z%R97wPpr2^3nK?B( zr6Wl)F%E<2;y0!~?q{^Z&fC{c5j~lA^NKl)23J@n+bv$$--h=~Y0JYvPZ)C8S){!s zIlmg+=21O%$2yA(xf;5vKO_XgMb>;NKgGb3MsDUrAMMY_1}6^+arKM}?1X~~wf}I< z8E~y@iGl9e)IPupp#z>(IDfSK;M`f4f2FnFNu{_kk`Xx)5kP!2#|$cQ!eGXK_V(VK z_20Mgb1>^}Y~H<9|G2xKcgb4#R7%TU0Q*phjtia>Fy|Et>QlRlb{lo$+|!-$Ak85y zA}+dd-Etbu5}>K@eK6SB0$J`;$~#RQ_s&!VLj+fnng$H85Fd$zoS{zsSFKp$+u1Lg z&vhLRxrl;#E zSULhbBpPMZM6`9isu-yvz$w^1|FWwz8Il{RUMo%sCq#j;9#p|A?cQ8S72r3jo*ROJ+cU5dZ7mz=q?$0wjB#pUM{0N1A9w89^k9;i7c z)GD7d?R_+vw+Un#_=K@6!SurNIdY@4yw-rVIA#6T2 z@iE4Ofii>pb~Km&{@|{|F(zK)qN;2x%wTTcH$QA_{_YM)z*qo(9l7?z&+x@;PG+p( zwYGbhSIwpb4ghJK!Yks#|Qo&8axKH-Lb@3_NDZ;}k0648j$3jb>l(+c;>4TDL+asI-2swDLv`{Dpc6ti+EM+VjYT)6gVyUY8(^`n zHQT`J{Zf<#{Zi`ub`{P@;qG^evgIqfpxB8mF$LA5l{p}{{Od{j)2wLo5itq~y}B}J z$ke)#X9K)J_J`A4j<(m-7~ zQX0sPqiqT!o10Pvtc8;_X2hG_UM_dXI9Omv-aATATFlLkwq3yt0xocc)8_VBJ zF&LSpQmcZ)*#xTNdY0aLS$G65?g3=t>8eTV&LwP7W?BFBhjwm5dbQ#qty~g4HOPEG zbkDQuQBsyLd0Rz%~K&^wdiy1X8M{9fU73AspRsN;7V_ z_zq;o`v=!)>9_V)Fs#19UvvCbvCJKfxl_PiV= z;mevbp1aW)!uklC7!{bV-5D!mx)Uc%)D&KuVwb&GWqjAz)9jN(mx0GdDId!J!sh`W z*Y#>X)%CAG_=&m(GA7;5+)iF%Cwh-O;$1iSV?=sTHVQ&eT`Ab^wQb5JVoVT{3vw;k zer3+OZH+rc!jP^2$Q2uSH|fAuLhyC4>+#hKRGKP&z23IR*gk-8Owxe_gccRI7F~H2m>_SQIt%XccKJ3!G6)mDWS%Ff)Pz@-H|ErnxC98s=7bm8zIaX}ivDr$y?Q%sW5>t`hmzYq13_jQ+rnIejbAja z13W$%W=7e(B9YepxfXR~f9%#l*#VTx=SNYl;~}8p<;`hVAPNIDj|WCYqN`pt4Ez9% zJQ88F+);Tn*S=u19+lH59Ng|BHzP8`4ew7i=$gIs3t|UC^U8=Z2lW#E#Ztk)0GYof zP|q>lzAbR`TbfOn??XPNYoBu&i6%#BJv5rH=YH-TAfTX-yHJD~E?-$!^1H$Uc)}2s za|Xt#Ny50hMZw(>Q<%^fE`W4+j>M&RKg0oo`{&Tb@+RnB{vh(T>0ORE-2Piz!S$|05ZyagZ#j=&HlKM|q zwS~w;^CJ<9Yu;Edx+bhw(#C>7Dds zzPB8iIyoo`ttvN018N@K;Mr*U)*fHZeE}4E_FAfr-o3|_*F7gi$bI-i*@C*$-vn>& zUmxylQFov$Uvsw{C5KEwl(HdSY`2|tk~p-nP=4-IKS&EG)d1Th0wh0cj=_QRZf;cC zH!lDnG^Wd@TI%4oWVHhZmY`favq6nNm(_e-ZOXk-cv#%|xL}XqFSc=yQ)scO5{Q09 z%2#_j!bAyafP_rjjnRje3f4Zng%l{dQmF~Vz{T;+B?trPbsNJnr;{dE3kNkGGp!gi z6AeJ#o53j9Z`}_Ta}{s~PH(NA;2%c>=DW?EIj>L~dqZK8okM}^G>1Yl(O56OS;o{; z!68Y~*(`(ZWYQJid;G)zS9)y8@>JRQa-kvtmH%2c{^3rE6uourzLuie`(>X0DxC=v zV`IiVNM46ualIySN8M)o!MWCDM=JS%ih;eXuc&mSf?IouaN zmg0;9vxB13({g5YZA3doh&e9lu7&015;WxP03B|-L=I~f;o+7Qb@lA&}iXM}wAcOL)V~e_&J#} z@qh%Q>S0;$=ZrJV1oDyohJB#m0D@w85FQA5%ze)IDVOqx>7MQte^G4Q@kw};(lr-+ zxY~knI9F!Dh7%^JoW|d&bg=kgx`oOAT!Ux7E2eFlDe2 z6Y|-S43{L4kq}&kRG9Y0-zN*63e>K!Lr1Q(MGUo-iF_augA?gz4X6}VI*i*(<8@=}RgMeVyeJk$kLYL%;BQu&c17)E#LLz)9@p-u@ z^=_Y-GEd*X#&-vI0U=1J_{~ZPQxReKd7W>7qA`AfI&|rNYOwJpK1+rg$i#B|E>b)~! zNs_Bk)oI?@{X8=riSc)-yFGzS1gNA;T`5<1+0eEq+P)Os$ITDF+;6R!!)DXu^D%QL zIbwpc7Phw#S@huzj0WSnoEm9+B`G9$`n-cVZ*PFdLu+#bUtSa`%{#y#0FqN8sySBP zFAoZ${|3U-2V@)uW3WVDj_V0Iau1pk&wmx8=u)rqgBSUk@k3u`++Ltn!Rv*ii~xmF z(&1j)ad0{dnO)@6DXIX9HmC2V>BO2Cz}{pxmu%HAReb>)aGMPP61+I5fN5{0*sn7Z zcUx0{2LA-;3@%5^+Z`S2X9rFgU#gJGNfF_U%Bcqq6gsG}nusRWtBAt0UW2?3TH=gL zpnXS}oWNg!cM$FN>ydF@^k$cjflISY-ASLvj!nQBnna!`oS&nNc^Cn~A*T5``38le`(Io=!fuOT-Hpe47oUYdt<7#4eGq*ep2kS0 zx2;cw)@MPx04Z~873UR1JJLW~NuvQo7x;DZ!tYfMGs5XU60ghODtdm4j!eqz{)e|M zL))d`W=UkLTzUp9O2MuGx4F~}Fo#&iR64yIez!ZeHX%5CG0zP~OD ztv`=zFH~?j>5_5P_&c#KxW~p@@z1haM-DPM7By=SYwTQ(T~&J6j%{k|9G(U83%O`s zm2UP*v}??CgU)IVwLSGSmXZ(? zG=yA4h;ZpX9!+AlByTdUnTc)!YE)wE;*h!Hnu9?+=B`F0+50bK2Wno>9z5Jx3%~a7 zAiDRGcZPwCY+VQ7TxFCoTvm{H0abb&H#hs@VjA4*T+T5>jNLE(AR-Tw6*ks&!s$urOn7dMgRrrr1=VK83W6 zsXrBwV};vDSXwGqZ8)6v&2X~GHtJIJ2(E*J~*4L&CK(25O_QMkg$R`*Ks z7ImHSQ%T>X_AX$u%4SlMJfhUfTcob$%vOL)#RY>qB17CsJ5CSx>?1 zrnurb3+Fn~`e_X`q%pS8?zH(};pj?GqG+0I*}7OlWQt9fmEPhxC7>i05b9eR<$u1N z+N<&0Yj}fdTfFR(p(=OHOei|(Z_}MGo%EyxPoCuCdnXb-PcG;jalvGhZtX+1>_fyr zOHsc00o8#S>&b+exS;i9e6@uR%*gopIDzymub2AHP*rZ!tJfpd^n){^r`o1~q^LT$ zx6cKm`-L++*16gz#r#wdWAVt+Y=i3If@d{LE1^z-%HrP_$ZUW*WA)2B3$4FUto}ja z;AxSM+utkq#lw4I&zM$N<^9=N2Y09yK`Q2P(aIwmRk{4%t*Vg7RVy$m{B=MY!@GNkQX_7oE&0{0wlC{FtjmJKer?4fU0CdKZ)C1>x0iicWd+ z^*YBSVKP8u!LzjC0K(~`MPuV42$i6)#Q4C+TfkF4(ZUNZ)5BeABmMVDoPagOGbaDz0o)`Ix61oBO?5`seWA&r=;PMd=X0 zX~D*E2fTcVGNsL$)>!()tZ=D2Fb`^=R7z1baY>B6Jj2eyEV%8skLQ{BnXj3)KrU56_GnmXUh%8~fXPL&Py`he70Ud99v$yOnX)ZyLXpxv6Aq zvTL=<%;3mWkxshMikxQwkF{Okx~!L2uUCyeqlYuI34b$~7eHi7cY0PNLNFR9BFdC} zF#0w&kv42+3{cS^YXSO%2??7g``?(dM29fLe5Qo5*h!4F^swBX({rYoBR9|G<~jxj zqU%>Wq_pH>+4{ZxLM|XoY9tNPL7Y>2Dxe7kR)W!hWcdga-i*a9VOaf7`Q#WsJ zdeS;JsCn8)5$)0JC@Fagu;Dtos|F&U9bPa@Aa`9IZ?bTrZXWT=PQeJdKrr zu7+DCa@$opQ^i8U7x%EvSnyjMspo5|c5xTM%KI1uh@z@Lj`^AQNuWPd(sg0# zXgfWHUZU4I&(pp%(Ib8s%pz-3`z_HU4V{BClxAF52bWXM9O)h2kD$Z5x<3`|g6g_K!$U<(dg=GLzYK`y-+fut z6(Nf8@eQaE2U3I$;DA8-eKO;8YA4$ul^QV8j5FgYtnWALKPEkp=$~2+t<9l)9iPj{ zb7`d=B=`9P1{bxm_o33vz>CS&8lP&VKQaKDwgskP;hPj`n#p9r7^OA|Gjg^4KS?l& zo-NdPN}gNEb18)xvse1d ztSCpZ6NMCV`c{{vJVjjx!Y2tEbDu3I{(`(Srvfg&^qm%y;FrxG*@dzb`$V;sGx$Yb z1fB=cn!fCXp$U!~J{u9|FdNsJ8`rS5!Z##jn}uHSj>&no7B;tZP{E=bh(LRlD4qWO z+<0z33yJc^ORCePTDb{9&H&CD9-6re?s5LzFhUf{Dys?B91tW&H|G*q0WMAx3NUO% z1OHf@iq3R}@!h`-;F;!AQL>reqB(F3W~tuXmz)PN9gk9o9~{f#!Kk6mQ7vO&nVgRB zdbG{t6&uYC;nxRCpph^lxV^( z7h3dhmq)doYhP;jNekeZaTF8@FfH-`)v8zZnz8uCsuFoApyTX$Yy4tD<%U`3I&J{Q z-U3=OCT@+_pWypA!-VAY(v9Vydbj>iSgiW!<)#i|M4-S1GGe#(-PZiOvLQI9QrIFg z`z*q;IkJDzQGVPwtReRUQGRjpQ&VVgx>i7&RrGAuF(d=0LNHfy{R<*&TN%RCh$RHg ztc|A09&ExLy}|aQEp7qOm_^1VBz3y*8$r}nRO;C#txshkv0$_&3-Km=6KD1)kmDk< zPVbQcW^S*(V<&~|JrKCWiRv@D_*c?}4k8lBSZ|kWbZvn&V7lLYcB3?mdY#7RJl4|eZiL|l8ViXD{`Mk_SRvcCi0WBMzQ za;CG6Qr)_cG73chg-V@Gpu?`J0ov!bwxEJaZmR|+1g;JhI>rL zYC;*)S@jos(+h~Hxb}!KUg2~>QVukW)ovxtBC=g@=^^MlXi+uv%GaJZ&KV+RylKV( z_wn1wwzi3{-NC?V;V+SJY+A}L$%KYf${M^ID#0>~I?ERQA;sCVo>>PlV=#g-5a^xo z@#-h04Cc`C2OH79mU{n04+kPcA2%Ki$TmREke3$h{95^M3r8twdQ@Oc)knlFWEr%g zuj8j@Wqc#BB*{YLfSC)c4QX%Z=4?^K8(dMii928L?5-E|GP z^6m<$V`_59@cj|bsXW|Vhgm}{t17oXPbK`9u-nSifT1F$h?aGXhUXw9Z;ZdI`|uY4 zsOo>23l38ZTguSNK%TA_G&Niv-`z2Gec;kcd)6#U+wY)IwBd)fj>{xn;9>vb5b3plr}7zbv22sw_WvQV^o( zSCXHMPkfPt!0avydDVBn2h_lw@bYqFn7|UY&}IOkrl(X4 zQ=yw@r#ts=c@;K`!#~mYHcQBVvYLQ~z&QWzPV6T`&91u3b@~`7w64p&JEEBrqOQp2 zu{qWH&!-Gg%_61tcTIm4OtXpLiiu#G7+Z2md7Nv{+7M+lUjK7vy>8Jm{aaXMkssi9 zN&Z*<#Z~8~RX^gzcc67s>|~ecOF@2O;xBqbPKHBS_d*TYy!vz`m2!0zwH&<qrU?|1s?6M z3!9!LCKLJLKjTJ1pG|d?D^kQ+X9O_RmWoCun@~_#Z=jKBLNxxEHCuc;ZrFTdoQu31 zSCg@75eU(!$lo)UYNTvvh?Ji$Co^)x9|3xR{+qF1E<@h%6+GD|cVV`JCp~+G+n9AohY_5NNp}0n?tc8|7aCVx%C~T9hSy zd@$du#V;yeEdx3fsvo4xP>hd_h)5mqTnkvoHdR3i<=>QU^!AXpE&g@Doly>~9t$l$ zC)0X{HAjjr#7kLED=+DE-BOATx@cC$ zu@r`=!cSHdg@&vCU9T9}saETqyve*7IriFU-I1bug;lR)Z?Fla?aSYyvzbjqMfDTF zW5N~)06oZlH>g)~J0+pmDI;37O%z2S8j$e7oa)5pOq-|IGlr_8vLbS)*y1tAMNb&4 zSKj$s;yZ9;f2S?|+K`wxHyOcQVTP24ggMvs1L)6@QU|?|@u~RzTSBR-&T)XWnnoJ# zxw+puY-BETA9*{st*;OQqP4H}XvS?b{{VrL;6k#C`$P(H=MC)&b)|TiBq`U;?q5{qfn`+X}-b#2`;851; zG*LML6fp*GVClpZ?bEb2?qHUxO|J~3q>j~Q{2 zG>4U;Dn*j1SIa1n%;d=}68dm*Zc__({XlRfhRL`U1I5I}7byCIG=VS-P|qLPANyiE zZ>Ux)ST_+~8QFhq-Q$UD(-e~GSo1P7{?@TCK4_=Fpg`H8Q8EG7JyOh`!Z6q z*BUd!`zM2go%OFGMVqj2hF^?x#kDx5xy$WudLNoWC)~AU{aq1VhOp=fHQzvM-byJu zVtYNNt2ZRQdz-2ROl0CA17xg`GVt|lfVu_nhFO9(u+E+N*Mt5Y&)1cC*-PKN0DtNK zx&Tbei-?@C`uo@P%0h~O2fea&hEDl)wRB?%lio?y#6BVB$9AZhTYi8g!a;yb+~;Fn zGEUnW8~3$Yyiispz;ZE1ro}BcGGj=^=?~Zzm{8em$z-rO%2%UiFU`8VecoIa7H^XW z6hRg3-+Rk0xPzR5|;o=D=6HpwEM=dJ1;A6=T4;XO% ztZ)13Kq;ZyN7{nAh8n43z*n*#Ak91nTH9n&*Jz!ai;d}N--d}yyJR0Qx-*%)Z7=?& z3uM#(vADXJD7f^t7k$$OY)Yh9nr13UX*MCA_3(Evz7c^ZM~kzo4}mprWFmZSzdE$Q zUP~NyqzCv`@~Zq)uZ?tlK(zr?8&FpHyRnyxRiJ1h<{Xy>Ybcfxvt z&B442n;80QG9&E|&H!B_fV!C@Vmdq@44zX3Pi3>a_yPI#AR-V*IiUPI541u1SA4hL zl%drPYGhevEE+A=rzJj`l6^okx5RqCgUq6%&(XMQBW+7B1VhCeX+41GS93uZO_!W| zPAe1s?=k}Quw_tOKqVOgu|*n)BfoG4U-8!CmY=S^X=9}hx;E$yLF;LalCx#=g&W=z zXAhURDDW1t^7844aE4Vl8**n|ACHQ$JW_wD*NZMXR$|ZU4=mj-`sDzaPFx@OPjRVe zuYR~`JI;+WWD1JixgMND(Wqb1v!A(p{ZR$C24evt6Ve2P*p4SRt&-BG;6hE$$2z0q zNMe9a46L6_+ZAP$L4j-t?)*?$t(nLdJ|q>AKIWlNilEB6r{X%|#yV-Y_Ksi6VI|b- zz7}1XbH<1BqsXp-UNvQRt7s?}OlP5$OKn)5Hpkb{Rn=8uhJj0LFh z_<0Vw<WGogp6UVVKvkK5W-fN;~v9~~Qa*3kj^GQYZy>jT$}PR*}|^M4IrIux7@Vf9|3j=8lagE8&YsGGaNz*d?6C2FGC(owQoyvm=QB!dT zVsf&4TyKax6^`QWNjZx+Gm#k|0J-+-oR3@qU#?_<-ZS|qS43WG4QyRmG2*A3@cohz zJ3%K^_QRd!d2^j4Q_$(70PL7nleuHX(r#dPW=7(OQ7YWfx$zgv-Ah$Xe#*ID-W>{p z{~a4UDBI&l5}8ajUA4adXutJ&w40Gs(@Uqjk}KUFd51Z~&}JZG9nF>GNvVsdPEiN; zEy#wKe}&_kvJX`ABkf2BTW1NzRE znM_=%Mm0NGcGCB|e20+)4eTNYPB3AM(e~}0P(U5dF-01B;U`oq`S_R|2UvmGqm^B@ zdLl$M-IeOP6?R~C)Eu6581i(^c`K#ZU)*-YAwotUE{u8fU!Xcceyi@EY&H}!IPQ%W z23AzH_drPWF3{A?0~miox>5K;HbjJB@JbDPt8eADXw@P64VPl$u4b7|$xk;$vYkMQ zBWH{>^fr6qpMdU2QgET=_gU$O2s@29R?&}n0+o%93VTqIAcwJV$`{B+s z{@cuel5}#$4wMTvn@air*~Z1x!csh)mu&loV@$ z;v~-z7jw1cRN;PGG$Ed*+mQqOM&1rXpcron8?9W>+s^`xXrny;I~1eL2+)0wutX=j z2Bp}Sr)WG9A-bqr2jHg)Z!M*UnWE=jb`iNR16szw1SZ}Kj9IAA{Q0>gE-f-+Qrt>3 z2ZC4>#FXGk0oCQyZQ$lDHy{h^{P`*Uy0GDok-|iCNETogRA9J3|vNZ~C^QWR zmd8)zO)W};$kwK_ODa_Isv>k8>Ck_~+}xn^W~4|1^|ME1 z1QlMXSqht+_fUWm3bX||F~F@!vEwb$ za60FnG6dfvzFt1}A>0`hkvqeu}TH^#R1E*a&(M_Z=C!BjI>xc#M+PjCqc5E)9*q_{61JiGNFZ-O( z(X1FE-}&B>vmnmiN)HD1Jz!0)0@}>PY9&zz>6|P1Umyc_)PW9Y(5`!fcLt)w51gy z6cF?MYwKG)L-?V2z-wG?6+JY}JO`nPYXAL4ed#P}G@up`GXBJKQq~GJg>osn+=lkq zRJGE!;ed0l2Bs{YtoO6s9zIta4jb)ieh}vJo&`iD*&0JSLMQnD>GpvBbrdy_7=D-t zRonk<=Kco_tBBcp6%v)hy`NE^_yQ1CLO{zv#9jS}oN}>rT{yIVT($BQu3+PXPh`5~ zs>g};EbDA4IH|>n^rA>Lb-aYEHe&pF=JoHL32!da!(A|9Crn3%RSKj(f|g6q84IRf zW1#RP|2ssbiz0}T2dP286q&W0T`Z~~ayl zRR_{yxflJ}3kn5$HCM42?lKPXih)6;v+CtWYr9G)-=!Kzby+=pE>Kn8ps>tZcu3d4 z$#c#w`wnnJbgV#pUkah?xg?@=S4eGIIji0~H@!ChF=G!7g3>;sn`7AsyROgiuJso5 zy!;Sl7Bm?V9P{RwedC%ErXwREpjeR~H}o_souFqOqC}i-`|mY_K4AlmJp)|b$|bh6 zS3i#Eo0V{=!mmo_gTrSScwU}DHJ(CiP+M%$rni%1Gkx&%4$<@c**vl<1Ox6C3VN+d z%EFUr-V*ir&Hnip^MMyN|AT7+^fhAcKHM1!UCRo3r6vxJ#o+!iH}WRxgYBGjkC5Yi z`+bk6ubg9^5cTOGXx}(pl3+|9=ZTA; zR$@Y1KRS0#1sGgP+RGF1qLaisTT#Rm!#+gI1kEZgL+02r z`(rQ7;@b#mjI`CYLgC2DE8qYGXJ6Nx2+#}-e;a2MU>%h|Hv*0*LkZ`!Q$T$NP8>3O zH+^ILjGHaK!WYdVhDDw|FfQ7@g+uW+pfG*tVl+iVCe%NsM z^U`Ov3#UP|8pvQV#ojXlAWik{7|fo-5of>n{zM#d>!5ih1!Dsr8nFf_Vht<+3Ed=J z2O`o6m?Gzj$B{93SzB#4Y3JS05!P{F8>1>O9fa7e-w!>lT^$$SMI zi^yyzRU6Bb3g&kMQc;if=e+gSVr|O~*%M!u5)u&l} z2ggn>H5_l9bRaP8H8W|9^{>RBq+1_8xik+ReqfGMOlXj|mrIl}xZz?(RlbqR7{4XH z0m{^lN*>igk75rlnKHi@N2ar=TzUsb<@-**t2&9n%(M*}xvRdpJ=)LZx!JawMfgKc z^lwdS-_C1kVcs2J?-TvU4yc{L_Hi>O&1vy=#6$28v^b{qU#(Pu_@Rem;k|&{Zi*0As0q~OZo&MGZdWO2 zD)-X4b2nnoBsydWQ+}cdh91Ziv1gve5GBN#EY$Fv(HesbscXfysjf44V3^|sZ-N~h!&4Q(Oi5^AG*Vgh$o1w^+f#8x7(21{nfMb zMP{t!6%HW7Mi#`18mE*d{Q9>!0${2mZ1ET=SMN9Dv9?S3UAQ(KcTmy7?BBgS*#bk@y3L(^5tJ|KijQCm5_?N^_ykagObhapd6y&r_^Ibn< zwhx)R6ZwgG$P4V2T&DEoO%dqbga&FnIebtMT|55WI5;(^R&by-#n^W5Q=^S6sJAv{ z9yHZgnR$5RoxW)oRXE3_Je?@J6+&*EaFcJ^;tm*P6Ziy-oytmo)~45=UFL~P zWkB9!o;(9&KRbZBe>)fhMA;cR5N$X-zVo)v%)f+)6R{Pp@5_;8;SlObs*O3&)-!SsQA z5zCsr;Hhc(gU8?swF5VRJnfa{_nWa$N0lfGIUW8^Im;4TI3BUk+j>ZK)$`f!=<@%p zq4E=-_UFwB00l$1NPaoHVRl_zH$YC&mU)DVa4vdQ7s>(+s&Nhs!a=a+{SE(*wyyw- zGHt(K*A*5-krq)=6ah&Y=`sLCN+on?mF|)nVO$X6oDrq`RbRh8p-k z@2DH!?)Uv%^?zN<;xNj@`#kq^?sLw423U~ive3T7NmGYf?w3?U$z`?owW#1T-Jmfm zSk=E%zg_)cb+)cw@j|~NEL~);bKkB()t4e=pev8#`|7eV9k z6j{*g-I((TuaZ4YqnBuU<0n_(?O z-K{D_^MiP(W#c)qSd&ut(7A+%SynyOBakbMq)9K0Kcy;Hk^520ccob!+8@1Mtw>Di ze1m@jgj7vH?&h6cNkHze0Err?jD3qt&HCF4u8&Eqg;i@2u2y_^19ndgJp`xw^R%5vQ-b4&nBt3?(r^WNY)H0>;?Fn3p^|R@4i}4JANA5!oIfN{q%dM9-HB&Ce`H+4%na3U?KI30a470?2#; z#oC0uT?5szV-R2?3-T<#*3cvW*cd?7a|6t$4`j$b`ztlp4IS>YS|2Vqia_k_Q(g5A-__ZxFQuT(=7cannZJ(Qd_n~G*niEiTgr9-8}Rp`S9psV=`o^9DGjpciw0Pv{L4x) z?eab}8p|EOrSVXO%w^IqYRlO+KYixl|LFcb@i&%rj&dU1pUk9e!gm6K_3-qc1Jq1b zuYAfflJA$L6*#p2CsLrC<4F4yDGknLB{*|u093Zl<-Y-BII`Pjll&xiyPoaqa9Dn1 z|EE*sTMFj{eeSU^9o~k-x)fc}(i_P|fOe0~D7I$Lxc9C~RU4#BsE?rD|A;=%RdtNZ z;&@~YVGg?7!2IXN%QhD2w60eE!9@M02PzA| zGdF?0#CCYLSqW^1GC ze*n~20TaUee~G?f-$Y1m)Qg?=xEwxW)mQhF<#(<@=V5aN8ItD zKUT`O1pAzCiDqc!`ANU}M|L^@AcB#Om50UbZ8PZvU$Sj?bI+=Ry^3mt$5}MLj|7!h z$#`ePYD2ai)Z|*nHa2B2D%Gt_HWe=H0M|>xpF6C78eIPmLz+&{Ia^QQN{~{2D<47h zlRW>y`k1mS7PnqAEXPcKXLyz&AIM+^eyMq)ZM_FR(BKdV(cI!-*nXGBuin*|V^ogfhrgKPvwHT-IpQWh(hMwY_lPsEIY27Yvzrnc%K0v2YzyW)ZZNMIEq1?H+D{=5zXVt2*8GwbKu*exZoGd zUfx|~C|QrJdAYfP0%q4GLDuLL?u=u1`&JWBWHN8J#asy*l-)$!?zISu?36L2>jxO*qUX3VYHKk-iF zCq7_)XBN!W6x59M}XB?JY-d!y(GukIl9 z{xwg!Rgd-_^l&Q@s-<;vD+(!Td~nEy&Xt*)`)p8YnT9)>KdI6vh^cF3rN|+Be$mRV zox+GrSV7N8#2|j6U{L^(3ncXqpXSre3T;lyVO4=4)u6sB+8j^I7xpVUgF<=(#kS{IsjN-$s}X z$@cT_IO@IiPY^4!9QfA9jFF9;5>v5?c^l4W-hr@j?w8&xW0+TlwSW~myw>Cus6W{Q z=N~$|YpAZvvzj`ztlb&v(MQ+4_)8SUx~=o6pL4moCC`e!?=GdK7q4-jjaq-WmNT}s zdcSS$%6+R5=oT zv9TlZhSy5*W;1qlx>h*VQM*WxvnuU6N>A8w(8g1t6k?6E>N%$ZHrgxjY{-J)YI~T6 zh}G?qgNfGmBg!A^B6Q%octe8(pY5E+CHXPBWiP!lA3OJUEmT>K$XjkonRD$N z75$=Q_*BMuclW1Q{u1ZGm!Q7w zc#}75r!Qk{A?u*#o2Ct=X3>LeiF#rDHX1>O2^20ANC}wI5!v?@|**7B=J*kd{2S36vpBxe@(vvTPE{Z3)eE1a?0;M-Mw8?&32MEZ(Zye!H%4Kh&t zAw$$ll$l}qLe9#%ws!D|!z7tC`}e;Vc>}FTgVisa;P?Zt%F2FBL?={6L&AnQ{ZP2$ z$B(`nr9Z%?bT-G!aI$y6O{vIv@CXG-UO>Hg8LyMSUN27Zc0tK0uaW}EYHm*tcd#iN zMX501`9`}17otMqBA%gmi#5UoS+DWUYjqRee|mdvN?DaX1|EAM`0KEuw!5!O!l5mR z(ah=>H48}gl*_7%{B2_1Le^rHN0~!bO>owZF0P>MF!l(H_@KAx2gcPF3?`fW%(c1y(tj;!`{3a;C8Siv3+eeT>rX%SXWW+URAx>&TrrwY-#Hd7g}(9mlE zE7bpT&1Ggkog6i8;N3t@^@F+q6%Obt4lI@tds%+4X?B~7map7z=wMd_TlWArB1!)w ztUy1)QJ=d;&N^dF1Z;w!)bg!zcXv^zDL3Vu@zGa$lOgbvjn1tcpuE|zfK~SC*iQ!= z^mW(+(_7&GMFHSZKp|Z%VP3(V3^u#1;#X$c!L=)SP~uGh>zV9DK3J)$wzI@~!d?X? zjloQ)?<6WJG%XIz&$7(0I4G@tQmj8TL}DYnX}kv2Xn0dJmen)n(|^|1U|7kR=-cowRjnkWBq1`>YO)GuZ5H2%ZvsVqziBBdfqr) z@|LO~wRJ6{ap8mB^EZ_@)7S>J%Dq9cm(!dS;;frrt?ir$s4(c<3At}XE>+KT@*Gcc z>)MdeTJZbN2*s!I6~pg%ktt2ZQAUDoZQ6I;czTs(~V}IXlZ{J{QH`|Z{3s>gyrV&!r63#2iPwXt)_0oj0F>Q0OAQt4S zygp6$GFnc5aC{>BrWluq+%DKO zS9uHUyRjf`cj0A$Q2{e>?ZQos9)WAg6t>BUSi`C@WsrM0-NLr!$!rI5@?9-u8l5uE z#XlK-AYl)Bo45^SMu`&Jx2iz!P-S_vKs|^sgCEKf{0z^T8n)%=}!g#humUOnv&8h)tk>qpPzeP)Z#LbIjCE}sp2>pa^2w)zpDk#Rc($6;Et1TJJ!B^ zFSqg{907C(+ls!FzZ-K~TIp$ViRH1KgSt&0oz^U62+n1!+P+z^ z9bGP4X3%760-(Q=5BpGRppM(=mUeYdcxjjaklT)^uU($|wXQ*`BLwRnq@nl_`I1nhH$=&gX)LPj_ZAzfO9k zG!gK`|ADT=Kgtf?UxxONL7 zQUPYva#*#6euRqt=47;k^k?RRS}IWTu&04Wz6_1FE7dt4j7;d6ZlaF^N2w!-r1Gcb zQg!I{ax+dBMdb?x@qMHkW4M3(xc4ch2f_M8EM5vPogFUGorSAP5J6Ds*wT5Bp#^0oaxDJyVB`l4P>NaJ4wEwxQZ_1~_BkBZT zZUhQxBQ8wN=SdzL*?;JMQJPra6=Kr6qo?=9I*s{cH0PmGL3xF^4Cu>9IidfLTVH+} zs-+u(`b4ilL?EyOH&#cO8 zk+9E*WODMTIyFQbo%iL#Xp#k$f}>ysYkeAe2913tHMBW%^r}`l=~r@^G2ZDNOIij@ zylQtDsHax^3%dOw;GD&^FnDZ4yT6RNPTRq191jZCGjE>}{4*8frndX=Gu&Gr>&dED zKXRvhdXcin0@(xzY|C5-O-vizZeF`tI(`hh=Q~0hD|Mc1<;R8)X6El;w6<%RU zJjbB70S|Z(Ho&Qk&gaG1$BJomIh=oBkgV7K_;hZf+IVkc@DPJzA0XQ_Qa5m*s-79t zk_9#Ii3Y$Gcf060(BYLW%;B-2*tN=szqh~tt@ZtyI@X@oS%YWylaq3uJo>Jm1OoYgzf_;OhaGc|owBLncRzt^b(__;ay2%i#Gsb zs^C?sGTMzW8(_#5kRCG0>4Dcy9JBA$4Ci3)-!>!a{$mS(R|Fvq2sVu@ zhBN$$NXP{{WXHS#FmRPaLIU81qtgT%M1iHXHiRhYGE!Rt9!G0uH7XFSx|3Y?seBV{ zBHZ;K*-IemU}+WW_+VNf;mFj}A=52xCaBa_TiK6XG#nDQs2_0ac-%eCixxg)QBZPZ zJf3JYTYgeu`gc<2ztK@*p!<1yOUgi}rm4QstY=ckB1@D;)B0HbV4bPAMW|j_82}iy z<9dmK>Xm?j`n{CiVx|CXnDX7W(YE@EpQwV&Uc5KjSk2|{EeHmAH`h@x-2!sr%mu|X zwnQ^kE0RP?+TPrux^L{!8?e8|DKAimqOodRi4`2ESLT~%Gdq8(V z3)OL9M{wwFmtI)mwr$a3f8E23e}OuUy!ynBr_Z!SbHXW{auuB!O^3NMYhRg>R!}6q z6aCi7((qY5NyQVU4z3|HCQq@d5pj@9Qp^7 z_isxX0ZDz+c2YUV|98650=e~|Ntdl6zS#9k}g;6w% zQqemqOP^oyZoHBWwE+@H4tW@}{ih{HHeGtFaV+oC{zhl>`m* zv~vppBu`Sy{Zk94oJfq{>qm@M*;3~ncpDVbQutTp&WfH2t0>sM%!ws=Hef;C zX|&bdt){g72i;C!AE1T`Qf9zeFVX^yir6)lD0$^)8$;6pd6rfL1p#Fll{LG`;W(Yq|r6V5yM-B z`9#Pcy)<*37|cKu>(OVm7=>-&Lw4Z|nsoFFJd`bdY+KEHCG?^GY+F^Dq}iPvy9x|f zoQl+}Ni2g3m&6dcrq`^%d@i2+@ZrQi5pe$i{nP(?TmV3j+`57-a1BxE=(VazJbQpG zSeCvFZw4PYTCU}+XK&iq=Zi#AM)WWGOk}L2=A8ZR_I2cZN3;ma3L-Dxdpb`y>XQRyJiWz0lp@isRO>?pq zht}9T>Y`9g?5Mq}r1@av*u=0i9eWeTE#dJxU}z{7!Y;}-yP!HlcfDL#ZuK1xTNoc=x4lql)XIN=*UvJ` zus6H56fpk8J7>D}L{SG-9D62zjGl+-Pv(1$rkao9z1P(>X?fnj_E}G1ep{S8PHD9d z&29#V*K%fETqF>uhAg)j*5lfWywLn1Un7oy6E}t<0!Ws<60yg6!cjx?v}yji#L0~F zG2p2vKygB36IKfyHK3>O!fe=A6BoA~D6a(clVQdp zEU^aQ=_z(p3V9S%;__h*X;b;fK96KGjI{=tA1?dQGFHjIO`kLQR}f)cZb@ z18_;**Aup{YnB*7u0(=9bTMtm8+r876YN>5{2tx!lIS4Z7%q1@wZL(j?imrRkaoza z5B27Plf&apmQluItw6U_81&AcM})r}$-(lIyK*q`V%jsaB{$?xX8`dCfH9B00Ogt< zeWQ!>Gj6lr45F9c{giC^@ss3zlWcat(Cgw3y%8W|ca%67T-w-*8;};OH(y8~r65e> z?tUCOQj;|Q&Va|2eRgsiU?`!!au)DxF4L|Sl`)h*pYa_d$KKC5eEl_~GspL79-)v? zzM0|;de^Vfl_FjD?}vo>_z$CgnjG2bbu>ko6-lL+*&#hvFad28G5my?F5l3!EAGXhXeo9hEG>5N%!yvtnFB_oAMx%OuDeM2&Zm-MqG zH|IO}XX_${)oxPDw$f4{WehQ=59AYYo!OS{hzw45zJOjM=S$xL>2Z)=4`CxWl#My) zwZ4ek?V#aVDq#lX$Y%}j3&s5EN=@noSj)L4G8iJvW8?YH)p?+3We&k_z7DX3wLpCa zV!nkGdn=4*6@&H0&-vI{YPaVXpiYD(t^A8-Gdac{g-tKrVc;gtXe!sw;Rq@Hgidy+ zOb;X9#7JdZ%@?+WoXfcg^NmsIwC@`25!`L$e4oqrHk!#7WpaZEiXRwR!Aak8#dRa z!)_p>;ZvsxAxCu8U%OA}XCWoi|Iw(fbg39b$=#yY4H642D*@`FKcLg7T6RdCRr_x% zQaEl3oN3XFoXzqkUK`tQYvvqPXF?1}sK2)lJ&-ik zqD|5ZtTU;*qYIcQa5|uSy2Mar-75!gCj1%{ZRROExNA2$ddHSzI@Jv}#*o`&&>0Ho zr58iNah#(qgI*!~?yQFx07T)+LajeM?{d=%G&?v&H5-W}q;neIpKIM0A>|9n7y$M0 zKmu1eY4W<{Cwvf71~tZ*>xSCXP&kP~njHIfuKd&v)8%IuiD`6ICW0)r{{49lVlen? zh%QzqEH0UQ#JgjwcbqBp+sh!tB{=znT?krx*l&I7P>6H;0#|q=V0~b#b$NA3p0E~4 z=_sAo`yBeIUT|c@MxE&XpN6i#_-BM(JJy2E$0?pOvPYt1-~eR1%eL)UU3{2%+%^n~nZP!3Bwytxy{Ph4N+3lu5thw)nQ%TjYzJ{1`c_ z{s0$GU}&puR!-SEy$}1?lxS#yIPU`sIO@f2CeCPUo5+KYe)%F)5M(<@k6W_6a;gxC4?F7L2ci$cFp~=DS*ICN}`F z1q9K|IM{-&{Ni$cc-d|!k@cJ0a{l65699U&<`sS$%MIu~(hQ@p$uPuRWd~rAJAEbN zx?Xt&CUkC#xSTfT0m*@j^)?`W8I2|)gEBBoA7xFqKF;Nz4cRf)^ce3 z3KF=)(zYm@1?3S=BgMZ96aP(=_$Rp{XE01EShNL^JSurh2pyTHH~WdyI#!eq_Q-Bo z!b^RcNQ@YEj?b@6FB-nlqJ`wFcFKRKD5*Q%3xHf3bC|T1kQ}1bqQ%&2<&IN+&qS#E z)5NXNZlLT|IR1FNdWca1sKx7QD%1$G}JFNb%a3S>b{puFlhtX*V?u zVC`Ws{Sf`>sU(FrM$2ghSLc{`{jW(%>wf^e{4_x(39^fM!c%?&S+jOIY(u1rFqIWobL`O0ipH@Hy$cdvyqBFtTWtyT1#V3mw=d zht(FP$Eln5slPMh=&G$Ait)NYEuh&T$n^}5jnMP55Zj`Pr zhGF43=>z-=q@r!eSkkylZvtD8lw#)^wqRmzz57}Bu@M}7`QwrGoS4$IwL*1UwF z?nUqB>PE*&_^XGy64@1b@9zweTX77m>1H{zzkaCiAg_?bCk0%SjCXSu6b9Y|!#g6i z3JHYu|7UPj>VVt}&<<31U)+XC?OffD`R!NfK#{ZWXvxWR7M^|uA7`oa6QNF! zUP*WiG1;m}t2?E2eULR6I%lHZ;A*G`V?5c5Cp0hR6#Yq84!7_pV{-jj{YeKudm=i| z!_`{(tISRv{A}k_fp^gMN`gD2Q9_PMnrmCx4cvV?gmN; z=q5$7w;IhYIhzH{Yt?eiO!|$&R7B6dGj#q@T}&$IQ?hEJ!8@5V09{d&gj=zw*u@4Zg1tmP=5)m(M*D!}qCQ0Rrr48k3;!Fq@@3X*|d2eAQ^GlZq zeJA|Pe3DD}i$YCU@st^*58wB&D7|`u!K8Z8pxy$X$w5yku#b}n6}llNwaozGqkhoM$o@qceBl@j{Q_w{Ir#=? z*?^1l(|ff(rr^*sndFXhQ2&^BFa1*3`Lj)${h91=n~Vv%X3S}#L$jA$D^Syv ztzaFQ>o?PA{NUEeP1P`I%*Y{w8^+FQx0lgWHIkbo2n z71`H2_-o>UsXopn_Y6Q^PlI?BMAF)6P}c=r+3e_Rx7rjoUfp8%VoR8CRx@O??LpqZ z7kTr;yx>=nAD}qComs)Eo!K)zkdvDFbAbB>`Yf&h;@kFYumx3aupAES!#fXxqttRR zkz%`DOPRl24r(p)El6Ckq3wO!)qReR;ghESzr>mU;dp&}1&wJ3&%oLmHN|+8r_e5| z{nb{*3ystPlEa7VKTcS81&NJej#Iwtx&Pu}kA&vwtzW9h-i<|C>N=jl&lT>L68cMM zYUh*N5b6ei<29cZvS!Obeugk0KT&QXS}HIwU|hI@o+Ih%GbEPfKrqa=TFW7Zxaq=n zr%s2p48}I;0y!ZIQE-d?VG%$6v^Gei3hf_`Bq&F;mtwuqRY0L_p>c7_jfrm&m4(0^hL!lZgZ5C{0aDz$;{qD<5-3 z5FN<5MqXfz%UHSHw4SHB)LB`o&}*@ky><;*tP8P5ka6O(P6nBCfSKa+b~OMe;}_Ef z_zS*#jvoo5HBw;xXWQDSJ24bILqyIyf_@iY^-`C+FINcYW<`LvOmTIkFqhq>Pkz)s z3EZh3`%Mi$X1Ka+pZsb=5JV(E#bn98*#~GFky?&`@Bu}GIy6Qi7I%uSS@M~;RXEs> z3WM!;{l0hsHd91@tTs;C11qS_^W#&5#-(_+Q$5{s*YnzZ&Id;=K2NV%|E6{!%R!n( zt;{EX&3r@lSYjT99yeP=SJz9;>#>i%TfoT0By5#cMCx?(8j^Y<_UE59yK(2cV0#;L z%W7n@;4NiosO1IjeTW>JUw!6YZzM;3)&D_{3^ z{5n5z;0o2=t5&P|R*&$>GQX-La*^N!eL}C>!TVY-G%(!CuIL=oNK*M^Ui}iAw8mcQ zzvS_pJA+GWAPWO{asW&Liy!+fgep0OKuvtWwpbG5+%IlrgFe_!Y^r!5#|!z6f7BDz zz|34UYfxN7%{58uAvxqs0^NBq_|x+70lJyO?9R-)$KShc2ursC6aCRFSIStP;QuQ; z19+UyP$v8fXtI-|#5)=2PTa1hWZ>=X#DZq9{5j+fgtZLw!bT(|OM&LF*1qvtE(?88 zfE=Gn(&K8EM*m2<>&pEk;ClI5o2!cEgk)RfhDKN;qg&?cF;}`MS3m{$&s? zR=SZt@UBJa^80T#e1I#$3a*G7shYkkQXK+Ir1F4>hYD36q6&ToD;CfTP8Yx3IYdIu za*t`-tW`*DgUenu_OS%l)%QWh(lkG21~8e?emr$Mlw)BlBiP)DDa>UX@BmJfK9&q#HNI*JTOq=OO9fn-RF}YL) zbsoedazWoc-5U?;HkdRa&X}iH0%LY3`o|>m9*N_?z(8iLUe@-ivPJCtVhHUmU`CPe z_}+QIw^xz?M~&eLQ0o}7M6TY98-gUZiib1ywaxh-2MiO1c}^`lw>%-UC=&^*N(3xFHkec9C>~yJ|>}Avn~Fs6dnSf8xB~_CfQZw@sJpxoF|t zV0c-_XcEj5QBXh=MZ;P-)HZ4&OD~Fo*si|n%k(-ss(7Y^r>=vaQ!`!u_^a4ZyNplb zfp-a=M||?$q0eJ}hG2kKwz6*V4i5J?Y~^r6GlOE}uFt`{N^HYZ^%S9Y$6R57P8%uAXzi!z+y>eKxiQeq5% z*v4~DN8z+t4na9YkYDKY++Ffn$d2+6)QRVN{Nz;s z00v=V0{q3rTYc2qpe~q>q#Dce#quh0#>^Yx6XuJx0|+gE2jr3re*Vdo^?4AE@H3cw z+wXD#1n2x6Cel)x#Y1AOgHTVcY`cIT;i%>)>nL3q@b&l|+Ixt1C{(tdNnpwLi#F2@ zQr2&7_0-GD@m^9VOTlaUl>?T;l{reO4@^4=5W%KSKHn2a#WaM%&VMf?CcTZFV^w-$Sbgaw@~kOM{iDZqi6GzS+kQbz@CvggCLp7&Br+);w+^%UDF(| z+#?$NtXS;D@!V6gdtroF`0~>ly-QIU$6==Sgzx zvqzm7Sa`B0>?C+ea%`h%-LG6Jr8PL(jBcei>7%Tm&v!PYtk(~O^EE_W}nqpZKIP%FR4ho;%lBT zKNq5L4l;LYFS*BwE4{}VcD#fCMowJ&5@dLQKakOPkq;0$kXz6e1c*C@etu67Mbe`m z0EDhT^$6eOJRSACEbZ8m4a(6}xxt6CE`E<-Yy9L=3c~$3OxO1VZOOk@@Xs9=Nm9bQA#Bh9YRy0a$1i zw^UL4L6$wf%Y%_6drEZ;;*W>hsyLy^lC>=`_EM;*D=0DxIhv(?^j6@ct@NdsmRE$*b|92?ggHeVoBlCgy(K`d20--6DLY_?0FuD?Cv*=cfG&pSBjFmS zITviQv%7so!0gSH)Y~SFHJ&I5INp%Ot|8v}i6N~EY&O*x5#e$DubYgx1_lYQX<+gk z*@WvhWfdb-#rI8!y6nF|cl*A6O9stp3r_HzPA&{@32K-=`hB_U@# z5czMmwSrKMA$1eaaNB1^KvO#gV#q1~MQn!~js2Ex?$SkbqcMQ;U4BdIqF&6><^ZM+ zDlmvW?$W|7IbcYflchl&F$cGN21~a*ml(UKX*#qLY}WrG}cX@>4wLHIm zLc}fL=k@7euz2+FTiqapj`R&&wXO0droCp^In0$g0^~!u)S)tlmNV8xcC%Z2^1+OV ztDetfq$myMK0{{hJbH2-BuX6M*=c`yB%|IkDKXskid!K_r_sJ1R%yXKlIl77ow^$I zv{c`ep|`4-KJ;{zjqv#`<^A>~M&$wK`IN?4FJMo+2|)TwZ9wi<9_xPq<|Et=nkD>X z*qQl=sJo7aQmD@~8*@oC4HL>9VSJPaxoUouD z*fKD9oNEy!Fb>X=cP7?7Q7pWIIyyKYp#94b55k<=?zv5-sxcN5J#H)L66zf8EXC|@eMYxE+5KFC%-T=QmlwMCGHs{Xm&iQekVLdjn> z{9**=If8@^beO)fAjr-#$lH@MnMM}HQMy%l_y%B(A3p@`y7&oO%QX83xz~g2WQ_U; zjc%!!kxXsP@pQ$Z|HNa#>_d6eto&WyL4d%UlC$V#RF*n373v zYMN+{`_o{!^tLH&2Y>11bnvdsL{A$G+Q*)7(scY6IUdS)--uNuu&Qm`0C^tkkp{Qc zMJ`{M15(6^@42{3AFu7LVPD`>I)xQoLf0%rN z&dazbK6|#gu;W_dGBk^!{9S}OM8G_fBGm=t%dT2D5tDb8!OO5$T84C_0RItn~NIoDqswTcxUD4!&Y`I`8FH#Jwn$ zXwnmu!#A;DSeSl701@ni_pZPL2kkSu2iZ@zI8~e{doF&T8>SzMABcgaJ@Hv-%5yQ8 zd$!%aNal6>5Q$m31taT`bu{9fT4o!n#@OWJZ~FkFf0>w4ob^&r=w)eOV?zE^p>81} zwiay(;yAvFy8%6S%rjC_9SUgVGDPnou+{_xw^S`E76!u&tDI-S;9S)*UF2gcP*t1Tk)y z9H8n{Ab=}%oxznkfQbn}xdt$e1W7zE-)r{=UPERvGckd3SLNJl%@Nash@^jJc=F7c ziNO)S$G94aJ-FjyaJdlX;rU%_Ws;Ui8^@onjC?Io21em;HRzJcfnN{#1fbDV`7%>j zarbL4;JTICl$JH$hh|yb<6LmIdA&U`Ew!qFyYMeozDyM!&kU)|4?yoSm7g;aho;_5e# zI1}aqn}MN=H3-B??bf&$m|o`e(x7dYOy$5U+-z;^>qdyGnWk)Ea)5KDf{GNj^d;Ai zC9No`^P3xkx1%XTegG@ znGEA@`qrs?(Eie>y@~M`;>+ox7MgLtD6N<-DQeU%PNOXOWa7=6E71=r6J=G*Nv8Dn zJlp`aP%w7!tJ0{zuBA*q&6Jm5TXGO`YSAUWTLWP?(&=tNF`-YQpZu6qn=9J~I{za^H{X9^9HQS+bByIG z)ONB8>p>3%625-Tb|*XN0?Mz_-K)*F1;NDTe@wOr0uZ2 zdC{P)gclT9CAc)aagf`yUT3m9DQXs+d>i!qy-;k>qkFOm-^<5;lCe1Xb{K)#B2(h- zqBLUd-Q%APC(w(BZ~`V9J1Jr2IMbB=+w!u&;=pn^^Gy=!@YX!yt49g`!-4yGpuI8a z+0whmPm_O~q4p+MO^!jNqqO+4hhja8#HP$ZfB1vTba29^bN9wY)1wg;C-+0}hop z`}YG5iiyEx2-JM9H9W%npkiO=cd_mgwd z&zNy%n0Te_8e_xg=qPwzz#Mq;yf=6*6{)c8W7(jfgUO?sB_au&nlU|L+kU^S*1S>+ zUA#!dg3#tJ4izOp#V5tM{h@9gF|-}I(}%+MmEK}y{*>jnFrLFM);IV4{k_TLXb+3{ zG70pE+~9>bgn5L<_=;~`7sqD#HtgY1d-j8@HlY6%>z5lEyepAl)*^kiy+j*vN7ZHT zIbfc5HrXHaaqrcl6?Wi%4vafPT$2+LZ00iefH43a?09!d)OMzwdGkY^IKKtiuK>4D zfy%ljqSROBgDthX)(NcN2X&pJ9^USwL6s@5f|&=_1eMF*+tqhsU@a~m6J2iXu50h{ zS)4roCS9}S9C8nbl(6tF0wSAW_&t4v*zndDVUECk4q3hdzX9??IJ3uFR6)N2<)2x% zQ$S|({>2G#82Y&J%Uo94g2fOv+xNaAy9S3=I(_l|gIX!QFm!^)SNdpQ>POz{1QUvR zAPqn(^UZFqT`J_PoZmQ*`s^228RQ`^83_WT1k-d_xqGHpL=?cZ?&CZVT9`wg4#G1j zVJc>mJ%VM+wRfhzHLrj$ZEq116?a0#Qh4eq5}TUC;l{jOmM{;V7Nk7MnOu{oRrcb59WkmNum9o&_zq$Z1On0A03SSV1oESH zD}G|Cc)02m&ZW9j*#RuH`DR07k}MHAY)0=mcrm=wzRoFp57PQHulFmQp>3}GzC0P_ z!(p&EJbhuj%s4365{j0&ELL?}R& z5B#dV^T<^J}&EBQoe4r5GfTBWb zMC)%&T)ZI?uMe7J?8LVU26KjZGw^Ofhw4N!bmdRkmm#ql$WXk0l?%yDJhL!-<| zCDL({zQ}sQb7xFqH+Cm@Z$+Hqsr+>4W57HGhy-5hs4q7k;%0jDBoJRNGd~Qg4gVly@{HBK)8ZJ?SbjKT z?sn7H8-X=DJNUQpOZ?_GA833g#Cv~YBUm#;=F*54^#q1nvXTSqlU?KDJvy4iTAoEKLK57g85 z=`q_@>z{?#pF?X>)!diBFu6=GFRSso2#nM9c<|{)qjdREa!+j%#p#Rl(MRaU*lrKR zht8UGBpy^OW-&oE4TpZ|F8(3O`^(TDl+Wij#0f<>e@g#`M1y$VRXN?Qxt!dbW=TnL zTv1Dd@7{(lx}xH;>E4vYo+G$EGCZqu!N|?v38kp5SE3F}I^%=y->Wz-6itl}4_`{x zC}IMq9EuaUOou#;+%7bQ?6G@r@zg?`^^mg0Ed+ zy%-(@EUZGw9iAgSz!UqB#E{3E(dw zU{ad-Mr%ba&I&I0ZltE9WTd$BvGW^`CfK$8P+KXe8|#i8L1yJUE9mw`+*LF`gU_=^ zGqT#pig3`&i*GZm7g;Pl13rL!XOwON+F}zX+S_X9&v9y&*vUJIp^o0)TfFPWq?S8; zQ6WJ`m(ERJ%&Y_3RE)e!yE}}}TQUmkfMylf%*i$w6(=*4Pn-RPI**Cr>btmAD^PxV z4{NapJTpu!_F#2%y5*epq@Bj(3F4=Hci%#L{#PyJYS_>##=T|6;Jvk|-GE16)3-!$ zl8108Mv0%;ymLS*_^fnB9~dx(GI43#YV%@wVficVg61}ycD4P|h`4D8l=p|lamvc? z4NRQh0Dx0HpC|2&WHUCGWR8cR<#WyaMZ#G#xTaXYE@^f|FzX|>| zFg|R)*T$=C=4o%|h1}IN1Y}C?e3S~9%TNUAs06N#X?+ajhuJ-0Qt`Kob}_&PaKV=P zcx7efFSpKx@ux-9Uv$ySP_wTcWu+&LY7ku?mxb!E7RLs0p$w?c<#1 zI3GJPu^R1e3tZW*vogJ_s+R5gxxWHC>mKfUdjjR`_t;94iHVMCLhnA#9(kvHdtkA# zxw%;`>NL~JLDbU0m5`Q4(k_cQ7w&wdv$N*zGUMK@^4;|euln&0{s3d>M=g7s5;hXo zhkNr(R^}~`BG}BwMiO4B7sKZ3*qlb=%7k_c?K(x!zwL8e0z>U318zF{aDTi0)4yV; zRLyEb+}$<))qt$#+W!6CgtL01R)Dujn2KU^sfv7}JzjRjMgnJZ##v&SO5*gL46Q(L z${~MEuQ_q*r7GC1fh>xVx?A3&eCDB^N)Q)pMm+I&yJ6urqE$H&@$LfDG8#D=O zzGR$giVP~M?AV4nYF!-y974jv{s94Ctofip)OP>WctZfo%F;;H@L-DzHmBY2;y5_q z+N)c3{1`VWHZca`XOQc6kjz7SyEuScRwmFZqh8#Ni4u6D&8=<}7x+;5d-_<8V!Qd! zoq-{IgnNBwkAan$ZP|97Jz-SDVW6aq*2o=K>7vg{sU{)GwOT<@I$#za2&%1IJJxysj}fl@Ul7r)E*vbxB`xn=sW z-t4VV8f^VE6&dU&Uqq$f#J~F-12JM)%cQC(S{AyR+I#ET!HoDe#zRdU+k?1r7UWJH zl4EMB>PfYM#5R0c)Z$}ylcMuSr4@G63cK}0BMqTRp6N&={CR{N$%+K05$7HrbY+Pl z{?lN+Ue2;?=WUYdehJu@vs~xoJ3cWkAXA+b&J+t`0?gDw1jO=5#i$X`-;fQ4=ZH#z7a5`Utwh0=!ZLueR`0(NJ0sEmtG|iGbRV4m% z_Sli`H0$p&>z@uJC?2btU5tE<1MBVJ|fBeYJYUVH|CMeBacO|6UQ{q1X5kXXA`^D({r3(?LSs1Wv(PHTf!40bWzrtCt+}~ij|!VLnB;`@ zaV;deKTQwWZhEN8JW!Vk>?!~tfq;459$D%p#5GSfmGO~F7gqBQ|> zg@z4yWVy2YZ%aPWOKyjv&RBFJdO@l=?{?}9SwY6*#}$t2TvbAzSnI=mbpZT-1Wv51 zH7dDj=fhPXT(AqyaywmAju9dv1Y7E50NBl`(J(z)H85)YAuRRn4$zj{UW>I9Gj@)H zdPxc#$J`PU;iTaSAb)@vvS@nz@7&-2%!kj9Ni+^M0Xat*d3hjCZ3OxMX#47bDD&<8 zK@?YVRYE~L8MNteoq+1lEo1sAwT|q!XkQlmaK%|+W1VorYi5Yt6PU-%AUW~hU z?|$#yUH9id$_p}Yd_Lzq=XsuUUgQkrp43*zazn5E{>Z*8&@Mc*VKOND(fak$(CIWb z?)ev#AI_cy(}s->-}`&-V@T&~63EJ&2x*=O!AywTBFE2QPAaalhOQoa|8e^L&8?@l z_lt1AN}Nww=~41%@Tc6ai!PMmujN0CgMmIo%mX{LigRf%{@GwhTgjnMR8M6Q&p zVn&PE@qV-|%`K+BZ%XhF)Nv1@LG&x74c#A^kTq|jS9DUV=FZ0Nv)`b zE0ewMLX+kX+$XvW@iC!=*w-iIHm{|Ps|68`SlVw|DJ zuOlnD(l}bplxovFt?O29wXe>#@i;| zH;(HQUsFv{hR4O0dg8myGUiHKgfYbauGuB|+2v);0Yp>gTz+X0hvnUp6+JL@{%zLP z>(4{cTznlD(nKbjb4znER0rk-c~to28sEw_GJf(u?mr?wR`sb34 zCp;ONd_XPO!>H>UB8bbJ-0Cm~52184bIzDov(Ja7WQj&U{Ff%@(O>bt#tTzhq!;2< zuP-jY@sz7%l)UxE26t<%Z*5eUu_X(fj5N1AN0?rW#t@4_sc^octL?pn<)L?ylXcI1 zTm6rdr9|Y-pnvmYPf>!L;F6S5J7NTIY|#_IP_q(+7I5iYbTmW>?a;zz6I-9GdDaCy zKu)xCC2yo!VAb8t{Zi_JjyzPW3q0a{C~uX2&EQbzGe(q8y53ecM!$Etti_L^NrVoD zF#owy^ECs%ugB2k!b+M=)~CX`fy04uBD#!1v9x3<;5-<>sh)@PfRipl3FY6ma;Z1= zm9mqWQ}V>!p0mhH9<6(R!IGOHWGIrQjm^p#p^Du+_7OVEMAOt@idnL9|Fx`9SZ6ANR8lDapCk+^6u-XP(%y z1lXP7l%2X(p*`%f^5q^~2>cut4eru!?hp$&FX%QtPZAp)`^}A{yBKabFL#zT_Bjuy ziARRYOyn}@^*MJbeHHUaUq_u4wx^c;PX1Q}^dp?`CkJRU@6u^5RVB_*+Bd1{6mEo} zY1NiV=dw>aS+Hr`C9`8`q?WHF@o+VC6GMVq0)6N^rC2-r-#xvr=Vcp~*8Am$zCB=` z8PuJyhqW+GH}kb+faH|~$YI;QDQYGd9?s#Y(y<8PStWu=Ma!6FvBBnGgQPy*dN!|f zoB3J(LS-XUk;^0PTvrv57S2f{@s76UC-Rpu;u=KEGQ=CV%$8tXm#*5~0;}k3X{d|s zz5Lm_=Tde}yaMbTA=-Agz@0l=dZBpR>c}?A#IP2|8vEd`ve;64RuYFZN?kC#OaWED z-9O*YM}xaYTY>Yx)5maXI-)2;8TvwNKyT29PYJ*+reEWttAU9C!2DcO1F@}X!0V>@6UJo z>vo2gi;7wcPE~-$PiJC3j#8U|AWl+mB46H#JbXJJv^Mj6<=!*Lf^e{@neHsKXWrq9*0m zKR&ziMB5srN@wO*cXqC1n6lSx9<>fw^{4E)C1;lleI>6E1O)~0OK8!#lBDVGN5qF0 z0^t?Ju!T=Sp46juC*cGS!MTbh`7mk3Tr|95n2{x-sbsC+O^GYQcA)A}a1 z{5V2aup^@D5lMWOn_Rp0kHsQQ&RCC;bEm7_`j?m>s%{;W0)a9S70ktvktZwGhx|2k zb?a7F1&mEhu8YQ7mCp)z5SCgbg1Wl86yv6mmFVdeyuO#X-c~pM0L-dl@ut}F$Ls6c z;}SvEo=efrfCLQ$+D~oCVm*Tr+p7g%Zg(P3rvH-J!REO$t)I4y{N)WRD$k?bV0ma+ zTf`@azG%L*$t#DmSZ9_xne>83g-Z`7y5^PJrMy(Fpgy+a85p!ji!MS^eLyQuMLte& zV7RYhHvFRwtb*c@>%*{AGZLEB=MC+C$!HpoO-gTVXr3!As16ASd*Ut1;=R_Q#oq3Y^GcR}K#Rwmz4OrXi+KeF#4zWM zEIr@p+;U2jme@LWWX0oKOfCoUI`$N~Wm9Z~X=KTe-^Z<7^yZYJgzD?*6f!!lfwjs# z@mP;l%@{yz1r84=?2afbloNI+dwP0c-s5pzOpsDYIw^Ixwe46>UDD#KbFK(p$17;N zgFR#o9!PWfh-HU6OU@vKc&kxhOs(l3*cY{R85@-igM@j-Y4&&tEk`$hS`VY+@sHU( zYrQF=W9@v3_T6KvHH)z)ZyLifXOfxTmGj4{|BpNcE?ww@iq)57_HB=zT2>Z0wf%(rpYi%zLY6r-$}0G;tPNc#xtsMd&7MmGym1@kdajcfW@cuftE`5W zxmlrKF>2Ow-M@03EJn;lnXVYx)M7i!ZRs5%ef-v5TRr(+d!E@yQkc*U`ieu_=M0d&?;quN7 z&C&2IgjKFn1nq!aPt0yB`_o6oKTE)@NyqPJxG}zcvJHU5eDb5fh$$u&2BK5h6PN4e zv#}4EQ!qg~V~#C{>?Y-iKXg3Wo+3D$KhHpNd+yi&10SSA$BRe837n5A*ElRr7^FBm zbv=j}ocNTap{Iw#&!N`zAlkONv-3p%&K!KWAZ{m{OBvJBGAv5ipp13@e8*!iny_tL z0f?je?rwm2BqV|`7&)=UcgL?(wz!Y+0upBZFrkpjbHV3fq1O78dZiz9*m<#zZt(*< za-hbSY#o&I&mff>y`{fah94w8y$*A7a`dK?KMVv$X=@v~(wo^dS5&G~n(fN|m1H=VJyWy+&cQ*qr>m=)La^5xVw|CH zFgiWg4W5?gMw9;L++H2G=cmH*F|^0l!cyE`#WadrucEWixLZ|S*OyMunaEqv!`{P# z5fDu8YN;f8RfSYMlj*bE4bciO-ksG^HDCKbHs3$-W|u56rNo$K>I zcPnrJ&=B=V&lUDl!gdG8v549>1&M(DL>XPV!63Fi;8Wr-dCaP8l6f1@lGQ0P5t8$w zPBR)2ct&{dqX<3MBaq}p3M(0lGGAy^@H|E(zWj4hadCc0$;9+y6*g+phlL_EjrfW& z{*ooEXn9$ukhDmP*aoT07@zEgVJ(}oL@utvhUPVpD3`m9&>3O3i|w0={f*raJRX?W zb|(e;c-FgTeTVzItsXU3Q}R!W@t$uJxB$Nau0KRH5(HrWsju1m*=gv!76TOk8&!^rf>IeWhOGs0(dMnD_JNM^1@2{3I$WTJre=`(h!| zgJQ*%QC;7I|H?{+!XTOc`T=a51aiLIX7Ir3Y|k^K$E^7}*zcXAju?Ktfs+%h+v-d; zSha3C9xEN|KuF{k+nh*()ucHdsH*lkyck|wafrKO;lS!lmrqu187a8;(b3Vh+o3O| z{Y+wOM36NiZ(QYj$Z4Cp{g>CY=s4aoa3;v&2B>8mbK;OgtD}^Sd4>E>7mr_5(JkOy zGNWs`aW?q&`D4$azrv0`c+kxZ+g@*wsQ00|@+;7a_NvCOtO@H15qsZEdUW`-`_KA& zoA|{lVybvOBr#0SdGL_?YC&Ax#?rXQ8n_nyTJZl)Xn`LmfQ=2|fee+j1#2LQ2szEX z=87Sj=(^)H*DIWzlLG~;y2`z+%Hemxkj%Uo+<H%5xvQe_z#gUrItvli!wMGStYo4|1GIoXrCe5?)N) zmD_gsPLPjmcegik5uEz&i*+Bjxb!N7KF))ES_Q8BI$mcw#kk=4&7`ASj%S&8M03h; zhuFg;$@DX^&T3&V@G=^9w#^r8+wO;QJWrb>9hpN)o+F&^__Jg#7Cn3rHk4ic&ztuaNbTO#A(XBUIC=(;o7RCkW}x-M9W>;1HIu=dov;5eGF-aFG=wfJ`bMnjS0 zUM;xEmR~~)R70*V?ZYW}6N|ca9Md;1fQ0#ZxPV6M=b?8YMG+Kt7G=XkCm(AKn19Yo z0;5CBbMYQeSKl@qnRl}_>e}+`?lF)=kz6HUIoUi@K(ZUNNHb9DUG{qjQW%2ls(OPK zwdbTH@m%E|?(di0AkSaDul~jmhN^L@XpZXjp+UK&|5T+nf1GUPh_B13)IU`l&2da!Sm!Lj-(JQufo{lU5H9K=UrezyRf%w7}QT*rHMGA z`!dd_S!(wcD8^4Qm=AoMWfyWYCVF;d*Am5$<C-37gnGcwOAv&7&rGL~Om^aM*M| z732>>)#XL-0PpqeZygg0H7L^uPC>_Itibe`b+;DD@kr_x9yZokm(R0Bnn)LO_l&yF z9fge{T=KRYS0=GMs#kdn_y(Hp`&XjJ0>`vWW``obMIaD}>=HTTdfeB_2?2JcFOqHr z-2*{UUB_06u(h|WutyEKz-*^;mdPqn*T zW7kh=^9zkv$Q^L6yXsGBb@LKiR5apKoL2f$`mh;Cqw_+!+&=s>1y%D`oDb!_7W(f@ zQ7uSw&=r5-kr3OOp{ZQOi>VMC^IXYVQ~;*~^9F?<4Fob4Q@A}?mv@8QN6wY3cAGCE z=G_lVY<>!|_+_O7_zU6&83^knrAQWCIVLA>m-$XjMD<7}@nIQs2I7>XMfRiX8b=tX zWeLgbut4R*U}trpsWkL~SKM}qsb{|)*=HRhFDZqaE%xlu{Wqo{su>hSymCF8p z+ivb!1Uo5JJ?c%X3(l-?;T6*T5FM9Jl`k3H!n@-N0{~Z3px~fzflf9bjSjpR=Y9-i zt%Nlca5TKTC9$d0Yumu+OxWswwzz2PK0tiB3QEtw)PX{~rKP1^78HEeV_nGzRc=JE zc~^Y-C0<@1P~Z|n!jW$i_k24AclivD`+Du^`ezc`h8~qHqRMrb;pibp(?V5!`$4Sw zN3pz#a;)4fICEKU=@N}~0nA>D?W*!mr&6%IReM*}s0cX?WC>t`?C_Y62=*@omVXZ> z!2pk?3ZjlwZjW+bZ?Hmd7`p@g{oT8F2chyScK$Lyzi(QcCq0z0Y}@1n2aQf|oq#IN zVNiT=F*7qS03+UM*aEH?O|!K&-%yNR;k2-{92||%CxLBm#O(>kx=yi*?XC}(Ol4-- z0Jq5SnHlb^>V1kJs^;owalasH7@IX4iup z*o~e*>j1U@06Dq6y}k2Hegg~!;KpOFSY5Z-Lg$Uqh))msT+zFeX>s+?dVnkrKrux> z0=AjK=NDsd^An<6gTaF+#QnAhH4A%vAI|pcZy%-PW8w#(C|! zP?PrOx&(=@p3usn4P+AcTdNos7l(ZUrWK~ZXb`(>b-k&zK7tO!;{dF?EgCu}BG z7YaXxn6;;x)qjdt;I{aI(7Y-XXm7e;*|!O-P(~1oBHqB*P@Qo7naxx?>-Lyn<)jN? zXy|NsM8wJlnWNbW?1lU1c*?+--FNIpEUC zza-W~s!iOiss9+zjZ&=6zGwIyL+^DP>69L3M{x1J-56(&>fls&T%4JSJTxgLqvD22 zEbp4i!al;LoiH4(jXg{dwGiXn4@Lr@cDOxvA7CyEHz>S*et{u{$af_WqyQJsS*d-% z3v^7LvD9Rb?ekdc6N1m3@&u8>DWK1JIA9$V6@Jiy0s{4V%igwv*v3d`XE~fZ@Rac5 zXQw?+RBm=4-H|8niirZZ1C#Rr5y9LFungNfggtTSOAZc>0dQ{s<8T(j$smv1I_*I% zv0hD;mzU=a>`i>p7{guGY%>cBa=~63AEXV*?M2y)EN9$Ys~s+)BC)HUC+aY*CS8j+ z69W++OxF1jZ{b{I&XA@DhOx8|3!C`&jCGc*j@P_R09o-_x?*RZ0^a7Xb^4rDsysq3v2|WRfNWkO_tMWr^Kd8=w*1A zwyi=TAwouLA1p(}KlC8qgP`)2NnX1IJyIwy?(@tSy>Q8_<_MD=hm{WXVSt7wu7cYQ zxSXSvK&D=qdwqvbK;Wr45=9F=F$ck}nwkU9^Ql-ZL_$O%unkJA>)C^io2baLjJPMw zT3`dQLHYTYb;=y>qykdeR65mC`p+>9Z`xgO3dV!v3uX17#)^|3B!}w#h0wU|WOMp4 zP-iaq>u)D!*^t113+PyMO&8b}%Xj5eg~|t?G9-VbIa@rl<85IXKp{59R;*Kp!*x~} z9Yuhr(o`<;of6wm!~YlgZ`kDl?ir-;eC3L?x?c@-SxsmYHgQ(xZYbN>dmEgQ8;R(q zHgF&PRqt*k-gctw9{ET%7W%q>Bp#nHx2qJDdGc=OMJAOk?S!~{uZO3E=GFloc=SIo z7}KapzzCLP|Jk*!zDWpXjr9N^s?YMtBYFmg+G+hgS5$qZ2$dD>FQ4z~+j~gGjXkPE zH=uB0DpITBy=y`*2ngi5pP64*^U;(Efsp7! zH^N+hp2s=sfzABK;>&&v_+z2#L8n{j!-=6TsGtIzP)Z|DE2;Kyb1#Ph*o#I+dS}K# z?j%FFikL--%@};n8o=`4ZLhDqnAPRHObu4C$uzE%?8ag#0cJ3x_Lqv0 z@Y1V%2DmMS%)GGCxom@{n^Dn|Vi|V?$Y1szg~Htb3V)iNmkhv-0E^HDl>Y0aLW%3( zPH@cV&|qN$P{RJQo0a`cK8=KDxl-_%c-&Dioq=bQ%FVeS05sdGb zBs{ZI^qDIZ-(RVyxzTq#U?9(V_!Uin&&TS|mM%3yO3(;Xes7FZPX(YKqA<)56QA11 z8=|bjON5MnkZYTYRUMq!oJO9W43sF=>6mt(G+K(XWl9}$IKtwrzrqh6$nPM(X60xJ zctdJw#AA)HVIukt)98PQP@?E_NnMV@SCG`*J6gFjYi$d1RR!QtO!J?2m(9Fs%Cjb1 zsQ{6r9(poyJ2NJ_Yq!TW`ASFG6p=*7XUQnY#jKb#Zh#UI?^~q(*)f*?XZB|falW_z zbPN%a>)~6;R7a7j>$zOWEg2&1x>5C5&t;URV!2gj9b@h30|+YC0d{T#8r807T=kSjosru+sMwtf%9>LPkl%JtgZC^u2tI4+}nBII1-2*yx<2oF(7>uV83yN zUnv#SX*=_Ij$@jwmk`=2Dxx;=sk*NCVfkv=EF*TV%E+XwCGuDA8!}%C+evNEv#ZlD{MGSwO8Y*cCOPp25F}n6wM<# zj*ZoXkzR-qFZg*x^_By zU^+zA1kz-PgD@-APDx7~7QiJ!PAzStW+ImQQ-cpqj6rinV@dY!Qx}?Mb-XJciU}Mu zOiFfjtl@`{5AL85Y-AU#NsSiV+_LdC>*cy)D!!bsK9x%W9O4@(&f&4$;GG8lVlYN; zXhs#COkoKvtf5dJT_m`gn`e)|)~x8(6YA$jqiiZn9;KIP5>N5Rt1kH|5(k$1Ir>(& zF%hk&>XwaWUV%5Mp0?Krf3)r0fYYnxXuVvKXkafA<8?R942!`|C~&sBwA=EB-xpi& zW8U9~Z~q0hA-%*K3TVw_l^T|#r@Uq|!<~Kgka0EU; z#S3^2oj>KgH$08aSY0zRqBx}+=MB=W(caqOs#$jQ+4lCbJ(T;QOR(1q=(@g3zhEVI zs^_@lbrsD!F7X!jngL^ z9uxobnmzz;zhvk_S^41$mxlJ7yvl7-@#2C;FSs>a8gJKB^gio<6MmZWJS+S_XZ zAlTfVz`m<84a07R%uSI-KNX&Eex1uvtm6ytFwLhAdgGA=BE;0vLISvj`flH<^E?Gq zB5DlBo-{TKvH9{|SbCJrgw_tCla#2w)POnc7_(T~75Q7-wSPL0IPk5inH}G^Tg-?5 z&JrVya;7cx4jpwYE@Aml`5>I^p>*c!QLOq?J*2sr*n={|1KWH(y@;JZT8t*oAfE3jv0Q5_L2<0Ue2OwD?j zN`0TQNiso6cZtKV-N(7OehF}X|20$%y*qZx+|GAvX&O|QF&-1f^Xm!GAZR~EGcgy`esC&Q)%b+;m#DHZ zKH|1BVwhHNQ>#3hYsyyQ5MJ!KBi}?D*$;%6MDf7kK#b=&T;D4B?{!Jw1DtcK2`WT21lA0Q}vZN-H>}G?_743Y@i ztouCSETh!3!8OKPENp?TR?(Tx%wMD%2v;P=yq8>T{$Le-6@K~ROm{r%uZdPGQ8g!5 zQF#7OFdFGi#o5+U1|GcnlI;oi{0^g9n94a7A)mD)zMJimh+B;6MtACc2$pZ4SbV1m zB6#<0bkH7j0gkV)XUtR&z5@}E`=+qDk(MaFSFYnHfA;ubTxq1_MWNNh!QRHtIkg<=Usl3f)bD zqWj&cBY_itf^6o_0@bHT^JYMk`dc~T#ND32H{e&k7MpSQee=@(tr=oTO)hSFeilCy zMb7lrEK1)ogrXU*hGZ0b5DKgOvD)s9vKYf#TsSp$hUO#5j+_74Ux8UqB}1-fX0iJp zD&m?fQHy|~v6*a5K@*791Ep#kx7VgsFENaI8QEthx}2J+X&s-n;)0_eXRsPYCHXRn zB~46V4_+E&1TRYI;tk69$vpP?wldXFAN>!=N6|$`ZV-;}bh2KULJBcH99t91M32?% z1B5}~DOh@G9^5f;Ds%0CL5fXLA%CiH2MG~#d8z4D#eAM>_6jM@CD~M)P5vL-q-WU~ zNDlHpO!CgN|738;|K5(s$Il!EzrID;oEm1o6@dMb|hV*YIQ0^Cc_zcdcRnkhA`DZ)3YQ{}Z z2Kt7~$Lh@|h_nOsW}ZI)o5!YDy7a0YPtIUAXHCv*fJv?qB^oJ2`C@py`Cu}O;Z9WGY^E$Ll~y3 z?&IT^!W1eBE*5kVcZ(yeyN%+wiq7vFOwLcJUP^rlPB7nqq;4vD_2he=U%iscFtUHz zysL-X|9sBEo<5zJN|K$PfGDu%(HZ`kKc^t&9CCd#Rj0cjw7Gj|RI|x#kg6T;v)I(K zX?}TcB`1P51B8`vCF|YwAlbRoycwyHIg2xC|$jmyV2VZ4+Xc zko{zFLR7gZ&8=Gv2-IbS?rc|hi{uedwr^^^l(KftLys&LyEv*xmZb&`#?`P+vCJF~ z%Gk;IsBpRYZ_@oAM{)g{=g+aphVXj1Te?!y}oYxX&rO$hofC^ zuG<{=Z-VDSYa;c1uAuV1a2kKiCw(za|DT4m?hXp#8;N}PoeU;@cLVGlg9^}30K64< znpmZouhP)j184){k8D=~+L+*0w^@~-gTX-M=FUK#T(6hwWUXw#VljMxml%d|YbxHQ zV%n;dm>4m;3dadg_oouSz74PSX_ombq|a49lSEk=y6EmU9*?$@JyMrdy-8^UNH~*E zyCVYn1Xi|4G`4JCBgx9WPV5FFGE0j;t|gpmk9Yf`2g@Mcg!5lac@Z}v6rh^C6cA1B zZy-*+^cDxa->HZCd5E;-Pw{}_+YzLe?!%vD)5v@CDZj9gAEz0F(TR|=)QIL#qX#nqnWT*@Cj{cZ%Y-`d^R`HPo09u!3;QWThf~<$J`6lU-Dg=7DAhd!T{#aSFP0 z2KNcL-S#j$D#pZ@8)zA%vJc3f4?%Ef{Mok!|1Y-2suAJ1|J66*`NpEOwzAmq2U?g( zZ2Jva2QUQ8RZd^U4$HYw*1GHVRqxd)Cp>%5M%x>z>j3vTh{D}=o&;%^isvU!R0~) z7npWl-@KD1ufB4TdaAm7#re$gadP_#Ld=%uuzdk9aYNg_BY$53{{~RMnRk0y_6-XJ z&ZO<^;NC6+2;a(d>j|8%<5fx>&E|F#Nnu9iUV@_&kr{nL9*p-#&BnDfE_U5WJMqBi znVbqp;7fdX-s6uan&si zd;H>cIR5TAQk2znJu+8T?1E z7a;{M#O16fb}b;!jj@ZQRE%jV;0BgIzMM=PW`D%r z++Bgj%bt*`AyFPbeF%eD-ScYr@(F)T*nD4y|JmXKG5@zkT5ga#uo}iyc@0_gVv4QZc6B4${anb&t?ZZC| z0c-Lzd<{~&O#&I-)&gGHtRN5qZ+L+DbE$CA{Z3F4~z+~u)a?D zRG=3EWIQSUS4UQPU$j0Y>ih=?iDYNy4{zJG3_#x$U6Vt8h8{7qH_!~5sau*kuv4=5 z#V%elp)%h5@+ueQ-|Z^EL&$=S#gn7dbEyiSz$~3z+I4a#!&f9oB)2}~#o@}H`;dbT zYNDWRL^OXT6h4E<{M8{K{sMdlYWDfUv^B=@n*x*XjmU6OeIoh^hu4-kZ#B&HfC%V= z8i*!o%=aDx5}EYa*A6Q8ZIc!b=jp^kGn`1vO|#5Y*2p5((sNLw&>wVkB<&v$(e^Ap zxZ4j}fPz_W9PbxY6#@y6OyQLOgB`w|1tY{g!jlFhsfhr8h;PA_vUxQ3{M2^^6uOrG zGb28Eu=bVThJFn6f|=ggM}Tq-V!(6<_QP&&s>Ok-u4h)OxtBSU5rF#&Do505u4GR5 zspt2MAd#iG{^;_WD~mv$Al;#^J*D1Y+n|8Rr97qJ521m5GUgAA%WEwE?t_AMAz4Po z3quw9Qtmu22a~frNjV(G<6SIttu;UB%7JXVN(JULwSo*O>}`ZFpzyj))acr}20DW-eIf`-C#Y+{X!vezgwl zCs)w(CgoXt9+8 zhi#AwLLo7K)r|wn@8DlkJMVws;7C!rpEyRhqV&5>y`L&;O@W$GU_R=80Y~?fJE>cD4I-F*T?hfScX~@jD>firX(= z?+0umVT9ZLowI!;={v8$IU;|q)~N{`GWL?5JNY;O(d`8{{QK>h=RxT*lR4-?rmaX2yCQ)5*pPXZWF3ELT3zUk(%n`D>LeIZCz9YcJ>T8&g6+T^t$8#la7gU5B^v!8>QlP6PceUbSuQ2Dn7`I6m4% zeD>jTvWJGEKC44PNqUhre99_9Ykvc41}EIH-nrbN`@l&j5@?`#D4SkUJ;WlZqVH<= zgQJ0%-0Zis{Xq7DE_Ig!J(-@x8-;W+_{89Ecd8kG3vkr_=|GA3jg$K?JK*n(N3z)a zwYkvFBz~qzQQKi4UKXL;WS+g^502pur3w&(m8RIM5$Mp{wEz91d&PkfsLLX(A@I#Tbl>F$Sr@>VLG^7UZhI%YMAe)V;gW z<6QGkZ_ZD?s>9F=NZ6hH2*6tYDc2d$@!gP-HJC4w#VbPQEg$|g_`)xJ!uZTO(Wh&% zsDAieenHVBi9*G3rG^fTki`Lb^#0a_jP_q8ZOGFd%m4lCp8@0Wch7!^=tcpvoUYX@ zLh7sxXsLBNm$06U6sWa!`ul{POfP&dXI9 zaS{)+Be1-E!8q)Y0dttXTy1YReeL>(;M)zbOL~iF(TTYjhK!7;r}Bv0RgbDoEicX& z(QAJ`I_?{@A&~H;BRHH7)f(miTD~t3OAPXKKbvitTRwY`bFK3Q5=Tuqqj}WhMufh zLVBRE7rdjr9cqq8E@h*p?*mG4BELIB`7~9nM($=HMwhQaA!b?Qq6~T{=EJcOI4(j& zKWp~zjc7TfrND$nyNZ25ADOEBto25&Z$Q>?$uKCA4+sAD_?wvA|=kz8acY@6|Z?Z`wOV?BZ0hizh#vgp?aSXo+v-&xa-X>>&me_y0@JJnEnW9a;` zx|iCyQ}Vzd_ZYNLKyW)DRcVv-=Io&jqIHp;O~$SMG)2GOYMKf{a~O%&Dd5BD7qJN) z+)gSpgn{~XUE8BW_~QokIScpBzZybN{+!92V8Li);@@Hn7PIv)zAD4V>^^h~1Omrq z1tEWxO&uk*+d{!LurlOVi=3m9C-C{(DFq=`QWE^(rE0$mDPKXkhBQqiiJT<>pJ$fI ze6hJ9#)FNTh)?=sE|o?pV-9z$rGNI7_|V?0w{7NX_ENvHc<6;Qd6o7v3=30~vWm(r zIY6~+s*pJ{IqC#lI7#JM+z185eb4z95W2r3L6h`XTred#^E>34SOpZ{ybX=sg=L!Z2XF)-btc!^UnZncRf{`B=zP~1XbDL1yWhS z7d*Fny++P^g}mHljwy_S_%Y8v8U&oAK(5dBJd{Fu8-6Zc)=!phNdT7kbmpX2tyb=>VG#Nk{OC##KAjn#okk@+-9)>f;O zBoW_&h#xP9pTm&s>?6R%Ict0?d~Bkc^-bCjc3IEbo*F;)JoNTk8D$kZ#L5K%Uw!&` zrnb!N-7fP_9@-)TxT=I6^`~Q0qLe_43^Z=&>OTPU%0h}97Kyv}B!J58ukuCkeBZaA zDfrX-OuVacYO`dhcJ95c*m4pp`l}f%C%v5(vvn?TZ|DC$QGY9S*@0iue>_?r&{qlg z$H)%f%SN1em>#P|+);C{Nb?x}G?@%qY_DCRzatPimbLICM59isD5BHzplE$Ec3>0U z9MH{7B>fNUE+_{guc^>=h#Q{)Z*EQ8w@3kevz)JWJ=?|LC}5@mmARS9OG3DK*{!r& zQ=hYHt{ex8vG??ZMdg>|zW@{w;NXV(4DKscKzy8pt)0PI*KN7MFw^J|(Zugn`+vbd zkd17+wT`2M6?a7)e|JqSZ-5_6uF3t{vhNs2PeqH(PCs1kLUDXILYB#iFKH+)s(Z2m zji_rDhwPIi!~pqCCjlqk=f6*_?voghFe%09n2V&5)?I@kSmUOc9Jc|;21D!BQAnRh zTt$+ss=I((nTmN!=xOLi({k))m`2Z4`vtA}>+dCcqJ(p3uA2Cb14|n;mm$n!4_l*#C^_> znlYo`<)`3H&*N`>ysaC8E$I9~rYjhvgAZ@--U=@e8J07QnW26b({WP2LCq$?-M%|@ zYNWO8e24ioKT5H(Ok#zCxSI(*_r?`7?aJ#csh`!J=~7p8{GzC@z&Y@u6`4?pB~aR& z|8vdUv_Q9mh8HP|UeBy~St^>kKPlma2+ifU8erB_qRPb1?c()h_|8FhQ8P0ERSi*% z#o40CVKe(Jkx!C5P_ICrZ-n+N=duUOVg3odFBYWMX+3$4q)ozAyYWz{E7A&dmG95Y zk(oQ%#0gEx@uU$9_h>t=dPZ75dO}A9s8s#!?<9{r?D!bNHM!CZU?5+b%zYI)4=L31 zDuXOsDeZ~2lQ6zC6he5~+{#47$7wBc!;)(9M%BvSOZ9W*QQ#$rC0c2W%EdUZl`P5CdKRrt4 zVw-@}*~;dO2wkb9Dt^@$%6)p7Hlv7k?>qCcL#g-xwO);zU_O&)z-Q@#b!v{slZlAx z+*}Y*(ehxvl@!vSc7)Ta2tHtOLgiY|TGLy(>%VI$Jq!51V~!*2JSY3q!Qc}bS4*g` zxV^asVB3ST@haX?TnWzI(=Zhz5wNh(-v6#flRxZm_bq@rjAs(ZxUF1u?dt+Y<*)s` zAh+g#<&JMx6fsC?JFD4NzA>(*$tehZ^m(hIQNj%S%Fe;~yc-=4Q-(VP%hBa%72Zv=1l8V& zTQpbGYMbw`>6)+WXO#`R>%x6Q;Dj5u4>KiNJO5l%TEo(W-z_KmA>%F35Xa_1s_z{o zy`XQ12`dmu^1ltT2QaRp6=3??n{+EA2dXh7t>JU`%?n7#v^TqNtWC>WfldYMOYwxH zzE7#U6rjRd8#z&E#jEoFLAgu8tt(Hl9iS)lb=_pE(Dwmb^FIPzoIF z7pcc=Uz7Bie;G68+Sl~_!QDNEulhbF0I0wF@uX4wUg?8cSnzvp-DWemX~x;eG666H z2%Ldr8b_4M75x}b856(MDK+yBMSc4f50pqp$iwY-cTS6X=Ze9$rwa-lJH}L8b9nhN z?Gah3wP2#&r}5!e=Mv_Rx3ee=lDG9*?A=TH0p%hX0wA3H9-lN3kKFPO$kMmbsGGRA zF<=7O3)@nr-nIJXhv#>CmM5`KM63ozr{f1=v_C7x3SWP9%JT>jdH6}LLK63i=;FR(QDndBJ!H6m`3L;o+HNMnzg{)AeLvH! zOg8(Bvj%xzKJ+d^uT9_e(8G>AFfyw@H#+pfKujOGv@!eyuxz{)B=?RmF%z#M%-yuV znB_`3z8oR#{h=fCPFfcXA)-6G<-Jn4t93$fKL5`TkAaZlely{3N3!=t8SbveQvqErTG|XCM!|$wjL_{X}SL$-yZSYecS7x-d^tFtN=lc+S z@HN%_m4!anik~-+JQcROl0N*R1%oC3tt|4b?)*NlKmwcxB=x-bU8^)?PY4I4J|&MT zOc@V0D)5NRy9sYUdyg*_NWad0iGs(H)vAutx$gn0bBpywf%=)FS0|!uk`hb1=6u=G zR*T*7HT6G$2eoseVq-OWvmJ3OW*xw`W#~33fXc-i(qFEd)2uDCs{4p8i6M&-ZMshXTF>+-68eMN-7&<9W+*(R4nTasUn z;eW8F4r+UT{_f8CH}Sbv*^+nYTE3WnA2MmQ_nGL>wNTBnnZsVgKo?0>LXcN9zn5`g zF{}!_UFe6}i}!`+NU4hjx1B)$&n`h?<{D@Y)6TMRd^(E_wJPoI&>YL=22O6`XIA=W zxMMr;y`MCRb96!N^4*WAB!54_nHeCI4aNOopva$8Sq8z#481csgA_eF+#EDC(r6Z2yqs zF)@sds}L_{Po<{V3;PK3^VPyX-i{dwN1b-Sq^HK*>(U0CswQk8$7?Kmo^m{0TKreI z`S;8FPrK5;3~>M9;DIK2LQ>Iox)t?%=BlzDitQk;$GyKDN`fO3gmBOEx zsOZ%jiW*q=(fSwczEu0o{`j^1aevAkR^N$y_@VC^RojrpMk+YJbyrOsG&&3$KEy#c&zZ#S(TC&>jb!G zkA$~vTq7U$3z3-wQ5V=Ujf?_rHyCbLQx@e7E8%{u6%ttY!-OaKDoBS23O^_4tkF{u-ZOq1k}kXZ{x`!_ArNH9qwHXWzVZaTopQ|$hGIbn@?)QDBJ#& z*R~jt7W9r_NVLA8tR<;ICeSO@ZN96Uv($mjZkvn>VAziuXJKM!tuUwp|5^y)OA((Px>mhI$|gn1E6Y(*52yOc7+u zTF49iO}=+K+&=%|ImllR+f@PPF*lcui>^ZgF06HJxfQrU=c-A2H%NiKf3p*TTozN4 z&YHN}n{A5A;}~=9G$%s3S#G~;eBS^AnsUYXR$%7X56`p6*x$YPuVE&SF{hm}=oeip zE2%I-Dz?w{c5C2YpGL+Cz5wOycAaF)sgLq>Bs%zCS3=_)_a7B7)| zb0rJstzB)5`iG^>6hqM#Fi6J+OpF>bmV<`@B1sfs?16r(RXFHWM&9^y+7i@8xZc69 zO6_mQF~#}M^)x~jAA24$Nn_hA{RF7VC~!_TURd`t?k3=YbU6>7>d|_r5BHIJKLpUk z29bnzn30kK<{yzSSnNUXY;F){=%tJ99Y^~Ok>YN~`=QSej~a@2$v+(XKn2UuO6#N@ z)!=*k4`5=M9IX*6&MNI&*}xv@1YyF54YYjeOBQBkx0Z+zSYhBN2dX5@_ZUE{zdB^^ zaso8fWorM^589m`1`P{E+>W`CNd2XEZo={>MV4w%7SVpT?pR&{iGrZ@FttKz12qSF zu}uf^H7%6ua#&Q(^x3Ia;(8xmWn`zawE_bKU0ek(tT>hPe4Q&7%;OJBu8&=B1=>&Av^JgFz0qdVupr zwy)E++&J_5$Zu^_=8mC4{j=Hy%^IMm-RzqhIcJU;EHm1eNPiGw`)53{5DE^pnL z4R-86)i0yZfyEC2j(^}N7-j)tbu zf1OPSO&k;h^8~*R?xX_U!{0l1&%-~P$VHX)c8mJoIFc-qQqO+N`}y3oZM*90sELXQ zVVVMb{ze%Xk2z?tKs5s9Cd15Za1odE7W~vqTFCJ(xPm-^qe~^a;Z_0aCwlGqQVQ)ncdT9@wn7A~sXOy8C$q6noDeI+g5`u|^(~K%2;u(u6W+x> z56{^>3I5b;u_pNn%&bA18~)9aHD>@x+f5^-J8b@tM!K_`UIuQ`_#B&h2)6;oBOXGE za!%ePh5_5BM1TtQogaKN08pn!)qoxcCLuSMfqKpTpW3^+VO^J@T9dL5JtV4*(5(i8 zNprbDb=V`*pu+uOK@6`V ztj!;#D`_A91P=DH=R3N#jDt}M}xsL zD~|Un&BQO8F=@O5;0eHPK-R;mKCL{jVi#r9l!h1zEg+kt4sB3j;C|NNy9QVKD2O30 zCYm3V2UpanrGZc^g7!TYn7g^*cuP0y@%3W7_p5VY3WRH~Fx~4SVg2fL(g6cF&ax@< z2a_#$avv%{=+_%0-<6Ee&ILlXm_vROhgerD;G`b`wL$Y0NUdOK?n+@^VDZ3Q)U`tm zPUZzZ+5bd|UBDxXk^R!D7MyEd?YrxvLcc51yn2+D;yC1 zTvV=6seO#mJqQ!$=fgO*G#@a~xEP7XEuh&1&x`4_RHjxpJ&Q6PX)EF{TV)|F6 zl3RW8Ez{M3+DP&9{LQW_Kz{G_(ffbxU3pkj*SAHnSgSa;A|T+1RS}Q?QAQQfQW{Ya zBq)o=4h#)ML>xLLKs8_Wfr1LA;gM+NJ5w~hJYFb8Dp3OBzfn8Sbg^WUSI8> zkMFx*Ztl&Od$?!swbxqvB)Obbf*jSVGp7tIJ#^_*XS9mCI;FuEzNNd*Fp^|{I(t7oJPb%kp3?gAr=HdD`;xN@`p^e){7!TH7vs04z?X~>|&rr;|9mf<|a$}oQ z&x>#GpIrK*+Pa?xhow%P_A&GE!I+&+Jc=>1ky?>8Y}!8);C5=QNuntl?s7|4w@^s7 zX9cvGSWD5pv*{})#{!G?3)}^mnrogNOU?#jV_I&zU9~;I;Z97MZ4j*|QH%Y`-j`qO zkGdy!i0p^jSqg2Z=qE2MOHA)xyT!QnN^+?x@@M6w51Se3*4ZicNtKT$5GajhFvgwM zZL63H+&KSY*fSk(eSvkxWnWdLv%?!0W4q!DGy^WQOwn?)vwc`o!!zAJP0wV>HQcw- zax3V0H9{h8aDvwn*h>h@6m0JHvsm2$GepyXfGaG5O zW~_p3dG^9|y*tVNbMh7@5qdQH;!IuAnWgdQ2A5%930&`Q?#HZmaII{%ap{iDimP5w z3mh(98Z($QP{Ne6q_;7BgKw6wG7RbYyg;MSJyqb&xxqXKi6MzWMQ`KXZ@ljrQY|%D zW8;?}()UB>A3sGi&Ro>@48nS1Ui+EO1=HrjTW1eB>@5AQem>R?vCrD~I)Z73Q_q<5 zEe+U1=bd*M*QWR7tZvW=H7897w>7K&4qyILCo7>mR*`Vc*Yte~5wA{YA~mAuV%e2# zq!3Lwy@YS3j!!Z|qZ(n2O&)Mu<&lbyzisVP4Tx;_(4U>k^$5;8JZGHEbX%9>l5BJTPh50>M5UVEKR8)$ESCz z4V>K`YbL-RV6(kbJo?QqrVM+O9f$jvPX69X4_hRLJYo`kw>L~~#SY^QfuY?LZWspnv)C`m7y5M6X z(D)eRAIeS@&+S3aKHa`NQ&-sAcA0=~uvC*Iea}CV2;UMpIWRlu<~ePJD;H};8-p)|*4@7AsAFKV_N3u05sH$;C9CRDX%w9Te{ZY>3XdmfpRILb{mj(lpsi{*$yo zI7)zNE;YluhjmQdd32qKo=bkQhBX#N%WiCUmLHdBHiiX;66oo)utjkE#vQUtz7Mj# znqjII;XY{D@@gPifzhFQ2OF}HA03S2Mt4O-gO8R+yy#*fhAp4w*G1LX`&5tG7?eTC zseU`X`w)BlLu{YVO&KnP@BqQCgF^k;!oM+|uYnjj@xVml{HtMJ1kEG9(O}Ya$CyMj zH@$kA>zjI{WxShd4{}WD?JxSa)Ip{EU1zOwS=E@(r#pu5C=ZTD`L%vhL+H&nmpCEnQn6e}G7me7_3hBcRDH`BYHFwVw(f=gLoY$@O+P#(UA zNhyMKtIbig>ydsKpNkf}dnJ4_D0~va=KHDnVeN`-f(gN%zlhx02ZiTGRtpeBw{IU1 zEbsbj@(&{PY19+d!{8Sv*J(%?rkC5V=kw;-=tk+GaKm<1G-$FmclQUN8o}gzU|5S8ApvaHC;qs$ktkUkG*W*>;C^&vU{He!n*{h$5FL#Z8dj%hZK+8jX@v zj?std6~YIuMg9)S%)ah}0ywIub&!^OH&HucwmHpp15~`31;Gy8@3u^)R z+oSE(NRQ5G(j9xuZ;Hxg&b2$>Cn*MP|9xZiToKyf=b(p~=rVvk<#m8qSzc{ySTjI3*Rh zt+S*Lz0P)mKfiah2-Fv*>SLvs*I>>G;}V6D_hJX&ngIc~TGnd!5}mSy?kKD$ywr6K zRasj;x>cHimF$B?3q|DzfP(T1L~0$xUEn?{xcPr&yZ77+sBP-=G>Ner79k&o#jXja zRH(A9qvE%(dPSw)a615sIum{KEh;{}<_EzU$vVwUvct#s24oB>kY9oym|oS;4&|7> z-heAX$B^sB7{1`gy=fXB1)dVdy1N9IN1 zuK)#-gnTbG*ucblvxlKQe>4m##uI!60oy2b-F4%{L0aL=a==TR=%kk~k2T6g519-v z=blTe;c&7zo}DYDqWED#*rNBFD^}~b_?=$BxrX$z+jC&DfP4#&SznyN z>lUx1>4Y9zt#8|v42SRVt4?kXT6-kJsAbXtigC$wNhE&nt*@Ir8HMuGAFsn zO3aQ`rUVP@M(qxhzm=6Zj;RIH(VrI!!s7$b6;**+Ms|ZH+Z3>$3s~+qFH|orgbu}^&l&C`stJ(dy(EA@8+O_ zrggg3(M+Up90Rukl)3fneee};pboWA&iRm7ijUR%!Ys<|4(QCdamM)K@n$YGL1|py;Ejd_oz=BXTQ%so3GZgwY`UPza=xBzgbSSoZjHEYLaIN*qu;uqjHxQN$+&E%dCxu*z=U5@y*W8f6K9Z3U9t+34Bu2~lzr^f z6acQ*GknH1Ij%f4F0gz&$0kmnyJ5@3WCjtR#IXXA1&OiDL$UqlA7$BsJp1c2Kn!T9 zj;^pgs>aHKUrkzu7r9g!tJB>^HVLV-l{A{NXtwVOhg_oqbii~6Iq~3-^w~c83h;sV zQ+uw!=qtl;T%%U<`x3ZwMW#o>`%OCk0`cWQ*mx{?hh$+RZYzoIRMb}8cabCuV<~yu zse)xU!D^px%%|vIOVV#UJUUo=lPymz1e(Zb59%iOol(n%>opOx?VZUMApJ+X>>3LQ zM%;bVwx1K_?D}`Maq6K*mR35gZ#P%Z-sQa3T@n>c26M`K`SuE^T~MyzwKQenZ$?N@ z@s7J+KHX#$K|r`LWgI#gGsH-dcF<&SA4kaF8%bI`G3kpVujdUEa(EVryxH~}bCM7N z$-g<(zZwwJv#$3)VB#dCsps?}#J?~CR|GET`$62I#SQwbL|TqkM2KnzD#^+w&V4;r z1OhGB>PU@foT`oT-Rm>UR{(wY4acI~)tYl|h7a4=Tu0;O7|H&=dLsK6`jVgJE7d`< z>b16onPSDS-(xHT%bn+$&PgB@q3UgX6$E%l&l5nBKT@qP4U!K&h?x>>bP$p{8}-RR z^?kdW^*y=F(&mV^aTNjWEYiBZGOTtuNp*AZ!6=oA)6}u=xVC9E%~JilI5UCdPZ=jf zgXXUO4k{NtGTM;k0<4e1=j}569^uBO;GQ7RSD-TSyO5d3maq6ZYNofQ@fR4WTQKEA z0}u=AQdp-_s}$&lg2kGeznOjl90Ny}y(FVO9Nrd@dfyQX0tYHCV9ViB7YEG@DJXb< zEO&ox63kl&B|1TW3P>OoHNTOSIFS8ghr1eu?9eX&{HxneRJxbMQ>*p!=Ol2pkzY|P zHM%MG`lORWv!7I!4N- zw?M51RU32sezmZ3OtgdXH6%Ls9QbkXzFtCl?A{56FRpzw3daq;LqPL;3nG)`4I>K@ z5329Zed;BeEqRb}yY~cxmcg}sD)kD9+1FoQkM(W*#P_&QwgzGKVPaY90j4d;#95D8 z0Y%ETN16_ElHs=3W7U|C&Lo-H9bOJ(akHv3~a!#V# zer9S}_%zWoruKDRdQLq?^7Xfi#ZKok1y=wy-l-$6tNVOocvi|g5-0#YC`DNBuu7Jk z!DbMCwN?>RK1`lpPQ9Pd^(Uz8$7TQ}J3m?|3Gy@+ly@KjoP@Fzl8^Vv!bcfKdf!dd zid{BBd9dS{s0o33AF2hEmO(6iy0`Jf>NA60vXTbgzKp z7%yv8kvLBMruhGh&tS{IX#(>?{v14I=d@+5Sed%Y-UENqc$Y@-s%FYGjdlkVyla4VN%>r}KK z+rzwT`6fL*_1;mV&>UI;l80JygR-ecsw>+j;0i{yiPN{-=QdA3VPD}O=0IRh$^*g= zQMh@Ae=#xnHIO z9socFdr_#QNK8|C;OyP^`#Lk#_DAW~KZhre5LW@%z*%XUCQG-gG4w;I*pLLU(w$Ih z-t%I;$}%f6P|*}ll?rNudnk8#QziXV#Nx_Nhng}RurOCdSGO~eiF*2TkL`imx@}DU z05!+6nM9gKlZ%2;ZN-DDswVvTj0(WI+ej7eiQ_}V<6~aa3F^QKtBt9>avjt73wNbu z%K?^Nvu6+X-)@*2!1|l({^WdJ``575396M2Ew4UwGvaJkvmm#O-{=mOS-?Nn8%(>k6sr|I-fX|SNLw&aGnbP6V)8j z%$*c*VwSAF1n6K|!36NNXrWV_Ff-JIrRNRW5@QwT1k6TL=N_byHQ=*on}nAp`WqE( zsKN&XKNI&|H%9Ugp?2<&e7Gz+0EJKNm!)bFJF0$^v6=7@k|L`Aqp|QQkudgb(*gaPK|M?#rgVfiXvdl zsq-iMy0A7bL7n^itdxmcD1j*HaKm0D8PD=CSq=fc|Ep^;NHr~!#tXE}cFx=Dyrv_h z+G7NsTFuzXc;!ycAjrN6rihQd7EK_qZ4L{URz7jO&u=H;@8;SX};r(4)*-y?lsQxjtinHpDa{f2)IanrdeJ$NLqVP99!Xz^0Hg#i0c z9_M=HRmp!E7{I1L92{M->1Y6|4LAU`kHs~!n&gcc8_9d|?(2_x*VqFq=m{AZRsSfN z8g#@}>(p-pw$wddjp>5OFToIz9sV-&0*A~^4ei`Cqx5hW{Q<&0O z;u*#?3I(+JE&q(T-H1IbhaRNkdN$=--39R<`#%Z)?O&dWb!6m<%St@{IVPl`0t`HT zCZgMQ+HRJoW|Y2k7>!E)p>>Tybj=S5+wCgF{*G>=h{;MoCO)on(Z#XuXKt=)q+KNK zbOL+muzerF!8Mk1q&>gNFIH5Cc!tliqbS%rSd{PhL!2mQerByq}&8h~u0R z&J3*TM-?M3ZR8-$jkfO8SndieDH6>K;7d``xVyJ1O}!q{LqtvI14Aq38Z)}iYLa;w zFA_==8K+nYE$#ZOM*tPB*MVte4^e)aZC6f3UUcP}C`$e@)@yMKsF|uk?w%xN9d1Gy z$2t7(RxzoN;uc_e&X+f191`(_26NIdWNZSqgb5|trYO$3$bWLG%AttK!Z~ELy|Z<~ zXosdb|KP$zz0{x{R<|M|$##PJeu&NX`c$UPA8TRzE;to) z)I|-6FaS!;xJe9Voa7FyJcr`&d_^w7u?v(6pJLe6_@6TgE504hZa*i(7u&4vz#2F8=ru&kJM~|Dlw>Pro;Xs z(8xUD8wD9b&N1n-=ljCm~(M@1j*UJ4q^(1iIIRE|1BLP93@iYEk zAu8g#7fn$PHXZ(57R4L58{VC7WxWkn490UJ)TR7PI@@NOwz-Gyz>Y2!7L&lFllG!Y z?sXY+0yF9Z6d){8Jx&gA#o!?-+BikjhC~BsNI+f#ci}&rYm>E18rO6e9d(*NxiEWnXt9Er)I*Q581#+7A;soF)2 zsM4ilv5o^s>>DaUwj8jOSOrq;T3Tb9!^F!dNf@cM3D$6zzD%aoQ}^HW8d}GIF%Hn_ z{y}vQ86ZNpbAguz?jFB_#O_X+tyL7uVk-jDSP@ng zAWjaP-Qus3%+hB?%Aif6V-(L^&=JPw`*Ys;$4RcTJU0F7Mw_684*orr7PIQfN#9R> zeW*PP>DYy38oof+F83KFJKoOMXko9MlaKNB-z@@aRs)DDsJ7ErN5~pb4vusJqrIC# zNW-LWh7Uxm)2`|MqgD+0r$8>I{L6|(SvHfP!m8H#J->8H7 zhui%hhXhQ-5!fN)j=aE~c{b5^Lpve%fn!>6xB!0D$kYd9iqt{DXdV!!xXEhJel^h; zf?n90&OXZUHvUOtHHV4z8jXxxjNkfIjR>#^vd@VR}sES&uM`Wdp1UF z@r};C&2A$%X1gg~>vrmj*U<{=bWD3u73iR2!kH0mjOIy$dJXz7qPV+SZ#XjQg zTBbo6efRU|eKd-hS;2E9)mmM@%X75p)h9cZ@0=^gkXi7*@cfxciiRW|_HLWk#Z1t# zB)xv*q-b8@{6jLgV0JH8K>=MaO{QmU<$_}3A$yO3tRQ?sIac5CuP#Kq+Xk%VG^Tp1 zx7G$l^4V&#Wju0XAQC~JD3o;#DyAd51)~~gsq)*ky*fdzF!ycJtB1FOr41$g;wwPk zcjp<-mJRypfZxCpX{5~w3dm@iA2_rY0hn(eI*}A$LO|BweC%6^EStB{bi{u&y1Jd% zc|B<(QX~gsthFdIVsD?bNdBYaQ$vGY3Qrx1guzn1%ws$&_^f}rQUye}*lxtvcH@9& zA!fUhcV#N28d{_RIig?odO!N4U-XBeE-*9@)o??M%Ax3mH8=m$dK=KE`C=swXh9A< z2J2rQls&K$^N9t_?=L)z53SZw^7hF|>r0a2PDzU%_mA9%Pu@*H|E&c!9z-!QdcnW6 z22RY@$BwD^cg8ATr@2T>#^E!*EOrTXjceH*EfrZ)lTxEQ*PMC|Ea&4G%x6u_xnppj zV)o}fEpy-!h>OJ;pr*Ov*Z;;1|IIw%fS8y#&gk&LQ-68$p%5^S|Cd>%*nz08T!tlP zVfBA^jl~)^&bjNoiKZBxq1E8?jUN_(fwtvb`GUY{mUgA~&sATUAnc!T@sMpcf5KTU z+Xyt<#j@*ClL&@V?5|v|1m5TKHl`STZP%k`3Mw+u(FW0hbBl>?f$^ja50@--9iYd# zV;=n;b!j=I+G5c}rf2 Z#AC_~euz$!Xn@|^$nef{rarf}@y_r8VnIt!N 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ȥ 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 z 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