mirror of
https://github.com/SPLURT-Station/S.P.L.U.R.T-Station-13.git
synced 2025-12-10 09:54:52 +00:00
407 lines
11 KiB
Plaintext
407 lines
11 KiB
Plaintext
#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)
|
|
#define TIMER_ID_MAX (2**24) //max float with integer precision
|
|
|
|
SUBSYSTEM_DEF(timer)
|
|
name = "Timer"
|
|
wait = 1 //SS_TICKER subsystem, so wait is in ticks
|
|
init_order = INIT_ORDER_TIMER
|
|
|
|
flags_1 = SS_TICKER|SS_NO_INIT
|
|
|
|
var/list/datum/timedevent/processing = list()
|
|
var/list/hashes = list()
|
|
|
|
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() //list of buckets, each bucket holds every timer that has to run that byond tick.
|
|
|
|
var/list/timer_id_dict = list() //list of all active timers assoicated to their timer id (for easy lookup)
|
|
|
|
var/list/clienttime_timers = list() //special snowflake timers that run on fancy pansy "client time"
|
|
|
|
var/last_invoke_tick = 0
|
|
var/static/last_invoke_warning = 0
|
|
var/static/bucket_auto_reset = TRUE
|
|
|
|
/datum/controller/subsystem/timer/stat_entry(msg)
|
|
..("B:[bucket_count] P:[length(processing)] H:[length(hashes)] C:[length(clienttime_timers)]")
|
|
|
|
/datum/controller/subsystem/timer/fire(resumed = FALSE)
|
|
var/lit = last_invoke_tick
|
|
var/last_check = world.time - TIMER_NO_INVOKE_WARNING
|
|
var/list/bucket_list = src.bucket_list
|
|
if(!bucket_count)
|
|
last_invoke_tick = world.time
|
|
|
|
if(lit && lit < last_check && last_invoke_warning < last_check)
|
|
last_invoke_warning = world.time
|
|
var/msg = "No regular timers processed in the last [TIMER_NO_INVOKE_WARNING] ticks[bucket_auto_reset ? ", resetting buckets" : ""]!"
|
|
message_admins(msg)
|
|
WARNING(msg)
|
|
if(bucket_auto_reset)
|
|
bucket_resolution = 0
|
|
|
|
log_world("Timer bucket reset. world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]")
|
|
for (var/i in 1 to length(bucket_list))
|
|
var/datum/timedevent/bucket_head = bucket_list[i]
|
|
if (!bucket_head)
|
|
continue
|
|
|
|
log_world("Active timers at index [i]:")
|
|
|
|
var/datum/timedevent/bucket_node = bucket_head
|
|
var/anti_loop_check = 1000
|
|
do
|
|
log_world(get_timer_debug_string(bucket_node))
|
|
bucket_node = bucket_node.next
|
|
anti_loop_check--
|
|
while(bucket_node && bucket_node != bucket_head && anti_loop_check)
|
|
log_world("Active timers in the processing queue:")
|
|
for(var/I in processing)
|
|
log_world(get_timer_debug_string(I))
|
|
|
|
while(length(clienttime_timers))
|
|
var/datum/timedevent/ctime_timer = clienttime_timers[clienttime_timers.len]
|
|
if (ctime_timer.timeToRun <= REALTIMEOFDAY)
|
|
--clienttime_timers.len
|
|
var/datum/callback/callBack = ctime_timer.callBack
|
|
ctime_timer.spent = TRUE
|
|
callBack.InvokeAsync()
|
|
qdel(ctime_timer)
|
|
else
|
|
break //None of the rest are ready to run
|
|
if (MC_TICK_CHECK)
|
|
return
|
|
|
|
var/static/list/spent = list()
|
|
var/static/datum/timedevent/timer
|
|
var/static/datum/timedevent/head
|
|
|
|
if (practical_offset > BUCKET_LEN || (!resumed && length(bucket_list) != BUCKET_LEN || world.tick_lag != bucket_resolution))
|
|
shift_buckets()
|
|
bucket_list = src.bucket_list
|
|
resumed = FALSE
|
|
|
|
|
|
if (!resumed)
|
|
timer = null
|
|
head = null
|
|
|
|
while (practical_offset <= BUCKET_LEN && head_offset + (practical_offset*world.tick_lag) <= world.time && !MC_TICK_CHECK)
|
|
if (!timer || !head || timer == head)
|
|
head = bucket_list[practical_offset]
|
|
if (!head)
|
|
practical_offset++
|
|
if (MC_TICK_CHECK)
|
|
break
|
|
continue
|
|
timer = head
|
|
do
|
|
var/datum/callback/callBack = timer.callBack
|
|
if (!callBack)
|
|
qdel(timer)
|
|
bucket_resolution = null //force bucket recreation
|
|
CRASH("Invalid timer: [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]")
|
|
|
|
if (!timer.spent)
|
|
spent += timer
|
|
timer.spent = TRUE
|
|
callBack.InvokeAsync()
|
|
last_invoke_tick = world.time
|
|
|
|
timer = timer.next
|
|
|
|
if (MC_TICK_CHECK)
|
|
return
|
|
while (timer && timer != head)
|
|
timer = null
|
|
bucket_list[practical_offset++] = null
|
|
if (MC_TICK_CHECK)
|
|
return
|
|
|
|
bucket_count -= length(spent)
|
|
|
|
for (var/spent_timer in spent)
|
|
qdel(spent_timer)
|
|
|
|
spent.len = 0
|
|
|
|
//formated this way to be runtime resistant
|
|
/datum/controller/subsystem/timer/proc/get_timer_debug_string(datum/timedevent/TE)
|
|
. = "Timer: [TE]"
|
|
. += "Prev: [TE.prev ? TE.prev : "NULL"], Next: [TE.next ? TE.next : "NULL"]"
|
|
if(TE.spent)
|
|
. += ", SPENT"
|
|
if(QDELETED(TE))
|
|
. += ", QDELETED"
|
|
|
|
/datum/controller/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.len = 0
|
|
bucket_list.len = BUCKET_LEN
|
|
|
|
practical_offset = 1
|
|
bucket_count = 0
|
|
head_offset = world.time
|
|
bucket_resolution = world.tick_lag
|
|
|
|
alltimers += processing
|
|
if (!length(alltimers))
|
|
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/controller/subsystem/timer/Recover()
|
|
processing |= SStimer.processing
|
|
hashes |= SStimer.hashes
|
|
timer_id_dict |= SStimer.timer_id_dict
|
|
bucket_list |= SStimer.bucket_list
|
|
|
|
/datum/timedevent
|
|
var/id
|
|
var/datum/callback/callBack
|
|
var/timeToRun
|
|
var/hash
|
|
var/list/flags_1
|
|
var/spent = FALSE //set to true right before running.
|
|
var/name //for easy debugging.
|
|
//cicular doublely linked list
|
|
var/datum/timedevent/next
|
|
var/datum/timedevent/prev
|
|
|
|
var/static/nextid = 1
|
|
|
|
/datum/timedevent/New(datum/callback/callBack, timeToRun, flags_1, hash)
|
|
id = TIMER_ID_NULL
|
|
src.callBack = callBack
|
|
src.timeToRun = timeToRun
|
|
src.flags_1 = flags_1
|
|
src.hash = hash
|
|
|
|
if (flags_1 & TIMER_UNIQUE)
|
|
SStimer.hashes[hash] = src
|
|
if (flags_1 & TIMER_STOPPABLE)
|
|
do
|
|
if (nextid >= TIMER_ID_MAX)
|
|
nextid = 1
|
|
id = nextid++
|
|
while(SStimer.timer_id_dict["timerid" + num2text(id, 8)])
|
|
SStimer.timer_id_dict["timerid" + num2text(id, 8)] = src
|
|
|
|
name = "Timer: " + num2text(id, 8) + ", TTR: [timeToRun], Flags: [jointext(bitfield2list(flags_1, list("TIMER_UNIQUE", "TIMER_OVERRIDE", "TIMER_CLIENT_TIME", "TIMER_STOPPABLE", "TIMER_NO_HASH_WAIT")), ", ")], callBack: \ref[callBack], callBack.object: [callBack.object]\ref[callBack.object]([getcallingtype()]), callBack.delegate:[callBack.delegate]([callBack.arguments ? callBack.arguments.Join(", ") : ""])"
|
|
|
|
if (callBack.object != GLOBAL_PROC)
|
|
LAZYADD(callBack.object.active_timers, src)
|
|
|
|
if (flags_1 & TIMER_CLIENT_TIME)
|
|
//sorted insert
|
|
var/list/ctts = SStimer.clienttime_timers
|
|
var/cttl = length(ctts)
|
|
if(cttl)
|
|
var/datum/timedevent/Last = ctts[cttl]
|
|
if(Last.timeToRun >= timeToRun)
|
|
ctts += src
|
|
else if(cttl > 1)
|
|
for(var/I in cttl to 1)
|
|
var/datum/timedevent/E = ctts[I]
|
|
if(E.timeToRun <= timeToRun)
|
|
ctts.Insert(src, I)
|
|
break
|
|
else
|
|
ctts += 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()
|
|
..()
|
|
if (flags_1 & TIMER_UNIQUE)
|
|
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_1 & TIMER_STOPPABLE)
|
|
SStimer.timer_id_dict -= "timerid" + num2text(id, 8)
|
|
|
|
if (flags_1 & 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
|
|
|
|
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 = next
|
|
if (next && next.prev == src)
|
|
next.prev = prev
|
|
next = null
|
|
prev = null
|
|
return QDEL_HINT_IWILLGC
|
|
|
|
/datum/timedevent/proc/getcallingtype()
|
|
. = "ERROR"
|
|
if (callBack.object == GLOBAL_PROC)
|
|
. = "GLOBAL_PROC"
|
|
else
|
|
. = "[callBack.object.type]"
|
|
|
|
/proc/addtimer(datum/callback/callback, wait, flags_1)
|
|
if (!callback)
|
|
return
|
|
|
|
wait = max(wait, 0)
|
|
|
|
var/hash
|
|
|
|
if (flags_1 & TIMER_UNIQUE)
|
|
var/list/hashlist
|
|
if(flags_1 & TIMER_NO_HASH_WAIT)
|
|
hashlist = list(callback.object, "(\ref[callback.object])", callback.delegate, flags_1 & TIMER_CLIENT_TIME)
|
|
else
|
|
hashlist = list(callback.object, "(\ref[callback.object])", callback.delegate, wait, flags_1 & TIMER_CLIENT_TIME)
|
|
hashlist += callback.arguments
|
|
hash = hashlist.Join("|||||||")
|
|
|
|
var/datum/timedevent/hash_timer = SStimer.hashes[hash]
|
|
if(hash_timer)
|
|
if (hash_timer.spent) //it's pending deletion, pretend it doesn't exist.
|
|
hash_timer.hash = null
|
|
SStimer.hashes -= hash
|
|
else
|
|
|
|
if (flags_1 & TIMER_OVERRIDE)
|
|
qdel(hash_timer)
|
|
else
|
|
if (hash_timer.flags_1 & TIMER_STOPPABLE)
|
|
. = hash_timer.id
|
|
return
|
|
|
|
|
|
var/timeToRun = world.time + wait
|
|
if (flags_1 & TIMER_CLIENT_TIME)
|
|
timeToRun = REALTIMEOFDAY + wait
|
|
|
|
var/datum/timedevent/timer = new(callback, timeToRun, flags_1, hash)
|
|
return timer.id
|
|
|
|
/proc/deltimer(id)
|
|
if (!id)
|
|
return FALSE
|
|
if (id == TIMER_ID_NULL)
|
|
CRASH("Tried to delete a null timerid. Use TIMER_STOPPABLE flag")
|
|
if (!istext(id))
|
|
if (istype(id, /datum/timedevent))
|
|
qdel(id)
|
|
return TRUE
|
|
//id is string
|
|
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
|