Unfuck shields by not making them process a huge list constantly. (#21428)

Ship shields currently occupy something like 40% of every machinery
process ticks. Basically generators make a fuckhuge list of all
generated shields (1.5k in the Horizon's case) and then they iterate
over this list every tick strengthening the shields.

The idea in this PR is to shunt shield logic to an abstract datum.
Fields no longer store strength individually; they store damage taken.
They only process once they take damage, they reduce that damage by the
field gain tick value every tick, and then they stop processing when
they no longer need to. Shield strengthening is shunted off to a signal,
so we only need to traverse the big ass list once every round probably.

---------

Co-authored-by: Matt Atlas <liermattia@gmail.com>
This commit is contained in:
Matt Atlas
2025-10-07 12:34:20 +00:00
committed by GitHub
co-authored by Matt Atlas
parent 8ce0b27e04
commit 1af71a7c45
13 changed files with 262 additions and 202 deletions
+1 -1
View File
@@ -71,7 +71,7 @@
//If there's shields and it's strong enough, the power of the explosion is reduced, but it won't stop it
if(istype(A, /obj/effect/energy_field))
var/obj/effect/energy_field/impacted_energy_field = A
if(impacted_energy_field.strength > SHIELD_MINIMUM_STRENGTH_TO_REDUCE_EXPLOSION_POWER)
if(impacted_energy_field.damage < 1)
hitpwr *= 0.5
qdel(impacted_energy_field)
@@ -439,7 +439,7 @@ ABSTRACT_TYPE(/mob/living/simple_animal/hostile)
found_obj = locate(/obj/effect/energy_field) in target_turf
if(found_obj && !found_obj.invisibility && found_obj.density)
var/obj/effect/energy_field/e = found_obj
e.Stress(rand(0.5, 1.5))
e.damage_field(rand(0.5, 1.5))
visible_message(SPAN_DANGER("[capitalize_first_letters(src.name)] [attacktext] \the [e]!"))
src.do_attack_animation(e)
set_last_found_target(e)
@@ -91,7 +91,7 @@
return
if(istype(last_found_target, /obj/effect/energy_field))
var/obj/effect/energy_field/e = last_found_target
e.Stress(rand(1,2))
e.damage_field(rand(1,2))
visible_message(SPAN_DANGER("\the [src] bites \the [e]!"))
src.do_attack_animation(e)
return e
@@ -1,83 +0,0 @@
/datum/artifact_effect/forcefield
effecttype = "forcefield"
var/list/created_field = list()
effect_type = 4
/datum/artifact_effect/forcefield/New()
..()
trigger = TRIGGER_TOUCH
/datum/artifact_effect/forcefield/ToggleActivate()
..()
if(created_field.len)
for(var/obj/effect/energy_field/F in created_field)
created_field.Remove(F)
qdel(F)
else if(holder)
var/turf/T = get_turf(holder)
while(created_field.len < 16)
var/obj/effect/energy_field/E = new (locate(T.x,T.y,T.z))
created_field.Add(E)
E.strength = 1
E.density = 1
E.anchored = 1
E.set_invisibility(0)
spawn(10)
UpdateMove()
return 1
/datum/artifact_effect/forcefield/process()
..()
for(var/obj/effect/energy_field/E in created_field)
if(E.strength < 1)
E.Strengthen(0.15)
else if(E.strength < 5)
E.Strengthen(0.25)
/datum/artifact_effect/forcefield/UpdateMove()
if(created_field.len && holder)
var/turf/T = get_turf(holder)
if(!T)
return
while(created_field.len < 16)
//for now, just instantly respawn the fields when they get destroyed
var/obj/effect/energy_field/E = new (locate(T.x,T.y,T))
created_field.Add(E)
E.anchored = 1
E.density = 1
E.set_invisibility(0)
var/obj/effect/energy_field/E = created_field[1]
E.forceMove(locate(T.x + 2,T.y + 2,T.z))
E = created_field[2]
E.forceMove(locate(T.x + 2,T.y + 1,T.z))
E = created_field[3]
E.forceMove(locate(T.x + 2,T.y,T.z))
E = created_field[4]
E.forceMove(locate(T.x + 2,T.y - 1,T.z))
E = created_field[5]
E.forceMove(locate(T.x + 2,T.y - 2,T.z))
E = created_field[6]
E.forceMove(locate(T.x + 1,T.y + 2,T.z))
E = created_field[7]
E.forceMove(locate(T.x + 1,T.y - 2,T.z))
E = created_field[8]
E.forceMove(locate(T.x,T.y + 2,T.z))
E = created_field[9]
E.forceMove(locate(T.x,T.y - 2,T.z))
E = created_field[10]
E.forceMove(locate(T.x - 1,T.y + 2,T.z))
E = created_field[11]
E.forceMove(locate(T.x - 1,T.y - 2,T.z))
E = created_field[12]
E.forceMove(locate(T.x - 2,T.y + 2,T.z))
E = created_field[13]
E.forceMove(locate(T.x - 2,T.y + 1,T.z))
E = created_field[14]
E.forceMove(locate(T.x - 2,T.y,T.z))
E = created_field[15]
E.forceMove(locate(T.x - 2,T.y - 1,T.z))
E = created_field[16]
E.forceMove(locate(T.x - 2,T.y - 2,T.z))
+101
View File
@@ -0,0 +1,101 @@
// This is an abstracted energy field to cut down on processing thousands of shields per process tick.
/datum/energy_field
/// A gigantic ass fucking list of energy fields.
var/list/field = list()
/// The actual strength of the field.
var/field_strength = 0
/// Current strengthening rate of a single field.
var/strengthen_rate = 0.2
/// Maximum rate by which an energy field can be strengthened.
var/max_strengthen_rate = 0.5
/// The percentage of the shield strength that needs to be replaced each second
var/dissipation_rate = 0.030
/// An energy field will dissipate by at least this rate in renwicks per field tile (otherwise field would never dissipate completely as dissipation is a percentage)
var/min_dissipation = 0.01
/// Our target field strength.
var/target_field_strength = 10
/// The maximum field strength we can go to.
var/max_field_strength = 10
/// The time passed since the last "fail", AKA losing charge faster than you can replenish it.
var/time_since_fail = 100
/// How many renwicks per watt.
var/energy_conversion_rate = 0.0002
/// If the field is strong, then the energy field objects will turn dense.
var/strong_field = FALSE
/datum/energy_field/Destroy(force)
clear_field()
return ..()
/**
* This proc, called when a shield generator processes, makes the whole thing tick by updating strength and etc.
* @assumed_charge: the charge that is given to this energy field. You have to get the required energy first, if you want a balanced field.
*/
/datum/energy_field/proc/handle_strength(assumed_charge = 0)
if(length(field))
time_since_fail++
//the amount of renwicks that the generator can add this tick, over the entire field
var/total_renwick_increase = assumed_charge * energy_conversion_rate
var/renwick_increase_per_field = total_renwick_increase / length(field) //per field tile
var/renwick_upkeep_per_field = max(field_strength * dissipation_rate, min_dissipation)
var/amount_to_strengthen = renwick_increase_per_field - renwick_upkeep_per_field
field_strength = min(field_strength + amount_to_strengthen, max_field_strength)
if(field_strength < 1)
if(strong_field)
strong_field = FALSE
SEND_SIGNAL(src, COMSIG_SHIELDS_UPDATE_STRENGTH_STATUS)
time_since_fail = 0
else
if(!strong_field)
strong_field = TRUE
SEND_SIGNAL(src, COMSIG_SHIELDS_UPDATE_STRENGTH_STATUS)
else
field_strength = 0
/**
* Returns the required energy upkeep for the field.
*/
/datum/energy_field/proc/get_required_energy()
var/required_energy = 0
if(length(field))
var/renwick_upkeep_per_field = max(field_strength * dissipation_rate, min_dissipation)
var/target_renwick_increase = min(target_field_strength - field_strength, strengthen_rate) + renwick_upkeep_per_field //per field tile
required_energy = length(field) * target_renwick_increase / energy_conversion_rate
return required_energy
/**
* Clears the field of active field objects.
*/
/datum/energy_field/proc/clear_field()
field_strength = 0
strong_field = FALSE
for(var/obj/effect/energy_field/D as anything in field)
field.Remove(D)
qdel(D)
/**
* Sets the shielded turfs of the energy field.
*/
/datum/energy_field/proc/set_shielded_turfs(list/shielded_turfs)
if(length(shielded_turfs))
for(var/turf/T in shielded_turfs)
var/obj/effect/energy_field/F = new(T, src)
field += F
/**
* Adds a bunch of UI data for TGUIs with relevant field data.
*/
/datum/energy_field/proc/add_field_ui_data(list/data)
if(!istype(data))
return
data["average_field"] = round(field_strength, 0.01)
data["progress_field"] = (target_field_strength ? round(100 * field_strength / target_field_strength, 0.1) : "NA")
data["power_take"] = round(length(field) * max(field_strength * dissipation_rate, min_dissipation) / energy_conversion_rate)
data["shield_power"] = round(length(field) * min(strengthen_rate, target_field_strength - field_strength) / energy_conversion_rate)
data["strengthen_rate"] = (strengthen_rate * 10)
data["max_strengthen_rate"] = (max_strengthen_rate * 10)
data["target_field_strength"] = target_field_strength
return data
+53 -21
View File
@@ -11,20 +11,32 @@
anchored = 1
layer = ABOVE_HUMAN_LAYER
density = 0
var/strength = 0
/// A ref holding the /datum/energy_field that this field object belongs to.
var/datum/energy_field/energy_field
/// The damage suffered by the field.
var/damage = 0
var/ticks_recovering = 10
var/diffused_for = 0
var/diffused = FALSE
var/is_strong = FALSE // if strength goes is 1 or above, this is set to TRUE, this is to prevent flickering and animate being called constantly
/// If strength goes is 1 or above, this is set to TRUE, this is to prevent flickering and animate being called constantly
var/is_strong = FALSE
atmos_canpass = CANPASS_ALWAYS
/obj/effect/energy_field/Initialize()
/obj/effect/energy_field/Initialize(mapload, datum/energy_field/mother_field)
. = ..()
if(istype(mother_field))
energy_field = mother_field
else
log_debug("Energy field generated without mother field, deleting.")
qdel_self()
RegisterSignal(mother_field, COMSIG_SHIELDS_UPDATE_STRENGTH_STATUS, PROC_REF(update_strength))
update_nearby_tiles()
/obj/effect/energy_field/Destroy()
if(istype(energy_field))
energy_field.field -= src
energy_field = null
update_nearby_tiles()
return ..()
@@ -39,7 +51,7 @@
else
user.visible_message(SPAN_WARNING("[user] attacks \the [src] with \the [attacking_item]."),
SPAN_WARNING("You attack \the [src] with \the [attacking_item]."))
Stress(attacking_item.force / 10)
damage_field(attacking_item.force / 10)
/obj/effect/energy_field/attack_hand(mob/living/carbon/human/H)
if(istype(H))
@@ -47,35 +59,51 @@
H.setClickCooldown(DEFAULT_ATTACK_COOLDOWN)
H.do_attack_animation(src, FIST_ATTACK_ANIMATION)
H.visible_message(SPAN_WARNING("[H] shreds \the [src]!"), SPAN_WARNING("You shred \the [src]!"))
Stress(1)
damage_field(1)
return
to_chat(H, SPAN_WARNING("You touch \the [src], and it repulses your hand."))
/obj/effect/energy_field/ex_act(var/severity)
Stress(0.5 + severity)
damage_field(0.5 + severity)
/obj/effect/energy_field/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit)
. = ..()
if(. != BULLET_ACT_HIT)
return .
Stress(hitting_projectile.get_structure_damage() / 10)
damage_field(hitting_projectile.get_structure_damage() / 10)
/obj/effect/energy_field/proc/Stress(var/severity)
strength -= severity
/obj/effect/energy_field/proc/damage_field(var/severity)
if(!severity)
return
damage += severity
if(!(datum_flags & DF_ISPROCESSING))
// Start processing ONLY when we're damaged. Through processing, we're going to slowly climb back up to field strength.
START_PROCESSING(SSprocessing, src)
var/shield_health = get_health()
//if we take too much damage, drop out - the generator will bring us back up if we have enough power
ticks_recovering = min(ticks_recovering + 2, 10)
if(strength < 1 && is_strong)
if(shield_health < 1 && is_strong)
density_check(FALSE)
is_strong = FALSE
ticks_recovering = 10
strength = 0
else if(strength >= 1 && !is_strong)
else if(shield_health >= 1 && !is_strong)
density_check(TRUE)
is_strong = TRUE
//
/obj/effect/energy_field/process(seconds_per_tick)
if(!damage) // No need to process anymore.
update_strength()
return PROCESS_KILL
if(ticks_recovering)
ticks_recovering--
else
damage = max(damage - energy_field.strengthen_rate, 0)
/obj/effect/energy_field/proc/density_check(var/turn_on)
if(turn_on && !diffused)
alpha = 0
@@ -99,17 +127,15 @@
diffused = FALSE
density_check(TRUE)
/obj/effect/energy_field/proc/Strengthen(var/severity)
strength += severity
if (strength < 0)
strength = 0
/obj/effect/energy_field/proc/update_strength()
SIGNAL_HANDLER
//if we take too much damage, drop out - the generator will bring us back up if we have enough power
var/old_density = density
if(strength >= 1 && !is_strong)
var/shield_health = get_health()
if(shield_health >= 1 && !is_strong)
is_strong = TRUE
density_check(TRUE)
else if(strength < 1 && is_strong)
else if(shield_health < 1 && is_strong)
is_strong = FALSE
density_check(FALSE)
@@ -118,6 +144,12 @@
diffuse_check()
/**
* Easy helper proc to return the shield's current health.
*/
/obj/effect/energy_field/proc/get_health()
return max(energy_field.field_strength - damage, 0)
/obj/effect/energy_field/CanPass(atom/movable/mover, turf/target, height=1.5, air_group = 0)
if(mover?.movement_type & PHASING)
return TRUE
+43 -88
View File
@@ -10,41 +10,36 @@
desc = "Machine that generates an impenetrable field of energy when activated."
icon = 'icons/obj/machinery/shielding.dmi'
icon_state = "generator0"
var/active = FALSE
var/field_radius = 3
var/max_field_radius = 100
var/list/field
density = TRUE
var/locked = FALSE
var/average_field_strength = 0
var/strengthen_rate = 0.2
var/max_strengthen_rate = 0.5 //the maximum rate that the generator can increase the average field strength
var/dissipation_rate = 0.030 //the percentage of the shield strength that needs to be replaced each second
var/min_dissipation = 0.01 //will dissipate by at least this rate in renwicks per field tile (otherwise field would never dissipate completely as dissipation is a percentage)
var/powered = FALSE
var/check_powered = TRUE
var/obj/machinery/shield_capacitor/owned_capacitor
var/target_field_strength = 10
var/max_field_strength = 10
var/time_since_fail = 100
var/energy_conversion_rate = 0.0002 //how many renwicks per watt?
use_power = POWER_USE_OFF //doesn't use APC power
var/multiz = TRUE
var/multi_unlocked = TRUE
req_one_access = list(ACCESS_CAPTAIN, ACCESS_SECURITY, ACCESS_ENGINE)
/// Our owned energy field.
var/datum/energy_field/energy_field
/// If the machine is powered or not.
var/powered = FALSE
/// Whether the shield generator is active or not.
var/active = FALSE
/// ID lock.
var/locked = FALSE
/// Radius of the field.
var/field_radius = 3
/// Maximum field radius.
var/max_field_radius = 100
/// The shield capacitor attached to this shield generator.
var/obj/machinery/shield_capacitor/owned_capacitor
/// If this shield generator supports multi-z.
var/multiz = TRUE
/obj/machinery/shield_gen/Initialize()
for(var/obj/machinery/shield_capacitor/possible_cap in range(1, src))
if(get_dir(possible_cap, src) == possible_cap.dir)
owned_capacitor = possible_cap
break
field = list()
energy_field = new(src, get_shielded_turfs())
. = ..()
/obj/machinery/shield_gen/Destroy()
for(var/obj/effect/energy_field/D as anything in field)
field.Remove(D)
D.loc = null
owned_capacitor = null
return ..()
/obj/machinery/shield_gen/emag_act(var/remaining_charges, var/mob/user)
@@ -77,7 +72,6 @@
if(get_dir(cap, src) == cap.dir && cap.anchored)
owned_capacitor = cap
owned_capacitor.owned_gen = src
updateDialog()
break
else
if(owned_capacitor && owned_capacitor.owned_gen == src)
@@ -128,46 +122,21 @@
toggle()
return PROCESS_KILL
average_field_strength = max(average_field_strength, 0)
if(istype(energy_field))
var/required_energy = energy_field.get_required_energy()
var/assumed_charge = assume_charge(required_energy)
energy_field.handle_strength(assumed_charge)
if(field.len)
time_since_fail++
var/total_renwick_increase = 0 //the amount of renwicks that the generator can add this tick, over the entire field
var/renwick_upkeep_per_field = max(average_field_strength * dissipation_rate, min_dissipation)
//figure out how much energy we need to draw from the capacitor
if(active && owned_capacitor?.active)
var/target_renwick_increase = min(target_field_strength - average_field_strength, strengthen_rate) + renwick_upkeep_per_field //per field tile
var/required_energy = field.len * target_renwick_increase / energy_conversion_rate
var/assumed_charge = min(owned_capacitor.stored_charge, required_energy)
total_renwick_increase = assumed_charge * energy_conversion_rate
assumed_charge = max(assumed_charge, 0)
owned_capacitor.stored_charge -= assumed_charge
else
renwick_upkeep_per_field = max(renwick_upkeep_per_field, 0.5)
var/renwick_increase_per_field = total_renwick_increase/field.len //per field tile
average_field_strength = 0 //recalculate the average field strength
for(var/obj/effect/energy_field/E as anything in field)
if(!E)
field -= E
continue
var/amount_to_strengthen = renwick_increase_per_field - renwick_upkeep_per_field
if(E.ticks_recovering > 0 && amount_to_strengthen > 0)
E.Strengthen( min(amount_to_strengthen / 10, 0.1) )
E.ticks_recovering -= 1
else
E.Strengthen(amount_to_strengthen)
average_field_strength += E.strength
average_field_strength /= field.len
if(average_field_strength < 1)
time_since_fail = 0
else
average_field_strength = 0
/**
* Called whenever the field needs to take charge from the capacitor.
*/
/obj/machinery/shield_gen/proc/assume_charge(required_energy)
if(!owned_capacitor)
return 0
var/assumed_charge = min(owned_capacitor.stored_charge, required_energy)
assumed_charge = max(assumed_charge, 0)
owned_capacitor.stored_charge -= assumed_charge
return assumed_charge
/obj/machinery/shield_gen/ex_act(var/severity)
if(active)
@@ -178,22 +147,13 @@
active = !active
update_icon()
if(active)
var/list/covered_turfs = get_shielded_turfs()
var/turf/T = get_turf(src)
for(var/turf/O as anything in covered_turfs - T)
var/obj/effect/energy_field/E = new(O)
field.Add(E)
covered_turfs = null
for(var/mob/M as anything in hearers(5,src))
to_chat(M, "[icon2html(src, M)] You hear heavy droning start up.")
to_chat(M, SPAN_NOTICE("[icon2html(src, M)] You hear heavy droning start up."))
energy_field.set_shielded_turfs(get_shielded_turfs())
else
for(var/obj/effect/energy_field/D as anything in field)
field.Remove(D)
D.loc = null
for(var/mob/M as anything in hearers(5,src))
to_chat(M, "[icon2html(src, M)] You hear heavy droning fade out.")
to_chat(M, SPAN_NOTICE("[icon2html(src, M)] You hear heavy droning fade out."))
energy_field.clear_field()
/obj/machinery/shield_gen/update_icon()
if(stat & BROKEN)
@@ -212,7 +172,7 @@
if(field_radius > 0 && T)
var/connected_levels = list(T.z)
if(multiz)
for(var/turf/connected_z_turf as anything in (getzabove(src) + getzbelow(src)))
for(var/turf/connected_z_turf as anything in (getzabove(T) + getzbelow(T)))
connected_levels += connected_z_turf.z
. += block(\
@@ -235,6 +195,8 @@
locate(T.x + field_radius, T.y - field_radius + 1, max(connected_levels))\
)
return . - get_turf(src)
/obj/machinery/shield_gen/ui_interact(mob/user, var/datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
@@ -246,19 +208,12 @@
data["owned_capacitor"] = !!owned_capacitor
data["active"] = active
data["time_since_fail"] = time_since_fail
data["multi_unlocked"] = multi_unlocked
data["time_since_fail"] = energy_field ? energy_field.time_since_fail : 0
data["multiz"] = multiz
data["field_radius"] = field_radius
data["min_field_radius"] = 1
data["max_field_radius"] = max_field_radius
data["average_field"] = round(average_field_strength, 0.01)
data["progress_field"] = (target_field_strength ? round(100 * average_field_strength / target_field_strength, 0.1) : "NA")
data["power_take"] = round(field.len * max(average_field_strength * dissipation_rate, min_dissipation) / energy_conversion_rate)
data["shield_power"] = round(field.len * min(strengthen_rate, target_field_strength - average_field_strength) / energy_conversion_rate)
data["strengthen_rate"] = (strengthen_rate * 10)
data["max_strengthen_rate"] = (max_strengthen_rate * 10)
data["target_field_strength"] = target_field_strength
data = energy_field.add_field_ui_data(data)
return data
@@ -278,10 +233,10 @@
field_radius = between(1, text2num(params["size_set"]), max_field_radius)
if ("charge_set")
strengthen_rate = (between(1, text2num(params["charge_set"]), (max_strengthen_rate * 10)) / 10)
energy_field.strengthen_rate = (between(1, text2num(params["charge_set"]), (energy_field.max_strengthen_rate * 10)) / 10)
if ("field_set")
target_field_strength = between(1, text2num(params["field_set"]), 10)
energy_field.target_field_strength = between(1, text2num(params["field_set"]), 10)
add_fingerprint(usr)
@@ -3,7 +3,6 @@
/obj/machinery/shield_gen/external
name = "hull shield generator"
multi_unlocked = TRUE
//Search for space turfs within range that are adjacent to a simulated turf.
/obj/machinery/shield_gen/external/get_shielded_turfs()