diff --git a/code/__defines/_planes+layers.dm b/code/__defines/_planes+layers.dm
index 96d65c6f495..551e94183c1 100644
--- a/code/__defines/_planes+layers.dm
+++ b/code/__defines/_planes+layers.dm
@@ -93,6 +93,8 @@ What is the naming convention for planes or layers?
#define BELOW_MOB_LAYER 3.9 // Should be converted to plane swaps
#define ABOVE_MOB_LAYER 4.1 // Should be converted to plane swaps
+#define ABOVE_MOB_PLANE -24
+
// Invisible things plane
#define CLOAKED_PLANE -15
@@ -122,6 +124,7 @@ What is the naming convention for planes or layers?
#define PLANE_PLANETLIGHTING 4 //Lighting on planets
#define PLANE_LIGHTING 5 //Where the lighting (and darkness) lives
#define PLANE_LIGHTING_ABOVE 6 //For glowy eyes etc. that shouldn't be affected by darkness
+#define PLANE_RUNECHAT 7
#define PLANE_GHOSTS 10 //Spooooooooky ghooooooosts
#define PLANE_AI_EYE 11 //The AI eye lives here
diff --git a/code/_helpers/time.dm b/code/_helpers/time.dm
index 6e63dabb45a..b75aeb77e38 100644
--- a/code/_helpers/time.dm
+++ b/code/_helpers/time.dm
@@ -20,6 +20,8 @@
#define TICKS2DS(T) ((T) TICKS) // Convert ticks to deciseconds
#define DS2NEARESTTICK(DS) TICKS2DS(-round(-(DS2TICKS(DS))))
+var/world_startup_time
+
/proc/get_game_time()
var/global/time_offset = 0
var/global/last_time = 0
diff --git a/code/datums/chat_message.dm b/code/datums/chat_message.dm
new file mode 100644
index 00000000000..5cd8a9babe7
--- /dev/null
+++ b/code/datums/chat_message.dm
@@ -0,0 +1,330 @@
+#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_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
+
+#define CHAT_MESSAGE_WIDTH 96 // pixels
+#define CHAT_MESSAGE_EXT_WIDTH 128
+#define CHAT_MESSAGE_LENGTH 68 // characters
+#define CHAT_MESSAGE_EXT_LENGTH 150
+
+#define CHAT_MESSAGE_MOB 1
+#define CHAT_MESSAGE_OBJ 2
+#define WXH_TO_HEIGHT(x) text2num(copytext((x), findtextEx((x), "x") + 1)) // thanks lummox
+
+#define CHAT_RUNE_EMOTE 0x1
+#define CHAT_RUNE_RADIO 0x2
+
+/**
+ * # Chat Message Overlay
+ *
+ * Datum for generating a message overlay on the map
+ * Ported from TGStation; https://github.com/tgstation/tgstation/pull/50608/, author: bobbahbrown
+ */
+
+// Cached runechat icon
+var/list/runechat_image_cache = list()
+
+
+/hook/startup/proc/runechat_images()
+ var/image/radio_image = image('icons/UI_Icons/chat/chat_icons.dmi', icon_state = "radio")
+ runechat_image_cache["radio"] = radio_image
+
+ var/image/emote_image = image('icons/UI_Icons/chat/chat_icons.dmi', icon_state = "emote")
+ runechat_image_cache["emote"] = emote_image
+
+ return TRUE
+
+/datum/chatmessage
+ /// The visual element of the chat messsage
+ var/image/message
+ /// The location in which the message is appearing
+ var/atom/message_loc
+ /// The client who heard this message
+ var/client/owned_by
+ /// Contains the scheduled destruction time
+ var/scheduled_destruction
+ /// Contains the approximate amount of lines for height decay
+ var/approx_lines
+ /// If we are currently processing animation and cleanup at EOL
+ var/ending_life
+
+/**
+ * Constructs a chat message overlay
+ *
+ * Arguments:
+ * * text - The text content of the overlay
+ * * target - The target atom to display the overlay at
+ * * owner - The mob that owns this overlay, only this mob will be able to view it
+ * * extra_classes - Extra classes to apply to the span that holds the text
+ * * lifespan - The lifespan of the message in deciseconds
+ */
+/datum/chatmessage/New(text, atom/target, mob/owner, list/extra_classes = null, lifespan = CHAT_MESSAGE_LIFESPAN)
+ . = ..()
+ if(!istype(target))
+ CRASH("Invalid target given for chatmessage")
+ if(!istype(owner) || QDELETED(owner) || !owner.client)
+ stack_trace("/datum/chatmessage created with [isnull(owner) ? "null" : "invalid"] mob owner")
+ qdel(src)
+ return
+ generate_image(text, target, owner, extra_classes, lifespan)
+
+/datum/chatmessage/Destroy()
+ if(owned_by)
+ LAZYREMOVEASSOC(owned_by.seen_messages, message_loc, src)
+ owned_by.images.Remove(message)
+ UnregisterSignal(owned_by, COMSIG_PARENT_QDELETING)
+ if(message_loc)
+ UnregisterSignal(message_loc, COMSIG_PARENT_QDELETING)
+ owned_by = null
+ message_loc = null
+ message = null
+ return ..()
+
+/**
+ * Generates a chat message image representation
+ *
+ * Arguments:
+ * * text - The text content of the overlay
+ * * target - The target atom to display the overlay at
+ * * owner - The mob that owns this overlay, only this mob will be able to view it
+ * * extra_classes - Extra classes to apply to the span that holds the text
+ * * 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
+
+ // Register client who owns this message
+ owned_by = owner.client
+ RegisterSignal(owned_by, COMSIG_PARENT_QDELETING, .proc/qdel_self)
+
+ var/extra_length = owned_by.is_preference_enabled(/datum/client_preference/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)
+ text = copytext_char(text, 1, maxlen + 1) + "..." // BYOND index moment
+
+ // Calculate target color if not already present
+ if(!target.chat_color || target.chat_color_name != target.name)
+ target.chat_color = colorize_string(target.name)
+ target.chat_color_darkened = colorize_string(target.name, 0.85, 0.85)
+ target.chat_color_name = target.name
+
+ // Get rid of any URL schemes that might cause BYOND to automatically wrap something in an anchor tag
+ var/static/regex/url_scheme = new(@"[A-Za-z][A-Za-z0-9+-\.]*:\/\/", "g")
+ text = replacetext(text, url_scheme, "")
+
+ // Reject whitespace
+ var/static/regex/whitespace = new(@"^\s*$")
+ if(whitespace.Find(text))
+ qdel(src)
+ return
+
+ // Non mobs speakers can be small
+ if(!ismob(target))
+ extra_classes |= "small"
+
+ // If we heard our name, it's important
+ // Differnt from our own system of name emphasis, maybe unify
+ var/list/names = splittext(owner.name, " ")
+ for (var/word in names)
+ text = replacetext(text, word, "[word]")
+
+ var/list/prefixes
+
+ // Append prefixes
+ if(extra_classes.Find("virtual-speaker"))
+ LAZYADD(prefixes, "\icon[runechat_image_cache["radio"]]")
+ if(extra_classes.Find("emote"))
+ // Icon on both ends?
+ //var/image/I = runechat_image_cache["emote"]
+ //text = "\icon[I][text]\icon[I]"
+
+ // Icon on one end?
+ //LAZYADD(prefixes, "\icon[runechat_image_cache["emote"]]")
+
+ // Asterisks instead?
+ text = "* [text] *"
+
+ text = "[prefixes?.Join(" ")][text]"
+
+ // We dim italicized text to make it more distinguishable from regular text
+ var/tgt_color = extra_classes.Find("italics") ? target.chat_color_darkened : target.chat_color
+
+ // Approximate text height
+ var/complete_text = ""
+ var/mheight = WXH_TO_HEIGHT(owned_by.MeasureText(complete_text, null, msgwidth))
+ approx_lines = max(1, mheight / CHAT_MESSAGE_APPROX_LHEIGHT)
+
+ // Translate any existing messages upwards, apply exponential decay factors to timers
+ message_loc = target
+ RegisterSignal(message_loc, COMSIG_PARENT_QDELETING, .proc/qdel_self)
+ 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)
+ 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
+ spawn(remaining_time)
+ m.end_of_life()
+
+ // Build message image
+ message = image(loc = message_loc, layer = ABOVE_MOB_LAYER)
+ message.plane = PLANE_RUNECHAT
+ message.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA | KEEP_APART
+ message.alpha = 0
+ message.pixel_y = owner.bound_height * 0.95
+ message.maptext_width = msgwidth
+ message.maptext_height = mheight
+ message.maptext_x = (msgwidth - owner.bound_width) * -0.5
+ message.maptext = complete_text
+
+ if(owner.contains(target)) // Special case, holding an atom speaking (pAI, recorder...)
+ message.plane = PLANE_PLAYER_HUD_ABOVE
+
+ // View the message
+ LAZYADDASSOCLIST(owned_by.seen_messages, message_loc, src)
+ owned_by.images += message
+ animate(message, alpha = 255, time = CHAT_MESSAGE_SPAWN_TIME)
+
+ // Prepare for destruction
+ scheduled_destruction = world.time + (lifespan - CHAT_MESSAGE_EOL_FADE)
+ spawn(lifespan - CHAT_MESSAGE_EOL_FADE)
+ end_of_life()
+
+/**
+ * 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)
+ if(gc_destroyed || ending_life)
+ return
+ ending_life = TRUE
+ animate(message, alpha = 0, time = fadetime, flags = ANIMATION_PARALLEL)
+ spawn(fadetime)
+ qdel(src)
+
+/**
+ * Creates a message overlay at a defined location for a given speaker
+ *
+ * Arguments:
+ * * speaker - The atom who is saying this message
+ * * message - The text content of the message
+ * * italics - Decides if this should be small or not, as generally italics text are for whisper/radio overhear
+ * * existing_extra_classes - Additional classes to add to the message
+ */
+/mob/proc/create_chat_message(atom/movable/speaker, message, italics, list/existing_extra_classes, audible = TRUE)
+ if(!client)
+ return
+
+ // Doesn't want to hear
+ if(ismob(speaker) && !client.is_preference_enabled(/datum/client_preference/runechat_mob))
+ return
+ else if(isobj(speaker) && !client.is_preference_enabled(/datum/client_preference/runechat_obj))
+ return
+
+ // Incapable of receiving
+ if((audible && is_deaf()) || (!audible && is_blind()))
+ return
+
+ // Check for virtual speakers (aka hearing a message through a radio)
+ if(existing_extra_classes.Find("radio"))
+ return
+
+ /* Not currently necessary
+ message = strip_html_properly(message)
+ if(!message)
+ return
+ */
+
+ var/list/extra_classes = list()
+ extra_classes += existing_extra_classes
+
+ if(italics)
+ extra_classes |= "italics"
+
+ if(client.is_preference_enabled(/datum/client_preference/runechat_border))
+ extra_classes |= "black_outline"
+
+ var/dist = get_dist(src, speaker)
+ switch (dist)
+ if(4 to 5)
+ extra_classes |= "small"
+ if(5 to 16)
+ extra_classes |= "very_small"
+
+ // Display visual above source
+ new /datum/chatmessage(message, speaker, src, extra_classes)
+
+// Tweak these defines to change the available color ranges
+#define CM_COLOR_SAT_MIN 0.6
+#define CM_COLOR_SAT_MAX 0.95
+#define CM_COLOR_LUM_MIN 0.70
+#define CM_COLOR_LUM_MAX 0.90
+
+/**
+ * Gets a color for a name, will return the same color for a given string consistently within a round.atom
+ *
+ * Note that this proc aims to produce pastel-ish colors using the HSL colorspace. These seem to be favorable for displaying on the map.
+ *
+ * Arguments:
+ * * name - The name to generate a color for
+ * * sat_shift - A value between 0 and 1 that will be multiplied against the saturation
+ * * lum_shift - A value between 0 and 1 that will be multiplied against the luminescence
+ */
+/datum/chatmessage/proc/colorize_string(name, sat_shift = 1, lum_shift = 1)
+ // seed to help randomness
+ var/static/rseed = rand(1,26)
+
+ // get hsl using the selected 6 characters of the md5 hash
+ var/hash = copytext(md5(name + "[world_startup_time]"), rseed, rseed + 6)
+ var/h = hex2num(copytext(hash, 1, 3)) * (360 / 255)
+ var/s = (hex2num(copytext(hash, 3, 5)) >> 2) * ((CM_COLOR_SAT_MAX - CM_COLOR_SAT_MIN) / 63) + CM_COLOR_SAT_MIN
+ var/l = (hex2num(copytext(hash, 5, 7)) >> 2) * ((CM_COLOR_LUM_MAX - CM_COLOR_LUM_MIN) / 63) + CM_COLOR_LUM_MIN
+
+ // adjust for shifts
+ s *= clamp(sat_shift, 0, 1)
+ l *= clamp(lum_shift, 0, 1)
+
+ // convert to rgba
+ var/h_int = round(h/60) // mapping each section of H to 60 degree sections
+ var/c = (1 - abs(2 * l - 1)) * s
+ var/x = c * (1 - abs((h / 60) % 2 - 1))
+ var/m = l - c * 0.5
+ x = (x + m) * 255
+ c = (c + m) * 255
+ m *= 255
+ switch(h_int)
+ if(0)
+ return rgb(c,x,m)
+ if(1)
+ return rgb(x,c,m)
+ if(2)
+ return rgb(m,c,x)
+ if(3)
+ return rgb(m,x,c)
+ if(4)
+ return rgb(x,m,c)
+ if(5)
+ return rgb(c,m,x)
+
+/atom/proc/runechat_message(message, range = world.view, italics, list/classes = list(), audible = TRUE)
+ var/list/hear = get_mobs_and_objs_in_view_fast(get_turf(src), range, remote_ghosts = FALSE)
+
+ var/list/hearing_mobs = hear["mobs"]
+
+ for(var/mob in hearing_mobs)
+ var/mob/M = mob
+ if(!M.client)
+ continue
+ M.create_chat_message(src, message, italics, classes, audible)
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 46f001c8e1b..fc27bb9f4e0 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -34,6 +34,15 @@
// Track if we are already had initialize() called to prevent double-initialization.
var/initialized = FALSE
+ /// Last name used to calculate a color for the chatmessage overlays
+ var/chat_color_name
+ /// Last color calculated for the the chatmessage overlays
+ var/chat_color
+ /// A luminescence-shifted value of the last color calculated for chatmessage overlays
+ var/chat_color_darkened
+ /// The chat color var, without alpha.
+ var/chat_color_hover
+
/atom/New(loc, ...)
// Don't call ..() unless /datum/New() ever exists
@@ -490,7 +499,7 @@
// Use for objects performing visible actions
// message is output to anyone who can see, e.g. "The [src] does something!"
// blind_message (optional) is what blind people will hear e.g. "You hear something!"
-/atom/proc/visible_message(var/message, var/blind_message, var/list/exclude_mobs, var/range = world.view)
+/atom/proc/visible_message(var/message, var/blind_message, var/list/exclude_mobs, var/range = world.view, var/runemessage = "ðŸ‘")
//VOREStation Edit
var/list/see
@@ -513,6 +522,8 @@
var/mob/M = mob
if(M.see_invisible >= invisibility && MOB_CAN_SEE_PLANE(M, plane))
M.show_message(message, VISIBLE_MESSAGE, blind_message, AUDIBLE_MESSAGE)
+ if(runemessage != -1)
+ M.create_chat_message(src, "[runemessage]", FALSE, list("emote"), audible = FALSE)
else if(blind_message)
M.show_message(blind_message, AUDIBLE_MESSAGE)
@@ -521,7 +532,7 @@
// message is the message output to anyone who can hear.
// deaf_message (optional) is what deaf people will see.
// hearing_distance (optional) is the range, how many tiles away the message can be heard.
-/atom/proc/audible_message(var/message, var/deaf_message, var/hearing_distance, var/radio_message)
+/atom/proc/audible_message(var/message, var/deaf_message, var/hearing_distance, var/radio_message, var/runemessage)
var/range = hearing_distance || world.view
var/list/hear = get_mobs_and_objs_in_view_fast(get_turf(src),range,remote_ghosts = FALSE)
@@ -542,6 +553,8 @@
var/mob/M = mob
var/msg = message
M.show_message(msg, AUDIBLE_MESSAGE, deaf_message, VISIBLE_MESSAGE)
+ if(runemessage != -1)
+ M.create_chat_message(src, "[runemessage || message]", FALSE, list("emote"))
/atom/movable/proc/dropInto(var/atom/destination)
while(istype(destination))
diff --git a/code/game/machinery/air_alarm.dm b/code/game/machinery/air_alarm.dm
index c8222d00b4b..f330d839f6a 100644
--- a/code/game/machinery/air_alarm.dm
+++ b/code/game/machinery/air_alarm.dm
@@ -191,7 +191,7 @@
update_use_power(USE_POWER_ACTIVE)
regulating_temperature = 1
audible_message("\The [src] clicks as it starts [environment.temperature > target_temperature ? "cooling" : "heating"] the room.",\
- "You hear a click and a faint electronic hum.")
+ "You hear a click and a faint electronic hum.", runemessage = "* click *")
playsound(src, 'sound/machines/click.ogg', 50, 1)
else
//check for when we should stop adjusting temperature
@@ -199,7 +199,7 @@
update_use_power(USE_POWER_IDLE)
regulating_temperature = 0
audible_message("\The [src] clicks quietly as it stops [environment.temperature > target_temperature ? "cooling" : "heating"] the room.",\
- "You hear a click as a faint electronic humming stops.")
+ "You hear a click as a faint electronic humming stops.", runemessage = "* click *")
playsound(src, 'sound/machines/click.ogg', 50, 1)
if(regulating_temperature)
diff --git a/code/game/machinery/cloning.dm b/code/game/machinery/cloning.dm
index 2c34326fa7d..197848d9adb 100644
--- a/code/game/machinery/cloning.dm
+++ b/code/game/machinery/cloning.dm
@@ -206,7 +206,7 @@
else if((occupant.health >= heal_level || occupant.health == occupant.getMaxHealth()) && (!eject_wait))
playsound(src, 'sound/machines/medbayscanner1.ogg', 50, 1)
- audible_message("\The [src] signals that the cloning process is complete.")
+ audible_message("\The [src] signals that the cloning process is complete.", runemessage = "* ding *")
connected_message("Cloning Process Complete.")
locked = 0
go_out()
diff --git a/code/game/machinery/computer/arcade.dm b/code/game/machinery/computer/arcade.dm
index 77e7e037a9d..df72c0d7678 100644
--- a/code/game/machinery/computer/arcade.dm
+++ b/code/game/machinery/computer/arcade.dm
@@ -519,9 +519,9 @@
if(electronics)
sleep(10)
if(oldfuel > fuel && oldfood > food)
- src.audible_message("\The [src] lets out a somehow reassuring chime.")
+ src.audible_message("\The [src] lets out a somehow reassuring chime.", runemessage = "* reassuring chime *")
else if(oldfuel < fuel || oldfood < food)
- src.audible_message("\The [src] lets out a somehow ominous chime.")
+ src.audible_message("\The [src] lets out a somehow ominous chime.", runemessage = "* ominous chime *")
food = oldfood
fuel = oldfuel
diff --git a/code/game/objects/effects/map_effects/portal.dm b/code/game/objects/effects/map_effects/portal.dm
index 6eda8b6c726..094de59e2af 100644
--- a/code/game/objects/effects/map_effects/portal.dm
+++ b/code/game/objects/effects/map_effects/portal.dm
@@ -277,7 +277,8 @@ when portals are shortly lived, or when portals are made to be obvious with spec
for(var/thing in mobs_to_relay)
var/mob/mob = thing
- var/message = mob.combine_message(message_pieces, verb, M)
+ var/list/combined = mob.combine_message(message_pieces, verb, M)
+ var/message = combined["formatted"]
var/name_used = M.GetVoice()
var/rendered = null
rendered = "[name_used] [message]"
diff --git a/code/game/objects/items/devices/communicator/phone.dm b/code/game/objects/items/devices/communicator/phone.dm
index bb25dce953f..16543ee2c05 100644
--- a/code/game/objects/items/devices/communicator/phone.dm
+++ b/code/game/objects/items/devices/communicator/phone.dm
@@ -246,7 +246,8 @@
//VOREStation Edit End
for(var/mob/mob in mobs_to_relay)
- var/message = mob.combine_message(message_pieces, verb, M)
+ var/list/combined = mob.combine_message(message_pieces, verb, M)
+ var/message = combined["formatted"]
var/name_used = M.GetVoice()
var/rendered = null
rendered = "[bicon(src)] [name_used] [message]"
diff --git a/code/game/objects/items/devices/defib.dm b/code/game/objects/items/devices/defib.dm
index f938f0eab18..4bd2ac9a307 100644
--- a/code/game/objects/items/devices/defib.dm
+++ b/code/game/objects/items/devices/defib.dm
@@ -460,7 +460,7 @@
return
playsound(src, 'sound/machines/defib_charge.ogg', 50, 0)
- audible_message("\The [src] lets out a steadily rising hum...")
+ audible_message("\The [src] lets out a steadily rising hum...", runemessage = "* whines *")
if(!do_after(user, chargetime, H))
return
@@ -527,7 +527,7 @@
H.setBrainLoss(brain_damage)
/obj/item/weapon/shockpaddles/proc/make_announcement(var/message, var/msg_class)
- audible_message("\The [src] [message]", "\The [src] vibrates slightly.")
+ audible_message("\The [src] [message]", "\The [src] vibrates slightly.", runemessage = "* buzz *")
/obj/item/weapon/shockpaddles/emag_act(mob/user)
if(safety)
diff --git a/code/game/objects/items/devices/megaphone.dm b/code/game/objects/items/devices/megaphone.dm
index 32c9d75959a..86602094c1c 100644
--- a/code/game/objects/items/devices/megaphone.dm
+++ b/code/game/objects/items/devices/megaphone.dm
@@ -31,12 +31,13 @@
/obj/item/device/megaphone/proc/do_broadcast(var/mob/living/user, var/message)
if(emagged)
if(insults)
- user.audible_message("[user.GetVoice()][user.GetAltName()] broadcasts, \"[pick(insultmsg)]\"")
+ var/insult = pick(insultmsg)
+ user.audible_message("[user.GetVoice()][user.GetAltName()] broadcasts, \"[insult]\"", runemessage = insult)
insults--
else
to_chat(user, "*BZZZZzzzzzt*")
else
- user.audible_message("[user.GetVoice()][user.GetAltName()] broadcasts, \"[message]\"")
+ user.audible_message("[user.GetVoice()][user.GetAltName()] broadcasts, \"[message]\"", runemessage = message)
/obj/item/device/megaphone/attack_self(var/mob/living/user)
var/message = sanitize(input(user, "Shout a message?", "Megaphone", null) as text)
@@ -131,7 +132,8 @@
/obj/item/device/megaphone/super/do_broadcast(var/mob/living/user, var/message)
if(emagged)
if(insults)
- user.audible_message("[user.GetVoice()][user.GetAltName()] broadcasts, \"[pick(insultmsg)]\"")
+ var/insult = pick(insultmsg)
+ user.audible_message("[user.GetVoice()][user.GetAltName()] broadcasts, \"[insult]\"", runemessage = insult)
if(broadcast_size >= 11)
var/turf/T = get_turf(user)
playsound(src, 'sound/items/AirHorn.ogg', 100, 1)
@@ -160,4 +162,4 @@
qdel(src)
return
else
- user.audible_message("[user.GetVoice()][user.GetAltName()] broadcasts, \"[message]\"")
+ user.audible_message("[user.GetVoice()][user.GetAltName()] broadcasts, \"[message]\"", runemessage = message)
diff --git a/code/game/objects/items/devices/taperecorder.dm b/code/game/objects/items/devices/taperecorder.dm
index bdb35fb6431..3e16975fbe3 100644
--- a/code/game/objects/items/devices/taperecorder.dm
+++ b/code/game/objects/items/devices/taperecorder.dm
@@ -258,13 +258,13 @@
var/playedmessage = mytape.storedinfo[i]
if (findtextEx(playedmessage,"*",1,2)) //remove marker for action sounds
playedmessage = copytext(playedmessage,2)
- T.audible_message("Tape Recorder: [playedmessage]")
+ T.audible_message("Tape Recorder: [playedmessage]", runemessage = playedmessage)
if(mytape.storedinfo.len < i+1)
playsleepseconds = 1
sleep(10)
T = get_turf(src)
- T.audible_message("Tape Recorder: End of recording.")
+ T.audible_message("Tape Recorder: End of recording.", runemessage = "* click *")
break
else
playsleepseconds = mytape.timestamp[i+1] - mytape.timestamp[i]
@@ -272,7 +272,7 @@
if(playsleepseconds > 14)
sleep(10)
T = get_turf(src)
- T.audible_message("Tape Recorder: Skipping [playsleepseconds] seconds of silence")
+ T.audible_message("Tape Recorder: Skipping [playsleepseconds] seconds of silence", runemessage = "* tape winding *")
playsleepseconds = 1
sleep(10 * playsleepseconds)
@@ -282,7 +282,7 @@
if(emagged)
var/turf/T = get_turf(src)
- T.audible_message("Tape Recorder: This tape recorder will self-destruct in... Five.")
+ T.audible_message("Tape Recorder: This tape recorder will self-destruct in... Five.", runemessage = "* beep beep *")
sleep(10)
T = get_turf(src)
T.audible_message("Tape Recorder: Four.")
diff --git a/code/game/objects/items/devices/text_to_speech.dm b/code/game/objects/items/devices/text_to_speech.dm
index f3feacd86ea..edbb49b6a8a 100644
--- a/code/game/objects/items/devices/text_to_speech.dm
+++ b/code/game/objects/items/devices/text_to_speech.dm
@@ -24,5 +24,6 @@
var/message = sanitize(input(user,"Choose a message to relay to those around you.") as text|null)
if(message)
- var/obj/item/device/text_to_speech/O = src
- audible_message("[bicon(O)] \The [O.name] states, \"[message]\"")
+ audible_message("[bicon(src)] \The [src.name] states, \"[message]\"", runemessage = "* synthesized speech *")
+ if(ismob(loc))
+ loc.audible_message("", runemessage = "\[TTS Voice\] [message]")
diff --git a/code/game/objects/items/devices/whistle.dm b/code/game/objects/items/devices/whistle.dm
index cf8cebeec15..54ed5404757 100644
--- a/code/game/objects/items/devices/whistle.dm
+++ b/code/game/objects/items/devices/whistle.dm
@@ -33,12 +33,12 @@
if(isnull(insults))
playsound(src, 'sound/voice/halt.ogg', 100, 1, vary = 0)
- user.audible_message("[user]'s [name] rasps, \"[use_message]\"", "\The [user] holds up \the [name].")
+ user.audible_message("[user]'s [name] rasps, \"[use_message]\"", "\The [user] holds up \the [name].", runemessage = "\[TTS Voice\] [use_message]")
else
if(insults > 0)
playsound(src, 'sound/voice/binsult.ogg', 100, 1, vary = 0)
// Yes, it used to show the transcription of the sound clip. That was a) inaccurate b) immature as shit.
- user.audible_message("[user]'s [name] gurgles something indecipherable and deeply offensive.", "\The [user] holds up \the [name].")
+ user.audible_message("[user]'s [name] gurgles something indecipherable and deeply offensive.", "\The [user] holds up \the [name].", runemessage = "\[TTS Voice\] #&@&^%(*")
insults--
else
to_chat(user, "*BZZZZZZZZT*")
diff --git a/code/game/objects/items/uav.dm b/code/game/objects/items/uav.dm
index 37dd6ec1469..168eec0a3ff 100644
--- a/code/game/objects/items/uav.dm
+++ b/code/game/objects/items/uav.dm
@@ -305,7 +305,8 @@
for(var/wr_master in masters)
var/weakref/wr = wr_master
var/mob/master = wr.resolve()
- var/message = master.combine_message(message_pieces, verb, M)
+ var/list/combined = master.combine_message(message_pieces, verb, M)
+ var/message = combined["formatted"]
var/rendered = "UAV received: [name_used] [message]"
master.show_message(rendered, 2)
diff --git a/code/game/world.dm b/code/game/world.dm
index 9f84772b9fc..2895a191dba 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -1,5 +1,6 @@
#define RECOMMENDED_VERSION 501
/world/New()
+ world_startup_time = world.timeofday
to_world_log("Map Loading Complete")
//logs
//VOREStation Edit Start
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index d8deb0dce60..b0f553a4871 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -324,8 +324,7 @@
var/message = sanitize(input("What do you want the message to be?", "Make Sound") as text|null)
if(!message)
return
- for (var/mob/V in hearers(O))
- V.show_message(message, 2)
+ O.audible_message(message)
log_admin("[key_name(usr)] made [O] at [O.x], [O.y], [O.z]. make a sound")
message_admins("[key_name_admin(usr)] made [O] at [O.x], [O.y], [O.z]. make a sound.", 1)
feedback_add_details("admin_verb","MS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
diff --git a/code/modules/admin/verbs/smite_vr.dm b/code/modules/admin/verbs/smite_vr.dm
index cbf78c6f5ae..f065293a26b 100644
--- a/code/modules/admin/verbs/smite_vr.dm
+++ b/code/modules/admin/verbs/smite_vr.dm
@@ -104,7 +104,7 @@
sleep(1 SECOND)
shadekin.dir = SOUTH
sleep(1 SECOND)
- shadekin.audible_message("[shadekin] belches loudly!")
+ shadekin.audible_message("[shadekin] belches loudly!", runemessage = "* URRRRRP *")
sleep(2 SECONDS)
shadekin.phase_shift()
target.transforming = FALSE //Undo cheap hack
diff --git a/code/modules/client/client defines.dm b/code/modules/client/client defines.dm
index 9fb7295266c..81ec230f9b6 100644
--- a/code/modules/client/client defines.dm
+++ b/code/modules/client/client defines.dm
@@ -76,3 +76,6 @@
var/connection_realtime
///world.timeofday they connected
var/connection_timeofday
+
+ // Runechat messages
+ var/list/seen_messages
diff --git a/code/modules/client/preference_setup/global/setting_datums.dm b/code/modules/client/preference_setup/global/setting_datums.dm
index 24cca9ca18b..25eae07ff1b 100644
--- a/code/modules/client/preference_setup/global/setting_datums.dm
+++ b/code/modules/client/preference_setup/global/setting_datums.dm
@@ -290,6 +290,32 @@ var/list/_client_preferences_by_type
enabled_description = "Show"
disabled_description = "Hide"
+/datum/client_preference/runechat_mob
+ description = "Runechat (Mobs)"
+ key = "RUNECHAT_MOB"
+ enabled_description = "Show"
+ disabled_description = "Hide"
+
+/datum/client_preference/runechat_obj
+ description = "Runechat (Objs)"
+ key = "RUNECHAT_OBJ"
+ enabled_description = "Show"
+ disabled_description = "Hide"
+
+/datum/client_preference/runechat_border
+ description = "Runechat Message Border"
+ key = "RUNECHAT_BORDER"
+ enabled_description = "Show"
+ disabled_description = "Hide"
+ enabled_by_default = FALSE
+
+/datum/client_preference/runechat_long_messages
+ description = "Runechat Message Length"
+ key = "RUNECHAT_LONG"
+ enabled_description = "ERP KING"
+ disabled_description = "Normie"
+ enabled_by_default = FALSE
+
/datum/client_preference/status_indicators/toggled(mob/preference_mob, enabled)
. = ..()
if(preference_mob && preference_mob.plane_holder)
diff --git a/code/modules/clothing/under/accessories/accessory_vr.dm b/code/modules/clothing/under/accessories/accessory_vr.dm
index 5422e294b4e..e9d1ae21ac3 100644
--- a/code/modules/clothing/under/accessories/accessory_vr.dm
+++ b/code/modules/clothing/under/accessories/accessory_vr.dm
@@ -124,7 +124,7 @@
if(usr.stat) return
if(!jingled)
- usr.audible_message("[usr] jingles the [src]'s bell.")
+ usr.audible_message("[usr] jingles the [src]'s bell.", runemessage = "* jingle *")
playsound(src, 'sound/items/pickup/ring.ogg', 50, 1)
jingled = 1
addtimer(CALLBACK(src, .proc/jingledreset), 50)
diff --git a/code/modules/emotes/emote_define.dm b/code/modules/emotes/emote_define.dm
index 6b50b09dde1..d8184b9bae2 100644
--- a/code/modules/emotes/emote_define.dm
+++ b/code/modules/emotes/emote_define.dm
@@ -103,11 +103,14 @@ var/global/list/emotes_by_key
if(target)
use_1p = replace_target_tokens(use_1p, target)
use_1p = "[capitalize(replace_user_tokens(use_1p, user))]"
- var/use_3p = get_emote_message_3p(user, target, extra_params)
- if(use_3p)
+ var/prefinal_3p
+ var/use_3p
+ var/raw_3p = get_emote_message_3p(user, target, extra_params)
+ if(raw_3p)
if(target)
- use_3p = replace_target_tokens(use_3p, target)
- use_3p = "\The [user] [replace_user_tokens(use_3p, user)]"
+ raw_3p = replace_target_tokens(raw_3p, target)
+ prefinal_3p = replace_user_tokens(raw_3p, user)
+ use_3p = "\The [user] [prefinal_3p]"
var/use_radio = get_radio_message(user)
if(use_radio)
if(target)
@@ -124,12 +127,12 @@ var/global/list/emotes_by_key
if(isliving(user))
var/mob/living/L = user
if(L.silent)
- M.visible_message(message = "[user] opens their mouth silently!", self_message = "You cannot say anything!", blind_message = emote_message_impaired)
+ M.visible_message(message = "[user] opens their mouth silently!", self_message = "You cannot say anything!", blind_message = emote_message_impaired, runemessage = "opens their mouth silently!")
return
else
- M.audible_message(message = use_3p, self_message = use_1p, deaf_message = emote_message_impaired, hearing_distance = use_range, radio_message = use_radio)
+ M.audible_message(message = use_3p, self_message = use_1p, deaf_message = emote_message_impaired, hearing_distance = use_range, radio_message = use_radio, runemessage = prefinal_3p)
else
- M.visible_message(message = use_3p, self_message = use_1p, blind_message = emote_message_impaired, range = use_range)
+ M.visible_message(message = use_3p, self_message = use_1p, blind_message = emote_message_impaired, range = use_range, runemessage = prefinal_3p)
do_extra(user, target)
do_sound(user)
diff --git a/code/modules/emotes/emote_mob.dm b/code/modules/emotes/emote_mob.dm
index cbc53019d86..3baccce8e32 100644
--- a/code/modules/emotes/emote_mob.dm
+++ b/code/modules/emotes/emote_mob.dm
@@ -86,7 +86,8 @@
return
if(use_emote.message_type == AUDIBLE_MESSAGE && is_muzzled())
- audible_message("\The [src] [use_emote.emote_message_muffled || "makes a muffled sound."]")
+ var/muffle_message = use_emote.emote_message_muffled || "makes a muffled sound."
+ audible_message("\The [src] [muffle_message]", runemessage = "* [muffle_message] *")
return
next_emote = world.time + use_emote.emote_delay
@@ -149,7 +150,7 @@
subtext = html_encode(subtext)
// Store the player's name in a nice bold, naturalement
nametext = "[emoter]"
- return pretext + nametext + subtext
+ return list("pretext" = pretext, "nametext" = nametext, "subtext" = subtext)
/mob/proc/custom_emote(var/m_type = VISIBLE_MESSAGE, var/message, var/range = world.view)
@@ -163,8 +164,14 @@
else
input = message
+ var/list/formatted
+ var/runemessage
if(input)
- message = format_emote(src, message)
+ formatted = format_emote(src, message)
+ message = formatted["pretext"] + formatted["nametext"] + formatted["subtext"]
+ runemessage = formatted["subtext"]
+ // This is just personal preference (but I'm objectively right) that custom emotes shouldn't have periods at the end in runechat
+ runemessage = replacetext(runemessage,".","",length(runemessage),length(runemessage)+1)
else
return
@@ -192,6 +199,7 @@
if(isobserver(M))
message = "[src] ([ghost_follow_link(src, M)]) [input]"
M.show_message(message, m_type)
+ M.create_chat_message(src, "[runemessage]", FALSE, list("emote"), (m_type == AUDIBLE_MESSAGE))
for(var/obj in o_viewers)
var/obj/O = obj
diff --git a/code/modules/integrated_electronics/subtypes/output.dm b/code/modules/integrated_electronics/subtypes/output.dm
index 39dae337848..56a6e543070 100644
--- a/code/modules/integrated_electronics/subtypes/output.dm
+++ b/code/modules/integrated_electronics/subtypes/output.dm
@@ -134,7 +134,7 @@
text = get_pin_data(IC_INPUT, 1)
if(!isnull(text))
var/obj/O = assembly ? loc : assembly
- audible_message("[bicon(O)] \The [O.name] states, \"[text]\"")
+ audible_message("[bicon(O)] \The [O.name] states, \"[text]\"", runemessage = text)
/obj/item/integrated_circuit/output/text_to_speech/advanced
name = "advanced text-to-speech circuit"
diff --git a/code/modules/mob/hear_say.dm b/code/modules/mob/hear_say.dm
index c5685aa0dcc..8e4fd3970ea 100644
--- a/code/modules/mob/hear_say.dm
+++ b/code/modules/mob/hear_say.dm
@@ -2,7 +2,9 @@
/mob/proc/combine_message(var/list/message_pieces, var/verb, var/mob/speaker, always_stars = FALSE, var/radio = FALSE)
var/iteration_count = 0
var/msg = "" // This is to make sure that the pieces have actually added something
- . = "[verb], \""
+ var/raw_msg = ""
+ . = list("formatted" = "[verb], \"", "raw" = "")
+
for(var/datum/multilingual_say_piece/SP in message_pieces)
iteration_count++
var/piece = SP.message
@@ -27,6 +29,9 @@
if(istype(S.say_list) && length(S.say_list.speak))
piece = pick(S.say_list.speak)
+ raw_msg += (piece + " ")
+
+ //HTML formatting
if(!SP.speaking) // Catch the most generic case first
piece = "[piece]"
else if(radio) // SP.speaking == TRUE enforced by previous !SP.speaking
@@ -38,10 +43,11 @@
if(msg == "")
// There is literally no content left in this message, we need to shut this shit down
- . = "" // hear_say will suppress it
+ .["formatted"] = "" // hear_say will suppress it
else
- . = trim(. + trim(msg))
- . += "\""
+ .["formatted"] = trim(.["formatted"] + trim(msg))
+ .["formatted"] += "\""
+ .["raw"] = trim(raw_msg)
/mob/proc/saypiece_scramble(datum/multilingual_say_piece/SP)
if(SP.speaking)
@@ -76,7 +82,8 @@
var/mob/living/carbon/human/H = speaker
speaker_name = H.GetVoice()
- var/message = combine_message(message_pieces, verb, speaker)
+ var/list/combined = combine_message(message_pieces, verb, speaker)
+ var/message = combined["formatted"]
if(message == "")
return
@@ -109,6 +116,7 @@
message_to_send = "[message_to_send]"
on_hear_say(message_to_send)
+ create_chat_message(speaker, combined["raw"], italics, list())
if(speech_sound && (get_dist(speaker, src) <= world.view && z == speaker.z))
var/turf/source = speaker ? get_turf(speaker) : get_turf(src)
@@ -164,7 +172,8 @@
if(!client)
return
- var/message = combine_message(message_pieces, verb, speaker, always_stars = hard_to_hear, radio = TRUE)
+ var/list/combined = combine_message(message_pieces, verb, speaker, always_stars = hard_to_hear, radio = TRUE)
+ var/message = combined["formatted"]
if(sleeping || stat == UNCONSCIOUS) //If unconscious or sleeping
hear_sleep(multilingual_to_message(message_pieces))
return
@@ -272,7 +281,8 @@
return
/mob/proc/hear_holopad_talk(list/message_pieces, var/verb = "says", var/mob/speaker = null)
- var/message = combine_message(message_pieces, verb, speaker)
+ var/list/combined = combine_message(message_pieces, verb, speaker)
+ var/message = combined["formatted"]
var/name = speaker.name
if(!say_understands(speaker))
diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm
index ae1b09ae714..5686993d7af 100644
--- a/code/modules/mob/living/say.dm
+++ b/code/modules/mob/living/say.dm
@@ -1,57 +1,63 @@
var/list/department_radio_keys = list(
- ":r" = "right ear", ".r" = "right ear",
- ":l" = "left ear", ".l" = "left ear",
- ":i" = "intercom", ".i" = "intercom",
- ":h" = "department", ".h" = "department",
- ":+" = "special", ".+" = "special", //activate radio-specific special functions
- ":c" = "Command", ".c" = "Command",
- ":n" = "Science", ".n" = "Science",
- ":m" = "Medical", ".m" = "Medical",
- ":e" = "Engineering", ".e" = "Engineering",
- ":k" = "Response Team", ".k" = "Response Team",
- ":s" = "Security", ".s" = "Security",
- ":w" = "whisper", ".w" = "whisper",
- ":t" = "Mercenary", ".t" = "Mercenary",
- ":x" = "Raider", ".x" = "Raider",
- ":u" = "Supply", ".u" = "Supply",
- ":v" = "Service", ".v" = "Service",
- ":p" = "AI Private", ".p" = "AI Private",
- ":y" = "Explorer", ".y" = "Explorer",
- ":a" = "Talon", ".a" = "Talon", //VOREStation Add,
+ ":r" = "right ear", ".r" = "right ear",
+ ":l" = "left ear", ".l" = "left ear",
+ ":i" = "intercom", ".i" = "intercom",
+ ":h" = "department", ".h" = "department",
+ ":+" = "special", ".+" = "special", //activate radio-specific special functions
+ ":c" = "Command", ".c" = "Command",
+ ":n" = "Science", ".n" = "Science",
+ ":m" = "Medical", ".m" = "Medical",
+ ":e" = "Engineering", ".e" = "Engineering",
+ ":k" = "Response Team", ".k" = "Response Team",
+ ":s" = "Security", ".s" = "Security",
+ ":w" = "whisper", ".w" = "whisper",
+ ":t" = "Mercenary", ".t" = "Mercenary",
+ ":x" = "Raider", ".x" = "Raider",
+ ":u" = "Supply", ".u" = "Supply",
+ ":v" = "Service", ".v" = "Service",
+ ":p" = "AI Private", ".p" = "AI Private",
+ ":y" = "Explorer", ".y" = "Explorer",
+ ":a" = "Talon", ".a" = "Talon", //VOREStation Add,
- ":R" = "right ear", ".R" = "right ear",
- ":L" = "left ear", ".L" = "left ear",
- ":I" = "intercom", ".I" = "intercom",
- ":H" = "department", ".H" = "department",
- ":C" = "Command", ".C" = "Command",
- ":N" = "Science", ".N" = "Science",
- ":M" = "Medical", ".M" = "Medical",
- ":E" = "Engineering", ".E" = "Engineering",
- ":k" = "Response Team", ".k" = "Response Team",
- ":S" = "Security", ".S" = "Security",
- ":W" = "whisper", ".W" = "whisper",
- ":T" = "Mercenary", ".T" = "Mercenary",
- ":X" = "Raider", ".X" = "Raider",
- ":U" = "Supply", ".U" = "Supply",
- ":V" = "Service", ".V" = "Service",
- ":P" = "AI Private", ".P" = "AI Private",
- ":Y" = "Explorer", ".Y" = "Explorer",
- ":A" = "Talon", ".A" = "Talon", //VOREStation Add,
+ ":R" = "right ear", ".R" = "right ear",
+ ":L" = "left ear", ".L" = "left ear",
+ ":I" = "intercom", ".I" = "intercom",
+ ":H" = "department", ".H" = "department",
+ ":C" = "Command", ".C" = "Command",
+ ":N" = "Science", ".N" = "Science",
+ ":M" = "Medical", ".M" = "Medical",
+ ":E" = "Engineering", ".E" = "Engineering",
+ ":k" = "Response Team", ".k" = "Response Team",
+ ":S" = "Security", ".S" = "Security",
+ ":W" = "whisper", ".W" = "whisper",
+ ":T" = "Mercenary", ".T" = "Mercenary",
+ ":X" = "Raider", ".X" = "Raider",
+ ":U" = "Supply", ".U" = "Supply",
+ ":V" = "Service", ".V" = "Service",
+ ":P" = "AI Private", ".P" = "AI Private",
+ ":Y" = "Explorer", ".Y" = "Explorer",
+ ":A" = "Talon", ".A" = "Talon", //VOREStation Add,
- //kinda localization -- rastaf0
- //same keys as above, but on russian keyboard layout. This file uses cp1251 as encoding.
- ":ê" = "right ear", ".ê" = "right ear",
- ":ä" = "left ear", ".ä" = "left ear",
- ":ø" = "intercom", ".ø" = "intercom",
- ":ð" = "department", ".ð" = "department",
- ":ñ" = "Command", ".ñ" = "Command",
- ":ò" = "Science", ".ò" = "Science",
- ":ü" = "Medical", ".ü" = "Medical",
- ":ó" = "Engineering", ".ó" = "Engineering",
- ":û" = "Security", ".û" = "Security",
- ":ö" = "whisper", ".ö" = "whisper",
- ":å" = "Mercenary", ".å" = "Mercenary",
- ":é" = "Supply", ".é" = "Supply",
+ // Cyrillic characters on the same keys on the Russian QWERTY (phonetic) layout
+ ":к" = "right ear", ".к" = "right ear",
+ ":д" = "left ear", ".д" = "left ear",
+ ":ш" = "intercom", ".ш" = "intercom",
+ ":Ñ€" = "department", ".Ñ€" = "department",
+ ":+" = "special", ".+" = "special", //activate radio-specific special functions
+ ":Ñ" = "Command", ".Ñ" = "Command",
+ ":Ñ‚" = "Science", ".Ñ‚" = "Science",
+ ":ь" = "Medical", ".ь" = "Medical",
+ ":у" = "Engineering", ".у" = "Engineering",
+ ":л" = "Response Team", ".л" = "Response Team",
+ ":Ñ‹" = "Security", ".Ñ‹" = "Security",
+ ":ц" = "whisper", ".ц" = "whisper",
+ ":е" = "Mercenary", ".е" = "Mercenary",
+ ":ч" = "Raider", ".ч" = "Raider",
+ ":г" = "Supply", ".г" = "Supply",
+ ":м" = "Service", ".м" = "Service",
+ ":з" = "AI Private", ".з" = "AI Private",
+ ":н" = "Explorer", ".н" = "Explorer",
+ ":Ñ„" = "Talon", ".Ñ„" = "Talon" //VOREStation Add
)
@@ -362,16 +368,17 @@ proc/get_radio_key_from_channel(var/channel)
//VOREStation Add End
var/dst = get_dist(get_turf(M),get_turf(src))
+ var/runechat_enabled = M.client?.is_preference_enabled(/datum/client_preference/runechat_mob)
if(dst <= message_range || (M.stat == DEAD && !forbid_seeing_deadchat)) //Inside normal message range, or dead with ears (handled in the view proc)
- if(M.client)
+ if(M.client && !runechat_enabled)
var/image/I1 = listening[M] || speech_bubble
images_to_clients[I1] |= M.client
M << I1
M.hear_say(message_pieces, verb, italics, src, speech_sound, sound_vol)
if(whispering && !isobserver(M)) //Don't even bother with these unless whispering
if(dst > message_range && dst <= w_scramble_range) //Inside whisper scramble range
- if(M.client)
+ if(M.client && !runechat_enabled)
var/image/I2 = listening[M] || speech_bubble
images_to_clients[I2] |= M.client
M << I2
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index f3cdc716e32..a4789aa7250 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -1,1002 +1,1003 @@
-#define AI_CHECK_WIRELESS 1
-#define AI_CHECK_RADIO 2
-
-var/list/ai_verbs_default = list(
- // /mob/living/silicon/ai/proc/ai_recall_shuttle,
- /mob/living/silicon/ai/proc/ai_emergency_message,
- /mob/living/silicon/ai/proc/ai_goto_location,
- /mob/living/silicon/ai/proc/ai_remove_location,
- /mob/living/silicon/ai/proc/ai_hologram_change,
- /mob/living/silicon/ai/proc/ai_network_change,
- /mob/living/silicon/ai/proc/ai_statuschange,
- /mob/living/silicon/ai/proc/ai_store_location,
- /mob/living/silicon/ai/proc/control_integrated_radio,
- /mob/living/silicon/ai/proc/pick_icon,
- /mob/living/silicon/ai/proc/sensor_mode,
- /mob/living/silicon/ai/proc/show_laws_verb,
- /mob/living/silicon/ai/proc/toggle_acceleration,
- /mob/living/silicon/ai/proc/toggle_hologram_movement,
- /mob/living/silicon/ai/proc/ai_announcement,
- /mob/living/silicon/ai/proc/ai_call_shuttle,
- /mob/living/silicon/ai/proc/ai_camera_track,
- /mob/living/silicon/ai/proc/ai_camera_list,
- /mob/living/silicon/ai/proc/ai_checklaws,
- /mob/living/silicon/ai/proc/toggle_camera_light,
- /mob/living/silicon/ai/proc/take_image,
- /mob/living/silicon/ai/proc/view_images,
- /mob/living/silicon/ai/proc/toggle_multicam_verb,
- /mob/living/silicon/ai/proc/add_multicam_verb
-)
-
-//Not sure why this is necessary...
-/proc/AutoUpdateAI(obj/subject)
- var/is_in_use = 0
- if (subject!=null)
- for(var/A in ai_list)
- var/mob/living/silicon/ai/M = A
- if ((M.client && M.machine == subject))
- is_in_use = 1
- subject.attack_ai(M)
- return is_in_use
-
-
-/mob/living/silicon/ai
- name = "AI"
- icon = 'icons/mob/AI.dmi'//
- icon_state = "ai"
- anchored = 1 // -- TLE
- density = 1
- status_flags = CANSTUN|CANPARALYSE|CANPUSH
- shouldnt_see = list(/mob/observer/eye, /obj/effect/rune)
- var/list/network = list(NETWORK_DEFAULT)
- var/obj/machinery/camera/camera = null
- var/aiRestorePowerRoutine = 0
- var/viewalerts = 0
- var/icon/holo_icon//Default is assigned when AI is created.
- var/list/connected_robots = list()
- var/obj/item/device/pda/ai/aiPDA = null
- var/obj/item/device/communicator/aiCommunicator = null
- var/obj/item/device/multitool/aiMulti = null
- var/obj/item/device/radio/headset/heads/ai_integrated/aiRadio = null
- var/camera_light_on = 0 //Defines if the AI toggled the light on the camera it's looking through.
- var/datum/trackable/track = null
- var/last_announcement = ""
- var/control_disabled = 0
- var/datum/announcement/priority/announcement
- var/obj/machinery/ai_powersupply/psupply = null // Backwards reference to AI's powersupply object.
- var/hologram_follow = 1 //This is used for the AI eye, to determine if a holopad's hologram should follow it or not.
- var/is_dummy = 0 //Used to prevent dummy AIs from spawning with communicators.
- //NEWMALF VARIABLES
- var/malfunctioning = 0 // Master var that determines if AI is malfunctioning.
- var/datum/malf_hardware/hardware = null // Installed piece of hardware.
- var/datum/malf_research/research = null // Malfunction research datum.
- var/obj/machinery/power/apc/hack = null // APC that is currently being hacked.
- var/list/hacked_apcs = null // List of all hacked APCs
- var/APU_power = 0 // If set to 1 AI runs on APU power
- var/hacking = 0 // Set to 1 if AI is hacking APC, cyborg, other AI, or running system override.
- var/system_override = 0 // Set to 1 if system override is initiated, 2 if succeeded.
- var/hack_can_fail = 1 // If 0, all abilities have zero chance of failing.
- var/hack_fails = 0 // This increments with each failed hack, and determines the warning message text.
- var/errored = 0 // Set to 1 if runtime error occurs. Only way of this happening i can think of is admin fucking up with varedit.
- var/bombing_core = 0 // Set to 1 if core auto-destruct is activated
- var/bombing_station = 0 // Set to 1 if station nuke auto-destruct is activated
- var/override_CPUStorage = 0 // Bonus/Penalty CPU Storage. For use by admins/testers.
- var/override_CPURate = 0 // Bonus/Penalty CPU generation rate. For use by admins/testers.
-
- var/datum/ai_icon/selected_sprite // The selected icon set
- var/custom_sprite = 0 // Whether the selected icon is custom
- var/carded
-
- // Multicam Vars
- var/multicam_allowed = TRUE
- var/multicam_on = FALSE
- var/obj/screen/movable/pic_in_pic/ai/master_multicam
- var/list/multicam_screens = list()
- var/list/all_eyes = list()
- var/max_multicams = 6
-
- can_be_antagged = TRUE
-
-/mob/living/silicon/ai/proc/add_ai_verbs()
- src.verbs |= ai_verbs_default
- src.verbs |= silicon_subsystems
-
-/mob/living/silicon/ai/proc/remove_ai_verbs()
- src.verbs -= ai_verbs_default
- src.verbs -= silicon_subsystems
-
-/mob/living/silicon/ai/New(loc, var/datum/ai_laws/L, var/obj/item/device/mmi/B, var/safety = 0)
- announcement = new()
- announcement.title = "A.I. Announcement"
- announcement.announcement_type = "A.I. Announcement"
- announcement.newscast = 1
-
- var/list/possibleNames = ai_names
-
- var/pickedName = null
- while(!pickedName)
- pickedName = pick(ai_names)
- for (var/mob/living/silicon/ai/A in mob_list)
- if (A.real_name == pickedName && possibleNames.len > 1) //fixing the theoretically possible infinite loop
- possibleNames -= pickedName
- pickedName = null
-
- if(!is_dummy)
- aiPDA = new/obj/item/device/pda/ai(src)
- SetName(pickedName)
- anchored = 1
- canmove = 0
- density = 1
- loc = loc
-
- if(!is_dummy)
- aiCommunicator = new /obj/item/device/communicator/integrated(src)
-
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holo1"))
-
- proc_holder_list = new()
-
- if(L)
- if (istype(L, /datum/ai_laws))
- laws = L
- else
- laws = new using_map.default_law_type
-
- aiMulti = new(src)
- aiRadio = new(src)
- common_radio = aiRadio
- aiRadio.myAi = src
- additional_law_channels["Binary"] = "#b"
- additional_law_channels["Holopad"] = ":h"
-
- aiCamera = new/obj/item/device/camera/siliconcam/ai_camera(src)
-
- if (istype(loc, /turf))
- add_ai_verbs(src)
-
- //Languages
- add_language("Robot Talk", 1)
- add_language(LANGUAGE_GALCOM, 1)
- add_language(LANGUAGE_SOL_COMMON, 1)
- add_language(LANGUAGE_UNATHI, 1)
- add_language(LANGUAGE_SIIK, 1)
- add_language(LANGUAGE_AKHANI, 1)
- add_language(LANGUAGE_SKRELLIAN, 1)
- add_language(LANGUAGE_SKRELLIANFAR, 0)
- add_language(LANGUAGE_TRADEBAND, 1)
- add_language(LANGUAGE_GUTTER, 1)
- add_language(LANGUAGE_EAL, 1)
- add_language(LANGUAGE_SCHECHI, 1)
- add_language(LANGUAGE_SIGN, 1)
- add_language(LANGUAGE_ROOTLOCAL, 1)
- add_language(LANGUAGE_TERMINUS, 1)
- add_language(LANGUAGE_ZADDAT, 1)
-
- if(!safety)//Only used by AIize() to successfully spawn an AI.
- if (!B)//If there is no player/brain inside.
- empty_playable_ai_cores += new/obj/structure/AIcore/deactivated(loc)//New empty terminal.
- qdel(src)//Delete AI.
- return
- else
- if (B.brainmob.mind)
- B.brainmob.mind.transfer_to(src)
-
- on_mob_init()
-
- spawn(5)
- new /obj/machinery/ai_powersupply(src)
-
- ai_list += src
- ..()
- return
-
-/mob/living/silicon/ai/proc/on_mob_init()
- to_chat(src, "You are playing the station's AI. The AI cannot move, but can interact with many objects while viewing them (through cameras).")
- to_chat(src, "To look at other parts of the station, click on yourself to get a camera menu.")
- to_chat(src, "While observing through a camera, you can use most (networked) devices which you can see, such as computers, APCs, intercoms, doors, etc.")
- to_chat(src, "To use something, simply click on it.")
- to_chat(src, "Use say #b to speak to your cyborgs through binary. Use say :h to speak from an active holopad.")
- to_chat(src, "For department channels, use the following say commands:")
-
- var/radio_text = ""
- for(var/i = 1 to common_radio.channels.len)
- var/channel = common_radio.channels[i]
- var/key = get_radio_key_from_channel(channel)
- radio_text += "[key] - [channel]"
- if(i != common_radio.channels.len)
- radio_text += ", "
-
- to_chat(src,radio_text)
-
- // Vorestation Edit: Meta Info for AI's. Mostly used for Holograms
- if (client)
- var/meta_info = client.prefs.metadata
- if (meta_info)
- ooc_notes = meta_info
-
- if (malf && !(mind in malf.current_antagonists))
- show_laws()
- to_chat(src, "These laws may be changed by other players, or by you being the traitor.")
-
- job = "AI"
- setup_icon()
-
-/mob/living/silicon/ai/Destroy()
- ai_list -= src
-
- QDEL_NULL(announcement)
- QDEL_NULL(eyeobj)
- QDEL_NULL(psupply)
- QDEL_NULL(aiPDA)
- QDEL_NULL(aiCommunicator)
- QDEL_NULL(aiMulti)
- QDEL_NULL(aiRadio)
- QDEL_NULL(aiCamera)
- hack = null
-
- return ..()
-
-/mob/living/silicon/ai/Stat()
- ..()
- if(statpanel("Status"))
- if(!stat) // Make sure we're not unconscious/dead.
- stat(null, text("System integrity: [(health+100)/2]%"))
- stat(null, text("Connected synthetics: [connected_robots.len]"))
- for(var/mob/living/silicon/robot/R in connected_robots)
- var/robot_status = "Nominal"
- if(R.shell)
- robot_status = "AI SHELL"
- else if(R.stat || !R.client)
- robot_status = "OFFLINE"
- else if(!R.cell || R.cell.charge <= 0)
- robot_status = "DEPOWERED"
- //Name, Health, Battery, Module, Area, and Status! Everything an AI wants to know about its borgies!
- stat(null, text("[R.name] | S.Integrity: [R.health]% | Cell: [R.cell ? "[R.cell.charge]/[R.cell.maxcharge]" : "Empty"] | \
- Module: [R.modtype] | Loc: [get_area_name(R, TRUE)] | Status: [robot_status]"))
- stat(null, text("AI shell beacons detected: [LAZYLEN(GLOB.available_ai_shells)]")) //Count of total AI shells
- else
- stat(null, text("Systems nonfunctional"))
-
-
-/mob/living/silicon/ai/proc/setup_icon()
- var/file = file2text("config/custom_sprites.txt")
- var/lines = splittext(file, "\n")
-
- for(var/line in lines)
- // split & clean up
- var/list/Entry = splittext(line, ":")
- for(var/i = 1 to Entry.len)
- Entry[i] = trim(Entry[i])
-
- if(Entry.len < 2)
- continue;
-
- if(Entry[1] == src.ckey && Entry[2] == src.real_name)
- icon = CUSTOM_ITEM_SYNTH
- custom_sprite = 1
- selected_sprite = new/datum/ai_icon("Custom", "[src.ckey]-ai", "4", "[ckey]-ai-crash", "#FFFFFF", "#FFFFFF", "#FFFFFF")
- else
- selected_sprite = default_ai_icon
- updateicon()
-
-/mob/living/silicon/ai/pointed(atom/A as mob|obj|turf in view())
- set popup_menu = 0
- set src = usr.contents
- return 0
-
-/mob/living/silicon/ai/SetName(pickedName as text)
- ..()
- announcement.announcer = pickedName
- if(eyeobj)
- eyeobj.name = "[pickedName] (AI Eye)"
-
- // Set ai pda name
- if(aiPDA)
- aiPDA.ownjob = "AI"
- aiPDA.owner = pickedName
- aiPDA.name = pickedName + " (" + aiPDA.ownjob + ")"
-
- if(aiCommunicator)
- aiCommunicator.register_device(src.name)
-
-/*
- The AI Power supply is a dummy object used for powering the AI since only machinery should be using power.
- The alternative was to rewrite a bunch of AI code instead here we are.
-*/
-/obj/machinery/ai_powersupply
- name="Power Supply"
- active_power_usage=50000 // Station AIs use significant amounts of power. This, when combined with charged SMES should mean AI lasts for 1hr without external power.
- use_power = USE_POWER_ACTIVE
- power_channel = EQUIP
- var/mob/living/silicon/ai/powered_ai = null
- invisibility = 100
-
-/obj/machinery/ai_powersupply/New(var/mob/living/silicon/ai/ai=null)
- powered_ai = ai
- powered_ai.psupply = src
- if(istype(powered_ai,/mob/living/silicon/ai/announcer)) //Don't try to get a loc for a nullspace announcer mob, just put it into it
- forceMove(powered_ai)
- else
- forceMove(powered_ai.loc)
-
- ..()
- use_power(1) // Just incase we need to wake up the power system.
-
-/obj/machinery/ai_powersupply/Destroy()
- . = ..()
- powered_ai = null
-
-/obj/machinery/ai_powersupply/process()
- if(!powered_ai || powered_ai.stat == DEAD)
- qdel(src)
- return
- if(powered_ai.psupply != src) // For some reason, the AI has different powersupply object. Delete this one, it's no longer needed.
- qdel(src)
- return
- if(powered_ai.APU_power)
- update_use_power(USE_POWER_OFF)
- return
- if(!powered_ai.anchored)
- loc = powered_ai.loc
- update_use_power(USE_POWER_OFF)
- use_power(50000) // Less optimalised but only called if AI is unwrenched. This prevents usage of wrenching as method to keep AI operational without power. Intellicard is for that.
- if(powered_ai.anchored)
- update_use_power(USE_POWER_ACTIVE)
-
-/mob/living/silicon/ai/proc/pick_icon()
- set category = "AI Settings"
- set name = "Set AI Core Display"
- if(stat || aiRestorePowerRoutine)
- return
-
- if (!custom_sprite)
- var/new_sprite = input("Select an icon!", "AI", selected_sprite) as null|anything in ai_icons
- if(new_sprite) selected_sprite = new_sprite
- updateicon()
-
-/mob/living/silicon/ai/var/message_cooldown = 0
-/mob/living/silicon/ai/proc/ai_announcement()
- set category = "AI Commands"
- set name = "Make Station Announcement"
- if(check_unable(AI_CHECK_WIRELESS | AI_CHECK_RADIO))
- return
-
- if(message_cooldown)
- to_chat(src, "Please allow one minute to pass between announcements.")
- return
- var/input = input(usr, "Please write a message to announce to the station crew.", "A.I. Announcement")
- if(!input)
- return
-
- if(check_unable(AI_CHECK_WIRELESS | AI_CHECK_RADIO))
- return
-
- announcement.Announce(input)
- message_cooldown = 1
- spawn(600)//One minute cooldown
- message_cooldown = 0
-
-/mob/living/silicon/ai/proc/ai_call_shuttle()
- set category = "AI Commands"
- set name = "Call Emergency Shuttle"
- if(check_unable(AI_CHECK_WIRELESS))
- return
-
- var/confirm = alert("Are you sure you want to call the shuttle?", "Confirm Shuttle Call", "Yes", "No")
-
- if(check_unable(AI_CHECK_WIRELESS))
- return
-
- if(confirm == "Yes")
- call_shuttle_proc(src)
-
- // hack to display shuttle timer
- if(emergency_shuttle.online())
- post_status(src, "shuttle", user = src)
-
-/mob/living/silicon/ai/proc/ai_recall_shuttle()
- set category = "AI Commands"
- set name = "Recall Emergency Shuttle"
-
- if(check_unable(AI_CHECK_WIRELESS))
- return
-
- var/confirm = alert("Are you sure you want to recall the shuttle?", "Confirm Shuttle Recall", "Yes", "No")
- if(check_unable(AI_CHECK_WIRELESS))
- return
-
- if(confirm == "Yes")
- cancel_call_proc(src)
-
-/mob/living/silicon/ai/var/emergency_message_cooldown = 0
-
-/mob/living/silicon/ai/proc/ai_emergency_message()
- set category = "AI Commands"
- set name = "Send Emergency Message"
-
- if(check_unable(AI_CHECK_WIRELESS))
- return
- if(emergency_message_cooldown)
- to_chat(usr, "Arrays recycling. Please stand by.")
- return
- var/input = sanitize(input(usr, "Please choose a message to transmit to [using_map.boss_short] via quantum entanglement. Please be aware that this process is very expensive, and abuse will lead to... termination. Transmission does not guarantee a response. There is a 30 second delay before you may send another message, be clear, full and concise.", "To abort, send an empty message.", ""))
- if(!input)
- return
- CentCom_announce(input, usr)
- to_chat(usr, "Message transmitted.")
- log_game("[key_name(usr)] has made an IA [using_map.boss_short] announcement: [input]")
- emergency_message_cooldown = 1
- spawn(300)
- emergency_message_cooldown = 0
-/mob/living/silicon/ai/check_eye(var/mob/user as mob)
- if (!camera)
- return -1
- return 0
-
-/mob/living/silicon/ai/restrained()
- return 0
-
-/mob/living/silicon/ai/emp_act(severity)
- disconnect_shell("Disconnected from remote shell due to ionic interfe%*@$^___")
- if (prob(30))
- view_core()
- ..()
-
-/mob/living/silicon/ai/Topic(href, href_list)
- if(..()) //VOREstation edit: So the AI can actually can actually get its OOC prefs read
- return
- if(usr != src)
- return
- /*if(..()) // <------ MOVED FROM HERE
- return*/
- if (href_list["mach_close"])
- if (href_list["mach_close"] == "aialerts")
- viewalerts = 0
- var/t1 = text("window=[]", href_list["mach_close"])
- unset_machine()
- src << browse(null, t1)
- if (href_list["switchcamera"])
- switchCamera(locate(href_list["switchcamera"])) in cameranet.cameras
- if (href_list["showalerts"])
- subsystem_alarm_monitor()
- //Carn: holopad requests
- if (href_list["jumptoholopad"])
- var/obj/machinery/hologram/holopad/H = locate(href_list["jumptoholopad"])
- if(stat == CONSCIOUS)
- if(H)
- H.attack_ai(src) //may as well recycle
- else
- to_chat(src, "Unable to locate the holopad.")
-
- if (href_list["track"])
- var/mob/target = locate(href_list["track"]) in mob_list
-
- if(target && (!istype(target, /mob/living/carbon/human) || html_decode(href_list["trackname"]) == target:get_face_name()))
- ai_actual_track(target)
- else
- to_chat(src, "System error. Cannot locate [html_decode(href_list["trackname"])].")
- return
-
- if(href_list["trackbot"])
- var/mob/living/bot/target = locate(href_list["trackbot"]) in mob_list
- if(target)
- ai_actual_track(target)
- else
- to_chat(src, "Target is not on or near any active cameras on the station.")
- return
-
- if(href_list["open"])
- var/mob/target = locate(href_list["open"]) in mob_list
- if(target)
- open_nearest_door(target)
-
- return
-
-/mob/living/silicon/ai/proc/camera_visibility(mob/observer/eye/aiEye/moved_eye)
- cameranet.visibility(moved_eye, client, all_eyes)
-
-/mob/living/silicon/ai/forceMove(atom/destination)
- . = ..()
- if(.)
- end_multicam()
-
-/mob/living/silicon/ai/reset_view(atom/A)
- if(camera)
- camera.set_light(0)
- if(istype(A,/obj/machinery/camera))
- camera = A
- if(A != GLOB.ai_camera_room_landmark)
- end_multicam()
- . = ..()
- if(.)
- if(!A && isturf(loc) && eyeobj)
- end_multicam()
- client.eye = eyeobj
- client.perspective = MOB_PERSPECTIVE
- if(istype(A,/obj/machinery/camera))
- if(camera_light_on) A.set_light(AI_CAMERA_LUMINOSITY)
- else A.set_light(0)
-
-
-/mob/living/silicon/ai/proc/switchCamera(var/obj/machinery/camera/C)
- if (!C || stat == DEAD) //C.can_use())
- return 0
-
- if(!src.eyeobj)
- view_core()
- return
- // ok, we're alive, camera is good and in our network...
- eyeobj.setLoc(get_turf(C))
- //machine = src
-
- return 1
-
-/mob/living/silicon/ai/cancel_camera()
- set category = "AI Commands"
- set name = "Cancel Camera View"
- view_core()
-
-//Replaces /mob/living/silicon/ai/verb/change_network() in ai.dm & camera.dm
-//Adds in /mob/living/silicon/ai/proc/ai_network_change() instead
-//Addition by Mord_Sith to define AI's network change ability
-/mob/living/silicon/ai/proc/get_camera_network_list()
- if(check_unable())
- return
-
- var/list/cameralist = new()
- for (var/obj/machinery/camera/C in cameranet.cameras)
- if(!C.can_use())
- continue
- var/list/tempnetwork = difflist(C.network,restricted_camera_networks,1)
- for(var/i in tempnetwork)
- cameralist[i] = i
-
- cameralist = sortAssoc(cameralist)
- return cameralist
-
-/mob/living/silicon/ai/proc/ai_network_change(var/network in get_camera_network_list())
- set category = "AI Commands"
- set name = "Jump To Network"
- unset_machine()
-
- if(!network)
- return
-
- if(!eyeobj)
- view_core()
- return
-
- src.network = network
-
- for(var/obj/machinery/camera/C in cameranet.cameras)
- if(!C.can_use())
- continue
- if(network in C.network)
- eyeobj.setLoc(get_turf(C))
- break
- to_chat(src, "Switched to [network] camera network.")
-//End of code by Mord_Sith
-
-/mob/living/silicon/ai/proc/ai_statuschange()
- set category = "AI Settings"
- set name = "AI Status"
-
- if(check_unable(AI_CHECK_WIRELESS))
- return
-
- set_ai_status_displays(src)
- return
-
-//I am the icon meister. Bow fefore me. //>fefore
-/mob/living/silicon/ai/proc/ai_hologram_change()
- set name = "Change Hologram"
- set desc = "Change the default hologram available to AI to something else."
- set category = "AI Settings"
-
- if(check_unable())
- return
-
- var/input
- var/choice = alert("Would you like to select a hologram based on a (visible) crew member, switch to unique avatar, or load your character from your character slot?",,"Crew Member","Unique","My Character")
-
- switch(choice)
- if("Crew Member") //A seeable crew member (or a dog)
- var/list/targets = trackable_mobs()
- if(targets.len)
- input = input("Select a crew member:") as null|anything in targets //The definition of "crew member" is a little loose...
- //This is torture, I know. If someone knows a better way...
- if(!input) return
- var/new_holo = getHologramIcon(getCompoundIcon(targets[input]))
- qdel(holo_icon)
- holo_icon = new_holo
-
- else
- alert("No suitable records found. Aborting.")
-
- if("My Character") //Loaded character slot
- if(!client || !client.prefs) return
- var/mob/living/carbon/human/dummy/dummy = new ()
- //This doesn't include custom_items because that's ... hard.
- client.prefs.dress_preview_mob(dummy)
- sleep(1 SECOND) //Strange bug in preview code? Without this, certain things won't show up. Yay race conditions?
- dummy.regenerate_icons()
-
- var/new_holo = getHologramIcon(getCompoundIcon(dummy))
- qdel(holo_icon)
- qdel(dummy)
- holo_icon = new_holo
-
- else //A premade from the dmi
- var/icon_list[] = list(
- "default",
- "floating face",
- "singularity",
- "drone",
- "carp",
- "spider",
- "bear",
- "slime",
- "ian",
- "runtime",
- "poly",
- "pun pun",
- "male human",
- "female human",
- "male unathi",
- "female unathi",
- "male tajaran",
- "female tajaran",
- "male tesharii",
- "female tesharii",
- "male skrell",
- "female skrell"
- )
- input = input("Please select a hologram:") as null|anything in icon_list
- if(input)
- qdel(holo_icon)
- switch(input)
- if("default")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holo1"))
- if("floating face")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holo2"))
- if("singularity")
- holo_icon = getHologramIcon(icon('icons/obj/singularity.dmi',"singularity_s1"))
- if("drone")
- holo_icon = getHologramIcon(icon('icons/mob/animal.dmi',"drone0"))
- if("carp")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holo4"))
- if("spider")
- holo_icon = getHologramIcon(icon('icons/mob/animal.dmi',"nurse"))
- if("bear")
- holo_icon = getHologramIcon(icon('icons/mob/animal.dmi',"brownbear"))
- if("slime")
- holo_icon = getHologramIcon(icon('icons/mob/slimes.dmi',"cerulean adult slime"))
- if("ian")
- holo_icon = getHologramIcon(icon('icons/mob/animal.dmi',"corgi"))
- if("runtime")
- holo_icon = getHologramIcon(icon('icons/mob/animal.dmi',"cat"))
- if("poly")
- holo_icon = getHologramIcon(icon('icons/mob/animal.dmi',"parrot_fly"))
- if("pun pun")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"punpun"))
- if("male human")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holohumm"))
- if("female human")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holohumf"))
- if("male unathi")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holounam"))
- if("female unathi")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holounaf"))
- if("male tajaran")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holotajm"))
- if("female tajaran")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holotajf"))
- if("male tesharii")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holotesm"))
- if("female tesharii")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holotesf"))
- if("male skrell")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holoskrm"))
- if("female skrell")
- holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holoskrf"))
-
-//Toggles the luminosity and applies it by re-entereing the camera.
-/mob/living/silicon/ai/proc/toggle_camera_light()
- set name = "Toggle Camera Light"
- set desc = "Toggles the light on the camera the AI is looking through."
- set category = "AI Commands"
- if(check_unable())
- return
-
- camera_light_on = !camera_light_on
- to_chat(src, "Camera lights [camera_light_on ? "activated" : "deactivated"].")
- if(!camera_light_on)
- if(camera)
- camera.set_light(0)
- camera = null
- else
- lightNearbyCamera()
-
-
-
-// Handled camera lighting, when toggled.
-// It will get the nearest camera from the eyeobj, lighting it.
-
-/mob/living/silicon/ai/proc/lightNearbyCamera()
- if(camera_light_on && camera_light_on < world.timeofday)
- if(src.camera)
- var/obj/machinery/camera/camera = near_range_camera(src.eyeobj)
- if(camera && src.camera != camera)
- src.camera.set_light(0)
- if(!camera.light_disabled)
- src.camera = camera
- src.camera.set_light(AI_CAMERA_LUMINOSITY)
- else
- src.camera = null
- else if(isnull(camera))
- src.camera.set_light(0)
- src.camera = null
- else
- var/obj/machinery/camera/camera = near_range_camera(src.eyeobj)
- if(camera && !camera.light_disabled)
- src.camera = camera
- src.camera.set_light(AI_CAMERA_LUMINOSITY)
- camera_light_on = world.timeofday + 1 * 20 // Update the light every 2 seconds.
-
-
-/mob/living/silicon/ai/attackby(obj/item/weapon/W as obj, mob/user as mob)
- if(istype(W, /obj/item/device/aicard))
-
- var/obj/item/device/aicard/card = W
- card.grab_ai(src, user)
-
- else if(W.is_wrench())
- if(user == deployed_shell)
- to_chat(user, "The shell's subsystems resist your efforts to tamper with your bolts.")
- return
- if(anchored)
- playsound(src, W.usesound, 50, 1)
- user.visible_message("\The [user] starts to unbolt \the [src] from the plating...")
- if(!do_after(user,40 * W.toolspeed))
- user.visible_message("\The [user] decides not to unbolt \the [src].")
- return
- user.visible_message("\The [user] finishes unfastening \the [src]!")
- anchored = 0
- return
- else
- playsound(src, W.usesound, 50, 1)
- user.visible_message("\The [user] starts to bolt \the [src] to the plating...")
- if(!do_after(user,40 * W.toolspeed))
- user.visible_message("\The [user] decides not to bolt \the [src].")
- return
- user.visible_message("\The [user] finishes fastening down \the [src]!")
- anchored = 1
- return
- else
- return ..()
-
-/mob/living/silicon/ai/proc/control_integrated_radio()
- set name = "Radio Settings"
- set desc = "Allows you to change settings of your radio."
- set category = "AI Settings"
-
- if(check_unable(AI_CHECK_RADIO))
- return
-
- to_chat(src, "Accessing Subspace Transceiver control...")
- if (src.aiRadio)
- src.aiRadio.interact(src)
-
-/mob/living/silicon/ai/proc/sensor_mode()
- set name = "Set Sensor Augmentation"
- set category = "AI Settings"
- set desc = "Augment visual feed with internal sensor overlays"
- toggle_sensor_mode()
-
-/mob/living/silicon/ai/proc/toggle_hologram_movement()
- set name = "Toggle Hologram Movement"
- set category = "AI Settings"
- set desc = "Toggles hologram movement based on moving with your virtual eye."
-
- hologram_follow = !hologram_follow
- //VOREStation Add - Required to stop movement because we use walk_to(wards) in hologram.dm
- if(holo)
- var/obj/effect/overlay/aiholo/hologram = holo.masters[src]
- walk(hologram, 0)
- //VOREStation Add End
- to_chat(usr, "Your hologram will [hologram_follow ? "follow" : "no longer follow"] you now.")
-
-
-/mob/living/silicon/ai/proc/check_unable(var/flags = 0, var/feedback = 1)
- if(stat == DEAD)
- if(feedback)
- to_chat(src, "You are dead!")
- return 1
-
- if(aiRestorePowerRoutine)
- if(feedback)
- to_chat(src, "You lack power!")
- return 1
-
- if((flags & AI_CHECK_WIRELESS) && src.control_disabled)
- if(feedback)
- to_chat(src, "Wireless control is disabled!")
- return 1
- if((flags & AI_CHECK_RADIO) && src.aiRadio.disabledAi)
- if(feedback)
- to_chat(src, "System Error - Transceiver Disabled!")
- return 1
- return 0
-
-/mob/living/silicon/ai/proc/is_in_chassis()
- return istype(loc, /turf)
-
-/mob/living/silicon/ai/proc/open_nearest_door(mob/living/target) // Rykka ports AI opening doors
- if(!istype(target))
- return
-
- if(target && ai_actual_track(target))
- var/obj/machinery/door/airlock/A = null
-
- var/dist = -1
- for(var/obj/machinery/door/airlock/D in range(3, target))
- if(!D.density)
- continue
-
- var/curr_dist = get_dist(D, target)
-
- if(dist < 0)
- dist = curr_dist
- A = D
- else if(dist > curr_dist)
- dist = curr_dist
- A = D
-
- if(istype(A))
- switch(alert(src, "Do you want to open \the [A] for [target]?", "Doorknob_v2a.exe", "Yes", "No"))
- if("Yes")
- A.AIShiftClick()
- to_chat(src, "You open \the [A] for [target].")
- else
- to_chat(src, "You deny the request.")
- else
- to_chat(src, "Unable to locate an airlock near [target].")
-
- else
- to_chat(src, "Target is not on or near any active cameras on the station.")
-
-/mob/living/silicon/ai/ex_act(var/severity)
- if(severity == 1.0)
- qdel(src)
- return
- ..()
-
-/mob/living/silicon/ai/updateicon()
- if(!selected_sprite) selected_sprite = default_ai_icon
-
- if(stat == DEAD)
- icon_state = selected_sprite.dead_icon
- set_light(3, 1, selected_sprite.dead_light)
- else if(aiRestorePowerRoutine)
- icon_state = selected_sprite.nopower_icon
- set_light(1, 1, selected_sprite.nopower_light)
- else
- icon_state = selected_sprite.alive_icon
- set_light(1, 1, selected_sprite.alive_light)
-
-// Pass lying down or getting up to our pet human, if we're in a rig.
-/mob/living/silicon/ai/lay_down()
- set name = "Rest"
- set category = "IC"
-
- resting = 0
- var/obj/item/weapon/rig/rig = src.get_rig()
- if(rig)
- rig.force_rest(src)
-
-/mob/living/silicon/ai/is_sentient()
- // AI cores don't store what brain was used to build them so we're just gonna assume they can think to some degree.
- // If that is ever fixed please update this proc.
- return TRUE
-
-
-/mob/living/silicon/ai/handle_track(message, verb = "says", mob/speaker = null, speaker_name, hard_to_hear)
- if(hard_to_hear)
- return
-
- var/jobname // the mob's "job"
- var/mob/living/carbon/human/impersonating //The crew member being impersonated, if any.
- var/changed_voice
-
- if(ishuman(speaker))
- var/mob/living/carbon/human/H = speaker
-
- if(H.wear_mask && istype(H.wear_mask,/obj/item/clothing/mask/gas/voice))
- changed_voice = 1
- var/list/impersonated = new()
- var/mob/living/carbon/human/I = impersonated[speaker_name]
-
- if(!I)
- for(var/mob/living/carbon/human/M in mob_list)
- if(M.real_name == speaker_name)
- I = M
- impersonated[speaker_name] = I
- break
-
- // If I's display name is currently different from the voice name and using an agent ID then don't impersonate
- // as this would allow the AI to track I and realize the mismatch.
- if(I && !(I.name != speaker_name && I.wear_id && istype(I.wear_id,/obj/item/weapon/card/id/syndicate)))
- impersonating = I
- jobname = impersonating.get_assignment()
- else
- jobname = "Unknown"
- else
- jobname = H.get_assignment()
-
- else if(iscarbon(speaker)) // Nonhuman carbon mob
- jobname = "No id"
- else if(isAI(speaker))
- jobname = "AI"
- else if(isrobot(speaker))
- jobname = "Cyborg"
- else if(istype(speaker, /mob/living/silicon/pai))
- jobname = "Personal AI"
- else
- jobname = "Unknown"
-
- var/track = ""
- if(changed_voice) // They have a fake name
- if(impersonating) // And we found a mob with that name above, track them instead
- track = "[speaker_name] ([jobname])"
- track += "\[OPEN\]" // Rykka ports AI opening doors
- else // We couldn't find a mob with their fake name, don't track at all
- track = "[speaker_name] ([jobname])"
- else // Not faking their name
- if(istype(speaker, /mob/living/bot)) // It's a bot, and no fake name! (That'd be kinda weird.) :p
- track = "[speaker_name] ([jobname])"
- else // It's not a bot, and no fake name!
- track = "[speaker_name] ([jobname])"
- track += "\[OPEN\]" // Rykka ports AI opening doors
-
- return track // Feed variable back to AI
-
-/mob/living/silicon/ai/proc/relay_speech(mob/living/M, list/message_pieces, verb)
- var/message = combine_message(message_pieces, verb, M)
- var/name_used = M.GetVoice()
- //This communication is imperfect because the holopad "filters" voices and is only designed to connect to the master only.
- var/rendered = "Relayed Speech: [name_used] [message]"
- show_message(rendered, 2)
-
-/mob/living/silicon/ai/proc/toggle_multicam_verb()
- set name = "Toggle Multicam"
- set category = "AI Commands"
- toggle_multicam()
-
-/mob/living/silicon/ai/proc/add_multicam_verb()
- set name = "Add Multicam Viewport"
- set category = "AI Commands"
- drop_new_multicam()
-
-//Special subtype kept around for global announcements
-/mob/living/silicon/ai/announcer
- is_dummy = 1
-
-/mob/living/silicon/ai/announcer/Initialize()
- . = ..()
- mob_list -= src
- living_mob_list -= src
- dead_mob_list -= src
- ai_list -= src
- silicon_mob_list -= src
- QDEL_NULL(eyeobj)
-
-/mob/living/silicon/ai/announcer/Life()
- mob_list -= src
- living_mob_list -= src
- dead_mob_list -= src
- ai_list -= src
- silicon_mob_list -= src
- QDEL_NULL(eyeobj)
-
-#undef AI_CHECK_WIRELESS
-#undef AI_CHECK_RADIO
+#define AI_CHECK_WIRELESS 1
+#define AI_CHECK_RADIO 2
+
+var/list/ai_verbs_default = list(
+ // /mob/living/silicon/ai/proc/ai_recall_shuttle,
+ /mob/living/silicon/ai/proc/ai_emergency_message,
+ /mob/living/silicon/ai/proc/ai_goto_location,
+ /mob/living/silicon/ai/proc/ai_remove_location,
+ /mob/living/silicon/ai/proc/ai_hologram_change,
+ /mob/living/silicon/ai/proc/ai_network_change,
+ /mob/living/silicon/ai/proc/ai_statuschange,
+ /mob/living/silicon/ai/proc/ai_store_location,
+ /mob/living/silicon/ai/proc/control_integrated_radio,
+ /mob/living/silicon/ai/proc/pick_icon,
+ /mob/living/silicon/ai/proc/sensor_mode,
+ /mob/living/silicon/ai/proc/show_laws_verb,
+ /mob/living/silicon/ai/proc/toggle_acceleration,
+ /mob/living/silicon/ai/proc/toggle_hologram_movement,
+ /mob/living/silicon/ai/proc/ai_announcement,
+ /mob/living/silicon/ai/proc/ai_call_shuttle,
+ /mob/living/silicon/ai/proc/ai_camera_track,
+ /mob/living/silicon/ai/proc/ai_camera_list,
+ /mob/living/silicon/ai/proc/ai_checklaws,
+ /mob/living/silicon/ai/proc/toggle_camera_light,
+ /mob/living/silicon/ai/proc/take_image,
+ /mob/living/silicon/ai/proc/view_images,
+ /mob/living/silicon/ai/proc/toggle_multicam_verb,
+ /mob/living/silicon/ai/proc/add_multicam_verb
+)
+
+//Not sure why this is necessary...
+/proc/AutoUpdateAI(obj/subject)
+ var/is_in_use = 0
+ if (subject!=null)
+ for(var/A in ai_list)
+ var/mob/living/silicon/ai/M = A
+ if ((M.client && M.machine == subject))
+ is_in_use = 1
+ subject.attack_ai(M)
+ return is_in_use
+
+
+/mob/living/silicon/ai
+ name = "AI"
+ icon = 'icons/mob/AI.dmi'//
+ icon_state = "ai"
+ anchored = 1 // -- TLE
+ density = 1
+ status_flags = CANSTUN|CANPARALYSE|CANPUSH
+ shouldnt_see = list(/mob/observer/eye, /obj/effect/rune)
+ var/list/network = list(NETWORK_DEFAULT)
+ var/obj/machinery/camera/camera = null
+ var/aiRestorePowerRoutine = 0
+ var/viewalerts = 0
+ var/icon/holo_icon//Default is assigned when AI is created.
+ var/list/connected_robots = list()
+ var/obj/item/device/pda/ai/aiPDA = null
+ var/obj/item/device/communicator/aiCommunicator = null
+ var/obj/item/device/multitool/aiMulti = null
+ var/obj/item/device/radio/headset/heads/ai_integrated/aiRadio = null
+ var/camera_light_on = 0 //Defines if the AI toggled the light on the camera it's looking through.
+ var/datum/trackable/track = null
+ var/last_announcement = ""
+ var/control_disabled = 0
+ var/datum/announcement/priority/announcement
+ var/obj/machinery/ai_powersupply/psupply = null // Backwards reference to AI's powersupply object.
+ var/hologram_follow = 1 //This is used for the AI eye, to determine if a holopad's hologram should follow it or not.
+ var/is_dummy = 0 //Used to prevent dummy AIs from spawning with communicators.
+ //NEWMALF VARIABLES
+ var/malfunctioning = 0 // Master var that determines if AI is malfunctioning.
+ var/datum/malf_hardware/hardware = null // Installed piece of hardware.
+ var/datum/malf_research/research = null // Malfunction research datum.
+ var/obj/machinery/power/apc/hack = null // APC that is currently being hacked.
+ var/list/hacked_apcs = null // List of all hacked APCs
+ var/APU_power = 0 // If set to 1 AI runs on APU power
+ var/hacking = 0 // Set to 1 if AI is hacking APC, cyborg, other AI, or running system override.
+ var/system_override = 0 // Set to 1 if system override is initiated, 2 if succeeded.
+ var/hack_can_fail = 1 // If 0, all abilities have zero chance of failing.
+ var/hack_fails = 0 // This increments with each failed hack, and determines the warning message text.
+ var/errored = 0 // Set to 1 if runtime error occurs. Only way of this happening i can think of is admin fucking up with varedit.
+ var/bombing_core = 0 // Set to 1 if core auto-destruct is activated
+ var/bombing_station = 0 // Set to 1 if station nuke auto-destruct is activated
+ var/override_CPUStorage = 0 // Bonus/Penalty CPU Storage. For use by admins/testers.
+ var/override_CPURate = 0 // Bonus/Penalty CPU generation rate. For use by admins/testers.
+
+ var/datum/ai_icon/selected_sprite // The selected icon set
+ var/custom_sprite = 0 // Whether the selected icon is custom
+ var/carded
+
+ // Multicam Vars
+ var/multicam_allowed = TRUE
+ var/multicam_on = FALSE
+ var/obj/screen/movable/pic_in_pic/ai/master_multicam
+ var/list/multicam_screens = list()
+ var/list/all_eyes = list()
+ var/max_multicams = 6
+
+ can_be_antagged = TRUE
+
+/mob/living/silicon/ai/proc/add_ai_verbs()
+ src.verbs |= ai_verbs_default
+ src.verbs |= silicon_subsystems
+
+/mob/living/silicon/ai/proc/remove_ai_verbs()
+ src.verbs -= ai_verbs_default
+ src.verbs -= silicon_subsystems
+
+/mob/living/silicon/ai/New(loc, var/datum/ai_laws/L, var/obj/item/device/mmi/B, var/safety = 0)
+ announcement = new()
+ announcement.title = "A.I. Announcement"
+ announcement.announcement_type = "A.I. Announcement"
+ announcement.newscast = 1
+
+ var/list/possibleNames = ai_names
+
+ var/pickedName = null
+ while(!pickedName)
+ pickedName = pick(ai_names)
+ for (var/mob/living/silicon/ai/A in mob_list)
+ if (A.real_name == pickedName && possibleNames.len > 1) //fixing the theoretically possible infinite loop
+ possibleNames -= pickedName
+ pickedName = null
+
+ if(!is_dummy)
+ aiPDA = new/obj/item/device/pda/ai(src)
+ SetName(pickedName)
+ anchored = 1
+ canmove = 0
+ density = 1
+ loc = loc
+
+ if(!is_dummy)
+ aiCommunicator = new /obj/item/device/communicator/integrated(src)
+
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holo1"))
+
+ proc_holder_list = new()
+
+ if(L)
+ if (istype(L, /datum/ai_laws))
+ laws = L
+ else
+ laws = new using_map.default_law_type
+
+ aiMulti = new(src)
+ aiRadio = new(src)
+ common_radio = aiRadio
+ aiRadio.myAi = src
+ additional_law_channels["Binary"] = "#b"
+ additional_law_channels["Holopad"] = ":h"
+
+ aiCamera = new/obj/item/device/camera/siliconcam/ai_camera(src)
+
+ if (istype(loc, /turf))
+ add_ai_verbs(src)
+
+ //Languages
+ add_language("Robot Talk", 1)
+ add_language(LANGUAGE_GALCOM, 1)
+ add_language(LANGUAGE_SOL_COMMON, 1)
+ add_language(LANGUAGE_UNATHI, 1)
+ add_language(LANGUAGE_SIIK, 1)
+ add_language(LANGUAGE_AKHANI, 1)
+ add_language(LANGUAGE_SKRELLIAN, 1)
+ add_language(LANGUAGE_SKRELLIANFAR, 0)
+ add_language(LANGUAGE_TRADEBAND, 1)
+ add_language(LANGUAGE_GUTTER, 1)
+ add_language(LANGUAGE_EAL, 1)
+ add_language(LANGUAGE_SCHECHI, 1)
+ add_language(LANGUAGE_SIGN, 1)
+ add_language(LANGUAGE_ROOTLOCAL, 1)
+ add_language(LANGUAGE_TERMINUS, 1)
+ add_language(LANGUAGE_ZADDAT, 1)
+
+ if(!safety)//Only used by AIize() to successfully spawn an AI.
+ if (!B)//If there is no player/brain inside.
+ empty_playable_ai_cores += new/obj/structure/AIcore/deactivated(loc)//New empty terminal.
+ qdel(src)//Delete AI.
+ return
+ else
+ if (B.brainmob.mind)
+ B.brainmob.mind.transfer_to(src)
+
+ on_mob_init()
+
+ spawn(5)
+ new /obj/machinery/ai_powersupply(src)
+
+ ai_list += src
+ ..()
+ return
+
+/mob/living/silicon/ai/proc/on_mob_init()
+ to_chat(src, "You are playing the station's AI. The AI cannot move, but can interact with many objects while viewing them (through cameras).")
+ to_chat(src, "To look at other parts of the station, click on yourself to get a camera menu.")
+ to_chat(src, "While observing through a camera, you can use most (networked) devices which you can see, such as computers, APCs, intercoms, doors, etc.")
+ to_chat(src, "To use something, simply click on it.")
+ to_chat(src, "Use say #b to speak to your cyborgs through binary. Use say :h to speak from an active holopad.")
+ to_chat(src, "For department channels, use the following say commands:")
+
+ var/radio_text = ""
+ for(var/i = 1 to common_radio.channels.len)
+ var/channel = common_radio.channels[i]
+ var/key = get_radio_key_from_channel(channel)
+ radio_text += "[key] - [channel]"
+ if(i != common_radio.channels.len)
+ radio_text += ", "
+
+ to_chat(src,radio_text)
+
+ // Vorestation Edit: Meta Info for AI's. Mostly used for Holograms
+ if (client)
+ var/meta_info = client.prefs.metadata
+ if (meta_info)
+ ooc_notes = meta_info
+
+ if (malf && !(mind in malf.current_antagonists))
+ show_laws()
+ to_chat(src, "These laws may be changed by other players, or by you being the traitor.")
+
+ job = "AI"
+ setup_icon()
+
+/mob/living/silicon/ai/Destroy()
+ ai_list -= src
+
+ QDEL_NULL(announcement)
+ QDEL_NULL(eyeobj)
+ QDEL_NULL(psupply)
+ QDEL_NULL(aiPDA)
+ QDEL_NULL(aiCommunicator)
+ QDEL_NULL(aiMulti)
+ QDEL_NULL(aiRadio)
+ QDEL_NULL(aiCamera)
+ hack = null
+
+ return ..()
+
+/mob/living/silicon/ai/Stat()
+ ..()
+ if(statpanel("Status"))
+ if(!stat) // Make sure we're not unconscious/dead.
+ stat(null, text("System integrity: [(health+100)/2]%"))
+ stat(null, text("Connected synthetics: [connected_robots.len]"))
+ for(var/mob/living/silicon/robot/R in connected_robots)
+ var/robot_status = "Nominal"
+ if(R.shell)
+ robot_status = "AI SHELL"
+ else if(R.stat || !R.client)
+ robot_status = "OFFLINE"
+ else if(!R.cell || R.cell.charge <= 0)
+ robot_status = "DEPOWERED"
+ //Name, Health, Battery, Module, Area, and Status! Everything an AI wants to know about its borgies!
+ stat(null, text("[R.name] | S.Integrity: [R.health]% | Cell: [R.cell ? "[R.cell.charge]/[R.cell.maxcharge]" : "Empty"] | \
+ Module: [R.modtype] | Loc: [get_area_name(R, TRUE)] | Status: [robot_status]"))
+ stat(null, text("AI shell beacons detected: [LAZYLEN(GLOB.available_ai_shells)]")) //Count of total AI shells
+ else
+ stat(null, text("Systems nonfunctional"))
+
+
+/mob/living/silicon/ai/proc/setup_icon()
+ var/file = file2text("config/custom_sprites.txt")
+ var/lines = splittext(file, "\n")
+
+ for(var/line in lines)
+ // split & clean up
+ var/list/Entry = splittext(line, ":")
+ for(var/i = 1 to Entry.len)
+ Entry[i] = trim(Entry[i])
+
+ if(Entry.len < 2)
+ continue;
+
+ if(Entry[1] == src.ckey && Entry[2] == src.real_name)
+ icon = CUSTOM_ITEM_SYNTH
+ custom_sprite = 1
+ selected_sprite = new/datum/ai_icon("Custom", "[src.ckey]-ai", "4", "[ckey]-ai-crash", "#FFFFFF", "#FFFFFF", "#FFFFFF")
+ else
+ selected_sprite = default_ai_icon
+ updateicon()
+
+/mob/living/silicon/ai/pointed(atom/A as mob|obj|turf in view())
+ set popup_menu = 0
+ set src = usr.contents
+ return 0
+
+/mob/living/silicon/ai/SetName(pickedName as text)
+ ..()
+ announcement.announcer = pickedName
+ if(eyeobj)
+ eyeobj.name = "[pickedName] (AI Eye)"
+
+ // Set ai pda name
+ if(aiPDA)
+ aiPDA.ownjob = "AI"
+ aiPDA.owner = pickedName
+ aiPDA.name = pickedName + " (" + aiPDA.ownjob + ")"
+
+ if(aiCommunicator)
+ aiCommunicator.register_device(src.name)
+
+/*
+ The AI Power supply is a dummy object used for powering the AI since only machinery should be using power.
+ The alternative was to rewrite a bunch of AI code instead here we are.
+*/
+/obj/machinery/ai_powersupply
+ name="Power Supply"
+ active_power_usage=50000 // Station AIs use significant amounts of power. This, when combined with charged SMES should mean AI lasts for 1hr without external power.
+ use_power = USE_POWER_ACTIVE
+ power_channel = EQUIP
+ var/mob/living/silicon/ai/powered_ai = null
+ invisibility = 100
+
+/obj/machinery/ai_powersupply/New(var/mob/living/silicon/ai/ai=null)
+ powered_ai = ai
+ powered_ai.psupply = src
+ if(istype(powered_ai,/mob/living/silicon/ai/announcer)) //Don't try to get a loc for a nullspace announcer mob, just put it into it
+ forceMove(powered_ai)
+ else
+ forceMove(powered_ai.loc)
+
+ ..()
+ use_power(1) // Just incase we need to wake up the power system.
+
+/obj/machinery/ai_powersupply/Destroy()
+ . = ..()
+ powered_ai = null
+
+/obj/machinery/ai_powersupply/process()
+ if(!powered_ai || powered_ai.stat == DEAD)
+ qdel(src)
+ return
+ if(powered_ai.psupply != src) // For some reason, the AI has different powersupply object. Delete this one, it's no longer needed.
+ qdel(src)
+ return
+ if(powered_ai.APU_power)
+ update_use_power(USE_POWER_OFF)
+ return
+ if(!powered_ai.anchored)
+ loc = powered_ai.loc
+ update_use_power(USE_POWER_OFF)
+ use_power(50000) // Less optimalised but only called if AI is unwrenched. This prevents usage of wrenching as method to keep AI operational without power. Intellicard is for that.
+ if(powered_ai.anchored)
+ update_use_power(USE_POWER_ACTIVE)
+
+/mob/living/silicon/ai/proc/pick_icon()
+ set category = "AI Settings"
+ set name = "Set AI Core Display"
+ if(stat || aiRestorePowerRoutine)
+ return
+
+ if (!custom_sprite)
+ var/new_sprite = input("Select an icon!", "AI", selected_sprite) as null|anything in ai_icons
+ if(new_sprite) selected_sprite = new_sprite
+ updateicon()
+
+/mob/living/silicon/ai/var/message_cooldown = 0
+/mob/living/silicon/ai/proc/ai_announcement()
+ set category = "AI Commands"
+ set name = "Make Station Announcement"
+ if(check_unable(AI_CHECK_WIRELESS | AI_CHECK_RADIO))
+ return
+
+ if(message_cooldown)
+ to_chat(src, "Please allow one minute to pass between announcements.")
+ return
+ var/input = input(usr, "Please write a message to announce to the station crew.", "A.I. Announcement")
+ if(!input)
+ return
+
+ if(check_unable(AI_CHECK_WIRELESS | AI_CHECK_RADIO))
+ return
+
+ announcement.Announce(input)
+ message_cooldown = 1
+ spawn(600)//One minute cooldown
+ message_cooldown = 0
+
+/mob/living/silicon/ai/proc/ai_call_shuttle()
+ set category = "AI Commands"
+ set name = "Call Emergency Shuttle"
+ if(check_unable(AI_CHECK_WIRELESS))
+ return
+
+ var/confirm = alert("Are you sure you want to call the shuttle?", "Confirm Shuttle Call", "Yes", "No")
+
+ if(check_unable(AI_CHECK_WIRELESS))
+ return
+
+ if(confirm == "Yes")
+ call_shuttle_proc(src)
+
+ // hack to display shuttle timer
+ if(emergency_shuttle.online())
+ post_status(src, "shuttle", user = src)
+
+/mob/living/silicon/ai/proc/ai_recall_shuttle()
+ set category = "AI Commands"
+ set name = "Recall Emergency Shuttle"
+
+ if(check_unable(AI_CHECK_WIRELESS))
+ return
+
+ var/confirm = alert("Are you sure you want to recall the shuttle?", "Confirm Shuttle Recall", "Yes", "No")
+ if(check_unable(AI_CHECK_WIRELESS))
+ return
+
+ if(confirm == "Yes")
+ cancel_call_proc(src)
+
+/mob/living/silicon/ai/var/emergency_message_cooldown = 0
+
+/mob/living/silicon/ai/proc/ai_emergency_message()
+ set category = "AI Commands"
+ set name = "Send Emergency Message"
+
+ if(check_unable(AI_CHECK_WIRELESS))
+ return
+ if(emergency_message_cooldown)
+ to_chat(usr, "Arrays recycling. Please stand by.")
+ return
+ var/input = sanitize(input(usr, "Please choose a message to transmit to [using_map.boss_short] via quantum entanglement. Please be aware that this process is very expensive, and abuse will lead to... termination. Transmission does not guarantee a response. There is a 30 second delay before you may send another message, be clear, full and concise.", "To abort, send an empty message.", ""))
+ if(!input)
+ return
+ CentCom_announce(input, usr)
+ to_chat(usr, "Message transmitted.")
+ log_game("[key_name(usr)] has made an IA [using_map.boss_short] announcement: [input]")
+ emergency_message_cooldown = 1
+ spawn(300)
+ emergency_message_cooldown = 0
+/mob/living/silicon/ai/check_eye(var/mob/user as mob)
+ if (!camera)
+ return -1
+ return 0
+
+/mob/living/silicon/ai/restrained()
+ return 0
+
+/mob/living/silicon/ai/emp_act(severity)
+ disconnect_shell("Disconnected from remote shell due to ionic interfe%*@$^___")
+ if (prob(30))
+ view_core()
+ ..()
+
+/mob/living/silicon/ai/Topic(href, href_list)
+ if(..()) //VOREstation edit: So the AI can actually can actually get its OOC prefs read
+ return
+ if(usr != src)
+ return
+ /*if(..()) // <------ MOVED FROM HERE
+ return*/
+ if (href_list["mach_close"])
+ if (href_list["mach_close"] == "aialerts")
+ viewalerts = 0
+ var/t1 = text("window=[]", href_list["mach_close"])
+ unset_machine()
+ src << browse(null, t1)
+ if (href_list["switchcamera"])
+ switchCamera(locate(href_list["switchcamera"])) in cameranet.cameras
+ if (href_list["showalerts"])
+ subsystem_alarm_monitor()
+ //Carn: holopad requests
+ if (href_list["jumptoholopad"])
+ var/obj/machinery/hologram/holopad/H = locate(href_list["jumptoholopad"])
+ if(stat == CONSCIOUS)
+ if(H)
+ H.attack_ai(src) //may as well recycle
+ else
+ to_chat(src, "Unable to locate the holopad.")
+
+ if (href_list["track"])
+ var/mob/target = locate(href_list["track"]) in mob_list
+
+ if(target && (!istype(target, /mob/living/carbon/human) || html_decode(href_list["trackname"]) == target:get_face_name()))
+ ai_actual_track(target)
+ else
+ to_chat(src, "System error. Cannot locate [html_decode(href_list["trackname"])].")
+ return
+
+ if(href_list["trackbot"])
+ var/mob/living/bot/target = locate(href_list["trackbot"]) in mob_list
+ if(target)
+ ai_actual_track(target)
+ else
+ to_chat(src, "Target is not on or near any active cameras on the station.")
+ return
+
+ if(href_list["open"])
+ var/mob/target = locate(href_list["open"]) in mob_list
+ if(target)
+ open_nearest_door(target)
+
+ return
+
+/mob/living/silicon/ai/proc/camera_visibility(mob/observer/eye/aiEye/moved_eye)
+ cameranet.visibility(moved_eye, client, all_eyes)
+
+/mob/living/silicon/ai/forceMove(atom/destination)
+ . = ..()
+ if(.)
+ end_multicam()
+
+/mob/living/silicon/ai/reset_view(atom/A)
+ if(camera)
+ camera.set_light(0)
+ if(istype(A,/obj/machinery/camera))
+ camera = A
+ if(A != GLOB.ai_camera_room_landmark)
+ end_multicam()
+ . = ..()
+ if(.)
+ if(!A && isturf(loc) && eyeobj)
+ end_multicam()
+ client.eye = eyeobj
+ client.perspective = MOB_PERSPECTIVE
+ if(istype(A,/obj/machinery/camera))
+ if(camera_light_on) A.set_light(AI_CAMERA_LUMINOSITY)
+ else A.set_light(0)
+
+
+/mob/living/silicon/ai/proc/switchCamera(var/obj/machinery/camera/C)
+ if (!C || stat == DEAD) //C.can_use())
+ return 0
+
+ if(!src.eyeobj)
+ view_core()
+ return
+ // ok, we're alive, camera is good and in our network...
+ eyeobj.setLoc(get_turf(C))
+ //machine = src
+
+ return 1
+
+/mob/living/silicon/ai/cancel_camera()
+ set category = "AI Commands"
+ set name = "Cancel Camera View"
+ view_core()
+
+//Replaces /mob/living/silicon/ai/verb/change_network() in ai.dm & camera.dm
+//Adds in /mob/living/silicon/ai/proc/ai_network_change() instead
+//Addition by Mord_Sith to define AI's network change ability
+/mob/living/silicon/ai/proc/get_camera_network_list()
+ if(check_unable())
+ return
+
+ var/list/cameralist = new()
+ for (var/obj/machinery/camera/C in cameranet.cameras)
+ if(!C.can_use())
+ continue
+ var/list/tempnetwork = difflist(C.network,restricted_camera_networks,1)
+ for(var/i in tempnetwork)
+ cameralist[i] = i
+
+ cameralist = sortAssoc(cameralist)
+ return cameralist
+
+/mob/living/silicon/ai/proc/ai_network_change(var/network in get_camera_network_list())
+ set category = "AI Commands"
+ set name = "Jump To Network"
+ unset_machine()
+
+ if(!network)
+ return
+
+ if(!eyeobj)
+ view_core()
+ return
+
+ src.network = network
+
+ for(var/obj/machinery/camera/C in cameranet.cameras)
+ if(!C.can_use())
+ continue
+ if(network in C.network)
+ eyeobj.setLoc(get_turf(C))
+ break
+ to_chat(src, "Switched to [network] camera network.")
+//End of code by Mord_Sith
+
+/mob/living/silicon/ai/proc/ai_statuschange()
+ set category = "AI Settings"
+ set name = "AI Status"
+
+ if(check_unable(AI_CHECK_WIRELESS))
+ return
+
+ set_ai_status_displays(src)
+ return
+
+//I am the icon meister. Bow fefore me. //>fefore
+/mob/living/silicon/ai/proc/ai_hologram_change()
+ set name = "Change Hologram"
+ set desc = "Change the default hologram available to AI to something else."
+ set category = "AI Settings"
+
+ if(check_unable())
+ return
+
+ var/input
+ var/choice = alert("Would you like to select a hologram based on a (visible) crew member, switch to unique avatar, or load your character from your character slot?",,"Crew Member","Unique","My Character")
+
+ switch(choice)
+ if("Crew Member") //A seeable crew member (or a dog)
+ var/list/targets = trackable_mobs()
+ if(targets.len)
+ input = input("Select a crew member:") as null|anything in targets //The definition of "crew member" is a little loose...
+ //This is torture, I know. If someone knows a better way...
+ if(!input) return
+ var/new_holo = getHologramIcon(getCompoundIcon(targets[input]))
+ qdel(holo_icon)
+ holo_icon = new_holo
+
+ else
+ alert("No suitable records found. Aborting.")
+
+ if("My Character") //Loaded character slot
+ if(!client || !client.prefs) return
+ var/mob/living/carbon/human/dummy/dummy = new ()
+ //This doesn't include custom_items because that's ... hard.
+ client.prefs.dress_preview_mob(dummy)
+ sleep(1 SECOND) //Strange bug in preview code? Without this, certain things won't show up. Yay race conditions?
+ dummy.regenerate_icons()
+
+ var/new_holo = getHologramIcon(getCompoundIcon(dummy))
+ qdel(holo_icon)
+ qdel(dummy)
+ holo_icon = new_holo
+
+ else //A premade from the dmi
+ var/icon_list[] = list(
+ "default",
+ "floating face",
+ "singularity",
+ "drone",
+ "carp",
+ "spider",
+ "bear",
+ "slime",
+ "ian",
+ "runtime",
+ "poly",
+ "pun pun",
+ "male human",
+ "female human",
+ "male unathi",
+ "female unathi",
+ "male tajaran",
+ "female tajaran",
+ "male tesharii",
+ "female tesharii",
+ "male skrell",
+ "female skrell"
+ )
+ input = input("Please select a hologram:") as null|anything in icon_list
+ if(input)
+ qdel(holo_icon)
+ switch(input)
+ if("default")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holo1"))
+ if("floating face")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holo2"))
+ if("singularity")
+ holo_icon = getHologramIcon(icon('icons/obj/singularity.dmi',"singularity_s1"))
+ if("drone")
+ holo_icon = getHologramIcon(icon('icons/mob/animal.dmi',"drone0"))
+ if("carp")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holo4"))
+ if("spider")
+ holo_icon = getHologramIcon(icon('icons/mob/animal.dmi',"nurse"))
+ if("bear")
+ holo_icon = getHologramIcon(icon('icons/mob/animal.dmi',"brownbear"))
+ if("slime")
+ holo_icon = getHologramIcon(icon('icons/mob/slimes.dmi',"cerulean adult slime"))
+ if("ian")
+ holo_icon = getHologramIcon(icon('icons/mob/animal.dmi',"corgi"))
+ if("runtime")
+ holo_icon = getHologramIcon(icon('icons/mob/animal.dmi',"cat"))
+ if("poly")
+ holo_icon = getHologramIcon(icon('icons/mob/animal.dmi',"parrot_fly"))
+ if("pun pun")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"punpun"))
+ if("male human")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holohumm"))
+ if("female human")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holohumf"))
+ if("male unathi")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holounam"))
+ if("female unathi")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holounaf"))
+ if("male tajaran")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holotajm"))
+ if("female tajaran")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holotajf"))
+ if("male tesharii")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holotesm"))
+ if("female tesharii")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holotesf"))
+ if("male skrell")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holoskrm"))
+ if("female skrell")
+ holo_icon = getHologramIcon(icon('icons/mob/AI.dmi',"holoskrf"))
+
+//Toggles the luminosity and applies it by re-entereing the camera.
+/mob/living/silicon/ai/proc/toggle_camera_light()
+ set name = "Toggle Camera Light"
+ set desc = "Toggles the light on the camera the AI is looking through."
+ set category = "AI Commands"
+ if(check_unable())
+ return
+
+ camera_light_on = !camera_light_on
+ to_chat(src, "Camera lights [camera_light_on ? "activated" : "deactivated"].")
+ if(!camera_light_on)
+ if(camera)
+ camera.set_light(0)
+ camera = null
+ else
+ lightNearbyCamera()
+
+
+
+// Handled camera lighting, when toggled.
+// It will get the nearest camera from the eyeobj, lighting it.
+
+/mob/living/silicon/ai/proc/lightNearbyCamera()
+ if(camera_light_on && camera_light_on < world.timeofday)
+ if(src.camera)
+ var/obj/machinery/camera/camera = near_range_camera(src.eyeobj)
+ if(camera && src.camera != camera)
+ src.camera.set_light(0)
+ if(!camera.light_disabled)
+ src.camera = camera
+ src.camera.set_light(AI_CAMERA_LUMINOSITY)
+ else
+ src.camera = null
+ else if(isnull(camera))
+ src.camera.set_light(0)
+ src.camera = null
+ else
+ var/obj/machinery/camera/camera = near_range_camera(src.eyeobj)
+ if(camera && !camera.light_disabled)
+ src.camera = camera
+ src.camera.set_light(AI_CAMERA_LUMINOSITY)
+ camera_light_on = world.timeofday + 1 * 20 // Update the light every 2 seconds.
+
+
+/mob/living/silicon/ai/attackby(obj/item/weapon/W as obj, mob/user as mob)
+ if(istype(W, /obj/item/device/aicard))
+
+ var/obj/item/device/aicard/card = W
+ card.grab_ai(src, user)
+
+ else if(W.is_wrench())
+ if(user == deployed_shell)
+ to_chat(user, "The shell's subsystems resist your efforts to tamper with your bolts.")
+ return
+ if(anchored)
+ playsound(src, W.usesound, 50, 1)
+ user.visible_message("\The [user] starts to unbolt \the [src] from the plating...")
+ if(!do_after(user,40 * W.toolspeed))
+ user.visible_message("\The [user] decides not to unbolt \the [src].")
+ return
+ user.visible_message("\The [user] finishes unfastening \the [src]!")
+ anchored = 0
+ return
+ else
+ playsound(src, W.usesound, 50, 1)
+ user.visible_message("\The [user] starts to bolt \the [src] to the plating...")
+ if(!do_after(user,40 * W.toolspeed))
+ user.visible_message("\The [user] decides not to bolt \the [src].")
+ return
+ user.visible_message("\The [user] finishes fastening down \the [src]!")
+ anchored = 1
+ return
+ else
+ return ..()
+
+/mob/living/silicon/ai/proc/control_integrated_radio()
+ set name = "Radio Settings"
+ set desc = "Allows you to change settings of your radio."
+ set category = "AI Settings"
+
+ if(check_unable(AI_CHECK_RADIO))
+ return
+
+ to_chat(src, "Accessing Subspace Transceiver control...")
+ if (src.aiRadio)
+ src.aiRadio.interact(src)
+
+/mob/living/silicon/ai/proc/sensor_mode()
+ set name = "Set Sensor Augmentation"
+ set category = "AI Settings"
+ set desc = "Augment visual feed with internal sensor overlays"
+ toggle_sensor_mode()
+
+/mob/living/silicon/ai/proc/toggle_hologram_movement()
+ set name = "Toggle Hologram Movement"
+ set category = "AI Settings"
+ set desc = "Toggles hologram movement based on moving with your virtual eye."
+
+ hologram_follow = !hologram_follow
+ //VOREStation Add - Required to stop movement because we use walk_to(wards) in hologram.dm
+ if(holo)
+ var/obj/effect/overlay/aiholo/hologram = holo.masters[src]
+ walk(hologram, 0)
+ //VOREStation Add End
+ to_chat(usr, "Your hologram will [hologram_follow ? "follow" : "no longer follow"] you now.")
+
+
+/mob/living/silicon/ai/proc/check_unable(var/flags = 0, var/feedback = 1)
+ if(stat == DEAD)
+ if(feedback)
+ to_chat(src, "You are dead!")
+ return 1
+
+ if(aiRestorePowerRoutine)
+ if(feedback)
+ to_chat(src, "You lack power!")
+ return 1
+
+ if((flags & AI_CHECK_WIRELESS) && src.control_disabled)
+ if(feedback)
+ to_chat(src, "Wireless control is disabled!")
+ return 1
+ if((flags & AI_CHECK_RADIO) && src.aiRadio.disabledAi)
+ if(feedback)
+ to_chat(src, "System Error - Transceiver Disabled!")
+ return 1
+ return 0
+
+/mob/living/silicon/ai/proc/is_in_chassis()
+ return istype(loc, /turf)
+
+/mob/living/silicon/ai/proc/open_nearest_door(mob/living/target) // Rykka ports AI opening doors
+ if(!istype(target))
+ return
+
+ if(target && ai_actual_track(target))
+ var/obj/machinery/door/airlock/A = null
+
+ var/dist = -1
+ for(var/obj/machinery/door/airlock/D in range(3, target))
+ if(!D.density)
+ continue
+
+ var/curr_dist = get_dist(D, target)
+
+ if(dist < 0)
+ dist = curr_dist
+ A = D
+ else if(dist > curr_dist)
+ dist = curr_dist
+ A = D
+
+ if(istype(A))
+ switch(alert(src, "Do you want to open \the [A] for [target]?", "Doorknob_v2a.exe", "Yes", "No"))
+ if("Yes")
+ A.AIShiftClick()
+ to_chat(src, "You open \the [A] for [target].")
+ else
+ to_chat(src, "You deny the request.")
+ else
+ to_chat(src, "Unable to locate an airlock near [target].")
+
+ else
+ to_chat(src, "Target is not on or near any active cameras on the station.")
+
+/mob/living/silicon/ai/ex_act(var/severity)
+ if(severity == 1.0)
+ qdel(src)
+ return
+ ..()
+
+/mob/living/silicon/ai/updateicon()
+ if(!selected_sprite) selected_sprite = default_ai_icon
+
+ if(stat == DEAD)
+ icon_state = selected_sprite.dead_icon
+ set_light(3, 1, selected_sprite.dead_light)
+ else if(aiRestorePowerRoutine)
+ icon_state = selected_sprite.nopower_icon
+ set_light(1, 1, selected_sprite.nopower_light)
+ else
+ icon_state = selected_sprite.alive_icon
+ set_light(1, 1, selected_sprite.alive_light)
+
+// Pass lying down or getting up to our pet human, if we're in a rig.
+/mob/living/silicon/ai/lay_down()
+ set name = "Rest"
+ set category = "IC"
+
+ resting = 0
+ var/obj/item/weapon/rig/rig = src.get_rig()
+ if(rig)
+ rig.force_rest(src)
+
+/mob/living/silicon/ai/is_sentient()
+ // AI cores don't store what brain was used to build them so we're just gonna assume they can think to some degree.
+ // If that is ever fixed please update this proc.
+ return TRUE
+
+
+/mob/living/silicon/ai/handle_track(message, verb = "says", mob/speaker = null, speaker_name, hard_to_hear)
+ if(hard_to_hear)
+ return
+
+ var/jobname // the mob's "job"
+ var/mob/living/carbon/human/impersonating //The crew member being impersonated, if any.
+ var/changed_voice
+
+ if(ishuman(speaker))
+ var/mob/living/carbon/human/H = speaker
+
+ if(H.wear_mask && istype(H.wear_mask,/obj/item/clothing/mask/gas/voice))
+ changed_voice = 1
+ var/list/impersonated = new()
+ var/mob/living/carbon/human/I = impersonated[speaker_name]
+
+ if(!I)
+ for(var/mob/living/carbon/human/M in mob_list)
+ if(M.real_name == speaker_name)
+ I = M
+ impersonated[speaker_name] = I
+ break
+
+ // If I's display name is currently different from the voice name and using an agent ID then don't impersonate
+ // as this would allow the AI to track I and realize the mismatch.
+ if(I && !(I.name != speaker_name && I.wear_id && istype(I.wear_id,/obj/item/weapon/card/id/syndicate)))
+ impersonating = I
+ jobname = impersonating.get_assignment()
+ else
+ jobname = "Unknown"
+ else
+ jobname = H.get_assignment()
+
+ else if(iscarbon(speaker)) // Nonhuman carbon mob
+ jobname = "No id"
+ else if(isAI(speaker))
+ jobname = "AI"
+ else if(isrobot(speaker))
+ jobname = "Cyborg"
+ else if(istype(speaker, /mob/living/silicon/pai))
+ jobname = "Personal AI"
+ else
+ jobname = "Unknown"
+
+ var/track = ""
+ if(changed_voice) // They have a fake name
+ if(impersonating) // And we found a mob with that name above, track them instead
+ track = "[speaker_name] ([jobname])"
+ track += "\[OPEN\]" // Rykka ports AI opening doors
+ else // We couldn't find a mob with their fake name, don't track at all
+ track = "[speaker_name] ([jobname])"
+ else // Not faking their name
+ if(istype(speaker, /mob/living/bot)) // It's a bot, and no fake name! (That'd be kinda weird.) :p
+ track = "[speaker_name] ([jobname])"
+ else // It's not a bot, and no fake name!
+ track = "[speaker_name] ([jobname])"
+ track += "\[OPEN\]" // Rykka ports AI opening doors
+
+ return track // Feed variable back to AI
+
+/mob/living/silicon/ai/proc/relay_speech(mob/living/M, list/message_pieces, verb)
+ var/list/combined = combine_message(message_pieces, verb, M)
+ var/message = combined["formatted"]
+ var/name_used = M.GetVoice()
+ //This communication is imperfect because the holopad "filters" voices and is only designed to connect to the master only.
+ var/rendered = "Relayed Speech: [name_used] [message]"
+ show_message(rendered, 2)
+
+/mob/living/silicon/ai/proc/toggle_multicam_verb()
+ set name = "Toggle Multicam"
+ set category = "AI Commands"
+ toggle_multicam()
+
+/mob/living/silicon/ai/proc/add_multicam_verb()
+ set name = "Add Multicam Viewport"
+ set category = "AI Commands"
+ drop_new_multicam()
+
+//Special subtype kept around for global announcements
+/mob/living/silicon/ai/announcer
+ is_dummy = 1
+
+/mob/living/silicon/ai/announcer/Initialize()
+ . = ..()
+ mob_list -= src
+ living_mob_list -= src
+ dead_mob_list -= src
+ ai_list -= src
+ silicon_mob_list -= src
+ QDEL_NULL(eyeobj)
+
+/mob/living/silicon/ai/announcer/Life()
+ mob_list -= src
+ living_mob_list -= src
+ dead_mob_list -= src
+ ai_list -= src
+ silicon_mob_list -= src
+ QDEL_NULL(eyeobj)
+
+#undef AI_CHECK_WIRELESS
+#undef AI_CHECK_RADIO
diff --git a/code/modules/mob/living/silicon/say.dm b/code/modules/mob/living/silicon/say.dm
index 34f6096ede0..daa144bf898 100644
--- a/code/modules/mob/living/silicon/say.dm
+++ b/code/modules/mob/living/silicon/say.dm
@@ -1,126 +1,127 @@
-/mob/living/silicon/robot/handle_message_mode(message_mode, message, verb, speaking, used_radios)
- ..()
- if(message_mode)
- if(!is_component_functioning("radio"))
- to_chat(src, "Your radio isn't functional at this time.")
- return 0
- if(message_mode == "general")
- message_mode = null
- return radio.talk_into(src,message,message_mode,verb,speaking)
-
-/mob/living/silicon/speech_bubble_appearance()
- return "synthetic"
-
-/mob/living/silicon/ai/handle_message_mode(message_mode, message, verb, speaking, used_radios)
- ..()
- if(message_mode == "department")
- return holopad_talk(message, verb, speaking)
- else if(message_mode)
- if (aiRadio.disabledAi || aiRestorePowerRoutine || stat)
- to_chat(src, "System Error - Transceiver Disabled.")
- return 0
- if(message_mode == "general")
- message_mode = null
- return aiRadio.talk_into(src,message,message_mode,verb,speaking)
-
-/mob/living/silicon/pai/handle_message_mode(message_mode, message, verb, speaking, used_radios)
- ..()
- if(message_mode)
- if(message_mode == "general")
- message_mode = null
- return radio.talk_into(src,message,message_mode,verb,speaking)
-
-/mob/living/silicon/say_quote(var/text)
- var/ending = copytext(text, length(text))
-
- if (ending == "?")
- return speak_query
- else if (ending == "!")
- return speak_exclamation
-
- return speak_statement
-
-#define IS_AI 1
-#define IS_ROBOT 2
-#define IS_PAI 3
-
-/mob/living/silicon/say_understands(var/other, var/datum/language/speaking = null)
- //These only pertain to common. Languages are handled by mob/say_understands()
- if(!speaking)
- if(iscarbon(other))
- return TRUE
- if(issilicon(other))
- return TRUE
- if(isbrain(other))
- return TRUE
- return ..()
-
-//For holopads only. Usable by AI.
-/mob/living/silicon/ai/proc/holopad_talk(list/message_pieces, verb)
- log_say("(HPAD) [multilingual_to_message(message_pieces)]",src)
-
- var/obj/machinery/hologram/holopad/T = src.holo
- if(T && T.masters[src])//If there is a hologram and its master is the user.
- var/list/listeners = get_mobs_and_objs_in_view_fast(get_turf(T), world.view)
- var/list/listening = listeners["mobs"]
- var/list/listening_obj = listeners["objs"]
- for(var/mob/M in listening)
- M.hear_holopad_talk(message_pieces, verb, src)
- for(var/obj/O in listening_obj)
- if(O == T) //Don't recieve your own speech
- continue
- O.hear_talk(src, message_pieces, verb)
- /*Radios "filter out" this conversation channel so we don't need to account for them.
- This is another way of saying that we won't bother dealing with them.*/
- to_chat(src, "Holopad transmitted, [real_name] [combine_message(message_pieces, verb, src)]")
- else
- to_chat(src, "No holopad connected.")
- return 0
- return 1
-
-/mob/living/silicon/ai/proc/holopad_emote(var/message) //This is called when the AI uses the 'me' verb while using a holopad.
- message = trim(message)
-
- if(!message)
- return
-
- var/obj/machinery/hologram/holopad/T = src.holo
- if(T && T.masters[src])
- var/rendered = "[name] [message]"
- to_chat(src, "Holopad action relayed, [real_name] [message]")
- var/obj/effect/overlay/aiholo/hologram = T.masters[src] //VOREStation Add for people in the hologram to hear the messages
-
- //var/obj/effect/overlay/hologram = T.masters[src] //VOREStation edit. Done above.
- var/list/in_range = get_mobs_and_objs_in_view_fast(get_turf(hologram), world.view, 2) //Emotes are displayed from the hologram, not the pad
- var/list/m_viewers = in_range["mobs"]
- var/list/o_viewers = in_range["objs"]
-
- for(var/mob/M in m_viewers)
- spawn(0)
- if(M)
- M.show_message(rendered, 2)
-
- for(var/obj/O in o_viewers)
- if(O == T)
- continue
- spawn(0)
- if(O)
- O.see_emote(src, message)
-
- log_emote("(HPAD) [message]", src)
-
- else //This shouldn't occur, but better safe then sorry.
- to_chat(src, "No holopad connected.")
- return 0
- return 1
-
-/mob/living/silicon/ai/emote(var/act, var/m_type, var/message)
- var/obj/machinery/hologram/holopad/T = holo
- if(T && T.masters[src]) //Is the AI using a holopad?
- . = holopad_emote(message)
- else //Emote normally, then.
- . = ..()
-
-#undef IS_AI
-#undef IS_ROBOT
-#undef IS_PAI
+/mob/living/silicon/robot/handle_message_mode(message_mode, message, verb, speaking, used_radios)
+ ..()
+ if(message_mode)
+ if(!is_component_functioning("radio"))
+ to_chat(src, "Your radio isn't functional at this time.")
+ return 0
+ if(message_mode == "general")
+ message_mode = null
+ return radio.talk_into(src,message,message_mode,verb,speaking)
+
+/mob/living/silicon/speech_bubble_appearance()
+ return "synthetic"
+
+/mob/living/silicon/ai/handle_message_mode(message_mode, message, verb, speaking, used_radios)
+ ..()
+ if(message_mode == "department")
+ return holopad_talk(message, verb, speaking)
+ else if(message_mode)
+ if (aiRadio.disabledAi || aiRestorePowerRoutine || stat)
+ to_chat(src, "System Error - Transceiver Disabled.")
+ return 0
+ if(message_mode == "general")
+ message_mode = null
+ return aiRadio.talk_into(src,message,message_mode,verb,speaking)
+
+/mob/living/silicon/pai/handle_message_mode(message_mode, message, verb, speaking, used_radios)
+ ..()
+ if(message_mode)
+ if(message_mode == "general")
+ message_mode = null
+ return radio.talk_into(src,message,message_mode,verb,speaking)
+
+/mob/living/silicon/say_quote(var/text)
+ var/ending = copytext(text, length(text))
+
+ if (ending == "?")
+ return speak_query
+ else if (ending == "!")
+ return speak_exclamation
+
+ return speak_statement
+
+#define IS_AI 1
+#define IS_ROBOT 2
+#define IS_PAI 3
+
+/mob/living/silicon/say_understands(var/other, var/datum/language/speaking = null)
+ //These only pertain to common. Languages are handled by mob/say_understands()
+ if(!speaking)
+ if(iscarbon(other))
+ return TRUE
+ if(issilicon(other))
+ return TRUE
+ if(isbrain(other))
+ return TRUE
+ return ..()
+
+//For holopads only. Usable by AI.
+/mob/living/silicon/ai/proc/holopad_talk(list/message_pieces, verb)
+ log_say("(HPAD) [multilingual_to_message(message_pieces)]",src)
+
+ var/obj/machinery/hologram/holopad/T = src.holo
+ if(T && T.masters[src])//If there is a hologram and its master is the user.
+ var/list/listeners = get_mobs_and_objs_in_view_fast(get_turf(T), world.view)
+ var/list/listening = listeners["mobs"]
+ var/list/listening_obj = listeners["objs"]
+ for(var/mob/M in listening)
+ M.hear_holopad_talk(message_pieces, verb, src)
+ for(var/obj/O in listening_obj)
+ if(O == T) //Don't recieve your own speech
+ continue
+ O.hear_talk(src, message_pieces, verb)
+ /*Radios "filter out" this conversation channel so we don't need to account for them.
+ This is another way of saying that we won't bother dealing with them.*/
+ var/list/combined = combine_message(message_pieces, verb, src)
+ to_chat(src, "Holopad transmitted, [real_name] [combined["formatted"]]")
+ else
+ to_chat(src, "No holopad connected.")
+ return 0
+ return 1
+
+/mob/living/silicon/ai/proc/holopad_emote(var/message) //This is called when the AI uses the 'me' verb while using a holopad.
+ message = trim(message)
+
+ if(!message)
+ return
+
+ var/obj/machinery/hologram/holopad/T = src.holo
+ if(T && T.masters[src])
+ var/rendered = "[name] [message]"
+ to_chat(src, "Holopad action relayed, [real_name] [message]")
+ var/obj/effect/overlay/aiholo/hologram = T.masters[src] //VOREStation Add for people in the hologram to hear the messages
+
+ //var/obj/effect/overlay/hologram = T.masters[src] //VOREStation edit. Done above.
+ var/list/in_range = get_mobs_and_objs_in_view_fast(get_turf(hologram), world.view, 2) //Emotes are displayed from the hologram, not the pad
+ var/list/m_viewers = in_range["mobs"]
+ var/list/o_viewers = in_range["objs"]
+
+ for(var/mob/M in m_viewers)
+ spawn(0)
+ if(M)
+ M.show_message(rendered, 2)
+
+ for(var/obj/O in o_viewers)
+ if(O == T)
+ continue
+ spawn(0)
+ if(O)
+ O.see_emote(src, message)
+
+ log_emote("(HPAD) [message]", src)
+
+ else //This shouldn't occur, but better safe then sorry.
+ to_chat(src, "No holopad connected.")
+ return 0
+ return 1
+
+/mob/living/silicon/ai/emote(var/act, var/m_type, var/message)
+ var/obj/machinery/hologram/holopad/T = holo
+ if(T && T.masters[src]) //Is the AI using a holopad?
+ . = holopad_emote(message)
+ else //Emote normally, then.
+ . = ..()
+
+#undef IS_AI
+#undef IS_ROBOT
+#undef IS_PAI
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 00d705f8e1a..547bb19304e 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -77,7 +77,7 @@
// message is the message output to anyone who can see e.g. "[src] does something!"
// self_message (optional) is what the src mob sees e.g. "You do something!"
// blind_message (optional) is what blind people will hear e.g. "You hear something!"
-/mob/visible_message(var/message, var/self_message, var/blind_message, var/list/exclude_mobs = null, var/range = world.view)
+/mob/visible_message(var/message, var/self_message, var/blind_message, var/list/exclude_mobs = null, var/range = world.view, var/runemessage)
if(self_message)
if(LAZYLEN(exclude_mobs))
exclude_mobs |= src
@@ -87,7 +87,9 @@
// Transfer messages about what we are doing to upstairs
if(shadow)
shadow.visible_message(message, self_message, blind_message, exclude_mobs, range)
- . = ..(message, blind_message, exclude_mobs, range) // Really not ideal that atom/visible_message has different arg numbering :(
+ if(isnull(runemessage))
+ runemessage = -1
+ . = ..(message, blind_message, exclude_mobs, range, runemessage) // Really not ideal that atom/visible_message has different arg numbering :(
// Returns an amount of power drawn from the object (-1 if it's not viable).
// If drain_check is set it will not actually drain power, just return a value.
@@ -102,7 +104,7 @@
// self_message (optional) is what the src mob hears.
// deaf_message (optional) is what deaf people will see.
// hearing_distance (optional) is the range, how many tiles away the message can be heard.
-/mob/audible_message(var/message, var/deaf_message, var/hearing_distance, var/self_message, var/radio_message)
+/mob/audible_message(var/message, var/deaf_message, var/hearing_distance, var/self_message, var/radio_message, var/runemessage)
var/range = hearing_distance || world.view
var/list/hear = get_mobs_and_objs_in_view_fast(get_turf(src),range,remote_ghosts = FALSE)
@@ -110,6 +112,9 @@
var/list/hearing_mobs = hear["mobs"]
var/list/hearing_objs = hear["objs"]
+ if(isnull(runemessage))
+ runemessage = -1 // Symmetry with mob/audible_message, despite the fact this one doesn't call parent. Maybe it should!
+
if(radio_message)
for(var/obj in hearing_objs)
var/obj/O = obj
@@ -125,6 +130,8 @@
if(self_message && M==src)
msg = self_message
M.show_message(msg, AUDIBLE_MESSAGE, deaf_message, VISIBLE_MESSAGE)
+ if(runemessage != -1)
+ M.create_chat_message(src, "[runemessage || message]", FALSE, list("emote"), audible = FALSE)
/mob/proc/findname(msg)
for(var/mob/M in mob_list)
diff --git a/code/modules/mob/mob_defines_vr.dm b/code/modules/mob/mob_defines_vr.dm
index ff5f1ae5826..3854620e7c1 100644
--- a/code/modules/mob/mob_defines_vr.dm
+++ b/code/modules/mob/mob_defines_vr.dm
@@ -7,6 +7,8 @@
var/obj/screen/shadekin/shadekin_display = null
var/obj/screen/xenochimera/danger_level/xenochimera_danger_display = null
+ var/size_multiplier = 1 //multiplier for the mob's icon size
+
/mob/drop_location()
if(temporary_form)
return temporary_form.drop_location()
diff --git a/code/modules/multiz/ladders.dm b/code/modules/multiz/ladders.dm
index cca3019543c..c9797203027 100644
--- a/code/modules/multiz/ladders.dm
+++ b/code/modules/multiz/ladders.dm
@@ -98,7 +98,7 @@
"You begin climbing [direction] \the [src]!",
"You hear the grunting and clanging of a metal ladder being used.")
- target_ladder.audible_message("You hear something coming [direction] \the [src]")
+ target_ladder.audible_message("You hear something coming [direction] \the [src]", runemessage = "* clank clank *")
if(do_after(M, climb_time, src))
var/turf/T = get_turf(target_ladder)
diff --git a/code/modules/multiz/movement.dm b/code/modules/multiz/movement.dm
index 40447a5a4b9..1c197530e5e 100644
--- a/code/modules/multiz/movement.dm
+++ b/code/modules/multiz/movement.dm
@@ -59,7 +59,7 @@
if(lattice)
var/pull_up_time = max(5 SECONDS + (src.movement_delay() * 10), 1)
to_chat(src, "You grab \the [lattice] and start pulling yourself upward...")
- destination.audible_message("You hear something climbing up \the [lattice].")
+ destination.audible_message("You hear something climbing up \the [lattice].", runemessage = "* clank clang *")
if(do_after(src, pull_up_time))
to_chat(src, "You pull yourself up.")
else
@@ -74,7 +74,7 @@
if(!destination?.Enter(src, old_dest))
to_chat(src, "There's something in the way up above in that direction, try another.")
return 0
- destination.audible_message("You hear something climbing up \the [catwalk].")
+ destination.audible_message("You hear something climbing up \the [catwalk].", runemessage = "* clank clang *")
if(do_after(src, pull_up_time))
to_chat(src, "You pull yourself up.")
else
@@ -90,8 +90,8 @@
return 0
var/fly_time = max(7 SECONDS + (H.movement_delay() * 10), 1) //So it's not too useful for combat. Could make this variable somehow, but that's down the road.
to_chat(src, "You begin to fly upwards...")
- destination.audible_message("You hear the flapping of wings.")
- H.audible_message("[H] begins to flap \his wings, preparing to move upwards!")
+ destination.audible_message("You hear the flapping of wings.", runemessage = "* flap flap *")
+ H.audible_message("[H] begins to flap \his wings, preparing to move upwards!", runemessage = "* flap flap *")
if(do_after(H, fly_time) && H.flying)
to_chat(src, "You fly upwards.")
else
diff --git a/code/modules/nifsoft/software/14_commlink.dm b/code/modules/nifsoft/software/14_commlink.dm
index 34e36f65276..93e1af799c3 100644
--- a/code/modules/nifsoft/software/14_commlink.dm
+++ b/code/modules/nifsoft/software/14_commlink.dm
@@ -77,7 +77,8 @@
mobs_to_relay = in_range["mobs"]
for(var/mob/mob in mobs_to_relay)
- var/message = mob.combine_message(message_pieces, verb, M)
+ var/list/combined = mob.combine_message(message_pieces, verb, M)
+ var/message = combined["formatted"]
var/name_used = M.GetVoice()
var/rendered = null
rendered = "[bicon(icon_object)] [name_used] [message]"
diff --git a/code/modules/overmap/ships/engines/gas_thruster.dm b/code/modules/overmap/ships/engines/gas_thruster.dm
index 667cbae441b..182fd6c3f32 100644
--- a/code/modules/overmap/ships/engines/gas_thruster.dm
+++ b/code/modules/overmap/ships/engines/gas_thruster.dm
@@ -150,7 +150,7 @@
if(!is_on())
return 0
if(!check_fuel() || (use_power_oneoff(charge_per_burn) < charge_per_burn) || check_blockage())
- audible_message(src,"[src] coughs once and goes silent!")
+ audible_message(src,"[src] coughs once and goes silent!", runemessage = "* sputtercough *")
update_use_power(USE_POWER_OFF)
return 0
diff --git a/code/modules/paperwork/photocopier.dm b/code/modules/paperwork/photocopier.dm
index 07009a99c09..2306b3eca1f 100644
--- a/code/modules/paperwork/photocopier.dm
+++ b/code/modules/paperwork/photocopier.dm
@@ -111,28 +111,28 @@
playsound(src, "sound/machines/copier.ogg", 100, 1)
sleep(11)
copy(copyitem)
- audible_message("You can hear [src] whirring as it finishes printing.")
+ audible_message("You can hear [src] whirring as it finishes printing.", runemessage = "* whirr *")
playsound(src, "sound/machines/buzzbeep.ogg", 30)
else if (istype(copyitem, /obj/item/weapon/photo))
playsound(src, "sound/machines/copier.ogg", 100, 1)
sleep(11)
photocopy(copyitem)
- audible_message("You can hear [src] whirring as it finishes printing.")
+ audible_message("You can hear [src] whirring as it finishes printing.", runemessage = "* whirr *")
playsound(src, "sound/machines/buzzbeep.ogg", 30)
else if (istype(copyitem, /obj/item/weapon/paper_bundle))
sleep(11)
playsound(src, "sound/machines/copier.ogg", 100, 1)
var/obj/item/weapon/paper_bundle/B = bundlecopy(copyitem)
sleep(11*B.pages.len)
- audible_message("You can hear [src] whirring as it finishes printing.")
+ audible_message("You can hear [src] whirring as it finishes printing.", runemessage = "* whirr *")
playsound(src, "sound/machines/buzzbeep.ogg", 30)
else if (has_buckled_mobs()) // VOREStation EDIT: For ass-copying.
playsound(src, "sound/machines/copier.ogg", 100, 1)
- audible_message("You can hear [src] whirring as it attempts to scan.")
+ audible_message("You can hear [src] whirring as it attempts to scan.", runemessage = "* whirr *")
sleep(rand(20,45)) // Sit with your bare ass on the copier for a random time, feel like a fool, get stared at.
copyass(user)
sleep(15)
- audible_message("You can hear [src] whirring as it finishes printing.")
+ audible_message("You can hear [src] whirring as it finishes printing.", runemessage = "* whirr *")
playsound(src, "sound/machines/buzzbeep.ogg", 30)
else
to_chat(user, "\The [copyitem] can't be copied by [src].")
diff --git a/code/modules/projectiles/guns/magnetic/bore.dm b/code/modules/projectiles/guns/magnetic/bore.dm
index 897a43d8880..a39e383afff 100644
--- a/code/modules/projectiles/guns/magnetic/bore.dm
+++ b/code/modules/projectiles/guns/magnetic/bore.dm
@@ -208,7 +208,7 @@
/obj/item/weapon/gun/magnetic/matfed/phoronbore/process()
if(generator_state && !mat_storage)
- audible_message(SPAN_NOTICE("\The [src] goes quiet."),SPAN_NOTICE("A motor noise cuts out."))
+ audible_message(SPAN_NOTICE("\The [src] goes quiet."),SPAN_NOTICE("A motor noise cuts out."), runemessage = "* goes quiet *")
soundloop.stop()
generator_state = GEN_OFF
@@ -258,12 +258,12 @@
soundloop.start()
time_started = world.time
cell?.use(100)
- audible_message(SPAN_NOTICE("\The [src] starts chugging."),SPAN_NOTICE("A motor noise starts up."))
+ audible_message(SPAN_NOTICE("\The [src] starts chugging."),SPAN_NOTICE("A motor noise starts up."), runemessage = "* whirr *")
generator_state = GEN_IDLE
else if(generator_state > GEN_OFF && time_started + 3 SECONDS < world.time)
soundloop.stop()
- audible_message(SPAN_NOTICE("\The [src] goes quiet."),SPAN_NOTICE("A motor noise cuts out."))
+ audible_message(SPAN_NOTICE("\The [src] goes quiet."),SPAN_NOTICE("A motor noise cuts out."), runemessage = "* goes quiet *")
generator_state = GEN_OFF
/obj/item/weapon/gun/magnetic/matfed/phoronbore/loaded
diff --git a/code/modules/projectiles/guns/magnetic/magnetic.dm b/code/modules/projectiles/guns/magnetic/magnetic.dm
index 1b13bd8f8c4..babeb7643a1 100644
--- a/code/modules/projectiles/guns/magnetic/magnetic.dm
+++ b/code/modules/projectiles/guns/magnetic/magnetic.dm
@@ -284,7 +284,7 @@
visible_message("\The [src] begins to rattle, its acceleration chamber collapsing in on itself!")
removable_components = FALSE
spawn(15)
- audible_message("\The [src]'s power supply begins to overload as the device crumples!") //Why are you still holding this?
+ audible_message("\The [src]'s power supply begins to overload as the device crumples!", runemessage = "* VWRRRRRRRR *") //Why are you still holding this?
playsound(src, 'sound/effects/grillehit.ogg', 10, 1)
var/datum/effect/effect/system/spark_spread/sparks = new /datum/effect/effect/system/spark_spread()
var/turf/T = get_turf(src)
diff --git a/code/modules/research/message_server.dm b/code/modules/research/message_server.dm
index 81f888155d1..ae745dcc1fd 100644
--- a/code/modules/research/message_server.dm
+++ b/code/modules/research/message_server.dm
@@ -130,12 +130,12 @@ var/global/list/obj/machinery/message_server/message_servers = list()
if(2)
if(!Console.silent)
playsound(Console, 'sound/machines/twobeep.ogg', 50, 1)
- Console.audible_message(text("[bicon(Console)] *The Requests Console beeps: 'PRIORITY Alert in [sender]'"),,5)
+ Console.audible_message(text("[bicon(Console)] *The Requests Console beeps: 'PRIORITY Alert in [sender]'"),,5, runemessage = "* beep! beep! *")
Console.message_log += list(list("High Priority message from [sender]", "[authmsg]"))
else
if(!Console.silent)
playsound(Console, 'sound/machines/twobeep.ogg', 50, 1)
- Console.audible_message(text("[bicon(Console)] *The Requests Console beeps: 'Message from [sender]'"),,4)
+ Console.audible_message(text("[bicon(Console)] *The Requests Console beeps: 'Message from [sender]'"),,4, runemessage = "* beep beep *")
Console.message_log += list(list("Message from [sender]", "[authmsg]"))
Console.set_light(2)
diff --git a/code/modules/resleeving/machines.dm b/code/modules/resleeving/machines.dm
index 1ddab8b9b42..c8b697220de 100644
--- a/code/modules/resleeving/machines.dm
+++ b/code/modules/resleeving/machines.dm
@@ -148,7 +148,7 @@
else if(((occupant.health == occupant.maxHealth)) && (!eject_wait))
playsound(src, 'sound/machines/ding.ogg', 50, 1)
- audible_message("\The [src] signals that the growing process is complete.")
+ audible_message("\The [src] signals that the growing process is complete.", runemessage = "* ding *")
connected_message("Growing Process Complete.")
locked = 0
go_out()
diff --git a/code/modules/shuttles/shuttles_web.dm b/code/modules/shuttles/shuttles_web.dm
index ea11f21d482..7f4cc22abaa 100644
--- a/code/modules/shuttles/shuttles_web.dm
+++ b/code/modules/shuttles/shuttles_web.dm
@@ -119,7 +119,7 @@
continue
if(!H.shuttle_comp || !(get_area(H) in shuttle_area))
H.shuttle_comp = null
- H.audible_message("\The [H] pings as it loses it's connection with the ship.")
+ H.audible_message("\The [H] pings as it loses it's connection with the ship.", runemessage = "* ping *")
H.update_hud("discon")
helmets -= H
else
diff --git a/code/modules/turbolift/turbolift.dm b/code/modules/turbolift/turbolift.dm
index 25a5f70601f..d8f8373f096 100644
--- a/code/modules/turbolift/turbolift.dm
+++ b/code/modules/turbolift/turbolift.dm
@@ -28,7 +28,7 @@
priority_mode = TRUE
cancel_pending_floors()
update_ext_panel_icons()
- control_panel_interior.audible_message("This turbolift is responding to a priority call. Please exit the lift when it stops and make way.")
+ control_panel_interior.audible_message("This turbolift is responding to a priority call. Please exit the lift when it stops and make way.", runemessage = "* BUZZ *")
spawn(time)
priority_mode = FALSE
update_ext_panel_icons()
@@ -158,7 +158,7 @@
doors_closing = 0
if(!fire_mode)
open_doors()
- control_panel_interior.audible_message("\The [current_floor.ext_panel] buzzes loudly.")
+ control_panel_interior.audible_message("\The [current_floor.ext_panel] buzzes loudly.", runemessage = "* BUZZ *")
playsound(control_panel_interior, "sound/machines/buzz-two.ogg", 50, 1)
return 0
diff --git a/code/modules/turbolift/turbolift_console.dm b/code/modules/turbolift/turbolift_console.dm
index 9868188d189..e3719bc97ec 100644
--- a/code/modules/turbolift/turbolift_console.dm
+++ b/code/modules/turbolift/turbolift_console.dm
@@ -130,10 +130,10 @@
return
lift.update_fire_mode(!lift.fire_mode)
if(lift.fire_mode)
- audible_message("Firefighter Mode Activated. Door safeties disabled. Manual control engaged.")
+ audible_message("Firefighter Mode Activated. Door safeties disabled. Manual control engaged.", runemessage = "* SCREECH *")
playsound(src, 'sound/machines/airalarm.ogg', 25, 0, 4, volume_channel = VOLUME_CHANNEL_ALARMS)
else
- audible_message("Firefighter Mode Deactivated. Door safeties enabled. Automatic control engaged.")
+ audible_message("Firefighter Mode Deactivated. Door safeties enabled. Automatic control engaged.", runemessage = "* ding *")
return
. = ..()
diff --git a/code/modules/turbolift/turbolift_door.dm b/code/modules/turbolift/turbolift_door.dm
index 4b83bc60cf9..d21e344097f 100644
--- a/code/modules/turbolift/turbolift_door.dm
+++ b/code/modules/turbolift/turbolift_door.dm
@@ -40,7 +40,7 @@
if(!moved) // nowhere to go....
LM.gib()
else // the mob is too big to just move, so we need to give up what we're doing
- audible_message("\The [src]'s motors grind as they quickly reverse direction, unable to safely close.")
+ audible_message("\The [src]'s motors grind as they quickly reverse direction, unable to safely close.", runemessage = "* WRRRRR *")
cur_command = null // the door will just keep trying otherwise
return 0
return ..()
\ No newline at end of file
diff --git a/code/modules/ventcrawl/ventcrawl_atmospherics.dm b/code/modules/ventcrawl/ventcrawl_atmospherics.dm
index 1fd9dd077ca..80348c76994 100644
--- a/code/modules/ventcrawl/ventcrawl_atmospherics.dm
+++ b/code/modules/ventcrawl/ventcrawl_atmospherics.dm
@@ -42,7 +42,16 @@
user.client.eye = target_move //if we don't do this, Byond only updates the eye every tick - required for smooth movement
if(world.time > user.next_play_vent)
user.next_play_vent = world.time+30
- playsound(src, 'sound/machines/ventcrawl.ogg', 50, 1, -3)
+ var/turf/T = get_turf(src)
+ playsound(T, 'sound/machines/ventcrawl.ogg', 50, 1, -3)
+ var/message = pick(
+ prob(90);"* clunk *",
+ prob(90);"* thud *",
+ prob(90);"* clatter *",
+ prob(1);"* à¶ž *"
+ )
+ T.runechat_message(message)
+
else
if((direction & initialize_directions) || is_type_in_list(src, ventcrawl_machinery) && src.can_crawl_through()) //if we move in a way the pipe can connect, but doesn't - or we're in a vent
user.remove_ventcrawl()
diff --git a/code/modules/vore/fluffstuff/custom_items_vr.dm b/code/modules/vore/fluffstuff/custom_items_vr.dm
index ba633a1cf24..8ba4a14b3a9 100644
--- a/code/modules/vore/fluffstuff/custom_items_vr.dm
+++ b/code/modules/vore/fluffstuff/custom_items_vr.dm
@@ -551,7 +551,7 @@
//He's dead, jim
if((state == 1) && owner && (owner.stat == DEAD))
update_state(2)
- audible_message("The [name] begins flashing red.")
+ visible_message("The [name] begins flashing red.")
sleep(30)
visible_message("The [name] shatters into dust!")
if(owner_c)
diff --git a/code/modules/vore/resizing/resize_vr.dm b/code/modules/vore/resizing/resize_vr.dm
index 04c3e47dba2..8dc575ce208 100644
--- a/code/modules/vore/resizing/resize_vr.dm
+++ b/code/modules/vore/resizing/resize_vr.dm
@@ -1,7 +1,6 @@
// Adding needed defines to /mob/living
// Note: Polaris had this on /mob/living/carbon/human We need it higher up for animals and stuff.
/mob/living
- var/size_multiplier = 1 //multiplier for the mob's icon size
var/holder_default
var/step_mechanics_pref = TRUE // Allow participation in macro-micro step mechanics
var/pickup_pref = TRUE // Allow participation in macro-micro pickup mechanics
diff --git a/interface/skin.dmf b/interface/skin.dmf
index fc14eb8a69d..b1d9a6267e2 100644
--- a/interface/skin.dmf
+++ b/interface/skin.dmf
@@ -1282,7 +1282,7 @@ window "mapwindow"
saved-params = "icon-size"
on-show = ".winset\"mainwindow.mainvsplit.left=mapwindow\""
on-hide = ".winset\"mainwindow.mainvsplit.left=\""
- style=".center { text-align: center; } .maptext { font-family: 'Small Fonts'; font-size: 7px; -dm-text-outline: 1px black; color: white; line-height: 1.1; } .small { font-size: 6px; } .big { font-size: 8px; } .reallybig { font-size: 8px; } .extremelybig { font-size: 8px; } .clown { color: #FF69Bf;} .tajaran {color: #803B56;} .skrell {color: #00CED1;} .solcom {color: #22228B;} .com_srus {color: #7c4848;} .zombie {color: #ff0000;} .soghun {color: #228B22;} .vox {color: #AA00AA;} .diona {color: #804000; font-weight: bold;} .trinary {color: #727272;} .kidan {color: #664205;} .slime {color: #0077AA;} .drask {color: #a3d4eb;} .vulpkanin {color: #B97A57;} .abductor {color: #800080; font-style: italic;} .his_grace { color: #15D512; } .hypnophrase { color: #0d0d0d; font-weight: bold; } .yell { font-weight: bold; }"
+ style=".center { text-align: center; } .runechatdiv {background-color: #20202070} .black_outline { -dm-text-outline: 1px black } .boldtext { font-weight: bold; } .maptext { font-family: 'Small Fonts'; font-size: 7px; color: white; line-height: 1.1; } .command_headset { font-weight: bold; font-size: 8px; } .small { font-size: 6px; } .very_small { font-size: 5px;} .big { font-size: 8px; } .reallybig { font-size: 8px; } .extremelybig { font-size: 8px; } .greentext { color: #00FF00; font-size: 7px; } .redtext { color: #FF0000; font-size: 7px; } .clown { color: #FF69Bf; font-size: 7px; font-weight: bold; } .his_grace { color: #15D512; } .hypnophrase { color: #0d0d0d; font-weight: bold; } .yell { font-weight: bold; } .italics { font-size: 7px; font-style: italic; }"
window "outputwindow"
elem "outputwindow"
diff --git a/vorestation.dme b/vorestation.dme
index f3e9b290287..f5938321638 100644
--- a/vorestation.dme
+++ b/vorestation.dme
@@ -308,6 +308,7 @@
#include "code\datums\browser.dm"
#include "code\datums\callback.dm"
#include "code\datums\category.dm"
+#include "code\datums\chat_message.dm"
#include "code\datums\computerfiles.dm"
#include "code\datums\datacore.dm"
#include "code\datums\datum.dm"