Files
Bubberstation/code/controllers/subsystem/air.dm
T
LemonInTheDark 23bfdec8f4 Multiz Rework: Human Suffering Edition (Contains PLANE CUBE) (#69115)
About The Pull Request

I've reworked multiz. This was done because our current implementation of multiz flattens planes down into just the openspace plane. This breaks any effects we attach to plane masters (including lighting), but it also totally kills the SIDE_MAP map format, which we NEED for wallening (A major 3/4ths resprite of all wall and wall adjacent things, making them more then one tile high. Without sidemap we would be unable to display things both in from of and behind objects on map. Stupid.)

This required MASSIVE changes. Both to all uses of the plane var for reasons I'll discuss later, and to a ton of different systems that interact with rendering.

I'll do my best to keep this compact, but there's only so much I can do. Sorry brother.
Core idea

OK: first thing.
vis_contents as it works now squishes the planes of everything inside it down into the plane of the vis_loc.
This is bad. But how to do better?

It's trivially easy to make copies of our existing plane masters but offset, and relay them to the bottom of the plane above. Not a problem. The issue is how to get the actual atoms on the map to "land" on them properly.

We could use FLOAT_PLANE to offset planes based off how they're being seen, in theory this would allow us to create lens for how objects are viewed.
But that's not a stable thing to do, because properly "landing" a plane on a desired plane master would require taking into account every bit of how it's being seen, would inherently break this effect.

Ok so we need to manually edit planes based off "z layer" (IE: what layer of a z stack are you on).

That's the key conceit of this pr. Implementing the plane cube, and ensuring planes are always offset properly.
Everything else is just gravy.
About the Plane Cube

Each plane master (except ones that opt out) is copied down by some constant value equal to the max absolute change between the first and the last plane.
We do this based off the max z stack size detected by SSmapping. This is also where updates come from, and where all our updating logic will live.

As mentioned, plane masters can choose to opt out of being mirrored down. In this case, anything that interacts with them assuming that they'll be offset will instead just get back the valid plane value. This works for render targets too, since I had to work them into the system as well.

Plane masters can also be temporarily hidden from the client's screen. This is done as an attempt at optimization, and applies to anything used in niche cases, or planes only used if there's a z layer below you.
About Plane Master Groups

BYOND supports having different "maps" on screen at once (IE: groups of items/turfs/etc)
Plane masters cannot cover 2 maps at once, since their location is determined by their screen_loc.
So we need to maintain a mirror of each plane for every map we have open.

This was quite messy, so I've refactored it (and maps too) to be a bit more modular.

Rather then storing a list of plane masters, we store a list of plane master group datums.
Each datum is in charge of the plane masters for its particular map, both creating them, and managing them.

Like I mentioned, I also refactored map views. Adding a new mapview is now as simple as newing a /atom/movable/screen/map_view, calling generate_view with the appropriate map id, setting things you want to display in its vis_contents, and then calling display_to on it, passing in the mob to show ourselves to.

Much better then the hardcoded pattern we used to use. So much duplicated code man.

Oh and plane master controllers, that system we have that allows for applying filters to sets of plane masters? I've made it use lookups on plane master groups now, rather then hanging references to all impacted planes. This makes logic easier, and prevents the need to manage references and update the controllers.

image

In addition, I've added a debug ui for plane masters.
It allows you to view all of your own plane masters and short descriptions of what they do, alongside tools for editing them and their relays.

It ALSO supports editing someone elses plane masters, AND it supports (in a very fragile and incomplete manner) viewing literally through someone else's eyes, including their plane masters. This is very useful, because it means you can debug "hey my X is yorked" issues yourself, on live.

In order to accomplish this I have needed to add setters for an ungodly amount of visual impacting vars. Sight flags, eye, see_invis, see_in_dark, etc.

It also comes with an info dump about the ui, and plane masters/relays in general.

Sort of on that note. I've documented everything I know that's niche/useful about our visual effects and rendering system. My hope is this will serve to bring people up to speed on what can be done more quickly, alongside making my sin here less horrible.
See https://github.com/LemonInTheDark/tgstation/blob/multiz-hell/.github/guides/VISUALS.md.
"Landing" planes

Ok so I've explained the backend, but how do we actually land planes properly?
Most of the time this is really simple. When a plane var is set, we need to provide some spokesperson for the appearance's z level. We can use this to derive their z layer, and thus what offset to use.

This is just a lot of gruntwork, but it's occasionally more complex.
Sometimes we need to cache a list of z layer -> effect, and then use that.
Also a LOT of updating on z move. So much z move shit.

Oh. and in order to make byond darkness work properly, I needed to add SEE_BLACKNESS to all sight flags.
This draws darkness to plane 0, which means I'm able to relay it around and draw it on different z layers as is possible. fun darkness ripple effects incoming someday

I also need to update mob overlays on move.
I do this by realiizing their appearances, mutating their plane, and then readding the overlay in the correct order.

The cost of this is currently 3N. I'm convinced this could be improved, but I've not got to it yet.
It can also occasionally cause overlays to corrupt. This is fixed by laying a protective ward of overlays.Copy in the sand, but that spell makes the compiler confused, so I'll have to bully lummy about fixing it at some point.
Behavior changes

We've had to give up on the already broken gateway "see through" effect. Won't work without managing gateway plane masters or something stupid. Not worth it.
So instead we display the other side as a ui element. It's worse, but not that bad.

Because vis_contents no longer flattens planes (most of the time), some uses of it now have interesting behavior.
The main thing that comes to mind is alert popups that display mobs. They can impact the lighting plane.
I don't really care, but it should be fixable, I think, given elbow grease.

Ah and I've cleaned up layers and plane defines to make them a bit easier to read/reason about, at least I think.
Why It's Good For The Game
<visual candy>

Fixes #65800
Fixes #68461
Changelog

cl
refactor: Refactored... well a lot really. Map views, anything to do with planes, multiz, a shit ton of rendering stuff. Basically if you see anything off visually report it
admin: VV a mob, and hit View/Edit Planes in the dropdown to steal their view, and modify it as you like. You can do the same to yourself using the Edit/Debug Planes verb
/cl
2022-09-27 20:11:04 +13:00

840 lines
27 KiB
Plaintext

SUBSYSTEM_DEF(air)
name = "Atmospherics"
init_order = INIT_ORDER_AIR
priority = FIRE_PRIORITY_AIR
wait = 0.5 SECONDS
flags = SS_BACKGROUND
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
var/cached_cost = 0
var/cost_atoms = 0
var/cost_turfs = 0
var/cost_hotspots = 0
var/cost_groups = 0
var/cost_highpressure = 0
var/cost_superconductivity = 0
var/cost_pipenets = 0
var/cost_atmos_machinery = 0
var/cost_rebuilds = 0
var/cost_adjacent = 0
var/list/excited_groups = list()
var/list/active_turfs = list()
var/list/hotspots = list()
var/list/networks = list()
var/list/rebuild_queue = list()
//Subservient to rebuild queue
var/list/expansion_queue = list()
/// List of turfs to recalculate adjacent turfs on before processing
var/list/adjacent_rebuild = list()
/// A list of machines that will be processed when currentpart == SSAIR_ATMOSMACHINERY. Use SSair.begin_processing_machine and SSair.stop_processing_machine to add and remove machines.
var/list/obj/machinery/atmos_machinery = list()
var/list/pipe_init_dirs_cache = list()
//atmos singletons
var/list/gas_reactions = list()
var/list/atmos_gen
var/list/planetary = list() //Lets cache static planetary mixes
/// List of gas string -> canonical gas mixture
var/list/strings_to_mix = list()
//Special functions lists
var/list/turf/active_super_conductivity = list()
var/list/turf/open/high_pressure_delta = list()
var/list/atom_process = list()
/// Reactions which will contribute to a hotspot's size.
var/list/hotspot_reactions
/// A cache of objects that perisists between processing runs when resumed == TRUE. Dangerous, qdel'd objects not cleared from this may cause runtimes on processing.
var/list/currentrun = list()
var/currentpart = SSAIR_PIPENETS
var/map_loading = TRUE
var/list/queued_for_activation
var/display_all_groups = FALSE
var/list/reaction_handbook
var/list/gas_handbook
/datum/controller/subsystem/air/stat_entry(msg)
msg += "C:{"
msg += "AT:[round(cost_turfs,1)]|"
msg += "HS:[round(cost_hotspots,1)]|"
msg += "EG:[round(cost_groups,1)]|"
msg += "HP:[round(cost_highpressure,1)]|"
msg += "SC:[round(cost_superconductivity,1)]|"
msg += "PN:[round(cost_pipenets,1)]|"
msg += "AM:[round(cost_atmos_machinery,1)]|"
msg += "AO:[round(cost_atoms, 1)]|"
msg += "RB:[round(cost_rebuilds,1)]|"
msg += "AJ:[round(cost_adjacent,1)]|"
msg += "} "
msg += "AT:[active_turfs.len]|"
msg += "HS:[hotspots.len]|"
msg += "EG:[excited_groups.len]|"
msg += "HP:[high_pressure_delta.len]|"
msg += "SC:[active_super_conductivity.len]|"
msg += "PN:[networks.len]|"
msg += "AM:[atmos_machinery.len]|"
msg += "AO:[atom_process.len]|"
msg += "RB:[rebuild_queue.len]|"
msg += "EP:[expansion_queue.len]|"
msg += "AJ:[adjacent_rebuild.len]|"
msg += "AT/MS:[round((cost ? active_turfs.len/cost : 0),0.1)]"
return ..()
/datum/controller/subsystem/air/Initialize()
map_loading = FALSE
gas_reactions = init_gas_reactions()
hotspot_reactions = init_hotspot_reactions()
setup_allturfs()
setup_atmos_machinery()
setup_pipenets()
setup_turf_visuals()
process_adjacent_rebuild()
atmos_handbooks_init()
return SS_INIT_SUCCESS
/datum/controller/subsystem/air/fire(resumed = FALSE)
var/timer = TICK_USAGE_REAL
//Rebuilds can happen at any time, so this needs to be done outside of the normal system
cost_rebuilds = 0
cost_adjacent = 0
// We need to have a solid setup for turfs before fire, otherwise we'll get massive runtimes and strange behavior
if(length(adjacent_rebuild))
timer = TICK_USAGE_REAL
process_adjacent_rebuild()
//This does mean that the apperent rebuild costs fluctuate very quickly, this is just the cost of having them always process, no matter what
cost_adjacent = TICK_USAGE_REAL - timer
if(state != SS_RUNNING)
return
// Every time we fire, we want to make sure pipenets are rebuilt. The game state could have changed between each fire() proc call
// and anything missing a pipenet can lead to unintended behaviour at worse and various runtimes at best.
if(length(rebuild_queue) || length(expansion_queue))
timer = TICK_USAGE_REAL
process_rebuilds()
//This does mean that the apperent rebuild costs fluctuate very quickly, this is just the cost of having them always process, no matter what
cost_rebuilds = TICK_USAGE_REAL - timer
if(state != SS_RUNNING)
return
if(currentpart == SSAIR_PIPENETS || !resumed)
timer = TICK_USAGE_REAL
if(!resumed)
cached_cost = 0
process_pipenets(resumed)
cached_cost += TICK_USAGE_REAL - timer
if(state != SS_RUNNING)
return
cost_pipenets = MC_AVERAGE(cost_pipenets, TICK_DELTA_TO_MS(cached_cost))
resumed = FALSE
currentpart = SSAIR_ATMOSMACHINERY
if(currentpart == SSAIR_ATMOSMACHINERY)
timer = TICK_USAGE_REAL
if(!resumed)
cached_cost = 0
process_atmos_machinery(resumed)
cached_cost += TICK_USAGE_REAL - timer
if(state != SS_RUNNING)
return
cost_atmos_machinery = MC_AVERAGE(cost_atmos_machinery, TICK_DELTA_TO_MS(cached_cost))
resumed = FALSE
currentpart = SSAIR_ACTIVETURFS
if(currentpart == SSAIR_ACTIVETURFS)
timer = TICK_USAGE_REAL
if(!resumed)
cached_cost = 0
process_active_turfs(resumed)
cached_cost += TICK_USAGE_REAL - timer
if(state != SS_RUNNING)
return
cost_turfs = MC_AVERAGE(cost_turfs, TICK_DELTA_TO_MS(cached_cost))
resumed = FALSE
currentpart = SSAIR_HOTSPOTS
if(currentpart == SSAIR_HOTSPOTS) //We do this before excited groups to allow breakdowns to be independent of adding turfs while still *mostly preventing mass fires
timer = TICK_USAGE_REAL
if(!resumed)
cached_cost = 0
process_hotspots(resumed)
cached_cost += TICK_USAGE_REAL - timer
if(state != SS_RUNNING)
return
cost_hotspots = MC_AVERAGE(cost_hotspots, TICK_DELTA_TO_MS(cached_cost))
resumed = FALSE
currentpart = SSAIR_EXCITEDGROUPS
if(currentpart == SSAIR_EXCITEDGROUPS)
timer = TICK_USAGE_REAL
if(!resumed)
cached_cost = 0
process_excited_groups(resumed)
cached_cost += TICK_USAGE_REAL - timer
if(state != SS_RUNNING)
return
cost_groups = MC_AVERAGE(cost_groups, TICK_DELTA_TO_MS(cached_cost))
resumed = FALSE
currentpart = SSAIR_HIGHPRESSURE
if(currentpart == SSAIR_HIGHPRESSURE)
timer = TICK_USAGE_REAL
if(!resumed)
cached_cost = 0
process_high_pressure_delta(resumed)
cached_cost += TICK_USAGE_REAL - timer
if(state != SS_RUNNING)
return
cost_highpressure = MC_AVERAGE(cost_highpressure, TICK_DELTA_TO_MS(cached_cost))
resumed = FALSE
currentpart = SSAIR_SUPERCONDUCTIVITY
if(currentpart == SSAIR_SUPERCONDUCTIVITY)
timer = TICK_USAGE_REAL
if(!resumed)
cached_cost = 0
process_super_conductivity(resumed)
cached_cost += TICK_USAGE_REAL - timer
if(state != SS_RUNNING)
return
cost_superconductivity = MC_AVERAGE(cost_superconductivity, TICK_DELTA_TO_MS(cached_cost))
resumed = FALSE
currentpart = SSAIR_PROCESS_ATOMS
if(currentpart == SSAIR_PROCESS_ATOMS)
timer = TICK_USAGE_REAL
if(!resumed)
cached_cost = 0
process_atoms(resumed)
cached_cost += TICK_USAGE_REAL - timer
if(state != SS_RUNNING)
return
cost_atoms = MC_AVERAGE(cost_atoms, TICK_DELTA_TO_MS(cached_cost))
resumed = FALSE
currentpart = SSAIR_PIPENETS
SStgui.update_uis(SSair) //Lightning fast debugging motherfucker
/datum/controller/subsystem/air/Recover()
excited_groups = SSair.excited_groups
active_turfs = SSair.active_turfs
hotspots = SSair.hotspots
networks = SSair.networks
rebuild_queue = SSair.rebuild_queue
expansion_queue = SSair.expansion_queue
adjacent_rebuild = SSair.adjacent_rebuild
atmos_machinery = SSair.atmos_machinery
pipe_init_dirs_cache = SSair.pipe_init_dirs_cache
gas_reactions = SSair.gas_reactions
atmos_gen = SSair.atmos_gen
planetary = SSair.planetary
active_super_conductivity = SSair.active_super_conductivity
high_pressure_delta = SSair.high_pressure_delta
atom_process = SSair.atom_process
currentrun = SSair.currentrun
queued_for_activation = SSair.queued_for_activation
/datum/controller/subsystem/air/proc/process_adjacent_rebuild(init = FALSE)
var/list/queue = adjacent_rebuild
while (length(queue))
var/turf/currT = queue[1]
var/goal = queue[currT]
queue.Cut(1,2)
currT.immediate_calculate_adjacent_turfs()
if(goal == MAKE_ACTIVE)
add_to_active(currT)
else if(goal == KILL_EXCITED)
add_to_active(currT, TRUE)
if(init)
CHECK_TICK
else
if(MC_TICK_CHECK)
break
/datum/controller/subsystem/air/proc/process_pipenets(resumed = FALSE)
if (!resumed)
src.currentrun = networks.Copy()
//cache for sanic speed (lists are references anyways)
var/list/currentrun = src.currentrun
while(currentrun.len)
var/datum/thing = currentrun[currentrun.len]
currentrun.len--
if(thing)
thing.process()
else
networks.Remove(thing)
if(MC_TICK_CHECK)
return
/datum/controller/subsystem/air/proc/add_to_rebuild_queue(obj/machinery/atmospherics/atmos_machine)
if(istype(atmos_machine, /obj/machinery/atmospherics) && !atmos_machine.rebuilding)
rebuild_queue += atmos_machine
atmos_machine.rebuilding = TRUE
/datum/controller/subsystem/air/proc/add_to_expansion(datum/pipeline/line, starting_point)
var/list/new_packet = new(SSAIR_REBUILD_QUEUE)
new_packet[SSAIR_REBUILD_PIPELINE] = line
new_packet[SSAIR_REBUILD_QUEUE] = list(starting_point)
expansion_queue += list(new_packet)
/datum/controller/subsystem/air/proc/remove_from_expansion(datum/pipeline/line)
for(var/list/packet in expansion_queue)
if(packet[SSAIR_REBUILD_PIPELINE] == line)
expansion_queue -= packet
return
/datum/controller/subsystem/air/proc/process_atoms(resumed = FALSE)
if(!resumed)
src.currentrun = atom_process.Copy()
//cache for sanic speed (lists are references anyways)
var/list/currentrun = src.currentrun
while(currentrun.len)
var/atom/talk_to = currentrun[currentrun.len]
currentrun.len--
if(!talk_to)
return
talk_to.process_exposure()
if(MC_TICK_CHECK)
return
/datum/controller/subsystem/air/proc/process_atmos_machinery(resumed = FALSE)
if (!resumed)
src.currentrun = atmos_machinery.Copy()
//cache for sanic speed (lists are references anyways)
var/list/currentrun = src.currentrun
while(currentrun.len)
var/obj/machinery/M = currentrun[currentrun.len]
currentrun.len--
if(!M)
atmos_machinery -= M
if(M.process_atmos() == PROCESS_KILL)
stop_processing_machine(M)
if(MC_TICK_CHECK)
return
/datum/controller/subsystem/air/proc/process_super_conductivity(resumed = FALSE)
if (!resumed)
src.currentrun = active_super_conductivity.Copy()
//cache for sanic speed (lists are references anyways)
var/list/currentrun = src.currentrun
while(currentrun.len)
var/turf/T = currentrun[currentrun.len]
currentrun.len--
T.super_conduct()
if(MC_TICK_CHECK)
return
/datum/controller/subsystem/air/proc/process_hotspots(resumed = FALSE)
if (!resumed)
src.currentrun = hotspots.Copy()
//cache for sanic speed (lists are references anyways)
var/list/currentrun = src.currentrun
while(currentrun.len)
var/obj/effect/hotspot/H = currentrun[currentrun.len]
currentrun.len--
if (H)
H.process()
else
hotspots -= H
if(MC_TICK_CHECK)
return
/datum/controller/subsystem/air/proc/process_high_pressure_delta(resumed = FALSE)
while (high_pressure_delta.len)
var/turf/open/T = high_pressure_delta[high_pressure_delta.len]
high_pressure_delta.len--
T.high_pressure_movements()
T.pressure_difference = 0
if(MC_TICK_CHECK)
return
/datum/controller/subsystem/air/proc/process_active_turfs(resumed = FALSE)
//cache for sanic speed
var/fire_count = times_fired
if (!resumed)
src.currentrun = active_turfs.Copy()
//cache for sanic speed (lists are references anyways)
var/list/currentrun = src.currentrun
while(currentrun.len)
var/turf/open/T = currentrun[currentrun.len]
currentrun.len--
if (T)
T.process_cell(fire_count)
if (MC_TICK_CHECK)
return
/datum/controller/subsystem/air/proc/process_excited_groups(resumed = FALSE)
if (!resumed)
src.currentrun = excited_groups.Copy()
//cache for sanic speed (lists are references anyways)
var/list/currentrun = src.currentrun
while(currentrun.len)
var/datum/excited_group/EG = currentrun[currentrun.len]
currentrun.len--
EG.breakdown_cooldown++
EG.dismantle_cooldown++
if(EG.breakdown_cooldown >= EXCITED_GROUP_BREAKDOWN_CYCLES)
EG.self_breakdown(poke_turfs = TRUE)
else if(EG.dismantle_cooldown >= EXCITED_GROUP_DISMANTLE_CYCLES && !(EG.turf_reactions & (REACTING | STOP_REACTIONS)))
EG.dismantle()
EG.turf_reactions = NONE
if (MC_TICK_CHECK)
return
/datum/controller/subsystem/air/proc/process_rebuilds()
//Yes this does mean rebuilding pipenets can freeze up the subsystem forever, but if we're in that situation something else is very wrong
var/list/currentrun = rebuild_queue
while(currentrun.len || length(expansion_queue))
while(currentrun.len && !length(expansion_queue)) //If we found anything, process that first
var/obj/machinery/atmospherics/remake = currentrun[currentrun.len]
currentrun.len--
if (!remake)
continue
remake.rebuild_pipes()
if (MC_TICK_CHECK)
return
var/list/queue = expansion_queue
while(queue.len)
var/list/pack = queue[queue.len]
//We operate directly with the pipeline like this because we can trust any rebuilds to remake it properly
var/datum/pipeline/linepipe = pack[SSAIR_REBUILD_PIPELINE]
var/list/border = pack[SSAIR_REBUILD_QUEUE]
expand_pipeline(linepipe, border)
if(state != SS_RUNNING) //expand_pipeline can fail a tick check, we shouldn't let things get too fucky here
return
linepipe.building = FALSE
queue.len--
if (MC_TICK_CHECK)
return
///Rebuilds a pipeline by expanding outwards, while yielding when sane
/datum/controller/subsystem/air/proc/expand_pipeline(datum/pipeline/net, list/border)
while(border.len)
var/obj/machinery/atmospherics/borderline = border[border.len]
border.len--
var/list/result = borderline.pipeline_expansion(net)
if(!length(result))
continue
for(var/obj/machinery/atmospherics/considered_device in result)
if(!istype(considered_device, /obj/machinery/atmospherics/pipe))
considered_device.set_pipenet(net, borderline)
net.add_machinery_member(considered_device)
continue
var/obj/machinery/atmospherics/pipe/item = considered_device
if(net.members.Find(item))
continue
if(item.parent)
var/static/pipenetwarnings = 10
if(pipenetwarnings > 0)
log_mapping("build_pipeline(): [item.type] added to a pipenet while still having one. (pipes leading to the same spot stacking in one turf) around [AREACOORD(item)].")
pipenetwarnings--
if(pipenetwarnings == 0)
log_mapping("build_pipeline(): further messages about pipenets will be suppressed")
net.members += item
border += item
net.air.volume += item.volume
item.parent = net
if(item.air_temporary)
net.air.merge(item.air_temporary)
item.air_temporary = null
if (MC_TICK_CHECK)
return
///Removes a turf from processing, and causes its excited group to clean up so things properly adapt to the change
/datum/controller/subsystem/air/proc/remove_from_active(turf/open/T)
active_turfs -= T
if(currentpart == SSAIR_ACTIVETURFS)
currentrun -= T
#ifdef VISUALIZE_ACTIVE_TURFS //Use this when you want details about how the turfs are moving, display_all_groups should work for normal operation
T.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, COLOR_VIBRANT_LIME)
#endif
if(istype(T))
T.excited = FALSE
if(T.excited_group)
//If this fires during active turfs it'll cause a slight removal of active turfs, as they breakdown if they have no excited group
//The group also expands by a tile per rebuild on each edge, suffering
T.excited_group.garbage_collect() //Kill the excited group, it'll reform on its own later
///Puts an active turf to sleep so it doesn't process. Do this without cleaning up its excited group.
/datum/controller/subsystem/air/proc/sleep_active_turf(turf/open/T)
active_turfs -= T
if(currentpart == SSAIR_ACTIVETURFS)
currentrun -= T
#ifdef VISUALIZE_ACTIVE_TURFS
T.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, COLOR_VIBRANT_LIME)
#endif
if(istype(T))
T.excited = FALSE
///Adds a turf to active processing, handles duplicates. Call this with blockchanges == TRUE if you want to nuke the assoc excited group
/datum/controller/subsystem/air/proc/add_to_active(turf/open/T, blockchanges = FALSE)
if(istype(T) && T.air)
T.significant_share_ticker = 0
if(blockchanges && T.excited_group) //This is used almost exclusivly for shuttles, so the excited group doesn't stay behind
T.excited_group.garbage_collect() //Nuke it
if(T.excited) //Don't keep doing it if there's no point
return
#ifdef VISUALIZE_ACTIVE_TURFS
T.add_atom_colour(COLOR_VIBRANT_LIME, TEMPORARY_COLOUR_PRIORITY)
#endif
T.excited = TRUE
active_turfs += T
if(currentpart == SSAIR_ACTIVETURFS)
currentrun += T
else if(T.flags_1 & INITIALIZED_1)
for(var/turf/S in T.atmos_adjacent_turfs)
add_to_active(S, TRUE)
else if(map_loading)
if(queued_for_activation)
queued_for_activation[T] = T
return
else
T.requires_activation = TRUE
/datum/controller/subsystem/air/StartLoadingMap()
LAZYINITLIST(queued_for_activation)
map_loading = TRUE
/datum/controller/subsystem/air/StopLoadingMap()
map_loading = FALSE
for(var/T in queued_for_activation)
add_to_active(T, TRUE)
queued_for_activation.Cut()
/datum/controller/subsystem/air/proc/setup_allturfs()
var/list/turfs_to_init = block(locate(1, 1, 1), locate(world.maxx, world.maxy, world.maxz))
var/list/active_turfs = src.active_turfs
times_fired++
// Clear active turfs - faster than removing every single turf in the world
// one-by-one, and Initalize_Atmos only ever adds `src` back in.
#ifdef VISUALIZE_ACTIVE_TURFS
for(var/jumpy in active_turfs)
var/turf/active = jumpy
active.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, COLOR_VIBRANT_LIME)
#endif
active_turfs.Cut()
var/time = 0
for(var/turf/T as anything in turfs_to_init)
if (!T.init_air)
continue
// We pass the tick as the current step so if we sleep the step changes
// This way we can make setting up adjacent turfs O(n) rather then O(n^2)
T.Initalize_Atmos(time)
if(CHECK_TICK)
time--
if(active_turfs.len)
var/starting_ats = active_turfs.len
sleep(world.tick_lag)
var/timer = world.timeofday
log_mapping("There are [starting_ats] active turfs at roundstart caused by a difference of the air between the adjacent turfs. \
To locate these active turfs, go into the \"Debug\" tab of your stat-panel. Then hit the verb that says \"Mapping Verbs - Enable\". \
Now, you can see all of the associated coordinates using \"Mapping -> Show roundstart AT list\" verb.")
for(var/turf/T in active_turfs)
GLOB.active_turfs_startlist += T
//now lets clear out these active turfs
var/list/turfs_to_check = active_turfs.Copy()
do
var/list/new_turfs_to_check = list()
for(var/turf/open/T in turfs_to_check)
new_turfs_to_check += T.resolve_active_graph()
CHECK_TICK
active_turfs += new_turfs_to_check
turfs_to_check = new_turfs_to_check
while (turfs_to_check.len)
var/ending_ats = active_turfs.len
for(var/thing in excited_groups)
var/datum/excited_group/EG = thing
EG.self_breakdown(roundstart = TRUE)
EG.dismantle()
CHECK_TICK
var/msg = "HEY! LISTEN! [DisplayTimeText(world.timeofday - timer, 0.00001)] were wasted processing [starting_ats] turf(s) (connected to [ending_ats - starting_ats] other turfs) with atmos differences at round start."
to_chat(world, span_boldannounce("[msg]"))
warning(msg)
/turf/open/proc/resolve_active_graph()
. = list()
var/datum/excited_group/EG = excited_group
if (blocks_air || !air)
return
if (!EG)
EG = new
EG.add_turf(src)
for (var/turf/open/ET in atmos_adjacent_turfs)
if (ET.blocks_air || !ET.air)
continue
var/ET_EG = ET.excited_group
if (ET_EG)
if (ET_EG != EG)
EG.merge_groups(ET_EG)
EG = excited_group //merge_groups() may decide to replace our current EG
else
EG.add_turf(ET)
if (!ET.excited)
ET.excited = TRUE
. += ET
/turf/open/space/resolve_active_graph()
return list()
/datum/controller/subsystem/air/proc/setup_atmos_machinery()
for (var/obj/machinery/atmospherics/AM in atmos_machinery)
AM.atmos_init()
CHECK_TICK
//this can't be done with setup_atmos_machinery() because
// all atmos machinery has to initalize before the first
// pipenet can be built.
/datum/controller/subsystem/air/proc/setup_pipenets()
for (var/obj/machinery/atmospherics/AM in atmos_machinery)
var/list/targets = AM.get_rebuild_targets()
for(var/datum/pipeline/build_off as anything in targets)
build_off.build_pipeline_blocking(AM)
CHECK_TICK
GLOBAL_LIST_EMPTY(colored_turfs)
GLOBAL_LIST_EMPTY(colored_images)
/datum/controller/subsystem/air/proc/setup_turf_visuals()
for(var/sharp_color in GLOB.contrast_colors)
var/list/add_to = list()
GLOB.colored_turfs += list(add_to)
for(var/offset in 0 to SSmapping.max_plane_offset)
var/obj/effect/overlay/atmos_excited/suger_high = new()
SET_PLANE_W_SCALAR(suger_high, HIGH_GAME_PLANE, offset)
add_to += suger_high
var/image/shiny = new('icons/effects/effects.dmi', suger_high, "atmos_top")
SET_PLANE_W_SCALAR(shiny, HIGH_GAME_PLANE, offset)
shiny.color = sharp_color
GLOB.colored_images += shiny
/datum/controller/subsystem/air/proc/setup_template_machinery(list/atmos_machines)
var/obj/machinery/atmospherics/AM
for(var/A in 1 to atmos_machines.len)
AM = atmos_machines[A]
AM.atmos_init()
CHECK_TICK
for(var/A in 1 to atmos_machines.len)
AM = atmos_machines[A]
var/list/targets = AM.get_rebuild_targets()
for(var/datum/pipeline/build_off as anything in targets)
build_off.build_pipeline_blocking(AM)
CHECK_TICK
/datum/controller/subsystem/air/proc/get_init_dirs(type, dir, init_dir)
if(!pipe_init_dirs_cache[type])
pipe_init_dirs_cache[type] = list()
if(!pipe_init_dirs_cache[type]["[init_dir]"])
pipe_init_dirs_cache[type]["[init_dir]"] = list()
if(!pipe_init_dirs_cache[type]["[init_dir]"]["[dir]"])
var/obj/machinery/atmospherics/temp = new type(null, FALSE, dir, init_dir)
pipe_init_dirs_cache[type]["[init_dir]"]["[dir]"] = temp.get_init_directions()
qdel(temp)
return pipe_init_dirs_cache[type]["[init_dir]"]["[dir]"]
/datum/controller/subsystem/air/proc/generate_atmos()
atmos_gen = list()
for(var/T in subtypesof(/datum/atmosphere))
var/datum/atmosphere/atmostype = T
atmos_gen[initial(atmostype.id)] = new atmostype
/// Takes a gas string, returns the matching mutable gas_mixture
/datum/controller/subsystem/air/proc/parse_gas_string(gas_string, gastype = /datum/gas_mixture)
var/datum/gas_mixture/cached = strings_to_mix["[gas_string]-[gastype]"]
if(cached)
if(istype(cached, /datum/gas_mixture/immutable))
return cached
return cached.copy()
var/datum/gas_mixture/canonical_mix = new gastype()
// We set here so any future key changes don't fuck us
strings_to_mix["[gas_string]-[gastype]"] = canonical_mix
gas_string = preprocess_gas_string(gas_string)
var/list/gases = canonical_mix.gases
var/list/gas = params2list(gas_string)
if(gas["TEMP"])
canonical_mix.temperature = text2num(gas["TEMP"])
canonical_mix.temperature_archived = canonical_mix.temperature
gas -= "TEMP"
else // if we do not have a temp in the new gas mix lets assume room temp.
canonical_mix.temperature = T20C
for(var/id in gas)
var/path = id
if(!ispath(path))
path = gas_id2path(path) //a lot of these strings can't have embedded expressions (especially for mappers), so support for IDs needs to stick around
ADD_GAS(path, gases)
gases[path][MOLES] = text2num(gas[id])
if(istype(canonical_mix, /datum/gas_mixture/immutable))
return canonical_mix
return canonical_mix.copy()
/datum/controller/subsystem/air/proc/preprocess_gas_string(gas_string)
if(!atmos_gen)
generate_atmos()
if(!atmos_gen[gas_string])
return gas_string
var/datum/atmosphere/mix = atmos_gen[gas_string]
return mix.gas_string
/**
* Adds a given machine to the processing system for SSAIR_ATMOSMACHINERY processing.
*
* Arguments:
* * machine - The machine to start processing. Can be any /obj/machinery.
*/
/datum/controller/subsystem/air/proc/start_processing_machine(obj/machinery/machine)
if(machine.atmos_processing)
return
if(QDELETED(machine))
stack_trace("We tried to add a garbage collecting machine to SSair. Don't")
return
machine.atmos_processing = TRUE
atmos_machinery += machine
/**
* Removes a given machine to the processing system for SSAIR_ATMOSMACHINERY processing.
*
* Arguments:
* * machine - The machine to stop processing.
*/
/datum/controller/subsystem/air/proc/stop_processing_machine(obj/machinery/machine)
if(!machine.atmos_processing)
return
machine.atmos_processing = FALSE
atmos_machinery -= machine
// If we're currently processing atmos machines, there's a chance this machine is in
// the currentrun list, which is a cache of atmos_machinery. Remove it from that list
// as well to prevent processing qdeleted objects in the cache.
if(currentpart == SSAIR_ATMOSMACHINERY)
currentrun -= machine
/datum/controller/subsystem/air/ui_state(mob/user)
return GLOB.debug_state
/datum/controller/subsystem/air/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "AtmosControlPanel")
ui.set_autoupdate(FALSE)
ui.open()
/datum/controller/subsystem/air/ui_data(mob/user)
var/list/data = list()
data["excited_groups"] = list()
for(var/datum/excited_group/group in excited_groups)
var/turf/T = group.turf_list[1]
var/area/target = get_area(T)
var/max = 0
#ifdef TRACK_MAX_SHARE
for(var/who in group.turf_list)
var/turf/open/lad = who
max = max(lad.max_share, max)
#endif
data["excited_groups"] += list(list(
"jump_to" = REF(T), //Just go to the first turf
"group" = REF(group),
"area" = target.name,
"breakdown" = group.breakdown_cooldown,
"dismantle" = group.dismantle_cooldown,
"size" = group.turf_list.len,
"should_show" = group.should_display,
"max_share" = max
))
data["active_size"] = active_turfs.len
data["hotspots_size"] = hotspots.len
data["excited_size"] = excited_groups.len
data["conducting_size"] = active_super_conductivity.len
data["frozen"] = can_fire
data["show_all"] = display_all_groups
data["fire_count"] = times_fired
#ifdef TRACK_MAX_SHARE
data["display_max"] = TRUE
#else
data["display_max"] = FALSE
#endif
data["showing_user"] = user.hud_used.atmos_debug_overlays
return data
/datum/controller/subsystem/air/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(. || !check_rights_for(usr.client, R_DEBUG))
return
switch(action)
if("move-to-target")
var/turf/target = locate(params["spot"])
if(!target)
return
usr.forceMove(target)
usr.update_parallax_contents()
if("toggle-freeze")
can_fire = !can_fire
return TRUE
if("toggle_show_group")
var/datum/excited_group/group = locate(params["group"])
if(!group)
return
group.should_display = !group.should_display
if(display_all_groups)
return TRUE
if(group.should_display)
group.display_turfs()
else
group.hide_turfs()
return TRUE
if("toggle_show_all")
display_all_groups = !display_all_groups
for(var/datum/excited_group/group in excited_groups)
if(display_all_groups)
group.display_turfs()
else if(!group.should_display) //Don't flicker yeah?
group.hide_turfs()
return TRUE
if("toggle_user_display")
var/mob/user = ui.user
user.hud_used.atmos_debug_overlays = !user.hud_used.atmos_debug_overlays
if(user.hud_used.atmos_debug_overlays)
user.client.images += GLOB.colored_images
else
user.client.images -= GLOB.colored_images
return TRUE