diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm
index 3667dca5804..f8193e8d4c1 100644
--- a/code/_onclick/other_mobs.dm
+++ b/code/_onclick/other_mobs.dm
@@ -22,6 +22,9 @@
if(S.prevents_buckled_mobs_attacking())
return
+ if(SEND_SIGNAL(A, COMSIG_HUMAN_MELEE_UNARMED_ATTACKBY, src) & COMPONENT_CANCEL_ATTACK_CHAIN)
+ return
+
A.attack_hand(src)
/atom/proc/attack_hand(mob/user as mob)
diff --git a/code/datums/components/sticky.dm b/code/datums/components/sticky.dm
new file mode 100644
index 00000000000..dcb05ab2a22
--- /dev/null
+++ b/code/datums/components/sticky.dm
@@ -0,0 +1,118 @@
+// kinda like duct tape but not shit (just kidding, its shit but in a different way)
+/datum/component/sticky
+ /// The atom we are stickied to
+ var/atom/attached_to
+ /// Our priority overlay put on top of attached_to
+ var/icon/overlay
+ /// Do we drop on attached_to's destroy? If not, we qdel
+ var/drop_on_attached_destroy = FALSE
+
+/datum/component/sticky/Initialize(_drop_on_attached_destroy = FALSE)
+ if(!isitem(parent))
+ stack_trace("/datum/component/sticky's parent is not an item, its [parent.type]")
+ return COMPONENT_INCOMPATIBLE
+
+ drop_on_attached_destroy = _drop_on_attached_destroy
+ RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK, PROC_REF(stick_to_it))
+
+/datum/component/sticky/proc/stick_to_it(obj/item/I, atom/target, mob/user, params)
+ SIGNAL_HANDLER
+ if(!in_range(I, target))
+ return
+ if(isstorage(target))
+ var/obj/item/storage/S = target
+ if(S.can_be_inserted(parent))
+ return
+ if(target.GetComponent(/datum/component/sticky))
+ return
+ if(!user.canUnEquip(I))
+ return
+
+ INVOKE_ASYNC(user, TYPE_PROC_REF(/mob, unEquip), I)
+
+ var/list/click_params = params2list(params)
+ //Center the icon where the user clicked.
+ if(!click_params || !click_params["icon-x"] || !click_params["icon-y"])
+ return
+
+ attached_to = target
+ move_to_the_thing(parent)
+ if(user)
+ to_chat(user, "You attach [parent] to [attached_to].")
+
+ overlay = icon(I.icon, I.icon_state)
+ //Clamp it so that the icon never moves more than 16 pixels in either direction
+ overlay.Shift(EAST, clamp(text2num(click_params["icon-x"]) - 16, -(world.icon_size/2), world.icon_size/2))
+ overlay.Shift(NORTH, clamp(text2num(click_params["icon-y"]) - 16, -(world.icon_size/2), world.icon_size/2))
+
+ attached_to.add_overlay(overlay, priority = TRUE)
+ I.invisibility = INVISIBILITY_ABSTRACT
+
+ RegisterSignal(attached_to, COMSIG_HUMAN_MELEE_UNARMED_ATTACKBY, PROC_REF(pick_up))
+ RegisterSignal(attached_to, COMSIG_PARENT_EXAMINE, PROC_REF(add_sticky_text))
+ RegisterSignal(attached_to, COMSIG_PARENT_QDELETING, PROC_REF(on_attached_destroy))
+ if(ismovable(attached_to))
+ RegisterSignal(attached_to, COMSIG_MOVABLE_MOVED, PROC_REF(on_move))
+ START_PROCESSING(SSobj, src)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/datum/component/sticky/proc/pick_up(atom/A, mob/living/carbon/human/user)
+ SIGNAL_HANDLER
+ if(!attached_to)
+ return
+ if(user.a_intent != INTENT_GRAB)
+ return
+ if(user.get_active_hand())
+ return
+ attached_to.cut_overlay(overlay, priority = TRUE)
+
+ var/obj/item/I = parent
+ I.pixel_x = initial(I.pixel_x)
+ I.pixel_y = initial(I.pixel_y)
+ move_to_the_thing(parent)
+ INVOKE_ASYNC(user, TYPE_PROC_REF(/mob, put_in_hands), I)
+ if(user)
+ to_chat(user, "You take [parent] off of [attached_to].")
+
+
+ I.invisibility = initial(I.invisibility)
+ UnregisterSignal(attached_to, list(COMSIG_HUMAN_MELEE_UNARMED_ATTACKBY, COMSIG_PARENT_EXAMINE))
+ STOP_PROCESSING(SSobj, src)
+ attached_to = null
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/datum/component/sticky/proc/add_sticky_text(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ if(!in_range(user, attached_to))
+ return
+ examine_list += "There is [parent] attached to [source], grab [attached_to.p_them()] to remove it."
+
+/datum/component/sticky/proc/on_move(datum/source, oldloc, move_dir)
+ SIGNAL_HANDLER
+ if(!attached_to)
+ stack_trace("/datum/component/sticky was called on_move, but without an attached atom")
+ return
+ move_to_the_thing(parent)
+
+/datum/component/sticky/process() // because sometimes the item can move inside something, like a crate
+ if(!attached_to)
+ return PROCESS_KILL
+ move_to_the_thing(parent)
+
+/datum/component/sticky/proc/on_attached_destroy(datum/source)
+ SIGNAL_HANDLER
+ if(!drop_on_attached_destroy)
+ qdel(parent)
+ return
+
+ var/turf/T = get_turf(source)
+ if(!T)
+ T = get_turf(parent)
+ move_to_the_thing(parent, T)
+
+/datum/component/sticky/proc/move_to_the_thing(obj/item/I, turf/target)
+ if(!istype(I))
+ return // only items should be able to have the sticky component
+ if(!target)
+ target = get_turf(attached_to)
+ INVOKE_ASYNC(I, TYPE_PROC_REF(/atom/movable, forceMove), target)
diff --git a/code/datums/uplink_items/uplink_general.dm b/code/datums/uplink_items/uplink_general.dm
index b1dd0135e29..625531234a7 100644
--- a/code/datums/uplink_items/uplink_general.dm
+++ b/code/datums/uplink_items/uplink_general.dm
@@ -464,9 +464,9 @@ GLOBAL_LIST_INIT(uplink_items, subtypesof(/datum/uplink_item))
/datum/uplink_item/stealthy_tools/camera_bug
name = "Camera Bug"
- desc = "Enables you to view all cameras on the network to track a target."
+ desc = "Enables you to view all cameras on the network to track a target. Also has 5 sticky hidden cameras, allowing you remote view of any object you can stick a camera on."
reference = "CB"
- item = /obj/item/camera_bug
+ item = /obj/item/storage/box/syndie_kit/camera_bug
cost = 1
surplus = 90
diff --git a/code/game/machinery/camera/camera.dm b/code/game/machinery/camera/camera.dm
index 7fffc5358c5..141144dc750 100644
--- a/code/game/machinery/camera/camera.dm
+++ b/code/game/machinery/camera/camera.dm
@@ -406,6 +406,7 @@
return TRUE
/obj/machinery/camera/portable //Cameras which are placed inside of things, such as helmets.
+ start_active = TRUE // theres no real way to reactivate these, so never break them when they init
var/turf/prev_turf
/obj/machinery/camera/portable/Initialize(mapload)
diff --git a/code/game/objects/items/devices/camera_bug.dm b/code/game/objects/items/devices/camera_bug.dm
index 88bfa0ed9bf..389e3da2694 100644
--- a/code/game/objects/items/devices/camera_bug.dm
+++ b/code/game/objects/items/devices/camera_bug.dm
@@ -3,14 +3,15 @@
name = "camera bug"
desc = "For illicit snooping through the camera network."
icon = 'icons/obj/device.dmi'
- icon_state = "camera_bug"
- w_class = WEIGHT_CLASS_TINY
- item_state = "camera_bug"
+ icon_state = "camera_bug"
+ w_class = WEIGHT_CLASS_TINY
+ item_state = "camera_bug"
throw_speed = 4
throw_range = 20
origin_tech = "syndicate=1;engineering=3"
/// Integrated camera console to serve UI data
var/obj/machinery/computer/security/camera_bug/integrated_console
+ var/connections = 0
/obj/machinery/computer/security/camera_bug
name = "invasive camera utility"
@@ -22,7 +23,7 @@
. = ..()
integrated_console = new(src)
integrated_console.parent = src
- integrated_console.network = list("SS13")
+ integrated_console.network = list("SS13", "camera_bug[UID()]")
/obj/item/camera_bug/Destroy()
QDEL_NULL(integrated_console)
@@ -43,3 +44,56 @@
. = ..()
integrated_console.network = list("ERT")
+/obj/item/wall_bug
+ name = "\improper small camera"
+ desc = "A camera with a sticky backside."
+ icon = 'icons/obj/device.dmi'
+ icon_state = "wall_bug"
+ w_class = WEIGHT_CLASS_TINY
+ var/obj/machinery/camera/portable/camera
+ var/index = "REPORT THIS TO CODERS"
+
+/obj/item/wall_bug/Initialize(mapload, obj/item/camera_bug/the_bug)
+ . = ..()
+ link_to_camera(the_bug)
+ AddComponent(/datum/component/sticky)
+
+/obj/item/wall_bug/Destroy()
+ QDEL_NULL(camera)
+ . = ..()
+
+/obj/item/wall_bug/examine(mob/user)
+ . = ..()
+ if(in_range(user, src))
+ . += "It has a small label on it reading \"[index]\"."
+
+/obj/item/wall_bug/proc/link_to_camera(obj/item/camera_bug/camera_bug)
+ if(!istype(camera_bug))
+ return
+ if(camera) // we can't link twice
+ return
+ camera_bug.connections++
+ index = camera_bug.connections
+
+ camera = new /obj/machinery/camera/portable(src)
+ camera.network = list("camera_bug[camera_bug.UID()]")
+ camera.c_tag = "Hidden Camera [index]"
+
+/obj/item/paper/camera_bug
+ name = "Camera Bug Guide"
+ icon_state = "paper"
+ info = {"Instructions on your new invasive camera utility
+
+ This camera bug can access all default cameras on the station, along with the hidden cameras provided in this kit.
+
+ The cameras in this kit have a sticky backside, allowing you to attach them to literally anything, even people.
+
+ You may remove the cameras from said objects by grabbing them with an empty hand.
+
+ You can view these hidden cameras by looking up "Hidden Camera" on your camera bug.
+
+ Only the camera bug provided in this kit can see these hidden cameras.
+
+ Other Camera bugs cannot see your hidden cameras.
+
+ There is no other way to get these hidden cameras, so make sure to not lose them!
"}
diff --git a/code/game/objects/items/weapons/dice.dm b/code/game/objects/items/weapons/dice.dm
index 59ae3a2fa75..9af05a5019e 100644
--- a/code/game/objects/items/weapons/dice.dm
+++ b/code/game/objects/items/weapons/dice.dm
@@ -252,7 +252,7 @@
/obj/item/storage/toolbox/syndicate,
/obj/item/storage/backpack/clown/syndie,
/obj/item/storage/backpack/satchel_flat,
- /obj/item/camera_bug,
+ /obj/item/storage/box/syndie_kit/camera_bug,
/obj/item/storage/belt/military/traitor,
/obj/item/clothing/glasses/chameleon/thermal,
/obj/item/borg/upgrade/modkit/indoors,
diff --git a/code/game/objects/items/weapons/storage/uplink_kits.dm b/code/game/objects/items/weapons/storage/uplink_kits.dm
index af25037ff74..00d249bfd12 100644
--- a/code/game/objects/items/weapons/storage/uplink_kits.dm
+++ b/code/game/objects/items/weapons/storage/uplink_kits.dm
@@ -374,3 +374,12 @@ To apply, hold the injector a short distance away from the outer thigh before ap
new /obj/item/gun/projectile/automatic/pistol(src)
new /obj/item/ammo_box/magazine/m10mm(src)
new /obj/item/ammo_box/magazine/m10mm(src)
+
+/obj/item/storage/box/syndie_kit/camera_bug
+ name = "\improper Camera Bug kit"
+
+/obj/item/storage/box/syndie_kit/camera_bug/populate_contents()
+ var/camera = new /obj/item/camera_bug(src)
+ new /obj/item/paper/camera_bug(src)
+ for(var/i in 1 to 5)
+ new /obj/item/wall_bug(src, camera)
diff --git a/icons/obj/device.dmi b/icons/obj/device.dmi
index 212df4631b6..abf40126598 100644
Binary files a/icons/obj/device.dmi and b/icons/obj/device.dmi differ
diff --git a/paradise.dme b/paradise.dme
index 1af32ce2f5c..5d4bcf5629d 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -379,6 +379,7 @@
#include "code\datums\components\spooky.dm"
#include "code\datums\components\squeak.dm"
#include "code\datums\components\squish.dm"
+#include "code\datums\components\sticky.dm"
#include "code\datums\components\surgery_initiator.dm"
#include "code\datums\components\swarming.dm"
#include "code\datums\discord\discord_manager.dm"