diff --git a/code/__DEFINES/dcs/signals/signals_traitor.dm b/code/__DEFINES/dcs/signals/signals_traitor.dm
index 66aebd7ae95..1728ef8c679 100644
--- a/code/__DEFINES/dcs/signals/signals_traitor.dm
+++ b/code/__DEFINES/dcs/signals/signals_traitor.dm
@@ -3,6 +3,8 @@
/// Called whenever the uplink handler receives any sort of update. Used by uplinks to update their UI. No arguments passed
#define COMSIG_UPLINK_HANDLER_ON_UPDATE "uplink_handler_on_update"
+/// Sent from the uplink handler when the traitor uses the syndicate uplink beacon to order a replacement uplink.
+#define COMSIG_UPLINK_HANDLER_REPLACEMENT_ORDERED "uplink_handler_replacement_ordered"
/// Called before the traitor objective is generated
#define COMSIG_TRAITOR_OBJECTIVE_PRE_GENERATE "traitor_objective_pre_generate"
diff --git a/code/__DEFINES/radio.dm b/code/__DEFINES/radio.dm
index 9644a1c09f2..e75ff9ea9eb 100644
--- a/code/__DEFINES/radio.dm
+++ b/code/__DEFINES/radio.dm
@@ -90,6 +90,8 @@
#define FREQ_SIGNALER 1457 // the default for new signalers
#define FREQ_COMMON 1459 // Common comms frequency, dark green
+#define MIN_UNUSED_FREQ 1461 // Prevents rolling AI Private or Common
+
#define MAX_FREQ 1489 // ------------------------------------------------------
#define MAX_FREE_FREQ 1599 // -------------------------------------------------
diff --git a/code/datums/components/crafting/crafting.dm b/code/datums/components/crafting/crafting.dm
index 51f74fd389e..1b50b4e7b63 100644
--- a/code/datums/components/crafting/crafting.dm
+++ b/code/datums/components/crafting/crafting.dm
@@ -605,10 +605,15 @@
//Mind helpers
-/datum/mind/proc/teach_crafting_recipe(R)
+/// proc that teaches user a non-standard crafting recipe
+/datum/mind/proc/teach_crafting_recipe(recipe)
if(!learned_recipes)
learned_recipes = list()
- learned_recipes |= R
+ learned_recipes |= recipe
+
+/// proc that makes user forget a specific crafting recipe
+/datum/mind/proc/forget_crafting_recipe(recipe)
+ learned_recipes -= recipe
/datum/mind/proc/has_crafting_recipe(mob/user, potential_recipe)
if(!learned_recipes)
diff --git a/code/datums/components/crafting/structures.dm b/code/datums/components/crafting/structures.dm
index 4a56852ecd5..2cbe2c35302 100644
--- a/code/datums/components/crafting/structures.dm
+++ b/code/datums/components/crafting/structures.dm
@@ -60,3 +60,17 @@
/obj/item/stack/cable_coil = 10,
)
category = CAT_STRUCTURE
+
+/datum/crafting_recipe/syndicate_uplink_beacon
+ name = "Syndicate Uplink Beacon"
+ result = /obj/structure/syndicate_uplink_beacon
+ tool_behaviors = list(TOOL_SCREWDRIVER)
+ always_available = FALSE
+ time = 6 SECONDS
+ reqs = list(
+ /obj/item/stack/sheet/iron = 5,
+ /obj/item/stack/cable_coil = 5,
+ /obj/item/beacon = 1,
+ /obj/item/stack/ore/bluespace_crystal = 1,
+ )
+ category = CAT_STRUCTURE
diff --git a/code/datums/components/uplink.dm b/code/datums/components/uplink.dm
index 50907528ce4..eea548a7551 100644
--- a/code/datums/components/uplink.dm
+++ b/code/datums/components/uplink.dm
@@ -65,6 +65,8 @@
RegisterSignal(parent, COMSIG_RADIO_NEW_MESSAGE, PROC_REF(new_message))
else if(istype(parent, /obj/item/pen))
RegisterSignal(parent, COMSIG_PEN_ROTATED, PROC_REF(pen_rotation))
+ else if(istype(parent, /obj/item/uplink/replacement))
+ RegisterSignal(parent, COMSIG_MOVABLE_HEAR, PROC_REF(on_heard))
if(owner)
src.owner = owner
@@ -86,6 +88,7 @@
else
uplink_handler = uplink_handler_override
RegisterSignal(uplink_handler, COMSIG_UPLINK_HANDLER_ON_UPDATE, PROC_REF(handle_uplink_handler_update))
+ RegisterSignal(uplink_handler, COMSIG_UPLINK_HANDLER_REPLACEMENT_ORDERED, PROC_REF(handle_uplink_replaced))
if(!lockable)
active = TRUE
locked = FALSE
@@ -96,6 +99,19 @@
SIGNAL_HANDLER
SStgui.update_uis(src)
+/// When a new uplink is made via the syndicate beacon it locks all lockable uplinks and destroys replacement uplinks
+/datum/component/uplink/proc/handle_uplink_replaced()
+ SIGNAL_HANDLER
+ if(lockable)
+ lock_uplink()
+ if(!istype(parent, /obj/item/uplink/replacement))
+ return
+ var/obj/item/uplink_item = parent
+ do_sparks(number = 3, cardinal_only = FALSE, source = uplink_item)
+ uplink_item.visible_message(span_warning("The [uplink_item] suddenly combusts!"), vision_distance = COMBAT_MESSAGE_RANGE)
+ new /obj/effect/decal/cleanable/ash(get_turf(uplink_item))
+ qdel(uplink_item)
+
/// Adds telecrystals to the uplink. It is bad practice to use this outside of the component itself.
/datum/component/uplink/proc/add_telecrystals(telecrystals_added)
set_telecrystals(uplink_handler.telecrystals + telecrystals_added)
@@ -236,7 +252,7 @@
data["extra_purchasable_stock"] = extra_purchasable_stock
data["current_stock"] = remaining_stock
data["shop_locked"] = uplink_handler.shop_locked
- data["purchased_items"] = length(uplink_handler.purchase_log.purchase_log)
+ data["purchased_items"] = length(uplink_handler.purchase_log?.purchase_log)
return data
/datum/component/uplink/ui_static_data(mob/user)
@@ -277,9 +293,7 @@
if("lock")
if(!lockable)
return TRUE
- active = FALSE
- locked = TRUE
- SStgui.close_uis(src)
+ lock_uplink()
if(!uplink_handler.has_objectives)
return TRUE
@@ -321,6 +335,12 @@
uplink_handler.abort_objective(objective)
return TRUE
+/// Proc that locks uplinks
+/datum/component/uplink/proc/lock_uplink()
+ active = FALSE
+ locked = TRUE
+ SStgui.close_uis(src)
+
// Implant signal responses
/datum/component/uplink/proc/implant_activation()
SIGNAL_HANDLER
@@ -459,6 +479,17 @@
return returnable_code
+/// Proc that unlocks a locked replacement uplink when it hears the unlock code from their datum
+/datum/component/uplink/proc/on_heard(datum/source, list/hearing_args)
+ SIGNAL_HANDLER
+ if(!locked)
+ return
+ if(!findtext(hearing_args[HEARING_RAW_MESSAGE], unlock_code))
+ return
+ var/atom/replacement_uplink = parent
+ locked = FALSE
+ replacement_uplink.balloon_alert_to_viewers("beep", vision_distance = COMBAT_MESSAGE_RANGE)
+
/datum/component/uplink/proc/failsafe(mob/living/carbon/user)
if(!parent)
return
diff --git a/code/datums/looping_sounds/machinery_sounds.dm b/code/datums/looping_sounds/machinery_sounds.dm
index 9287f927333..1e8ce73e992 100644
--- a/code/datums/looping_sounds/machinery_sounds.dm
+++ b/code/datums/looping_sounds/machinery_sounds.dm
@@ -132,3 +132,16 @@
mid_sounds = list('sound/effects/bubbles2.ogg' = 1)
mid_length = 7 SECONDS
volume = 25
+
+/datum/looping_sound/typing
+ mid_sounds = list(
+ 'sound/machines/terminal_button01.ogg' = 1,
+ 'sound/machines/terminal_button02.ogg' = 1,
+ 'sound/machines/terminal_button03.ogg' = 1,
+ 'sound/machines/terminal_button04.ogg' = 1,
+ 'sound/machines/terminal_button05.ogg' = 1,
+ 'sound/machines/terminal_button06.ogg' = 1,
+ 'sound/machines/terminal_button07.ogg' = 1,
+ 'sound/machines/terminal_button08.ogg' = 1,
+ )
+ mid_length = 0.3 SECONDS
diff --git a/code/game/objects/structures/syndicate_uplink_beacon.dm b/code/game/objects/structures/syndicate_uplink_beacon.dm
new file mode 100644
index 00000000000..f4d4ea6fe64
--- /dev/null
+++ b/code/game/objects/structures/syndicate_uplink_beacon.dm
@@ -0,0 +1,114 @@
+/// Device that traitors can craft in order to be sent a new, undisguised uplink
+/obj/structure/syndicate_uplink_beacon
+ name = "suspicious beacon"
+ icon = 'icons/obj/machines/telecomms.dmi'
+ icon_state = "relay_traitor"
+ desc = "This ramshackle device seems capable of recieving and sending signals for some nefarious purpose."
+ density = TRUE
+ anchored = TRUE
+ /// Traitor's code that they speak into the radio
+ var/uplink_code = ""
+ /// weakref to person who is going to use the beacon to get a replacement uplink
+ var/datum/weakref/owner
+ /// while constructed the teleport beacon is still active
+ var/obj/item/beacon/teleport_beacon
+ /// Radio that the device needs to listen to the codeword from the traitor
+ var/obj/item/radio/listening_radio
+ /// prevents traitor from activating teleport_beacon proc too much in a small period of time
+ COOLDOWN_DECLARE(beacon_cooldown)
+
+/obj/structure/syndicate_uplink_beacon/Initialize(mapload)
+ . = ..()
+ register_context()
+
+ var/static/list/tool_behaviors = list(
+ TOOL_SCREWDRIVER = list(
+ SCREENTIP_CONTEXT_RMB = "Deconstruct",
+ ),
+ )
+ AddElement(/datum/element/contextual_screentip_tools, tool_behaviors)
+ listening_radio = new(src)
+ listening_radio.canhear_range = 0
+ teleport_beacon = new(src)
+
+/obj/structure/syndicate_uplink_beacon/attack_hand(mob/living/user, list/modifiers)
+ if(!IS_TRAITOR(user))
+ balloon_alert(user, "don't know how to use!")
+ return
+ if(IS_WEAKREF_OF(owner, user))
+ balloon_alert(user, "already synchronized to you!")
+ return
+ if(owner != null)
+ balloon_alert(user, "already claimed!")
+ return
+ var/datum/looping_sound/typing/typing_sounds = new(src, start_immediately = TRUE)
+ balloon_alert(user, "synchronizing...")
+ if(!do_after(user = user, delay = 3 SECONDS, target = src, interaction_key = REF(src)))
+ typing_sounds.stop()
+ return
+ typing_sounds.stop()
+ probe_traitor(user)
+
+/obj/structure/syndicate_uplink_beacon/screwdriver_act_secondary(mob/living/user, obj/item/tool)
+ tool.play_tool_sound(src)
+ balloon_alert(user, "deconstructing...")
+ if (!do_after(user, 5 SECONDS, target = src))
+ return FALSE
+ var/turf/beacon_tile = get_turf(src)
+ new /obj/item/stack/sheet/iron/five(beacon_tile)
+ new /obj/item/stack/cable_coil/five(beacon_tile)
+ teleport_beacon.forceMove(beacon_tile)
+ teleport_beacon = null
+ qdel(src)
+ return TRUE
+
+/obj/structure/syndicate_uplink_beacon/Destroy()
+ QDEL_NULL(listening_radio)
+ if(teleport_beacon)
+ QDEL_NULL(teleport_beacon)
+ return ..()
+
+/// Proc reads the user, sets radio to the correct frequency and starts to listen for the replacement uplink code
+/obj/structure/syndicate_uplink_beacon/proc/probe_traitor(mob/living/user)
+ owner = WEAKREF(user)
+ var/datum/antagonist/traitor/traitor_datum = user.mind.has_antag_datum(/datum/antagonist/traitor)
+
+ uplink_code = traitor_datum.replacement_uplink_code
+ listening_radio.set_frequency(traitor_datum.replacement_uplink_frequency)
+ become_hearing_sensitive()
+
+/obj/structure/syndicate_uplink_beacon/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, list/message_mods, message_range)
+ if(ismob(speaker) || radio_freq != listening_radio.get_frequency())
+ return
+ if(!findtext(message, uplink_code))
+ return
+ teleport_uplink()
+
+/// Proc uses owners uplink handler to create replacement uplink and then lock or destroy their other uplinks
+/obj/structure/syndicate_uplink_beacon/proc/teleport_uplink()
+ if(!COOLDOWN_FINISHED(src, beacon_cooldown))
+ return
+ COOLDOWN_START(src, beacon_cooldown, 10 MINUTES)
+
+ var/mob/living/resolved_owner = owner.resolve()
+ if(isnull(resolved_owner))
+ return
+
+ var/datum/antagonist/traitor/traitor_datum = resolved_owner.mind.has_antag_datum(/datum/antagonist/traitor)
+ if(isnull(traitor_datum))
+ return
+
+ var/datum/uplink_handler/uplink_handler = traitor_datum.uplink_handler
+
+ SEND_SIGNAL(uplink_handler, COMSIG_UPLINK_HANDLER_REPLACEMENT_ORDERED)
+ new /obj/item/uplink/replacement(get_turf(src), /*owner = */resolved_owner, /*tc_amount = */0, /*uplink_handler_override = */uplink_handler)
+ flick("relay_traitor_activate", src)
+ do_sparks(number = 5, cardinal_only = FALSE, source = src)
+ log_traitor("[key_name(resolved_owner)] acquired a replacement uplink via the syndicate uplink beacon.")
+
+// Adds screentips
+/obj/structure/syndicate_uplink_beacon/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ if(held_item || !IS_TRAITOR(user))
+ return NONE
+ context[SCREENTIP_CONTEXT_LMB] = "Synchronize with beacon"
+ return CONTEXTUAL_SCREENTIP_SET
diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm
index 2d57748f754..486e6e37586 100644
--- a/code/modules/antagonists/traitor/datum_traitor.dm
+++ b/code/modules/antagonists/traitor/datum_traitor.dm
@@ -18,6 +18,10 @@
var/should_give_codewords = TRUE
///give this traitor an uplink?
var/give_uplink = TRUE
+ /// Code that allows traitor to get a replacement uplink
+ var/replacement_uplink_code = ""
+ /// Radio frequency that traitor must speak on to get a replacement uplink
+ var/replacement_uplink_frequency = ""
///if TRUE, this traitor will always get hijacking as their final objective
var/is_hijacker = FALSE
@@ -47,6 +51,7 @@
if(give_uplink)
owner.give_uplink(silent = TRUE, antag_datum = src)
+ generate_replacement_codes()
var/datum/component/uplink/uplink = owner.find_syndicate_uplink()
uplink_ref = WEAKREF(uplink)
@@ -82,6 +87,8 @@
pick_employer()
+ owner.teach_crafting_recipe(/datum/crafting_recipe/syndicate_uplink_beacon)
+
owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/tatoralert.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE)
return ..()
@@ -139,8 +146,14 @@
result += "Force add objective
"
return result
+/// proc that generates the traitors replacement uplink code and radio frequency
+/datum/antagonist/traitor/proc/generate_replacement_codes()
+ replacement_uplink_code = "[pick(GLOB.phonetic_alphabet)] [rand(10,99)]"
+ replacement_uplink_frequency = sanitize_frequency(rand(MIN_UNUSED_FREQ, MAX_FREQ), free = FALSE, syndie = FALSE)
+
/datum/antagonist/traitor/on_removal()
owner.special_role = null
+ owner.forget_crafting_recipe(/datum/crafting_recipe/syndicate_uplink_beacon)
return ..()
/datum/antagonist/traitor/proc/pick_employer()
@@ -258,6 +271,8 @@
data["theme"] = traitor_flavor["ui_theme"]
data["code"] = uplink?.unlock_code
data["failsafe_code"] = uplink?.failsafe_code
+ data["replacement_code"] = replacement_uplink_code
+ data["replacement_frequency"] = format_frequency(replacement_uplink_frequency)
data["intro"] = traitor_flavor["introduction"]
data["allies"] = traitor_flavor["allies"]
data["goal"] = traitor_flavor["goal"]
diff --git a/code/modules/uplink/uplink_devices.dm b/code/modules/uplink/uplink_devices.dm
index c9c2fa3174d..c2409773ada 100644
--- a/code/modules/uplink/uplink_devices.dm
+++ b/code/modules/uplink/uplink_devices.dm
@@ -23,15 +23,25 @@
/// The uplink flag for this type.
/// See [`code/__DEFINES/uplink.dm`]
var/uplink_flag = UPLINK_TRAITORS
+ /// If the uplink is lockable, which defaults to false which most subtypes of this item are for debug reasons
+ var/lockable_uplink = FALSE
-/obj/item/uplink/Initialize(mapload, owner, tc_amount = 20)
+/obj/item/uplink/Initialize(mapload, owner, tc_amount = 20, datum/uplink_handler/uplink_handler_override = null)
. = ..()
- AddComponent(/datum/component/uplink, owner, FALSE, TRUE, uplink_flag, tc_amount)
+ AddComponent(\
+ /datum/component/uplink, \
+ owner = owner, \
+ lockable = lockable_uplink, \
+ enabled = TRUE, \
+ uplink_flag = uplink_flag, \
+ starting_tc = tc_amount, \
+ uplink_handler_override = uplink_handler_override, \
+ )
/obj/item/uplink/debug
name = "debug uplink"
-/obj/item/uplink/debug/Initialize(mapload, owner, tc_amount = 9000)
+/obj/item/uplink/debug/Initialize(mapload, owner, tc_amount = 9000, datum/uplink_handler/uplink_handler_override = null)
. = ..()
var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink)
hidden_uplink.name = "debug uplink"
@@ -44,7 +54,7 @@
name = "debug nuclear uplink"
uplink_flag = UPLINK_NUKE_OPS
-/obj/item/uplink/nuclear/debug/Initialize(mapload, owner, tc_amount = 9000)
+/obj/item/uplink/nuclear/debug/Initialize(mapload, owner, tc_amount = 9000, datum/uplink_handler/uplink_handler_override = null)
. = ..()
var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink)
hidden_uplink.name = "debug nuclear uplink"
@@ -65,17 +75,45 @@
name = "dusty radio"
desc = "A dusty looking radio."
-/obj/item/uplink/old/Initialize(mapload, owner, tc_amount = 10)
+/obj/item/uplink/old/Initialize(mapload, owner, tc_amount = 10, datum/uplink_handler/uplink_handler_override = null)
. = ..()
var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink)
hidden_uplink.name = "dusty radio"
+// Uplink subtype used as replacement uplink
+/obj/item/uplink/replacement
+ lockable_uplink = TRUE
+
+/obj/item/uplink/replacement/Initialize(mapload, owner, tc_amount = 10, datum/uplink_handler/uplink_handler_override = null)
+ . = ..()
+ var/datum/component/uplink/hidden_uplink = GetComponent(/datum/component/uplink)
+ var/mob/living/replacement_needer = owner
+ if(!istype(replacement_needer))
+ return
+ var/datum/antagonist/traitor/traitor_datum = replacement_needer?.mind.has_antag_datum(/datum/antagonist/traitor)
+ hidden_uplink.unlock_code = traitor_datum?.replacement_uplink_code
+ become_hearing_sensitive()
+
+/obj/item/uplink/replacement/screwdriver_act_secondary(mob/living/user, obj/item/tool)
+ tool.play_tool_sound(src)
+ balloon_alert(user, "deconstructing...")
+ if (!do_after(user, 3 SECONDS, target = src))
+ return FALSE
+ qdel(src)
+ return TRUE
+
+/obj/item/uplink/replacement/examine(mob/user)
+ . = ..()
+ if(!IS_TRAITOR(user))
+ return
+ . += span_notice("You can destroy this device with a screwdriver.")
+
// Multitool uplink
-/obj/item/multitool/uplink/Initialize(mapload, owner, tc_amount = 20)
+/obj/item/multitool/uplink/Initialize(mapload, owner, tc_amount = 20, datum/uplink_handler/uplink_handler_override = null)
. = ..()
AddComponent(/datum/component/uplink, owner, FALSE, TRUE, UPLINK_TRAITORS, tc_amount)
// Pen uplink
-/obj/item/pen/uplink/Initialize(mapload, owner, tc_amount = 20)
+/obj/item/pen/uplink/Initialize(mapload, owner, tc_amount = 20, datum/uplink_handler/uplink_handler_override = null)
. = ..()
AddComponent(/datum/component/uplink, owner, TRUE, FALSE, UPLINK_TRAITORS, tc_amount)
diff --git a/icons/obj/machines/telecomms.dmi b/icons/obj/machines/telecomms.dmi
index 93bb9c477aa..ddce8a96a93 100644
Binary files a/icons/obj/machines/telecomms.dmi and b/icons/obj/machines/telecomms.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 9caadb0df95..57e8cdf1b5c 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -2135,6 +2135,7 @@
#include "code\game\objects\structures\spawner.dm"
#include "code\game\objects\structures\spirit_board.dm"
#include "code\game\objects\structures\stairs.dm"
+#include "code\game\objects\structures\syndicate_uplink_beacon.dm"
#include "code\game\objects\structures\table_frames.dm"
#include "code\game\objects\structures\tables_racks.dm"
#include "code\game\objects\structures\tank_dispenser.dm"
diff --git a/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx b/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx
index cd79be0621d..07d1e1beff3 100644
--- a/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx
+++ b/tgui/packages/tgui/interfaces/AntagInfoTraitor.tsx
@@ -35,6 +35,8 @@ type Info = {
intro: string;
code: string;
failsafe_code: string;
+ replacement_code: string;
+ replacement_frequency: string;
has_uplink: BooleanLike;
uplink_intro: string;
uplink_unlock_info: string;
@@ -120,8 +122,15 @@ const EmployerSection = (props, context) => {
const UplinkSection = (props, context) => {
const { data } = useBackend(context);
- const { has_uplink, uplink_intro, uplink_unlock_info, code, failsafe_code } =
- data;
+ const {
+ has_uplink,
+ uplink_intro,
+ uplink_unlock_info,
+ code,
+ failsafe_code,
+ replacement_code,
+ replacement_frequency,
+ } = data;
return (
@@ -149,6 +158,19 @@ const UplinkSection = (props, context) => {
>
)}
+
+
+ If you lose your uplink, you can craft a Syndicate Uplink Beacon and
+ then speak{' '}
+
+ {replacement_code}
+ {' '}
+ on radio frequency{' '}
+
+ {replacement_frequency}
+ {' '}
+ after synchronizing with the beacon.
+
);
};