diff --git a/code/__DEFINES/dcs/signals/signals_global.dm b/code/__DEFINES/dcs/signals/signals_global.dm
index bafb53f1ffd..f0938ea68ee 100644
--- a/code/__DEFINES/dcs/signals/signals_global.dm
+++ b/code/__DEFINES/dcs/signals/signals_global.dm
@@ -51,6 +51,8 @@
#define LINKED_UP (1<<0)
/// an obj/item is created! (obj/item/created_item)
#define COMSIG_GLOB_NEW_ITEM "!new_item"
+/// an obj/machinery is created! (obj/machinery/created_machine)
+#define COMSIG_GLOB_NEW_MACHINE "!new_machine"
/// a client (re)connected, after all /client/New() checks have passed : (client/connected_client)
#define COMSIG_GLOB_CLIENT_CONNECT "!client_connect"
/// a weather event of some kind occured
diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm
index 36c28708959..3f0edab3f75 100644
--- a/code/__DEFINES/dcs/signals/signals_object.dm
+++ b/code/__DEFINES/dcs/signals/signals_object.dm
@@ -416,3 +416,6 @@
/// from /obj/item/detective_scanner/scan(): (mob/user, list/extra_data)
#define COMSIG_DETECTIVE_SCANNED "det_scanned"
+
+/// from /obj/machinery/mineral/ore_redemption/pickup_item when it successfully picks something up
+#define COMSIG_ORM_COLLECTED_ORE "orm_collected_ore"
diff --git a/code/__DEFINES/dcs/signals/signals_traitor.dm b/code/__DEFINES/dcs/signals/signals_traitor.dm
index 666ff575837..66aebd7ae95 100644
--- a/code/__DEFINES/dcs/signals/signals_traitor.dm
+++ b/code/__DEFINES/dcs/signals/signals_traitor.dm
@@ -21,6 +21,9 @@
#define COMPONENT_FORCE_PLACEMENT (1<<0)
#define COMPONENT_FORCE_FAIL_PLACEMENT (1<<1)
+/// Called when a machine a traitor has booby trapped triggers its payload
+#define COMSIG_TRAITOR_MACHINE_TRAP_TRIGGERED "traitor_machine_trap_triggered"
+
/// Called when a device a traitor has planted effects someone's mood. Pass the mind of the viewer.
#define COMSIG_DEMORALISING_EVENT "traitor_demoralise_event"
/// Called when you finish drawing some graffiti so we can register more signals on it. Pass the graffiti effect.
diff --git a/code/__DEFINES/do_afters.dm b/code/__DEFINES/do_afters.dm
index 693c0dba6f5..f46710643d0 100644
--- a/code/__DEFINES/do_afters.dm
+++ b/code/__DEFINES/do_afters.dm
@@ -5,3 +5,4 @@
#define DOAFTER_SOURCE_CLIMBING_LADDER "doafter_climbingladder"
#define DOAFTER_SOURCE_SPIDER "doafter_spider"
#define DOAFTER_SOURCE_HEAL_TOUCH "doafter_heal_touch"
+#define DOAFTER_SOURCE_PLANTING_DEVICE "doafter_planting_device"
diff --git a/code/datums/components/interaction_booby_trap.dm b/code/datums/components/interaction_booby_trap.dm
new file mode 100644
index 00000000000..03892f3e93b
--- /dev/null
+++ b/code/datums/components/interaction_booby_trap.dm
@@ -0,0 +1,102 @@
+/**
+ * Attached to an atom, creates an explosion when it is interacted with
+ */
+/datum/component/interaction_booby_trap
+ /// Explosion radius of light damage
+ var/explosion_light_range
+ /// Explosion radius of heavy damage
+ var/explosion_heavy_range
+ /// Sound to play when triggered
+ var/triggered_sound
+ /// Time to wait between being triggered and blowing up
+ var/trigger_delay
+ /// Looping sound to clue people in that something is up
+ var/datum/looping_sound/active_sound_loop
+ /// Using this tool on the atom will defuse the explosive
+ var/defuse_tool
+ /// List of additional signals which should make this explode
+ var/list/additional_triggers
+ /// Callback to run when we're going to explode
+ var/datum/callback/on_triggered_callback
+ /// Callback to run when we've been defused
+ var/datum/callback/on_defused_callback
+ /// Time until we explode
+ var/explode_timer
+
+/datum/component/interaction_booby_trap/Initialize(
+ explosion_light_range = 3,
+ explosion_heavy_range = 1, // So we destroy some machine components
+ triggered_sound = 'sound/machines/triple_beep.ogg',
+ trigger_delay = 0.5 SECONDS,
+ sound_loop_type = /datum/looping_sound/trapped_machine_beep,
+ defuse_tool = TOOL_SCREWDRIVER,
+ additional_triggers = list(),
+ datum/callback/on_triggered_callback = null,
+ datum/callback/on_defused_callback = null,
+)
+ if(!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.explosion_light_range = explosion_light_range
+ src.explosion_heavy_range = explosion_heavy_range
+ src.triggered_sound = triggered_sound
+ src.trigger_delay = trigger_delay
+ src.defuse_tool = defuse_tool
+ src.additional_triggers = additional_triggers
+ src.on_triggered_callback = on_triggered_callback
+ src.on_defused_callback = on_defused_callback
+ if (sound_loop_type)
+ active_sound_loop = new sound_loop_type(parent)
+ active_sound_loop.start()
+
+ RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_touched))
+ RegisterSignal(parent, COMSIG_PARENT_EXAMINE_MORE, PROC_REF(on_examine))
+ if (defuse_tool)
+ RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(defuse_tool), PROC_REF(on_defused))
+ if (length(additional_triggers))
+ RegisterSignals(parent, additional_triggers, PROC_REF(trigger_explosive))
+
+/datum/component/interaction_booby_trap/Destroy(force, silent)
+ UnregisterSignal(parent, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_TOOL_ACT(defuse_tool), COMSIG_PARENT_EXAMINE_MORE) + additional_triggers)
+ QDEL_NULL(active_sound_loop)
+ QDEL_NULL(on_triggered_callback)
+ QDEL_NULL(on_defused_callback)
+ return ..()
+
+/// Called when someone touches the parent atom with their hands, we want to blow up
+/datum/component/interaction_booby_trap/proc/on_touched(atom/source)
+ SIGNAL_HANDLER
+ trigger_explosive(source)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/// Start a countdown until destruction
+/datum/component/interaction_booby_trap/proc/trigger_explosive(atom/source)
+ SIGNAL_HANDLER
+ if (explode_timer)
+ return
+ explode_timer = addtimer(CALLBACK(src, PROC_REF(explode), source), 0.5 SECONDS)
+ source.balloon_alert_to_viewers("beep")
+ playsound(parent, triggered_sound, 50, FALSE)
+ return
+
+/// Blow up the parent atom and delete ourselves
+/datum/component/interaction_booby_trap/proc/explode(atom/source)
+ on_triggered_callback?.Invoke(source)
+ var/turf/origin_turf = get_turf(source)
+ new /obj/effect/temp_visual/explosion/fast(origin_turf)
+ EX_ACT(source, EXPLODE_HEAVY, source)
+ explosion(origin = origin_turf, light_impact_range = explosion_light_range, heavy_impact_range = explosion_heavy_range, explosion_cause = src)
+ qdel(src)
+
+/// Defuse the bomb and delete ourselves
+/datum/component/interaction_booby_trap/proc/on_defused(atom/source, mob/user, obj/item/tool)
+ SIGNAL_HANDLER
+ on_defused_callback?.Invoke(source, user, tool)
+ qdel(src)
+ return COMPONENT_BLOCK_TOOL_ATTACK
+
+/// Give people a little hint
+/datum/component/interaction_booby_trap/proc/on_examine(atom/source, mob/examiner, list/examine_list)
+ SIGNAL_HANDLER
+ var/defuse_hint = (defuse_tool) ? "Perhaps [tool_behaviour_name(defuse_tool)] could help..." : ""
+ examine_list += span_warning("There's a light flashing red inside the maintenance panel. [defuse_hint]")
diff --git a/code/datums/looping_sounds/item_sounds.dm b/code/datums/looping_sounds/item_sounds.dm
index 9a7028aab6e..93de93ec94b 100644
--- a/code/datums/looping_sounds/item_sounds.dm
+++ b/code/datums/looping_sounds/item_sounds.dm
@@ -18,3 +18,11 @@
mid_sounds = list('sound/items/taperecorder/taperecorder_hiss_mid.ogg' = 1)
start_sound = list('sound/items/taperecorder/taperecorder_hiss_start.ogg' = 1)
volume = 10
+
+/datum/looping_sound/trapped_machine_beep
+ mid_sounds = list('sound/machines/beep.ogg' = 1)
+ mid_length = 10 SECONDS
+ mid_length_vary = 5 SECONDS
+ falloff_exponent = 10
+ falloff_distance = 1
+ volume = 5
diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm
index 8fc4f302304..677717356b1 100644
--- a/code/game/machinery/_machinery.dm
+++ b/code/game/machinery/_machinery.dm
@@ -183,6 +183,7 @@
if(HAS_TRAIT(SSstation, STATION_TRAIT_BOTS_GLITCHED))
randomize_language_if_on_station()
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_NEW_MACHINE, src)
return INITIALIZE_HINT_LATELOAD
@@ -1200,3 +1201,7 @@
if(isliving(user))
last_used_time = world.time
last_user_mobtype = user.type
+
+/// Called if this machine is supposed to be a sabotage machine objective.
+/obj/machinery/proc/add_as_sabotage_target()
+ return
diff --git a/code/modules/antagonists/traitor/objectives/destroy_machinery.dm b/code/modules/antagonists/traitor/objectives/destroy_machinery.dm
deleted file mode 100644
index 29cc7e42856..00000000000
--- a/code/modules/antagonists/traitor/objectives/destroy_machinery.dm
+++ /dev/null
@@ -1,76 +0,0 @@
-/datum/traitor_objective_category/destroy_machinery
- name = "Destroy Protolathe"
- objectives = list(
- /datum/traitor_objective/destroy_machinery = 1,
- /datum/traitor_objective/destroy_machinery/high_risk = 1
- )
-
-/datum/traitor_objective/destroy_machinery
- name = "Destroy the %MACHINE%"
- description = "Destroy the %MACHINE% to cause disarray and disrupt the operations of the %JOB%'s department."
-
- progression_reward = list(2 MINUTES, 8 MINUTES)
- telecrystal_reward = list(0, 1)
-
- progression_minimum = 0 MINUTES
- progression_maximum = 10 MINUTES
-
- /// The maximum amount of this type of objective a traitor can have.
- var/maximum_allowed = 2
- /// The possible target machinery and the jobs tied to each one.
- var/list/applicable_jobs = list(
- JOB_RESEARCH_DIRECTOR = /obj/machinery/rnd/production/protolathe/department/science,
- JOB_CHIEF_MEDICAL_OFFICER = /obj/machinery/rnd/production/techfab/department/medical,
- JOB_CHIEF_ENGINEER = /obj/machinery/rnd/production/protolathe/department/engineering,
- JOB_HEAD_OF_PERSONNEL = /obj/machinery/rnd/production/techfab/department/service,
- JOB_SHAFT_MINER = /obj/machinery/mineral/ore_redemption,
- JOB_QUARTERMASTER = /obj/machinery/rnd/production/techfab/department/cargo,
- )
- /// Whether this can bypass the maximum_allowed value or not
- var/allow_more_than_max = FALSE
- /// The chosen job. Used to check for duplicates
- var/chosen_job
-
-/datum/traitor_objective/destroy_machinery/high_risk
- progression_reward = list(5 MINUTES, 10 MINUTES)
- telecrystal_reward = list(3, 4)
-
- progression_minimum = 15 MINUTES
- progression_maximum = 30 MINUTES
- allow_more_than_max = TRUE
- applicable_jobs = list(
- JOB_STATION_ENGINEER = /obj/machinery/telecomms/hub,
- JOB_SCIENTIST = /obj/machinery/rnd/server,
- )
-
-/datum/traitor_objective/destroy_machinery/can_generate_objective(datum/mind/generating_for, list/possible_duplicates)
- if(length(possible_duplicates) >= maximum_allowed && !allow_more_than_max)
- return FALSE
- return TRUE
-
-/datum/traitor_objective/destroy_machinery/generate_objective(datum/mind/generating_for, list/possible_duplicates)
- for(var/datum/traitor_objective/destroy_machinery/objective as anything in possible_duplicates)
- applicable_jobs -= objective.chosen_job
- if(!length(applicable_jobs))
- return FALSE
- var/list/obj/machinery/possible_machines = list()
- while(length(possible_machines) <= 0 && length(applicable_jobs) > 0)
- var/target_head = pick(applicable_jobs)
- var/obj/machinery/machine_to_find = applicable_jobs[target_head]
- applicable_jobs -= target_head
-
- chosen_job = target_head
- for(var/obj/machinery/machine as anything in GLOB.machines)
- if(istype(machine, machine_to_find) && is_station_level(machine.z))
- possible_machines += machine
-
- if(!length(possible_machines))
- return FALSE
-
- for(var/obj/machinery/machine as anything in possible_machines)
- AddComponent(/datum/component/traitor_objective_register, machine, succeed_signals = list(COMSIG_PARENT_QDELETING))
-
- replace_in_name("%JOB%", lowertext(chosen_job))
- replace_in_name("%MACHINE%", possible_machines[1].name)
- return TRUE
-
diff --git a/code/modules/antagonists/traitor/objectives/sabotage_machinery.dm b/code/modules/antagonists/traitor/objectives/sabotage_machinery.dm
new file mode 100644
index 00000000000..b5e26c725ad
--- /dev/null
+++ b/code/modules/antagonists/traitor/objectives/sabotage_machinery.dm
@@ -0,0 +1,242 @@
+/// Datum which manages references to things we are instructed to destroy
+GLOBAL_DATUM_INIT(objective_machine_handler, /datum/objective_target_machine_handler, new())
+
+/// Marks a machine as a possible traitor sabotage target
+/proc/add_sabotage_machine(source, typepath)
+ LAZYADD(GLOB.objective_machine_handler.machine_instances_by_path[typepath], source)
+ return typepath
+
+/// Traitor objective to destroy a machine the crew cares about
+/datum/traitor_objective_category/sabotage_machinery
+ name = "Sabotage Worksite"
+ objectives = list(
+ /datum/traitor_objective/sabotage_machinery/trap = 1,
+ /datum/traitor_objective/sabotage_machinery/destroy = 1,
+ )
+
+/datum/traitor_objective/sabotage_machinery
+ name = "Sabotage the %MACHINE%"
+ description = "Abstract objective holder which shouldn't appear in your uplink."
+ abstract_type = /datum/traitor_objective/sabotage_machinery
+
+ /// The maximum amount of this type of objective a traitor can have, set to 0 for no limit.
+ var/maximum_allowed = 0
+ /// The possible target machinery and the jobs tied to each one.
+ var/list/applicable_jobs = list()
+ /// The chosen job. Used to check for duplicates
+ var/chosen_job
+
+/datum/traitor_objective/sabotage_machinery/can_generate_objective(datum/mind/generating_for, list/possible_duplicates)
+ if(!maximum_allowed)
+ return TRUE
+ if(length(possible_duplicates) >= maximum_allowed)
+ return FALSE
+ return TRUE
+
+/datum/traitor_objective/sabotage_machinery/generate_objective(datum/mind/generating_for, list/possible_duplicates)
+ var/list/possible_jobs = applicable_jobs.Copy()
+ for(var/datum/traitor_objective/sabotage_machinery/objective as anything in possible_duplicates)
+ possible_jobs -= objective.chosen_job
+ for(var/available_job in possible_jobs)
+ var/job_machine_path = possible_jobs[available_job]
+ if (!length(GLOB.objective_machine_handler.machine_instances_by_path[job_machine_path]))
+ possible_jobs -= available_job
+ if(!length(possible_jobs))
+ return FALSE
+
+ chosen_job = pick(possible_jobs)
+ var/list/obj/machinery/possible_machines = GLOB.objective_machine_handler.machine_instances_by_path[possible_jobs[chosen_job]]
+ for(var/obj/machinery/machine as anything in possible_machines)
+ prepare_machine(machine)
+
+ replace_in_name("%JOB%", lowertext(chosen_job))
+ replace_in_name("%MACHINE%", possible_machines[1].name)
+ return TRUE
+
+/// Marks a given machine as our target
+/datum/traitor_objective/sabotage_machinery/proc/prepare_machine(obj/machinery/machine)
+ AddComponent(/datum/component/traitor_objective_register, machine, succeed_signals = list(COMSIG_PARENT_QDELETING))
+
+// Destroy machines which are in annoying locations, are annoying when destroyed, and aren't directly interacted with
+/datum/traitor_objective/sabotage_machinery/destroy
+ name = "Destroy the %MACHINE%"
+ description = "Destroy the %MACHINE% to cause disarray and disrupt the operations of the %JOB%'s department."
+
+ progression_reward = list(5 MINUTES, 10 MINUTES)
+ telecrystal_reward = list(3, 4)
+
+ progression_minimum = 15 MINUTES
+ progression_maximum = 30 MINUTES
+
+ applicable_jobs = list(
+ JOB_STATION_ENGINEER = /obj/machinery/telecomms/hub,
+ JOB_SCIENTIST = /obj/machinery/rnd/server,
+ )
+
+// Rig machines which are in public locations to explode when interacted with
+/datum/traitor_objective/sabotage_machinery/trap
+ name = "Sabotage the %MACHINE%"
+ description = "Destroy the %MACHINE% to cause disarray and disrupt the operations of the %JOB%'s department. If you can get another crew member to destroy the machine using the provided booby trap, you will be rewarded with an additional %PROGRESSION% reputation and %TC% telecrystals."
+
+ progression_reward = list(2 MINUTES, 4 MINUTES)
+ telecrystal_reward = 0 // Only from completing the bonus objective
+
+ progression_minimum = 0 MINUTES
+ progression_maximum = 10 MINUTES
+
+ maximum_allowed = 2
+ applicable_jobs = list(
+ JOB_CHIEF_ENGINEER = /obj/machinery/rnd/production/protolathe/department/engineering,
+ JOB_CHIEF_MEDICAL_OFFICER = /obj/machinery/rnd/production/techfab/department/medical,
+ JOB_HEAD_OF_PERSONNEL = /obj/machinery/rnd/production/techfab/department/service,
+ JOB_QUARTERMASTER = /obj/machinery/rnd/production/techfab/department/cargo,
+ JOB_RESEARCH_DIRECTOR = /obj/machinery/rnd/production/protolathe/department/science,
+ JOB_SHAFT_MINER = /obj/machinery/mineral/ore_redemption,
+ )
+
+ /// Bonus reward to grant if you booby trap successfully
+ var/bonus_tc = 2
+ /// Bonus progression to grant if you booby trap successfully
+ var/bonus_progression = 5 MINUTES
+ /// The trap device we give out
+ var/obj/item/traitor_machine_trapper/tool
+
+/datum/traitor_objective/sabotage_machinery/trap/generate_objective(datum/mind/generating_for, list/possible_duplicates)
+ . = ..()
+ if (!.)
+ return FALSE
+
+ replace_in_name("%TC%", bonus_tc)
+ replace_in_name("%PROGRESSION%", DISPLAY_PROGRESSION(bonus_progression))
+ return TRUE
+
+/datum/traitor_objective/sabotage_machinery/trap/prepare_machine(obj/machinery/machine)
+ RegisterSignal(machine, COMSIG_TRAITOR_MACHINE_TRAP_TRIGGERED, PROC_REF(sabotage_success))
+ return ..()
+
+/// Called when you successfully proc the booby trap, gives a bonus reward
+/datum/traitor_objective/sabotage_machinery/trap/proc/sabotage_success(obj/machinery/machine)
+ progression_reward += bonus_progression
+ telecrystal_reward += bonus_tc
+ succeed_objective()
+
+/datum/traitor_objective/sabotage_machinery/trap/generate_ui_buttons(mob/user)
+ var/list/buttons = list()
+ if(!tool)
+ buttons += add_ui_button("", "Pressing this will materialize an explosive trap in your hand, which you can conceal within the target machine", "wifi", "summon_gear")
+ return buttons
+
+/datum/traitor_objective/sabotage_machinery/trap/ui_perform_action(mob/living/user, action)
+ . = ..()
+ switch(action)
+ if("summon_gear")
+ if(tool)
+ return
+ tool = new(user.drop_location())
+ user.put_in_hands(tool)
+ tool.balloon_alert(user, "a booby trap materializes in your hand")
+ tool.target_machine_path = applicable_jobs[chosen_job]
+
+/// Item which you use on a machine to cause it to explode next time someone interacts with it
+/obj/item/traitor_machine_trapper
+ name = "suspicious device"
+ desc = "It looks dangerous."
+ icon = 'icons/obj/weapons/items_and_weapons.dmi'
+ icon_state = "boobytrap"
+
+ /// Light explosion range, to hurt the person using the machine
+ var/explosion_range = 3
+ /// The type of object on which this can be planted on.
+ var/obj/machinery/target_machine_path
+ /// The time it takes to deploy the bomb.
+ var/deploy_time = 10 SECONDS
+
+/obj/item/traitor_machine_trapper/examine(mob/user)
+ . = ..()
+ if(!user.mind?.has_antag_datum(/datum/antagonist/traitor))
+ return
+ if(target_machine_path)
+ . += span_notice("This device must be placed by clicking on a [initial(target_machine_path.name)] with it. It can be removed with a screwdriver.")
+ . += span_notice("Remember, you may leave behind fingerprints on the device. Wear gloves when handling it to be safe!")
+
+/obj/item/traitor_machine_trapper/afterattack(atom/movable/target, mob/user, proximity_flag, click_parameters)
+ . = ..()
+ if(!user.Adjacent(target))
+ return
+ if(!istype(target, target_machine_path))
+ balloon_alert(user, "invalid target!")
+ return
+ . |= AFTERATTACK_PROCESSED_ITEM
+ balloon_alert(user, "planting device...")
+ if(!do_after(user, delay = deploy_time, target = src, interaction_key = DOAFTER_SOURCE_PLANTING_DEVICE))
+ return
+ target.AddComponent(\
+ /datum/component/interaction_booby_trap,\
+ additional_triggers = list(COMSIG_ORM_COLLECTED_ORE),\
+ on_triggered_callback = CALLBACK(src, PROC_REF(on_triggered)),\
+ on_defused_callback = CALLBACK(src, PROC_REF(on_defused)),\
+ )
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, GLOBAL_PROC_REF(qdel), src)
+ moveToNullspace()
+
+/// Called when applied trap is triggered, mark success
+/obj/item/traitor_machine_trapper/proc/on_triggered(atom/machine)
+ SEND_SIGNAL(machine, COMSIG_TRAITOR_MACHINE_TRAP_TRIGGERED)
+ qdel(src)
+
+/// Called when applied trap has been defused, retrieve this item from nullspace
+/obj/item/traitor_machine_trapper/proc/on_defused(atom/machine, mob/defuser, obj/item/tool)
+ UnregisterSignal(machine, COMSIG_PARENT_QDELETING)
+ playsound(machine, 'sound/effects/structure_stress/pop3.ogg', 100, vary = TRUE)
+ forceMove(get_turf(machine))
+ visible_message(span_warning("A [src] falls out from the [machine]!"))
+
+/// Datum which manages references to things we are instructed to destroy
+/datum/objective_target_machine_handler
+ /// Existing instances of machines organised by typepath
+ var/list/machine_instances_by_path = list()
+
+/datum/objective_target_machine_handler/New()
+ . = ..()
+ RegisterSignal(SSdcs, COMSIG_GLOB_NEW_MACHINE, PROC_REF(on_machine_created))
+ RegisterSignal(SSatoms, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(finalise_valid_targets))
+
+/// Adds a newly created machine to our list of machines, if we need it
+/datum/objective_target_machine_handler/proc/on_machine_created(datum/source, obj/machinery/new_machine)
+ SIGNAL_HANDLER
+ new_machine.add_as_sabotage_target()
+
+/// Confirm that everything added to the list is a valid target, then prevent new targets from being added
+/datum/objective_target_machine_handler/proc/finalise_valid_targets()
+ SIGNAL_HANDLER
+ for (var/machine_type in machine_instances_by_path)
+ for (var/obj/machinery/machine as anything in machine_instances_by_path[machine_type])
+ var/turf/place = get_turf(machine)
+ if(!place || !is_station_level(place.z))
+ machine_instances_by_path[machine_type] -= machine
+ continue
+ RegisterSignal(machine, COMSIG_PARENT_QDELETING, PROC_REF(machine_destroyed))
+ UnregisterSignal(SSdcs, COMSIG_GLOB_NEW_MACHINE)
+
+/datum/objective_target_machine_handler/proc/machine_destroyed(atom/machine)
+ SIGNAL_HANDLER
+ // Sadly can't do a direct typepath association because of some map helper subtypes
+ for (var/machine_type in machine_instances_by_path)
+ machine_instances_by_path[machine_type] -= machine
+
+// Mark valid machines as targets, add a new entry here if you add a new potential target
+
+/obj/machinery/telecomms/hub/add_as_sabotage_target()
+ return add_sabotage_machine(src, /obj/machinery/telecomms/hub) // Not always our specific type because of map helper subtypes
+
+/obj/machinery/rnd/server/add_as_sabotage_target()
+ return add_sabotage_machine(src, type)
+
+/obj/machinery/rnd/production/protolathe/department/add_as_sabotage_target()
+ return add_sabotage_machine(src, type)
+
+/obj/machinery/rnd/production/techfab/department/add_as_sabotage_target()
+ return add_sabotage_machine(src, type)
+
+/obj/machinery/mineral/ore_redemption/add_as_sabotage_target()
+ return add_sabotage_machine(src, type)
diff --git a/code/modules/mining/machine_redemption.dm b/code/modules/mining/machine_redemption.dm
index c8b14a20310..f37092fcfc8 100644
--- a/code/modules/mining/machine_redemption.dm
+++ b/code/modules/mining/machine_redemption.dm
@@ -48,34 +48,36 @@
. += span_notice("Alt-click to rotate the input and output direction.")
/// Turns ore into its refined type, and sends it to its material container
-/obj/machinery/mineral/ore_redemption/proc/smelt_ore(obj/item/stack/ore/O)
- if(QDELETED(O))
+/obj/machinery/mineral/ore_redemption/proc/smelt_ore(obj/item/stack/ore/gathered_ore)
+ if(QDELETED(gathered_ore))
return
var/datum/component/material_container/mat_container = materials.mat_container
if (!mat_container)
return
- if(O.refined_type == null)
+ if(gathered_ore.refined_type == null)
return
- if(O?.refined_type)
- points += O.points * point_upgrade * O.amount
+ if(gathered_ore?.refined_type)
+ points += gathered_ore.points * point_upgrade * gathered_ore.amount
- var/material_amount = mat_container.get_item_material_amount(O, BREAKDOWN_FLAGS_ORM)
+ var/material_amount = mat_container.get_item_material_amount(gathered_ore, BREAKDOWN_FLAGS_ORM)
if(!material_amount)
- qdel(O) //no materials, incinerate it
+ qdel(gathered_ore) //no materials, incinerate it
- else if(!mat_container.has_space(material_amount * O.amount)) //if there is no space, eject it
- unload_mineral(O)
+ else if(!mat_container.has_space(material_amount * gathered_ore.amount)) //if there is no space, eject it
+ unload_mineral(gathered_ore)
else
- var/list/stack_mats = O.get_material_composition(BREAKDOWN_FLAGS_ORM)
+ var/list/stack_mats = gathered_ore.get_material_composition(BREAKDOWN_FLAGS_ORM)
var/mats = stack_mats & mat_container.materials
- var/amount = O.amount
- mat_container.insert_item(O, ore_multiplier, breakdown_flags=BREAKDOWN_FLAGS_ORM) //insert it
+ var/amount = gathered_ore.amount
+ mat_container.insert_item(gathered_ore, ore_multiplier, breakdown_flags=BREAKDOWN_FLAGS_ORM) //insert it
materials.silo_log(src, "smelted", amount, "someone", mats)
- qdel(O)
+ qdel(gathered_ore)
+
+ SEND_SIGNAL(src, COMSIG_ORM_COLLECTED_ORE)
/// Returns the amount of a specific alloy design, based on the accessible materials
/obj/machinery/mineral/ore_redemption/proc/can_smelt_alloy(datum/design/D)
diff --git a/icons/obj/weapons/items_and_weapons.dmi b/icons/obj/weapons/items_and_weapons.dmi
index 584ae5f7d19..2ae955091f6 100644
Binary files a/icons/obj/weapons/items_and_weapons.dmi and b/icons/obj/weapons/items_and_weapons.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index b8715c03bcb..81616728c1e 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -976,6 +976,7 @@
#include "code\datums\components\holderloving.dm"
#include "code\datums\components\igniter.dm"
#include "code\datums\components\infective.dm"
+#include "code\datums\components\interaction_booby_trap.dm"
#include "code\datums\components\irradiated.dm"
#include "code\datums\components\itembound.dm"
#include "code\datums\components\itempicky.dm"
@@ -2686,12 +2687,12 @@
#include "code\modules\antagonists\traitor\objectives\demoralise_poster.dm"
#include "code\modules\antagonists\traitor\objectives\destroy_heirloom.dm"
#include "code\modules\antagonists\traitor\objectives\destroy_item.dm"
-#include "code\modules\antagonists\traitor\objectives\destroy_machinery.dm"
#include "code\modules\antagonists\traitor\objectives\eyesnatching.dm"
#include "code\modules\antagonists\traitor\objectives\hack_comm_console.dm"
#include "code\modules\antagonists\traitor\objectives\kidnapping.dm"
#include "code\modules\antagonists\traitor\objectives\kill_pet.dm"
#include "code\modules\antagonists\traitor\objectives\locate_weakpoint.dm"
+#include "code\modules\antagonists\traitor\objectives\sabotage_machinery.dm"
#include "code\modules\antagonists\traitor\objectives\sleeper_protocol.dm"
#include "code\modules\antagonists\traitor\objectives\steal.dm"
#include "code\modules\antagonists\traitor\objectives\final_objective\battlecruiser.dm"