diff --git a/code/__DEFINES/MC.dm b/code/__DEFINES/MC.dm index 543f7f814a1..0919488f211 100644 --- a/code/__DEFINES/MC.dm +++ b/code/__DEFINES/MC.dm @@ -86,3 +86,11 @@ ss_id="processing_[#X]";\ }\ /datum/controller/subsystem/processing/##X + +#define TIMER_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/timer/##X);\ +/datum/controller/subsystem/timer/##X/New(){\ + NEW_SS_GLOBAL(SS##X);\ + PreInit();\ +}\ +/datum/controller/subsystem/timer/##X/fire() {..() /*just so it shows up on the profiler*/} \ +/datum/controller/subsystem/timer/##X diff --git a/code/__DEFINES/_tick.dm b/code/__DEFINES/_tick.dm index 1ac004b5673..47b88720ba5 100644 --- a/code/__DEFINES/_tick.dm +++ b/code/__DEFINES/_tick.dm @@ -20,6 +20,11 @@ /// runs stoplag if tick_usage is above the limit #define CHECK_TICK ( TICK_CHECK ? stoplag() : 0 ) +/// Checks if a sleeping proc is running before or after the master controller +#define RUNNING_BEFORE_MASTER ( Master.last_run != null && Master.last_run != world.time ) +/// Returns true if a verb ought to yield to the MC (IE: queue up to be processed by a subsystem) +#define VERB_SHOULD_YIELD ( TICK_CHECK || RUNNING_BEFORE_MASTER ) + /// Returns true if tick usage is above 95, for high priority usage #define TICK_CHECK_HIGH_PRIORITY ( TICK_USAGE > 95 ) /// runs stoplag if tick_usage is above 95, for high priority usage diff --git a/code/controllers/subsystem/runechat.dm b/code/controllers/subsystem/runechat.dm index b1dce57c1a2..d4dcd75c051 100644 --- a/code/controllers/subsystem/runechat.dm +++ b/code/controllers/subsystem/runechat.dm @@ -1,234 +1,16 @@ -/// 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. Made by Bobbahbrown of /tg/station13 - * - * 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. - * - * AA Note: - * One of the primary reasons for this is because each chatmessage has a timer attached to it, which is extra load on the GC - * At 150 population, the GC literally cannot keep up with processing 368,000 runechats and 368,000 extra timers in a 1 hour 30 minute round - * This also makes performance profiling a lot easier. - * - */ -SUBSYSTEM_DEF(runechat) +TIMER_SUBSYSTEM_DEF(runechat) name = "Runechat" - flags = SS_TICKER | SS_NO_INIT - wait = 1 priority = FIRE_PRIORITY_RUNECHAT offline_implications = "Runechat messages will no longer clear. Shuttle call recommended." cpu_display = SS_CPUDISPLAY_HIGH - /// 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() + var/list/datum/callback/message_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/get_stat_details() - return "ActMsgs:[bucket_count] SecQueue:[length(second_queue)]" - -/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 - - // 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 - -/** - * 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, scheduled_destruction) - 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 +/datum/controller/subsystem/timer/runechat/fire(resumed) + . = ..() //poggers + while(length(message_queue)) + var/datum/callback/queued_message = message_queue[length(message_queue)] + queued_message.Invoke() + message_queue.len-- + if(MC_TICK_CHECK) + return diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm index 514a261ca13..9bf37826e6e 100644 --- a/code/datums/chatmessage.dm +++ b/code/datums/chatmessage.dm @@ -1,9 +1,11 @@ /// How long the chat message's spawn-in animation will occur for -#define CHAT_MESSAGE_SPAWN_TIME 0.2 SECONDS +#define CHAT_MESSAGE_SPAWN_TIME (0.2 SECONDS) /// How long the chat message will exist prior to any exponential decay -#define CHAT_MESSAGE_LIFESPAN 5 SECONDS +#define CHAT_MESSAGE_LIFESPAN (5 SECONDS) /// How long the chat message's end of life fading animation will occur for -#define CHAT_MESSAGE_EOL_FADE 0.7 SECONDS +#define CHAT_MESSAGE_EOL_FADE (0.7 SECONDS) +/// Grace period for fade before we actually delete the chat message +#define CHAT_MESSAGE_GRACE_PERIOD (0.2 SECONDS) /// Factor of how much the message index (number of messages) will account to exponential decay #define CHAT_MESSAGE_EXP_DECAY 0.7 /// Factor of how much height will account to exponential decay @@ -18,8 +20,13 @@ #define CHAT_LAYER_Z_STEP 0.0001 /// The number of z-layer 'slices' usable by the chat message layering #define CHAT_LAYER_MAX_Z (CHAT_LAYER_MAX - CHAT_LAYER) / CHAT_LAYER_Z_STEP -/// Macro from Lummox used to get height from a MeasureText proc -#define WXH_TO_HEIGHT(x) text2num(copytext(x, findtextEx(x, "x") + 1)) +/// Macro from Lummox used to get height from a MeasureText proc. +/// resolves the MeasureText() return value once, then resolves the height, then sets return_var to that. +#define WXH_TO_HEIGHT(measurement, return_var) \ + do { \ + var/_measurement = measurement; \ + return_var = text2num(copytext(_measurement, findtextEx(_measurement, "x") + 1)); \ + } while(FALSE); /** * # Chat Message Overlay @@ -33,18 +40,14 @@ var/atom/message_loc /// The client who heard this message var/client/owned_by - /// Contains the scheduled destruction time, used for scheduling EOL - var/scheduled_destruction - /// Contains the time that the EOL for the message will be complete, used for qdel scheduling - var/eol_complete /// Contains the approximate amount of lines for height decay var/approx_lines /// 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 the reference to the next chatmessage in the bucket, used by runechat subsystem - var/datum/chatmessage/next - /// Contains the reference to the previous chatmessage in the bucket, used by runechat subsystem - var/datum/chatmessage/prev + /// When we started animating the message + var/animate_start = 0 + /// Our animation lifespan, how long this message will last + var/animate_lifespan = 0 /** * Constructs a chat message overlay @@ -59,7 +62,7 @@ */ /datum/chatmessage/New(text, atom/target, mob/owner, italics, size, lifespan = CHAT_MESSAGE_LIFESPAN, symbol) . = ..() - if (!istype(target)) + if(!istype(target)) CRASH("Invalid target given for chatmessage") if(QDELETED(owner) || !istype(owner) || !owner.client) stack_trace("/datum/chatmessage created with [isnull(owner) ? "null" : "invalid"] mob owner") @@ -68,14 +71,15 @@ INVOKE_ASYNC(src, PROC_REF(generate_image), text, target, owner, lifespan, italics, size, symbol) /datum/chatmessage/Destroy() - if (owned_by) - if (owned_by.seen_messages) + if(REALTIMEOFDAY < (animate_start + animate_lifespan)) + stack_trace("Del'd before we finished fading, with [(animate_start + animate_lifespan) - REALTIMEOFDAY] time left") + if(owned_by) + if(owned_by.seen_messages) LAZYREMOVEASSOC(owned_by.seen_messages, message_loc, src) owned_by.images.Remove(message) owned_by = null message_loc = null message = null - leave_subsystem() return ..() /** @@ -105,12 +109,12 @@ // Clip message var/maxlen = CHAT_MESSAGE_MAX_LENGTH var/datum/html/split_holder/s = split_html(text) - if (length_char(s.inner_text) > maxlen) + if(length_char(s.inner_text) > maxlen) var/chattext = copytext_char(s.inner_text, 1, maxlen + 1) + "..." text = jointext(s.opening, "") + chattext + jointext(s.closing, "") // Calculate target color if not already present - if (!target.chat_color || target.chat_color_name != target.name) + if(!target.chat_color || target.chat_color_name != target.name) target.chat_color = colorize_string(target.name) target.chat_color_name = target.name @@ -120,7 +124,7 @@ // Reject whitespace var/static/regex/whitespace = new(@"^\s*$") - if (whitespace.Find(text)) + if(whitespace.Find(text)) qdel(src) return @@ -137,25 +141,56 @@ // Approximate text height var/static/regex/html_metachars = new(@"&[A-Za-z]{1,7};", "g") var/complete_text = "[symbol][text]" - var/mheight = WXH_TO_HEIGHT(owned_by.MeasureText(complete_text, null, CHAT_MESSAGE_WIDTH)) + var/mheight + WXH_TO_HEIGHT(owned_by.MeasureText(complete_text, null, CHAT_MESSAGE_WIDTH), mheight) + + if(!VERB_SHOULD_YIELD) + return finish_image_generation(mheight, target, owner, complete_text, lifespan) + + var/datum/callback/our_callback = CALLBACK(src, PROC_REF(finish_image_generation), mheight, target, owner, complete_text, lifespan) + SSrunechat.message_queue += our_callback + return + +///finishes the image generation after the MeasureText() call in generate_image(). +///necessary because after that call the proc can resume at the end of the tick and cause overtime. +/datum/chatmessage/proc/finish_image_generation(mheight, atom/target, mob/owner, complete_text, lifespan) + var/rough_time = REALTIMEOFDAY approx_lines = max(1, mheight / CHAT_MESSAGE_APPROX_LHEIGHT) // Translate any existing messages upwards, apply exponential decay factors to timers message_loc = target - if (owned_by.seen_messages) + if(owned_by.seen_messages) var/idx = 1 var/combined_height = approx_lines - for(var/msg in owned_by.seen_messages[message_loc]) - var/datum/chatmessage/m = msg - animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME) + for(var/datum/chatmessage/m as anything in owned_by.seen_messages[message_loc]) combined_height += m.approx_lines - var/sched_remaining = m.scheduled_destruction - world.time - if (!m.eol_complete) - 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 + + var/time_alive = rough_time - m.animate_start + var/lifespan_until_fade = m.animate_lifespan - CHAT_MESSAGE_EOL_FADE + + if(time_alive >= lifespan_until_fade) // If already fading out or dead, just shift upwards + animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL) + continue + + // Ensure we don't accidentially spike alpha up or something silly like that + m.message.alpha = m.get_current_alpha(time_alive) + + var/adjusted_lifespan_until_fade = lifespan_until_fade * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height) + m.animate_lifespan = adjusted_lifespan_until_fade + CHAT_MESSAGE_EOL_FADE + + var/remaining_lifespan_until_fade = adjusted_lifespan_until_fade - time_alive + if(remaining_lifespan_until_fade > 0) // Still got some lifetime to go; stay faded in for the remainder, then fade out + animate(m.message, alpha = 255, time = remaining_lifespan_until_fade) + animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE) + else // Current time alive is beyond new adjusted lifespan, your time has come my son + animate(m.message, alpha = 0, time = CHAT_MESSAGE_EOL_FADE) + + // We run this after the alpha animate, because we don't want to interrup it, but also don't want to block it by running first + // Sooo instead we do this. bit messy but it fuckin works + animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL) // Reset z index if relevant - if (current_z_idx >= CHAT_LAYER_MAX_Z) + if(current_z_idx >= CHAT_LAYER_MAX_Z) current_z_idx = 0 // Build message image @@ -169,22 +204,33 @@ message.maptext_x = (CHAT_MESSAGE_WIDTH - owner.bound_width) * -0.5 message.maptext = complete_text + animate_start = rough_time + animate_lifespan = lifespan + // View the message LAZYADDASSOC(owned_by.seen_messages, message_loc, src) owned_by.images |= message + + // Fade in animate(message, alpha = 255, time = CHAT_MESSAGE_SPAWN_TIME) + // Stay faded in + animate(alpha = 255, time = lifespan - CHAT_MESSAGE_SPAWN_TIME - CHAT_MESSAGE_EOL_FADE) + // Fade out + animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE) - // Prepare for destruction - scheduled_destruction = world.time + (lifespan - CHAT_MESSAGE_EOL_FADE) - enter_subsystem() + // Register with the runechat SS to handle destruction + addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), src), lifespan + CHAT_MESSAGE_GRACE_PERIOD, TIMER_DELETE_ME, SSrunechat) -/** - * Applies final animations to overlay CHAT_MESSAGE_EOL_FADE deciseconds prior to message deletion - */ -/datum/chatmessage/proc/end_of_life(fadetime = CHAT_MESSAGE_EOL_FADE) - eol_complete = scheduled_destruction + fadetime - 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 +/// Returns the current alpha of the message based on the time spent +/datum/chatmessage/proc/get_current_alpha(time_alive) + if(time_alive < CHAT_MESSAGE_SPAWN_TIME) + return (time_alive / CHAT_MESSAGE_SPAWN_TIME) * 255 + + var/lifespan_until_fade = animate_lifespan - CHAT_MESSAGE_EOL_FADE + if(time_alive <= lifespan_until_fade) + return 255 + + return (1 - ((time_alive - lifespan_until_fade) / CHAT_MESSAGE_EOL_FADE)) * 255 /** * Creates a message overlay at a defined location for a given speaker @@ -197,11 +243,6 @@ * * symbol - The symbol type of the message */ /mob/proc/create_chat_message(atom/movable/speaker, raw_message, italics = FALSE, size, symbol) - - if(isobserver(src)) - return - - // Display visual above source new /datum/chatmessage(raw_message, speaker, src, italics, size, CHAT_MESSAGE_LIFESPAN, symbol)