diff --git a/code/controllers/subsystem/runechat.dm b/code/controllers/subsystem/runechat.dm index 8e3b22921ca..9f441f60fe4 100644 --- a/code/controllers/subsystem/runechat.dm +++ b/code/controllers/subsystem/runechat.dm @@ -1,238 +1,3 @@ -/// Controls how many buckets should be kept, each representing a tick. (30 seconds worth) -#define BUCKET_LEN (world.fps * 1 * 30) -/// Helper for getting the correct bucket for a given chatmessage -#define BUCKET_POS(scheduled_destruction) (((round((scheduled_destruction - SSrunechat.head_offset) / world.tick_lag) + 1) % BUCKET_LEN) || BUCKET_LEN) -/// Gets the maximum time at which messages will be handled in buckets, used for deferring to secondary queue -#define BUCKET_LIMIT (SSrunechat.head_offset + TICKS2DS(BUCKET_LEN + SSrunechat.practical_offset - 1)) - -/** - * # Runechat Subsystem - * - * Maintains a timer-like system to handle destruction of runechat messages. Much of this code is modeled - * after or adapted from the timer subsystem. - * - * Note that this has the same structure for storing and queueing messages as the timer subsystem does - * for handling timers: the bucket_list is a list of chatmessage datums, each of which are the head - * of a circularly linked list. Any given index in bucket_list could be null, representing an empty bucket. - */ -SUBSYSTEM_DEF(runechat) +TIMER_SUBSYSTEM_DEF(runechat) name = "Runechat" - flags = SS_TICKER | SS_NO_INIT - wait = 1 priority = FIRE_PRIORITY_RUNECHAT - - /// world.time of the first entry in the bucket list, effectively the 'start time' of the current buckets - var/head_offset = 0 - /// Index of the first non-empty bucket - var/practical_offset = 1 - /// world.tick_lag the bucket was designed for - var/bucket_resolution = 0 - /// How many messages are in the buckets - var/bucket_count = 0 - /// List of buckets, each bucket holds every message that has to be killed that byond tick - var/list/bucket_list = list() - /// Queue used for storing messages that are scheduled for deletion too far in the future for the buckets - var/list/datum/chatmessage/second_queue = list() - -/datum/controller/subsystem/runechat/PreInit() - bucket_list.len = BUCKET_LEN - head_offset = world.time - bucket_resolution = world.tick_lag - -/datum/controller/subsystem/runechat/stat_entry(msg) - msg = "ActMsgs:[bucket_count] SecQueue:[length(second_queue)]" - return msg - -/datum/controller/subsystem/runechat/fire(resumed = FALSE) - // Store local references to datum vars as it is faster to access them this way - var/list/bucket_list = src.bucket_list - - if (MC_TICK_CHECK) - return - - - // Check for when we need to loop the buckets, this occurs when - // the head_offset is approaching BUCKET_LEN ticks in the past - if (practical_offset > BUCKET_LEN) - head_offset += TICKS2DS(BUCKET_LEN) - practical_offset = 1 - resumed = FALSE - - // Check for when we have to reset buckets, typically from auto-reset - if ((length(bucket_list) != BUCKET_LEN) || (world.tick_lag != bucket_resolution)) - reset_buckets() - bucket_list = src.bucket_list - resumed = FALSE - // Store a reference to the 'working' chatmessage so that we can resume if the MC - // has us stop mid-way through processing - var/static/datum/chatmessage/cm - if (!resumed) - cm = null - - // Iterate through each bucket starting from the practical offset - while (practical_offset <= BUCKET_LEN && head_offset + ((practical_offset - 1) * world.tick_lag) <= world.time) - var/datum/chatmessage/bucket_head = bucket_list[practical_offset] - if (!cm || !bucket_head || cm == bucket_head) - bucket_head = bucket_list[practical_offset] - cm = bucket_head - - while (cm) - // If the chatmessage hasn't yet had its life ended then do that now - var/datum/chatmessage/next = cm.next - if (!cm.eol_complete) - cm.end_of_life() - else if (!QDELETED(cm)) // otherwise if we haven't deleted it yet, do so (this is after EOL completion) - qdel(cm) - - if (MC_TICK_CHECK) - return - - // Break once we've processed the entire bucket - cm = next - if (cm == bucket_head) - break - - // Empty the bucket, check if anything in the secondary queue should be shifted to this bucket - bucket_list[practical_offset++] = null - var/i = 0 - for (i in 1 to length(second_queue)) - cm = second_queue[i] - if (cm.scheduled_destruction >= BUCKET_LIMIT) - i-- - break - - // Transfer the message into the bucket, performing necessary circular doubly-linked list operations - bucket_count++ - var/bucket_pos = max(1, BUCKET_POS(cm.scheduled_destruction)) - var/datum/timedevent/head = bucket_list[bucket_pos] - if (!head) - bucket_list[bucket_pos] = cm - cm.next = null - cm.prev = null - continue - - if (!head.prev) - head.prev = head - cm.next = head - cm.prev = head.prev - cm.next.prev = cm - cm.prev.next = cm - if (i) - second_queue.Cut(1, i + 1) - cm = null - -/datum/controller/subsystem/runechat/Recover() - bucket_list |= SSrunechat.bucket_list - second_queue |= SSrunechat.second_queue - -/datum/controller/subsystem/runechat/proc/reset_buckets() - bucket_list.len = BUCKET_LEN - head_offset = world.time - bucket_resolution = world.tick_lag - -/** - * Enters the runechat subsystem with this chatmessage, inserting it into the end-of-life queue - * - * This will also account for a chatmessage already being registered, and in which case - * the position will be updated to remove it from the previous location if necessary - * - * Arguments: - * * new_sched_destruction Optional, when provided is used to update an existing message with the new specified time - */ -/datum/chatmessage/proc/enter_subsystem(new_sched_destruction = 0) - // Get local references from subsystem as they are faster to access than the datum references - var/list/bucket_list = SSrunechat.bucket_list - var/list/second_queue = SSrunechat.second_queue - - // When necessary, de-list the chatmessage from its previous position - if (new_sched_destruction) - if (scheduled_destruction >= BUCKET_LIMIT) - second_queue -= src - else - SSrunechat.bucket_count-- - var/bucket_pos = BUCKET_POS(scheduled_destruction) - if (bucket_pos > 0) - var/datum/chatmessage/bucket_head = bucket_list[bucket_pos] - if (bucket_head == src) - bucket_list[bucket_pos] = next - if (prev != next) - prev.next = next - next.prev = prev - else - prev?.next = null - next?.prev = null - prev = next = null - scheduled_destruction = new_sched_destruction - - // Ensure the scheduled destruction time is properly bound to avoid missing a scheduled event - scheduled_destruction = max(CEILING(scheduled_destruction, world.tick_lag), world.time + world.tick_lag) - - // Handle insertion into the secondary queue if the required time is outside our tracked amounts - if (scheduled_destruction >= BUCKET_LIMIT) - BINARY_INSERT(src, SSrunechat.second_queue, /datum/chatmessage, src, scheduled_destruction, COMPARE_KEY) - return - - // Get bucket position and a local reference to the datum var, it's faster to access this way - var/bucket_pos = BUCKET_POS(scheduled_destruction) - - // Get the bucket head for that bucket, increment the bucket count - var/datum/chatmessage/bucket_head = bucket_list[bucket_pos] - SSrunechat.bucket_count++ - - // If there is no existing head of this bucket, we can set this message to be that head - if (!bucket_head) - bucket_list[bucket_pos] = src - return - - // Otherwise it's a simple insertion into the circularly doubly-linked list - if (!bucket_head.prev) - bucket_head.prev = bucket_head - next = bucket_head - prev = bucket_head.prev - next.prev = src - prev.next = src - - -/** - * Removes this chatmessage datum from the runechat subsystem - */ -/datum/chatmessage/proc/leave_subsystem() - // Attempt to find the bucket that contains this chat message - var/bucket_pos = BUCKET_POS(scheduled_destruction) - - // Get local references to the subsystem's vars, faster than accessing on the datum - var/list/bucket_list = SSrunechat.bucket_list - var/list/second_queue = SSrunechat.second_queue - - // Attempt to get the head of the bucket - var/datum/chatmessage/bucket_head - if (bucket_pos > 0) - bucket_head = bucket_list[bucket_pos] - - // Decrement the number of messages in buckets if the message is - // the head of the bucket, or has a SD less than BUCKET_LIMIT implying it fits - // into an existing bucket, or is otherwise not present in the secondary queue - if(bucket_head == src) - bucket_list[bucket_pos] = next - SSrunechat.bucket_count-- - else if(scheduled_destruction < BUCKET_LIMIT) - SSrunechat.bucket_count-- - else - var/l = length(second_queue) - second_queue -= src - if(l == length(second_queue)) - SSrunechat.bucket_count-- - - // Remove the message from the bucket, ensuring to maintain - // the integrity of the bucket's list if relevant - if(prev != next) - prev.next = next - next.prev = prev - else - prev?.next = null - next?.prev = null - prev = next = null - -#undef BUCKET_LEN -#undef BUCKET_POS -#undef BUCKET_LIMIT diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm index b7b7bd1305b..599c4cd6d7f 100644 --- a/code/datums/chatmessage.dm +++ b/code/datums/chatmessage.dm @@ -50,6 +50,10 @@ var/datum/chatmessage/prev /// The current index used for adjusting the layer of each sequential chat message such that recent messages will overlay older ones var/static/current_z_idx = 0 + /// Contains ID of assigned timer for end_of_life fading event + var/fadertimer = null + /// States if end_of_life is being executed + var/isFading = FALSE /** * Constructs a chat message overlay @@ -80,7 +84,6 @@ owned_by = null message_loc = null message = null - leave_subsystem() return ..() /** @@ -88,7 +91,6 @@ */ /datum/chatmessage/proc/on_parent_qdel() SIGNAL_HANDLER - qdel(src) /** @@ -180,11 +182,15 @@ combined_height += m.approx_lines // When choosing to update the remaining time we have to be careful not to update the - // scheduled time once the EOL completion time has been set. - var/sched_remaining = m.scheduled_destruction - world.time - if (!m.eol_complete) + // scheduled time once the EOL has been executed. + if (!m.isFading) + var/sched_remaining = timeleft(m.fadertimer, SSrunechat) var/remaining_time = (sched_remaining) * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height) - m.enter_subsystem(world.time + remaining_time) // push updated time to runechat SS + if (remaining_time) + deltimer(m.fadertimer, SSrunechat) + m.fadertimer = addtimer(CALLBACK(m, .proc/end_of_life), remaining_time, TIMER_STOPPABLE|TIMER_DELETE_ME, SSrunechat) + else + m.end_of_life() // Reset z index if relevant if (current_z_idx >= CHAT_LAYER_MAX_Z) @@ -208,20 +214,20 @@ animate(message, alpha = 255, time = CHAT_MESSAGE_SPAWN_TIME) // Register with the runechat SS to handle EOL and destruction - scheduled_destruction = world.time + (lifespan - CHAT_MESSAGE_EOL_FADE) - enter_subsystem() + var/duration = lifespan - CHAT_MESSAGE_EOL_FADE + fadertimer = addtimer(CALLBACK(src, .proc/end_of_life), duration, TIMER_STOPPABLE|TIMER_DELETE_ME, SSrunechat) /** * Applies final animations to overlay CHAT_MESSAGE_EOL_FADE deciseconds prior to message deletion, - * sets time for scheduling deletion and re-enters the runechat SS for qdeling + * sets timer for scheduling deletion * * Arguments: * * fadetime - The amount of time to animate the message's fadeout for */ /datum/chatmessage/proc/end_of_life(fadetime = CHAT_MESSAGE_EOL_FADE) - eol_complete = scheduled_destruction + fadetime + isFading = TRUE animate(message, alpha = 0, time = fadetime, flags = ANIMATION_PARALLEL) - enter_subsystem(eol_complete) // re-enter the runechat SS with the EOL completion time to QDEL self + addtimer(CALLBACK(GLOBAL_PROC, /proc/qdel, src), fadetime, TIMER_DELETE_ME, SSrunechat) /** * Creates a message overlay at a defined location for a given speaker