Files
vgstation13/code/controllers/subsystem/air.dm
2021-10-10 13:07:13 +02:00

430 lines
13 KiB
Plaintext

#define SSAIR_TILES 1
#define SSAIR_DEFERRED 2
#define SSAIR_EDGES 3
//#define SSAIR_FIRE_ZONE 4 //This involves behavior to be added in a future PR.
#define SSAIR_HOTSPOT 4 //These two should each be increased by one once that one is uncommented.
#define SSAIR_ZONE 5 //These two should each be increased by one once that one is uncommented.
#define SSAIR_FIRST_PART SSAIR_TILES //The first part to be processed.
#define SSAIR_LAST_PART SSAIR_ZONE //The last part to be processed.
#define SSAIR_PROCESS_KEEP SSAIR_EDGES, SSAIR_HOTSPOT //The lists for these parts are kept between ticks. Any parts not included here are wiped each tick.
var/datum/subsystem/air/SSair
var/tick_multiplier = 2
/*
Overview:
The air controller does everything. There are tons of procs in here.
Class Vars:
zones - All zones currently holding one or more turfs.
edges - All processing edges.
processing_parts[SSAIR_TILES] - Tiles scheduled to update next tick.
processing_parts[SSAIR_ZONE] - Zones which have had their air changed and need air archival.
processing_parts[SSAIR_HOTSPOT] - All processing fire objects.
active_zones - The number of zones which were archived last tick. Used in debug verbs.
next_id - The next UID to be applied to a zone. Mostly useful for debugging purposes as zones do not need UIDs to function.
Class Procs:
mark_for_update(turf/T)
Adds the turf to the update list. When updated, update_air_properties() will be called.
When stuff changes that might affect airflow, call this. It's basically the only thing you need.
add_zone(zone/Z) and remove_zone(zone/Z)
Adds zones to the zones list. Does not mark them for update.
air_blocked(turf/A, turf/B)
Returns a bitflag consisting of:
AIR_BLOCKED - The connection between turfs is physically blocked. No air can pass.
ZONE_BLOCKED - There is a door between the turfs, so zones cannot cross. Air may or may not be permeable.
has_valid_zone(turf/T)
Checks the presence and validity of T's zone.
May be called on unsimulated turfs, returning 0.
merge(zone/A, zone/B)
Called when zones have a direct connection and equivalent pressure and temperature.
Merges the zones to create a single zone.
connect(turf/simulated/A, turf/B)
Called by turf/update_air_properties(). The first argument must be simulated.
Creates a connection between A and B.
mark_zone_update(zone/Z)
Adds zone to the update list. Unlike mark_for_update(), this one is called automatically whenever
air is returned from a simulated turf.
equivalent_pressure(zone/A, zone/B)
Currently identical to A.air.compare(B.air). Returns 1 when directly connected zones are ready to be merged.
get_edge(zone/A, zone/B)
get_edge(zone/A, turf/B)
Gets a valid connection_edge between A and B, creating a new one if necessary.
has_same_air(turf/A, turf/B)
Used to determine if an unsimulated edge represents a specific turf.
Simulated edges use connection_edge/contains_zone() for the same purpose.
Returns 1 if A has identical gases and temperature to B.
remove_edge(connection_edge/edge)
Called when an edge is erased. Removes it from processing.
*/
/datum/subsystem/air
name = "Air"
init_order = SS_INIT_AIR
priority = SS_PRIORITY_AIR
wait = 2 SECONDS
display_order = SS_DISPLAY_AIR
var/list/currentrun
var/currentpart = SSAIR_TILES
var/list/cost_parts = list(SSAIR_TILES = 0,\
SSAIR_DEFERRED = 0,\
SSAIR_EDGES = 0,\
/* SSAIR_FIRE_ZONE = 0,*/\
SSAIR_HOTSPOT = 0,\
SSAIR_ZONE = 0)
var/list/zones = list()
var/list/edges = list()
//Geometry process lists
var/list/processing_parts = list(SSAIR_TILES = list(),\
SSAIR_DEFERRED = list(),\
SSAIR_EDGES = list(),\
/* SSAIR_FIRE_ZONE = list(),*/\
SSAIR_HOTSPOT = list(),\
SSAIR_ZONE = list())
var/active_zones = 0
var/current_cycle = 0
var/update_delay = 5 //How long between check should it try to process atmos again.
var/failed_ticks = 0 //How many ticks have runtimed?
var/next_id = 1 //Used to keep track of zone UIDs.
/datum/subsystem/air/New()
NEW_SS_GLOBAL(SSair)
/datum/subsystem/air/stat_entry(msg)
var/list/p_tiles = processing_parts[SSAIR_TILES]
var/list/p_zone = processing_parts[SSAIR_ZONE]
// var/list/p_fire_zone = processing_parts[SSAIR_FIRE_ZONE]
var/list/p_hotspot = processing_parts[SSAIR_HOTSPOT]
var/list/p_edges = processing_parts[SSAIR_EDGES]
msg += "C:{\
T:[round(cost_parts[SSAIR_TILES], 1)]|\
D:[round(cost_parts[SSAIR_DEFERRED], 1)]|\
E:[round(cost_parts[SSAIR_EDGES], 1)]|\
"/* F:[round(cost_parts[SSAIR_FIRE_ZONE], 1)]|*/+"\
H:[round(cost_parts[SSAIR_HOTSPOT], 1)]|\
Z:[round(cost_parts[SSAIR_ZONE], 1)]|\
} T:{\
Z:[zones.len]|\
E:[edges.len]\
} \
T:[p_tiles.len]|\
Z:[p_zone.len]|\
"/* F:[p_fire_zone.len]|*/+"\
H:[p_hotspot.len]|\
E:[p_edges.len]|\
A:[active_zones]"
..(msg) //Note to self: Don't fuck that up when uncommenting after adding fire zones
/datum/subsystem/air/Initialize(timeofday)
#ifndef ZASDBG
set background = 1 //The for loop later is sufficiently long to trip BYOND's infinite loop detection.
#endif
to_chat(world, "<span class='danger'>Processing Geometry...</span>")
sleep(-1)
var/simulated_turf_count = 0
for(var/turf/simulated/S in world)
simulated_turf_count++
S.update_air_properties()
to_chat(world, {"<span class='info'>Total Simulated Turfs: [simulated_turf_count]
Total Zones: [zones.len]
Total Edges: [edges.len]
Total Active Edges: [length(processing_parts[SSAIR_EDGES]) ? "<span class='danger'>[length(processing_parts[SSAIR_EDGES])]</span>" : "None"]
Total Unsimulated Turfs: [world.maxx*world.maxy*world.maxz - simulated_turf_count]</span>"})
..()
/datum/subsystem/air/fire(resumed = FALSE)
if (!resumed)
current_cycle++
currentpart = SSAIR_FIRST_PART
var/timer
while(currentpart <= SSAIR_LAST_PART)
timer = world.tick_usage
if(!resumed)
currentrun = processing_parts[currentpart]
switch(currentpart)
if(SSAIR_PROCESS_KEEP)
currentrun = currentrun.Copy()
process_part(currentpart)
cost_parts[currentpart] = MC_AVERAGE(cost_parts[currentpart], TICK_DELTA_TO_MS(world.tick_usage - timer))
if(state != SS_RUNNING)
return
resumed = FALSE
currentpart++
/datum/subsystem/air/proc/process_part(part = currentpart) //This whole proc is pretty disgusting, but I don't want to fuck EVERYTHING up at the same time. Rewrite later, maybe.
var/list/currentrun = src.currentrun //Accessing a proc var is faster than acccessing an object var. In the unlikely event Lummox ever fixes this, delete this line.
#define LOOP_DECLARATION(iter_type, iterator) var/iter_type/iterator; while(currentrun.len && !(MC_TICK_CHECK) && (iterator = currentrun[currentrun.len]) && currentrun.len--)
//The loop declaration is a macro so it can be duplicated without just copying+pasting. This removes the need for the following switch() to be evaluated every iteration.
//It's a huge mess in order to have all the checks and list manipulation built in. It's a bit slower than it would be if it were just copypasted due to the extra &&s, but only a bit.
switch(part)
if(SSAIR_TILES)
LOOP_DECLARATION(turf, T)
if(T.c_airblock(T) == ZONE_BLOCKED) //== instead of & because if it's also AIR_BLOCKED, it doesn't need to be deferred since it's just going to rebuild the zone.
processing_parts[SSAIR_DEFERRED] += T
continue
T.update_air_properties()
T.post_update_air_properties()
T.needs_air_update = 0
#ifdef ZASDBG
T.overlays -= mark
updated++
#endif
//sleep(1)
if(SSAIR_DEFERRED)
LOOP_DECLARATION(turf, T)
T.update_air_properties()
T.post_update_air_properties()
T.needs_air_update = 0
#ifdef ZASDBG
T.overlays -= mark
updated++
#endif
if(SSAIR_EDGES)
LOOP_DECLARATION(connection_edge, edge)
edge.tick()
// if(SSAIR_FIRE_ZONE)
// LOOP_DECLARATION(zone, Z)
// Z.process_fire()
if(SSAIR_HOTSPOT)
LOOP_DECLARATION(obj/effect/fire, fire)
fire.process()
if(SSAIR_ZONE)
LOOP_DECLARATION(zone, zone)
zone.tick()
zone.needs_update = 0
#undef LOOP_DECLARATION //Let's pretend that never existed now
/datum/subsystem/air/proc/add_zone(zone/z)
zones.Add(z)
z.name = "Zone [next_id++]"
mark_zone_update(z)
/datum/subsystem/air/proc/remove_zone(zone/z)
zones.Remove(z)
processing_parts[SSAIR_ZONE] -= z
/datum/subsystem/air/proc/air_blocked(turf/A, turf/B)
#ifdef ZASDBG
ASSERT(isturf(A))
ASSERT(isturf(B))
#endif
var/ablock = A.c_airblock(B)
if(ablock == BLOCKED)
return BLOCKED
return ablock | B.c_airblock(A)
/datum/subsystem/air/proc/has_valid_zone(turf/simulated/T)
#ifdef ZASDBG
ASSERT(istype(T))
#endif
return istype(T) && T.zone && !T.zone.invalid
/datum/subsystem/air/proc/merge(zone/A, zone/B)
#ifdef ZASDBG
ASSERT(istype(A))
ASSERT(istype(B))
ASSERT(!A.invalid)
ASSERT(!B.invalid)
ASSERT(A != B)
#endif
if(A.contents.len < B.contents.len)
A.c_merge(B)
mark_zone_update(B)
else
B.c_merge(A)
mark_zone_update(A)
/datum/subsystem/air/proc/connect(turf/simulated/A, turf/simulated/B)
#ifdef ZASDBG
ASSERT(istype(A))
ASSERT(isturf(B))
ASSERT(A.zone)
ASSERT(!A.zone.invalid)
//ASSERT(B.zone)
ASSERT(A != B)
#endif
var/block = SSair.air_blocked(A,B)
if(block & AIR_BLOCKED)
return
var/direct = !(block & ZONE_BLOCKED)
var/space = !istype(B)
if(!space)
// if(min(A.zone.contents.len, B.zone.contents.len) < ZONE_MIN_SIZE || (direct && (equivalent_pressure(A.zone,B.zone) || current_cycle == 0))) //This is the new behavior, but I don't want to include a balance change with a system change where avoidable. To be uncommented later.
if(direct && (equivalent_pressure(A.zone,B.zone) || current_cycle == 0)) //This is the old behavior, albeit with one check moved down an if().
merge(A.zone,B.zone)
return
if(!A.connections)
A.connections = list()
if(!B.connections)
B.connections = list()
if(B in A.connections)
return
if(A in B.connections)
return
if(!space)
if(A.zone == B.zone)
return
var/connection/c = new /connection(A, B)
A.connections[B] = c
B.connections[A] = c
if(direct)
c.mark_direct()
/datum/subsystem/air/proc/mark_for_update(turf/T)
#ifdef ZASDBG
ASSERT(isturf(T))
#endif
if(T.needs_air_update)
return
processing_parts[SSAIR_TILES] += T
#ifdef ZASDBG
T.overlays += mark
#endif
T.needs_air_update = 1
/datum/subsystem/air/proc/mark_zone_update(zone/Z)
#ifdef ZASDBG
ASSERT(istype(Z))
#endif
if(Z.needs_update)
return
processing_parts[SSAIR_ZONE] |= Z
Z.needs_update = 1
/datum/subsystem/air/proc/mark_edge_sleeping(connection_edge/E)
#ifdef ZASDBG
ASSERT(istype(E))
#endif
if(E.sleeping)
return
processing_parts[SSAIR_EDGES] -= E
E.sleeping = 1
/datum/subsystem/air/proc/mark_edge_active(connection_edge/E)
#ifdef ZASDBG
ASSERT(istype(E))
#endif
if(!E.sleeping)
return
processing_parts[SSAIR_EDGES] |= E
E.sleeping = 0
#ifdef ZASDBG
if(istype(E, /connection_edge/zone/))
var/connection_edge/zone/ZE = E
world << "ZASDBG: Active edge! Areas: [get_area(pick(ZE.A.contents))] / [get_area(pick(ZE.B.contents))]"
else
world << "ZASDBG: Active edge! Area: [get_area(pick(E.A.contents))]"
#endif
/datum/subsystem/air/proc/equivalent_pressure(zone/A, zone/B)
return A.air.compare(B.air)
/datum/subsystem/air/proc/get_edge(zone/A, zone/B)
if(istype(B))
for(var/connection_edge/zone/edge in A.edges)
if(edge.contains_zone(B))
return edge
var/connection_edge/edge = new/connection_edge/zone(A,B)
edges.Add(edge)
edge.recheck()
return edge
else
for(var/connection_edge/unsimulated/edge in A.edges)
if(has_same_air(edge.B,B))
return edge
var/connection_edge/edge = new/connection_edge/unsimulated(A,B)
edges.Add(edge)
edge.recheck()
return edge
/datum/subsystem/air/proc/has_same_air(turf/A, turf/B)
if(A.oxygen != B.oxygen)
return 0
if(A.nitrogen != B.nitrogen)
return 0
if(A.toxins != B.toxins)
return 0
if(A.carbon_dioxide != B.carbon_dioxide)
return 0
if(A.temperature != B.temperature)
return 0
return 1
/datum/subsystem/air/proc/remove_edge(connection_edge/E)
edges.Remove(E)
if(!E.sleeping)
processing_parts[SSAIR_EDGES] -= E
/datum/subsystem/air/proc/add_hotspot(var/obj/effect/fire/H)
#ifdef ZASDBG
ASSERT(istype(H))
#endif
processing_parts[SSAIR_HOTSPOT] |= H
/datum/subsystem/air/proc/remove_hotspot(var/obj/effect/fire/H)
#ifdef ZASDBG
ASSERT(istype(H))
#endif
processing_parts[SSAIR_HOTSPOT] -= H