diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index d59ce9f34c9..8b3660c2da9 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -93,7 +93,7 @@
#define COMSIG_ATOM_HULK_ATTACK "hulk_attack"
///from base of atom/animal_attack(): (/mob/user)
#define COMSIG_ATOM_ATTACK_ANIMAL "attack_animal"
-///from base of atom/examine(): (/mob)
+///from base of atom/examine(): (/mob, list/examine_text)
#define COMSIG_PARENT_EXAMINE "atom_examine"
///from base of atom/get_examine_name(): (/mob, list/overrides)
#define COMSIG_ATOM_GET_EXAMINE_NAME "atom_examine_name"
@@ -1310,5 +1310,33 @@
/// Cancels adding the component to the circuit.
#define COMPONENT_CANCEL_ADD_COMPONENT (1<<0)
+/// Sent when a [/obj/item/circuit_component] is added to a circuit manually, by putting the item inside directly.
+/// Accepts COMPONENT_CANCEL_ADD_COMPONENT.
+#define COMSIG_CIRCUIT_ADD_COMPONENT_MANUALLY "circuit_add_component_manually"
+
+/// Sent when a circuit is removed from its shell
+#define COMSIG_CIRCUIT_SHELL_REMOVED "circuit_shell_removed"
+
+/// Sent to [/obj/item/circuit_component] when it is removed from a circuit. (/obj/item/integrated_circuit)
+#define COMSIG_CIRCUIT_COMPONENT_REMOVED "circuit_component_removed"
+
+/// Sent to an atom when a [/obj/item/usb_cable] attempts to connect to something. (/obj/item/usb_cable/usb_cable, /mob/user)
+#define COMSIG_ATOM_USB_CABLE_TRY_ATTACH "usb_cable_try_attach"
+ /// Attaches the USB cable to the atom. If the USB cables moves away, it will disconnect.
+ #define COMSIG_USB_CABLE_ATTACHED (1<<0)
+
+ /// Attaches the USB cable to a circuit. Producers of this are expected to set the usb_cable's
+ /// `attached_circuit` variable.
+ #define COMSIG_USB_CABLE_CONNECTED_TO_CIRCUIT (1<<1)
+
+ /// Cancels the attack chain, but without performing any other action.
+ #define COMSIG_CANCEL_USB_CABLE_ATTACK (1<<2)
+
+/// Sent from /obj/structure/industrial_lift/tram when its travelling status updates. (travelling)
+#define COMSIG_TRAM_SET_TRAVELLING "tram_set_travelling"
+
+/// Sent from /obj/structure/industrial_lift/tram when it begins to travel. (obj/effect/landmark/tram/from_where, obj/effect/landmark/tram/to_where)
+#define COMSIG_TRAM_TRAVEL "tram_travel"
+
/// Called in /obj/structure/moneybot/add_money(). (to_add)
#define COMSIG_MONEYBOT_ADD_MONEY "moneybot_add_money"
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index 0c8292b9857..b92f3c4d13b 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -2,6 +2,9 @@
#define in_range(source, user) (get_dist(source, user) <= 1 && (get_step(source, 0)?:z) == (get_step(user, 0)?:z))
+/// Within given range, but not counting z-levels
+#define IN_GIVEN_RANGE(source, other, given_range) (get_dist(source, other) <= given_range && (get_step(source, 0)?:z) == (get_step(other, 0)?:z))
+
#define isatom(A) (isloc(A))
#define isweakref(D) (istype(D, /datum/weakref))
diff --git a/code/__DEFINES/wiremod.dm b/code/__DEFINES/wiremod.dm
index b6e2a70d6b9..752d915fc93 100644
--- a/code/__DEFINES/wiremod.dm
+++ b/code/__DEFINES/wiremod.dm
@@ -92,12 +92,18 @@
/// Whether the shell needs to be anchored for the circuit to be on.
#define SHELL_FLAG_REQUIRE_ANCHOR (1<<1)
+/// Whether or not the shell has a USB port.
+#define SHELL_FLAG_USB_PORT (1<<2)
+
// Shell capacities. These can be converted to configs very easily later
#define SHELL_CAPACITY_SMALL 10
#define SHELL_CAPACITY_MEDIUM 25
#define SHELL_CAPACITY_LARGE 50
#define SHELL_CAPACITY_VERY_LARGE 500
+/// The maximum range a USB cable can be apart from a source
+#define USB_CABLE_MAX_RANGE 2
+
// Circuit flags
/// Creates an input trigger that means the component won't be triggered unless the trigger is pulsed.
#define CIRCUIT_FLAG_INPUT_SIGNAL (1<<0)
diff --git a/code/datums/components/shell.dm b/code/datums/components/shell.dm
index acea9746714..00e08e846a2 100644
--- a/code/datums/components/shell.dm
+++ b/code/datums/components/shell.dm
@@ -6,7 +6,7 @@
var/obj/item/integrated_circuit/attached_circuit
/// Flags containing what this shell can do
- var/shell_flags = 0
+ var/shell_flags = NONE
/// The capacity of the shell.
var/capacity = INFINITY
@@ -38,6 +38,7 @@
RegisterSignal(parent, COMSIG_OBJ_DECONSTRUCT, .proc/on_object_deconstruct)
if(shell_flags & SHELL_FLAG_REQUIRE_ANCHOR)
RegisterSignal(parent, COMSIG_OBJ_DEFAULT_UNFASTEN_WRENCH, .proc/on_unfasten)
+ RegisterSignal(parent, COMSIG_ATOM_USB_CABLE_TRY_ATTACH, .proc/on_atom_usb_cable_try_attach)
/datum/component/shell/UnregisterFromParent()
@@ -48,7 +49,8 @@
COMSIG_OBJ_DECONSTRUCT,
COMSIG_OBJ_DEFAULT_UNFASTEN_WRENCH,
COMSIG_PARENT_EXAMINE,
- COMSIG_ATOM_ATTACK_GHOST
+ COMSIG_ATOM_ATTACK_GHOST,
+ COMSIG_ATOM_USB_CABLE_TRY_ATTACH,
))
QDEL_NULL(attached_circuit)
@@ -77,6 +79,8 @@
var/obj/item/stock_parts/cell/cell = attached_circuit.cell
examine_text += span_notice("The charge meter reads [cell ? round(cell.percent(), 1) : 0]%.")
+ if (shell_flags & SHELL_FLAG_USB_PORT)
+ examine_text += span_notice("There is a USB port on the front.")
/**
* Called when the shell is wrenched.
@@ -163,8 +167,9 @@
SIGNAL_HANDLER
remove_circuit()
-/datum/component/shell/proc/on_circuit_add_component(datum/source, obj/item/circuit_component/added_comp)
+/datum/component/shell/proc/on_circuit_add_component_manually(datum/source, obj/item/circuit_component/added_comp)
SIGNAL_HANDLER
+
return COMPONENT_CANCEL_ADD_COMPONENT
/**
@@ -180,7 +185,7 @@
for(var/obj/item/circuit_component/to_add as anything in unremovable_circuit_components)
to_add.forceMove(attached_circuit)
attached_circuit.add_component(to_add)
- RegisterSignal(circuitboard, COMSIG_CIRCUIT_ADD_COMPONENT, .proc/on_circuit_add_component)
+ RegisterSignal(circuitboard, COMSIG_CIRCUIT_ADD_COMPONENT_MANUALLY, .proc/on_circuit_add_component_manually)
attached_circuit.set_shell(parent)
if(shell_flags & SHELL_FLAG_REQUIRE_ANCHOR)
@@ -206,3 +211,17 @@
attached_circuit.remove_component(to_remove)
to_remove.moveToNullspace()
attached_circuit = null
+
+/datum/component/shell/proc/on_atom_usb_cable_try_attach(atom/source, obj/item/usb_cable/usb_cable, mob/user)
+ SIGNAL_HANDLER
+
+ if (!(shell_flags & SHELL_FLAG_USB_PORT))
+ source.balloon_alert(user, "this shell has no usb ports")
+ return COMSIG_CANCEL_USB_CABLE_ATTACK
+
+ if (isnull(attached_circuit))
+ source.balloon_alert(user, "no circuit inside")
+ return COMSIG_CANCEL_USB_CABLE_ATTACK
+
+ usb_cable.attached_circuit = attached_circuit
+ return COMSIG_USB_CABLE_CONNECTED_TO_CIRCUIT
diff --git a/code/datums/components/usb_port.dm b/code/datums/components/usb_port.dm
new file mode 100644
index 00000000000..9131e45f42d
--- /dev/null
+++ b/code/datums/components/usb_port.dm
@@ -0,0 +1,162 @@
+/// Opens up a USB port that can be connected to by circuits, creating registerable circuit components
+/datum/component/usb_port
+ /// The component types to create when something plugs in
+ var/list/circuit_component_types
+
+ /// The currently connected circuit
+ var/obj/item/integrated_circuit/attached_circuit
+
+ /// The currently connected USB cable
+ var/datum/weakref/usb_cable_ref
+
+ /// The components inside the parent
+ var/list/obj/item/circuit_component/circuit_components
+
+ /// The beam connecting the USB cable to the machine
+ var/datum/beam/usb_cable_beam
+
+/datum/component/usb_port/Initialize(list/circuit_component_types)
+ if (!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.circuit_component_types = circuit_component_types
+
+/datum/component/usb_port/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ATOM_USB_CABLE_TRY_ATTACH, .proc/on_atom_usb_cable_try_attach)
+ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/on_moved)
+ RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/on_examine)
+
+/datum/component/usb_port/UnregisterFromParent()
+ UnregisterSignal(parent, list(
+ COMSIG_ATOM_USB_CABLE_TRY_ATTACH,
+ COMSIG_MOVABLE_MOVED,
+ COMSIG_PARENT_EXAMINE,
+ ))
+
+ unregister_circuit_signals()
+
+/datum/component/usb_port/Destroy()
+ QDEL_LAZYLIST(circuit_components)
+ QDEL_NULL(usb_cable_beam)
+
+ attached_circuit = null
+ usb_cable_ref = null
+
+ return ..()
+
+/datum/component/usb_port/proc/unregister_circuit_signals()
+ if (isnull(attached_circuit))
+ return
+
+ UnregisterSignal(attached_circuit, list(
+ COMSIG_CIRCUIT_SHELL_REMOVED,
+ COMSIG_PARENT_QDELETING,
+ ))
+
+ var/shell = attached_circuit.shell
+ if (!isnull(shell))
+ UnregisterSignal(shell, COMSIG_MOVABLE_MOVED)
+ UnregisterSignal(shell, COMSIG_PARENT_EXAMINE)
+
+/datum/component/usb_port/proc/create_circuit_components(obj/item/integrated_circuit/circuitboard)
+ var/created_circuit_components = list()
+
+ for(var/circuit_component_type in circuit_component_types)
+ var/obj/item/circuit_component/circuit_component = new circuit_component_type(parent)
+ circuitboard.add_component(circuit_component)
+ created_circuit_components += circuit_component
+ RegisterSignal(circuit_component, COMSIG_CIRCUIT_COMPONENT_REMOVED, .proc/on_circuit_component_removed)
+
+ return created_circuit_components
+
+/datum/component/usb_port/proc/on_examine(datum/source, mob/user, list/examine_text)
+ SIGNAL_HANDLER
+
+ if (isnull(attached_circuit))
+ examine_text += span_notice("There is a USB port on the front.")
+ else
+ examine_text += span_notice("[attached_circuit.shell || attached_circuit] is connected to [parent.p_them()] by a USB port.")
+
+/datum/component/usb_port/proc/on_examine_shell(datum/source, mob/user, list/examine_text)
+ SIGNAL_HANDLER
+
+ examine_text += span_notice("[source.p_they(TRUE)] [source.p_are()] attached to [parent] with a USB cable.")
+
+/datum/component/usb_port/proc/on_atom_usb_cable_try_attach(datum/source, obj/item/usb_cable/connecting_cable, mob/user)
+ SIGNAL_HANDLER
+
+ var/atom/atom_parent = parent
+
+ if (!isnull(attached_circuit))
+ atom_parent.balloon_alert(user, "usb already connected")
+ return COMSIG_CANCEL_USB_CABLE_ATTACK
+
+ if (isnull(connecting_cable.attached_circuit))
+ connecting_cable.balloon_alert(user, "connect to a shell first")
+ return COMSIG_CANCEL_USB_CABLE_ATTACK
+
+ if (!IN_GIVEN_RANGE(connecting_cable.attached_circuit, parent, USB_CABLE_MAX_RANGE))
+ connecting_cable.balloon_alert(user, "too far away")
+ return COMSIG_CANCEL_USB_CABLE_ATTACK
+
+ usb_cable_ref = WEAKREF(connecting_cable)
+ attached_circuit = connecting_cable.attached_circuit
+
+ connecting_cable.forceMove(attached_circuit)
+ circuit_components = create_circuit_components(attached_circuit)
+ attached_circuit.interact(user)
+
+ usb_cable_beam = atom_parent.Beam(attached_circuit.shell, "usb_cable_beam", 'icons/obj/wiremod.dmi')
+
+ RegisterSignal(attached_circuit, COMSIG_CIRCUIT_SHELL_REMOVED, .proc/on_circuit_shell_removed)
+ RegisterSignal(attached_circuit, COMSIG_PARENT_QDELETING, .proc/on_circuit_deleting)
+ RegisterSignal(attached_circuit.shell, COMSIG_MOVABLE_MOVED, .proc/on_moved)
+ RegisterSignal(attached_circuit.shell, COMSIG_PARENT_EXAMINE, .proc/on_examine_shell)
+
+ return COMSIG_USB_CABLE_ATTACHED
+
+/datum/component/usb_port/proc/on_moved()
+ SIGNAL_HANDLER
+
+ if (isnull(attached_circuit))
+ return
+
+ if (IN_GIVEN_RANGE(attached_circuit, parent, USB_CABLE_MAX_RANGE))
+ return
+
+ detach()
+
+/datum/component/usb_port/proc/on_circuit_deleting()
+ SIGNAL_HANDLER
+ unregister_circuit_signals()
+ attached_circuit = null
+ qdel(usb_cable_ref)
+
+/datum/component/usb_port/proc/on_circuit_component_removed(datum/source)
+ SIGNAL_HANDLER
+
+ qdel(source)
+ detach()
+
+/datum/component/usb_port/proc/on_circuit_shell_removed()
+ SIGNAL_HANDLER
+
+ detach()
+
+/datum/component/usb_port/proc/detach()
+ var/obj/item/usb_cable/usb_cable = usb_cable_ref?.resolve()
+ if (isnull(usb_cable))
+ return
+
+ unregister_circuit_signals()
+
+ var/atom/atom_parent = parent
+ usb_cable.forceMove(atom_parent.drop_location())
+ usb_cable.balloon_alert_to_viewers("snap")
+
+ QDEL_LAZYLIST(circuit_components)
+
+ attached_circuit = null
+ usb_cable_ref = null
+
+ QDEL_NULL(usb_cable_beam)
diff --git a/code/game/machinery/computer/tram_controls.dm b/code/game/machinery/computer/tram_controls.dm
index d73f807ee69..5edb3b9895a 100644
--- a/code/game/machinery/computer/tram_controls.dm
+++ b/code/game/machinery/computer/tram_controls.dm
@@ -1,5 +1,3 @@
-
-
/obj/machinery/computer/tram_controls
name = "tram controls"
desc = "An interface for the tram that lets you tell the tram where to go and hopefully it makes it there. I'm here to describe the controls to you, not to inspire confidence."
@@ -12,6 +10,11 @@
var/obj/structure/industrial_lift/tram/tram_part
light_color = LIGHT_COLOR_GREEN
+/obj/machinery/computer/tram_controls/Initialize(mapload, obj/item/circuitboard/C)
+ . = ..()
+ AddComponent(/datum/component/usb_port, list(/obj/item/circuit_component/tram_controls))
+ return INITIALIZE_HINT_LATELOAD
+
/obj/machinery/computer/tram_controls/LateInitialize()
. = ..()
//find the tram, late so the tram is all... set up so when this is called? i'm seriously stupid and 90% of what i do consists of barely educated guessing :)
@@ -73,17 +76,107 @@
/obj/machinery/computer/tram_controls/ui_act(action, params)
. = ..()
- if(. || tram_part.travelling)
- return
- var/destination_id = params["destination"]
- var/obj/effect/landmark/tram/to_where
- for(var/obj/effect/landmark/tram/destination as anything in GLOB.tram_landmarks)
- if(destination.destination_id == destination_id)
- to_where = destination
- break
- if(!to_where)
+ if (.)
return
+
+ switch (action)
+ if ("send")
+ var/obj/effect/landmark/tram/to_where
+ for (var/obj/effect/landmark/tram/destination as anything in GLOB.tram_landmarks)
+ if(destination.destination_id == params["destination"])
+ to_where = destination
+ break
+
+ if (!to_where)
+ return FALSE
+
+ return try_send_tram(to_where)
+
+/// Attempts to sends the tram to the given destination
+/obj/machinery/computer/tram_controls/proc/try_send_tram(obj/effect/landmark/tram/to_where)
+ if(tram_part.travelling)
+ return FALSE
if(tram_part.controls_locked || tram_part.travelling) // someone else started
- return
+ return FALSE
tram_part.tram_travel(to_where)
return TRUE
+
+/obj/item/circuit_component/tram_controls
+ display_name = "Tram Controls"
+
+ /// The destination to go
+ var/datum/port/input/new_destination
+
+ /// The trigger to send the tram
+ var/datum/port/input/trigger_move
+
+ /// The current location
+ var/datum/port/output/location
+
+ /// Whether or not the tram is moving
+ var/datum/port/output/travelling_output
+
+ /// The tram controls computer (/obj/machinery/computer/tram_controls)
+ var/datum/weakref/computer
+
+/obj/item/circuit_component/tram_controls/Initialize()
+ . = ..()
+
+ var/obj/machinery/computer/tram_controls/computer_object = get(src, /obj/machinery/computer/tram_controls)
+ if (computer_object)
+ computer = WEAKREF(computer_object)
+
+ RegisterSignal(computer_object.tram_part, COMSIG_TRAM_SET_TRAVELLING, .proc/on_tram_set_travelling)
+ RegisterSignal(computer_object.tram_part, COMSIG_TRAM_TRAVEL, .proc/on_tram_travel)
+
+ new_destination = add_input_port("Destination", PORT_TYPE_STRING, FALSE)
+ trigger_move = add_input_port("Send Tram", PORT_TYPE_SIGNAL)
+
+ location = add_output_port("Location", PORT_TYPE_STRING)
+ travelling_output = add_output_port("Travelling", PORT_TYPE_NUMBER)
+
+/obj/item/circuit_component/tram_controls/Destroy()
+ new_destination = null
+ trigger_move = null
+ location = null
+ travelling_output = null
+ computer = null
+ return ..()
+
+/obj/item/circuit_component/tram_controls/input_received(datum/port/input/port)
+ . = ..()
+ if (.)
+ return
+
+ if (!COMPONENT_TRIGGERED_BY(trigger_move, port))
+ return
+
+ var/obj/machinery/computer/tram_controls/tram_controls = computer.resolve()
+
+ if (isnull(tram_controls))
+ return
+
+ if (!tram_controls.powered())
+ return
+
+ var/destination
+
+ for(var/obj/effect/landmark/tram/possible_destination as anything in GLOB.tram_landmarks)
+ if(possible_destination.name == new_destination.input_value)
+ destination = possible_destination
+ break
+
+ if (!destination)
+ return
+
+ tram_controls.try_send_tram(destination)
+
+/obj/item/circuit_component/tram_controls/proc/on_tram_set_travelling(datum/source, travelling)
+ SIGNAL_HANDLER
+
+ travelling_output.set_output(travelling)
+
+/obj/item/circuit_component/tram_controls/proc/on_tram_travel(datum/source, obj/effect/landmark/tram/from_where, obj/effect/landmark/tram/to_where)
+ SIGNAL_HANDLER
+
+ location.set_output(to_where.name)
diff --git a/code/game/objects/structures/industrial_lift.dm b/code/game/objects/structures/industrial_lift.dm
index 9e7bb26536b..410540b712b 100644
--- a/code/game/objects/structures/industrial_lift.dm
+++ b/code/game/objects/structures/industrial_lift.dm
@@ -208,7 +208,7 @@ GLOBAL_LIST_EMPTY(lifts)
bumped_atom.visible_message(span_userdanger("[src] crashes into the field violently!"))
for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_master_datum.lift_platforms)
tram_part.travel_distance = 0
- tram_part.travelling = FALSE
+ tram_part.set_travelling(FALSE)
if(prob(15) || locate(/mob/living) in tram_part.lift_load) //always go boom on people on the track
explosion(tram_part, devastation_range = rand(0,1), heavy_impact_range = 2, light_impact_range = 3) //50% chance of gib
qdel(tram_part)
@@ -445,11 +445,18 @@ GLOBAL_DATUM(central_tram, /obj/structure/industrial_lift/tram/central)
* even in the worst cast scenario.
*/
/obj/structure/industrial_lift/tram/proc/find_our_location()
- for(var/obj/effect/landmark/tram/our_location as anything in GLOB.tram_landmarks)
+ for(var/obj/effect/landmark/tram/our_location in GLOB.landmarks_list)
if(our_location.destination_id == initial_id)
from_where = our_location
break
+/obj/structure/industrial_lift/tram/proc/set_travelling(travelling)
+ if (src.travelling == travelling)
+ return
+
+ src.travelling = travelling
+ SEND_SIGNAL(src, COMSIG_TRAM_SET_TRAVELLING, travelling)
+
/obj/structure/industrial_lift/tram/use(mob/user) //dont click the floor dingus we use computers now
return
@@ -482,7 +489,8 @@ GLOBAL_DATUM(central_tram, /obj/structure/industrial_lift/tram/central)
for(var/obj/structure/industrial_lift/tram/other_tram_part as anything in lift_master_datum.lift_platforms) //only thing everyone needs to know is the new location.
if(other_tram_part.travelling) //wee woo wee woo there was a double action queued. damn multi tile structs
return //we don't care to undo locked controls, though, as that will resolve itself
- other_tram_part.travelling = TRUE
+ SEND_SIGNAL(src, COMSIG_TRAM_TRAVEL, from_where, to_where)
+ other_tram_part.set_travelling(TRUE)
other_tram_part.from_where = to_where
lift_master_datum.MoveLiftHorizontal(travel_direction, z)
travel_distance--
@@ -499,7 +507,7 @@ GLOBAL_DATUM(central_tram, /obj/structure/industrial_lift/tram/central)
/obj/structure/industrial_lift/tram/proc/unlock_controls()
visible_message("[src]'s controls are now unlocked.