diff --git a/code/__DEFINES/flags.dm b/code/__DEFINES/flags.dm
index ba816ade982..bcdef181b1e 100644
--- a/code/__DEFINES/flags.dm
+++ b/code/__DEFINES/flags.dm
@@ -35,6 +35,8 @@
#define NO_EMP_WIRES "no_emp_wires"
#define HOLOGRAM "hologram"
#define FROZEN "frozen"
+#define STATIONLOVING "stationloving"
+#define INFORM_ADMINS_ON_RELOCATE "inform_admins_on_relocate"
//turf-only flags
#define NOJAUNT 1
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index 78b70bc4c7b..83e4d4e5505 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -392,4 +392,4 @@ var/global/list/ghost_others_options = list(GHOST_OTHERS_SIMPLE, GHOST_OTHERS_DE
#define TURF_DECAL_DIRT "dirt"
//Error handler defines
-#define ERROR_USEFUL_LEN 2
\ No newline at end of file
+#define ERROR_USEFUL_LEN 2
diff --git a/code/controllers/subsystem/processing/inbounds.dm b/code/controllers/subsystem/processing/inbounds.dm
new file mode 100644
index 00000000000..d43a2d6074e
--- /dev/null
+++ b/code/controllers/subsystem/processing/inbounds.dm
@@ -0,0 +1,32 @@
+var/datum/controller/subsystem/objects/SSinbounds
+/datum/controller/subsystem/inbounds
+ name = "Inbounds"
+ priority = 40
+ flags = SS_NO_INIT
+
+ var/list/processing = list()
+ var/list/currentrun = list()
+
+/datum/controller/subsystem/inbounds/New()
+ NEW_SS_GLOBAL(SSinbounds)
+/datum/controller/subsystem/inbounds/stat_entry()
+ ..("P:[processing.len]")
+
+/datum/controller/subsystem/inbounds/fire(resumed = 0)
+ if (!resumed)
+ src.currentrun = processing.Copy()
+ //cache for sanic speed (lists are references anyways)
+ var/list/currentrun = src.currentrun
+
+ while(currentrun.len)
+ var/atom/movable/thing = currentrun[currentrun.len]
+ currentrun.len--
+ if(thing)
+ thing.check_in_bounds(wait)
+ else
+ SSinbounds.processing -= thing
+ if(MC_TICK_CHECK)
+ return
+
+/datum/controller/subsystem/inbounds/Recover()
+ processing = SSinbounds.processing
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index cfc699d3070..ed07b8a77e7 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -101,7 +101,26 @@
orbiting.Check()
return 1
-/atom/movable/Destroy()
+/atom/movable/Destroy(force)
+ var/inform_admins = HAS_SECONDARY_FLAG(src, INFORM_ADMINS_ON_RELOCATE)
+ var/stationloving = HAS_SECONDARY_FLAG(src, STATIONLOVING)
+
+ if(inform_admins && force)
+ var/turf/T = get_turf(src)
+ message_admins("[src] has been !!force deleted!! in [ADMIN_COORDJMP(T)].")
+ log_game("[src] has been !!force deleted!! in [COORD(T)].")
+
+ if(stationloving && !force)
+ var/turf/currentturf = get_turf(src)
+ var/turf/targetturf = relocate()
+ log_game("[src] has been destroyed in [COORD(currentturf)]. Moving it to [COORD(targetturf)].")
+ if(inform_admins)
+ message_admins("[src] has been destroyed in [ADMIN_COORDJMP(currentturf)]. Moving it to [ADMIN_COORDJMP(targetturf)].")
+ return QDEL_HINT_LETMELIVE
+
+ if(stationloving && force)
+ STOP_PROCESSING(SSinbounds, src)
+
. = ..()
if(loc)
loc.handle_atom_del(src)
@@ -420,3 +439,78 @@
else if (!on && floating)
animate(src, pixel_y = initial(pixel_y), time = 10)
floating = FALSE
+
+/* Stationloving
+*
+* A stationloving atom will always teleport back to the station
+* if it ever leaves the station z-levels or Centcom. It will also,
+* when Destroy() is called, will teleport to a random turf on the
+* station.
+*
+* The turf is guaranteed to be "safe" for normal humans, probably.
+* If the station is SUPER SMASHED UP, it might not work.
+*
+* Here are some important procs:
+* relocate()
+* moves the atom to a safe turf on the station
+*
+* check_in_bounds()
+* regularly called and checks if `in_bounds()` returns true. If false, it
+* triggers a `relocate()`.
+*
+* in_bounds()
+* By default, checks that the atom's z is the station z or centcom.
+*/
+
+/atom/movable/proc/set_stationloving(state, inform_admins=FALSE)
+ var/currently = HAS_SECONDARY_FLAG(src, STATIONLOVING)
+
+ if(inform_admins)
+ SET_SECONDARY_FLAG(src, INFORM_ADMINS_ON_RELOCATE)
+ else
+ CLEAR_SECONDARY_FLAG(src, INFORM_ADMINS_ON_RELOCATE)
+
+ if(state == currently)
+ return
+ else if(!state)
+ STOP_PROCESSING(SSinbounds, src)
+ CLEAR_SECONDARY_FLAG(src, STATIONLOVING)
+ else
+ START_PROCESSING(SSinbounds, src)
+ SET_SECONDARY_FLAG(src, STATIONLOVING)
+
+/atom/movable/proc/relocate()
+ var/targetturf = find_safe_turf(ZLEVEL_STATION)
+ if(!targetturf)
+ if(blobstart.len > 0)
+ targetturf = get_turf(pick(blobstart))
+ else
+ throw EXCEPTION("Unable to find a blobstart landmark")
+
+ if(ismob(loc))
+ var/mob/M = loc
+ M.transferItemToLoc(src, targetturf, TRUE) //nodrops disks when?
+ else if(istype(loc, /obj/item/weapon/storage))
+ var/obj/item/weapon/storage/S = loc
+ S.remove_from_storage(src, targetturf)
+ else
+ forceMove(targetturf)
+ // move the disc, so ghosts remain orbiting it even if it's "destroyed"
+ return targetturf
+
+/atom/movable/proc/check_in_bounds()
+ if(in_bounds())
+ return
+ else
+ var/turf/currentturf = get_turf(src)
+ get(src, /mob) << "You can't help but feel that you just lost something back there..."
+ var/turf/targetturf = relocate()
+ log_game("[src] has been moved out of bounds in [COORD(currentturf)]. Moving it to [COORD(targetturf)].")
+ if(HAS_SECONDARY_FLAG(src, INFORM_ADMINS_ON_RELOCATE))
+ message_admins("[src] has been moved out of bounds in [ADMIN_COORDJMP(currentturf)]. Moving it to [ADMIN_COORDJMP(targetturf)].")
+
+/atom/movable/proc/in_bounds()
+ . = FALSE
+ var/turf/currentturf = get_turf(src)
+ if(currentturf && (currentturf.z == ZLEVEL_CENTCOM || currentturf.z == ZLEVEL_STATION))
+ . = TRUE
diff --git a/code/game/gamemodes/nuclear/nuclearbomb.dm b/code/game/gamemodes/nuclear/nuclearbomb.dm
index 1e18e830645..9f0dc5ae796 100644
--- a/code/game/gamemodes/nuclear/nuclearbomb.dm
+++ b/code/game/gamemodes/nuclear/nuclearbomb.dm
@@ -502,7 +502,7 @@ This is here to make the tiles around the station mininuke change when it's arme
/obj/item/weapon/disk/nuclear/New()
..()
poi_list |= src
- START_PROCESSING(SSobj, src)
+ set_stationloving(TRUE, inform_admins=TRUE)
/obj/item/weapon/disk/nuclear/attackby(obj/item/I, mob/living/user, params)
if(istype(I, /obj/item/weapon/claymore/highlander))
@@ -518,6 +518,12 @@ This is here to make the tiles around the station mininuke change when it's arme
return 1
return ..()
+/obj/item/weapon/disk/nuclear/Destroy(force=FALSE)
+ // respawning is handled in /obj/Destroy()
+ if(force)
+ poi_list -= src
+ . = ..()
+
/obj/item/weapon/disk/nuclear/suicide_act(mob/user)
user.visible_message("[user] is going delta! It looks like [user.p_theyre()] trying to commit suicide!")
playsound(user.loc, 'sound/machines/Alarm.ogg', 50, -1, 1)
@@ -536,56 +542,6 @@ This is here to make the tiles around the station mininuke change when it's arme
user.visible_message("[user] was destroyed by the nuclear blast!")
return OXYLOSS
-/obj/item/weapon/disk/nuclear/process()
- var/turf/diskturf = get_turf(src)
- if(diskturf && (diskturf.z == ZLEVEL_CENTCOM || diskturf.z == ZLEVEL_STATION))
- return
- else
- get(src, /mob) << "You can't help but feel that you just lost something back there..."
- var/turf/targetturf = relocate()
- message_admins("[src] has been moved out of bounds in \
- [ADMIN_COORDJMP(diskturf)]. Moving it to \
- [ADMIN_COORDJMP(targetturf)].")
- log_game("[src] has been moved out of bounds in [COORD(diskturf)]. \
- Moving it to [COORD(targetturf)].")
-
-/obj/item/weapon/disk/nuclear/proc/relocate()
- var/targetturf = find_safe_turf(ZLEVEL_STATION)
- if(!targetturf)
- if(blobstart.len > 0)
- targetturf = get_turf(pick(blobstart))
- else
- throw EXCEPTION("Unable to find a blobstart landmark")
-
- if(ismob(loc))
- var/mob/M = loc
- M.transferItemToLoc(src, targetturf, TRUE) //nodrops disks when?
- else if(istype(loc, /obj/item/weapon/storage))
- var/obj/item/weapon/storage/S = loc
- S.remove_from_storage(src, targetturf)
- else
- forceMove(targetturf)
- // move the disc, so ghosts remain orbiting it even if it's "destroyed"
- return targetturf
-
-/obj/item/weapon/disk/nuclear/Destroy(force)
- var/turf/diskturf = get_turf(src)
-
- if(force)
- message_admins("[src] has been !!force deleted!! in \
- [ADMIN_COORDJMP(diskturf)].")
- log_game("[src] has been !!force deleted!! in [COORD(diskturf)].")
- poi_list -= src
- STOP_PROCESSING(SSobj, src)
- return ..()
-
- var/turf/targetturf = relocate()
- message_admins("[src] has been destroyed in [ADMIN_COORDJMP(diskturf)]. \
- Moving it to [ADMIN_COORDJMP(targetturf)].")
- log_game("[src] has been destroyed in [COORD(diskturf)]. Moving it to \
- [COORD(targetturf)].")
- return QDEL_HINT_LETMELIVE //Cancel destruction unless forced
-
/obj/item/weapon/disk/fakenucleardisk
name = "cheap plastic imitation of the nuclear authentication disk"
desc = "Broken dreams and a faint odor of cheese."
diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm
index 14ea548379c..8a854d98766 100644
--- a/code/game/objects/objs.dm
+++ b/code/game/objects/objs.dm
@@ -38,11 +38,11 @@
else
T.add_blueprints_preround(src)
-/obj/Destroy()
+/obj/Destroy(force=FALSE)
if(!istype(src, /obj/machinery))
STOP_PROCESSING(SSobj, src) // TODO: Have a processing bitflag to reduce on unnecessary loops through the processing lists
SStgui.close_uis(src)
- return ..()
+ . = ..()
/obj/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback)
..()
diff --git a/tgstation.dme b/tgstation.dme
index 15863b63d78..fcfdefc1d59 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -189,6 +189,7 @@
#include "code\controllers\subsystem\weather.dm"
#include "code\controllers\subsystem\processing\fastprocess.dm"
#include "code\controllers\subsystem\processing\flightpacks.dm"
+#include "code\controllers\subsystem\processing\inbounds.dm"
#include "code\controllers\subsystem\processing\objects.dm"
#include "code\controllers\subsystem\processing\overlays.dm"
#include "code\controllers\subsystem\processing\processing.dm"