diff --git a/code/_globalvars/lists/faxes.dm b/code/_globalvars/lists/faxes.dm new file mode 100644 index 00000000000000..54037f2ca5005f --- /dev/null +++ b/code/_globalvars/lists/faxes.dm @@ -0,0 +1,21 @@ +/** + * This defines the list of faxes managed by the server administrators. They are not physically present in + * the game, but are shown in the fax list as existing. + * Lists: + * * additional_faxes_list - A list of "legal" faxes available with authorization. + * * syndicate_faxes_list - List of faxes available after hacking. + * + * The list consists of the following elements: + * * fax_name - The name displayed in the fax list. + * * button_color - The color of this fax button in the list of all faxes. + */ +GLOBAL_LIST_INIT(additional_faxes_list, list( + list("fax_name" = "Central Command", "button_color" = "#34c924"), + list("fax_name" = "Clown Planet", "button_color" = "#f4c800"), +)) + +GLOBAL_LIST_INIT(syndicate_faxes_list, list( + list("fax_name" = "Syndicate Coordination Center", "button_color" = "#ff0000"), + list("fax_name" = "Federation of Wizards", "button_color" = "#8b00ff"), + list("fax_name" = "Nar-Sie Church", "button_color" = "#8b0000"), +)) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 912f056518c0fa..8d95c7a633cffb 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -20,7 +20,8 @@ GLOBAL_PROTECT(admin_verbs_default) /client/proc/tag_datum_mapview, /client/proc/debugstatpanel, /client/proc/fix_air, /*resets air in designated radius to its default atmos composition*/ - /client/proc/requests + /client/proc/requests, + /client/proc/fax_manager, ) GLOBAL_LIST_INIT(admin_verbs_admin, world.AVerbsAdmin()) GLOBAL_PROTECT(admin_verbs_admin) diff --git a/code/modules/admin/verbs/fax_manager.dm b/code/modules/admin/verbs/fax_manager.dm new file mode 100644 index 00000000000000..127cbf7df5fe5d --- /dev/null +++ b/code/modules/admin/verbs/fax_manager.dm @@ -0,0 +1,6 @@ +/client/proc/fax_manager() + set name = "Fax Manager" + set desc = "Open the manager panel to view all requests during the round in progress." + set category = "Admin.Game" + SSblackbox.record_feedback("tally", "admin_verb", 1, "Fax Manager") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + GLOB.fax_manager.ui_interact(usr) diff --git a/code/modules/paperwork/fax.dm b/code/modules/paperwork/fax.dm index 40255285be9a14..a73790abc51653 100644 --- a/code/modules/paperwork/fax.dm +++ b/code/modules/paperwork/fax.dm @@ -18,6 +18,10 @@ var/seconds_electrified = MACHINE_NOT_ELECTRIFIED /// If true, the fax machine is jammed and needs cleaning var/jammed = FALSE + /// Determines the possibility of sending papers to the additional faxes. + var/access_additional_faxes = FALSE + /// Defines a list of accesses whose owners can open a connection with the additional faxes. + var/static/access_additional_faxes_required = list(ACCESS_CAPTAIN) /// Necessary to hide syndicate faxes from the general list. Doesn't mean he's EMAGGED! var/syndicate_network = FALSE /// True if the fax machine should be visible to other fax machines in general. @@ -150,6 +154,26 @@ return return ..() +// Checks if the card has access to switch "legal" faxes of administrators. +/obj/machinery/fax/proc/access_additional_faxes_check(mob/living/user) + if(isAdminGhostAI(user)) + return TRUE + + var/obj/item/card/id/used_id = user.get_idcard(TRUE) + // We check if it makes sense to check access at all. + if(!access_additional_faxes_required || !used_id.access) + return FALSE + + for(var/requested_access in access_additional_faxes_required) + if(requested_access in used_id.access) + return TRUE + return FALSE + +// Switches access to the "legal" administrator's fax list. Access to the "illegal" is switched by hacking. +/obj/machinery/fax/proc/access_additional_faxes_toggle() + access_additional_faxes = !access_additional_faxes + say("The channel of communication with CentCom is [access_additional_faxes ? "open" : "close"].") + /** * Attempts to clean out a jammed machine using a passed item. * Returns true if successful. @@ -204,6 +228,12 @@ ui = new(user, src, "Fax") ui.open() +/obj/machinery/fax/ui_static_data(mob/user) + var/list/data = list() + data["additional_faxes_list"] = GLOB.additional_faxes_list + data["syndicate_faxes_list"] = GLOB.syndicate_faxes_list + return data + /obj/machinery/fax/ui_data(mob/user) var/list/data = list() //Record a list of all existing faxes. @@ -223,6 +253,8 @@ data["fax_id"] = fax_id data["fax_name"] = fax_name data["visible"] = visible_to_network + data["access_additional_faxes"] = access_additional_faxes + data["сan_switch_access"] = access_additional_faxes_check(user) // In this case, we don't care if the fax is hacked or in the syndicate's network. The main thing is to check the visibility of other faxes. data["syndicate_network"] = (syndicate_network || (obj_flags & EMAGGED)) data["has_paper"] = !!loaded_item_ref?.resolve() @@ -245,6 +277,8 @@ playsound(src, 'sound/machines/eject.ogg', 50, FALSE) update_appearance() return TRUE + if("access_additional_faxes_toggle") + access_additional_faxes_toggle() if("send") var/obj/item/loaded = loaded_item_ref?.resolve() if (!loaded) @@ -255,6 +289,17 @@ loaded_item_ref = null update_appearance() return TRUE + if("send_to_additional_fax") + var/obj/item/loaded = loaded_item_ref?.resolve() + if (!loaded) + return + if(istype(loaded, /obj/item/paper)) + if(send_to_additional_faxes(loaded, usr, params["name"], params["color"])) + loaded_item_ref = null + update_appearance() + return TRUE + else + say("The destination fax blocks the reception of this item.") if("history_clear") history_clear() return TRUE @@ -293,12 +338,39 @@ playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) return FALSE FAX.receive(loaded, fax_name) - history_add("Send", FAX.fax_name) - INVOKE_ASYNC(src, .proc/animate_object_travel, loaded, "fax_receive", find_overlay_state(loaded, "send")) - playsound(src, 'sound/machines/high_tech_confirm.ogg', 50, FALSE) + playback_sending(loaded, FAX.fax_name) return TRUE return FALSE +/** + * The procedure for sending a paper to virtual admins fax machine. + * + * This procedure is similar to the send procedure except that it sends the paper to + * a "virtual" fax to a special administrator list. + * Arguments: + * * paper - The paper to be sent. + * * sender - Reference to the sender's substance. + * * receiver_name - The recipient's fax name, which will be displayed in the administrator's list. + * * receiver_color - The color the receiver_name will be colored in. + */ +/obj/machinery/fax/proc/send_to_additional_faxes(obj/item/paper/paper, mob/sender, receiver_name, receiver_color) + GLOB.fax_manager.receive_request(sender, src, receiver_name, paper, receiver_color) + playback_sending(paper, receiver_name) + return TRUE + +/** + * The procedure for playing the animation and the sending sound. + * + * Procedure called to add to the history of sending messages, playing the sending animation and sound. + * Arguments: + * * loaded - Sending item to determine the animation.. + * * receiver_name - Recipient's name to be added to the message history. + */ +/obj/machinery/fax/proc/playback_sending(obj/item/loaded, receiver_name) + history_add("Send", receiver_name) + INVOKE_ASYNC(src, .proc/animate_object_travel, loaded, "fax_receive", find_overlay_state(loaded, "send")) + playsound(src, 'sound/machines/high_tech_confirm.ogg', 50, FALSE) + /** * Procedure for accepting papers from another fax machine. * diff --git a/code/modules/paperwork/fax_manager.dm b/code/modules/paperwork/fax_manager.dm new file mode 100644 index 00000000000000..67fd231dc87166 --- /dev/null +++ b/code/modules/paperwork/fax_manager.dm @@ -0,0 +1,149 @@ +GLOBAL_DATUM_INIT(fax_manager, /datum/fax_manager, new) + +/** + * Fax Request Manager + * + * In its functionality it is similar to the usual Request Manager, but respectively for faxes. + * This manager allows you to send faxes on behalf of certain virtual faxes to all existing faxes, + * as well as receive faxes in their name from the players. + */ +/datum/fax_manager + /// A list that contains faxes from players and other related information. You can view the filling of its fields in procedure receive_request. + var/list/requests = list() + +/datum/fax_manager/Destroy(force, ...) + QDEL_LIST(requests) + return ..() + +/datum/fax_manager/ui_interact(mob/user, datum/tgui/ui = null) + ui = SStgui.try_update_ui(user, src, ui) + if (!ui) + ui = new(user, src, "FaxManager") + ui.open() + +/datum/fax_manager/ui_state(mob/user) + return GLOB.admin_state + +/datum/fax_manager/ui_static_data(mob/user) + var/list/data = list() + // Record additional faxes on a separate list + data["additional_faxes"] = GLOB.additional_faxes_list + GLOB.syndicate_faxes_list + return data + +/datum/fax_manager/ui_data(mob/user) + var/list/data = list() + //Record a list of all existing faxes. + for(var/obj/machinery/fax/FAX in GLOB.machines) + var/list/fax_data = list() + fax_data["fax_name"] = FAX.fax_name + fax_data["fax_id"] = FAX.fax_id + fax_data["syndicate_network"] = FAX.syndicate_network + data["faxes"] += list(fax_data) + for(var/list/REQUEST in requests) + var/list/request = list() + request["id_message"] = REQUEST["id_message"] + request["time"] = REQUEST["time"] + var/mob/sender = REQUEST["sender"] + request["sender_name"] = sender.name + request["sender_fax_id"] = REQUEST["sender_fax_id"] + request["sender_fax_name"] = REQUEST["sender_fax_name"] + request["receiver_fax_name"] = REQUEST["receiver_fax_name"] + data["requests"] += list(request) + return data + +/datum/fax_manager/ui_act(action, list/params) + . = ..() + if(.) + return + + switch(action) + if("send") + for(var/obj/machinery/fax/FAX in GLOB.machines) + if (FAX.fax_id == params["fax_id"]) + var/obj/item/paper/paper = new() + paper.add_raw_text(params["message"]) + FAX.receive(paper, params["fax_name"]) + return TRUE + if("flw_fax") + for(var/obj/machinery/fax/FAX in GLOB.machines) + if (FAX.fax_id == params["fax_id"]) + usr.client.admin_follow(FAX) + return TRUE + if("read_message") + var/list/REQUEST = get_request(params["id_message"]) + var/obj/item/paper/request/paper = REQUEST["paper"] + paper.ui_interact(usr) + return TRUE + if("flw") + var/list/REQUEST = get_request(params["id_message"]) + usr.client.admin_follow(REQUEST["sender"]) + return TRUE + if("pp") + var/list/REQUEST = get_request(params["id_message"]) + usr.client.holder.show_player_panel(REQUEST["sender"]) + return TRUE + if("vv") + var/list/REQUEST = get_request(params["id_message"]) + usr.client.debug_variables(REQUEST["sender"]) + return TRUE + if("sm") + var/list/REQUEST = get_request(params["id_message"]) + usr.client.cmd_admin_subtle_message(REQUEST["sender"]) + return TRUE + if("logs") + var/list/REQUEST = get_request(params["id_message"]) + if(!ismob(REQUEST["sender"])) + to_chat(usr, "This can only be used on instances of type /mob.", confidential = TRUE) + return TRUE + show_individual_logging_panel(REQUEST["sender"], null, null) + return TRUE + if("smite") + var/list/REQUEST = get_request(params["id_message"]) + if(!check_rights(R_FUN)) + to_chat(usr, "Insufficient permissions to smite, you require +FUN", confidential = TRUE) + return TRUE + var/mob/living/carbon/human/H = REQUEST["sender"] + if (!H || !istype(H)) + to_chat(usr, "This can only be used on instances of type /mob/living/carbon/human", confidential = TRUE) + return TRUE + usr.client.smite(H) + return TRUE + +/datum/fax_manager/proc/get_request(id_message) + for(var/list/REQUEST in requests) + if(REQUEST["id_message"] == id_message) + return REQUEST + +/datum/fax_manager/proc/receive_request(mob/sender, obj/machinery/fax/sender_fax, receiver_fax_name, obj/item/paper/paper, receiver_color) + var/list/request = list() + var/obj/item/paper/request/message = new() + request["id_message"] = requests.len + request["time"] = gameTimestamp() + request["sender"] = sender + request["sender_fax_id"] = sender_fax.fax_id + request["sender_fax_name"] = sender_fax.fax_name + request["receiver_fax_name"] = receiver_fax_name + message.copy_properties(paper) + request["paper"] = message + requests += list(request) + var/msg = span_adminnotice("[sanitize(receiver_fax_name)] fax received a message from [sanitize(sender_fax.fax_name)][ADMIN_JMP(sender_fax)]/[ADMIN_FULLMONTY(sender)]") + to_chat(GLOB.admins, msg, confidential = TRUE) + for(var/client/admin in GLOB.admins) + if((admin.prefs.chat_toggles & CHAT_PRAYER) && (admin.prefs.toggles & SOUND_PRAYERS)) + SEND_SOUND(admin, sound('sound/machines/printer.ogg')) + +// A special piece of paper for the administrator that will open the interface no matter what. +/obj/item/paper/request/ui_status() + return UI_INTERACTIVE + +// I'm sure there's a better way to transfer it, I just couldn't find it +/obj/item/paper/request/proc/copy_properties(obj/item/paper/paper) + raw_text_inputs = paper.raw_text_inputs + raw_stamp_data = paper.raw_stamp_data + raw_field_input_data = paper.raw_field_input_data + show_written_words = paper.show_written_words + stamp_cache = paper.stamp_cache + contact_poison = paper.contact_poison + contact_poison_volume = paper.contact_poison_volume + default_raw_text = paper.default_raw_text + input_field_count = paper.input_field_count diff --git a/tgstation.dme b/tgstation.dme index 9470644eaa48e1..beff93e271b164 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -403,6 +403,7 @@ #include "code\_globalvars\lists\ambience.dm" #include "code\_globalvars\lists\client.dm" #include "code\_globalvars\lists\color.dm" +#include "code\_globalvars\lists\faxes.dm" #include "code\_globalvars\lists\flavor_misc.dm" #include "code\_globalvars\lists\keybindings.dm" #include "code\_globalvars\lists\maintenance_loot.dm" @@ -2097,6 +2098,7 @@ #include "code\modules\admin\verbs\debug.dm" #include "code\modules\admin\verbs\diagnostics.dm" #include "code\modules\admin\verbs\ert.dm" +#include "code\modules\admin\verbs\fax_manager.dm" #include "code\modules\admin\verbs\fix_air.dm" #include "code\modules\admin\verbs\fov.dm" #include "code\modules\admin\verbs\fps.dm" @@ -3912,6 +3914,7 @@ #include "code\modules\paperwork\contract.dm" #include "code\modules\paperwork\desk_bell.dm" #include "code\modules\paperwork\fax.dm" +#include "code\modules\paperwork\fax_manager.dm" #include "code\modules\paperwork\filingcabinet.dm" #include "code\modules\paperwork\folders.dm" #include "code\modules\paperwork\folders_premade.dm" diff --git a/tgui/packages/tgui/interfaces/Fax.tsx b/tgui/packages/tgui/interfaces/Fax.tsx index 6c5a2256089d23..febb9f0cd7c40f 100644 --- a/tgui/packages/tgui/interfaces/Fax.tsx +++ b/tgui/packages/tgui/interfaces/Fax.tsx @@ -9,7 +9,11 @@ type FaxData = { fax_name: string; visible: boolean; has_paper: string; + access_additional_faxes: boolean; + сan_switch_access: boolean; + additional_faxes_list: AdditionalFaxesList[]; syndicate_network: boolean; + syndicate_faxes_list: AdditionalFaxesList[]; fax_history: FaxHistory[]; }; @@ -21,6 +25,11 @@ type FaxInfo = { syndicate_network: boolean; }; +type AdditionalFaxesList = { + fax_name: string; + button_color: string; +}; + type FaxHistory = { history_type: string; history_fax_name: string; @@ -67,8 +76,50 @@ export const Fax = (props, context) => { )} -
+
act('access_additional_faxes_toggle')} + disabled={!data.сan_switch_access} + tooltip="Manage access to the expanded fax list."> + {data.access_additional_faxes ? 'Unlogin' : 'Login'} + + }> + {!!data.access_additional_faxes && + data.additional_faxes_list.map((fax: AdditionalFaxesList) => ( + + ))} + {!!data.syndicate_network && + data.syndicate_faxes_list.map((fax: AdditionalFaxesList) => ( + + ))} {faxes.map((fax: FaxInfo) => ( + ))} +
+
+ + {!!data.requests && + data.requests.map((request: RequestsList) => ( + + { + setMessageText(paper_raw_text); + }} + /> + + ))} + +
+ + {!!messagingAssociates && ( + setMessagingAssociates(false)} + onSubmit={(selectedFaxId, selectedSenderName, messageInput) => { + setMessagingAssociates(false); + act('send', { + fax_id: selectedFaxId, + fax_name: selectedSenderName, + message: messageInput, + }); + }} + onFollow={(selectedFaxId) => { + act('follow_fax', { + fax_id: selectedFaxId, + }); + }} + /> + )} + + ); +}; + +const FaxMessageModal = (props, context) => { + const { data } = useBackend(context); + const [messageInput, setMessageInput] = useLocalState( + context, + props.messageInput, + '' + ); + + const [selectedSenderName, setSelectedSenderName] = useLocalState( + context, + 'selectedSenderName', + '' + ); + const [сustomSenderName, setCustomSenderName] = useLocalState( + context, + 'сustomSenderName', + '' + ); + + const additional_faxes: string[] = []; + for (let fax of data.additional_faxes) { + additional_faxes.push(fax.fax_name); + } + additional_faxes.push('Custom Name'); + + return ( + + + + Send a message to {props.selectedFaxName}/{props.selectedFaxId}: + + + + setSelectedSenderName(value)} + /> + + + {selectedSenderName === 'Custom Name' && ( + + + setCustomSenderName(value)} /> + + + )} + +