mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-29 11:32:13 +00:00
Optimization/Rewrite of Radiation Controller
* The performance of the radiation controller as-is was not fast enough for inclusion in production servers, but it has some nice featuers, so rewrote it to be more performant. * Instead of storing the radiation strength for every turf, we only store the sources of radiation, and calculate the strength only for mobs who might be in range. * Old method was ray-tracing to every turf in range whether anything was there to be irradiated or not. Could be hundreds of turfs. New method only lazily calcualtes strength at a turf if we actually need to know it. Often times this is zero turfs if nobody is standing in engineering. * Removed the automatic processing of objects with "rad_power" set. Objects are responsible for calling the repository to create/update their radiation sources. Saves some extra overhead that in practice was redundant with other process controllers. * Also tweaked to be more respectful of qdel'd objects and added some comments.
This commit is contained in:
@@ -7,37 +7,43 @@
|
||||
linked = radiation_repository
|
||||
|
||||
/datum/controller/process/radiation/doWork()
|
||||
// set background = 1
|
||||
for(var/turf/T in linked.irradiated_turfs)
|
||||
if(!T)
|
||||
linked.irradiated_turfs.Remove(T)
|
||||
// Step 1 - Sources Decay
|
||||
var/list/sources = linked.sources
|
||||
for(var/thing in sources)
|
||||
if(deleted(thing))
|
||||
sources.Remove(thing)
|
||||
continue
|
||||
linked.irradiated_turfs[T] -= config.radiation_decay_rate
|
||||
if(linked.irradiated_turfs[T] <= config.radiation_lower_limit)
|
||||
linked.irradiated_turfs.Remove(T)
|
||||
SCHECK
|
||||
for(var/mob/living/L in linked.irradiated_mobs)
|
||||
if(!L)
|
||||
linked.irradiated_mobs.Remove(L)
|
||||
continue
|
||||
if(get_turf(L) in linked.irradiated_turfs)
|
||||
L.rad_act(linked.irradiated_turfs[get_turf(L)])
|
||||
if(!L.radiation)
|
||||
linked.irradiated_mobs.Remove(L)
|
||||
SCHECK
|
||||
for(var/thing in linked.sources)
|
||||
if(!thing)
|
||||
linked.sources.Remove(thing)
|
||||
continue
|
||||
var/atom/emitter = thing
|
||||
linked.radiate(emitter, emitter.rad_power)
|
||||
to_process.Cut()
|
||||
SCHECK
|
||||
for(var/thing in linked.resistance_cache)
|
||||
if(!thing)
|
||||
linked.resistance_cache.Remove(thing)
|
||||
var/datum/radiation_source/S = thing
|
||||
if(S.decay)
|
||||
S.update_rad_power(S.rad_power - config.radiation_decay_rate)
|
||||
if(S.rad_power <= config.radiation_lower_limit)
|
||||
sources.Remove(S)
|
||||
SCHECK // This scheck probably just wastes resources, but better safe than sorry in this case.
|
||||
|
||||
// Step 2 - Cache Expires
|
||||
var/list/resistance_cache = linked.resistance_cache
|
||||
for(var/thing in resistance_cache)
|
||||
if(deleted(thing))
|
||||
resistance_cache.Remove(thing)
|
||||
continue
|
||||
var/turf/T = thing
|
||||
if((length(T.contents) + 1) != linked.resistance_cache[T])
|
||||
T.calc_rad_resistance()
|
||||
if((length(T.contents) + 1) != resistance_cache[T])
|
||||
resistance_cache.Remove(T) // If its stale REMOVE it! It will get added if its needed.
|
||||
SCHECK
|
||||
|
||||
// Step 3 - Registered irradiatable things are checked for radiation
|
||||
var/list/registered_listeners = living_mob_list // For now just use this. Nothing else is interested anyway.
|
||||
if(length(linked.sources) > 0)
|
||||
for(var/thing in registered_listeners)
|
||||
if(deleted(thing))
|
||||
continue
|
||||
var/atom/A = thing
|
||||
var/turf/T = get_turf(thing)
|
||||
var/rads = linked.get_rads_at_turf(T)
|
||||
if(rads)
|
||||
A.rad_act(rads)
|
||||
SCHECK
|
||||
|
||||
/datum/controller/process/radiation/statProcess()
|
||||
..()
|
||||
stat(null, "[linked.sources.len] sources, [linked.resistance_cache.len] cached turfs")
|
||||
|
||||
@@ -1,114 +1,132 @@
|
||||
//#define RADDBG
|
||||
|
||||
var/repository/radiation/radiation_repository = new()
|
||||
|
||||
var/list/to_process = list()
|
||||
var/global/repository/radiation/radiation_repository = new()
|
||||
|
||||
/repository/radiation
|
||||
var/list/sources = list() //All the radiation sources we know about
|
||||
var/list/irradiated_turfs = list()
|
||||
var/list/irradiated_mobs = list()
|
||||
var/list/resistance_cache = list()
|
||||
var/list/sources = list() // all radiation source datums
|
||||
var/list/sources_assoc = list() // Sources indexed by turf for de-duplication.
|
||||
var/list/resistance_cache = list() // Cache of turf's radiation resistance.
|
||||
|
||||
/repository/radiation/proc/report_rads(var/turf/T as turf)
|
||||
return irradiated_turfs[T]
|
||||
// Describes a point source of radiation. Created either in response to a pulse of radiation, or over an irradiated atom.
|
||||
// Sources will decay over time, unless something is renewing their power!
|
||||
/datum/radiation_source
|
||||
var/turf/source_turf // Location of the radiation source.
|
||||
var/rad_power // Strength of the radiation being emitted.
|
||||
var/decay = TRUE // True for automatic decay. False if owner promises to handle it (i.e. supermatter)
|
||||
var/respect_maint = FALSE // True for not affecting RAD_SHIELDED areas.
|
||||
var/flat = FALSE // True for power falloff with distance.
|
||||
var/range // Cached maximum range, used for quick checks against mobs.
|
||||
|
||||
/datum/radiation_source/Destroy()
|
||||
radiation_repository.sources -= src
|
||||
if(radiation_repository.sources_assoc[src.source_turf] == src)
|
||||
radiation_repository.sources -= src.source_turf
|
||||
. = ..()
|
||||
|
||||
/datum/radiation_source/proc/update_rad_power(var/new_power = null)
|
||||
if(new_power != null && new_power != rad_power)
|
||||
rad_power = new_power
|
||||
. = 1
|
||||
if(. && !flat)
|
||||
range = min(round(sqrt(rad_power / config.radiation_lower_limit)), 31)
|
||||
|
||||
// Ray trace from all active radiation sources to T and return the strongest effect.
|
||||
/repository/radiation/proc/get_rads_at_turf(var/turf/T)
|
||||
if(!istype(T)) return 0
|
||||
|
||||
. = 0
|
||||
for(var/value in sources)
|
||||
var/datum/radiation_source/source = value
|
||||
if(source.rad_power < .)
|
||||
continue // Already being affected by a stronger source
|
||||
var/dist = get_dist(source.source_turf, T)
|
||||
if(dist > source.range)
|
||||
continue // Too far to possibly affect
|
||||
if(source.respect_maint)
|
||||
var/atom/A = T.loc
|
||||
if(A.flags & RAD_SHIELDED)
|
||||
continue // In shielded area
|
||||
if(source.flat)
|
||||
. = max(., source.rad_power)
|
||||
continue // No need to ray trace for flat field
|
||||
|
||||
// Okay, now ray trace to find resistence!
|
||||
var/turf/origin = source.source_turf
|
||||
var/working = source.rad_power
|
||||
while(origin != T)
|
||||
origin = get_step_towards(origin, T) //Raytracing
|
||||
if(!(origin in resistance_cache)) //Only get the resistance if we don't already know it.
|
||||
origin.calc_rad_resistance()
|
||||
working = max((working - (origin.cached_rad_resistance * config.radiation_resistance_multiplier)), 0)
|
||||
if(working <= .)
|
||||
break // Already affected by a stronger source (or its zero...)
|
||||
. = max((working * (1 / (dist ** 2))), .) //Butchered version of the inverse square law. Works for this purpose
|
||||
|
||||
// Add a radiation source instance to the repository. It will override any existing source on the same turf.
|
||||
/repository/radiation/proc/add_source(var/datum/radiation_source/S)
|
||||
var/datum/radiation_source/existing = sources_assoc[S.source_turf]
|
||||
if(existing)
|
||||
qdel(existing)
|
||||
sources += S
|
||||
sources_assoc[S.source_turf] = S
|
||||
|
||||
// Creates a temporary radiation source that will decay
|
||||
/repository/radiation/proc/radiate(source, power) //Sends out a radiation pulse, taking walls into account
|
||||
if(!(source && power)) //Sanity checking
|
||||
return
|
||||
var/datum/radiation_source/S = new()
|
||||
S.source_turf = get_turf(source)
|
||||
S.update_rad_power(power)
|
||||
add_source(S)
|
||||
return S
|
||||
|
||||
var/range = min(round(sqrt(power / config.radiation_lower_limit)), 31)
|
||||
var/turf/epicentre = get_turf(source)
|
||||
to_process = list()
|
||||
|
||||
range = min(epicentre.x, world.maxx - epicentre.x, epicentre.y, world.maxy - epicentre.y, range)
|
||||
|
||||
to_process = trange(range, epicentre)
|
||||
to_process[epicentre] = power
|
||||
|
||||
for(var/turf/spot in to_process)
|
||||
var/turf/origin = get_turf(epicentre)
|
||||
var/working = power
|
||||
while(origin != spot)
|
||||
origin = get_step_towards(origin, spot) //Raytracing
|
||||
if(!(origin in resistance_cache)) //Only get the resistance if we don't already know it.
|
||||
origin.calc_rad_resistance()
|
||||
working = max((working - (origin.rad_resistance * config.radiation_resistance_multiplier)), 0)
|
||||
if(!working)
|
||||
break
|
||||
if(!to_process[origin])
|
||||
to_process[origin] = working
|
||||
|
||||
else
|
||||
to_process[origin] = max(to_process[origin], working)
|
||||
|
||||
for(var/turf/spot in to_process)
|
||||
irradiated_turfs[spot] = max(((to_process[spot]) * (1 / (get_dist(epicentre, spot) ** 2))), irradiated_turfs[spot]) //Butchered version of the inverse square law. Works for this purpose
|
||||
#ifdef RADDBG
|
||||
var/x = Clamp( irradiated_turfs[spot], 0, 255)
|
||||
spot.color = rgb(5,x,5)
|
||||
#endif
|
||||
|
||||
/repository/radiation/proc/flat_radiate(source, power, range, var/respect_maint=0) //Sets the radiation in a range to a constant value.
|
||||
// Sets the radiation in a range to a constant value.
|
||||
/repository/radiation/proc/flat_radiate(source, power, range, var/respect_maint = FALSE)
|
||||
if(!(source && power && range))
|
||||
return
|
||||
var/turf/epicentre = get_turf(source)
|
||||
range = min(epicentre.x, world.maxx - epicentre.x, epicentre.y, world.maxy - epicentre.y, range)
|
||||
if(!respect_maint)
|
||||
for(var/turf/T in trange(range, epicentre))
|
||||
irradiated_turfs[T] = max(power, irradiated_turfs[T])
|
||||
else
|
||||
for(var/turf/T in trange(range, epicentre))
|
||||
var/area/A = T.loc
|
||||
if(A.flags & RAD_SHIELDED)
|
||||
continue
|
||||
irradiated_turfs[T] = max(power, irradiated_turfs[T])
|
||||
var/datum/radiation_source/S = new()
|
||||
S.flat = TRUE
|
||||
S.range = range
|
||||
S.respect_maint = respect_maint
|
||||
S.source_turf = get_turf(source)
|
||||
S.update_rad_power(power)
|
||||
add_source(S)
|
||||
return S
|
||||
|
||||
/repository/radiation/proc/z_radiate(var/atom/source, power, var/respect_maint=0) //Irradiates a full Z-level. Hacky way of doing it, but not too expensive.
|
||||
// Irradiates a full Z-level. Hacky way of doing it, but not too expensive.
|
||||
/repository/radiation/proc/z_radiate(var/atom/source, power, var/respect_maint = FALSE)
|
||||
if(!(power && source))
|
||||
return
|
||||
var/turf/epicentre = locate(round(world.maxx / 2), round(world.maxy / 2), source.z)
|
||||
flat_radiate(epicentre, power, world.maxx, respect_maint)
|
||||
|
||||
/turf
|
||||
var/cached_rad_resistance = 0
|
||||
|
||||
/turf/proc/calc_rad_resistance()
|
||||
rad_resistance = 0
|
||||
cached_rad_resistance = 0
|
||||
for(var/obj/O in src.contents)
|
||||
if(O.rad_resistance) //Override
|
||||
rad_resistance += O.rad_resistance
|
||||
cached_rad_resistance += O.rad_resistance
|
||||
|
||||
else if(O.density) //So doors don't get counted
|
||||
else if(O.density) //So open doors don't get counted
|
||||
var/material/M = O.get_material()
|
||||
if(!M) continue
|
||||
rad_resistance += M.weight
|
||||
cached_rad_resistance += M.weight
|
||||
// Looks like storing the contents length is meant to be a basic check if the cache is stale due to items enter/exiting. Better than nothing so I'm leaving it as is. ~Leshana
|
||||
radiation_repository.resistance_cache[src] = (length(contents) + 1)
|
||||
|
||||
/turf/simulated/wall/calc_rad_resistance()
|
||||
radiation_repository.resistance_cache[src] = (length(contents) + 1)
|
||||
rad_resistance = (density ? material.weight : 0)
|
||||
cached_rad_resistance = (density ? material.weight : 0)
|
||||
|
||||
/atom
|
||||
var/rad_power = 0
|
||||
var/rad_resistance = 0
|
||||
/obj
|
||||
var/rad_resistance = 0 // Allow overriding rad resistance
|
||||
|
||||
/atom/Destroy()
|
||||
if(rad_power)
|
||||
radiation_repository.sources.Remove(src)
|
||||
. = ..()
|
||||
|
||||
/atom/proc/update_radiation() //For VV'ing something to make it radioactive at runtime
|
||||
if((rad_power) && (!(src in radiation_repository.sources)))
|
||||
radiation_repository.sources.Add(src)
|
||||
else if((!rad_power) && (src in radiation_repository.sources))
|
||||
radiation_repository.sources.Remove(src)
|
||||
|
||||
/atom/proc/rad_act(var/severity) //If people expand the system, this may be useful. Here as a placeholder until then
|
||||
// If people expand the system, this may be useful. Here as a placeholder until then
|
||||
/atom/proc/rad_act(var/severity)
|
||||
return 1
|
||||
|
||||
/mob/living/rad_act(var/severity)
|
||||
if(severity)
|
||||
src.apply_effect(severity, IRRADIATE, src.getarmor(null, "rad"))
|
||||
for(var/obj/I in src)
|
||||
for(var/atom/I in src)
|
||||
I.rad_act(severity)
|
||||
|
||||
|
||||
//#undef RADDBG
|
||||
@@ -53,9 +53,6 @@
|
||||
pulledby = null
|
||||
|
||||
/atom/movable/proc/initialize()
|
||||
if(rad_power)
|
||||
radiation_repository.sources.Add(src)
|
||||
|
||||
if(!isnull(gcDestroyed))
|
||||
crash_with("GC: -- [type] had initialize() called after qdel() --")
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@
|
||||
icon = 'icons/obj/doors/Dooruranium.dmi'
|
||||
mineral = "uranium"
|
||||
var/last_event = 0
|
||||
rad_power = 7.5
|
||||
var/rad_power = 7.5
|
||||
|
||||
/obj/machinery/door/airlock/process()
|
||||
// Deliberate no call to parent.
|
||||
@@ -278,6 +278,13 @@
|
||||
|
||||
..()
|
||||
|
||||
/obj/machinery/door/airlock/uranium/process()
|
||||
if(world.time > last_event+20)
|
||||
if(prob(50))
|
||||
radiation_repository.radiate(src, rad_power)
|
||||
last_event = world.time
|
||||
..()
|
||||
|
||||
/obj/machinery/door/airlock/phoron
|
||||
name = "Phoron Airlock"
|
||||
desc = "No way this can end badly."
|
||||
|
||||
@@ -53,8 +53,7 @@
|
||||
icon_state = icon_state_closed
|
||||
else
|
||||
icon_state = icon_state_open
|
||||
var/turf/T = get_turf(src)
|
||||
T.calc_rad_resistance()
|
||||
radiation_repository.resistance_cache.Remove(get_turf(src))
|
||||
return
|
||||
|
||||
// Proc: force_open()
|
||||
|
||||
@@ -362,8 +362,7 @@
|
||||
icon_state = "door1"
|
||||
else
|
||||
icon_state = "door0"
|
||||
var/turf/T = get_turf(src)
|
||||
T.calc_rad_resistance()
|
||||
radiation_repository.resistance_cache.Remove(get_turf(src))
|
||||
return
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
/obj/item/device/geiger/process()
|
||||
if(!scanning)
|
||||
return
|
||||
radiation_count = radiation_repository.report_rads(get_turf(src))
|
||||
radiation_count = radiation_repository.get_rads_at_turf(get_turf(src))
|
||||
update_icon()
|
||||
|
||||
/obj/item/device/geiger/examine(mob/user)
|
||||
|
||||
@@ -69,7 +69,7 @@ REAGENT SCANNER
|
||||
user.show_message("<span class='notice'>Analyzing Results for [M]:</span>")
|
||||
user.show_message("<span class='notice'>Overall Status: dead</span>")
|
||||
else
|
||||
user.show_message("<span class='notice'>Analyzing Results for [M]:\n\t Overall Status: [M.stat > 1 ? "dead" : "[round(M.health*100/M.getMaxHealth())]% healthy"]</span>")
|
||||
user.show_message("<span class='notice'>Analyzing Results for [M]:\n\t Overall Status: [M.stat > 1 ? "dead" : "[round((M.health/M.getMaxHealth())*100) ]% healthy"]</span>")
|
||||
user.show_message("<span class='notice'> Key: <font color='cyan'>Suffocation</font>/<font color='green'>Toxin</font>/<font color='#FFA500'>Burns</font>/<font color='red'>Brute</font></span>", 1)
|
||||
user.show_message("<span class='notice'> Damage Specifics: <font color='cyan'>[OX]</font> - <font color='green'>[TX]</font> - <font color='#FFA500'>[BU]</font> - <font color='red'>[BR]</font></span>")
|
||||
user.show_message("<span class='notice'>Body Temperature: [M.bodytemperature-T0C]°C ([M.bodytemperature*1.8-459.67]°F)</span>", 1)
|
||||
@@ -100,9 +100,7 @@ REAGENT SCANNER
|
||||
OX = fake_oxy > 50 ? "<span class='warning'>Severe oxygen deprivation detected</span>" : "Subject bloodstream oxygen level normal"
|
||||
user.show_message("[OX] | [TX] | [BU] | [BR]")
|
||||
if(M.radiation)
|
||||
user.show_message("<span class='notice'>Radiation Level: [M.radiation]</span>")
|
||||
else
|
||||
user.show_message("<span class='notice'>No radiation detected.</span>")
|
||||
user.show_message("<span class='warning'>Radiation detected.</span>")
|
||||
if(istype(M, /mob/living/carbon))
|
||||
var/mob/living/carbon/C = M
|
||||
if(C.reagents.total_volume)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
if(can_open == WALL_OPENING)
|
||||
return
|
||||
|
||||
calc_rad_resistance()
|
||||
radiation_repository.resistance_cache.Remove(src)
|
||||
|
||||
if(density)
|
||||
can_open = WALL_OPENING
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
else if(material.opacity < 0.5 && opacity)
|
||||
set_light(0)
|
||||
|
||||
calc_rad_resistance()
|
||||
radiation_repository.resistance_cache.Remove(src)
|
||||
update_connections(1)
|
||||
update_icon()
|
||||
|
||||
|
||||
@@ -140,12 +140,6 @@ var/const/enterloopsanity = 100
|
||||
|
||||
if(ismob(A))
|
||||
var/mob/M = A
|
||||
var/mob/living/L = A
|
||||
if(istype(L))
|
||||
if(!(L in radiation_repository.irradiated_mobs))
|
||||
if(src in radiation_repository.irradiated_turfs)
|
||||
radiation_repository.irradiated_mobs.Add(L)
|
||||
L.handle_footstep(src)
|
||||
if(!M.lastarea)
|
||||
M.lastarea = get_area(M.loc)
|
||||
if(M.lastarea.has_gravity == 0)
|
||||
@@ -153,6 +147,9 @@ var/const/enterloopsanity = 100
|
||||
else if(!is_space())
|
||||
M.inertia_dir = 0
|
||||
M.make_floating(0)
|
||||
if(isliving(M))
|
||||
var/mob/living/L = M
|
||||
L.handle_footstep(src)
|
||||
..()
|
||||
var/objects = 0
|
||||
if(A && (A.flags & PROXMOVE))
|
||||
|
||||
@@ -27,14 +27,17 @@
|
||||
radiate()
|
||||
|
||||
/datum/event/solar_storm/proc/radiate()
|
||||
var/radiation_level = rand(15, 30)
|
||||
for(var/area/A in all_areas)
|
||||
if(!(A.z in using_map.player_levels))
|
||||
// Note: Too complicated to be worth trying to use the radiation system for this. Its only in space anyway, so we make an exception in this case.
|
||||
for(var/mob/living/L in living_mob_list)
|
||||
var/turf/T = get_turf(L)
|
||||
if(!T)
|
||||
continue
|
||||
for(var/turf/T in A)
|
||||
if(!istype(T.loc,/area/space) && !istype(T,/turf/space))
|
||||
|
||||
if(!istype(T.loc,/area/space) && !istype(T,/turf/space)) //Make sure you're in a space area or on a space turf
|
||||
continue
|
||||
radiation_repository.irradiated_turfs[T] = radiation_level
|
||||
|
||||
//Todo: Apply some burn damage from the heat of the sun. Until then, enjoy some moderate radiation.
|
||||
L.rad_act(rand(15, 30))
|
||||
|
||||
/datum/event/solar_storm/end()
|
||||
command_announcement.Announce("The solar storm has passed the station. It is now safe to resume EVA activities. Please report to medbay if you experience any unusual symptoms. ", "Anomaly Alert")
|
||||
|
||||
@@ -31,10 +31,10 @@ var/global/list/rad_collectors = list()
|
||||
last_power_new = 0
|
||||
|
||||
|
||||
var/turf/T = get_turf(src)
|
||||
if(T in radiation_repository.irradiated_turfs)
|
||||
receive_pulse((radiation_repository.irradiated_turfs[T] * 5)) //Maths is hard
|
||||
|
||||
if(P && active)
|
||||
var/rads = radiation_repository.get_rads_at_turf(get_turf(src))
|
||||
if(rads)
|
||||
receive_pulse(rads * 5) //Maths is hard
|
||||
|
||||
if(P)
|
||||
if(P.air_contents.gas["phoron"] == 0)
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
anchored = 0
|
||||
light_range = 4
|
||||
|
||||
rad_power = 1 //So it gets added to the repository
|
||||
var/gasefficency = 0.25
|
||||
|
||||
var/base_icon_state = "darkmatter"
|
||||
@@ -112,7 +111,7 @@
|
||||
var/mob/living/carbon/human/H = mob
|
||||
H.hallucination += max(50, min(300, DETONATION_HALLUCINATION * sqrt(1 / (get_dist(mob, src) + 1)) ) )
|
||||
spawn(pull_time)
|
||||
explosion(TS, explosion_power, explosion_power * 2, explosion_power * 3, explosion_power * 4, 1)
|
||||
explosion(get_turf(src), explosion_power, explosion_power * 2, explosion_power * 3, explosion_power * 4, 1)
|
||||
qdel(src)
|
||||
return
|
||||
|
||||
@@ -273,7 +272,7 @@
|
||||
var/rads = (power / 10) * ( 1 / (radius**2) )
|
||||
l.apply_effect(rads, IRRADIATE)
|
||||
*/
|
||||
rad_power = power * 1.5//Better close those shutters!
|
||||
radiation_repository.radiate(src, power * 1.5) //Better close those shutters!
|
||||
|
||||
power -= (power/DECAY_FACTOR)**3 //energy losses due to radiation
|
||||
|
||||
@@ -403,7 +402,6 @@
|
||||
return
|
||||
|
||||
|
||||
|
||||
/obj/machinery/power/supermatter/GotoAirflowDest(n) //Supermatter not pushed around by airflow
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user