├── .rustfmt.toml ├── .gitignore ├── .editorconfig ├── .gitattributes ├── crates ├── auxmacros │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── auxcallback │ ├── Cargo.toml │ └── src │ └── lib.rs ├── .github └── workflows │ ├── build.yml │ ├── check.yml │ └── release.yml ├── LICENSE ├── README.md ├── src ├── parser.rs ├── reaction │ ├── yogs.rs │ └── citadel.rs ├── turfs │ ├── groups.rs │ ├── putnamos.rs │ ├── superconduct.rs │ └── processing.rs ├── reaction.rs ├── gas │ ├── constants.rs │ ├── types.rs │ └── mixture.rs ├── gas.rs ├── turfs.rs └── lib.rs ├── docs ├── AUXGM.md ├── FIRE.md └── MIGRATING.md └── Cargo.toml /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.vscode 3 | /.cargo/config.toml 4 | /bindings.dm 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = tab 3 | indent_size = 4 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | # Guaranteed to be text 4 | *.c text=auto eol=lf 5 | *.cpp text=auto eol=lf 6 | *.rs text=auto eol=lf 7 | *.h text=auto eol=lf 8 | *.txt text=auto eol=lf 9 | *.md text=auto eol=lf 10 | *.toml text=auto eol=lf 11 | -------------------------------------------------------------------------------- /crates/auxmacros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "auxmacros" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Macros for auxmos" 6 | license = "MIT" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | quote = "1.0" 15 | proc-macro2 = "1.0" 16 | 17 | [dependencies.syn] 18 | version = "2.0" 19 | features = ["full", "parsing", "printing"] 20 | -------------------------------------------------------------------------------- /crates/auxcallback/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "auxcallback" 3 | version = "0.2.1" 4 | authors = ["Putnam "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [lib] 10 | crate-type = ["lib"] 11 | 12 | [dependencies] 13 | byondapi = { workspace = true } 14 | flume = { workspace = true } 15 | coarsetime = { workspace = true } 16 | eyre = { workspace = true } 17 | tracing = { workspace = true } 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Linux build 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | linux-build: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions-rs/toolchain@v1 11 | with: 12 | toolchain: stable 13 | target: i686-unknown-linux-gnu 14 | override: true 15 | 16 | - name: Install g++-multilib 17 | run: | 18 | sudo apt update 19 | sudo apt install g++-multilib -y 20 | - name: Build auxmos 21 | run: cargo build --target=i686-unknown-linux-gnu --release --features citadel_reactions 22 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check compile 2 | on: pull_request 3 | jobs: 4 | linux-build: 5 | runs-on: ubuntu-20.04 6 | steps: 7 | - uses: actions/checkout@v4 8 | - uses: actions-rs/toolchain@v1 9 | with: 10 | toolchain: stable 11 | target: i686-unknown-linux-gnu 12 | override: true 13 | 14 | - name: Install g++-multilib 15 | run: | 16 | sudo apt update 17 | sudo apt install g++-multilib -y 18 | - name: Check formatting 19 | run: cargo fmt --all -- --check 20 | - name: Check auxmos build 21 | run: cargo check --target=i686-unknown-linux-gnu --release --features citadel_reactions 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Putnam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rust-based atmospherics for Space Station 13 using [byondapi](https://github.com/spacestation13/byondapi-rs). 2 | 3 | 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). 4 | 5 | Binaries in releases are without these optimizations for compatibility. But it runs slower and you might still run into issues, in that case, please build the project yourself. 6 | 7 | You can build auxmos like any rust project, though you're gonna need `clang` version `6` or more installed. And `LIBCLANG_PATH` environment variable set to the bin path of clang in case of windows. Auxmos only supports `i686-unknown-linux-gnu` or `i686-pc-windows-msvc` targets on the build. 8 | 9 | Use `cargo t generate_binds` to generate the `bindings.dm` file to include in your codebase, for the byond to actually use the library, or use the one on the repository here (generated with feature `katmos`). 10 | 11 | The `master` branch is to be considered unstable; use the releases if you want to make sure it actually works. [The latest release is here](https://github.com/Putnam3145/auxmos/releases/latest). 12 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use nom::branch::alt; 2 | use nom::bytes::complete::tag; 3 | use nom::character::complete::alphanumeric1; 4 | use nom::combinator::recognize; 5 | use nom::multi::{many1_count, separated_list0}; 6 | use nom::number::complete::float; 7 | use nom::sequence::separated_pair; 8 | use nom::IResult; 9 | 10 | //parses gas id, must be an alphanumeric 11 | fn parse_gas_id(input: &str) -> IResult<&str, &str> { 12 | recognize(many1_count(alt((alphanumeric1, tag("_")))))(input) 13 | } 14 | 15 | //parses moles in floating point form 16 | fn parse_moles(input: &str) -> IResult<&str, f32> { 17 | float(input) 18 | } 19 | 20 | /// Parses gas strings, invalid patterns will be ignored 21 | /// E.g: "o2=2500;plasma=5000;TEMP=370" will return vec![("o2", 2500_f32), ("plasma", 5000_f32), ("TEMP", 370_f32)] 22 | pub(crate) fn parse_gas_string(input: &str) -> IResult<&str, Vec<(&str, f32)>> { 23 | separated_list0( 24 | tag(";"), 25 | separated_pair(parse_gas_id, tag("="), parse_moles), 26 | )(input) 27 | } 28 | 29 | #[test] 30 | fn test_parser() { 31 | let test_str = "o2=2500;plasma=5000;TEMP=370"; 32 | let result = parse_gas_string(test_str).unwrap(); 33 | 34 | assert_eq!( 35 | result, 36 | ( 37 | "", 38 | vec![("o2", 2500_f32), ("plasma", 5000_f32), ("TEMP", 370_f32)] 39 | ) 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /docs/AUXGM.md: -------------------------------------------------------------------------------- 1 | Auxgm is auxtools's way of consolidating gas info and allowing gases to be added at runtime. Its name is primarily a send-up of XGM, and... it mostly means the same thing, but it's subtly different. 2 | 3 | # Requirements 4 | 5 | Auxtools expects a global proc `/proc/_auxtools_register_gas(datum/gas/gas)` and a global variable called `gas_data`, with a list called `datums`. When `gas_data` is initialized, it should go through each individual gas datum and initialize them one-by-one, at the *very least* by adding an initialized gas variable to the `datums` list, but preferably using `_auxtools_register_gas`. Duplicated entries will overwrite the previous entry (as of 1.1.1). 6 | 7 | Individual gas datums require the following entries: 8 | 9 | `id`: string 10 | `name`: string 11 | `specific_heat`: number 12 | 13 | ...And that's it. The rest is optional. 14 | 15 | If you're using Auxmos's built-in tile-based atmos, auxgm also requires an `overlays` list, which is initialized alongside `datums`. This can be completely empty if you want and Auxmos will run happily. Citadel initializes it like so: 16 | 17 | ```dm 18 | if(gas.moles_visible) 19 | visibility[g] = gas.moles_visible 20 | overlays[g] = new /list(FACTOR_GAS_VISIBLE_MAX) 21 | for(var/i in 1 to FACTOR_GAS_VISIBLE_MAX) 22 | overlays[g][i] = new /obj/effect/overlay/gas(gas.gas_overlay, i * 255 / FACTOR_GAS_VISIBLE_MAX) 23 | else 24 | visibility[g] = 0 25 | overlays[g] = 0 26 | ``` 27 | 28 | Everything except the overlays lines are unnecessary. However, overlays does need to be organized as seen here, as auxgm handles gas overlays. 29 | 30 | If you're *not* using tile-based atmos, you're done; Auxmos's update_visuals is only called from a callback in auxgm's tile processing code and thus can be ignored. 31 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*"] 3 | 4 | [workspace.dependencies] 5 | byondapi = "0.4.11" 6 | coarsetime = "0.1.35" 7 | flume = "0.11.1" 8 | eyre = "0.6.12" 9 | tracing = "0.1.41" 10 | 11 | [package] 12 | name = "auxmos" 13 | version = "2.3.0" 14 | authors = ["Putnam "] 15 | edition = "2021" 16 | 17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 18 | 19 | [features] 20 | default = ["turf_processing", "katmos"] 21 | zas_hooks = [] 22 | turf_processing = [] 23 | superconductivity = ["turf_processing"] 24 | fastmos = ["turf_processing"] 25 | katmos = ["fastmos"] 26 | katmos_slow_decompression = ["fastmos"] 27 | reaction_hooks = [] 28 | citadel_reactions = ["reaction_hooks"] 29 | yogs_reactions = ["reaction_hooks"] 30 | 31 | # Tracing will expose this application to a local port, use with care 32 | tracy = ["dep:tracing-tracy", "dep:tracing-subscriber", "dep:tracing"] 33 | 34 | [lib] 35 | crate-type = ["cdylib"] 36 | 37 | [dependencies] 38 | byondapi = { workspace = true } 39 | flume = { workspace = true } 40 | coarsetime = { workspace = true } 41 | eyre = { workspace = true } 42 | auxcallback = { path = "./crates/auxcallback" } 43 | auxmacros = { path = "./crates/auxmacros" } 44 | itertools = "0.14.0" 45 | rayon = "1.10.0" 46 | float-ord = "0.3.2" 47 | parking_lot = "0.12.3" 48 | rustc-hash = "2.1.0" 49 | ahash = "0.8.11" 50 | indexmap = { version = "2.5.0", features = ["rayon"] } 51 | dashmap = { version = "6.1.0", features = ["rayon"] } 52 | hashbrown = "0.15.2" 53 | atomic_float = "1.1.0" 54 | petgraph = "0.7.0" 55 | bitflags = "2.6.0" 56 | nom = "7.1.3" 57 | mimalloc = { version = "0.1.43", default-features = false } 58 | 59 | tracing = { version = "0.1.41", optional = true } 60 | tracing-tracy = { version = "0.11.4", optional = true } 61 | tracing-subscriber = { version = "0.3.19", optional = true } 62 | 63 | [dependencies.tinyvec] 64 | version = "1.8.1" 65 | features = ["rustc_1_61", "alloc"] 66 | 67 | [profile.release] 68 | lto = 'fat' 69 | debug = true 70 | codegen-units = 1 71 | panic = 'abort' 72 | -------------------------------------------------------------------------------- /docs/FIRE.md: -------------------------------------------------------------------------------- 1 | Auxmos has its own system for fires, which tries to replicate many of the original features of plasma/trit fires that Citadel had at the time of implementation (circa 2019 TG, essentially) but is different in some key ways. Here are the important parts you need to know: 2 | 3 | # Disabling 4 | 5 | You can disable generic fires entirely by simply setting the reaction to `exclude = TRUE` *or* by compiling without the `generic_fires` hook, using the other hooks instead. 6 | 7 | # Balance concerns 8 | 9 | ## Fire danger 10 | 11 | The hottest fires using the "default" values for things--the one Citadel uses right now--is plasma fires, specifically ones that are *not* oxygen-rich. Trtiium generation sucks a lot of the heat out of it, but that heat is put right back in when the tritium itself burns. This is liable to be unintuitive to many, but it's... probably fine? At the heat scales we're talking, all fires are *essentially* identically dangerous anyway, i.e. "don't go there". Plasma fires are, I would say, hotter than they were before on average, but in oxygen-rich environments less hot, because trit fires were stupid. 12 | 13 | ## Tritium fires 14 | 15 | Tritium fires are now an ordinary kind of fire. They're hotter than hydrogen fires, if done on their own, and release radiation, but they don't have weird properties like eating all of the oxygen out of the air ASAP. In fact, they're the *least* oxygen-hungry fire now (tied with hydrogen), which should be considered. 16 | 17 | ## Bombs 18 | 19 | The strongest bombs are tritium. This has always been true, even though trit fires are less hot than plasma fires, because tritium has a very, very low heat capacity and trit fires are fuel rich. The new ideal bomb fuel mix is 2 : 1 tritium to oxygen. I have personally been able to get up to 5000 shockwave radius with such a bomb using hyper-noblium as the hot side; this is about equal to the old "normal" trit bomb. Balancing should be done accordingly. I changed the formula to have an explicit "baseline" where you get 50,000 points, a sharp dropoff before that point and asymptotically approaching 100,000 points as you get higher and higher, for this purpose. 20 | 21 | ## Heat cans 22 | 23 | The new ideal plasma/oxy ratio for maximum heat is 1.4 : 1. This is 58.333...% plasma. Not much else to say about that. 24 | 25 | ## Newly flammable gases 26 | 27 | You can make any gas flammable by just changing its properties. By default, on Citadel, nitrous oxide and nitrogen dioxide ("nitryl") can be used as oxidizers, and nitrogen is "flammable" at high temperatures (but the reaction actually *reduces* the total heat). Pluoxium is an oxidizer that reduces the fire's heat a good deal and eats a lot of the fuel. All of this is byond-side and can be taken or left as desired. 28 | 29 | ## Speed 30 | 31 | Auxmos is fast. Very, very fast. Generic fires are balanced for this; default plasma fires and trit fires are not. Auxmos will make fires seem to burn much, much hotter than normal. This is not actually the case: they're burning just as hot, they're just going way, way faster. There's a constant in auxmos that determines what the maximum amount of fuel that can be burned in a fire in one tick is: right now it's 20%, specifically to avoid fires burning too hot too fast. I'm not sure it's even enough, ha. 32 | -------------------------------------------------------------------------------- /crates/auxcallback/src/lib.rs: -------------------------------------------------------------------------------- 1 | use byondapi::prelude::*; 2 | use coarsetime::{Duration, Instant}; 3 | use eyre::Result; 4 | use std::convert::TryInto; 5 | 6 | type DeferredFunc = Box Result<()> + Send + Sync>; 7 | type CallbackChannel = (flume::Sender, flume::Receiver); 8 | pub type CallbackSender = flume::Sender; 9 | pub type CallbackReceiver = flume::Receiver; 10 | 11 | static CALLBACK_CHANNEL: std::sync::OnceLock = std::sync::OnceLock::new(); 12 | 13 | pub fn clean_callbacks() { 14 | if let Some((_, rx)) = CALLBACK_CHANNEL.get() { 15 | rx.drain().for_each(std::mem::drop) 16 | }; 17 | } 18 | 19 | fn with_callback_receiver(f: impl Fn(&flume::Receiver) -> T) -> T { 20 | f(&CALLBACK_CHANNEL.get_or_init(flume::unbounded).1) 21 | } 22 | 23 | /// This gives you a copy of the callback sender. Send to it with try_send or send, then later it'll be processed 24 | /// if one of the process_callbacks functions is called for any reason. 25 | pub fn byond_callback_sender() -> flume::Sender { 26 | CALLBACK_CHANNEL.get_or_init(flume::unbounded).0.clone() 27 | } 28 | 29 | /// Goes through every single outstanding callback and calls them. 30 | fn process_callbacks() { 31 | //let stack_trace = Proc::find("/proc/auxtools_stack_trace").unwrap(); 32 | with_callback_receiver(|receiver| { 33 | receiver 34 | .try_iter() 35 | .filter_map(|cb| cb().err()) 36 | .for_each(|e| { 37 | let error_string = format!("{e:?}").try_into().unwrap(); 38 | byondapi::global_call::call_global_id( 39 | byond_string!("byondapi_stack_trace"), 40 | &[error_string], 41 | ) 42 | .unwrap(); 43 | }) 44 | }) 45 | } 46 | 47 | /// Goes through every single outstanding callback and calls them, until a given time limit is reached. 48 | fn process_callbacks_for(duration: Duration) -> bool { 49 | let timer = Instant::now(); 50 | with_callback_receiver(|receiver| { 51 | for callback in receiver.try_iter() { 52 | if let Err(e) = callback() { 53 | let error_string = format!("{e:?}").try_into().unwrap(); 54 | byondapi::global_call::call_global_id( 55 | byond_string!("byondapi_stack_trace"), 56 | &[error_string], 57 | ) 58 | .unwrap(); 59 | } 60 | if timer.elapsed() >= duration { 61 | return true; 62 | } 63 | } 64 | false 65 | }) 66 | } 67 | 68 | /// Goes through every single outstanding callback and calls them, until a given time limit in milliseconds is reached. 69 | pub fn process_callbacks_for_millis(millis: u64) -> bool { 70 | process_callbacks_for(Duration::from_millis(millis)) 71 | } 72 | 73 | /// This function is to be called from byond, preferably once a tick. 74 | /// Calling with no arguments will process every outstanding callback. 75 | /// Calling with one argument will process the callbacks until a given time limit is reached. 76 | /// Time limit is in milliseconds. 77 | /// This has to be manually hooked in the code, e.g. 78 | /// ``` 79 | /// #[bind("/proc/process_atmos_callbacks")] 80 | /// fn atmos_callback_handle(remaining: ByondValue) { 81 | /// auxcallback::callback_processing_hook(remaining) 82 | /// } 83 | /// ``` 84 | 85 | pub fn callback_processing_hook(time_remaining: ByondValue) -> Result { 86 | if time_remaining.is_num() { 87 | let limit = time_remaining.get_number().unwrap() as u64; 88 | Ok(process_callbacks_for_millis(limit).into()) 89 | } else { 90 | process_callbacks(); 91 | Ok(ByondValue::null()) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/reaction/yogs.rs: -------------------------------------------------------------------------------- 1 | use crate::gas::{constants::*, gas_idx_from_string, with_mix, with_mix_mut}; 2 | use byondapi::prelude::*; 3 | use eyre::Result; 4 | 5 | #[must_use] 6 | pub fn func_from_id(id: &str) -> Option { 7 | match id { 8 | "plasmafire" => Some(plasma_fire), 9 | //"tritfire" => Some(tritium_fire), 10 | //"fusion" => Some(fusion), 11 | _ => None, 12 | } 13 | } 14 | 15 | const SUPER_SATURATION_THRESHOLD: f32 = 96.0; 16 | fn plasma_fire(byond_air: ByondValue, holder: ByondValue) -> Result { 17 | const PLASMA_UPPER_TEMPERATURE: f32 = 1370.0 + T0C; 18 | const OXYGEN_BURN_RATE_BASE: f32 = 1.4; 19 | const PLASMA_OXYGEN_FULLBURN: f32 = 10.0; 20 | const PLASMA_BURN_RATE_DELTA: f32 = 9.0; 21 | const FIRE_PLASMA_ENERGY_RELEASED: f32 = 3_000_000.0; 22 | let o2 = gas_idx_from_string(GAS_O2)?; 23 | let plasma = gas_idx_from_string(GAS_PLASMA)?; 24 | let co2 = gas_idx_from_string(GAS_CO2)?; 25 | let tritium = gas_idx_from_string(GAS_TRITIUM)?; 26 | let (oxygen_burn_rate, plasma_burn_rate, initial_oxy, initial_plasma, initial_energy) = 27 | with_mix(&byond_air, |air| { 28 | let temperature_scale = { 29 | if air.get_temperature() > PLASMA_UPPER_TEMPERATURE { 30 | 1.0 31 | } else { 32 | (air.get_temperature() - PLASMA_MINIMUM_BURN_TEMPERATURE) 33 | / (PLASMA_UPPER_TEMPERATURE - PLASMA_MINIMUM_BURN_TEMPERATURE) 34 | } 35 | }; 36 | if temperature_scale > 0.0 { 37 | let oxygen_burn_rate = OXYGEN_BURN_RATE_BASE - temperature_scale; 38 | let oxy = air.get_moles(o2); 39 | let plas = air.get_moles(plasma); 40 | let plasma_burn_rate = { 41 | if oxy > plas * PLASMA_OXYGEN_FULLBURN { 42 | plas * temperature_scale / PLASMA_BURN_RATE_DELTA 43 | } else { 44 | (temperature_scale * (oxy / PLASMA_OXYGEN_FULLBURN)) 45 | / PLASMA_BURN_RATE_DELTA 46 | } 47 | } 48 | .min(plas) 49 | .min(oxy / oxygen_burn_rate); 50 | Ok(( 51 | oxygen_burn_rate, 52 | plasma_burn_rate, 53 | oxy, 54 | plas, 55 | air.thermal_energy(), 56 | )) 57 | } else { 58 | Ok((0.0, -1.0, 0.0, 0.0, 0.0)) 59 | } 60 | })?; 61 | let fire_amount = plasma_burn_rate * (1.0 + oxygen_burn_rate); 62 | if fire_amount > 0.0 { 63 | let temperature = with_mix_mut(&byond_air, |air| { 64 | air.set_moles(plasma, initial_plasma - plasma_burn_rate); 65 | air.set_moles(o2, initial_oxy - (plasma_burn_rate * oxygen_burn_rate)); 66 | if initial_oxy / initial_plasma > SUPER_SATURATION_THRESHOLD { 67 | air.adjust_moles(tritium, plasma_burn_rate); 68 | } else { 69 | air.adjust_moles(co2, plasma_burn_rate); 70 | } 71 | let new_temp = (initial_energy + plasma_burn_rate * FIRE_PLASMA_ENERGY_RELEASED) 72 | / air.heat_capacity(); 73 | air.set_temperature(new_temp); 74 | air.garbage_collect(); 75 | Ok(new_temp) 76 | })?; 77 | let mut cached_results = byond_air.read_var_id(byond_string!("reaction_results"))?; 78 | cached_results.write_list_index("fire", fire_amount)?; 79 | if temperature > FIRE_MINIMUM_TEMPERATURE_TO_EXIST { 80 | byondapi::global_call::call_global_id( 81 | byond_string!("fire_expose"), 82 | &[holder, byond_air, temperature.into()], 83 | )?; 84 | } 85 | Ok(true.into()) 86 | } else { 87 | Ok(false.into()) 88 | } 89 | } 90 | /* 91 | fn tritium_fire(byond_air: ByondValue, holder: ByondValue) -> Result { 92 | Ok(ByondValue::new()) 93 | } 94 | 95 | fn fusion(byond_air: ByondValue, holder: ByondValue) -> Result { 96 | Ok(ByondValue::new()) 97 | } 98 | */ 99 | -------------------------------------------------------------------------------- /crates/auxmacros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, TokenStream}; 2 | use quote::quote; 3 | use syn::spanned::Spanned; 4 | 5 | fn strip_mut_and_filter(arg: &syn::FnArg) -> Option { 6 | let syn::FnArg::Typed(pattype) = arg else { 7 | return None; 8 | }; 9 | let mut ident_clone = pattype.clone(); 10 | 11 | match &mut *ident_clone.pat { 12 | syn::Pat::Ident(p) => { 13 | p.mutability = None; 14 | Some(syn::FnArg::Typed(ident_clone)) 15 | } 16 | syn::Pat::Tuple(tuple) => { 17 | tuple.elems.iter_mut().for_each(|item| { 18 | let syn::Pat::Ident(item) = item else { return }; 19 | item.mutability = None; 20 | }); 21 | Some(syn::FnArg::Typed(ident_clone)) 22 | } 23 | _ => Some(syn::FnArg::Typed(ident_clone)), 24 | } 25 | } 26 | 27 | /// This macros generates simd versions of functions as well as regular ones, 28 | /// allowing these functions to run in cpus without the required instructions. 29 | /// The specific simd feature used here is avx2. 30 | /// Example usage: 31 | /// ``` 32 | ///#[auxmacros::generate_simd_functions] 33 | ///#[byondapi::bind("/proc/process_atmos_callbacks")] 34 | ///fn atmos_callback_handle(remaining: ByondValue) -> Result { 35 | /// auxcallback::callback_processing_hook(remaining) 36 | ///} 37 | /// ``` 38 | #[proc_macro_attribute] 39 | pub fn generate_simd_functions( 40 | _: proc_macro::TokenStream, 41 | item: proc_macro::TokenStream, 42 | ) -> proc_macro::TokenStream { 43 | let input = syn::parse_macro_input!(item as syn::ItemFn); 44 | 45 | let attrs = input 46 | .attrs 47 | .into_iter() 48 | .map(|attr| quote! { #attr }) 49 | .collect::(); 50 | 51 | let func_name = &input.sig.ident; 52 | let func_name_disp = quote!(#func_name).to_string(); 53 | let func_name_simd = format!("{func_name_disp}_simd"); 54 | let func_ident_simd = Ident::new(&func_name_simd, func_name.span()); 55 | let func_name_fallback = format!("{func_name_disp}_fallback"); 56 | let func_ident_fallback = Ident::new(&func_name_fallback, func_name.span()); 57 | 58 | let args = &input.sig.inputs; 59 | let body = input.block; 60 | let func_return = input.sig.output; 61 | 62 | if let Some(recv) = args 63 | .iter() 64 | .find(|item| matches!(item, syn::FnArg::Receiver(_))) 65 | { 66 | return syn::Error::new(recv.span(), "Self is not supported!") 67 | .to_compile_error() 68 | .into(); 69 | } 70 | 71 | let args_nonmut = args 72 | .iter() 73 | .filter_map(strip_mut_and_filter) 74 | .map(|item| quote! {#item}) 75 | .collect::>(); 76 | 77 | let args_typeless = args 78 | .iter() 79 | .filter_map(strip_mut_and_filter) 80 | .filter_map(|arg| { 81 | let syn::FnArg::Typed(pattype) = arg else { 82 | return None; 83 | }; 84 | let pattype = &*pattype.pat; 85 | Some(quote! {#pattype}) 86 | }) 87 | .collect::>(); 88 | 89 | quote! { 90 | #attrs 91 | fn #func_name(#args_nonmut) #func_return { 92 | // This `unsafe` block is safe because we're testing 93 | // that the `avx2` feature is indeed available on our CPU. 94 | if *crate::_SIMD_DETECTED.get_or_init(|| is_x86_feature_detected!("avx2")) { 95 | unsafe { #func_ident_simd(#args_typeless) } 96 | } else { 97 | #func_ident_fallback(#args_typeless) 98 | } 99 | } 100 | 101 | #[target_feature(enable = "avx2")] 102 | unsafe fn #func_ident_simd(#args_nonmut) #func_return { 103 | #func_ident_fallback(#args_typeless) 104 | } 105 | 106 | #[inline(always)] 107 | fn #func_ident_fallback(#args) #func_return 108 | #body 109 | } 110 | .into() 111 | } 112 | -------------------------------------------------------------------------------- /docs/MIGRATING.md: -------------------------------------------------------------------------------- 1 | # 0.2 to 0.3 2 | 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. 4 | 5 | # 0.3 to 1.0 6 | 7 | New functions were added: 8 | 9 | 1. `/datum/gas_mixture/proc/adjust_moles_temp(gas_type, amt, temperature)` 10 | 2. `/datum/gas_mixture/proc/adjust_multi()` (it's variadic, of the form `gas1, amt1, gas2, amt2, ...`) 11 | 3. `/datum/gas_mixture/proc/add(amt)` 12 | 4. `/datum/gas_mixture/proc/subtract(amt)` 13 | 5. `/datum/gas_mixture/proc/multiply(factor)` 14 | 6. `/datum/gas_mixture/proc/divide(factor)` 15 | 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 16 | 8. `/datum/gas_mixture/proc/get_by_flag(flag)` 17 | 18 | There's also new feature flags: 19 | 20 | 1. `turf_processing`: on by default. Enables the hooks for turf processing, heat processing etc. Required for katmos, of course. 21 | 2. `zas_hooks`: Adds a `/datum/gas_mixture/proc/share_ratio(sharer, ratio, share_size, one_way = FALSE)` hook. 22 | 23 | Monstermos is now deprecated. Use katmos instead. It inherently has explosive decompression, sorry. 24 | 25 | `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. 26 | 27 | # 1.1 to 2.0 28 | 29 | 1. `equalization` feature flag renamed to `fastmos`, since that's what everyone calls it; if you used `equalization`, change it to `fastmos` 30 | 2. `monstermos`, `putnamos`, `explosive_decompression`, `putnamos_decompression` feature flags have been removed entirely (this is why it's 2.0.0); use `fastmos` or `katmos` instead. 31 | 3. `--package auxmos` needs to be added due to the repo restructuring. 32 | 4. New function: `/datum/gas_mixture/proc/__auxtools_parse_gas_string`. Call it from `parse_gas_string` with the string as the argument and it'll parse it much faster in Rust, which should reduce load times a bunch. 33 | 5. **Adjacencies list has been reworked**. Instead of being a bitfield reference to the direction of the adjacency, it is now flags. The flags should be ATMOS_ADJACENT_ANY = 1 and ATMOS_ADJACENT_FIRELOCK = 2. **Adjacency code must be rewritten with this in mind for auxmos to work.** You're going to have to do some weird stuff with firelock code. If you're not using katmos, you can get away with just replacing instances of `= dir` or similar in ImmediateCalculateAdjacentTurfs with `= 1`. **You will need both flags set for it to work when there's a firelock (1 | )** 34 | 35 | # 2.3 - 2.4.0 36 | 1. New hook `/datum/controller/subsystem/air/proc/equalize_turfs_auxtools` required for feature `katmos` 37 | 2. New hook `/datum/controller/subsystem/air/proc/process_excited_groups_auxtools` required for feature `processing` 38 | 3. `--package auxmos` flag is no longer needed 39 | 4. Flag ATMOS_ADJACENT_ANY = 1 is no longer needed to be set for the adjacent turf to be added 40 | 41 | 5. The following functions now only take one argument, `TICK_REMAINING_MS` of the master controller 42 | `/datum/controller/subsystem/air/proc/finish_turf_processing_auxtools()` 43 | `/datum/controller/subsystem/air/proc/equalize_turfs_auxtools()` 44 | `/datum/controller/subsystem/air/proc/process_turfs_auxtools()` 45 | `/datum/controller/subsystem/air/proc/post_process_turfs_auxtools()` 46 | `/datum/controller/subsystem/air/proc/process_turf_equalize_auxtools()` 47 | `/datum/controller/subsystem/air/proc/process_excited_groups_auxtools()` 48 | 49 | # 2.4.0 - 2.5.0 50 | 1. Generating a `bindings.dm` file is now needed, please generate it with `cargo t generate_binds` and include it in your repo, I personally include it in `__DEFINES` 51 | 2. Arguments that each binded function takes is now shown 52 | 3. `update_air_ref()` now requires a flag again, `-1` for closed turfs, `1` for planets, `2` for regular turfs 53 | 4. You need to call each process functions on the air subsystem for them to actually run 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build Release 2 | 3 | permissions: 4 | id-token: write 5 | attestations: write 6 | contents: write 7 | 8 | on: 9 | release: 10 | types: [published] 11 | 12 | jobs: 13 | release: 14 | name: Build and Release 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | include: 19 | - os: ubuntu-20.04 20 | target_name: i686-unknown-linux-gnu 21 | artifact_name: libauxmos.so 22 | - os: windows-latest 23 | target_name: i686-pc-windows-msvc 24 | artifact_name: auxmos.dll 25 | debug_pdb_name: auxmos.pdb 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: Setup Toolchains (Windows) 31 | uses: actions-rs/toolchain@v1 32 | with: 33 | toolchain: stable 34 | target: i686-pc-windows-msvc 35 | if: matrix.os == 'windows-latest' 36 | 37 | - name: Install g++ multilib 38 | run: | 39 | sudo apt-get update 40 | sudo apt install g++-multilib -y 41 | if: matrix.os == 'ubuntu-20.04' 42 | 43 | - name: Setup Toolchains (Ubuntu) 44 | uses: actions-rs/toolchain@v1 45 | with: 46 | toolchain: stable 47 | target: i686-unknown-linux-gnu 48 | if: matrix.os == 'ubuntu-20.04' 49 | 50 | - name: Build auxmos (Windows) 51 | uses: actions-rs/cargo@v1 52 | with: 53 | toolchain: stable 54 | command: build 55 | args: --target i686-pc-windows-msvc --release 56 | if: matrix.os == 'windows-latest' 57 | 58 | - name: Build auxmos (Ubuntu) 59 | uses: actions-rs/cargo@v1 60 | with: 61 | toolchain: stable 62 | command: build 63 | args: --target i686-unknown-linux-gnu --release 64 | if: matrix.os == 'ubuntu-20.04' 65 | 66 | - name: Create bindings (Ubuntu) 67 | uses: actions-rs/cargo@v1 68 | with: 69 | toolchain: stable 70 | command: test 71 | args: --target i686-unknown-linux-gnu --release --package auxmos --lib -- generate_binds --exact --show-output 72 | if: matrix.os == 'ubuntu-20.04' 73 | 74 | - name: Upload binary to release 75 | uses: svenstaro/upload-release-action@v1-release 76 | with: 77 | repo_token: ${{ secrets.GITHUB_TOKEN }} 78 | file: target/${{ matrix.target_name }}/release/${{ matrix.artifact_name }} 79 | asset_name: ${{ matrix.artifact_name }} 80 | tag: ${{ github.ref }} 81 | 82 | - name: Upload debug informations to release 83 | uses: svenstaro/upload-release-action@v1-release 84 | with: 85 | repo_token: ${{ secrets.GITHUB_TOKEN }} 86 | file: target/${{ matrix.target_name }}/release/${{ matrix.debug_pdb_name }} 87 | asset_name: ${{ matrix.debug_pdb_name }} 88 | tag: ${{ github.ref }} 89 | if: matrix.os == 'windows-latest' 90 | 91 | - name: Upload binding to release 92 | uses: svenstaro/upload-release-action@v1-release 93 | with: 94 | repo_token: ${{ secrets.GITHUB_TOKEN }} 95 | file: bindings.dm 96 | asset_name: bindings.dm 97 | tag: ${{ github.ref }} 98 | if: matrix.os == 'ubuntu-20.04' 99 | 100 | - name: Generate build provenance (Binaries) 101 | uses: actions/attest-build-provenance@v1 102 | with: 103 | subject-path: target/${{ matrix.target_name }}/release/${{ matrix.artifact_name }} 104 | 105 | - name: Generate build provenance (Debug information) 106 | uses: actions/attest-build-provenance@v1 107 | with: 108 | subject-path: target/${{ matrix.target_name }}/release/${{ matrix.debug_pdb_name }} 109 | if: matrix.os == 'windows-latest' 110 | 111 | - name: Generate build provenance (Bindings) 112 | uses: actions/attest-build-provenance@v1 113 | with: 114 | subject-path: bindings.dm 115 | if: matrix.os == 'ubuntu-20.04' 116 | -------------------------------------------------------------------------------- /src/turfs/groups.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use coarsetime::{Duration, Instant}; 3 | use parking_lot::{const_mutex, Mutex}; 4 | use std::collections::{BTreeSet, HashSet, VecDeque}; 5 | 6 | static GROUPS_CHANNEL: Mutex>> = const_mutex(None); 7 | 8 | pub fn flush_groups_channel() { 9 | *GROUPS_CHANNEL.lock() = None; 10 | } 11 | 12 | fn with_groups(f: impl Fn(Option>) -> T) -> T { 13 | f(GROUPS_CHANNEL.lock().take()) 14 | } 15 | 16 | pub fn send_to_groups(sent: BTreeSet) { 17 | GROUPS_CHANNEL.try_lock().map(|mut opt| opt.replace(sent)); 18 | } 19 | /// Returns: If this cycle is interrupted by overtiming or not. Starts a processing excited groups cycle, does nothing if process_turfs isn't ran. 20 | #[byondapi::bind("/datum/controller/subsystem/air/proc/process_excited_groups_auxtools")] 21 | fn groups_hook(mut src: ByondValue, remaining: ByondValue) -> Result { 22 | let group_pressure_goal = src 23 | .read_number_id(byond_string!("excited_group_pressure_goal")) 24 | .unwrap_or(0.5); 25 | let remaining_time = Duration::from_millis(remaining.get_number().unwrap_or(50.0) as u64); 26 | let start_time = Instant::now(); 27 | let (num_eq, is_cancelled) = with_groups(|thing| { 28 | if let Some(high_pressure_turfs) = thing { 29 | excited_group_processing( 30 | group_pressure_goal, 31 | high_pressure_turfs, 32 | (&start_time, remaining_time), 33 | ) 34 | } else { 35 | (0, false) 36 | } 37 | }); 38 | 39 | let bench = start_time.elapsed().as_millis(); 40 | let prev_cost = src 41 | .read_number_id(byond_string!("cost_groups")) 42 | .map_err(|_| eyre::eyre!("Attempt to interpret non-number value as number"))?; 43 | src.write_var_id( 44 | byond_string!("cost_groups"), 45 | &(0.8 * prev_cost + 0.2 * (bench as f32)).into(), 46 | )?; 47 | src.write_var_id( 48 | byond_string!("num_group_turfs_processed"), 49 | &(num_eq as f32).into(), 50 | )?; 51 | Ok(is_cancelled.into()) 52 | } 53 | 54 | // Finds small differences in turf pressures and equalizes them. 55 | #[cfg_attr(not(target_feature = "avx2"), auxmacros::generate_simd_functions)] 56 | #[cfg_attr(feature = "tracy", tracing::instrument(skip_all))] 57 | fn excited_group_processing( 58 | pressure_goal: f32, 59 | low_pressure_turfs: BTreeSet, 60 | (start_time, remaining_time): (&Instant, Duration), 61 | ) -> (usize, bool) { 62 | let mut found_turfs: HashSet = Default::default(); 63 | let mut is_cancelled = false; 64 | for initial_turf in low_pressure_turfs { 65 | if found_turfs.contains(&initial_turf) { 66 | continue; 67 | } 68 | 69 | if start_time.elapsed() >= remaining_time { 70 | is_cancelled = true; 71 | break; 72 | } 73 | 74 | with_turf_gases_read(|arena| { 75 | let Some(initial_mix_ref) = arena.get_from_id(initial_turf) else { 76 | return; 77 | }; 78 | if !initial_mix_ref.enabled() { 79 | return; 80 | } 81 | 82 | let mut border_turfs: VecDeque = VecDeque::with_capacity(40); 83 | let mut turfs: Vec<&TurfMixture> = Vec::with_capacity(200); 84 | let mut min_pressure = initial_mix_ref.return_pressure(); 85 | let mut max_pressure = min_pressure; 86 | let mut fully_mixed = Mixture::new(); 87 | 88 | border_turfs.push_back(initial_turf); 89 | found_turfs.insert(initial_turf); 90 | GasArena::with_all_mixtures(|all_mixtures| { 91 | loop { 92 | if turfs.len() >= 2500 { 93 | break; 94 | } 95 | if let Some(idx) = border_turfs.pop_front() { 96 | let Some(tmix) = arena.get_from_id(idx) else { 97 | break; 98 | }; 99 | if let Some(lock) = all_mixtures.get(tmix.mix) { 100 | let mix = lock.read(); 101 | let pressure = mix.return_pressure(); 102 | let this_max = max_pressure.max(pressure); 103 | let this_min = min_pressure.min(pressure); 104 | if (this_max - this_min).abs() >= pressure_goal { 105 | continue; 106 | } 107 | min_pressure = this_min; 108 | max_pressure = this_max; 109 | turfs.push(tmix); 110 | fully_mixed.merge(&mix); 111 | fully_mixed.volume += mix.volume; 112 | arena 113 | .adjacent_turf_ids(arena.get_id(idx).unwrap()) 114 | .filter(|&loc| found_turfs.insert(loc)) 115 | .filter(|&loc| { 116 | arena.get_from_id(loc).filter(|b| b.enabled()).is_some() 117 | }) 118 | .for_each(|loc| border_turfs.push_back(loc)); 119 | } 120 | } else { 121 | break; 122 | } 123 | } 124 | fully_mixed.multiply(1.0 / turfs.len() as f32); 125 | if !fully_mixed.is_corrupt() { 126 | turfs 127 | .par_iter() 128 | .filter_map(|turf| all_mixtures.get(turf.mix)) 129 | .for_each(|mix_lock| mix_lock.write().copy_from_mutable(&fully_mixed)); 130 | } 131 | }); 132 | }); 133 | } 134 | (found_turfs.len(), is_cancelled) 135 | } 136 | -------------------------------------------------------------------------------- /src/reaction.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "citadel_reactions")] 2 | mod citadel; 3 | 4 | #[cfg(feature = "yogs_reactions")] 5 | mod yogs; 6 | 7 | use crate::gas::{gas_idx_to_id, total_num_gases, GasIDX, Mixture}; 8 | use byondapi::prelude::*; 9 | use eyre::{Context, Result}; 10 | use float_ord::FloatOrd; 11 | use hashbrown::HashMap; 12 | use rustc_hash::FxBuildHasher; 13 | use std::{ 14 | cell::RefCell, 15 | hash::{Hash, Hasher}, 16 | }; 17 | 18 | pub type ReactionPriority = FloatOrd; 19 | pub type ReactionIdentifier = u64; 20 | 21 | #[derive(Clone, Debug)] 22 | pub struct Reaction { 23 | id: ReactionIdentifier, 24 | priority: ReactionPriority, 25 | min_temp_req: Option, 26 | max_temp_req: Option, 27 | min_ener_req: Option, 28 | min_fire_req: Option, 29 | min_gas_reqs: Vec<(GasIDX, f32)>, 30 | } 31 | 32 | type ReactFunc = fn(ByondValue, ByondValue) -> Result; 33 | 34 | enum ReactionSide { 35 | ByondSide(ByondValue), 36 | RustSide(ReactFunc), 37 | } 38 | 39 | thread_local! { 40 | static REACTION_VALUES: RefCell> = Default::default(); 41 | } 42 | 43 | /// Runs a reaction given a `ReactionIdentifier`. Returns the result of the reaction, error or success. 44 | /// # Errors 45 | /// If the reaction itself has a runtime. 46 | pub fn react_by_id( 47 | id: ReactionIdentifier, 48 | src: ByondValue, 49 | holder: ByondValue, 50 | ) -> Result { 51 | REACTION_VALUES.with_borrow(|r| { 52 | r.get(&id).map_or_else( 53 | || Err(eyre::eyre!("Reaction with invalid id")), 54 | |reaction| match reaction { 55 | ReactionSide::ByondSide(val) => val 56 | .call_id(byond_string!("react"), &[src, holder]) 57 | .wrap_err("calling byond side react in react_by_id"), 58 | ReactionSide::RustSide(func) => { 59 | func(src, holder).wrap_err("calling rust side react in react_by_id") 60 | } 61 | }, 62 | ) 63 | }) 64 | } 65 | 66 | impl Reaction { 67 | /// Takes a `/datum/gas_reaction` and makes a byond reaction out of it. 68 | pub fn from_byond_reaction(reaction: ByondValue) -> Result { 69 | let priority = FloatOrd( 70 | reaction 71 | .read_number_id(byond_string!("priority")) 72 | .map_err(|_| eyre::eyre!("Reaction priority must be a number!"))?, 73 | ); 74 | let string_id = reaction 75 | .read_string_id(byond_string!("id")) 76 | .map_err(|_| eyre::eyre!("Reaction id must be a string!"))?; 77 | let func = { 78 | #[cfg(feature = "citadel_reactions")] 79 | { 80 | citadel::func_from_id(string_id.as_str()) 81 | } 82 | #[cfg(feature = "yogs_reactions")] 83 | { 84 | yogs::func_from_id(string_id.as_str()) 85 | } 86 | #[cfg(not(feature = "reaction_hooks"))] 87 | { 88 | None 89 | } 90 | }; 91 | 92 | let id = { 93 | let mut state = rustc_hash::FxHasher::default(); 94 | string_id.as_bytes().hash(&mut state); 95 | state.finish() 96 | }; 97 | 98 | let our_reaction = { 99 | if let Some(min_reqs) = reaction 100 | .read_var_id(byond_string!("min_requirements")) 101 | .map_or(None, |value| value.is_list().then_some(value)) 102 | { 103 | let mut min_gas_reqs: Vec<(GasIDX, f32)> = Vec::new(); 104 | for i in 0..total_num_gases() { 105 | if let Ok(req_amount) = min_reqs 106 | .read_list_index(gas_idx_to_id(i)) 107 | .and_then(|v| v.get_number()) 108 | { 109 | min_gas_reqs.push((i, req_amount)); 110 | } 111 | } 112 | let min_temp_req = min_reqs 113 | .read_list_index("TEMP") 114 | .ok() 115 | .and_then(|item| item.get_number().ok()); 116 | let max_temp_req = min_reqs 117 | .read_list_index("MAX_TEMP") 118 | .ok() 119 | .and_then(|item| item.get_number().ok()); 120 | let min_ener_req = min_reqs 121 | .read_list_index("ENER") 122 | .ok() 123 | .and_then(|item| item.get_number().ok()); 124 | let min_fire_req = min_reqs 125 | .read_list_index("FIRE_REAGENTS") 126 | .ok() 127 | .and_then(|item| item.get_number().ok()); 128 | Ok(Reaction { 129 | id, 130 | priority, 131 | min_temp_req, 132 | max_temp_req, 133 | min_ener_req, 134 | min_fire_req, 135 | min_gas_reqs, 136 | }) 137 | } else { 138 | Err(eyre::eyre!(format!( 139 | "Reaction {string_id} doesn't have a gas requirements list!" 140 | ))) 141 | } 142 | }?; 143 | 144 | REACTION_VALUES.with_borrow_mut(|reaction_map| -> Result<()> { 145 | match func { 146 | Some(function) => { 147 | reaction_map.insert(our_reaction.id, ReactionSide::RustSide(function)) 148 | } 149 | None => reaction_map.insert(our_reaction.id, ReactionSide::ByondSide(reaction)), 150 | }; 151 | Ok(()) 152 | })?; 153 | Ok(our_reaction) 154 | } 155 | /// Gets the reaction's identifier. 156 | #[must_use] 157 | pub fn get_id(&self) -> ReactionIdentifier { 158 | self.id 159 | } 160 | /// Checks if the given gas mixture can react with this reaction. 161 | pub fn check_conditions(&self, mix: &Mixture) -> bool { 162 | self.min_temp_req 163 | .map_or(true, |temp_req| mix.get_temperature() >= temp_req) 164 | && self 165 | .max_temp_req 166 | .map_or(true, |temp_req| mix.get_temperature() <= temp_req) 167 | && self 168 | .min_gas_reqs 169 | .iter() 170 | .all(|&(k, v)| mix.get_moles(k) >= v) 171 | && self 172 | .min_ener_req 173 | .map_or(true, |ener_req| mix.thermal_energy() >= ener_req) 174 | && self.min_fire_req.map_or(true, |fire_req| { 175 | let (oxi, fuel) = mix.get_burnability(); 176 | oxi.min(fuel) >= fire_req 177 | }) 178 | } 179 | /// Returns the priority of the reaction. 180 | #[must_use] 181 | pub fn get_priority(&self) -> ReactionPriority { 182 | self.priority 183 | } 184 | /// Calls the reaction with the given arguments. 185 | /// # Errors 186 | /// If the reaction itself has a runtime error, this will propagate it up. 187 | pub fn react(&self, src: ByondValue, holder: ByondValue) -> Result { 188 | react_by_id(self.id, src, holder) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/gas/constants.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | 3 | /// kPa*L/(K*mol) 4 | pub const R_IDEAL_GAS_EQUATION: f32 = 8.31; 5 | /// kPa 6 | pub const ONE_ATMOSPHERE: f32 = 101.325; 7 | /// -270.3degC 8 | pub const TCMB: f32 = 2.7; 9 | /// -48.15degC 10 | pub const TCRYO: f32 = 225.0; 11 | /// 0degC 12 | pub const T0C: f32 = 273.15; 13 | /// 20degC 14 | pub const T20C: f32 = 293.15; 15 | /// Amount of gas below which any amounts will be truncated to 0. 16 | pub const GAS_MIN_MOLES: f32 = 0.0001; 17 | /// Heat capacities below which heat will be considered 0. 18 | pub const MINIMUM_HEAT_CAPACITY: f32 = 0.0003; 19 | 20 | /// liters in a cell 21 | pub const CELL_VOLUME: f32 = 2500.0; 22 | /// moles in a 2.5 m^3 cell at 101.325 Pa and 20 degC 23 | pub const MOLES_CELLSTANDARD: f32 = ONE_ATMOSPHERE * CELL_VOLUME / (T20C * R_IDEAL_GAS_EQUATION); 24 | /// compared against for superconductivity 25 | pub const M_CELL_WITH_RATIO: f32 = MOLES_CELLSTANDARD * 0.005; 26 | /// percentage of oxygen in a normal mixture of air 27 | pub const O2STANDARD: f32 = 0.21; 28 | /// same but for nitrogen 29 | pub const N2STANDARD: f32 = 0.79; 30 | /// O2 standard value (21%) 31 | pub const MOLES_O2STANDARD: f32 = MOLES_CELLSTANDARD * O2STANDARD; 32 | /// N2 standard value (79%) 33 | pub const MOLES_N2STANDARD: f32 = MOLES_CELLSTANDARD * N2STANDARD; 34 | /// liters in a normal breath 35 | pub const BREATH_VOLUME: f32 = 0.5; 36 | /// Amount of air to take a from a tile 37 | pub const BREATH_PERCENTAGE: f32 = BREATH_VOLUME / CELL_VOLUME; 38 | 39 | /// EXCITED GROUPS 40 | 41 | /// number of FULL air controller ticks before an excited group breaks down (averages gas contents across turfs) 42 | pub const EXCITED_GROUP_BREAKDOWN_CYCLES: i32 = 4; 43 | /// number of FULL air controller ticks before an excited group dismantles and removes its turfs from active 44 | pub const EXCITED_GROUP_DISMANTLE_CYCLES: i32 = 16; 45 | 46 | /// Ratio of air that must move to/from a tile to reset group processing 47 | pub const MINIMUM_AIR_RATIO_TO_SUSPEND: f32 = 0.1; 48 | /// Minimum ratio of air that must move to/from a tile 49 | pub const MINIMUM_AIR_RATIO_TO_MOVE: f32 = 0.001; 50 | /// Minimum amount of air that has to move before a group processing can be suspended 51 | pub const MINIMUM_AIR_TO_SUSPEND: f32 = MOLES_CELLSTANDARD * MINIMUM_AIR_RATIO_TO_SUSPEND; 52 | /// Either this must be active 53 | pub const MINIMUM_MOLES_DELTA_TO_MOVE: f32 = MOLES_CELLSTANDARD * MINIMUM_AIR_RATIO_TO_MOVE; 54 | /// or this (or both, obviously) 55 | pub const MINIMUM_TEMPERATURE_TO_MOVE: f32 = T20C + 100.0; 56 | /// Minimum temperature difference before group processing is suspended 57 | pub const MINIMUM_TEMPERATURE_DELTA_TO_SUSPEND: f32 = 4.0; 58 | /// Minimum temperature difference before the gas temperatures are just set to be equal 59 | pub const MINIMUM_TEMPERATURE_DELTA_TO_CONSIDER: f32 = 0.5; 60 | pub const MINIMUM_TEMPERATURE_FOR_SUPERCONDUCTION: f32 = T20C + 10.0; 61 | pub const MINIMUM_TEMPERATURE_START_SUPERCONDUCTION: f32 = T20C + 200.0; 62 | 63 | /// The amount of gas that is diffused between tiles every tick. Must be less than 1/6. 64 | pub const GAS_DIFFUSION_CONSTANT: f32 = 0.125; 65 | 66 | /// This number minus the number of adjacent turfs is how much the original gas needs to be multiplied by to represent loss by diffusion 67 | pub const GAS_LOSS_CONSTANT: f32 = 1.0 / GAS_DIFFUSION_CONSTANT; 68 | 69 | /// HEAT TRANSFER COEFFICIENTS 70 | 71 | /// Must be between 0 and 1. Values closer to 1 equalize temperature faster 72 | 73 | /// Should not exceed 0.4 else the algorithm will diverge 74 | 75 | pub const WALL_HEAT_TRANSFER_COEFFICIENT: f32 = 0.0; 76 | pub const OPEN_HEAT_TRANSFER_COEFFICIENT: f32 = 0.4; 77 | /// a hack for now 78 | pub const WINDOW_HEAT_TRANSFER_COEFFICIENT: f32 = 0.1; 79 | /// a hack to help make vacuums "cold", sacrificing realism for gameplay 80 | pub const HEAT_CAPACITY_VACUUM: f32 = 7000.0; 81 | 82 | /// The Stefan-Boltzmann constant. M T^-3 Θ^-4 83 | pub const STEFAN_BOLTZMANN_CONSTANT: f64 = 5.670_373e-08; // watts/(meter^2*kelvin^4) 84 | 85 | const SPACE_TEMP: f64 = TCMB as f64; 86 | 87 | /// How much power is coming in from space per square meter. M T^-3 88 | pub const RADIATION_FROM_SPACE: f64 = 89 | STEFAN_BOLTZMANN_CONSTANT * SPACE_TEMP * SPACE_TEMP * SPACE_TEMP * SPACE_TEMP; // watts/meter^2 90 | 91 | /// FIRE 92 | 93 | pub const FIRE_MINIMUM_TEMPERATURE_TO_SPREAD: f32 = 150.0 + T0C; 94 | pub const FIRE_MINIMUM_TEMPERATURE_TO_EXIST: f32 = 100.0 + T0C; 95 | pub const FIRE_SPREAD_RADIOSITY_SCALE: f32 = 0.85; 96 | /// For small fires 97 | pub const FIRE_GROWTH_RATE: f32 = 40000.0; 98 | pub const PLASMA_MINIMUM_BURN_TEMPERATURE: f32 = 100.0 + T0C; 99 | pub const PLASMA_UPPER_TEMPERATURE: f32 = 1370.0 + T0C; 100 | pub const PLASMA_OXYGEN_FULLBURN: f32 = 10.0; 101 | pub const FIRE_MAXIMUM_BURN_RATE: f32 = 0.2; 102 | 103 | /// GASES 104 | 105 | pub const MIN_TOXIC_GAS_DAMAGE: i32 = 1; 106 | pub const MAX_TOXIC_GAS_DAMAGE: i32 = 10; 107 | /// Moles in a standard cell after which gases are visible 108 | pub const MOLES_GAS_VISIBLE: f32 = 0.25; 109 | 110 | /// `moles_visible` * `FACTOR_GAS_VISIBLE_MAX` = Moles after which gas is at maximum visibility 111 | pub const FACTOR_GAS_VISIBLE_MAX: f32 = 20.0; 112 | /// Mole step for alpha updates. This means alpha can update at 0.25, 0.5, 0.75 and so on 113 | pub const MOLES_GAS_VISIBLE_STEP: f32 = 0.25; 114 | 115 | /// REACTIONS 116 | 117 | // Maximum amount of ReactionIdentifiers in the TinyVec that all_reactions returns. 118 | // We can't guarantee the max number of reactions that will ever be registered, 119 | // so this is here to prevent that from getting out of control. 120 | // TinyVec is used mostly to prevent too much heap stuff from going on, since there can be a LOT of reactions going. 121 | // ReactionIdentifier is 12 bytes, so this can be pretty generous. 122 | pub const MAX_REACTION_TINYVEC_SIZE: usize = 32; 123 | 124 | bitflags! { 125 | /// return values for reactions (bitflags) 126 | pub struct ReactionReturn: u32 { 127 | const NO_REACTION = 0b0; 128 | const REACTING = 0b1; 129 | const STOP_REACTIONS = 0b10; 130 | } 131 | } 132 | 133 | pub const GAS_O2: &str = "o2"; 134 | pub const GAS_N2: &str = "n2"; 135 | pub const GAS_CO2: &str = "co2"; 136 | pub const GAS_PLASMA: &str = "plasma"; 137 | pub const GAS_H2O: &str = "water_vapor"; 138 | pub const GAS_HYPERNOB: &str = "nob"; 139 | pub const GAS_NITROUS: &str = "n2o"; 140 | pub const GAS_NITRYL: &str = "no2"; 141 | pub const GAS_TRITIUM: &str = "tritium"; 142 | pub const GAS_BZ: &str = "bz"; 143 | pub const GAS_STIMULUM: &str = "stim"; 144 | pub const GAS_PLUOXIUM: &str = "pluox"; 145 | pub const GAS_MIASMA: &str = "miasma"; 146 | pub const GAS_METHANE: &str = "methane"; 147 | pub const GAS_METHYL_BROMIDE: &str = "methyl_bromide"; 148 | -------------------------------------------------------------------------------- /src/turfs/putnamos.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | use std::collections::VecDeque; 4 | 5 | use std::collections::{BTreeMap, BTreeSet}; 6 | 7 | use std::cell::Cell; 8 | 9 | use auxcallback::byond_callback_sender; 10 | 11 | const OPP_DIR_INDEX: [u8; 7] = [1, 0, 3, 2, 5, 4, 6]; 12 | 13 | // If you can't tell, this is mostly a massively simplified copy of monstermos. 14 | 15 | fn explosively_depressurize( 16 | turf_idx: TurfID, 17 | turf: TurfMixture, 18 | equalize_hard_turf_limit: usize, 19 | max_x: i32, 20 | max_y: i32, 21 | ) -> Result { 22 | let mut turfs: Vec<(TurfID, TurfMixture)> = Vec::new(); 23 | let mut space_turfs: Vec<(TurfID, TurfMixture)> = Vec::new(); 24 | turfs.push((turf_idx, turf)); 25 | let mut warned_about_planet_atmos = false; 26 | let mut cur_queue_idx = 0; 27 | while cur_queue_idx < turfs.len() { 28 | let (i, m) = turfs[cur_queue_idx]; 29 | let actual_turf = unsafe { ByondValue::turf_by_id_unchecked(i) }; 30 | cur_queue_idx += 1; 31 | if m.planetary_atmos.is_some() { 32 | warned_about_planet_atmos = true; 33 | continue; 34 | } 35 | if m.is_immutable() { 36 | space_turfs.push((i, m)); 37 | actual_turf.set("pressure_specific_target", &actual_turf)?; 38 | } else { 39 | if cur_queue_idx > equalize_hard_turf_limit { 40 | continue; 41 | } 42 | for (j, loc) in adjacent_tile_ids(m.adjacency, i, max_x, max_y) { 43 | actual_turf.call( 44 | "consider_firelocks", 45 | &[&unsafe { ByondValue::turf_by_id_unchecked(loc) }], 46 | )?; 47 | if let Some(new_m) = turf_gases().get(&i) { 48 | let bit = 1 << j; 49 | if new_m.adjacency & bit == bit { 50 | if let Some(adj) = turf_gases().get(&loc) { 51 | let (&adj_i, &adj_m) = (adj.key(), adj.value()); 52 | turfs.push((adj_i, adj_m)); 53 | } 54 | } 55 | } 56 | } 57 | } 58 | if warned_about_planet_atmos { 59 | return Ok(ByondValue::null()); // planet atmos > space 60 | } 61 | } 62 | let mut progression_order: Vec<(TurfID, TurfMixture)> = Vec::with_capacity(space_turfs.len()); 63 | let mut adjacency_info: BTreeMap> = BTreeMap::new(); 64 | for (i, m) in space_turfs.iter() { 65 | progression_order.push((*i, *m)); 66 | adjacency_info.insert(*i, Cell::new((6, 0.0))); 67 | } 68 | cur_queue_idx = 0; 69 | while cur_queue_idx < progression_order.len() { 70 | let (i, m) = progression_order[cur_queue_idx]; 71 | let actual_turf = unsafe { ByondValue::turf_by_id_unchecked(i) }; 72 | for (j, loc) in adjacent_tile_ids(m.adjacency, i, max_x, max_y) { 73 | if let Some(adj) = turf_gases().get(&loc) { 74 | let (adj_i, adj_m) = (*adj.key(), adj.value()); 75 | if !adjacency_info.contains_key(&adj_i) && !adj_m.is_immutable() { 76 | adjacency_info.insert(i, Cell::new((OPP_DIR_INDEX[j as usize], 0.0))); 77 | unsafe { ByondValue::turf_by_id_unchecked(adj_i) } 78 | .set("pressure_specific_target", &actual_turf)?; 79 | progression_order.push((adj_i, *adj_m)); 80 | } 81 | } 82 | } 83 | cur_queue_idx += 1; 84 | } 85 | let hpd = auxtools::ByondValue::globals() 86 | .get("SSAir")? 87 | .get_list("high_pressure_delta") 88 | .map_err(|_| { 89 | eyre::eyre!( 90 | "Attempt to interpret non-list value as list {} {}:{}", 91 | std::file!(), 92 | std::line!(), 93 | std::column!() 94 | ) 95 | })?; 96 | for (i, m) in progression_order.iter().rev() { 97 | let cur_orig = adjacency_info.get(i).unwrap(); 98 | let mut cur_info = cur_orig.get(); 99 | if cur_info.0 == 6 { 100 | continue; 101 | } 102 | let actual_turf = unsafe { ByondValue::turf_by_id_unchecked(*i) }; 103 | hpd.set(&actual_turf, 1.0)?; 104 | let loc = adjacent_tile_id(cur_info.0, *i, max_x, max_y); 105 | if let Some(adj) = turf_gases().get(&loc) { 106 | let (adj_i, adj_m) = (*adj.key(), adj.value()); 107 | let adj_orig = adjacency_info.get(&adj_i).unwrap(); 108 | let mut adj_info = adj_orig.get(); 109 | let sum = adj_m.total_moles(); 110 | cur_info.1 += sum; 111 | adj_info.1 += cur_info.1; 112 | if adj_info.0 != 6 { 113 | let adj_turf = unsafe { ByondValue::turf_by_id_unchecked(adj_i) }; 114 | adj_turf.set("pressure_difference", cur_info.1)?; 115 | adj_turf.set("pressure_direction", (1 << cur_info.0) as f32)?; 116 | } 117 | m.clear_air(); 118 | actual_turf.set("pressure_difference", cur_info.1)?; 119 | actual_turf.set("pressure_direction", (1 << cur_info.0) as f32)?; 120 | actual_turf.call("handle decompression floor rip", &[&ByondValue::from(sum)])?; 121 | adj_orig.set(adj_info); 122 | cur_orig.set(cur_info); 123 | } 124 | } 125 | Ok(ByondValue::null()) 126 | } 127 | 128 | // Just floodfills to lower-pressure turfs until it can't find any more. 129 | 130 | #[deprecated( 131 | note = "Katmos should be used intead of Monstermos or Putnamos, as that one is actively maintained." 132 | )] 133 | pub fn equalize( 134 | equalize_turf_limit: usize, 135 | equalize_hard_turf_limit: usize, 136 | max_x: i32, 137 | max_y: i32, 138 | high_pressure_turfs: &BTreeSet, 139 | ) -> usize { 140 | let sender = byond_callback_sender(); 141 | let mut turfs_processed = 0; 142 | let mut merger = Mixture::new(); 143 | let mut found_turfs: BTreeSet = BTreeSet::new(); 144 | 'turf_loop: for &initial_idx in high_pressure_turfs.iter() { 145 | if let Some(initial_turf) = turf_gases().get(&initial_idx) { 146 | let mut turfs: Vec<(TurfID, TurfMixture, TurfID, f32)> = 147 | Vec::with_capacity(equalize_turf_limit); 148 | let mut border_turfs: VecDeque<(TurfID, TurfMixture, TurfID, f32)> = 149 | VecDeque::with_capacity(equalize_turf_limit); 150 | merger.clear_with_vol(0.0); 151 | border_turfs.push_back((initial_idx, *initial_turf, initial_idx, 0.0)); 152 | found_turfs.insert(initial_idx); 153 | if GasArena::with_all_mixtures(|all_mixtures| { 154 | // floodfill 155 | while !border_turfs.is_empty() && turfs.len() < equalize_turf_limit { 156 | let (cur_idx, cur_turf, parent_turf, pressure_delta) = 157 | border_turfs.pop_front().unwrap(); 158 | if let Some(our_gas_entry) = all_mixtures.get(cur_turf.mix) { 159 | let gas = our_gas_entry.read(); 160 | merger.merge(&gas); 161 | merger.volume += gas.volume; 162 | turfs.push((cur_idx, cur_turf, parent_turf, pressure_delta)); 163 | if !gas.is_immutable() { 164 | for (_, loc) in 165 | adjacent_tile_ids(cur_turf.adjacency, cur_idx, max_x, max_y) 166 | { 167 | if found_turfs.contains(&loc) { 168 | continue; 169 | } 170 | found_turfs.insert(loc); 171 | if let Some(adj_turf) = turf_gases().get(&loc) { 172 | if cfg!(feature = "putnamos_decompression") 173 | && adj_turf.is_immutable() 174 | { 175 | let _ = sender.try_send(Box::new(move || { 176 | explosively_depressurize( 177 | cur_idx, 178 | cur_turf, 179 | equalize_hard_turf_limit, 180 | max_x, 181 | max_y, 182 | ) 183 | })); 184 | return true; 185 | } else { 186 | let delta = 187 | adj_turf.return_pressure() - merger.return_pressure(); 188 | if delta < 0.0 { 189 | border_turfs.push_back(( 190 | loc, 191 | *adj_turf.value(), 192 | cur_idx, 193 | -delta, 194 | )); 195 | } 196 | } 197 | } 198 | } 199 | } 200 | } 201 | } 202 | false 203 | }) || turfs.len() == 1 204 | { 205 | continue 'turf_loop; 206 | } 207 | merger.multiply(1.0 / turfs.len() as f32); 208 | turfs_processed += turfs.len(); 209 | let to_send = GasArena::with_all_mixtures(|all_mixtures| { 210 | turfs 211 | .par_iter() 212 | .with_min_len(50) 213 | .map(|(cur_idx, cur_turf, parent_turf, pressure_delta)| { 214 | if let Some(entry) = all_mixtures.get(cur_turf.mix) { 215 | let gas: &mut Mixture = &mut entry.write(); 216 | gas.copy_from_mutable(&merger); 217 | } 218 | (*cur_idx, *parent_turf, *pressure_delta) 219 | }) 220 | .collect::>() 221 | }); 222 | for chunk_prelude in to_send.chunks(20) { 223 | let chunk: Vec<_> = chunk_prelude.iter().copied().collect(); 224 | let _ = sender.try_send(Box::new(move || { 225 | for &(idx, parent, delta) in chunk.iter() { 226 | if parent != 0 { 227 | let turf = unsafe { ByondValue::turf_by_id_unchecked(idx) }; 228 | let enemy_turf = unsafe { ByondValue::turf_by_id_unchecked(parent) }; 229 | enemy_turf.call( 230 | "consider_pressure_difference", 231 | &[&turf, &ByondValue::from(delta)], 232 | )?; 233 | } 234 | } 235 | Ok(ByondValue::null()) 236 | })); 237 | } 238 | } 239 | } 240 | turfs_processed 241 | } 242 | -------------------------------------------------------------------------------- /src/gas.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | pub mod constants; 3 | pub mod mixture; 4 | pub mod types; 5 | 6 | use byondapi::prelude::*; 7 | use eyre::Result; 8 | pub use mixture::Mixture; 9 | use parking_lot::{const_rwlock, RwLock}; 10 | pub use types::*; 11 | 12 | pub type GasIDX = usize; 13 | 14 | /// A static container, with a bunch of helper functions for accessing global data. It's horrible, I know, but video games. 15 | pub struct GasArena {} 16 | 17 | /* 18 | This is where the gases live. 19 | This is just a big vector, acting as a gas mixture pool. 20 | As you can see, it can be accessed by any thread at any time; 21 | of course, it has a RwLock preventing this, and you can't access the 22 | vector directly. Seriously, please don't. I have the wrapper functions for a reason. 23 | */ 24 | static GAS_MIXTURES: RwLock>>> = const_rwlock(None); 25 | 26 | static NEXT_GAS_IDS: RwLock>> = const_rwlock(None); 27 | 28 | #[byondapi::init] 29 | pub fn initialize_gases() { 30 | *GAS_MIXTURES.write() = Some(Vec::with_capacity(240_000)); 31 | *NEXT_GAS_IDS.write() = Some(Vec::with_capacity(2000)); 32 | } 33 | 34 | pub fn shut_down_gases() { 35 | crate::turfs::wait_for_tasks(); 36 | GAS_MIXTURES.write().as_mut().unwrap().clear(); 37 | NEXT_GAS_IDS.write().as_mut().unwrap().clear(); 38 | } 39 | 40 | impl GasArena { 41 | /// Locks the gas arena and and runs the given closure with it locked. 42 | /// # Panics 43 | /// if `GAS_MIXTURES` hasn't been initialized, somehow. 44 | pub fn with_all_mixtures(f: F) -> T 45 | where 46 | F: FnOnce(&[RwLock]) -> T, 47 | { 48 | f(GAS_MIXTURES.read().as_ref().unwrap()) 49 | } 50 | 51 | /// Locks the gas arena and and runs the given closure with it locked, fails if it can't acquire a lock in 30ms. 52 | /// # Panics 53 | /// if `GAS_MIXTURES` hasn't been initialized, somehow. 54 | pub fn with_all_mixtures_fallible(f: F) -> T 55 | where 56 | F: FnOnce(Option<&[RwLock]>) -> T, 57 | { 58 | let gases = GAS_MIXTURES.try_read_for(std::time::Duration::from_millis(30)); 59 | f(gases.as_ref().unwrap().as_ref().map(|vec| vec.as_slice())) 60 | } 61 | /// Read locks the given gas mixture and runs the given closure on it. 62 | /// # Errors 63 | /// If no such gas mixture exists or the closure itself errors. 64 | /// # Panics 65 | /// if `GAS_MIXTURES` hasn't been initialized, somehow. 66 | pub fn with_gas_mixture(id: usize, f: F) -> Result 67 | where 68 | F: FnOnce(&Mixture) -> Result, 69 | { 70 | let lock = GAS_MIXTURES.read(); 71 | let gas_mixtures = lock.as_ref().unwrap(); 72 | let mix = gas_mixtures 73 | .get(id) 74 | .ok_or_else(|| eyre::eyre!("No gas mixture with ID {id} exists!"))? 75 | .read(); 76 | f(&mix) 77 | } 78 | /// Write locks the given gas mixture and runs the given closure on it. 79 | /// # Errors 80 | /// If no such gas mixture exists or the closure itself errors. 81 | /// # Panics 82 | /// if `GAS_MIXTURES` hasn't been initialized, somehow. 83 | pub fn with_gas_mixture_mut(id: usize, f: F) -> Result 84 | where 85 | F: FnOnce(&mut Mixture) -> Result, 86 | { 87 | let lock = GAS_MIXTURES.read(); 88 | let gas_mixtures = lock.as_ref().unwrap(); 89 | let mut mix = gas_mixtures 90 | .get(id) 91 | .ok_or_else(|| eyre::eyre!("No gas mixture with ID {id} exists!"))? 92 | .write(); 93 | f(&mut mix) 94 | } 95 | /// Read locks the given gas mixtures and runs the given closure on them. 96 | /// # Errors 97 | /// If no such gas mixture exists or the closure itself errors. 98 | /// # Panics 99 | /// if `GAS_MIXTURES` hasn't been initialized, somehow. 100 | pub fn with_gas_mixtures(src: usize, arg: usize, f: F) -> Result 101 | where 102 | F: FnOnce(&Mixture, &Mixture) -> Result, 103 | { 104 | let lock = GAS_MIXTURES.read(); 105 | let gas_mixtures = lock.as_ref().unwrap(); 106 | let src_gas = gas_mixtures 107 | .get(src) 108 | .ok_or_else(|| eyre::eyre!("No gas mixture with ID {src} exists!"))? 109 | .read(); 110 | let arg_gas = gas_mixtures 111 | .get(arg) 112 | .ok_or_else(|| eyre::eyre!("No gas mixture with ID {arg} exists!"))? 113 | .read(); 114 | f(&src_gas, &arg_gas) 115 | } 116 | /// Locks the given gas mixtures and runs the given closure on them. 117 | /// # Errors 118 | /// If no such gas mixture exists or the closure itself errors. 119 | /// # Panics 120 | /// if `GAS_MIXTURES` hasn't been initialized, somehow. 121 | pub fn with_gas_mixtures_mut(src: usize, arg: usize, f: F) -> Result 122 | where 123 | F: FnOnce(&mut Mixture, &mut Mixture) -> Result, 124 | { 125 | let lock = GAS_MIXTURES.read(); 126 | let gas_mixtures = lock.as_ref().unwrap(); 127 | if src == arg { 128 | let mut entry = gas_mixtures 129 | .get(src) 130 | .ok_or_else(|| eyre::eyre!("No gas mixture with ID {src} exists!"))? 131 | .write(); 132 | let mix = &mut entry; 133 | let mut copied = mix.clone(); 134 | f(mix, &mut copied) 135 | } else { 136 | f( 137 | &mut gas_mixtures 138 | .get(src) 139 | .ok_or_else(|| eyre::eyre!("No gas mixture with ID {src} exists!"))? 140 | .write(), 141 | &mut gas_mixtures 142 | .get(arg) 143 | .ok_or_else(|| eyre::eyre!("No gas mixture with ID {arg} exists!"))? 144 | .write(), 145 | ) 146 | } 147 | } 148 | /// Runs the given closure on the gas mixture *locks* rather than an already-locked version. 149 | /// # Errors 150 | /// If no such gas mixture exists or the closure itself errors. 151 | /// # Panics 152 | /// if `GAS_MIXTURES` hasn't been initialized, somehow. 153 | fn with_gas_mixtures_custom(src: usize, arg: usize, f: F) -> Result 154 | where 155 | F: FnOnce(&RwLock, &RwLock) -> Result, 156 | { 157 | let lock = GAS_MIXTURES.read(); 158 | let gas_mixtures = lock.as_ref().unwrap(); 159 | if src == arg { 160 | let entry = gas_mixtures 161 | .get(src) 162 | .ok_or_else(|| eyre::eyre!("No gas mixture with ID {src} exists!"))?; 163 | let gas_copy = entry.read().clone(); 164 | f(entry, &RwLock::new(gas_copy)) 165 | } else { 166 | f( 167 | gas_mixtures 168 | .get(src) 169 | .ok_or_else(|| eyre::eyre!("No gas mixture with ID {src} exists!"))?, 170 | gas_mixtures 171 | .get(arg) 172 | .ok_or_else(|| eyre::eyre!("No gas mixture with ID {arg} exists!"))?, 173 | ) 174 | } 175 | } 176 | /// Fills in the first unused slot in the gas mixtures vector, or adds another one, then sets the argument ByondValue to point to it. 177 | /// # Errors 178 | /// If `initial_volume` is incorrect or `_extools_pointer_gasmixture` doesn't exist, somehow. 179 | /// # Panics 180 | /// If not called from the main thread 181 | /// If `NEXT_GAS_IDS` is not initialized, somehow. 182 | pub fn register_mix(mut mix: ByondValue) -> Result { 183 | let init_volume = mix.read_number_id(byond_string!("initial_volume"))?; 184 | if NEXT_GAS_IDS.read().as_ref().unwrap().is_empty() { 185 | let mut gas_lock = GAS_MIXTURES.write(); 186 | let gas_mixtures = gas_lock.as_mut().unwrap(); 187 | let next_idx = gas_mixtures.len(); 188 | gas_mixtures.push(RwLock::new(Mixture::from_vol(init_volume))); 189 | 190 | mix.write_var_id( 191 | byond_string!("_extools_pointer_gasmixture"), 192 | &(next_idx as f32).into(), 193 | ) 194 | .unwrap(); 195 | 196 | let mut ids_lock = NEXT_GAS_IDS.write(); 197 | let cur_last = gas_mixtures.len(); 198 | let next_gas_ids = ids_lock.as_mut().unwrap(); 199 | let cap = { 200 | let to_cap = gas_mixtures.capacity() - cur_last; 201 | if to_cap == 0 { 202 | next_gas_ids.capacity() - 100 203 | } else { 204 | (next_gas_ids.capacity() - 100).min(to_cap) 205 | } 206 | }; 207 | next_gas_ids.extend(cur_last..(cur_last + cap)); 208 | gas_mixtures.resize_with(cur_last + cap, Default::default); 209 | } else { 210 | let idx = { 211 | let mut next_gas_ids = NEXT_GAS_IDS.write(); 212 | next_gas_ids.as_mut().unwrap().pop().unwrap() 213 | }; 214 | GAS_MIXTURES 215 | .read() 216 | .as_ref() 217 | .unwrap() 218 | .get(idx) 219 | .unwrap() 220 | .write() 221 | .clear_with_vol(init_volume); 222 | mix.write_var_id( 223 | byond_string!("_extools_pointer_gasmixture"), 224 | &(idx as f32).into(), 225 | ) 226 | .unwrap(); 227 | } 228 | Ok(ByondValue::null()) 229 | } 230 | /// Marks the ByondValue's gas mixture as unused, allowing it to be reallocated to another. 231 | /// # Panics 232 | /// If not called from the main thread 233 | /// If `NEXT_GAS_IDS` hasn't been initialized, somehow. 234 | pub fn unregister_mix(mix: &ByondValue) { 235 | if let Ok(idx) = mix.read_number_id(byond_string!("_extools_pointer_gasmixture")) { 236 | let mut next_gas_ids = NEXT_GAS_IDS.write(); 237 | next_gas_ids.as_mut().unwrap().push(idx as usize); 238 | } else { 239 | panic!("Tried to unregister uninitialized mix") 240 | } 241 | } 242 | } 243 | 244 | /// Gets the mix for the given value, and calls the provided closure with a reference to that mix as an argument. 245 | /// # Errors 246 | /// If a gasmixture ID is not a number or the callback returns an error. 247 | pub fn with_mix(mix: &ByondValue, f: F) -> Result 248 | where 249 | F: FnOnce(&Mixture) -> Result, 250 | { 251 | GasArena::with_gas_mixture( 252 | mix.read_number_id(byond_string!("_extools_pointer_gasmixture"))? as usize, 253 | f, 254 | ) 255 | } 256 | 257 | /// As `with_mix`, but mutable. 258 | /// # Errors 259 | /// If a gasmixture ID is not a number or the callback returns an error. 260 | pub fn with_mix_mut(mix: &ByondValue, f: F) -> Result 261 | where 262 | F: FnOnce(&mut Mixture) -> Result, 263 | { 264 | GasArena::with_gas_mixture_mut( 265 | mix.read_number_id(byond_string!("_extools_pointer_gasmixture"))? as usize, 266 | f, 267 | ) 268 | } 269 | 270 | /// As `with_mix`, but with two mixes. 271 | /// # Errors 272 | /// If a gasmixture ID is not a number or the callback returns an error. 273 | pub fn with_mixes(src_mix: &ByondValue, arg_mix: &ByondValue, f: F) -> Result 274 | where 275 | F: FnOnce(&Mixture, &Mixture) -> Result, 276 | { 277 | GasArena::with_gas_mixtures( 278 | src_mix.read_number_id(byond_string!("_extools_pointer_gasmixture"))? as usize, 279 | arg_mix.read_number_id(byond_string!("_extools_pointer_gasmixture"))? as usize, 280 | f, 281 | ) 282 | } 283 | 284 | /// As `with_mix_mut`, but with two mixes. 285 | /// # Errors 286 | /// If a gasmixture ID is not a number or the callback returns an error. 287 | pub fn with_mixes_mut(src_mix: &ByondValue, arg_mix: &ByondValue, f: F) -> Result 288 | where 289 | F: FnOnce(&mut Mixture, &mut Mixture) -> Result, 290 | { 291 | GasArena::with_gas_mixtures_mut( 292 | src_mix.read_number_id(byond_string!("_extools_pointer_gasmixture"))? as usize, 293 | arg_mix.read_number_id(byond_string!("_extools_pointer_gasmixture"))? as usize, 294 | f, 295 | ) 296 | } 297 | 298 | /// Allows different lock levels for each gas. Instead of relevant refs to the gases, returns the `RWLock` object. 299 | /// # Errors 300 | /// If a gasmixture ID is not a number or the callback returns an error. 301 | pub fn with_mixes_custom(src_mix: &ByondValue, arg_mix: &ByondValue, f: F) -> Result 302 | where 303 | F: FnMut(&RwLock, &RwLock) -> Result, 304 | { 305 | GasArena::with_gas_mixtures_custom( 306 | src_mix.read_number_id(byond_string!("_extools_pointer_gasmixture"))? as usize, 307 | arg_mix.read_number_id(byond_string!("_extools_pointer_gasmixture"))? as usize, 308 | f, 309 | ) 310 | } 311 | 312 | /// Gets the amount of gases that are active in byond. 313 | /// # Panics 314 | /// if `GAS_MIXTURES` hasn't been initialized, somehow. 315 | pub fn amt_gases() -> usize { 316 | GAS_MIXTURES.read().as_ref().unwrap().len() - NEXT_GAS_IDS.read().as_ref().unwrap().len() 317 | } 318 | 319 | /// Gets the amount of gases that are allocated, but not necessarily active in byond. 320 | /// # Panics 321 | /// if `GAS_MIXTURES` hasn't been initialized, somehow. 322 | pub fn tot_gases() -> usize { 323 | GAS_MIXTURES.read().as_ref().unwrap().len() 324 | } 325 | -------------------------------------------------------------------------------- /src/reaction/citadel.rs: -------------------------------------------------------------------------------- 1 | use crate::gas::{ 2 | constants::*, gas_fusion_power, gas_idx_from_string, with_gas_info, with_mix, with_mix_mut, 3 | FireProductInfo, GasIDX, 4 | }; 5 | use byondapi::prelude::*; 6 | use eyre::Result; 7 | 8 | const SUPER_SATURATION_THRESHOLD: f32 = 96.0; 9 | 10 | #[must_use] 11 | pub fn func_from_id(id: &str) -> Option { 12 | match id { 13 | "plasmafire" => Some(plasma_fire), 14 | "tritfire" => Some(tritium_fire), 15 | "fusion" => Some(fusion), 16 | "genericfire" => Some(generic_fire), 17 | _ => None, 18 | } 19 | } 20 | 21 | fn plasma_fire(byond_air: ByondValue, holder: ByondValue) -> Result { 22 | const PLASMA_UPPER_TEMPERATURE: f32 = 1390.0 + T0C; 23 | const OXYGEN_BURN_RATE_BASE: f32 = 1.4; 24 | const PLASMA_OXYGEN_FULLBURN: f32 = 10.0; 25 | const PLASMA_BURN_RATE_DELTA: f32 = 9.0; 26 | const FIRE_PLASMA_ENERGY_RELEASED: f32 = 3_000_000.0; 27 | let o2 = gas_idx_from_string(GAS_O2)?; 28 | let plasma = gas_idx_from_string(GAS_PLASMA)?; 29 | let co2 = gas_idx_from_string(GAS_CO2)?; 30 | let tritium = gas_idx_from_string(GAS_TRITIUM)?; 31 | let (oxygen_burn_rate, plasma_burn_rate, initial_oxy, initial_plasma, initial_energy) = 32 | with_mix(&byond_air, |air| { 33 | let temperature_scale = { 34 | if air.get_temperature() > PLASMA_UPPER_TEMPERATURE { 35 | 1.0 36 | } else { 37 | (air.get_temperature() - FIRE_MINIMUM_TEMPERATURE_TO_EXIST) 38 | / (PLASMA_UPPER_TEMPERATURE - FIRE_MINIMUM_TEMPERATURE_TO_EXIST) 39 | } 40 | }; 41 | if temperature_scale > 0.0 { 42 | let oxygen_burn_rate = OXYGEN_BURN_RATE_BASE - temperature_scale; 43 | let oxy = air.get_moles(o2); 44 | let plas = air.get_moles(plasma); 45 | let plasma_burn_rate = { 46 | if oxy > plas * PLASMA_OXYGEN_FULLBURN { 47 | plas * temperature_scale / PLASMA_BURN_RATE_DELTA 48 | } else { 49 | (temperature_scale * (oxy / PLASMA_OXYGEN_FULLBURN)) 50 | / PLASMA_BURN_RATE_DELTA 51 | } 52 | } 53 | .min(plas) 54 | .min(oxy / oxygen_burn_rate); 55 | Ok(( 56 | oxygen_burn_rate, 57 | plasma_burn_rate, 58 | oxy, 59 | plas, 60 | air.thermal_energy(), 61 | )) 62 | } else { 63 | Ok((0.0, -1.0, 0.0, 0.0, 0.0)) 64 | } 65 | })?; 66 | let fire_amount = plasma_burn_rate * (1.0 + oxygen_burn_rate); 67 | if fire_amount > 0.0 { 68 | let temperature = with_mix_mut(&byond_air, |air| { 69 | air.set_moles(plasma, initial_plasma - plasma_burn_rate); 70 | air.set_moles(o2, initial_oxy - (plasma_burn_rate * oxygen_burn_rate)); 71 | if initial_oxy / initial_plasma > SUPER_SATURATION_THRESHOLD { 72 | air.adjust_moles(tritium, plasma_burn_rate); 73 | } else { 74 | air.adjust_moles(co2, plasma_burn_rate); 75 | } 76 | let new_temp = (initial_energy + plasma_burn_rate * FIRE_PLASMA_ENERGY_RELEASED) 77 | / air.heat_capacity(); 78 | air.set_temperature(new_temp); 79 | air.garbage_collect(); 80 | Ok(new_temp) 81 | })?; 82 | let mut cached_results = byond_air.read_var_id(byond_string!("reaction_results"))?; 83 | cached_results.write_list_index("fire", fire_amount)?; 84 | if temperature > FIRE_MINIMUM_TEMPERATURE_TO_EXIST { 85 | byondapi::global_call::call_global_id( 86 | byond_string!("fire_expose"), 87 | &[holder, byond_air, temperature.into()], 88 | )?; 89 | } 90 | Ok(true.into()) 91 | } else { 92 | Ok(false.into()) 93 | } 94 | } 95 | 96 | fn tritium_fire(byond_air: ByondValue, holder: ByondValue) -> Result { 97 | const TRITIUM_BURN_OXY_FACTOR: f32 = 100.0; 98 | const TRITIUM_BURN_TRIT_FACTOR: f32 = 10.0; 99 | const TRITIUM_MINIMUM_RADIATION_FACTOR: f32 = 0.1; 100 | const FIRE_HYDROGEN_ENERGY_RELEASED: f32 = 280_000.0; 101 | let o2 = gas_idx_from_string(GAS_O2)?; 102 | let tritium = gas_idx_from_string(GAS_TRITIUM)?; 103 | let water = gas_idx_from_string(GAS_H2O)?; 104 | let (burned_fuel, energy_released, temperature) = with_mix_mut(&byond_air, |air| { 105 | let initial_oxy = air.get_moles(o2); 106 | let initial_trit = air.get_moles(tritium); 107 | let initial_energy = air.thermal_energy(); 108 | let burned_fuel = { 109 | if initial_oxy < initial_trit { 110 | let r = initial_oxy / TRITIUM_BURN_OXY_FACTOR; 111 | air.set_moles(tritium, initial_trit - r); 112 | r 113 | } else { 114 | // yes, we set burned_fuel to trit times ten. times ten!! and then the actual amount burned is 1% of that. 115 | // this is why trit bombs are Like That. 116 | let r = initial_trit * TRITIUM_BURN_TRIT_FACTOR; 117 | air.set_moles( 118 | tritium, 119 | initial_trit - initial_trit / TRITIUM_BURN_TRIT_FACTOR, 120 | ); 121 | air.set_moles(o2, initial_oxy - initial_trit); 122 | r 123 | } 124 | }; 125 | air.adjust_moles(water, burned_fuel / TRITIUM_BURN_OXY_FACTOR); 126 | let energy_released = FIRE_HYDROGEN_ENERGY_RELEASED * burned_fuel; 127 | let new_temp = (initial_energy + energy_released) / air.heat_capacity(); 128 | let mut cached_results = byond_air.read_var_id(byond_string!("reaction_results"))?; 129 | cached_results.write_list_index("fire", burned_fuel)?; 130 | air.set_temperature(new_temp); 131 | air.garbage_collect(); 132 | Ok((burned_fuel, energy_released, new_temp)) 133 | })?; 134 | if burned_fuel > TRITIUM_MINIMUM_RADIATION_FACTOR { 135 | byondapi::global_call::call_global_id( 136 | byond_string!("radiation_burn"), 137 | &[holder, energy_released.into()], 138 | )?; 139 | } 140 | if temperature > FIRE_MINIMUM_TEMPERATURE_TO_EXIST { 141 | byondapi::global_call::call_global_id( 142 | byond_string!("fire_expose"), 143 | &[holder, byond_air, temperature.into()], 144 | )?; 145 | } 146 | Ok(true.into()) 147 | } 148 | 149 | fn fusion(byond_air: ByondValue, holder: ByondValue) -> Result { 150 | const TOROID_CALCULATED_THRESHOLD: f32 = 5.96; // changing it by 0.1 generally doubles or halves fusion temps 151 | const INSTABILITY_GAS_POWER_FACTOR: f32 = 3.0; 152 | const PLASMA_BINDING_ENERGY: f32 = 20_000_000.0; 153 | const FUSION_TRITIUM_MOLES_USED: f32 = 1.0; 154 | const FUSION_INSTABILITY_ENDOTHERMALITY: f32 = 2.0; 155 | const FUSION_TRITIUM_CONVERSION_COEFFICIENT: f32 = 0.002; 156 | const FUSION_MOLE_THRESHOLD: f32 = 250.0; 157 | const FUSION_SCALE_DIVISOR: f32 = 10.0; // Used to be Pi 158 | const FUSION_MINIMAL_SCALE: f32 = 50.0; 159 | const FUSION_SLOPE_DIVISOR: f32 = 1250.0; // This number is probably the safest number to change 160 | const FUSION_ENERGY_TRANSLATION_EXPONENT: f32 = 1.25; // This number is probably the most dangerous number to change 161 | const FUSION_BASE_TEMPSCALE: f32 = 6.0; // This number is responsible for orchestrating fusion temperatures 162 | const FUSION_MIDDLE_ENERGY_REFERENCE: f32 = 1E+6; // This number is deceptively dangerous; sort of tied to TOROID_CALCULATED_THRESHOLD 163 | const FUSION_BUFFER_DIVISOR: f32 = 1.0; // Increase this to cull unrobust fusions faster 164 | const INFINITY: f32 = 1E+30; // Well, infinity in byond 165 | let plas = gas_idx_from_string(GAS_PLASMA)?; 166 | let co2 = gas_idx_from_string(GAS_CO2)?; 167 | let trit = gas_idx_from_string(GAS_TRITIUM)?; 168 | let h2o = gas_idx_from_string(GAS_H2O)?; 169 | let bz = gas_idx_from_string(GAS_BZ)?; 170 | let o2 = gas_idx_from_string(GAS_O2)?; 171 | let ( 172 | initial_energy, 173 | initial_plasma, 174 | initial_carbon, 175 | scale_factor, 176 | temperature_scale, 177 | gas_power, 178 | ) = with_mix(&byond_air, |air| { 179 | Ok(( 180 | air.thermal_energy(), 181 | air.get_moles(plas), 182 | air.get_moles(co2), 183 | (air.volume / FUSION_SCALE_DIVISOR).max(FUSION_MINIMAL_SCALE), 184 | air.get_temperature().log10(), 185 | air.enumerate() 186 | .fold(0.0, |acc, (i, amt)| acc + gas_fusion_power(&i) * amt), 187 | )) 188 | })?; 189 | //The size of the phase space hypertorus 190 | let toroidal_size = TOROID_CALCULATED_THRESHOLD + { 191 | if temperature_scale <= FUSION_BASE_TEMPSCALE { 192 | (temperature_scale - FUSION_BASE_TEMPSCALE) / FUSION_BUFFER_DIVISOR 193 | } else { 194 | (4.0_f32.powf(temperature_scale - FUSION_BASE_TEMPSCALE)) / FUSION_SLOPE_DIVISOR 195 | } 196 | }; 197 | let instability = (gas_power * INSTABILITY_GAS_POWER_FACTOR).rem_euclid(toroidal_size); 198 | byond_air.call_id(byond_string!("set_analyzer_results"), &[instability.into()])?; 199 | let mut thermal_energy = initial_energy; 200 | 201 | //We have to scale the amounts of carbon and plasma down a significant amount in order to show the chaotic dynamics we want 202 | let mut plasma = (initial_plasma - FUSION_MOLE_THRESHOLD) / scale_factor; 203 | //We also subtract out the threshold amount to make it harder for fusion to burn itself out. 204 | let mut carbon = (initial_carbon - FUSION_MOLE_THRESHOLD) / scale_factor; 205 | 206 | //count the rings. ss13's modulus is positive, this ain't, who knew 207 | plasma = (plasma - instability * carbon.sin()).rem_euclid(toroidal_size); 208 | carbon = (carbon - plasma).rem_euclid(toroidal_size); 209 | 210 | //Scales the gases back up 211 | plasma = plasma * scale_factor + FUSION_MOLE_THRESHOLD; 212 | carbon = carbon * scale_factor + FUSION_MOLE_THRESHOLD; 213 | 214 | let delta_plasma = (initial_plasma - plasma).min(toroidal_size * scale_factor * 1.5); 215 | 216 | //Energy is gained or lost corresponding to the creation or destruction of mass. 217 | //Low instability prevents endothermality while higher instability acutally encourages it. 218 | //Reaction energy can be negative or positive, for both exothermic and endothermic reactions. 219 | let reaction_energy = { 220 | if (delta_plasma > 0.0) || (instability <= FUSION_INSTABILITY_ENDOTHERMALITY) { 221 | (delta_plasma * PLASMA_BINDING_ENERGY).max(0.0) 222 | } else { 223 | delta_plasma 224 | * PLASMA_BINDING_ENERGY 225 | * ((instability - FUSION_INSTABILITY_ENDOTHERMALITY).sqrt()) 226 | } 227 | }; 228 | 229 | //To achieve faster equilibrium. Too bad it is not that good at cooling down. 230 | if reaction_energy != 0.0 { 231 | let middle_energy = (((TOROID_CALCULATED_THRESHOLD / 2.0) * scale_factor) 232 | + FUSION_MOLE_THRESHOLD) 233 | * (200.0 * FUSION_MIDDLE_ENERGY_REFERENCE); 234 | thermal_energy = middle_energy 235 | * FUSION_ENERGY_TRANSLATION_EXPONENT.powf((thermal_energy / middle_energy).log10()); 236 | //This bowdlerization is a double-edged sword. Tread with care! 237 | let bowdlerized_reaction_energy = reaction_energy.clamp( 238 | thermal_energy * ((1.0 / (FUSION_ENERGY_TRANSLATION_EXPONENT.powi(2))) - 1.0), 239 | thermal_energy * (FUSION_ENERGY_TRANSLATION_EXPONENT.powi(2) - 1.0), 240 | ); 241 | thermal_energy = middle_energy 242 | * 10_f32.powf( 243 | ((thermal_energy + bowdlerized_reaction_energy) / middle_energy) 244 | .log(FUSION_ENERGY_TRANSLATION_EXPONENT), 245 | ); 246 | }; 247 | 248 | //The decay of the tritium and the reaction's energy produces waste gases, different ones depending on whether the reaction is endo or exothermic 249 | let standard_waste_gas_output = 250 | scale_factor * (FUSION_TRITIUM_CONVERSION_COEFFICIENT * FUSION_TRITIUM_MOLES_USED); 251 | 252 | let standard_energy = with_mix_mut(&byond_air, |air| { 253 | air.set_moles(plas, plasma); 254 | air.set_moles(co2, carbon); 255 | 256 | //The reason why you should set up a tritium production line. 257 | air.adjust_moles(trit, -FUSION_TRITIUM_MOLES_USED); 258 | 259 | //Adds waste products 260 | if delta_plasma > 0.0 { 261 | air.adjust_moles(h2o, standard_waste_gas_output); 262 | } else { 263 | air.adjust_moles(bz, standard_waste_gas_output); 264 | } 265 | air.adjust_moles(o2, standard_waste_gas_output); //Oxygen is a bit touchy subject 266 | 267 | let new_heat_cap = air.heat_capacity(); 268 | let standard_energy = 400_f32 * air.get_moles(plas) * air.get_temperature(); //Prevents putting meaningless waste gases to achieve high rads. 269 | 270 | //Change the temperature 271 | if new_heat_cap > MINIMUM_HEAT_CAPACITY 272 | && (reaction_energy != 0.0 || instability <= FUSION_INSTABILITY_ENDOTHERMALITY) 273 | { 274 | air.set_temperature((thermal_energy / new_heat_cap).clamp(TCMB, INFINITY)); 275 | } 276 | 277 | air.garbage_collect(); 278 | Ok(standard_energy) 279 | })?; 280 | if reaction_energy != 0.0 { 281 | byondapi::global_call::call_global_id( 282 | byond_string!("fusion_ball"), 283 | &[holder, reaction_energy.into(), standard_energy.into()], 284 | )?; 285 | Ok(true.into()) 286 | } else if reaction_energy == 0.0 && instability <= FUSION_INSTABILITY_ENDOTHERMALITY { 287 | Ok(true.into()) 288 | } else { 289 | Ok(false.into()) 290 | } 291 | } 292 | 293 | fn generic_fire(byond_air: ByondValue, holder: ByondValue) -> Result { 294 | use hashbrown::HashMap; 295 | use rustc_hash::FxBuildHasher; 296 | let mut burn_results: HashMap = HashMap::with_capacity_and_hasher( 297 | super::total_num_gases() as usize, 298 | FxBuildHasher::default(), 299 | ); 300 | let mut radiation_released = 0.0; 301 | with_gas_info(|gas_info| { 302 | if let Some(fire_amount) = with_mix(&byond_air, |air| { 303 | let (mut fuels, mut oxidizers) = air.get_fire_info_with_lock(gas_info); 304 | let oxidation_power = oxidizers 305 | .iter() 306 | .copied() 307 | .fold(0.0, |acc, (_, _, power)| acc + power); 308 | let total_fuel = fuels 309 | .iter() 310 | .copied() 311 | .fold(0.0, |acc, (_, _, power)| acc + power); 312 | if oxidation_power < GAS_MIN_MOLES { 313 | Err(eyre::eyre!( 314 | "Gas has no oxidizer even though it passed oxidizer check!" 315 | )) 316 | } else if total_fuel <= GAS_MIN_MOLES { 317 | Err(eyre::eyre!( 318 | "Gas has no fuel even though it passed fuel check!" 319 | )) 320 | } else { 321 | let oxidation_ratio = oxidation_power / total_fuel; 322 | if oxidation_ratio > 1.0 { 323 | for (_, amt, power) in &mut oxidizers { 324 | *amt /= oxidation_ratio; 325 | *power /= oxidation_ratio; 326 | } 327 | } else { 328 | for (_, amt, power) in &mut fuels { 329 | *amt *= oxidation_ratio; 330 | *power *= oxidation_ratio; 331 | } 332 | } 333 | for (i, a, _) in oxidizers.iter().copied().chain(fuels.iter().copied()) { 334 | let amt = FIRE_MAXIMUM_BURN_RATE * a; 335 | let this_gas_info = &gas_info[i]; 336 | radiation_released += amt * this_gas_info.fire_radiation_released; 337 | if let Some(product_info) = this_gas_info.fire_products.as_ref() { 338 | match product_info { 339 | FireProductInfo::Generic(products) => { 340 | for (product_idx, product_amt) in products.iter() { 341 | burn_results 342 | .entry(product_idx.get()?) 343 | .and_modify(|r| *r += product_amt * amt) 344 | .or_insert_with(|| product_amt * amt); 345 | } 346 | } 347 | FireProductInfo::Plasma => { 348 | let product = if oxidation_ratio > SUPER_SATURATION_THRESHOLD { 349 | GAS_TRITIUM 350 | } else { 351 | GAS_CO2 352 | }; 353 | burn_results 354 | .entry(gas_idx_from_string(product)?) 355 | .and_modify(|r| *r += amt) 356 | .or_insert_with(|| amt); 357 | } 358 | } 359 | } 360 | burn_results 361 | .entry(i) 362 | .and_modify(|r| *r -= amt) 363 | .or_insert(-amt); 364 | } 365 | Ok(Some( 366 | oxidation_power.min(total_fuel) * 2.0 * FIRE_MAXIMUM_BURN_RATE, 367 | )) 368 | } 369 | })? { 370 | let temperature = with_mix_mut(&byond_air, |air| { 371 | // internal energy + PV, which happens to be reducible to this 372 | let initial_enthalpy = air.get_temperature() 373 | * (air.heat_capacity() + R_IDEAL_GAS_EQUATION * air.total_moles()); 374 | let mut delta_enthalpy = 0.0; 375 | for (&i, &amt) in &burn_results { 376 | air.adjust_moles(i, amt); 377 | delta_enthalpy -= amt * gas_info[i].enthalpy; 378 | } 379 | air.set_temperature( 380 | (initial_enthalpy + delta_enthalpy) 381 | / (air.heat_capacity() + R_IDEAL_GAS_EQUATION * air.total_moles()), 382 | ); 383 | Ok(air.get_temperature()) 384 | })?; 385 | let mut cached_results = byond_air.read_var_id(byond_string!("reaction_results"))?; 386 | cached_results.write_list_index("fire", fire_amount)?; 387 | if temperature > FIRE_MINIMUM_TEMPERATURE_TO_EXIST { 388 | byondapi::global_call::call_global_id( 389 | byond_string!("fire_expose"), 390 | &[holder, byond_air, temperature.into()], 391 | )?; 392 | } 393 | if radiation_released > 0.0 { 394 | byondapi::global_call::call_global_id( 395 | byond_string!("radiation_burn"), 396 | &[holder, radiation_released.into()], 397 | )?; 398 | } 399 | Ok((fire_amount > 0.0).into()) 400 | } else { 401 | Ok(false.into()) 402 | } 403 | }) 404 | } 405 | -------------------------------------------------------------------------------- /src/turfs/superconduct.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use byondapi::prelude::*; 3 | //use indexmap::IndexSet; 4 | use crate::GasArena; 5 | use auxcallback::byond_callback_sender; 6 | use coarsetime::Instant; 7 | use eyre::Result; 8 | use parking_lot::Once; 9 | 10 | static INIT_HEAT: Once = Once::new(); 11 | 12 | static TURF_HEAT: RwLock> = const_rwlock(None); 13 | 14 | lazy_static::lazy_static! { 15 | static ref HEAT_CHANNEL: (flume::Sender, flume::Receiver) = 16 | flume::bounded(1); 17 | } 18 | 19 | #[init(partial)] 20 | fn initialize_heat_statics() -> Result<(), String> { 21 | *TURF_HEAT.write() = Some(TurfHeat { 22 | graph: StableDiGraph::with_capacity(650_250, 1_300_500), 23 | map: IndexMap::with_capacity_and_hasher(650_250, FxBuildHasher::default()), 24 | }); 25 | Ok(()) 26 | } 27 | 28 | #[shutdown] 29 | fn shutdown_turfs() { 30 | wait_for_tasks(); 31 | *TURF_HEAT.write() = None; 32 | } 33 | 34 | fn with_turf_heat_read(f: F) -> T 35 | where 36 | F: FnOnce(&TurfHeat) -> T, 37 | { 38 | f(TURF_HEAT.read().as_ref().unwrap()) 39 | } 40 | 41 | fn with_turf_heat_write(f: F) -> T 42 | where 43 | F: FnOnce(&mut TurfHeat) -> T, 44 | { 45 | f(TURF_HEAT.write().as_mut().unwrap()) 46 | } 47 | 48 | #[derive(Copy, Clone)] 49 | struct SSheatInfo { 50 | time_delta: f64, 51 | } 52 | 53 | #[derive(Default)] 54 | struct ThermalInfo { 55 | pub id: TurfID, 56 | 57 | pub thermal_conductivity: f32, 58 | pub heat_capacity: f32, 59 | pub adjacent_to_space: bool, 60 | 61 | pub temperature: RwLock, 62 | } 63 | 64 | fn with_heat_processing_callback_receiver(f: impl Fn(&flume::Receiver) -> T) -> T { 65 | f(&HEAT_CHANNEL.1) 66 | } 67 | 68 | fn heat_processing_callbacks_sender() -> flume::Sender { 69 | HEAT_CHANNEL.0.clone() 70 | } 71 | type HeatGraphMap = IndexMap, FxBuildHasher>; 72 | 73 | //turf temperature infos goes here 74 | struct TurfHeat { 75 | graph: StableDiGraph, 76 | map: HeatGraphMap, 77 | } 78 | 79 | impl TurfHeat { 80 | pub fn insert_turf(&mut self, info: ThermalInfo) { 81 | if let Some(&node_id) = self.map.get(&info.id) { 82 | let thin = self.graph.node_weight_mut(node_id).unwrap(); 83 | thin.thermal_conductivity = info.thermal_conductivity; 84 | thin.heat_capacity = info.heat_capacity; 85 | thin.adjacent_to_space = info.adjacent_to_space; 86 | } else { 87 | self.map.insert(info.id, self.graph.add_node(info)); 88 | } 89 | } 90 | 91 | pub fn remove_turf(&mut self, id: TurfID) { 92 | if let Some(index) = self.map.remove(&id) { 93 | self.graph.remove_node(index); 94 | } 95 | } 96 | 97 | pub fn get(&self, idx: NodeIndex) -> Option<&ThermalInfo> { 98 | self.graph.node_weight(idx) 99 | } 100 | 101 | pub fn get_id(&self, idx: &TurfID) -> Option<&NodeIndex> { 102 | self.map.get(idx) 103 | } 104 | 105 | pub fn adjacent_node_ids<'a>( 106 | &'a self, 107 | index: NodeIndex, 108 | ) -> impl Iterator> + '_ { 109 | self.graph.neighbors(index) 110 | } 111 | 112 | pub fn adjacent_heats( 113 | &self, 114 | index: NodeIndex, 115 | ) -> impl Iterator + '_ { 116 | self.graph 117 | .neighbors(index) 118 | .filter_map(|neighbor| self.graph.node_weight(neighbor)) 119 | } 120 | 121 | pub fn update_adjacencies( 122 | &mut self, 123 | idx: TurfID, 124 | blocked_dirs: Directions, 125 | max_x: i32, 126 | max_y: i32, 127 | ) { 128 | if let Some(&this_node) = self.get_id(&idx) { 129 | self.remove_adjacencies(this_node); 130 | for (_, adj_idx) in adjacent_tile_ids( 131 | Directions::ALL_CARDINALS_MULTIZ - blocked_dirs, 132 | idx, 133 | max_x, 134 | max_y, 135 | ) { 136 | if let Some(&adjacent_node) = self.get_id(&adj_idx) { 137 | //this fucking happens, I don't even know anymore 138 | if adjacent_node != this_node { 139 | self.graph.add_edge(this_node, adjacent_node, ()); 140 | } 141 | } 142 | } 143 | } 144 | } 145 | 146 | pub fn remove_adjacencies(&mut self, index: NodeIndex) { 147 | let edges = self 148 | .graph 149 | .edges(index) 150 | .map(|edgeref| edgeref.id()) 151 | .collect::>(); 152 | edges.into_iter().for_each(|edgeindex| { 153 | self.graph.remove_edge(edgeindex); 154 | }); 155 | } 156 | } 157 | 158 | pub fn supercond_update_ref(src: ByondValue) -> Result<()> { 159 | let id = unsafe { src.raw.data.id }; 160 | let therm_cond = src.read_number("thermal_conductivity").unwrap_or(0.0); 161 | let therm_cap = src.read_number("heat_capacity").unwrap_or(0.0); 162 | if therm_cond > 0.0 && therm_cap > 0.0 { 163 | let therm_info = ThermalInfo { 164 | id, 165 | adjacent_to_space: src 166 | .call_id(byond_string!("should_conduct_to_space"), &[])? 167 | .as_number()? 168 | > 0.0, 169 | heat_capacity: therm_cap, 170 | thermal_conductivity: therm_cond, 171 | temperature: RwLock::new(src.read_number("initial_temperature").unwrap_or(TCMB)), 172 | }; 173 | with_turf_heat_write(|arena| arena.insert_turf(therm_info)); 174 | } else { 175 | with_turf_heat_write(|arena| arena.remove_turf(id)); 176 | } 177 | Ok(()) 178 | } 179 | 180 | pub fn supercond_update_adjacencies(id: u32) -> Result<()> { 181 | let max_x = auxtools::ByondValue::world() 182 | .read_number("maxx") 183 | .map_err(|_| { 184 | eyre::eyre!( 185 | "Attempt to interpret non-number value as number {} {}:{}", 186 | std::file!(), 187 | std::line!(), 188 | std::column!() 189 | ) 190 | })? as i32; 191 | let max_y = auxtools::ByondValue::world() 192 | .read_number("maxy") 193 | .map_err(|_| { 194 | eyre::eyre!( 195 | "Attempt to interpret non-number value as number {} {}:{}", 196 | std::file!(), 197 | std::line!(), 198 | std::column!() 199 | ) 200 | })? as i32; 201 | let src_turf = unsafe { ByondValue::turf_by_id_unchecked(id) }; 202 | with_turf_heat_write(|arena| -> Result<()> { 203 | if let Ok(blocked_dirs) = src_turf.read_number("conductivity_blocked_directions") { 204 | let actual_dir = Directions::from_bits_truncate(blocked_dirs as u8); 205 | arena.update_adjacencies(id, actual_dir, max_x, max_y) 206 | } else if let Some(&idx) = arena.get_id(&id) { 207 | arena.remove_adjacencies(idx) 208 | } 209 | Ok(()) 210 | })?; 211 | Ok(()) 212 | } 213 | 214 | #[byondapi_hooks::bind("/turf/proc/return_temperature")] 215 | fn hook_turf_temperature() -> Result { 216 | with_turf_heat_read(|arena| -> Result { 217 | if let Some(&node_index) = arena.get_id(&unsafe { src.raw.data.id }) { 218 | let info = arena.get(node_index).unwrap(); 219 | let read = info.temperature.read(); 220 | if read.is_normal() { 221 | Ok(ByondValue::from(*read)) 222 | } else { 223 | Ok(ByondValue::from(300)) 224 | } 225 | } else { 226 | Ok(ByondValue::from(102)) 227 | } 228 | }) 229 | } 230 | 231 | // Expected function call: process_turf_heat() 232 | // Returns: TRUE if thread not done, FALSE otherwise 233 | #[byondapi_hooks::bind("/datum/controller/subsystem/air/proc/process_turf_heat")] 234 | fn process_heat_notify() -> Result { 235 | rebuild_turf_graph()?; 236 | /* 237 | Replacing LINDA's superconductivity system is this much more brute-force 238 | system--it shares heat between turfs and their neighbors, 239 | then receives and emits radiation to space, then shares 240 | between turfs and their gases. Since the latter requires a write lock, 241 | it's done after the previous step. This one doesn't care about 242 | consistency like the processing step does--this can run in full parallel. 243 | Can't get a number from src in the thread, so we get it here. 244 | Have to get the time delta because the radiation 245 | is actually physics-based--the stefan boltzmann constant 246 | and radiation from space both have dimensions of second^-1 that 247 | need to be multiplied out to have any physical meaning. 248 | They also have dimensions of meter^-2, but I'm assuming 249 | turf tiles are 1 meter^2 anyway--the atmos subsystem 250 | does this in general, thus turf gas mixtures being 2.5 m^3. 251 | */ 252 | let sender = heat_processing_callbacks_sender(); 253 | let time_delta = (src.read_number("wait").map_err(|_| { 254 | eyre::eyre!( 255 | "Attempt to interpret non-number value as number {} {}:{}", 256 | std::file!(), 257 | std::line!(), 258 | std::column!() 259 | ) 260 | })? / 10.0) as f64; 261 | _ = sender.try_send(SSheatInfo { time_delta }); 262 | Ok(ByondValue::null()) 263 | } 264 | 265 | fn get_share_energy(delta: f32, cap_1: f32, cap_2: f32) -> f32 { 266 | delta * ((cap_1 * cap_2) / (cap_1 + cap_2)) 267 | } 268 | 269 | //Fires the task into the thread pool, once 270 | #[init(full)] 271 | fn process_heat_start() -> Result<(), String> { 272 | INIT_HEAT.call_once(|| { 273 | rayon::spawn(|| loop { 274 | //this will block until process_turf_heat is called 275 | let info = with_heat_processing_callback_receiver(|receiver| receiver.recv().unwrap()); 276 | let task_lock = TASKS.read(); 277 | let start_time = Instant::now(); 278 | let sender = byond_callback_sender(); 279 | let _emissivity_constant: f64 = STEFAN_BOLTZMANN_CONSTANT * info.time_delta; 280 | let _radiation_from_space_tick: f64 = RADIATION_FROM_SPACE * info.time_delta; 281 | with_turf_heat_read(|arena| { 282 | with_turf_gases_read(|air_arena| { 283 | let adjacencies_to_consider = arena 284 | .map 285 | .par_iter() 286 | .filter_map(|(&turf_id, &heat_index)| { 287 | /* 288 | If it has no thermal conductivity, low thermal capacity or has no adjacencies, 289 | then it's not gonna interact, or at least shouldn't. 290 | */ 291 | let info = arena.get(heat_index).unwrap(); 292 | let temp = { *info.temperature.read() }; 293 | //can share w/ adjacents? 294 | if arena.adjacent_heats(heat_index).any(|item| { 295 | (temp - *item.temperature.read()).abs() 296 | > MINIMUM_TEMPERATURE_DELTA_TO_CONSIDER 297 | }) { 298 | return Some((turf_id, heat_index, true)); 299 | } 300 | if temp > MINIMUM_TEMPERATURE_FOR_SUPERCONDUCTION { 301 | //can share w/ space/air? 302 | if info.adjacent_to_space 303 | || air_arena 304 | .get_id(&turf_id) 305 | .and_then(|&nodeid| { 306 | air_arena.get(nodeid)?.enabled().then(|| ()) 307 | }) 308 | .is_some() 309 | { 310 | Some((turf_id, heat_index, false)) 311 | } else { 312 | None 313 | } 314 | } else if let Some(node) = air_arena.get_id(&turf_id) { 315 | let cur_mix = air_arena.get(*node).unwrap(); 316 | if !cur_mix.enabled() { 317 | return None; 318 | } 319 | GasArena::with_all_mixtures(|all_mixtures| { 320 | let air_temp = all_mixtures[cur_mix.mix].try_read(); 321 | if air_temp.is_none() { 322 | return false; 323 | } 324 | let air_temp = air_temp.unwrap().get_temperature(); 325 | 326 | if air_temp < MINIMUM_TEMPERATURE_FOR_SUPERCONDUCTION { 327 | return false; 328 | } 329 | (temp - air_temp).abs() > MINIMUM_TEMPERATURE_DELTA_TO_CONSIDER 330 | }) 331 | .then(|| (turf_id, heat_index, false)) 332 | } else { 333 | None 334 | } 335 | }) 336 | .filter_map(|(id, node_index, has_adjacents)| { 337 | let info = arena.get(node_index).unwrap(); 338 | let mut temp_write = info.temperature.try_write()?; 339 | 340 | /* 341 | //share w/ space 342 | if info.adjacent_to_space && *temp_write > T0C { 343 | /* 344 | Straight up the standard blackbody radiation 345 | equation. All these are f64s because 346 | f32::MAX^4 < f64::MAX, and t.temperature 347 | is ordinarily an f32, meaning that 348 | this will never go into infinities. 349 | */ 350 | let blackbody_radiation: f64 = (emissivity_constant 351 | * STEFAN_BOLTZMANN_CONSTANT 352 | * (f64::from(*temp_write).powi(4))) 353 | - radiation_from_space_tick; 354 | *temp_write -= blackbody_radiation as f32 / info.heat_capacity; 355 | } 356 | */ 357 | //share w/ space 358 | if info.adjacent_to_space && *temp_write > T20C { 359 | let delta = *temp_write - TCMB; 360 | let energy = get_share_energy( 361 | info.thermal_conductivity * delta, 362 | HEAT_CAPACITY_VACUUM, 363 | info.heat_capacity, 364 | ); 365 | *temp_write -= energy / info.heat_capacity; 366 | } 367 | 368 | //share w/ air 369 | if let Some(&id) = air_arena.get_id(&id) { 370 | let tmix = air_arena.get(id).unwrap(); 371 | if tmix.enabled() { 372 | GasArena::with_all_mixtures(|all_mixtures| { 373 | if let Some(entry) = all_mixtures.get(tmix.mix) { 374 | if let Some(mut gas) = entry.try_write() { 375 | *temp_write = gas.temperature_share_non_gas( 376 | /* 377 | This value should be lower than the 378 | turf-to-turf conductivity for balance reasons 379 | as well as realism, otherwise fires will 380 | just sort of solve theirselves over time. 381 | */ 382 | info.thermal_conductivity 383 | * OPEN_HEAT_TRANSFER_COEFFICIENT, 384 | *temp_write, 385 | info.heat_capacity, 386 | ); 387 | } 388 | } 389 | }) 390 | } 391 | } 392 | 393 | if !temp_write.is_normal() { 394 | *temp_write = TCMB; 395 | } 396 | 397 | if *temp_write > MINIMUM_TEMPERATURE_START_SUPERCONDUCTION 398 | && *temp_write > info.heat_capacity 399 | { 400 | // not what heat capacity means but whatever 401 | drop(sender.try_send(Box::new(move || { 402 | let turf = unsafe { ByondValue::turf_by_id_unchecked(id) }; 403 | turf.set("to_be_destroyed", 1.0)?; 404 | Ok(()) 405 | }))); 406 | } 407 | has_adjacents.then(|| node_index) 408 | }) 409 | .collect::>(); 410 | 411 | _ = adjacencies_to_consider 412 | .par_iter() 413 | .try_for_each(|&cur_index| { 414 | let info = arena.get(cur_index).unwrap(); 415 | if let Some(mut temp_write) = info.temperature.try_write() { 416 | //share w/ adjacents that are strictly in zone 417 | for other in arena 418 | .adjacent_node_ids(cur_index) 419 | .filter_map(|idx| arena.get(idx)) 420 | { 421 | /* 422 | The horrible line below is essentially 423 | sharing between solids--making it the minimum of both 424 | conductivities makes this consistent, funnily enough. 425 | */ 426 | if let Some(mut other_write) = other.temperature.try_write() { 427 | let shareds = 428 | info.thermal_conductivity 429 | .min(other.thermal_conductivity) * get_share_energy( 430 | *other_write - *temp_write, 431 | info.heat_capacity, 432 | other.heat_capacity, 433 | ); 434 | *temp_write += shareds / info.heat_capacity; 435 | *other_write -= shareds / other.heat_capacity; 436 | } 437 | } 438 | } 439 | Ok(()) 440 | }); 441 | }); 442 | }); 443 | let bench = start_time.elapsed().as_millis(); 444 | drop(sender.try_send(Box::new(move || { 445 | let ssair = auxtools::ByondValue::globals().get("SSair")?; 446 | let prev_cost = ssair.read_number("cost_superconductivity").map_err(|_| { 447 | eyre::eyre!( 448 | "Attempt to interpret non-number value as number {} {}:{}", 449 | std::file!(), 450 | std::line!(), 451 | std::column!() 452 | ) 453 | })?; 454 | ssair.set( 455 | "cost_superconductivity", 456 | ByondValue::from(0.8 * prev_cost + 0.2 * (bench as f32)), 457 | )?; 458 | Ok(()) 459 | }))); 460 | drop(task_lock); 461 | }); 462 | }); 463 | Ok(()) 464 | } 465 | /* 466 | 467 | fn flood_fill_temps( 468 | input: Vec>, 469 | arena: &TurfHeat, 470 | ) -> Vec, FxBuildHasher>> { 471 | let mut found_turfs: HashSet, FxBuildHasher> = Default::default(); 472 | let mut return_val: Vec, FxBuildHasher>> = Default::default(); 473 | for temp_id in input { 474 | let mut turfs: IndexSet, FxBuildHasher> = Default::default(); 475 | let mut border_turfs: std::collections::VecDeque> = Default::default(); 476 | border_turfs.push_back(temp_id); 477 | found_turfs.insert(temp_id); 478 | while let Some(cur_index) = border_turfs.pop_front() { 479 | for adj_index in arena.adjacent_node_ids(cur_index) { 480 | if found_turfs.insert(adj_index) { 481 | border_turfs.push_back(adj_index) 482 | } 483 | } 484 | turfs.insert(cur_index); 485 | } 486 | return_val.push(turfs) 487 | } 488 | return_val 489 | } 490 | */ 491 | -------------------------------------------------------------------------------- /src/turfs/processing.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::{react_hook, GasArena}; 3 | use auxcallback::{byond_callback_sender, process_callbacks_for_millis}; 4 | use byondapi::{byond_string, prelude::*}; 5 | use coarsetime::{Duration, Instant}; 6 | use parking_lot::RwLock; 7 | use std::collections::{BTreeMap, BTreeSet}; 8 | use tinyvec::TinyVec; 9 | 10 | /// Returns: If a processing thread is running or not. 11 | #[byondapi::bind("/datum/controller/subsystem/air/proc/thread_running")] 12 | fn thread_running_hook() -> Result { 13 | Ok(TASKS.try_write().is_none().into()) 14 | } 15 | 16 | /// Returns: If this cycle is interrupted by overtiming or not. Calls all outstanding callbacks created by other processes, usually ones that can't run on other threads and only the main thread. 17 | #[byondapi::bind("/datum/controller/subsystem/air/proc/finish_turf_processing_auxtools")] 18 | fn finish_process_turfs(time_remaining: ByondValue) -> Result { 19 | Ok(process_callbacks_for_millis(time_remaining.get_number()? as u64).into()) 20 | } 21 | /// Returns: If this cycle is interrupted by overtiming or not. Starts a processing turfs cycle. 22 | #[byondapi::bind("/datum/controller/subsystem/air/proc/process_turfs_auxtools")] 23 | fn process_turf_hook(src: ByondValue, remaining: ByondValue) -> Result { 24 | let remaining_time = Duration::from_millis(remaining.get_number().unwrap_or(50.0) as u64); 25 | let fdm_max_steps = src 26 | .read_number_id(byond_string!("share_max_steps")) 27 | .unwrap_or(1.0) as i32; 28 | let equalize_enabled = 29 | cfg!(feature = "fastmos") && src.read_number_id(byond_string!("equalize_enabled"))? != 0.0; 30 | 31 | let planet_share_ratio = src 32 | .read_number_id(byond_string!("planet_share_ratio")) 33 | .unwrap_or(GAS_DIFFUSION_CONSTANT); 34 | 35 | process_turf( 36 | remaining_time, 37 | fdm_max_steps, 38 | equalize_enabled, 39 | planet_share_ratio, 40 | src, 41 | )?; 42 | Ok(ByondValue::null()) 43 | } 44 | 45 | #[cfg_attr(feature = "tracy", tracing::instrument(skip_all))] 46 | fn process_turf( 47 | remaining: Duration, 48 | fdm_max_steps: i32, 49 | equalize_enabled: bool, 50 | planet_share_ratio: f32, 51 | mut ssair: ByondValue, 52 | ) -> Result<()> { 53 | //this will block until process_turfs is called 54 | let (low_pressure_turfs, _high_pressure_turfs) = { 55 | let start_time = Instant::now(); 56 | let (low_pressure_turfs, high_pressure_turfs) = 57 | fdm((&start_time, remaining), fdm_max_steps, equalize_enabled); 58 | let bench = start_time.elapsed().as_millis(); 59 | let (lpt, hpt) = (low_pressure_turfs.len(), high_pressure_turfs.len()); 60 | let prev_cost = ssair.read_number_id(byond_string!("cost_turfs"))?; 61 | ssair.write_var_id( 62 | byond_string!("cost_turfs"), 63 | &(0.8 * prev_cost + 0.2 * (bench as f32)).into(), 64 | )?; 65 | ssair.write_var_id(byond_string!("low_pressure_turfs"), &(lpt as f32).into())?; 66 | ssair.write_var_id(byond_string!("high_pressure_turfs"), &(hpt as f32).into())?; 67 | (low_pressure_turfs, high_pressure_turfs) 68 | }; 69 | { 70 | let start_time = Instant::now(); 71 | post_process(); 72 | let bench = start_time.elapsed().as_millis(); 73 | let prev_cost = ssair.read_number_id(byond_string!("cost_post_process"))?; 74 | ssair.write_var_id( 75 | byond_string!("cost_post_process"), 76 | &(0.8 * prev_cost + 0.2 * (bench as f32)).into(), 77 | )?; 78 | } 79 | { 80 | planet_process(planet_share_ratio); 81 | } 82 | { 83 | super::groups::send_to_groups(low_pressure_turfs); 84 | } 85 | if equalize_enabled { 86 | #[cfg(feature = "fastmos")] 87 | { 88 | super::katmos::send_to_equalize(_high_pressure_turfs); 89 | } 90 | } 91 | Ok(()) 92 | } 93 | 94 | #[cfg_attr(not(target_feature = "avx2"), auxmacros::generate_simd_functions)] 95 | #[cfg_attr(feature = "tracy", tracing::instrument(skip_all))] 96 | fn planet_process(planet_share_ratio: f32) { 97 | with_turf_gases_read(|arena| { 98 | GasArena::with_all_mixtures(|all_mixtures| { 99 | with_planetary_atmos(|map| { 100 | arena 101 | .map 102 | .par_values() 103 | .filter_map(|&node_idx| { 104 | let mix = arena.get(node_idx)?; 105 | Some((mix, mix.planetary_atmos.and_then(|id| map.get(&id))?)) 106 | }) 107 | .for_each(|(turf_mix, planet_atmos)| { 108 | if let Some(gas_read) = all_mixtures 109 | .get(turf_mix.mix) 110 | .and_then(|lock| lock.try_upgradable_read()) 111 | { 112 | let comparison = gas_read.compare(planet_atmos); 113 | let has_temp_difference = gas_read.temperature_compare(planet_atmos); 114 | if let Some(mut gas) = (has_temp_difference 115 | || (comparison > GAS_MIN_MOLES)) 116 | .then(|| { 117 | parking_lot::lock_api::RwLockUpgradableReadGuard::try_upgrade( 118 | gas_read, 119 | ) 120 | .ok() 121 | }) 122 | .flatten() 123 | { 124 | if comparison > 0.1 || has_temp_difference { 125 | gas.share_ratio(planet_atmos, planet_share_ratio); 126 | } else { 127 | gas.copy_from_mutable(planet_atmos); 128 | } 129 | } 130 | } 131 | }) 132 | }) 133 | }) 134 | }); 135 | } 136 | 137 | // Compares with neighbors, returning early if any of them are valid. 138 | fn should_process( 139 | index: NodeIndex, 140 | mixture: &TurfMixture, 141 | all_mixtures: &[RwLock], 142 | arena: &TurfGases, 143 | ) -> bool { 144 | mixture.enabled() 145 | && arena.adjacent_node_ids(index).next().is_some() 146 | && all_mixtures 147 | .get(mixture.mix) 148 | .and_then(RwLock::try_read) 149 | .map_or(false, |gas| { 150 | for entry in arena.adjacent_mixes(index, all_mixtures) { 151 | if let Some(mix) = entry.try_read() { 152 | if gas.temperature_compare(&mix) 153 | || gas.compare_with(&mix, MINIMUM_MOLES_DELTA_TO_MOVE) 154 | { 155 | return true; 156 | } 157 | } else { 158 | return false; 159 | } 160 | } 161 | false 162 | }) 163 | } 164 | 165 | // Creates the combined gas mixture of all this mix's neighbors, as well as gathering some other pertinent info for future processing. 166 | // Clippy go away, this type is only used once 167 | #[allow(clippy::type_complexity)] 168 | fn process_cell( 169 | index: NodeIndex, 170 | all_mixtures: &[RwLock], 171 | arena: &TurfGases, 172 | ) -> Option<(NodeIndex, Mixture, TinyVec<[(TurfID, f32); 6]>, i32)> { 173 | let mut adj_amount = 0; 174 | /* 175 | Getting write locks is potential danger zone, 176 | so we make sure we don't do that unless we 177 | absolutely need to. Saving is fast enough. 178 | */ 179 | let mut end_gas = Mixture::from_vol(crate::constants::CELL_VOLUME); 180 | let mut pressure_diffs: TinyVec<[(TurfID, f32); 6]> = Default::default(); 181 | /* 182 | The pressure here is negative 183 | because we're going to be adding it 184 | to the base turf's pressure later on. 185 | It's multiplied by the diffusion constant 186 | because it's not representing the total 187 | gas pressure difference but the force exerted 188 | due to the pressure gradient. 189 | Technically that's ρν², but, like, video games. 190 | */ 191 | for (&loc, entry) in 192 | arena.adjacent_mixes_with_adj_ids(index, all_mixtures, petgraph::Direction::Incoming) 193 | { 194 | match entry.try_read() { 195 | Some(mix) => { 196 | end_gas.merge(&mix); 197 | adj_amount += 1; 198 | pressure_diffs.push((loc, -mix.return_pressure() * GAS_DIFFUSION_CONSTANT)); 199 | } 200 | None => return None, // this would lead to inconsistencies--no bueno 201 | } 202 | } 203 | /* 204 | This method of simulating diffusion 205 | diverges at coefficients that are 206 | larger than the inverse of the number 207 | of adjacent finite elements. 208 | As such, we must multiply it 209 | by a coefficient that is at most 210 | as big as this coefficient. The 211 | GAS_DIFFUSION_CONSTANT chosen here 212 | is 1/8, chosen both because it is 213 | smaller than 1/7 and because, in 214 | floats, 1/8 is exact and so are 215 | all multiples of it up to 1. 216 | (Technically up to 2,097,152, 217 | but I digress.) 218 | */ 219 | end_gas.multiply(GAS_DIFFUSION_CONSTANT); 220 | Some((index, end_gas, pressure_diffs, adj_amount)) 221 | } 222 | 223 | // Solving the heat equation using a Finite Difference Method, an iterative stencil loop. 224 | #[cfg_attr(not(target_feature = "avx2"), auxmacros::generate_simd_functions)] 225 | #[cfg_attr(feature = "tracy", tracing::instrument(skip_all))] 226 | fn fdm( 227 | (start_time, remaining_time): (&Instant, Duration), 228 | fdm_max_steps: i32, 229 | equalize_enabled: bool, 230 | ) -> (BTreeSet, BTreeSet) { 231 | /* 232 | This is the replacement system for LINDA. LINDA requires a lot of bookkeeping, 233 | which, when coefficient-wise operations are this fast, is all just unnecessary overhead. 234 | This is a much simpler FDM system, basically like LINDA but without its most important feature, 235 | sleeping turfs, which is why I've renamed it to fdm. 236 | */ 237 | let mut low_pressure_turfs: BTreeSet = Default::default(); 238 | let mut high_pressure_turfs: BTreeSet = Default::default(); 239 | let mut cur_count = 1; 240 | with_turf_gases_read(|arena| { 241 | loop { 242 | if cur_count > fdm_max_steps || start_time.elapsed() >= remaining_time { 243 | break; 244 | } 245 | GasArena::with_all_mixtures(|all_mixtures| { 246 | let turfs_to_save = arena 247 | .map 248 | /* 249 | This directly yanks the internal node vec 250 | of the graph as a slice to parallelize the process. 251 | The speedup gained from this is actually linear 252 | with the amount of cores the CPU has, which, to be frank, 253 | is way better than I was expecting, even though this operation 254 | is technically embarassingly parallel. It'll probably reach 255 | some maximum due to the global turf mixture lock access, 256 | but it's already blazingly fast on my i7, so it should be fine. 257 | */ 258 | .par_values() 259 | .map(|&idx| (idx, arena.get(idx).unwrap())) 260 | .filter(|(index, mixture)| should_process(*index, mixture, all_mixtures, arena)) 261 | .filter_map(|(index, _)| process_cell(index, all_mixtures, arena)) 262 | .collect::>(); 263 | /* 264 | For the optimization-heads reading this: this is not an unnecessary collect(). 265 | Saving all this to the turfs_to_save vector is, in fact, the reason 266 | that gases don't need an archive anymore--this *is* the archival step, 267 | simultaneously saving how the gases will change after the fact. 268 | In short: the above actually needs to finish before the below starts 269 | for consistency, so collect() is desired. This has been tested, by the way. 270 | */ 271 | let (low_pressure, high_pressure): (Vec<_>, Vec<_>) = turfs_to_save 272 | .into_par_iter() 273 | .filter_map(|(i, end_gas, mut pressure_diffs, adj_amount)| { 274 | let m = arena.get(i).unwrap(); 275 | all_mixtures.get(m.mix).map(|entry| { 276 | let mut max_diff = 0.0_f32; 277 | let moved_pressure = { 278 | let gas = entry.read(); 279 | gas.return_pressure() * GAS_DIFFUSION_CONSTANT 280 | }; 281 | for pressure_diff in &mut pressure_diffs { 282 | // pressure_diff.1 here was set to a negative above, so we just add. 283 | pressure_diff.1 += moved_pressure; 284 | max_diff = max_diff.max(pressure_diff.1.abs()); 285 | } 286 | /* 287 | 1.0 - GAS_DIFFUSION_CONSTANT * adj_amount is going to be 288 | precisely equal to the amount the surrounding tiles' 289 | end_gas have "taken" from this tile-- 290 | they didn't actually take anything, just calculated 291 | how much would be. This is the "taking" step. 292 | Just to illustrate: say you have a turf with 3 neighbors. 293 | Each of those neighbors will have their end_gas added to by 294 | GAS_DIFFUSION_CONSTANT (at this writing, 0.125) times 295 | this gas. So, 1.0 - (0.125 * adj_amount) = 0.625-- 296 | exactly the amount those gases "took" from this. 297 | */ 298 | { 299 | let gas: &mut Mixture = &mut entry.write(); 300 | gas.multiply(1.0 - (adj_amount as f32 * GAS_DIFFUSION_CONSTANT)); 301 | gas.merge(&end_gas); 302 | } 303 | /* 304 | If there is neither a major pressure difference 305 | nor are there any visible gases nor does it need 306 | to react, we're done outright. We don't need 307 | to do any more and we don't need to send the 308 | value to byond, so we don't. However, if we do... 309 | */ 310 | (m.id, pressure_diffs, max_diff, i) 311 | }) 312 | }) 313 | .partition(|&(_, _, max_diff, _)| max_diff <= 5.0); 314 | 315 | high_pressure_turfs.par_extend(high_pressure.par_iter().map(|(i, _, _, _)| i)); 316 | low_pressure_turfs.par_extend(low_pressure.par_iter().map(|(i, _, _, _)| i)); 317 | //tossing things around is already handled by katmos, so we don't need to do it here. 318 | if !equalize_enabled { 319 | high_pressure 320 | .into_par_iter() 321 | .filter_map(|(_, pressures, _, node_id)| { 322 | Some((arena.get(node_id)?.id, pressures)) 323 | }) 324 | .for_each(|(id, diffs)| { 325 | let sender = byond_callback_sender(); 326 | drop(sender.try_send(Box::new(move || { 327 | let turf = ByondValue::new_ref(ValueType::Turf, id); 328 | for (id, diff) in diffs.iter().copied() { 329 | if id != 0 { 330 | let enemy_tile = ByondValue::new_ref(ValueType::Turf, id); 331 | if diff > 5.0 { 332 | turf.call_id( 333 | byond_string!("consider_pressure_difference"), 334 | &[enemy_tile, diff.into()], 335 | ) 336 | .wrap_err("Processing consider pressure differences")?; 337 | } else if diff < -5.0 { 338 | enemy_tile 339 | .call_id( 340 | byond_string!("consider_pressure_difference"), 341 | &[turf, (-diff).into()], 342 | ) 343 | .wrap_err( 344 | "Processing consider pressure differences", 345 | )?; 346 | } 347 | } 348 | } 349 | Ok(()) 350 | }))); 351 | }); 352 | } 353 | }); 354 | 355 | cur_count += 1; 356 | } 357 | }); 358 | (low_pressure_turfs, high_pressure_turfs) 359 | } 360 | 361 | // Checks if the gas can react or can update visuals, returns None if not. 362 | fn post_process_cell<'a>( 363 | mixture: &'a TurfMixture, 364 | vis: &[Option], 365 | all_mixtures: &[RwLock], 366 | reactions: &BTreeMap, 367 | ) -> Option<(&'a TurfMixture, bool, bool)> { 368 | all_mixtures 369 | .get(mixture.mix) 370 | .and_then(RwLock::try_read) 371 | .and_then(|gas| { 372 | let should_update_visuals = gas.vis_hash_changed(vis, &mixture.vis_hash); 373 | let reactable = gas.can_react_with_reactions(reactions); 374 | (should_update_visuals || reactable).then_some(( 375 | mixture, 376 | should_update_visuals, 377 | reactable, 378 | )) 379 | }) 380 | } 381 | 382 | // Goes through every turf, checks if it should reset to planet atmos, if it should 383 | // update visuals, if it should react, sends a callback if it should. 384 | #[cfg_attr(not(target_feature = "avx2"), auxmacros::generate_simd_functions)] 385 | #[cfg_attr(feature = "tracy", tracing::instrument(skip_all))] 386 | fn post_process() { 387 | let vis = crate::gas::visibility_copies(); 388 | with_turf_gases_read(|arena| { 389 | let processables = crate::gas::types::with_reactions(|reactions| { 390 | GasArena::with_all_mixtures(|all_mixtures| { 391 | arena 392 | .map 393 | .par_values() 394 | .filter_map(|&node_index| { 395 | let mix = arena.get(node_index).unwrap(); 396 | mix.enabled().then_some(mix) 397 | }) 398 | .filter_map(|mixture| post_process_cell(mixture, &vis, all_mixtures, reactions)) 399 | .collect::>() 400 | }) 401 | }); 402 | processables 403 | .into_par_iter() 404 | .for_each(|(tmix, should_update_vis, should_react)| { 405 | let sender = byond_callback_sender(); 406 | let id = tmix.id; 407 | 408 | if should_react { 409 | drop(sender.try_send(Box::new(move || { 410 | let turf = ByondValue::new_ref(ValueType::Turf, id); 411 | match turf.read_var_id(byond_string!("air")) { 412 | Ok(air) if !air.is_null() => { 413 | react_hook(air, turf).wrap_err("Reacting")?; 414 | Ok(()) 415 | } 416 | //turf is no longer valid for reactions 417 | _ => Ok(()), 418 | } 419 | }))); 420 | } 421 | 422 | if should_update_vis { 423 | drop(sender.try_send(Box::new(move || { 424 | let turf = ByondValue::new_ref(ValueType::Turf, id); 425 | 426 | //turf is checked for validity in update_visuals 427 | update_visuals(turf).wrap_err("Updating Visuals")?; 428 | Ok(()) 429 | }))); 430 | } 431 | }); 432 | }); 433 | } 434 | -------------------------------------------------------------------------------- /src/gas/types.rs: -------------------------------------------------------------------------------- 1 | use super::GasIDX; 2 | use crate::reaction::{Reaction, ReactionPriority}; 3 | use auxcallback::byond_callback_sender; 4 | use byondapi::prelude::*; 5 | use dashmap::DashMap; 6 | use eyre::{Context, Result}; 7 | use hashbrown::HashMap; 8 | use parking_lot::{const_rwlock, RwLock}; 9 | use rustc_hash::FxBuildHasher; 10 | use std::{ 11 | cell::RefCell, 12 | collections::BTreeMap, 13 | sync::atomic::{AtomicUsize, Ordering}, 14 | }; 15 | 16 | static TOTAL_NUM_GASES: AtomicUsize = AtomicUsize::new(0); 17 | static REACTION_INFO: RwLock>> = const_rwlock(None); 18 | 19 | /// The temperature at which this gas can oxidize and how much fuel it can oxidize when it can. 20 | #[derive(Clone, Copy)] 21 | pub struct OxidationInfo { 22 | temperature: f32, 23 | power: f32, 24 | } 25 | 26 | impl OxidationInfo { 27 | #[must_use] 28 | pub fn temperature(&self) -> f32 { 29 | self.temperature 30 | } 31 | #[must_use] 32 | pub fn power(&self) -> f32 { 33 | self.power 34 | } 35 | } 36 | 37 | /// The temperature at which this gas can burn and how much it burns when it does. 38 | /// This may seem redundant with `OxidationInfo`, but `burn_rate` is actually the inverse, dimensions-wise, moles^-1 rather than moles. 39 | #[derive(Clone, Copy)] 40 | pub struct FuelInfo { 41 | temperature: f32, 42 | burn_rate: f32, 43 | } 44 | 45 | impl FuelInfo { 46 | #[must_use] 47 | pub fn temperature(&self) -> f32 { 48 | self.temperature 49 | } 50 | #[must_use] 51 | pub fn burn_rate(&self) -> f32 { 52 | self.burn_rate 53 | } 54 | } 55 | 56 | /// Contains either oxidation info, fuel info or neither. 57 | /// Just use it with match always, no helpers here. 58 | #[derive(Clone, Copy)] 59 | pub enum FireInfo { 60 | Oxidation(OxidationInfo), 61 | Fuel(FuelInfo), 62 | None, 63 | } 64 | 65 | /// We can't guarantee the order of loading gases, so any properties of gases that reference other gases 66 | /// must have a reference like this that can get the proper index at runtime. 67 | #[derive(Clone)] 68 | pub enum GasRef { 69 | Deferred(String), 70 | Found(GasIDX), 71 | } 72 | 73 | impl GasRef { 74 | /// Gets the index of the gas. 75 | /// # Errors 76 | /// Propagates error from `gas_idx_from_string`. 77 | pub fn get(&self) -> Result { 78 | match self { 79 | Self::Deferred(s) => gas_idx_from_string(s), 80 | Self::Found(id) => Ok(*id), 81 | } 82 | } 83 | /// Like `get`, but also caches the result if found. 84 | /// # Errors 85 | /// If the string is not a valid gas name. 86 | pub fn update(&mut self) -> Result { 87 | match self { 88 | Self::Deferred(s) => { 89 | *self = Self::Found(gas_idx_from_string(s)?); 90 | self.get() 91 | } 92 | Self::Found(id) => Ok(*id), 93 | } 94 | } 95 | } 96 | 97 | #[derive(Clone)] 98 | pub enum FireProductInfo { 99 | Generic(Vec<(GasRef, f32)>), 100 | Plasma, // yeah, just hardcoding the funny trit production 101 | } 102 | 103 | /// An individual gas type. Contains a whole lot of info attained from Byond when the gas is first registered. 104 | /// If you don't have any of these, just fork auxmos and remove them, many of these are not necessary--for example, 105 | /// if you don't have fusion, you can just remove `fusion_power`. 106 | /// Each individual member also has the byond /datum/gas equivalent listed. 107 | #[derive(Clone)] 108 | pub struct GasType { 109 | /// The index of this gas in the moles vector of a mixture. Usually the most common representation in Auxmos, for speed. 110 | /// No byond equivalent. 111 | pub idx: GasIDX, 112 | /// The ID on the byond end, as a boxed str. Most convenient way to reference it in code; use the function gas_idx_from_string to get idx from this. 113 | /// Byond: `id`, a string. 114 | pub id: Box, 115 | /// The gas's name. Not used in auxmos as of yet. 116 | /// Byond: `name`, a string. 117 | pub name: Box, 118 | /// Byond: `flags`, a number (bitflags). 119 | pub flags: u32, 120 | /// The specific heat of the gas. Duplicated in the GAS_SPECIFIC_HEATS vector for speed. 121 | /// Byond: `specific_heat`, a number. 122 | pub specific_heat: f32, 123 | /// Gas's fusion power. Used in fusion hooking, so this can be removed and ignored if you don't have fusion. 124 | /// Byond: `fusion_power`, a number. 125 | pub fusion_power: f32, 126 | /// The moles at which the gas's overlay or other appearance shows up. If None, gas is never visible. 127 | /// Byond: `moles_visible`, a number. 128 | pub moles_visible: Option, 129 | /// Standard enthalpy of formation. 130 | /// Byond: `fire_energy_released`, a number. 131 | pub enthalpy: f32, 132 | /// Amount of radiation released per mole burned. 133 | /// Byond: `fire_radiation_released`, a number. 134 | pub fire_radiation_released: f32, 135 | /// Either fuel info, oxidation info or neither. See the documentation on the respective types. 136 | /// Byond: `oxidation_temperature` and `oxidation_rate` XOR `fire_temperature` and `fire_burn_rate` 137 | pub fire_info: FireInfo, 138 | /// A vector of gas-amount pairs. GasRef is just which gas, the f32 is moles made/mole burned. 139 | /// Byond: `fire_products`, a list of gas IDs associated with amounts. 140 | pub fire_products: Option, 141 | } 142 | 143 | impl GasType { 144 | // This absolute monster is what you want to override to add or remove certain gas properties, based on what a gas datum has. 145 | fn new(gas: &ByondValue, idx: GasIDX) -> Result { 146 | Ok(Self { 147 | idx, 148 | id: gas.read_string_id(byond_string!("id"))?.into_boxed_str(), 149 | name: gas.read_string_id(byond_string!("name"))?.into_boxed_str(), 150 | flags: gas 151 | .read_number_id(byond_string!("flags")) 152 | .unwrap_or_default() as u32, 153 | specific_heat: gas.read_number_id(byond_string!("specific_heat"))?, 154 | fusion_power: gas 155 | .read_number_id(byond_string!("fusion_power")) 156 | .unwrap_or_default(), 157 | moles_visible: gas.read_number_id(byond_string!("moles_visible")).ok(), 158 | fire_info: { 159 | if let Ok(temperature) = gas.read_number_id(byond_string!("oxidation_temperature")) 160 | { 161 | FireInfo::Oxidation(OxidationInfo { 162 | temperature, 163 | power: gas.read_number_id(byond_string!("oxidation_rate"))?, 164 | }) 165 | } else if let Ok(temperature) = 166 | gas.read_number_id(byond_string!("fire_temperature")) 167 | { 168 | FireInfo::Fuel(FuelInfo { 169 | temperature, 170 | burn_rate: gas.read_number_id(byond_string!("fire_burn_rate"))?, 171 | }) 172 | } else { 173 | FireInfo::None 174 | } 175 | }, 176 | fire_products: gas 177 | .read_var_id(byond_string!("fire_products")) 178 | .ok() 179 | .and_then(|product_info| { 180 | if product_info.is_list() { 181 | Some(FireProductInfo::Generic( 182 | product_info 183 | .iter() 184 | .unwrap() 185 | .filter_map(|(k, v)| { 186 | k.get_string().ok().and_then(|s_str| { 187 | v.get_number() 188 | .ok() 189 | .map(|amt| (GasRef::Deferred(s_str), amt)) 190 | }) 191 | }) 192 | .collect(), 193 | )) 194 | } else if product_info.is_num() { 195 | Some(FireProductInfo::Plasma) // if we add another snowflake later, add it, but for now we hack this in 196 | } else { 197 | None 198 | } 199 | }), 200 | enthalpy: gas 201 | .read_number_id(byond_string!("enthalpy")) 202 | .unwrap_or_default(), 203 | fire_radiation_released: gas 204 | .read_number_id(byond_string!("fire_radiation_released")) 205 | .unwrap_or_default(), 206 | }) 207 | } 208 | } 209 | 210 | static GAS_INFO_BY_STRING: RwLock, GasType, FxBuildHasher>>> = 211 | const_rwlock(None); 212 | 213 | static GAS_INFO_BY_IDX: RwLock>> = const_rwlock(None); 214 | 215 | static GAS_SPECIFIC_HEATS: RwLock>> = const_rwlock(None); 216 | 217 | #[byondapi::init] 218 | pub fn initialize_gas_info_structs() { 219 | *GAS_INFO_BY_STRING.write() = Some(DashMap::with_hasher(FxBuildHasher)); 220 | *GAS_INFO_BY_IDX.write() = Some(Vec::new()); 221 | *GAS_SPECIFIC_HEATS.write() = Some(Vec::new()); 222 | } 223 | 224 | pub fn destroy_gas_info_structs() { 225 | crate::turfs::wait_for_tasks(); 226 | GAS_INFO_BY_STRING.write().as_mut().unwrap().clear(); 227 | GAS_INFO_BY_IDX.write().as_mut().unwrap().clear(); 228 | GAS_SPECIFIC_HEATS.write().as_mut().unwrap().clear(); 229 | TOTAL_NUM_GASES.store(0, Ordering::Release); 230 | CACHED_GAS_IDS.with_borrow_mut(|gas_ids| { 231 | gas_ids.clear(); 232 | }); 233 | CACHED_IDX_TO_STRINGS.with_borrow_mut(|gas_ids| { 234 | gas_ids.clear(); 235 | }); 236 | } 237 | /// For registering gases, do not touch this. 238 | #[byondapi::bind("/proc/_auxtools_register_gas")] 239 | fn hook_register_gas(gas: ByondValue) -> Result { 240 | let gas_id = gas.read_string_id(byond_string!("id"))?; 241 | match GAS_INFO_BY_STRING 242 | .read() 243 | .as_ref() 244 | .unwrap() 245 | .get_mut(&gas_id as &str) 246 | { 247 | Some(mut old_gas) => { 248 | let gas_cache = GasType::new(&gas, old_gas.idx)?; 249 | *old_gas = gas_cache.clone(); 250 | GAS_SPECIFIC_HEATS.write().as_mut().unwrap()[old_gas.idx] = gas_cache.specific_heat; 251 | GAS_INFO_BY_IDX.write().as_mut().unwrap()[old_gas.idx] = gas_cache; 252 | } 253 | None => { 254 | let gas_cache = GasType::new(&gas, TOTAL_NUM_GASES.load(Ordering::Acquire))?; 255 | let cached_id = gas_id.clone(); 256 | let cached_idx = gas_cache.idx; 257 | GAS_INFO_BY_STRING 258 | .read() 259 | .as_ref() 260 | .unwrap() 261 | .insert(gas_id.into_boxed_str(), gas_cache.clone()); 262 | GAS_SPECIFIC_HEATS 263 | .write() 264 | .as_mut() 265 | .unwrap() 266 | .push(gas_cache.specific_heat); 267 | GAS_INFO_BY_IDX.write().as_mut().unwrap().push(gas_cache); 268 | CACHED_IDX_TO_STRINGS 269 | .with_borrow_mut(|map| map.insert(cached_idx, cached_id.into_boxed_str())); 270 | TOTAL_NUM_GASES.fetch_add(1, Ordering::Release); // this is the only thing that stores it other than shutdown 271 | } 272 | } 273 | Ok(ByondValue::null()) 274 | } 275 | 276 | /// Registers gases, and get reaction infos for auxmos, only call when ssair is initing. 277 | #[byondapi::bind("/proc/auxtools_atmos_init")] 278 | fn hook_init(gas_data: ByondValue) -> Result { 279 | let data = gas_data.read_var_id(byond_string!("datums"))?; 280 | data.iter()? 281 | .map(|(_, gas)| hook_register_gas(gas)) 282 | .try_for_each(|res| res.map(drop)) 283 | .wrap_err("auxtools_atmos_init failed to register gas")?; 284 | *REACTION_INFO.write() = Some(get_reaction_info()); 285 | Ok(true.into()) 286 | } 287 | 288 | fn get_reaction_info() -> BTreeMap { 289 | let gas_reactions = ByondValue::new_global_ref() 290 | .read_var_id(byond_string!("SSair")) 291 | .unwrap() 292 | .read_var_id(byond_string!("gas_reactions")) 293 | .unwrap(); 294 | let mut reaction_cache: BTreeMap = Default::default(); 295 | let sender = byond_callback_sender(); 296 | for (reaction, _) in gas_reactions.iter().unwrap() { 297 | match Reaction::from_byond_reaction(reaction) { 298 | Ok(reaction) => { 299 | if let std::collections::btree_map::Entry::Vacant(e) = 300 | reaction_cache.entry(reaction.get_priority()) 301 | { 302 | e.insert(reaction); 303 | } else { 304 | drop(sender.try_send(Box::new(move || { 305 | Err(eyre::eyre!(format!( 306 | "Duplicate reaction priority {}, this reaction will be ignored!", 307 | reaction.get_priority().0 308 | ))) 309 | }))); 310 | } 311 | } 312 | //maybe awful error handling 313 | Err(runtime) => { 314 | drop(sender.try_send(Box::new(move || Err(runtime)))); 315 | } 316 | } 317 | } 318 | reaction_cache 319 | } 320 | 321 | /// For updating reaction informations for auxmos, only call this when it is changed. 322 | #[byondapi::bind("/datum/controller/subsystem/air/proc/auxtools_update_reactions")] 323 | fn update_reactions() -> Result { 324 | *REACTION_INFO.write() = Some(get_reaction_info()); 325 | Ok(true.into()) 326 | } 327 | 328 | /// Calls the given closure with all reaction info as an argument. 329 | /// # Panics 330 | /// If reactions aren't loaded yet. 331 | pub fn with_reactions(mut f: F) -> T 332 | where 333 | F: FnMut(&BTreeMap) -> T, 334 | { 335 | f(REACTION_INFO 336 | .read() 337 | .as_ref() 338 | .unwrap_or_else(|| panic!("Reactions not loaded yet! Uh oh!"))) 339 | } 340 | 341 | /// Runs the given closure with the global specific heats vector locked. 342 | /// # Panics 343 | /// If gas info isn't loaded yet. 344 | pub fn with_specific_heats(f: impl FnOnce(&[f32]) -> T) -> T { 345 | f(GAS_SPECIFIC_HEATS.read().as_ref().unwrap().as_slice()) 346 | } 347 | 348 | /// Gets the fusion power of the given gas. 349 | /// # Panics 350 | /// If gas info isn't loaded yet. 351 | #[cfg(feature = "reaction_hooks")] 352 | #[must_use] 353 | pub fn gas_fusion_power(idx: &GasIDX) -> f32 { 354 | GAS_INFO_BY_IDX 355 | .read() 356 | .as_ref() 357 | .unwrap_or_else(|| panic!("Gases not loaded yet! Uh oh!")) 358 | .get(*idx) 359 | .unwrap() 360 | .fusion_power 361 | } 362 | 363 | /// Returns the total number of gases in use. Only used by gas mixtures; should probably stay that way. 364 | pub fn total_num_gases() -> GasIDX { 365 | TOTAL_NUM_GASES.load(Ordering::Acquire) 366 | } 367 | 368 | /// Gets the gas visibility threshold for the given gas ID. 369 | /// # Panics 370 | /// If gas info isn't loaded yet. 371 | #[must_use] 372 | pub fn gas_visibility(idx: usize) -> Option { 373 | GAS_INFO_BY_IDX 374 | .read() 375 | .as_ref() 376 | .unwrap_or_else(|| panic!("Gases not loaded yet! Uh oh!")) 377 | .get(idx) 378 | .unwrap() 379 | .moles_visible 380 | } 381 | 382 | /// Gets a copy of all the gas visibilities. 383 | /// # Panics 384 | /// If gas info isn't loaded yet. 385 | #[must_use] 386 | pub fn visibility_copies() -> Box<[Option]> { 387 | GAS_INFO_BY_IDX 388 | .read() 389 | .as_ref() 390 | .unwrap_or_else(|| panic!("Gases not loaded yet! Uh oh!")) 391 | .iter() 392 | .map(|g| g.moles_visible) 393 | .collect::>() 394 | .into_boxed_slice() 395 | } 396 | 397 | /// Allows one to run a closure with a lock on the global gas info vec. 398 | /// # Panics 399 | /// If gas info isn't loaded yet. 400 | pub fn with_gas_info(f: impl FnOnce(&[GasType]) -> T) -> T { 401 | f(GAS_INFO_BY_IDX 402 | .read() 403 | .as_ref() 404 | .unwrap_or_else(|| panic!("Gases not loaded yet! Uh oh!"))) 405 | } 406 | 407 | /// Updates all the `GasRef`s in the global gas info vec with proper indices instead of strings. 408 | /// # Panics 409 | /// If gas info is not loaded yet. 410 | pub fn update_gas_refs() { 411 | GAS_INFO_BY_IDX 412 | .write() 413 | .as_mut() 414 | .unwrap_or_else(|| panic!("Gases not loaded yet! Uh oh!")) 415 | .iter_mut() 416 | .for_each(|gas| { 417 | if let Some(FireProductInfo::Generic(products)) = gas.fire_products.as_mut() { 418 | for product in products.iter_mut() { 419 | product.0.update().unwrap(); 420 | } 421 | } 422 | }); 423 | } 424 | /// For updating reagent gas fire products, do not use for now. 425 | #[byondapi::bind("/proc/finalize_gas_refs")] 426 | fn finalize_gas_refs() -> Result { 427 | update_gas_refs(); 428 | Ok(ByondValue::null()) 429 | } 430 | 431 | thread_local! { 432 | static CACHED_GAS_IDS: RefCell> = const { RefCell::new(HashMap::with_hasher(FxBuildHasher)) }; 433 | static CACHED_IDX_TO_STRINGS: RefCell, FxBuildHasher>> = const { RefCell::new(HashMap::with_hasher(FxBuildHasher)) }; 434 | } 435 | 436 | /// Returns the appropriate index to be used by auxmos for a given ID string. 437 | /// # Errors 438 | /// If gases aren't loaded or an invalid gas ID is given. 439 | pub fn gas_idx_from_string(id: &str) -> Result { 440 | Ok(GAS_INFO_BY_STRING 441 | .read() 442 | .as_ref() 443 | .ok_or_else(|| eyre::eyre!("Gases not loaded yet! Uh oh!"))? 444 | .get(id) 445 | .ok_or_else(|| eyre::eyre!("Invalid gas ID: {id}"))? 446 | .idx) 447 | } 448 | 449 | /// Returns the appropriate index to be used by the game for a given Byond string. 450 | /// # Errors 451 | /// If the given string is not a string or is not a valid gas ID. 452 | pub fn gas_idx_from_value(string_val: &ByondValue) -> Result { 453 | CACHED_GAS_IDS.with_borrow_mut(|cache| { 454 | if let Some(idx) = cache.get(&string_val.get_strid().unwrap()) { 455 | Ok(*idx) 456 | } else { 457 | let id = &string_val.get_string()?; 458 | let idx = gas_idx_from_string(id)?; 459 | cache.insert(string_val.get_strid().unwrap(), idx); 460 | Ok(idx) 461 | } 462 | }) 463 | } 464 | 465 | /// Takes an index and returns a borrowed string representing the string ID of the gas datum stored in that index. 466 | /// # Panics 467 | /// If an invalid gas index is given to this. This should never happen, so we panic instead of runtiming. 468 | pub fn gas_idx_to_id(idx: GasIDX) -> ByondValue { 469 | CACHED_IDX_TO_STRINGS.with_borrow(|stuff| { 470 | ByondValue::new_str( 471 | stuff 472 | .get(&idx) 473 | .unwrap_or_else(|| panic!("Invalid gas index: {idx}")) 474 | .as_ref(), 475 | ) 476 | .unwrap_or_else(|_| panic!("Cannot convert gas index to byond string: {idx}")) 477 | }) 478 | } 479 | 480 | #[cfg(test)] 481 | pub fn register_gas_manually(gas_id: &'static str, specific_heat: f32) { 482 | let gas_cache = GasType { 483 | idx: total_num_gases(), 484 | id: gas_id.into(), 485 | name: gas_id.into(), 486 | flags: 0, 487 | specific_heat, 488 | fusion_power: 0.0, 489 | moles_visible: None, 490 | enthalpy: 0.0, 491 | fire_radiation_released: 0.0, 492 | fire_info: FireInfo::None, 493 | fire_products: None, 494 | }; 495 | let cached_idx = gas_cache.idx; 496 | GAS_INFO_BY_STRING 497 | .read() 498 | .as_ref() 499 | .unwrap() 500 | .insert(gas_id.into(), gas_cache.clone()); 501 | 502 | GAS_SPECIFIC_HEATS 503 | .write() 504 | .as_mut() 505 | .unwrap() 506 | .push(gas_cache.specific_heat); 507 | GAS_INFO_BY_IDX.write().as_mut().unwrap().push(gas_cache); 508 | CACHED_IDX_TO_STRINGS.with_borrow_mut(|map| map.insert(cached_idx, gas_id.into())); 509 | TOTAL_NUM_GASES.fetch_add(1, Ordering::Release); // this is the only thing that stores it other than shutdown 510 | } 511 | 512 | #[cfg(test)] 513 | pub fn set_gas_statics_manually() { 514 | initialize_gas_info_structs(); 515 | } 516 | 517 | #[cfg(test)] 518 | pub fn destroy_gas_statics() { 519 | destroy_gas_info_structs(); 520 | } 521 | -------------------------------------------------------------------------------- /src/turfs.rs: -------------------------------------------------------------------------------- 1 | pub mod groups; 2 | pub mod processing; 3 | /* 4 | #[cfg(feature = "monstermos")] 5 | mod monstermos; 6 | #[cfg(feature = "putnamos")] 7 | mod putnamos; 8 | */ 9 | #[cfg(feature = "katmos")] 10 | pub mod katmos; 11 | #[cfg(feature = "superconductivity")] 12 | mod superconduct; 13 | 14 | use crate::{constants::*, gas::Mixture, GasArena}; 15 | use bitflags::bitflags; 16 | use byondapi::prelude::*; 17 | use eyre::{Context, Result}; 18 | use indexmap::IndexMap; 19 | use parking_lot::{const_rwlock, RwLock, RwLockUpgradableReadGuard}; 20 | use petgraph::{graph::NodeIndex, stable_graph::StableDiGraph, visit::EdgeRef, Direction}; 21 | use rayon::prelude::*; 22 | use rustc_hash::FxBuildHasher; 23 | use std::hash::{Hash, Hasher}; 24 | use std::time::Duration; 25 | use std::{mem::drop, sync::atomic::AtomicU64}; 26 | 27 | bitflags! { 28 | #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] 29 | pub struct Directions: u8 { 30 | const NORTH = 0b1; 31 | const SOUTH = 0b10; 32 | const EAST = 0b100; 33 | const WEST = 0b1000; 34 | const UP = 0b10000; 35 | const DOWN = 0b100000; 36 | const ALL_CARDINALS = Self::NORTH.bits() | Self::SOUTH.bits() | Self::EAST.bits() | Self::WEST.bits(); 37 | const ALL_CARDINALS_MULTIZ = Self::NORTH.bits() | Self::SOUTH.bits() | Self::EAST.bits() | Self::WEST.bits() | Self::UP.bits() | Self::DOWN.bits(); 38 | } 39 | 40 | #[derive(Default, Debug)] 41 | pub struct SimulationFlags: u8 { 42 | const SIMULATION_DIFFUSE = 0b1; 43 | const SIMULATION_ALL = 0b10; 44 | const SIMULATION_ANY = Self::SIMULATION_DIFFUSE.bits() | Self::SIMULATION_ALL.bits(); 45 | } 46 | 47 | #[derive(Default, Debug)] 48 | pub struct AdjacentFlags: u8 { 49 | const ATMOS_ADJACENT_FIRELOCK = 0b10; 50 | } 51 | 52 | #[derive(Default, Debug, Clone, Copy)] 53 | pub struct DirtyFlags: u8 { 54 | const DIRTY_MIX_REF = 0b1; 55 | const DIRTY_ADJACENT = 0b10; 56 | const DIRTY_ADJACENT_TO_SPACE = 0b100; 57 | } 58 | } 59 | 60 | #[allow(unused)] 61 | const fn adj_flag_to_idx(adj_flag: Directions) -> u8 { 62 | match adj_flag { 63 | Directions::NORTH => 0, 64 | Directions::SOUTH => 1, 65 | Directions::EAST => 2, 66 | Directions::WEST => 3, 67 | Directions::UP => 4, 68 | Directions::DOWN => 5, 69 | _ => 6, 70 | } 71 | } 72 | 73 | #[allow(unused)] 74 | const fn idx_to_adj_flag(idx: u8) -> Directions { 75 | match idx { 76 | 0 => Directions::NORTH, 77 | 1 => Directions::SOUTH, 78 | 2 => Directions::EAST, 79 | 3 => Directions::WEST, 80 | 4 => Directions::UP, 81 | 5 => Directions::DOWN, 82 | _ => Directions::from_bits_truncate(0), 83 | } 84 | } 85 | 86 | type TurfID = u32; 87 | 88 | // TurfMixture can be treated as "immutable" for all intents and purposes--put other data somewhere else 89 | #[derive(Default, Debug)] 90 | struct TurfMixture { 91 | pub mix: usize, 92 | pub id: TurfID, 93 | pub flags: SimulationFlags, 94 | pub planetary_atmos: Option, 95 | pub vis_hash: AtomicU64, 96 | } 97 | 98 | #[allow(dead_code)] 99 | impl TurfMixture { 100 | /// Whether the turf is processed at all or not 101 | pub fn enabled(&self) -> bool { 102 | self.flags.intersects(SimulationFlags::SIMULATION_ANY) 103 | } 104 | 105 | /// Whether the turf's gas is immutable or not, see [`super::gas::Mixture`] 106 | pub fn is_immutable(&self) -> bool { 107 | GasArena::with_all_mixtures(|all_mixtures| { 108 | all_mixtures 109 | .get(self.mix) 110 | .unwrap_or_else(|| panic!("Gas mixture not found for turf: {}", self.mix)) 111 | .read() 112 | .is_immutable() 113 | }) 114 | } 115 | /// Returns the pressure of the turf's gas, see [`super::gas::Mixture`] 116 | pub fn return_pressure(&self) -> f32 { 117 | GasArena::with_all_mixtures(|all_mixtures| { 118 | all_mixtures 119 | .get(self.mix) 120 | .unwrap_or_else(|| panic!("Gas mixture not found for turf: {}", self.mix)) 121 | .read() 122 | .return_pressure() 123 | }) 124 | } 125 | /// Returns the temperature of the turf's gas, see [`super::gas::Mixture`] 126 | pub fn return_temperature(&self) -> f32 { 127 | GasArena::with_all_mixtures(|all_mixtures| { 128 | all_mixtures 129 | .get(self.mix) 130 | .unwrap_or_else(|| panic!("Gas mixture not found for turf: {}", self.mix)) 131 | .read() 132 | .get_temperature() 133 | }) 134 | } 135 | /// Returns the total moles of the turf's gas, see [`super::gas::Mixture`] 136 | pub fn total_moles(&self) -> f32 { 137 | GasArena::with_all_mixtures(|all_mixtures| { 138 | all_mixtures 139 | .get(self.mix) 140 | .unwrap_or_else(|| panic!("Gas mixture not found for turf: {}", self.mix)) 141 | .read() 142 | .total_moles() 143 | }) 144 | } 145 | /// Clears the turf's airs, see [`super::gas::Mixture`] 146 | pub fn clear_air(&self) { 147 | GasArena::with_all_mixtures(|all_mixtures| { 148 | all_mixtures 149 | .get(self.mix) 150 | .unwrap_or_else(|| panic!("Gas mixture not found for turf: {}", self.mix)) 151 | .write() 152 | .clear(); 153 | }); 154 | } 155 | /// Copies from a given gas mixture to the turf's airs, see [`super::gas::Mixture`] 156 | pub fn copy_from_mutable(&self, sample: &Mixture) { 157 | GasArena::with_all_mixtures(|all_mixtures| { 158 | all_mixtures 159 | .get(self.mix) 160 | .unwrap_or_else(|| panic!("Gas mixture not found for turf: {}", self.mix)) 161 | .write() 162 | .copy_from_mutable(sample); 163 | }); 164 | } 165 | /// Clears a number of moles from the turf's air 166 | /// If the number of moles is greater than the turf's total moles, just clears the turf 167 | pub fn clear_moles(&self, amt: f32) { 168 | GasArena::with_all_mixtures(|all_mixtures| { 169 | let moles = all_mixtures 170 | .get(self.mix) 171 | .unwrap_or_else(|| panic!("Gas mixture not found for turf: {}", self.mix)) 172 | .read() 173 | .total_moles(); 174 | if amt >= moles { 175 | all_mixtures 176 | .get(self.mix) 177 | .unwrap_or_else(|| panic!("Gas mixture not found for turf: {}", self.mix)) 178 | .write() 179 | .clear(); 180 | } else { 181 | drop( 182 | all_mixtures 183 | .get(self.mix) 184 | .unwrap_or_else(|| panic!("Gas mixture not found for turf: {}", self.mix)) 185 | .write() 186 | .remove(amt), 187 | ); 188 | } 189 | }); 190 | } 191 | /// Gets a copy of the turf's airs, see [`super::gas::Mixture`] 192 | pub fn get_gas_copy(&self) -> Mixture { 193 | let mut ret: Mixture = Mixture::new(); 194 | GasArena::with_all_mixtures(|all_mixtures| { 195 | let to_copy = all_mixtures 196 | .get(self.mix) 197 | .unwrap_or_else(|| panic!("Gas mixture not found for turf: {}", self.mix)) 198 | .read(); 199 | ret.copy_from_mutable(&to_copy); 200 | ret.volume = to_copy.volume; 201 | }); 202 | ret 203 | } 204 | /// Invalidates the turf's visibility cache 205 | /// This turf will most likely be visually updated the next processing cycle 206 | /// If that is even running 207 | pub fn invalidate_vis_cache(&self) { 208 | self.vis_hash.store(0, std::sync::atomic::Ordering::Relaxed); 209 | } 210 | } 211 | 212 | type TurfGraphMap = IndexMap; 213 | 214 | //adjacency/turf infos goes here 215 | #[derive(Debug)] 216 | struct TurfGases { 217 | graph: StableDiGraph, 218 | map: TurfGraphMap, 219 | } 220 | 221 | impl TurfGases { 222 | pub fn insert_turf(&mut self, tmix: TurfMixture) { 223 | if let Some(&node_id) = self.map.get(&tmix.id) { 224 | let thin = self.graph.node_weight_mut(node_id).unwrap(); 225 | *thin = tmix 226 | } else { 227 | self.map.insert(tmix.id, self.graph.add_node(tmix)); 228 | } 229 | } 230 | pub fn remove_turf(&mut self, id: TurfID) { 231 | if let Some(index) = self.map.shift_remove(&id) { 232 | self.graph.remove_node(index); 233 | } 234 | } 235 | pub fn update_adjacencies(&mut self, idx: TurfID, adjacent_list: ByondValue) -> Result<()> { 236 | if let Some(&this_index) = self.map.get(&idx) { 237 | self.remove_adjacencies(this_index); 238 | adjacent_list 239 | .iter()? 240 | .filter_map(|(k, v)| Some((k.get_ref().ok()?, v.get_number().unwrap_or(0.0) as u8))) 241 | .filter_map(|(adj_ref, flag)| Some((self.map.get(&adj_ref)?, flag))) 242 | .for_each(|(adj_index, flag)| { 243 | let flags = AdjacentFlags::from_bits_truncate(flag); 244 | self.graph.add_edge(this_index, *adj_index, flags); 245 | }) 246 | }; 247 | Ok(()) 248 | } 249 | 250 | pub fn remove_adjacencies(&mut self, index: NodeIndex) { 251 | let edges = self 252 | .graph 253 | .edges(index) 254 | .map(|edgeref| edgeref.id()) 255 | .collect::>(); 256 | edges.into_iter().for_each(|edgeindex| { 257 | self.graph.remove_edge(edgeindex); 258 | }); 259 | } 260 | 261 | pub fn get(&self, idx: NodeIndex) -> Option<&TurfMixture> { 262 | self.graph.node_weight(idx) 263 | } 264 | 265 | #[allow(unused)] 266 | pub fn get_from_id(&self, idx: TurfID) -> Option<&TurfMixture> { 267 | self.map 268 | .get(&idx) 269 | .and_then(|&idx| self.graph.node_weight(idx)) 270 | } 271 | 272 | #[allow(unused)] 273 | pub fn get_id(&self, idx: TurfID) -> Option { 274 | self.map.get(&idx).copied() 275 | } 276 | 277 | pub fn adjacent_node_ids(&self, index: NodeIndex) -> impl Iterator + '_ { 278 | self.graph.neighbors(index) 279 | } 280 | 281 | #[allow(unused)] 282 | pub fn adjacent_turf_ids(&self, index: NodeIndex) -> impl Iterator + '_ { 283 | self.graph 284 | .neighbors(index) 285 | .filter_map(|index| Some(self.get(index)?.id)) 286 | } 287 | 288 | #[allow(unused)] 289 | pub fn adjacent_node_ids_enabled( 290 | &self, 291 | index: NodeIndex, 292 | ) -> impl Iterator + '_ { 293 | self.graph.neighbors(index).filter(|&adj_index| { 294 | self.graph 295 | .node_weight(adj_index) 296 | .map_or(false, |mix| mix.enabled()) 297 | }) 298 | } 299 | 300 | pub fn adjacent_mixes<'a>( 301 | &'a self, 302 | index: NodeIndex, 303 | all_mixtures: &'a [parking_lot::RwLock], 304 | ) -> impl Iterator> { 305 | self.graph 306 | .neighbors(index) 307 | .filter_map(|neighbor| self.graph.node_weight(neighbor)) 308 | .filter_map(move |idx| all_mixtures.get(idx.mix)) 309 | } 310 | 311 | pub fn adjacent_mixes_with_adj_ids<'a>( 312 | &'a self, 313 | index: NodeIndex, 314 | all_mixtures: &'a [parking_lot::RwLock], 315 | dir: Direction, 316 | ) -> impl Iterator)> { 317 | self.graph 318 | .neighbors_directed(index, dir) 319 | .filter_map(|neighbor| self.graph.node_weight(neighbor)) 320 | .filter_map(move |idx| Some((&idx.id, all_mixtures.get(idx.mix)?))) 321 | } 322 | pub fn clear(&mut self) { 323 | self.graph.clear(); 324 | self.map.clear(); 325 | } 326 | 327 | /* 328 | pub fn adjacent_infos( 329 | &self, 330 | index: NodeIndex, 331 | dir: Direction, 332 | ) -> impl Iterator { 333 | self.graph 334 | .neighbors_directed(index, dir) 335 | .filter_map(|neighbor| self.graph.node_weight(neighbor)) 336 | } 337 | 338 | pub fn adjacent_ids<'a>(&'a self, idx: TurfID) -> impl Iterator { 339 | self.graph 340 | .neighbors(*self.map.get(&idx).unwrap()) 341 | .filter_map(|index| self.graph.node_weight(index)) 342 | .map(|tmix| &tmix.id) 343 | } 344 | pub fn adjacents_enabled<'a>(&'a self, idx: TurfID) -> impl Iterator { 345 | self.graph 346 | .neighbors(*self.map.get(&idx).unwrap()) 347 | .filter_map(|index| self.graph.node_weight(index)) 348 | .filter(|tmix| tmix.enabled()) 349 | .map(|tmix| &tmix.id) 350 | } 351 | pub fn get_mixture(&self, idx: TurfID) -> Option { 352 | self.mixtures.read().get(&idx).cloned() 353 | } 354 | */ 355 | } 356 | 357 | static TURF_GASES: RwLock> = const_rwlock(None); 358 | 359 | // We store planetary atmos by hash of the initial atmos string here for speed. 360 | static PLANETARY_ATMOS: RwLock>> = const_rwlock(None); 361 | 362 | //whether there is any tasks running 363 | static TASKS: RwLock<()> = const_rwlock(()); 364 | 365 | pub fn wait_for_tasks() { 366 | match TASKS.try_write_for(Duration::from_secs(5)) { 367 | Some(_) => (), 368 | None => panic!( 369 | "Threads failed to release resources within 5 seconds, this may indicate a deadlock!" 370 | ), 371 | } 372 | } 373 | #[byondapi::init] 374 | pub fn initialize_turfs() { 375 | // 10x 255x255 zlevels 376 | // double that for edges since each turf can have up to 6 edges but eehhhh 377 | *TURF_GASES.write() = Some(TurfGases { 378 | graph: StableDiGraph::with_capacity(650_250, 1_300_500), 379 | map: IndexMap::with_capacity_and_hasher(650_250, FxBuildHasher), 380 | }); 381 | *PLANETARY_ATMOS.write() = Some(Default::default()); 382 | } 383 | 384 | pub fn shutdown_turfs() { 385 | wait_for_tasks(); 386 | TURF_GASES.write().as_mut().unwrap().clear(); 387 | PLANETARY_ATMOS.write().as_mut().unwrap().clear(); 388 | } 389 | 390 | fn with_turf_gases_read(f: F) -> T 391 | where 392 | F: FnOnce(&TurfGases) -> T, 393 | { 394 | f(TURF_GASES.read().as_ref().unwrap()) 395 | } 396 | 397 | fn with_turf_gases_write(f: F) -> T 398 | where 399 | F: FnOnce(&mut TurfGases) -> T, 400 | { 401 | f(TURF_GASES.write().as_mut().unwrap()) 402 | } 403 | 404 | fn with_planetary_atmos(f: F) -> T 405 | where 406 | F: FnOnce(&IndexMap) -> T, 407 | { 408 | f(PLANETARY_ATMOS.read().as_ref().unwrap()) 409 | } 410 | 411 | fn with_planetary_atmos_upgradeable_read(f: F) -> T 412 | where 413 | F: FnOnce(RwLockUpgradableReadGuard<'_, Option>>) -> T, 414 | { 415 | f(PLANETARY_ATMOS.upgradable_read()) 416 | } 417 | 418 | /// Returns: null. Updates turf air infos, whether the turf is closed, is space or a regular turf, or even a planet turf is decided here. 419 | #[byondapi::bind("/turf/proc/update_air_ref")] 420 | fn hook_register_turf(src: ByondValue, flag: ByondValue) -> Result { 421 | let id = src.get_ref()?; 422 | let flag = flag.get_number()? as i32; 423 | if let Ok(blocks) = src.read_number_id(byond_string!("blocks_air")) { 424 | if blocks > 0.0 { 425 | with_turf_gases_write(|arena| arena.remove_turf(id)); 426 | #[cfg(feature = "superconductivity")] 427 | superconduct::supercond_update_ref(src)?; 428 | return Ok(ByondValue::null()); 429 | } 430 | } 431 | if flag >= 0 { 432 | let mut to_insert: TurfMixture = TurfMixture::default(); 433 | let air = src.read_var_id(byond_string!("air"))?; 434 | to_insert.mix = air.read_number_id(byond_string!("_extools_pointer_gasmixture"))? as usize; 435 | to_insert.flags = SimulationFlags::from_bits_truncate(flag as u8); 436 | to_insert.id = id; 437 | 438 | if let Ok(is_planet) = src.read_number_id(byond_string!("planetary_atmos")) { 439 | if is_planet != 0.0 { 440 | if let Ok(at_str) = src.read_string_id(byond_string!("initial_gas_mix")) { 441 | with_planetary_atmos_upgradeable_read(|lock| { 442 | to_insert.planetary_atmos = Some({ 443 | let mut state = rustc_hash::FxHasher::default(); 444 | at_str.hash(&mut state); 445 | state.finish() as u32 446 | }); 447 | if lock 448 | .as_ref() 449 | .unwrap() 450 | .contains_key(&to_insert.planetary_atmos.unwrap()) 451 | { 452 | return; 453 | } 454 | 455 | let mut write = 456 | parking_lot::lock_api::RwLockUpgradableReadGuard::upgrade(lock); 457 | 458 | write 459 | .as_mut() 460 | .unwrap() 461 | .insert(to_insert.planetary_atmos.unwrap(), { 462 | let mut gas = to_insert.get_gas_copy(); 463 | gas.mark_immutable(); 464 | gas 465 | }); 466 | }); 467 | } 468 | } 469 | } 470 | with_turf_gases_write(|arena| arena.insert_turf(to_insert)); 471 | } else { 472 | with_turf_gases_write(|arena| arena.remove_turf(id)); 473 | } 474 | 475 | #[cfg(feature = "superconductivity")] 476 | superconduct::supercond_update_ref(src)?; 477 | Ok(ByondValue::null()) 478 | } 479 | 480 | /* will come back to you later 481 | const PLANET_TURF: i32 = 1; 482 | const SPACE_TURF: i32 = 0; 483 | const CLOSED_TURF: i32 = -1; 484 | const OPEN_TURF: i32 = 2; 485 | 486 | //hardcoded because we can't have nice things 487 | fn determine_turf_flag(src: &ByondValue) -> i32 { 488 | let path = src 489 | .read_string_id(byond_string!("("type") 490 | .unwrap_or_else(|_| "TYPPENOTFOUND".to_string()); 491 | if !path.as_str().starts_with("/turf/open") { 492 | CLOSED_TURF 493 | } else if src.read_number_id(byond_string!("planetary_atmos")).unwrap_or(0.0) > 0.0 { 494 | PLANET_TURF 495 | } else if path.as_str().starts_with("/turf/open/space") { 496 | SPACE_TURF 497 | } else { 498 | OPEN_TURF 499 | } 500 | } 501 | */ 502 | /// Updates adjacency infos for turfs, only use this in immediateupdateturfs. 503 | #[byondapi::bind("/turf/proc/__update_auxtools_turf_adjacency_info")] 504 | fn hook_infos(src: ByondValue) -> Result { 505 | let id = src.get_ref()?; 506 | with_turf_gases_write(|arena| -> Result<()> { 507 | if let Some(adjacent_list) = src 508 | .read_var_id(byond_string!("atmos_adjacent_turfs")) 509 | .ok() 510 | .and_then(|adjs| adjs.is_list().then_some(adjs)) 511 | { 512 | arena.update_adjacencies(id, adjacent_list)?; 513 | } else if let Some(&idx) = arena.map.get(&id) { 514 | arena.remove_adjacencies(idx); 515 | } 516 | Ok(()) 517 | })?; 518 | 519 | #[cfg(feature = "superconductivity")] 520 | superconduct::supercond_update_adjacencies(id)?; 521 | Ok(ByondValue::null()) 522 | } 523 | 524 | /// Updates the visual overlays for the given turf. 525 | /// Will use a cached overlay list if one exists. 526 | /// # Errors 527 | /// If auxgm wasn't implemented properly or there's an invalid gas mixture. 528 | fn update_visuals(src: ByondValue) -> Result { 529 | use super::gas; 530 | match src.read_var_id(byond_string!("air")) { 531 | Ok(air) if !air.is_null() => { 532 | // gas_overlays: list( GAS_ID = list( VIS_FACTORS = OVERLAYS )) got it? I don't 533 | let gas_overlays = ByondValue::new_global_ref() 534 | .read_var_id(byond_string!("GLOB")) 535 | .wrap_err("Unable to get GLOB from BYOND globals")? 536 | .read_var_id(byond_string!("gas_data")) 537 | .wrap_err("gas_data is undefined on GLOB")? 538 | .read_var_id(byond_string!("overlays")) 539 | .wrap_err("overlays is undefined in GLOB.gas_data")?; 540 | let ptr = air 541 | .read_var_id(byond_string!("_extools_pointer_gasmixture")) 542 | .wrap_err("air is undefined on turf")? 543 | .get_number() 544 | .wrap_err("Gas mixture has invalid pointer")? as usize; 545 | let overlay_types = GasArena::with_gas_mixture(ptr, |mix| { 546 | Ok(mix 547 | .enumerate() 548 | .filter_map(|(idx, moles)| Some((idx, moles, gas::types::gas_visibility(idx)?))) 549 | .filter(|(_, moles, amt)| moles > amt) 550 | // getting the list(VIS_FACTORS = OVERLAYS) with GAS_ID 551 | .filter_map(|(idx, moles, _)| { 552 | Some(( 553 | gas_overlays.read_list_index(gas::gas_idx_to_id(idx)).ok()?, 554 | moles, 555 | )) 556 | }) 557 | // getting the OVERLAYS with VIS_FACTOR 558 | .filter_map(|(this_overlay_list, moles)| { 559 | this_overlay_list 560 | .read_list_index(gas::mixture::visibility_step(moles) as f32) 561 | .ok() 562 | }) 563 | .collect::>()) 564 | })?; 565 | 566 | Ok(src 567 | .call_id( 568 | byond_string!("set_visuals"), 569 | &[overlay_types.as_slice().try_into()?], 570 | ) 571 | .wrap_err("Calling set_visuals")?) 572 | } 573 | // If air is null, clear the visuals 574 | Ok(_) => Ok(src 575 | .call_id(byond_string!("set_visuals"), &[]) 576 | .wrap_err("Calling set_visuals with no args")?), 577 | // If air is not defined, it must be a closed turf. Do .othing 578 | Err(_) => Ok(ByondValue::null()), 579 | } 580 | } 581 | 582 | const fn adjacent_tile_id(id: u8, i: TurfID, max_x: i32, max_y: i32) -> TurfID { 583 | let z_size = max_x * max_y; 584 | let i = i as i32; 585 | match id { 586 | 0 => (i + max_x) as TurfID, 587 | 1 => (i - max_x) as TurfID, 588 | 2 => (i + 1) as TurfID, 589 | 3 => (i - 1) as TurfID, 590 | 4 => (i + z_size) as TurfID, 591 | 5 => (i - z_size) as TurfID, 592 | _ => panic!("Invalid id passed to adjacent_tile_id!"), 593 | } 594 | } 595 | 596 | #[derive(Clone, Copy)] 597 | struct AdjacentTileIDs { 598 | adj: Directions, 599 | i: TurfID, 600 | max_x: i32, 601 | max_y: i32, 602 | count: u8, 603 | } 604 | 605 | impl Iterator for AdjacentTileIDs { 606 | type Item = (Directions, TurfID); 607 | 608 | fn next(&mut self) -> Option { 609 | loop { 610 | if self.count == 6 { 611 | return None; 612 | } 613 | //SAFETY: count can never be invalid 614 | let dir = Directions::from_bits_retain(1 << self.count); 615 | self.count += 1; 616 | if self.adj.contains(dir) { 617 | return Some(( 618 | dir, 619 | adjacent_tile_id(self.count - 1, self.i, self.max_x, self.max_y), 620 | )); 621 | } 622 | } 623 | } 624 | 625 | fn size_hint(&self) -> (usize, Option) { 626 | (0, Some(self.adj.bits().count_ones() as usize)) 627 | } 628 | } 629 | 630 | use std::iter::FusedIterator; 631 | 632 | impl FusedIterator for AdjacentTileIDs {} 633 | 634 | #[allow(unused)] 635 | fn adjacent_tile_ids(adj: Directions, i: TurfID, max_x: i32, max_y: i32) -> AdjacentTileIDs { 636 | AdjacentTileIDs { 637 | adj, 638 | i, 639 | max_x, 640 | max_y, 641 | count: 0, 642 | } 643 | } 644 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod gas; 2 | mod parser; 3 | mod reaction; 4 | #[cfg(feature = "turf_processing")] 5 | pub mod turfs; 6 | 7 | use byondapi::prelude::*; 8 | use eyre::Result; 9 | use gas::constants::{ReactionReturn, GAS_MIN_MOLES, MINIMUM_MOLES_DELTA_TO_MOVE}; 10 | use gas::{ 11 | amt_gases, constants, gas_idx_from_string, gas_idx_from_value, gas_idx_to_id, tot_gases, types, 12 | with_gas_info, with_mix, with_mix_mut, with_mixes, with_mixes_custom, with_mixes_mut, GasArena, 13 | Mixture, 14 | }; 15 | use reaction::react_by_id; 16 | 17 | #[global_allocator] 18 | static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; 19 | 20 | static _SIMD_DETECTED: ::std::sync::OnceLock = ::std::sync::OnceLock::new(); 21 | 22 | #[cfg(feature = "tracy")] 23 | #[byondapi::init] 24 | pub fn init_eyre() { 25 | use tracing_subscriber::layer::SubscriberExt; 26 | 27 | tracing::subscriber::set_global_default( 28 | tracing_subscriber::registry().with(tracing_tracy::TracyLayer::default()), 29 | ) 30 | .expect("setup tracy layer"); 31 | } 32 | 33 | /// Args: (ms). Runs callbacks until time limit is reached. If time limit is omitted, runs all callbacks. 34 | #[byondapi::bind("/proc/process_atmos_callbacks")] 35 | fn atmos_callback_handle(remaining: ByondValue) -> Result { 36 | auxcallback::callback_processing_hook(remaining) 37 | } 38 | 39 | /// Fills in the first unused slot in the gas mixtures vector, or adds another one, then sets the argument ByondValue to point to it. 40 | #[byondapi::bind("/datum/gas_mixture/proc/__gasmixture_register")] 41 | fn register_gasmixture_hook(src: ByondValue) -> Result { 42 | gas::GasArena::register_mix(src) 43 | } 44 | 45 | /// Adds the gas mixture's ID to the queue of mixtures that have been deleted, to be reused later. 46 | /// This version is only if auxcleanup is not being used; it should be called from /datum/gas_mixture/Del. 47 | #[byondapi::bind("/datum/gas_mixture/proc/__gasmixture_unregister")] 48 | fn unregister_gasmixture_hook(src: ByondValue) -> Result { 49 | gas::GasArena::unregister_mix(&src); 50 | Ok(ByondValue::null()) 51 | } 52 | 53 | /// Returns: Heat capacity, in J/K (probably). 54 | #[byondapi::bind("/datum/gas_mixture/proc/heat_capacity")] 55 | fn heat_cap_hook(src: ByondValue) -> Result { 56 | with_mix(&src, |mix| Ok(mix.heat_capacity().into())) 57 | } 58 | 59 | /// Args: (min_heat_cap). Sets the mix's minimum heat capacity. 60 | #[byondapi::bind("/datum/gas_mixture/proc/set_min_heat_capacity")] 61 | fn min_heat_cap_hook(src: ByondValue, arg_min: ByondValue) -> Result { 62 | let min = arg_min.get_number()?; 63 | with_mix_mut(&src, |mix| { 64 | mix.set_min_heat_capacity(min); 65 | Ok(ByondValue::null()) 66 | }) 67 | } 68 | 69 | /// Returns: Amount of substance, in moles. 70 | #[byondapi::bind("/datum/gas_mixture/proc/total_moles")] 71 | fn total_moles_hook(src: ByondValue) -> Result { 72 | with_mix(&src, |mix| Ok(mix.total_moles().into())) 73 | } 74 | 75 | /// Returns: the mix's pressure, in kilopascals. 76 | #[byondapi::bind("/datum/gas_mixture/proc/return_pressure")] 77 | fn return_pressure_hook(src: ByondValue) -> Result { 78 | with_mix(&src, |mix| Ok(mix.return_pressure().into())) 79 | } 80 | 81 | /// Returns: the mix's temperature, in kelvins. 82 | #[byondapi::bind("/datum/gas_mixture/proc/return_temperature")] 83 | fn return_temperature_hook(src: ByondValue) -> Result { 84 | with_mix(&src, |mix| Ok(mix.get_temperature().into())) 85 | } 86 | 87 | /// Returns: the mix's volume, in liters. 88 | #[byondapi::bind("/datum/gas_mixture/proc/return_volume")] 89 | fn return_volume_hook(src: ByondValue) -> Result { 90 | with_mix(&src, |mix| Ok(mix.volume.into())) 91 | } 92 | 93 | /// Returns: the mix's thermal energy, the product of the mixture's heat capacity and its temperature. 94 | #[byondapi::bind("/datum/gas_mixture/proc/thermal_energy")] 95 | fn thermal_energy_hook(src: ByondValue) -> Result { 96 | with_mix(&src, |mix| Ok(mix.thermal_energy().into())) 97 | } 98 | 99 | /// Args: (mixture). Merges the gas from the giver into src, without modifying the giver mix. 100 | #[byondapi::bind("/datum/gas_mixture/proc/merge")] 101 | fn merge_hook(src: ByondValue, giver: ByondValue) -> Result { 102 | with_mixes_custom(&src, &giver, |src_mix, giver_mix| { 103 | src_mix.write().merge(&giver_mix.read()); 104 | Ok(ByondValue::null()) 105 | }) 106 | } 107 | 108 | /// 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. 109 | #[byondapi::bind("/datum/gas_mixture/proc/__remove_ratio")] 110 | fn remove_ratio_hook( 111 | src: ByondValue, 112 | into: ByondValue, 113 | ratio_arg: ByondValue, 114 | ) -> Result { 115 | let ratio = ratio_arg.get_number().unwrap_or_default(); 116 | with_mixes_mut(&src, &into, |src_mix, into_mix| { 117 | src_mix.remove_ratio_into(ratio, into_mix); 118 | Ok(ByondValue::null()) 119 | }) 120 | } 121 | 122 | /// 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. 123 | #[byondapi::bind("/datum/gas_mixture/proc/__remove")] 124 | fn remove_hook(src: ByondValue, into: ByondValue, amount_arg: ByondValue) -> Result { 125 | let amount = amount_arg.get_number().unwrap_or_default(); 126 | with_mixes_mut(&src, &into, |src_mix, into_mix| { 127 | src_mix.remove_into(amount, into_mix); 128 | Ok(ByondValue::null()) 129 | }) 130 | } 131 | 132 | /// Arg: (mixture). Makes src into a copy of the argument mixture. 133 | #[byondapi::bind("/datum/gas_mixture/proc/copy_from")] 134 | fn copy_from_hook(src: ByondValue, giver: ByondValue) -> Result { 135 | with_mixes_custom(&src, &giver, |src_mix, giver_mix| { 136 | src_mix.write().copy_from_mutable(&giver_mix.read()); 137 | Ok(ByondValue::null()) 138 | }) 139 | } 140 | 141 | /// Args: (src, mixture, conductivity) or (src, conductivity, temperature, heat_capacity). Adjusts temperature of src based on parameters. Returns: temperature of sharer after sharing is complete. 142 | #[byondapi::bind_raw_args("/datum/gas_mixture/proc/temperature_share")] 143 | fn temperature_share_hook() -> Result { 144 | let arg_num = args.len(); 145 | match arg_num { 146 | 3 => with_mixes_mut(&args[0], &args[1], |src_mix, share_mix| { 147 | Ok(src_mix 148 | .temperature_share(share_mix, args[2].get_number().unwrap_or_default()) 149 | .into()) 150 | }), 151 | 4 => with_mix_mut(&args[0], |mix| { 152 | Ok(mix 153 | .temperature_share_non_gas( 154 | args[1].get_number().unwrap_or_default(), 155 | args[2].get_number().unwrap_or_default(), 156 | args[3].get_number().unwrap_or_default(), 157 | ) 158 | .into()) 159 | }), 160 | _ => Err(eyre::eyre!("Invalid args for temperature_share")), 161 | } 162 | } 163 | 164 | /// Returns: a list of the gases in the mixture, associated with their IDs. 165 | #[byondapi::bind("/datum/gas_mixture/proc/get_gases")] 166 | fn get_gases_hook(src: ByondValue) -> Result { 167 | with_mix(&src, |mix| { 168 | let mut gases_list = ByondValue::new_list()?; 169 | mix.for_each_gas(|idx, gas| { 170 | if gas > GAS_MIN_MOLES { 171 | gases_list.push_list(gas_idx_to_id(idx))?; 172 | } 173 | Ok(()) 174 | })?; 175 | 176 | Ok(gases_list) 177 | }) 178 | } 179 | 180 | /// Args: (temperature). Sets the temperature of the mixture. Will be set to 2.7 if it's too low. 181 | #[byondapi::bind("/datum/gas_mixture/proc/set_temperature")] 182 | fn set_temperature_hook(src: ByondValue, arg_temp: ByondValue) -> Result { 183 | let v = arg_temp.get_number()?; 184 | if v.is_finite() { 185 | with_mix_mut(&src, |mix| { 186 | mix.set_temperature(v.max(2.7)); 187 | Ok(ByondValue::null()) 188 | }) 189 | } else { 190 | Err(eyre::eyre!( 191 | "Attempted to set a temperature to a number that is NaN or infinite." 192 | )) 193 | } 194 | } 195 | 196 | /// Args: (gas_id). Returns the heat capacity from the given gas, in J/K (probably). 197 | #[byondapi::bind("/datum/gas_mixture/proc/partial_heat_capacity")] 198 | fn partial_heat_capacity(src: ByondValue, gas_id: ByondValue) -> Result { 199 | with_mix(&src, |mix| { 200 | Ok(mix 201 | .partial_heat_capacity(gas_idx_from_value(&gas_id)?) 202 | .into()) 203 | }) 204 | } 205 | 206 | /// Args: (volume). Sets the volume of the gas. 207 | #[byondapi::bind("/datum/gas_mixture/proc/set_volume")] 208 | fn set_volume_hook(src: ByondValue, vol_arg: ByondValue) -> Result { 209 | let volume = vol_arg.get_number()?; 210 | with_mix_mut(&src, |mix| { 211 | mix.volume = volume; 212 | Ok(ByondValue::null()) 213 | }) 214 | } 215 | 216 | /// Args: (gas_id). Returns: the amount of substance of the given gas, in moles. 217 | #[byondapi::bind("/datum/gas_mixture/proc/get_moles")] 218 | fn get_moles_hook(src: ByondValue, gas_id: ByondValue) -> Result { 219 | with_mix(&src, |mix| { 220 | Ok(mix.get_moles(gas_idx_from_value(&gas_id)?).into()) 221 | }) 222 | } 223 | 224 | /// Args: (gas_id, moles). Sets the amount of substance of the given gas, in moles. 225 | #[byondapi::bind("/datum/gas_mixture/proc/set_moles")] 226 | fn set_moles_hook(src: ByondValue, gas_id: ByondValue, amt_val: ByondValue) -> Result { 227 | let vf = amt_val.get_number()?; 228 | if !vf.is_finite() { 229 | return Err(eyre::eyre!("Attempted to set moles to NaN or infinity.")); 230 | } 231 | if vf < 0.0 { 232 | return Err(eyre::eyre!("Attempted to set moles to a negative number.")); 233 | } 234 | with_mix_mut(&src, |mix| { 235 | mix.set_moles(gas_idx_from_value(&gas_id)?, vf); 236 | Ok(ByondValue::null()) 237 | }) 238 | } 239 | /// 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. 240 | #[byondapi::bind("/datum/gas_mixture/proc/adjust_moles")] 241 | fn adjust_moles_hook( 242 | src: ByondValue, 243 | id_val: ByondValue, 244 | num_val: ByondValue, 245 | ) -> Result { 246 | let vf = num_val.get_number().unwrap_or_default(); 247 | with_mix_mut(&src, |mix| { 248 | mix.adjust_moles(gas_idx_from_value(&id_val)?, vf); 249 | Ok(ByondValue::null()) 250 | }) 251 | } 252 | 253 | /// 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. 254 | #[byondapi::bind("/datum/gas_mixture/proc/adjust_moles_temp")] 255 | fn adjust_moles_temp_hook( 256 | src: ByondValue, 257 | id_val: ByondValue, 258 | num_val: ByondValue, 259 | temp_val: ByondValue, 260 | ) -> Result { 261 | let vf = num_val.get_number().unwrap_or_default(); 262 | let temp = temp_val.get_number().unwrap_or(2.7); 263 | if vf < 0.0 { 264 | return Err(eyre::eyre!( 265 | "Attempted to add a negative gas in adjust_moles_temp." 266 | )); 267 | } 268 | if !vf.is_normal() { 269 | return Ok(ByondValue::null()); 270 | } 271 | let mut new_mix = Mixture::new(); 272 | new_mix.set_moles(gas_idx_from_value(&id_val)?, vf); 273 | new_mix.set_temperature(temp); 274 | with_mix_mut(&src, |mix| { 275 | mix.merge(&new_mix); 276 | Ok(ByondValue::null()) 277 | }) 278 | } 279 | 280 | /// Args: (gas_id_1, amount_1, gas_id_2, amount_2, ...). As adjust_moles, but with variadic arguments. 281 | #[byondapi::bind_raw_args("/datum/gas_mixture/proc/adjust_multi")] 282 | fn adjust_multi_hook() -> Result { 283 | if args.len() % 2 == 0 { 284 | Err(eyre::eyre!( 285 | "Incorrect arg len for adjust_multi (is even, must be odd to account for src)." 286 | )) 287 | } else if let Some((src, rest)) = args.split_first() { 288 | let adjustments = rest 289 | .chunks(2) 290 | .filter_map(|chunk| { 291 | (chunk.len() == 2) 292 | .then(|| { 293 | gas_idx_from_value(&chunk[0]) 294 | .ok() 295 | .map(|idx| (idx, chunk[1].get_number().unwrap_or_default())) 296 | }) 297 | .flatten() 298 | }) 299 | .collect::>(); 300 | with_mix_mut(src, |mix| { 301 | mix.adjust_multi(&adjustments); 302 | Ok(ByondValue::null()) 303 | }) 304 | } else { 305 | Err(eyre::eyre!("Invalid number of args for adjust_multi")) 306 | } 307 | } 308 | 309 | /// Args: (amount). Adds the given amount to each gas. 310 | #[byondapi::bind("/datum/gas_mixture/proc/add")] 311 | fn add_hook(src: ByondValue, num_val: ByondValue) -> Result { 312 | let vf = num_val.get_number().unwrap_or_default(); 313 | with_mix_mut(&src, |mix| { 314 | mix.add(vf); 315 | Ok(ByondValue::null()) 316 | }) 317 | } 318 | 319 | /// Args: (amount). Subtracts the given amount from each gas. 320 | #[byondapi::bind("/datum/gas_mixture/proc/subtract")] 321 | fn subtract_hook(src: ByondValue, num_val: ByondValue) -> Result { 322 | let vf = num_val.get_number().unwrap_or_default(); 323 | with_mix_mut(&src, |mix| { 324 | mix.add(-vf); 325 | Ok(ByondValue::null()) 326 | }) 327 | } 328 | 329 | /// Args: (coefficient). Multiplies all gases by this amount. 330 | #[byondapi::bind("/datum/gas_mixture/proc/multiply")] 331 | fn multiply_hook(src: ByondValue, num_val: ByondValue) -> Result { 332 | let vf = num_val.get_number().unwrap_or(1.0); 333 | with_mix_mut(&src, |mix| { 334 | mix.multiply(vf); 335 | Ok(ByondValue::null()) 336 | }) 337 | } 338 | 339 | /// Args: (coefficient). Divides all gases by this amount. 340 | #[byondapi::bind("/datum/gas_mixture/proc/divide")] 341 | fn divide_hook(src: ByondValue, num_val: ByondValue) -> Result { 342 | let vf = num_val.get_number().unwrap_or(1.0).recip(); 343 | with_mix_mut(&src, |mix| { 344 | mix.multiply(vf); 345 | Ok(ByondValue::null()) 346 | }) 347 | } 348 | 349 | /// 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. 350 | #[byondapi::bind("/datum/gas_mixture/proc/__remove_by_flag")] 351 | fn remove_by_flag_hook( 352 | src: ByondValue, 353 | into: ByondValue, 354 | flag_val: ByondValue, 355 | amount_val: ByondValue, 356 | ) -> Result { 357 | let flag = flag_val.get_number().map_or(0, |n: f32| n as u32); 358 | let amount = amount_val.get_number().unwrap_or(0.0); 359 | let pertinent_gases = with_gas_info(|gas_info| { 360 | gas_info 361 | .iter() 362 | .filter(|g| g.flags & flag != 0) 363 | .map(|g| g.idx) 364 | .collect::>() 365 | }); 366 | if pertinent_gases.is_empty() { 367 | return Ok(false.into()); 368 | } 369 | with_mixes_mut(&src, &into, |src_gas, dest_gas| { 370 | let tot = src_gas.total_moles(); 371 | src_gas.transfer_gases_to(amount / tot, &pertinent_gases, dest_gas); 372 | Ok(true.into()) 373 | }) 374 | } 375 | /// Args: (flag). As get_gases(), but only returns gases with the given flag. 376 | #[byondapi::bind("/datum/gas_mixture/proc/get_by_flag")] 377 | fn get_by_flag_hook(src: ByondValue, flag_val: ByondValue) -> Result { 378 | let flag = flag_val.get_number().map_or(0, |n: f32| n as u32); 379 | let pertinent_gases = with_gas_info(|gas_info| { 380 | gas_info 381 | .iter() 382 | .filter(|g| g.flags & flag != 0) 383 | .map(|g| g.idx) 384 | .collect::>() 385 | }); 386 | if pertinent_gases.is_empty() { 387 | return Ok(0.0.into()); 388 | } 389 | with_mix(&src, |mix| { 390 | Ok(pertinent_gases 391 | .iter() 392 | .fold(0.0, |acc, idx| acc + mix.get_moles(*idx)) 393 | .into()) 394 | }) 395 | } 396 | 397 | /// Args: (mixture, ratio, gas_list). Takes gases given by `gas_list` and moves `ratio` amount of those gases from `src` into `mixture`. 398 | #[byondapi::bind("/datum/gas_mixture/proc/scrub_into")] 399 | fn scrub_into_hook( 400 | src: ByondValue, 401 | into: ByondValue, 402 | ratio_v: ByondValue, 403 | gas_list: ByondValue, 404 | ) -> Result { 405 | let ratio = ratio_v.get_number()?; 406 | if !gas_list.is_list() { 407 | return Err(eyre::eyre!("Non-list gas_list passed to scrub_into!")); 408 | } 409 | if gas_list.builtin_length()?.get_number()? as u32 == 0 { 410 | return Ok(false.into()); 411 | } 412 | let gas_scrub_vec = gas_list 413 | .iter()? 414 | .filter_map(|(k, _)| gas_idx_from_value(&k).ok()) 415 | .collect::>(); 416 | with_mixes_mut(&src, &into, |src_gas, dest_gas| { 417 | src_gas.transfer_gases_to(ratio, &gas_scrub_vec, dest_gas); 418 | Ok(true.into()) 419 | }) 420 | } 421 | 422 | /// Marks the mix as immutable, meaning it will never change. This cannot be undone. 423 | #[byondapi::bind("/datum/gas_mixture/proc/mark_immutable")] 424 | fn mark_immutable_hook(src: ByondValue) -> Result { 425 | with_mix_mut(&src, |mix| { 426 | mix.mark_immutable(); 427 | Ok(ByondValue::null()) 428 | }) 429 | } 430 | 431 | /// Clears the gas mixture my removing all of its gases. 432 | #[byondapi::bind("/datum/gas_mixture/proc/clear")] 433 | fn clear_hook(src: ByondValue) -> Result { 434 | with_mix_mut(&src, |mix| { 435 | mix.clear(); 436 | Ok(ByondValue::null()) 437 | }) 438 | } 439 | 440 | /// Returns: true if the two mixtures are different enough for processing, false otherwise. 441 | #[byondapi::bind("/datum/gas_mixture/proc/compare")] 442 | fn compare_hook(src: ByondValue, other: ByondValue) -> Result { 443 | with_mixes(&src, &other, |gas_one, gas_two| { 444 | Ok((gas_one.temperature_compare(gas_two) 445 | || gas_one.compare_with(gas_two, MINIMUM_MOLES_DELTA_TO_MOVE)) 446 | .into()) 447 | }) 448 | } 449 | 450 | /// Args: (holder). Runs all reactions on this gas mixture. Holder is used by the reactions, and can be any arbitrary datum or null. 451 | #[byondapi::bind("/datum/gas_mixture/proc/react")] 452 | fn react_hook(src: ByondValue, holder: ByondValue) -> Result { 453 | let mut ret = ReactionReturn::NO_REACTION; 454 | let reactions = with_mix(&src, |mix| Ok(mix.all_reactable()))?; 455 | for reaction in reactions { 456 | ret |= ReactionReturn::from_bits_truncate( 457 | react_by_id(reaction, src, holder)? 458 | .get_number() 459 | .unwrap_or_default() as u32, 460 | ); 461 | if ret.contains(ReactionReturn::STOP_REACTIONS) { 462 | return Ok((ret.bits() as f32).into()); 463 | } 464 | } 465 | Ok((ret.bits() as f32).into()) 466 | } 467 | 468 | /// Args: (heat). Adds a given amount of heat to the mixture, i.e. in joules taking into account capacity. 469 | #[byondapi::bind("/datum/gas_mixture/proc/adjust_heat")] 470 | fn adjust_heat_hook(src: ByondValue, temp: ByondValue) -> Result { 471 | with_mix_mut(&src, |mix| { 472 | mix.adjust_heat(temp.get_number()?); 473 | Ok(ByondValue::null()) 474 | }) 475 | } 476 | 477 | /// Args: (mixture, amount). Takes the `amount` given and transfers it from `src` to `mixture`. 478 | #[byondapi::bind("/datum/gas_mixture/proc/transfer_to")] 479 | fn transfer_hook(src: ByondValue, other: ByondValue, moles: ByondValue) -> Result { 480 | with_mixes_mut(&src, &other, |our_mix, other_mix| { 481 | other_mix.merge(&our_mix.remove(moles.get_number()?)); 482 | Ok(ByondValue::null()) 483 | }) 484 | } 485 | 486 | /// Args: (mixture, ratio). Transfers `ratio` of `src` to `mixture`. 487 | #[byondapi::bind("/datum/gas_mixture/proc/transfer_ratio_to")] 488 | fn transfer_ratio_hook( 489 | src: ByondValue, 490 | other: ByondValue, 491 | ratio: ByondValue, 492 | ) -> Result { 493 | with_mixes_mut(&src, &other, |our_mix, other_mix| { 494 | other_mix.merge(&our_mix.remove_ratio(ratio.get_number()?)); 495 | Ok(ByondValue::null()) 496 | }) 497 | } 498 | 499 | /// Args: (mixture). Makes `src` a copy of `mixture`, with volumes taken into account. 500 | #[byondapi::bind("/datum/gas_mixture/proc/equalize_with")] 501 | fn equalize_with_hook(src: ByondValue, total: ByondValue) -> Result { 502 | with_mixes_custom(&src, &total, |src_lock, total_lock| { 503 | let src_gas = &mut src_lock.write(); 504 | let vol = src_gas.volume; 505 | let total_gas = total_lock.read(); 506 | src_gas.copy_from_mutable(&total_gas); 507 | src_gas.multiply(vol / total_gas.volume); 508 | Ok(ByondValue::null()) 509 | }) 510 | } 511 | 512 | /// 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. 513 | #[byondapi::bind("/datum/gas_mixture/proc/get_fuel_amount")] 514 | fn fuel_amount_hook(src: ByondValue, temp: ByondValue) -> Result { 515 | with_mix(&src, |air| { 516 | Ok(temp 517 | .get_number() 518 | .ok() 519 | .map_or_else( 520 | || air.get_fuel_amount(), 521 | |new_temp| { 522 | let mut test_air = air.copy_to_mutable(); 523 | test_air.set_temperature(new_temp); 524 | test_air.get_fuel_amount() 525 | }, 526 | ) 527 | .into()) 528 | }) 529 | } 530 | 531 | /// 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. 532 | #[byondapi::bind("/datum/gas_mixture/proc/get_oxidation_power")] 533 | fn oxidation_power_hook(src: ByondValue, temp: ByondValue) -> Result { 534 | with_mix(&src, |air| { 535 | Ok(temp 536 | .get_number() 537 | .ok() 538 | .map_or_else( 539 | || air.get_oxidation_power(), 540 | |new_temp| { 541 | let mut test_air = air.clone(); 542 | test_air.set_temperature(new_temp); 543 | test_air.get_oxidation_power() 544 | }, 545 | ) 546 | .into()) 547 | }) 548 | } 549 | 550 | /// Args: (mixture, ratio, one_way). Shares the given `ratio` of `src` with `mixture`, and, unless `one_way` is truthy, vice versa. 551 | #[cfg(feature = "zas_hooks")] 552 | #[byondapi::bind("/datum/gas_mixture/proc/share_ratio")] 553 | fn share_ratio_hook( 554 | other_gas: ByondValue, 555 | ratio_val: ByondValue, 556 | one_way_val: ByondValue, 557 | ) -> Result { 558 | let one_way = one_way_val.as_bool().unwrap_or(false); 559 | let ratio = ratio_val.as_number().ok().map_or(0.6); 560 | let mut inbetween = Mixture::new(); 561 | if one_way { 562 | with_mixes_custom(src, other_gas, |src_lock, other_lock| { 563 | let src_mix = src_lock.write(); 564 | let other_mix = other_lock.read(); 565 | inbetween.copy_from_mutable(other_mix); 566 | inbetween.multiply(ratio); 567 | inbetween.merge(&src_mix.remove_ratio(ratio)); 568 | inbetween.multiply(0.5); 569 | src_mix.merge(inbetween); 570 | Ok(ByondValue::from( 571 | src_mix.temperature_compare(other_mix) 572 | || src_mix.compare_with(other_mix, MINIMUM_MOLES_DELTA_TO_MOVE), 573 | )) 574 | }) 575 | } else { 576 | with_mixes_mut(src, other_gas, |src_mix, other_mix| { 577 | src_mix.remove_ratio_into(ratio, &mut inbetween); 578 | inbetween.merge(&other_mix.remove_ratio(ratio)); 579 | inbetween.multiply(0.5); 580 | src_mix.merge(inbetween); 581 | other_mix.merge(inbetween); 582 | Ok(ByondValue::from( 583 | src_mix.temperature_compare(other_mix) 584 | || src_mix.compare_with(other_mix, MINIMUM_MOLES_DELTA_TO_MOVE), 585 | )) 586 | }) 587 | } 588 | } 589 | 590 | /// 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. 591 | #[byondapi::bind("/proc/equalize_all_gases_in_list")] 592 | fn equalize_all_hook(gas_list: ByondValue) -> Result { 593 | use std::collections::BTreeSet; 594 | let gas_list = gas_list 595 | .iter()? 596 | .filter_map(|(value, _)| { 597 | value 598 | .read_number_id(byond_string!("_extools_pointer_gasmixture")) 599 | .ok() 600 | .map(|f| f as usize) 601 | }) 602 | .collect::>(); 603 | GasArena::with_all_mixtures(move |all_mixtures| { 604 | let mut tot = gas::Mixture::new(); 605 | let mut tot_vol: f64 = 0.0; 606 | gas_list 607 | .iter() 608 | .filter_map(|&id| all_mixtures.get(id)) 609 | .for_each(|src_gas_lock| { 610 | let src_gas = src_gas_lock.read(); 611 | tot.merge(&src_gas); 612 | tot_vol += f64::from(src_gas.volume); 613 | }); 614 | if tot_vol > 0.0 { 615 | gas_list 616 | .iter() 617 | .filter_map(|&id| all_mixtures.get(id)) 618 | .for_each(|dest_gas_lock| { 619 | let dest_gas = &mut dest_gas_lock.write(); 620 | let vol = dest_gas.volume; // don't wanna borrow it in the below 621 | dest_gas.copy_from_mutable(&tot); 622 | dest_gas.multiply((f64::from(vol) / tot_vol) as f32); 623 | }); 624 | } 625 | }); 626 | Ok(ByondValue::null()) 627 | } 628 | 629 | /// Returns: the amount of gas mixtures that are attached to a byond gas mixture. 630 | #[byondapi::bind("/datum/controller/subsystem/air/proc/get_amt_gas_mixes")] 631 | fn hook_amt_gas_mixes() -> Result { 632 | Ok((amt_gases() as f32).into()) 633 | } 634 | 635 | /// Returns: the total amount of gas mixtures in the arena, including "free" ones. 636 | #[byondapi::bind("/datum/controller/subsystem/air/proc/get_max_gas_mixes")] 637 | fn hook_max_gas_mixes() -> Result { 638 | Ok((tot_gases() as f32).into()) 639 | } 640 | /// Returns: true. Parses gas strings like "o2=2500;plasma=5000;TEMP=370" and turns src mixes into the parsed gas mixture, invalid patterns will be ignored 641 | #[byondapi::bind("/datum/gas_mixture/proc/__auxtools_parse_gas_string")] 642 | fn parse_gas_string(src: ByondValue, string: ByondValue) -> Result { 643 | let actual_string = string.get_string()?; 644 | 645 | let (_, vec) = parser::parse_gas_string(&actual_string) 646 | .map_err(|_| eyre::eyre!(format!("Failed to parse gas string: {actual_string}")))?; 647 | 648 | with_mix_mut(&src, move |air| { 649 | air.clear(); 650 | for (gas, moles) in vec.iter() { 651 | if let Ok(idx) = gas_idx_from_string(gas) { 652 | if (*moles).is_normal() && *moles > 0.0 { 653 | air.set_moles(idx, *moles) 654 | } 655 | } else if gas.contains("TEMP") { 656 | let mut checked_temp = *moles; 657 | if !checked_temp.is_normal() || checked_temp < constants::TCMB { 658 | checked_temp = constants::TCMB 659 | } 660 | air.set_temperature(checked_temp) 661 | } else { 662 | return Err(eyre::eyre!(format!("Unknown gas id: {gas}"))); 663 | } 664 | } 665 | Ok(()) 666 | })?; 667 | Ok(true.into()) 668 | } 669 | 670 | #[test] 671 | fn generate_binds() { 672 | byondapi::generate_bindings(env!("CARGO_CRATE_NAME")); 673 | } 674 | -------------------------------------------------------------------------------- /src/gas/mixture.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | constants::*, gas_visibility, total_num_gases, with_reactions, with_specific_heats, GasIDX, 3 | }; 4 | use crate::reaction::{Reaction, ReactionPriority}; 5 | use atomic_float::AtomicF32; 6 | use eyre::Result; 7 | use itertools::{ 8 | Either, 9 | EitherOrBoth::{Both, Left, Right}, 10 | Itertools, 11 | }; 12 | use std::collections::BTreeMap; 13 | use std::sync::atomic::{AtomicU64, Ordering::Relaxed}; 14 | use tinyvec::TinyVec; 15 | 16 | type SpecificFireInfo = (usize, f32, f32); 17 | 18 | #[derive(Debug)] 19 | struct GasCache(AtomicF32); 20 | 21 | impl Clone for GasCache { 22 | fn clone(&self) -> Self { 23 | Self(AtomicF32::new(self.0.load(Relaxed))) 24 | } 25 | } 26 | 27 | impl Default for GasCache { 28 | fn default() -> Self { 29 | Self(AtomicF32::new(f32::NAN)) 30 | } 31 | } 32 | 33 | impl GasCache { 34 | pub fn invalidate(&self) { 35 | self.0.store(f32::NAN, Relaxed); 36 | } 37 | //cannot fix this, because f is FnMut and then() takes FnOnce 38 | pub fn get_or_else(&self, mut f: impl FnMut() -> f32) -> f32 { 39 | match self 40 | .0 41 | .fetch_update(Relaxed, Relaxed, |x| x.is_nan().then(&mut f)) 42 | { 43 | Ok(_) => self.0.load(Relaxed), 44 | Err(x) => x, 45 | } 46 | } 47 | pub fn set(&self, v: f32) { 48 | self.0.store(v, Relaxed); 49 | } 50 | } 51 | 52 | pub fn visibility_step(gas_amt: f32) -> u32 { 53 | (gas_amt / MOLES_GAS_VISIBLE_STEP) 54 | .ceil() 55 | .clamp(1.0, FACTOR_GAS_VISIBLE_MAX) as u32 56 | } 57 | 58 | /// The data structure representing a Space Station 13 gas mixture. 59 | /// Unlike Monstermos, this doesn't have the archive built-in; instead, 60 | /// the archive is a feature of the turf grid, only existing during 61 | /// turf processing. 62 | /// Also missing is `last_share`; due to the usage of Rust, 63 | /// processing no longer requires sleeping turfs. Instead, we're using 64 | /// a proper, fully-simulated FDM system, much like LINDA but without 65 | /// sleeping turfs. 66 | #[derive(Clone, Debug)] 67 | pub struct Mixture { 68 | temperature: f32, 69 | pub volume: f32, 70 | min_heat_capacity: f32, 71 | moles: TinyVec<[f32; 8]>, 72 | cached_heat_capacity: GasCache, 73 | immutable: bool, 74 | } 75 | 76 | impl Default for Mixture { 77 | fn default() -> Self { 78 | Self::new() 79 | } 80 | } 81 | 82 | impl Mixture { 83 | /// Makes an empty gas mixture. 84 | #[must_use] 85 | pub fn new() -> Self { 86 | Self { 87 | moles: TinyVec::new(), 88 | temperature: 2.7, 89 | volume: 2500.0, 90 | min_heat_capacity: 0.0, 91 | immutable: false, 92 | cached_heat_capacity: GasCache::default(), 93 | } 94 | } 95 | /// Makes an empty gas mixture with the given volume. 96 | #[must_use] 97 | pub fn from_vol(vol: f32) -> Self { 98 | let mut ret = Self::new(); 99 | ret.volume = vol; 100 | ret 101 | } 102 | /// Returns if any data is corrupt. 103 | pub fn is_corrupt(&self) -> bool { 104 | !self.temperature.is_normal() || self.moles.len() > total_num_gases() 105 | } 106 | /// Fixes any corruption found. 107 | pub fn fix_corruption(&mut self) { 108 | self.garbage_collect(); 109 | if self.temperature < 2.7 || !self.temperature.is_normal() { 110 | self.set_temperature(293.15); 111 | } 112 | } 113 | /// Returns the temperature of the mix. T 114 | pub fn get_temperature(&self) -> f32 { 115 | self.temperature 116 | } 117 | /// Sets the temperature, if the mix isn't immutable. T 118 | pub fn set_temperature(&mut self, temp: f32) { 119 | if !self.immutable && temp.is_normal() { 120 | self.temperature = temp; 121 | } 122 | } 123 | /// Sets the minimum heat capacity of this mix. 124 | pub fn set_min_heat_capacity(&mut self, amt: f32) { 125 | self.min_heat_capacity = amt; 126 | } 127 | /// Returns an iterator over the gas keys and mole amounts thereof. 128 | pub fn enumerate(&self) -> impl Iterator + '_ { 129 | self.moles.iter().copied().enumerate() 130 | } 131 | /// Allows closures to iterate over each gas. 132 | /// # Errors 133 | /// If the closure errors. 134 | pub fn for_each_gas(&self, mut f: impl FnMut(GasIDX, f32) -> Result<()>) -> Result<()> { 135 | self.enumerate().try_for_each(|(i, g)| f(i, g))?; 136 | Ok(()) 137 | } 138 | /// As `for_each_gas`, but with mut refs to the mole counts instead of copies. 139 | /// # Errors 140 | /// If the closure errors. 141 | pub fn for_each_gas_mut( 142 | &mut self, 143 | mut f: impl FnMut(GasIDX, &mut f32) -> Result<()>, 144 | ) -> Result<()> { 145 | self.moles 146 | .iter_mut() 147 | .enumerate() 148 | .try_for_each(|(i, g)| f(i, g))?; 149 | Ok(()) 150 | } 151 | /// Returns (by value) the amount of moles of a given index the mix has. M 152 | pub fn get_moles(&self, idx: GasIDX) -> f32 { 153 | self.moles.get(idx).copied().unwrap_or(0.0) 154 | } 155 | /// Sets the mix to be internally immutable. Rust doesn't know about any of this, obviously. 156 | pub fn mark_immutable(&mut self) { 157 | self.immutable = true; 158 | } 159 | /// Returns whether this gas mixture is immutable. 160 | pub fn is_immutable(&self) -> bool { 161 | self.immutable 162 | } 163 | fn maybe_expand(&mut self, size: usize) { 164 | if self.moles.len() < size { 165 | self.moles.resize(size, 0.0); 166 | } 167 | } 168 | /// If mix is not immutable, sets the gas at the given `idx` to the given `amt`. 169 | pub fn set_moles(&mut self, idx: GasIDX, amt: f32) { 170 | if !self.immutable 171 | && idx < total_num_gases() 172 | && (idx <= self.moles.len() || (amt > GAS_MIN_MOLES && amt.is_normal())) 173 | { 174 | self.maybe_expand(idx + 1); 175 | unsafe { 176 | *self.moles.get_unchecked_mut(idx) = amt; 177 | }; 178 | self.cached_heat_capacity.invalidate(); 179 | } 180 | } 181 | pub fn adjust_moles(&mut self, idx: GasIDX, amt: f32) { 182 | if !self.immutable && amt.is_normal() && idx < total_num_gases() { 183 | self.maybe_expand(idx + 1); 184 | let r = unsafe { self.moles.get_unchecked_mut(idx) }; 185 | *r += amt; 186 | if amt <= 0.0 { 187 | self.garbage_collect(); 188 | } 189 | self.cached_heat_capacity.invalidate(); 190 | } 191 | } 192 | pub fn adjust_multi(&mut self, adjustments: &[(usize, f32)]) { 193 | if !self.immutable { 194 | let num_gases = total_num_gases(); 195 | self.maybe_expand( 196 | adjustments 197 | .iter() 198 | .filter_map(|&(i, _)| (i < num_gases).then_some(i)) 199 | .max() 200 | .unwrap_or(0) + 1, 201 | ); 202 | let mut dirty = false; 203 | let mut should_collect = false; 204 | for (idx, amt) in adjustments { 205 | if *idx < num_gases && amt.is_normal() { 206 | let r = unsafe { self.moles.get_unchecked_mut(*idx) }; 207 | *r += *amt; 208 | if *amt <= 0.0 { 209 | should_collect = true; 210 | } 211 | dirty = true; 212 | } 213 | } 214 | if dirty { 215 | self.cached_heat_capacity.invalidate(); 216 | } 217 | if should_collect { 218 | self.garbage_collect(); 219 | } 220 | } 221 | } 222 | #[inline(never)] // mostly this makes it so that heat_capacity itself is inlined 223 | fn slow_heat_capacity(&self) -> f32 { 224 | with_specific_heats(|heats| { 225 | self.moles 226 | .iter() 227 | .copied() 228 | .zip(heats.iter()) 229 | .fold(0.0, |acc, (amt, cap)| cap.mul_add(amt, acc)) 230 | }) 231 | .max(self.min_heat_capacity) 232 | } 233 | /// The heat capacity of the material. [joules?]/mole-kelvin. 234 | pub fn heat_capacity(&self) -> f32 { 235 | self.cached_heat_capacity 236 | .get_or_else(|| self.slow_heat_capacity()) 237 | } 238 | /// Heat capacity of exactly one gas in this mix. 239 | pub fn partial_heat_capacity(&self, idx: GasIDX) -> f32 { 240 | self.moles 241 | .get(idx) 242 | .filter(|amt| amt.is_normal()) 243 | .map_or(0.0, |amt| amt * with_specific_heats(|heats| heats[idx])) 244 | } 245 | /// The total mole count of the mixture. Moles. 246 | pub fn total_moles(&self) -> f32 { 247 | self.moles.iter().sum() 248 | } 249 | /// Pressure. Kilopascals. 250 | pub fn return_pressure(&self) -> f32 { 251 | self.total_moles() * R_IDEAL_GAS_EQUATION * self.temperature / self.volume 252 | } 253 | /// Thermal energy. Joules? 254 | pub fn thermal_energy(&self) -> f32 { 255 | self.heat_capacity() * self.temperature 256 | } 257 | /// Merges one gas mixture into another. 258 | pub fn merge(&mut self, giver: &Self) { 259 | if self.immutable { 260 | return; 261 | } 262 | let our_heat_capacity = self.heat_capacity(); 263 | let other_heat_capacity = giver.heat_capacity(); 264 | self.maybe_expand(giver.moles.len()); 265 | self.moles 266 | .iter_mut() 267 | .zip(giver.moles.iter()) 268 | .for_each(|(a, b)| *a += b); 269 | let combined_heat_capacity = our_heat_capacity + other_heat_capacity; 270 | if combined_heat_capacity > MINIMUM_HEAT_CAPACITY { 271 | self.set_temperature( 272 | (our_heat_capacity * self.temperature + other_heat_capacity * giver.temperature) 273 | / (combined_heat_capacity), 274 | ); 275 | } 276 | self.cached_heat_capacity.set(combined_heat_capacity); 277 | } 278 | /// Turns a gas mixture into the weighted average of us and the giver, with the weights being (1-ratio, ratio), for self and the giver respectively. 279 | pub fn share_ratio(&mut self, giver: &Self, r: f32) { 280 | if self.immutable { 281 | return; 282 | } 283 | let ratio = r.clamp(0.0, 1.0); 284 | self.multiply(1.0 - ratio); 285 | let our_heat_capacity = self.heat_capacity(); 286 | let other_heat_capacity = giver.heat_capacity() * ratio; 287 | self.maybe_expand(giver.moles.len()); 288 | self.moles 289 | .iter_mut() 290 | .zip(giver.moles.iter()) 291 | .for_each(|(a, b)| *a += b * ratio); 292 | let combined_heat_capacity = our_heat_capacity + other_heat_capacity; 293 | if combined_heat_capacity > MINIMUM_HEAT_CAPACITY { 294 | self.set_temperature( 295 | (our_heat_capacity * self.temperature + other_heat_capacity * giver.temperature) 296 | / (combined_heat_capacity), 297 | ); 298 | } 299 | self.cached_heat_capacity.set(combined_heat_capacity); 300 | } 301 | /// Transfers only the given gases from us to another mix. 302 | pub fn transfer_gases_to(&mut self, r: f32, gases: &[GasIDX], into: &mut Self) { 303 | let ratio = r.clamp(0.0, 1.0); 304 | let initial_energy = into.thermal_energy(); 305 | let mut heat_transfer = 0.0; 306 | with_specific_heats(|heats| { 307 | for i in gases.iter().copied() { 308 | if let Some(orig) = self.moles.get_mut(i) { 309 | let delta = *orig * ratio; 310 | heat_transfer += delta * self.temperature * heats[i]; 311 | *orig -= delta; 312 | into.adjust_moles(i, delta); 313 | } 314 | } 315 | }); 316 | self.cached_heat_capacity.invalidate(); 317 | into.cached_heat_capacity.invalidate(); 318 | into.set_temperature((initial_energy + heat_transfer) / into.heat_capacity()); 319 | } 320 | /// 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. 321 | pub fn remove_ratio_into(&mut self, mut ratio: f32, into: &mut Self) { 322 | if ratio <= 0.0 { 323 | return; 324 | } 325 | if ratio >= 1.0 { 326 | ratio = 1.0; 327 | } 328 | into.copy_from_mutable(self); 329 | into.multiply(ratio); 330 | self.multiply(1.0 - ratio); 331 | } 332 | /// As `remove_ratio_into`, but a raw number of moles instead of a ratio. 333 | pub fn remove_into(&mut self, amount: f32, into: &mut Self) { 334 | self.remove_ratio_into(amount / self.total_moles(), into); 335 | } 336 | /// A convenience function that makes the mixture for `remove_ratio_into` on the spot and returns it. 337 | #[must_use] 338 | pub fn remove_ratio(&mut self, ratio: f32) -> Self { 339 | let mut removed = Self::from_vol(self.volume); 340 | self.remove_ratio_into(ratio, &mut removed); 341 | removed 342 | } 343 | /// Like `remove_ratio`, but with moles. 344 | #[must_use] 345 | pub fn remove(&mut self, amount: f32) -> Self { 346 | self.remove_ratio(amount / self.total_moles()) 347 | } 348 | /// Copies from a given gas mixture, if we're mutable. 349 | pub fn copy_from_mutable(&mut self, sample: &Self) { 350 | if self.immutable { 351 | return; 352 | } 353 | self.moles = sample.moles.clone(); 354 | self.temperature = sample.temperature; 355 | self.cached_heat_capacity = sample.cached_heat_capacity.clone(); 356 | } 357 | /// Makes a copy of this gas mixture that is guaranteed mutable, regardless of whether this one is immutable 358 | pub fn copy_to_mutable(&self) -> Self { 359 | let mut new_mix = self.clone(); 360 | new_mix.immutable = false; 361 | new_mix 362 | } 363 | /// A very simple finite difference solution to the heat transfer equation. 364 | /// Works well enough for our purposes, though perhaps called less often 365 | /// than it ought to be while we're working in Rust. 366 | /// Differs from the original by not using archive, since we don't put the archive into the gas mix itself anymore. 367 | pub fn temperature_share(&mut self, sharer: &mut Self, conduction_coefficient: f32) -> f32 { 368 | let temperature_delta = self.temperature - sharer.temperature; 369 | if temperature_delta.abs() > MINIMUM_TEMPERATURE_DELTA_TO_CONSIDER { 370 | let self_heat_capacity = self.heat_capacity(); 371 | let sharer_heat_capacity = sharer.heat_capacity(); 372 | 373 | if sharer_heat_capacity > MINIMUM_HEAT_CAPACITY 374 | && self_heat_capacity > MINIMUM_HEAT_CAPACITY 375 | { 376 | let heat = conduction_coefficient 377 | * temperature_delta 378 | * (self_heat_capacity * sharer_heat_capacity 379 | / (self_heat_capacity + sharer_heat_capacity)); 380 | if !self.immutable { 381 | self.set_temperature((self.temperature - heat / self_heat_capacity).max(TCMB)); 382 | } 383 | if !sharer.immutable { 384 | sharer.set_temperature( 385 | (sharer.temperature + heat / sharer_heat_capacity).max(TCMB), 386 | ); 387 | } 388 | } 389 | } 390 | sharer.temperature 391 | } 392 | /// As above, but you may put in any arbitrary coefficient, temp, heat capacity. 393 | /// Only used for superconductivity as of right now. 394 | pub fn temperature_share_non_gas( 395 | &mut self, 396 | conduction_coefficient: f32, 397 | sharer_temperature: f32, 398 | sharer_heat_capacity: f32, 399 | ) -> f32 { 400 | let temperature_delta = self.temperature - sharer_temperature; 401 | if temperature_delta.abs() > MINIMUM_TEMPERATURE_DELTA_TO_CONSIDER { 402 | let self_heat_capacity = self.heat_capacity(); 403 | 404 | if sharer_heat_capacity > MINIMUM_HEAT_CAPACITY 405 | && self_heat_capacity > MINIMUM_HEAT_CAPACITY 406 | { 407 | let heat = conduction_coefficient 408 | * temperature_delta 409 | * (self_heat_capacity * sharer_heat_capacity 410 | / (self_heat_capacity + sharer_heat_capacity)); 411 | if !self.immutable { 412 | self.set_temperature((self.temperature - heat / self_heat_capacity).max(TCMB)); 413 | } 414 | return (sharer_temperature + heat / sharer_heat_capacity).max(TCMB); 415 | } 416 | } 417 | sharer_temperature 418 | } 419 | /// The second part of old compare(). Compares temperature, but only if this gas has sufficiently high moles. 420 | pub fn temperature_compare(&self, sample: &Self) -> bool { 421 | (self.get_temperature() - sample.get_temperature()).abs() 422 | > MINIMUM_TEMPERATURE_DELTA_TO_SUSPEND 423 | && (self.total_moles() > MINIMUM_MOLES_DELTA_TO_MOVE) 424 | } 425 | /// Returns the maximum mole delta for an individual gas. 426 | pub fn compare(&self, sample: &Self) -> f32 { 427 | self.moles 428 | .iter() 429 | .copied() 430 | .zip_longest(sample.moles.iter().copied()) 431 | .fold(0.0, |acc, pair| acc.max(pair.reduce(|a, b| (b - a).abs()))) 432 | } 433 | pub fn compare_with(&self, sample: &Self, amt: f32) -> bool { 434 | self.moles 435 | .as_slice() 436 | .iter() 437 | .zip_longest(sample.moles.as_slice().iter()) 438 | .rev() 439 | .any(|pair| match pair { 440 | Left(a) => a >= &amt, 441 | Right(b) => b >= &amt, 442 | Both(a, b) => (a - b).abs() >= amt, 443 | }) 444 | } 445 | /// Clears the moles from the gas. 446 | pub fn clear(&mut self) { 447 | if !self.immutable { 448 | self.moles.clear(); 449 | self.cached_heat_capacity.invalidate(); 450 | } 451 | } 452 | /// Resets the gas mixture to an initialized-with-volume state. 453 | pub fn clear_with_vol(&mut self, vol: f32) { 454 | self.temperature = 2.7; 455 | self.volume = vol; 456 | self.min_heat_capacity = 0.0; 457 | self.immutable = false; 458 | self.clear(); 459 | } 460 | /// Multiplies every gas molage with this value. 461 | pub fn multiply(&mut self, multiplier: f32) { 462 | if !self.immutable { 463 | self.moles.iter_mut().for_each(|amt| *amt *= multiplier); 464 | self.cached_heat_capacity.invalidate(); 465 | self.garbage_collect(); 466 | } 467 | } 468 | pub fn add(&mut self, num: f32) { 469 | if !self.immutable { 470 | self.moles.iter_mut().for_each(|amt| *amt += num); 471 | self.cached_heat_capacity.invalidate(); 472 | self.garbage_collect(); 473 | } 474 | } 475 | pub fn can_react_with_reactions( 476 | &self, 477 | reactions: &BTreeMap, 478 | ) -> bool { 479 | //priorities are inversed because fuck you 480 | reactions 481 | .values() 482 | .rev() 483 | .any(|reaction| reaction.check_conditions(self)) 484 | } 485 | /// Checks if the proc can react with any reactions. 486 | pub fn can_react(&self) -> bool { 487 | with_reactions(|reactions| self.can_react_with_reactions(reactions)) 488 | } 489 | pub fn all_reactable_with_slice( 490 | &self, 491 | reactions: &BTreeMap, 492 | ) -> TinyVec<[u64; MAX_REACTION_TINYVEC_SIZE]> { 493 | //priorities are inversed because fuck you 494 | reactions 495 | .values() 496 | .rev() 497 | .filter(|thin| thin.check_conditions(self)) 498 | .map(|thin| thin.get_id()) 499 | .collect() 500 | } 501 | /// Gets all of the reactions this mix should do. 502 | pub fn all_reactable(&self) -> TinyVec<[u64; MAX_REACTION_TINYVEC_SIZE]> { 503 | with_reactions(|reactions| self.all_reactable_with_slice(reactions)) 504 | } 505 | /// Returns a tuple with oxidation power and fuel amount of this gas mixture. 506 | pub fn get_burnability(&self) -> (f32, f32) { 507 | use crate::types::FireInfo; 508 | super::with_gas_info(|gas_info| { 509 | self.moles 510 | .iter() 511 | .zip(gas_info) 512 | .fold((0.0, 0.0), |mut acc, (&amt, this_gas_info)| { 513 | if amt > GAS_MIN_MOLES { 514 | match this_gas_info.fire_info { 515 | FireInfo::Oxidation(oxidation) => { 516 | if self.temperature > oxidation.temperature() { 517 | let amount = amt 518 | * (1.0 - oxidation.temperature() / self.temperature) 519 | .max(0.0); 520 | acc.0 += amount * oxidation.power(); 521 | } 522 | } 523 | FireInfo::Fuel(fire) => { 524 | if self.temperature > fire.temperature() { 525 | let amount = amt 526 | * (1.0 - fire.temperature() / self.temperature).max(0.0); 527 | acc.1 += amount / fire.burn_rate(); 528 | } 529 | } 530 | FireInfo::None => (), 531 | } 532 | } 533 | acc 534 | }) 535 | }) 536 | } 537 | /// Returns only the oxidation power. Since this calculates burnability anyway, prefer `get_burnability`. 538 | pub fn get_oxidation_power(&self) -> f32 { 539 | self.get_burnability().0 540 | } 541 | /// Returns only fuel amount. Since this calculates burnability anyway, prefer `get_burnability`. 542 | pub fn get_fuel_amount(&self) -> f32 { 543 | self.get_burnability().1 544 | } 545 | /// Like `get_fire_info`, but takes a reference to a gas info vector, 546 | /// so one doesn't need to do a recursive lock on the global list. 547 | pub fn get_fire_info_with_lock( 548 | &self, 549 | gas_info: &[super::GasType], 550 | ) -> (Vec, Vec) { 551 | use crate::types::FireInfo; 552 | self.moles 553 | .iter() 554 | .zip(gas_info) 555 | .enumerate() 556 | .filter_map(|(i, (&amt, this_gas_info))| { 557 | (amt > GAS_MIN_MOLES) 558 | .then(|| match this_gas_info.fire_info { 559 | FireInfo::Oxidation(oxidation) => (self.get_temperature() 560 | > oxidation.temperature()) 561 | .then(|| { 562 | let amount = amt 563 | * (1.0 - oxidation.temperature() / self.get_temperature()).max(0.0); 564 | Either::Right((i, amount, amount * oxidation.power())) 565 | }), 566 | FireInfo::Fuel(fuel) => { 567 | (self.get_temperature() > fuel.temperature()).then(|| { 568 | let amount = amt 569 | * (1.0 - fuel.temperature() / self.get_temperature()).max(0.0); 570 | Either::Left((i, amount, amount / fuel.burn_rate())) 571 | }) 572 | } 573 | FireInfo::None => None, 574 | }) 575 | .flatten() 576 | }) 577 | .partition_map(|r| r) 578 | } 579 | /// Returns two vectors: 580 | /// The first contains all oxidizers in this list, as well as their actual mole amounts and how much fuel they can oxidize. 581 | /// The second contains all fuel sources in this list, as well as their actual mole amounts and how much oxidizer they can react with. 582 | pub fn get_fire_info(&self) -> (Vec, Vec) { 583 | super::with_gas_info(|gas_info| self.get_fire_info_with_lock(gas_info)) 584 | } 585 | /// Adds heat directly to the gas mixture, in joules (probably). 586 | pub fn adjust_heat(&mut self, heat: f32) { 587 | let cap = self.heat_capacity(); 588 | self.set_temperature(((cap * self.temperature) + heat) / cap); 589 | } 590 | /// Returns true if there's a visible gas in this mix. 591 | pub fn is_visible(&self) -> bool { 592 | self.enumerate() 593 | .any(|(i, gas)| gas_visibility(i).map_or(false, |amt| gas >= amt)) 594 | } 595 | pub fn vis_hash(&self, gas_visibility: &[Option]) -> u64 { 596 | use std::hash::Hasher; 597 | let mut hasher: ahash::AHasher = ahash::AHasher::default(); 598 | 599 | self.enumerate() 600 | .filter(|&(i, gas_amt)| { 601 | unsafe { gas_visibility.get_unchecked(i) } 602 | .filter(|&amt| gas_amt > amt) 603 | .is_some() 604 | }) 605 | .for_each(|(i, gas_amt)| { 606 | hasher.write_usize(i); 607 | hasher.write_usize(visibility_step(gas_amt) as usize) 608 | }); 609 | hasher.finish() 610 | } 611 | /// Compares the current vis hash to the provided one; returns true if they are 612 | pub fn vis_hash_changed( 613 | &self, 614 | gas_visibility: &[Option], 615 | hash_holder: &AtomicU64, 616 | ) -> bool { 617 | let cur_hash = self.vis_hash(gas_visibility); 618 | let old_hash = hash_holder.swap(cur_hash, Relaxed); 619 | old_hash == 0 || old_hash != cur_hash 620 | } 621 | // Removes all redundant zeroes from the gas mixture. 622 | pub fn garbage_collect(&mut self) { 623 | let mut last_valid_found = 0; 624 | for (i, amt) in self.moles.iter_mut().enumerate() { 625 | if *amt > GAS_MIN_MOLES { 626 | last_valid_found = i; 627 | } else { 628 | *amt = 0.0; 629 | } 630 | } 631 | self.moles.truncate(last_valid_found + 1); 632 | } 633 | } 634 | 635 | use std::ops::{Add, Mul}; 636 | 637 | /// Takes a copy of the mix, merges the right hand side, then returns the copy. 638 | impl Add<&Mixture> for Mixture { 639 | type Output = Self; 640 | 641 | fn add(self, rhs: &Mixture) -> Self { 642 | let mut ret = self.copy_to_mutable(); 643 | ret.merge(rhs); 644 | ret 645 | } 646 | } 647 | 648 | /// Takes a copy of the mix, merges the right hand side, then returns the copy. 649 | impl<'a, 'b> Add<&'a Mixture> for &'b Mixture { 650 | type Output = Mixture; 651 | 652 | fn add(self, rhs: &Mixture) -> Mixture { 653 | let mut ret = self.copy_to_mutable(); 654 | ret.merge(rhs); 655 | ret 656 | } 657 | } 658 | 659 | /// Makes a copy of the given mix, multiplied by a scalar. 660 | impl Mul for Mixture { 661 | type Output = Self; 662 | 663 | fn mul(self, rhs: f32) -> Self { 664 | let mut ret = self.copy_to_mutable(); 665 | ret.multiply(rhs); 666 | ret 667 | } 668 | } 669 | 670 | /// Makes a copy of the given mix, multiplied by a scalar. 671 | impl<'a> Mul for &'a Mixture { 672 | type Output = Mixture; 673 | 674 | fn mul(self, rhs: f32) -> Mixture { 675 | let mut ret = self.copy_to_mutable(); 676 | ret.multiply(rhs); 677 | ret 678 | } 679 | } 680 | 681 | impl PartialEq for Mixture { 682 | fn eq(&self, other: &Self) -> bool { 683 | self.moles.len() == other.moles.len() 684 | && self.temperature == other.temperature 685 | && self 686 | .moles 687 | .iter() 688 | .zip(other.moles.iter()) 689 | .all(|(a, b)| (a - b).abs() < GAS_MIN_MOLES) 690 | } 691 | } 692 | 693 | impl Eq for Mixture {} 694 | 695 | #[cfg(test)] 696 | mod tests { 697 | 698 | use super::*; 699 | use crate::gas::types::{destroy_gas_statics, register_gas_manually, set_gas_statics_manually}; 700 | 701 | fn initialize_gases() { 702 | set_gas_statics_manually(); 703 | register_gas_manually("o2", 20.0); 704 | register_gas_manually("n2", 20.0); 705 | register_gas_manually("n2o", 20.0); 706 | } 707 | 708 | #[test] 709 | fn test_gases() { 710 | initialize_gases(); 711 | let mut into = Mixture::new(); 712 | into.set_moles(0, 82.0); 713 | into.set_moles(1, 22.0); 714 | into.set_temperature(293.15); 715 | let mut source = Mixture::new(); 716 | source.set_moles(2, 100.0); 717 | source.set_temperature(313.15); 718 | into.merge(&source); 719 | // make sure that the merge successfuly moved the moles 720 | assert_eq!(into.get_moles(2), 100.0); 721 | assert_eq!(source.get_moles(2), 100.0); // source is not modified by merge 722 | /* 723 | make sure that the merge successfuly changed the temperature of the mix merged into: 724 | test gases have heat capacities of (82 * 20 + 22 * 20) and (100 * 20) respectively, so total thermal energies of 725 | (82 * 20 + 22 * 20) * 293.15 and (100 * 20) * 313.15 respectively once multiplied by temperatures. add those together, 726 | then divide by new total heat capacity: 727 | (609,752 + 626,300)/(2,080 + 2,000) = 728 | ~ 729 | 302.953 730 | so we compare to see if it's relatively close to 302.953, cause of floating point precision 731 | */ 732 | assert!( 733 | (into.get_temperature() - 302.953).abs() < 0.01, 734 | "{} should be near 302.953, is {}", 735 | into.get_temperature(), 736 | (into.get_temperature() - 302.953) 737 | ); 738 | 739 | // test merges 740 | // also tests multiply, copy_from_mutable 741 | let mut removed = Mixture::new(); 742 | removed.set_moles(0, 22.0); 743 | removed.set_moles(1, 82.0); 744 | let new = removed.remove_ratio(0.5); 745 | assert_eq!(removed.compare(&new) >= MINIMUM_MOLES_DELTA_TO_MOVE, false); 746 | assert_eq!(removed.get_moles(0), 11.0); 747 | assert_eq!(removed.get_moles(1), 41.0); 748 | removed.mark_immutable(); 749 | let new_two = removed.remove_ratio(0.5); 750 | assert_eq!( 751 | removed.compare(&new_two) >= MINIMUM_MOLES_DELTA_TO_MOVE, 752 | true 753 | ); 754 | assert_eq!(removed.get_moles(0), 11.0); 755 | assert_eq!(removed.get_moles(1), 41.0); 756 | assert_eq!(new_two.get_moles(0), 5.5); 757 | destroy_gas_statics(); 758 | } 759 | } 760 | --------------------------------------------------------------------------------