diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index cb2ff740a55..136f42a77eb 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -1393,3 +1393,11 @@ #define COMSIG_VACUUM_BAG_ATTACH "comsig_vacuum_bag_attach" /// Called on a bag being detached from a vacuum parent #define COMSIG_VACUUM_BAG_DETACH "comsig_vacuum_bag_detach" + +// Organ signals +/// Called on the organ when it is implanted into someone (mob/living/carbon/receiver) +#define COMSIG_ORGAN_IMPLANTED "comsig_organ_implanted" + +/// Called on the organ when it is removed from someone (mob/living/carbon/old_owner) +#define COMSIG_ORGAN_REMOVED "comsig_organ_removed" + diff --git a/code/__DEFINES/wiremod.dm b/code/__DEFINES/wiremod.dm index 807270544cc..07278ad0f7d 100644 --- a/code/__DEFINES/wiremod.dm +++ b/code/__DEFINES/wiremod.dm @@ -138,3 +138,5 @@ #define CIRCUIT_FLAG_INPUT_SIGNAL (1<<0) /// Creates an output trigger that sends a pulse whenever the component is successfully triggered #define CIRCUIT_FLAG_OUTPUT_SIGNAL (1<<1) +/// This circuit component does not show in the menu. +#define CIRCUIT_FLAG_HIDDEN (1<<2) diff --git a/code/modules/research/designs/wiremod_designs.dm b/code/modules/research/designs/wiremod_designs.dm index b7734cfd4c5..a8200913a3d 100644 --- a/code/modules/research/designs/wiremod_designs.dm +++ b/code/modules/research/designs/wiremod_designs.dm @@ -321,3 +321,23 @@ build_path = /obj/item/shell/airlock build_type = PROTOLATHE | COMPONENT_PRINTER category = list("Circuitry", "Shells") + +/datum/design/bci_shell + name = "Brain-Computer Interface Shell" + desc = "An implant that can be placed in a user's head to control circuits using their brain." + id = "bci_shell" + materials = list( + /datum/material/glass = 2000, + /datum/material/iron = 8000, + ) + build_path = /obj/item/shell/bci + build_type = PROTOLATHE | COMPONENT_PRINTER + category = list("Circuitry", "Shells") + +/datum/design/board/bci_implanter + name = "Brain-Computer Interface Manipulation Chamber" + desc = "A machine that, when given a brain-computer interface, will implant it into an occupant. Otherwise, will remove any brain-computer interfaces they already have." + id = "bci_implanter" + build_path = /obj/item/circuitboard/machine/bci_implanter + build_type = IMPRINTER | COMPONENT_PRINTER + category = list("Circuitry", "Core") diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm index 6905d92ebe7..da16d8e1d80 100644 --- a/code/modules/research/techweb/all_nodes.dm +++ b/code/modules/research/techweb/all_nodes.dm @@ -656,6 +656,8 @@ description = "Grants access to more complicated shell designs." prereq_ids = list("basic_circuitry", "engineering") design_ids = list( + "bci_implanter", + "bci_shell", "bot_shell", "door_shell", "controller_shell", diff --git a/code/modules/surgery/organs/organ_internal.dm b/code/modules/surgery/organs/organ_internal.dm index 0441a7ba465..cf251c74d67 100644 --- a/code/modules/surgery/organs/organ_internal.dm +++ b/code/modules/surgery/organs/organ_internal.dm @@ -61,6 +61,7 @@ else qdel(replaced) + SEND_SIGNAL(src, COMSIG_ORGAN_IMPLANTED, reciever) SEND_SIGNAL(reciever, COMSIG_CARBON_GAIN_ORGAN, src, special) owner = reciever @@ -92,6 +93,7 @@ for(var/datum/action/action as anything in actions) action.Remove(organ_owner) + SEND_SIGNAL(src, COMSIG_ORGAN_REMOVED, organ_owner) SEND_SIGNAL(organ_owner, COMSIG_CARBON_LOSE_ORGAN, src, special) START_PROCESSING(SSobj, src) diff --git a/code/modules/wiremod/integrated_circuit.dm b/code/modules/wiremod/integrated_circuit.dm index b1415f682b7..b203be6411e 100644 --- a/code/modules/wiremod/integrated_circuit.dm +++ b/code/modules/wiremod/integrated_circuit.dm @@ -224,6 +224,10 @@ . = list() .["components"] = list() for(var/obj/item/circuit_component/component as anything in attached_components) + if (component.circuit_flags & CIRCUIT_FLAG_HIDDEN) + .["components"] += null + continue + var/list/component_data = list() component_data["input_ports"] = list() for(var/datum/port/input/port as anything in component.input_ports) diff --git a/code/modules/wiremod/shell/brain_computer_interface.dm b/code/modules/wiremod/shell/brain_computer_interface.dm new file mode 100644 index 00000000000..a97b6dad692 --- /dev/null +++ b/code/modules/wiremod/shell/brain_computer_interface.dm @@ -0,0 +1,545 @@ +/obj/item/organ/cyberimp/bci + name = "brain-computer interface" + desc = "An implant that can be placed in a user's head to control circuits using their brain." + icon = 'icons/obj/wiremod.dmi' + icon_state = "bci" + zone = BODY_ZONE_HEAD + w_class = WEIGHT_CLASS_TINY + +/obj/item/organ/cyberimp/bci/Initialize() + . = ..() + + AddComponent(/datum/component/shell, list( + new /obj/item/circuit_component/bci_core, + new /obj/item/circuit_component/bci_action("One"), + new /obj/item/circuit_component/bci_action("Two"), + new /obj/item/circuit_component/bci_action("Three"), + ), SHELL_CAPACITY_SMALL) + +/obj/item/organ/cyberimp/bci/Insert(mob/living/carbon/reciever, special, drop_if_replaced) + . = ..() + + // Organs are put in nullspace, but this breaks circuit interactions + forceMove(reciever) + +/obj/item/organ/cyberimp/bci/say(message, bubble_type, list/spans, sanitize, datum/language/language, ignore_spam, forced) + if (owner) + // Otherwise say_dead will be called. + // It's intentional that a circuit for a dead person does not speak from the shell. + if (owner.stat == DEAD) + return + + owner.say(message, forced = "circuit speech") + else + return ..() + +/obj/item/circuit_component/bci + display_name = "Brain-Computer Interface" + display_desc = "Used to receive inputs for the brain-computer interface. User is presented with three buttons." + +/obj/item/circuit_component/bci_action + display_name = "BCI Action" + display_desc = "Represents an action the user can take when implanted with the brain-computer interface." + + /// The name to use for the button + var/datum/port/input/button_name + + /// Called when the user presses the button + var/datum/port/output/signal + + /// A reference to the action button itself + var/datum/action/innate/bci_action/bci_action + +/obj/item/circuit_component/bci_action/Initialize(mapload, default_icon) + . = ..() + + if (!isnull(default_icon)) + set_option(default_icon) + + button_name = add_input_port("Name", PORT_TYPE_STRING) + + signal = add_output_port("Signal", PORT_TYPE_SIGNAL) + +/obj/item/circuit_component/bci_action/Destroy() + button_name = null + signal = null + + QDEL_NULL(bci_action) + + return ..() + +/obj/item/circuit_component/bci_action/populate_options() + var/static/action_options = list( + "Blank", + + "One", + "Two", + "Three", + "Four", + "Five", + + "Blood", + "Bomb", + "Brain", + "Brain Damage", + "Cross", + "Electricity", + "Exclamation", + "Heart", + "Id", + "Info", + "Injection", + "Magnetism", + "Minus", + "Network", + "Plus", + "Power", + "Question", + "Radioactive", + "Reaction", + "Repair", + "Say", + "Scan", + "Shield", + "Skull", + "Sleep", + "Wireless", + ) + + options = action_options + +/obj/item/circuit_component/bci_action/register_shell(atom/movable/shell) + var/obj/item/organ/cyberimp/bci/bci = shell + + bci_action = new(src) + update_action() + + bci.actions += list(bci_action) + +/obj/item/circuit_component/bci_action/unregister_shell(atom/movable/shell) + var/obj/item/organ/cyberimp/bci/bci = shell + + bci.actions -= bci_action + QDEL_NULL(bci_action) + +/obj/item/circuit_component/bci_action/input_received(datum/port/input/port) + . = ..() + + if (.) + return + + if (!isnull(bci_action)) + update_action() + +/obj/item/circuit_component/bci_action/proc/update_action() + bci_action.name = button_name.input_value + bci_action.button_icon_state = "nanite_[replacetextEx(lowertext(current_option), " ", "_")]" + +/datum/action/innate/bci_action + name = "Action" + icon_icon = 'icons/mob/actions/actions_items.dmi' + check_flags = AB_CHECK_CONSCIOUS + button_icon_state = "nanite_power" + + var/obj/item/circuit_component/bci_action/circuit_component + +/datum/action/innate/bci_action/New(obj/item/circuit_component/bci_action/circuit_component) + ..() + + src.circuit_component = circuit_component + +/datum/action/innate/bci_action/Destroy() + circuit_component.bci_action = null + circuit_component = null + + return ..() + +/datum/action/innate/bci_action/Activate() + circuit_component.signal.set_output(COMPONENT_SIGNAL) + +/obj/item/circuit_component/bci_core + display_name = "BCI Core" + display_desc = "Controls the core operations of the BCI." + + /// A reference to the action button to look at charge/get info + var/datum/action/innate/bci_charge_action/charge_action + + var/datum/port/input/message + var/datum/port/input/send_message_signal + + var/datum/port/output/user_port + + var/datum/weakref/user + +/obj/item/circuit_component/bci_core/Initialize() + . = ..() + + message = add_input_port("Message", PORT_TYPE_STRING) + send_message_signal = add_input_port("Send Message", PORT_TYPE_SIGNAL) + + user_port = add_output_port("User", PORT_TYPE_ATOM) + +/obj/item/circuit_component/bci_core/Destroy() + QDEL_NULL(charge_action) + + message = null + send_message_signal = null + + user_port = null + + user = null + + return ..() + +/obj/item/circuit_component/bci_core/register_shell(atom/movable/shell) + var/obj/item/organ/cyberimp/bci/bci = shell + + charge_action = new(src) + bci.actions += list(charge_action) + + RegisterSignal(shell, COMSIG_ORGAN_IMPLANTED, .proc/on_organ_implanted) + RegisterSignal(shell, COMSIG_ORGAN_REMOVED, .proc/on_organ_removed) + +/obj/item/circuit_component/bci_core/unregister_shell(atom/movable/shell) + var/obj/item/organ/cyberimp/bci/bci = shell + + bci.actions -= charge_action + QDEL_NULL(charge_action) + + UnregisterSignal(shell, list( + COMSIG_ORGAN_IMPLANTED, + COMSIG_ORGAN_REMOVED, + )) + +/obj/item/circuit_component/bci_core/input_received(datum/port/input/port) + . = ..() + if (.) + return . + + if (COMPONENT_TRIGGERED_BY(send_message_signal, port)) + var/sent_message = trim(message.input_value) + if (!sent_message) + return + + var/mob/living/carbon/resolved_owner = user?.resolve() + if (isnull(resolved_owner)) + return + + if (resolved_owner.stat == DEAD) + return + + to_chat(resolved_owner, "You hear a strange, robotic voice in your head... \"[span_robot("[html_encode(sent_message)]")]\"") + +/obj/item/circuit_component/bci_core/proc/on_organ_implanted(datum/source, mob/living/carbon/owner) + SIGNAL_HANDLER + + user_port.set_output(owner) + user = WEAKREF(owner) + + RegisterSignal(owner, COMSIG_PROCESS_BORGCHARGER_OCCUPANT, .proc/on_borg_charge) + RegisterSignal(owner, COMSIG_LIVING_ELECTROCUTE_ACT, .proc/on_electrocute) + +/obj/item/circuit_component/bci_core/proc/on_organ_removed(datum/source, mob/living/carbon/owner) + SIGNAL_HANDLER + + user_port.set_output(null) + user = null + + UnregisterSignal(owner, list( + COMSIG_PROCESS_BORGCHARGER_OCCUPANT, + COMSIG_LIVING_ELECTROCUTE_ACT, + )) + +/obj/item/circuit_component/bci_core/proc/on_borg_charge(datum/source, amount) + SIGNAL_HANDLER + + if (isnull(parent.cell)) + return + + parent.cell.give(amount) + +/obj/item/circuit_component/bci_core/proc/on_electrocute(datum/source, shock_damage, siemens_coefficient, flags) + SIGNAL_HANDLER + + if (isnull(parent.cell)) + return + + if (flags & SHOCK_ILLUSION) + return + + parent.cell.give(shock_damage * 2) + to_chat(source, span_notice("You absorb some of the shock into your [parent.name]!")) + +/datum/action/innate/bci_charge_action + name = "Check BCI Charge" + check_flags = NONE + icon_icon = 'icons/obj/power.dmi' + button_icon_state = "cell" + + var/obj/item/circuit_component/bci_core/circuit_component + +/datum/action/innate/bci_charge_action/New(obj/item/circuit_component/bci_core/circuit_component) + ..() + + src.circuit_component = circuit_component + + button.maptext_x = 2 + button.maptext_y = 0 + update_maptext() + + START_PROCESSING(SSobj, src) + +/datum/action/innate/bci_charge_action/Destroy() + circuit_component.charge_action = null + circuit_component = null + + STOP_PROCESSING(SSobj, src) + + return ..() + +/datum/action/innate/bci_charge_action/Trigger() + var/obj/item/stock_parts/cell/cell = circuit_component.parent.cell + + if (isnull(cell)) + to_chat(owner, span_boldwarning("[circuit_component.parent] has no power cell.")) + else + to_chat(owner, span_info("[circuit_component.parent]'s [cell.name] has [cell.percent()]% charge left.")) + to_chat(owner, span_info("You can recharge it by using a cyborg recharging station.")) + +/datum/action/innate/bci_charge_action/process(delta_time) + update_maptext() + +/datum/action/innate/bci_charge_action/proc/update_maptext() + var/obj/item/stock_parts/cell/cell = circuit_component.parent.cell + button.maptext = cell ? MAPTEXT("[cell.percent()]%") : "" + +/obj/machinery/bci_implanter + name = "brain-computer interface manipulation chamber" + desc = "A machine that, when given a brain-computer interface, will implant it into an occupant. Otherwise, will remove any brain-computer interfaces they already have." + circuit = /obj/item/circuitboard/machine/bci_implanter + icon = 'icons/obj/machines/nanite_chamber.dmi' + icon_state = "nanite_chamber" + base_icon_state = "nanite_chamber" + layer = ABOVE_WINDOW_LAYER + use_power = IDLE_POWER_USE + anchored = TRUE + density = TRUE + obj_flags = NO_BUILD // Becomes undense when the door is open + idle_power_usage = 50 + active_power_usage = 300 + + var/busy = FALSE + var/busy_icon_state + var/locked = FALSE + + var/datum/weakref/bci_to_implant + + COOLDOWN_DECLARE(message_cooldown) + +/obj/machinery/bci_implanter/Initialize() + . = ..() + occupant_typecache = typecacheof(/mob/living/carbon) + +/obj/machinery/bci_implanter/Destroy() + QDEL_NULL(bci_to_implant) + return ..() + +/obj/machinery/bci_implanter/examine(mob/user) + . = ..() + + if (isnull(bci_to_implant?.resolve())) + . += span_notice("There is no BCI inserted.") + else + . += span_notice("Right-click to remove current BCI.") + +/obj/machinery/bci_implanter/proc/set_busy(status, working_icon) + busy = status + busy_icon_state = working_icon + update_appearance() + +/obj/machinery/bci_implanter/update_icon_state() + if (occupant) + icon_state = busy ? busy_icon_state : "[base_icon_state]_occupied" + return ..() + icon_state = "[base_icon_state][state_open ? "_open" : null]" + return ..() + +/obj/machinery/bci_implanter/update_overlays() + var/list/overlays = ..() + + if ((machine_stat & MAINT) || panel_open) + overlays += "maint" + return overlays + + if (machine_stat & (NOPOWER|BROKEN)) + return overlays + + if (busy || locked) + overlays += "red" + if (locked) + overlays += "bolted" + return overlays + + overlays += "green" + + return overlays + +/obj/machinery/bci_implanter/attack_hand_secondary(mob/user, list/modifiers) + . = ..() + if (. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN) + return . + + if (locked) + balloon_alert(user, "it's locked!") + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + var/obj/item/organ/cyberimp/bci/bci_to_implant_resolved = bci_to_implant?.resolve() + if (isnull(bci_to_implant_resolved) && user.Adjacent(src)) + balloon_alert(user, "no bci inserted!") + else + user.put_in_hands(bci_to_implant_resolved) + balloon_alert(user, "ejected bci") + + bci_to_implant = null + + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/machinery/bci_implanter/attackby(obj/item/weapon, mob/user, params) + var/obj/item/organ/cyberimp/bci/new_bci = weapon + if (istype(new_bci)) + if (!(locate(/obj/item/integrated_circuit) in new_bci)) + balloon_alert(user, "bci has no circuit!") + return + + var/obj/item/organ/cyberimp/bci/previous_bci_to_implant = bci_to_implant?.resolve() + + bci_to_implant = WEAKREF(weapon) + weapon.forceMove(src) + + if (isnull(previous_bci_to_implant)) + balloon_alert(user, "inserted bci") + else + balloon_alert(user, "swapped bci") + user.put_in_hands(previous_bci_to_implant) + + return + + return ..() + +/obj/machinery/bci_implanter/attackby_secondary(obj/item/weapon, mob/user, params) + if (!occupant && default_deconstruction_screwdriver(user, icon_state, icon_state, weapon)) + update_appearance() + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + if (default_pry_open(weapon)) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + if (default_deconstruction_crowbar(weapon)) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/machinery/bci_implanter/proc/start_process() + if (machine_stat & (NOPOWER|BROKEN)) + return + if ((machine_stat & MAINT) || panel_open) + return + if (!occupant || busy) + return + + var/locked_state = locked + locked = TRUE + + set_busy(TRUE, "[initial(icon_state)]_raising") + addtimer(CALLBACK(src, .proc/set_busy, TRUE, "[initial(icon_state)]_active"), 1 SECONDS) + addtimer(CALLBACK(src, .proc/set_busy, TRUE, "[initial(icon_state)]_falling"), 2 SECONDS) + addtimer(CALLBACK(src, .proc/complete_process, locked_state), 3 SECONDS) + +/obj/machinery/bci_implanter/proc/complete_process(locked_state) + locked = locked_state + set_busy(FALSE) + + var/mob/living/carbon/carbon_occupant = occupant + if (!istype(carbon_occupant)) + return + + playsound(loc, 'sound/machines/ping.ogg', 30, FALSE) + + var/obj/item/organ/cyberimp/bci/bci_organ = carbon_occupant.getorgan(/obj/item/organ/cyberimp/bci) + var/obj/item/organ/cyberimp/bci/bci_to_implant_resolved = bci_to_implant?.resolve() + + if (bci_organ) + bci_organ.Remove(carbon_occupant) + + if (isnull(bci_to_implant_resolved)) + say("Occupant's previous brain-computer interface has been transferred to internal storage unit.") + bci_organ.forceMove(src) + bci_to_implant = WEAKREF(bci_organ) + else + say("Occupant's previous brain-computer interface has been ejected.") + bci_organ.forceMove(drop_location()) + else if (!isnull(bci_to_implant_resolved)) + say("Occupant has been injected with [bci_to_implant_resolved].") + bci_to_implant_resolved.Insert(carbon_occupant) + bci_to_implant = null + +/obj/machinery/bci_implanter/open_machine() + if(state_open) + return FALSE + + ..() + + return TRUE + +/obj/machinery/bci_implanter/close_machine(mob/living/carbon/user) + if(!state_open) + return FALSE + + ..() + + var/mob/living/carbon/carbon_occupant = occupant + if (istype(occupant)) + var/obj/item/organ/cyberimp/bci/existing_bci_organ = carbon_occupant.getorgan(/obj/item/organ/cyberimp/bci) + if (isnull(existing_bci_organ) && isnull(bci_to_implant?.resolve())) + say("No brain-computer interface inserted, and occupant does not have one. Insert a BCI to implant one.") + playsound(src, 'sound/machines/buzz-sigh.ogg', 30, TRUE) + return FALSE + + addtimer(CALLBACK(src, .proc/start_process), 1 SECONDS) + return TRUE + +/obj/machinery/bci_implanter/relaymove(mob/living/user, direction) + var/message + + if (locked) + message = "it won't budge!" + else if (user.stat != CONSCIOUS) + message = "you don't have the energy!" + + if (!isnull(message)) + if (COOLDOWN_FINISHED(src, message_cooldown)) + COOLDOWN_START(src, message_cooldown, 5 SECONDS) + balloon_alert(user, "it won't budge!") + + return + + open_machine() + +/obj/machinery/bci_implanter/interact(mob/user) + if (state_open) + close_machine(null, user) + return + else if (locked) + balloon_alert(user, "it's locked!") + return + + open_machine() + +/obj/item/circuitboard/machine/bci_implanter + name = "Brain-Computer Interface Manipulation Chamber (Machine Board)" + greyscale_colors = CIRCUIT_COLOR_SCIENCE + build_path = /obj/machinery/bci_implanter + req_components = list( + /obj/item/stock_parts/micro_laser = 2, + /obj/item/stock_parts/manipulator = 1, + ) diff --git a/code/modules/wiremod/shell/shell_items.dm b/code/modules/wiremod/shell/shell_items.dm index 6f2abdfd12b..95e70d28ea0 100644 --- a/code/modules/wiremod/shell/shell_items.dm +++ b/code/modules/wiremod/shell/shell_items.dm @@ -52,3 +52,8 @@ icon_state = "construction" shell_to_spawn = /obj/machinery/door/airlock/shell screw_delay = 10 SECONDS + +/obj/item/shell/bci + name = "brain-computer interface assembly" + icon_state = "bci-open" + shell_to_spawn = /obj/item/organ/cyberimp/bci diff --git a/icons/obj/wiremod.dmi b/icons/obj/wiremod.dmi index e4f9afc8681..6e6fec0e55e 100644 Binary files a/icons/obj/wiremod.dmi and b/icons/obj/wiremod.dmi differ diff --git a/tgstation.dme b/tgstation.dme index e8a1f8bf1fd..50b0b51f978 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -3693,6 +3693,7 @@ #include "code\modules\wiremod\preset\speech_relay.dm" #include "code\modules\wiremod\shell\airlock.dm" #include "code\modules\wiremod\shell\bot.dm" +#include "code\modules\wiremod\shell\brain_computer_interface.dm" #include "code\modules\wiremod\shell\compact_remote.dm" #include "code\modules\wiremod\shell\controller.dm" #include "code\modules\wiremod\shell\drone.dm" diff --git a/tgui/packages/tgui/interfaces/IntegratedCircuit/index.js b/tgui/packages/tgui/interfaces/IntegratedCircuit/index.js index a28f880d39c..3667c66f9fd 100644 --- a/tgui/packages/tgui/interfaces/IntegratedCircuit/index.js +++ b/tgui/packages/tgui/interfaces/IntegratedCircuit/index.js @@ -232,7 +232,7 @@ export class IntegratedCircuit extends Component { backgroundImage={resolveAsset('grid_background.png')} imageWidth={900} > - {components.map((comp, index) => ( + {components.map((comp, index) => comp && ( { const connections = []; for (const comp of components) { + if (comp === null) { + continue; + } + for (const port of comp.input_ports) { if (port.connected_to === NULL_REF) continue; const output_port = locations[port.connected_to];