diff --git a/code/__defines/_tick.dm b/code/__defines/_tick.dm index f336c2e27b2..5c7b2a3a868 100644 --- a/code/__defines/_tick.dm +++ b/code/__defines/_tick.dm @@ -19,6 +19,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/__defines/subsystems.dm b/code/__defines/subsystems.dm index 0a5b9428e09..ab63a266b08 100644 --- a/code/__defines/subsystems.dm +++ b/code/__defines/subsystems.dm @@ -188,6 +188,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G #define FIRE_PRIORITY_PROJECTILES 150 #define FIRE_PRIORITY_STATPANEL 390 #define FIRE_PRIORITY_CHAT 400 +#define FIRE_PRIORITY_RUNECHAT 410 #define FIRE_PRIORITY_OVERLAYS 500 #define FIRE_PRIORITY_TIMER 700 #define FIRE_PRIORITY_SPEECH_CONTROLLER 900 diff --git a/code/controllers/subsystems/runechat.dm b/code/controllers/subsystems/runechat.dm new file mode 100644 index 00000000000..663bb8cf347 --- /dev/null +++ b/code/controllers/subsystems/runechat.dm @@ -0,0 +1,14 @@ +TIMER_SUBSYSTEM_DEF(runechat) + name = "Runechat" + priority = FIRE_PRIORITY_RUNECHAT + + var/list/datum/callback/message_queue = list() + +/datum/controller/subsystem/timer/runechat/fire(resumed) + . = ..() //poggers + while(message_queue.len) + var/datum/callback/queued_message = message_queue[message_queue.len] + queued_message.Invoke() + message_queue.len-- + if(MC_TICK_CHECK) + return diff --git a/code/datums/chat_message.dm b/code/datums/chat_message.dm index b5c41302ba6..e3e7b6d5c8a 100644 --- a/code/datums/chat_message.dm +++ b/code/datums/chat_message.dm @@ -1,6 +1,7 @@ #define CHAT_MESSAGE_SPAWN_TIME 0.2 SECONDS #define CHAT_MESSAGE_LIFESPAN 5 SECONDS #define CHAT_MESSAGE_EOL_FADE 0.7 SECONDS +#define CHAT_MESSAGE_GRACE_PERIOD 0.2 SECONDS #define CHAT_MESSAGE_EXP_DECAY 0.8 // Messages decay at pow(factor, idx in stack) #define CHAT_MESSAGE_HEIGHT_DECAY 0.7 // Increase message decay based on the height of the message #define CHAT_MESSAGE_APPROX_LHEIGHT 11 // Approximate height in pixels of an 'average' line, used for height decay @@ -50,6 +51,12 @@ var/list/runechat_image_cache = list() var/approx_lines /// If we are currently processing animation and cleanup at EOL var/ending_life + /// When we started animating the message + var/animate_start = 0 + /// Our animation lifespan, how long this message will last + var/animate_lifespan = 0 + /// Callback to finish_image_generation passed to SSrunechat + var/datum/callback/finish_callback /** @@ -70,7 +77,7 @@ var/list/runechat_image_cache = list() stack_trace("/datum/chatmessage created with [isnull(owner) ? "null" : "invalid"] mob owner") qdel(src) return - generate_image(text, target, owner, extra_classes, lifespan) + INVOKE_ASYNC(src, PROC_REF(generate_image), text, target, owner, extra_classes, lifespan) /datum/chatmessage/Destroy() if(istype(owned_by, /client)) // hopefully the PARENT_QDELETING on client should beat this if it's a disconnect @@ -78,6 +85,11 @@ var/list/runechat_image_cache = list() if(owned_by.seen_messages) LAZYREMOVEASSOC(owned_by.seen_messages, message_loc, src) owned_by.images.Remove(message) + + if (finish_callback) + SSrunechat.message_queue -= finish_callback + finish_callback = null + owned_by = null message_loc = null message = null @@ -94,7 +106,6 @@ var/list/runechat_image_cache = list() * * lifespan - The lifespan of the message in deciseconds */ /datum/chatmessage/proc/generate_image(text, atom/target, mob/owner, list/extra_classes, lifespan) - set waitfor = FALSE if(!target || !owner) qdel(src) @@ -106,7 +117,6 @@ var/list/runechat_image_cache = list() var/extra_length = owned_by.prefs?.read_preference(/datum/preference/toggle/runechat_long_messages) var/maxlen = extra_length ? CHAT_MESSAGE_EXT_LENGTH : CHAT_MESSAGE_LENGTH - var/msgwidth = extra_length ? CHAT_MESSAGE_EXT_WIDTH : CHAT_MESSAGE_WIDTH // Clip message if(length_char(text) > maxlen) @@ -170,8 +180,22 @@ var/list/runechat_image_cache = list() // Approximate text height var/complete_text = "[text]" + + var/msgwidth = extra_length ? CHAT_MESSAGE_EXT_WIDTH : CHAT_MESSAGE_WIDTH var/mheight = WXH_TO_HEIGHT(owned_by.MeasureText(complete_text, null, msgwidth)) + + if(!VERB_SHOULD_YIELD) + return finish_image_generation(msgwidth, mheight, target, owner, complete_text, lifespan) + + finish_callback = CALLBACK(src, PROC_REF(finish_image_generation), msgwidth, mheight, target, owner, complete_text, lifespan) + SSrunechat.message_queue += finish_callback + +/datum/chatmessage/proc/finish_image_generation(msgwidth, mheight, atom/target, mob/owner, complete_text, lifespan) + finish_callback = null + var/rough_time = REALTIMEOFDAY + approx_lines = max(1, mheight / CHAT_MESSAGE_APPROX_LHEIGHT) + var/starting_height = target.maptext_height + owner.get_oversized_icon_offsets()["y"] // Translate any existing messages upwards, apply exponential decay factors to timers message_loc = target.runechat_holder(src) @@ -180,15 +204,45 @@ var/list/runechat_image_cache = list() var/idx = 1 var/combined_height = approx_lines for(var/datum/chatmessage/m as anything in owned_by.seen_messages[message_loc]) - animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME) combined_height += m.approx_lines - if(!m.ending_life) // Don't bother! - var/sched_remaining = m.scheduled_destruction - world.time - if(sched_remaining > CHAT_MESSAGE_SPAWN_TIME) - var/remaining_time = (sched_remaining) * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height) - m.scheduled_destruction = world.time + remaining_time - m.schedule_end_of_life(remaining_time) + var/time_spent = rough_time - m.animate_start + var/time_before_fade = m.animate_lifespan - CHAT_MESSAGE_EOL_FADE + + // When choosing to update the remaining time we have to be careful not to update the + // scheduled time once the EOL has been executed. + if (time_spent >= time_before_fade) + if(m.message.pixel_y < starting_height) + var/max_height = m.message.pixel_y + m.approx_lines * CHAT_MESSAGE_APPROX_LHEIGHT - starting_height + if(max_height > 0) + animate(m.message, pixel_y = m.message.pixel_y + max_height, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL) + else if(mheight + starting_height >= m.message.pixel_y) + animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL) + continue + + var/remaining_time = time_before_fade * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height) + // Ensure we don't accidentially spike alpha up or something silly like that + m.message.alpha = m.get_current_alpha(time_spent) + if(remaining_time > 0) + if(time_spent < CHAT_MESSAGE_SPAWN_TIME) + // We haven't even had the time to fade in yet! + animate(m.message, alpha = 255, CHAT_MESSAGE_SPAWN_TIME - time_spent) + // Stay faded in for a while, then + animate(m.message, alpha = 255, remaining_time/*, flags=ANIMATION_CONTINUE*/) // No blinking + // Fade out + animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE) + m.animate_lifespan = remaining_time + CHAT_MESSAGE_EOL_FADE + else + // Your time has come my son + animate(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 + if(m.message.pixel_y < starting_height) + var/max_height = m.message.pixel_y + m.approx_lines * CHAT_MESSAGE_APPROX_LHEIGHT - starting_height + if(max_height > 0) + animate(m.message, pixel_y = m.message.pixel_y + max_height, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL) + else if(mheight + starting_height >= m.message.pixel_y) + animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL) // Build message image message = image(loc = message_loc, layer = ABOVE_MOB_LAYER) @@ -197,10 +251,14 @@ var/list/runechat_image_cache = list() message.alpha = 0 message.maptext_width = msgwidth message.maptext_height = mheight - message.maptext_x = message_loc.runechat_x_offset(msgwidth, mheight) - message.maptext_y = message_loc.runechat_y_offset(msgwidth, mheight) + message.pixel_x = message_loc.runechat_x_offset(msgwidth, mheight) + message.pixel_y = starting_height message.maptext = complete_text + + animate_start = rough_time + animate_lifespan = lifespan + if(!owner) qdel(src) return @@ -209,20 +267,34 @@ var/list/runechat_image_cache = list() // View the message LAZYADDASSOCLIST(owned_by.seen_messages, message_loc, src) - owned_by.images += message - animate(message, alpha = 255, time = CHAT_MESSAGE_SPAWN_TIME) + owned_by.images |= message - // Prepare for destruction - scheduled_destruction = world.time + (lifespan - CHAT_MESSAGE_EOL_FADE) - schedule_end_of_life(lifespan - CHAT_MESSAGE_EOL_FADE) + // Fade in + animate(message, alpha = 255, time = CHAT_MESSAGE_SPAWN_TIME) + var/time_before_fade = lifespan - CHAT_MESSAGE_SPAWN_TIME - CHAT_MESSAGE_EOL_FADE + // Stay faded in + animate(alpha = 255, time = time_before_fade) + // Fade out + animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE) + + // 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) /datum/chatmessage/proc/unregister_qdel_self() // this should only call owned_by if the client is destroyed + SIGNAL_HANDLER UnregisterSignal(owned_by, COMSIG_PARENT_QDELETING) owned_by = null qdel_self() -/datum/chatmessage/proc/schedule_end_of_life(var/schedule) - addtimer(CALLBACK(src, PROC_REF(end_of_life)), schedule, TIMER_DELETE_ME) +/datum/chatmessage/proc/get_current_alpha(time_spent) + if(time_spent < CHAT_MESSAGE_SPAWN_TIME) + return (time_spent / CHAT_MESSAGE_SPAWN_TIME) * 255 + + var/time_before_fade = animate_lifespan - CHAT_MESSAGE_EOL_FADE + if(time_spent <= time_before_fade) + return 255 + + return (1 - ((time_spent - time_before_fade) / CHAT_MESSAGE_EOL_FADE)) * 255 /** * Applies final animations to overlay CHAT_MESSAGE_EOL_FADE deciseconds prior to message deletion @@ -393,6 +465,7 @@ var/list/runechat_image_cache = list() #undef CHAT_MESSAGE_SPAWN_TIME #undef CHAT_MESSAGE_LIFESPAN #undef CHAT_MESSAGE_EOL_FADE +#undef CHAT_MESSAGE_GRACE_PERIOD #undef CHAT_MESSAGE_EXP_DECAY #undef CHAT_MESSAGE_HEIGHT_DECAY #undef CHAT_MESSAGE_APPROX_LHEIGHT diff --git a/code/modules/planet/planet.dm b/code/modules/planet/planet.dm index ad24114beb5..e19d215aa40 100644 --- a/code/modules/planet/planet.dm +++ b/code/modules/planet/planet.dm @@ -63,8 +63,6 @@ weather_holder.process() /datum/planet/proc/update_sun_deferred(var/new_brightness, var/new_color) - if(new_brightness < 0 || new_brightness > 1) - CRASH("Planetary sun brightness was outside of sane bounds. Expected 0.00 to 1.00, got [new_brightness].") sun["brightness"] = new_brightness sun["color"] = new_color needs_work |= PLANET_PROCESS_SUN diff --git a/tgui/package.json b/tgui/package.json index 960b6df5900..6228bc72e6b 100644 --- a/tgui/package.json +++ b/tgui/package.json @@ -26,6 +26,8 @@ "@swc/jest": "^0.2.37", "@types/jest": "^29.5.14", "@types/node": "^22.9.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", "@types/webpack-env": "^1.18.5", "@typescript-eslint/parser": "^8.13.0", "@typescript-eslint/utils": "^8.13.0", diff --git a/tgui/packages/tgui-bench/package.json b/tgui/packages/tgui-bench/package.json index 4acca086a41..1f91e58638b 100644 --- a/tgui/packages/tgui-bench/package.json +++ b/tgui/packages/tgui-bench/package.json @@ -4,6 +4,7 @@ "version": "5.0.3", "dependencies": { "@fastify/static": "^8.0.2", + "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "common": "workspace:*", "fastify": "^5.1.0", diff --git a/tgui/yarn.lock b/tgui/yarn.lock index 291973b59df..084ea20ff63 100644 --- a/tgui/yarn.lock +++ b/tgui/yarn.lock @@ -19043,6 +19043,7 @@ __metadata: resolution: "tgui-bench@workspace:packages/tgui-bench" dependencies: "@fastify/static": "npm:^8.0.2" + "@types/react": "npm:^18.3.12" "@types/react-dom": "npm:^18.3.1" common: "workspace:*" fastify: "npm:^5.1.0" @@ -19129,6 +19130,8 @@ __metadata: "@swc/jest": "npm:^0.2.37" "@types/jest": "npm:^29.5.14" "@types/node": "npm:^22.9.0" + "@types/react": "npm:^18.3.12" + "@types/react-dom": "npm:^18.3.1" "@types/webpack-env": "npm:^1.18.5" "@typescript-eslint/parser": "npm:^8.13.0" "@typescript-eslint/utils": "npm:^8.13.0" diff --git a/vorestation.dme b/vorestation.dme index 8ff2a4bdb50..dd2d89589b2 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -379,6 +379,7 @@ #include "code\controllers\subsystems\player_tips.dm" #include "code\controllers\subsystems\radiation.dm" #include "code\controllers\subsystems\robot_sprites.dm" +#include "code\controllers\subsystems\runechat.dm" #include "code\controllers\subsystems\server_maint.dm" #include "code\controllers\subsystems\shuttles.dm" #include "code\controllers\subsystems\skybox.dm"