diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ed14014..8d2bffe 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -15,5 +15,7 @@ jobs: run: | sudo apt update sudo apt install g++-multilib -y + - name: Check formatting + run: cargo fmt --all -- --check - name: Check auxmos build run: cargo check --target=i686-unknown-linux-gnu --release --features "all_reaction_hooks katmos" diff --git a/Cargo.lock b/Cargo.lock index 350cd87..c4ba878 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,6 +22,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "atomic_float" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62af46d040ba9df09edc6528dae9d8e49f5f3e82f55b7d2ec31a733c38dbc49d" + [[package]] name = "autocfg" version = "1.1.0" @@ -63,9 +69,10 @@ dependencies = [ [[package]] name = "auxmos" -version = "0.3.1" +version = "1.0.0" dependencies = [ "ahash 0.7.6", + "atomic_float", "auxcallback", "auxcleanup", "auxtools", @@ -715,9 +722,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] diff --git a/Cargo.toml b/Cargo.toml index 173343f..62117c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,17 @@ [package] name = "auxmos" -version = "0.3.1" +version = "1.0.0" authors = ["Putnam "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["auxcleanup_gas_deletion"] +default = ["auxcleanup_gas_deletion", "turf_processing"] auxcleanup_gas_deletion = ["auxcleanup"] -equalization = [] +zas_hooks = [] +turf_processing = [] +equalization = ["turf_processing"] monstermos = ["equalization"] putnamos = ["equalization"] katmos = ["equalization"] @@ -41,6 +43,7 @@ ahash = "0.7.6" lazy_static = "1.4.0" indexmap = { version = "1.8.0", features = ["rayon"] } dashmap = { version = "5.2.0", features = ["rayon"] } +atomic_float = "0.1.0" [dependencies.tinyvec] version = "1.5.1" diff --git a/README.md b/README.md index fc16cfe..302102a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,3 @@ Rust-based atmospherics for Space Station 13 using [auxtools](https://github.com/willox/auxtools). -Still quite early. Monstermos has an annoying anisotropy--it prefers to go left and right rather than up or down. Up and down are first in the adjacency bitfield (little endian wise), so this isn't *terribly* surprising, but it is annoying. Perhaps it's a problem with the algorithm--am I using a stack instead of a queue? - -This code relies on some byond code on [this fork of Citadel](https://github.com/Putnam3145/Citadel-Station-13/tree/auxtools-atmos). Documentation on this is associated with the individual data structures that hold them, in this repository. - -The compiled binary on Citadel is compiled for Citadel's CPU, which therefore means that it uses [AVX2 fused-multiply-accumulate](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions#Advanced_Vector_Extensions_2). Yes, really. If you have issues, compile it yourself, via `cargo rustc --target=i686-pc-windows-msvc --release --features "all_reaction_hooks" -- -C target-cpu=native`. - -TODO: -I would quite a lot like monstermos to work. +The compiled binary on Citadel is compiled for Citadel's CPU, which therefore means that it uses [AVX2 fused-multiply-accumulate](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions#Advanced_Vector_Extensions_2). Yes, really. If you have issues, compile it yourself, via `cargo rustc --target=i686-pc-windows-msvc --release --features "all_reaction_hooks" -- -C target-cpu=native`. It has to be 32-bit, mind. diff --git a/docs/MIGRATING.md b/docs/MIGRATING.md new file mode 100644 index 0000000..213aaf4 --- /dev/null +++ b/docs/MIGRATING.md @@ -0,0 +1,26 @@ +# 0.2 to 0.3 + +If you're using generic fires, `fire_enthalpy_released` was replaced with a more general `enthalpy`. If you're not, you don't need to do anything in auxgm. + +# 0.3 to 1.0 + +New functions were added: + +1. `/datum/gas_mixture/proc/adjust_moles_temp(gas_type, amt, temperature)` +2. `/datum/gas_mixture/proc/adjust_multi()` (it's variadic, of the form `gas1, amt1, gas2, amt2, ...`) +3. `/datum/gas_mixture/proc/add(amt)` +4. `/datum/gas_mixture/proc/subtract(amt)` +5. `/datum/gas_mixture/proc/multiply(factor)` +6. `/datum/gas_mixture/proc/divide(factor)` +7. `/datum/gas_mixture/proc/__remove_by_flag(taker, flag, amount)` should be paired with a proper remove_by_flag, like remove and remove_ratio +8. `/datum/gas_mixture/proc/get_by_flag(flag)` + +There's also new feature flags: + +1. `turf_processing`: on by default. Enables the hooks for turf processing, heat processing etc. Required for katmos, of course. +2. `zas_hooks`: Adds a `/datum/gas_mixture/proc/share_ratio(sharer, ratio, share_size, one_way = FALSE)` hook. + +Monstermos is now deprecated. Use katmos instead. It inherently has explosive decompression, sorry. + +`fire_products = "plasma_fire"` should be replaced with `fire_products = 0` or, preferably, `fire_products = FIRE_PRODUCT_PLASMA` or similar, with `FIRE_PRODUCT_PLASMA` being `#define FIRE_PRODUCT_PLASMA 0`. String conversion like this is why fires weren't working on linux before; this breaking change is required for it not to be a total hack. + diff --git a/src/callbacks.rs b/src/callbacks.rs index 0295a68..6ceff81 100644 --- a/src/callbacks.rs +++ b/src/callbacks.rs @@ -1,19 +1,21 @@ -use auxtools::*; +use auxtools::{init, shutdown, DMResult, Proc, Value}; type DeferredFunc = Box DMResult + Send + Sync>; type CallbackChannel = (flume::Sender, flume::Receiver); -static mut CALLBACK_CHANNELS: Option<[CallbackChannel; 2]> = None; +static mut CALLBACK_CHANNELS: Option<[CallbackChannel; 3]> = None; pub(crate) const TURFS: usize = 0; pub(crate) const TEMPERATURE: usize = 1; +pub(crate) const ADJACENCIES: usize = 2; + #[init(partial)] fn _start_aux_callbacks() -> Result<(), String> { unsafe { - CALLBACK_CHANNELS = Some([flume::unbounded(), flume::unbounded()]); + CALLBACK_CHANNELS = Some([flume::unbounded(), flume::unbounded(), flume::unbounded()]); } Ok(()) } @@ -30,20 +32,29 @@ fn with_aux_callback_receiver( f(unsafe { &CALLBACK_CHANNELS.as_ref().unwrap()[item].1 }) } +/// Returns a clone of the sender for the given callback channel. +/// # Panics +/// If callback channels have (somehow) not been initialized yet. +#[must_use] pub fn aux_callbacks_sender(item: usize) -> flume::Sender { unsafe { CALLBACK_CHANNELS.as_ref().unwrap()[item].0.clone() } } +/// Process all the callbacks for the given channel. +/// # Panics +/// If `auxtools_stack_trace` does not exist. pub fn process_aux_callbacks(item: usize) { let stack_trace = Proc::find("/proc/auxtools_stack_trace").unwrap(); with_aux_callback_receiver( |receiver| { for callback in receiver.try_iter() { if let Err(e) = callback() { - let _ = stack_trace.call(&[&Value::from_string(e.message.as_str()).unwrap()]); + std::mem::drop( + stack_trace.call(&[&Value::from_string(e.message.as_str()).unwrap()]), + ); } } }, item, - ) + ); } diff --git a/src/gas.rs b/src/gas.rs index 77c987a..df41366 100644 --- a/src/gas.rs +++ b/src/gas.rs @@ -16,6 +16,7 @@ pub use mixture::Mixture; pub type GasIDX = usize; +/// A static container, with a bunch of helper functions for accessing global data. It's horrible, I know, but video games. pub struct GasArena {} /* @@ -74,12 +75,20 @@ fn _shut_down_gases() { } impl GasArena { + /// Locks the gas arena and and runs the given closure with it locked. + /// # Panics + /// if `GAS_MIXTURES` hasn't been initialized, somehow. pub fn with_all_mixtures(f: F) -> T where F: FnOnce(&[RwLock]) -> T, { f(GAS_MIXTURES.read().as_ref().unwrap()) } + /// Read locks the given gas mixture and runs the given closure on it. + /// # Errors + /// If no such gas mixture exists or the closure itself errors. + /// # Panics + /// if `GAS_MIXTURES` hasn't been initialized, somehow. pub fn with_gas_mixture(id: usize, f: F) -> Result where F: FnOnce(&Mixture) -> Result, @@ -92,6 +101,11 @@ impl GasArena { .read(); f(&mix) } + /// Write locks the given gas mixture and runs the given closure on it. + /// # Errors + /// If no such gas mixture exists or the closure itself errors. + /// # Panics + /// if `GAS_MIXTURES` hasn't been initialized, somehow. pub fn with_gas_mixture_mut(id: usize, f: F) -> Result where F: FnOnce(&mut Mixture) -> Result, @@ -104,6 +118,11 @@ impl GasArena { .write(); f(&mut mix) } + /// Read locks the given gas mixtures and runs the given closure on them. + /// # Errors + /// If no such gas mixture exists or the closure itself errors. + /// # Panics + /// if `GAS_MIXTURES` hasn't been initialized, somehow. pub fn with_gas_mixtures(src: usize, arg: usize, f: F) -> Result where F: FnOnce(&Mixture, &Mixture) -> Result, @@ -120,6 +139,11 @@ impl GasArena { .read(); f(&src_gas, &arg_gas) } + /// Locks the given gas mixtures and runs the given closure on them. + /// # Errors + /// If no such gas mixture exists or the closure itself errors. + /// # Panics + /// if `GAS_MIXTURES` hasn't been initialized, somehow. pub fn with_gas_mixtures_mut(src: usize, arg: usize, f: F) -> Result where F: FnOnce(&mut Mixture, &mut Mixture) -> Result, @@ -149,6 +173,11 @@ impl GasArena { ) } } + /// Runs the given closure on the gas mixture *locks* rather than an already-locked version. + /// # Errors + /// If no such gas mixture exists or the closure itself errors. + /// # Panics + /// if `GAS_MIXTURES` hasn't been initialized, somehow. fn with_gas_mixtures_custom(src: usize, arg: usize, f: F) -> Result where F: FnOnce(&RwLock, &RwLock) -> Result, @@ -175,6 +204,10 @@ impl GasArena { } } /// Fills in the first unused slot in the gas mixtures vector, or adds another one, then sets the argument Value to point to it. + /// # Errors + /// If `initial_volume` is incorrect or `_extools_pointer_gasmixture` doesn't exist, somehow. + /// # Panics + /// If `NEXT_GAS_IDS` is not initialized, somehow. pub fn register_mix(mix: &Value) -> DMResult { if NEXT_GAS_IDS.read().as_ref().unwrap().is_empty() { let mut lock = GAS_MIXTURES.write(); @@ -227,6 +260,8 @@ impl GasArena { Ok(Value::null()) } /// Marks the Value's gas mixture as unused, allowing it to be reallocated to another. + /// # Panics + /// Panics if `NEXT_GAS_IDS` hasn't been initialized, somehow. pub fn unregister_mix(mix: u32) { if is_registered_mix(mix) { use raw_types::values::{ValueData, ValueTag}; @@ -258,6 +293,8 @@ impl GasArena { } /// Gets the mix for the given value, and calls the provided closure with a reference to that mix as an argument. +/// # Errors +/// If a gasmixture ID is not a number or the callback returns an error. pub fn with_mix(mix: &Value, f: F) -> Result where F: FnMut(&Mixture) -> Result, @@ -278,6 +315,8 @@ where } /// As `with_mix`, but mutable. +/// # Errors +/// If a gasmixture ID is not a number or the callback returns an error. pub fn with_mix_mut(mix: &Value, f: F) -> Result where F: FnMut(&mut Mixture) -> Result, @@ -298,6 +337,8 @@ where } /// As `with_mix`, but with two mixes. +/// # Errors +/// If a gasmixture ID is not a number or the callback returns an error. pub fn with_mixes(src_mix: &Value, arg_mix: &Value, f: F) -> Result where F: FnMut(&Mixture, &Mixture) -> Result, @@ -330,6 +371,8 @@ where } /// As `with_mix_mut`, but with two mixes. +/// # Errors +/// If a gasmixture ID is not a number or the callback returns an error. pub fn with_mixes_mut(src_mix: &Value, arg_mix: &Value, f: F) -> Result where F: FnMut(&mut Mixture, &mut Mixture) -> Result, @@ -362,6 +405,8 @@ where } /// Allows different lock levels for each gas. Instead of relevant refs to the gases, returns the `RWLock` object. +/// # Errors +/// If a gasmixture ID is not a number or the callback returns an error. pub fn with_mixes_custom(src_mix: &Value, arg_mix: &Value, f: F) -> Result where F: FnMut(&RwLock, &RwLock) -> Result, diff --git a/src/gas/constants.rs b/src/gas/constants.rs index eb7c10d..00744b7 100644 --- a/src/gas/constants.rs +++ b/src/gas/constants.rs @@ -112,6 +112,13 @@ pub const MOLES_GAS_VISIBLE_STEP: f32 = 0.25; /// REACTIONS +// Maximum amount of ReactionIdentifiers in the TinyVec that all_reactions returns. +// We can't guarantee the max number of reactions that will ever be registered, +// so this is here to prevent that from getting out of control. +// TinyVec is used mostly to prevent too much heap stuff from going on, since there can be a LOT of reactions going. +// ReactionIdentifier is 12 bytes, so this can be pretty generous. +pub(crate) const MAX_REACTION_TINYVEC_SIZE: usize = 32; + /// return values for reactions (bitflags) pub const NO_REACTION: i32 = 0; pub const REACTING: i32 = 1; diff --git a/src/gas/mixture.rs b/src/gas/mixture.rs index bfff877..46629de 100644 --- a/src/gas/mixture.rs +++ b/src/gas/mixture.rs @@ -4,10 +4,9 @@ use itertools::{ Itertools, }; -use std::{ - cell::Cell, - sync::atomic::{AtomicU64, Ordering::Relaxed}, -}; +use std::sync::atomic::Ordering::Relaxed; + +use atomic_float::AtomicF32; use tinyvec::TinyVec; @@ -19,11 +18,35 @@ use super::{ type SpecificFireInfo = (usize, f32, f32); -struct VisHash(AtomicU64); +struct GasCache(AtomicF32); -impl Clone for VisHash { +impl Clone for GasCache { fn clone(&self) -> Self { - VisHash(AtomicU64::new(self.0.load(Relaxed))) + Self(AtomicF32::new(self.0.load(Relaxed))) + } +} + +impl Default for GasCache { + fn default() -> Self { + Self(AtomicF32::new(f32::NAN)) + } +} + +impl GasCache { + pub fn invalidate(&self) { + self.0.store(f32::NAN, Relaxed); + } + pub fn get_or_else(&self, mut f: impl FnMut() -> f32) -> f32 { + match self + .0 + .fetch_update(Relaxed, Relaxed, |x| x.is_nan().then(|| f())) + { + Ok(_) => self.0.load(Relaxed), + Err(x) => x, + } + } + pub fn set(&self, v: f32) { + self.0.store(v, Relaxed); } } @@ -40,25 +63,11 @@ pub struct Mixture { temperature: f32, pub volume: f32, min_heat_capacity: f32, - immutable: bool, moles: TinyVec<[f32; 8]>, - cached_heat_capacity: Cell>, - cached_vis_hash: VisHash, + cached_heat_capacity: GasCache, + immutable: bool, } -/* - Cell is not thread-safe. However, we use it only for caching heat capacity. The worst case race condition - is thus thread A and B try to access heat capacity at the same time; both find that it's currently - uncached, so both go to calculate it; both calculate it, and both calculate it to the same value, - then one sets the cache to that value, then the other does. - - Technically, a worse one would be thread A mutates the gas mixture, changing a gas amount, - while thread B tries to get its heat capacity; thread B finds a well-defined heat capacity, - which is not correct, and uses it for a calculation, but this cannot happen: thread A would - have a write lock, precluding thread B from accessing it. -*/ -unsafe impl Sync for Mixture {} - impl Default for Mixture { fn default() -> Self { Self::new() @@ -67,6 +76,7 @@ impl Default for Mixture { impl Mixture { /// Makes an empty gas mixture. + #[must_use] pub fn new() -> Self { Self { moles: TinyVec::new(), @@ -74,11 +84,11 @@ impl Mixture { volume: 2500.0, min_heat_capacity: 0.0, immutable: false, - cached_heat_capacity: Cell::new(None), - cached_vis_hash: VisHash(AtomicU64::new(0)), + cached_heat_capacity: GasCache::default(), } } /// Makes an empty gas mixture with the given volume. + #[must_use] pub fn from_vol(vol: f32) -> Self { let mut ret = Self::new(); ret.volume = vol; @@ -114,6 +124,8 @@ impl Mixture { self.moles.iter().copied().enumerate() } /// Allows closures to iterate over each gas. + /// # Errors + /// If the closure errors. pub fn for_each_gas( &self, mut f: impl FnMut(GasIDX, f32) -> Result<(), auxtools::Runtime>, @@ -123,6 +135,18 @@ impl Mixture { } Ok(()) } + /// As `for_each_gas`, but with mut refs to the mole counts instead of copies. + /// # Errors + /// If the closure errors. + pub fn for_each_gas_mut( + &mut self, + mut f: impl FnMut(GasIDX, &mut f32) -> Result<(), auxtools::Runtime>, + ) -> Result<(), auxtools::Runtime> { + for (i, g) in self.moles.iter_mut().enumerate() { + f(i, g)?; + } + Ok(()) + } /// Returns (by value) the amount of moles of a given index the mix has. M pub fn get_moles(&self, idx: GasIDX) -> f32 { self.moles.get(idx).copied().unwrap_or(0.0) @@ -150,7 +174,7 @@ impl Mixture { unsafe { *self.moles.get_unchecked_mut(idx) = amt; }; - self.cached_heat_capacity.set(None); + self.cached_heat_capacity.invalidate(); } } pub fn adjust_moles(&mut self, idx: GasIDX, amt: f32) { @@ -158,31 +182,57 @@ impl Mixture { self.maybe_expand((idx + 1) as usize); let r = unsafe { self.moles.get_unchecked_mut(idx) }; *r += amt; - if amt < 0.0 { + if amt <= 0.0 { + self.garbage_collect(); + } + self.cached_heat_capacity.invalidate(); + } + } + pub fn adjust_multi(&mut self, adjustments: &[(usize, f32)]) { + if !self.immutable { + let num_gases = total_num_gases(); + self.maybe_expand( + adjustments + .iter() + .filter_map(|&(i, _)| (i < num_gases).then(|| i)) + .max() + .unwrap_or(0) + 1, + ); + let mut dirty = false; + let mut should_collect = false; + for (idx, amt) in adjustments { + if *idx < num_gases && amt.is_normal() { + let r = unsafe { self.moles.get_unchecked_mut(*idx) }; + *r += *amt; + if *amt <= 0.0 { + should_collect = true; + } + dirty = true; + } + } + if dirty { + self.cached_heat_capacity.invalidate(); + } + if should_collect { self.garbage_collect(); } - self.cached_heat_capacity.set(None); } } #[inline(never)] // mostly this makes it so that heat_capacity itself is inlined fn slow_heat_capacity(&self) -> f32 { - let heat_cap = with_specific_heats(|heats| { + with_specific_heats(|heats| { self.moles .iter() .copied() .zip(heats.iter()) .fold(0.0, |acc, (amt, cap)| cap.mul_add(amt, acc)) }) - .max(self.min_heat_capacity); - self.cached_heat_capacity.set(Some(heat_cap)); - heat_cap + .max(self.min_heat_capacity) } /// The heat capacity of the material. [joules?]/mole-kelvin. pub fn heat_capacity(&self) -> f32 { self.cached_heat_capacity - .get() - .filter(|cap| cap.is_finite() && cap.is_sign_positive()) - .unwrap_or_else(|| self.slow_heat_capacity()) + .get_or_else(|| self.slow_heat_capacity()) } /// Heat capacity of exactly one gas in this mix. pub fn partial_heat_capacity(&self, idx: GasIDX) -> f32 { @@ -221,7 +271,7 @@ impl Mixture { / (combined_heat_capacity), ); } - self.cached_heat_capacity.set(Some(combined_heat_capacity)); + self.cached_heat_capacity.set(combined_heat_capacity); } /// Transfers only the given gases from us to another mix. pub fn transfer_gases_to(&mut self, r: f32, gases: &[GasIDX], into: &mut Self) { @@ -238,8 +288,8 @@ impl Mixture { } } }); - self.cached_heat_capacity.set(None); - into.cached_heat_capacity.set(None); + self.cached_heat_capacity.invalidate(); + into.cached_heat_capacity.invalidate(); into.set_temperature((initial_energy + heat_transfer) / into.heat_capacity()); } /// Takes a percentage of this gas mixture's moles and puts it into another mixture. if this mix is mutable, also removes those moles from the original. @@ -262,12 +312,14 @@ impl Mixture { self.remove_ratio_into(amount / self.total_moles(), into); } /// A convenience function that makes the mixture for `remove_ratio_into` on the spot and returns it. + #[must_use] pub fn remove_ratio(&mut self, ratio: f32) -> Self { let mut removed = Self::from_vol(self.volume); self.remove_ratio_into(ratio, &mut removed); removed } /// Like `remove_ratio`, but with moles. + #[must_use] pub fn remove(&mut self, amount: f32) -> Self { self.remove_ratio(amount / self.total_moles()) } @@ -278,8 +330,7 @@ impl Mixture { } self.moles = sample.moles.clone(); self.temperature = sample.temperature; - self.cached_heat_capacity - .set(sample.cached_heat_capacity.get()); + self.cached_heat_capacity = sample.cached_heat_capacity.clone(); } /// A very simple finite difference solution to the heat transfer equation. /// Works well enough for our purposes, though perhaps called less often @@ -358,14 +409,14 @@ impl Mixture { .any(|pair| match pair { Left(a) => a >= &amt, Right(b) => b >= &amt, - Both(a, b) => a != b && (a - b).abs() >= amt, + Both(a, b) => (a - b).abs() >= amt, }) } /// Clears the moles from the gas. pub fn clear(&mut self) { if !self.immutable { self.moles.clear(); - self.cached_heat_capacity.set(None); + self.cached_heat_capacity.invalidate(); } } /// Resets the gas mixture to an initialized-with-volume state. @@ -382,7 +433,16 @@ impl Mixture { for amt in self.moles.iter_mut() { *amt *= multiplier; } - self.cached_heat_capacity.set(None); + self.cached_heat_capacity.invalidate(); + self.garbage_collect(); + } + } + pub fn add(&mut self, num: f32) { + if !self.immutable { + for amt in self.moles.iter_mut() { + *amt += num; + } + self.cached_heat_capacity.invalidate(); self.garbage_collect(); } } @@ -391,7 +451,7 @@ impl Mixture { with_reactions(|reactions| reactions.iter().any(|r| r.check_conditions(self))) } /// Gets all of the reactions this mix should do. - pub fn all_reactable(&self) -> Vec { + pub fn all_reactable(&self) -> TinyVec<[ReactionIdentifier; MAX_REACTION_TINYVEC_SIZE]> { with_reactions(|reactions| { reactions .iter() @@ -489,8 +549,7 @@ impl Mixture { self.enumerate() .any(|(i, gas)| gas_visibility(i as usize).map_or(false, |amt| gas >= amt)) } - /// A hashed representation of the visibility of a gas, so that it only needs to update vis when actually changed. - pub fn vis_hash_changed(&self, gas_visibility: &[Option]) -> bool { + pub fn vis_hash(&self, gas_visibility: &[Option]) -> u64 { use std::hash::Hasher; let mut hasher: ahash::AHasher = ahash::AHasher::default(); for (i, gas) in self.enumerate() { @@ -500,8 +559,12 @@ impl Mixture { hasher.write_usize((FACTOR_GAS_VISIBLE_MAX).min((gas / amt).ceil()) as usize); } } - let cur_hash = hasher.finish(); - self.cached_vis_hash.0.swap(cur_hash, Relaxed) != cur_hash + hasher.finish() + } + /// Compares the current vis hash to the provided one; returns `Some(new_hash)` if they're different, otherwise None. + pub fn vis_hash_changed(&self, gas_visibility: &[Option], prev_hash: u64) -> Option { + let cur_hash = self.vis_hash(gas_visibility); + (cur_hash != prev_hash).then(|| cur_hash) } // Removes all redundant zeroes from the gas mixture. pub fn garbage_collect(&mut self) { diff --git a/src/gas/types.rs b/src/gas/types.rs index 132f9c6..a9f2c74 100644 --- a/src/gas/types.rs +++ b/src/gas/types.rs @@ -28,16 +28,18 @@ pub struct OxidationInfo { } impl OxidationInfo { + #[must_use] pub fn temperature(&self) -> f32 { self.temperature } + #[must_use] pub fn power(&self) -> f32 { self.power } } /// The temperature at which this gas can burn and how much it burns when it does. -/// This may seem redundant with OxidationInfo, but burn_rate is actually the inverse, dimensions-wise, moles^-1 rather than moles. +/// This may seem redundant with `OxidationInfo`, but `burn_rate` is actually the inverse, dimensions-wise, moles^-1 rather than moles. #[derive(Clone, Copy)] pub struct FuelInfo { temperature: f32, @@ -45,9 +47,11 @@ pub struct FuelInfo { } impl FuelInfo { + #[must_use] pub fn temperature(&self) -> f32 { self.temperature } + #[must_use] pub fn burn_rate(&self) -> f32 { self.burn_rate } @@ -71,12 +75,18 @@ pub enum GasRef { } impl GasRef { + /// Gets the index of the gas. + /// # Errors + /// Propagates error from `gas_idx_from_string`. pub fn get(&self) -> Result { match self { Self::Deferred(s) => gas_idx_from_string(s), Self::Found(id) => Ok(*id), } } + /// Like `get`, but also caches the result if found. + /// # Errors + /// If the string is not a valid gas name. pub fn update(&mut self) -> Result { match self { Self::Deferred(s) => { @@ -96,7 +106,7 @@ pub enum FireProductInfo { /// An individual gas type. Contains a whole lot of info attained from Byond when the gas is first registered. /// If you don't have any of these, just fork auxmos and remove them, many of these are not necessary--for example, -/// if you don't have fusion, you can just remove fusion_power. +/// if you don't have fusion, you can just remove `fusion_power`. /// Each individual member also has the byond /datum/gas equivalent listed. #[derive(Clone)] pub struct GasType { @@ -109,7 +119,6 @@ pub struct GasType { /// The gas's name. Not used in auxmos as of yet. /// Byond: `name`, a string. pub name: Box, - /// Not used in auxmos, there for completeness. Only flag on Citadel is GAS_DANGEROUS. /// Byond: `flags`, a number (bitflags). pub flags: u32, /// The specific heat of the gas. Duplicated in the GAS_SPECIFIC_HEATS vector for speed. @@ -192,8 +201,8 @@ impl GasType { }) .collect(), )) - } else if product_info.as_string().is_ok() { - Some(FireProductInfo::Plasma) + } else if let Ok(_) = product_info.as_number() { + Some(FireProductInfo::Plasma) // if we add another snowflake later, add it, but for now we hack this in } else { None } @@ -244,7 +253,7 @@ fn _destroy_gas_info_structs() { fn _hook_register_gas(gas: Value) { let gas_id = gas.get_string(byond_string!("id"))?; let gas_cache = GasType::new(gas, TOTAL_NUM_GASES.load(Ordering::Acquire))?; - let cached_id = gas_id.to_owned(); + let cached_id = gas_id.clone(); let cached_idx = gas_cache.idx; unsafe { GAS_INFO_BY_STRING.as_ref() } .unwrap() @@ -299,6 +308,9 @@ fn _update_reactions() { Ok(Value::from(true)) } +/// Calls the given closure with all reaction info as an argument. +/// # Panics +/// If reactions aren't loaded yet. pub fn with_reactions(mut f: F) -> T where F: FnMut(&[Reaction]) -> T, @@ -309,11 +321,18 @@ where .unwrap_or_else(|| panic!("Reactions not loaded yet! Uh oh!"))) } +/// Runs the given closure with the global specific heats vector locked. +/// # Panics +/// If gas info isn't loaded yet. pub fn with_specific_heats(f: impl FnOnce(&[f32]) -> T) -> T { f(GAS_SPECIFIC_HEATS.read().as_ref().unwrap().as_slice()) } +/// Gets the fusion power of the given gas. +/// # Panics +/// If gas info isn't loaded yet. #[cfg(feature = "reaction_hooks")] +#[must_use] pub fn gas_fusion_power(idx: &GasIDX) -> f32 { GAS_INFO_BY_IDX .read() @@ -330,6 +349,9 @@ pub fn total_num_gases() -> GasIDX { } /// Gets the gas visibility threshold for the given gas ID. +/// # Panics +/// If gas info isn't loaded yet. +#[must_use] pub fn gas_visibility(idx: usize) -> Option { GAS_INFO_BY_IDX .read() @@ -341,6 +363,9 @@ pub fn gas_visibility(idx: usize) -> Option { } /// Gets a copy of all the gas visibilities. +/// # Panics +/// If gas info isn't loaded yet. +#[must_use] pub fn visibility_copies() -> Box<[Option]> { GAS_INFO_BY_IDX .read() @@ -353,6 +378,8 @@ pub fn visibility_copies() -> Box<[Option]> { } /// Allows one to run a closure with a lock on the global gas info vec. +/// # Panics +/// If gas info isn't loaded yet. pub fn with_gas_info(f: impl FnOnce(&[GasType]) -> T) -> T { f(GAS_INFO_BY_IDX .read() @@ -360,7 +387,9 @@ pub fn with_gas_info(f: impl FnOnce(&[GasType]) -> T) -> T { .unwrap_or_else(|| panic!("Gases not loaded yet! Uh oh!"))) } -/// Updates all the GasRefs in the global gas info vec with proper indices instead of strings. +/// Updates all the `GasRef`s in the global gas info vec with proper indices instead of strings. +/// # Panics +/// If gas info is not loaded yet. pub fn update_gas_refs() { GAS_INFO_BY_IDX .write() @@ -388,6 +417,8 @@ thread_local! { } /// Returns the appropriate index to be used by auxmos for a given ID string. +/// # Errors +/// If gases aren't loaded or an invalid gas ID is given. pub fn gas_idx_from_string(id: &str) -> Result { Ok(unsafe { GAS_INFO_BY_STRING.as_ref() } .ok_or_else(|| runtime!("Gases not loaded yet! Uh oh!"))? @@ -397,6 +428,8 @@ pub fn gas_idx_from_string(id: &str) -> Result { } /// Returns the appropriate index to be used by the game for a given Byond string. +/// # Errors +/// If the given string is not a string or is not a valid gas ID. pub fn gas_idx_from_value(string_val: &Value) -> Result { CACHED_GAS_IDS.with(|c| { let mut cache = c.borrow_mut(); @@ -412,6 +445,8 @@ pub fn gas_idx_from_value(string_val: &Value) -> Result { } /// Takes an index and returns a borrowed string representing the string ID of the gas datum stored in that index. +/// # Panics +/// If an invalid gas index is given to this. This should never happen, so we panic instead of runtiming. pub fn gas_idx_to_id(idx: GasIDX) -> DMResult { CACHED_IDX_TO_STRINGS.with(|thin| { let stuff = thin.borrow(); diff --git a/src/lib.rs b/src/lib.rs index 2009d87..af581e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,34 +1,39 @@ pub mod gas; +#[cfg(feature = "turf_processing")] pub mod turfs; pub mod reaction; pub mod callbacks; -use auxtools::*; +use auxtools::{byond_string, hook, inventory, runtime, List, Value}; -use auxcleanup::*; +use auxcleanup::{datum_del, DelDatumFunc}; -use gas::*; +use gas::{ + amt_gases, constants, gas_idx_from_value, gas_idx_to_id, tot_gases, types, with_gas_info, + with_mix, with_mix_mut, with_mixes, with_mixes_custom, with_mixes_mut, GasArena, Mixture, +}; use reaction::react_by_id; -use gas::constants::*; - -#[macro_use] -extern crate lazy_static; +use gas::constants::{GAS_MIN_MOLES, MINIMUM_MOLES_DELTA_TO_MOVE, STOP_REACTIONS}; +/// Args: (ms). Runs callbacks until time limit is reached. If time limit is omitted, runs all callbacks. #[hook("/proc/process_atmos_callbacks")] fn _atmos_callback_handle() { auxcallback::callback_processing_hook(args) } +/// Fills in the first unused slot in the gas mixtures vector, or adds another one, then sets the argument Value to point to it. #[hook("/datum/gas_mixture/proc/__gasmixture_register")] fn _register_gasmixture_hook() { gas::GasArena::register_mix(src) } +/// Adds the gas mixture's ID to the queue of mixtures that have been deleted, to be reused later. +/// This version is only if auxcleanup is not being used; it should be called from /datum/gas_mixture/Del. #[cfg(not(feature = "auxcleanup_gas_deletion"))] #[hook("/datum/gas_mixture/proc/__gasmixture_unregister")] fn _unregister_gasmixture_hook() { @@ -36,104 +41,98 @@ fn _unregister_gasmixture_hook() { Ok(Value::null()) } +/// Adds the gas mixture's ID to the queue of mixtures that have been deleted, to be reused later. Called automatically on all datum deletion. #[cfg(feature = "auxcleanup_gas_deletion")] #[datum_del] fn _unregister_gasmixture_hook(v: u32) { gas::GasArena::unregister_mix(v); } +/// Returns: Heat capacity, in J/K (probably). #[hook("/datum/gas_mixture/proc/heat_capacity")] fn _heat_cap_hook() { with_mix(src, |mix| Ok(Value::from(mix.heat_capacity()))) } +/// Args: (min_heat_cap). Sets the mix's minimum heat capacity. #[hook("/datum/gas_mixture/proc/set_min_heat_capacity")] -fn _min_heat_cap_hook() { - if args.is_empty() { - Err(runtime!( - "attempted to set min heat capacity with no argument" - )) - } else { - with_mix_mut(src, |mix| { - mix.set_min_heat_capacity(args[0].as_number().unwrap_or_default()); - Ok(Value::null()) - }) - } +fn _min_heat_cap_hook(arg_min: Value) { + let min = arg_min.as_number()?; + with_mix_mut(src, |mix| { + mix.set_min_heat_capacity(min); + Ok(Value::null()) + }) } +/// Returns: Amount of substance, in moles. #[hook("/datum/gas_mixture/proc/total_moles")] fn _total_moles_hook() { with_mix(src, |mix| Ok(Value::from(mix.total_moles()))) } +/// Returns: the mix's pressure, in kilopascals. #[hook("/datum/gas_mixture/proc/return_pressure")] fn _return_pressure_hook() { with_mix(src, |mix| Ok(Value::from(mix.return_pressure()))) } +/// Returns: the mix's temperature, in kelvins. #[hook("/datum/gas_mixture/proc/return_temperature")] fn _return_temperature_hook() { with_mix(src, |mix| Ok(Value::from(mix.get_temperature()))) } +/// Returns: the mix's volume, in liters. #[hook("/datum/gas_mixture/proc/return_volume")] fn _return_volume_hook() { with_mix(src, |mix| Ok(Value::from(mix.volume))) } +/// Returns: the mix's thermal energy, the product of the mixture's heat capacity and its temperature. #[hook("/datum/gas_mixture/proc/thermal_energy")] fn _thermal_energy_hook() { with_mix(src, |mix| Ok(Value::from(mix.thermal_energy()))) } +/// Args: (mixture). Merges the gas from the giver into src, without modifying the giver mix. #[hook("/datum/gas_mixture/proc/merge")] -fn _merge_hook() { - if args.is_empty() { - Err(runtime!("Tried merging nothing into a gas mixture")) - } else { - with_mixes_mut(src, &args[0], |src_mix, giver_mix| { - src_mix.merge(giver_mix); - Ok(Value::null()) - }) - } +fn _merge_hook(giver: Value) { + with_mixes_custom(src, giver, |src_mix, giver_mix| { + src_mix.write().merge(&giver_mix.read()); + Ok(Value::null()) + }) } +/// Args: (mixture, ratio). Takes the given ratio of gas from src and puts it into the argument mixture. Ratio is a number between 0 and 1. #[hook("/datum/gas_mixture/proc/__remove_ratio")] -fn _remove_ratio_hook() { - if args.len() < 2 { - Err(runtime!("remove_ratio called with fewer than 2 arguments")) - } else { - with_mixes_mut(src, &args[0], |src_mix, into_mix| { - src_mix.remove_ratio_into(args[1].as_number().unwrap_or_default(), into_mix); - Ok(Value::null()) - }) - } +fn _remove_ratio_hook(into: Value, ratio_arg: Value) { + let ratio = ratio_arg.as_number().unwrap_or_default(); + with_mixes_mut(src, into, |src_mix, into_mix| { + src_mix.remove_ratio_into(ratio, into_mix); + Ok(Value::null()) + }) } +/// Args: (mixture, amount). Takes the given amount of gas from src and puts it into the argument mixture. Amount is amount of substance in moles. #[hook("/datum/gas_mixture/proc/__remove")] -fn _remove_hook() { - if args.len() < 2 { - Err(runtime!("remove called with fewer than 2 arguments")) - } else { - with_mixes_mut(src, &args[0], |src_mix, into_mix| { - src_mix.remove_into(args[1].as_number().unwrap_or_default(), into_mix); - Ok(Value::null()) - }) - } +fn _remove_hook(into: Value, amount_arg: Value) { + let amount = amount_arg.as_number().unwrap_or_default(); + with_mixes_mut(src, into, |src_mix, into_mix| { + src_mix.remove_into(amount, into_mix); + Ok(Value::null()) + }) } +/// Arg: (mixture). Makes src into a copy of the argument mixture. #[hook("/datum/gas_mixture/proc/copy_from")] -fn _copy_from_hook() { - if args.is_empty() { - Err(runtime!("Tried copying a gas mix from nothing")) - } else { - with_mixes_mut(src, &args[0], |src_mix, giver_mix| { - src_mix.copy_from_mutable(giver_mix); - Ok(Value::null()) - }) - } +fn _copy_from_hook(giver: Value) { + with_mixes_custom(src, giver, |src_mix, giver_mix| { + src_mix.write().copy_from_mutable(&giver_mix.read()); + Ok(Value::null()) + }) } +/// Args: (mixture, conductivity) or (null, conductivity, temperature, heat_capacity). Adjusts temperature of src based on parameters. Returns: temperature of sharer after sharing is complete. #[hook("/datum/gas_mixture/proc/temperature_share")] fn _temperature_share_hook() { let arg_num = args.len(); @@ -155,6 +154,7 @@ fn _temperature_share_hook() { } } +/// Returns: a list of the gases in the mixture, associated with their IDs. #[hook("/datum/gas_mixture/proc/get_gases")] fn _get_gases_hook() { with_mix(src, |mix| { @@ -169,20 +169,17 @@ fn _get_gases_hook() { }) } +/// Args: (temperature). Sets the temperature of the mixture. Will be set to 2.7 if it's too low. #[hook("/datum/gas_mixture/proc/set_temperature")] -fn _set_temperature_hook() { - let v = args - .get(0) - .ok_or_else(|| runtime!("Wrong amount of arguments for set_temperature: 0!"))? - .as_number() - .map_err(|_| { - runtime!( - "Attempt to interpret non-number value as number {} {}:{}", - std::file!(), - std::line!(), - std::column!() - ) - })?; +fn _set_temperature_hook(arg_temp: Value) { + let v = arg_temp.as_number().map_err(|_| { + runtime!( + "Attempt to interpret non-number value as number {} {}:{}", + std::file!(), + std::line!(), + std::column!() + ) + })?; if v.is_finite() { with_mix_mut(src, |mix| { mix.set_temperature(v.max(2.7)); @@ -195,55 +192,45 @@ fn _set_temperature_hook() { } } +/// Args: (gas_id). Returns the heat capacity from the given gas, in J/K (probably). #[hook("/datum/gas_mixture/proc/partial_heat_capacity")] -fn _partial_heat_capacity() { - if args.is_empty() { - Err(runtime!("Incorrect arg len for partial_heat_capacity (0).")) - } else { - with_mix(src, |mix| { - Ok(Value::from( - mix.partial_heat_capacity(gas_idx_from_value(&args[0])?), - )) - }) - } +fn _partial_heat_capacity(gas_id: Value) { + with_mix(src, |mix| { + Ok(Value::from( + mix.partial_heat_capacity(gas_idx_from_value(gas_id)?), + )) + }) } +/// Args: (volume). Sets the volume of the gas. #[hook("/datum/gas_mixture/proc/set_volume")] -fn _set_volume_hook() { - if args.is_empty() { - Err(runtime!("Attempted to set volume to nothing.")) - } else { - with_mix_mut(src, |mix| { - mix.volume = args[0].as_number().map_err(|_| { - runtime!( - "Attempt to interpret non-number value as number {} {}:{}", - std::file!(), - std::line!(), - std::column!() - ) - })?; - Ok(Value::null()) - }) - } +fn _set_volume_hook(vol_arg: Value) { + let volume = vol_arg.as_number().map_err(|_| { + runtime!( + "Attempt to interpret non-number value as number {} {}:{}", + std::file!(), + std::line!(), + std::column!() + ) + })?; + with_mix_mut(src, |mix| { + mix.volume = volume; + Ok(Value::null()) + }) } +/// Args: (gas_id). Returns: the amount of substance of the given gas, in moles. #[hook("/datum/gas_mixture/proc/get_moles")] -fn _get_moles_hook() { - if args.is_empty() { - Err(runtime!("Incorrect arg len for get_moles (0).")) - } else { - with_mix(src, |mix| { - Ok(Value::from(mix.get_moles(gas_idx_from_value(&args[0])?))) - }) - } +fn _get_moles_hook(gas_id: Value) { + with_mix(src, |mix| { + Ok(Value::from(mix.get_moles(gas_idx_from_value(gas_id)?))) + }) } +/// Args: (gas_id, moles). Sets the amount of substance of the given gas, in moles. #[hook("/datum/gas_mixture/proc/set_moles")] -fn _set_moles_hook() { - if args.len() < 2 { - return Err(runtime!("Incorrect arg len for set_moles (less than 2).")); - } - let vf = args[1].as_number().unwrap_or_default(); +fn _set_moles_hook(gas_id: Value, amt_val: Value) { + let vf = amt_val.as_number()?; if !vf.is_finite() { return Err(runtime!("Attempted to set moles to NaN or infinity.")); } @@ -251,11 +238,11 @@ fn _set_moles_hook() { return Err(runtime!("Attempted to set moles to a negative number.")); } with_mix_mut(src, |mix| { - mix.set_moles(gas_idx_from_value(&args[0])?, vf); + mix.set_moles(gas_idx_from_value(gas_id)?, vf); Ok(Value::null()) }) } - +/// Args: (gas_id, moles). Adjusts the given gas's amount by the given amount, e.g. (GAS_O2, -0.1) will remove 0.1 moles of oxygen from the mixture. #[hook("/datum/gas_mixture/proc/adjust_moles")] fn _adjust_moles_hook(id_val: Value, num_val: Value) { let vf = num_val.as_number().unwrap_or_default(); @@ -265,6 +252,140 @@ fn _adjust_moles_hook(id_val: Value, num_val: Value) { }) } +/// Args: (gas_id, moles, temp). Adjusts the given gas's amount by the given amount, with that gas being treated as if it is at the given temperature. +#[hook("/datum/gas_mixture/proc/adjust_moles_temp")] +fn _adjust_moles_temp_hook(id_val: Value, num_val: Value, temp_val: Value) { + let vf = num_val.as_number().unwrap_or_default(); + let temp = temp_val.as_number().unwrap_or(2.7); + if vf < 0.0 { + return Err(runtime!( + "Attempted to add a negative gas in adjust_moles_temp." + )); + } + if !vf.is_normal() { + return Ok(Value::null()); + } + let mut new_mix = Mixture::new(); + new_mix.set_moles(gas_idx_from_value(id_val)?, vf); + new_mix.set_temperature(temp); + with_mix_mut(src, |mix| { + mix.merge(&new_mix); + Ok(Value::null()) + }) +} + +/// Args: (gas_id_1, amount_1, gas_id_2, amount_2, ...). As adjust_moles, but with variadic arguments. +#[hook("/datum/gas_mixture/proc/adjust_multi")] +fn _adjust_multi_hook() { + if args.len() % 2 != 0 { + Err(runtime!( + "Incorrect arg len for adjust_multi (not divisible by 2)." + )) + } else { + let adjustments = args + .chunks(2) + .filter_map(|chunk| { + (chunk.len() == 2) + .then(|| { + gas_idx_from_value(&chunk[0]) + .ok() + .map(|idx| (idx, chunk[1].as_number().unwrap_or_default())) + }) + .flatten() + }) + .collect::>(); + with_mix_mut(src, |mix| { + mix.adjust_multi(&adjustments); + Ok(Value::null()) + }) + } +} + +///Args: (amount). Adds the given amount to each gas. +#[hook("/datum/gas_mixture/proc/add")] +fn _add_hook(num_val: Value) { + let vf = num_val.as_number().unwrap_or_default(); + with_mix_mut(src, |mix| { + mix.add(vf); + Ok(Value::null()) + }) +} + +///Args: (amount). Subtracts the given amount from each gas. +#[hook("/datum/gas_mixture/proc/subtract")] +fn _subtract_hook(num_val: Value) { + let vf = num_val.as_number().unwrap_or_default(); + with_mix_mut(src, |mix| { + mix.add(-vf); + Ok(Value::null()) + }) +} + +///Args: (coefficient). Multiplies all gases by this amount. +#[hook("/datum/gas_mixture/proc/multiply")] +fn _multiply_hook(num_val: Value) { + let vf = num_val.as_number().unwrap_or(1.0); + with_mix_mut(src, |mix| { + mix.multiply(vf); + Ok(Value::null()) + }) +} + +///Args: (coefficient). Divides all gases by this amount. +#[hook("/datum/gas_mixture/proc/divide")] +fn _divide_hook(num_val: Value) { + let vf = num_val.as_number().unwrap_or(1.0).recip(); + with_mix_mut(src, |mix| { + mix.multiply(vf); + Ok(Value::null()) + }) +} + +///Args: (mixture, flag, amount). Takes `amount` from src that have the given `flag` and puts them into the given `mixture`. Returns: 0 if gas didn't have any with that flag, 1 if it did. +#[hook("/datum/gas_mixture/proc/__remove_by_flag")] +fn _remove_by_flag_hook(into: Value, flag_val: Value, amount_val: Value) { + let flag = flag_val.as_number().map_or(0, |n| n as u32); + let amount = amount_val.as_number().unwrap_or(0.0); + let pertinent_gases = with_gas_info(|gas_info| { + gas_info + .iter() + .filter(|g| g.flags & flag != 0) + .map(|g| g.idx) + .collect::>() + }); + if pertinent_gases.is_empty() { + return Ok(Value::from(false)); + } + with_mixes_mut(src, into, |src_gas, dest_gas| { + let tot = src_gas.total_moles(); + src_gas.transfer_gases_to(amount / tot, &pertinent_gases, dest_gas); + Ok(Value::from(true)) + }) +} +///Args: (flag). As get_gases(), but only returns gases with the given flag. +#[hook("/datum/gas_mixture/proc/get_by_flag")] +fn get_by_flag_hook(flag_val: Value) { + let flag = flag_val.as_number().map_or(0, |n| n as u32); + let pertinent_gases = with_gas_info(|gas_info| { + gas_info + .iter() + .filter(|g| g.flags & flag != 0) + .map(|g| g.idx) + .collect::>() + }); + if pertinent_gases.is_empty() { + return Ok(Value::from(0.0)); + } + with_mix(src, |mix| { + Ok(Value::from( + pertinent_gases + .iter() + .fold(0.0, |acc, idx| acc + mix.get_moles(*idx)), + )) + }) +} + +/// Args: (mixture, ratio, gas_list). Takes gases given by `gas_list` and moves `ratio` amount of those gases from `src` into `mixture`. #[hook("/datum/gas_mixture/proc/scrub_into")] fn _scrub_into_hook(into: Value, ratio_v: Value, gas_list: Value) { let ratio = ratio_v.as_number().map_err(|_| { @@ -295,6 +416,7 @@ fn _scrub_into_hook(into: Value, ratio_v: Value, gas_list: Value) { }) } +/// Marks the mix as immutable, meaning it will never change. This cannot be undone. #[hook("/datum/gas_mixture/proc/mark_immutable")] fn _mark_immutable_hook() { with_mix_mut(src, |mix| { @@ -303,6 +425,7 @@ fn _mark_immutable_hook() { }) } +/// Clears the gas mixture my removing all of its gases. #[hook("/datum/gas_mixture/proc/clear")] fn _clear_hook() { with_mix_mut(src, |mix| { @@ -311,38 +434,24 @@ fn _clear_hook() { }) } +/// Returns: true if the two mixtures are different enough for processing, false otherwise. #[hook("/datum/gas_mixture/proc/compare")] -fn _compare_hook() { - if args.is_empty() { - Err(runtime!("Tried comparing a gas mix to nothing")) - } else { - with_mixes(src, &args[0], |gas_one, gas_two| { - Ok(Value::from( - gas_one.temperature_compare(gas_two) - || gas_one.compare_with(gas_two, MINIMUM_MOLES_DELTA_TO_MOVE), - )) - }) - } -} - -#[hook("/datum/gas_mixture/proc/multiply")] -fn _multiply_hook() { - with_mix_mut(src, |mix| { - mix.multiply(if args.is_empty() { - 1.0 - } else { - args[0].as_number().unwrap_or(1.0) - }); - Ok(Value::null()) +fn _compare_hook(other: Value) { + with_mixes(src, other, |gas_one, gas_two| { + Ok(Value::from( + gas_one.temperature_compare(gas_two) + || gas_one.compare_with(gas_two, MINIMUM_MOLES_DELTA_TO_MOVE), + )) }) } +/// Args: (holder). Runs all reactions on this gas mixture. Holder is used by the reactions, and can be any arbitrary datum or null. #[hook("/datum/gas_mixture/proc/react")] fn _react_hook(holder: Value) { let mut ret: i32 = 0; let reactions = with_mix(src, |mix| Ok(mix.all_reactable()))?; for reaction in reactions { - ret |= react_by_id(reaction, src, holder)? + ret |= react_by_id(&reaction, src, holder)? .as_number() .unwrap_or_default() as i32; if ret & STOP_REACTIONS == STOP_REACTIONS { @@ -352,6 +461,7 @@ fn _react_hook(holder: Value) { Ok(Value::from(ret as f32)) } +/// Args: (heat). Adds a given amount of heat to the mixture, i.e. in joules taking into account capacity. #[hook("/datum/gas_mixture/proc/adjust_heat")] fn _adjust_heat_hook() { with_mix_mut(src, |mix| { @@ -372,6 +482,7 @@ fn _adjust_heat_hook() { }) } +/// Args: (mixture, amount). Takes the `amount` given and transfers it from `src` to `mixture`. #[hook("/datum/gas_mixture/proc/transfer_to")] fn _transfer_hook(other: Value, moles: Value) { with_mixes_mut(src, other, |our_mix, other_mix| { @@ -387,6 +498,7 @@ fn _transfer_hook(other: Value, moles: Value) { }) } +/// Args: (mixture, ratio). Transfers `ratio` of `src` to `mixture`. #[hook("/datum/gas_mixture/proc/transfer_ratio_to")] fn _transfer_ratio_hook(other: Value, ratio: Value) { with_mixes_mut(src, other, |our_mix, other_mix| { @@ -402,23 +514,20 @@ fn _transfer_ratio_hook(other: Value, ratio: Value) { }) } +/// Args: (mixture). Makes `src` a copy of `mixture`, with volumes taken into account. #[hook("/datum/gas_mixture/proc/equalize_with")] -fn _equalize_with_hook() { - with_mixes_custom( - src, - args.get(0) - .ok_or_else(|| runtime!("Wrong number of args for equalize_with: 0"))?, - |src_lock, total_lock| { - let src_gas = &mut src_lock.write(); - let vol = src_gas.volume; - let total_gas = total_lock.read(); - src_gas.copy_from_mutable(&total_gas); - src_gas.multiply(vol / total_gas.volume); - Ok(Value::null()) - }, - ) +fn _equalize_with_hook(total: Value) { + with_mixes_custom(src, total, |src_lock, total_lock| { + let src_gas = &mut src_lock.write(); + let vol = src_gas.volume; + let total_gas = total_lock.read(); + src_gas.copy_from_mutable(&total_gas); + src_gas.multiply(vol / total_gas.volume); + Ok(Value::null()) + }) } +/// Args: (temperature). Returns: how much fuel for fire is in the mixture at the given temperature. If temperature is omitted, just uses current temperature instead. #[hook("/datum/gas_mixture/proc/get_fuel_amount")] fn _fuel_amount_hook(temp: Value) { with_mix(src, |air| { @@ -433,6 +542,7 @@ fn _fuel_amount_hook(temp: Value) { }) } +/// Args: (temperature). Returns: how much oxidizer for fire is in the mixture at the given temperature. If temperature is omitted, just uses current temperature instead. #[hook("/datum/gas_mixture/proc/get_oxidation_power")] fn _oxidation_power_hook(temp: Value) { with_mix(src, |air| { @@ -447,6 +557,43 @@ fn _oxidation_power_hook(temp: Value) { }) } +/// Args: (mixture, ratio, one_way). Shares the given `ratio` of `src` with `mixture`, and, unless `one_way` is truthy, vice versa. +#[cfg(feature = "zas_hooks")] +#[hook("/datum/gas_mixture/proc/share_ratio")] +fn _share_ratio_hook(other_gas: Value, ratio_val: Value, one_way_val: Value) { + let one_way = one_way_val.as_bool().unwrap_or(false); + let ratio = ratio_val.as_number().ok().map_or(0.6); + let mut inbetween = Mixture::new(); + if one_way { + with_mixes_custom(src, other_gas, |src_lock, other_lock| { + let src_mix = src_lock.write(); + let other_mix = other_lock.read(); + inbetween.copy_from_mutable(other_mix); + inbetween.multiply(ratio); + inbetween.merge(&src_mix.remove_ratio(ratio)); + inbetween.multiply(0.5); + src_mix.merge(inbetween); + Ok(Value::from( + src_mix.temperature_compare(other_mix) + || src_mix.compare_with(other_mix, MINIMUM_MOLES_DELTA_TO_MOVE), + )) + }) + } else { + with_mixes_mut(src, other_gas, |src_mix, other_mix| { + src_mix.remove_ratio_into(ratio, &mut inbetween); + inbetween.merge(&other_mix.remove_ratio(ratio)); + inbetween.multiply(0.5); + src_mix.merge(inbetween); + other_mix.merge(inbetween); + Ok(Value::from( + src_mix.temperature_compare(other_mix) + || src_mix.compare_with(other_mix, MINIMUM_MOLES_DELTA_TO_MOVE), + )) + }) + } +} + +/// Args: (list). Takes every gas in the list and makes them all identical, scaled to their respective volumes. The total heat and amount of substance in all of the combined gases is conserved. #[hook("/proc/equalize_all_gases_in_list")] fn _equalize_all_hook() { use std::collections::BTreeSet; @@ -479,7 +626,7 @@ fn _equalize_all_hook() { if let Some(src_gas_lock) = all_mixtures.get(id) { let src_gas = src_gas_lock.read(); tot.merge(&src_gas); - tot_vol += src_gas.volume as f64; + tot_vol += f64::from(src_gas.volume); } } if tot_vol > 0.0 { @@ -488,7 +635,7 @@ fn _equalize_all_hook() { let dest_gas = &mut dest_gas_lock.write(); let vol = dest_gas.volume; // don't wanna borrow it in the below dest_gas.copy_from_mutable(&tot); - dest_gas.multiply((vol as f64 / tot_vol) as f32); + dest_gas.multiply((f64::from(vol) / tot_vol) as f32); } } } @@ -496,11 +643,13 @@ fn _equalize_all_hook() { Ok(Value::null()) } +/// Returns: the amount of gas mixtures that are attached to a byond gas mixture. #[hook("/datum/controller/subsystem/air/proc/get_amt_gas_mixes")] fn _hook_amt_gas_mixes() { Ok(Value::from(amt_gases() as f32)) } +/// Returns: the total amount of gas mixtures in the arena, including "free" ones. #[hook("/datum/controller/subsystem/air/proc/get_max_gas_mixes")] fn _hook_max_gas_mixes() { Ok(Value::from(tot_gases() as f32)) diff --git a/src/reaction.rs b/src/reaction.rs index 05e85fa..bb93ae1 100644 --- a/src/reaction.rs +++ b/src/reaction.rs @@ -1,7 +1,7 @@ #[cfg(feature = "reaction_hooks")] pub mod hooks; -use auxtools::*; +use auxtools::{byond_string, inventory, runtime, shutdown, DMResult, Value}; use std::cell::RefCell; @@ -19,7 +19,7 @@ pub struct Reaction { min_gas_reqs: Vec<(GasIDX, f32)>, } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Default)] pub struct ReactionIdentifier { string_id_hash: u64, priority: f32, @@ -86,9 +86,12 @@ fn clean_up_reaction_values() { }); } -pub fn react_by_id(id: ReactionIdentifier, src: &Value, holder: &Value) -> DMResult { +/// Runs a reaction given a `ReactionIdentifier`. Returns the result of the reaction, error or success. +/// # Errors +/// If the reaction itself has a runtime. +pub fn react_by_id(id: &ReactionIdentifier, src: &Value, holder: &Value) -> DMResult { REACTION_VALUES.with(|r| { - r.borrow().get(&id).map_or_else( + r.borrow().get(id).map_or_else( || Err(runtime!("Reaction with invalid id")), |reaction| match reaction { ReactionSide::ByondSide(val) => val.call("react", &[src, holder]), @@ -105,6 +108,7 @@ impl Reaction { /// /// /// If given anything but a `/datum/gas_reaction`, this will panic. + #[must_use] pub fn from_byond_reaction(reaction: &Value) -> Self { let priority = -reaction .get_number(byond_string!("priority")) @@ -186,6 +190,7 @@ impl Reaction { } our_reaction } + #[must_use] pub fn get_id(&self) -> ReactionIdentifier { self.id } @@ -209,11 +214,14 @@ impl Reaction { }) } /// Returns the priority of the reaction. + #[must_use] pub fn get_priority(&self) -> f32 { self.id.priority } /// Calls the reaction with the given arguments. + /// # Errors + /// If the reaction itself has a runtime error, this will propagate it up. pub fn react(&self, src: &Value, holder: &Value) -> DMResult { - react_by_id(self.id, src, holder) + react_by_id(&self.id, src, holder) } } diff --git a/src/reaction/hooks.rs b/src/reaction/hooks.rs index f6d5ec1..a9be24e 100644 --- a/src/reaction/hooks.rs +++ b/src/reaction/hooks.rs @@ -7,6 +7,7 @@ use crate::gas::{ const SUPER_SATURATION_THRESHOLD: f32 = 96.0; +#[must_use] pub fn func_from_id(id: &str) -> Option { match id { #[cfg(feature = "plasma_fire_hook")] @@ -29,7 +30,7 @@ fn plasma_fire(byond_air: &Value, holder: &Value) -> DMResult { const OXYGEN_BURN_RATE_BASE: f32 = 1.4; const PLASMA_OXYGEN_FULLBURN: f32 = 10.0; const PLASMA_BURN_RATE_DELTA: f32 = 9.0; - const FIRE_PLASMA_ENERGY_RELEASED: f32 = 3000000.0; + const FIRE_PLASMA_ENERGY_RELEASED: f32 = 3_000_000.0; let o2 = gas_idx_from_string(GAS_O2)?; let plasma = gas_idx_from_string(GAS_PLASMA)?; let co2 = gas_idx_from_string(GAS_CO2)?; @@ -118,7 +119,7 @@ fn tritium_fire(byond_air: &Value, holder: &Value) -> DMResult { const TRITIUM_BURN_OXY_FACTOR: f32 = 100.0; const TRITIUM_BURN_TRIT_FACTOR: f32 = 10.0; const TRITIUM_MINIMUM_RADIATION_FACTOR: f32 = 0.1; - const FIRE_HYDROGEN_ENERGY_RELEASED: f32 = 280000.0; + const FIRE_HYDROGEN_ENERGY_RELEASED: f32 = 280_000.0; let o2 = gas_idx_from_string(GAS_O2)?; let tritium = gas_idx_from_string(GAS_TRITIUM)?; let water = gas_idx_from_string(GAS_H2O)?; @@ -165,11 +166,11 @@ fn tritium_fire(byond_air: &Value, holder: &Value) -> DMResult { if let Some(radiation_burn) = Proc::find(byond_string!("/proc/radiation_burn")) { radiation_burn.call(&[holder, &Value::from(energy_released)])?; } else { - let _ = Proc::find(byond_string!("/proc/stack_trace")) + drop(Proc::find(byond_string!("/proc/stack_trace")) .ok_or_else(|| runtime!("Couldn't find stack_trace!"))? .call(&[&Value::from_string( "radiation_burn not found! Auxmos hooked trit fires won't irradiated without it!" - )?]); + )?])); } } if temperature > FIRE_MINIMUM_TEMPERATURE_TO_EXIST { @@ -283,7 +284,7 @@ fn fusion(byond_air: &Value, holder: &Value) -> DMResult { * 10_f32.powf( ((thermal_energy + bowdlerized_reaction_energy) / middle_energy) .log(FUSION_ENERGY_TRANSLATION_EXPONENT), - ) + ); }; //The decay of the tritium and the reaction's energy produces waste gases, different ones depending on whether the reaction is endo or exothermic @@ -365,12 +366,12 @@ fn generic_fire(byond_air: &Value, holder: &Value) -> DMResult { } else { let oxidation_ratio = oxidation_power / total_fuel; if oxidation_ratio > 1.0 { - for (_, amt, power) in oxidizers.iter_mut() { + for (_, amt, power) in &mut oxidizers { *amt /= oxidation_ratio; *power /= oxidation_ratio; } } else { - for (_, amt, power) in fuels.iter_mut() { + for (_, amt, power) in &mut fuels { *amt *= oxidation_ratio; *power *= oxidation_ratio; } @@ -417,7 +418,7 @@ fn generic_fire(byond_air: &Value, holder: &Value) -> DMResult { let initial_enthalpy = air.get_temperature() * (air.heat_capacity() + R_IDEAL_GAS_EQUATION * air.total_moles()); let mut delta_enthalpy = 0.0; - for (&i, &amt) in burn_results.iter() { + for (&i, &amt) in &burn_results { air.adjust_moles(i, amt); delta_enthalpy -= amt * gas_info[i as usize].enthalpy; } @@ -453,11 +454,11 @@ fn generic_fire(byond_air: &Value, holder: &Value) -> DMResult { if let Some(radiation_burn) = Proc::find(byond_string!("/proc/radiation_burn")) { radiation_burn.call(&[holder, &Value::from(radiation_released)])?; } else { - let _ = Proc::find(byond_string!("/proc/stack_trace")) + drop(Proc::find(byond_string!("/proc/stack_trace")) .ok_or_else(|| runtime!("Couldn't find stack_trace!"))? .call(&[&Value::from_string( "radiation_burn not found! Auxmos hooked fires won't irradiate without it!" - )?]); + )?])); } } Ok(Value::from(if fire_amount > 0.0 { 1.0 } else { 0.0 })) diff --git a/src/turfs.rs b/src/turfs.rs index 77cb986..d132d54 100644 --- a/src/turfs.rs +++ b/src/turfs.rs @@ -25,6 +25,8 @@ use rayon; use rayon::prelude::*; +use std::mem::drop; + use crate::callbacks::aux_callbacks_sender; const NORTH: u8 = 1; @@ -46,6 +48,13 @@ const fn adj_flag_to_idx(adj_flag: u8) -> usize { } } +pub const NONE: u8 = 0; +pub const SIMULATION_DIFFUSE: u8 = 1; +pub const SIMULATION_ALL: u8 = 2; +pub const SIMULATION_ANY: u8 = SIMULATION_DIFFUSE | SIMULATION_ALL; +pub const SIMULATION_DISABLED: u8 = 4; +pub const SIMULATION_FLAGS: u8 = SIMULATION_ANY | SIMULATION_DISABLED; + type TurfID = u32; // TurfMixture can be treated as "immutable" for all intents and purposes--put other data somewhere else @@ -53,7 +62,7 @@ type TurfID = u32; struct TurfMixture { pub mix: usize, pub adjacency: u8, - pub simulation_level: u8, + pub flags: u8, pub planetary_atmos: Option, pub adjacents: [Option; 6], // this baby saves us 50% of the cpu time in FDM calcs } @@ -61,8 +70,8 @@ struct TurfMixture { #[allow(dead_code)] impl TurfMixture { pub fn enabled(&self) -> bool { - self.simulation_level > 0 - && self.simulation_level & SIMULATION_LEVEL_DISABLED != SIMULATION_LEVEL_DISABLED + let simul_flags = self.flags & SIMULATION_FLAGS; + simul_flags & SIMULATION_DISABLED == 0 && simul_flags & SIMULATION_ANY != 0 } pub fn adjacent_mixes<'a>( &'a self, @@ -198,10 +207,10 @@ fn _hook_register_turf() { let sender = aux_callbacks_sender(crate::callbacks::TURFS); if simulation_level < 0.0 { let id = unsafe { src.raw.data.id }; - let _ = sender.send(Box::new(move || { + drop(sender.send(Box::new(move || { turf_gases().remove(&id); Ok(Value::null()) - })); + }))); Ok(Value::null()) } else { let mut to_insert: TurfMixture = TurfMixture::default(); @@ -217,7 +226,7 @@ fn _hook_register_turf() { ) })? .to_bits() as usize; - to_insert.simulation_level = args[0].as_number().map_err(|_| { + to_insert.flags = args[0].as_number().map_err(|_| { runtime!( "Attempt to interpret non-number value as number {} {}:{}", std::file!(), @@ -241,10 +250,10 @@ fn _hook_register_turf() { } } let id = unsafe { src.raw.data.id }; - let _ = sender.send(Box::new(move || { + drop(sender.send(Box::new(move || { turf_gases().insert(id, to_insert); Ok(Value::null()) - })); + }))); Ok(Value::null()) } } @@ -307,15 +316,15 @@ fn _hook_turf_update_temp() { std::column!() ) })?; - let _ = sender.send(Box::new(move || { + drop(sender.send(Box::new(move || { turf_temperatures().insert(id, to_insert); Ok(Value::null()) - })); + }))); } else { - let _ = sender.send(Box::new(move || { + drop(sender.send(Box::new(move || { turf_temperatures().remove(&id); Ok(Value::null()) - })); + }))); } Ok(Value::null()) } @@ -338,11 +347,11 @@ fn _hook_sleep() { let src_id = unsafe { src.raw.data.id }; if arg == 0.0 { turf_gases().entry(src_id).and_modify(|turf| { - turf.simulation_level &= !SIMULATION_LEVEL_DISABLED; + turf.flags &= !SIMULATION_DISABLED; }); } else { turf_gases().entry(src_id).and_modify(|turf| { - turf.simulation_level |= SIMULATION_LEVEL_DISABLED; + turf.flags |= SIMULATION_DISABLED; }); } Ok(Value::from(true)) @@ -352,7 +361,7 @@ fn _hook_infos(arg0: Value, arg1: Value) { let update_now = arg1.as_number().unwrap_or(0.0) != 0.0; let adjacent_to_spess = arg0.as_number().unwrap_or(0.0) != 0.0; let id = unsafe { src.raw.data.id }; - let sender = aux_callbacks_sender(crate::callbacks::TURFS); + let sender = aux_callbacks_sender(crate::callbacks::ADJACENCIES); let boxed_fn: Box DMResult + Send + Sync> = Box::new(move || { let src_turf = unsafe { Value::turf_by_id_unchecked(id) }; if let Ok(adjacent_list) = src_turf.get_list(byond_string!("atmos_adjacent_turfs")) { @@ -379,11 +388,11 @@ fn _hook_infos(arg0: Value, arg1: Value) { if let Ok(blocks_air) = src_turf.get_number(byond_string!("blocks_air")) { if blocks_air == 0.0 { turf_gases().entry(id).and_modify(|turf| { - turf.simulation_level &= !SIMULATION_LEVEL_DISABLED; + turf.flags &= !SIMULATION_DISABLED; }); } else { turf_gases().entry(id).and_modify(|turf| { - turf.simulation_level &= !SIMULATION_LEVEL_DISABLED; + turf.flags |= SIMULATION_DISABLED; }); } } @@ -415,7 +424,7 @@ fn _hook_infos(arg0: Value, arg1: Value) { if update_now { boxed_fn()?; } else { - let _ = sender.send(boxed_fn); + drop(sender.send(boxed_fn)); } Ok(Value::null()) } @@ -432,9 +441,12 @@ fn _hook_turf_temperature() { src.get(byond_string!("initial_temperature")) } } -///* // gas_overlays: list( GAS_ID = list( VIS_FACTORS = OVERLAYS )) got it? I don't -pub fn update_visuals(src: Value) -> DMResult { +/// Updates the visual overlays for the given turf. +/// Will use a cached overlay list if one exists. +/// # Errors +/// If auxgm wasn't implemented properly or there's an invalid gas mixture. +pub fn update_visuals(src: &Value) -> DMResult { use super::gas; match src.get(byond_string!("air")) { Err(_) => Ok(Value::null()), @@ -466,7 +478,7 @@ pub fn update_visuals(src: Value) -> DMResult { .min(FACTOR_GAS_VISIBLE_MAX) .max(1.0) as u32, ) { - overlay_types.append(this_gas_overlay) + overlay_types.append(this_gas_overlay); } } } @@ -482,12 +494,6 @@ pub fn update_visuals(src: Value) -> DMResult { } } } -//*/ -pub const SIMULATION_LEVEL_NONE: u8 = 0; -pub const SIMULATION_LEVEL_DIFFUSE: u8 = 1; -pub const SIMULATION_LEVEL_ALL: u8 = 2; -pub const SIMULATION_LEVEL_ANY: u8 = SIMULATION_LEVEL_DIFFUSE | SIMULATION_LEVEL_ALL; -pub const SIMULATION_LEVEL_DISABLED: u8 = 4; fn adjacent_tile_id(id: u8, i: TurfID, max_x: i32, max_y: i32) -> TurfID { let z_size = max_x * max_y; diff --git a/src/turfs/katmos.rs b/src/turfs/katmos.rs index 9e38443..6aaa0b5 100644 --- a/src/turfs/katmos.rs +++ b/src/turfs/katmos.rs @@ -19,7 +19,7 @@ use crate::callbacks::process_aux_callbacks; use auxcallback::byond_callback_sender; -use dashmap::*; +use dashmap::DashMap; type TransferInfo = [f32; 7]; @@ -149,10 +149,10 @@ fn finalize_eq( if turf.total_moles() < planet_transfer_amount { finalize_eq_neighbors(i, turf, turfs, transfer_dirs, max_x, max_y, info); } - let _ = GasArena::with_gas_mixture_mut(turf.mix, |gas| { - gas.remove(planet_transfer_amount); + drop(GasArena::with_gas_mixture_mut(turf.mix, |gas| { + gas.add(-planet_transfer_amount); Ok(()) - }); + })); } else if planet_transfer_amount < 0.0 { if let Some(air_entry) = turf .planetary_atmos @@ -161,10 +161,10 @@ fn finalize_eq( let planet_air = air_entry.value(); let planet_sum = planet_air.total_moles(); if planet_sum > 0.0 { - let _ = GasArena::with_gas_mixture_mut(turf.mix, |gas| { + drop(GasArena::with_gas_mixture_mut(turf.mix, |gas| { gas.merge(&(planet_air * (-planet_transfer_amount / planet_sum))); Ok(()) - }); + })); } } } @@ -178,16 +178,16 @@ fn finalize_eq( if let Some(adj_turf) = turfs.get(&adj_id) { adj_info.transfer_dirs[OPP_DIR_INDEX[j as usize]] = 0.0; if turf.mix != adj_turf.mix { - let _ = GasArena::with_gas_mixtures_mut( + drop(GasArena::with_gas_mixtures_mut( turf.mix, adj_turf.mix, |air, other_air| { other_air.merge(&air.remove(amount)); Ok(()) }, - ); + )); } - let _ = sender.try_send(Box::new(move || { + drop(sender.try_send(Box::new(move || { let real_amount = Value::from(amount); let turf = unsafe { Value::turf_by_id_unchecked(i as u32) }; let other_turf = unsafe { Value::turf_by_id_unchecked(adj_id as u32) }; @@ -199,7 +199,7 @@ fn finalize_eq( .call(&[&Value::from_string(e.message.as_str())?])?; } Ok(Value::null()) - })); + }))); } } } @@ -231,7 +231,7 @@ fn finalize_eq_neighbors( } fn monstermos_fast_process( - i: &TurfID, + i: TurfID, m: &TurfMixture, turfs: &IndexMap, max_x: i32, @@ -239,7 +239,7 @@ fn monstermos_fast_process( info: &DashMap, ) { let mut cur_info = { - let maybe_cur_orig = info.get_mut(i); + let maybe_cur_orig = info.get_mut(&i); if maybe_cur_orig.is_none() { return; } @@ -249,8 +249,8 @@ fn monstermos_fast_process( }; let mut eligible_adjacents: u8 = 0; if cur_info.mole_delta > 0.0 { - for (j, loc) in adjacent_tile_ids_no_orig(m.adjacency, *i, max_x, max_y) { - if turfs.get(&loc).map_or(false, |thin| thin.enabled()) { + for (j, loc) in adjacent_tile_ids_no_orig(m.adjacency, i, max_x, max_y) { + if turfs.get(&loc).map_or(false, TurfMixture::enabled) { if let Some(adj_info) = info.get(&loc) { if !adj_info.fast_done { eligible_adjacents |= 1 << j; @@ -260,24 +260,24 @@ fn monstermos_fast_process( } let amt_eligible = eligible_adjacents.count_ones(); if amt_eligible == 0 { - info.entry(*i).and_modify(|entry| *entry = cur_info); + info.entry(i).and_modify(|entry| *entry = cur_info); return; } let moles_to_move = cur_info.mole_delta / amt_eligible as f32; - for (j, loc) in adjacent_tile_ids_no_orig(eligible_adjacents, *i, max_x, max_y) { + for (j, loc) in adjacent_tile_ids_no_orig(eligible_adjacents, i, max_x, max_y) { if let Some(mut adj_info) = info.get_mut(&loc) { cur_info.adjust_eq_movement(Some(&mut adj_info), j as usize, moles_to_move); cur_info.mole_delta -= moles_to_move; adj_info.mole_delta += moles_to_move; } - info.entry(*i).and_modify(|entry| *entry = cur_info); + info.entry(i).and_modify(|entry| *entry = cur_info); } } } fn give_to_takers( - giver_turfs: Vec, - _taker_turfs: Vec, + giver_turfs: &[RefMixWithID], + _taker_turfs: &[RefMixWithID], turfs: &IndexMap, max_x: i32, max_y: i32, @@ -286,7 +286,7 @@ fn give_to_takers( let mut queue: IndexMap = IndexMap::with_hasher(FxBuildHasher::default()); - for (i, m) in giver_turfs { + for &(i, m) in giver_turfs { let mut giver_info = { let maybe_giver_orig = info.get_mut(i); if maybe_giver_orig.is_none() { @@ -358,8 +358,8 @@ fn give_to_takers( } fn take_from_givers( - taker_turfs: Vec, - _giver_turfs: Vec, + taker_turfs: &[RefMixWithID], + _giver_turfs: &[RefMixWithID], turfs: &IndexMap, max_x: i32, max_y: i32, @@ -368,7 +368,7 @@ fn take_from_givers( let mut queue: IndexMap = IndexMap::with_hasher(FxBuildHasher::default()); - for (i, m) in taker_turfs { + for &(i, m) in taker_turfs { let mut taker_info = { let maybe_taker_orig = info.get_mut(i); if maybe_taker_orig.is_none() { @@ -482,9 +482,12 @@ fn explosively_depressurize( continue; } for (_, loc) in adjacent_tile_ids(m.adjacency, i, max_x, max_y) { - let mut insert_success = false; - if turf_gases().get(&loc).is_some() { - insert_success = turfs.insert(loc) + let insert_success = { + if turf_gases().get(&loc).is_some() { + turfs.insert(loc) + } else { + false + } }; if insert_success { unsafe { Value::turf_by_id_unchecked(i) }.call( @@ -500,6 +503,7 @@ fn explosively_depressurize( } process_aux_callbacks(crate::callbacks::TURFS); + process_aux_callbacks(crate::callbacks::ADJACENCIES); if space_turfs.is_empty() { return Ok(Value::null()); @@ -658,9 +662,9 @@ fn flood_fill_equalize_turfs( // NOT ONE OF YOU IS GONNA SURVIVE THIS // (I just made explosions less laggy, you're welcome) if !ignore_zone { - let _ = sender.send(Box::new(move || { + drop(sender.send(Box::new(move || { explosively_depressurize(i, max_x, max_y, equalize_hard_turf_limit) - })); + }))); } ignore_zone = true; } @@ -673,7 +677,7 @@ fn flood_fill_equalize_turfs( } fn process_planet_turfs( - planet_turfs: IndexMap, + planet_turfs: &IndexMap, turfs: &IndexMap, average_moles: f32, max_x: i32, @@ -719,12 +723,12 @@ fn process_planet_turfs( for (j, loc) in adjacent_tile_ids_no_orig(m.adjacency, i, max_x, max_y) { if let Some(mut adj_info) = info.get_mut(&loc) { if queue_idx < equalize_hard_turf_limit { - let _ = sender.try_send(Box::new(move || { + drop(sender.try_send(Box::new(move || { let that_turf = unsafe { Value::turf_by_id_unchecked(loc) }; let this_turf = unsafe { Value::turf_by_id_unchecked(i) }; this_turf.call("consider_firelocks", &[&that_turf])?; Ok(Value::null()) - })); + }))); } if let Some(adj) = turfs .get(&loc) @@ -821,7 +825,7 @@ pub(crate) fn equalize( let log_n = ((turfs.len() as f32).log2().floor()) as usize; if giver_turfs.len() > log_n && taker_turfs.len() > log_n { - for (i, m) in &turfs { + for (&i, m) in &turfs { monstermos_fast_process(i, m, &turfs, max_x, max_y, &info); } giver_turfs.clear(); @@ -839,17 +843,19 @@ pub(crate) fn equalize( // alright this is the part that can become O(n^2). if giver_turfs.len() < taker_turfs.len() { // as an optimization, we choose one of two methods based on which list is smaller. - give_to_takers(giver_turfs, taker_turfs, &turfs, max_x, max_y, &info); + give_to_takers(&giver_turfs, &taker_turfs, &turfs, max_x, max_y, &info); } else { - take_from_givers(taker_turfs, giver_turfs, &turfs, max_x, max_y, &info); + take_from_givers(&taker_turfs, &giver_turfs, &turfs, max_x, max_y, &info); } - if !planet_turfs.is_empty() { + if planet_turfs.is_empty() { + turfs_processed.fetch_add(turfs.len(), std::sync::atomic::Ordering::Relaxed); + } else { turfs_processed.fetch_add( turfs.len() + planet_turfs.len(), std::sync::atomic::Ordering::Relaxed, ); process_planet_turfs( - planet_turfs, + &planet_turfs, &turfs, average_moles, max_x, @@ -857,8 +863,6 @@ pub(crate) fn equalize( equalize_hard_turf_limit, &info, ); - } else { - turfs_processed.fetch_add(turfs.len(), std::sync::atomic::Ordering::Relaxed); } (turfs, info) }) diff --git a/src/turfs/processing.rs b/src/turfs/processing.rs index 651da41..98598fb 100644 --- a/src/turfs/processing.rs +++ b/src/turfs/processing.rs @@ -16,7 +16,7 @@ use parking_lot::{Once, RwLock}; use crate::callbacks::process_aux_callbacks; -lazy_static! { +lazy_static::lazy_static! { static ref TURF_CHANNEL: ( flume::Sender>, flume::Receiver> @@ -89,6 +89,7 @@ fn _finish_process_turfs() { })?; let processing_callbacks_unfinished = process_callbacks_for_millis(arg_limit as u64); process_aux_callbacks(crate::callbacks::TURFS); + process_aux_callbacks(crate::callbacks::ADJACENCIES); if processing_callbacks_unfinished { Ok(Value::from(true)) } else { @@ -147,7 +148,8 @@ fn _process_turf_notify() { .unwrap_or(1.0) != 0.0; process_aux_callbacks(crate::callbacks::TURFS); - let _ = sender.try_send(Box::new(SSairInfo { + process_aux_callbacks(crate::callbacks::ADJACENCIES); + drop(sender.try_send(Box::new(SSairInfo { fdm_max_steps, equalize_turf_limit, equalize_hard_turf_limit, @@ -156,7 +158,7 @@ fn _process_turf_notify() { max_x, max_y, planet_enabled, - })); + }))); Ok(Value::null()) } @@ -180,7 +182,7 @@ fn _process_turf_start() -> Result<(), String> { ); let bench = start_time.elapsed().as_millis(); let (lpt, hpt) = (low_pressure_turfs.len(), high_pressure_turfs.len()); - let _ = sender.try_send(Box::new(move || { + drop(sender.try_send(Box::new(move || { let ssair = auxtools::Value::globals().get(byond_string!("SSair"))?; let prev_cost = ssair.get_number(byond_string!("cost_turfs")).map_err(|_| { @@ -201,7 +203,7 @@ fn _process_turf_start() -> Result<(), String> { Value::from(hpt as f32), )?; Ok(Value::null()) - })); + }))); (low_pressure_turfs, high_pressure_turfs) }; { @@ -213,7 +215,7 @@ fn _process_turf_start() -> Result<(), String> { &low_pressure_turfs, ); let bench = start_time.elapsed().as_millis(); - let _ = sender.try_send(Box::new(move || { + drop(sender.try_send(Box::new(move || { let ssair = auxtools::Value::globals().get(byond_string!("SSair"))?; let prev_cost = ssair @@ -235,7 +237,7 @@ fn _process_turf_start() -> Result<(), String> { Value::from(processed_turfs as f32), )?; Ok(Value::null()) - })); + }))); } if info.equalize_enabled { let start_time = Instant::now(); @@ -276,7 +278,7 @@ fn _process_turf_start() -> Result<(), String> { } }; let bench = start_time.elapsed().as_millis(); - let _ = sender.try_send(Box::new(move || { + drop(sender.try_send(Box::new(move || { let ssair = auxtools::Value::globals().get(byond_string!("SSair"))?; let prev_cost = ssair @@ -298,13 +300,13 @@ fn _process_turf_start() -> Result<(), String> { Value::from(processed_turfs as f32), )?; Ok(Value::null()) - })); + }))); } { let start_time = Instant::now(); post_process(); let bench = start_time.elapsed().as_millis(); - let _ = sender.try_send(Box::new(move || { + drop(sender.try_send(Box::new(move || { let ssair = auxtools::Value::globals().get(byond_string!("SSair"))?; let prev_cost = ssair .get_number(byond_string!("cost_post_process")) @@ -321,7 +323,7 @@ fn _process_turf_start() -> Result<(), String> { Value::from(0.8 * prev_cost + 0.2 * (bench as f32)), )?; Ok(Value::null()) - })); + }))); } TASKS_RUNNING.fetch_sub(1, Ordering::SeqCst); }); @@ -521,7 +523,7 @@ fn fdm( .for_each(|temp_value| { let sender = byond_callback_sender(); let these_pressure_deltas = temp_value.to_vec(); - let _ = sender.try_send(Box::new(move || { + drop(sender.try_send(Box::new(move || { for &(turf_id, pressure_diffs, _) in these_pressure_deltas.iter().filter(|&(id, _, _)| *id != 0) { @@ -544,7 +546,7 @@ fn fdm( } } Ok(Value::null()) - })); + }))); }); } high_pressure_turfs.extend(high_pressure.iter().map(|(i, _, _)| i)); @@ -606,12 +608,12 @@ fn excited_group_processing( continue; } found_turfs.insert(loc); - if let Some(border_mix) = turf_gases().try_get(&loc).try_unwrap() { - if border_mix.simulation_level & SIMULATION_LEVEL_DISABLED - != SIMULATION_LEVEL_DISABLED - { - border_turfs.push_back((loc, *border_mix)); - } + if let Some(border_mix) = turf_gases() + .try_get(&loc) + .try_unwrap() + .filter(|b| b.enabled()) + { + border_turfs.push_back((loc, *border_mix)); } } } @@ -650,25 +652,36 @@ fn remove_trace_planet_gases( .and_then(RwLock::try_read) .map_or(false, |gas| !gas.compare_with(planet_atmos, 0.1)) { - if let Some(mut gas) = all_mixtures.get(m.mix).and_then(|lock| lock.try_write()) { + if let Some(mut gas) = all_mixtures.get(m.mix).and_then(RwLock::try_write) { gas.copy_from_mutable(planet_atmos); } } } } +static mut VISUALS_CACHE: Option> = None; + // Checks if the gas can react or can update visuals, returns None if not. fn post_process_cell( i: TurfID, m: TurfMixture, vis: &[Option], all_mixtures: &[RwLock], + vis_cache: &std::collections::HashMap, + updates: &flume::Sender<(usize, u64)>, ) -> Option<(TurfID, bool, bool)> { all_mixtures .get(m.mix) .and_then(RwLock::try_read) .and_then(|gas| { - let should_update_visuals = gas.vis_hash_changed(vis); + let should_update_visuals = + match gas.vis_hash_changed(vis, vis_cache.get(&m.mix).copied().unwrap_or(0)) { + Some(hash) => { + let _ = updates.send((m.mix, hash)); + true + } + None => false, + }; let reactable = gas.can_react(); (should_update_visuals || reactable).then(|| (i, should_update_visuals, reactable)) }) @@ -687,29 +700,36 @@ fn post_process() { } }; let vis = crate::gas::visibility_copies(); - let processables = turf_gases() - .par_iter() - .map(|entry| { - let (&i, &m) = entry.pair(); - (i, m) - }) - .filter_map(|(i, m)| { - m.enabled() - .then(|| { - GasArena::with_all_mixtures(|all_mixtures| { - if should_check_planet_turfs { - let planetary_atmos = planetary_atmos(); - remove_trace_planet_gases(m, planetary_atmos, all_mixtures); - } - post_process_cell(i, m, &vis, all_mixtures) + let (sender, receiver) = flume::unbounded(); + let vis_cache = unsafe { + VISUALS_CACHE + .get_or_insert_with(|| std::collections::HashMap::with_hasher(FxBuildHasher::default())) + }; + let processables = { + turf_gases() + .par_iter() + .map(|entry| { + let (&i, &m) = entry.pair(); + (i, m) + }) + .filter_map(|(i, m)| { + m.enabled() + .then(|| { + GasArena::with_all_mixtures(|all_mixtures| { + if should_check_planet_turfs { + let planetary_atmos = planetary_atmos(); + remove_trace_planet_gases(m, planetary_atmos, all_mixtures); + } + post_process_cell(i, m, &vis, all_mixtures, vis_cache, &sender.clone()) + }) }) - }) - .flatten() - }) - .collect::>(); + .flatten() + }) + .collect::>() + }; processables.into_par_iter().chunks(30).for_each(|chunk| { let sender = byond_callback_sender(); - let _ = sender.try_send(Box::new(move || { + drop(sender.try_send(Box::new(move || { for (i, should_update_vis, should_react) in chunk.clone() { let turf = unsafe { Value::turf_by_id_unchecked(i) }; if should_react { @@ -721,12 +741,15 @@ fn post_process() { } if should_update_vis { //turf.call("update_visuals", &[])?; - update_visuals(turf)?; + update_visuals(&turf)?; } } Ok(Value::null()) - })); + }))); }); + for (k, v) in receiver.drain() { + vis_cache.insert(k, v); + } } static HEAT_PROCESS_TIME: AtomicU64 = AtomicU64::new(1_000_000); @@ -828,7 +851,7 @@ fn _process_heat_start() -> Result<(), String> { let is_temp_delta_with_air = turf_gases() .try_get(&i) .try_unwrap() - .filter(|m| m.simulation_level & SIMULATION_LEVEL_ANY > 0) + .filter(|m| m.enabled()) .and_then(|m| { GasArena::with_all_mixtures(|all_mixtures| { all_mixtures.get(m.mix).and_then(RwLock::try_read).map( @@ -838,9 +861,10 @@ fn _process_heat_start() -> Result<(), String> { }) .unwrap_or(false); for (_, loc) in adjacent_tile_ids(adj, i, info.max_x, info.max_y) { - heat_delta += turf_temperatures().try_get(&loc).try_unwrap().map_or( - 0.0, - |other| { + heat_delta += turf_temperatures() + .try_get(&loc) + .try_unwrap() + .map_or(0.0, |other| { /* The horrible line below is essentially sharing between solids--making it the minimum of both @@ -850,8 +874,7 @@ fn _process_heat_start() -> Result<(), String> { * (other.temperature - t.temperature) * (t.heat_capacity * other.heat_capacity / (t.heat_capacity + other.heat_capacity)) - }, - ) + }); } if t.adjacent_to_space { /* @@ -862,7 +885,7 @@ fn _process_heat_start() -> Result<(), String> { this will never go into infinities. */ let blackbody_radiation: f64 = (emissivity_constant - * ((t.temperature as f64).powi(4))) + * (f64::from(t.temperature).powi(4))) - radiation_from_space_tick; heat_delta -= blackbody_radiation as f32; } @@ -912,11 +935,11 @@ fn _process_heat_start() -> Result<(), String> { && t.temperature > t.heat_capacity { // not what heat capacity means but whatever - let _ = sender.try_send(Box::new(move || { + drop(sender.try_send(Box::new(move || { let turf = unsafe { Value::turf_by_id_unchecked(i) }; turf.set(byond_string!("to_be_destroyed"), 1.0)?; Ok(Value::null()) - })); + }))); } }); //Alright, now how much time did that take? @@ -932,5 +955,6 @@ fn _process_heat_start() -> Result<(), String> { #[shutdown] fn reset_auxmos_processing() { + unsafe { VISUALS_CACHE = None }; HEAT_PROCESS_TIME.store(1_000_000, Ordering::Relaxed); }