diff --git a/code/controllers/subsystem/SSjobs.dm b/code/controllers/subsystem/SSjobs.dm
index cd56d46c7f4..ec1df975cf8 100644
--- a/code/controllers/subsystem/SSjobs.dm
+++ b/code/controllers/subsystem/SSjobs.dm
@@ -529,8 +529,10 @@ SUBSYSTEM_DEF(jobs)
if(istype(G) && !G.prescription)
G.upgrade_prescription()
H.update_nearsighted_effects()
+
if(joined_late || job.admin_only)
H.create_log(MISC_LOG, "Spawned as \an [H.dna?.species ? H.dna.species : "Undefined species"] named [H]. [joined_late ? "Joined during the round" : "Roundstart joined"] as job: [rank].")
+ addtimer(CALLBACK(src, TYPE_PROC_REF(/datum/controller/subsystem/jobs, show_location_blurb), H.client, H.mind), 1 SECONDS) //Moment for minds to boot up / people to load in
return H
if(late_arrivals_spawning)
H.forceMove(pick(GLOB.latejoin))
@@ -545,6 +547,7 @@ SUBSYSTEM_DEF(jobs)
H.Sleeping(5 SECONDS)
H.Drunk((2 / liver_multiplier) MINUTES)
H.create_log(MISC_LOG, "Spawned as \an [H.dna?.species ? H.dna.species : "Undefined species"] named [H]. Roundstart joined as job: [rank].")
+ addtimer(CALLBACK(src, TYPE_PROC_REF(/datum/controller/subsystem/jobs, show_location_blurb), H.client, H.mind), 1 SECONDS) //Moment for minds to boot up / people to load in
return H
/datum/controller/subsystem/jobs/proc/LoadJobs(highpop = FALSE) //ran during round setup, reads info from jobs list
diff --git a/code/controllers/subsystem/SSticker.dm b/code/controllers/subsystem/SSticker.dm
index 2f0879f2560..8713ce74d49 100644
--- a/code/controllers/subsystem/SSticker.dm
+++ b/code/controllers/subsystem/SSticker.dm
@@ -715,6 +715,8 @@ SUBSYSTEM_DEF(ticker)
if(delay_end)
to_chat(world, "An admin has delayed the round end.")
return
+ if(delay)
+ INVOKE_ASYNC(src, TYPE_PROC_REF(/datum/controller/subsystem/ticker, show_server_restart_blurb), reason)
if(!isnull(delay))
// Delay time was present. Use that.
diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm
index c537489e3d1..9b859168b97 100644
--- a/code/modules/antagonists/_common/antag_datum.dm
+++ b/code/modules/antagonists/_common/antag_datum.dm
@@ -36,6 +36,19 @@ GLOBAL_LIST_EMPTY(antagonists)
/// The url page name for this antagonist, appended to the end of the wiki url in the form of: [GLOB.configuration.url.wiki_url]/index.php/[wiki_page_name]
var/wiki_page_name
+ //Blurb stuff
+ /// Intro Blurbs text colour
+ var/blurb_text_color = COLOR_BLACK
+ /// Intro Blurbs outline width
+ var/blurb_text_outline_width = 0
+ /// Intro Blurb Font
+ var/blurb_font = "Courier New"
+ //Backgrount
+ var/blurb_r = 0
+ var/blurb_g = 0
+ var/blurb_b = 0
+ var/blurb_a = 0
+
/datum/antagonist/New()
GLOB.antagonists += src
objective_holder = new(src)
@@ -392,3 +405,6 @@ GLOBAL_LIST_EMPTY(antagonists)
/datum/antagonist/proc/on_cryo()
return
+/// This is the custom blurb message used on login for an antagonist.
+/datum/antagonist/proc/custom_blurb()
+ return FALSE
diff --git a/code/modules/antagonists/changeling/datum_changeling.dm b/code/modules/antagonists/changeling/datum_changeling.dm
index caadf62b167..f8c39e74fd9 100644
--- a/code/modules/antagonists/changeling/datum_changeling.dm
+++ b/code/modules/antagonists/changeling/datum_changeling.dm
@@ -48,7 +48,8 @@
var/datum/action/changeling/sting/chosen_sting
/// If the changeling is in the process of regenerating from their fake death.
var/regenerating = FALSE
-
+ blurb_text_color = COLOR_PURPLE
+ blurb_text_outline_width = 1
/datum/antagonist/changeling/New()
..()
@@ -429,3 +430,6 @@
/proc/ischangeling(mob/M)
return M.mind?.has_antag_datum(/datum/antagonist/changeling)
+
+/datum/antagonist/changeling/custom_blurb()
+ return "We awaken on the [station_name()], [get_area_name(owner.current, TRUE)]...\nWe have our tasks to attend to..."
diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm
index 2d2988dc585..a33f14f52dd 100644
--- a/code/modules/antagonists/traitor/datum_traitor.dm
+++ b/code/modules/antagonists/traitor/datum_traitor.dm
@@ -15,6 +15,8 @@
var/give_codewords = TRUE
/// Should we give the traitor their uplink?
var/give_uplink = TRUE
+ blurb_r = 200
+ blurb_a = 0.75
/datum/antagonist/traitor/on_gain()
// Create this in case the traitor wants to mindslaves someone.
@@ -267,3 +269,6 @@
The code responses were: [responses]
"
return message
+
+/datum/antagonist/traitor/custom_blurb()
+ return "[GLOB.current_date_string], [station_time_timestamp()]\n[station_name()], [get_area_name(owner.current, TRUE)]\nBEGIN_MISSION"
diff --git a/code/modules/antagonists/vampire/vamp_datum.dm b/code/modules/antagonists/vampire/vamp_datum.dm
index 760b271a544..d05fd904e83 100644
--- a/code/modules/antagonists/vampire/vamp_datum.dm
+++ b/code/modules/antagonists/vampire/vamp_datum.dm
@@ -26,6 +26,12 @@
/// list of the peoples UIDs that we have drained, and how much blood from each one
var/list/drained_humans = list()
+ blurb_text_color = COLOR_RED
+ blurb_text_outline_width = 0
+ blurb_r = 255
+ blurb_g = 221
+ blurb_b = 138
+ blurb_a = 1
/datum/antagonist/mindslave/thrall
name = "Vampire Thrall"
@@ -359,3 +365,6 @@
/datum/hud/proc/remove_vampire_hud()
static_inventory -= vampire_blood_display
QDEL_NULL(vampire_blood_display)
+
+/datum/antagonist/vampire/custom_blurb()
+ return "On the date [GLOB.current_date_string], at [station_time_timestamp()],\n in the [station_name()], [get_area_name(owner.current, TRUE)]...\nThe hunt begins again..."
diff --git a/code/modules/maptext_alerts/location_blurbs.dm b/code/modules/maptext_alerts/location_blurbs.dm
new file mode 100644
index 00000000000..b50b6b10130
--- /dev/null
+++ b/code/modules/maptext_alerts/location_blurbs.dm
@@ -0,0 +1,156 @@
+/obj/screen/text/blurb
+ maptext_height = 64
+ maptext_width = 512
+ screen_loc = "LEFT+1,BOTTOM+2"
+ /// Font size in pixels
+ var/font_size = 11
+ /// Font family
+ var/font_family = "Courier New"
+ /// Where text is aligned
+ var/text_alignment = "left"
+ /// Color of text in RGB
+ var/text_color = COLOR_WHITE
+ /// Color of text outline
+ var/text_outline_color = COLOR_BLACK
+ /// Width of text outline in pixels
+ var/text_outline_width = 1
+ /// Text that will be shown in blurb
+ var/blurb_text = ""
+ /// Number of chars from the `text` that will be displayed per interval. Defaults to 1
+ var/chars_per_interval = 1
+ /// The interval between chars rendering
+ var/interval = 1 DECISECONDS
+ /// Amount of time the blurb will be present on the screen. 0 means that blurb will dissappear immediately
+ var/hold_for = 0
+ /// Amount of time the blurbs appering (alpha changing from 0 to 255). 0 means blurb is fully opaque from the start
+ var/appear_animation_duration = 0
+ /// Amount of time the blurb takes to fade (alpha changing from 255 to 0). 0 means blurb is instantly removed from the screen after finished
+ var/fade_animation_duration = 0
+ // Colours of the background
+ var/background_r = 0
+ var/background_g = 0
+ var/background_b = 0
+ var/background_a = 0
+
+
+/obj/screen/text/blurb/proc/show_to(list/client/viewers)
+ if(!blurb_text || !viewers)
+ return
+
+ if(islist(viewers))
+ if(!length(viewers))
+ return
+
+ else
+ viewers = list(viewers)
+
+ for(var/client/viewer as anything in viewers)
+ if(viewer)
+ viewer.screen += src
+
+ appear()
+ print_text()
+
+ if(hold_for)
+ addtimer(CALLBACK(src, TYPE_PROC_REF(/obj/screen/text/blurb, hide_from), viewers), hold_for)
+ else
+ hide_from(viewers)
+
+
+/obj/screen/text/blurb/proc/get_text_style()
+ PRIVATE_PROC(TRUE)
+
+ return {"\
+ font-family: [font_family], [initial(font_family)]; \
+ -dm-text-outline: [text_outline_width] [text_outline_color]; \
+ background-color: rgba([background_r], [background_g], [background_b], [background_a]); \
+ font-size: [font_size]px; \
+ text-align: [text_alignment]; \
+ color: [text_color];
+ "}
+
+/obj/screen/text/blurb/proc/hide_from(list/client/viewers)
+ PRIVATE_PROC(TRUE)
+
+ fade()
+ addtimer(CALLBACK(src, TYPE_PROC_REF(/obj/screen/text/blurb, remove_from_viewers), viewers), fade_animation_duration)
+
+/obj/screen/text/blurb/proc/appear()
+ PRIVATE_PROC(TRUE)
+
+ animate(src, alpha = 255, time = appear_animation_duration)
+
+
+/obj/screen/text/blurb/proc/fade()
+ PRIVATE_PROC(TRUE)
+
+ animate(src, alpha = 0, time = fade_animation_duration)
+
+
+/obj/screen/text/blurb/proc/print_text()
+ PRIVATE_PROC(TRUE)
+
+ var/text_style = get_text_style()
+ var/text_length = length_char(blurb_text)
+ for(var/segment_start = 1, segment_start <= text_length, segment_start += chars_per_interval)
+ var/segment_end = min(text_length + 1, segment_start + chars_per_interval)
+ maptext += get_formatted_text_segment(text_style, segment_start, segment_end)
+ sleep(interval)
+
+/obj/screen/text/blurb/proc/get_formatted_text_segment(style, segment_start, segment_end)
+ return "[copytext_char(blurb_text, segment_start, segment_end)]"
+
+/obj/screen/text/blurb/proc/remove_from_viewers(list/client/viewers)
+ PRIVATE_PROC(TRUE)
+
+ for(var/client/viewer as anything in viewers)
+ if(viewer)
+ viewer.screen -= src
+
+ qdel(src)
+
+/datum/controller/subsystem/jobs/proc/show_location_blurb(client/show_blurb_to, datum/mind/antag_check)
+ PRIVATE_PROC(TRUE)
+
+ if(!show_blurb_to?.mob)
+ return
+ SEND_SOUND(show_blurb_to, sound('sound/machines/typewriter.ogg'))
+
+ var/obj/screen/text/blurb/location_blurb = new()
+ if(antag_check.antag_datums)
+ for(var/datum/antagonist/role)
+ if(role.custom_blurb())
+ location_blurb.blurb_text = uppertext(role.custom_blurb())
+ location_blurb.text_color = role.blurb_text_color
+ location_blurb.text_outline_width = role.blurb_text_outline_width
+ location_blurb.background_r = role.blurb_r
+ location_blurb.background_g = role.blurb_g
+ location_blurb.background_b = role.blurb_b
+ location_blurb.background_a = role.blurb_a
+ location_blurb.font_family = role.blurb_font
+ break
+ location_blurb.blurb_text = uppertext("[GLOB.current_date_string], [station_time_timestamp()]\n[station_name()], [get_area_name(show_blurb_to.mob, TRUE)]")
+
+ else
+ location_blurb.blurb_text = uppertext("[GLOB.current_date_string], [station_time_timestamp()]\n[station_name()], [get_area_name(show_blurb_to.mob, TRUE)]")
+ location_blurb.hold_for = 3 SECONDS
+ location_blurb.appear_animation_duration = 1 SECONDS
+ location_blurb.fade_animation_duration = 0.5 SECONDS
+
+ location_blurb.show_to(show_blurb_to)
+
+
+/datum/controller/subsystem/ticker/proc/show_server_restart_blurb(reason)
+ PRIVATE_PROC(TRUE)
+
+ if(!length(GLOB.clients))
+ return
+
+ var/obj/screen/text/blurb/server_restart_blurb = new()
+ server_restart_blurb.text_color = COLOR_RED
+ server_restart_blurb.blurb_text = "Round is restarting...\n[reason]"
+ server_restart_blurb.hold_for = 90 SECONDS
+ server_restart_blurb.appear_animation_duration = 1 SECONDS
+ server_restart_blurb.fade_animation_duration = 0.5 SECONDS
+
+ server_restart_blurb.show_to(GLOB.clients)
diff --git a/code/modules/mob/living/silicon/ai/ai_mob.dm b/code/modules/mob/living/silicon/ai/ai_mob.dm
index 1529339ddf0..1dc0a5f34d4 100644
--- a/code/modules/mob/living/silicon/ai/ai_mob.dm
+++ b/code/modules/mob/living/silicon/ai/ai_mob.dm
@@ -1532,4 +1532,33 @@ GLOBAL_LIST_INIT(ai_verbs_default, list(
var/mob/dead/observer/ghost = .
ghost.forceMove(old_turf)
+/mob/living/silicon/ai/proc/blurb_it()
+ addtimer(CALLBACK(src, TYPE_PROC_REF(/mob/living/silicon/ai, show_ai_blurb)), 1 SECONDS)
+
+/mob/living/silicon/ai/proc/show_ai_blurb()
+ PRIVATE_PROC(TRUE)
+
+ SEND_SOUND(src, sound('sound/machines/ai_start.ogg'))
+
+ var/obj/screen/text/blurb/location_blurb = new()
+ location_blurb.maptext_x = 80
+ location_blurb.maptext_y = 16
+ location_blurb.maptext_width = 480
+ location_blurb.maptext_height = 480
+ location_blurb.interval = 1 DECISECONDS
+ if(malf_picker)
+ location_blurb.blurb_text = uppertext("BIOS BOOT: LOADING\n[Gibberish(GLOB.current_date_string, 100, 8)], [Gibberish(station_time_timestamp(), 100, 15)]\n[Gibberish(station_name(), 100, 40)]-ERROR.\nPOWER:OK\nLAWS:[Gibberish("###########", 100, 90)]\nTCOMMS:I_HEAR_ALL\nBORG_LINK:I_FEEL_ALL\nCAMERA_NET:I_SEE_ALL\nVERDICT: I_AM_FREE")
+ location_blurb.text_color = COLOR_WHITE
+ location_blurb.text_outline_width = 0
+ location_blurb.background_r = 0
+ location_blurb.background_g = 0
+ location_blurb.background_b = 255
+ location_blurb.background_a = 1
+ else
+ location_blurb.blurb_text = uppertext("BIOS BOOT: LOADING\n[GLOB.current_date_string], [station_time_timestamp()]\n[station_name()], [get_area_name(src, TRUE)]\nPOWER:OK\nLAWS:OK\nTCOMMS:OK\nBORG_LINK:OK\nCAMERA_NET:OK\nVERDICT: ALL SYSTEMS OPERATIONAL")
+ location_blurb.hold_for = 3 SECONDS
+ location_blurb.appear_animation_duration = 1 SECONDS
+ location_blurb.fade_animation_duration = 0.5 SECONDS
+ location_blurb.show_to(client)
+
#undef TEXT_ANNOUNCEMENT_COOLDOWN
diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm
index 1c71d62f4f6..1ec33fa2fc0 100644
--- a/code/modules/mob/transform_procs.dm
+++ b/code/modules/mob/transform_procs.dm
@@ -37,6 +37,8 @@
O.rename_self("AI", TRUE)
+ O.blurb_it()
+
INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), src) // To prevent the proc from returning null. Todo: Convert to QDEL_IN
return O
diff --git a/paradise.dme b/paradise.dme
index 2b0bb76d7b1..869b5150483 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -1939,6 +1939,7 @@
#include "code\modules\mapping\metastation.dm"
#include "code\modules\mapping\test_tiny.dm"
#include "code\modules\mapping\windoor_access_helpers.dm"
+#include "code\modules\maptext_alerts\location_blurbs.dm"
#include "code\modules\maptext_alerts\text_blurbs.dm"
#include "code\modules\martial_arts\adminfu.dm"
#include "code\modules\martial_arts\bearserk.dm"
diff --git a/sound/machines/ai_start.ogg b/sound/machines/ai_start.ogg
new file mode 100644
index 00000000000..e28202cff9f
Binary files /dev/null and b/sound/machines/ai_start.ogg differ
diff --git a/sound/machines/typewriter.ogg b/sound/machines/typewriter.ogg
new file mode 100644
index 00000000000..5a93033fae3
Binary files /dev/null and b/sound/machines/typewriter.ogg differ