diff --git a/code/__DEFINES/movement.dm b/code/__DEFINES/movement.dm index 5b2ea68d853..10f9e67025f 100644 --- a/code/__DEFINES/movement.dm +++ b/code/__DEFINES/movement.dm @@ -35,6 +35,10 @@ GLOBAL_VAR_INIT(glide_size_multiplier, 1.0) ///Should we override the loop's glide? #define MOVEMENT_LOOP_IGNORE_GLIDE (1<<2) +//Index defines for movement bucket data packets +#define MOVEMENT_BUCKET_TIME 1 +#define MOVEMENT_BUCKET_LIST 2 + /** * currently_z_moving defines. Higher numbers mean higher priority. * This one is for falling down open space from stuff such as deleted tile, pit grate... diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index 7c034257ce6..570d4523f7c 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -133,6 +133,43 @@ };\ } while(FALSE) +#define SORT_FIRST_INDEX(list) (list[1]) +#define SORT_VAR_NO_TYPE(varname) var/varname +/**** + * Even more custom binary search sorted insert, using defines instead of vars + * INPUT: Item to be inserted + * LIST: List to insert INPUT into + * TYPECONT: A define setting the var to the typepath of the contents of the list + * COMPARE: The item to compare against, usualy the same as INPUT + * COMPARISON: A define that takes an item to compare as input, and returns their comparable value + * COMPTYPE: How should the list be compared? Either COMPARE_KEY or COMPARE_VALUE. + */ +#define BINARY_INSERT_DEFINE(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \ + do {\ + var/list/__BIN_LIST = LIST;\ + var/__BIN_CTTL = length(__BIN_LIST);\ + if(!__BIN_CTTL) {\ + __BIN_LIST += INPUT;\ + } else {\ + var/__BIN_LEFT = 1;\ + var/__BIN_RIGHT = __BIN_CTTL;\ + var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + ##TYPECONT(__BIN_ITEM);\ + while(__BIN_LEFT < __BIN_RIGHT) {\ + __BIN_ITEM = COMPTYPE;\ + if(##COMPARISON(__BIN_ITEM) <= ##COMPARISON(COMPARE)) {\ + __BIN_LEFT = __BIN_MID + 1;\ + } else {\ + __BIN_RIGHT = __BIN_MID;\ + };\ + __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + };\ + __BIN_ITEM = COMPTYPE;\ + __BIN_MID = ##COMPARISON(__BIN_ITEM) > ##COMPARISON(COMPARE) ? __BIN_MID : __BIN_MID + 1;\ + __BIN_LIST.Insert(__BIN_MID, INPUT);\ + };\ + } while(FALSE) + ///Returns a list in plain english as a string /proc/english_list(list/input, nothing_text = "nothing", and_text = " and ", comma_text = ", ", final_comma_text = "" ) var/total = length(input) diff --git a/code/controllers/subsystem/movement/movement.dm b/code/controllers/subsystem/movement/movement.dm index 21342ac8f78..ebf441749e8 100644 --- a/code/controllers/subsystem/movement/movement.dm +++ b/code/controllers/subsystem/movement/movement.dm @@ -2,17 +2,28 @@ SUBSYSTEM_DEF(movement) name = "Movement Loops" flags = SS_NO_INIT|SS_BACKGROUND|SS_TICKER wait = 1 //Fire each tick - ///The list of datums we're processing - var/list/processing = list() - ///Used to make pausing possible - var/list/currentrun = list() + /* + A breif aside about the bucketing system here + + The goal is to allow for higher loads of semi long delays while reducing cpu usage + Bucket insertion and management are much less complex then what you might see in SStimer + This is intentional, as we loop our delays much more often then that ss is designed for + We also have much shorter term timers, so we need to worry about redundant buckets much less + */ + ///Assoc list of "target time" -> list(things to process). Used for quick lookup + var/list/buckets = list() + ///Sorted list of list(target time, bucket to process) + var/list/sorted_buckets = list() ///The time we started our last fire at var/canonical_time = 0 ///The visual delay of the subsystem var/visual_delay = 1 /datum/controller/subsystem/movement/stat_entry(msg) - msg = "P:[length(processing)]" + var/total_len = 0 + for(var/list/bucket as anything in sorted_buckets) + total_len += length(bucket[MOVEMENT_BUCKET_LIST]) + msg = "B:[length(sorted_buckets)] E:[total_len]" return ..() /datum/controller/subsystem/movement/Recover() @@ -22,30 +33,75 @@ SUBSYSTEM_DEF(movement) var/our_name = typenames[length(typenames)] //Get the last name in the list, IE the subsystem identifier var/datum/controller/subsystem/movement/old_version = global.vars["SS[our_name]"] - processing = old_version.processing - currentrun = old_version.currentrun + buckets = old_version.buckets + sorted_buckets = old_version.sorted_buckets /datum/controller/subsystem/movement/fire(resumed) if(!resumed) canonical_time = world.time - currentrun = processing.Copy() - var/list/running = currentrun //Cache for... you've heard this before - while(running.len) - var/datum/move_loop/loop = running[running.len] - running.len-- - if(loop.timer <= canonical_time) - loop.process() //This shouldn't get nulls, if it does, runtime - if (MC_TICK_CHECK) + 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) + +/// 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) + 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-- + loop.process() //This shouldn't get nulls, if it does, runtime + if(!QDELETED(loop)) //Re-Insert the loop + loop.timer = world.time + loop.delay + queue_loop(loop) + if (MC_TICK_CHECK) + break + + if(length(processing)) + return // Still work to be done + var/bucket_time = bucket_info[MOVEMENT_BUCKET_TIME] + smash_bucket(1, bucket_time) // We assume we're the first bucket in the queue right now visual_delay = MC_AVERAGE_FAST(visual_delay, max((world.time - canonical_time) / wait, 1)) +/// Removes a bucket from our system. You only need to pass in the time, but if you pass in the index of the list you save us some work +/datum/controller/subsystem/movement/proc/smash_bucket(index, bucket_time) + if(!index) + for(var/i in 1 to length(sorted_buckets)) + var/list/bucket_info = sorted_buckets[i] + if(bucket_info[MOVEMENT_BUCKET_TIME] != bucket_time) + continue + index = i + break + sorted_buckets.Cut(index, index + 1) //Removes just this list + //Removes the assoc lookup too + buckets -= "[bucket_time]" + +/datum/controller/subsystem/movement/proc/queue_loop(datum/move_loop/loop) + var/target_time = loop.timer + var/string_time = "[target_time]" + if(buckets[string_time]) + buckets[string_time] += loop + else + buckets[string_time] = list(loop) + // This makes buckets and sorted buckets point to the same place, allowing for quicker inserts + var/list/new_bucket = list(list(target_time, buckets[string_time])) + BINARY_INSERT_DEFINE(new_bucket, sorted_buckets, SORT_VAR_NO_TYPE, list(target_time), SORT_FIRST_INDEX, COMPARE_KEY) + +/datum/controller/subsystem/movement/proc/dequeue_loop(datum/move_loop/loop) + var/list/our_entries = buckets["[loop.timer]"] + our_entries -= loop + if(!our_entries) + smash_bucket(bucket_time = loop.timer) // We can't pass an index in for context because we don't know our position + /datum/controller/subsystem/movement/proc/add_loop(datum/move_loop/add) - processing += add add.start_loop() + if(QDELETED(add)) + return + queue_loop(add) /datum/controller/subsystem/movement/proc/remove_loop(datum/move_loop/remove) - processing -= remove - currentrun -= remove + dequeue_loop(remove) remove.stop_loop() diff --git a/code/controllers/subsystem/movement/movement_types.dm b/code/controllers/subsystem/movement/movement_types.dm index 7ce34591133..d1ea92376fd 100644 --- a/code/controllers/subsystem/movement/movement_types.dm +++ b/code/controllers/subsystem/movement/movement_types.dm @@ -20,6 +20,8 @@ ///Delay between each move in deci-seconds var/delay = 1 ///The next time we should process + ///Used primarially as a hint to be reasoned about by our [controller], and as the id of our bucket + ///Should not be modified directly outside of [start_loop] var/timer = 0 ///Is this loop running or not var/running = FALSE @@ -71,10 +73,9 @@ extra_info = null return ..() -///Exists as a helper so outside code can modify delay while also modifying timer +///Exists as a helper so outside code can modify delay in a sane way /datum/move_loop/proc/set_delay(new_delay) delay = max(new_delay, world.tick_lag) - timer = world.time + delay /datum/move_loop/process() var/old_delay = delay //The signal can sometimes change delay @@ -93,7 +94,6 @@ SEND_SIGNAL(src, COMSIG_MOVELOOP_POSTPROCESS, success, delay * visual_delay) - timer = world.time + delay if(QDELETED(src) || !success) //Can happen return @@ -399,7 +399,6 @@ /datum/move_loop/has_target/dist_bound/move() if(!check_dist()) //If we're too close don't do the move - timer = world.time //Make sure to move as soon as possible return FALSE return TRUE diff --git a/code/controllers/subsystem/processing/singulo.dm b/code/controllers/subsystem/processing/singulo.dm new file mode 100644 index 00000000000..882d8e70f1a --- /dev/null +++ b/code/controllers/subsystem/processing/singulo.dm @@ -0,0 +1,6 @@ +/// Very rare subsystem, provides any active singularities with the timings and seclusion they need to succeed +PROCESSING_SUBSYSTEM_DEF(singuloprocess) + name = "Singularity" + wait = 0.5 + priority = FIRE_PRIORITY_DEFAULT + stat_tag = "SIN" diff --git a/code/datums/components/singularity.dm b/code/datums/components/singularity.dm index 96fc734b7d8..04d5690d733 100644 --- a/code/datums/components/singularity.dm +++ b/code/datums/components/singularity.dm @@ -40,7 +40,11 @@ /// If specified, the singularity will slowly move to this target var/atom/target + /// List of turfs we have yet to consume, but need to + var/list/turf/turfs_to_consume = list() + /// The time that has elapsed since our last move/eat call + var/time_since_last_eat /datum/component/singularity/Initialize( bsa_targetable = TRUE, @@ -65,7 +69,7 @@ src.singularity_size = singularity_size /datum/component/singularity/RegisterWithParent() - START_PROCESSING(SSdcs, src) + START_PROCESSING(SSsinguloprocess, src) // The singularity stops drifting for no man! parent.AddElement(/datum/element/forced_gravity, FALSE) @@ -104,7 +108,7 @@ return ..() /datum/component/singularity/UnregisterFromParent() - STOP_PROCESSING(SSdcs, src) + STOP_PROCESSING(SSsinguloprocess, src) parent.RemoveElement(/datum/element/bsa_blocker) parent.RemoveElement(/datum/element/forced_gravity) @@ -122,9 +126,17 @@ )) /datum/component/singularity/process(delta_time) - if (roaming) - move() - eat() + // We want to move and eat once a second, but want to process our turf consume queue the rest of the time + time_since_last_eat += delta_time + digest() + if(TICK_CHECK) + return + if(time_since_last_eat > 1) // Delta time is in seconds for "reasons" + time_since_last_eat = 0 + if (roaming) + move() + eat() + digest() // Try and process as much as you can with the time we have left /datum/component/singularity/proc/block_blob() SIGNAL_HANDLER @@ -166,33 +178,50 @@ thing.singularity_act(singularity_size, parent) /datum/component/singularity/proc/eat() + turfs_to_consume |= spiral_range_turfs(grav_pull, parent) + +/datum/component/singularity/proc/digest() var/atom/atom_parent = parent - for (var/_tile in spiral_range_turfs(grav_pull, parent)) - var/turf/tile = _tile - if (!tile || !isturf(atom_parent.loc)) + if(!isturf(atom_parent.loc)) + return + + // We use a static index for this to prevent infinite runtimes. + // Maybe a might overengineered, but let's be safe yes? + var/static/cached_index = 0 + if(cached_index) + var/old_index = cached_index + cached_index = 0 // Prevents infinite Cut() runtimes. Sorry MSO + turfs_to_consume.Cut(1, old_index + 1) + + for (cached_index in 1 to length(turfs_to_consume)) + var/turf/tile = turfs_to_consume[cached_index] + var/dist_to_tile = get_dist(tile, parent) + + if(grav_pull < dist_to_tile) //If we've exited the singulo's range already, just skip us continue - if (get_dist(tile, parent) > consume_range) - tile.singularity_pull(src, singularity_size) - else + + var/in_consume_range = (dist_to_tile <= consume_range) + if (in_consume_range) consume(src, tile) + else + tile.singularity_pull(parent, singularity_size) - for (var/_thing in tile) - var/atom/thing = _thing - - // Because we can possibly yield in the middle of iteration, let's make sure what were looking at is still there - // Without this, you get "Qdeleted thing being thrown around" - if (QDELETED(thing)) + for (var/atom/movable/thing as anything in tile) + if(thing == parent) continue + if (in_consume_range) + consume(src, thing) + else + thing.singularity_pull(parent, singularity_size) - if (isturf(atom_parent.loc) && thing != parent) - var/atom/movable/movable_thing = thing - if (get_dist(movable_thing, parent) > consume_range) - movable_thing.singularity_pull(parent, singularity_size) - else - consume(src, movable_thing) + if(TICK_CHECK) //Yes this means the singulo can eat all of its host subsystem's cpu, but like it's the singulo, and it was gonna do that anyway + turfs_to_consume.Cut(1, cached_index + 1) + cached_index = 0 + return - CHECK_TICK + turfs_to_consume.Cut() + cached_index = 0 /datum/component/singularity/proc/move() var/drifting_dir = pick(GLOB.alldirs - last_failed_movement) diff --git a/code/modules/power/singularity/singularity.dm b/code/modules/power/singularity/singularity.dm index 77669d05dd5..feb309ce0cf 100644 --- a/code/modules/power/singularity/singularity.dm +++ b/code/modules/power/singularity/singularity.dm @@ -36,6 +36,8 @@ var/move_self = TRUE ///If the singularity has eaten a supermatter shard and can go to stage six var/consumed_supermatter = FALSE + /// How long it's been since the singulo last acted, in seconds + var/time_since_act = 0 flags_1 = SUPERMATTER_IGNORES_1 resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF @@ -46,7 +48,7 @@ energy = starting_energy - START_PROCESSING(SSobj, src) + START_PROCESSING(SSsinguloprocess, src) SSpoints_of_interest.make_point_of_interest(src) var/datum/component/singularity/new_component = AddComponent( @@ -76,7 +78,7 @@ /obj/singularity/Destroy() - STOP_PROCESSING(SSobj, src) + STOP_PROCESSING(SSsinguloprocess, src) return ..() /obj/singularity/attack_tk(mob/user) @@ -146,6 +148,10 @@ energy -= round(((energy + 1) / 4), 1) /obj/singularity/process(delta_time) + time_since_act += delta_time + if(time_since_act < 2) + return + time_since_act = 0 if(current_size >= STAGE_TWO) if(prob(event_chance)) event() diff --git a/tgstation.dme b/tgstation.dme index e7bf67ab420..7e99466b6e1 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -515,6 +515,7 @@ #include "code\controllers\subsystem\processing\projectiles.dm" #include "code\controllers\subsystem\processing\quirks.dm" #include "code\controllers\subsystem\processing\reagents.dm" +#include "code\controllers\subsystem\processing\singulo.dm" #include "code\controllers\subsystem\processing\station.dm" #include "code\controllers\subsystem\processing\tramprocess.dm" #include "code\controllers\subsystem\processing\wet_floors.dm"