diff --git a/code/__DEFINES/MC.dm b/code/__DEFINES/MC.dm index 2ea7258816b..87374b6672c 100644 --- a/code/__DEFINES/MC.dm +++ b/code/__DEFINES/MC.dm @@ -45,5 +45,12 @@ #define SS_POST_FIRE_TIMING 128 //Timing subsystem -#define TIMER_NORMAL "normal" -#define TIMER_UNIQUE "unique" \ No newline at end of file +//Don't run if there is an identical unique timer active +#define TIMER_UNIQUE 0x1 +//For unique timers: Replace the old timer rather then not start this one +#define TIMER_OVERRIDE 0x2 +//Timing should be based on how timing progresses on clients, not the sever. +// tracking this is more expensive, +// should only be used in conjuction with things that have to progress client side, such as animate() or sound() +#define TIMER_CLIENT_TIME 0x4 + diff --git a/code/__DEFINES/math.dm b/code/__DEFINES/math.dm index d454c0a0676..4726d210012 100644 --- a/code/__DEFINES/math.dm +++ b/code/__DEFINES/math.dm @@ -17,3 +17,8 @@ #define TICK_USAGE_TO_MS(starting_tickusage) (TICK_DELTA_TO_MS(world.tick_usage-starting_tickusage)) #define PERCENT(val) (round(val*100, 0.1)) + +//time of day but automatically adjusts to the server going into the next day within the same round. +//for when you need a reliable time number that doesn't depend on byond time. +#define REALTIMEOFDAY (world.timeofday + (MIDNIGHT_ROLLOVER * MIDNIGHT_ROLLOVER_CHECK)) +#define MIDNIGHT_ROLLOVER_CHECK ( rollovercheck_last_timeofday != world.timeofday ? update_midnight_rollover() : midnight_rollovers ) diff --git a/code/__HELPERS/cmp.dm b/code/__HELPERS/cmp.dm index 1cad2a5e5e4..f963f09a35c 100644 --- a/code/__HELPERS/cmp.dm +++ b/code/__HELPERS/cmp.dm @@ -40,8 +40,12 @@ var/cmp_field = "name" /proc/cmp_subsystem_priority(datum/subsystem/a, datum/subsystem/b) return a.priority - b.priority +/proc/cmp_timer(datum/timedevent/a, datum/timedevent/b) + return a.timeToRun - b.timeToRun + /proc/cmp_clientcolour_priority(datum/client_colour/A, datum/client_colour/B) return B.priority - A.priority /proc/cmp_clockscripture_priority(datum/clockwork_scripture/A, datum/clockwork_scripture/B) - return initial(A.sort_priority) - initial(B.sort_priority) \ No newline at end of file + return initial(A.sort_priority) - initial(B.sort_priority) + diff --git a/code/__HELPERS/time.dm b/code/__HELPERS/time.dm index 8d000ce6a57..22ff1c2bef9 100644 --- a/code/__HELPERS/time.dm +++ b/code/__HELPERS/time.dm @@ -22,4 +22,13 @@ //returns timestamp in a sql and ISO 8601 friendly format /proc/SQLtime() - return time2text(world.realtime, "YYYY-MM-DD hh:mm:ss") \ No newline at end of file + return time2text(world.realtime, "YYYY-MM-DD hh:mm:ss") + + +/var/midnight_rollovers = 0 +/var/rollovercheck_last_timeofday = 0 +/proc/update_midnight_rollover() + if (world.timeofday < rollovercheck_last_timeofday) //TIME IS GOING BACKWARDS! + return midnight_rollovers++ + return midnight_rollovers + diff --git a/code/controllers/subsystem/garbage.dm b/code/controllers/subsystem/garbage.dm index 73de5173c60..ece8d7c5678 100644 --- a/code/controllers/subsystem/garbage.dm +++ b/code/controllers/subsystem/garbage.dm @@ -170,7 +170,9 @@ var/datum/subsystem/garbage_collector/SSgarbage switch(hint) if (QDEL_HINT_QUEUE) //qdel should queue the object for deletion. SSgarbage.QueueForQueuing(D) - if (QDEL_HINT_LETMELIVE, QDEL_HINT_IWILLGC) //qdel should let the object live after calling destory. + if (QDEL_HINT_IWILLGC) + return + if (QDEL_HINT_LETMELIVE) //qdel should let the object live after calling destory. if(!force) D.gc_destroyed = null //clear the gc variable (important!) return @@ -224,6 +226,13 @@ var/datum/subsystem/garbage_collector/SSgarbage // Return the appropriate QDEL_HINT; in most cases this is QDEL_HINT_QUEUE. /datum/proc/Destroy(force=FALSE) tag = null + var/list/timers = active_timers + active_timers = null + for(var/thing in timers) + var/datum/timedevent/timer = thing + if (timer.spent) + continue + qdel(timer) return QDEL_HINT_QUEUE /datum/var/gc_destroyed //Time when this object was destroyed. diff --git a/code/controllers/subsystem/timer.dm b/code/controllers/subsystem/timer.dm index 11edde6a064..d8b283b0009 100644 --- a/code/controllers/subsystem/timer.dm +++ b/code/controllers/subsystem/timer.dm @@ -1,90 +1,334 @@ +#define BUCKET_LEN (world.fps*1*60) //how many ticks should we keep in the bucket. (1 minutes worth) +#define BUCKET_POS(timer) (round((timer.timeToRun - SStimer.head_offset) / world.tick_lag) + 1) var/datum/subsystem/timer/SStimer /datum/subsystem/timer name = "Timer" - wait = 2 //SS_TICKER subsystem, so wait is in ticks + wait = 1 //SS_TICKER subsystem, so wait is in ticks init_order = 1 display_order = 3 - can_fire = 0 //start disabled - flags = SS_FIRE_IN_LOBBY|SS_TICKER|SS_POST_FIRE_TIMING|SS_NO_INIT + + flags = SS_FIRE_IN_LOBBY|SS_TICKER|SS_NO_INIT var/list/datum/timedevent/processing var/list/hashes + var/head_offset = 0 //world.time of the first entry in the the bucket. + var/practical_offset = 0 //index of the first non-empty item in the bucket. + var/bucket_resolution = 0 //world.tick_lag the bucket was designed for + var/bucket_count = 0 //how many timers are in the buckets + + var/list/bucket_list //list of buckets, each bucket holds every timer that has to run that byond tick. + + var/list/timer_id_dict //list of all active timers assoicated to their timer id (for easy lookup) + + var/list/clienttime_timers //special snowflake timers that run on fancy pansy "client time" + /datum/subsystem/timer/New() processing = list() hashes = list() + + timer_id_dict = list() + + clienttime_timers = list() + NEW_SS_GLOBAL(SStimer) /datum/subsystem/timer/stat_entry(msg) - ..("P:[processing.len]") + ..("B:[bucket_count] P:[length(processing)] H:[length(hashes)] C:[length(clienttime_timers)]") /datum/subsystem/timer/fire() - if(!processing.len) - can_fire = 0 //nothing to do, lets stop firing. + if (length(clienttime_timers)) + for (var/thing in clienttime_timers) + var/datum/timedevent/timer = thing + if (timer.spent) + qdel(timer) + continue + if (timer.timeToRun <= REALTIMEOFDAY) + var/datum/callback/callBack = timer.callBack + timer.spent = TRUE + callBack.InvokeAsync() + qdel(timer) + + if (MC_TICK_CHECK) + return + + if (head_offset + (world.tick_lag * BUCKET_LEN) < world.time || length(src.bucket_list) != BUCKET_LEN || world.tick_lag != bucket_resolution) + shift_buckets() + + var/list/bucket_list = src.bucket_list + var/static/list/spent = list() + LOOP_OUTER + while (practical_offset <= BUCKET_LEN && head_offset + (practical_offset*world.tick_lag) <= world.time && !MC_TICK_CHECK) + var/datum/timedevent/head = bucket_list[practical_offset] + if (!head) + practical_offset++ + if (MC_TICK_CHECK) + break + continue + var/datum/timedevent/timer = head + do + var/datum/callback/callBack = timer.callBack + if (!callBack || timer.spent) + qdel(timer) + bucket_resolution = null //force bucket recreation + CRASH("Invalid timer: timer.timeToRun=[timer.timeToRun]||qdeleted(timer)=[qdeleted(timer)]||world.time=[world.time]||head_offset=[head_offset]||practical_offset=[practical_offset]||timer.spent=[timer.spent]") + + spent += timer + timer.spent = TRUE + + callBack.InvokeAsync() + + timer = timer.next + + if (MC_TICK_CHECK) + if (!timer || timer == head) + break + if (head.prev) + head.prev.next = timer + if (timer.prev) + timer.prev.next = head + timer.prev = head.prev + bucket_list[practical_offset] = timer + break LOOP_OUTER + while (timer && timer != head) + + bucket_list[practical_offset++] = null + if (MC_TICK_CHECK) + break + + timer_id_dict -= spent + bucket_count -= length(spent) + for (var/timer in spent) + qdel(timer) + spent.len = 0 + + +/datum/subsystem/timer/proc/shift_buckets() + var/list/bucket_list = src.bucket_list + var/list/alltimers = list() + //collect the timers currently in the bucket + for (var/bucket_head in bucket_list) + if (!bucket_head) + continue + var/datum/timedevent/bucket_node = bucket_head + do + alltimers += bucket_node + bucket_node = bucket_node.next + while(bucket_node && bucket_node != bucket_head) + + + bucket_list = new(BUCKET_LEN) + src.bucket_list = bucket_list //cache update + practical_offset = 1 + bucket_count = 0 + head_offset = world.time + bucket_resolution = world.tick_lag + + alltimers += processing + if (!length(alltimers)) return - for(var/datum/timedevent/event in processing) - if(event.timeToRun <= world.time) - event.callback.InvokeAsync() - qdel(event) - if (MC_TICK_CHECK) - return + + sortTim(alltimers, .proc/cmp_timer) + + var/datum/timedevent/head = alltimers[1] + + if (head.timeToRun < head_offset) + head_offset = head.timeToRun + + var/list/timers_to_remove = list() + + for (var/thing in alltimers) + var/datum/timedevent/timer = thing + if (!timer) + timers_to_remove += timer + continue + + var/bucket_pos = BUCKET_POS(timer) + if (bucket_pos > BUCKET_LEN) + break + + timers_to_remove += timer //remove it from the big list once we are done + if (!timer.callBack || timer.spent) + continue + bucket_count++ + var/datum/timedevent/bucket_head = bucket_list[bucket_pos] + if (!bucket_head) + bucket_list[bucket_pos] = timer + timer.next = null + timer.prev = null + continue + + if (!bucket_head.prev) + bucket_head.prev = bucket_head + timer.next = bucket_head + timer.prev = bucket_head.prev + timer.next.prev = timer + timer.prev.next = timer + + processing = (alltimers - timers_to_remove) + /datum/subsystem/timer/Recover() processing |= SStimer.processing hashes |= SStimer.hashes + timer_id_dict |= SStimer.timer_id_dict + bucket_list |= SStimer.bucket_list +/datum/var/list/active_timers /datum/timedevent - var/datum/callback/callback - var/timeToRun var/id + var/datum/callback/callBack + var/timeToRun var/hash + var/list/flags + var/spent = FALSE //set to true right before running. + + //cicular doublely linked list + var/datum/timedevent/next + var/datum/timedevent/prev + var/static/nextid = 1 -/datum/timedevent/New() +/datum/timedevent/New(datum/callback/callBack, timeToRun, flags, hash) id = nextid++ + src.callBack = callBack + src.timeToRun = timeToRun + src.flags = flags + + if (hash) + src.hash = hash + SStimer.hashes[hash] = src + + SStimer.timer_id_dict["timerid[id]"] = src + + if (callBack.object != GLOBAL_PROC) + LAZYINITLIST(callBack.object.active_timers) + callBack.object.active_timers += src + + if (flags & TIMER_CLIENT_TIME) + SStimer.clienttime_timers += src + return + + //get the list of buckets + var/list/bucket_list = SStimer.bucket_list + //calculate our place in the bucket list + var/bucket_pos = BUCKET_POS(src) + //we are too far aways from needing to run to be in the bucket list, shift_buckets() will handle us. + if (bucket_pos > length(bucket_list)) + SStimer.processing += src + return + //get the bucket for our tick + var/datum/timedevent/bucket_head = bucket_list[bucket_pos] + SStimer.bucket_count++ + //empty bucket, we will just add ourselves + if (!bucket_head) + bucket_list[bucket_pos] = src + if (bucket_pos < SStimer.practical_offset) + SStimer.practical_offset = bucket_pos + return + //other wise, lets do a simplified linked list add. + if (!bucket_head.prev) + bucket_head.prev = bucket_head + next = bucket_head + prev = bucket_head.prev + next.prev = src + prev.next = src + /datum/timedevent/Destroy() - SStimer.processing -= src - SStimer.hashes -= hash + if (hash) + SStimer.hashes -= hash + + + if (callBack && callBack.object && callBack.object != GLOBAL_PROC && callBack.object.active_timers) + callBack.object.active_timers -= src + UNSETEMPTY(callBack.object.active_timers) + + callBack = null + + if (flags & TIMER_CLIENT_TIME) + SStimer.clienttime_timers -= src + return QDEL_HINT_IWILLGC + + if (!spent) + if (prev == next && next) + next.prev = null + prev.next = null + else + if (prev) + prev.next = next + if (next) + next.prev = prev + + SStimer.timer_id_dict -= "timerid[id]" + + var/bucketpos = BUCKET_POS(src) + var/datum/timedevent/buckethead + var/list/bucket_list = SStimer.bucket_list + + if (bucketpos > 0 && bucketpos <= length(bucket_list)) + buckethead = bucket_list[bucketpos] + SStimer.bucket_count-- + else + SStimer.processing -= src + + if (buckethead == src) + bucket_list[bucketpos] = next + else + if (prev && prev.next == src) + prev.next = null + if (next && next.prev == src) + next.prev = null + return QDEL_HINT_IWILLGC -/proc/addtimer(datum/callback/callback, wait, unique = TIMER_NORMAL) + +/proc/addtimer(datum/callback/callback, wait, flags) if (!callback) return - if (!SStimer.can_fire) - SStimer.can_fire = 1 - var/datum/timedevent/event = new() - event.callback = callback - event.timeToRun = world.time + wait - var/list/hashlist = args.Copy() - - hashlist[1] = "[callback.object](\ref[callback.object])" - hashlist.Insert(2, callback.delegate, callback.arguments) - event.hash = jointext(hashlist, null) - - if(unique == TIMER_UNIQUE) - var/datum/timedevent/hash_event = SStimer.hashes[event.hash] - if(hash_event) - return hash_event.id - - SStimer.hashes[event.hash] = event if (wait <= 0) callback.InvokeAsync() - SStimer.hashes -= event.hash return - // If we are unique (or we're not checking that), add the timer and return the id. - SStimer.processing += event + var/hash + + if (flags & TIMER_UNIQUE) + var/list/hashlist = list(callback.object, "(\ref[callback.object])", callback.delegate, wait, flags & TIMER_CLIENT_TIME) + hashlist += callback.arguments + hash = hashlist.Join("|||||||") + + var/datum/timedevent/hash_timer = SStimer.hashes[hash] + if(hash_timer) + if (flags & TIMER_OVERRIDE) + qdel(hash_timer) + else + return hash_timer.id + + var/timeToRun = world.time + wait + if (flags & TIMER_CLIENT_TIME) + timeToRun = REALTIMEOFDAY + wait + + var/datum/timedevent/timer = new(callback, timeToRun, flags, hash) + return timer.id - return event.id /proc/deltimer(id) - for(var/datum/timedevent/event in SStimer.processing) - if(event.id == id) - qdel(event) - return 1 - return 0 + if (!id) + return FALSE + if (!istext(id)) + if (istype(id, /datum/timedevent)) + qdel(id) + return TRUE + var/datum/timedevent/timer = SStimer.timer_id_dict["timerid[id]"] + if (timer && !timer.spent) + qdel(timer) + return TRUE + return FALSE + + +#undef BUCKET_LEN +#undef BUCKET_POS \ No newline at end of file