diff --git a/code/game/objects/effects/portals.dm b/code/game/objects/effects/portals.dm index dd297b9dc33..d248188db11 100644 --- a/code/game/objects/effects/portals.dm +++ b/code/game/objects/effects/portals.dm @@ -1,10 +1,9 @@ - -/proc/create_portal_pair(turf/source, turf/destination, _lifespan = 300, accuracy = 0, newtype = /obj/effect/portal, atmos_link_override) +/proc/create_portal_pair(turf/source, turf/destination, _lifespan = 300, accuracy = 0, newtype = /obj/effect/portal) if(!istype(source) || !istype(destination)) return var/turf/actual_destination = get_teleport_turf(destination, accuracy) - var/obj/effect/portal/P1 = new newtype(source, _lifespan, null, FALSE, null, atmos_link_override) - var/obj/effect/portal/P2 = new newtype(actual_destination, _lifespan, P1, TRUE, null, atmos_link_override) + var/obj/effect/portal/P1 = new newtype(source, _lifespan, null, FALSE, null) + var/obj/effect/portal/P2 = new newtype(actual_destination, _lifespan, P1, TRUE, null) if(!istype(P1)||!istype(P2)) return P1.link_portal(P2) @@ -24,9 +23,6 @@ var/hardlinked = TRUE //Requires a linked portal at all times. Destroy if there's no linked portal, if there is destroy it when this one is deleted. var/teleport_channel = TELEPORT_CHANNEL_BLUESPACE var/turf/hard_target //For when a portal needs a hard target and isn't to be linked. - var/atmos_link = FALSE //Link source/destination atmos. - var/turf/open/atmos_source //Atmos link source - var/turf/open/atmos_destination //Atmos link destination var/allow_anchored = FALSE var/innate_accuracy_penalty = 0 var/last_effect = 0 @@ -74,7 +70,7 @@ if(Adjacent(user)) teleport(user) -/obj/effect/portal/Initialize(mapload, _lifespan = 0, obj/effect/portal/_linked, automatic_link = FALSE, turf/hard_target_override, atmos_link_override) +/obj/effect/portal/Initialize(mapload, _lifespan = 0, obj/effect/portal/_linked, automatic_link = FALSE, turf/hard_target_override) . = ..() GLOB.portals += src if(!istype(_linked) && automatic_link) @@ -82,8 +78,6 @@ CRASH("Somebody fucked up.") if(_lifespan > 0) QDEL_IN(src, _lifespan) - if(!isnull(atmos_link_override)) - atmos_link = atmos_link_override link_portal(_linked) hardlinked = automatic_link if(isturf(hard_target_override)) @@ -97,51 +91,9 @@ /obj/effect/portal/proc/link_portal(obj/effect/portal/newlink) linked = newlink - if(atmos_link) - link_atmos() - -//This proc breaks as soon as atmos turfs are reacalculated, someone fix it -/obj/effect/portal/proc/link_atmos() - if(atmos_source || atmos_destination) - unlink_atmos() - if(!isopenturf(get_turf(src))) - return FALSE - if(linked) - if(isopenturf(get_turf(linked))) - atmos_source = get_turf(src) - atmos_destination = get_turf(linked) - else if(hard_target) - if(isopenturf(hard_target)) - atmos_source = get_turf(src) - atmos_destination = hard_target - else - return FALSE - if(!istype(atmos_source) || !istype(atmos_destination)) - return FALSE - LAZYINITLIST(atmos_source.atmos_adjacent_turfs) - LAZYINITLIST(atmos_destination.atmos_adjacent_turfs) - if(atmos_source.atmos_adjacent_turfs[atmos_destination] || atmos_destination.atmos_adjacent_turfs[atmos_source]) //Already linked! - return FALSE - atmos_source.atmos_adjacent_turfs[atmos_destination] = TRUE - atmos_destination.atmos_adjacent_turfs[atmos_source] = TRUE - atmos_source.air_update_turf(FALSE, FALSE) - atmos_destination.air_update_turf(FALSE, FALSE) - -/obj/effect/portal/proc/unlink_atmos() - if(istype(atmos_source)) - if(istype(atmos_destination)) - LAZYREMOVE(atmos_source.atmos_adjacent_turfs, atmos_destination) - atmos_source.immediate_calculate_adjacent_turfs() //Just in case they were next to each other - atmos_source = null - if(istype(atmos_destination)) - if(istype(atmos_source)) - LAZYREMOVE(atmos_destination.atmos_adjacent_turfs, atmos_source) - atmos_destination.immediate_calculate_adjacent_turfs() - atmos_destination = null /obj/effect/portal/Destroy() GLOB.portals -= src - unlink_atmos() if(hardlinked && !QDELETED(linked)) QDEL_NULL(linked) else diff --git a/code/game/objects/items/teleportation.dm b/code/game/objects/items/teleportation.dm index 1152bc2c6dd..f8842db9013 100644 --- a/code/game/objects/items/teleportation.dm +++ b/code/game/objects/items/teleportation.dm @@ -115,7 +115,6 @@ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ACID_PROOF var/list/active_portal_pairs var/max_portal_pairs = 3 - var/atmos_link_override /** * Represents the last place we teleported to, for making quick portals. @@ -245,7 +244,7 @@ if (!can_teleport_notifies(user)) return - var/list/obj/effect/portal/created = create_portal_pair(get_turf(user), get_teleport_turf(get_turf(teleport_target)), 300, 1, null, atmos_link_override) + var/list/obj/effect/portal/created = create_portal_pair(get_turf(user), get_teleport_turf(get_turf(teleport_target)), 300, 1, null) if(LAZYLEN(created) != 2) return diff --git a/code/game/objects/structures/transit_tubes/station.dm b/code/game/objects/structures/transit_tubes/station.dm index d0a77653cd6..73ba9019946 100644 --- a/code/game/objects/structures/transit_tubes/station.dm +++ b/code/game/objects/structures/transit_tubes/station.dm @@ -166,11 +166,10 @@ /obj/structure/transit_tube/station/proc/finish_stopped(obj/structure/transit_tube_pod/pod) pod_moving = FALSE - if(!QDELETED(pod)) - var/datum/gas_mixture/floor_mixture = loc.return_air() - floor_mixture.archive() - pod.air_contents.archive() - pod.air_contents.share(floor_mixture, 1) //mix the pod's gas mixture with the tile it's on + if(QDELETED(pod)) + return + var/datum/gas_mixture/floor_mixture = loc.return_air() + if(pod.air_contents.equalize(floor_mixture)) //equalize the pod's mix with the tile it's on air_update_turf(FALSE, FALSE) /obj/structure/transit_tube/station/init_tube_dirs() diff --git a/code/game/turfs/open/space/space.dm b/code/game/turfs/open/space/space.dm index 9ee43e83a85..e07faf3669f 100644 --- a/code/game/turfs/open/space/space.dm +++ b/code/game/turfs/open/space/space.dm @@ -14,6 +14,7 @@ var/destination_y var/static/datum/gas_mixture/immutable/space/space_gas = new + run_later = TRUE plane = PLANE_SPACE layer = SPACE_LAYER light_power = 0.25 diff --git a/code/modules/atmospherics/Atmospherics.md b/code/modules/atmospherics/Atmospherics.md index 49b688830e4..a71bd4887c1 100644 --- a/code/modules/atmospherics/Atmospherics.md +++ b/code/modules/atmospherics/Atmospherics.md @@ -157,6 +157,78 @@ As for react(), it is where all the behavior of the reaction is defined. The pro This is a rather large subject, we will need to cover gas flow, turf sleeping, superconduction, and much more. Strap in and enjoy the ride! +### A Word On `Share()` + +Each pair of turfs will only ever call `share()` on each other once. They use an archived cycle to keep track of +this ordering + +That means turf A calling share on turf B should work the same as turf B calling share on turf A + +The key idea of FEA, the core sharing system we use is that neighboring cells should effectively equalize with each other. +So taken on a line, you'd have two sharing partners, the cells to your left and right. The end goal of the simulation is for all the tiles on the line to have the same mix. But we can't just jump to that. So each "tick" we take our mix and average it with the mixes of the two tiles next to us. + +There's an equation for this that's considered standard in heat simulation. (Watch this video: https://www.youtube.com/watch?v=ly4S0oi3Yz8) +We can't use it because means each pair of turfs needs to talk to each other twice, which is pain expensive. That and I'm pretty sure it would prevent us from yielding + +So instead of a complex form of averaging, we portion up tiles. So if you have two neighbors and you have something they don't, you can give them each a third. Have to keep one for ourselves mind, because otherwise we'll run out of gas. They can then act on this portion however they like, and we can likewise act on a portion of them to our liking. + +We know how much gas a tile had at the outset because of the archived moles list index. If we take more then we're owed in any shares before all other turfs have had their say, we could end up with negative moles. We expend a lot of effort to avoid this. + +The math for this looks like (totaldeltagas)/(neighborcount + 1) + +You may notice something like this in `process_cell()`. It's not quite the same though. + +Back in the old FEA days, neighbor count was hardcoded to 4 (Likely because this is what cell sharing on an infinite grid would look like). This means that turf A -> turf B is the same as turf B -> turf A, because they're each portioning up the gas in the same way. + +But when we moved to LINDA, we started using the length of our atmos_adjacent_turfs list (or an analog). +We need this so things like multiz can work, and so tiles in a corner share in a way that makes sense. + +Because of this, turf A -> turf B was no longer the same as turf B -> turf A, assuming one of those turfs had a different neighbor count, from I DON'T KNOW WALLS? + +The fix for this was to use our neighbor count when moving gas from our tile to someone else's, and use the sharer's neighbor count when taking from it. + +This makes sense intuitively if you think of it like portioning up a tile, but I've included a rundown to make +it a bit easier to prove to yourself. + +
+Take a look + +I have 10 +You have 20 +let's share +I've got 2 partners +you've got 3 partners +so you want to give me 1/4th of your gas +I want to give you 1/3rd of my gas + +the total gas diff between me and you is -10 +since it's negative you get to decide how to portion it +so the total amount to share is -2.5 +I end up with 12.5 +you end up with 17.5 + +again + +total diff is -5 +to share is 1.25 +I end up with 13.75 +you end up with 16.25 + +again + +total diff is -2.5 +to share is 0.3125 +I end up with 14.0625 +you end up with 15.9375 + +
+ +We need to do this because if the portions get mixed up, our archived gas list ends up lying about how much of each gas type we have available to share. +This can lead to negative moles, which the system is not prepared for. + +This is also why we queue space's sucking till the end of a tile's `process_cell()` btw, by that point we can ensure that no other tile will need to check for our mix, so we can freely violate our portioning. + + ### Active Turfs ![](https://raw.githubusercontent.com/tgstation/documentation-assets/main/atmos/FlowVisuals.png) @@ -164,7 +236,7 @@ This is a rather large subject, we will need to cover gas flow, turf sleeping, s Active turfs are the backbone of how gas moves from tile to tile. While most of `process_cell()` should be easy enough to understand, I am going to go into some detail about archiving, since I think it's a common source of hiccups. -* *`archived_cycle`* this var stores the last cycle of the atmos loop that the turf processed on. The key point to notice here is that when processing a turf, we don't share with all its neighbors, we only talk to those who haven't processed yet. This is because the remainder of `process_cell()` and especially `share()` ought to be similar in form to addition. We can add in any order we like, and we only need to add once. This is what archived gases are for by the way, they store the state of the relevant tile before any processing occurs. This additive behavior isn't strictly the case unfortunately, but it's minor enough that we can ignore the effects. +* *`archived_cycle`* this var stores the last cycle of the atmos loop that the turf processed on. The key point to notice here is that when processing a turf, we don't share with all its neighbors, we only talk to those who haven't processed yet. This is because the remainder of `process_cell()` and especially `share()` are like addition. We can add in any order we like, and we only need to add once. This is what archived gases are for by the way, they store the state of the relevant tile before any processing occurs. Alright then, with that out of the way, what is an active turf. diff --git a/code/modules/atmospherics/environmental/LINDA_turf_tile.dm b/code/modules/atmospherics/environmental/LINDA_turf_tile.dm index b09a4bba60b..ad673e6ca0b 100644 --- a/code/modules/atmospherics/environmental/LINDA_turf_tile.dm +++ b/code/modules/atmospherics/environmental/LINDA_turf_tile.dm @@ -39,8 +39,11 @@ ///If there is an active hotspot on us store a reference to it here var/obj/effect/hotspot/active_hotspot - ///Whether or not we are a planetary turf - var/planetary_atmos = FALSE //air will revert to initial_gas_mix + /// air will slowly revert to initial_gas_mix + var/planetary_atmos = FALSE + /// once our paired turfs are finished with all other shares, do one 100% share + /// exists so things like space can ask to take 100% of a tile's gas + var/run_later = FALSE ///gas IDs of current active gas overlays var/list/atmos_overlay_types @@ -249,15 +252,22 @@ //cache for sanic speed var/list/adjacent_turfs = atmos_adjacent_turfs var/datum/excited_group/our_excited_group = excited_group - var/adjacent_turfs_length = LAZYLEN(adjacent_turfs) + var/our_share_coeff = 1/(LAZYLEN(adjacent_turfs) + 1) var/datum/gas_mixture/our_air = air + var/list/share_end + #ifdef TRACK_MAX_SHARE max_share = 0 //Gotta reset our tracker #endif for(var/turf/open/enemy_tile as anything in adjacent_turfs) + // This var is only rarely set, exists so turfs can request to share at the end of our sharing + // We need this so we can assume share is communative, which we need to do to avoid a hellish amount of garbage_collect()s + if(enemy_tile.run_later) + LAZYADD(share_end, enemy_tile) + if(fire_count <= enemy_tile.current_cycle) continue LINDA_CYCLE_ARCHIVE(enemy_tile) @@ -292,7 +302,7 @@ //air sharing if(should_share_air) - var/difference = our_air.share(enemy_air, adjacent_turfs_length) + var/difference = our_air.share(enemy_air, our_share_coeff, 1 / (LAZYLEN(enemy_tile.atmos_adjacent_turfs) + 1)) if(difference) if(difference > 0) consider_pressure_difference(enemy_tile, difference) @@ -314,12 +324,25 @@ new_group.add_turf(src) our_excited_group = excited_group // shares 4/5 of our difference in moles with the atmosphere - our_air.share(planetary_mix, 0.25) + our_air.share(planetary_mix, 0.8, 0.8) // temperature share with the atmosphere with an inflated heat capacity to simulate faster sharing with a large atmosphere our_air.temperature_share(planetary_mix, OPEN_HEAT_TRANSFER_COEFFICIENT, planetary_mix.temperature_archived, planetary_mix.heat_capacity() * 5) planetary_mix.garbage_collect() PLANET_SHARE_CHECK + for(var/turf/open/enemy_tile as anything in share_end) + var/datum/gas_mixture/enemy_mix = enemy_tile.air + archive() + // We share 100% of our mix in this step. Let's jive + var/difference = our_air.share(enemy_mix, 1, 1) + LAST_SHARE_CHECK + if(!difference) + continue + if(difference > 0) + consider_pressure_difference(enemy_tile, difference) + else + enemy_tile.consider_pressure_difference(src, difference) + our_air.react(src) update_visuals() diff --git a/code/modules/atmospherics/gasmixtures/gas_mixture.dm b/code/modules/atmospherics/gasmixtures/gas_mixture.dm index 567dffe417c..c34ea2f469d 100644 --- a/code/modules/atmospherics/gasmixtures/gas_mixture.dm +++ b/code/modules/atmospherics/gasmixtures/gas_mixture.dm @@ -335,12 +335,17 @@ GLOBAL_LIST_INIT(gaslist_cache, init_gaslist_cache()) gases[path][MOLES] = text2num(gas[id]) return 1 -///Performs air sharing calculations between two gas_mixtures assuming only 1 boundary length -///Returns: amount of gas exchanged (+ if sharer received) -/datum/gas_mixture/proc/share(datum/gas_mixture/sharer, atmos_adjacent_turfs = 4) +/// Performs air sharing calculations between two gas_mixtures +/// share() is communitive, which means A.share(B) needs to be the same as B.share(A) +/// If we don't retain this, we will get negative moles. Don't do it +/// Returns: amount of gas exchanged (+ if sharer received) +/datum/gas_mixture/proc/share(datum/gas_mixture/sharer, our_coeff, sharer_coeff) var/list/cached_gases = gases var/list/sharer_gases = sharer.gases + var/list/only_in_sharer = sharer_gases - cached_gases + var/list/only_in_cached = cached_gases - sharer_gases + var/temperature_delta = temperature_archived - sharer.temperature_archived var/abs_temperature_delta = abs(temperature_delta) @@ -357,17 +362,29 @@ GLOBAL_LIST_INIT(gaslist_cache, init_gaslist_cache()) var/abs_moved_moles = 0 //GAS TRANSFER - for(var/id in sharer_gases - cached_gases) // create gases not in our cache - ADD_GAS(id, gases) - for(var/id in cached_gases) // transfer gases - ASSERT_GAS(id, sharer) + //Prep + for(var/id in only_in_sharer) //create gases not in our cache + ADD_GAS(id, cached_gases) + for(var/id in only_in_cached) //create gases not in the sharing mix + ADD_GAS(id, sharer_gases) + + for(var/id in cached_gases) //transfer gases var/gas = cached_gases[id] var/sharergas = sharer_gases[id] + var/delta = QUANTIZE(gas[ARCHIVE] - sharergas[ARCHIVE]) //the amount of gas that gets moved between the mixtures - var/delta = QUANTIZE(gas[ARCHIVE] - sharergas[ARCHIVE])/(atmos_adjacent_turfs+1) //the amount of gas that gets moved between the mixtures + if(!delta) + continue - if(delta && abs_temperature_delta > MINIMUM_TEMPERATURE_DELTA_TO_CONSIDER) + // If we have more gas then they do, gas is moving from us to them + // This means we want to scale it by our coeff. Vis versa for their case + if(delta > 0) + delta = delta * our_coeff + else + delta = delta * sharer_coeff + + if(abs_temperature_delta > MINIMUM_TEMPERATURE_DELTA_TO_CONSIDER) var/gas_heat_capacity = delta * gas[GAS_META][META_GAS_SPECIFIC_HEAT] if(delta > 0) heat_capacity_self_to_sharer += gas_heat_capacity @@ -398,8 +415,12 @@ GLOBAL_LIST_INIT(gaslist_cache, init_gaslist_cache()) if(abs(new_sharer_heat_capacity/old_sharer_heat_capacity - 1) < 0.1) // <10% change in sharer heat capacity temperature_share(sharer, OPEN_HEAT_TRANSFER_COEFFICIENT) - garbage_collect() - sharer.garbage_collect() + if(length(only_in_sharer + only_in_cached)) //if all gases were present in both mixtures, we know that no gases are 0 + garbage_collect(only_in_cached) //any gases the sharer had, we are guaranteed to have. gases that it didn't have we are not. + sharer.garbage_collect(only_in_sharer) //the reverse is equally true + else if (initial(sharer.gc_share)) + sharer.garbage_collect() + if(temperature_delta > MINIMUM_TEMPERATURE_TO_MOVE || abs(moved_moles) > MINIMUM_MOLES_DELTA_TO_MOVE) var/our_moles TOTAL_MOLES(cached_gases,our_moles) diff --git a/code/modules/atmospherics/gasmixtures/immutable_mixtures.dm b/code/modules/atmospherics/gasmixtures/immutable_mixtures.dm index 6720b59e28f..e6cd8f75499 100644 --- a/code/modules/atmospherics/gasmixtures/immutable_mixtures.dm +++ b/code/modules/atmospherics/gasmixtures/immutable_mixtures.dm @@ -20,8 +20,8 @@ /datum/gas_mixture/immutable/merge() return FALSE //we're immutable. -/datum/gas_mixture/immutable/share(datum/gas_mixture/sharer, atmos_adjacent_turfs = 4) - . = ..(sharer, 0) +/datum/gas_mixture/immutable/share(datum/gas_mixture/sharer, our_coeff, sharer_coeff) + . = ..() sharer.temperature = initial_temperature garbage_collect() diff --git a/code/modules/events/wormholes.dm b/code/modules/events/wormholes.dm index a7c64aa63ed..d5575ce6072 100644 --- a/code/modules/events/wormholes.dm +++ b/code/modules/events/wormholes.dm @@ -52,7 +52,7 @@ GLOBAL_LIST_EMPTY(all_wormholes) // So we can pick wormholes to teleport to mech_sized = TRUE -/obj/effect/portal/wormhole/Initialize(mapload, _creator, _lifespan = 0, obj/effect/portal/_linked, automatic_link = FALSE, turf/hard_target_override, atmos_link_override) +/obj/effect/portal/wormhole/Initialize(mapload, _creator, _lifespan = 0, obj/effect/portal/_linked, automatic_link = FALSE, turf/hard_target_override) . = ..() GLOB.all_wormholes += src diff --git a/code/modules/projectiles/guns/energy/special.dm b/code/modules/projectiles/guns/energy/special.dm index 49dc07ef1d2..5394570da47 100644 --- a/code/modules/projectiles/guns/energy/special.dm +++ b/code/modules/projectiles/guns/energy/special.dm @@ -213,7 +213,6 @@ automatic_charge_overlays = FALSE var/obj/effect/portal/p_blue var/obj/effect/portal/p_orange - var/atmos_link = FALSE var/firing_core = FALSE /obj/item/gun/energy/wormhole_projector/examine(mob/user) @@ -292,7 +291,7 @@ p_blue.link_portal(p_orange) /obj/item/gun/energy/wormhole_projector/proc/create_portal(obj/projectile/beam/wormhole/W, turf/target) - var/obj/effect/portal/P = new /obj/effect/portal(target, 300, null, FALSE, null, atmos_link) + var/obj/effect/portal/P = new /obj/effect/portal(target, 300, null, FALSE, null) RegisterSignal(P, COMSIG_PARENT_QDELETING, .proc/on_portal_destroy) if(istype(W, /obj/projectile/beam/wormhole/orange)) qdel(p_orange)