diff --git a/baystation12.dme b/baystation12.dme
index 315f0d53d9a..d4be6003a94 100644
--- a/baystation12.dme
+++ b/baystation12.dme
@@ -212,6 +212,7 @@
#include "code\datums\helper_datums\global_iterator.dm"
#include "code\datums\helper_datums\teleport.dm"
#include "code\datums\helper_datums\topic_input.dm"
+#include "code\datums\observation\move.dm"
#include "code\datums\observation\observation.dm"
#include "code\datums\uplink\ammunition.dm"
#include "code\datums\uplink\announcements.dm"
@@ -1885,6 +1886,7 @@
#include "code\unit_tests\equipment_tests.dm"
#include "code\unit_tests\map_tests.dm"
#include "code\unit_tests\mob_tests.dm"
+#include "code\unit_tests\observation_tests.dm"
#include "code\unit_tests\unit_test.dm"
#include "code\unit_tests\zas_tests.dm"
#include "code\ZAS\_docs.dm"
diff --git a/code/_onclick/hud/movable_screen_objects.dm b/code/_onclick/hud/movable_screen_objects.dm
index d543b8b12a4..7e132390f69 100644
--- a/code/_onclick/hud/movable_screen_objects.dm
+++ b/code/_onclick/hud/movable_screen_objects.dm
@@ -10,7 +10,7 @@
/obj/screen/movable
var/snap2grid = FALSE
- var/moved = FALSE
+ var/has_moved = FALSE
//Snap Screen Object
//Tied to the grid, snaps to the nearest turf
diff --git a/code/_onclick/observer.dm b/code/_onclick/observer.dm
index 55fe373301e..a504f3eb4aa 100644
--- a/code/_onclick/observer.dm
+++ b/code/_onclick/observer.dm
@@ -25,7 +25,7 @@
// Otherwise jump
else
- following = null
+ stop_following()
forceMove(get_turf(A))
/mob/dead/observer/ClickOn(var/atom/A, var/params)
diff --git a/code/datums/observation/move.dm b/code/datums/observation/move.dm
new file mode 100644
index 00000000000..d9888246d67
--- /dev/null
+++ b/code/datums/observation/move.dm
@@ -0,0 +1,59 @@
+/datum/observ/moved/New(var/atom/movable/event_holder)
+ if(!istype(event_holder))
+ CRASH("Improper event holder type: '[event_holder]'/[event_holder.type]")
+ ..()
+
+/datum/observ/moved/register(var/datum/procOwner, var/proc_call)
+ . = ..()
+ var/atom/movable/child = event_holder
+ if(.)
+ var/atom/movable/parent = child.loc
+ while(istype(parent) && parent.moved && !parent.moved.is_listening(child))
+ parent.moved.register(child, /atom/movable/proc/recursive_move)
+ child = parent
+ parent = child.loc
+
+/***********************
+* Movement Handling *
+***********************/
+/atom/movable
+ var/datum/observ/moved/moved
+
+/atom/movable/Move()
+ var/old_loc = loc
+ if(..() && moved)
+ moved.raise_event(list(src, old_loc, loc))
+
+/atom/movable/init_observers(var/event_holder)
+ . = ..()
+ if(.)
+ moved = new(event_holder)
+
+/atom/movable/destroy_observers()
+ . = ..()
+ if(.)
+ qdel(moved)
+ moved = null
+
+/atom/movable/proc/move_to_destination(var/atom/movable/am, var/old_loc, var/new_loc)
+ var/turf/T = get_turf(new_loc)
+ if(T && T != loc)
+ forceMove(T)
+
+/atom/movable/proc/recursive_move(var/atom/movable/am, var/old_loc, var/new_loc)
+ moved.raise_event(list(src, old_loc, new_loc))
+
+/atom/Entered(var/atom/movable/am, atom/old_loc)
+ ..()
+ if(am.moved)
+ am.moved.raise_event(list(am, old_loc, am.loc))
+
+/atom/movable/Entered(var/atom/movable/am, atom/old_loc)
+ ..()
+ if(src.moved && am.moved && am.moved.has_listeners() && !src.moved.is_listening(am))
+ src.moved.register(am, /atom/movable/proc/recursive_move)
+
+/atom/movable/Exited(var/atom/movable/am, atom/old_loc)
+ ..()
+ if(src.moved && src.moved.is_listening(am, /atom/movable/proc/recursive_move))
+ src.moved.unregister(am)
diff --git a/code/datums/observation/observation.dm b/code/datums/observation/observation.dm
index eefbcb1fc22..742e367b5a9 100644
--- a/code/datums/observation/observation.dm
+++ b/code/datums/observation/observation.dm
@@ -1,26 +1,40 @@
/datum/observ
+ var/event_holder
var/list/listeners
+/datum/observ/New(var/event_holder)
+ src.event_holder = event_holder
+ ..()
+
/datum/observ/Destroy()
+ event_holder = null
if(listeners)
for(var/listener in listeners)
unregister(listener)
listeners.Cut()
return ..()
+/datum/observ/proc/is_listening(var/datum/procOwner, var/proc_call)
+ return listeners && (procOwner in listeners) && (!proc_call || listeners[procOwner] == proc_call)
+
+/datum/observ/proc/has_listeners()
+ return listeners && listeners.len
+
/datum/observ/proc/register(var/datum/procOwner, var/proc_call)
if(!(procOwner && procOwner.destruction))
- return
+ return FALSE
if(!listeners)
listeners = list()
listeners[procOwner] = proc_call
procOwner.destruction.register(src, /datum/observ/proc/unregister)
+ return TRUE
/datum/observ/proc/unregister(var/datum/procOwner)
if(!(listeners && procOwner && procOwner.destruction))
- return
+ return FALSE
listeners -= procOwner
procOwner.destruction.unregister(src)
+ return TRUE
/datum/observ/proc/raise_event(var/list/args = list())
if(!listeners)
@@ -35,15 +49,15 @@
var/datum/observ/destruction
/datum/New()
- init_observers()
+ init_observers(src)
..()
/datum/Destroy()
destroy_observers()
return ..()
-/datum/proc/init_observers()
- destruction = new()
+/datum/proc/init_observers(var/event_holder)
+ destruction = new(event_holder)
return TRUE
/datum/proc/destroy_observers()
diff --git a/code/game/objects/items/glassjar.dm b/code/game/objects/items/glassjar.dm
index 54062ff8530..bcf94b4a99a 100644
--- a/code/game/objects/items/glassjar.dm
+++ b/code/game/objects/items/glassjar.dm
@@ -26,14 +26,14 @@
return
var/mob/L = A
user.visible_message("[user] scoops [L] into \the [src].", "You scoop [L] into \the [src].")
- L.loc = src
+ L.forceMove(src)
contains = 2
update_icon()
return
else if(istype(A, /obj/effect/spider/spiderling))
var/obj/effect/spider/spiderling/S = A
user.visible_message("[user] scoops [S] into \the [src].", "You scoop [S] into \the [src].")
- S.loc = src
+ S.forceMove(src)
processing_objects.Remove(S) // No growing inside jars
contains = 3
update_icon()
@@ -73,7 +73,7 @@
var/obj/item/weapon/spacecash/S = W
user.visible_message("[user] puts [S.worth] [S.worth > 1 ? "thalers" : "thaler"] into \the [src].")
user.drop_from_inventory(S)
- S.loc = src
+ S.forceMove(src)
update_icon()
/obj/item/glass_jar/update_icon() // Also updates name and desc
diff --git a/code/game/objects/items/weapons/storage/storage.dm b/code/game/objects/items/weapons/storage/storage.dm
index c6ee578dcee..310d5f3f101 100644
--- a/code/game/objects/items/weapons/storage/storage.dm
+++ b/code/game/objects/items/weapons/storage/storage.dm
@@ -501,7 +501,7 @@
remove_from_storage(I, T)
/obj/item/weapon/storage/New()
-
+ ..()
if(allow_quick_empty)
verbs += /obj/item/weapon/storage/verb/quick_empty
else
@@ -559,7 +559,6 @@
src.closer.icon_state = "x"
src.closer.layer = 20
orient2hud()
- return
/obj/item/weapon/storage/emp_act(severity)
if(!istype(src.loc, /mob/living))
diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm
index 69a37f00728..7a83eb1298c 100644
--- a/code/game/objects/structures/crates_lockers/closets.dm
+++ b/code/game/objects/structures/crates_lockers/closets.dm
@@ -22,6 +22,7 @@
var/store_mobs = 1
/obj/structure/closet/initialize()
+ ..()
if(!opened) // if closed, any item at the crate's loc is put in the contents
var/obj/item/I
for(I in src.loc)
diff --git a/code/modules/admin/verbs/adminjump.dm b/code/modules/admin/verbs/adminjump.dm
index a8d98beaf0d..1c788d148b2 100644
--- a/code/modules/admin/verbs/adminjump.dm
+++ b/code/modules/admin/verbs/adminjump.dm
@@ -2,7 +2,7 @@
return
/mob/dead/observer/on_mob_jump()
- following = null
+ stop_following()
/client/proc/Jump(var/area/A in return_sorted_areas())
set name = "Jump to Area"
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 4a82b68b3e7..82192830b4a 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -86,6 +86,7 @@ var/global/list/image/ghost_sightless_images = list() //this is a list of images
..()
/mob/dead/observer/Destroy()
+ stop_following()
qdel(ghost_multitool)
ghost_multitool = null
@@ -94,7 +95,7 @@ var/global/list/image/ghost_sightless_images = list() //this is a list of images
qdel(ghostimage)
ghostimage = null
updateallghostimages()
- ..()
+ return ..()
/mob/dead/observer/Topic(href, href_list)
if (href_list["track"])
@@ -225,6 +226,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
if(!found_rune)
usr << "The astral cord that ties your body and your spirit has been severed. You are likely to wander the realm beyond until your body is finally dead and thus reunited with you."
return
+ stop_following()
mind.current.ajourn=0
mind.current.key = key
mind.current.teleop = null
@@ -305,8 +307,8 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
else
usr << "No area available."
+ stop_following()
usr.forceMove(pick(L))
- following = null
/mob/dead/observer/verb/follow(input in getmobs())
set category = "Ghost"
@@ -319,67 +321,30 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
// This is the ghost's follow verb with an argument
/mob/dead/observer/proc/ManualFollow(var/atom/movable/target)
- if(!target)
+ if(!target || target == following || target == src)
return
- var/turf/targetloc = get_turf(target)
- if(check_holy(targetloc))
- usr << "You cannot follow a mob standing on holy grounds!"
+ stop_following()
+ following = target
+ following.moved.register(src, /atom/movable/proc/move_to_destination)
+ following.destruction.register(src, /mob/dead/observer/proc/stop_following)
+
+ src << "Now following \the [following]"
+ move_to_destination(following, following.loc, following.loc)
+
+/mob/dead/observer/proc/stop_following()
+ if(following)
+ src << "No longer following \the [following]"
+ following.moved.unregister(src)
+ following.destruction.unregister(src)
+ following = null
+
+/mob/dead/observer/move_to_destination(var/atom/movable/am, var/old_loc, var/new_loc)
+ var/turf/T = get_turf(new_loc)
+ if(check_holy(T))
+ usr << "You cannot follow something standing on holy grounds!"
return
- if(target != src)
- if(following && following == target)
- return
- following = target
- src << "Now following \the [target]"
- if(ismob(target))
- forceMove(get_turf(target))
- var/mob/M = target
- M.following_mobs += src
- else
- spawn(0)
- while(target && following == target && client)
- var/turf/T = get_turf(target)
- if(!T)
- break
- // To stop the ghost flickering.
- if(loc != T)
- forceMove(T)
- sleep(15)
-
-/mob/proc/update_following()
- . = get_turf(src)
- for(var/mob/dead/observer/M in following_mobs)
- if(M.following != src)
- following_mobs -= M
- else
- if(M.loc != .)
- M.forceMove(.)
-
-/mob
- var/list/following_mobs = list()
-
-/mob/Destroy()
- for(var/mob/dead/observer/M in following_mobs)
- M.following = null
- following_mobs = null
- return ..()
-
-/mob/dead/observer/Destroy()
- if(ismob(following))
- var/mob/M = following
- M.following_mobs -= src
- following = null
- return ..()
-
-/mob/Move()
- . = ..()
- if(.)
- update_following()
-
-/mob/Life()
- // to catch teleports etc which directly set loc
- update_following()
- return ..()
+ ..()
/mob/proc/check_holy(var/turf/T)
return 0
@@ -404,8 +369,8 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
var/turf/T = get_turf(M) //Turf of the destination mob
if(T && isturf(T)) //Make sure the turf exists, then move the source to that destination.
+ stop_following()
forceMove(T)
- following = null
else
src << "This mob is not located in the game world."
/*
@@ -433,7 +398,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
src << "\red You are dead! You have no mind to store memory!"
/mob/dead/observer/Post_Incorpmove()
- following = null
+ stop_following()
/mob/dead/observer/verb/analyze_air()
set name = "Analyze Air"
diff --git a/code/modules/mob/freelook/update_triggers.dm b/code/modules/mob/freelook/update_triggers.dm
index 405e4bdd9e5..482025426eb 100644
--- a/code/modules/mob/freelook/update_triggers.dm
+++ b/code/modules/mob/freelook/update_triggers.dm
@@ -15,7 +15,7 @@
/turf/simulated/Destroy()
updateVisibility(src)
- ..()
+ return ..()
/turf/simulated/New()
..()
@@ -26,9 +26,10 @@
/obj/structure/Destroy()
updateVisibility(src)
- ..()
+ return ..()
/obj/structure/New()
+ ..()
updateVisibility(src)
// EFFECTS
diff --git a/code/modules/mob/living/carbon/brain/MMI.dm b/code/modules/mob/living/carbon/brain/MMI.dm
index 07004f97546..c07647a57b7 100644
--- a/code/modules/mob/living/carbon/brain/MMI.dm
+++ b/code/modules/mob/living/carbon/brain/MMI.dm
@@ -2,12 +2,11 @@
/obj/item/device/mmi/digital/New()
src.brainmob = new(src)
+ src.brainmob.stat = CONSCIOUS
src.brainmob.add_language("Robot Talk")
- src.brainmob.loc = src
src.brainmob.container = src
- src.brainmob.stat = 0
src.brainmob.silent = 0
- dead_mob_list -= src.brainmob
+ ..()
/obj/item/device/mmi/digital/transfer_identity(var/mob/living/carbon/H)
brainmob.dna = H.dna
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 64ca2469b07..02974e8d647 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -16,19 +16,19 @@
var/armor = getarmor(def_zone, attack_flag)
var/absorb = 0
-
+
//Roll armour
if(prob(armor))
absorb += 1
if(prob(armor))
absorb += 1
-
+
//Roll penetration
if(prob(armour_pen))
absorb -= 1
if(prob(armour_pen))
absorb -= 1
-
+
if(absorb >= 2)
if(absorb_text)
show_message("[absorb_text]")
@@ -129,7 +129,7 @@
//returns 0 if the effects failed to apply for some reason, 1 otherwise.
/mob/living/proc/standard_weapon_hit_effects(obj/item/I, mob/living/user, var/effective_force, var/blocked, var/hit_zone)
- if(!effective_force || blocked >= 2)
+ if(!effective_force || blocked >= 2)
return 0
//Hulk modifier
@@ -298,7 +298,7 @@
return 0
//Scale quadratically so that single digit numbers of fire stacks don't burn ridiculously hot.
- //lower limit of 700 K, same as matches and roughly the temperature of a cool flame.
+ //lower limit of 700 K, same as matches and roughly the temperature of a cool flame.
return max(2.25*round(FIRESUIT_MAX_HEAT_PROTECTION_TEMPERATURE*(fire_stacks/FIRE_MAX_FIRESUIT_STACKS)**2), 700)
/mob/living/proc/reagent_permeability()
@@ -339,7 +339,7 @@
hud_used.hide_actions_toggle = new(hud_used)
hud_used.hide_actions_toggle.UpdateIcon()
- if(!hud_used.hide_actions_toggle.moved)
+ if(!hud_used.hide_actions_toggle.has_moved)
hud_used.hide_actions_toggle.screen_loc = hud_used.ButtonNumberToScreenCoords(1)
//hud_used.SetButtonCoords(hud_used.hide_actions_toggle,1)
@@ -362,7 +362,7 @@
client.screen += B
- if(!B.moved)
+ if(!B.has_moved)
B.screen_loc = hud_used.ButtonNumberToScreenCoords(button_number)
//hud_used.SetButtonCoords(B,button_number)
@@ -370,7 +370,7 @@
if(!hud_used.hide_actions_toggle)
hud_used.hide_actions_toggle = new(hud_used)
hud_used.hide_actions_toggle.InitialiseIcon(src)
- if(!hud_used.hide_actions_toggle.moved)
+ if(!hud_used.hide_actions_toggle.has_moved)
hud_used.hide_actions_toggle.screen_loc = hud_used.ButtonNumberToScreenCoords(button_number+1)
//hud_used.SetButtonCoords(hud_used.hide_actions_toggle,button_number+1)
client.screen += hud_used.hide_actions_toggle
diff --git a/code/modules/mob/living/silicon/decoy/decoy.dm b/code/modules/mob/living/silicon/decoy/decoy.dm
index bbbfe19dadb..86ae4097cda 100644
--- a/code/modules/mob/living/silicon/decoy/decoy.dm
+++ b/code/modules/mob/living/silicon/decoy/decoy.dm
@@ -9,4 +9,7 @@
src.icon = 'icons/mob/AI.dmi'
src.icon_state = "ai"
src.anchored = 1
- src.canmove = 0
\ No newline at end of file
+ src.canmove = 0
+
+/mob/living/silicon/decoy/initialize()
+ return
diff --git a/code/unit_tests/observation_tests.dm b/code/unit_tests/observation_tests.dm
new file mode 100644
index 00000000000..caa2468791d
--- /dev/null
+++ b/code/unit_tests/observation_tests.dm
@@ -0,0 +1,108 @@
+/proc/is_listening_to_movement(var/atom/movable/listening_to, var/listener)
+ return listening_to.moved.is_listening(listener)
+
+datum/unit_test/observation
+ name = "OBSERVATION template"
+ async = 0
+
+datum/unit_test/observation/moved_observer_shall_register_on_follow
+ name = "OBSERVATION: Moved - Observer Shall Register on Follow"
+
+datum/unit_test/observation/moved_observer_shall_register_on_follow/start_test()
+ var/turf/T = locate(20,20,1)
+ var/mob/living/carbon/human/H = new(T)
+ var/mob/dead/observer/O = new(T)
+
+ O.ManualFollow(H)
+ if(is_listening_to_movement(H, O))
+ pass("The observer is now following the mob.")
+ else
+ fail("The observer is not following the mob.")
+
+ qdel(H)
+ qdel(O)
+ return 1
+
+datum/unit_test/observation/moved_observer_shall_unregister_on_nofollow
+ name = "OBSERVATION: Moved - Observer Shall Unregister on NoFollow"
+
+datum/unit_test/observation/moved_observer_shall_unregister_on_nofollow/start_test()
+ var/turf/T = locate(20,20,1)
+ var/mob/living/carbon/human/H = new(T)
+ var/mob/dead/observer/O = new(T)
+
+ O.ManualFollow(H)
+ O.stop_following()
+ if(!is_listening_to_movement(H, O))
+ pass("The observer is no longer following the mob.")
+ else
+ fail("The observer is still following the mob.")
+
+ qdel(H)
+ qdel(O)
+ return 1
+
+datum/unit_test/observation/moved_shall_not_register_on_enter_without_listeners
+ name = "OBSERVATION: Moved - Shall Not Register on Enter Without Listeners"
+
+datum/unit_test/observation/moved_shall_not_register_on_enter_without_listeners/start_test()
+ var/turf/T = locate(20,20,1)
+ var/mob/living/carbon/human/H = new(T)
+ var/obj/structure/closet/C = new(T)
+
+ H.forceMove(C)
+ if(!is_listening_to_movement(C, H))
+ pass("The mob did not register to the closet's moved event.")
+ else
+ fail("The mob has registered to the closet's moved event.")
+
+ qdel(C)
+ qdel(H)
+ return 1
+
+datum/unit_test/observation/moved_shall_registers_recursively_on_new_listener
+ name = "OBSERVATION: Moved - Shall Register Recursively on New Listener"
+
+datum/unit_test/observation/moved_shall_registers_recursively_on_new_listener/start_test()
+ var/turf/T = locate(20,20,1)
+ var/mob/living/carbon/human/H = new(T)
+ var/obj/structure/closet/C = new(T)
+ var/mob/dead/observer/O = new(T)
+
+ H.forceMove(C)
+ O.ManualFollow(H)
+ var/listening_to_closet = is_listening_to_movement(C, H)
+ var/listening_to_human = is_listening_to_movement(H, O)
+ if(listening_to_closet && listening_to_human)
+ pass("Recursive moved registration succesful.")
+ else
+ fail("Recursive moved registration failed. Human listening to closet: [listening_to_closet] - Observer listening to human: [listening_to_human]")
+
+ qdel(C)
+ qdel(H)
+ qdel(O)
+ return 1
+
+datum/unit_test/observation/moved_shall_registers_recursively_with_existing_listener
+ name = "OBSERVATION: Moved - Shall Register Recursively with Existing Listener"
+
+datum/unit_test/observation/moved_shall_registers_recursively_with_existing_listener/start_test()
+ var/turf/T = locate(20,20,1)
+ var/mob/living/carbon/human/H = new(T)
+ var/obj/structure/closet/C = new(T)
+ var/mob/dead/observer/O = new(T)
+
+ O.ManualFollow(H)
+ H.forceMove(C)
+ var/listening_to_closet = is_listening_to_movement(C, H)
+ var/listening_to_human = is_listening_to_movement(H, O)
+ if(listening_to_closet && listening_to_human)
+ pass("Recursive moved registration succesful.")
+ else
+ fail("Recursive moved registration failed. Human listening to closet: [listening_to_closet] - Observer listening to human: [listening_to_human]")
+
+ qdel(C)
+ qdel(H)
+ qdel(O)
+
+ return 1