diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index aaeab50a661..b94f144e493 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -80,3 +80,17 @@ #define isenergy(A) istype(A, /obj/projectile/energy) #define istransparentturf(A) (HAS_TRAIT(A, TURF_Z_TRANSPARENT_TRAIT)) + +/** + * A common layered filter pattern for psionics. Helps with reducing the size of guard clauses. + * Includes every psionic check for RECEIVING starting with the Zona Bovina, and ending with Psi-sensitivity. + * Set the sensitivity_threshold for your desired minimum Psi-sensitivity to pass. + * The most common ones you might want are: + * 2 for "Antag-like". + * 1 for "Skrell-like". + * 0 for "Human-like". + * + * This one is a define instead of a proc, which gives better code performance. + * This define is only usable if user is at least an /atom/movable/, such as /mob/ + */ +#define IS_TELEPATHY_BLOCKED(user, sensitivity_threshold) (!user.has_zona_bovinae() || user.is_psi_blocked() || user.check_psi_sensitivity() < sensitivity_threshold) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 3e874504b73..38278df1383 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -49,6 +49,7 @@ GLOBAL_LIST_INIT(admin_verbs_admin, list( /client/proc/cmd_admin_local_narrate, //sends text to all mobs within 7 tiles of src.mob /client/proc/cmd_admin_local_screen_text, /client/proc/cmd_admin_global_screen_text, + /client/proc/cmd_admin_open_narrate_panel, // Admin logged variant of the narrator panel. /client/proc/cmd_admin_world_narrate, //sends text to all players with no padding, /client/proc/cmd_admin_create_centcom_report, /client/proc/check_ai_laws, //shows AI and borg laws, @@ -309,6 +310,7 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list( /client/proc/cmd_admin_local_narrate, /client/proc/cmd_admin_local_screen_text, /client/proc/cmd_admin_global_screen_text, + /client/proc/cmd_admin_open_narrate_panel, // Admin logged variant of the narrator panel. /client/proc/cmd_admin_world_narrate, /client/proc/cmd_admin_grab_observers, /client/proc/cmd_admin_create_centcom_report, diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm index c503cbbd30f..2855f174b1c 100644 --- a/code/modules/admin/verbs/randomverbs.dm +++ b/code/modules/admin/verbs/randomverbs.dm @@ -26,7 +26,7 @@ to_chat(src, "Only administrators may use this command.") return - var/msg = sanitize(input("Message:", "Subtle PM to [M.key]")) + var/msg = tgui_input_text(usr, "Enter your subtle message", "Subtle PM to [M.key]") if (!msg) return @@ -83,7 +83,7 @@ to_chat(src, "Only administrators may use this command.") return - var/msg = html_decode(sanitize(input("Message:", "Enter the text you wish to appear to everyone:"))) + var/msg = tgui_input_text(usr, "Enter the text you wish to appear to everyone:", "Message:") if (!msg) return @@ -110,7 +110,7 @@ else return - var/msg = html_decode(sanitize(input("Message:", "Enter the text you wish to appear to everyone within seven tiles of you:"))) + var/msg = tgui_input_text(usr, "Enter the text you wish to appear to everyone within seven tiles of you:", "Message:") if(!msg) return for(var/M in message_mobs) @@ -192,7 +192,7 @@ if(!M) return - var/msg = html_decode(sanitize(input("Message:", "Enter the text you wish to appear to your target:"))) + var/msg = tgui_input_text(usr, "Enter the text you wish to appear to your target:", "Message:") if( !msg ) return @@ -1126,3 +1126,18 @@ Traitors and the like can also be revived with the previous role mostly intact. message_admins("[key_name_admin(usr)] sent a pregenerated tip of the round.") log_admin("[key_name(usr)] sent a pregenerated Tip of the Round.") feedback_add_details("admin_verb","FAP") + +// Variant of the narrate panel that is for admins. This is logged to check if people are abusing it. +/client/proc/cmd_admin_open_narrate_panel() + set category = "Special Verbs" + set name = "Narration Panel" + + if (!check_rights(R_ADMIN, TRUE)) + return + + var/datum/tgui_module/narrate_panel/NP = new /datum/tgui_module/narrate_panel(usr) + NP.ui_interact(usr) + + message_admins("[key_name_admin(usr)] used the Narration Panel") + log_admin("[key_name(usr)] used the Narration Panel") + feedback_add_details("admin_verb", "AONP") diff --git a/code/modules/nano/modules/narrate_panel.dm b/code/modules/nano/modules/narrate_panel.dm index 7a59f2f89ba..6e7ffdd64a4 100644 --- a/code/modules/nano/modules/narrate_panel.dm +++ b/code/modules/nano/modules/narrate_panel.dm @@ -8,6 +8,7 @@ var/list/data = list() data["narrate_styles"] = list("danger", "notice", "warning", "alien", "cult") data["narrate_locations"] = list("View", "Range", "Z-Level", "Global") + data["narrate_filters"] = list("None", "Skrell-like Psi-sensitives", "Human-like Psi-sensitives", "Silicons", "Silicons + Implants", "Hivenet") return data /datum/tgui_module/narrate_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) @@ -63,5 +64,65 @@ if("Global") mobs_to_message = GLOB.player_list.Copy() + // Skip the switch if no filter was selected. + if (params["narrate_filter"] == "None") + for(var/mob/actor in mobs_to_message) + to_chat(actor, "[narrate_text]") + return + + // Create a copy of the message receivers. We have to iterate the copy of the list in order to safely remove entries from the list of message receivers. + // Otherwise we'll get memory exceptions. + var/list/filtered_list = mobs_to_message.Copy() + var/narrate_filter = params["narrate_filter"] + switch(narrate_filter) + if ("Skrell-like Psi-sensitives") + for(var/mob/filteree in filtered_list) + // This will check for anyone who is capable of receiving and interpreting telepathic messages. + // It will include Skrell who don't have a mindshield or the Low Psi-sensitivity trait. + // It will also include characters who have Psi-receivers, the High Psi-sensitivity trait, or are under the effects of Psycho-nootropic drugs. + if (IS_TELEPATHY_BLOCKED(filteree, PSI_RANK_SENSITIVE)) + mobs_to_message.Remove(filteree) + continue + if ("Human-like Psi-sensitives") + for(var/mob/filteree in filtered_list) + // This will check for anyone who has a Zona Bovina capable of hearing psionics at all. + if (IS_TELEPATHY_BLOCKED(filteree, 0)) + mobs_to_message.Remove(filteree) + continue + if ("Silicons") + for(var/mob/filteree in filtered_list) + // List will include Borgs, Robots, Shipbounds, pAIs, and IPCs. + if(issilicon(filteree) || isipc(filteree) || ispAI(filteree)) + continue + + // Remove anyone not made of silicon. + mobs_to_message.Remove(filteree) + if ("Silicons + Implants") + for(var/mob/filteree in filtered_list) + // List will include Borgs, Robots, Shipbounds, pAIs, and IPCs. + if(issilicon(filteree) || isipc(filteree) || ispAI(filteree)) + continue + + + if (!ishuman(filteree)) + // The mob in question can't have a brain at all, filter it early. + mobs_to_message.Remove(filteree) + continue + + var/mob/living/carbon/human/brain_haver = filteree + // Check for silicon brain implants. + var/obj/item/organ/internal/brain = brain_haver.internal_organs_by_name[BP_BRAIN] + if (brain && ((brain.status & ORGAN_ASSISTED) || (brain.status & ORGAN_ROBOT))) + continue + + // Remove anyone not made of silicon or doesn't have a cranial implant. + mobs_to_message.Remove(filteree) + if ("Hivenet") + for(var/mob/filteree in filtered_list) + // Filter anyone who doesn't have the Vaurca language at all. + if (!(GLOB.all_languages[LANGUAGE_VAURCA] in filteree.languages)) + mobs_to_message.Remove(filteree) + continue + for(var/mob/actor in mobs_to_message) to_chat(actor, "[narrate_text]") diff --git a/code/modules/psionics/mob/mob_helpers.dm b/code/modules/psionics/mob/mob_helpers.dm index 91d31e6f2d6..55018364f44 100644 --- a/code/modules/psionics/mob/mob_helpers.dm +++ b/code/modules/psionics/mob/mob_helpers.dm @@ -74,3 +74,15 @@ /mob/living/simple_animal/is_psi_pingable() return psi_pingable + +/** + * A common layered filter pattern for psionics. Helps with reducing the size of guard clauses. + * Includes every psionic check for RECEIVING starting with the Zona Bovina, and ending with Psi-sensitivity. + * Set the sensitivity_threshold for your desired minimum Psi-sensitivity to pass. + * The most common ones you might want are: + * 2 for "Antag-like". + * 1 for "Skrell-like". + * 0 for "Human-like". + */ +/atom/movable/proc/is_telepathy_blocked(var/sensitivity_threshold = 0) + return (!has_zona_bovinae() || is_psi_blocked() || check_psi_sensitivity() < sensitivity_threshold) diff --git a/html/changelogs/hellfirejag narrate-panel-filters.yml b/html/changelogs/hellfirejag narrate-panel-filters.yml new file mode 100644 index 00000000000..a94924eba1f --- /dev/null +++ b/html/changelogs/hellfirejag narrate-panel-filters.yml @@ -0,0 +1,6 @@ +author: Hellfirejag +delete-after: True +changes: + - rscadd: "The Narrator panel now includes optional filter selections, allowing admins and storytellers to narrate to certain kinds of players, such as Synthetics or Psi-sensitives." + - rscadd: "The Narration panel can now be found in the Special Verbs tab for admins. Its usage will be logged." + - bugfix: "Fixed issues with admin announcements being improperly sanitized." diff --git a/tgui/packages/tgui/interfaces/NarratePanel.tsx b/tgui/packages/tgui/interfaces/NarratePanel.tsx index 3677d63cedb..14f5cb72447 100644 --- a/tgui/packages/tgui/interfaces/NarratePanel.tsx +++ b/tgui/packages/tgui/interfaces/NarratePanel.tsx @@ -13,6 +13,7 @@ import { Window } from '../layouts'; export type NarrateData = { narrate_styles: string[]; narrate_locations: string[]; + narrate_filters: string[]; }; export const NarratePanel = (props, context) => { @@ -42,6 +43,11 @@ export const NarratePanel = (props, context) => { 'narrateLocation', 'View', ); + const [narrateFilter, setNarrateFilter] = useLocalState( + context, + 'narrateFilter', + 'None', + ); return ( @@ -61,6 +67,7 @@ export const NarratePanel = (props, context) => { narrate_range: narrateRange, narrate_style: narrateStyle, narrate_location: narrateLocation, + narrate_filter: narrateFilter, }) } /> @@ -116,6 +123,15 @@ export const NarratePanel = (props, context) => { onSelected={(value) => setNarrateLocation(value)} /> + + setNarrateFilter(value)} + /> +