diff --git a/_maps/RandomRuins/SpaceRuins/syndicate_depot.dmm b/_maps/RandomRuins/SpaceRuins/syndicate_depot.dmm
index a3f4699ad7e4d..877d566283886 100644
--- a/_maps/RandomRuins/SpaceRuins/syndicate_depot.dmm
+++ b/_maps/RandomRuins/SpaceRuins/syndicate_depot.dmm
@@ -3349,11 +3349,6 @@
/obj/item/circuitboard/machine/quantumpad,
/obj/item/storage/toolbox/electrical,
/obj/item/circuitboard/machine/spaceship_navigation_beacon,
-/obj/item/manipulator_filter,
-/obj/item/manipulator_filter,
-/obj/item/manipulator_filter,
-/obj/item/manipulator_filter/cargo,
-/obj/item/manipulator_filter/cargo,
/obj/item/circuitboard/machine/big_manipulator,
/obj/item/circuitboard/machine/big_manipulator,
/obj/item/circuitboard/machine/big_manipulator,
diff --git a/code/__DEFINES/wires.dm b/code/__DEFINES/wires.dm
index 6fa56bd69681a..4b1038d8e8ad8 100644
--- a/code/__DEFINES/wires.dm
+++ b/code/__DEFINES/wires.dm
@@ -26,6 +26,12 @@
#define WIRE_DENY "Scan Fail"
#define WIRE_DISABLE "Disable"
#define WIRE_DISARM "Disarm"
+#define WIRE_ON "On"
+#define WIRE_DROP "Drop"
+#define WIRE_ITEM_TYPE "Item Type"
+#define WIRE_CHANGE_MODE "Change Mode"
+#define WIRE_ONE_PRIORITY_BUTTON "One Priority Button"
+#define WIRE_THROW_RANGE "Throw Range"
#define WIRE_DUD_PREFIX "__dud"
#define WIRE_HACK "Hack"
#define WIRE_IDSCAN "ID Scan"
diff --git a/code/__HELPERS/matrices.dm b/code/__HELPERS/matrices.dm
index 0a61ea86eb77e..2e8ed10c24e9a 100644
--- a/code/__HELPERS/matrices.dm
+++ b/code/__HELPERS/matrices.dm
@@ -127,6 +127,10 @@ c f 1
/matrix/proc/get_y_shift()
. = f
+///The angle of this matrix
+/matrix/proc/get_angle()
+ . = -ATAN2(a,d)
+
/////////////////////
// COLOUR MATRICES //
/////////////////////
diff --git a/code/datums/elements/deliver_first.dm b/code/datums/elements/deliver_first.dm
index 65aceca26dd55..0fb83a2545603 100644
--- a/code/datums/elements/deliver_first.dm
+++ b/code/datums/elements/deliver_first.dm
@@ -28,7 +28,6 @@
RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
RegisterSignal(target, COMSIG_ATOM_EMAG_ACT, PROC_REF(on_emag))
RegisterSignal(target, COMSIG_CLOSET_POST_OPEN, PROC_REF(on_post_open))
- RegisterSignal(target, COMSIG_FILTER_CHECK, PROC_REF(on_filter_check))
ADD_TRAIT(target, TRAIT_BANNED_FROM_CARGO_SHUTTLE, REF(src))
//registers pre_open when appropriate
area_check(target)
@@ -103,9 +102,4 @@
playsound(src, SFX_SPARKS, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
target.RemoveElement(/datum/element/deliver_first, goal_area_type, payment)
-/datum/element/deliver_first/proc/on_filter_check(obj/structure/closet/target, list/filter_locations)
- var/name = departmental_destination_to_tag(goal_area_type)
- if(name in filter_locations)
- return TRUE
- return FALSE
#undef DENY_SOUND_COOLDOWN
diff --git a/code/datums/greyscale/config_types/greyscale_configs.dm b/code/datums/greyscale/config_types/greyscale_configs.dm
index 00db5b2c356c6..89d98cfc36100 100644
--- a/code/datums/greyscale/config_types/greyscale_configs.dm
+++ b/code/datums/greyscale/config_types/greyscale_configs.dm
@@ -1258,16 +1258,6 @@
icon_file = 'monkestation/icons/mob/clothing/head_32x48.dmi'
json_config = 'code/datums/greyscale/json_configs/playbunny_ears_worn.json'
-/datum/greyscale_config/big_manipulator
- name = "Big Manipulator"
- icon_file = 'monkestation/code/modules/factory_type_beat/icons/big_manipulator_core.dmi'
- json_config = 'code/datums/greyscale/json_configs/big_manipulator.json'
-
-/datum/greyscale_config/manipulator_hand
- name = "Manipulator Hand"
- icon_file = 'monkestation/code/modules/factory_type_beat/icons/big_manipulator_hand.dmi'
- json_config = 'code/datums/greyscale/json_configs/manipulator_hand.json'
-
/datum/greyscale_config/linjacket
name = "Tassled Jacket"
icon_file = 'monkestation/icons/obj/clothing/necks.dmi'
@@ -1332,3 +1322,13 @@
name = "green_jester_shoes"
icon_file = 'monkestation/icons/mob/clothing/feet.dmi'
json_config = 'code/datums/greyscale/json_configs/green_jester_shoes_worn.json'
+
+/datum/greyscale_config/big_manipulator
+ name = "Big Manipulator"
+ icon_file = 'icons/obj/machines/big_manipulator_parts/big_manipulator_core.dmi'
+ json_config = 'code/datums/greyscale/json_configs/big_manipulator.json'
+
+/datum/greyscale_config/manipulator_arm
+ name = "Manipulator Arm"
+ icon_file = 'icons/obj/machines/big_manipulator_parts/big_manipulator_hand.dmi'
+ json_config = 'code/datums/greyscale/json_configs/manipulator_arm.json'
diff --git a/code/datums/greyscale/json_configs/manipulator_arm.json b/code/datums/greyscale/json_configs/manipulator_arm.json
new file mode 100644
index 0000000000000..ff1f927795707
--- /dev/null
+++ b/code/datums/greyscale/json_configs/manipulator_arm.json
@@ -0,0 +1,15 @@
+{
+ "hand": [
+ {
+ "type": "icon_state",
+ "icon_state": "hand",
+ "blend_mode": "overlay"
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "hand_colour",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ]
+}
diff --git a/code/datums/wires/big_manipulator.dm b/code/datums/wires/big_manipulator.dm
new file mode 100644
index 0000000000000..9ad7dbeb3a2e2
--- /dev/null
+++ b/code/datums/wires/big_manipulator.dm
@@ -0,0 +1,43 @@
+/datum/wires/big_manipulator
+ holder_type = /obj/machinery/big_manipulator
+ proper_name = "Big_Manipulator"
+
+/datum/wires/big_manipulator/New(atom/holder)
+ wires = list(
+ WIRE_ON,
+ WIRE_DROP,
+ WIRE_ITEM_TYPE,
+ WIRE_CHANGE_MODE,
+ WIRE_ONE_PRIORITY_BUTTON,
+ WIRE_THROW_RANGE
+ )
+ return ..()
+
+/datum/wires/big_manipulator/interactable(mob/user)
+ var/obj/machinery/big_manipulator/holder_manipulator = holder
+
+ return holder_manipulator.panel_open ? ..() : FALSE
+
+/datum/wires/big_manipulator/get_status()
+ var/obj/machinery/big_manipulator/holder_manipulator = holder
+ var/list/status = list()
+ status += "The big light bulb [holder_manipulator.power_access_wire_cut ? "is off" : "is glowing [holder_manipulator.on ? "green" : "red"]"]."
+ status += "The small light bulb [holder_manipulator.held_object ? "is glowing bright green" : "is off"]."
+ status += "The number on the display shows [length(holder_manipulator.tasks)]."
+ return status
+
+/datum/wires/big_manipulator/on_pulse(wire)
+ var/obj/machinery/big_manipulator/holder_manipulator = holder
+ switch(wire)
+ if(WIRE_ON)
+ holder_manipulator.try_press_on(usr)
+ if(WIRE_DROP)
+ holder_manipulator.drop_held_atom()
+
+/datum/wires/big_manipulator/on_cut(wire, mend, source)
+ var/obj/machinery/big_manipulator/holder_manipulator = holder
+ if(wire == WIRE_ON)
+ if(mend)
+ holder_manipulator.power_access_wire_cut = FALSE
+ return
+ holder_manipulator.power_access_wire_cut = TRUE
diff --git a/code/game/machinery/big_manipulator/_defines.dm b/code/game/machinery/big_manipulator/_defines.dm
new file mode 100644
index 0000000000000..b04d1b90bb601
--- /dev/null
+++ b/code/game/machinery/big_manipulator/_defines.dm
@@ -0,0 +1,62 @@
+// How should the manipulator interact with the point
+#define INTERACT_DROP "DROP"
+#define INTERACT_USE "USE"
+#define INTERACT_THROW "THROW"
+
+// What should be picked up from the point
+#define TAKE_ITEMS 1
+#define TAKE_CLOSETS 2
+#define TAKE_HUMANS 3
+
+#define MIN_SPEED_MULTIPLIER_TIER_1 0.5
+#define MIN_SPEED_MULTIPLIER_TIER_2 0.4
+#define MIN_SPEED_MULTIPLIER_TIER_3 0.3
+#define MIN_SPEED_MULTIPLIER_TIER_4 0.1
+
+#define MAX_SPEED_MULTIPLIER_TIER_1 2
+#define MAX_SPEED_MULTIPLIER_TIER_2 3
+#define MAX_SPEED_MULTIPLIER_TIER_3 5
+#define MAX_SPEED_MULTIPLIER_TIER_4 6
+
+#define MAX_TASKS_TIER_1 6
+#define MAX_TASKS_TIER_2 12
+#define MAX_TASKS_TIER_3 24
+#define MAX_TASKS_TIER_4 32
+
+
+// How should the worker interact with the point
+#define WORKER_SINGLE_USE "SINGLE TIME"
+#define WORKER_EMPTY_USE "EMPTY HAND"
+#define WORKER_NORMAL_USE "NORMAL"
+
+#define BASE_POWER_USAGE 0.2
+#define BASE_INTERACTION_TIME 0.3 SECONDS
+
+/// How long will the manipulator wait if there's nothing to do
+#define CYCLE_SKIP_TIMEOUT 1 SECONDS
+
+// How should overflow should be handled
+#define POINT_OVERFLOW_ALLOWED "ALLOW"
+#define POINT_OVERFLOW_FILTERS "TO FILTERS"
+#define POINT_OVERFLOW_HELD "TO HELD"
+#define POINT_OVERFLOW_FORBIDDEN "FORBID"
+
+// What should the manipulator do after there's nothing else to interact with on this point anymore
+#define POST_INTERACTION_DROP_AT_POINT "AT DROPOFF"
+#define POST_INTERACTION_DROP_AT_MACHINE "AT MACHINE"
+#define POST_INTERACTION_DROP_NEXT_FITTING "AT ANY FITTING"
+#define POST_INTERACTION_WAIT "CONTINUE"
+
+
+#define PICKUP_EAGER "Always Pick Up"
+#define PICKUP_CAN_WAIT "Wait For Suiting"
+
+#define TASK_TYPE_PICKUP "pickup"
+#define TASK_TYPE_DROP "drop"
+#define TASK_TYPE_THROW "throw"
+#define TASK_TYPE_USE "use"
+#define TASK_TYPE_INTERACT "interact"
+#define TASK_TYPE_WAIT "wait"
+
+#define TASKING_SEQUENTIAL "Sequential"
+#define TASKING_STRICT "Strict order"
diff --git a/code/game/machinery/big_manipulator/big_manipulator.dm b/code/game/machinery/big_manipulator/big_manipulator.dm
new file mode 100644
index 0000000000000..b805e0338744a
--- /dev/null
+++ b/code/game/machinery/big_manipulator/big_manipulator.dm
@@ -0,0 +1,908 @@
+/obj/machinery/big_manipulator
+ name = "big manipulator"
+ desc = "Operates different objects. Truly, a groundbreaking innovation..."
+ icon = 'icons/obj/machines/big_manipulator_parts/big_manipulator_core.dmi'
+ icon_state = "core"
+ density = TRUE
+ circuit = /obj/item/circuitboard/machine/big_manipulator
+ greyscale_colors = "#d8ce13"
+ greyscale_config = /datum/greyscale_config/big_manipulator
+
+ /// Is the manipulator turned on?
+ var/on = FALSE
+ /// Was the next cycle already scheduled?
+ var/next_cycle_scheduled = FALSE
+
+ /// How quickly the manipulator will process it's actions.
+ var/speed_multiplier = 1
+ var/min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_1
+ var/max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_1
+
+ /// The object inside the manipulator.
+ var/datum/weakref/held_object = null
+ /// The chimp worker that uses the manipulator (handles USE cases).
+ var/datum/weakref/monkey_worker = null
+ /// Weakref to the ID that locked this manipulator.
+ var/datum/weakref/id_lock = null
+ /// Inserted manipulator task disk.
+ var/obj/item/disk/manipulator/task_disk = null
+ /// The manipulator's arm.
+ var/obj/effect/big_manipulator_arm/manipulator_arm = null
+ /// Is the power access wire cut? Disables the power button if `TRUE`.
+ var/power_access_wire_cut = FALSE
+
+ /// How many tasks total we can have.
+ var/interaction_point_limit = MAX_TASKS_TIER_1
+
+ /// A list of tasks for the manipulator.
+ var/list/tasks = list()
+ /// The task we're currently working on.
+ var/datum/manipulator_task/current_task
+
+ /// Is the manipulator in the process of stopping?
+ var/stopping = FALSE
+ /// Is the manipulator waiting for a turf signal to retry?
+ var/waiting_for_signal = FALSE
+ /// Turfs we registered enter/exit signals on while waiting.
+ var/list/signal_turfs = list()
+
+ /// Which tasking scenario we use for iterating tasks.
+ var/tasking_strategy = TASKING_SEQUENTIAL
+ /// Tasking strategy instance.
+ var/datum/tasking_strategy/master_tasking
+
+/// Attempts to find a suitable turf near the manipulator for creating a cargo task.
+/obj/machinery/big_manipulator/proc/find_suitable_turf()
+ var/turf/base = get_turf(src)
+ for(var/turf/checked_turf in orange(base, 1))
+ if(!isclosedturf(checked_turf))
+ return checked_turf
+ return null
+
+/// Attempts to create a new task and assign it to the list.
+/obj/machinery/big_manipulator/proc/create_new_task(mob/user, task_type, turf/new_turf)
+ if(length(tasks) >= interaction_point_limit)
+ balloon_alert(user, "task limit reached!")
+ return FALSE
+
+ var/datum/stock_part/manipulator/locate_servo = locate() in component_parts
+ var/manipulator_tier = locate_servo ? locate_servo.tier : 1
+
+ var/datum/manipulator_task/new_task
+ var/needs_turf = task_type in list(TASK_TYPE_PICKUP, TASK_TYPE_DROP, TASK_TYPE_THROW, TASK_TYPE_USE, TASK_TYPE_INTERACT)
+
+ if(needs_turf)
+ if(!new_turf)
+ new_turf = find_suitable_turf()
+ if(!new_turf)
+ return FALSE
+
+ switch(task_type)
+ if(TASK_TYPE_PICKUP)
+ new_task = new /datum/manipulator_task/cargo/pickup(new_turf, manipulator_tier)
+ if(TASK_TYPE_DROP)
+ new_task = new /datum/manipulator_task/cargo/dropoff_base/drop(new_turf, manipulator_tier)
+ if(TASK_TYPE_THROW)
+ new_task = new /datum/manipulator_task/cargo/dropoff_base/throw(new_turf, manipulator_tier)
+ if(TASK_TYPE_USE)
+ new_task = new /datum/manipulator_task/cargo/dropoff_base/use(new_turf, manipulator_tier)
+ if(TASK_TYPE_INTERACT)
+ new_task = new /datum/manipulator_task/cargo/interact(new_turf, manipulator_tier)
+ if(TASK_TYPE_WAIT)
+ new_task = new /datum/manipulator_task/simple/wait()
+
+ if(QDELETED(new_task))
+ return FALSE
+
+ tasks += new_task
+
+ if(istype(new_task, /datum/manipulator_task/cargo))
+ var/datum/manipulator_task/cargo/cargo_task = new_task
+ cargo_task.offset_dx = new_turf.x - x
+ cargo_task.offset_dy = new_turf.y - y
+
+ if((obj_flags & EMAGGED) && istype(new_task, /datum/manipulator_task/cargo))
+ var/datum/manipulator_task/cargo/cargo_task = new_task
+ cargo_task.type_filters += /mob/living
+
+ return new_task
+
+/obj/machinery/big_manipulator/Initialize(mapload)
+ . = ..()
+ create_manipulator_arm()
+ process_upgrades()
+ if(on)
+ toggle_power_state(null)
+ set_wires(new /datum/wires/big_manipulator(src))
+ register_context()
+
+ update_strategies()
+
+/// Checks the component tiers, adjusting the properties of the manipulator.
+/obj/machinery/big_manipulator/proc/process_upgrades()
+ var/datum/stock_part/manipulator/locate_servo = locate() in component_parts
+ if(!locate_servo)
+ return
+
+ var/manipulator_tier = locate_servo.tier
+ switch(manipulator_tier)
+ if(-INFINITY to 1)
+ min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_1
+ max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_1
+ interaction_point_limit = MAX_TASKS_TIER_1
+ set_greyscale(COLOR_YELLOW)
+ manipulator_arm?.set_greyscale(COLOR_YELLOW)
+ if(2)
+ min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_2
+ max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_2
+ interaction_point_limit = MAX_TASKS_TIER_2
+ set_greyscale(COLOR_ORANGE)
+ manipulator_arm?.set_greyscale(COLOR_ORANGE)
+ if(3)
+ min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_3
+ max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_3
+ interaction_point_limit = MAX_TASKS_TIER_3
+ set_greyscale(COLOR_RED)
+ manipulator_arm?.set_greyscale(COLOR_RED)
+ if(4 to INFINITY)
+ min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_4
+ max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_4
+ interaction_point_limit = MAX_TASKS_TIER_4
+ set_greyscale(COLOR_PURPLE)
+ manipulator_arm?.set_greyscale(COLOR_PURPLE)
+
+ active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * BASE_POWER_USAGE * manipulator_tier
+
+ for(var/datum/manipulator_task/cargo/cargo_task in tasks)
+ cargo_task.interaction_priorities = cargo_task.fill_priority_list(manipulator_tier)
+
+/obj/machinery/big_manipulator/examine(mob/user)
+ . = ..()
+ var/mob/monkey_resolve = monkey_worker?.resolve()
+ if(!isnull(monkey_resolve))
+ . += "You can see a poor [monkey_resolve.name] buckled to [src]. You wonder if it's getting paid enough."
+
+/obj/machinery/big_manipulator/attack_hand_secondary(mob/living/user, list/modifiers)
+ try_press_on(user)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+/obj/machinery/big_manipulator/click_alt(mob/user)
+ eject_task_disk(user)
+ return CLICK_ACTION_SUCCESS
+
+/obj/machinery/big_manipulator/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+
+ context[SCREENTIP_CONTEXT_RMB] = "Toggle"
+ context[SCREENTIP_CONTEXT_ALT_LMB] = "Eject disk"
+
+ if(isnull(held_item))
+ context[SCREENTIP_CONTEXT_LMB] = panel_open ? "Interact with wires" : "Open UI"
+ return CONTEXTUAL_SCREENTIP_SET
+
+ if(held_item.tool_behaviour == TOOL_WRENCH)
+ context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Una" : "A"]nchor"
+ return CONTEXTUAL_SCREENTIP_SET
+ if(held_item.tool_behaviour == TOOL_SCREWDRIVER)
+ context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel"
+ return CONTEXTUAL_SCREENTIP_SET
+ if(held_item.tool_behaviour == TOOL_CROWBAR && panel_open)
+ context[SCREENTIP_CONTEXT_LMB] = "Deconstruct"
+ return CONTEXTUAL_SCREENTIP_SET
+ if(is_wire_tool(held_item) && panel_open)
+ context[SCREENTIP_CONTEXT_LMB] = "Interact with wires"
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/machinery/big_manipulator/Destroy(force)
+ if(task_disk)
+ task_disk.forceMove(drop_location())
+ task_disk = null
+ unregister_task_turf_signals()
+ QDEL_NULL(manipulator_arm)
+ QDEL_LIST(tasks)
+ id_lock = null
+ return ..()
+
+/obj/machinery/big_manipulator/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(isnull(monkey_worker))
+ return
+
+ var/mob/living/carbon/human/species/monkey/poor_monkey = monkey_worker.resolve()
+ if(gone != poor_monkey)
+ return
+
+ vis_contents -= poor_monkey
+ poor_monkey.transform = matrix()
+ monkey_worker = null
+
+
+/// Removes an invalid task from the list.
+/obj/machinery/big_manipulator/proc/remove_invalid_task(datum/manipulator_task/task)
+ if(!task)
+ return
+ tasks -= task
+ qdel(task)
+
+/obj/machinery/big_manipulator/emag_act(mob/user, obj/item/card/emag/emag_card)
+ . = ..()
+ if(obj_flags & EMAGGED)
+ return FALSE
+
+ balloon_alert(user, "overloaded")
+ obj_flags |= EMAGGED
+
+ for(var/datum/manipulator_task/cargo/cargo_task in tasks)
+ cargo_task.type_filters += /mob/living
+
+ return TRUE
+
+/obj/machinery/big_manipulator/wrench_act(mob/living/user, obj/item/tool)
+ . = ..()
+ default_unfasten_wrench(user, tool, time = 1 SECONDS)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/big_manipulator/can_be_unfasten_wrench(mob/user, silent)
+ if(on || stopping)
+ to_chat(user, span_warning("[src] is activated!"))
+ return FAILED_UNFASTEN
+ return ..()
+
+/obj/machinery/big_manipulator/default_unfasten_wrench(mob/user, obj/item/wrench, time)
+ . = ..()
+ if(. == SUCCESSFUL_UNFASTEN)
+ if(anchored)
+ validate_all_tasks()
+
+/obj/machinery/big_manipulator/screwdriver_act(mob/living/user, obj/item/tool)
+ return default_deconstruction_screwdriver(user, "core", "core", tool)
+
+/obj/machinery/big_manipulator/crowbar_act(mob/living/user, obj/item/tool)
+ return default_deconstruction_crowbar(tool)
+
+/obj/machinery/big_manipulator/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
+ if(user.istate & ISTATE_HARM)
+ return NONE
+
+ if(istype(tool, /obj/item/disk/manipulator))
+ if(on || stopping)
+ balloon_alert(user, "turn it off first!")
+ return ITEM_INTERACT_BLOCKING
+ if(task_disk)
+ task_disk.forceMove(drop_location())
+ task_disk = null
+ if(!user.transferItemToLoc(tool, src))
+ return ITEM_INTERACT_BLOCKING
+ task_disk = tool
+ balloon_alert(user, "disk inserted")
+ SStgui.update_uis(src)
+ return ITEM_INTERACT_SUCCESS
+
+ if(!panel_open || !is_wire_tool(tool))
+ return NONE
+ wires.interact(user)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/big_manipulator/RefreshParts()
+ . = ..()
+ process_upgrades()
+
+/obj/machinery/big_manipulator/mouse_drop_dragged(atom/drop_point, mob/user, src_location, over_location, params)
+ if(on || stopping)
+ balloon_alert(user, "turn it off first!")
+ return
+
+ var/mob/living/carbon/human/species/monkey/poor_monkey = monkey_worker?.resolve()
+ if(!poor_monkey)
+ return
+
+ balloon_alert(user, "trying to unbuckle...")
+ if(!do_after(user, 3 SECONDS, src))
+ balloon_alert(user, "interrupted")
+ return
+
+ balloon_alert(user, "unbuckled")
+ poor_monkey.drop_all_held_items()
+ poor_monkey.forceMove(drop_location())
+
+/obj/machinery/big_manipulator/mouse_drop_receive(atom/monkey, mob/user, params)
+ if(on || stopping)
+ balloon_alert(user, "turn it off first!")
+ return
+
+ if(monkey_worker?.resolve())
+ return
+
+ if(!ismonkey(monkey))
+ return
+
+ var/mob/living/carbon/human/species/monkey/poor_monkey = monkey
+ if(poor_monkey.mind)
+ balloon_alert(user, "too smart!")
+ return
+
+ poor_monkey.balloon_alert(user, "trying to buckle...")
+ if(!do_after(user, 3 SECONDS, poor_monkey))
+ poor_monkey.balloon_alert(user, "interrupted")
+ return
+
+ balloon_alert(user, "buckled")
+ monkey_worker = WEAKREF(poor_monkey)
+ poor_monkey.drop_all_held_items()
+ poor_monkey.forceMove(src)
+ vis_contents += poor_monkey
+ poor_monkey.dir = manipulator_arm.dir
+ poor_monkey.transform = manipulator_arm.transform
+
+/obj/machinery/big_manipulator/attackby(obj/item/some_item, mob/user, params)
+ . = ..()
+ if(!isidcard(some_item))
+ return
+
+ var/obj/item/card/id/clicked_by_this_id = some_item
+
+ if(!id_lock)
+ id_lock = WEAKREF(clicked_by_this_id)
+ balloon_alert(user, "successfully locked")
+ return
+ var/obj/item/card/id/resolve_id = id_lock.resolve()
+ if(clicked_by_this_id != resolve_id)
+ balloon_alert(user, "locked by another id")
+ return
+ id_lock = null
+ balloon_alert(user, "successfully unlocked")
+
+/// Attaching the arm effect to the core.
+/obj/machinery/big_manipulator/proc/create_manipulator_arm()
+ manipulator_arm = new /obj/effect/big_manipulator_arm(src)
+ manipulator_arm.dir = NORTH
+ manipulator_arm.target_dir = NORTH
+ vis_contents += manipulator_arm
+
+/obj/machinery/big_manipulator/proc/toggle_power_state(mob/user)
+ var/newly_on = !on
+
+ if(!user)
+ on = newly_on
+ return
+
+ if(newly_on)
+ if(!powered())
+ balloon_alert(user, "no power!")
+ return
+
+ if(!anchored)
+ balloon_alert(user, "anchor first!")
+ return
+
+ validate_all_tasks()
+
+ on = newly_on
+ SStgui.update_uis(src)
+ try_kickstart(user)
+
+ else
+ drop_held_atom()
+ on = newly_on
+ next_cycle_scheduled = FALSE
+ if(current_task != null && !stopping)
+ stopping = TRUE
+ addtimer(CALLBACK(src, PROC_REF(complete_stopping_task)), 1 SECONDS)
+ else
+ stopping = FALSE
+ unregister_task_turf_signals()
+ waiting_for_signal = FALSE
+ SStgui.update_uis(src)
+
+/// Validates all cargo tasks, removing those on closed turfs.
+/obj/machinery/big_manipulator/proc/validate_all_tasks()
+ for(var/datum/manipulator_task/cargo/cargo_task in tasks)
+ if(!cargo_task.is_valid())
+ tasks -= cargo_task
+ qdel(cargo_task)
+
+/// Attempts to press the power button.
+/obj/machinery/big_manipulator/proc/try_press_on(mob/living/carbon/human/user)
+ if(power_access_wire_cut)
+ balloon_alert(user, "unresponsive!")
+ return
+
+ if(stopping)
+ balloon_alert(user, "stopping in progress!")
+ return
+
+ toggle_power_state(user)
+ if(on)
+ balloon_alert(user, "activated")
+ else
+ balloon_alert(user, "deactivated")
+
+/obj/machinery/big_manipulator/proc/_collect_filter_names(list/filters)
+ var/list/names = list()
+ for(var/atom/f as anything in filters)
+ names += initial(f.name)
+ return names
+
+/obj/machinery/big_manipulator/proc/_collect_priorities(list/priorities)
+ var/list/out = list()
+ for(var/datum/manipulator_priority/pr in priorities)
+ var/list/entry = list()
+ entry["name"] = pr.name
+ entry["active"] = pr.active
+ out += list(entry)
+ return out
+
+/obj/machinery/big_manipulator/ui_act(action, params, datum/tgui/ui)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("run_cycle")
+ try_press_on(ui.user)
+ return TRUE
+
+ if("drop_held_atom")
+ drop_held_atom()
+ return TRUE
+
+ if("create_task")
+ create_new_task(ui.user, params["task_type"])
+ maybe_wake()
+ return TRUE
+
+ if("reset_tasking_index")
+ master_tasking.current_index = 1
+ balloon_alert(ui.user, "tasking index reset")
+ maybe_wake()
+ return TRUE
+
+ if("cycle_tasking_strategy")
+ var/new_strategy = params["new_strategy"]
+ if(new_strategy in list(TASKING_SEQUENTIAL, TASKING_STRICT))
+ tasking_strategy = new_strategy
+ update_strategies()
+ maybe_wake()
+ return TRUE
+
+ if("adjust_interaction_speed")
+ var/new_speed = text2num(params["new_speed"])
+ if(isnull(new_speed))
+ return FALSE
+ speed_multiplier = clamp(new_speed, min_speed_multiplier, max_speed_multiplier)
+ return TRUE
+
+ if("unbuckle")
+ if(monkey_worker)
+ var/mob/living/carbon/human/species/monkey/poor_monkey = monkey_worker.resolve()
+ if(poor_monkey && poor_monkey.loc == src)
+ poor_monkey.forceMove(drop_location())
+ return TRUE
+
+ if("adjust_task_param")
+ var/success = adjust_param_for_task(params["taskId"], params["param"], params["value"], ui.user)
+ if(success)
+ maybe_wake()
+ return success
+
+ if("disk_eject")
+ return eject_task_disk(ui.user)
+
+ if("disk_read")
+ if(read_disk_tasks(ui.user))
+ maybe_wake()
+ return TRUE
+
+ if("disk_write")
+ return write_disk_tasks(ui.user)
+
+ if("disk_clear")
+ return clear_disk_tasks(ui.user)
+
+
+/obj/machinery/big_manipulator/proc/eject_task_disk(mob/user)
+ if(on || stopping)
+ balloon_alert(user, "turn it off first!")
+ return FALSE
+ if(!task_disk)
+ return FALSE
+ var/obj/item/disk/manipulator/ejectable_disk = task_disk
+ task_disk = null
+ if(istype(user) && user.put_in_hands(ejectable_disk))
+ balloon_alert(user, "disk ejected")
+ else
+ ejectable_disk.forceMove(drop_location())
+ balloon_alert(user, "disk dropped")
+ SStgui.update_uis(src)
+ return TRUE
+
+/obj/machinery/big_manipulator/proc/clear_disk_tasks(mob/user)
+ if(on || stopping)
+ balloon_alert(user, "turn it off first!")
+ return FALSE
+ if(!task_disk)
+ return FALSE
+ if(task_disk.read_only)
+ balloon_alert(user, "disk protected")
+ return FALSE
+ task_disk.set_tasks(list())
+ balloon_alert(user, "cleared")
+ SStgui.update_uis(src)
+ return TRUE
+
+/obj/machinery/big_manipulator/proc/write_disk_tasks(mob/user)
+ if(on || stopping)
+ balloon_alert(user, "turn it off first!")
+ return FALSE
+ if(!task_disk)
+ return FALSE
+ if(task_disk.read_only)
+ balloon_alert(user, "disk protected")
+ return FALSE
+
+ var/list/out = list()
+ for(var/datum/manipulator_task/task as anything in tasks)
+ out += list(task.serialize())
+
+ task_disk.set_tasks(out)
+ balloon_alert(user, "written")
+ SStgui.update_uis(src)
+ return TRUE
+
+/obj/machinery/big_manipulator/proc/read_disk_tasks(mob/user)
+ if(on || stopping)
+ balloon_alert(user, "turn it off first!")
+ return FALSE
+ if(!task_disk)
+ return FALSE
+
+ QDEL_LIST(tasks)
+ tasks = list()
+ current_task = null
+
+ var/turf/base = get_turf(src)
+ var/datum/stock_part/manipulator/locate_servo = locate() in component_parts
+ var/manipulator_tier = locate_servo ? locate_servo.tier : 1
+
+ for(var/list/task_data as anything in task_disk.tasks_data)
+ if(length(tasks) >= interaction_point_limit)
+ break
+ if(!islist(task_data))
+ continue
+ var/task_type = task_data["type"]
+ if(!ispath(task_type, /datum/manipulator_task))
+ continue
+ var/datum/manipulator_task/new_task
+ if(ispath(task_type, /datum/manipulator_task/cargo))
+ if(!base)
+ continue
+ var/list/offset = task_data["offset"]
+ if(!islist(offset))
+ continue
+ var/dx = offset["dx"]
+ var/dy = offset["dy"]
+ if(!isnum(dx) || !isnum(dy))
+ continue
+ if(dx < -1 || dx > 1 || dy < -1 || dy > 1)
+ continue
+ if(dx == 0 && dy == 0)
+ continue
+ var/turf/target_turf = locate(base.x + dx, base.y + dy, base.z)
+ if(!target_turf || isclosedturf(target_turf))
+ continue
+ new_task = new task_type(target_turf, manipulator_tier, serialized_data = task_data)
+ if(istype(new_task, /datum/manipulator_task/cargo))
+ var/datum/manipulator_task/cargo/c = new_task
+ c.offset_dx = dx
+ c.offset_dy = dy
+ else
+ new_task = new task_type(serialized_data = task_data)
+ if(!new_task || QDELETED(new_task))
+ continue
+ tasks += new_task
+
+ process_upgrades()
+ validate_all_tasks()
+ balloon_alert(user, "loaded")
+ SStgui.update_uis(src)
+ return TRUE
+
+/obj/machinery/big_manipulator/proc/adjust_param_for_task(task_ref, param, value, mob/user)
+ if(!param)
+ return FALSE
+
+ var/datum/manipulator_task/target_task = locate(task_ref) in tasks
+ if(!target_task)
+ return FALSE
+
+ switch(param)
+ if("set_name")
+ if(!value)
+ return FALSE
+ target_task.name = sanitize_name(value, allow_numbers = TRUE)
+ return TRUE
+
+ if("set_wait_time")
+ if(!istype(target_task, /datum/manipulator_task/simple/wait))
+ return FALSE
+ var/datum/manipulator_task/simple/wait/t = target_task
+ t.time_seconds = clamp(text2num(value), 1, 60)
+ return TRUE
+
+ if("remove_task")
+ tasks.Remove(target_task)
+ qdel(target_task)
+ return TRUE
+
+ if("move_up")
+ var/idx = tasks.Find(target_task)
+ if(idx <= 1)
+ return FALSE
+ tasks.Swap(idx, idx - 1)
+ return TRUE
+
+ if("move_down")
+ var/idx = tasks.Find(target_task)
+ if(idx >= length(tasks))
+ return FALSE
+ tasks.Swap(idx, idx + 1)
+ return TRUE
+
+ if("move_to")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/cargo_task = target_task
+ var/button_number = text2num(value["buttonNumber"])
+ if(button_number < 1 || button_number > 9)
+ return
+ var/dx = ((button_number - 1) % 3) - 1
+ var/dy = 1 - round((button_number - 1) / 3)
+ var/turf/new_turf = locate(x + dx, y + dy, z)
+ if(!new_turf || isclosedturf(new_turf))
+ return FALSE
+ cargo_task.interaction_turf = new_turf
+ cargo_task.offset_dx = dx
+ cargo_task.offset_dy = dy
+ return TRUE
+
+ if("toggle_filter_skip")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/ct = target_task
+ ct.should_use_filters = !ct.should_use_filters
+ return TRUE
+
+ if("reset_atom_filters")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/ct = target_task
+ ct.atom_filters = list()
+ return TRUE
+
+ if("add_atom_filter_from_held")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/ct = target_task
+ var/obj/item/held_item = user.get_active_held_item()
+ if(!held_item)
+ return FALSE
+ for(var/filter_path in ct.atom_filters)
+ if(istype(held_item, filter_path))
+ return FALSE
+ ct.atom_filters += held_item.type
+ return TRUE
+
+ if("delete_filter")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/ct = target_task
+ ct.atom_filters.Cut(value, value + 1)
+ return TRUE
+
+ if("cycle_filtering_mode")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/ct = target_task
+ ct.filtering_mode = cycle_value(ct.filtering_mode, obj_flags & EMAGGED ? list(TAKE_ITEMS, TAKE_CLOSETS, TAKE_HUMANS) : list(TAKE_ITEMS, TAKE_CLOSETS))
+ return TRUE
+
+ if("toggle_priority")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/current_task = target_task
+ return current_task.tick_priority_by_index(value)
+
+ if("priority_move_up")
+ if(!istype(target_task, /datum/manipulator_task/cargo))
+ return FALSE
+ var/datum/manipulator_task/cargo/current_task = target_task
+ return current_task.move_priority_up_by_index(value)
+
+ if("cycle_pickup_eagerness")
+ if(!istype(target_task, /datum/manipulator_task/cargo/pickup))
+ return FALSE
+ var/datum/manipulator_task/cargo/pickup/cycle_target_task = target_task
+ cycle_target_task.pickup_eagerness = cycle_value(cycle_target_task.pickup_eagerness, list(PICKUP_CAN_WAIT, PICKUP_EAGER))
+ return TRUE
+
+ if("cycle_overflow_status")
+ if(!istype(target_task, /datum/manipulator_task/cargo/dropoff_base/drop))
+ return FALSE
+ var/datum/manipulator_task/cargo/dropoff_base/drop/cycle_target_task = target_task
+ cycle_target_task.overflow_status = cycle_value(cycle_target_task.overflow_status, list(POINT_OVERFLOW_ALLOWED, POINT_OVERFLOW_FILTERS, POINT_OVERFLOW_HELD, POINT_OVERFLOW_FORBIDDEN))
+ return TRUE
+
+ if("cycle_throw_range")
+ if(!istype(target_task, /datum/manipulator_task/cargo/dropoff_base/throw))
+ return FALSE
+ var/datum/manipulator_task/cargo/dropoff_base/throw/cycle_target_task = target_task
+ cycle_target_task.throw_range = cycle_value(cycle_target_task.throw_range, list(1, 2, 3, 4, 5, 6, 7))
+ return TRUE
+
+ if("cycle_worker_interaction")
+ var/list/vals = list(WORKER_NORMAL_USE, WORKER_SINGLE_USE, WORKER_EMPTY_USE)
+ if(istype(target_task, /datum/manipulator_task/cargo/dropoff_base/use))
+ var/datum/manipulator_task/cargo/dropoff_base/use/cycle_target_task = target_task
+ cycle_target_task.worker_interaction = cycle_value(cycle_target_task.worker_interaction, vals)
+ return TRUE
+ if(istype(target_task, /datum/manipulator_task/cargo/interact))
+ var/datum/manipulator_task/cargo/interact/cycle_target_task = target_task
+ cycle_target_task.worker_interaction = cycle_value(cycle_target_task.worker_interaction, vals)
+ return TRUE
+ return FALSE
+
+ if("cycle_post_interaction")
+ var/list/vals = list(POST_INTERACTION_DROP_AT_POINT, POST_INTERACTION_DROP_AT_MACHINE, POST_INTERACTION_DROP_NEXT_FITTING, POST_INTERACTION_WAIT)
+ if(istype(target_task, /datum/manipulator_task/cargo/dropoff_base/use))
+ var/datum/manipulator_task/cargo/dropoff_base/use/cycle_target_task = target_task
+ cycle_target_task.use_post_interaction = cycle_value(cycle_target_task.use_post_interaction, vals)
+ return TRUE
+ if(istype(target_task, /datum/manipulator_task/cargo/interact))
+ var/datum/manipulator_task/cargo/interact/cycle_target_task = target_task
+ cycle_target_task.use_post_interaction = cycle_value(cycle_target_task.use_post_interaction, vals)
+ return TRUE
+ return FALSE
+
+ if("toggle_worker_rmb")
+ if(istype(target_task, /datum/manipulator_task/cargo/dropoff_base/use))
+ var/datum/manipulator_task/cargo/dropoff_base/use/cycle_target_task = target_task
+ cycle_target_task.worker_use_rmb = !cycle_target_task.worker_use_rmb
+ return TRUE
+ if(istype(target_task, /datum/manipulator_task/cargo/interact))
+ var/datum/manipulator_task/cargo/interact/cycle_target_task = target_task
+ cycle_target_task.worker_use_rmb = !cycle_target_task.worker_use_rmb
+ return TRUE
+ return FALSE
+
+ if("toggle_worker_combat")
+ if(istype(target_task, /datum/manipulator_task/cargo/dropoff_base/use))
+ var/datum/manipulator_task/cargo/dropoff_base/use/cycle_target_task = target_task
+ cycle_target_task.worker_combat_mode = !cycle_target_task.worker_combat_mode
+ return TRUE
+ if(istype(target_task, /datum/manipulator_task/cargo/interact))
+ var/datum/manipulator_task/cargo/interact/cycle_target_task = target_task
+ cycle_target_task.worker_combat_mode = !cycle_target_task.worker_combat_mode
+ return TRUE
+ return FALSE
+
+/// Cycles the given value in the given list.
+/obj/machinery/big_manipulator/proc/cycle_value(current_value, list/possible_values)
+ var/current_index = possible_values.Find(current_value)
+ if(current_index == 0)
+ return possible_values[1]
+ return possible_values[(current_index % length(possible_values)) + 1]
+
+/// Retries the task loop if we're waiting for a signal and the machine is on.
+/obj/machinery/big_manipulator/proc/maybe_wake()
+ if(on && !stopping && waiting_for_signal)
+ something_happened()
+
+/obj/machinery/big_manipulator/proc/update_strategies()
+ master_tasking = create_strategy(tasking_strategy)
+
+/obj/machinery/big_manipulator/proc/create_strategy(strategy_mode)
+ switch(strategy_mode)
+ if(TASKING_SEQUENTIAL)
+ return new /datum/tasking_strategy/sequential()
+ if(TASKING_STRICT)
+ return new /datum/tasking_strategy/strict()
+ return new /datum/tasking_strategy/sequential()
+
+/obj/machinery/big_manipulator/ui_interact(mob/user, datum/tgui/ui)
+ if(id_lock)
+ to_chat(user, span_warning("[src] is locked behind ID authentication!"))
+ ui?.close()
+ return
+ if(!anchored)
+ to_chat(user, span_warning("[src] isn't attached to the ground!"))
+ ui?.close()
+ return
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "BigManipulator")
+ ui.open()
+
+/obj/machinery/big_manipulator/ui_data(mob/user)
+ . = list(
+ "active" = on,
+ "stopping" = stopping,
+ "current_task" = current_task ? REF(current_task) : null,
+ "speed_multiplier" = speed_multiplier,
+ "min_speed_multiplier" = min_speed_multiplier,
+ "max_speed_multiplier" = max_speed_multiplier,
+ "manipulator_position" = "[x],[y]",
+ "tasking_strategy" = tasking_strategy,
+ "has_monkey" = !isnull(monkey_worker),
+ "disk_inserted" = !isnull(task_disk),
+ "disk_read_only" = task_disk?.read_only || FALSE,
+ "disk_task_count" = length(task_disk?.tasks_data || list()),
+ )
+
+ .["tasks_data"] = list()
+ for(var/datum/manipulator_task/task as anything in tasks)
+ var/task_type = ""
+ if(istype(task, /datum/manipulator_task/cargo/pickup))
+ task_type = "pickup"
+ else if(istype(task, /datum/manipulator_task/cargo/dropoff_base/drop))
+ task_type = "drop"
+ else if(istype(task, /datum/manipulator_task/cargo/dropoff_base/throw))
+ task_type = "throw"
+ else if(istype(task, /datum/manipulator_task/cargo/dropoff_base/use))
+ task_type = "use"
+ else if(istype(task, /datum/manipulator_task/cargo/interact))
+ task_type = "interact"
+ else if(istype(task, /datum/manipulator_task/simple/wait))
+ task_type = "wait"
+
+ var/list/task_data = list(
+ "name" = task.name,
+ "id" = "[REF(task)]",
+ "task_type" = task_type,
+ )
+ if(istype(task, /datum/manipulator_task/cargo))
+ var/datum/manipulator_task/cargo/task_cargo = task
+ if(task_cargo.interaction_turf)
+ task_data["turf"] = "[task_cargo.interaction_turf.x - x],[task_cargo.interaction_turf.y - y]"
+ task_data["filters_status"] = task_cargo.should_use_filters
+ task_data["filtering_mode"] = task_cargo.filtering_mode
+ task_data["settings_list"] = list()
+ for(var/datum/manipulator_priority/priority as anything in task_cargo.interaction_priorities)
+ task_data["settings_list"] += list(list(
+ "name" = priority.name,
+ "active" = priority.active,
+ ))
+ task_data["item_filters"] = list()
+ for(var/atom/movable/filter_atom as anything in task_cargo.atom_filters)
+ task_data["item_filters"] += "[filter_atom]"
+
+ if(istype(task, /datum/manipulator_task/cargo/pickup))
+ var/datum/manipulator_task/cargo/pickup/task_pickup = task
+ task_data["pickup_eagerness"] = task_pickup.pickup_eagerness
+
+ if(istype(task, /datum/manipulator_task/cargo/dropoff_base/drop))
+ var/datum/manipulator_task/cargo/dropoff_base/drop/task_drop = task
+ task_data["overflow_status"] = task_drop.overflow_status
+
+ if(istype(task, /datum/manipulator_task/cargo/dropoff_base/throw))
+ var/datum/manipulator_task/cargo/dropoff_base/throw/task_throw = task
+ task_data["throw_range"] = task_throw.throw_range
+
+ if(istype(task, /datum/manipulator_task/cargo/dropoff_base/use))
+ var/datum/manipulator_task/cargo/dropoff_base/use/task_use = task
+ task_data["worker_interaction"] = task_use.worker_interaction
+ task_data["use_post_interaction"] = task_use.use_post_interaction
+ task_data["worker_use_rmb"] = task_use.worker_use_rmb
+ task_data["worker_combat_mode"] = task_use.worker_combat_mode
+
+ if(istype(task, /datum/manipulator_task/cargo/interact))
+ var/datum/manipulator_task/cargo/interact/task_interact = task
+ task_data["worker_interaction"] = task_interact.worker_interaction
+ task_data["use_post_interaction"] = task_interact.use_post_interaction
+ task_data["worker_use_rmb"] = task_interact.worker_use_rmb
+ task_data["worker_combat_mode"] = task_interact.worker_combat_mode
+
+ if(istype(task, /datum/manipulator_task/simple/wait))
+ var/datum/manipulator_task/simple/wait/task_wait = task
+ task_data["time"] = task_wait.time_seconds
+
+ .["tasks_data"] += list(task_data)
diff --git a/code/game/machinery/big_manipulator/big_manipulator_interactions.dm b/code/game/machinery/big_manipulator/big_manipulator_interactions.dm
new file mode 100644
index 0000000000000..3adaca2b945b3
--- /dev/null
+++ b/code/game/machinery/big_manipulator/big_manipulator_interactions.dm
@@ -0,0 +1,366 @@
+/// We have no tasks to execute for some reason. Waits for a turf signal to retry.
+/obj/machinery/big_manipulator/proc/nothing_ever_happens()
+ if(stopping)
+ complete_stopping_task()
+ return FALSE
+
+ current_task = null
+ waiting_for_signal = TRUE
+ register_task_turf_signals()
+
+ return FALSE
+
+/// A signal ran or some settings changed; checking if we can run the tasks now.
+/obj/machinery/big_manipulator/proc/something_happened()
+ next_cycle_scheduled = FALSE
+ step_tasks()
+
+/// Runs the next task. Or doesn't.
+/obj/machinery/big_manipulator/proc/step_tasks()
+ if(!on || stopping)
+ return
+ next_cycle_scheduled = FALSE
+ if(waiting_for_signal)
+ unregister_task_turf_signals()
+ waiting_for_signal = FALSE
+ if(!length(tasks))
+ nothing_ever_happens()
+ return
+ var/datum/manipulator_task/next_task = master_tasking.get_next_task(tasks, src)
+ if(!next_task)
+ nothing_ever_happens()
+ return
+ current_task = next_task
+ SStgui.update_uis(src)
+ next_task.run_task(src)
+
+/// Attempts to launch the work cycle. Should only be ran on pressing the "Run" button.
+/obj/machinery/big_manipulator/proc/try_kickstart(mob/user)
+ if(!on || !anchored || stopping || current_task != null)
+ return FALSE
+
+ if(!use_energy(active_power_usage, force = FALSE))
+ on = FALSE
+ balloon_alert_to_viewers("not enough power!")
+ return FALSE
+
+ next_cycle_scheduled = FALSE
+ step_tasks()
+
+/// Safely schedules the next step to prevent overlapping.
+/obj/machinery/big_manipulator/proc/schedule_next_cycle(time_seconds = BASE_INTERACTION_TIME)
+ if(next_cycle_scheduled || stopping)
+ return
+
+ next_cycle_scheduled = TRUE
+ addtimer(CALLBACK(src, PROC_REF(step_tasks)), time_seconds)
+
+/// Rotates the manipulator arm to face the target task's turf.
+/obj/machinery/big_manipulator/proc/rotate_to_point(datum/manipulator_task/cargo/target_task, callback_object, callback)
+ if(stopping)
+ return
+
+ if(!target_task)
+ return FALSE
+
+ var/task_dir = get_dir(get_turf(src), target_task.interaction_turf)
+ manipulator_arm.target_dir = task_dir
+
+ var/target_angle = dir2angle(task_dir)
+ var/current_angle = manipulator_arm.transform.get_angle()
+ var/angle_diff = closer_angle_difference(current_angle, target_angle)
+
+ var/num_rotations = round(abs(angle_diff) / 45)
+
+ if(!num_rotations)
+ var/datum/callback/cb = CALLBACK(callback_object, callback, src)
+ cb.Invoke()
+ return TRUE
+
+ var/rotation_step = 45 * sign(angle_diff)
+ do_step_rotation(target_task, callback_object, callback, current_angle, target_angle, rotation_step)
+ return TRUE
+
+/// Does a 45 degree step, animating the claw
+/obj/machinery/big_manipulator/proc/do_step_rotation(datum/manipulator_task/cargo/target_task, callback_object, callback, current_angle, target_angle, rotation_step)
+ if(stopping)
+ return
+
+ var/angle_diff = closer_angle_difference(current_angle, target_angle)
+ if(abs(angle_diff) < abs(rotation_step))
+ var/matrix/final_matrix = matrix()
+ final_matrix.Turn(target_angle)
+ animate(manipulator_arm, transform = final_matrix, time = BASE_INTERACTION_TIME / speed_multiplier)
+ var/mob/living/carbon/human/species/monkey/monkey_resolve = monkey_worker?.resolve()
+ if(monkey_resolve && monkey_resolve.loc == src)
+ animate(monkey_resolve, transform = final_matrix, time = BASE_INTERACTION_TIME / speed_multiplier)
+ addtimer(CALLBACK(callback_object, callback, src), BASE_INTERACTION_TIME / speed_multiplier)
+ return
+
+ var/next_angle = current_angle + rotation_step
+ var/matrix/next_matrix = matrix()
+ next_matrix.Turn(next_angle)
+ animate(manipulator_arm, transform = next_matrix, time = BASE_INTERACTION_TIME / speed_multiplier)
+ var/mob/living/carbon/human/species/monkey/monkey_resolve = monkey_worker?.resolve()
+ if(monkey_resolve && monkey_resolve.loc == src)
+ animate(monkey_resolve, transform = next_matrix, time = BASE_INTERACTION_TIME / speed_multiplier)
+
+ addtimer(CALLBACK(src, PROC_REF(do_step_rotation), target_task, callback_object, callback, next_angle, target_angle, rotation_step), BASE_INTERACTION_TIME / speed_multiplier)
+
+/obj/machinery/big_manipulator/proc/try_drop_thing(datum/manipulator_task/cargo/dropoff_base/drop/destination_task)
+ var/drop_endpoint = destination_task.find_type_priority()
+ var/obj/actual_held_object = held_object?.resolve()
+
+ if(!actual_held_object)
+ drop_held_atom()
+ return FALSE
+
+ if(isnull(drop_endpoint))
+ drop_held_atom()
+ return FALSE
+
+ var/atom/drop_target = drop_endpoint
+ if(drop_target.atom_storage && actual_held_object && (!drop_target.atom_storage.attempt_insert(actual_held_object, override = TRUE)))
+ actual_held_object.forceMove(drop_target.drop_location())
+ finish_manipulation()
+ return TRUE
+
+ actual_held_object?.forceMove(drop_endpoint)
+ finish_manipulation()
+ return TRUE
+
+/obj/machinery/big_manipulator/proc/try_use_thing(datum/manipulator_task/cargo/interact/destination_task, work_done_at_point = FALSE)
+ if(stopping)
+ return
+
+ var/obj/obj_resolve = held_object?.resolve()
+ var/mob/living/carbon/human/species/monkey/monkey_resolve = monkey_worker?.resolve()
+ var/destination_turf = destination_task.interaction_turf
+
+ if(!obj_resolve || QDELETED(obj_resolve) || obj_resolve.loc != src)
+ drop_held_atom()
+ return FALSE
+
+ if(!monkey_resolve || !destination_turf)
+ drop_held_atom()
+ return FALSE
+
+ if(monkey_resolve.loc != src)
+ drop_held_atom()
+ return FALSE
+
+ var/obj/item/held_item = obj_resolve
+ var/atom/type_to_use = destination_task.find_type_priority()
+
+ if(isnull(type_to_use))
+ drop_held_atom()
+ return FALSE
+
+ if(isitem(type_to_use) && !destination_task.check_filters_for_atom(type_to_use))
+ drop_held_atom()
+ return FALSE
+
+ var/original_loc = held_item.loc
+
+ monkey_resolve.put_in_active_hand(held_item)
+ if(held_item.GetComponent(/datum/component/two_handed))
+ held_item.attack_self(monkey_resolve)
+
+ if(destination_task.worker_combat_mode)
+ monkey_resolve.istate |= ISTATE_HARM
+ else
+ monkey_resolve.istate &= ~ISTATE_HARM
+
+ if(destination_task.worker_use_rmb)
+ monkey_resolve.istate |= ISTATE_SECONDARY
+ else
+ monkey_resolve.istate &= ~ISTATE_SECONDARY
+
+ held_item.melee_attack_chain(monkey_resolve, type_to_use, list(RIGHT_CLICK = destination_task.worker_use_rmb ? TRUE : FALSE))
+ monkey_resolve.istate &= ~(ISTATE_HARM | ISTATE_SECONDARY)
+ do_attack_animation(destination_turf)
+ manipulator_arm.do_attack_animation(destination_turf)
+
+ if(QDELETED(held_item) || !held_item || (held_item.loc != monkey_resolve && held_item.loc != original_loc))
+ held_object = null
+ manipulator_arm.update_claw(null)
+ finish_manipulation()
+ return TRUE
+
+ if(held_item.loc == monkey_resolve)
+ held_item.forceMove(original_loc)
+
+ check_for_cycle_end_drop(destination_task, TRUE, TRUE)
+
+/obj/machinery/big_manipulator/proc/check_for_cycle_end_drop(datum/manipulator_task/cargo/interact/destination_task, item_used_this_iteration, work_done_at_point = FALSE)
+ var/obj/obj_resolve = held_object?.resolve()
+
+ if(!obj_resolve || QDELETED(obj_resolve))
+ finish_manipulation()
+ return
+
+ if(obj_resolve.loc != src)
+ obj_resolve.forceMove(src)
+
+ if(destination_task.worker_interaction == WORKER_SINGLE_USE && item_used_this_iteration)
+ current_task = null
+ schedule_next_cycle()
+ return
+
+ if(!on)
+ finish_manipulation()
+ return
+
+ if(item_used_this_iteration)
+ addtimer(CALLBACK(src, PROC_REF(try_use_thing), destination_task, TRUE), BASE_INTERACTION_TIME * 2)
+ return
+
+ drop_held_after_use(destination_task)
+
+/obj/machinery/big_manipulator/proc/drop_held_after_use(datum/manipulator_task/cargo/interact/destination_task)
+ var/obj/obj_resolve = held_object?.resolve()
+ var/turf/drop_turf = destination_task.interaction_turf
+
+ switch(destination_task.use_post_interaction)
+ if(POST_INTERACTION_DROP_AT_POINT)
+ obj_resolve.forceMove(drop_turf)
+ obj_resolve.dir = get_dir(get_turf(obj_resolve), get_turf(src))
+ finish_manipulation()
+
+ if(POST_INTERACTION_DROP_AT_MACHINE)
+ obj_resolve.forceMove(get_turf(src))
+ finish_manipulation()
+
+ if(POST_INTERACTION_DROP_NEXT_FITTING)
+ var/datum/manipulator_task/next = master_tasking.get_next_task(tasks, src)
+ if(istype(next, /datum/manipulator_task/cargo/dropoff_base))
+ rotate_to_point(next, next, TYPE_PROC_REF(/datum/manipulator_task/cargo/dropoff_base, try_dropoff))
+ return
+ obj_resolve.forceMove(drop_turf)
+ obj_resolve.dir = get_dir(get_turf(obj_resolve), get_turf(src))
+ finish_manipulation()
+ else
+ schedule_next_cycle()
+
+/obj/machinery/big_manipulator/proc/throw_thing(datum/manipulator_task/cargo/dropoff_base/throw/throw_task)
+ var/drop_turf = throw_task.interaction_turf
+ var/atom/movable/held_atom = held_object?.resolve()
+
+ held_atom.forceMove(drop_turf)
+ do_attack_animation(drop_turf)
+ manipulator_arm.do_attack_animation(drop_turf)
+
+ if(isliving(held_atom) && !(obj_flags & EMAGGED))
+ held_atom.dir = get_dir(get_turf(held_atom), get_turf(src))
+ finish_manipulation()
+ return
+
+ held_atom.throw_at(get_edge_target_turf(get_turf(src), get_dir(get_turf(src), get_turf(held_atom))), throw_task.throw_range, 2)
+ finish_manipulation()
+
+/obj/machinery/big_manipulator/proc/use_thing_with_empty_hand(datum/manipulator_task/cargo/interact/destination_task)
+ var/mob/living/carbon/human/species/monkey/monkey_resolve = monkey_worker?.resolve()
+ if(isnull(monkey_resolve))
+ finish_manipulation()
+ return
+
+ if(monkey_resolve.loc != src)
+ finish_manipulation()
+ return
+
+ var/atom/type_to_use = destination_task.find_type_priority()
+ if(isnull(type_to_use))
+ check_end_of_use_for_use_with_empty_hand(destination_task, FALSE)
+ return
+
+ if(isitem(type_to_use))
+ var/obj/item/interact_with_item = type_to_use
+ var/resolve_loc = interact_with_item.loc
+ monkey_resolve.put_in_active_hand(interact_with_item)
+ interact_with_item.attack_self(monkey_resolve)
+ interact_with_item.forceMove(resolve_loc)
+ else
+ if(destination_task.worker_combat_mode)
+ monkey_resolve.istate |= ISTATE_HARM
+ else
+ monkey_resolve.istate &= ~ISTATE_HARM
+ monkey_resolve.UnarmedAttack(type_to_use)
+ monkey_resolve.istate &= ~ISTATE_HARM
+
+ var/turf/dest_turf = destination_task.interaction_turf
+ if(dest_turf)
+ do_attack_animation(dest_turf)
+ manipulator_arm.do_attack_animation(dest_turf)
+
+ check_end_of_use_for_use_with_empty_hand(destination_task, TRUE)
+
+/obj/machinery/big_manipulator/proc/check_end_of_use_for_use_with_empty_hand(datum/manipulator_task/cargo/interact/destination_task, item_was_used = TRUE)
+ if(!on || destination_task.worker_interaction != WORKER_EMPTY_USE)
+ finish_manipulation()
+ return
+
+ if(!item_was_used)
+ finish_manipulation()
+ return
+
+ addtimer(CALLBACK(src, PROC_REF(use_thing_with_empty_hand), destination_task), BASE_INTERACTION_TIME)
+
+/// Completes the current manipulation action and schedules the next step.
+/obj/machinery/big_manipulator/proc/finish_manipulation()
+ held_object = null
+ manipulator_arm.update_claw(null)
+ current_task = null
+
+ SStgui.update_uis(src)
+
+ if(stopping)
+ complete_stopping_task()
+ return
+
+ schedule_next_cycle()
+
+/// Completes the stopping task and transitions to idle
+/obj/machinery/big_manipulator/proc/complete_stopping_task()
+ on = FALSE
+ stopping = FALSE
+ next_cycle_scheduled = FALSE
+ current_task = null
+ unregister_task_turf_signals()
+ waiting_for_signal = FALSE
+ SStgui.update_uis(src)
+
+/// Registers enter/exit signals on all unique cargo task turfs.
+/obj/machinery/big_manipulator/proc/register_task_turf_signals()
+ unregister_task_turf_signals()
+ for(var/datum/manipulator_task/cargo/task in tasks)
+ if(!task.interaction_turf || (task.interaction_turf in signal_turfs))
+ continue
+ signal_turfs += task.interaction_turf
+ RegisterSignals(task.interaction_turf, list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_EXITED), PROC_REF(on_task_turf_changed))
+
+/// Unregisters all previously registered turf signals.
+/obj/machinery/big_manipulator/proc/unregister_task_turf_signals()
+ for(var/turf/t in signal_turfs)
+ UnregisterSignal(t, list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_EXITED))
+ signal_turfs = list()
+
+/// Fires when something enters or leaves a watched task turf.
+/obj/machinery/big_manipulator/proc/on_task_turf_changed(datum/source)
+ SIGNAL_HANDLER
+ if(!on || stopping || !waiting_for_signal)
+ return
+ something_happened()
+
+/// Drop the held atom and anything the monkey is holding.
+/obj/machinery/big_manipulator/proc/drop_held_atom()
+ // Drop the manipulator's held object
+ var/obj/obj_resolve = held_object?.resolve()
+ if(obj_resolve)
+ obj_resolve.forceMove(drop_location())
+
+ // Also drop whatever the monkey is holding
+ var/mob/living/carbon/human/species/monkey/monkey_resolve = monkey_worker?.resolve()
+ if(monkey_resolve)
+ monkey_resolve.drop_all_held_items()
+
+ finish_manipulation()
diff --git a/code/game/machinery/big_manipulator/big_manipulator_items.dm b/code/game/machinery/big_manipulator/big_manipulator_items.dm
new file mode 100644
index 0000000000000..07c197e983172
--- /dev/null
+++ b/code/game/machinery/big_manipulator/big_manipulator_items.dm
@@ -0,0 +1,28 @@
+/obj/item/circuitboard/machine/big_manipulator
+ name = "Big Manipulator"
+ greyscale_colors = CIRCUIT_COLOR_ENGINEERING
+ build_path = /obj/machinery/big_manipulator
+ req_components = list(
+ /datum/stock_part/manipulator = 1,
+ )
+
+/obj/item/disk/manipulator
+ name = "manipulator task disk"
+ desc = "A floppy disk containing manipulator tasks."
+ icon = 'icons/obj/module.dmi'
+ icon_state = "datadisk1"
+ var/list/tasks_data = list()
+ var/read_only = FALSE
+
+/obj/item/disk/manipulator/proc/set_tasks(list/new_tasks_data)
+ if(read_only)
+ return FALSE
+ tasks_data = islist(new_tasks_data) ? new_tasks_data : list()
+ return TRUE
+
+/obj/item/disk/manipulator/proc/get_tasks()
+ return tasks_data?.Copy() || list()
+
+/obj/item/disk/manipulator/examine(mob/user)
+ . = ..()
+ . += span_notice("It has [length(tasks_data)] task data chunk\s stored.")
diff --git a/code/game/machinery/big_manipulator/interaction_priorities.dm b/code/game/machinery/big_manipulator/interaction_priorities.dm
new file mode 100644
index 0000000000000..0540c1f59275f
--- /dev/null
+++ b/code/game/machinery/big_manipulator/interaction_priorities.dm
@@ -0,0 +1,37 @@
+// Prioritizes the type of atom that the manipulator interact with. Interaction lists get built on the points themselves.
+
+/datum/manipulator_priority
+ /// The name of the priority for the UI display.
+ var/name
+ /// Which typepath does this priority handle.
+ var/atom_typepath
+ /// Is this priority active? If not, it will be ignored.
+ var/active = TRUE
+
+/datum/manipulator_priority/drop/on_floor
+ name = "DROP ON FLOOR"
+ atom_typepath = /turf
+
+/datum/manipulator_priority/drop/in_storage
+ name = "DROP IN STORAGE"
+ atom_typepath = /obj/item/storage
+
+/datum/manipulator_priority/interact/with_living
+ name = "USE ON LIVING"
+ atom_typepath = /mob/living
+
+/datum/manipulator_priority/interact/with_structure
+ name = "USE ON STRUCTURE"
+ atom_typepath = /obj/structure
+
+/datum/manipulator_priority/interact/with_machinery
+ name = "USE ON MACHINERY"
+ atom_typepath = /obj/machinery
+
+/datum/manipulator_priority/interact/with_items
+ name = "USE ON ITEM"
+ atom_typepath = /obj/item
+
+/datum/manipulator_priority/interact/with_vehicles
+ name = "USE ON VEHICLES"
+ atom_typepath = /obj/vehicle
diff --git a/code/game/machinery/big_manipulator/manipulator_arm.dm b/code/game/machinery/big_manipulator/manipulator_arm.dm
new file mode 100644
index 0000000000000..8cafd6568b0c1
--- /dev/null
+++ b/code/game/machinery/big_manipulator/manipulator_arm.dm
@@ -0,0 +1,53 @@
+/// Manipulator hand. Effect we animate to show that the manipulator is working and moving something.
+/obj/effect/big_manipulator_arm
+ name = "mechanical claw"
+ desc = "Takes and drops objects."
+ icon = 'icons/obj/machines/big_manipulator_parts/big_manipulator_hand.dmi'
+ icon_state = "hand"
+ layer = LOW_ITEM_LAYER
+ appearance_flags = KEEP_TOGETHER | LONG_GLIDE | TILE_BOUND | PIXEL_SCALE
+ anchored = TRUE
+ greyscale_config = /datum/greyscale_config/manipulator_arm
+ pixel_x = -32
+ pixel_y = -32
+ /// The actual target direction (may be diagonal) used for offset calculations.
+ var/target_dir = SOUTH
+ /// We get item from big manipulator and takes its icon to create overlay.
+ var/datum/weakref/item_in_my_claw
+ /// Var to icon that used as overlay on manipulator claw to show what item it grabs.
+ var/mutable_appearance/icon_overlay
+
+
+/obj/effect/big_manipulator_arm/update_overlays()
+ . = ..()
+ . += update_item_overlay()
+
+/obj/effect/big_manipulator_arm/proc/update_item_overlay()
+ if(isnull(item_in_my_claw))
+ return icon_overlay = null
+ var/atom/movable/item_data = item_in_my_claw.resolve()
+ icon_overlay = mutable_appearance(item_data.icon, item_data.icon_state, item_data.layer, src, item_data.plane, item_data.alpha, item_data.appearance_flags)
+ icon_overlay.color = item_data.color
+ icon_overlay.appearance = item_data.appearance
+ icon_overlay.pixel_w = 32 + calculate_item_offset(is_x = TRUE)
+ icon_overlay.pixel_z = 32 + calculate_item_offset(is_x = FALSE)
+ return icon_overlay
+
+/// Updates item that is in the claw.
+/obj/effect/big_manipulator_arm/proc/update_claw(clawed_item)
+ item_in_my_claw = clawed_item
+ update_appearance()
+
+/// Calculate x and y coordinates so that the item icon appears in the claw and not somewhere in the corner.
+/obj/effect/big_manipulator_arm/proc/calculate_item_offset(is_x = TRUE, pixels_to_offset = 32)
+ var/offset
+ switch(dir)
+ if(NORTH)
+ offset = is_x ? 0 : pixels_to_offset
+ if(SOUTH)
+ offset = is_x ? 0 : -pixels_to_offset
+ if(EAST)
+ offset = is_x ? pixels_to_offset : 0
+ if(WEST)
+ offset = is_x ? -pixels_to_offset : 0
+ return offset
diff --git a/code/game/machinery/big_manipulator/manipulator_tasks.dm b/code/game/machinery/big_manipulator/manipulator_tasks.dm
new file mode 100644
index 0000000000000..6935ffe8f0cc6
--- /dev/null
+++ b/code/game/machinery/big_manipulator/manipulator_tasks.dm
@@ -0,0 +1,503 @@
+/datum/manipulator_task
+ var/name = "task"
+
+/datum/manipulator_task/proc/can_run(obj/machinery/big_manipulator/manipulator)
+ return FALSE
+
+/datum/manipulator_task/proc/run_task(obj/machinery/big_manipulator/manipulator)
+ return
+
+/datum/manipulator_task/proc/serialize()
+ return list("type" = type)
+
+/datum/manipulator_task/New(...)
+ ..()
+ return
+
+// ===== WAIT =====
+
+/datum/manipulator_task/simple/wait
+ name = "wait"
+ var/time_seconds = 1
+
+/datum/manipulator_task/simple/wait/can_run(obj/machinery/big_manipulator/manipulator)
+ for(var/datum/manipulator_task/cargo/task in manipulator.tasks)
+ if(task.can_run(manipulator))
+ return TRUE
+ return FALSE
+
+/datum/manipulator_task/simple/wait/run_task(obj/machinery/big_manipulator/manipulator)
+ manipulator.schedule_next_cycle(time_seconds SECONDS)
+
+/datum/manipulator_task/simple/wait/serialize()
+ var/list/data = ..()
+ data["time_seconds"] = time_seconds
+ return data
+
+/datum/manipulator_task/simple/wait/New(..., serialized_data)
+ ..()
+ if(serialized_data)
+ time_seconds = serialized_data["time_seconds"]
+ return
+
+// ===== BASE CARGO =====
+
+/datum/manipulator_task/cargo
+ var/turf/interaction_turf
+ var/offset_dx
+ var/offset_dy
+ var/should_use_filters = FALSE
+ var/list/atom_filters = list()
+ var/filtering_mode = TAKE_ITEMS
+ var/list/type_filters = list(
+ /obj/item,
+ /obj/structure/closet,
+ )
+ var/list/interaction_priorities = list()
+
+/datum/manipulator_task/cargo/New(turf/new_turf, manipulator_tier, serialized_data)
+ if(serialized_data)
+ var/list/offset = serialized_data["offset"]
+ if(islist(offset))
+ offset_dx = offset["dx"]
+ offset_dy = offset["dy"]
+ if(new_turf)
+ interaction_turf = new_turf
+
+ should_use_filters = !!serialized_data["should_use_filters"]
+ atom_filters = serialized_data["atom_filters"] || list()
+ filtering_mode = serialized_data["filtering_mode"]
+ type_filters = serialized_data["type_filters"] || list()
+
+ var/list/prios_data = serialized_data["interaction_priorities"]
+ if(islist(prios_data))
+ interaction_priorities = list()
+ for(var/list/prio_data as anything in prios_data)
+ if(!islist(prio_data))
+ continue
+ var/prio_type = prio_data["type"]
+ if(!ispath(prio_type, /datum/manipulator_priority))
+ continue
+ var/datum/manipulator_priority/prio = new prio_type
+ prio.active = !!prio_data["active"]
+ interaction_priorities += prio
+
+ return ..()
+
+ if(!new_turf)
+ stack_trace("New manipulator task created with no valid turf reference passed.")
+ qdel(src)
+ return
+
+ if(isclosedturf(new_turf))
+ qdel(src)
+ return
+
+ interaction_turf = new_turf
+ interaction_priorities = fill_priority_list(manipulator_tier)
+ return ..()
+
+/datum/manipulator_task/cargo/proc/fill_priority_list(manipulator_tier)
+ return list()
+
+/datum/manipulator_task/cargo/proc/find_type_priority()
+ var/atom/movable/best_candidate = null
+ var/best_priority_index = INFINITY
+
+ if(!interaction_turf)
+ return null
+ for(var/atom/movable/thing as anything in interaction_turf.contents)
+ for(var/i in 1 to length(interaction_priorities))
+ if(i >= best_priority_index)
+ break
+
+ var/datum/manipulator_priority/prio = interaction_priorities[i]
+
+ if(!prio.active || ispath(prio, /turf))
+ continue
+
+ if(!istype(thing, prio.atom_typepath))
+ continue
+
+ if(isliving(thing))
+ var/mob/living/living_mob = thing
+ if(living_mob.stat == DEAD)
+ continue
+
+ best_candidate = thing
+ best_priority_index = i
+
+ if(best_priority_index == 1)
+ return best_candidate
+ break
+
+ for(var/i in 1 to length(interaction_priorities))
+ if(i >= best_priority_index)
+ break
+ var/datum/manipulator_priority/prio = interaction_priorities[i]
+ if(prio.active && prio.atom_typepath == /turf)
+ best_candidate = interaction_turf
+ best_priority_index = i
+ break
+
+ return best_candidate
+
+/datum/manipulator_task/cargo/proc/move_priority_up_by_index(index)
+ if(!index)
+ return FALSE
+ interaction_priorities.Swap(index, index + 1)
+ return TRUE
+
+/datum/manipulator_task/cargo/proc/tick_priority_by_index(index, reset = FALSE)
+ var/datum/manipulator_priority/target_priority = interaction_priorities[index + 1]
+ if(reset)
+ target_priority.active = TRUE
+ else
+ target_priority.active = !target_priority.active
+ return TRUE
+
+/datum/manipulator_task/cargo/proc/is_valid()
+ if(!interaction_turf)
+ return FALSE
+ return !isclosedturf(interaction_turf)
+
+/datum/manipulator_task/cargo/proc/check_filters_for_atom(atom/movable/target)
+ if(!target || target.anchored || HAS_TRAIT(target, TRAIT_NODROP))
+ return FALSE
+
+ switch(filtering_mode)
+ if(TAKE_CLOSETS)
+ return istype(target, /obj/structure/closet)
+ if(TAKE_HUMANS)
+ return ishuman(target)
+ if(TAKE_ITEMS)
+ if(!should_use_filters)
+ return isitem(target)
+ for(var/filter_path in atom_filters)
+ if(istype(target, filter_path))
+ return TRUE
+ return FALSE
+
+ return FALSE
+
+/datum/manipulator_task/cargo/can_run(obj/machinery/big_manipulator/manipulator)
+ return is_valid()
+
+/datum/manipulator_task/cargo/serialize()
+ var/list/data = ..()
+ data["offset"] = list(
+ "dx" = offset_dx,
+ "dy" = offset_dy,
+ )
+ data["should_use_filters"] = should_use_filters
+ data["atom_filters"] = atom_filters
+ data["filtering_mode"] = filtering_mode
+ data["type_filters"] = type_filters
+ data["interaction_priorities"] = list()
+ for(var/datum/manipulator_priority/prio as anything in interaction_priorities)
+ data["interaction_priorities"] += list(list(
+ "type" = prio.type,
+ "active" = prio.active,
+ ))
+ return data
+
+
+/datum/manipulator_task/cargo/Destroy()
+ interaction_turf = null
+ QDEL_LIST(interaction_priorities)
+ return ..()
+
+// ===== PICKUP =====
+
+/datum/manipulator_task/cargo/pickup
+ name = "pickup"
+ var/pickup_eagerness = PICKUP_CAN_WAIT
+
+/datum/manipulator_task/cargo/pickup/fill_priority_list(manipulator_tier)
+ return list()
+
+/datum/manipulator_task/cargo/pickup/can_run(obj/machinery/big_manipulator/manipulator)
+ if(!..())
+ return FALSE
+ if(manipulator.held_object)
+ return FALSE
+ for(var/atom/movable/candidate as anything in interaction_turf.contents)
+ if(!check_filters_for_atom(candidate))
+ continue
+ if(pickup_eagerness == PICKUP_EAGER)
+ return TRUE
+ for(var/datum/manipulator_task/cargo/dropoff_base/dest in manipulator.tasks)
+ if(dest.can_accept(candidate))
+ return TRUE
+ return FALSE
+
+/datum/manipulator_task/cargo/pickup/run_task(obj/machinery/big_manipulator/manipulator)
+ manipulator.rotate_to_point(src, src, PROC_REF(try_pickup))
+
+/datum/manipulator_task/cargo/pickup/proc/try_pickup(obj/machinery/big_manipulator/manipulator)
+ var/atom/movable/selected = find_pickup_candidate(manipulator)
+ if(!selected)
+ manipulator.nothing_ever_happens()
+ return
+
+ if(selected.anchored || HAS_TRAIT(selected, TRAIT_NODROP))
+ manipulator.nothing_ever_happens()
+ return
+
+ if(isitem(selected))
+ var/obj/item/selected_item = selected
+ if(selected_item.item_flags & (ABSTRACT|DROPDEL))
+ manipulator.nothing_ever_happens()
+ return
+
+ selected.forceMove(manipulator)
+ manipulator.held_object = WEAKREF(selected)
+ manipulator.manipulator_arm.update_claw(manipulator.held_object)
+ manipulator.schedule_next_cycle()
+
+/datum/manipulator_task/cargo/pickup/serialize()
+ var/list/data = ..()
+ data["pickup_eagerness"] = pickup_eagerness
+ return data
+
+/datum/manipulator_task/cargo/pickup/New(turf/new_turf, manipulator_tier, serialized_data)
+ ..(new_turf, manipulator_tier, serialized_data)
+ if(serialized_data)
+ pickup_eagerness = serialized_data["pickup_eagerness"]
+ return
+
+/datum/manipulator_task/cargo/pickup/proc/find_pickup_candidate(obj/machinery/big_manipulator/manipulator)
+ var/list/candidates = list()
+
+ for(var/atom/movable/candidate as anything in interaction_turf.contents)
+ if(candidate.anchored || HAS_TRAIT(candidate, TRAIT_NODROP))
+ continue
+ if(!check_filters_for_atom(candidate))
+ continue
+ if(pickup_eagerness == PICKUP_EAGER)
+ candidates += candidate
+ continue
+ for(var/datum/manipulator_task/cargo/dropoff_base/dest in manipulator.tasks)
+ if(dest.can_accept(candidate))
+ candidates += candidate
+ break
+
+ if(!length(candidates))
+ return null
+
+ return manipulator.master_tasking.get_next_candidate(candidates)
+
+// ===== BASE DROPOFF =====
+// Base type for anything that accepts a `held_object`: drop, throw, use.
+// Pickup iterates by this type to find a target point.
+
+/datum/manipulator_task/cargo/dropoff_base
+ name = "dropoff"
+
+/datum/manipulator_task/cargo/dropoff_base/proc/can_accept(atom/movable/target)
+ if(!is_valid())
+ return FALSE
+ if(should_use_filters && !check_filters_for_atom(target))
+ return FALSE
+ return TRUE
+
+/datum/manipulator_task/cargo/dropoff_base/can_run(obj/machinery/big_manipulator/manipulator)
+ if(!..())
+ return FALSE
+ var/atom/movable/target = manipulator.held_object?.resolve()
+ if(!target)
+ return FALSE
+ return can_accept(target)
+
+/datum/manipulator_task/cargo/dropoff_base/run_task(obj/machinery/big_manipulator/manipulator)
+ manipulator.rotate_to_point(src, src, PROC_REF(try_dropoff))
+
+/datum/manipulator_task/cargo/dropoff_base/proc/try_dropoff(obj/machinery/big_manipulator/manipulator)
+ var/obj/actual_held_object = manipulator.held_object?.resolve()
+ if(!actual_held_object || actual_held_object.loc != manipulator)
+ manipulator.nothing_ever_happens()
+ return FALSE
+ do_dropoff(manipulator)
+ return TRUE
+
+/datum/manipulator_task/cargo/dropoff_base/serialize()
+ var/list/data = ..()
+ return data
+
+
+/datum/manipulator_task/cargo/dropoff_base/proc/do_dropoff(obj/machinery/big_manipulator/manipulator)
+ return
+
+// ===== DROP =====
+
+/datum/manipulator_task/cargo/dropoff_base/drop
+ name = "drop"
+ var/overflow_status = POINT_OVERFLOW_ALLOWED
+
+/datum/manipulator_task/cargo/dropoff_base/drop/fill_priority_list(manipulator_tier)
+ return list(
+ new /datum/manipulator_priority/drop/in_storage,
+ new /datum/manipulator_priority/drop/on_floor
+ )
+
+/datum/manipulator_task/cargo/dropoff_base/drop/can_accept(atom/movable/target)
+ if(!..())
+ return FALSE
+
+ var/list/atoms_on_the_turf = interaction_turf.contents
+ switch(overflow_status)
+ if(POINT_OVERFLOW_ALLOWED)
+ return TRUE
+ if(POINT_OVERFLOW_FILTERS)
+ for(var/atom/movable/movable_atom as anything in atoms_on_the_turf)
+ if(check_filters_for_atom(movable_atom))
+ return FALSE
+ if(POINT_OVERFLOW_HELD)
+ for(var/atom/movable/movable_atom as anything in atoms_on_the_turf)
+ if(istype(movable_atom, target?.type))
+ return FALSE
+ if(POINT_OVERFLOW_FORBIDDEN)
+ if(locate(/obj/item) in atoms_on_the_turf)
+ return FALSE
+
+ return TRUE
+
+/datum/manipulator_task/cargo/dropoff_base/drop/serialize()
+ var/list/data = ..()
+ data["overflow_status"] = overflow_status
+ return data
+
+/datum/manipulator_task/cargo/dropoff_base/drop/New(turf/new_turf, manipulator_tier, serialized_data)
+ ..(new_turf, manipulator_tier, serialized_data)
+ if(serialized_data)
+ overflow_status = serialized_data["overflow_status"]
+ return
+
+/datum/manipulator_task/cargo/dropoff_base/drop/do_dropoff(obj/machinery/big_manipulator/manipulator)
+ manipulator.try_drop_thing(src)
+
+// ===== THROW =====
+
+/datum/manipulator_task/cargo/dropoff_base/throw
+ name = "throw"
+ var/throw_range = 1
+
+/datum/manipulator_task/cargo/dropoff_base/throw/can_accept(atom/movable/target)
+ if(!is_valid())
+ return FALSE
+ if(should_use_filters && !check_filters_for_atom(target))
+ return FALSE
+ return TRUE
+
+/datum/manipulator_task/cargo/dropoff_base/throw/serialize()
+ var/list/data = ..()
+ data["throw_range"] = throw_range
+ return data
+
+/datum/manipulator_task/cargo/dropoff_base/throw/New(turf/new_turf, manipulator_tier, serialized_data)
+ ..(new_turf, manipulator_tier, serialized_data)
+ if(serialized_data)
+ throw_range = serialized_data["throw_range"]
+ return
+
+/datum/manipulator_task/cargo/dropoff_base/throw/do_dropoff(obj/machinery/big_manipulator/manipulator)
+ manipulator.throw_thing(src)
+
+// ===== USE =====
+
+/datum/manipulator_task/cargo/dropoff_base/use
+ name = "use"
+ var/worker_interaction = WORKER_NORMAL_USE
+ var/use_post_interaction = POST_INTERACTION_DROP_AT_POINT
+ var/worker_combat_mode = FALSE
+ var/worker_use_rmb = FALSE
+
+/datum/manipulator_task/cargo/dropoff_base/use/fill_priority_list(manipulator_tier)
+ var/list/priorities = list(
+ new /datum/manipulator_priority/interact/with_living,
+ new /datum/manipulator_priority/interact/with_structure,
+ new /datum/manipulator_priority/interact/with_machinery,
+ new /datum/manipulator_priority/interact/with_items,
+ )
+ if(manipulator_tier == 4)
+ priorities += new /datum/manipulator_priority/interact/with_vehicles
+ return priorities
+
+/datum/manipulator_task/cargo/dropoff_base/use/can_accept(atom/movable/target)
+ if(!is_valid())
+ return FALSE
+ if(should_use_filters && !check_filters_for_atom(target))
+ return FALSE
+ return TRUE
+
+/datum/manipulator_task/cargo/dropoff_base/use/serialize()
+ var/list/data = ..()
+ data["worker_interaction"] = worker_interaction
+ data["use_post_interaction"] = use_post_interaction
+ data["worker_combat_mode"] = worker_combat_mode
+ data["worker_use_rmb"] = worker_use_rmb
+ return data
+
+/datum/manipulator_task/cargo/dropoff_base/use/New(turf/new_turf, manipulator_tier, serialized_data)
+ ..(new_turf, manipulator_tier, serialized_data)
+ if(serialized_data)
+ worker_interaction = serialized_data["worker_interaction"]
+ use_post_interaction = serialized_data["use_post_interaction"]
+ worker_combat_mode = !!serialized_data["worker_combat_mode"]
+ worker_use_rmb = !!serialized_data["worker_use_rmb"]
+ return
+
+/datum/manipulator_task/cargo/dropoff_base/use/do_dropoff(obj/machinery/big_manipulator/manipulator)
+ manipulator.try_use_thing(src)
+
+// ===== INTERACT (empty hand) =====
+
+/datum/manipulator_task/cargo/interact
+ name = "interact"
+ var/worker_interaction = WORKER_EMPTY_USE
+ var/use_post_interaction = POST_INTERACTION_DROP_AT_POINT
+ var/worker_combat_mode = FALSE
+ var/worker_use_rmb = FALSE
+
+/datum/manipulator_task/cargo/interact/fill_priority_list(manipulator_tier)
+ var/list/priorities = list(
+ new /datum/manipulator_priority/interact/with_living,
+ new /datum/manipulator_priority/interact/with_structure,
+ new /datum/manipulator_priority/interact/with_machinery,
+ new /datum/manipulator_priority/interact/with_items,
+ )
+ if(manipulator_tier == 4)
+ priorities += new /datum/manipulator_priority/interact/with_vehicles
+ return priorities
+
+/datum/manipulator_task/cargo/interact/can_run(obj/machinery/big_manipulator/manipulator)
+ if(!..())
+ return FALSE
+ return find_type_priority() != null
+
+/datum/manipulator_task/cargo/interact/run_task(obj/machinery/big_manipulator/manipulator)
+ manipulator.rotate_to_point(src, src, PROC_REF(try_interact))
+
+/datum/manipulator_task/cargo/interact/serialize()
+ var/list/data = ..()
+ data["worker_interaction"] = worker_interaction
+ data["use_post_interaction"] = use_post_interaction
+ data["worker_combat_mode"] = worker_combat_mode
+ data["worker_use_rmb"] = worker_use_rmb
+ return data
+
+/datum/manipulator_task/cargo/interact/New(turf/new_turf, manipulator_tier, serialized_data)
+ ..(new_turf, manipulator_tier, serialized_data)
+ if(serialized_data)
+ worker_interaction = serialized_data["worker_interaction"]
+ use_post_interaction = serialized_data["use_post_interaction"]
+ worker_combat_mode = !!serialized_data["worker_combat_mode"]
+ worker_use_rmb = !!serialized_data["worker_use_rmb"]
+ return
+
+/datum/manipulator_task/cargo/interact/proc/try_interact(obj/machinery/big_manipulator/manipulator)
+ var/atom/movable/held = manipulator.held_object?.resolve()
+ if(held)
+ manipulator.try_use_thing(src)
+ else
+ manipulator.use_thing_with_empty_hand(src)
diff --git a/code/game/machinery/big_manipulator/tasking.dm b/code/game/machinery/big_manipulator/tasking.dm
new file mode 100644
index 0000000000000..cc91434363e58
--- /dev/null
+++ b/code/game/machinery/big_manipulator/tasking.dm
@@ -0,0 +1,69 @@
+/datum/tasking_strategy
+ var/current_index = 1
+
+/// Returns the next task to run, or null if nothing is available.
+/datum/tasking_strategy/proc/get_next_task(list/tasks)
+ return null
+
+/// Picks a next candidate from a list of eligible atoms.
+/datum/tasking_strategy/proc/get_next_candidate(list/candidates)
+ if(!length(candidates))
+ return null
+ return candidates[1]
+
+// Moves through the list, skipping tasks that can't run.
+/datum/tasking_strategy/sequential
+
+/datum/tasking_strategy/sequential/get_next_task(list/tasks, obj/machinery/big_manipulator/manipulator)
+ if(!length(tasks))
+ return null
+ if(current_index < 1 || current_index > length(tasks))
+ current_index = 1
+ var/start = current_index
+ while(TRUE)
+ var/datum/manipulator_task/task = tasks[current_index]
+ current_index++
+ if(current_index > length(tasks))
+ current_index = 1
+ if(task.can_run(manipulator))
+ return task
+ if(current_index == start)
+ return null
+
+/datum/tasking_strategy/sequential/get_next_candidate(list/candidates)
+ if(!length(candidates))
+ return null
+ if(current_index < 1 || current_index > length(candidates))
+ current_index = 1
+ var/candidate = candidates[current_index]
+ current_index++
+ if(current_index > length(candidates))
+ current_index = 1
+ return candidate
+
+// Stays on the current task until it can run.
+/datum/tasking_strategy/strict
+
+/datum/tasking_strategy/strict/get_next_task(list/tasks, obj/machinery/big_manipulator/manipulator)
+ if(!length(tasks))
+ return null
+ if(current_index < 1 || current_index > length(tasks))
+ current_index = 1
+ var/datum/manipulator_task/task = tasks[current_index]
+ if(!task.can_run(manipulator))
+ return null
+ current_index++
+ if(current_index > length(tasks))
+ current_index = 1
+ return task
+
+/datum/tasking_strategy/strict/get_next_candidate(list/candidates)
+ if(!length(candidates))
+ return null
+ if(current_index < 1 || current_index > length(candidates))
+ current_index = 1
+ var/candidate = candidates[current_index]
+ current_index++
+ if(current_index > length(candidates))
+ current_index = 1
+ return candidate
diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm
index 8d977fd18994b..5b5e4176c797a 100644
--- a/code/modules/research/designs/machine_designs.dm
+++ b/code/modules/research/designs/machine_designs.dm
@@ -1279,6 +1279,16 @@
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING
+/datum/design/board/big_manipulator
+ name = "Big Manipulator Board"
+ desc = "The circuit board for a big manipulator."
+ id = "big_manipulator"
+ build_path = /obj/item/circuitboard/machine/big_manipulator
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_SERVICE
+
/datum/design/board/flatpacker
name = "Flatpacker Machine Board"
desc = "The circuit board for a Flatpacker."
diff --git a/code/modules/research/designs/tool_designs.dm b/code/modules/research/designs/tool_designs.dm
index 1a9bfacc53e1d..ed47745bccb47 100644
--- a/code/modules/research/designs/tool_designs.dm
+++ b/code/modules/research/designs/tool_designs.dm
@@ -448,6 +448,18 @@
RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_JANITORIAL
)
+/datum/design/manipulator_task_disk
+ name = "Manipulator Task Disk"
+ desc = "A floppy disk for storing and loading manipulator tasks."
+ id = "manipulator_task_disk"
+ build_type = PROTOLATHE | AWAY_LATHE
+ materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT, /datum/material/glass = SHEET_MATERIAL_AMOUNT * 0.5)
+ build_path = /obj/item/disk/manipulator
+ category = list(
+ RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_CARGO
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO
+
/datum/design/bolter_wrench
name = "Bolter Wrench"
desc = "A wrench that can unbolt airlocks regardless of power status."
diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm
index c1cb06c032ee6..d35f623b0b6a8 100644
--- a/code/modules/research/techweb/all_nodes.dm
+++ b/code/modules/research/techweb/all_nodes.dm
@@ -16,11 +16,7 @@
"basic_scanning",
"blast",
"ignition",
- "big_manipulator",
"assembler",
- "manipulator_filter",
- "manipulator_filter_cargo",
- "manipulator_filter_internal",
"bodybag",
"bounced_radio",
"bowl",
diff --git a/code/modules/research/techweb/engi_nodes.dm b/code/modules/research/techweb/engi_nodes.dm
index ffa0d193248d3..ae7f44c3413d0 100644
--- a/code/modules/research/techweb/engi_nodes.dm
+++ b/code/modules/research/techweb/engi_nodes.dm
@@ -64,6 +64,8 @@
"tram_display",
"crossing_signal",
"guideway_sensor",
+ "big_manipulator",
+ "manipulator_task_disk",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_5_POINTS)
discount_experiments = list(/datum/experiment/scanning/random/material/easy = TECHWEB_TIER_3_POINTS)
diff --git a/monkestation/code/modules/factory_type_beat/icons/big_manipulator.dmi b/icons/obj/machines/big_manipulator.dmi
similarity index 100%
rename from monkestation/code/modules/factory_type_beat/icons/big_manipulator.dmi
rename to icons/obj/machines/big_manipulator.dmi
diff --git a/monkestation/code/modules/factory_type_beat/icons/big_manipulator_core.dmi b/icons/obj/machines/big_manipulator_parts/big_manipulator_core.dmi
similarity index 100%
rename from monkestation/code/modules/factory_type_beat/icons/big_manipulator_core.dmi
rename to icons/obj/machines/big_manipulator_parts/big_manipulator_core.dmi
diff --git a/icons/obj/machines/big_manipulator_parts/big_manipulator_hand.dmi b/icons/obj/machines/big_manipulator_parts/big_manipulator_hand.dmi
new file mode 100644
index 0000000000000..37dfd7d82f53c
Binary files /dev/null and b/icons/obj/machines/big_manipulator_parts/big_manipulator_hand.dmi differ
diff --git a/monkestation/code/modules/factory_type_beat/circuits.dm b/monkestation/code/modules/factory_type_beat/circuits.dm
index da657bcc424af..418c0915dd24b 100644
--- a/monkestation/code/modules/factory_type_beat/circuits.dm
+++ b/monkestation/code/modules/factory_type_beat/circuits.dm
@@ -28,14 +28,6 @@
/datum/stock_part/matter_bin = 2,
)
-/obj/item/circuitboard/machine/big_manipulator
- name = "Big Manipulator"
- greyscale_colors = CIRCUIT_COLOR_ENGINEERING
- build_path = /obj/machinery/big_manipulator
- req_components = list(
- /datum/stock_part/manipulator = 1,
- )
-
/obj/item/circuitboard/machine/assembler
name = "Assembler"
greyscale_colors = CIRCUIT_COLOR_ENGINEERING
diff --git a/monkestation/code/modules/factory_type_beat/designs.dm b/monkestation/code/modules/factory_type_beat/designs.dm
index 59652a61d3148..e562dad9cce5b 100644
--- a/monkestation/code/modules/factory_type_beat/designs.dm
+++ b/monkestation/code/modules/factory_type_beat/designs.dm
@@ -1,41 +1,5 @@
#define FABRICATOR_SUBCATEGORY_MATERIALS "/Materials"
-/datum/design/manipulator_filter
- name = "Manipulator Filter"
- desc = "This can be inserted into a manipulator to give it filters."
- id = "manipulator_filter"
- build_path = /obj/item/manipulator_filter
- build_type = AUTOLATHE | PROTOLATHE | AWAY_LATHE | COLONY_FABRICATOR
- category = list(
- RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_CARGO
- )
- materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT)
- departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_SERVICE
-
-/datum/design/manipulator_filter_cargo
- name = "Manipulator Filter (Department)"
- desc = "This can be inserted into a manipulator to give it filters."
- id = "manipulator_filter_cargo"
- build_path = /obj/item/manipulator_filter/cargo
- build_type = AUTOLATHE | PROTOLATHE | AWAY_LATHE | COLONY_FABRICATOR
- category = list(
- RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_CARGO
- )
- materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT)
- departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_SERVICE
-
-/datum/design/manipulator_filter_internal
- name = "Manipulator Filter (Internal)"
- desc = "This can be inserted into a manipulator to give it filters."
- id = "manipulator_filter_internal"
- build_path = /obj/item/manipulator_filter/internal_filter
- build_type = AUTOLATHE | PROTOLATHE | AWAY_LATHE | COLONY_FABRICATOR
- category = list(
- RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_CARGO
- )
- materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT)
- departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_SERVICE
-
/datum/design/board/bookbinder
name = "Book Binder"
desc = "The circuit board for a book binder"
@@ -56,17 +20,6 @@
)
departmental_flags = DEPARTMENT_BITFLAG_SERVICE
-/datum/design/board/big_manipulator
- name = "Big Manipulator Board"
- desc = "The circuit board for a big manipulator."
- id = "big_manipulator"
- build_type = AUTOLATHE | PROTOLATHE | AWAY_LATHE | COLONY_FABRICATOR | IMPRINTER
- build_path = /obj/item/circuitboard/machine/big_manipulator
- category = list(
- RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING
- )
- departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_SERVICE
-
/datum/design/board/assembler
name = "Assembler Board"
desc = "The circuit board for an assembler."
diff --git a/monkestation/code/modules/factory_type_beat/icons/big_manipulator_hand.dmi b/monkestation/code/modules/factory_type_beat/icons/big_manipulator_hand.dmi
deleted file mode 100644
index e165441e8052e..0000000000000
Binary files a/monkestation/code/modules/factory_type_beat/icons/big_manipulator_hand.dmi and /dev/null differ
diff --git a/monkestation/code/modules/factory_type_beat/icons/items.dmi b/monkestation/code/modules/factory_type_beat/icons/items.dmi
deleted file mode 100644
index 9084cf3f8d28e..0000000000000
Binary files a/monkestation/code/modules/factory_type_beat/icons/items.dmi and /dev/null differ
diff --git a/monkestation/code/modules/factory_type_beat/machinery/assembler.dm b/monkestation/code/modules/factory_type_beat/machinery/assembler.dm
index 0c87fc6550c24..d0b9a88f77ec0 100644
--- a/monkestation/code/modules/factory_type_beat/machinery/assembler.dm
+++ b/monkestation/code/modules/factory_type_beat/machinery/assembler.dm
@@ -544,3 +544,21 @@
recipe_icon.icon = initial(atom.icon)
recipe_icon.icon_state = initial(atom.icon_state)
+
+/turf/proc/can_drop_off(atom/movable/target)
+ if(isclosedturf(src))
+ return FALSE
+ for(var/obj/structure/listed in contents)
+ if(!listed.can_drop_off(target))
+ return FALSE
+ for(var/obj/machinery/listed in contents)
+ if(!listed.can_drop_off(target))
+ return FALSE
+
+ return TRUE
+
+/obj/structure/proc/can_drop_off(atom/movable/target)
+ return TRUE
+
+/obj/machinery/proc/can_drop_off(atom/movable/target)
+ return TRUE
diff --git a/monkestation/code/modules/factory_type_beat/machinery/grabber.dm b/monkestation/code/modules/factory_type_beat/machinery/grabber.dm
deleted file mode 100644
index 357dee64db320..0000000000000
--- a/monkestation/code/modules/factory_type_beat/machinery/grabber.dm
+++ /dev/null
@@ -1,507 +0,0 @@
-/// Manipulator Core. Main part of the mechanism that carries out the entire process.
-/obj/machinery/big_manipulator
- name = "Big Manipulator"
- desc = "Take and drop objects. Innovation..."
- icon = 'monkestation/code/modules/factory_type_beat/icons/big_manipulator_core.dmi'
- icon_state = "core"
- density = TRUE
- circuit = /obj/item/circuitboard/machine/big_manipulator
- greyscale_colors = "#d8ce13"
- greyscale_config = /datum/greyscale_config/big_manipulator
- /// How many time manipulator need to take and drop item.
- var/working_speed = 2 SECONDS
- /// Using high tier manipulators speeds up big manipulator and requires more energy.
- var/power_use_lvl = 0.2
- /// When manipulator already working with item inside he don't take any new items.
- var/on_work = FALSE
- /// Activate mechanism.
- var/on = FALSE
- /// Dir to get turf where we take items.
- var/take_here = NORTH
- /// Dir to get turf where we drop items.
- var/drop_here = SOUTH
- /// Turf where we take items.
- var/turf/take_turf
- /// Turf where we drop items.
- var/turf/drop_turf
- /// Obj inside manipulator.
- var/datum/weakref/containment_obj
- /// Other manipulator component.
- var/obj/effect/big_manipulator_hand/manipulator_hand
- ///are we hacked?
- var/hacked = FALSE
- ///our installed filter
- var/obj/item/manipulator_filter/filter
- ///our failed attempts
- var/failed_attempts = 0
- var/atom/movable/failed_item
-
-/obj/machinery/big_manipulator/Initialize(mapload)
- . = ..()
- take_turf = get_step(src, take_here)
- drop_turf = get_step(src, drop_here)
- create_manipulator_hand()
- RegisterSignal(manipulator_hand, COMSIG_QDELETING, PROC_REF(on_hand_qdel))
- manipulator_lvl()
-
-/obj/machinery/big_manipulator/Destroy(force)
- . = ..()
- failed_item = null
- if(filter)
- filter.forceMove(get_turf(src))
- filter = null
- qdel(manipulator_hand)
- if(isnull(containment_obj))
- return
- var/obj/obj_resolve = containment_obj?.resolve()
- if(isnull(obj_resolve))
- return
- obj_resolve.forceMove(get_turf(obj_resolve))
-
-
-/obj/machinery/big_manipulator/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
- . = ..()
- take_and_drop_turfs_check()
- if(isnull(get_turf(src)))
- qdel(manipulator_hand)
- return
- if(!manipulator_hand)
- create_manipulator_hand()
- manipulator_hand.forceMove(get_turf(src))
-
-/obj/machinery/big_manipulator/wrench_act(mob/living/user, obj/item/tool)
- . = ..()
- default_unfasten_wrench(user, tool, time = 1 SECONDS)
- return TRUE
-
-/obj/machinery/big_manipulator/wrench_act_secondary(mob/living/user, obj/item/tool)
- . = ..()
- if(on_work || on)
- to_chat(user, span_warning("[src] is activated!"))
- return
- rotate_big_hand()
- playsound(src, 'sound/items/deconstruct.ogg', 50, TRUE)
- return TRUE
-
-/obj/machinery/big_manipulator/can_be_unfasten_wrench(mob/user, silent)
- if(on_work || on)
- to_chat(user, span_warning("[src] is activated!"))
- return FAILED_UNFASTEN
- return ..()
-
-/obj/machinery/big_manipulator/default_unfasten_wrench(mob/user, obj/item/wrench, time)
- . = ..()
- if(. == SUCCESSFUL_UNFASTEN)
- take_and_drop_turfs_check()
-
-/obj/machinery/big_manipulator/screwdriver_act(mob/living/user, obj/item/tool)
- if(default_deconstruction_screwdriver(user, icon_state, icon_state, tool))
- return TRUE
- return TRUE
-
-/obj/machinery/big_manipulator/crowbar_act(mob/living/user, obj/item/tool)
- . = ..()
- if(default_deconstruction_crowbar(tool))
- return TRUE
- return TRUE
-
-/obj/machinery/big_manipulator/RefreshParts()
- . = ..()
-
- manipulator_lvl()
-
-/// Creat manipulator hand effect on manipulator core.
-/obj/machinery/big_manipulator/proc/create_manipulator_hand()
- manipulator_hand = new/obj/effect/big_manipulator_hand(get_turf(src))
- manipulator_hand.dir = take_here
-
-/// Check servo tier and change manipulator speed, power_use and colour.
-/obj/machinery/big_manipulator/proc/manipulator_lvl()
- var/datum/stock_part/manipulator/locate_servo = locate() in component_parts
- if(!locate_servo)
- return
- switch(locate_servo.tier)
- if(1)
- working_speed = 2 SECONDS
- power_use_lvl = 0.02
- set_greyscale(COLOR_YELLOW)
- manipulator_hand?.set_greyscale(COLOR_YELLOW)
- if(2)
- working_speed = 1.4 SECONDS
- power_use_lvl = 0.04
- set_greyscale(COLOR_ORANGE)
- manipulator_hand?.set_greyscale(COLOR_ORANGE)
- if(3)
- working_speed = 0.8 SECONDS
- power_use_lvl = 0.06
- set_greyscale(COLOR_RED)
- manipulator_hand?.set_greyscale(COLOR_RED)
- if(4)
- working_speed = 0.2 SECONDS
- power_use_lvl = 0.08
- set_greyscale(COLOR_PURPLE)
- manipulator_hand?.set_greyscale(COLOR_PURPLE)
-
- active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * power_use_lvl
-
-/// Changing take and drop turf tiles when we anchore manipulator or if manipulator not in turf.
-/obj/machinery/big_manipulator/proc/take_and_drop_turfs_check()
- if(anchored && isturf(src.loc))
- take_turf = get_step(src, take_here)
- drop_turf = get_step(src, drop_here)
- else
- take_turf = null
- drop_turf = null
-
-/// Changing take and drop turf dirs and also changing manipulator hand sprite dir.
-/obj/machinery/big_manipulator/proc/rotate_big_hand()
- switch(take_here)
- if(NORTH)
- manipulator_hand.item_x = 64
- manipulator_hand.item_y = 32
- take_here = EAST
- drop_here = WEST
- if(EAST)
- manipulator_hand.item_x = 32
- manipulator_hand.item_y = 0
- take_here = SOUTH
- drop_here = NORTH
- if(SOUTH)
- manipulator_hand.item_x = 0
- manipulator_hand.item_y = 32
- take_here = WEST
- drop_here = EAST
- if(WEST)
- manipulator_hand.item_x = -32
- manipulator_hand.item_y = 0
- take_here = NORTH
- drop_here = SOUTH
- manipulator_hand.dir = take_here
- take_and_drop_turfs_check()
-
-/// Deliting hand will destroy our manipulator core.
-/obj/machinery/big_manipulator/proc/on_hand_qdel()
- SIGNAL_HANDLER
-
- deconstruct(TRUE)
-
-/// Pre take and drop proc from [take and drop procs loop]:
-/// Check if we have item on take_turf to start take and drop loop
-/obj/machinery/big_manipulator/proc/is_work_check()
- if(filter)
- var/atom/movable = filter_return()
- if(movable)
- try_take_thing(take_turf, movable)
- return
-
- if(hacked)
- for(var/mob/living/take_item in take_turf.contents)
- try_take_thing(take_turf, take_item)
- break
- for(var/obj/take_item in take_turf.contents)
- if(take_item.anchored)
- continue
- try_take_thing(take_turf, take_item)
- break
-
-/obj/machinery/big_manipulator/proc/filter_return()
- if(!filter)
- return null
- for(var/atom/movable/listed in take_turf.contents)
- if(filter.check_filter(listed))
- return listed
-
-/// First take and drop proc from [take and drop procs loop]:
-/// Check if we can take item from take_turf to work with him. This proc also calling from ATOM_ENTERED signal.
-/obj/machinery/big_manipulator/proc/try_take_thing(datum/source, atom/movable/target)
- SIGNAL_HANDLER
- if(target == failed_item)
- failed_item = null
- return
-
- if(!on)
- return
- if(!anchored)
- return
- if(QDELETED(source) || QDELETED(target))
- return
- if(on_work)
- return
- if(!directly_use_energy(active_power_usage))
- on = FALSE
- say("Not enough energy!")
- return
- failed_item = null
-
- if(filter)
- if(passes_filter(target))
- start_work(target)
- return
-
- if(isitem(target) || (isliving(target) && hacked) || (isobj(target) && !target.anchored))
- start_work(target)
-
-/obj/machinery/big_manipulator/proc/passes_filter(atom/movable/target)
- if(!filter)
- return FALSE
- return filter.check_filter(target)
-
-/// Second take and drop proc from [take and drop procs loop]:
-/// Taking our item and start manipulator hand rotate animation.
-/obj/machinery/big_manipulator/proc/start_work(atom/movable/target)
- target.forceMove(src)
- containment_obj = WEAKREF(target)
- manipulator_hand.picked = target
- manipulator_hand.update_appearance()
- on_work = TRUE
- do_rotate_animation(1)
- addtimer(CALLBACK(src, PROC_REF(drop_thing), target), working_speed)
-
-/// Third take and drop proc from [take and drop procs loop]:
-/// Drop our item and start manipulator hand backward animation.
-/obj/machinery/big_manipulator/proc/drop_thing(atom/movable/target)
- if(!drop_turf.can_drop_off(target))
- failed_attempts++
- if(failed_attempts >= 10)
- do_rotate_animation(0)
- addtimer(CALLBACK(src, PROC_REF(end_work_failed), target), working_speed)
- return
- addtimer(CALLBACK(src, PROC_REF(drop_thing), target), working_speed)
- return
- failed_attempts = 0
- target.forceMove(drop_turf)
- manipulator_hand.picked = null
- manipulator_hand.update_appearance()
- do_rotate_animation(0)
- addtimer(CALLBACK(src, PROC_REF(end_work)), working_speed)
-
-/// Fourth and last take and drop proc from take and drop procs loop:
-/// Finishes work and begins to look for a new item for [take and drop procs loop].
-/obj/machinery/big_manipulator/proc/end_work()
- on_work = FALSE
- is_work_check()
-
-/obj/machinery/big_manipulator/proc/end_work_failed(atom/movable/target)
- target.forceMove(take_turf)
- failed_item = target
- manipulator_hand.picked = null
- manipulator_hand.update_appearance()
- on_work = FALSE
- failed_attempts = 0
- is_work_check()
-
-/// Rotates manipulator hand 90 degrees.
-/obj/machinery/big_manipulator/proc/do_rotate_animation(backward)
- animate(manipulator_hand, transform = matrix(90, MATRIX_ROTATE), working_speed*0.5)
- addtimer(CALLBACK(src, PROC_REF(finish_rotate_animation), backward), working_speed*0.5)
-
-/// Rotates manipulator hand from 90 degrees to 180 or 0 if backward.
-/obj/machinery/big_manipulator/proc/finish_rotate_animation(backward)
- animate(manipulator_hand, transform = matrix(180 * backward, MATRIX_ROTATE), working_speed*0.5)
-
-/obj/machinery/big_manipulator/ui_interact(mob/user, datum/tgui/ui)
- if(!anchored)
- to_chat(user, span_warning("[src] isn't attached to the ground!"))
- return
- ui = SStgui.try_update_ui(user, src, ui)
- if(!ui)
- ui = new(user, src, "BigManipulator")
- ui.open()
-
-/obj/machinery/big_manipulator/click_alt(mob/living/user)
- if(!filter)
- return CLICK_ACTION_BLOCKING
- filter.forceMove(get_turf(src))
- filter = null
- return CLICK_ACTION_SUCCESS
-
-/obj/machinery/big_manipulator/ui_data(mob/user)
- var/list/data = list()
- data["active"] = on
- return data
-
-/obj/machinery/big_manipulator/ui_act(action, params, datum/tgui/ui)
- . = ..()
- if(.)
- return
- switch(action)
- if("on")
- on = !on
- if(on)
- RegisterSignal(take_turf, COMSIG_ATOM_ENTERED, PROC_REF(try_take_thing))
- else
- UnregisterSignal(take_turf, COMSIG_ATOM_ENTERED)
- is_work_check()
- return TRUE
-
-/// Manipulator hand. Effect we animate to show that the manipulator is working and moving something.
-/obj/effect/big_manipulator_hand
- name = "Manipulator claw"
- desc = "Take and drop objects. Innovation..."
- icon = 'monkestation/code/modules/factory_type_beat/icons/big_manipulator_hand.dmi'
- icon_state = "hand"
- layer = LOW_ITEM_LAYER
- anchored = TRUE
- appearance_flags = KEEP_TOGETHER | LONG_GLIDE | TILE_BOUND | PIXEL_SCALE
- greyscale_config = /datum/greyscale_config/manipulator_hand
- pixel_x = -32
- pixel_y = -32
-
- ///item offset x
- var/item_x = 32
- var/item_y = 64
- var/atom/movable/picked
-
-/obj/effect/big_manipulator_hand/update_overlays()
- . = ..()
- if(picked)
- var/mutable_appearance/ma = mutable_appearance(picked.icon, picked.icon_state, picked.layer, src, appearance_flags = KEEP_TOGETHER)
- ma.color = picked.color
- ma.appearance = picked.appearance
- ma.appearance_flags = appearance_flags
- ma.plane = plane
- ma.pixel_x = item_x
- ma.pixel_y = item_y
- . += ma
-
-/turf/proc/can_drop_off(atom/movable/target)
- if(isclosedturf(src))
- return FALSE
- for(var/obj/structure/listed in contents)
- if(!listed.can_drop_off(target))
- return FALSE
- for(var/obj/machinery/listed in contents)
- if(!listed.can_drop_off(target))
- return FALSE
-
- return TRUE
-
-/obj/structure/proc/can_drop_off(atom/movable/target)
- return TRUE
-
-/obj/machinery/proc/can_drop_off(atom/movable/target)
- return TRUE
-
-
-/obj/item/manipulator_filter
- name = "manipulator filter"
- desc = "A filter specifically designed to work inside of a manipulator."
-
- icon = 'monkestation/code/modules/factory_type_beat/icons/items.dmi'
- icon_state = "filter"
-
- var/list/filtered_items = list()
- var/max_filtered_items = 5
-
-
-/obj/item/manipulator_filter/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
- if(istype(target, /obj/machinery/big_manipulator))
- try_attach(target)
- return
-
- if(target == src)
- return ..()
- if(!proximity_flag)
- return ..()
- if(!ismovable(target))
- return ..()
- if(istype(target, /obj/effect/decal/conveyor_sorter))
- return
- if(is_type_in_list(target, filtered_items))
- to_chat(user, span_warning("[target] is already in [src]'s sorting list!"))
- return
- if(length(filtered_items) >= max_filtered_items)
- to_chat(user, span_warning("[src] already has [max_filtered_items] things within the sorting list!"))
- return
- filtered_items += target.type
- to_chat(user, span_notice("[target] has been added to [src]'s sorting list."))
-
-/obj/item/manipulator_filter/examine(mob/user)
- . = ..()
- . += span_notice("This sorter can sort up to [max_filtered_items] Items.")
- . += span_notice("Use Alt-Click to reset the sorting list.")
- . += span_notice("Attack things to attempt to add to the sorting list.")
-
-/obj/item/manipulator_filter/click_alt(mob/user)
- visible_message("[src] pings, resetting its sorting list!")
- playsound(src, 'sound/machines/ping.ogg', 30, TRUE)
- filtered_items = list()
- return CLICK_ACTION_SUCCESS
-
-/obj/item/manipulator_filter/proc/try_attach(obj/machinery/big_manipulator/target)
- if(target.filter)
- return FALSE
- target.filter = src
- src.forceMove(target)
- return TRUE
-
-/obj/item/manipulator_filter/proc/check_filter(atom/movable/target)
- if(target.type in filtered_items)
- return TRUE
- return FALSE
-
-
-/obj/item/manipulator_filter/cargo
- name = "manipulator filter"
- desc = "A filter specifically designed to work inside of a manipulator."
-
-/obj/item/manipulator_filter/cargo/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
- if(istype(target, /obj/machinery/big_manipulator))
- try_attach(target)
- return
-
-/obj/item/manipulator_filter/attack_self(mob/user, modifiers)
- . = ..()
- var/choice = tgui_input_list(user, "Add a destination to check", name, GLOB.TAGGERLOCATIONS - filtered_items)
- if(!choice)
- return
-
- if(length(filtered_items) >= max_filtered_items)
- return
-
- filtered_items |= choice
-
-/obj/item/manipulator_filter/cargo/check_filter(atom/movable/target)
- if(istype(target, /obj/item/delivery))
- var/obj/item/delivery/item = target
- var/name_tag = GLOB.TAGGERLOCATIONS[item.sort_tag]
- if(name_tag in filtered_items)
- return TRUE
-
- if(istype(target, /obj/item/mail))
- var/obj/item/mail/item = target
- var/name_tag = GLOB.TAGGERLOCATIONS[item.sort_tag]
- if(name_tag in filtered_items)
- return TRUE
-
- if(SEND_SIGNAL(target, COMSIG_FILTER_CHECK, filtered_items))
- return TRUE
-
- return FALSE
-
-
-/obj/item/manipulator_filter/internal_filter
- name = "internal filter"
- desc = "Checks the contents inside of an object and if it matches any of the filters grabs the object"
-
-/obj/item/manipulator_filter/internal_filter/check_filter(atom/movable/target)
- for(var/atom/movable/listed in target.contents)
- if(listed.type in filtered_items)
- return TRUE
- return FALSE
-
-
-/proc/departmental_destination_to_tag(destination)
- switch(destination)
- if(/area/station/engineering/main)
- return "Engineering"
- if(/area/station/science/research)
- return "Research"
- if(/area/station/hallway/secondary/service)
- return "Hydroponics"
- if(/area/station/service/bar/atrium)
- return "Bar"
- if(/area/station/security/office, /area/station/security/brig, /area/station/security/brig/upper)
- return "Security"
- if(/area/station/medical/medbay/central, /area/station/medical/medbay, /area/station/medical/treatment_center, /area/station/medical/storage)
- return "Medbay"
diff --git a/tgstation.dme b/tgstation.dme
index 6388f2135cb56..af0126bf34260 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1969,6 +1969,7 @@
#include "code\datums\wires\airlock.dm"
#include "code\datums\wires\apc.dm"
#include "code\datums\wires\autolathe.dm"
+#include "code\datums\wires\big_manipulator.dm"
#include "code\datums\wires\conveyor.dm"
#include "code\datums\wires\ecto_sniffer.dm"
#include "code\datums\wires\emitter.dm"
@@ -2134,6 +2135,14 @@
#include "code\game\machinery\transformer.dm"
#include "code\game\machinery\washing_machine.dm"
#include "code\game\machinery\wishgranter.dm"
+#include "code\game\machinery\big_manipulator\_defines.dm"
+#include "code\game\machinery\big_manipulator\big_manipulator.dm"
+#include "code\game\machinery\big_manipulator\big_manipulator_interactions.dm"
+#include "code\game\machinery\big_manipulator\big_manipulator_items.dm"
+#include "code\game\machinery\big_manipulator\interaction_priorities.dm"
+#include "code\game\machinery\big_manipulator\manipulator_arm.dm"
+#include "code\game\machinery\big_manipulator\manipulator_tasks.dm"
+#include "code\game\machinery\big_manipulator\tasking.dm"
#include "code\game\machinery\camera\camera.dm"
#include "code\game\machinery\camera\camera_construction.dm"
#include "code\game\machinery\camera\motion.dm"
@@ -7847,7 +7856,6 @@
#include "monkestation\code\modules\factory_type_beat\ai_behaviours\latch_onto.dm"
#include "monkestation\code\modules\factory_type_beat\machinery\assembler.dm"
#include "monkestation\code\modules\factory_type_beat\machinery\brine_chamber.dm"
-#include "monkestation\code\modules\factory_type_beat\machinery\grabber.dm"
#include "monkestation\code\modules\factory_type_beat\machinery\splitter.dm"
#include "monkestation\code\modules\factory_type_beat\machinery\test_boulder_spawner.dm"
#include "monkestation\code\modules\factory_type_beat\machinery\atmos_chem\chemical_infuser.dm"
diff --git a/tgui/packages/tgui/interfaces/BigManipulator.tsx b/tgui/packages/tgui/interfaces/BigManipulator.tsx
deleted file mode 100644
index f72d750546723..0000000000000
--- a/tgui/packages/tgui/interfaces/BigManipulator.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { BooleanLike } from 'common/react';
-
-import { useBackend } from '../backend';
-import { Button, Section, Stack } from '../components';
-import { Window } from '../layouts';
-
-type ManipulatorData = {
- active: BooleanLike;
-};
-
-export const BigManipulator = (props) => {
- const { data, act } = useBackend();
- const { active } = data;
- return (
-
-
-
-
-
- );
-};
diff --git a/tgui/packages/tgui/interfaces/BigManipulator/index.tsx b/tgui/packages/tgui/interfaces/BigManipulator/index.tsx
new file mode 100644
index 0000000000000..23ac8d84c9905
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/BigManipulator/index.tsx
@@ -0,0 +1,656 @@
+import { useEffect, useState } from 'react';
+import {
+ BlockQuote,
+ Box,
+ Button,
+ Dropdown,
+ Icon,
+ Modal,
+ Section,
+ Slider,
+ Stack,
+ Table,
+} from 'tgui-core/components';
+import type { BooleanLike } from 'tgui-core/react';
+
+import { useBackend } from '../../backend';
+import { Window } from '../../layouts';
+import type { ManipulatorData, ManipulatorTask } from './types';
+
+const TASK_TYPE_LABELS: Record = {
+ pickup: 'Pick up...',
+ drop: 'Drop...',
+ throw: 'Throw...',
+ use: 'Use held...',
+ interact: 'Interact...',
+ wait: 'Wait...',
+};
+
+const TASK_TYPE_ICONS: Record = {
+ pickup: 'hand',
+ drop: 'box-open',
+ interact: 'bolt',
+ wait: 'hourglass-half',
+};
+
+const TASKING_STRATEGY_ICONS: Record = {
+ Sequential: 'list-ol',
+ 'Strict order': 'lock',
+};
+
+const buttonNumberToIcon: Record = {
+ 1: '',
+ 2: 'arrow-up',
+ 3: '',
+ 4: 'arrow-left',
+ 5: 'arrows-to-dot',
+ 6: 'arrow-right',
+ 7: '',
+ 8: 'arrow-down',
+ 9: '',
+};
+
+function MasterControls() {
+ const { act, data } = useBackend();
+ const { speed_multiplier, min_speed_multiplier, max_speed_multiplier } = data;
+
+ return (
+
+
+
+
+
+ act('adjust_interaction_speed', { new_speed: value })
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+type ConfigRowProps = {
+ label: string;
+ content: string;
+ onClick: () => void;
+ tooltip?: string;
+ selected?: BooleanLike;
+};
+
+const ConfigRow = (props: ConfigRowProps) => {
+ const { label, content, onClick, tooltip = '', selected = false } = props;
+
+ return (
+
+
+ {label}
+
+
+
+
+
+ );
+};
+
+const getPointButtonNumber = (offset: string): number | null => {
+ const [dx, dy] = offset.split(',').map(Number);
+ if (!Number.isFinite(dx) || !Number.isFinite(dy)) return null;
+ if (dx < -1 || dx > 1 || dy < -1 || dy > 1) return null;
+ if (dx === 0 && dy === 0) return null;
+ const xIndex = dx + 1;
+ const yIndex = 1 - dy;
+ return yIndex * 3 + xIndex + 1;
+};
+
+const getFilteringModeText = (mode: number) => {
+ switch (mode) {
+ case 1: return 'Items';
+ case 2: return 'Closets';
+ case 3: return 'Humans';
+ default: return 'Unknown';
+ }
+};
+
+type TaskEditModalProps = {
+ task: ManipulatorTask;
+ onClose: () => void;
+};
+
+function TaskEditModal(props: TaskEditModalProps) {
+ const { act, data } = useBackend();
+ const { task, onClose } = props;
+
+ const adjust = (param: string, value?: any) =>
+ act('adjust_task_param', { taskId: task.id, param, value });
+
+ const isCargo = !!task.turf;
+ const isPickup = task.task_type.includes('pickup');
+ const isDropoff = task.task_type.includes('dropoff');
+ const isInteract = task.task_type.includes('interact');
+
+ const currentButton = task.turf
+ ? getPointButtonNumber(task.turf)
+ : null;
+
+ return (
+
+
+ }
+ >
+ {task.task_type.includes('wait') && (
+
+
+
+ Wait Time
+
+
+ adjust('set_wait_time', value)}
+ />
+
+
+
+ )}
+ {isCargo && (
+
+
+
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => (
+
+
+
+
+ adjust('cycle_filtering_mode')}
+ tooltip="Cycle object category"
+ />
+ adjust('toggle_filter_skip')}
+ tooltip="Toggle filter usage"
+ />
+ {isPickup && (
+ adjust('cycle_pickup_eagerness')}
+ tooltip="Wait for dropoff slot or pick up immediately"
+ />
+ )}
+ {isDropoff && (
+ <>
+ adjust('cycle_interaction_mode')}
+ tooltip="Drop / Throw / Use"
+ />
+ adjust('cycle_overflow_status')}
+ tooltip="Cycle overflow behaviour"
+ />
+ {task.interaction_mode?.toUpperCase() === 'THROW' && (
+ adjust('cycle_throw_range')}
+ tooltip="Cycle throwing range"
+ />
+ )}
+ >
+ )}
+ {(isDropoff || isInteract) && task.interaction_mode?.toUpperCase() !== 'THROW' && (
+ <>
+ adjust('cycle_worker_interaction')}
+ tooltip="Normal / Single use / Empty hand"
+ />
+ adjust('toggle_worker_rmb')}
+ tooltip="Simulate RMB click"
+ />
+ adjust('toggle_worker_combat')}
+ tooltip="Use combat mode during interaction"
+ />
+ adjust('cycle_post_interaction')}
+ tooltip="What to do when nothing left to interact with"
+ />
+ >
+ )}
+
+
+
+ )}
+
+
+ {isCargo && (
+ <>
+
+
+ adjust('reset_atom_filters')}
+ confirmContent="Reset?"
+ icon="trash"
+ />
+ >
+ }
+ >
+
+ {(task.item_filters ?? []).map((name, index) => (
+
+
+ {name}
+
+
+
+
+ ))}
+
+
+
+ {(task.settings_list ?? []).length > 0 && (
+
+
+ {task.settings_list!.map((setting, index) => (
+
+
+ {index + 1}
+
+
+ adjust('toggle_priority', index)}
+ checked={!!setting.active}
+ fluid
+ >
+ {setting.name}
+
+
+
+
+
+ ))}
+
+
+ )}
+ >
+ )}
+
+ );
+};
+
+const TaskList = () => {
+ const { act, data } = useBackend();
+ const { tasks_data, current_task, tasking_strategy } = data;
+
+ const [editingTask, setEditingTask] = useState(null);
+ const [editingNameId, setEditingNameId] = useState(null);
+ const [newName, setNewName] = useState('');
+ const [selectedType, setSelectedType] = useState('pickup');
+
+ const adjust = (taskId: string, param: string, value?: any) =>
+ act('adjust_task_param', { taskId, param, value });
+
+ const handleSaveName = (taskId: string) => {
+ adjust(taskId, 'set_name', newName);
+ setEditingNameId(null);
+ setNewName('');
+ };
+
+ // keep modal in sync with live data
+ useEffect(() => {
+ if (!editingTask) return;
+ const updated = tasks_data.find((t) => t.id === editingTask.id);
+ if (updated) setEditingTask(updated);
+ }, [tasks_data]);
+
+ const strategyIcon =
+ TASKING_STRATEGY_ICONS[tasking_strategy] ?? 'list-ol';
+
+ return (
+ <>
+
+
+
+ >
+ }
+ >
+
+ {tasks_data.map((task, index) => {
+ const isActive = current_task === task.id;
+ const taskTypeKey = Object.keys(TASK_TYPE_LABELS).find((k) =>
+ task.task_type.includes(k),
+ ) ?? 'wait';
+
+ return (
+
+
+
+
+ {index + 1}
+
+
+
+
+
+
+
+
+ {TASK_TYPE_LABELS[taskTypeKey] ?? task.task_type}
+
+
+
+ {task.item_filters && task.item_filters.length > 0 && (
+
+ {'...any of: ' +
+ task.item_filters.slice(0, 3).join(', ') +
+ (task.item_filters.length > 3 ? ` and ${task.item_filters.length - 3} more` : '') +
+ '...'}
+
+ )}
+ {task.turf && ...at [{task.turf}]...}
+ {task.time && ...for {task.time} second{task.time > 1 && "s"}...}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+
+
+
+
+ ({
+ value: k,
+ displayText: TASK_TYPE_LABELS[k],
+ }))}
+ selected={TASK_TYPE_LABELS[selectedType]}
+ onSelected={(val) => setSelectedType(val)}
+ />
+
+
+
+
+
+
+
+ {editingTask && (
+ setEditingTask(null)}
+ />
+ )}
+ >
+ );
+};
+
+export const BigManipulator = () => {
+ const { data, act } = useBackend();
+ const { active, stopping } = data;
+
+ return (
+
+
+ act('run_cycle')}
+ >
+ {!active ? 'Run' : stopping ? 'Stopping' : 'Stop'}
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ act('disk_clear')}
+ >Clear
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/BigManipulator/types.ts b/tgui/packages/tgui/interfaces/BigManipulator/types.ts
new file mode 100644
index 0000000000000..e220a453ffe96
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/BigManipulator/types.ts
@@ -0,0 +1,55 @@
+import type { BooleanLike } from 'tgui-core/react';
+
+export interface PrioritySettings {
+ name: string;
+ active: BooleanLike;
+}
+
+export type TaskType =
+ | 'Pick up...'
+ | 'Drop...'
+ | 'Throw...'
+ | 'Use...'
+ | 'Interact with...'
+ | 'Wait for...';
+
+export interface ManipulatorTask {
+ name: string;
+ id: string;
+ task_type: string;
+ // cargo fields
+ turf?: string;
+ item_filters?: string[];
+ filters_status?: BooleanLike;
+ filtering_mode?: number;
+ settings_list?: PrioritySettings[];
+ // pickup only
+ pickup_eagerness?: string;
+ // dropoff only
+ interaction_mode?: string;
+ overflow_status?: string;
+ throw_range?: number;
+ worker_interaction?: string;
+ use_post_interaction?: string;
+ worker_use_rmb?: BooleanLike;
+ worker_combat_mode?: BooleanLike;
+ // interact only
+ // (worker_interaction, use_post_interaction, worker_use_rmb, worker_combat_mode shared with dropoff)
+ time?: number;
+}
+
+export interface ManipulatorData {
+ active: BooleanLike;
+ stopping: BooleanLike;
+ current_task: string | null;
+ speed_multiplier: number;
+ min_speed_multiplier: number;
+ max_speed_multiplier: number;
+ tasks_data: ManipulatorTask[];
+ manipulator_position: string;
+ tasking_strategy: string;
+ has_monkey: BooleanLike;
+ disk_inserted: BooleanLike;
+ disk_read_only: BooleanLike;
+ disk_task_count: number;
+}