diff --git a/code/__DEFINES/wiremod.dm b/code/__DEFINES/wiremod.dm
index 421650e3bf1e..26d967fde93f 100644
--- a/code/__DEFINES/wiremod.dm
+++ b/code/__DEFINES/wiremod.dm
@@ -99,10 +99,12 @@
#define SHELL_FLAG_CIRCUIT_UNMODIFIABLE (1<<5)
// Shell capacities. These can be converted to configs very easily later
-#define SHELL_CAPACITY_TINY 12
-#define SHELL_CAPACITY_SMALL 25
-#define SHELL_CAPACITY_MEDIUM 50
-#define SHELL_CAPACITY_LARGE 100
+// NON-MODULE CHANGE START
+#define SHELL_CAPACITY_TINY 25
+#define SHELL_CAPACITY_SMALL 50
+#define SHELL_CAPACITY_MEDIUM 100
+#define SHELL_CAPACITY_LARGE 250
+// NON-MODULE CHANGE END
#define SHELL_CAPACITY_VERY_LARGE 500
/// The maximum range a USB cable can be apart from a source
diff --git a/code/modules/wiremod/core/component_printer.dm b/code/modules/wiremod/core/component_printer.dm
index 3fb736540ec1..f79c40edc14b 100644
--- a/code/modules/wiremod/core/component_printer.dm
+++ b/code/modules/wiremod/core/component_printer.dm
@@ -356,7 +356,7 @@
update_static_data_for_all_viewers()
-/obj/machinery/module_duplicator/ui_act(action, list/params)
+/obj/machinery/module_duplicator/ui_act(action, list/params, datum/tgui/ui) // NON-MODULAR CHANGE
. = ..()
if (.)
return
@@ -365,10 +365,43 @@
if ("print")
var/design_id = text2num(params["designId"])
- if (design_id < 1 || design_id > length(scanned_designs))
+ /// NON-MODULAR CHANGE START
+
+ var/list/all_designs = scanned_designs
+ if (!isnull(SSpersistence.circuit_designs[ui.user.client?.ckey]))
+ all_designs = scanned_designs | SSpersistence.circuit_designs[ui.user.client?.ckey]
+
+ if (design_id < 1 || design_id > length(all_designs))
+ return TRUE
+
+ var/list/design = all_designs[design_id]
+
+ if (design["author_ckey"] != ui.user.client?.ckey && !(design in scanned_designs)) // Get away from here, cheater
return TRUE
- var/list/design = scanned_designs[design_id]
+ var/list/design_data = null
+ if (islist(design["dupe_data"]))
+ design_data = json_decode(design["dupe_data"]["integrated_circuit"])
+ else
+ design_data = json_decode(design["dupe_data"])
+
+ if(!design_data)
+ say("Invalid design data.")
+ return FALSE
+
+ var/list/circuit_data = design_data["components"]
+ for(var/identifier in circuit_data)
+ var/list/component_data = circuit_data[identifier]
+ var/comp_type = text2path(component_data["type"])
+ if (!ispath(comp_type, /obj/item/circuit_component))
+ say("[component_data["name"]] component in this circuit has been recalled, unable to proceed.")
+ return TRUE
+
+ if (isnull(current_unlocked_designs[comp_type]) && !isnull(all_circuit_designs[comp_type]))
+ say("[component_data["name"]] component has not been researched yet.")
+ return TRUE
+
+ /// NON-MODULAR CHANGE END
if (materials.on_hold())
say("Mineral access is on hold, please contact the quartermaster.")
@@ -387,6 +420,33 @@
// SAFETY: eject_sheets checks for valid mats
materials.eject_sheets(material, amount)
+ // NON-MODULAR CHANGE START
+
+ if ("delete")
+ var/design_id = text2num(params["designId"])
+
+ var/list/all_designs = scanned_designs
+ if (!isnull(SSpersistence.circuit_designs[ui.user.client?.ckey]))
+ all_designs = scanned_designs | SSpersistence.circuit_designs[ui.user.client?.ckey]
+
+ if (design_id < 1 || design_id > length(all_designs))
+ return TRUE
+
+ var/list/design = all_designs[design_id]
+
+ if (design["author_ckey"] != ui.user.client?.ckey)
+ return TRUE
+
+ if(tgui_alert(ui.user, "Are you sure you want to delete [design["name"]]?", "Module Duplicator", list("Yes","No")) != "Yes")
+ return TRUE
+
+ if (!isnull(SSpersistence.circuit_designs[design["author_ckey"]]))
+ SSpersistence.circuit_designs[design["author_ckey"]] -= list(design)
+ scanned_designs -= list(design)
+ update_static_data_for_all_viewers()
+
+ // NON-MODULAR CHANGE END
+
return TRUE
/obj/machinery/module_duplicator/proc/print_module(list/design)
@@ -421,7 +481,7 @@
data["dupe_data"] = list()
module.save_data_to_list(data["dupe_data"])
- data["name"] = module.display_name
+ data["name"] = "[module.display_name]" // NON-MODULAR CHANGE
data["desc"] = "A module that has been loaded in by [user]."
data["materials"] = list(GET_MATERIAL_REF(/datum/material/glass) = module.circuit_size * cost_per_component)
else if(istype(weapon, /obj/item/integrated_circuit))
@@ -431,7 +491,7 @@
return ..()
data["dupe_data"] = integrated_circuit.convert_to_json()
- data["name"] = integrated_circuit.display_name
+ data["name"] = "[integrated_circuit.display_name]" // NON-MODULAR CHANGE
data["desc"] = "An integrated circuit that has been loaded in by [user]."
var/datum/design/integrated_circuit/circuit_design = SSresearch.techweb_design_by_id("integrated_circuit")
@@ -449,17 +509,38 @@
balloon_alert(user, "it needs a name!")
return ..()
- for(var/list/component_data as anything in scanned_designs)
+ // NON-MODULAR CHANGE START
+
+ data["author_ckey"] = user.client?.ckey
+
+ var/list/all_designs = scanned_designs
+ if (!isnull(user.client?.ckey))
+ if (isnull(SSpersistence.circuit_designs[user.client?.ckey]))
+ SSpersistence.load_circuits_by_ckey(user.client?.ckey)
+ all_designs = scanned_designs | SSpersistence.circuit_designs[user.client?.ckey]
+
+ for(var/list/component_data as anything in all_designs)
+ if (component_data["author_ckey"] != user.client?.ckey && !(component_data in scanned_designs))
+ continue
if(component_data["name"] == data["name"])
balloon_alert(user, "name already exists!")
return ..()
+ // NON-MODULAR CHANGE END
+
flick("module-fab-scan", src)
addtimer(CALLBACK(src, PROC_REF(finish_module_scan), user, data), 1.4 SECONDS)
/obj/machinery/module_duplicator/proc/finish_module_scan(mob/user, data)
scanned_designs += list(data)
+ // NON-MODULAR CHANGE START
+ if (!isnull(user.client?.ckey))
+ if (isnull(SSpersistence.circuit_designs[user.client?.ckey]))
+ SSpersistence.load_circuits_by_ckey(user.client?.ckey)
+ SSpersistence.circuit_designs[user.client?.ckey] += list(data)
+ // NON-MODULAR CHANGE END
+
balloon_alert(user, "module has been saved.")
playsound(src, 'sound/machines/ping.ogg', 50)
@@ -475,8 +556,16 @@
var/list/designs = list()
+ // NON-MODULAR CHANGE START
+
+ var/list/all_designs = scanned_designs
+ if (!isnull(user.client?.ckey))
+ if (isnull(SSpersistence.circuit_designs[user.client?.ckey]))
+ SSpersistence.load_circuits_by_ckey(user.client?.ckey)
+ all_designs = scanned_designs | SSpersistence.circuit_designs[user.client?.ckey]
+
var/index = 1
- for (var/list/design as anything in scanned_designs)
+ for (var/list/design as anything in all_designs)
var/list/cost = list()
var/list/materials = design["materials"]
@@ -490,11 +579,42 @@
"id" = "[index]",
"icon" = "integrated_circuit",
"categories" = list("/Saved Circuits"),
+ "can_delete" = (design["author_ckey"] == user.client?.ckey),
+ "print_error" = null,
)
+
+ var/list/invalid_list = list()
+ var/list/unresearched_list = list()
+ var/list/design_data = null
+ if (islist(design["dupe_data"]))
+ design_data = json_decode(design["dupe_data"]["integrated_circuit"])
+ else
+ design_data = json_decode(design["dupe_data"])
+
+ if(!design_data)
+ index++
+ continue
+
+ var/list/circuit_data = design_data["components"]
+ for(var/identifier in circuit_data)
+ var/list/component_data = circuit_data[identifier]
+ var/comp_type = text2path(component_data["type"])
+ if (!ispath(comp_type, /obj/item/circuit_component))
+ invalid_list |= component_data["name"]
+ else if (isnull(current_unlocked_designs[comp_type]) && !isnull(all_circuit_designs[comp_type]))
+ unresearched_list |= component_data["name"]
+
+ if (invalid_list.len)
+ designs["[index]"]["print_error"] = "Following components have been recalled: [invalid_list.Join(", ")]"
+ if (unresearched_list.len)
+ designs["[index]"]["print_error"] = (designs["[index]"]["print_error"] || "") + "[designs["[index]"]["print_error"] ? "; " : ""]Following components are yet to be researched: [unresearched_list.Join(", ")]"
+
index++
data["designs"] = designs
+ // NON-MODULAR CHANGE END
+
return data
/obj/machinery/module_duplicator/crowbar_act(mob/living/user, obj/item/tool)
diff --git a/code/modules/wiremod/core/duplicator.dm b/code/modules/wiremod/core/duplicator.dm
index 7b373db6e359..9676aa808a00 100644
--- a/code/modules/wiremod/core/duplicator.dm
+++ b/code/modules/wiremod/core/duplicator.dm
@@ -150,6 +150,7 @@ GLOBAL_LIST_INIT(circuit_dupe_whitelisted_types, list(
var/list/component_data = list()
component_data["type"] = component.type
+ component_data["name"] = component.name // NON-MODULAR CHANGE
var/list/connections = list()
var/list/input_ports_stored_data = list()
diff --git a/maplestation.dme b/maplestation.dme
index 1c9322709cb1..1d2395b01d4f 100644
--- a/maplestation.dme
+++ b/maplestation.dme
@@ -6498,7 +6498,17 @@
#include "maplestation_modules\code\modules\vending\_vending.dm"
#include "maplestation_modules\code\modules\vending\clothesmate.dm"
#include "maplestation_modules\code\modules\vending\wardrobes.dm"
-#include "maplestation_modules\code\modules\wiremod\shells.dm"
+#include "maplestation_modules\code\modules\wiremod\circuit_exporting.dm"
+#include "maplestation_modules\code\modules\wiremod\components\bci_click_intercept.dm"
+#include "maplestation_modules\code\modules\wiremod\components\camera.dm"
+#include "maplestation_modules\code\modules\wiremod\components\cell_charge.dm"
+#include "maplestation_modules\code\modules\wiremod\components\item_interaction.dm"
+#include "maplestation_modules\code\modules\wiremod\components\mining.dm"
+#include "maplestation_modules\code\modules\wiremod\components\mmi.dm"
+#include "maplestation_modules\code\modules\wiremod\components\modsuit.dm"
+#include "maplestation_modules\code\modules\wiremod\components\screen.dm"
+#include "maplestation_modules\code\modules\wiremod\components\tile_scanner.dm"
+#include "maplestation_modules\code\modules\wiremod\shells.dm"
#include "maplestation_modules\story_content\albert_equipment\code\albertclothing.dm"
#include "maplestation_modules\story_content\albert_equipment\code\albertitem.dm"
#include "maplestation_modules\story_content\armored_corps\code\clothing\aylie_cloak.dm"
diff --git a/maplestation_modules/code/modules/research/designs/wiremod_designs.dm b/maplestation_modules/code/modules/research/designs/wiremod_designs.dm
index 46a0da24b400..10ee74209601 100644
--- a/maplestation_modules/code/modules/research/designs/wiremod_designs.dm
+++ b/maplestation_modules/code/modules/research/designs/wiremod_designs.dm
@@ -16,7 +16,37 @@
// The module duplicate is also 1/4th the cost.
/obj/machinery/module_duplicator
cost_per_component = SHEET_MATERIAL_AMOUNT * 0.025
+
+/datum/design/component/bci_click
+ name = "Click Interceptor Component"
+ id = "comp_bci_click"
+ build_path = /obj/item/circuit_component/click_interceptor
+/datum/design/component/circuit_camera
+ name = "Camera Component"
+ id = "comp_circuit_camera"
+ build_path = /obj/item/circuit_component/circuit_camera
+
+/datum/design/component/cell_charge
+ name = "Cell Charge Component"
+ id = "comp_cell_charge"
+ build_path = /obj/item/circuit_component/cell_charge
+
+/datum/design/component/mining
+ name = "Mining Component"
+ id = "comp_mining"
+ build_path = /obj/item/circuit_component/mining
+
+/datum/design/component/screen
+ name = "Screen Component"
+ id = "comp_screen"
+ build_path = /obj/item/circuit_component/screen
+
+/datum/design/component/tile_scanner
+ name = "Tile Scanner Component"
+ id = "comp_tile_scanner"
+ build_path = /obj/item/circuit_component/tile_scanner
+
/datum/design/headset_shell
name = "Headset Shell"
desc = "A portable shell integrated with a radio headset."
diff --git a/maplestation_modules/code/modules/research/techweb/all_nodes.dm b/maplestation_modules/code/modules/research/techweb/all_nodes.dm
index 78a30555fdb2..dbda9468dff7 100644
--- a/maplestation_modules/code/modules/research/techweb/all_nodes.dm
+++ b/maplestation_modules/code/modules/research/techweb/all_nodes.dm
@@ -86,6 +86,20 @@
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
+/datum/techweb_node/basic_circuitry
+ id_additions = list(
+ "comp_circuit_camera",
+ "comp_cell_charge",
+ "comp_mining",
+ "comp_screen",
+ "comp_tile_scanner",
+ )
+
+/datum/techweb_node/bci_shells
+ id_additions = list(
+ "comp_bci_click",
+ )
+
/datum/techweb_node/adv_shells
id_additions = list(
"headset_shell",
diff --git a/maplestation_modules/code/modules/wiremod/circuit_exporting.dm b/maplestation_modules/code/modules/wiremod/circuit_exporting.dm
new file mode 100644
index 000000000000..f9a2775e492a
--- /dev/null
+++ b/maplestation_modules/code/modules/wiremod/circuit_exporting.dm
@@ -0,0 +1,173 @@
+#define CIRCUITS_DATA_FILEPATH "data/circuit_designs/"
+
+/obj/machinery/module_duplicator/attackby_secondary(obj/item/weapon, mob/user, params)
+ if (!istype(weapon, /obj/item/integrated_circuit))
+ return ..()
+
+ if (HAS_TRAIT(weapon, TRAIT_CIRCUIT_UNDUPABLE))
+ balloon_alert(user, "unable to scan!")
+ return ..()
+
+ flick("module-fab-scan", src)
+ addtimer(CALLBACK(src, PROC_REF(finish_json_export), weapon, user), 1.4 SECONDS)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+/obj/machinery/module_duplicator/proc/finish_json_export(obj/item/integrated_circuit/circuit, mob/user)
+ var/datum/tgui_input_text/default_value/text_input = new(user, "", "Circuit Export", circuit.convert_to_json(), INFINITY, TRUE, FALSE, 0, GLOB.always_state)
+ text_input.ui_interact(user)
+
+/datum/tgui_input_text/default_value/ui_static_data(mob/user)
+ . = ..()
+ .["default_value"] = default
+
+/obj/machinery/module_duplicator/attack_hand_secondary(mob/user, list/modifiers)
+ var/circuit_data = tgui_input_text(user, "Import JSON for your circuit", "Circuit Import", "", INFINITY, TRUE, FALSE)
+ if (!circuit_data)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ var/list/errors = list()
+ var/obj/item/integrated_circuit/temp_circuit = new(src)
+ temp_circuit.load_circuit_data(circuit_data, errors) // For data validation
+ if (errors.len)
+ qdel(temp_circuit)
+ balloon_alert(user, "invalid import!")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ var/circuit_name = tgui_input_text(user, "Name your circuit", "Circuit Import")
+ if (!circuit_name)
+ qdel(temp_circuit)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ for(var/list/component_data as anything in scanned_designs)
+ if(component_data["name"] == circuit_name)
+ balloon_alert(user, "name already exists!")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ var/list/data = list()
+ data["name"] = circuit_name
+ data["desc"] = "An integrated circuit that has been loaded in by [user]."
+ data["dupe_data"] = circuit_data
+
+ var/datum/design/integrated_circuit/circuit_design = SSresearch.techweb_design_by_id("integrated_circuit")
+ var/materials = list(GET_MATERIAL_REF(/datum/material/glass) = temp_circuit.current_size * cost_per_component)
+ for(var/material_type in circuit_design.materials)
+ materials[material_type] += circuit_design.materials[material_type]
+ qdel(temp_circuit)
+ data["materials"] = materials
+ data["integrated_circuit"] = TRUE
+ scanned_designs += list(data)
+ balloon_alert(user, "circuit loaded!")
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+/obj/machinery/component_printer/attackby(obj/item/weapon, mob/living/user, params)
+ if(istype(weapon, /obj/item/circuit_component/module) && !user.combat_mode)
+ var/obj/item/circuit_component/module/module = weapon
+ module.internal_circuit.linked_component_printer = WEAKREF(src)
+ module.internal_circuit.update_static_data_for_all_viewers()
+ balloon_alert(user, "successfully linked to the integrated circuit")
+ return
+ return ..()
+
+/obj/machinery/module_duplicator
+ /// The current unlocked circuit component designs. Used by integrated circuits to print off circuit components remotely.
+ var/list/current_unlocked_designs = list()
+ /// The techweb the duplicastor will get researched designs from
+ var/datum/techweb/techweb
+ /// List of all circuit designs
+ var/static/list/all_circuit_designs = null
+
+/obj/machinery/module_duplicator/Initialize(mapload)
+ . = ..()
+
+ if (all_circuit_designs)
+ return
+
+ all_circuit_designs = list()
+ for (var/datum/design/component/design as anything in subtypesof(/datum/design/component))
+ all_circuit_designs[design::build_path] = design::id
+
+/obj/machinery/module_duplicator/LateInitialize()
+ . = ..()
+ if(!CONFIG_GET(flag/no_default_techweb_link) && !techweb)
+ CONNECT_TO_RND_SERVER_ROUNDSTART(techweb, src)
+ if(techweb)
+ on_connected_techweb()
+
+/obj/machinery/module_duplicator/proc/connect_techweb(datum/techweb/new_techweb)
+ if(techweb)
+ UnregisterSignal(techweb, list(COMSIG_TECHWEB_ADD_DESIGN, COMSIG_TECHWEB_REMOVE_DESIGN))
+ techweb = new_techweb
+ if(!isnull(techweb))
+ on_connected_techweb()
+
+/obj/machinery/module_duplicator/proc/on_connected_techweb()
+ for (var/researched_design_id in techweb.researched_designs)
+ var/datum/design/design = SSresearch.techweb_design_by_id(researched_design_id)
+ if (!(design.build_type & COMPONENT_PRINTER) || !ispath(design.build_path, /obj/item/circuit_component))
+ continue
+
+ current_unlocked_designs[design.build_path] = design.id
+
+ RegisterSignal(techweb, COMSIG_TECHWEB_ADD_DESIGN, PROC_REF(on_research))
+ RegisterSignal(techweb, COMSIG_TECHWEB_REMOVE_DESIGN, PROC_REF(on_removed))
+
+/obj/machinery/module_duplicator/multitool_act(mob/living/user, obj/item/multitool/tool)
+ if(!QDELETED(tool.buffer) && istype(tool.buffer, /datum/techweb))
+ connect_techweb(tool.buffer)
+ return TRUE
+
+/obj/machinery/module_duplicator/proc/on_research(datum/source, datum/design/added_design, custom)
+ SIGNAL_HANDLER
+ if (!(added_design.build_type & COMPONENT_PRINTER) || !ispath(added_design.build_path, /obj/item/circuit_component))
+ return
+ current_unlocked_designs[added_design.build_path] = added_design.id
+
+/obj/machinery/module_duplicator/proc/on_removed(datum/source, datum/design/added_design, custom)
+ SIGNAL_HANDLER
+ if (!(added_design.build_type & COMPONENT_PRINTER) || !ispath(added_design.build_path, /obj/item/circuit_component))
+ return
+ current_unlocked_designs -= added_design.build_path
+
+/datum/controller/subsystem/persistence
+ ///Associated list of all saved circuits, ckey -> list of designs
+ var/list/circuit_designs = list()
+
+/datum/controller/subsystem/persistence/collect_data()
+ . = ..()
+ save_circuits()
+
+/datum/controller/subsystem/persistence/proc/load_circuits_by_ckey(user)
+ var/json_file = file("[CIRCUITS_DATA_FILEPATH][user].json")
+ if(!fexists(json_file))
+ circuit_designs[user] = list()
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ circuit_designs[user] = list()
+ return
+ var/list/new_circuit_designs = json["data"]
+ for (var/list/design in new_circuit_designs)
+ var/list/new_materials = list()
+ for (var/material in design["materials"])
+ new_materials[GET_MATERIAL_REF(text2path(material))] = design["materials"][material]
+ design["materials"] = new_materials
+ circuit_designs[user] = new_circuit_designs
+
+/datum/controller/subsystem/persistence/proc/save_circuits()
+ for (var/user in circuit_designs)
+ var/json_file = file("[CIRCUITS_DATA_FILEPATH][user].json")
+ var/file_data = list()
+ var/list/user_designs = circuit_designs[user]
+ var/list/designs_to_store = user_designs.Copy()
+
+ for (var/list/design in designs_to_store)
+ var/list/new_materials = list()
+ for (var/datum/material/material in design["materials"])
+ new_materials["[material.type]"] = design["materials"][material]
+ design["materials"] = new_materials
+
+ file_data["data"] = designs_to_store
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
+
+#undef CIRCUITS_DATA_FILEPATH
diff --git a/maplestation_modules/code/modules/wiremod/components/bci_click_intercept.dm b/maplestation_modules/code/modules/wiremod/components/bci_click_intercept.dm
new file mode 100644
index 000000000000..fcd296247095
--- /dev/null
+++ b/maplestation_modules/code/modules/wiremod/components/bci_click_intercept.dm
@@ -0,0 +1,128 @@
+/**
+ * # Click Interceptor Component
+ *
+ * Outputs a signal when the user clicks on something. If its been primed via a signal, their click will be negated completely.
+ * Requires a BCI shell.
+ */
+
+/obj/item/circuit_component/click_interceptor
+ display_name = "Click Interceptor"
+ desc = "A component that allows the user to pinpoint an object using their mind. Sending an input will negate the next input the user does. Requires a BCI shell."
+ category = "BCI"
+
+ required_shells = list(/obj/item/organ/internal/cyberimp/bci)
+
+ var/datum/port/input/click_toggle
+ var/datum/port/input/move_toggle
+
+ var/datum/port/output/left_click
+ var/datum/port/output/right_click
+ var/datum/port/output/middle_click
+
+ var/datum/port/output/north
+ var/datum/port/output/east
+ var/datum/port/output/south
+ var/datum/port/output/west
+
+ var/datum/port/output/clicked_atom
+
+ // Click modifiers
+ var/datum/port/output/alt_click
+ var/datum/port/output/ctrl_click
+ var/datum/port/output/shift_click
+
+ var/obj/item/organ/internal/cyberimp/bci/bci
+ var/intercept_click = FALSE
+ var/intercept_move = FALSE
+
+/obj/item/circuit_component/click_interceptor/populate_ports()
+ north = add_output_port("North", PORT_TYPE_SIGNAL)
+ east = add_output_port("East", PORT_TYPE_SIGNAL)
+ south = add_output_port("South", PORT_TYPE_SIGNAL)
+ west = add_output_port("West", PORT_TYPE_SIGNAL)
+
+ left_click = add_output_port("Left Click", PORT_TYPE_SIGNAL)
+ right_click = add_output_port("Right Click", PORT_TYPE_SIGNAL)
+ middle_click = add_output_port("Middle Click", PORT_TYPE_SIGNAL)
+
+ clicked_atom = add_output_port("Target", PORT_TYPE_ATOM)
+
+ alt_click = add_output_port("Alt Click", PORT_TYPE_NUMBER)
+ shift_click = add_output_port("Shift Click", PORT_TYPE_NUMBER)
+ ctrl_click = add_output_port("Ctrl Click", PORT_TYPE_NUMBER)
+
+ click_toggle = add_input_port("Intercept Next Click", PORT_TYPE_SIGNAL)
+ move_toggle = add_input_port("Intercept Next Move", PORT_TYPE_SIGNAL)
+
+/obj/item/circuit_component/click_interceptor/register_shell(atom/movable/shell)
+ if(!istype(shell, /obj/item/organ/internal/cyberimp/bci))
+ return
+
+ bci = shell
+ RegisterSignal(bci, COMSIG_ORGAN_IMPLANTED, PROC_REF(on_implanted))
+ RegisterSignal(bci, COMSIG_ORGAN_REMOVED, PROC_REF(on_removed))
+
+ var/mob/living/owner = bci.owner
+ if(istype(owner))
+ RegisterSignal(owner, COMSIG_MOB_CLICKON, PROC_REF(handle_click))
+ RegisterSignal(owner, COMSIG_MOB_CLIENT_PRE_MOVE, PROC_REF(handle_move))
+
+/obj/item/circuit_component/click_interceptor/unregister_shell(atom/movable/shell)
+ UnregisterSignal(bci, list(COMSIG_ORGAN_IMPLANTED, COMSIG_ORGAN_REMOVED))
+ if (istype(bci.owner))
+ UnregisterSignal(bci.owner, list(COMSIG_MOB_CLICKON, COMSIG_MOB_CLIENT_PRE_MOVE))
+ bci = null
+
+/obj/item/circuit_component/click_interceptor/input_received(datum/port/input/port)
+ if (COMPONENT_TRIGGERED_BY(click_toggle, port))
+ intercept_click = TRUE
+ if (COMPONENT_TRIGGERED_BY(move_toggle, port))
+ intercept_move = TRUE
+
+/obj/item/circuit_component/click_interceptor/proc/handle_move(datum/source, list/move_args)
+ SIGNAL_HANDLER
+ var/move_dir = get_dir(get_turf(source), move_args[MOVE_ARG_NEW_LOC])
+
+ if (move_dir & NORTH)
+ north.set_output(COMPONENT_SIGNAL)
+ if (move_dir & EAST)
+ east.set_output(COMPONENT_SIGNAL)
+ if (move_dir & SOUTH)
+ south.set_output(COMPONENT_SIGNAL)
+ if (move_dir & WEST)
+ west.set_output(COMPONENT_SIGNAL)
+
+ if (intercept_move)
+ return COMSIG_MOB_CLIENT_BLOCK_PRE_MOVE
+
+/obj/item/circuit_component/click_interceptor/proc/on_implanted(datum/source, mob/living/carbon/owner)
+ SIGNAL_HANDLER
+ RegisterSignal(owner, COMSIG_MOB_CLICKON, PROC_REF(handle_click))
+ RegisterSignal(owner, COMSIG_MOB_CLIENT_PRE_MOVE, PROC_REF(handle_move))
+
+/obj/item/circuit_component/click_interceptor/proc/on_removed(datum/source, mob/living/carbon/owner)
+ SIGNAL_HANDLER
+ UnregisterSignal(owner, list(COMSIG_MOB_CLICKON, COMSIG_MOB_CLIENT_PRE_MOVE))
+
+/obj/item/circuit_component/click_interceptor/proc/handle_click(mob/living/source, atom/target, list/modifiers)
+ SIGNAL_HANDLER
+
+ if (source.stat >= UNCONSCIOUS)
+ return
+
+ alt_click.set_output(LAZYACCESS(modifiers, ALT_CLICK))
+ shift_click.set_output(LAZYACCESS(modifiers, SHIFT_CLICK))
+ ctrl_click.set_output(LAZYACCESS(modifiers, CTRL_CLICK))
+
+ clicked_atom.set_output(target)
+
+ if (LAZYACCESS(modifiers, RIGHT_CLICK))
+ right_click.set_output(COMPONENT_SIGNAL)
+ else if (LAZYACCESS(modifiers, MIDDLE_CLICK))
+ middle_click.set_output(COMPONENT_SIGNAL)
+ else
+ left_click.set_output(COMPONENT_SIGNAL)
+
+ if (intercept_click)
+ intercept_click = FALSE
+ return COMSIG_MOB_CANCEL_CLICKON
diff --git a/maplestation_modules/code/modules/wiremod/components/camera.dm b/maplestation_modules/code/modules/wiremod/components/camera.dm
new file mode 100644
index 000000000000..98883d7503c1
--- /dev/null
+++ b/maplestation_modules/code/modules/wiremod/components/camera.dm
@@ -0,0 +1,52 @@
+/**
+ * # Camera component
+ *
+ * Adds a camera to the shell
+ */
+
+/obj/item/circuit_component/circuit_camera
+ display_name = "Camera"
+ desc = "A component that links to the station's camera network."
+ category = "Entity"
+
+ var/datum/port/input/enable_trigger
+ var/datum/port/input/disable_trigger
+ var/datum/port/input/tag_input
+
+ var/datum/port/output/enabled
+ var/datum/port/output/disabled
+
+ var/obj/machinery/camera/circuit/camera
+
+/obj/item/circuit_component/circuit_camera/populate_ports()
+ tag_input = add_input_port("Camera name", PORT_TYPE_STRING)
+ enable_trigger = add_input_port("Enable", PORT_TYPE_SIGNAL)
+ disable_trigger = add_input_port("Disable", PORT_TYPE_SIGNAL)
+
+ enabled = add_output_port("Enabled", PORT_TYPE_SIGNAL)
+ disabled = add_output_port("Disabled", PORT_TYPE_SIGNAL)
+
+/obj/item/circuit_component/circuit_camera/register_shell(atom/movable/shell)
+ camera = new(shell)
+
+/obj/item/circuit_component/circuit_camera/unregister_shell(atom/movable/shell)
+ QDEL_NULL(camera)
+
+/obj/item/circuit_component/circuit_camera/input_received(datum/port/input/port)
+ if (isnull(camera))
+ return
+
+ camera.c_tag = tag_input.value
+
+ if(COMPONENT_TRIGGERED_BY(enable_trigger, port) && !camera.status)
+ camera.toggle_cam()
+ enabled.set_output(COMPONENT_SIGNAL)
+
+ if(COMPONENT_TRIGGERED_BY(disable_trigger, port) && camera.status)
+ camera.toggle_cam()
+ disabled.set_output(COMPONENT_SIGNAL)
+
+/obj/machinery/camera/circuit
+ c_tag = "Circuit Shell: Unspecified"
+ desc = "This camera belongs in a circuit shell. If you see this, tell a coder!"
+ network = list("ss13", "rd")
diff --git a/maplestation_modules/code/modules/wiremod/components/cell_charge.dm b/maplestation_modules/code/modules/wiremod/components/cell_charge.dm
new file mode 100644
index 000000000000..d8ef8f2a93b7
--- /dev/null
+++ b/maplestation_modules/code/modules/wiremod/components/cell_charge.dm
@@ -0,0 +1,28 @@
+/**
+ * # Cell Charge Component
+ *
+ * Returns circuit's current cell charge and its capacity
+ */
+
+/obj/item/circuit_component/cell_charge
+ display_name = "Cell Charge"
+ desc = "A component that reads current cell charge and its maximum capacity."
+ category = "Sensor"
+
+ var/datum/port/output/current_charge
+ var/datum/port/output/max_charge
+
+ circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL
+
+/obj/item/circuit_component/cell_charge/populate_ports()
+ current_charge = add_output_port("Current Charge", PORT_TYPE_NUMBER)
+ max_charge = add_output_port("Max Charge", PORT_TYPE_NUMBER)
+
+/obj/item/circuit_component/cell_charge/input_received(datum/port/input/port)
+ var/obj/item/stock_parts/cell/battery = parent.cell
+
+ if(!istype(battery))
+ return
+
+ max_charge.set_output(battery.maxcharge)
+ current_charge.set_output(battery.charge)
diff --git a/maplestation_modules/code/modules/wiremod/components/item_interaction.dm b/maplestation_modules/code/modules/wiremod/components/item_interaction.dm
new file mode 100644
index 000000000000..1638c36eaa82
--- /dev/null
+++ b/maplestation_modules/code/modules/wiremod/components/item_interaction.dm
@@ -0,0 +1,88 @@
+s/*
+ * # Item Interaction component
+ *
+ * Allows a shell to left, right or middle click an atom next to it. Supports ctrl, shift and alt clicking. Drone shell only.
+ *
+ * Currently admin-only because I'm afraid this could generate far too many runtimes
+ */
+
+/obj/item/circuit_component/item_interact
+ display_name = "Item Interaction"
+ desc = "A component that allows a shell to interact with objects right next to it. Drone shell only."
+ category = "Action"
+
+ var/datum/port/input/target
+
+ var/datum/port/input/left_click
+ var/datum/port/input/middle_click
+ var/datum/port/input/right_click
+
+ var/datum/port/input/alt_click
+ var/datum/port/input/shift_click
+ var/datum/port/input/ctrl_click
+ var/datum/port/input/in_hand_click
+
+ var/click_delay = 0.8 SECONDS // Same as click cooldown
+ COOLDOWN_DECLARE(click_cooldown)
+
+/obj/item/circuit_component/item_interact/populate_ports()
+ target = add_input_port("Target", PORT_TYPE_ATOM)
+
+ left_click = add_input_port("Left Click", PORT_TYPE_SIGNAL)
+ middle_click = add_input_port("Middle Click", PORT_TYPE_SIGNAL)
+ right_click = add_input_port("Right Click", PORT_TYPE_SIGNAL)
+
+ alt_click = add_input_port("Alt Click", PORT_TYPE_NUMBER)
+ shift_click = add_input_port("Shift Click", PORT_TYPE_NUMBER)
+ ctrl_click = add_input_port("Ctrl Click", PORT_TYPE_NUMBER)
+ in_hand_click = add_input_port("In-Hand Click", PORT_TYPE_NUMBER)
+
+ trigger_output = add_output_port("Triggered", PORT_TYPE_SIGNAL, order = 2)
+
+/obj/item/circuit_component/item_interact/input_received(datum/port/input/port, list/return_values)
+ if (!COOLDOWN_FINISHED(src, click_delay))
+ return
+
+ if (isnull(target.value))
+ return
+
+ if (!COMPONENT_TRIGGERED_BY(left_click, port) && !COMPONENT_TRIGGERED_BY(middle_click, port) && !COMPONENT_TRIGGERED_BY(right_click, port))
+ return
+
+ var/mob/shell = parent.shell
+ if(!istype(shell) || !shell.CanReach(target.value))
+ return
+
+ var/list/modifiers = list()
+
+ if (alt_click.value)
+ modifiers[ALT_CLICK] = TRUE
+ if (shift_click.value)
+ modifiers[SHIFT_CLICK] = TRUE
+ if (ctrl_click.value)
+ modifiers[CTRL_CLICK] = TRUE
+
+ if (in_hand_click.value)
+ if (!isitem(target.value))
+ return
+
+ var/obj/item/target_item = target.value
+ if (COMPONENT_TRIGGERED_BY(left_click, port))
+ target_item.attack_self(shell, modifiers)
+ COOLDOWN_START(src, click_cooldown, click_delay)
+ trigger_output.set_output(COMPONENT_SIGNAL)
+
+ if (COMPONENT_TRIGGERED_BY(right_click, port))
+ target_item.attack_self_secondary(shell, modifiers)
+ COOLDOWN_START(src, click_cooldown, click_delay)
+ trigger_output.set_output(COMPONENT_SIGNAL)
+ return
+
+ if (COMPONENT_TRIGGERED_BY(left_click, port))
+ modifiers[LEFT_CLICK] = TRUE
+ if (COMPONENT_TRIGGERED_BY(middle_click, port))
+ modifiers[MIDDLE_CLICK] = TRUE
+ if (COMPONENT_TRIGGERED_BY(right_click, port))
+ modifiers[RIGHT_CLICK] = TRUE
+
+ INVOKE_ASYNC(shell, TYPE_PROC_REF(/mob, ClickOn), target.value, list2params(modifiers))
diff --git a/maplestation_modules/code/modules/wiremod/components/mining.dm b/maplestation_modules/code/modules/wiremod/components/mining.dm
new file mode 100644
index 000000000000..4a6e441a5e2c
--- /dev/null
+++ b/maplestation_modules/code/modules/wiremod/components/mining.dm
@@ -0,0 +1,33 @@
+/*
+ * # Mining component
+ *
+ * Allows to mine rocks walls and floors
+ */
+
+/obj/item/circuit_component/mining
+ display_name = "Mining"
+ desc = "A component that can mine rock turfs. Only works with drone shells."
+ category = "Action"
+
+ var/datum/port/input/target
+ circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL
+
+/obj/item/circuit_component/mining/populate_ports()
+ target = add_input_port("Target", PORT_TYPE_ATOM)
+
+/obj/item/circuit_component/mining/input_received(datum/port/input/port)
+ var/atom/target_atom = target.value
+
+ var/mob/shell = parent.shell
+ if(!istype(shell) || !shell.CanReach(target_atom))
+ return
+
+ if (ismineralturf(target_atom))
+ var/turf/closed/mineral/wall = target_atom
+ wall.gets_drilled(shell, FALSE)
+ return
+
+ if (isasteroidturf(target_atom))
+ var/turf/open/misc/asteroid/floor = target_atom
+ floor.getDug()
+ return
diff --git a/maplestation_modules/code/modules/wiremod/components/mmi.dm b/maplestation_modules/code/modules/wiremod/components/mmi.dm
new file mode 100644
index 000000000000..68756e1d6554
--- /dev/null
+++ b/maplestation_modules/code/modules/wiremod/components/mmi.dm
@@ -0,0 +1,33 @@
+/obj/item/circuit_component/mmi
+ // Called when the MMI middle clicks.
+ var/datum/port/output/middle_click
+
+ // Click modifiers
+ var/datum/port/output/alt_click
+ var/datum/port/output/ctrl_click
+ var/datum/port/output/shift_click
+
+/obj/item/circuit_component/mmi/populate_ports()
+ . = ..()
+ middle_click = add_output_port("Middle Click", PORT_TYPE_SIGNAL)
+ alt_click = add_output_port("Alt Click", PORT_TYPE_NUMBER)
+ shift_click = add_output_port("Shift Click", PORT_TYPE_NUMBER)
+ ctrl_click = add_output_port("Ctrl Click", PORT_TYPE_NUMBER)
+
+/obj/item/circuit_component/mmi/handle_mmi_attack(mob/living/source, atom/target, list/modifiers)
+ alt_click.set_output(LAZYACCESS(modifiers, ALT_CLICK))
+ shift_click.set_output(LAZYACCESS(modifiers, SHIFT_CLICK))
+ ctrl_click.set_output(LAZYACCESS(modifiers, CTRL_CLICK))
+ clicked_atom.set_output(target)
+
+ if (LAZYACCESS(modifiers, RIGHT_CLICK))
+ secondary_attack.set_output(COMPONENT_SIGNAL)
+ return COMSIG_MOB_CANCEL_CLICKON
+
+ if (LAZYACCESS(modifiers, MIDDLE_CLICK))
+ middle_click.set_output(COMPONENT_SIGNAL)
+ return COMSIG_MOB_CANCEL_CLICKON
+
+ attack.set_output(COMPONENT_SIGNAL)
+ if (!LAZYACCESS(modifiers, ALT_CLICK) && !LAZYACCESS(modifiers, SHIFT_CLICK) && !LAZYACCESS(modifiers, CTRL_CLICK))
+ return COMSIG_MOB_CANCEL_CLICKON
diff --git a/maplestation_modules/code/modules/wiremod/components/modsuit.dm b/maplestation_modules/code/modules/wiremod/components/modsuit.dm
new file mode 100644
index 000000000000..eda41ce40983
--- /dev/null
+++ b/maplestation_modules/code/modules/wiremod/components/modsuit.dm
@@ -0,0 +1,136 @@
+/// WHAT COULD GO WRONG I WONDER
+
+/obj/item/circuit_component/mod_adapter_core
+ /// do you think god stays in heaven
+ var/datum/port/input/north
+ var/datum/port/input/east
+ var/datum/port/input/south
+ var/datum/port/input/west
+
+ var/datum/port/output/moved
+ var/datum/port/output/move_fail
+
+ /// because he too lives in fear of what he's created
+ var/datum/port/input/target
+ var/datum/port/input/swap_hands
+
+ var/datum/port/output/swapped_hands
+
+ var/datum/port/input/left_click
+ var/datum/port/input/middle_click
+ var/datum/port/input/right_click
+
+ var/datum/port/input/alt_click
+ var/datum/port/input/shift_click
+ var/datum/port/input/ctrl_click
+
+ var/datum/port/output/clicked
+
+ var/datum/port/input/get_hands
+
+ var/datum/port/output/active_hand
+ var/datum/port/output/inactive_hand
+ var/datum/port/output/fetched_hands
+
+ COOLDOWN_DECLARE(move_cooldown)
+ COOLDOWN_DECLARE(click_cooldown)
+
+ var/click_delay = 0.8 SECONDS // Same as clicking
+
+/obj/item/circuit_component/mod_adapter_core/populate_ports()
+ . = ..()
+ north = add_input_port("North", PORT_TYPE_SIGNAL)
+ east = add_input_port("East", PORT_TYPE_SIGNAL)
+ south = add_input_port("South", PORT_TYPE_SIGNAL)
+ west = add_input_port("West", PORT_TYPE_SIGNAL)
+ moved = add_output_port("Successful Move", PORT_TYPE_SIGNAL)
+ move_fail = add_output_port("Failed Move", PORT_TYPE_SIGNAL)
+
+ left_click = add_input_port("Left Click", PORT_TYPE_SIGNAL)
+ right_click = add_input_port("Right Click", PORT_TYPE_SIGNAL)
+ middle_click = add_input_port("Middle Click", PORT_TYPE_SIGNAL)
+ clicked = add_output_port("After Click", PORT_TYPE_SIGNAL)
+
+ target = add_input_port("Target", PORT_TYPE_ATOM)
+ swap_hands = add_input_port("Swap Hands", PORT_TYPE_SIGNAL)
+ swapped_hands = add_output_port("Swapped Hands", PORT_TYPE_SIGNAL)
+
+ alt_click = add_input_port("Alt Click", PORT_TYPE_NUMBER)
+ shift_click = add_input_port("Shift Click", PORT_TYPE_NUMBER)
+ ctrl_click = add_input_port("Ctrl Click", PORT_TYPE_NUMBER)
+
+ get_hands = add_input_port("Get Currently Held Items", PORT_TYPE_SIGNAL)
+ active_hand = add_output_port("Active Hand", PORT_TYPE_ATOM)
+ inactive_hand = add_output_port("Inactive Hand", PORT_TYPE_ATOM)
+ fetched_hands = add_output_port("Fetched Held Items", PORT_TYPE_SIGNAL)
+
+/obj/item/circuit_component/mod_adapter_core/input_received(datum/port/input/port)
+ . = ..()
+
+ var/mob/living/carbon/wearer = attached_module?.mod?.wearer
+ if (!istype(wearer))
+ return
+
+ if (COMPONENT_TRIGGERED_BY(get_hands, port) && attached_module.mod.gauntlets?.loc == wearer)
+ active_hand.set_output(wearer.get_active_held_item())
+ active_hand.set_output(wearer.get_inactive_held_item())
+ fetched_hands.set_output(COMPONENT_SIGNAL)
+ return
+
+ if (COMPONENT_TRIGGERED_BY(swap_hands, port) && attached_module.mod.gauntlets?.loc == wearer)
+ wearer.swap_hand()
+ swapped_hands.set_output(COMPONENT_SIGNAL)
+ return
+
+ if (COOLDOWN_FINISHED(src, move_cooldown) && attached_module.mod.boots?.loc == wearer)
+ var/move_dir = null
+
+ if (COMPONENT_TRIGGERED_BY(north, port))
+ move_dir = NORTH
+ if (COMPONENT_TRIGGERED_BY(east, port))
+ move_dir = EAST
+ if (COMPONENT_TRIGGERED_BY(south, port))
+ move_dir = SOUTH
+ if (COMPONENT_TRIGGERED_BY(west, port))
+ move_dir = WEST
+
+ if (!isnull(move_dir))
+ COOLDOWN_START(src, move_cooldown, wearer.cached_multiplicative_slowdown)
+ if (!isnull(wearer.client))
+ COOLDOWN_START(wearer.client, move_delay, wearer.cached_multiplicative_slowdown)
+ if (wearer.Move(get_step(get_turf(wearer), NORTH)))
+ moved.set_output(COMPONENT_SIGNAL)
+ else
+ move_fail.set_output(COMPONENT_SIGNAL)
+ return
+
+ if (!COOLDOWN_FINISHED(src, click_cooldown) || attached_module.mod.gauntlets?.loc != wearer)
+ return
+
+ if (!COMPONENT_TRIGGERED_BY(left_click, port) && !COMPONENT_TRIGGERED_BY(middle_click, port) && !COMPONENT_TRIGGERED_BY(right_click, port))
+ return
+
+ var/list/modifiers = list()
+
+ if (alt_click.value)
+ modifiers[ALT_CLICK] = TRUE
+ if (shift_click.value)
+ modifiers[SHIFT_CLICK] = TRUE
+ if (ctrl_click.value)
+ modifiers[CTRL_CLICK] = TRUE
+ if (COMPONENT_TRIGGERED_BY(left_click, port))
+ modifiers[LEFT_CLICK] = TRUE
+ if (COMPONENT_TRIGGERED_BY(middle_click, port))
+ modifiers[MIDDLE_CLICK] = TRUE
+ if (COMPONENT_TRIGGERED_BY(right_click, port))
+ modifiers[RIGHT_CLICK] = TRUE
+
+ COOLDOWN_START(src, click_cooldown, click_delay)
+ INVOKE_ASYNC(src, PROC_REF(do_click), target.value, list2params(modifiers))
+
+/obj/item/circuit_component/mod_adapter_core/proc/do_click(atom/target, params)
+ var/mob/living/carbon/wearer = attached_module?.mod?.wearer
+ if (!istype(wearer))
+ return
+ wearer.ClickOn(target, params)
+ clicked.set_output(COMPONENT_SIGNAL)
diff --git a/maplestation_modules/code/modules/wiremod/components/screen.dm b/maplestation_modules/code/modules/wiremod/components/screen.dm
new file mode 100644
index 000000000000..ca6864906ad6
--- /dev/null
+++ b/maplestation_modules/code/modules/wiremod/components/screen.dm
@@ -0,0 +1,42 @@
+/**
+ * # Screen component
+ *
+ * Displays text when examined
+ * Can flash the message to all viewers
+ * Returns entity when examined
+ */
+/obj/item/circuit_component/screen
+ display_name = "Screen"
+ desc = "A component that displays information. Activating the component will make it flash the message to all nearby viewers."
+ category = "Entity"
+
+ /// The input port
+ var/datum/port/input/input_port
+
+ /// Entity that examined the circuit
+ var/datum/port/output/viewer
+ var/datum/port/output/observed
+
+ circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL
+
+/obj/item/circuit_component/screen/populate_ports()
+ input_port = add_input_port("Display text", PORT_TYPE_STRING)
+ viewer = add_output_port("Viewer", PORT_TYPE_ATOM)
+ observed = add_output_port("Observed", PORT_TYPE_SIGNAL)
+
+/obj/item/circuit_component/screen/input_received(datum/port/input/port)
+ var/atom/owner = parent.shell || parent
+ owner.visible_message("[icon2html(owner, viewers(owner))] [owner] flashes \"[span_notice(input_port.value)]\" on its screen.")
+
+/obj/item/circuit_component/screen/register_shell(atom/movable/shell)
+ RegisterSignal(shell, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+
+/obj/item/circuit_component/screen/unregister_shell(atom/movable/shell)
+ UnregisterSignal(shell, COMSIG_ATOM_EXAMINE)
+
+/obj/item/circuit_component/screen/proc/on_examine(atom/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+
+ examine_list += span_notice("It's screen is displaying \"[input_port.value]\"")
+ viewer.set_output(user)
+ observed.set_output(COMPONENT_SIGNAL)
diff --git a/maplestation_modules/code/modules/wiremod/components/tile_scanner.dm b/maplestation_modules/code/modules/wiremod/components/tile_scanner.dm
new file mode 100644
index 000000000000..aa2ed186b464
--- /dev/null
+++ b/maplestation_modules/code/modules/wiremod/components/tile_scanner.dm
@@ -0,0 +1,43 @@
+/*
+ * # Tile Scanner component
+ *
+ * Outputs a list of all atoms on a given tile, offset from circuit's position
+ */
+
+/obj/item/circuit_component/tile_scanner
+ display_name = "Tile Scanner"
+ desc = "A component that scans a tile based on an offset from the shell."
+ category = "Sensor"
+
+ var/datum/port/input/x_pos
+ var/datum/port/input/y_pos
+
+ var/scan_delay = 0.5 SECONDS
+ COOLDOWN_DECLARE(scan_cooldown)
+
+ var/datum/port/output/scanned_atoms
+
+ circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL
+
+
+/obj/item/circuit_component/tile_scanner/populate_ports()
+ x_pos = add_input_port("X offset", PORT_TYPE_NUMBER)
+ y_pos = add_input_port("Y offset", PORT_TYPE_NUMBER)
+ scanned_atoms = add_output_port("Scanned Objects", PORT_TYPE_LIST(PORT_TYPE_ATOM))
+ trigger_output = add_output_port("Triggered", PORT_TYPE_SIGNAL, order = 2)
+
+/obj/item/circuit_component/tile_scanner/input_received(datum/port/input/port, list/return_values)
+ if (!COOLDOWN_FINISHED(src, scan_cooldown))
+ return
+
+ if (isnull(x_pos.value) || isnull(y_pos.value))
+ return
+
+ var/turf/target_turf = locate(parent.shell.x + x_pos.value, parent.shell.y + y_pos.value, parent.shell.z)
+ if (!target_turf)
+ return
+
+ var/list/result = list(target_turf) + target_turf.contents
+ COOLDOWN_START(src, scan_cooldown, scan_delay)
+ scanned_atoms.set_output(result)
+ trigger_output.set_output(COMPONENT_SIGNAL)
diff --git a/tgui/packages/tgui/interfaces/ComponentPrinter.tsx b/tgui/packages/tgui/interfaces/ComponentPrinter.tsx
index 7c8f16a8c535..0e6fddc4a1e1 100644
--- a/tgui/packages/tgui/interfaces/ComponentPrinter.tsx
+++ b/tgui/packages/tgui/interfaces/ComponentPrinter.tsx
@@ -77,7 +77,9 @@ const Recipe = (props: RecipeProps) => {
const canPrint = !Object.entries(design.cost).some(
([material, amount]) =>
- !available[material] || amount > (available[material] ?? 0),
+ !available[material] ||
+ amount > (available[material] ?? 0) ||
+ !!design.print_error,
);
return (
@@ -93,6 +95,31 @@ const Recipe = (props: RecipeProps) => {