Files
Bubberstation/code/modules/mod/mod_link.dm
FeudeyTF 161d744035 Telecomms Update: Ability to change existing radio channels and create new ones (#91647)
## About The Pull Request
I have added the ability to create and edit station radio channels
through the existing telecommunications system.
You can change the name of the radio channel and its color.

The channel settings are changed and created through the servers
(Frequencies Settings)

![ui](https://github.com/user-attachments/assets/cb010d75-bb00-4e3c-86b6-351b39b832e3)

Here i created my own channel:
1) Add frequency at Receiver (you will not see channel name):

![TestChannelInReceiver](https://github.com/user-attachments/assets/d3934e96-fb2d-492a-a1d6-84e8bfbd6628)
2) Add frequency at Bus (you will not see channel name):

![TestChannelnBus](https://github.com/user-attachments/assets/9955fb9c-9ca0-44f5-8d8f-caf02c0b3a9c)
3) Add frequency at Server
4) Add settings for your frequency

![NewChannel](https://github.com/user-attachments/assets/14a86eae-a000-4106-848b-a140ab678c3c)
5) See the result:

![TestChannel](https://github.com/user-attachments/assets/a720c660-43f6-47c0-8e85-fd313e8ce8d7)

Important Notes:
1) Headsets, radios, and intercoms will not see a change in
telecommunications, but will use standard names (Common, Security etc.).
2) There are still reserved names that cannot be used: CentComm,
Syndicate, Uplink, CTFs channels
3) Servers must filter frequency for applying settings on them

## Why It's Good For The Game

Now telecommunication channels names and colors depends on the settings
of the network servers, which makes it more flexible and logical. It is
also useful for foreign language servers, as you can translate channel
names.

## Changelog

🆑
add: Added ability to change existing radio channels and create new
qol: Added color for some buttons in Telecomms UI
/🆑

(cherry picked from commit 35494b93bb)
2025-06-28 20:37:12 -04:00

557 lines
20 KiB
Plaintext

/proc/make_link_visual_generic(datum/mod_link/mod_link, proc_path)
var/mob/living/user = mod_link.get_user_callback.Invoke()
var/obj/effect/overlay/link_visual = new()
link_visual.name = "holocall ([mod_link.id])"
link_visual.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
LAZYADD(mod_link.holder.update_on_z, link_visual)
link_visual.appearance_flags |= KEEP_TOGETHER
link_visual.makeHologram(0.75)
mod_link.visual_overlays = user.overlays - user.active_thinking_indicator
link_visual.add_overlay(mod_link.visual_overlays)
mod_link.visual = link_visual
mod_link.holder.become_hearing_sensitive(REF(mod_link))
mod_link.holder.RegisterSignals(user, list(COMSIG_CARBON_APPLY_OVERLAY, COMSIG_CARBON_REMOVE_OVERLAY), proc_path)
return link_visual
/proc/get_link_visual_generic(datum/mod_link/mod_link, atom/movable/visuals, proc_path)
var/mob/living/user = mod_link.get_user_callback.Invoke()
playsound(mod_link.holder, 'sound/machines/terminal/terminal_processing.ogg', 50, vary = TRUE)
visuals.add_overlay(mutable_appearance('icons/effects/effects.dmi', "static_base", ABOVE_NORMAL_TURF_LAYER))
visuals.add_overlay(mutable_appearance('icons/effects/effects.dmi', "modlink", ABOVE_ALL_MOB_LAYER))
visuals.add_filter("crop_square", 1, alpha_mask_filter(icon = icon('icons/effects/effects.dmi', "modlink_filter")))
visuals.maptext_height = 6
visuals.alpha = 0
user.vis_contents += visuals
visuals.forceMove(user)
animate(visuals, 0.5 SECONDS, alpha = 255)
var/datum/callback/setdir_callback = CALLBACK(mod_link.holder, proc_path)
setdir_callback.Invoke(user, user.dir, user.dir)
mod_link.holder.RegisterSignal(mod_link.holder.loc, COMSIG_ATOM_DIR_CHANGE, proc_path)
/proc/delete_link_visual_generic(datum/mod_link/mod_link)
var/mob/living/user = mod_link.get_user_callback.Invoke()
playsound(mod_link.get_other().holder, 'sound/machines/terminal/terminal_processing.ogg', 50, vary = TRUE, frequency = -1)
LAZYREMOVE(mod_link.holder.update_on_z, mod_link.visual)
mod_link.holder.lose_hearing_sensitivity(REF(mod_link))
mod_link.holder.UnregisterSignal(user, list(COMSIG_CARBON_APPLY_OVERLAY, COMSIG_CARBON_REMOVE_OVERLAY, COMSIG_ATOM_DIR_CHANGE))
QDEL_NULL(mod_link.visual)
/proc/on_user_set_dir_generic(datum/mod_link/mod_link, newdir)
var/atom/other_visual = mod_link.get_other().visual
if(!newdir) //can sometimes be null or 0
return
other_visual.setDir(SOUTH)
other_visual.pixel_x = 0
other_visual.pixel_y = 0
var/matrix/new_transform = matrix()
if(newdir & NORTH)
other_visual.pixel_y = 13
other_visual.layer = BELOW_MOB_LAYER
if(newdir & SOUTH)
other_visual.pixel_y = -24
other_visual.layer = ABOVE_ALL_MOB_LAYER
new_transform.Scale(-1, 1)
new_transform.Translate(-1, 0)
if(newdir & EAST)
other_visual.pixel_x = 14
other_visual.layer = BELOW_MOB_LAYER
new_transform.Shear(0.5, 0)
new_transform.Scale(0.65, 1)
if(newdir & WEST)
other_visual.pixel_x = -14
other_visual.layer = BELOW_MOB_LAYER
new_transform.Shear(-0.5, 0)
new_transform.Scale(0.65, 1)
other_visual.transform = new_transform
/obj/item/mod/control/Initialize(mapload, datum/mod_theme/new_theme, new_skin, obj/item/mod/core/new_core)
. = ..()
mod_link = new(
src,
starting_frequency,
CALLBACK(src, PROC_REF(get_wearer)),
CALLBACK(src, PROC_REF(can_call)),
CALLBACK(src, PROC_REF(make_link_visual)),
CALLBACK(src, PROC_REF(get_link_visual)),
CALLBACK(src, PROC_REF(delete_link_visual))
)
/obj/item/mod/control/multitool_act_secondary(mob/living/user, obj/item/multitool/tool)
. = NONE
var/tool_frequency = null
if(istype(tool.buffer, /datum/mod_link))
var/datum/mod_link/buffer_link = tool.buffer
tool_frequency = buffer_link.frequency
balloon_alert(user, "frequency set")
. = ITEM_INTERACT_SUCCESS
if(!tool_frequency && mod_link.frequency)
tool.set_buffer(mod_link)
balloon_alert(user, "frequency copied")
. = ITEM_INTERACT_SUCCESS
else if(tool_frequency && !mod_link.frequency)
mod_link.frequency = tool_frequency
. = ITEM_INTERACT_SUCCESS
else if(tool_frequency && mod_link.frequency)
var/response = tgui_alert(user, "Would you like to copy or imprint the frequency?", "MODlink Frequency", list("Copy", "Imprint"))
if(!user.is_holding(tool))
return ITEM_INTERACT_BLOCKING
switch(response)
if("Copy")
tool.set_buffer(mod_link)
balloon_alert(user, "frequency copied")
. = ITEM_INTERACT_SUCCESS
if("Imprint")
mod_link.frequency = tool_frequency
balloon_alert(user, "frequency set")
. = ITEM_INTERACT_SUCCESS
/obj/item/mod/control/proc/can_call()
return get_charge() && wearer && wearer.stat < DEAD
/obj/item/mod/control/proc/make_link_visual()
return make_link_visual_generic(mod_link, PROC_REF(on_overlay_change))
/obj/item/mod/control/proc/get_link_visual(atom/movable/visuals)
return get_link_visual_generic(mod_link, visuals, PROC_REF(on_wearer_set_dir))
/obj/item/mod/control/proc/delete_link_visual()
return delete_link_visual_generic(mod_link)
/obj/item/mod/control/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, list/spans, list/message_mods, message_range)
. = ..()
if(speaker != wearer && speaker != ai_assistant)
return
mod_link.visual.say(raw_message, sanitize = FALSE, message_range = 2)
/obj/item/mod/control/proc/on_overlay_change(atom/source, cache_index, overlay)
SIGNAL_HANDLER
addtimer(CALLBACK(src, PROC_REF(update_link_visual)), 1 TICKS, TIMER_UNIQUE)
/obj/item/mod/control/proc/update_link_visual()
if(QDELETED(mod_link.link_call))
return
mod_link.visual.cut_overlay(mod_link.visual_overlays)
mod_link.visual_overlays = wearer.overlays - wearer.active_thinking_indicator
mod_link.visual.add_overlay(mod_link.visual_overlays)
/obj/item/mod/control/proc/on_wearer_set_dir(atom/source, dir, newdir)
SIGNAL_HANDLER
on_user_set_dir_generic(mod_link, newdir || SOUTH)
/obj/item/clothing/neck/link_scryer
name = "\improper MODlink scryer"
desc = "An intricate piece of machinery that creates a holographic video call with another MODlink-compatible device. Essentially a video necklace."
icon_state = "modlink"
actions_types = list(/datum/action/item_action/call_link)
/// The installed power cell.
var/obj/item/stock_parts/power_store/cell
/// The MODlink datum we operate.
var/datum/mod_link/mod_link
/// Initial frequency of the MODlink.
var/starting_frequency
/// An additional name tag for the scryer, seen as "MODlink scryer - [label]"
var/label
/obj/item/clothing/neck/link_scryer/Initialize(mapload)
. = ..()
mod_link = new(
src,
starting_frequency,
CALLBACK(src, PROC_REF(get_user)),
CALLBACK(src, PROC_REF(can_call)),
CALLBACK(src, PROC_REF(make_link_visual)),
CALLBACK(src, PROC_REF(get_link_visual)),
CALLBACK(src, PROC_REF(delete_link_visual))
)
START_PROCESSING(SSobj, src)
/obj/item/clothing/neck/link_scryer/Destroy()
QDEL_NULL(cell)
QDEL_NULL(mod_link)
STOP_PROCESSING(SSobj, src)
return ..()
/obj/item/clothing/neck/link_scryer/examine(mob/user)
. = ..()
// SKYRAT EDIT NIFSOFT SCRYERS - START
if(custom_examine_controls)
return
// SKYRAT EDIT NIFSOFT SCRYERS - END
if(cell)
. += span_notice("The battery charge reads [cell.percent()]%. <b>Right-click</b> with an empty hand to remove it.")
else
. += span_notice("It is missing a battery, one can be installed by clicking with a power cell on it.")
. += span_notice("The MODlink ID is [mod_link.id], frequency is [mod_link.frequency || "unset"]. <b>Right-click</b> with multitool to copy/imprint frequency.")
. += span_notice("Use in hand to set name.")
/obj/item/clothing/neck/link_scryer/equipped(mob/living/user, slot)
. = ..()
if(slot != ITEM_SLOT_NECK)
mod_link?.end_call()
/obj/item/clothing/neck/link_scryer/dropped(mob/living/user)
. = ..()
mod_link?.end_call()
/obj/item/clothing/neck/link_scryer/attack_self(mob/user, modifiers)
var/new_label = reject_bad_text(tgui_input_text(user, "Change the visible name", "Set Name", label, MAX_NAME_LEN))
if(!user.is_holding(src))
return
if(!new_label)
balloon_alert(user, "invalid name!")
return
label = new_label
balloon_alert(user, "name set")
update_name()
/obj/item/clothing/neck/link_scryer/process(seconds_per_tick)
if(!mod_link.link_call)
return
cell.use(0.02 * STANDARD_CELL_RATE * seconds_per_tick, force = TRUE)
/obj/item/clothing/neck/link_scryer/attackby(obj/item/attacked_by, mob/user, list/modifiers, list/attack_modifiers)
. = ..()
if(cell || !istype(attacked_by, /obj/item/stock_parts/power_store/cell))
return
if(!user.transferItemToLoc(attacked_by, src))
return
cell = attacked_by
balloon_alert(user, "cell installed")
/obj/item/clothing/neck/link_scryer/update_name(updates)
. = ..()
name = "[initial(name)][label ? " - [label]" : ""]"
/obj/item/clothing/neck/link_scryer/Exited(atom/movable/gone, direction)
. = ..()
if(gone == cell)
cell = null
/obj/item/clothing/neck/link_scryer/attack_hand_secondary(mob/user, list/modifiers)
if(!cell)
return SECONDARY_ATTACK_CONTINUE_CHAIN
balloon_alert(user, "cell removed")
user.put_in_hands(cell)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/item/clothing/neck/link_scryer/multitool_act_secondary(mob/living/user, obj/item/multitool/tool)
. = NONE
var/tool_frequency = null
if(istype(tool.buffer, /datum/mod_link))
var/datum/mod_link/buffer_link = tool.buffer
tool_frequency = buffer_link.frequency
balloon_alert(user, "frequency set")
. = ITEM_INTERACT_SUCCESS
if(!tool_frequency && mod_link.frequency)
tool.set_buffer(mod_link)
balloon_alert(user, "frequency copied")
. = ITEM_INTERACT_SUCCESS
else if(tool_frequency && !mod_link.frequency)
mod_link.frequency = tool_frequency
. = ITEM_INTERACT_SUCCESS
else if(tool_frequency && mod_link.frequency)
var/response = tgui_alert(user, "Would you like to copy or imprint the frequency?", "MODlink Frequency", list("Copy", "Imprint"))
if(!user.is_holding(tool))
return ITEM_INTERACT_BLOCKING
switch(response)
if("Copy")
tool.set_buffer(mod_link)
balloon_alert(user, "frequency copied")
. = ITEM_INTERACT_SUCCESS
if("Imprint")
mod_link.frequency = tool_frequency
balloon_alert(user, "frequency set")
. = ITEM_INTERACT_SUCCESS
/obj/item/clothing/neck/link_scryer/worn_overlays(mutable_appearance/standing, isinhands)
. = ..()
if(!QDELETED(mod_link.link_call))
. += mutable_appearance('icons/mob/clothing/neck.dmi', "modlink_active")
/obj/item/clothing/neck/link_scryer/ui_action_click(mob/user)
if(mod_link.link_call)
mod_link.end_call()
else if(QDELETED(cell))
user.balloon_alert(user, "no cell installed!")
else if(!cell.charge)
user.balloon_alert(user, "no charge!")
else
call_link(user, mod_link)
/obj/item/clothing/neck/link_scryer/proc/get_user()
var/mob/living/carbon/user = loc
return istype(user) && user.wear_neck == src ? user : null
/obj/item/clothing/neck/link_scryer/proc/can_call()
var/mob/living/user = loc
return istype(user) && cell?.charge && user.stat < DEAD
/obj/item/clothing/neck/link_scryer/proc/make_link_visual()
var/mob/living/user = mod_link.get_user_callback.Invoke()
user.update_worn_neck()
return make_link_visual_generic(mod_link, PROC_REF(on_overlay_change))
/obj/item/clothing/neck/link_scryer/proc/get_link_visual(atom/movable/visuals)
return get_link_visual_generic(mod_link, visuals, PROC_REF(on_user_set_dir))
/obj/item/clothing/neck/link_scryer/proc/delete_link_visual()
var/mob/living/user = mod_link.get_user_callback.Invoke()
if(!QDELETED(user))
user.update_worn_neck()
return delete_link_visual_generic(mod_link)
/obj/item/clothing/neck/link_scryer/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, list/spans, list/message_mods, message_range)
. = ..()
if(speaker != loc)
return
mod_link.visual.say(raw_message, sanitize = FALSE, message_range = 3)
/obj/item/clothing/neck/link_scryer/proc/on_overlay_change(atom/source, cache_index, overlay)
SIGNAL_HANDLER
addtimer(CALLBACK(src, PROC_REF(update_link_visual)), 1 TICKS, TIMER_UNIQUE)
/obj/item/clothing/neck/link_scryer/proc/update_link_visual()
if(QDELETED(mod_link.link_call))
return
var/mob/living/user = loc
mod_link.visual.cut_overlay(mod_link.visual_overlays)
mod_link.visual_overlays = user.overlays - user.active_thinking_indicator
mod_link.visual.add_overlay(mod_link.visual_overlays)
/obj/item/clothing/neck/link_scryer/proc/on_user_set_dir(atom/source, dir, newdir)
SIGNAL_HANDLER
on_user_set_dir_generic(mod_link, newdir || SOUTH)
/obj/item/clothing/neck/link_scryer/loaded
starting_frequency = MODLINK_FREQ_NANOTRASEN
/obj/item/clothing/neck/link_scryer/loaded/Initialize(mapload)
. = ..()
cell = new /obj/item/stock_parts/power_store/cell/high(src)
/obj/item/clothing/neck/link_scryer/loaded/charlie
starting_frequency = MODLINK_FREQ_CHARLIE
/// A MODlink datum, used to handle unique functions that will be used in the MODlink call.
/datum/mod_link
/// Generic name for multitool buffers.
var/name = "MODlink"
/// The frequency of the MODlink. You can only call other MODlinks on the same frequency.
var/frequency
/// The unique ID of the MODlink.
var/id = ""
/// The atom that holds the MODlink.
var/atom/movable/holder
/// A reference to the visuals generated by the MODlink.
var/atom/movable/visual
/// A list of all overlays of the user, copied everytime they have an overlay change.
var/list/visual_overlays = list()
/// A reference to the call between two MODlinks.
var/datum/mod_link_call/link_call
/// A callback that returns the user of the MODlink.
var/datum/callback/get_user_callback
/// A callback that returns whether the MODlink can currently call.
var/datum/callback/can_call_callback
/// A callback that returns the visuals of the MODlink.
var/datum/callback/make_visual_callback
/// A callback that receives the visuals of the other MODlink.
var/datum/callback/get_visual_callback
/// A callback that deletes the visuals of the MODlink.
var/datum/callback/delete_visual_callback
/datum/mod_link/New(
atom/holder,
frequency,
datum/callback/get_user_callback,
datum/callback/can_call_callback,
datum/callback/make_visual_callback,
datum/callback/get_visual_callback,
datum/callback/delete_visual_callback
)
var/attempts = 0
var/digits_to_make = 3
do
if(attempts == 10)
attempts = 0
digits_to_make++
id = ""
for(var/i in 1 to digits_to_make)
id += num2text(rand(0,9))
attempts++
while(GLOB.mod_link_ids[id])
GLOB.mod_link_ids[id] = src
src.frequency = frequency
src.holder = holder
src.get_user_callback = get_user_callback
src.can_call_callback = can_call_callback
src.make_visual_callback = make_visual_callback
src.get_visual_callback = get_visual_callback
src.delete_visual_callback = delete_visual_callback
RegisterSignal(holder, COMSIG_QDELETING, PROC_REF(on_holder_delete))
/datum/mod_link/Destroy()
GLOB.mod_link_ids -= id
if(link_call)
end_call()
get_user_callback = null
make_visual_callback = null
get_visual_callback = null
delete_visual_callback = null
return ..()
/datum/mod_link/proc/get_other()
RETURN_TYPE(/datum/mod_link)
if(!link_call)
return
return link_call.link_caller == src ? link_call.link_receiver : link_call.link_caller
/datum/mod_link/proc/call_link(datum/mod_link/called, mob/user)
if(!frequency)
return
if(!istype(called))
holder.balloon_alert(user, "invalid target!")
return
var/mob/living/link_user = get_user_callback.Invoke()
if(!link_user)
return
if(HAS_TRAIT(link_user, TRAIT_IN_CALL))
holder.balloon_alert(user, "already calling!")
return
var/mob/living/link_target = called.get_user_callback.Invoke()
if(!link_target)
holder.balloon_alert(user, "invalid target!")
return
if(HAS_TRAIT(link_target, TRAIT_IN_CALL))
holder.balloon_alert(user, "target already in call!")
return
if(!can_call_callback.Invoke() || !called.can_call_callback.Invoke())
holder.balloon_alert(user, "can't call!")
return
link_target.playsound_local(get_turf(called.holder), 'sound/items/weapons/ring.ogg', 15, vary = TRUE)
var/atom/movable/screen/alert/modlink_call/alert = link_target.throw_alert("[REF(src)]_modlink", /atom/movable/screen/alert/modlink_call)
alert.desc = "[holder] ([id]) is calling you! Left-click this to accept the call. Right-click to deny it."
alert.link_caller_ref = WEAKREF(src)
alert.link_receiver_ref = WEAKREF(called)
alert.user_ref = WEAKREF(user)
/datum/mod_link/proc/end_call()
QDEL_NULL(link_call)
/datum/mod_link/proc/on_holder_delete(atom/source)
SIGNAL_HANDLER
qdel(src)
/// A MODlink call datum, used to handle the call between two MODlinks.
/datum/mod_link_call
/// The MODlink that is calling.
var/datum/mod_link/link_caller
/// The MODlink that is being called.
var/datum/mod_link/link_receiver
/datum/mod_link_call/New(datum/mod_link/link_caller, datum/mod_link/link_receiver)
link_caller.link_call = src
link_receiver.link_call = src
src.link_caller = link_caller
src.link_receiver = link_receiver
var/mob/living/caller_mob = link_caller.get_user_callback.Invoke()
ADD_TRAIT(caller_mob, TRAIT_IN_CALL, REF(src))
var/mob/living/receiver_mob = link_receiver.get_user_callback.Invoke()
ADD_TRAIT(receiver_mob, TRAIT_IN_CALL, REF(src))
make_visuals()
START_PROCESSING(SSprocessing, src)
/datum/mod_link_call/Destroy()
var/mob/living/caller_mob = link_caller.get_user_callback.Invoke()
if(!QDELETED(caller_mob))
REMOVE_TRAIT(caller_mob, TRAIT_IN_CALL, REF(src))
var/mob/living/receiver_mob = link_receiver.get_user_callback.Invoke()
if(!QDELETED(receiver_mob))
REMOVE_TRAIT(receiver_mob, TRAIT_IN_CALL, REF(src))
STOP_PROCESSING(SSprocessing, src)
clear_visuals()
link_caller.link_call = null
link_receiver.link_call = null
return ..()
/datum/mod_link_call/process(seconds_per_tick)
if(can_continue_call())
return
qdel(src)
/datum/mod_link_call/proc/can_continue_call()
return link_caller.frequency == link_receiver.frequency && link_caller.can_call_callback.Invoke() && link_receiver.can_call_callback.Invoke()
/datum/mod_link_call/proc/make_visuals()
var/caller_visual = link_caller.make_visual_callback.Invoke()
var/receiver_visual = link_receiver.make_visual_callback.Invoke()
link_caller.get_visual_callback.Invoke(receiver_visual)
link_receiver.get_visual_callback.Invoke(caller_visual)
/datum/mod_link_call/proc/clear_visuals()
link_caller.delete_visual_callback.Invoke()
link_receiver.delete_visual_callback.Invoke()
/proc/call_link(mob/user, datum/mod_link/calling_link)
if(!calling_link.frequency)
return
var/list/callers = list()
for(var/id in GLOB.mod_link_ids)
var/datum/mod_link/link = GLOB.mod_link_ids[id]
if(link.frequency != calling_link.frequency)
continue
if(link == calling_link)
continue
if(!link.can_call_callback.Invoke())
continue
callers["[link.holder] ([id])"] = id
if(!length(callers))
calling_link.holder.balloon_alert(user, "no targets on freq [calling_link.frequency]!")
return
var/chosen_link = tgui_input_list(user, "Choose ID to call from [calling_link.frequency] frequency", "MODlink", callers)
if(!chosen_link)
return
calling_link.call_link(GLOB.mod_link_ids[callers[chosen_link]], user)
/atom/movable/screen/alert/modlink_call
name = "MODlink Call Incoming"
desc = "Someone is calling you! Left-click this to accept the call. Right-click to deny it."
icon_state = "called"
timeout = 10 SECONDS
clickable_glow = TRUE
var/end_message = "call timed out!"
/// A weak reference to the MODlink that is calling.
var/datum/weakref/link_caller_ref
/// A weak reference to the MODlink that is being called.
var/datum/weakref/link_receiver_ref
/// A weak reference to the mob that is calling.
var/datum/weakref/user_ref
/atom/movable/screen/alert/modlink_call/Click(location, control, params)
. = ..()
if(usr != owner)
return
var/datum/mod_link/link_caller = link_caller_ref.resolve()
var/datum/mod_link/link_receiver = link_receiver_ref.resolve()
if(!link_caller || !link_receiver)
return
if(link_caller.link_call || link_receiver.link_call)
return
var/list/modifiers = params2list(params)
if(LAZYACCESS(modifiers, RIGHT_CLICK))
end_message = "call denied!"
owner.clear_alert("[REF(link_caller)]_modlink")
return
end_message = "call accepted"
new /datum/mod_link_call(link_caller, link_receiver)
owner.clear_alert("[REF(link_caller)]_modlink")
/atom/movable/screen/alert/modlink_call/Destroy()
var/mob/living/user = user_ref?.resolve()
var/datum/mod_link/link_caller = link_caller_ref?.resolve()
if(!user || !link_caller)
return ..()
link_caller.holder.balloon_alert(user, end_message)
return ..()