diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm
index 87b9094b47e..02b36614cb5 100644
--- a/code/__DEFINES/components.dm
+++ b/code/__DEFINES/components.dm
@@ -274,4 +274,12 @@
//Ouch my toes!
#define CALTROP_BYPASS_SHOES 1
-#define CALTROP_IGNORE_WALKERS 2
\ No newline at end of file
+#define CALTROP_IGNORE_WALKERS 2
+
+//Xenobio hotkeys
+#define COMSIG_XENO_SLIME_CLICK_CTRL "xeno_slime_click_ctrl" //from slime CtrlClickOn(): (/mob)
+#define COMSIG_XENO_SLIME_CLICK_ALT "xeno_slime_click_alt" //from slime AltClickOn(): (/mob)
+#define COMSIG_XENO_SLIME_CLICK_SHIFT "xeno_slime_click_shift" //from slime ShiftClickOn(): (/mob)
+#define COMSIG_XENO_TURF_CLICK_SHIFT "xeno_turf_click_shift" //from turf ShiftClickOn(): (/mob)
+#define COMSIG_XENO_TURF_CLICK_CTRL "xeno_turf_click_alt" //from turf AltClickOn(): (/mob)
+#define COMSIG_XENO_MONKEY_CLICK_CTRL "xeno_monkey_click_ctrl" //from monkey CtrlClickOn(): (/mob)
\ No newline at end of file
diff --git a/code/modules/research/xenobiology/xenobio_camera.dm b/code/modules/research/xenobiology/xenobio_camera.dm
index d9fd5265e3e..36535ee1e6b 100644
--- a/code/modules/research/xenobiology/xenobio_camera.dm
+++ b/code/modules/research/xenobiology/xenobio_camera.dm
@@ -27,11 +27,14 @@
var/datum/action/innate/feed_slime/feed_slime_action = new
var/datum/action/innate/monkey_recycle/monkey_recycle_action = new
var/datum/action/innate/transfer_potion/transfer_potion_action = new
+ var/datum/action/innate/hotkey_help/hotkey_help_action = new
+ var/datum/action/innate/slime_scan/slime_scan_action = new
var/list/stored_slimes = list()
var/max_slimes = 5
var/monkeys = 0
var/obj/item/slimepotion/current_potion
+ var/obj/item/slime_scanner/scanner = new /obj/item/slime_scanner
icon_screen = "slime_comp"
icon_keyboard = "rd_key"
@@ -69,7 +72,32 @@
transfer_potion_action.target = src
transfer_potion_action.Grant(user)
actions += transfer_potion_action
+
+ if(hotkey_help_action)
+ hotkey_help_action.target = src
+ hotkey_help_action.Grant(user)
+ actions += hotkey_help_action
+
+ if(slime_scan_action)
+ slime_scan_action.target = src
+ slime_scan_action.Grant(user)
+ actions += slime_scan_action
+ RegisterSignal(user, COMSIG_XENO_SLIME_CLICK_CTRL, .proc/XenoSlimeClickCtrl)
+ RegisterSignal(user, COMSIG_XENO_SLIME_CLICK_ALT, .proc/XenoSlimeClickAlt)
+ RegisterSignal(user, COMSIG_XENO_SLIME_CLICK_SHIFT, .proc/XenoSlimeClickShift)
+ RegisterSignal(user, COMSIG_XENO_TURF_CLICK_SHIFT, .proc/XenoTurfClickShift)
+ RegisterSignal(user, COMSIG_XENO_TURF_CLICK_CTRL, .proc/XenoTurfClickCtrl)
+ RegisterSignal(user, COMSIG_XENO_MONKEY_CLICK_CTRL, .proc/XenoMonkeyClickCtrl)
+
+/obj/machinery/computer/camera_advanced/xenobio/remove_eye_control(mob/living/user)
+ UnregisterSignal(user, COMSIG_XENO_SLIME_CLICK_CTRL)
+ UnregisterSignal(user, COMSIG_XENO_SLIME_CLICK_ALT)
+ UnregisterSignal(user, COMSIG_XENO_SLIME_CLICK_SHIFT)
+ UnregisterSignal(user, COMSIG_XENO_TURF_CLICK_SHIFT)
+ UnregisterSignal(user, COMSIG_XENO_TURF_CLICK_CTRL)
+ UnregisterSignal(user, COMSIG_XENO_MONKEY_CLICK_CTRL)
+ ..()
/obj/machinery/computer/camera_advanced/xenobio/attack_hand(mob/user)
if(!ishuman(user)) //AIs using it might be weird
@@ -191,7 +219,7 @@
/datum/action/innate/transfer_potion
name = "Transfer Potion"
- button_icon_state = "transfer_potion"
+ button_icon_state = "slime_potion"
/datum/action/innate/transfer_potion/Activate()
if(!target || !ishuman(owner))
@@ -211,4 +239,161 @@
X.current_potion.attack(S, C)
break
else
- to_chat(owner, "Target is not near a camera. Cannot proceed.")
\ No newline at end of file
+ to_chat(owner, "Target is not near a camera. Cannot proceed.")
+
+/datum/action/innate/hotkey_help
+ name = "Hotkey Help"
+ button_icon_state = "hotkey_help"
+
+/datum/action/innate/hotkey_help/Activate()
+ if(!target || !isliving(owner))
+ return
+ to_chat(owner, "Click shortcuts:")
+ to_chat(owner, "Shift-click a slime to pick it up, or the floor to drop all held slimes.")
+ to_chat(owner, "Ctrl-click a slime to scan it.")
+ to_chat(owner, "Alt-click a slime to feed it a potion.")
+ to_chat(owner, "Ctrl-click or a dead monkey to recycle it, or the floor to place a new monkey.")
+
+/datum/action/innate/slime_scan
+ name = "Scan Slime"
+ button_icon_state = "slime_scan"
+
+/datum/action/innate/slime_scan/Activate()
+ if(!target || !isliving(owner))
+ return
+ var/mob/living/C = owner
+ var/mob/camera/aiEye/remote/xenobio/remote_eye = C.remote_control
+ var/obj/machinery/computer/camera_advanced/xenobio/X = target
+
+ if(cameranet.checkTurfVis(remote_eye.loc))
+ for(var/mob/living/carbon/slime/S in remote_eye.loc)
+ X.scanner.attack(S, C)
+ else
+ to_chat(owner, "Target is not near a camera. Cannot proceed.")
+
+//
+// Alternate clicks for slime, monkey and open turf if using a xenobio console
+
+// Scans slime
+/mob/living/carbon/slime/CtrlClick(mob/user)
+ SEND_SIGNAL(user, COMSIG_XENO_SLIME_CLICK_CTRL, src)
+ ..()
+
+//Feeds a potion to slime
+/mob/living/carbon/slime/AltClick(mob/user)
+ SEND_SIGNAL(user, COMSIG_XENO_SLIME_CLICK_ALT, src)
+ ..()
+
+//Picks up slime
+/mob/living/carbon/slime/ShiftClick(mob/user)
+ SEND_SIGNAL(user, COMSIG_XENO_SLIME_CLICK_SHIFT, src)
+ ..()
+
+//Place slimes
+/turf/simulated/ShiftClick(mob/user)
+ SEND_SIGNAL(user, COMSIG_XENO_TURF_CLICK_SHIFT, src)
+ ..()
+
+//Place monkey
+/turf/simulated/CtrlClick(mob/user)
+ SEND_SIGNAL(user, COMSIG_XENO_TURF_CLICK_CTRL, src)
+ ..()
+
+//Pick up monkey
+/mob/living/carbon/monkey/CtrlClick(mob/user)
+ SEND_SIGNAL(user, COMSIG_XENO_MONKEY_CLICK_CTRL, src)
+ ..()
+
+// Scans slime
+/obj/machinery/computer/camera_advanced/xenobio/proc/XenoSlimeClickCtrl(mob/living/user, mob/living/carbon/slime/S)
+ if(!cameranet.checkTurfVis(S.loc))
+ to_chat(user, "Target is not near a camera. Cannot proceed.")
+ return
+ var/mob/living/C = user
+ var/mob/camera/aiEye/remote/xenobio/E = C.remote_control
+ var/area/mobarea = get_area(S.loc)
+ if(mobarea.name == E.allowed_area || mobarea.xenobiology_compatible)
+ scanner.attack(S, C)
+
+//Feeds a potion to slime
+/obj/machinery/computer/camera_advanced/xenobio/proc/XenoSlimeClickAlt(mob/living/user, mob/living/carbon/slime/S)
+ if(!cameranet.checkTurfVis(S.loc))
+ to_chat(user, "Target is not near a camera. Cannot proceed.")
+ return
+ var/mob/living/C = user
+ var/mob/camera/aiEye/remote/xenobio/E = C.remote_control
+ var/obj/machinery/computer/camera_advanced/xenobio/X = E.origin
+ var/area/mobarea = get_area(S.loc)
+ if(QDELETED(X.current_potion))
+ to_chat(C, "No potion loaded.")
+ return
+ if(mobarea.name == E.allowed_area || mobarea.xenobiology_compatible)
+ X.current_potion.attack(S, C)
+
+//Picks up slime
+/obj/machinery/computer/camera_advanced/xenobio/proc/XenoSlimeClickShift(mob/living/user, mob/living/carbon/slime/S)
+ if(!cameranet.checkTurfVis(S.loc))
+ to_chat(user, "Target is not near a camera. Cannot proceed.")
+ return
+ var/mob/living/C = user
+ var/mob/camera/aiEye/remote/xenobio/E = C.remote_control
+ var/obj/machinery/computer/camera_advanced/xenobio/X = E.origin
+ var/area/mobarea = get_area(S.loc)
+ if(mobarea.name == E.allowed_area || mobarea.xenobiology_compatible)
+ if(X.stored_slimes.len >= X.max_slimes)
+ return
+ if(!S.ckey)
+ if(S.buckled)
+ S.buckled.unbuckle_mob()
+ S.Feedstop()
+ S.visible_message("[S] vanishes in a flash of light!")
+ S.forceMove(X)
+ X.stored_slimes += S
+
+//Place slimes
+/obj/machinery/computer/camera_advanced/xenobio/proc/XenoTurfClickShift(mob/living/user, turf/T)
+ if(!cameranet.checkTurfVis(T))
+ to_chat(user, "Target is not near a camera. Cannot proceed.")
+ return
+ var/mob/living/C = user
+ var/mob/camera/aiEye/remote/xenobio/E = C.remote_control
+ var/obj/machinery/computer/camera_advanced/xenobio/X = E.origin
+ var/area/turfarea = get_area(T)
+ if(turfarea.name == E.allowed_area || turfarea.xenobiology_compatible)
+ for(var/mob/living/carbon/slime/S in X.stored_slimes)
+ S.forceMove(T)
+ S.visible_message("[S] warps in!")
+ X.stored_slimes -= S
+
+//Place monkey
+/obj/machinery/computer/camera_advanced/xenobio/proc/XenoTurfClickCtrl(mob/living/user, turf/T)
+ if(!cameranet.checkTurfVis(T))
+ to_chat(user, "Target is not near a camera. Cannot proceed.")
+ return
+ var/mob/living/C = user
+ var/mob/camera/aiEye/remote/xenobio/E = C.remote_control
+ var/obj/machinery/computer/camera_advanced/xenobio/X = E.origin
+ var/area/turfarea = get_area(T)
+
+ if(turfarea.name == E.allowed_area || turfarea.xenobiology_compatible)
+ if(X.monkeys >= 1)
+ var/mob/living/carbon/human/monkey/food = new /mob/living/carbon/human/monkey(E.loc)
+ food.LAssailant = C
+ X.monkeys --
+ to_chat(user, "[X] now has [X.monkeys] monkeys left.")
+
+//Pick up monkey
+/obj/machinery/computer/camera_advanced/xenobio/proc/XenoMonkeyClickCtrl(mob/living/user, mob/living/carbon/monkey/M)
+ if(!cameranet.checkTurfVis(M.loc))
+ to_chat(user, "Target is not near a camera. Cannot proceed.")
+ return
+ var/mob/living/C = user
+ var/mob/camera/aiEye/remote/xenobio/E = C.remote_control
+ var/obj/machinery/computer/camera_advanced/xenobio/X = E.origin
+ var/area/mobarea = get_area(M.loc)
+
+ if(mobarea.name == E.allowed_area || mobarea.xenobiology_compatible)
+ if(issmall(M) && M.stat)
+ M.visible_message("[M] vanishes as [M.p_theyre()] reclaimed for recycling!")
+ X.monkeys = round(X.monkeys + 0.2,0.1)
+ qdel(M)
diff --git a/icons/mob/actions/actions.dmi b/icons/mob/actions/actions.dmi
index fdc89eb09a6..d21323c80b1 100644
Binary files a/icons/mob/actions/actions.dmi and b/icons/mob/actions/actions.dmi differ