diff --git a/code/__DEFINES/admin_defines.dm b/code/__DEFINES/admin_defines.dm
index 1856a55e010..fd1ada925ef 100644
--- a/code/__DEFINES/admin_defines.dm
+++ b/code/__DEFINES/admin_defines.dm
@@ -46,12 +46,14 @@
#define R_HOST ((1<<18)-1) //17 bit bitmask, update me if we ever add more admin permissions. Sum of all permissions to allow easy setting.
+
#define ADMIN_QUE(user,display) "[display]"
#define ADMIN_FLW(user,display) "[display]"
#define ADMIN_PP(user,display) "[display]"
#define ADMIN_VV(atom,display) "[display]"
#define ADMIN_SM(user,display) "[display]"
#define ADMIN_TP(user,display) "[display]"
+#define ADMIN_OBS(user, display) "[display]"
#define ADMIN_ALERT(user, display) "[display]"
#define ADMIN_BSA(user,display) "[display]"
#define ADMIN_CENTCOM_REPLY(user,display) "[display]"
diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 3dd763ba3da..84491eb549b 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -181,6 +181,8 @@
#define COMSIG_ATOM_ORBIT_BEGIN "atom_orbit_begin"
///called when an atom stops orbiting another atom: (atom)
#define COMSIG_ATOM_ORBIT_STOP "atom_orbit_stop"
+/// called on an atom who has stopped orbiting another atom (atom/orbiter, atom/formerly_orbited)
+#define COMSIG_ATOM_ORBITER_STOP "atom_orbiter_stop"
///from base of atom/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
#define COMSIG_ATOM_HITBY "atom_hitby"
/// Called when an atom is sharpened or dulled.
@@ -945,11 +947,26 @@
///from base of datum/action/proc/Trigger(): (datum/action)
#define COMSIG_ACTION_TRIGGER "action_trigger"
#define COMPONENT_ACTION_BLOCK_TRIGGER (1<<0)
+/// From /datum/action/Grant(): (mob/grant_to)
+#define COMSIG_ACTION_GRANTED "action_grant"
+/// From /datum/action/Grant(): (datum/action)
+#define COMSIG_MOB_GRANTED_ACTION "mob_action_grant"
+/// From /datum/action/Remove(): (mob/removed_from)
+#define COMSIG_ACTION_REMOVED "action_removed"
+/// From /datum/action/Remove(): (datum/action)
+#define COMSIG_MOB_REMOVED_ACTION "mob_action_removed"
// Note that this is only defined for actions because this could be a good bit expensive otherwise
/// From base of /atom/movable/screen/movable/action_button/MouseWheel(src, delta_x, delta_y, location, control, params)
#define COMSIG_ACTION_SCROLLED "action_scrolled"
+// ghost signals
+
+/// from observer_base/do_observe(): (mob/now_followed)
+#define COMSIG_GHOST_START_OBSERVING "ghost_start_observing"
+/// from observer_base/do_observe(): (mob/no_longer_following)
+#define COMSIG_GHOST_STOP_OBSERVING "ghost_stop_observing"
+
//Xenobio hotkeys
///from slime CtrlClickOn(): (/mob)
diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm
index 3e1275ba880..8f87cf4ec58 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -164,6 +164,8 @@
if(sight_check && !isInSight(A, O))
continue
L |= M
+ for(var/mob/dead/observer/ghost in M.observers)
+ L |= ghost
//log_world("[recursion_limit] = [M] - [get_turf(M)] - ([M.x], [M.y], [M.z])")
if(isobj(A) || ismob(A))
diff --git a/code/__HELPERS/lists.dm b/code/__HELPERS/lists.dm
index cd6d5d8820b..1f1af7032bd 100644
--- a/code/__HELPERS/lists.dm
+++ b/code/__HELPERS/lists.dm
@@ -676,6 +676,7 @@
#define UNSETEMPTY(L) if(L && !length(L)) L = null
#define LAZYREMOVE(L, I) if(L) { L -= I; if(!length(L)) { L = null; } }
#define LAZYADD(L, I) if(!L) { L = list(); } L += I;
+/// Add an item to the list if not already present, if the list is null it will initialize it
#define LAZYOR(L, I) if(!L) { L = list(); } L |= I;
/// Adds I to L, initializing L if necessary, if I is not already in L
#define LAZYDISTINCTADD(L, I) if(!L) { L = list(); } L |= I;
diff --git a/code/__HELPERS/mob_helpers.dm b/code/__HELPERS/mob_helpers.dm
index d4e50baa361..30c2693a643 100644
--- a/code/__HELPERS/mob_helpers.dm
+++ b/code/__HELPERS/mob_helpers.dm
@@ -554,7 +554,7 @@ GLOBAL_LIST_EMPTY(do_after_once_tracker)
to_chat(user, "Name = [M.name]; Real_name = [M.real_name]; Mind_name = [M.mind?"[M.mind.name]":""]; Key = [M.key];")
to_chat(user, "Location = [location_description];")
to_chat(user, "[special_role_description]")
- to_chat(user, "(PM) ([ADMIN_PP(M,"PP")]) ([ADMIN_VV(M,"VV")]) ([ADMIN_TP(M,"TP")]) ([ADMIN_SM(M,"SM")]) ([ADMIN_FLW(M,"FLW")])")
+ to_chat(user, "(PM) ([ADMIN_PP(M,"PP")]) ([ADMIN_VV(M,"VV")]) ([ADMIN_TP(M,"TP")]) ([ADMIN_SM(M,"SM")]) ([ADMIN_FLW(M,"FLW")]) ([ADMIN_OBS(M, "OBS")])")
// Gets the first mob contained in an atom, and warns the user if there's not exactly one
/proc/get_mob_in_atom_with_warning(atom/A, mob/user = usr)
diff --git a/code/__HELPERS/trait_helpers.dm b/code/__HELPERS/trait_helpers.dm
index e3778abe37f..112661491d7 100644
--- a/code/__HELPERS/trait_helpers.dm
+++ b/code/__HELPERS/trait_helpers.dm
@@ -235,6 +235,10 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_SLEIGHT_OF_HAND "sleight_of_hand"
#define TRAIT_KNOWS_COOKING_RECIPES "knows_cooking_recipes"
+/// used for dead mobs that are observing, but should not be afforded all the same platitudes as full ghosts.
+/// This is a mind trait because ghosts can be frequently deleted and we want to be sure this sticks.
+#define TRAIT_MENTOR_OBSERVING "mentor_observe"
+
//***** ITEM AND MOB TRAITS *****//
/// Show what machine/door wires do when held.
#define TRAIT_SHOW_WIRE_INFO "show_wire_info"
@@ -321,6 +325,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define HOLO_CIGAR "holo_cigar"
#define GLADIATOR "gladiator"
#define PULSEDEMON_TRAIT "pulse_demon"
+/// Mentor observing
+#define MENTOR_OBSERVING "mobserving"
//quirk traits
#define TRAIT_ALCOHOL_TOLERANCE "alcohol_tolerance"
diff --git a/code/_onclick/hud/action_button.dm b/code/_onclick/hud/action_button.dm
index 205d4fe1dde..a8303d46fb8 100644
--- a/code/_onclick/hud/action_button.dm
+++ b/code/_onclick/hud/action_button.dm
@@ -16,6 +16,8 @@
var/id
/// UID of the last thing we hovered over. Used for managing action button dragging.
var/last_hovered_ref
+ /// Whether or not this should be shown to observers
+ var/shown_to_observers = FALSE
/// Whether or not this button is locked, preventing it from being dragged.
var/locked = FALSE
@@ -32,8 +34,14 @@
return ..()
/atom/movable/screen/movable/action_button/proc/can_use(mob/user)
+ if(isobserver(user))
+ var/mob/dead/observer/dead_mob = user
+ if(dead_mob.mob_observed) // Observers can only click on action buttons if they're not observing something
+ return FALSE
if(!linked_action)
return TRUE
+ if(linked_action.owner != user)
+ return FALSE
return !isnull(linked_action.viewers[user.hud_used])
// Entered and Exited won't fire while you're dragging something, because you're still "holding" it
@@ -60,7 +68,8 @@
last_hovered.MouseExited(over_location, over_control, params)
closeToolTip(usr)
last_hovered_ref = UID(over_object)
- over_object.MouseEntered(over_location, over_control, params)
+ if(!isnull(over_object))
+ over_object.MouseEntered(over_location, over_control, params)
/atom/movable/screen/movable/action_button/MouseDrop(over_object)
last_hovered_ref = null
@@ -113,6 +122,8 @@
animate(src, transform = matrix(), time = 0.4 SECONDS, alpha = 255)
/atom/movable/screen/movable/action_button/Click(location, control, params)
+ if(!can_use(usr))
+ return FALSE
var/list/modifiers = params2list(params)
if(modifiers["ctrl"] && modifiers["shift"])
INVOKE_ASYNC(src, PROC_REF(set_to_keybind), usr)
@@ -578,4 +589,47 @@ GLOBAL_LIST_INIT(palette_removed_matrix, list(1.4,0,0,0, 0.7,0.4,0,0, 0.4,0,0.6,
button = action.viewers[src]
position_action(button, button.location)
+
+/**
+ * Show (most) of the another mob's action buttons to this mob
+ *
+ * Used for observers viewing another mob's screen
+ */
+/mob/proc/show_other_mob_action_buttons(mob/take_from)
+ if(!hud_used || !client)
+ return
+
+ for(var/datum/action/action as anything in take_from.actions)
+ if(!action.show_to_observers)
+ continue
+ action.GiveAction(src)
+ RegisterSignal(take_from, COMSIG_MOB_GRANTED_ACTION, PROC_REF(on_observing_action_granted), override = TRUE)
+ RegisterSignal(take_from, COMSIG_MOB_REMOVED_ACTION, PROC_REF(on_observing_action_removed), override = TRUE)
+
+/**
+ * Hide another mob's action buttons from this mob
+ *
+ * Used for observers viewing another mob's screen
+ */
+/mob/proc/hide_other_mob_action_buttons(mob/take_from)
+ for(var/datum/action/action as anything in take_from.actions)
+ action.HideFrom(src)
+ UnregisterSignal(take_from, list(COMSIG_MOB_GRANTED_ACTION, COMSIG_MOB_REMOVED_ACTION))
+
+/// Signal proc for [COMSIG_MOB_GRANTED_ACTION] - If we're viewing another mob's action buttons,
+/// we need to update with any newly added buttons granted to the mob.
+/mob/proc/on_observing_action_granted(mob/living/source, datum/action/action)
+ SIGNAL_HANDLER // COMSIG_MOB_GRANTED_ACTION
+
+ if(!action.show_to_observers)
+ return
+ action.GiveAction(src)
+
+/// Signal proc for [COMSIG_MOB_REMOVED_ACTION] - If we're viewing another mob's action buttons,
+/// we need to update with any removed buttons from the mob.
+/mob/proc/on_observing_action_removed(mob/living/source, datum/action/action)
+ SIGNAL_HANDLER // COMSIG_MOB_REMOVED_ACTION
+
+ action.HideFrom(src)
+
#undef AB_MAX_COLUMNS
diff --git a/code/_onclick/hud/ai_hud.dm b/code/_onclick/hud/ai_hud.dm
index d99b737cc1f..1f0cf58e237 100644
--- a/code/_onclick/hud/ai_hud.dm
+++ b/code/_onclick/hud/ai_hud.dm
@@ -1,20 +1,27 @@
/atom/movable/screen/ai
icon = 'icons/mob/screen_ai.dmi'
+/atom/movable/screen/ai/Click()
+ if(isobserver(usr) || usr.incapacitated())
+ return TRUE
+
/atom/movable/screen/ai/aicore
name = "AI core"
icon_state = "ai_core"
/atom/movable/screen/ai/aicore/Click()
- if(isAI(usr))
- var/mob/living/silicon/ai/AI = usr
- AI.view_core()
+ if(..())
+ return
+ var/mob/living/silicon/ai/AI = usr
+ AI.view_core()
/atom/movable/screen/ai/camera_list
name = "Show Camera List"
icon_state = "camera"
/atom/movable/screen/ai/camera_list/Click()
+ if(..())
+ return
var/mob/living/silicon/ai/AI = usr
var/camera = tgui_input_list(AI, "Choose which camera you want to view", "Cameras", AI.get_camera_list())
AI.ai_camera_list(camera)
@@ -24,53 +31,60 @@
icon_state = "track"
/atom/movable/screen/ai/camera_track/Click()
- if(isAI(usr))
- var/mob/living/silicon/ai/AI = usr
- var/target_name = tgui_input_list(AI, "Choose a target you want to track", "Tracking", AI.trackable_mobs())
- if(target_name)
- AI.ai_camera_track(target_name)
+ if(..())
+ return
+ var/mob/living/silicon/ai/AI = usr
+ var/target_name = tgui_input_list(AI, "Choose a target you want to track", "Tracking", AI.trackable_mobs())
+ if(target_name)
+ AI.ai_camera_track(target_name)
/atom/movable/screen/ai/camera_light
name = "Toggle Camera Light"
icon_state = "camera_light"
/atom/movable/screen/ai/camera_light/Click()
- if(isAI(usr))
- var/mob/living/silicon/ai/AI = usr
- AI.toggle_camera_light()
+ if(..())
+ return
+ var/mob/living/silicon/ai/AI = usr
+ AI.toggle_camera_light()
/atom/movable/screen/ai/crew_monitor
name = "Crew Monitoring Console"
icon_state = "crew_monitor"
/atom/movable/screen/ai/crew_monitor/Click()
- if(isAI(usr))
- var/mob/living/silicon/ai/AI = usr
- AI.subsystem_crew_monitor()
+ if(..())
+ return
+ var/mob/living/silicon/ai/AI = usr
+ AI.subsystem_crew_monitor()
/atom/movable/screen/ai/crew_manifest
name = "Crew Manifest"
icon_state = "manifest"
/atom/movable/screen/ai/crew_manifest/Click()
- if(isAI(usr))
- var/mob/living/silicon/ai/AI = usr
- AI.ai_roster()
+ if(..())
+ return
+ var/mob/living/silicon/ai/AI = usr
+ AI.ai_roster()
/atom/movable/screen/ai/alerts
name = "Show Alerts"
icon_state = "alerts"
/atom/movable/screen/ai/alerts/Click()
- if(isAI(usr))
- var/mob/living/silicon/ai/AI = usr
- AI.ai_alerts()
+ if(..())
+ return
+ var/mob/living/silicon/ai/AI = usr
+ AI.ai_alerts()
/atom/movable/screen/ai/announcement
name = "Make Announcement"
icon_state = "announcement"
/atom/movable/screen/ai/announcement/Click()
+ if(..())
+ return
var/mob/living/silicon/ai/AI = usr
AI.announcement()
@@ -79,15 +93,18 @@
icon_state = "call_shuttle"
/atom/movable/screen/ai/call_shuttle/Click()
- if(isAI(usr))
- var/mob/living/silicon/ai/AI = usr
- AI.ai_call_shuttle()
+ if(..())
+ return
+ var/mob/living/silicon/ai/AI = usr
+ AI.ai_call_shuttle()
/atom/movable/screen/ai/state_laws
name = "Law Manager"
icon_state = "state_laws"
/atom/movable/screen/ai/state_laws/Click()
+ if(..())
+ return
if(isAI(usr))
var/mob/living/silicon/ai/AI = usr
AI.subsystem_law_manager()
@@ -97,42 +114,48 @@
icon_state = "pda_send"
/atom/movable/screen/ai/pda_msg_send/Click()
- if(isAI(usr))
- var/mob/living/silicon/ai/AI = usr
- AI.aiPDA.cmd_send_pdamesg()
+ if(..())
+ return
+ var/mob/living/silicon/ai/AI = usr
+ AI.aiPDA.cmd_send_pdamesg()
/atom/movable/screen/ai/pda_msg_show
name = "PDA - Show Message Log"
icon_state = "pda_receive"
/atom/movable/screen/ai/pda_msg_show/Click()
- if(isAI(usr))
- var/mob/living/silicon/ai/AI = usr
- AI.aiPDA.cmd_show_message_log()
+ if(..())
+ return
+ var/mob/living/silicon/ai/AI = usr
+ AI.aiPDA.cmd_show_message_log()
/atom/movable/screen/ai/image_take
name = "Take Image"
icon_state = "take_picture"
/atom/movable/screen/ai/image_take/Click()
- if(isAI(usr))
- var/mob/living/silicon/ai/AI = usr
- AI.aiCamera.toggle_camera_mode()
+ if(..())
+ return
+ var/mob/living/silicon/ai/AI = usr
+ AI.aiCamera.toggle_camera_mode()
/atom/movable/screen/ai/image_view
name = "View Images"
icon_state = "view_images"
/atom/movable/screen/ai/image_view/Click()
- if(isAI(usr))
- var/mob/living/silicon/ai/AI = usr
- AI.aiCamera.viewpictures()
+ if(..())
+ return
+ var/mob/living/silicon/ai/AI = usr
+ AI.aiCamera.viewpictures()
/atom/movable/screen/ai/sensors
name = "Toggle Sensor Augmentation"
icon_state = "ai_sensor"
/atom/movable/screen/ai/sensors/Click()
+ if(..())
+ return
if(isAI(usr))
var/mob/living/silicon/ai/AI = usr
AI.sensor_mode()
diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm
index 8af3de91da0..80abb2b6f26 100644
--- a/code/_onclick/hud/alert.dm
+++ b/code/_onclick/hud/alert.dm
@@ -48,6 +48,8 @@
if(override)
alert.timeout = null
+ alert.attach_owner(src)
+
if(icon_override)
alert.icon = icon_override
@@ -100,10 +102,50 @@
name = "Alert"
desc = "Something seems to have gone wrong with this alert, so report this bug please"
mouse_opacity = MOUSE_OPACITY_ICON
- var/timeout = 0 //If set to a number, this alert will clear itself after that many deciseconds
+ /// How long before this alert automatically clears itself (in deciseconds). If zero, remains until cleared.
+ var/timeout = 0
+ /// Some alerts may have different icon states based on severity, this adjusts that.
var/severity = 0
+ /// Tool-tip for the alert.
var/alerttooltipstyle = ""
- var/override_alerts = FALSE //If it is overriding other alerts of the same type
+ /// If true, this should override any other alerts of the same type thrown.
+ var/override_alerts = FALSE
+ /// The mob that this alert was originally thrown to.
+ var/mob/owner
+
+/atom/movable/screen/alert/proc/attach_owner(mob/new_owner)
+ owner = new_owner
+ RegisterSignal(owner, COMSIG_PARENT_QDELETING, PROC_REF(remove_owner))
+
+/atom/movable/screen/alert/proc/remove_owner(mob/source, force)
+ SIGNAL_HANDLER // COMSIG_PARENT_QDELETING
+ if(owner == source && !isnull(owner))
+ UnregisterSignal(owner, COMSIG_PARENT_QDELETING)
+ owner = null
+
+/atom/movable/screen/alert/Destroy()
+ if(owner)
+ UnregisterSignal(owner, COMSIG_PARENT_QDELETING)
+ owner = null
+ severity = 0
+ master = null
+ screen_loc = ""
+ return ..()
+
+/atom/movable/screen/alert/Click(location, control, params)
+ ..()
+ if(!usr || !usr.client)
+ return FALSE
+ if(usr != owner)
+ to_chat(usr, "Only [owner] can use that!")
+ return FALSE
+ var/paramslist = params2list(params)
+ if(paramslist["shift"]) // screen objects don't do the normal Click() stuff so we'll cheat
+ to_chat(usr, "[name] - [desc]")
+ return FALSE
+ if(master)
+ usr.client.Click(master, location, control, params)
+ return TRUE
/atom/movable/screen/alert/MouseEntered(location, control, params)
. = ..()
@@ -310,7 +352,7 @@ or something covering your eyes."
icon_state = "embeddedobject"
/atom/movable/screen/alert/embeddedobject/Click()
- if(isliving(usr))
+ if(isliving(usr) && ..())
var/mob/living/carbon/human/M = usr
return M.help_shake_act(M)
@@ -333,7 +375,7 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
icon_state = "fire"
/atom/movable/screen/alert/fire/Click()
- if(isliving(usr))
+ if(isliving(usr) && ..())
var/mob/living/L = usr
return L.resist()
@@ -343,7 +385,7 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
icon_state = "direction_lock"
/atom/movable/screen/alert/direction_lock/Click()
- if(isliving(usr))
+ if(isliving(usr) && ..())
var/mob/living/L = usr
return L.clear_forced_look()
@@ -406,7 +448,7 @@ Recharging stations are available in robotics, the dormitory bathrooms, and the
desc = "You have merged with a diona gestalt and are now part of it's biomass. You can still wiggle yourself free though."
/atom/movable/screen/alert/nymph/Click()
- if(!usr || !usr.client)
+ if(!..())
return
if(isnymph(usr))
var/mob/living/simple_animal/diona/D = usr
@@ -417,7 +459,7 @@ Recharging stations are available in robotics, the dormitory bathrooms, and the
desc = "You have merged with one or more diona nymphs. Click here to drop it (or one of them)."
/atom/movable/screen/alert/gestalt/Click()
- if(!usr || !usr.client)
+ if(!..())
return
var/list/nymphs = list()
@@ -465,7 +507,7 @@ so as to remain in compliance with the most up-to-date laws."
return ..()
/atom/movable/screen/alert/hackingapc/Click()
- if(!usr || !usr.client)
+ if(!..())
return
if(!target)
return
@@ -491,7 +533,7 @@ so as to remain in compliance with the most up-to-date laws."
return ..()
/atom/movable/screen/alert/mech_port_available/Click()
- if(!usr || !usr.client)
+ if(!..())
return
if(!ismecha(usr.loc) || !target)
return
@@ -507,7 +549,7 @@ so as to remain in compliance with the most up-to-date laws."
icon_state = "mech_port_x"
/atom/movable/screen/alert/mech_port_disconnect/Click()
- if(!usr || !usr.client)
+ if(!..())
return
if(!ismecha(usr.loc))
return
@@ -566,7 +608,7 @@ so as to remain in compliance with the most up-to-date laws."
timeout = 300
/atom/movable/screen/alert/notify_cloning/Click()
- if(!usr || !usr.client)
+ if(!..())
return
var/mob/dead/observer/G = usr
G.reenter_corpse()
@@ -585,6 +627,8 @@ so as to remain in compliance with the most up-to-date laws."
overlays += I
/atom/movable/screen/alert/ghost/Click()
+ if(!..())
+ return
var/mob/living/carbon/human/infected_user = usr
if(!istype(infected_user) || infected_user.stat == DEAD)
infected_user.clear_alert("ghost_nest")
@@ -640,7 +684,7 @@ so as to remain in compliance with the most up-to-date laws."
return ..()
/atom/movable/screen/alert/notify_action/Click()
- if(!usr || !usr.client)
+ if(!..())
return
var/mob/dead/observer/G = usr
@@ -721,7 +765,7 @@ so as to remain in compliance with the most up-to-date laws."
var/stoner = null
/atom/movable/screen/alert/notify_soulstone/Click()
- if(!usr || !usr.client)
+ if(!..())
return
if(stone)
if(tgui_alert(usr, "Do you want to be captured by [stoner]'s soul stone? This will destroy your corpse and make it \
@@ -738,6 +782,7 @@ so as to remain in compliance with the most up-to-date laws."
icon_state = "map_vote"
/atom/movable/screen/alert/notify_mapvote/Click()
+ // ehh sure let observers click on it if they really want, who cares
usr.client.vote()
//OBJECT-BASED
@@ -756,11 +801,14 @@ so as to remain in compliance with the most up-to-date laws."
desc = "You're legcuffed, which slows you down considerably. Click the alert to free yourself."
/atom/movable/screen/alert/restrained/Click()
- if(isliving(usr))
- var/mob/living/L = usr
- return L.resist()
+ if(!isliving(usr) || !..())
+ return
+ var/mob/living/L = usr
+ return L.resist()
/atom/movable/screen/alert/restrained/buckled/Click()
+ if(!isliving(usr) || !..())
+ return
var/mob/living/L = usr
if(!istype(L) || !L.can_resist())
return
@@ -770,20 +818,23 @@ so as to remain in compliance with the most up-to-date laws."
// PRIVATE = only edit, use, or override these if you're editing the system as a whole
// Re-render all alerts - also called in /datum/hud/show_hud() because it's needed there
-/datum/hud/proc/reorganize_alerts()
+/datum/hud/proc/reorganize_alerts(mob/viewmob)
+ var/mob/screenmob = viewmob || mymob
+ if(!screenmob.client)
+ return
var/list/alerts = mymob.alerts
if(!alerts)
return FALSE
var/icon_pref
if(!hud_shown)
for(var/i in 1 to length(alerts))
- mymob.client.screen -= alerts[alerts[i]]
+ screenmob.client.screen -= alerts[alerts[i]]
return TRUE
for(var/i in 1 to length(alerts))
var/atom/movable/screen/alert/alert = alerts[alerts[i]]
if(alert.icon_state == "template")
if(!icon_pref)
- icon_pref = ui_style2icon(mymob.client.prefs.UI_style)
+ icon_pref = ui_style2icon(screenmob.client.prefs.UI_style)
alert.icon = icon_pref
switch(i)
if(1)
@@ -799,24 +850,12 @@ so as to remain in compliance with the most up-to-date laws."
else
. = ""
alert.screen_loc = .
- mymob.client.screen |= alert
+ screenmob.client.screen |= alert
+ if(!viewmob)
+ for(var/viewer in mymob.observers)
+ reorganize_alerts(viewer)
return TRUE
-/atom/movable/screen/alert/Click(location, control, params)
- if(!usr || !usr.client)
- return
- var/paramslist = params2list(params)
- if(paramslist["shift"]) // screen objects don't do the normal Click() stuff so we'll cheat
- to_chat(usr, "[name] - [desc]")
- return
- if(master)
- return usr.client.Click(master, location, control, params)
-
-/atom/movable/screen/alert/Destroy()
- severity = 0
- master = null
- screen_loc = ""
- return ..()
/// Gives the player the option to succumb while in critical condition
/atom/movable/screen/alert/succumb
@@ -825,7 +864,7 @@ so as to remain in compliance with the most up-to-date laws."
icon_state = "succumb"
/atom/movable/screen/alert/succumb/Click()
- if(!usr || !usr.client)
+ if(!..())
return
var/mob/living/living_owner = usr
if(!istype(usr))
diff --git a/code/_onclick/hud/alien_hud.dm b/code/_onclick/hud/alien_hud.dm
index eff377796e0..ad4e8d2a05c 100644
--- a/code/_onclick/hud/alien_hud.dm
+++ b/code/_onclick/hud/alien_hud.dm
@@ -146,19 +146,22 @@
inv_slots[inv.slot_id] = inv
inv.update_icon()
-/datum/hud/alien/persistent_inventory_update()
+/datum/hud/alien/persistent_inventory_update(mob/viewer)
if(!mymob)
return
var/mob/living/carbon/alien/humanoid/H = mymob
+ var/mob/screenmob = viewer || H
if(hud_version != HUD_STYLE_NOHUD)
if(H.r_hand)
H.r_hand.screen_loc = ui_rhand
- H.client.screen += H.r_hand
+ screenmob.client.screen += H.r_hand
if(H.l_hand)
H.l_hand.screen_loc = ui_lhand
- H.client.screen += H.l_hand
+ screenmob.client.screen += H.l_hand
else
if(H.r_hand)
H.r_hand.screen_loc = null
+ screenmob.client.screen -= H.r_hand
if(H.l_hand)
H.l_hand.screen_loc = null
+ screenmob.client.screen -= H.l_hand
diff --git a/code/_onclick/hud/fullscreen.dm b/code/_onclick/hud/fullscreen.dm
index afc30ca3422..b085669097b 100644
--- a/code/_onclick/hud/fullscreen.dm
+++ b/code/_onclick/hud/fullscreen.dm
@@ -49,16 +49,17 @@
for(var/category in screens)
clear_fullscreen(category)
-/datum/hud/proc/reload_fullscreen()
- if(mymob.client)
- var/atom/movable/screen/fullscreen/screen
- var/list/screens = mymob.screens
- for(var/category in screens)
- screen = screens[category]
- if(screen.should_show_to(mymob))
- mymob.client.screen |= screen
- continue
- mymob.client.screen -= screen
+/mob/proc/reload_fullscreen()
+ if(!client)
+ return
+ var/atom/movable/screen/fullscreen/screen
+ for(var/category in screens)
+ screen = screens[category]
+ if(screen.should_show_to(src))
+ screen.update_for_view(client.view)
+ client.screen |= screen
+ else
+ client.screen -= screen
/atom/movable/screen/fullscreen
icon = 'icons/mob/screen_full.dmi'
diff --git a/code/_onclick/hud/ghost_hud.dm b/code/_onclick/hud/ghost_hud.dm
index f7994d9aa45..40c957b26c9 100644
--- a/code/_onclick/hud/ghost_hud.dm
+++ b/code/_onclick/hud/ghost_hud.dm
@@ -110,7 +110,24 @@
for(var/atom/movable/screen/S in (static_inventory + toggleable_inventory))
S.hud = src
-/datum/hud/ghost/show_hud()
- mymob.client.screen = list()
- mymob.client.screen += static_inventory
- ..()
+/datum/hud/ghost/show_hud(version = 0, mob/viewmob)
+ // don't show this HUD if observing; show the HUD of the observee
+ var/mob/dead/observer/O = mymob
+ if(istype(O) && O.mob_observed)
+ plane_masters_update()
+ return FALSE
+
+ . = ..()
+ if(!.)
+ return
+ var/mob/screenmob = viewmob || mymob
+ screenmob.client.screen += static_inventory
+
+
+// We should only see observed mob alerts.
+/datum/hud/ghost/reorganize_alerts(mob/viewmob)
+ var/mob/dead/observer/O = mymob
+ if(istype(O) && O.mob_observed)
+ return
+ return ..()
+
diff --git a/code/_onclick/hud/hud_datum.dm b/code/_onclick/hud/hud_datum.dm
index 566e07c7bb9..4f11604ade2 100644
--- a/code/_onclick/hud/hud_datum.dm
+++ b/code/_onclick/hud/hud_datum.dm
@@ -11,7 +11,7 @@
var/hud_shown = TRUE
/// Current displayed version of the HUD
var/hud_version = 1
- /// Whether or not their toggleable inventory
+ /// Whether or not their toggleable inventory (generally their contents on the left) is expanded
var/inventory_shown = TRUE
/// This is to hide the buttons that can be used via hotkeys. (hotkeybuttons list of buttons)
var/hotkey_ui_hidden = FALSE
@@ -147,14 +147,22 @@
QDEL_NULL(screentip_text)
return ..()
-/datum/hud/proc/show_hud(version = 0)
+/**
+ * Shows this hud's hud to some mob
+ *
+ * Arguments
+ * * version - denotes which style should be displayed. blank or 0 means "next version"
+ * * viewmob - what mob to show the hud to. Can be this hud's mob, can be another mob, can be null (will use this hud's mob if so)
+ */
+/datum/hud/proc/show_hud(version = 0, mob/viewmob)
if(!ismob(mymob))
return FALSE
-
- if(!mymob.client)
+ var/mob/screenmob = viewmob || mymob
+ if(!screenmob.client)
return FALSE
- mymob.client.screen = list()
+ screenmob.client.clear_screen()
+ screenmob.client.apply_clickcatcher()
var/display_hud_version = version
if(!display_hud_version) //If 0 or blank, display the next hud version
@@ -166,15 +174,15 @@
if(HUD_STYLE_STANDARD) //Default HUD
hud_shown = TRUE //Governs behavior of other procs
if(length(static_inventory))
- mymob.client.screen += static_inventory
- if(length(toggleable_inventory) && inventory_shown)
- mymob.client.screen += toggleable_inventory
- if(length(hotkeybuttons) && !hotkey_ui_hidden)
- mymob.client.screen += hotkeybuttons
+ screenmob.client.screen += static_inventory
+ if(length(toggleable_inventory) && screenmob.hud_used?.inventory_shown)
+ screenmob.client.screen += toggleable_inventory
+ if(length(hotkeybuttons) && !screenmob.hud_used?.hotkey_ui_hidden)
+ screenmob.client.screen += hotkeybuttons
if(length(infodisplay))
- mymob.client.screen += infodisplay
+ screenmob.client.screen += infodisplay
- mymob.client.screen += toggle_palette
+ screenmob.client.screen += toggle_palette
if(action_intent)
action_intent.screen_loc = initial(action_intent.screen_loc) //Restore intent selection to the original position
@@ -182,33 +190,33 @@
if(HUD_STYLE_REDUCED) //Reduced HUD
hud_shown = FALSE //Governs behavior of other procs
if(length(static_inventory))
- mymob.client.screen -= static_inventory
+ screenmob.client.screen -= static_inventory
if(length(toggleable_inventory))
- mymob.client.screen -= toggleable_inventory
+ screenmob.client.screen -= toggleable_inventory
if(length(hotkeybuttons))
- mymob.client.screen -= hotkeybuttons
+ screenmob.client.screen -= hotkeybuttons
if(length(infodisplay))
- mymob.client.screen += infodisplay
+ screenmob.client.screen += infodisplay
//These ones are a part of 'static_inventory', 'toggleable_inventory' or 'hotkeybuttons' but we want them to stay
if(inv_slots[SLOT_HUD_LEFT_HAND])
- mymob.client.screen += inv_slots[SLOT_HUD_LEFT_HAND] //we want the hands to be visible
+ screenmob.client.screen += inv_slots[SLOT_HUD_LEFT_HAND] //we want the hands to be visible
if(inv_slots[SLOT_HUD_RIGHT_HAND])
- mymob.client.screen += inv_slots[SLOT_HUD_RIGHT_HAND] //we want the hands to be visible
+ screenmob.client.screen += inv_slots[SLOT_HUD_RIGHT_HAND] //we want the hands to be visible
if(action_intent)
- mymob.client.screen += action_intent //we want the intent switcher visible
+ screenmob.client.screen += action_intent //we want the intent switcher visible
action_intent.screen_loc = ui_acti_alt //move this to the alternative position, where zone_select usually is.
if(HUD_STYLE_NOHUD) //No HUD
hud_shown = FALSE //Governs behavior of other procs
if(length(static_inventory))
- mymob.client.screen -= static_inventory
+ screenmob.client.screen -= static_inventory
if(length(toggleable_inventory))
- mymob.client.screen -= toggleable_inventory
+ screenmob.client.screen -= toggleable_inventory
if(length(hotkeybuttons))
- mymob.client.screen -= hotkeybuttons
+ screenmob.client.screen -= hotkeybuttons
if(length(infodisplay))
- mymob.client.screen -= infodisplay
+ screenmob.client.screen -= infodisplay
if(HUD_STYLE_ACTIONHUD) //No HUD
hud_shown = TRUE //Governs behavior of other procs
@@ -220,36 +228,46 @@
mymob.client.screen -= infodisplay
hud_version = display_hud_version
- persistent_inventory_update()
- mymob.update_action_buttons(1)
- reorganize_alerts()
- reload_fullscreen()
- update_parallax_pref(mymob)
+ persistent_inventory_update(screenmob)
+ screenmob.update_action_buttons(TRUE)
+ reorganize_alerts(screenmob)
+ screenmob.reload_fullscreen()
+ update_parallax_pref(screenmob)
+ if(!viewmob)
+ // working off of mymob
+ plane_masters_update()
+ for(var/M in mymob.observers)
+ show_hud(hud_version, M)
+ else if(viewmob.hud_used)
+ viewmob.hud_used.plane_masters_update()
+ viewmob.show_other_mob_action_buttons(mymob)
plane_masters_update()
+ return TRUE
/datum/hud/proc/plane_masters_update()
// Plane masters are always shown to OUR mob, never to observers
for(var/thing in plane_masters)
var/atom/movable/screen/plane_master/PM = plane_masters[thing]
PM.backdrop(mymob)
- mymob.client.screen += PM
+ mymob.client?.screen += PM
-/datum/hud/human/show_hud(version = 0)
+/datum/hud/human/show_hud(version = 0, mob/viewmob)
. = ..()
if(!.)
return
- hidden_inventory_update()
+ var/mob/screenmob = viewmob || mymob
+ hidden_inventory_update(screenmob)
-/datum/hud/robot/show_hud(version = 0)
+/datum/hud/robot/show_hud(version = 0, mob/viewmob)
. = ..()
if(!.)
return
- update_robot_modules_display()
+ update_robot_modules_display(viewmob)
-/datum/hud/proc/hidden_inventory_update()
+/datum/hud/proc/hidden_inventory_update(mob/viewer)
return
-/datum/hud/proc/persistent_inventory_update()
+/datum/hud/proc/persistent_inventory_update(mob/viewer)
return
/mob/proc/hide_hud()
diff --git a/code/_onclick/hud/human_hud.dm b/code/_onclick/hud/human_hud.dm
index 68eb065069f..94c08989d00 100644
--- a/code/_onclick/hud/human_hud.dm
+++ b/code/_onclick/hud/human_hud.dm
@@ -6,14 +6,22 @@
icon_state = "toggle"
/atom/movable/screen/human/toggle/Click()
- if(usr.hud_used.inventory_shown)
+
+ var/mob/targetmob = usr
+
+ if(isobserver(usr))
+ if(ishuman(usr.client.eye) && (usr.client.eye != usr))
+ var/mob/M = usr.client.eye
+ targetmob = M
+
+ if(usr.hud_used.inventory_shown && targetmob.hud_used)
usr.hud_used.inventory_shown = FALSE
- usr.client.screen -= usr.hud_used.toggleable_inventory
+ usr.client.screen -= targetmob.hud_used.toggleable_inventory
else
usr.hud_used.inventory_shown = TRUE
- usr.client.screen += usr.hud_used.toggleable_inventory
+ usr.client.screen += targetmob.hud_used.toggleable_inventory
- usr.hud_used.hidden_inventory_update()
+ targetmob.hud_used.hidden_inventory_update(usr)
/atom/movable/screen/human/equip
name = "equip"
@@ -21,7 +29,7 @@
/atom/movable/screen/human/equip/Click()
if(ismecha(usr.loc)) // stops inventory actions in a mech
- return 1
+ return TRUE
var/mob/living/carbon/human/H = usr
H.quick_equip()
@@ -33,6 +41,8 @@
screen_loc = ui_lingstingdisplay
/atom/movable/screen/ling/sting/Click()
+ if(isobserver(usr))
+ return
var/datum/antagonist/changeling/cling = usr.mind.has_antag_datum(/datum/antagonist/changeling)
cling?.chosen_sting?.unset_sting()
@@ -389,100 +399,118 @@
else
crafting.invisibility = initial(crafting.invisibility)
-/datum/hud/human/hidden_inventory_update()
+/datum/hud/human/hidden_inventory_update(mob/viewer)
if(!mymob)
return
var/mob/living/carbon/human/H = mymob
- if(inventory_shown && hud_version == HUD_STYLE_STANDARD)
+
+ var/mob/screenmob = viewer || H
+ if(screenmob.hud_used.inventory_shown && screenmob.hud_used.hud_shown && screenmob.hud_used.hud_version == HUD_STYLE_STANDARD)
+
if(H.shoes)
H.shoes.screen_loc = ui_shoes
- H.client.screen += H.shoes
+ screenmob.client.screen += H.shoes
if(H.gloves)
H.gloves.screen_loc = ui_gloves
- H.client.screen += H.gloves
+ screenmob.client.screen += H.gloves
if(H.l_ear)
H.l_ear.screen_loc = ui_l_ear
- H.client.screen += H.l_ear
+ screenmob.client.screen += H.l_ear
if(H.r_ear)
H.r_ear.screen_loc = ui_r_ear
- H.client.screen += H.r_ear
+ screenmob.client.screen += H.r_ear
if(H.glasses)
H.glasses.screen_loc = ui_glasses
- H.client.screen += H.glasses
+ screenmob.client.screen += H.glasses
if(H.w_uniform)
H.w_uniform.screen_loc = ui_iclothing
- H.client.screen += H.w_uniform
+ screenmob.client.screen += H.w_uniform
if(H.wear_suit)
H.wear_suit.screen_loc = ui_oclothing
- H.client.screen += H.wear_suit
+ screenmob.client.screen += H.wear_suit
if(H.wear_mask)
H.wear_mask.screen_loc = ui_mask
- H.client.screen += H.wear_mask
+ screenmob.client.screen += H.wear_mask
if(H.head)
H.head.screen_loc = ui_head
- H.client.screen += H.head
+ screenmob.client.screen += H.head
else
- if(H.shoes) H.shoes.screen_loc = null
- if(H.gloves) H.gloves.screen_loc = null
- if(H.l_ear) H.l_ear.screen_loc = null
- if(H.r_ear) H.r_ear.screen_loc = null
- if(H.glasses) H.glasses.screen_loc = null
- if(H.w_uniform) H.w_uniform.screen_loc = null
- if(H.wear_suit) H.wear_suit.screen_loc = null
- if(H.wear_mask) H.wear_mask.screen_loc = null
- if(H.head) H.head.screen_loc = null
+ if(H.shoes)
+ screenmob.client.screen -= H.shoes
+ if(H.gloves)
+ screenmob.client.screen -= H.gloves
+ if(H.l_ear)
+ screenmob.client.screen -= H.l_ear
+ if(H.r_ear)
+ screenmob.client.screen -= H.r_ear
+ if(H.glasses)
+ screenmob.client.screen -= H.glasses
+ if(H.w_uniform)
+ screenmob.client.screen -= H.w_uniform
+ if(H.wear_suit)
+ screenmob.client.screen -= H.wear_suit
+ if(H.wear_mask)
+ screenmob.client.screen -= H.wear_mask
+ if(H.head)
+ screenmob.client.screen -= H.head
-/datum/hud/human/persistent_inventory_update()
+/datum/hud/human/persistent_inventory_update(mob/viewer)
if(!mymob)
return
+ ..()
var/mob/living/carbon/human/H = mymob
- if(hud_version == HUD_STYLE_STANDARD)
- if(H.s_store)
- H.s_store.screen_loc = ui_sstore1
- H.client.screen += H.s_store
- if(H.wear_id)
- H.wear_id.screen_loc = ui_id
- H.client.screen += H.wear_id
- if(H.wear_pda)
- H.wear_pda.screen_loc = ui_pda
- H.client.screen += H.wear_pda
- if(H.belt)
- H.belt.screen_loc = ui_belt
- H.client.screen += H.belt
- if(H.back)
- H.back.screen_loc = ui_back
- H.client.screen += H.back
- if(H.l_store)
- H.l_store.screen_loc = ui_storage1
- H.client.screen += H.l_store
- if(H.r_store)
- H.r_store.screen_loc = ui_storage2
- H.client.screen += H.r_store
- else
- if(H.s_store)
- H.s_store.screen_loc = null
- if(H.wear_id)
- H.wear_id.screen_loc = null
- if(H.wear_pda)
- H.wear_pda.screen_loc = null
- if(H.belt)
- H.belt.screen_loc = null
- if(H.back)
- H.back.screen_loc = null
- if(H.l_store)
- H.l_store.screen_loc = null
- if(H.r_store)
- H.r_store.screen_loc = null
+ var/mob/screenmob = viewer || H
+
+ if(screenmob.hud_used)
+ if(screenmob.hud_used.hud_shown && screenmob.hud_used.hud_version == HUD_STYLE_STANDARD)
+ if(H.s_store)
+ H.s_store.screen_loc = ui_sstore1
+ screenmob.client.screen += H.s_store
+ if(H.wear_id)
+ H.wear_id.screen_loc = ui_id
+ screenmob.client.screen += H.wear_id
+ if(H.wear_pda)
+ H.wear_pda.screen_loc = ui_pda
+ screenmob.client.screen += H.wear_pda
+ if(H.belt)
+ H.belt.screen_loc = ui_belt
+ screenmob.client.screen += H.belt
+ if(H.back)
+ H.back.screen_loc = ui_back
+ screenmob.client.screen += H.back
+ if(H.l_store)
+ H.l_store.screen_loc = ui_storage1
+ screenmob.client.screen += H.l_store
+ if(H.r_store)
+ H.r_store.screen_loc = ui_storage2
+ screenmob.client.screen += H.r_store
+ else
+ if(H.s_store)
+ screenmob.client.screen -= H.s_store
+ if(H.wear_id)
+ screenmob.client.screen -= H.wear_id
+ if(H.wear_pda)
+ screenmob.client.screen -= H.wear_pda
+ if(H.belt)
+ screenmob.client.screen -= H.belt
+ if(H.back)
+ screenmob.client.screen -= H.back
+ if(H.l_store)
+ screenmob.client.screen -= H.l_store
+ if(H.r_store)
+ screenmob.client.screen -= H.r_store
if(hud_version != HUD_STYLE_NOHUD)
if(H.r_hand)
H.r_hand.screen_loc = ui_rhand
- H.client.screen += H.r_hand
+ screenmob.client.screen += H.r_hand
if(H.l_hand)
H.l_hand.screen_loc = ui_lhand
- H.client.screen += H.l_hand
+ screenmob.client.screen += H.l_hand
else
if(H.r_hand)
- H.r_hand.screen_loc = null
+ screenmob.r_hand.screen_loc = null
+ screenmob.client.screen -= H.r_hand
if(H.l_hand)
- H.l_hand.screen_loc = null
+ screenmob.l_hand.screen_loc = null
+ screenmob.client.screen -= H.l_hand
diff --git a/code/_onclick/hud/parallax.dm b/code/_onclick/hud/parallax.dm
index 2dca02b6e6d..aec2360eded 100644
--- a/code/_onclick/hud/parallax.dm
+++ b/code/_onclick/hud/parallax.dm
@@ -1,6 +1,9 @@
-/datum/hud/proc/create_parallax()
- var/client/C = mymob.client
- if(!apply_parallax_pref())
+/datum/hud/proc/create_parallax(mob/viewmob)
+ var/mob/screenmob = viewmob || mymob
+ if(!screenmob.client)
+ return
+ var/client/C = screenmob.client
+ if(!apply_parallax_pref(screenmob))
return
// this is needed so it blends properly with the space plane and blackness plane.
var/atom/movable/screen/plane_master/space/S = plane_masters["[PLANE_SPACE]"]
@@ -34,16 +37,20 @@
C.screen |= (C.parallax_layers + C.parallax_static_layers_tail)
-/datum/hud/proc/remove_parallax()
- var/client/C = mymob.client
+/datum/hud/proc/remove_parallax(mob/viewmob)
+ var/mob/screenmob = viewmob || mymob
+ var/client/C = screenmob.client
C.screen -= (C.parallax_layers_cached + C.parallax_static_layers_tail)
C.parallax_layers = null
var/atom/movable/screen/plane_master/space/S = plane_masters["[PLANE_SPACE]"]
S.color = null
S.appearance_flags &= ~NO_CLIENT_COLOR
-/datum/hud/proc/apply_parallax_pref()
- var/client/C = mymob.client
+/datum/hud/proc/apply_parallax_pref(mob/viewmob)
+ var/mob/screen_mob = viewmob || mymob
+ var/client/C = screen_mob.client
+ if(!istype(C))
+ return FALSE
if(C.prefs)
var/pref = C.prefs.parallax
if(isnull(pref))
@@ -72,16 +79,22 @@
C.parallax_layers_max = 4
return TRUE
-/datum/hud/proc/update_parallax_pref()
- remove_parallax()
- create_parallax()
- update_parallax()
+/datum/hud/proc/update_parallax_pref(mob/viewmob)
+ var/mob/screen_mob = viewmob || mymob
+ if(!screen_mob.client)
+ return
+ remove_parallax(screen_mob)
+ create_parallax(screen_mob)
+ update_parallax(screen_mob)
// This sets which way the current shuttle is moving (returns true if the shuttle has stopped moving so the caller can append their animation)
// Well, it would if our shuttle code had dynamic areas
-/datum/hud/proc/set_parallax_movedir(new_parallax_movedir, skip_windups)
+/datum/hud/proc/set_parallax_movedir(mob/viewmob, new_parallax_movedir, skip_windups)
. = FALSE
- var/client/C = mymob.client
+ var/mob/screen_mob = viewmob || mymob
+ var/client/C = screen_mob.client
+ if(!istype(C))
+ return
if(new_parallax_movedir == C.parallax_movedir)
return
var/animatedir = new_parallax_movedir
@@ -157,8 +170,9 @@
animate(L, transform = L.transform, time = 0, loop = -1, flags = ANIMATION_END_NOW)
animate(transform = matrix(), time = T)
-/datum/hud/proc/update_parallax()
- var/client/C = mymob.client
+/datum/hud/proc/update_parallax(mob/viewmob)
+ var/mob/screenmob = viewmob || mymob
+ var/client/C = screenmob.client
var/turf/posobj = get_turf(C.eye)
if(!posobj)
return
@@ -167,9 +181,9 @@
// Update the movement direction of the parallax if necessary (for shuttles)
var/area/shuttle/SA = areaobj
if(!SA || !SA.moving)
- set_parallax_movedir(0)
+ set_parallax_movedir(screenmob, 0)
else
- set_parallax_movedir(SA.parallax_move_direction)
+ set_parallax_movedir(screenmob, SA.parallax_move_direction)
var/force
if(!C.previous_turf || (C.previous_turf.z != posobj.z))
@@ -193,7 +207,7 @@
for(var/thing in C.parallax_layers)
var/atom/movable/screen/parallax_layer/L = thing
- L.update_status(mymob)
+ L.update_status(screenmob)
if(L.view_sized != C.view)
L.update_o(C.view)
diff --git a/code/_onclick/hud/robot_hud.dm b/code/_onclick/hud/robot_hud.dm
index 30fae75f816..60310611b71 100644
--- a/code/_onclick/hud/robot_hud.dm
+++ b/code/_onclick/hud/robot_hud.dm
@@ -1,23 +1,34 @@
/atom/movable/screen/robot
icon = 'icons/mob/screen_robot.dmi'
+/atom/movable/screen/robot/Click()
+ . = ..()
+ if(isobserver(usr))
+ return TRUE
+
/atom/movable/screen/robot/module
name = "cyborg module"
icon_state = "nomod"
/atom/movable/screen/robot/module/Click()
- if(isrobot(usr))
+ if(isrobot(usr) && !..())
var/mob/living/silicon/robot/R = usr
- if(R.module)
- R.hud_used.toggle_show_robot_modules()
- return 1
- R.pick_module()
+ if(!R.module)
+ R.pick_module()
+ return
+
+ // we can let ghosts mess with this one
+ usr.hud_used.toggle_show_robot_modules(usr)
+ return TRUE
+
/atom/movable/screen/robot/module1
name = "module1"
icon_state = "inv1"
/atom/movable/screen/robot/module1/Click()
+ if(..())
+ return
if(isrobot(usr))
var/mob/living/silicon/robot/R = usr
R.toggle_module(1)
@@ -27,6 +38,8 @@
icon_state = "inv2"
/atom/movable/screen/robot/module2/Click()
+ if(..())
+ return
if(isrobot(usr))
var/mob/living/silicon/robot/R = usr
R.toggle_module(2)
@@ -36,6 +49,8 @@
icon_state = "inv3"
/atom/movable/screen/robot/module3/Click()
+ if(..())
+ return
if(isrobot(usr))
var/mob/living/silicon/robot/R = usr
R.toggle_module(3)
@@ -46,6 +61,8 @@
icon_state = "radio"
/atom/movable/screen/robot/radio/Click()
+ if(..())
+ return
if(issilicon(usr))
var/mob/living/silicon/robot/R = usr
R.radio_menu()
@@ -55,6 +72,8 @@
icon_state = "store"
/atom/movable/screen/robot/store/Click()
+ if(..())
+ return
if(isrobot(usr))
var/mob/living/silicon/robot/R = usr
R.uneq_active()
@@ -66,6 +85,8 @@
screen_loc = ui_borg_lamp
/atom/movable/screen/robot/lamp/Click()
+ if(..())
+ return
if(isrobot(usr))
var/mob/living/silicon/robot/R = usr
R.control_headlamp()
@@ -75,6 +96,8 @@
icon_state = "ionpulse0"
/atom/movable/screen/robot/thrusters/Click()
+ if(..())
+ return
var/mob/living/silicon/robot/R = usr
R.toggle_ionpulse()
@@ -83,11 +106,23 @@
icon_state = "running"
/atom/movable/screen/robot/mov_intent/Click()
+ if(..())
+ return
usr.toggle_move_intent()
+/datum/hud/robot
+ var/shown_robot_modules = FALSE // Used to determine whether they have the module menu shown or not
+ var/atom/movable/screen/robot_modules_background
+
/datum/hud/robot/New(mob/user)
..()
+
+ robot_modules_background = new()
+ robot_modules_background.icon_state = "block"
+ robot_modules_background.layer = HUD_LAYER // Objects that appear on screen are on layer 20, UI should be just below it.
+ robot_modules_background.plane = HUD_PLANE
+
var/atom/movable/screen/using
var/mob/living/silicon/robot/mymobR = mymob
@@ -184,41 +219,51 @@
return ..()
-/datum/hud/proc/toggle_show_robot_modules()
+/datum/hud/proc/toggle_show_robot_modules(mob/viewmob)
+ return
+
+/datum/hud/robot/toggle_show_robot_modules(mob/viewmob)
+ var/mob/screenmob = viewmob || mymob
+ if(istype(viewmob.hud_used, /datum/hud/robot))
+ var/datum/hud/robot/robohud = screenmob.hud_used
+ robohud.shown_robot_modules = !robohud.shown_robot_modules
+
+ update_robot_modules_display(viewmob)
+
+/datum/hud/proc/update_robot_modules_display(mob/viewer)
+ return
+
+/datum/hud/robot/update_robot_modules_display(mob/viewer)
if(!isrobot(mymob))
return
var/mob/living/silicon/robot/R = mymob
- R.shown_robot_modules = !R.shown_robot_modules
- update_robot_modules_display()
-
-/datum/hud/proc/update_robot_modules_display()
- if(!isrobot(mymob))
- return
-
- var/mob/living/silicon/robot/R = mymob
+ var/mob/screenmob = viewer || R
if(!R.client)
return
if(!R.module)
+ shown_robot_modules = FALSE
+ screenmob.client.screen -= robot_modules_background
+ screenmob.client?.screen -= screenmob.hud_used.module_store_icon
return
- if(R.shown_robot_modules && hud_version == HUD_STYLE_STANDARD)
+ if(shown_robot_modules && hud_shown && hud_version == HUD_STYLE_STANDARD)
//Modules display is shown
- R.client.screen += module_store_icon //"store" icon
+ screenmob.client.screen += module_store_icon //"store" icon
if(!R.module.modules)
to_chat(usr, "Selected module has no modules to select.")
return
- if(!R.robot_modules_background)
+ if(!robot_modules_background)
return
var/display_rows = CEILING(length(R.module.modules) / 8, 1)
- R.robot_modules_background.screen_loc = "CENTER-4:16,SOUTH+1:7 to CENTER+3:16,SOUTH+[display_rows]:7"
- R.client.screen += R.robot_modules_background
+ robot_modules_background.screen_loc = "CENTER-4:16,SOUTH+1:7 to CENTER+3:16,SOUTH+[display_rows]:7"
+ screenmob.client.screen += robot_modules_background
var/x = -4 //Start at CENTER-4,SOUTH+1
var/y = 1
@@ -226,7 +271,7 @@
for(var/atom/movable/A in R.module.modules)
if((A != R.module_state_1) && (A != R.module_state_2) && (A != R.module_state_3))
//Module is not currently active
- R.client.screen += A
+ screenmob.client.screen += A
if(x < 0)
A.screen_loc = "CENTER[x]:16,SOUTH+[y]:7"
else
@@ -241,11 +286,39 @@
else
//Modules display is hidden
- R.client.screen -= module_store_icon
+ screenmob.client.screen -= module_store_icon
for(var/atom/A in R.module.modules)
if((A != R.module_state_1) && (A != R.module_state_2) && (A != R.module_state_3))
//Module is not currently active
- R.client.screen -= A
- R.shown_robot_modules = FALSE
- R.client.screen -= R.robot_modules_background
+ screenmob.client.screen -= A
+ shown_robot_modules = FALSE
+ screenmob.client.screen -= robot_modules_background
+
+/datum/hud/robot/persistent_inventory_update(mob/viewer)
+ if(!mymob)
+ return
+ var/mob/living/silicon/robot/R = mymob
+
+ var/mob/screenmob = viewer || R
+
+ var/held_items = list(R.module_state_1, R.module_state_2, R.module_state_3)
+ if(!screenmob.hud_used)
+ return
+ if(screenmob.hud_used.hud_shown)
+ for(var/i in 1 to length(held_items))
+ var/obj/item/I = held_items[i]
+ if(I)
+ switch(i)
+ if(1)
+ I.screen_loc = ui_inv1
+ if(2)
+ I.screen_loc = ui_inv2
+ if(3)
+ I.screen_loc = ui_inv3
+ else
+ return
+ screenmob.client.screen += I
+ else
+ for(var/obj/item/I in held_items)
+ screenmob.client.screen -= I
diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm
index b4897cd5df7..9216a62bbf3 100644
--- a/code/_onclick/hud/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects.dm
@@ -367,6 +367,8 @@
screen_loc = ui_crafting
/atom/movable/screen/craft/Click()
+ if(!isliving(usr))
+ return
var/mob/living/M = usr
M.OpenCraftingMenu()
@@ -396,24 +398,26 @@
object_overlays.Cut()
/atom/movable/screen/inventory/proc/add_overlays()
- var/mob/user = hud.mymob
+ var/mob/user = hud?.mymob
- if(hud && user && slot_id)
- var/obj/item/holding = user.get_active_hand()
+ if(!user || !slot_id || user != usr)
+ return
- if(!holding || user.get_item_by_slot(slot_id))
- return
+ var/obj/item/holding = user.get_active_hand()
- var/image/item_overlay = image(holding)
- item_overlay.alpha = 92
+ if(!holding || user.get_item_by_slot(slot_id))
+ return
- if(!user.can_equip(holding, slot_id, disable_warning = TRUE))
- item_overlay.color = "#ff0000"
- else
- item_overlay.color = "#00ff00"
+ var/image/item_overlay = image(holding)
+ item_overlay.alpha = 92
- object_overlays += item_overlay
- add_overlay(object_overlays)
+ if(!user.can_equip(holding, slot_id, TRUE))
+ item_overlay.color = "#ff0000"
+ else
+ item_overlay.color = "#00ff00"
+
+ object_overlays += item_overlay
+ add_overlay(object_overlays)
/atom/movable/screen/inventory/MouseDrop(atom/over)
cut_overlay(object_overlays)
diff --git a/code/datums/action.dm b/code/datums/action.dm
index 0a099de316c..33c5681c9b6 100644
--- a/code/datums/action.dm
+++ b/code/datums/action.dm
@@ -16,6 +16,8 @@
var/default_button_position = SCRN_OBJ_IN_LIST
/// Map of huds viewing a button with our action -> their button
var/list/viewers = list()
+ /// Whether or not this will be shown to observers
+ var/show_to_observers = TRUE
/datum/action/New(Target)
@@ -49,6 +51,8 @@
Remove(owner)
owner = M
RegisterSignal(owner, COMSIG_PARENT_QDELETING, PROC_REF(clear_ref), override = TRUE)
+ SEND_SIGNAL(src, COMSIG_ACTION_GRANTED, owner)
+ SEND_SIGNAL(owner, COMSIG_MOB_GRANTED_ACTION, src)
GiveAction(M)
/datum/action/proc/Remove(mob/remove_from)
@@ -64,6 +68,9 @@
if(isnull(owner))
return
+ SEND_SIGNAL(src, COMSIG_ACTION_REMOVED, owner)
+ SEND_SIGNAL(owner, COMSIG_MOB_REMOVED_ACTION, src)
+
if(target == owner)
RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(clear_ref), override = TRUE)
if(owner == remove_from)
diff --git a/code/datums/components/orbiter.dm b/code/datums/components/orbiter.dm
index 50ff05d4af1..d7a5a3a8ade 100644
--- a/code/datums/components/orbiter.dm
+++ b/code/datums/components/orbiter.dm
@@ -163,6 +163,7 @@
orbiter.animate_movement = SLIDE_STEPS
if(!QDELETED(parent))
SEND_SIGNAL(parent, COMSIG_ATOM_ORBIT_STOP, orbiter)
+ SEND_SIGNAL(orbiter, COMSIG_ATOM_ORBITER_STOP, parent)
orbiter.transform = get_cached_transform(orbiter)
orbiter.layer = get_orbiter_layer(orbiter)
diff --git a/code/game/objects/items/weapons/storage/storage_base.dm b/code/game/objects/items/weapons/storage/storage_base.dm
index cfb1b6015dc..ea3e086b3d6 100644
--- a/code/game/objects/items/weapons/storage/storage_base.dm
+++ b/code/game/objects/items/weapons/storage/storage_base.dm
@@ -199,12 +199,9 @@
orient2hud(user) // this only needs to happen to make .contents show properly as screen objects.
if(user.s_active)
user.s_active.hide_from(user) // If there's already an interface open, close it.
- user.client.screen -= boxes
- user.client.screen -= closer
- user.client.screen -= contents
- user.client.screen += boxes
- user.client.screen += closer
- user.client.screen += contents
+ user.client.screen |= boxes
+ user.client.screen |= closer
+ user.client.screen |= contents
user.s_active = src
LAZYDISTINCTADD(mobs_viewing, user)
@@ -245,6 +242,9 @@
update_viewers()
/obj/item/storage/proc/open(mob/user)
+ if(isobserver(user))
+ show_to(user)
+ return
if(use_sound && isliving(user))
playsound(loc, use_sound, 50, TRUE, -5)
@@ -459,6 +459,10 @@
if(user)
if(user.client && user.s_active != src)
user.client.screen -= I
+ if(length(user.observers))
+ for(var/mob/observer in user.observers)
+ if(observer.client && observer.s_active != src)
+ observer.client.screen -= I
I.dropped(user, TRUE)
add_fingerprint(user)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index baf89390823..f6dbf439894 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -12,6 +12,8 @@ GLOBAL_LIST_INIT(admin_verbs_admin, list(
/client/proc/invisimin, /*allows our mob to go invisible/visible*/
/datum/admins/proc/announce, /*priority announce something to all clients.*/
/client/proc/admin_ghost, /*allows us to ghost/reenter body at will*/
+ /client/proc/admin_observe, /*allows us to freely observe mobs */
+ /client/proc/admin_observe_target, /*and gives it to us on right click*/
/client/proc/toggle_view_range, /*changes how far we can see*/
/client/proc/cmd_admin_pm_context, /*right-click adminPM interface*/
/client/proc/cmd_admin_pm_panel, /*admin-pm list*/
@@ -196,13 +198,17 @@ GLOBAL_LIST_INIT(admin_verbs_mod, list(
/client/proc/dsay,
/datum/admins/proc/show_player_panel,
/client/proc/ban_panel,
- /client/proc/debug_variables /*allows us to -see- the variables of any instance in the game. +VAREDIT needed to modify*/
+ /client/proc/debug_variables, /*allows us to -see- the variables of any instance in the game. +VAREDIT needed to modify*/
+ /client/proc/admin_observe,
+ /client/proc/admin_observe_target,
))
GLOBAL_LIST_INIT(admin_verbs_mentor, list(
/client/proc/cmd_admin_pm_context, /*right-click adminPM interface*/
/client/proc/cmd_admin_pm_panel, /*admin-pm list*/
/client/proc/cmd_admin_pm_by_key_panel, /*admin-pm list by key*/
/client/proc/openMentorTicketUI,
+ /client/proc/admin_observe, /* Allow mentors to observe as well, though they face some limitations */
+ /client/proc/admin_observe_target,
/client/proc/cmd_mentor_say /* mentor say*/
// cmd_mentor_say is added/removed by the toggle_mentor_chat verb
))
@@ -339,13 +345,22 @@ GLOBAL_LIST_INIT(view_runtimes_verbs, list(
to_chat(src, "All of your adminverbs are now visible.")
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Admin Verbs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
-/client/proc/admin_ghost()
- set category = "Admin"
- set name = "Aghost"
+/client/proc/mentor_ghost()
+ var/is_mentor = check_rights(R_MENTOR, FALSE)
+ var/is_full_admin = check_rights(R_ADMIN|R_MOD, FALSE)
- if(!check_rights(R_ADMIN|R_MOD))
+ if(!is_mentor && !is_full_admin)
+ to_chat(src, "You aren't allowed to use this!")
return
+ // mentors are allowed only if they have the observe trait, which is given on observe.
+ // they should also not be given this proc.
+ if(!is_full_admin && (is_mentor && !HAS_MIND_TRAIT(mob, TRAIT_MENTOR_OBSERVING) || !is_mentor))
+ return
+
+ do_aghost()
+
+/client/proc/do_aghost()
if(isobserver(mob))
//re-enter
var/mob/dead/observer/ghost = mob
@@ -372,6 +387,134 @@ GLOBAL_LIST_INIT(view_runtimes_verbs, list(
// TODO: SStgui.on_transfer() to move windows from old and new
SSblackbox.record_feedback("tally", "admin_verb", 1, "Aghost") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
+/client/proc/admin_ghost()
+ set category = "Admin"
+ set name = "Aghost"
+
+ if(!check_rights(R_ADMIN|R_MOD))
+ return
+
+ do_aghost()
+
+/// Allow an admin to observe someone.
+/// mentors are allowed to use this verb while living, but with some stipulations:
+/// if they attempt to do anything that would stop their orbit, they will immediately be returned to their body.
+/client/proc/admin_observe()
+ set name = "Aobserve"
+ set category = "Admin"
+ if(!check_rights(R_ADMIN|R_MOD|R_MENTOR))
+ return
+
+ if(isnewplayer(mob))
+ to_chat(src, "You cannot aobserve while in the lobby. Please join or observe first.")
+ return
+
+ var/mob/target
+
+ target = tgui_input_list(mob, "Select a mob to observe", "Aobserve", GLOB.player_list)
+ if(isnull(target))
+ return
+ if(target == src)
+ to_chat(src, "You can't observe yourself!")
+ return
+
+ if(isobserver(target))
+ to_chat(src, "[target] is a ghost, and cannot be observed.")
+ return
+
+ if(isnewplayer(target))
+ to_chat(src, "[target] is in the lobby, and cannot be observed.")
+ return
+
+ admin_observe_target(target)
+
+/client/proc/cleanup_admin_observe(mob/dead/observer/ghost)
+ if(!istype(ghost) || !ghost.mob_observed)
+ return FALSE
+
+ // un-follow them
+ ghost.cleanup_observe()
+ // if it's a mentor, make sure they go back to their body.
+ if(HAS_TRAIT(mob.mind, TRAIT_MENTOR_OBSERVING))
+ // handler will handle removing the trait
+ mob.stop_orbit()
+ log_admin("[key_name(src)] has de-activated Aobserve")
+ SSblackbox.record_feedback("tally", "admin_verb", 1, "Aobserve")
+ return TRUE
+
+/// targeted form of admin_observe: this should only appear in the right-click menu.
+/client/proc/admin_observe_target(mob/target as mob in GLOB.mob_list)
+ set name = "\[Admin\] Aobserve"
+ set category = null
+
+ if(!check_rights(R_ADMIN|R_MOD|R_MENTOR, mob))
+ return
+
+ var/full_admin = check_rights(R_ADMIN|R_MOD, FALSE, mob)
+
+ if(isnewplayer(mob))
+ to_chat(src, "You cannot aobserve while in the lobby. Please join or observe first.")
+ return
+
+ if(isnewplayer(target))
+ to_chat(src, "[target] is currently in the lobby.")
+ return
+
+ if(isobserver(target))
+ to_chat(src, "You can't observe a ghost.")
+ return
+
+ if(cleanup_admin_observe(mob))
+ return
+
+ if(isnull(target) || target == src)
+ // let the default one find the target if there isn't one
+ admin_observe()
+ return
+
+ // observers don't need to ghost, so we don't need to worry about adding any traits
+ if(isobserver(mob))
+ var/mob/dead/observer/ghost = mob
+ SSblackbox.record_feedback("tally", "admin_verb", 1, "Aobserve")
+ ghost.do_observe(target)
+ return
+
+ log_admin("[key_name(src)] has Aobserved out of their body to follow [target]")
+ do_aghost()
+ var/mob/dead/observer/ghost = mob
+
+ if(!full_admin)
+ // if they're a me and they're alive, add the MENTOR_OBSERVINGtrait to ensure that they can only go back to their body.
+ // we need to handle this here because when you aghost, your mob gets set to the ghost. Oops!
+ ADD_TRAIT(mob.mind, TRAIT_MENTOR_OBSERVING, MENTOR_OBSERVING)
+ RegisterSignal(ghost, COMSIG_ATOM_ORBITER_STOP, PROC_REF(on_mentor_observe_end), override = TRUE)
+ to_chat(src, "You have temporarily observed [target], either move or observe again to un-observe.")
+ log_admin("[key_name(src)] has mobserved out of their body to follow [target].")
+ else
+ log_admin("[key_name(src)] is aobserving [target].")
+
+
+ ghost.do_observe(target)
+
+/client/proc/on_mentor_observe_end(atom/movable/us, atom/movable/orbited)
+ SIGNAL_HANDLER // COMSIG_ATOM_ORBITER_STOP
+ if(!isobserver(mob))
+ log_and_message_admins("A mentor somehow managed to end observing while not being a ghost. Please investigate and notify coders.")
+ return
+ var/mob/dead/observer/ghost = mob
+
+ // just to be safe
+ ghost.cleanup_observe()
+
+ REMOVE_TRAIT(mob.mind, TRAIT_MENTOR_OBSERVING, MENTOR_OBSERVING)
+ UnregisterSignal(mob, COMSIG_ATOM_ORBITER_STOP)
+
+ if(!ghost.reenter_corpse())
+ // tell everyone since this is kinda nasty.
+ log_debug("Mentor [key_name_mentor(src)] was unable to re-enter their body after mentor observing.")
+ log_and_message_admins("[key_name_mentor(src)] was unable to re-enter their body after mentor observing.")
+ to_chat(src, "Unable to return you to your body after mentor ghosting. If your body still exists, please contact a coder, and you should probably ahelp.")
+
/client/proc/invisimin()
set name = "Invisimin"
set category = "Admin"
diff --git a/code/modules/admin/player_panel.dm b/code/modules/admin/player_panel.dm
index ca98106ea1b..3b33c728713 100644
--- a/code/modules/admin/player_panel.dm
+++ b/code/modules/admin/player_panel.dm
@@ -86,7 +86,8 @@
body += "PM - "
body += "SM - "
body += "FLW - "
- body += "ALERT"
+ body += "ALERT - "
+ body += "OBS"
if(eyeUID)
body += "|EYE"
body += "
"
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index cdbce0a63d8..ba2d9b6432e 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -1613,6 +1613,17 @@
show_player_panel(H)
//H.regenerate_icons()
+ else if(href_list["adminobserve"])
+ if(!check_rights(R_ADMIN|R_MOD|R_MENTOR))
+ return
+ var/client/C = usr.client
+ var/mob/M = locateUID(href_list["adminobserve"])
+
+ if(!ismob(M))
+ to_chat(usr, "This can only be used on instances of type /mob")
+ return
+ C.admin_observe_target(M)
+
else if(href_list["adminplayeropts"])
var/mob/M = locateUID(href_list["adminplayeropts"])
@@ -3586,3 +3597,5 @@
var/mob/dead/observer/O = target
if(O.mind && O.mind.current)
. += "|[ADMIN_FLW(O.mind.current,"BDY")]"
+ else if(ismob(target))
+ . += "|[ADMIN_OBS(target, "OBS")]"
diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm
index f094b4d9644..f45f123f57d 100644
--- a/code/modules/admin/verbs/adminpm.dm
+++ b/code/modules/admin/verbs/adminpm.dm
@@ -178,6 +178,7 @@
var/ping_link = check_rights(R_ADMIN, 0, mob) ? "(PING)" : ""
var/ticket_link
var/alert_link = check_rights(R_ADMIN, FALSE, mob) ? "(ALERT)" : ""
+ var/observe_link = check_rights(R_MENTOR, FALSE, mob) ? "([ADMIN_OBS(C, "OBS")])" : ""
if(ticket_id != -1)
if(message_type == MESSAGE_TYPE_MENTORPM)
ticket_link = "(TICKET)"
@@ -190,7 +191,7 @@
receive_window_link = ticket_link
else if(message_type == MESSAGE_TYPE_ADMINPM && check_rights(R_ADMIN, 0, C.mob))
receive_window_link = ticket_link
- receive_message = "[type] from-[receive_pm_type] [C.holder ? key_name(src, TRUE, type, ticket_id = ticket_id) : key_name_hidden(src, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg][C.holder ? "
[ping_link] [receive_window_link] [alert_link]" : ""]"
+ receive_message = "[type] from-[receive_pm_type] [C.holder ? key_name(src, TRUE, type, ticket_id = ticket_id) : key_name_hidden(src, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg][C.holder ? "
[ping_link] [receive_window_link] [alert_link] [observe_link]" : ""]"
if(message_type == MESSAGE_TYPE_MENTORPM)
receive_message = chat_box_mhelp(receive_message)
else
@@ -202,7 +203,7 @@
send_window_link = ticket_link
else if(message_type == MESSAGE_TYPE_ADMINPM && check_rights(R_ADMIN, 0, mob))
send_window_link = ticket_link
- var/send_message = "[send_pm_type][type] to-[holder ? key_name(C, TRUE, type, ticket_id = ticket_id) : key_name_hidden(C, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg]
[ping_link] [send_window_link] [alert_link]"
+ var/send_message = "[send_pm_type][type] to-[holder ? key_name(C, TRUE, type, ticket_id = ticket_id) : key_name_hidden(C, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg]
[ping_link] [send_window_link] [alert_link] [observe_link]"
if(message_type == MESSAGE_TYPE_MENTORPM)
send_message = chat_box_mhelp(send_message)
else
@@ -211,9 +212,9 @@
var/third_party_message
if(message_type == MESSAGE_TYPE_MENTORPM)
- third_party_message = chat_box_mhelp("[type]: [key_name(src, TRUE, type, ticket_id = ticket_id)]->[key_name(C, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg]
[ping_link] [ticket_link] [alert_link]")
+ third_party_message = chat_box_mhelp("[type]: [key_name(src, TRUE, type, ticket_id = ticket_id)]->[key_name(C, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg]
[ping_link] [ticket_link] [alert_link] [observe_link]")
else
- third_party_message = chat_box_ahelp("[type]: [key_name(src, TRUE, type, ticket_id = ticket_id)]->[key_name(C, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg]
[ping_link] [ticket_link] [alert_link]")
+ third_party_message = chat_box_ahelp("[type]: [key_name(src, TRUE, type, ticket_id = ticket_id)]->[key_name(C, TRUE, type, ticket_id = ticket_id)]:
[emoji_msg]
[ping_link] [ticket_link] [alert_link] [observe_link]")
//play the recieving admin the adminhelp sound (if they have them enabled)
//non-admins always hear the sound, as they cannot toggle it
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 64b3c5045d5..73c0d726df4 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -1172,6 +1172,14 @@
else
src << link(GLOB.configuration.system.region_map[choice])
+/client/proc/set_eye(new_eye)
+ if(new_eye == eye)
+ return
+ eye = new_eye
+
+/client/proc/clear_screen()
+ for(var/object in screen)
+ screen -= object
/client/verb/reload_graphics()
set category = "Special Verbs"
diff --git a/code/modules/mob/dead/observer/observer_base.dm b/code/modules/mob/dead/observer/observer_base.dm
index bc0624b2b86..9d1d9e9bb7a 100644
--- a/code/modules/mob/dead/observer/observer_base.dm
+++ b/code/modules/mob/dead/observer/observer_base.dm
@@ -20,6 +20,8 @@ GLOBAL_DATUM_INIT(ghost_crew_monitor, /datum/ui_module/crew_monitor/ghost, new)
move_resist = INFINITY // don't get pushed around
invisibility = INVISIBILITY_OBSERVER
blocks_emissive = FALSE // Ghosts are transparent, duh
+ hud_type = /datum/hud/ghost
+ speaks_ooc = TRUE
var/can_reenter_corpse
var/bootime = FALSE
var/started_as_observer //This variable is set to 1 when you enter the game as an observer.
@@ -41,8 +43,8 @@ GLOBAL_DATUM_INIT(ghost_crew_monitor, /datum/ui_module/crew_monitor/ghost, new)
var/datum/orbit_menu/orbit_menu
/// The "color" their runechat would have had
var/alive_runechat_color = "#FFFFFF"
- hud_type = /datum/hud/ghost
- speaks_ooc = TRUE
+ /// UID of the mob which we are currently observing
+ var/mob_observed
/mob/dead/observer/New(mob/body=null, flags=1)
set_invisibility(GLOB.observer_default_invisibility)
@@ -120,6 +122,8 @@ GLOBAL_DATUM_INIT(ghost_crew_monitor, /datum/ui_module/crew_monitor/ghost, new)
if(seerads)
STOP_PROCESSING(SSobj, src)
remove_observer_verbs()
+ if(mob_observed)
+ cleanup_observe()
return ..()
/mob/dead/observer/examine(mob/user)
@@ -184,25 +188,37 @@ Works together with spawning an observer, noted above.
return 1
/mob/proc/ghostize(flags = GHOST_CAN_REENTER, user_color, ghost_name)
- if(key)
- if(player_logged) //if they have disconnected we want to remove their SSD overlay
- overlays -= image('icons/effects/effects.dmi', icon_state = "zzz_glow")
- if(GLOB.non_respawnable_keys[ckey])
- flags &= ~GHOST_CAN_REENTER
- var/mob/dead/observer/ghost = new(src, flags) //Transfer safety to observer spawning proc.
- ghost.timeofdeath = src.timeofdeath //BS12 EDIT
- if(ghost.can_reenter_corpse)
- ADD_TRAIT(ghost, TRAIT_RESPAWNABLE, GHOSTED)
- else
- GLOB.non_respawnable_keys[ckey] = 1
- if(user_color)
- add_atom_colour(user_color, ADMIN_COLOUR_PRIORITY)
- ghost.color = user_color
- if(ghost_name)
- ghost.name = ghost_name
- ghost.key = key
- ghost.client?.init_verbs()
- return ghost
+ if(!key)
+ return
+ if(player_logged) // if they have disconnected we want to remove their SSD overlay
+ overlays -= image('icons/effects/effects.dmi', icon_state = "zzz_glow")
+ if(GLOB.non_respawnable_keys[ckey])
+ flags &= ~GHOST_CAN_REENTER
+ var/mob/dead/observer/ghost = new(src, flags) // Transfer safety to observer spawning proc.
+ ghost.timeofdeath = src.timeofdeath // BS12 EDIT
+ if(ghost.can_reenter_corpse)
+ ADD_TRAIT(ghost, TRAIT_RESPAWNABLE, GHOSTED)
+ else
+ GLOB.non_respawnable_keys[ckey] = 1
+
+ // mods, mentors, and the like will have admin observe anyway, so this is moot
+ if(((key in GLOB.antag_hud_users) || (key in GLOB.roundstart_observer_keys)) && !check_rights(R_MOD | R_ADMIN | R_MENTOR, FALSE, src))
+ ghost.verbs |= /mob/dead/observer/proc/do_observe
+ ghost.verbs |= /mob/dead/observer/proc/observe
+ if(user_color)
+ add_atom_colour(user_color, ADMIN_COLOUR_PRIORITY)
+ ghost.color = user_color
+ if(ghost_name)
+ ghost.name = ghost_name
+ ghost.key = key
+
+ ghost.client?.init_verbs()
+
+ for(var/mob/dead/observer/obs in observers)
+ obs.cleanup_observe()
+
+
+ return ghost
/*
This is the proc mobs get to turn into a ghost. Forked from ghostize due to compatibility issues.
@@ -492,52 +508,67 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
GLOB.ghost_crew_monitor.ui_interact(src)
/mob/dead/observer/proc/add_observer_verbs()
- verbs.Add(/mob/dead/observer/proc/ManualFollow)
+ verbs.Add(
+ /mob/dead/observer/proc/ManualFollow,
+ )
/mob/dead/observer/proc/remove_observer_verbs()
- verbs.Remove(/mob/dead/observer/proc/ManualFollow)
+ verbs.Remove(
+ /mob/dead/observer/proc/ManualFollow,
+ // these might not necessarily be here, but we want to make sure they're gonezo anyway
+ /mob/dead/observer/proc/observe,
+ /mob/dead/observer/proc/do_observe
+ )
-// This is the ghost's follow verb with an argument
+// This is the ghost's follow verb with an argument.
+// We need to do the usr check on this verb itself, but the logic follows.
/mob/dead/observer/proc/ManualFollow(atom/movable/target)
set name = "\[Observer\] Orbit"
set desc = "Orbits the specified movable atom."
set category = null
- if(!target || !isobserver(usr))
+ // this usr check is apparently necessary for security
+ if(!isobserver(usr))
return
+ return do_manual_follow(target)
+
+// We need to check usr when calling the verb, but we still want this logic to be accessible elsewhere
+/mob/dead/observer/proc/do_manual_follow(atom/movable/target)
if(!get_turf(target))
return
- if(target != src)
- if(src in target.get_orbiters())
- return
+ if(!target || target == src)
+ return
- var/icon/I = icon(target.icon,target.icon_state,target.dir)
+ if(src in target.get_orbiters())
+ return
- var/orbitsize = (I.Width()+I.Height())*0.5
+ var/icon/I = icon(target.icon, target.icon_state, target.dir)
- if(orbitsize == 0)
- orbitsize = 40
+ var/orbitsize = (I.Width() + I.Height())*0.5
- orbitsize -= (orbitsize/world.icon_size)*(world.icon_size*0.25)
+ if(orbitsize == 0)
+ orbitsize = 40
- var/rot_seg
+ orbitsize -= (orbitsize/world.icon_size)*(world.icon_size*0.25)
- switch(ghost_orbit)
- if(GHOST_ORBIT_TRIANGLE)
- rot_seg = 3
- if(GHOST_ORBIT_SQUARE)
- rot_seg = 4
- if(GHOST_ORBIT_PENTAGON)
- rot_seg = 5
- if(GHOST_ORBIT_HEXAGON)
- rot_seg = 6
- else //Circular
- rot_seg = 36 //360/10 bby, smooth enough aproximation of a circle
+ var/rot_seg
- to_chat(src, "Now following [target].")
- orbit(target,orbitsize, FALSE, 20, rot_seg)
+ switch(ghost_orbit)
+ if(GHOST_ORBIT_TRIANGLE)
+ rot_seg = 3
+ if(GHOST_ORBIT_SQUARE)
+ rot_seg = 4
+ if(GHOST_ORBIT_PENTAGON)
+ rot_seg = 5
+ if(GHOST_ORBIT_HEXAGON)
+ rot_seg = 6
+ else // Circular
+ rot_seg = 36 // 360/10 bby, smooth enough aproximation of a circle
+
+ to_chat(src, "Now following [target].")
+ orbit(target, orbitsize, FALSE, 20, rot_seg)
/mob/dead/observer/orbit(atom/A, radius = 10, clockwise = FALSE, rotation_speed = 20, rotation_segments = 36, pre_rotation = TRUE, lock_in_orbit = FALSE, force_move = FALSE, orbit_layer = GHOST_LAYER)
setDir(2)//reset dir so the right directional sprites show up
@@ -825,6 +856,102 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
/mob/dead/observer/get_runechat_color()
return alive_runechat_color
+/mob/dead/observer/proc/observe()
+ set name = "Observe"
+ set desc = "Observe a mob."
+ set category = "Ghost"
+
+ var/list/possible_targets = list()
+ for(var/mob/living/L in GLOB.player_list)
+ if(!L.mind)
+ continue
+ possible_targets.Add(L)
+
+ if(!length(possible_targets))
+ to_chat(src, "There's nobody for you to observe!")
+ return
+
+ var/mob/target = tgui_input_list(usr, "Please, select a player!", "Observe", possible_targets)
+ if(!istype(target) || QDELETED(target))
+ return
+ do_observe(target)
+
+/mob/dead/observer/proc/do_observe(mob/mob_eye)
+ set name = "\[Observer\] Observe"
+ set desc = "Observe the target mob."
+ set category = null
+
+ if(isnewplayer(mob_eye))
+ to_chat(src, "You can't observe someone in the lobby.")
+ return
+
+ if(isobserver(mob_eye))
+ to_chat(src, "You can't observe a ghost.")
+ return
+
+ if(!mob_eye.mind)
+ to_chat(src, "You can only observe mobs that have been or are being inhabited by a player!")
+ return
+
+ if(mob_eye == src)
+ to_chat(src, "You can't observe yourself!")
+ return
+
+ if(mob_observed)
+ // clean up first
+ stop_orbit()
+ cleanup_observe()
+
+ // Istype so we filter out points of interest that are not mobs
+ if(client && ismob(mob_eye))
+ // follow the mob so they're technically right there for visible messages n stuff
+ // call the sub-proc since the base one checks for usr
+ do_manual_follow(mob_eye)
+ client.set_eye(mob_eye)
+ add_attack_logs(src, mob_eye, "observed", ATKLOG_ALMOSTALL)
+ client.perspective = EYE_PERSPECTIVE
+ if(mob_eye.hud_used)
+ client.clear_screen()
+ LAZYOR(mob_eye.observers, src)
+ mob_eye.hud_used?.show_hud(mob_eye.hud_used.hud_version, src)
+ mob_observed = mob_eye.UID()
+
+
+ // mentor observing grants you this trait, and provides its own signal handler for this
+ if(!HAS_MIND_TRAIT(src, TRAIT_MENTOR_OBSERVING))
+ RegisterSignal(src, COMSIG_ATOM_ORBITER_STOP, PROC_REF(on_observer_orbit_end), override = TRUE)
+ else
+ if(!check_rights(R_MENTOR, FALSE, src))
+ log_debug("[key_name(src)] has the the mobserve trait while observing, but isn't a mentor. This is likely an error, and may result in them getting stuck")
+
+/// Clean up observing
+/mob/dead/observer/proc/cleanup_observe()
+ if(isnull(mob_observed))
+ return
+
+ var/mob/target = locateUID(mob_observed)
+ add_attack_logs(src, target, "un-observed", ATKLOG_ALL)
+ mob_observed = null
+ reset_perspective(null)
+ client?.perspective = initial(client.perspective)
+ set_sight(initial(sight))
+ UnregisterSignal(src, COMSIG_ATOM_ORBITER_STOP)
+
+ if(s_active)
+ var/obj/item/storage/bag = s_active
+ s_active = null
+ bag.update_viewers(src)
+
+ if(!QDELETED(target) && istype(target))
+ hide_other_mob_action_buttons(target)
+ target.observers -= src
+
+/mob/dead/observer/proc/on_observer_orbit_end(mob/follower, atom)
+ SIGNAL_HANDLER // COMSIG_ATOM_ORBITER_STOP
+ if(HAS_MIND_TRAIT(src, TRAIT_MENTOR_OBSERVING))
+ log_debug("[key_name(src)] ended up in regular cleanup_observe rather than the mentor cleanup observe despite having TRAIT_MENTOR_OBSERVING. This is likely a bug and may result in them being stuck outside of their bodies.")
+ cleanup_observe()
+
/mob/dead/observer/proc/update_dead_radio()
if(get_preference(PREFTOGGLE_CHAT_GHOSTRADIO))
GLOB.deadchat_radio.listeners |= src
diff --git a/code/modules/mob/dead/observer/observer_logout.dm b/code/modules/mob/dead/observer/observer_logout.dm
index 64ef90683c3..d6c1b6e96fb 100644
--- a/code/modules/mob/dead/observer/observer_logout.dm
+++ b/code/modules/mob/dead/observer/observer_logout.dm
@@ -1,6 +1,9 @@
/mob/dead/observer/Logout()
if(client)
client.images -= GLOB.ghost_images
+ if(mob_observed && ismob(locateUID(mob_observed)))
+ cleanup_observe()
+
..()
spawn(0)
if(src && !key) //we've transferred to another mob. This ghost should be deleted.
diff --git a/code/modules/mob/living/carbon/carbon_update_icons.dm b/code/modules/mob/living/carbon/carbon_update_icons.dm
index f174dd227ca..203b7ae10d4 100644
--- a/code/modules/mob/living/carbon/carbon_update_icons.dm
+++ b/code/modules/mob/living/carbon/carbon_update_icons.dm
@@ -49,6 +49,8 @@
r_hand.screen_loc = ui_rhand
client.screen += r_hand
+ update_observer_view(r_hand)
+
/mob/living/carbon/update_inv_l_hand(ignore_cuffs)
if(handcuffed && !ignore_cuffs)
drop_l_hand()
@@ -57,6 +59,7 @@
if(client && hud_used && hud_used.hud_version != HUD_STYLE_NOHUD)
l_hand.screen_loc = ui_lhand
client.screen += l_hand
+ update_observer_view(l_hand)
/mob/living/carbon/update_inv_wear_mask()
if(istype(wear_mask, /obj/item/clothing/mask))
@@ -86,3 +89,17 @@
/mob/living/carbon/proc/update_hud_back(obj/item/I)
return
+/mob/living/carbon/proc/update_observer_view(obj/item/worn_item, inventory)
+ if(!length(observers))
+ return
+ for(var/mob/dead/observe as anything in observers)
+ if(observe.client && observe.client.eye == src)
+ if(observe.hud_used)
+ if(inventory && !observe.hud_used.inventory_shown)
+ continue
+ observe.client.screen += worn_item
+ else
+ observers -= observe
+ if(!length(observers))
+ observers.Cut()
+ break
diff --git a/code/modules/mob/living/carbon/human/human_inventory.dm b/code/modules/mob/living/carbon/human/human_inventory.dm
index 3b39a48a1f0..1ee19379c31 100644
--- a/code/modules/mob/living/carbon/human/human_inventory.dm
+++ b/code/modules/mob/living/carbon/human/human_inventory.dm
@@ -186,6 +186,13 @@
update_inv_r_hand()
I.screen_loc = null
+ if(client)
+ client.screen -= I
+ if(length(observers))
+ for(var/mob/dead/observe as anything in observers)
+ if(observe.client)
+ observe.client.screen -= I
+
I.forceMove(src)
I.equipped(src, slot, initial)
I.layer = ABOVE_HUD_LAYER
@@ -271,8 +278,8 @@
var/obj/item/clothing/head/hat = I
if(hat.vision_flags || hat.see_in_dark || !isnull(hat.lighting_alpha))
update_sight()
+ // this calls update_inv_head() on its own
head_update(I)
- update_inv_head()
if(SLOT_HUD_SHOES)
shoes = I
update_inv_shoes()
diff --git a/code/modules/mob/living/carbon/human/human_update_icons.dm b/code/modules/mob/living/carbon/human/human_update_icons.dm
index feaad4085f9..c1109e078d1 100644
--- a/code/modules/mob/living/carbon/human/human_update_icons.dm
+++ b/code/modules/mob/living/carbon/human/human_update_icons.dm
@@ -567,10 +567,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
inv.update_icon()
if(w_uniform && istype(w_uniform, /obj/item/clothing/under))
- if(client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD)
- if(hud_used.inventory_shown) //if the inventory is open ...
- w_uniform.screen_loc = ui_iclothing //...draw the item in the inventory screen
- client.screen += w_uniform //Either way, add the item to the HUD
+ update_hud_uniform(w_uniform)
var/t_color = w_uniform.item_color
if(!t_color)
@@ -644,9 +641,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
inv.update_icon()
if(wear_id)
- if(client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD)
- wear_id.screen_loc = ui_id
- client.screen += wear_id
+ update_hud_id(wear_id)
if(w_uniform && w_uniform:displays_id)
overlays_standing[ID_LAYER] = mutable_appearance('icons/mob/mob.dmi', "id", layer = -ID_LAYER)
@@ -660,10 +655,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
inv.update_icon()
if(gloves)
- if(client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD)
- if(hud_used.inventory_shown) //if the inventory is open ...
- gloves.screen_loc = ui_gloves //...draw the item in the inventory screen
- client.screen += gloves //Either way, add the item to the HUD
+ update_hud_gloves(gloves)
var/t_state = gloves.item_state
if(!t_state) t_state = gloves.icon_state
@@ -702,10 +694,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
if(glasses)
var/mutable_appearance/new_glasses
var/obj/item/organ/external/head/head_organ = get_organ("head")
- if(client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD)
- if(hud_used.inventory_shown) //if the inventory is open ...
- glasses.screen_loc = ui_glasses //...draw the item in the inventory screen
- client.screen += glasses //Either way, add the item to the HUD
+ update_hud_glasses(glasses)
if(glasses.icon_override)
new_glasses = mutable_appearance(glasses.icon_override, "[glasses.icon_state]", layer = -GLASSES_LAYER)
@@ -744,10 +733,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
if(l_ear || r_ear)
if(l_ear)
- if(client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD)
- if(hud_used.inventory_shown) //if the inventory is open ...
- l_ear.screen_loc = ui_l_ear //...draw the item in the inventory screen
- client.screen += l_ear //Either way, add the item to the HUD
+ update_hud_l_ear(l_ear)
var/t_type = l_ear.item_state
if(!t_type)
@@ -761,10 +747,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
overlays_standing[EARS_LAYER] = mutable_appearance('icons/mob/clothing/ears.dmi', "[t_type]", layer = -EARS_LAYER)
if(r_ear)
- if(client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD)
- if(hud_used.inventory_shown) //if the inventory is open ...
- r_ear.screen_loc = ui_r_ear //...draw the item in the inventory screen
- client.screen += r_ear //Either way, add the item to the HUD
+ update_hud_r_ear(r_ear)
var/t_type = r_ear.item_state
if(!t_type)
@@ -786,10 +769,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
inv.update_icon()
if(shoes)
- if(client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD)
- if(hud_used.inventory_shown) //if the inventory is open ...
- shoes.screen_loc = ui_shoes //...draw the item in the inventory screen
- client.screen += shoes //Either way, add the item to the HUD
+ update_hud_shoes(shoes)
if(!wear_suit || !(wear_suit.flags_inv & HIDESHOES))
var/mutable_appearance/standing
@@ -822,9 +802,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
inv.update_icon()
if(s_store)
- if(client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD)
- s_store.screen_loc = ui_sstore1
- client.screen += s_store
+ update_hud_s_store(s_store)
var/t_state = s_store.item_state
if(!t_state)
@@ -844,6 +822,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
inv.update_icon()
if(head)
+ update_hud_head(head)
var/mutable_appearance/standing
if(head.sprite_sheets && head.sprite_sheets[dna.species.sprite_sheet_name])
standing = mutable_appearance(head.sprite_sheets[dna.species.sprite_sheet_name], "[head.icon_state]", layer = -HEAD_LAYER)
@@ -877,6 +856,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
belt.screen_loc = ui_belt
if(belt)
+ update_observer_view(belt)
var/t_state = belt.item_state
if(!t_state)
t_state = belt.icon_state
@@ -899,10 +879,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
inv.update_icon()
if(wear_suit && istype(wear_suit, /obj/item/clothing/suit))
- if(client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD)
- if(hud_used.inventory_shown) //if the inventory is open ...
- wear_suit.screen_loc = ui_oclothing //TODO //...draw the item in the inventory screen
- client.screen += wear_suit //Either way, add the item to the HUD
+ update_hud_wear_suit(wear_suit)
var/mutable_appearance/standing
if(wear_suit.sprite_sheets && wear_suit.sprite_sheets[dna.species.sprite_sheet_name])
@@ -950,12 +927,17 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
if(hud_used.hud_version == HUD_STYLE_STANDARD)
if(l_store)
- client.screen += l_store
l_store.screen_loc = ui_storage1
+ if(hud_used.hud_shown)
+ client.screen += l_store
+ update_observer_view(l_store)
if(r_store)
- client.screen += r_store
r_store.screen_loc = ui_storage2
+ if(hud_used.hud_shown)
+ client.screen += r_store
+ r_store.screen_loc = ui_storage2
+ update_observer_view(r_store)
/mob/living/carbon/human/update_inv_wear_pda()
if(client && hud_used)
@@ -964,8 +946,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
inv.update_icon()
if(wear_pda)
- client.screen += wear_pda
- wear_pda.screen_loc = ui_pda
+ update_hud_wear_pda(wear_pda)
/mob/living/carbon/human/update_inv_wear_mask()
..()
@@ -975,6 +956,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
if(inv)
inv.update_icon()
if(wear_mask && (istype(wear_mask, /obj/item/clothing/mask) || istype(wear_mask, /obj/item/clothing/accessory)))
+ update_hud_wear_mask(wear_mask)
if(!(SLOT_HUD_WEAR_MASK in check_obscured_slots()))
var/obj/item/organ/external/head/head_organ = get_organ("head")
if(!istype(head_organ))
@@ -1009,6 +991,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
..()
remove_overlay(BACK_LAYER)
if(back)
+ update_hud_back(back)
//determine the icon to use
var/t_state = back.item_state
if(!t_state)
@@ -1045,6 +1028,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
..()
remove_overlay(R_HAND_LAYER)
if(r_hand)
+ show_hand_to_observers(r_hand, left = FALSE)
var/t_state = r_hand.item_state
if(!t_state)
t_state = r_hand.icon_state
@@ -1064,6 +1048,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
..()
remove_overlay(L_HAND_LAYER)
if(l_hand)
+ show_hand_to_observers(l_hand, left = TRUE)
var/t_state = l_hand.item_state
if(!t_state)
t_state = l_hand.icon_state
@@ -1080,25 +1065,105 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
//human HUD updates for items in our inventory
-//update whether our head item appears on our hud.
-/mob/living/carbon/human/update_hud_head(obj/item/I)
- if(client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD)
- if(hud_used.inventory_shown)
- I.screen_loc = ui_head
- client.screen += I
+/mob/living/carbon/human/proc/show_hand_to_observers(obj/item/worn_item, left = TRUE)
+ if(client && hud_used && hud_used.hud_version != HUD_STYLE_NOHUD)
+ worn_item.screen_loc = ui_hand_position(left ? 1 : 2)
+ client.screen += worn_item
+ if(length(observers))
+ for(var/mob/dead/observe in observers)
+ if(observe.client && observe.client.eye == src)
+ observe.client.screen += worn_item
+ else
+ observers -= observe
+ if(!length(observers))
+ observers = null
+ break
-//update whether our mask item appears on our hud.
-/mob/living/carbon/human/update_hud_wear_mask(obj/item/I)
- if(client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD)
- if(hud_used.inventory_shown)
- I.screen_loc = ui_mask
- client.screen += I
-//update whether our back item appears on our hud.
-/mob/living/carbon/human/update_hud_back(obj/item/I)
- if(client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD)
- I.screen_loc = ui_back
- client.screen += I
+/mob/living/carbon/human/update_hud_wear_mask(obj/item/worn_item)
+ worn_item.screen_loc = ui_mask
+ if((client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD) && (hud_used.inventory_shown && hud_used.hud_shown))
+ client.screen += worn_item
+ update_observer_view(worn_item, TRUE)
+
+
+/mob/living/carbon/human/proc/update_hud_uniform(obj/item/worn_item)
+ worn_item.screen_loc = ui_iclothing
+ if((client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD) && (hud_used.inventory_shown && hud_used.hud_shown))
+ client.screen += worn_item
+ update_observer_view(worn_item, TRUE)
+
+/mob/living/carbon/human/proc/update_hud_id(obj/item/worn_item)
+ worn_item.screen_loc = ui_id
+ if(client && hud_used?.hud_shown && hud_used.hud_version == HUD_STYLE_STANDARD)
+ client.screen += worn_item
+ update_observer_view(worn_item)
+
+/mob/living/carbon/human/proc/update_hud_wear_pda(obj/item/worn_item)
+ worn_item.screen_loc = ui_pda
+ if(client && hud_used?.hud_shown && hud_used.hud_version == HUD_STYLE_STANDARD)
+ client.screen += worn_item
+ update_observer_view(worn_item)
+
+/mob/living/carbon/human/proc/update_hud_gloves(obj/item/worn_item)
+ worn_item.screen_loc = ui_gloves
+ if((client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD) && (hud_used.inventory_shown && hud_used.hud_shown))
+ client.screen += worn_item
+ update_observer_view(worn_item, TRUE)
+
+/mob/living/carbon/human/proc/update_hud_glasses(obj/item/worn_item)
+ worn_item.screen_loc = ui_glasses
+ if((client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD) && (hud_used.inventory_shown && hud_used.hud_shown))
+ client.screen += worn_item
+ update_observer_view(worn_item, TRUE)
+
+/mob/living/carbon/human/proc/update_hud_l_ear(obj/item/worn_item)
+ worn_item.screen_loc = ui_l_ear
+ if((client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD) && (hud_used.inventory_shown && hud_used.hud_shown))
+ client.screen += worn_item
+ update_observer_view(worn_item, TRUE)
+
+/mob/living/carbon/human/proc/update_hud_r_ear(obj/item/worn_item)
+ worn_item.screen_loc = ui_r_ear
+ if((client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD) && (hud_used.inventory_shown && hud_used.hud_shown))
+ client.screen += worn_item
+ update_observer_view(worn_item, TRUE)
+
+/mob/living/carbon/human/proc/update_hud_shoes(obj/item/worn_item)
+ worn_item.screen_loc = ui_shoes
+ if((client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD) && (hud_used.inventory_shown && hud_used.hud_shown))
+ client.screen += worn_item
+ update_observer_view(worn_item, TRUE)
+
+/mob/living/carbon/human/proc/update_hud_s_store(obj/item/worn_item)
+ worn_item.screen_loc = ui_sstore1
+ if(client && hud_used?.hud_shown && hud_used.hud_version == HUD_STYLE_STANDARD)
+ client.screen += worn_item
+ update_observer_view(worn_item, TRUE)
+
+/mob/living/carbon/human/proc/update_hud_wear_suit(obj/item/worn_item)
+ worn_item.screen_loc = ui_oclothing
+ if((client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD) && (hud_used.inventory_shown && hud_used.hud_shown))
+ client.screen += worn_item
+ update_observer_view(worn_item, TRUE)
+
+/mob/living/carbon/human/proc/update_hud_belt(obj/item/worn_item)
+ belt.screen_loc = ui_belt
+ if(client && hud_used?.hud_shown && hud_used.hud_version == HUD_STYLE_STANDARD)
+ client.screen += worn_item
+ update_observer_view(worn_item, TRUE)
+
+/mob/living/carbon/human/update_hud_head(obj/item/worn_item)
+ worn_item.screen_loc = ui_head
+ if((client && hud_used && hud_used.hud_version == HUD_STYLE_STANDARD) && (hud_used.inventory_shown && hud_used.hud_shown))
+ client.screen += worn_item
+ update_observer_view(worn_item, TRUE)
+
+/mob/living/carbon/human/update_hud_back(obj/item/worn_item)
+ worn_item.screen_loc = ui_back
+ if(client && hud_used?.hud_shown && hud_used.hud_version == HUD_STYLE_STANDARD)
+ client.screen += worn_item
+ update_observer_view(worn_item, TRUE)
/mob/living/carbon/human/proc/update_wing_layer()
remove_overlay(WING_UNDERLIMBS_LAYER)
diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm
index 890b5209345..23e6ba4f0ab 100644
--- a/code/modules/mob/living/death.dm
+++ b/code/modules/mob/living/death.dm
@@ -64,7 +64,7 @@
if(mind && suiciding)
mind.suicided = TRUE
reset_perspective(null)
- hud_used?.reload_fullscreen()
+ reload_fullscreen()
update_sight()
update_action_buttons_icon()
ADD_TRAIT(src, TRAIT_FLOORED, STAT_TRAIT)
diff --git a/code/modules/mob/living/silicon/robot/robot_inventory.dm b/code/modules/mob/living/silicon/robot/robot_inventory.dm
index 29cbd551747..78bf0e04a6d 100644
--- a/code/modules/mob/living/silicon/robot/robot_inventory.dm
+++ b/code/modules/mob/living/silicon/robot/robot_inventory.dm
@@ -15,6 +15,7 @@
O.mouse_opacity = MOUSE_OPACITY_OPAQUE
+ observer_screen_update(O, add = FALSE)
if(client)
client.screen -= O
contents -= O
@@ -78,6 +79,7 @@
set_actions(O)
else
to_chat(src, "You need to disable a module first!")
+ observer_screen_update(O, add = TRUE)
check_module_damage(FALSE)
update_icons()
@@ -250,3 +252,25 @@
hands.icon_state = "nomod"
else
hands.icon_state = lowertext(module.module_type)
+
+
+/**
+ * Updates the observers's screens with cyborg itemss.
+ * Arguments
+ * * item_module - the item being added or removed from the screen
+ * * add - whether or not the item is being added, or removed.
+ */
+/mob/living/silicon/robot/proc/observer_screen_update(obj/item/item_module, add = TRUE)
+ if(!length(observers))
+ return
+ for(var/mob/dead/observe in observers)
+ if(observe.client && observe.client.eye == src)
+ if(add)
+ observe.client.screen += item_module
+ else
+ observe.client.screen -= item_module
+ else
+ observers -= observe
+ if(!length(observers))
+ observers = null
+ break
diff --git a/code/modules/mob/living/silicon/robot/robot_mob.dm b/code/modules/mob/living/silicon/robot/robot_mob.dm
index d95d0d9d294..bb700a8d976 100644
--- a/code/modules/mob/living/silicon/robot/robot_mob.dm
+++ b/code/modules/mob/living/silicon/robot/robot_mob.dm
@@ -27,10 +27,6 @@ GLOBAL_LIST_INIT(robot_verbs_default, list(
var/atom/movable/screen/lamp_button = null
var/atom/movable/screen/thruster_button = null
- /// Used to determine whether the robot has the module menu shown or not.
- var/shown_robot_modules = FALSE
- var/atom/movable/screen/robot_modules_background
-
// 3 Modules can be activated at any one time.
var/obj/item/robot_module/module = null
var/module_active = null
@@ -172,11 +168,6 @@ GLOBAL_LIST_INIT(robot_verbs_default, list(
wires = new(src)
- robot_modules_background = new()
- robot_modules_background.icon_state = "block"
- robot_modules_background.layer = HUD_LAYER //Objects that appear on screen are on layer 20, UI should be just below it.
- robot_modules_background.plane = HUD_PLANE
-
ident = rand(1, 999)
rename_character(null, get_default_name())
update_icons()
@@ -664,9 +655,6 @@ GLOBAL_LIST_INIT(robot_verbs_default, list(
/mob/living/silicon/robot/proc/reset_module()
notify_ai(2)
-
- shown_robot_modules = 0
- client?.screen -= robot_modules_background
client?.screen -= hud_used.module_store_icon
uneq_all()
SStgui.close_user_uis(src)
@@ -699,6 +687,8 @@ GLOBAL_LIST_INIT(robot_verbs_default, list(
status_flags |= CANPUSH
+ hud_used.update_robot_modules_display()
+
//for borg hotkeys, here module refers to borg inv slot, not core module
/mob/living/silicon/robot/verb/cmd_toggle_module(module as num)
set name = "Toggle Module"
diff --git a/code/modules/mob/living/stat_states.dm b/code/modules/mob/living/stat_states.dm
index 551eac92d1d..0698dd7bf79 100644
--- a/code/modules/mob/living/stat_states.dm
+++ b/code/modules/mob/living/stat_states.dm
@@ -75,7 +75,7 @@
update_blind_effects()
update_sight()
updatehealth("update revive")
- hud_used?.reload_fullscreen()
+ reload_fullscreen()
SEND_SIGNAL(src, COMSIG_LIVING_REVIVE, updating)
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index b89bb7ec75a..489d8edacaf 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -27,6 +27,10 @@
viewing_alternate_appearances = null
LAssailant = null
runechat_msg_location = null
+ if(length(observers))
+ for(var/mob/dead/observe as anything in observers)
+ observe.reset_perspective(null)
+
return ..()
/mob/Initialize(mapload)
@@ -600,7 +604,7 @@ GLOBAL_LIST_INIT(slot_equipment_priority, list( \
if(.)
// Allows sharing HUDs with ghosts
if(hud_used)
- client.screen = list()
+ client.clear_screen()
hud_used.show_hud(hud_used.hud_version)
//mob verbs are faster than object verbs. See http://www.byond.com/forum/?post=1326139&page=2#comment8198716 for why this isn't atom/verb/examine()
diff --git a/code/modules/mob/mob_vars.dm b/code/modules/mob/mob_vars.dm
index 9f71bc02cfe..b46a1d29f4e 100644
--- a/code/modules/mob/mob_vars.dm
+++ b/code/modules/mob/mob_vars.dm
@@ -207,7 +207,7 @@
var/datum/input_focus = null
/// Is our mob currently suiciding? Used for suicide code along with many different revival checks
var/suiciding = FALSE
- /// Used for some screen objects, such as
+ /// Used for some screen objects
var/list/screens = list()
/// lazy list. contains /atom/movable/screen/alert only, On /mob so clientless mobs will throw alerts properly
var/list/alerts
@@ -246,7 +246,8 @@
var/next_click_modifier = 1
/// Tracks the open UIs that a mob has, used in TGUI for various things, such as updating UIs
var/list/open_uis = list()
-
+ /// List of observers currently observing us.
+ var/list/mob/dead/observer/observers = list()
/// Does this mob speak OOC?
/// Controls whether they can say some symbols.
var/speaks_ooc = FALSE
diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm
index 9e354111a88..ecd4db10155 100644
--- a/code/modules/mob/new_player/new_player.dm
+++ b/code/modules/mob/new_player/new_player.dm
@@ -182,7 +182,10 @@
var/period_human_readable = "within [GLOB.configuration.general.roundstart_observer_period] minute\s"
if(GLOB.configuration.general.roundstart_observer_period == 0)
period_human_readable = "before the round started"
- to_chat(src, "As you observed [period_human_readable], you can freely toggle antag-hud without losing respawnability.")
+ to_chat(src, "As you observed [period_human_readable], you can freely toggle antag-hud without losing respawnability, and can freely observe what other players see.")
+ if(!check_rights(R_MOD | R_ADMIN | R_MENTOR, FALSE, src))
+ // admins always get aobserve
+ add_verb(observer, list(/mob/dead/observer/proc/do_observe, /mob/dead/observer/proc/observe))
observer.started_as_observer = 1
close_spawn_windows()
var/obj/spawn_point
diff --git a/code/modules/tgui/modules/ghost_hud_panel.dm b/code/modules/tgui/modules/ghost_hud_panel.dm
index cff69061394..2e6a6b8d58d 100644
--- a/code/modules/tgui/modules/ghost_hud_panel.dm
+++ b/code/modules/tgui/modules/ghost_hud_panel.dm
@@ -81,6 +81,9 @@ GLOBAL_DATUM_INIT(ghost_hud_panel, /datum/ui_module/ghost_hud_panel, new)
GLOB.antag_hud_users |= ghost.ckey
+ if(!check_rights(R_MOD | R_ADMIN | R_MENTOR, FALSE))
+ // admins always get aobserve
+ add_verb(ghost, list(/mob/dead/observer/proc/do_observe, /mob/dead/observer/proc/observe))
ghost.antagHUD = TRUE
for(var/datum/atom_hud/antag/H in GLOB.huds)