diff --git a/code/__DEFINES/dcs/mob_signals.dm b/code/__DEFINES/dcs/mob_signals.dm
index 286daac792f..c876da9c5c8 100644
--- a/code/__DEFINES/dcs/mob_signals.dm
+++ b/code/__DEFINES/dcs/mob_signals.dm
@@ -206,3 +206,7 @@
/// called when a living mob's stun status is cleared: ()
#define COMSIG_LIVING_CLEAR_STUNS "living_clear_stuns"
+
+/// Sent from datum/spell/ethereal_jaunt/cast, before the mob enters jaunting as a pre-check: (mob/jaunter)
+#define COMSIG_MOB_PRE_JAUNT "spell_mob_pre_jaunt"
+ #define COMPONENT_BLOCK_JAUNT (1<<0)
diff --git a/code/__DEFINES/dcs/movable_signals.dm b/code/__DEFINES/dcs/movable_signals.dm
index c0ac15e5393..2de0ca3879d 100644
--- a/code/__DEFINES/dcs/movable_signals.dm
+++ b/code/__DEFINES/dcs/movable_signals.dm
@@ -63,3 +63,9 @@
// 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"
+
+/// Called before a movable is being teleported from multiple sources: (destination)
+#define COMSIG_MOVABLE_TELEPORTING "movable_teleporting"
+/// Called when blocking a teleport
+#define COMSIG_ATOM_INTERCEPT_TELEPORTED "intercept_teleported"
+ #define COMPONENT_BLOCK_TELEPORT (1<<0)
diff --git a/code/datums/diseases/wizarditis.dm b/code/datums/diseases/wizarditis.dm
index 30c18428030..4c02000bf63 100644
--- a/code/datums/diseases/wizarditis.dm
+++ b/code/datums/diseases/wizarditis.dm
@@ -90,6 +90,8 @@
/datum/disease/wizarditis/proc/teleport()
if(!is_teleport_allowed(affected_mob.z))
return
+ if(SEND_SIGNAL(affected_mob, COMSIG_MOVABLE_TELEPORTING, get_turf(affected_mob)) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
var/list/possible_areas = get_areas_in_range(80, affected_mob)
for(var/area/space/S in possible_areas)
diff --git a/code/datums/helper_datums/teleport.dm b/code/datums/helper_datums/teleport.dm
index 8f4aac056d2..a14b5722e23 100644
--- a/code/datums/helper_datums/teleport.dm
+++ b/code/datums/helper_datums/teleport.dm
@@ -149,6 +149,10 @@
if(!is_teleport_allowed(destturf.z) && !ignore_area_flag)
return FALSE
+ if(!ignore_area_flag) // Admin or contractor portal, let em through without running signals
+ if(SEND_SIGNAL(teleatom, COMSIG_MOVABLE_TELEPORTING, destination) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
+
// Only check the destination zlevel for is_teleport_allowed. Checking origin as well breaks ERT teleporters.
var/area/destarea = get_area(destturf)
diff --git a/code/datums/spells/bloodcrawl.dm b/code/datums/spells/bloodcrawl.dm
index b1ab9853096..007ff329932 100644
--- a/code/datums/spells/bloodcrawl.dm
+++ b/code/datums/spells/bloodcrawl.dm
@@ -29,6 +29,8 @@
return
if(!isliving(user))
return FALSE
+ if(SEND_SIGNAL(user, COMSIG_MOB_PRE_JAUNT, get_turf(user)) & COMPONENT_BLOCK_JAUNT)
+ return FALSE
/datum/spell/bloodcrawl/cast(list/targets, mob/living/user)
var/atom/target = targets[1]
diff --git a/code/datums/spells/ethereal_jaunt.dm b/code/datums/spells/ethereal_jaunt.dm
index e707040501d..b671244dfc8 100644
--- a/code/datums/spells/ethereal_jaunt.dm
+++ b/code/datums/spells/ethereal_jaunt.dm
@@ -26,6 +26,8 @@
/datum/spell/ethereal_jaunt/cast(list/targets, mob/user = usr) //magnets, so mostly hardcoded
playsound(get_turf(user), sound1, 50, TRUE, -1)
for(var/mob/living/target in targets)
+ if(SEND_SIGNAL(target, COMSIG_MOB_PRE_JAUNT, target) & COMPONENT_BLOCK_JAUNT)
+ continue
if(!target.can_safely_leave_loc()) // No more brainmobs hopping out of their brains
to_chat(target, "You are somehow too bound to your current location to abandon it.")
continue
diff --git a/code/datums/spells/turf_teleport.dm b/code/datums/spells/turf_teleport.dm
index 9c8e50c51c3..ee9cc820072 100644
--- a/code/datums/spells/turf_teleport.dm
+++ b/code/datums/spells/turf_teleport.dm
@@ -22,6 +22,8 @@
playsound(get_turf(user), sound1, 50, TRUE)
for(var/mob/living/target in targets)
+ if(SEND_SIGNAL(target, COMSIG_MOVABLE_TELEPORTING, get_turf(target)) & COMPONENT_BLOCK_TELEPORT)
+ continue
var/list/turfs = list()
for(var/turf/T in range(target,outer_tele_radius))
if(T in range(target,inner_tele_radius)) continue
diff --git a/code/game/gamemodes/cult/blood_magic.dm b/code/game/gamemodes/cult/blood_magic.dm
index 6bb2febd075..cc0b0f22c20 100644
--- a/code/game/gamemodes/cult/blood_magic.dm
+++ b/code/game/gamemodes/cult/blood_magic.dm
@@ -537,6 +537,8 @@
var/turf/origin = get_turf(teleportee)
var/turf/destination = get_turf(actual_selected_rune)
+ if(SEND_SIGNAL(target, COMSIG_MOVABLE_TELEPORTING, destination) & COMPONENT_BLOCK_TELEPORT)
+ return
INVOKE_ASYNC(actual_selected_rune, TYPE_PROC_REF(/obj/effect/rune, teleport_effect), teleportee, origin, destination)
if(is_mining_level(user.z) && !is_mining_level(destination.z)) //No effect if you stay on lavaland
diff --git a/code/game/gamemodes/cult/cult_items.dm b/code/game/gamemodes/cult/cult_items.dm
index 320941a6227..0f0427d29b1 100644
--- a/code/game/gamemodes/cult/cult_items.dm
+++ b/code/game/gamemodes/cult/cult_items.dm
@@ -324,6 +324,8 @@
if(C.pulling)
var/atom/movable/pulled = C.pulling
var/turf/turf_behind = get_turf(get_step(T, turn(C.dir, 180)))
+ if(SEND_SIGNAL(pulled, COMSIG_MOVABLE_TELEPORTING, turf_behind) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
if(!pulled.anchored) //Item may have been anchored while pulling, and pulling state isn't updated until you move away, so we double check.
pulled.forceMove(turf_behind)
. = pulled
@@ -338,6 +340,8 @@
step(src, pick(GLOB.alldirs))
to_chat(user, "[src] flickers out of your hands, too eager to move!")
return
+ if(SEND_SIGNAL(user, COMSIG_MOVABLE_TELEPORTING, get_turf(user)) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
if(user.holy_check())
return
var/outer_tele_radius = 9
diff --git a/code/game/gamemodes/cult/runes.dm b/code/game/gamemodes/cult/runes.dm
index 822b220f3d8..234bdcaf83b 100644
--- a/code/game/gamemodes/cult/runes.dm
+++ b/code/game/gamemodes/cult/runes.dm
@@ -494,6 +494,8 @@ structure_check() searches for nearby cultist structures required for the invoca
var/movedsomething = FALSE
var/moveuser = FALSE
for(var/atom/movable/A in T)
+ if(SEND_SIGNAL(A, COMSIG_MOVABLE_TELEPORTING, target) & COMPONENT_BLOCK_TELEPORT)
+ continue
if(ishuman(A))
if(A != user) // Teleporting someone else
INVOKE_ASYNC(src, PROC_REF(teleport_effect), A, T, target)
@@ -775,6 +777,11 @@ structure_check() searches for nearby cultist structures required for the invoca
fail_invoke()
log_game("Summon Cultist rune failed - target in away mission")
return
+ if(SEND_SIGNAL(cultist_to_summon, COMSIG_MOVABLE_TELEPORTING, get_turf(src)) & COMPONENT_BLOCK_TELEPORT)
+ to_chat(user, "[cultist_to_summon] is anchored in bluespace!")
+ fail_invoke()
+ log_game("Summon Cultist rune failed - anchored in bluespace")
+ return
cultist_to_summon.visible_message("[cultist_to_summon] suddenly disappears in a flash of red light!", \
"Overwhelming vertigo consumes you as you are hurled through the air!")
diff --git a/code/game/gamemodes/wizard/magic_tarot.dm b/code/game/gamemodes/wizard/magic_tarot.dm
index b3e825ab9e6..69ac8f6f33b 100644
--- a/code/game/gamemodes/wizard/magic_tarot.dm
+++ b/code/game/gamemodes/wizard/magic_tarot.dm
@@ -287,6 +287,8 @@
card_icon = "the_fool"
/datum/tarot/the_fool/activate(mob/living/target)
+ if(SEND_SIGNAL(target, COMSIG_MOVABLE_TELEPORTING, get_turf(target)) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
target.forceMove(pick(GLOB.latejoin))
to_chat(target, "You are abruptly pulled through space!")
@@ -364,7 +366,8 @@
if(!length(L))
to_chat(target, "Huh. No bridge? Well, that sucks.")
return
-
+ if(SEND_SIGNAL(target, COMSIG_MOVABLE_TELEPORTING, get_turf(target)) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
target.forceMove(pick(L))
to_chat(target, "You are abruptly pulled through space!")
@@ -440,7 +443,8 @@
if(!length(viable_vendors))
to_chat(target, "No vending machines? Well, with luck cargo will have something to offer. If you go there yourself.")
return
-
+ if(SEND_SIGNAL(target, COMSIG_MOVABLE_TELEPORTING, get_turf(target)) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
target.forceMove(get_turf(pick(viable_vendors)))
to_chat(target, "You are abruptly pulled through space!")
@@ -554,7 +558,8 @@
if(!length(L))
to_chat(target, "Huh. No evidence? Well, that means they can't charge you with a crime, right?")
return
-
+ if(SEND_SIGNAL(target, COMSIG_MOVABLE_TELEPORTING, get_turf(target)) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
target.forceMove(pick(L))
to_chat(target, "You are abruptly pulled through space!")
for(var/obj/structure/closet/C in shuffle(view(9, target)))
@@ -573,6 +578,8 @@
/datum/tarot/the_moon/activate(mob/living/target)
var/list/funny_ruin_list = list()
var/turf/target_turf = get_turf(target)
+ if(SEND_SIGNAL(target, COMSIG_MOVABLE_TELEPORTING, get_turf(target)) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
for(var/I in GLOB.ruin_landmarks)
var/obj/effect/landmark/ruin/ruin_landmark = I
if(ruin_landmark.z == target_turf.z)
@@ -722,7 +729,8 @@
if(!length(L))
to_chat(target, "Huh. No command members? I hope you didn't kill them all already...")
return
-
+ if(SEND_SIGNAL(target, COMSIG_MOVABLE_TELEPORTING, get_turf(target)) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
target.forceMove(get_turf(pick(L)))
to_chat(target, "You are abruptly pulled through space!")
@@ -975,6 +983,7 @@
if(!length(L))
to_chat(target, "Hmm. No base? A miner issue.")
return
-
+ if(SEND_SIGNAL(target, COMSIG_MOVABLE_TELEPORTING, get_turf(target)) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
target.forceMove(pick(L))
to_chat(target, "You are abruptly pulled through space!")
diff --git a/code/game/objects/items/devices/traitordevices.dm b/code/game/objects/items/devices/traitordevices.dm
index 2b279b5a781..71318e0dbcc 100644
--- a/code/game/objects/items/devices/traitordevices.dm
+++ b/code/game/objects/items/devices/traitordevices.dm
@@ -113,6 +113,8 @@
if(!is_teleport_allowed(starting.z) || starting_area.tele_proof)
to_chat(user, "[src] will not work here!")
return
+ if(SEND_SIGNAL(user, COMSIG_MOVABLE_TELEPORTING, starting) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
var/mob/living/M = user
var/turf/mobloc = get_turf(M)
var/list/turfs = list()
diff --git a/code/game/objects/items/weapons/bio_chips/bio_chip_tracking.dm b/code/game/objects/items/weapons/bio_chips/bio_chip_tracking.dm
index 5d0d1947471..f02b9c9216c 100644
--- a/code/game/objects/items/weapons/bio_chips/bio_chip_tracking.dm
+++ b/code/game/objects/items/weapons/bio_chips/bio_chip_tracking.dm
@@ -20,6 +20,13 @@
return ..()
/obj/item/bio_chip/tracking/implant(mob/target)
+ if(ishuman(target))
+ var/mob/living/carbon/human/H = target
+ var/obj/item/organ/internal/cyberimp/chest/bluespace_anchor/anchor = H.get_int_organ(/obj/item/organ/internal/cyberimp/chest/bluespace_anchor)
+ if(anchor)
+ target.visible_message("[src] sparks out, disrupted by [anchor] inside [H]!")
+ qdel(src)
+ return FALSE
. = ..()
if(!.)
return
diff --git a/code/modules/antagonists/vampire/vampire_powers/dantalion_powers.dm b/code/modules/antagonists/vampire/vampire_powers/dantalion_powers.dm
index 3aeda9ce3f0..16e0583471d 100644
--- a/code/modules/antagonists/vampire/vampire_powers/dantalion_powers.dm
+++ b/code/modules/antagonists/vampire/vampire_powers/dantalion_powers.dm
@@ -164,8 +164,10 @@
var/mob/living/target = targets[1]
var/turf/user_turf = get_turf(user)
var/turf/target_turf = get_turf(target)
- target.forceMove(user_turf)
- user.forceMove(target_turf)
+ if(!(SEND_SIGNAL(target, COMSIG_MOVABLE_TELEPORTING, user_turf) & COMPONENT_BLOCK_TELEPORT))
+ target.forceMove(user_turf)
+ if(!(SEND_SIGNAL(user, COMSIG_MOVABLE_TELEPORTING, target_turf) & COMPONENT_BLOCK_TELEPORT))
+ user.forceMove(target_turf)
/datum/spell/vampire/self/decoy
name = "Deploy Decoy (30)"
diff --git a/code/modules/antagonists/vampire/vampire_powers/umbrae_powers.dm b/code/modules/antagonists/vampire/vampire_powers/umbrae_powers.dm
index 691eed6ece6..fcbd61fd752 100644
--- a/code/modules/antagonists/vampire/vampire_powers/umbrae_powers.dm
+++ b/code/modules/antagonists/vampire/vampire_powers/umbrae_powers.dm
@@ -173,6 +173,8 @@
user.make_invisible()
addtimer(CALLBACK(user, TYPE_PROC_REF(/mob/living, reset_visibility)), 4 SECONDS)
else
+ if(SEND_SIGNAL(user, COMSIG_MOVABLE_TELEPORTING, end_turf) & COMPONENT_BLOCK_TELEPORT) //You can fake it out, but no teleporting
+ return FALSE
user.forceMove(end_turf)
if(end_turf.z == start_turf.z)
@@ -226,6 +228,8 @@
/datum/spell/vampire/dark_passage/cast(list/targets, mob/user)
var/turf/target = get_turf(targets[1])
+ if(SEND_SIGNAL(user, COMSIG_MOVABLE_TELEPORTING, target) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
new /obj/effect/temp_visual/vamp_mist_out(get_turf(user))
diff --git a/code/modules/mining/lavaland/loot/hierophant_loot.dm b/code/modules/mining/lavaland/loot/hierophant_loot.dm
index eb5b621a630..d5e747ed200 100644
--- a/code/modules/mining/lavaland/loot/hierophant_loot.dm
+++ b/code/modules/mining/lavaland/loot/hierophant_loot.dm
@@ -242,6 +242,8 @@
var/turf/turf_to_teleport_to = get_step(target, get_dir(source, M)) //get position relative to caster
if(!turf_to_teleport_to || is_blocked_turf(turf_to_teleport_to, TRUE))
return
+ if(SEND_SIGNAL(M, COMSIG_MOVABLE_TELEPORTING, turf_to_teleport_to) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
if(ishuman(M))
var/mob/living/carbon/human/H = M
H.set_alpha_tracking(0, src, update_alpha = FALSE)
diff --git a/code/modules/mining/lavaland/loot/tendril_loot.dm b/code/modules/mining/lavaland/loot/tendril_loot.dm
index ddcceca876f..384e4c4cc93 100644
--- a/code/modules/mining/lavaland/loot/tendril_loot.dm
+++ b/code/modules/mining/lavaland/loot/tendril_loot.dm
@@ -293,6 +293,8 @@
if(cooldown)
to_chat(user, "[src] sparks and fizzles.")
return
+ if(SEND_SIGNAL(user, COMSIG_MOVABLE_TELEPORTING, get_turf(linked)) & COMPONENT_BLOCK_TELEPORT)
+ return FALSE
var/datum/effect_system/smoke_spread/smoke = new
smoke.set_up(1, FALSE, user)
diff --git a/code/modules/research/designs/medical_designs.dm b/code/modules/research/designs/medical_designs.dm
index 8a7eb1bf4d6..c507ca5fdd3 100644
--- a/code/modules/research/designs/medical_designs.dm
+++ b/code/modules/research/designs/medical_designs.dm
@@ -634,6 +634,17 @@
build_path = /obj/item/organ/internal/cyberimp/brain/wire_interface
category = list("Medical")
+/datum/design/bluespace_anchor
+ name = "Bluespace Anchor Implant"
+ desc = "This large cybernetic implant anchors you in bluespace, preventing almost any teleportation effects from working. It disrupts GPS systems however."
+ id = "bluespace_anchor_implant"
+ req_tech = list("bluespace" = 7, "biotech" = 5)
+ build_type = PROTOLATHE | MECHFAB
+ construction_time = 6 SECONDS
+ materials = list(MAT_METAL = 10000, MAT_BLUESPACE = 2000)
+ build_path = /obj/item/organ/internal/cyberimp/chest/bluespace_anchor
+ category = list("Medical")
+
/datum/design/raiden_implant
name = "Reactive Repair Implant"
desc = "This implant reworks the IPC frame, in order to incorporate materials that return to their original shape after being damaged. Requires power to function."
diff --git a/code/modules/supply/supply_packs/pack_security.dm b/code/modules/supply/supply_packs/pack_security.dm
index 1adad3d58c9..0463a992fd7 100644
--- a/code/modules/supply/supply_packs/pack_security.dm
+++ b/code/modules/supply/supply_packs/pack_security.dm
@@ -318,6 +318,12 @@
cost = 500
containername = "chemical bio-chip crate"
+/datum/supply_packs/security/armory/bluespace_anchor
+ name = "Bluespace Anchor Crate"
+ contains = list(/obj/item/organ/internal/cyberimp/chest/bluespace_anchor)
+ cost = 250
+ containername = "bluespace anchor crate"
+
/datum/supply_packs/security/securitybarriers
name = "Security Barriers Crate"
contains = list(/obj/item/grenade/barrier,
diff --git a/code/modules/surgery/organs/augments_internal.dm b/code/modules/surgery/organs/augments_internal.dm
index a1807372bcf..8b9aa48a626 100644
--- a/code/modules/surgery/organs/augments_internal.dm
+++ b/code/modules/surgery/organs/augments_internal.dm
@@ -734,6 +734,51 @@
if(H.stat == CONSCIOUS)
to_chat(H, "You feel your heart beating again!")
+/obj/item/organ/internal/cyberimp/chest/bluespace_anchor
+ name = "bluespace anchor implant"
+ desc = "This large cybernetic implant anchors you in bluespace, preventing almost any teleportation effects from working. It disrupts GPS systems however."
+ icon_state = "bluespace_anchor"
+ implant_overlay = null
+ slot = "bluespace_anchor"
+ origin_tech = "bluespace=6;biotech=4"
+
+/obj/item/organ/internal/cyberimp/chest/bluespace_anchor/insert(mob/living/carbon/M, special = FALSE)
+ ..()
+ RegisterSignal(M, COMSIG_MOVABLE_TELEPORTING, PROC_REF(on_teleport))
+ RegisterSignal(M, COMSIG_MOB_PRE_JAUNT, PROC_REF(on_jaunt))
+ for(var/obj/item/bio_chip/tracking/T in M)
+ if(T && T.implanted)
+ qdel(T)
+
+/obj/item/organ/internal/cyberimp/chest/bluespace_anchor/remove(mob/living/carbon/M, special = FALSE)
+ UnregisterSignal(M, COMSIG_MOVABLE_TELEPORTING)
+ UnregisterSignal(M, COMSIG_MOB_PRE_JAUNT)
+ return ..()
+
+/// Blocks teleports and stuns the would-be-teleportee.
+/obj/item/organ/internal/cyberimp/chest/bluespace_anchor/proc/on_teleport(mob/living/teleportee, atom/destination, channel)
+ SIGNAL_HANDLER // COMSIG_MOVABLE_TELEPORTED
+
+ to_chat(teleportee, "You feel yourself teleporting, but are suddenly flung back to where you just were!")
+
+ teleportee.Weaken(5 SECONDS)
+ var/datum/effect_system/spark_spread/spark_system = new()
+ spark_system.set_up(5, TRUE, teleportee)
+ spark_system.start()
+ return COMPONENT_BLOCK_TELEPORT
+
+/// Prevents a user from entering a jaunt.
+/obj/item/organ/internal/cyberimp/chest/bluespace_anchor/proc/on_jaunt(mob/living/jaunter)
+ SIGNAL_HANDLER // COMSIG_MOB_PRE_JAUNT
+
+ to_chat(jaunter, "As you attempt to jaunt, you slam directly into the barrier between realities and are sent crashing back into corporeality!")
+
+ jaunter.Weaken(5 SECONDS)
+ var/datum/effect_system/spark_spread/spark_system = new()
+ spark_system.set_up(5, TRUE, jaunter)
+ spark_system.start()
+ return COMPONENT_BLOCK_JAUNT
+
/obj/item/organ/internal/cyberimp/chest/ipc_repair
name = "Reactive Repair Implant"
desc = "This implant reworks the IPC frame, in order to incorporate materials that return to their original shape after being damaged. Requires power to function."
diff --git a/icons/obj/surgery.dmi b/icons/obj/surgery.dmi
index 7b9baa40723..f1c9ac07b82 100644
Binary files a/icons/obj/surgery.dmi and b/icons/obj/surgery.dmi differ