Various Delta Time Fixes (#21871)

Organs were being given ~3 seconds of DT per ~2 seconds of real time,
because they were being hit by Process() TWICE, once during processing
for the "Set of all objects", and again during processing for the "Set
of all mobs". No species was hit harder than this but IPCs, who suffered
from having wildly inconsistent temperature mechanics that seemingly
never matched the expected numbers.

I have actually tested this PR to verify that it works. I even went the
extra mile to more extensively test IPC temperature mechanics to find a
separate bug unrelated to DTs that was causing suit coolers to just
straight up not work at all (space suit or otherwise) on a majority of
all IPC subspecies. I then tested the numbers in the PR for IPC cooling
to verify that things work as intended.

<img width="969" height="623" alt="image"
src="https://github.com/user-attachments/assets/01c3aca7-b436-4e52-86f7-aed847dcba5e"
/>

I also tweaked all the subsystems to have a small performance
improvement.

---------

Signed-off-by: VMSolidus <evilexecutive@gmail.com>
This commit is contained in:
VMSolidus
2026-02-27 01:28:20 +00:00
committed by GitHub
parent 8e602afb32
commit 1423fcd04e
19 changed files with 75 additions and 74 deletions
+2 -2
View File
@@ -162,8 +162,8 @@
#define FIRESUIT_MIN_PRESSURE 0.5 * ONE_ATMOSPHERE
#define TEMPERATURE_DAMAGE_COEFFICIENT 1.5 // This is used in handle_temperature_damage() for humans, and in reagents that affect body temperature. Temperature damage is multiplied by this amount.
#define BODYTEMP_AUTORECOVERY_DIVISOR 12 // This is the divisor which handles how much of the temperature difference between the current body temperature and 310.15K (optimal temperature) humans auto-regenerate each tick. The higher the number, the slower the recovery. This is applied each tick, so long as the mob is alive.
#define BODYTEMP_AUTORECOVERY_MINIMUM 1 // Minimum amount of kelvin moved toward 310.15K per tick. So long as abs(310.15 - bodytemp) is more than 50.
#define BODYTEMP_AUTORECOVERY_DIVISOR 24 // This is the divisor which handles how much of the temperature difference between the current body temperature and 310.15K (optimal temperature) humans auto-regenerate each second. The higher the number, the slower the recovery. This is applied each tick, so long as the mob is alive.
#define BODYTEMP_AUTORECOVERY_MINIMUM 0.5 // Minimum amount of kelvin moved toward 310.15K per second. So long as abs(310.15 - bodytemp) is more than 50.
#define BODYTEMP_COLD_DIVISOR 6 // Similar to the BODYTEMP_AUTORECOVERY_DIVISOR, but this is the divisor which is applied at the stage that follows autorecovery. This is the divisor which comes into play when the human's loc temperature is lower than their body temperature. Make it lower to lose bodytemp faster.
#define BODYTEMP_HEAT_DIVISOR 6 // Similar to the BODYTEMP_AUTORECOVERY_DIVISOR, but this is the divisor which is applied at the stage that follows autorecovery. This is the divisor which comes into play when the human's loc temperature is higher than their body temperature. Make it lower to gain bodytemp faster.
#define BODYTEMP_COOLING_MAX -30 // The maximum number of degrees that your body can cool down in 1 tick, when in a cold area.
+4 -2
View File
@@ -151,6 +151,7 @@ SUBSYSTEM_DEF(machinery)
if (!resumed)
queue = pipenets.Copy()
var/datum/pipe_network/network
var/seconds_per_tick = wait * 0.1
for (var/i = queue.len to 1 step -1)
network = queue[i]
if (QDELETED(network))
@@ -158,7 +159,7 @@ SUBSYSTEM_DEF(machinery)
network.datum_flags &= ~DF_ISPROCESSING
pipenets -= network
continue
network.process(wait * 0.1)
network.process(seconds_per_tick)
if (no_mc_tick)
CHECK_TICK
else if (MC_TICK_CHECK)
@@ -169,6 +170,7 @@ SUBSYSTEM_DEF(machinery)
if (!resumed)
queue = processing.Copy()
var/obj/machinery/machine
var/seconds_per_tick = wait * 0.1
for (var/i = queue.len to 1 step -1)
machine = queue[i]
@@ -196,7 +198,7 @@ SUBSYSTEM_DEF(machinery)
continue
//process_all was moved here because of calls overhead for no benefits
if((machine.processing_flags & MACHINERY_PROCESS_SELF))
if(machine.process(wait * 0.1) == PROCESS_KILL)
if(machine.process(seconds_per_tick) == PROCESS_KILL)
STOP_PROCESSING_MACHINE(machine, MACHINERY_PROCESS_SELF)
processing -= machine
if (no_mc_tick)
+2 -2
View File
@@ -80,14 +80,14 @@ SUBSYSTEM_DEF(mobs)
//of course, if we haven't resumed, this comparison would be useless, hence we skip it
var/list/currentrun = resumed ? (src.currentrun &= (GLOB.mob_list + processing)) : src.currentrun
var/seconds_per_tick = (1 SECONDS) / wait
var/seconds_per_tick = wait * 0.1
while(length(currentrun))
var/datum/thing = currentrun[length(currentrun)]
currentrun.len--
if(!ismob(thing))
if(!QDELETED(thing))
if(thing.process(wait, times_fired) == PROCESS_KILL)
if(thing.process(seconds_per_tick, times_fired) == PROCESS_KILL)
stop_processing(thing)
else
processing -= thing
@@ -39,22 +39,22 @@ SUBSYSTEM_DEF(movement)
/datum/controller/subsystem/movement/fire(resumed)
if(!resumed)
canonical_time = world.time
var/seconds_per_tick = wait * 0.1
for(var/list/bucket_info as anything in sorted_buckets)
var/time = bucket_info[MOVEMENT_BUCKET_TIME]
if(time > canonical_time || MC_TICK_CHECK)
return
pour_bucket(bucket_info)
pour_bucket(bucket_info, seconds_per_tick)
/// Processes a bucket of movement loops (This should only ever be called by fire(), it exists to prevent runtime fuckery)
/datum/controller/subsystem/movement/proc/pour_bucket(list/bucket_info)
/datum/controller/subsystem/movement/proc/pour_bucket(list/bucket_info, seconds_per_tick)
var/list/processing = bucket_info[MOVEMENT_BUCKET_LIST] // Cache for lookup speed
while(processing.len)
var/datum/move_loop/loop = processing[processing.len]
processing.len--
// No longer queued since we just got removed from the loop
loop.queued_time = null
loop.process() //This shouldn't get nulls, if it does, runtime
loop.process(seconds_per_tick) //This shouldn't get nulls, if it does, runtime
if(!QDELETED(loop) && loop.status & MOVELOOP_STATUS_QUEUED) //Re-Insert the loop
loop.status &= ~MOVELOOP_STATUS_QUEUED
loop.timer = world.time + loop.delay
+2 -1
View File
@@ -106,12 +106,13 @@ SUBSYSTEM_DEF(plants)
processing = old
var/list/queue = current
var/seconds_per_tick = wait * 0.1
while (queue.len)
var/obj/effect/plant/P = queue[queue.len]
queue.len--
if (!QDELETED(P))
P.process()
P.process(seconds_per_tick)
if (MC_TICK_CHECK)
return
@@ -12,12 +12,13 @@ PROCESSING_SUBSYSTEM_DEF(obj_tab_items)
currentrun = processing.Copy()
//cache for sanic speed (lists are references anyways)
var/list/current_run = currentrun
var/seconds_per_tick = wait * 0.1
while(current_run.len)
var/datum/thing = current_run[current_run.len]
if(QDELETED(thing))
processing -= thing
else if(thing.process(wait * 0.1) == PROCESS_KILL)
else if(thing.process(seconds_per_tick) == PROCESS_KILL)
// fully stop so that a future START_PROCESSING will work
STOP_PROCESSING(src, thing)
if (MC_TICK_CHECK)
@@ -19,13 +19,13 @@ SUBSYSTEM_DEF(processing)
currentrun = processing.Copy()
//cache for sanic speed (lists are references anyways)
var/list/current_run = currentrun
var/seconds_per_tick = wait * 0.1
while(current_run.len)
var/datum/thing = current_run[current_run.len]
current_run.len--
if(QDELETED(thing))
processing -= thing
else if(thing.process(wait * 0.1) == PROCESS_KILL)
else if(thing.process(seconds_per_tick) == PROCESS_KILL)
// fully stop so that a future START_PROCESSING will work
STOP_PROCESSING(src, thing)
if (MC_TICK_CHECK)
@@ -65,12 +65,13 @@ PROCESSING_SUBSYSTEM_DEF(tgui)
src.current_run = all_uis.Copy()
// Cache for sanic speed (lists are references anyways)
var/list/current_run = src.current_run
var/seconds_per_tick = wait * 0.1
while(current_run.len)
var/datum/tgui/ui = current_run[current_run.len]
current_run.len--
// TODO: Move user/src_object check to process()
if(ui?.user && ui.src_object)
ui.process(wait * 0.1)
ui.process(seconds_per_tick)
else
ui.close(0)
if(MC_TICK_CHECK)
@@ -212,10 +213,11 @@ PROCESSING_SUBSYSTEM_DEF(tgui)
if(!LAZYLEN(src_object?.open_uis))
return 0
var/count = 0
var/seconds_per_tick = wait * 0.1
for(var/datum/tgui/ui in src_object.open_uis)
// Check if UI is valid.
if(ui?.src_object && ui.user && ui.src_object.ui_host(ui.user))
INVOKE_ASYNC(ui, TYPE_PROC_REF(/datum/tgui, process), wait * 0.1, TRUE)
INVOKE_ASYNC(ui, TYPE_PROC_REF(/datum/tgui, process), seconds_per_tick, TRUE)
count++
return count
@@ -270,9 +272,10 @@ PROCESSING_SUBSYSTEM_DEF(tgui)
var/count = 0
if(length(user?.tgui_open_uis) == 0)
return count
var/seconds_per_tick = wait * 0.1
for(var/datum/tgui/ui in user.tgui_open_uis)
if(isnull(src_object) || ui.src_object == src_object)
ui.process(wait * 0.1, force = 1)
ui.process(seconds_per_tick, force = 1)
count++
return count
+2 -2
View File
@@ -5,8 +5,8 @@ SUBSYSTEM_DEF(turf_fire)
flags = SS_NO_INIT
var/list/fires = list()
/datum/controller/subsystem/turf_fire/fire()
var/seconds_per_tick = (wait * 0.1) // Equivalent to wait / (1 SECOND) but micro-optimized
/datum/controller/subsystem/turf_fire/fire(resumed)
var/seconds_per_tick = wait * 0.1
for(var/obj/turf_fire/fire as anything in fires)
fire.process(seconds_per_tick)
if(MC_TICK_CHECK)
+2 -2
View File
@@ -52,12 +52,12 @@
/obj/machinery/computer/slot_machine/Destroy()
return ..()
/obj/machinery/computer/slot_machine/process(delta_time)
/obj/machinery/computer/slot_machine/process(seconds_per_tick)
. = ..() //Sanity checks.
if(!.)
return .
money += round(delta_time / 2) //SPESSH MAJICKS
money += round(seconds_per_tick / 2) //SPESSH MAJICKS
/obj/machinery/computer/slot_machine/update_icon()
if(working)
@@ -23,7 +23,7 @@
var/on = 0 //is it turned on?
var/cover_open = 0 //is the cover open?
var/obj/item/cell/cell
var/max_cooling = 24 //in degrees per second - probably don't need to mess with heat capacity here
var/max_cooling = 24 //in degrees kelvin per second - probably don't need to mess with heat capacity here
var/charge_consumption = 8.3 //charge per second at max_cooling
var/thermostat = T20C
@@ -77,7 +77,7 @@
return (H.back == src) || (H.s_store == src)
/obj/item/suit_cooling_unit/process()
/obj/item/suit_cooling_unit/process(seconds_per_tick)
if(!on || !cell)
return
@@ -86,18 +86,16 @@
var/mob/living/carbon/human/H = loc
var/efficiency = !!(H.species.flags & ACCEPTS_COOLER) || 1 - H.get_pressure_weakness() //you need to have a good seal for effective cooling; some species can directly connect to the cooler, so get a free 100% efficiency here
var/efficiency = isipc(H) || 1 - H.get_pressure_weakness() //you need to have a good seal for effective cooling; some species can directly connect to the cooler, so get a free 100% efficiency here
var/env_temp = get_environment_temperature() //wont save you from a fire
var/temp_adj = min(H.bodytemperature - max(thermostat, env_temp), max_cooling)
if(temp_adj < 0.5) //only cools, doesn't heat, also we don't need extreme precision
return
var/charge_usage = (temp_adj/max_cooling)*charge_consumption
H.bodytemperature = max(T0C, H.bodytemperature - temp_adj * efficiency * seconds_per_tick)
H.bodytemperature = max(T0C, H.bodytemperature - temp_adj*efficiency)
cell.use(charge_usage)
cell.use((temp_adj/max_cooling) * charge_consumption * seconds_per_tick)
if(cell.charge <= 0)
turn_off()
@@ -30,18 +30,6 @@
for(var/obj/item/organ/external/Ex in organs)
bad_external_organs |= Ex
//processing internal organs is pretty cheap, do that first.
for(var/obj/item/organ/I in internal_organs)
if (QDELETED(I))
LOG_DEBUG("Organ [DEBUG_REF(src)] was not properly removed from its parent!")
internal_organs -= I
continue
if(I.status & ORGAN_DEAD)
continue
I.process(seconds_per_tick)
handle_stance()
handle_grasp()
@@ -55,7 +43,6 @@
bad_external_organs -= E
continue
else
E.process()
number_wounds += E.number_wounds
if (!lying && !buckled_to && world.time - l_move_time < 15)
+12 -13
View File
@@ -59,7 +59,7 @@
//Organs
handle_organs(seconds_per_tick)
stabilize_body_temperature() //Body temperature adjusts itself (self-regulation)
stabilize_body_temperature(seconds_per_tick) //Body temperature adjusts itself (self-regulation)
//Random events (vomiting etc)
handle_random_events()
@@ -303,7 +303,7 @@
return breath
/mob/living/carbon/human/handle_environment(datum/gas_mixture/environment)
/mob/living/carbon/human/handle_environment(datum/gas_mixture/environment, seconds_per_tick)
..()
if(!environment)
@@ -330,8 +330,10 @@
if(bodytemperature > (0.1 * HUMAN_HEAT_CAPACITY/(HUMAN_EXPOSED_SURFACE_AREA*STEFAN_BOLTZMANN_CONSTANT))**(1/4) + COSMIC_RADIATION_TEMPERATURE)
//Thermal radiation into space
var/heat_loss = HUMAN_EXPOSED_SURFACE_AREA * STEFAN_BOLTZMANN_CONSTANT * ((bodytemperature - COSMIC_RADIATION_TEMPERATURE)**4)
// Temperature loss in Watts (kgm^2/s^3)
var/temperature_loss = heat_loss/HUMAN_HEAT_CAPACITY
bodytemperature -= temperature_loss
// Since body temperature is in Joules (kgm^2/s^2), we multiply temperature loss by DT (in seconds) to convert from Watts to Joules before subtracting.
bodytemperature -= temperature_loss * seconds_per_tick
else
var/loc_temp = T0C
if(istype(loc, /obj/machinery/atmospherics/unary/cryo_cell))
@@ -437,9 +439,9 @@
baseline += clothing.body_temperature_change
return baseline
/mob/living/carbon/human/proc/stabilize_body_temperature()
/mob/living/carbon/human/proc/stabilize_body_temperature(seconds_per_tick)
if (species.passive_temp_gain) // We produce heat naturally.
species.handle_temperature_regulation(src)
species.handle_temperature_regulation(src, seconds_per_tick)
if (species.body_temperature == null)
return //this species doesn't have metabolic thermoregulation
@@ -453,19 +455,16 @@
if(bodytemperature < species.cold_level_1) //260.15 is 310.15 - 50, the temperature where you start to feel effects.
if(nutrition >= 2) //If we are very, very cold we'll use up quite a bit of nutriment to heat us up.
adjustNutritionLoss(2)
var/recovery_amt = max((body_temperature_difference / BODYTEMP_AUTORECOVERY_DIVISOR), BODYTEMP_AUTORECOVERY_MINIMUM)
bodytemperature += recovery_amt
adjustNutritionLoss(seconds_per_tick)
bodytemperature += max((body_temperature_difference / BODYTEMP_AUTORECOVERY_DIVISOR * seconds_per_tick), BODYTEMP_AUTORECOVERY_MINIMUM * seconds_per_tick)
else if(species.cold_level_1 <= bodytemperature && bodytemperature <= species.heat_level_1)
var/recovery_amt = body_temperature_difference / BODYTEMP_AUTORECOVERY_DIVISOR
bodytemperature += recovery_amt
bodytemperature += body_temperature_difference / BODYTEMP_AUTORECOVERY_DIVISOR * seconds_per_tick
else if(bodytemperature > species.heat_level_1) //360.15 is 310.15 + 50, the temperature where you start to feel effects.
if(hydration >= 2)
adjustHydrationLoss(2)
adjustHydrationLoss(seconds_per_tick)
if((species.flags & CAN_SWEAT) && fire_stacks == 0)
fire_stacks = -1
var/recovery_amt = min((body_temperature_difference / BODYTEMP_AUTORECOVERY_DIVISOR), -BODYTEMP_AUTORECOVERY_MINIMUM) //We're dealing with negative numbers
bodytemperature += recovery_amt
bodytemperature += min((body_temperature_difference * seconds_per_tick / BODYTEMP_AUTORECOVERY_DIVISOR), -BODYTEMP_AUTORECOVERY_MINIMUM * seconds_per_tick) //We're dealing with negative numbers
/**
* Returns a bitflag for the body parts currently heat-protected. Called and used by get_heat_protection()
@@ -238,7 +238,7 @@
var/heat_level_2 = 400
/// Heat damage level 3 above this point
var/heat_level_3 = 1000
/// Species will gain this much temperature every second
/// Species will gain this much temperature (in degrees kelvin per second)
var/passive_temp_gain = 0
/// Dangerously high pressure
var/hazard_high_pressure = HAZARD_HIGH_PRESSURE
@@ -1080,5 +1080,5 @@
* This proc handles the species temperature regulation. By default, it just adds `passive_temp_gain` to the human's bodytemperature.
* Can be overridden for more complex calculations.
*/
/datum/species/proc/handle_temperature_regulation(mob/living/carbon/human/human)
human.bodytemperature += passive_temp_gain
/datum/species/proc/handle_temperature_regulation(mob/living/carbon/human/human, seconds_per_tick)
human.bodytemperature += passive_temp_gain * seconds_per_tick
@@ -72,7 +72,7 @@
heat_level_3 = 2400
body_temperature = null
passive_temp_gain = 10
passive_temp_gain = 5 // Degrees kelvin per second
inherent_verbs = list(
/mob/living/carbon/human/proc/change_monitor,
@@ -171,11 +171,11 @@
var/machine_ui_theme = "hackerman"
/datum/species/machine/handle_temperature_regulation(mob/living/carbon/human/human)
/datum/species/machine/handle_temperature_regulation(mob/living/carbon/human/human, seconds_per_tick)
// No cooling unit = you're cooking. Broken cooling unit effects are handled by the organ itself.
// Here we just want to check if it's been removed.
// 500K is about 226 degrees. Spicy!
var/base_heat_gain = passive_temp_gain
var/base_heat_gain = passive_temp_gain * seconds_per_tick
var/obj/item/organ/internal/machine/cooling_unit/cooling = human.internal_organs_by_name[BP_COOLING_UNIT]
if(!cooling || (cooling?.status & ORGAN_DEAD))
base_heat_gain *= 4 //uh oh
+2 -2
View File
@@ -13,7 +13,7 @@
var/datum/gas_mixture/environment = loc.return_air()
//Handle temperature/pressure differences between body and environment
if(environment)
handle_environment(environment)
handle_environment(environment, seconds_per_tick)
blinded = 0 // Placing this here just show how out of place it is.
@@ -213,7 +213,7 @@
if(!.) // If we're under or inside shelter, use the z-level rain (for ambience)
. = SSweather.weather_by_z["[my_turf.z]"]
/mob/living/proc/handle_environment(var/datum/gas_mixture/environment)
/mob/living/proc/handle_environment(var/datum/gas_mixture/environment, seconds_per_tick)
SHOULD_CALL_PARENT(TRUE)
@@ -22,8 +22,11 @@
/// The power consumed when we are cooling down.
var/base_power_consumption = 8
/// The passive temperature change. Basically, cooling units counteract an IPC's passive temperature gain. But the IPC's temperature goes to get itself fucked if the cooling unit dies.
/// Remember, can be negative or positive. Depends on what we're trying to stabilize towards.
/**
* The passive temperature change in "Degrees Kelvin Per Second". This is reduced by a variety of conditions.
* Basically, cooling units counteract an IPC's passive temperature gain. But the IPC's temperature goes to get itself fucked if the cooling unit dies.
* Remember, can be negative or positive. Depends on what we're trying to stabilize towards.
*/
var/passive_temp_change = 2
/// The temperature that this cooling unit tries to regulate towards.
var/thermostat = T20C
@@ -41,6 +44,8 @@
var/safety_burnt = FALSE
/// If the thermostat is locked and thus cannot be changed. Used for spooky effects, like the high integrity damage in the positronic brain.
var/locked_thermostat = FALSE
/// The amount of degrees kelvin (per second) of heat the cooling unit will generate if it is blocked by a space suit.
var/spacesuited_heat_per_second = 5
/obj/item/organ/internal/machine/cooling_unit/Initialize()
. = ..()
@@ -138,13 +143,11 @@
if(prob(owner.bodytemperature * 0.1))
take_internal_damage(owner.bodytemperature * 0.01)
var/temperature_change = passive_temp_change
var/temperature_change = passive_temp_change * seconds_per_tick
if(owner.wear_suit)
if(!spaceproof && istype(owner.wear_suit, /obj/item/clothing/suit/space))
//cooling is going to SUCK if you have heat-regulating clothes
if(owner.bodytemperature < species.heat_level_3)
owner.bodytemperature = min(owner.bodytemperature + 5, owner.species.heat_level_2)
temperature_change *= 0.5
temperature_change *= 0.5
// Check if there is somehow no air, or if we are in an ambient without enough air to properly cool us.
if((!ambient || (ambient && owner.calculate_affecting_pressure(XGM_PRESSURE(ambient)) < owner.species.warning_low_pressure)))
@@ -365,14 +365,14 @@
else
owner.robot_pain.icon_state = null
/obj/item/organ/internal/machine/posibrain/low_integrity_damage(integrity)
/obj/item/organ/internal/machine/posibrain/low_integrity_damage(integrity, seconds_per_tick)
var/damage_probability = get_integrity_damage_probability(integrity)
if(prob(damage_probability))
if(SPT_PROB(damage_probability, seconds_per_tick))
to_chat(owner, SPAN_MACHINE_WARNING("Neural pathway error located at block 0x[generate_hex()]."))
take_internal_damage(2)
. = ..()
/obj/item/organ/internal/machine/posibrain/medium_integrity_damage(integrity)
/obj/item/organ/internal/machine/posibrain/medium_integrity_damage(integrity, seconds_per_tick)
var/damage_probability = get_integrity_damage_probability(integrity)
var/list/static/medium_integrity_damage_messages = list(
"Your neural subroutines' alarms are all going off at once.",
@@ -381,14 +381,14 @@
"Your software warns you of dangerously low neural coherence.",
"Your self-preservation subroutines threaten to kick in. [SPAN_DANGER("WARNING. WARNING.")]"
)
if(prob(damage_probability))
if(SPT_PROB(damage_probability, seconds_per_tick))
to_chat(owner, SPAN_MACHINE_WARNING(pick(medium_integrity_damage_messages)))
take_internal_damage(2)
. = ..()
/obj/item/organ/internal/machine/posibrain/high_integrity_damage(integrity)
/obj/item/organ/internal/machine/posibrain/high_integrity_damage(integrity, seconds_per_tick)
var/damage_probability = get_integrity_damage_probability(integrity)
if(prob(damage_probability))
if(SPT_PROB(damage_probability, seconds_per_tick))
var/damage_roll = rand(1, 50)
switch(damage_roll)
if(1 to 10)
@@ -0,0 +1,7 @@
author: Hellfirejag
delete-after: True
changes:
- bugfix: "Fixed organs ticking 50% faster than intended."
- rscadd: "Reworked most of IPC heat/cooling code to no longer create uncontrollable heat spikes."
- bugfix: "Fixed IPCs in space suits generating impossible to manage levels of heat even with a suit cooler."
- bugfix: "Fixed suit coolers not even working on IPCs in a space suit in the first place."