diff --git a/code/__DEFINES/keybindings.dm b/code/__DEFINES/keybindings.dm index b12d546c73c..fa50170f196 100644 --- a/code/__DEFINES/keybindings.dm +++ b/code/__DEFINES/keybindings.dm @@ -14,6 +14,7 @@ #define KB_CATEGORY_EMOTE_SILICON 14 #define KB_CATEGORY_EMOTE_ANIMAL 15 #define KB_CATEGORY_EMOTE_CUSTOM 16 +#define KB_CATEGORY_COMMUNICATION 17 #define KB_CATEGORY_UNSORTED 1000 ///Max length of a keypress command before it's considered to be a forged packet/bogus command diff --git a/code/__DEFINES/preferences.dm b/code/__DEFINES/preferences.dm index 2f218b3923d..2521f64a313 100644 --- a/code/__DEFINES/preferences.dm +++ b/code/__DEFINES/preferences.dm @@ -19,7 +19,7 @@ #define PREFTOGGLE_CHAT_GHOSTSIGHT (1<<3) #define PREFTOGGLE_CHAT_PRAYER (1<<4) #define PREFTOGGLE_CHAT_RADIO (1<<5) -#define PREFTOGGLE_AZERTY (1<<6) +// #define PREFTOGGLE_AZERTY (1<<6) // obsolete #define PREFTOGGLE_CHAT_DEBUGLOGS (1<<7) #define PREFTOGGLE_CHAT_LOOC (1<<8) #define PREFTOGGLE_CHAT_GHOSTRADIO (1<<9) @@ -33,37 +33,38 @@ //#define PREFTOGGLE_UI_DARKMODE (1<<17) //not used since tgchat #define PREFTOGGLE_DISABLE_KARMA (1<<18) #define PREFTOGGLE_CHAT_NO_MENTORTICKETLOGS (1<<19) -#define PREFTOGGLE_TYPING_ONCE (1<<20) +// #define PREFTOGGLE_TYPING_ONCE (1<<20) // Not used since tgui say #define PREFTOGGLE_AMBIENT_OCCLUSION (1<<21) #define PREFTOGGLE_CHAT_GHOSTPDA (1<<22) -#define PREFTOGGLE_NUMPAD_TARGET (1<<23) +// #define PREFTOGGLE_NUMPAD_TARGET 8388608 // Made obsolete with key bindings #define TOGGLES_TOTAL 16777215 // If you add or remove a preference toggle above, make sure you update this define with the total value of the toggles combined. -#define TOGGLES_DEFAULT (PREFTOGGLE_CHAT_OOC|PREFTOGGLE_CHAT_DEAD|PREFTOGGLE_CHAT_GHOSTEARS|PREFTOGGLE_CHAT_GHOSTSIGHT|PREFTOGGLE_CHAT_PRAYER|PREFTOGGLE_CHAT_RADIO|PREFTOGGLE_CHAT_LOOC|PREFTOGGLE_MEMBER_PUBLIC|PREFTOGGLE_DONATOR_PUBLIC|PREFTOGGLE_AMBIENT_OCCLUSION|PREFTOGGLE_CHAT_GHOSTPDA|PREFTOGGLE_NUMPAD_TARGET) +#define TOGGLES_DEFAULT (PREFTOGGLE_CHAT_OOC|PREFTOGGLE_CHAT_DEAD|PREFTOGGLE_CHAT_GHOSTEARS|PREFTOGGLE_CHAT_GHOSTSIGHT|PREFTOGGLE_CHAT_PRAYER|PREFTOGGLE_CHAT_RADIO|PREFTOGGLE_CHAT_LOOC|PREFTOGGLE_MEMBER_PUBLIC|PREFTOGGLE_DONATOR_PUBLIC|PREFTOGGLE_AMBIENT_OCCLUSION|PREFTOGGLE_CHAT_GHOSTPDA) // toggles_2 variables. These MUST be prefixed with PREFTOGGLE_2 -#define PREFTOGGLE_2_RANDOMSLOT (1<<0) // 1 -#define PREFTOGGLE_2_FANCYUI (1<<1) // 2 -#define PREFTOGGLE_2_ITEMATTACK (1<<2) // 4 -#define PREFTOGGLE_2_WINDOWFLASHING (1<<3) // 8 -#define PREFTOGGLE_2_ANON (1<<4) // 16 -#define PREFTOGGLE_2_AFKWATCH (1<<5) // 32 -#define PREFTOGGLE_2_RUNECHAT (1<<6) // 64 -#define PREFTOGGLE_2_DEATHMESSAGE (1<<7) // 128 -#define PREFTOGGLE_2_EMOTE_BUBBLE (1<<8) // 256 -#define PREFTOGGLE_2_SEE_ITEM_OUTLINES (1<<9) // 512 +#define PREFTOGGLE_2_RANDOMSLOT (1<<0) // 1 +#define PREFTOGGLE_2_FANCYUI (1<<1) // 2 +#define PREFTOGGLE_2_ITEMATTACK (1<<2) // 4 +#define PREFTOGGLE_2_WINDOWFLASHING (1<<3) // 8 +#define PREFTOGGLE_2_ANON (1<<4) // 16 +#define PREFTOGGLE_2_AFKWATCH (1<<5) // 32 +#define PREFTOGGLE_2_RUNECHAT (1<<6) // 64 +#define PREFTOGGLE_2_DEATHMESSAGE (1<<7) // 128 +// #define PREFTOGGLE_2_EMOTE_BUBBLE (1<<8) // 256 tgui say(maybe temporary) +#define PREFTOGGLE_2_SEE_ITEM_OUTLINES (1<<9) // 512 // Yes I know this being an "enable to disable" is misleading, but it avoids having to tweak all existing pref entries -#define PREFTOGGLE_2_REVERB_DISABLE (1<<10) // 1024 -#define PREFTOGGLE_2_MC_TAB (1<<11) // 2048 -#define PREFTOGGLE_2_DISABLE_TGUI_INPUT (1<<12) // 4096 -#define PREFTOGGLE_2_PARALLAX_MULTIZ (1<<13) // 8192 -#define PREFTOGGLE_2_DISABLE_VOTE_POPUPS (1<<14) // 16384 -#define PREFTOGGLE_2_SWAP_INPUT_BUTTONS (1<<15) // 32768 -#define PREFTOGGLE_2_LARGE_INPUT_BUTTONS (1<<16) // 65536 -#define PREFTOGGLE_2_BIG_STRIP_MENU (1<<17) // 131072 - -#define TOGGLES_2_TOTAL 262143 // If you add or remove a preference toggle above, make sure you update this define with the total value of the toggles combined. +#define PREFTOGGLE_2_REVERB_DISABLE (1<<10) // 1024 +#define PREFTOGGLE_2_MC_TAB (1<<11) // 2048 +#define PREFTOGGLE_2_DISABLE_TGUI_INPUT (1<<12) // 4096 +#define PREFTOGGLE_2_PARALLAX_MULTIZ (1<<13) // 8192 +#define PREFTOGGLE_2_DISABLE_VOTE_POPUPS (1<<14) // 16384 +#define PREFTOGGLE_2_SWAP_INPUT_BUTTONS (1<<15) // 32768 +#define PREFTOGGLE_2_LARGE_INPUT_BUTTONS (1<<16) // 65536 +#define PREFTOGGLE_2_BIG_STRIP_MENU (1<<17) // 131072 +#define PREFTOGGLE_2_ENABLE_TGUI_SAY_LIGHT_MODE (1<<18) // 262144 + +#define TOGGLES_2_TOTAL 524287// If you add or remove a preference toggle above, make sure you update this define with the total value of the toggles combined. #define TOGGLES_2_DEFAULT (PREFTOGGLE_2_FANCYUI|PREFTOGGLE_2_ITEMATTACK|PREFTOGGLE_2_WINDOWFLASHING|PREFTOGGLE_2_RUNECHAT|PREFTOGGLE_2_DEATHMESSAGE|PREFTOGGLE_2_SEE_ITEM_OUTLINES|PREFTOGGLE_2_PARALLAX_MULTIZ|PREFTOGGLE_2_SWAP_INPUT_BUTTONS|PREFTOGGLE_2_LARGE_INPUT_BUTTONS) diff --git a/code/__DEFINES/speech_channels.dm b/code/__DEFINES/speech_channels.dm new file mode 100644 index 00000000000..19ef7483f7e --- /dev/null +++ b/code/__DEFINES/speech_channels.dm @@ -0,0 +1,10 @@ +// Used to direct channels to speak into. +#define SAY_CHANNEL "Say" +#define RADIO_CHANNEL "Radio" +#define WHISPER_CHANNEL "Whisper" +#define ME_CHANNEL "Me" +#define OOC_CHANNEL "OOC" +#define LOOC_CHANNEL "LOOC" +#define MENTOR_CHANNEL "Mentor" +#define ADMIN_CHANNEL "Admin" +#define DSAY_CHANNEL "Dsay" diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm index 5bb0c78d327..06d534414c8 100644 --- a/code/__HELPERS/mobs.dm +++ b/code/__HELPERS/mobs.dm @@ -534,8 +534,6 @@ return locate(/mob) in A -// Suppress the mouse macros -/client/var/next_mouse_macro_warning /mob/proc/LogMouseMacro(verbused, params) if(!client) return diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm index c56067af859..2369f9c8f64 100644 --- a/code/__HELPERS/text.dm +++ b/code/__HELPERS/text.dm @@ -127,8 +127,6 @@ // Uses client.typing to check if the popup should appear or not /proc/typing_input(mob/user, message = "", title = "", default = "") - if(user.client.checkTyping()) // Prevent double windows - return null var/client/C = user.client // Save it in a var in case the client disconnects from the mob C.typing = TRUE var/msg = input(user, message, title, default) as text|null @@ -313,6 +311,10 @@ /proc/trim(text) return trim_reduced(text) +/// Returns a string that does not exceed max_length characters in size +/proc/trim_length(text, max_length) + return copytext_char(text, 1, max_length) + //Returns a string with the first element of the string capitalized. /proc/capitalize(var/t as text) return uppertext(copytext_char(t, 1, 2)) + copytext_char(t, 2) diff --git a/code/_globalvars/lists/keybindings.dm b/code/_globalvars/lists/keybindings.dm index 759a891c716..685f79518c6 100644 --- a/code/_globalvars/lists/keybindings.dm +++ b/code/_globalvars/lists/keybindings.dm @@ -5,6 +5,7 @@ GLOBAL_LIST_EMPTY(default_hotkeys) GLOBAL_LIST_INIT(keybindings_groups, list( "Movement" = KB_CATEGORY_MOVEMENT, + "Communication" = KB_CATEGORY_COMMUNICATION, "Living" = KB_CATEGORY_LIVING, "General" = KB_CATEGORY_MOB, "General Emote" = KB_CATEGORY_EMOTE_GENERIC, diff --git a/code/datums/emote/emote.dm b/code/datums/emote/emote.dm index 884cf24b934..e8ee68d5a50 100644 --- a/code/datums/emote/emote.dm +++ b/code/datums/emote/emote.dm @@ -483,7 +483,7 @@ if(intentional && only_unintentional) return FALSE - if(user.client?.prefs.muted & MUTE_EMOTE) + if(user.client && check_mute(user.client.ckey, MUTE_EMOTE)) to_chat(user, span_warning("You cannot send emotes (muted).")) return FALSE @@ -512,7 +512,7 @@ return FALSE else // deadchat handling - if(user.client?.prefs.muted & MUTE_DEADCHAT) + if(user.client && check_mute(user.client.ckey, MUTE_DEADCHAT)) to_chat(user, span_warning("You cannot send deadchat emotes (muted).")) return FALSE if(!(user.client?.prefs.toggles & PREFTOGGLE_CHAT_DEAD)) diff --git a/code/datums/keybindings/admin.dm b/code/datums/keybindings/admin.dm index e7f857ff97b..9da4684864f 100644 --- a/code/datums/keybindings/admin.dm +++ b/code/datums/keybindings/admin.dm @@ -27,32 +27,6 @@ return TRUE -/datum/keybinding/admin/msay - name = "Msay (for admins)" - keys = list("ShiftF5") - - -/datum/keybinding/admin/msay/down(client/user) - . = ..() - if(.) - return . - user.get_mentor_say() - return TRUE - - -/datum/keybinding/admin/asay - name = "Msay/Asay" - keys = list("F5") - - -/datum/keybinding/admin/asay/down(client/user) - . = ..() - if(.) - return . - user.get_admin_say() - return TRUE - - /datum/keybinding/admin/aghost name = "Aghost" keys = list("F6") @@ -104,17 +78,3 @@ return . user.invisimin() return TRUE - - -/datum/keybinding/admin/dsay - name = "Dsay" - keys = list("F10") - - -/datum/keybinding/admin/dsay/down(client/user) - . = ..() - if(.) - return . - user.get_dead_say() - return TRUE - diff --git a/code/datums/keybindings/client.dm b/code/datums/keybindings/client.dm index f9e26abca60..08fe04e18e0 100644 --- a/code/datums/keybindings/client.dm +++ b/code/datums/keybindings/client.dm @@ -15,58 +15,6 @@ return TRUE -/datum/keybinding/client/ooc - name = "OOC" - keys = list("F2", "O") - - -/datum/keybinding/client/ooc/down(client/user) - . = ..() - if(.) - return . - user.ooc() - return TRUE - - -/datum/keybinding/client/looc - name = "Локальный OOC" - keys = list("L") - - -/datum/keybinding/client/looc/down(client/user) - . = ..() - if(.) - return . - user.looc() - return TRUE - - -/datum/keybinding/client/say - name = "Say" - keys = list("F3", "T") - - -/datum/keybinding/client/say/down(client/user) - . = ..() - if(.) - return . - user.mob.say_wrapper() - return TRUE - - -/datum/keybinding/client/me - name = "Me" - keys = list("F4", "M") - - -/datum/keybinding/client/me/down(client/user) - . = ..() - if(.) - return . - user.mob.me_wrapper() - return TRUE - - /datum/keybinding/client/t_fullscreen name = "Переключить Fullscreen" keys = list("F11") diff --git a/code/datums/keybindings/communication_keybinds.dm b/code/datums/keybindings/communication_keybinds.dm new file mode 100644 index 00000000000..af706065c42 --- /dev/null +++ b/code/datums/keybindings/communication_keybinds.dm @@ -0,0 +1,74 @@ +/datum/keybinding/client/communication + category = KB_CATEGORY_COMMUNICATION + /// Used to store special rights if required by a keybind, such as R_ADMIN + var/required_rights + /// Used to map muted categories to channels + var/mute_category = MUTE_OOC + +/datum/keybinding/client/communication/down(client/C) + . = ..() + if(required_rights && !check_rights(required_rights, FALSE, C.mob)) + return + + if(mute_category && check_mute(C.ckey, mute_category)) + to_chat(C, "You cannot use [name] (muted).", MESSAGE_TYPE_WARNING) + return + + winset(C, null, "command=[C.tgui_say_create_open_command(name)]") + +/datum/keybinding/client/communication/ooc + name = OOC_CHANNEL + keys = list("O") + +/datum/keybinding/client/communication/ooc/down(client/C) + if(check_rights(R_ADMIN, FALSE, C.mob)) // You may pass + return ..() + + if(!CONFIG_GET(flag/ooc_allowed)) + to_chat(C, "OOC is globally muted.", MESSAGE_TYPE_WARNING) + return + + if(!CONFIG_GET(flag/dooc_allowed)) + to_chat(C, "OOC for dead mobs has been turned off.", MESSAGE_TYPE_WARNING) + return + + return ..() + +/datum/keybinding/client/communication/looc + name = LOOC_CHANNEL + keys = list("L") + +/datum/keybinding/client/communication/say + name = SAY_CHANNEL + keys = list("T") + mute_category = MUTE_IC + +/datum/keybinding/client/communication/me + name = ME_CHANNEL + keys = list("M") + mute_category = MUTE_EMOTE + +/datum/keybinding/client/communication/whisper + name = WHISPER_CHANNEL + keys = list("U") + mute_category = MUTE_IC + +/datum/keybinding/client/communication/radio + name = RADIO_CHANNEL + keys = list("Y") + mute_category = MUTE_IC + +/datum/keybinding/client/communication/msay + name = MENTOR_CHANNEL + keys = list("F4") + required_rights = R_MENTOR | R_ADMIN + +/datum/keybinding/client/communication/asay + name = ADMIN_CHANNEL + keys = list("F5") + required_rights = R_ADMIN + +/datum/keybinding/client/communication/dsay + name = DSAY_CHANNEL + keys = list("F10") + required_rights = R_ADMIN diff --git a/code/datums/keybindings/living.dm b/code/datums/keybindings/living.dm index 1bf4b74b7bb..f9cfda22a49 100644 --- a/code/datums/keybindings/living.dm +++ b/code/datums/keybindings/living.dm @@ -34,26 +34,6 @@ return TRUE -/datum/keybinding/living/whisper - name = "Шептать" - keys = list("ShiftT") - - -/datum/keybinding/living/whisper/down(client/user) - . = ..() - if(.) - return . - var/mob/living/living_mob = user.mob - living_mob.set_typing_indicator(TRUE) - living_mob.hud_typing = TRUE - var/message = typing_input(living_mob, "", "Whisper (text)") - living_mob.hud_typing = FALSE - living_mob.set_typing_indicator(FALSE) - if(message) - living_mob.whisper(message) - return TRUE - - /datum/keybinding/living/look_up name = "Взглянуть вверх" keys = list("Northwest") // Home diff --git a/code/game/dna/genes/vg_powers.dm b/code/game/dna/genes/vg_powers.dm index 9a3cfb7b4d8..8c906659065 100644 --- a/code/game/dna/genes/vg_powers.dm +++ b/code/game/dna/genes/vg_powers.dm @@ -234,10 +234,10 @@ for(var/mob/living/target in targets) var/datum/atom_hud/thoughts/hud = GLOB.huds[THOUGHTS_HUD] hud.manage_hud(target, THOUGHTS_HUD_PRECISE) - user.hud_typing = TRUE + // user.hud_typing = TRUE do not know what to do user.thoughts_hud_set(TRUE) var/say = tgui_input_text(user, "What do you wish to say?", "Project Mind") - user.hud_typing = FALSE + // user.hud_typing = FALSE user.typing = FALSE if(!say || usr.stat) hud.manage_hud(target, THOUGHTS_HUD_DISPERSE) @@ -305,10 +305,10 @@ var/mob/living/target = locateUID(href_list["target"]) if(!(target in available_targets)) return - target.hud_typing = TRUE + // target.hud_typing = TRUE target.thoughts_hud_set(TRUE) var/say = tgui_input_text(user, "What do you wish to say?", "Scan Mind") - target.hud_typing = FALSE + // target.hud_typing = FALSE target.typing = FALSE if(!say || target.stat) target.thoughts_hud_set(FALSE) diff --git a/code/game/gamemodes/blob/overmind.dm b/code/game/gamemodes/blob/overmind.dm index 076aa649e13..d7cc6325a31 100644 --- a/code/game/gamemodes/blob/overmind.dm +++ b/code/game/gamemodes/blob/overmind.dm @@ -64,7 +64,7 @@ return if(client) - if(client.prefs.muted & MUTE_IC) + if(check_mute(client.ckey, MUTE_IC)) to_chat(src, "You cannot send IC messages (muted).") return if(client.handle_spam_prevention(message, MUTE_IC)) diff --git a/code/game/gamemodes/miniantags/borer/borer.dm b/code/game/gamemodes/miniantags/borer/borer.dm index 9e57b8b711e..88687bf8e7c 100644 --- a/code/game/gamemodes/miniantags/borer/borer.dm +++ b/code/game/gamemodes/miniantags/borer/borer.dm @@ -6,7 +6,7 @@ /mob/living/captive_brain/say(message) if(client) - if(client.prefs.muted & MUTE_IC) + if(check_mute(client.ckey, MUTE_IC)) to_chat(src, span_warning("Вы не можете говорить в IC (muted).")) return if(client.handle_spam_prevention(message,MUTE_IC)) diff --git a/code/game/gamemodes/miniantags/demons/pulse_demon/pulse_demon.dm b/code/game/gamemodes/miniantags/demons/pulse_demon/pulse_demon.dm index 748f8e31b37..f2b46ca5558 100644 --- a/code/game/gamemodes/miniantags/demons/pulse_demon/pulse_demon.dm +++ b/code/game/gamemodes/miniantags/demons/pulse_demon/pulse_demon.dm @@ -563,7 +563,7 @@ . += pick("!", "@", "#", "$", "%", "^", "&", "*") /mob/living/simple_animal/demon/pulse_demon/say(message, verb, sanitize = TRUE, ignore_speech_problems = FALSE, ignore_atmospherics = FALSE, ignore_languages = FALSE) - if(client && (client.prefs.muted & MUTE_IC)) + if(check_mute(ckey, MUTE_IC)) to_chat(src, span_danger("You cannot speak in IC (Muted).")) return FALSE diff --git a/code/game/objects/items/devices/megaphone.dm b/code/game/objects/items/devices/megaphone.dm index 575068e4436..4befc88f872 100644 --- a/code/game/objects/items/devices/megaphone.dm +++ b/code/game/objects/items/devices/megaphone.dm @@ -14,7 +14,7 @@ var/list/insultmsg = list("ИДИТЕ НАХУЙ!", "Я АГЕНТ СИНДИКАТА!", "СБ, ЗАСТРЕЛИТЕ МЕНЯ НЕМЕДЛЕННО!", "У МЕНЯ БОМБА!", "КАПИТАН ГАНДОН!", "ЗА СИНДИКАТ!") /obj/item/megaphone/attack_self(mob/living/user as mob) - if(user.client && (user.client.prefs.muted & MUTE_IC)) + if(check_mute(user.ckey, MUTE_IC)) to_chat(src, "You cannot speak in IC (muted).") return if(!ishuman(user)) diff --git a/code/game/verbs/ooc.dm b/code/game/verbs/ooc.dm index 100f0f872b8..85fd0a5e5ed 100644 --- a/code/game/verbs/ooc.dm +++ b/code/game/verbs/ooc.dm @@ -4,10 +4,6 @@ GLOBAL_VAR_INIT(mentor_ooc_colour, "#00B0EB") GLOBAL_VAR_INIT(moderator_ooc_colour, "#184880") GLOBAL_VAR_INIT(admin_ooc_colour, "#b82e00") -//Checks if the client already has a text input open -/client/proc/checkTyping() - return (prefs.toggles & PREFTOGGLE_TYPING_ONCE && typing) - /client/verb/ooc(msg = "" as text) set name = "OOC" set category = "OOC" @@ -25,7 +21,7 @@ GLOBAL_VAR_INIT(admin_ooc_colour, "#b82e00") if(!CONFIG_GET(flag/dooc_allowed) && (mob.stat == DEAD)) to_chat(usr, span_danger("OOC for dead mobs has been turned off."), MESSAGE_TYPE_WARNING, confidential = TRUE) return - if(prefs.muted & MUTE_OOC) + if(check_mute(ckey, MUTE_OOC)) to_chat(src, span_danger("You cannot use OOC (muted)."), MESSAGE_TYPE_WARNING, confidential = TRUE) return @@ -138,7 +134,7 @@ GLOBAL_VAR_INIT(admin_ooc_colour, "#b82e00") if(!CONFIG_GET(flag/dooc_allowed) && (mob.stat == DEAD)) to_chat(usr, span_danger("LOOC for dead mobs has been turned off."), MESSAGE_TYPE_WARNING, confidential = TRUE) return - if(prefs.muted & MUTE_OOC) + if(check_mute(ckey, MUTE_OOC)) to_chat(src, span_danger("You cannot use LOOC (muted)."), MESSAGE_TYPE_WARNING, confidential = TRUE) return diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 70c3c0ca874..248e410b185 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -138,16 +138,15 @@ GLOBAL_VAR_INIT(nologevent, 0) body += "| Prison | " body += "\ Send back to Lobby | " - var/muted = M.client.prefs.muted body += {"
Mute: - \[IC | - OOC | - PRAY | - ADMINHELP | - DEADCHAT | - TTS | - EMOTE\] - (toggle all) + \[IC | + OOC | + PRAY | + ADMINHELP | + DEADCHAT | + TTS | + EMOTE\] + (toggle all) "} body += {"
Mob Manipulation: Randomize Name | diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 295145931b8..f604ed04c45 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -1030,6 +1030,7 @@ GLOBAL_LIST_INIT(view_runtimes_verbs, list( var/client/C = GLOB.directory[ckey] D.associate(C) + update_active_keybindings() message_admins("[key_name_admin(usr)] re-adminned themselves.") log_admin("[key_name(usr)] re-adminned themselves.") update_active_keybindings() diff --git a/code/modules/admin/mute.dm b/code/modules/admin/mute.dm new file mode 100644 index 00000000000..174b68e72b1 --- /dev/null +++ b/code/modules/admin/mute.dm @@ -0,0 +1,31 @@ +/// Associative list of people who are muted via admin mutes +GLOBAL_LIST_EMPTY(admin_mutes_assoc) + +/proc/check_mute(ckey, muteflag) + if(isnull(GLOB.admin_mutes_assoc[ckey])) + return FALSE + + if(GLOB.admin_mutes_assoc[ckey] & muteflag) + return TRUE + return FALSE + +/proc/toggle_mute(ckey, muteflag) + if(isnull(GLOB.admin_mutes_assoc[ckey])) + GLOB.admin_mutes_assoc[ckey] = 0 + + if(GLOB.admin_mutes_assoc[ckey] & muteflag) + GLOB.admin_mutes_assoc[ckey] &= ~muteflag + else + GLOB.admin_mutes_assoc[ckey] |= muteflag + +/proc/force_add_mute(ckey, muteflag) + if(isnull(GLOB.admin_mutes_assoc[ckey])) + GLOB.admin_mutes_assoc[ckey] = 0 + + GLOB.admin_mutes_assoc[ckey] |= muteflag + +/proc/force_remove_mute(ckey, muteflag) + if(isnull(GLOB.admin_mutes_assoc[ckey])) + GLOB.admin_mutes_assoc[ckey] = 0 + + GLOB.admin_mutes_assoc[ckey] &= ~muteflag diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm index 89cc6e1eda2..c93174998ff 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -6,7 +6,7 @@ GLOBAL_LIST_INIT(adminhelp_ignored_words, list("unknown", "the", "a", "an", "of" set name = "Adminhelp" //handle muting and automuting - if(prefs.muted & MUTE_ADMINHELP) + if(check_mute(ckey, MUTE_ADMINHELP)) to_chat(src, "Error: Admin-PM: You cannot send adminhelps (Muted).", MESSAGE_TYPE_ADMINPM, confidential = TRUE) return diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm index 5efe68878c5..488bceda349 100644 --- a/code/modules/admin/verbs/adminpm.dm +++ b/code/modules/admin/verbs/adminpm.dm @@ -61,7 +61,7 @@ //takes input from cmd_admin_pm_context, cmd_admin_pm_panel or /client/Topic and sends them a PM. //Fetching a message if needed. src is the sender and C is the target client /client/proc/cmd_admin_pm(whom, msg, type = "PM") - if(prefs.muted & MUTE_ADMINHELP) + if(check_mute(ckey, MUTE_ADMINHELP)) to_chat(src, "Error: Private-Message: You are unable to use PM-s (muted).") return @@ -215,7 +215,7 @@ return /client/proc/cmd_admin_discord_pm() - if(prefs.muted & MUTE_ADMINHELP) + if(check_mute(ckey, MUTE_ADMINHELP)) to_chat(src, "Error: Private-Message: You are unable to use PMs (muted).") return diff --git a/code/modules/admin/verbs/deadsay.dm b/code/modules/admin/verbs/deadsay.dm index 516fa47789e..6f3ec00e40b 100644 --- a/code/modules/admin/verbs/deadsay.dm +++ b/code/modules/admin/verbs/deadsay.dm @@ -8,7 +8,7 @@ if(!src.mob) return - if(prefs.muted & MUTE_DEADCHAT) + if(check_mute(ckey, MUTE_DEADCHAT)) to_chat(src, "You cannot send DSAY messages (muted).") return diff --git a/code/modules/admin/verbs/pray.dm b/code/modules/admin/verbs/pray.dm index b59edb7b620..3bf692469b5 100644 --- a/code/modules/admin/verbs/pray.dm +++ b/code/modules/admin/verbs/pray.dm @@ -11,7 +11,7 @@ return if(client) - if(client.prefs.muted & MUTE_PRAY) + if(check_mute(client.ckey, MUTE_PRAY)) to_chat(usr, "You cannot pray (muted).") return if(client.handle_spam_prevention(msg, MUTE_PRAY, OOC_COOLDOWN)) diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm index d1fbba2ffdc..9f346617f9a 100644 --- a/code/modules/admin/verbs/randomverbs.dm +++ b/code/modules/admin/verbs/randomverbs.dm @@ -246,19 +246,19 @@ if(automute) muteunmute = "auto-muted" - M.client.prefs.muted |= mute_type + force_add_mute(M.client.ckey, mute_type) log_admin("SPAM AUTOMUTE: [muteunmute] [key_name(M)] from [mute_string]") message_admins("SPAM AUTOMUTE: [muteunmute] [key_name_admin(M)] from [mute_string].") to_chat(M, "You have been [muteunmute] from [mute_string] by the SPAM AUTOMUTE system. Contact an admin.") SSblackbox.record_feedback("tally", "admin_verb", 1, "Automute") //If you are copy-pasting this, ensure the 4th parameter is unique to the new proc! return - if(M.client.prefs.muted & mute_type) - muteunmute = "unmuted" - M.client.prefs.muted &= ~mute_type - else + toggle_mute(M.client.ckey, mute_type) + + if(check_mute(M.client.ckey, mute_type)) muteunmute = "muted" - M.client.prefs.muted |= mute_type + else + muteunmute = "unmuted" log_and_message_admins("has [muteunmute] [key_name_admin(M)] from [mute_string].") to_chat(M, "You have been [muteunmute] from [mute_string].") diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index d9e44e73059..8afd646c043 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -173,6 +173,12 @@ /// On next move, subtract this dir from the move that would otherwise be done var/next_move_dir_sub + /// When to next alert admins that mouse macro use was attempted + var/next_mouse_macro_warning + + /// Assigned say modal of the client + var/datum/tgui_say/tgui_say + ///used to make a special mouse cursor, this one for mouse up icon var/mouse_up_icon = null ///used to make a special mouse cursor, this one for mouse up icon diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 0f0ddf1d02c..e931a889dee 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -110,7 +110,7 @@ if(!holder && received_discord_pm < world.time - 6000) // Worse they can do is spam discord for 10 minutes to_chat(usr, "You are no longer able to use this, it's been more then 10 minutes since an admin on Discord has responded to you") return - if(prefs.muted & MUTE_ADMINHELP) + if(check_mute(ckey, MUTE_ADMINHELP)) to_chat(usr, "You cannot use this as your client has been muted from sending messages to the admins on Discord") return cmd_admin_discord_pm() @@ -240,6 +240,7 @@ stat_panel.subscribe(src, PROC_REF(on_stat_panel_message)) tgui_panel = new(src, "chat_panel") + tgui_say = new(src, "tgui_say") if(connection != "seeker") //Invalid connection type. return null @@ -340,6 +341,9 @@ ) addtimer(CALLBACK(src, PROC_REF(check_panel_loaded)), 30 SECONDS) + // Initialize tgui say + tgui_say.initialize() + donator_check() check_ip_intel() send_resources() diff --git a/code/modules/client/preference/preferences.dm b/code/modules/client/preference/preferences.dm index ede3b3e16f0..1b81632c927 100644 --- a/code/modules/client/preference/preferences.dm +++ b/code/modules/client/preference/preferences.dm @@ -89,7 +89,6 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts //non-preference stuff var/warns = 0 - var/muted = 0 var/last_ip var/last_id @@ -602,7 +601,8 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts dat += " - Color: [UI_style_color]    
" dat += " - UI Style: [UI_style]
" dat += "Fancy TGUI: [(toggles2 & PREFTOGGLE_2_FANCYUI) ? "Yes" : "No"]
" - dat += "TGUI strip menu size: [toggles2 & PREFTOGGLE_2_BIG_STRIP_MENU ? "Full-size" : "Miniature"]
" + dat += " - TGUI strip menu size: [toggles2 & PREFTOGGLE_2_BIG_STRIP_MENU ? "Full-size" : "Miniature"]
" + dat += " - TGUI Say Theme: [(toggles2 & PREFTOGGLE_2_ENABLE_TGUI_SAY_LIGHT_MODE) ? "Light" : "Dark"]
" dat += " - TGUI Input: [(toggles2 & PREFTOGGLE_2_DISABLE_TGUI_INPUT) ? "No" : "Yes"]
" dat += " - TGUI Input - Large Buttons: [(toggles2 & PREFTOGGLE_2_LARGE_INPUT_BUTTONS) ? "Yes" : "No"]
" dat += " - TGUI Input - Swap Buttons: [(toggles2 & PREFTOGGLE_2_SWAP_INPUT_BUTTONS) ? "Yes" : "No"]
" @@ -2427,6 +2427,10 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts if("vote_popup") toggles2 ^= PREFTOGGLE_2_DISABLE_VOTE_POPUPS + if("tgui_say_light_mode") + toggles2 ^= PREFTOGGLE_2_ENABLE_TGUI_SAY_LIGHT_MODE + user?.client?.tgui_say?.load() + if("ghost_att_anim") toggles2 ^= PREFTOGGLE_2_ITEMATTACK diff --git a/code/modules/client/preference/preferences_toggles.dm b/code/modules/client/preference/preferences_toggles.dm index fb72ce10fe5..302c6c62ef5 100644 --- a/code/modules/client/preference/preferences_toggles.dm +++ b/code/modules/client/preference/preferences_toggles.dm @@ -544,15 +544,21 @@ disable_message = "You will no longer receive popups when vote starts." blackbox_message = "Toggle Vote Popup" -/datum/preference_toggle/toggle_emote_indicator - name = "Toggle Emote Typing Indicator" - description = "Toggles showing an indicator when you are typing an emote." - preftoggle_bitflag = PREFTOGGLE_2_EMOTE_BUBBLE - preftoggle_toggle = PREFTOGGLE_TOGGLE2 - preftoggle_category = PREFTOGGLE_CATEGORY_GENERAL - enable_message = "You will now display a typing indicator for emotes." - disable_message = "You will no longer display a typing indicator for emotes." - blackbox_message = "Toggle Typing Indicator (Emote)" +// /datum/preference_toggle/toggle_emote_indicator +// name = "Toggle Emote Typing Indicator" +// description = "Toggles showing an indicator when you are typing an emote." +// preftoggle_bitflag = PREFTOGGLE_2_EMOTE_BUBBLE +// preftoggle_toggle = PREFTOGGLE_TOGGLE2 +// preftoggle_category = PREFTOGGLE_CATEGORY_GENERAL +// enable_message = "You will now display a typing indicator for emotes." +// disable_message = "You will no longer display a typing indicator for emotes." +// blackbox_message = "Toggle Typing Indicator (Emote)" + +// /datum/preference_toggle/toggle_emote_indicator/set_toggles(client/user) +// . = ..() +// if(user.prefs.toggles & PREFTOGGLE_SHOW_TYPING) +// if(istype(usr)) +// usr.set_typing_emote_indicator(FALSE) /datum/preference_toggle/toggle_tgui_input name = "Toggle TGUI Input" diff --git a/code/modules/detective_work/footprints_and_rag.dm b/code/modules/detective_work/footprints_and_rag.dm index 5586511b9e1..8c13bb61d2a 100644 --- a/code/modules/detective_work/footprints_and_rag.dm +++ b/code/modules/detective_work/footprints_and_rag.dm @@ -1,11 +1,3 @@ - -/mob - var/bloody_hands = 0 - var/list/feet_blood_DNA - var/feet_blood_color - var/blood_state = BLOOD_STATE_NOT_BLOODY - var/list/bloody_feet = list(BLOOD_STATE_HUMAN = 0, BLOOD_STATE_XENO = 0, BLOOD_STATE_NOT_BLOODY = 0) - /obj/item/clothing/gloves var/transfer_blood = 0 diff --git a/code/modules/mob/living/carbon/human/species/_species.dm b/code/modules/mob/living/carbon/human/species/_species.dm index eeffd2db46b..64bc00d18ed 100644 --- a/code/modules/mob/living/carbon/human/species/_species.dm +++ b/code/modules/mob/living/carbon/human/species/_species.dm @@ -1160,8 +1160,10 @@ It'll return null if the organ doesn't correspond, so include null checks when u return TRUE /datum/species/proc/spec_hitby(atom/movable/AM, mob/living/carbon/human/H) + return /datum/species/proc/spec_attacked_by(obj/item/I, mob/living/user, obj/item/organ/external/affecting, intent, mob/living/carbon/human/H) + return /proc/get_random_species(species_name = FALSE) // Returns a random non black-listed or hazardous species, either as a string or datum var/static/list/random_species = list() diff --git a/code/modules/mob/living/living_emote.dm b/code/modules/mob/living/living_emote.dm index edfcc8071bd..766c0d56acf 100644 --- a/code/modules/mob/living/living_emote.dm +++ b/code/modules/mob/living/living_emote.dm @@ -543,7 +543,7 @@ if(QDELETED(user)) return FALSE - else if(user.client?.prefs.muted & MUTE_IC) + else if(user.client && check_mute(user.client.ckey, MUTE_IC)) to_chat(user, span_boldwarning("You cannot send IC messages (muted).")) return FALSE else if(!params) diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index 5d5d748346e..fdf99d5cd0c 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -202,7 +202,7 @@ GLOBAL_LIST_EMPTY(channel_to_radio_key) /mob/living/say(message, verb = "says", sanitize = TRUE, ignore_speech_problems = FALSE, ignore_atmospherics = FALSE, ignore_languages = FALSE) if(client) client.check_say_flood(5) - if(client?.prefs.muted & MUTE_IC) + if(check_mute(client.ckey, MUTE_IC)) to_chat(src, span_danger("You cannot speak in IC (Muted).")) return FALSE @@ -439,7 +439,7 @@ GLOBAL_LIST_EMPTY(channel_to_radio_key) /mob/living/whisper_say(list/message_pieces, verb = "whispers") - if(client?.prefs.muted & MUTE_IC) + if(client && check_mute(client.ckey, MUTE_IC)) to_chat(src, span_danger("You cannot speak in IC (Muted).")) return diff --git a/code/modules/mob/logout.dm b/code/modules/mob/logout.dm index 06b89882c93..44d97c84b3a 100644 --- a/code/modules/mob/logout.dm +++ b/code/modules/mob/logout.dm @@ -1,5 +1,6 @@ /mob/Logout() SEND_SIGNAL(src, COMSIG_MOB_LOGOUT) + set_typing_indicator(FALSE) SStgui.on_logout(src) // Cleanup any TGUIs the user has open unset_machine() GLOB.player_list -= src diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 85a824597ba..cbf7250be22 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1107,6 +1107,8 @@ GLOBAL_LIST_INIT(holy_areas, typecacheof(list( . = stat stat = new_stat SEND_SIGNAL(src, COMSIG_MOB_STATCHANGE, new_stat, .) + if(.) + set_typing_indicator(FALSE) /** * Called when this mob slips over, override as needed diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index 0bcf57f89bd..fba28929d50 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -22,6 +22,25 @@ /// Contains /atom/movable/screen/alert only // On /mob so clientless mobs will throw alerts properly var/list/alerts + var/bloody_hands = 0 + /// Basically a lazy list, copies the DNA of blood you step in + var/list/feet_blood_DNA + /// affects the blood color of your feet, color taken from the blood you step in + var/feet_blood_color + /// Weirdly named, effects how blood transfers onto objects + var/blood_state = BLOOD_STATE_NOT_BLOODY + /// Assoc list for tracking how "bloody" a mobs feet are, used for creating bloody foot/shoeprints on turfs when moving + var/list/bloody_feet = list(BLOOD_STATE_HUMAN = 0, BLOOD_STATE_XENO = 0, BLOOD_STATE_NOT_BLOODY = 0, BLOOD_BASE_ALPHA = BLOODY_FOOTPRINT_BASE_ALPHA) + + /// Affects if you have a typing indicator + var/typing + /// Affects if you have a thinking indicator + var/thinking + /// Last thing we typed in to the typing indicator, probably does not need to exist + var/last_typed + /// Last time we typed something in to the typing popup + var/last_typed_time + var/datum/mind/mind blocks_emissive = EMISSIVE_BLOCK_GENERIC diff --git a/code/modules/mob/mob_say.dm b/code/modules/mob/mob_say.dm index e7376509f0b..34adbed0045 100644 --- a/code/modules/mob/mob_say.dm +++ b/code/modules/mob/mob_say.dm @@ -62,7 +62,7 @@ to_chat(src, span_danger("Deadchat is globally muted.")) return - if(client.prefs.muted & MUTE_DEADCHAT) + if(check_mute(client.ckey, MUTE_DEADCHAT)) to_chat(src, span_warning("You cannot talk in deadchat (muted).")) return diff --git a/code/modules/mob/typing_indicator.dm b/code/modules/mob/typing_indicator.dm index dc9d3c935d2..9b030fe2b1c 100644 --- a/code/modules/mob/typing_indicator.dm +++ b/code/modules/mob/typing_indicator.dm @@ -1,10 +1,5 @@ -#define TYPING_INDICATOR_LIFETIME 30 * 10 //grace period after which typing indicator disappears regardless of text in chatbar - -/mob/var/hud_typing = 0 //set when typing in an input window instead of chatline -/mob/var/typing -/mob/var/last_typed -/mob/var/last_typed_time GLOBAL_LIST_EMPTY(typing_indicator) +GLOBAL_LIST_EMPTY(thinking_indicator) /** * Toggles the floating chat bubble above a players head. @@ -14,104 +9,104 @@ GLOBAL_LIST_EMPTY(typing_indicator) */ /mob/proc/set_typing_indicator(state) if(!GLOB.typing_indicator[bubble_icon]) - GLOB.typing_indicator[bubble_icon] = mutable_appearance('icons/mob/talk.dmi', "[bubble_icon]typing", FLY_LAYER) + GLOB.typing_indicator[bubble_icon] = image('icons/mob/talk.dmi', null, "[bubble_icon]_typing", ABOVE_HUD_LAYER) var/image/I = GLOB.typing_indicator[bubble_icon] I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA - if(ishuman(src)) - if(HAS_TRAIT(src, TRAIT_MUTE)) - cut_overlay(GLOB.typing_indicator[bubble_icon]) - return - - if(client) - if(stat != CONSCIOUS || is_muzzled() || (client.prefs.toggles & PREFTOGGLE_SHOW_TYPING)) - cut_overlay(GLOB.typing_indicator[bubble_icon]) - else - if(state) - if(!typing) - add_overlay(GLOB.typing_indicator[bubble_icon]) - typing = TRUE - else - if(typing) - cut_overlay(GLOB.typing_indicator[bubble_icon]) - typing = FALSE - return state - -/mob/proc/set_typing_emote_indicator(state) - if(!GLOB.typing_indicator[bubble_emote_icon]) - GLOB.typing_indicator[bubble_emote_icon] = mutable_appearance('icons/mob/talk.dmi', "[bubble_emote_icon]typing", FLY_LAYER, src, GAME_PLANE) - var/image/I = GLOB.typing_indicator[bubble_emote_icon] + if(ishuman(src) && HAS_TRAIT(src, TRAIT_MUTE)) + cut_overlay(GLOB.typing_indicator[bubble_icon]) + typing = FALSE + return FALSE + + if(!client) + return FALSE + + if(stat != CONSCIOUS || is_muzzled() || (client.prefs.toggles & PREFTOGGLE_SHOW_TYPING)) + cut_overlay(GLOB.typing_indicator[bubble_icon]) + typing = FALSE + return FALSE + + if(state && !typing) + add_overlay(GLOB.typing_indicator[bubble_icon]) + typing = TRUE + + if(!state && typing) + cut_overlay(GLOB.typing_indicator[bubble_icon]) + typing = FALSE + + return state + +/** + * Toggles the floating thought bubble above a players head. + * + * Arguments: + * * state - Should a thought bubble be shown or hidden + */ +/mob/proc/set_thinking_indicator(state) + if(!GLOB.thinking_indicator[bubble_icon]) + GLOB.thinking_indicator[bubble_icon] = image('icons/mob/talk.dmi', null, "[bubble_icon]_thinking", ABOVE_HUD_LAYER) + var/image/I = GLOB.thinking_indicator[bubble_icon] I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA - if(client) - if(stat != CONSCIOUS || is_muzzled() || (client.prefs.toggles2 & PREFTOGGLE_2_EMOTE_BUBBLE)) - cut_overlay(GLOB.typing_indicator[bubble_emote_icon]) - else - if(state) - if(!typing) - add_overlay(GLOB.typing_indicator[bubble_emote_icon]) - typing = TRUE - else - if(typing) - cut_overlay(GLOB.typing_indicator[bubble_emote_icon]) - typing = FALSE - return state + if(!client && !isliving(src)) + return FALSE + + if(stat != CONSCIOUS || (client.prefs.toggles & PREFTOGGLE_SHOW_TYPING)) + cut_overlay(GLOB.thinking_indicator[bubble_icon]) + thinking = FALSE + return FALSE + + if(!state && thinking) + cut_overlay(GLOB.thinking_indicator[bubble_icon]) + thinking = FALSE + + if(state && !thinking) + add_overlay(GLOB.thinking_indicator[bubble_icon]) + thinking = TRUE + + return state + +// /mob/proc/set_typing_emote_indicator(state) MAYBE TEMPORARY REMOVED +// if(!GLOB.typing_indicator[bubble_emote_icon]) +// GLOB.typing_indicator[bubble_emote_icon] = mutable_appearance('icons/mob/talk.dmi', "[bubble_emote_icon]typing", ABOVE_HUD_LAYER, src, GAME_PLANE) +// var/image/I = GLOB.typing_indicator[bubble_emote_icon] +// I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA + +// if(client) +// if(stat != CONSCIOUS || is_muzzled() || (client.prefs.toggles2 & PREFTOGGLE_2_EMOTE_BUBBLE)) +// cut_overlay(GLOB.typing_indicator[bubble_emote_icon]) +// else +// if(state) +// if(!typing) +// add_overlay(GLOB.typing_indicator[bubble_emote_icon]) +// typing = TRUE +// else +// if(typing) +// cut_overlay(GLOB.typing_indicator[bubble_emote_icon]) +// typing = FALSE +// return state /mob/verb/say_wrapper() set name = ".Say" - set hidden = 1 + set hidden = TRUE set_typing_indicator(TRUE) - hud_typing = 1 + typing = TRUE var/message = typing_input(src, "", "say (text)") - hud_typing = 0 + typing = FALSE set_typing_indicator(FALSE) if(message) say_verb(message) /mob/verb/me_wrapper() set name = ".Me" - set hidden = 1 + set hidden = TRUE - set_typing_emote_indicator(TRUE) - hud_typing = 1 + set_typing_indicator(TRUE, TRUE) + typing = TRUE var/message = typing_input(src, "", "me (text)") - hud_typing = 0 - set_typing_emote_indicator(FALSE) + typing = FALSE + set_typing_indicator(FALSE) if(message) me_verb(message) -/mob/verb/whisper_wrapper() - set name = ".Whisper" - set hidden = 1 - - set_typing_indicator(1) - hud_typing = 1 - var/message = typing_input(src, "", "whisper (text)") - hud_typing = 0 - set_typing_indicator(0) - if(message) - whisper(message) - -/mob/proc/handle_typing_indicator() - if(client) - if(!(client.prefs.toggles & PREFTOGGLE_SHOW_TYPING) && !hud_typing) - var/temp = winget(client, "input", "text") - - if(temp != last_typed) - last_typed = temp - last_typed_time = world.time - - if(world.time > last_typed_time + TYPING_INDICATOR_LIFETIME) - set_typing_indicator(FALSE) - return - if(length(temp) > 5 && findtext(temp, "Say \"", 1, 7)) - set_typing_indicator(TRUE) - else if(length(temp) > 3 && findtext(temp, "Me ", 1, 5)) - set_typing_emote_indicator(TRUE, TRUE) - - else - set_typing_indicator(FALSE) - - -#undef TYPING_INDICATOR_LIFETIME diff --git a/code/modules/tgui_input/say_modal/tgui_say_modal.dm b/code/modules/tgui_input/say_modal/tgui_say_modal.dm new file mode 100644 index 00000000000..3dc8718e80b --- /dev/null +++ b/code/modules/tgui_input/say_modal/tgui_say_modal.dm @@ -0,0 +1,123 @@ +/** + * Creates a JSON encoded message to open TGUI say modals properly. + * + * Arguments: + * channel - The channel to open the modal in. + * Returns: + * string - A JSON encoded message to open the modal. + */ +/client/proc/tgui_say_create_open_command(channel) + var/message = TGUI_CREATE_MESSAGE("open", list( + channel = channel, + )) + return "\".output tgui_say.browser:update [message]\"" + +/** + * The tgui say modal. This initializes an input window which hides until + * the user presses one of the speech hotkeys. Once something is entered, it will + * delegate the speech to the proper channel. + */ +/datum/tgui_say + /// The user who opened the window + var/client/client + /// The modal window + var/datum/tgui_window/window + /// Boolean for whether the tgui_say was opened by the user. + var/window_open + +/** Creates the new input window to exist in the background. */ +/datum/tgui_say/New(client/client, id) + src.client = client + window = new(client, id) + winset(client, "tgui_say", "size=1,1;is-visible=0;") + window.subscribe(src, PROC_REF(on_message)) + window.is_browser = TRUE + +/** + * After a brief period, injects the scripts into + * the window to listen for open commands. + */ +/datum/tgui_say/proc/initialize() + set waitfor = FALSE + // Sleep to defer initialization to after client constructor + sleep(3 SECONDS) + window.initialize( + strict_mode = TRUE, + fancy = TRUE, + inline_css = file("tgui/public/tgui-say.bundle.css"), + inline_js = file("tgui/public/tgui-say.bundle.js"), + ); + +/** + * Ensures nothing funny is going on window load. + * Minimizes the window, sets max length, closes all + * typing and thinking indicators. This is triggered + * as soon as the window sends the "ready" message. + */ +/datum/tgui_say/proc/load() + window_open = FALSE + winset(client, "tgui_say", "pos=848,500;size=275,30;is-visible=0;") + window.send_message("props", list( + "lightMode" = (client.prefs.toggles2 & PREFTOGGLE_2_ENABLE_TGUI_SAY_LIGHT_MODE), + "maxLength" = MAX_MESSAGE_LEN, + )) + stop_thinking() + return TRUE + +/** + * Sets the window as "opened" server side, though it is already + * visible to the user. We do this to set local vars & + * start typing (if enabled and in an IC channel). + * + * Arguments: + * payload - A list containing the channel the window was opened in. + */ +/datum/tgui_say/proc/open(payload) + if(!payload?["channel"]) + CRASH("No channel provided to an open TGUI-Say") + window_open = TRUE + switch(payload["channel"]) + if(ME_CHANNEL, RADIO_CHANNEL, SAY_CHANNEL, WHISPER_CHANNEL) + start_thinking() + return TRUE + +/** + * Closes the window serverside. Closes any open chat bubbles + * regardless of preference. + */ +/datum/tgui_say/proc/close() + window_open = FALSE + stop_thinking() + stop_typing() + +/** + * The equivalent of ui_act, this waits on messages from the window + * and delegates actions. + */ +/datum/tgui_say/proc/on_message(type, payload) + switch(type) + if("ready") + load() + return TRUE + if("open") + open(payload) + return TRUE + if("close") + close() + return TRUE + if("thinking") + if(payload["visible"] == TRUE) + start_thinking() + return TRUE + if(payload["visible"] == FALSE) + stop_thinking() + return TRUE + return FALSE + if("typing") + start_typing(payload["isMeChannel"]) + return TRUE + if("entry") + handle_entry(payload) + return TRUE + + return FALSE diff --git a/code/modules/tgui_input/say_modal/tgui_say_speech.dm b/code/modules/tgui_input/say_modal/tgui_say_speech.dm new file mode 100644 index 00000000000..8843a80431b --- /dev/null +++ b/code/modules/tgui_input/say_modal/tgui_say_speech.dm @@ -0,0 +1,61 @@ +/** + * Delegates the speech to the proper channel. + * + * Arguments: + * entry - the text to broadcast + * channel - the channel to broadcast in + * Returns: + * boolean - on success or failure + */ +/datum/tgui_say/proc/delegate_speech(entry, channel) + switch(channel) + if(SAY_CHANNEL) + client.mob.say_verb(entry) + return TRUE + if(RADIO_CHANNEL) + client.mob.say_verb((isliving(client.mob) ? ";" : "") + entry) + return TRUE + if(WHISPER_CHANNEL) + client.mob.whisper(entry) + return TRUE + if(ME_CHANNEL) + client.mob.me_verb(entry) + return TRUE + if(OOC_CHANNEL) + client.ooc(entry) + return TRUE + if(LOOC_CHANNEL) + client.looc(entry) + return TRUE + if(ADMIN_CHANNEL) + client.cmd_admin_say(entry) + return TRUE + if(MENTOR_CHANNEL) + client.cmd_mentor_say(entry) + return TRUE + if(DSAY_CHANNEL) + client.dsay(entry) + return TRUE + return FALSE + +/** + * Handles text entry and forced speech. + * + * Arguments: + * payload - a string list containing entry & channel + * Returns: + * boolean - success or failure + */ +/datum/tgui_say/proc/handle_entry(payload) + if(!payload?["channel"] || !payload["entry"]) + var/hacker_man_ckey = usr.client.ckey + qdel(usr.client) + message_admins("[hacker_man_ckey] was kicked for attemping to send a null message to TGUI-say.") + CRASH("[hacker_man_ckey] entered in a null payload to the chat window.") + if(length_char(payload["entry"]) > MAX_MESSAGE_LEN) + var/hacker_man_ckey = usr.client.ckey + qdel(usr.client) + message_admins("[hacker_man_ckey] was kicked for attemping to bypass TGUI-say character limits.") + CRASH("[hacker_man_ckey] has entered more characters than allowed into a TGUI-Say.") + delegate_speech(payload["entry"], payload["channel"]) + return TRUE diff --git a/code/modules/tgui_input/say_modal/tgui_say_typing.dm b/code/modules/tgui_input/say_modal/tgui_say_typing.dm new file mode 100644 index 00000000000..9c137ec5ad9 --- /dev/null +++ b/code/modules/tgui_input/say_modal/tgui_say_typing.dm @@ -0,0 +1,41 @@ +/** Sets the mob as "thinking" - with indicator */ +/datum/tgui_say/proc/start_thinking() + if(!client?.mob || !window_open) + return FALSE + /// Special exemptions + if(isabductor(client.mob)) + return FALSE + client.mob.set_thinking_indicator(TRUE) + addtimer(CALLBACK(src, PROC_REF(stop_thinking)), 5 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_STOPPABLE) + +/** Removes typing/thinking indicators and flags the mob as not thinking */ +/datum/tgui_say/proc/stop_thinking() + if(!client?.mob) + return FALSE + client.mob.set_thinking_indicator(FALSE) + +/** + * Handles the user typing. After a brief period of inactivity, + * signals the client mob to revert to the "thinking" icon. + */ +/datum/tgui_say/proc/start_typing(me = FALSE) + if(!client?.mob) + return FALSE + client.mob.set_typing_indicator(FALSE) + client.mob.set_thinking_indicator(FALSE) + if(!window_open) + return FALSE + client.mob.set_typing_indicator(TRUE, me) + addtimer(CALLBACK(src, PROC_REF(stop_typing)), 5 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_STOPPABLE) + +/** + * Callback to remove the typing indicator after a brief period of inactivity. + * If the user was typing IC, the thinking indicator is shown. + */ +/datum/tgui_say/proc/stop_typing() + if(!client?.mob) + return FALSE + client.mob.set_typing_indicator(FALSE) + if(!window_open) + return FALSE + client.mob.set_thinking_indicator(TRUE) diff --git a/icons/mob/talk.dmi b/icons/mob/talk.dmi index 2cb323e2430..35417b389d7 100644 Binary files a/icons/mob/talk.dmi and b/icons/mob/talk.dmi differ diff --git a/interface/skin.dmf b/interface/skin.dmf index b0669dc400f..6228dc1e47a 100644 --- a/interface/skin.dmf +++ b/interface/skin.dmf @@ -389,3 +389,22 @@ window "infowindow" is-default = true highlight-color = #00aa00 on-show = ".winset \"rpane.infob.is-checked=true?rpane.rpanewindow.top=infowindow:rpane.rpanewindow.top=\"" + +window "tgui_say" + elem "tgui_say" + type = MAIN + pos = 848,500 + size = 275x30 + anchor1 = 50,50 + anchor2 = 50,50 + is-visible = false + saved-params = "" + statusbar = false + can-minimize = false + elem "browser" + type = BROWSER + pos = 0,0 + size = 275x30 + anchor1 = 0,0 + anchor2 = 0,0 + saved-params = "" diff --git a/paradise.dme b/paradise.dme index 6102df08245..394f091b790 100644 --- a/paradise.dme +++ b/paradise.dme @@ -123,6 +123,7 @@ #include "code\__DEFINES\sound.dm" #include "code\__DEFINES\space_ninja_defines.dm" #include "code\__DEFINES\span.dm" +#include "code\__DEFINES\speech_channels.dm" #include "code\__DEFINES\speech_controller.dm" #include "code\__DEFINES\stat.dm" #include "code\__DEFINES\stat_tracking.dm" @@ -549,6 +550,7 @@ #include "code\datums\keybindings\admin.dm" #include "code\datums\keybindings\carbon.dm" #include "code\datums\keybindings\client.dm" +#include "code\datums\keybindings\communication_keybinds.dm" #include "code\datums\keybindings\emote.dm" #include "code\datums\keybindings\human.dm" #include "code\datums\keybindings\living.dm" @@ -1483,6 +1485,7 @@ #include "code\modules\admin\ipintel.dm" #include "code\modules\admin\IsBanned.dm" #include "code\modules\admin\machine_upgrade.dm" +#include "code\modules\admin\mute.dm" #include "code\modules\admin\NewBan.dm" #include "code\modules\admin\outfits.dm" #include "code\modules\admin\player_panel.dm" @@ -3114,6 +3117,9 @@ #include "code\modules\tgui\tgui_panel\tgui_panel_external.dm" #include "code\modules\tgui\tgui_panel\tgui_panel_message.dm" #include "code\modules\tgui\tgui_panel\to_chat.dm" +#include "code\modules\tgui_input\say_modal\tgui_say_modal.dm" +#include "code\modules\tgui_input\say_modal\tgui_say_speech.dm" +#include "code\modules\tgui_input\say_modal\tgui_say_typing.dm" #include "code\modules\tooltip\tooltip.dm" #include "code\modules\unit_tests\_unit_tests.dm" #include "code\modules\vehicle\ambulance.dm" diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss index 689a6072367..820b57fb212 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss @@ -1097,6 +1097,10 @@ span.body .coderesponses { /* SS1984 UNIC */ +.spyradio { + color: #776f96; +} + .sovradio { color: #f7941d; } diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss index e169843958e..d18f8c94b3f 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss @@ -1153,7 +1153,6 @@ span.body .coderesponses { font-weight: bold; font-style: italic; } - .blobradioactive_gel { color: #2476f0; font-weight: bold; diff --git a/tgui/packages/tgui-say/ChannelIterator.test.ts b/tgui/packages/tgui-say/ChannelIterator.test.ts new file mode 100644 index 00000000000..49c96d3e403 --- /dev/null +++ b/tgui/packages/tgui-say/ChannelIterator.test.ts @@ -0,0 +1,49 @@ +import { ChannelIterator } from './ChannelIterator'; + +describe('ChannelIterator', () => { + let channelIterator: ChannelIterator; + + beforeEach(() => { + channelIterator = new ChannelIterator(); + }); + + it('should cycle through channels properly', () => { + expect(channelIterator.current()).toBe('Say'); + expect(channelIterator.next()).toBe('Radio'); + expect(channelIterator.next()).toBe('Whisper'); + expect(channelIterator.next()).toBe('Me'); + expect(channelIterator.next()).toBe('OOC'); + expect(channelIterator.next()).toBe('LOOC'); + expect(channelIterator.next()).toBe('Say'); // Admin, Mentor, and Dsay are blacklisted so should be skipped + }); + + it('should set a channel properly', () => { + channelIterator.set('OOC'); + expect(channelIterator.current()).toBe('OOC'); + }); + + it('should return true when current channel is "Say"', () => { + channelIterator.set('Say'); + expect(channelIterator.isSay()).toBe(true); + }); + + it('should return false when current channel is not "Say"', () => { + channelIterator.set('Radio'); + expect(channelIterator.isSay()).toBe(false); + }); + + it('should return true when current channel is visible', () => { + channelIterator.set('Say'); + expect(channelIterator.isVisible()).toBe(true); + }); + + it('should return false when current channel is not visible', () => { + channelIterator.set('OOC'); + expect(channelIterator.isVisible()).toBe(false); + }); + + it('should not leak a message from a blacklisted channel', () => { + channelIterator.set('Admin'); + expect(channelIterator.next()).toBe('Admin'); + }); +}); diff --git a/tgui/packages/tgui-say/ChannelIterator.ts b/tgui/packages/tgui-say/ChannelIterator.ts new file mode 100644 index 00000000000..088949ca3a1 --- /dev/null +++ b/tgui/packages/tgui-say/ChannelIterator.ts @@ -0,0 +1,83 @@ +export type Channel = + | 'Say' + | 'Radio' + | 'Whisper' + | 'Me' + | 'OOC' + | 'LOOC' + | 'Mentor' + | 'Admin' + | 'Dsay'; + +/** + * ### ChannelIterator + * Cycles a predefined list of channels, + * skipping over blacklisted ones, + * and providing methods to manage and query the current channel. + */ +export class ChannelIterator { + private index: number = 0; + private readonly channels: Channel[] = [ + 'Say', + 'Radio', + 'Whisper', + 'Me', + 'OOC', + 'LOOC', + 'Mentor', + 'Admin', + 'Dsay', + ]; + private readonly blacklist: Channel[] = ['Mentor', 'Admin', 'Dsay']; + private readonly quiet: Channel[] = [ + 'OOC', + 'LOOC', + 'Mentor', + 'Admin', + 'Dsay', + ]; + + public next(): Channel { + if (this.blacklist.includes(this.channels[this.index])) { + return this.channels[this.index]; + } + + for (let index = 1; index <= this.channels.length; index++) { + let nextIndex = (this.index + index) % this.channels.length; + if (!this.blacklist.includes(this.channels[nextIndex])) { + this.index = nextIndex; + break; + } + } + + return this.channels[this.index]; + } + + public isCurrentChannelBlacklisted(): boolean { + return this.blacklist.includes(this.channels[this.index]); + } + + public set(channel: Channel): void { + this.index = this.channels.indexOf(channel) || 0; + } + + public current(): Channel { + return this.channels[this.index]; + } + + public isMe(): boolean { + return this.channels[this.index] === 'Me'; + } + + public isSay(): boolean { + return this.channels[this.index] === 'Say'; + } + + public isVisible(): boolean { + return !this.quiet.includes(this.channels[this.index]); + } + + public reset(): void { + this.index = 0; + } +} diff --git a/tgui/packages/tgui-say/ChatHistory.test.ts b/tgui/packages/tgui-say/ChatHistory.test.ts new file mode 100644 index 00000000000..c6d8c1c2e27 --- /dev/null +++ b/tgui/packages/tgui-say/ChatHistory.test.ts @@ -0,0 +1,50 @@ +import { ChatHistory } from './ChatHistory'; + +describe('ChatHistory', () => { + let chatHistory: ChatHistory; + + beforeEach(() => { + chatHistory = new ChatHistory(); + }); + + it('should add a message to the history', () => { + chatHistory.add('Hello'); + expect(chatHistory.getOlderMessage()).toEqual('Hello'); + }); + + it('should retrieve older and newer messages', () => { + chatHistory.add('Hello'); + chatHistory.add('World'); + expect(chatHistory.getOlderMessage()).toEqual('World'); + expect(chatHistory.getOlderMessage()).toEqual('Hello'); + expect(chatHistory.getNewerMessage()).toEqual('World'); + expect(chatHistory.getNewerMessage()).toBeNull(); + expect(chatHistory.getOlderMessage()).toEqual('World'); + }); + + it('should limit the history to 5 messages', () => { + for (let i = 1; i <= 6; i++) { + chatHistory.add(`Message ${i}`); + } + + expect(chatHistory.getOlderMessage()).toEqual('Message 6'); + for (let i = 5; i >= 2; i--) { + expect(chatHistory.getOlderMessage()).toEqual(`Message ${i}`); + } + expect(chatHistory.getOlderMessage()).toBeNull(); + }); + + it('should handle temp message correctly', () => { + chatHistory.saveTemp('Temp message'); + expect(chatHistory.getTemp()).toEqual('Temp message'); + expect(chatHistory.getTemp()).toBeNull(); + }); + + it('should reset correctly', () => { + chatHistory.add('Hello'); + chatHistory.getOlderMessage(); + chatHistory.reset(); + expect(chatHistory.isAtLatest()).toBe(true); + expect(chatHistory.getOlderMessage()).toEqual('Hello'); + }); +}); diff --git a/tgui/packages/tgui-say/ChatHistory.ts b/tgui/packages/tgui-say/ChatHistory.ts new file mode 100644 index 00000000000..b5490b1887f --- /dev/null +++ b/tgui/packages/tgui-say/ChatHistory.ts @@ -0,0 +1,59 @@ +/** + * ### ChatHistory + * A class to manage a chat history, + * maintaining a maximum of five messages and supporting navigation, + * temporary message storage, and query operations. + */ +export class ChatHistory { + private messages: string[] = []; + private index: number = -1; // Initialize index at -1 + private temp: string | null = null; + + public add(message: string): void { + this.messages.unshift(message); + this.index = -1; // Reset index + if (this.messages.length > 5) { + this.messages.pop(); + } + } + + public getIndex(): number { + return this.index + 1; + } + + public getOlderMessage(): string | null { + if (this.messages.length === 0 || this.index >= this.messages.length - 1) { + return null; + } + this.index++; + return this.messages[this.index]; + } + + public getNewerMessage(): string | null { + if (this.index <= 0) { + this.index = -1; + return null; + } + this.index--; + return this.messages[this.index]; + } + + public isAtLatest(): boolean { + return this.index === -1; + } + + public saveTemp(message: string): void { + this.temp = message; + } + + public getTemp(): string | null { + const temp = this.temp; + this.temp = null; + return temp; + } + + public reset(): void { + this.index = -1; + this.temp = null; + } +} diff --git a/tgui/packages/tgui-say/TguiSay.tsx b/tgui/packages/tgui-say/TguiSay.tsx new file mode 100644 index 00000000000..93cd33db151 --- /dev/null +++ b/tgui/packages/tgui-say/TguiSay.tsx @@ -0,0 +1,390 @@ +import { Channel, ChannelIterator } from './ChannelIterator'; +import { ChatHistory } from './ChatHistory'; +import { Component, createRef, InfernoKeyboardEvent, RefObject } from 'inferno'; +import { LINE_LENGTHS, RADIO_PREFIXES, WINDOW_SIZES } from './constants'; +import { byondMessages } from './timers'; +import { dragStartHandler } from 'tgui/drag'; +import { windowOpen, windowClose, windowSet } from './helpers'; +import { BooleanLike } from 'common/react'; +import { KEY } from 'common/keys'; + +type ByondOpen = { + channel: Channel; +}; + +type ByondProps = { + maxLength: number; + lightMode: BooleanLike; +}; + +type State = { + buttonContent: string | number; + size: WINDOW_SIZES; +}; + +// Picks out radio channel keycodes +const CHANNEL_REGEX = /^[:#.][^\s]\s/; + +const ROWS: Record = { + small: 1, + medium: 2, + large: 3, + width: 1, // not used +} as const; + +export class TguiSay extends Component<{}, State> { + private channelIterator: ChannelIterator; + private chatHistory: ChatHistory; + private currentPrefix: keyof typeof RADIO_PREFIXES | null; + private innerRef: RefObject; + private lightMode: boolean; + private maxLength: number; + private messages: typeof byondMessages; + // eslint-disable-next-line react/state-in-constructor + state: State; + + constructor(props: never) { + super(props); + + this.channelIterator = new ChannelIterator(); + this.chatHistory = new ChatHistory(); + this.currentPrefix = null; + this.innerRef = createRef(); + this.lightMode = false; + this.maxLength = 1024; + this.messages = byondMessages; + this.state = { + buttonContent: '', + size: WINDOW_SIZES.small, + }; + + this.handleArrowKeys = this.handleArrowKeys.bind(this); + this.handleBackspaceDelete = this.handleBackspaceDelete.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleEnter = this.handleEnter.bind(this); + this.handleIncrementChannel = this.handleIncrementChannel.bind(this); + this.handleInput = this.handleInput.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleOpen = this.handleOpen.bind(this); + this.handleProps = this.handleProps.bind(this); + this.reset = this.reset.bind(this); + this.setSize = this.setSize.bind(this); + this.setValue = this.setValue.bind(this); + this.handleButtonClick = this.handleButtonClick.bind(this); + } + + componentDidMount() { + Byond.subscribeTo('props', this.handleProps); + Byond.subscribeTo('open', this.handleOpen); + } + + handleArrowKeys(direction: KEY.Up | KEY.Down) { + const currentValue = this.innerRef.current?.value; + + if (direction === KEY.Up) { + if (this.chatHistory.isAtLatest() && currentValue) { + // Save current message to temp history if at the most recent message + this.chatHistory.saveTemp(currentValue); + } + // Try to get the previous message, fall back to the current value if none + const prevMessage = this.chatHistory.getOlderMessage(); + + if (prevMessage) { + this.setState({ buttonContent: this.chatHistory.getIndex() }); + this.setSize(prevMessage.length); + this.setValue(prevMessage); + } + } else { + const nextMessage = + this.chatHistory.getNewerMessage() || this.chatHistory.getTemp() || ''; + + const buttonContent = this.chatHistory.isAtLatest() + ? this.channelIterator.current() + : this.chatHistory.getIndex(); + + this.setState({ buttonContent }); + this.setSize(nextMessage.length); + this.setValue(nextMessage); + } + } + + handleBackspaceDelete() { + const typed = this.innerRef.current?.value; + + // User is on a chat history message + if (!this.chatHistory.isAtLatest()) { + this.chatHistory.reset(); + this.setState({ + buttonContent: this.currentPrefix ?? this.channelIterator.current(), + }); + // Empty input, resets the channel + } else if ( + !!this.currentPrefix && + this.channelIterator.isSay() && + typed?.length === 0 + ) { + this.currentPrefix = null; + this.setState({ buttonContent: this.channelIterator.current() }); + } else if ( + this.innerRef.current?.selectionStart === 0 && + this.innerRef.current?.selectionEnd === 0 && + !this.channelIterator.isCurrentChannelBlacklisted() + ) { + this.currentPrefix = null; + this.channelIterator.set('Say'); + this.setState({ buttonContent: this.channelIterator.current() }); + } + + this.setSize(typed?.length); + } + + handleClose() { + const current = this.innerRef.current; + + if (current) { + current.blur(); + } + + this.reset(); + this.chatHistory.reset(); + this.channelIterator.reset(); + this.currentPrefix = null; + windowClose(); + } + + handleEnter() { + const prefix = this.currentPrefix ?? ''; + const value = this.innerRef.current?.value; + + if (value?.length && value.length < this.maxLength) { + this.chatHistory.add(value); + Byond.sendMessage('entry', { + channel: this.channelIterator.current(), + entry: this.channelIterator.isSay() ? prefix + value : value, + }); + } + + this.handleClose(); + } + + handleIncrementChannel() { + // Binary talk is a special case, tell byond to show thinking indicators + if ( + this.channelIterator.isSay() && + this.currentPrefix === (':b ' || '.b ' || '#b ') + ) { + this.messages.channelIncrementMsg(true); + } + + this.currentPrefix = null; + + this.channelIterator.next(); + + // If we've looped onto a quiet channel, tell byond to hide thinking indicators + if (!this.channelIterator.isVisible()) { + this.messages.channelIncrementMsg(false); + } + + this.setState({ buttonContent: this.channelIterator.current() }); + } + + // Throw focus back on the text area while executing button function as expecrted + handleButtonClick(selectionStart: number, selectionEnd: number) { + this.handleIncrementChannel(); + const textArea = this.innerRef?.current; + if (textArea) { + textArea.focus(); + textArea.setSelectionRange(selectionStart, selectionEnd); + } + } + + handleInput() { + const typed = this.innerRef.current?.value; + + // If we're typing, send the message + if ( + this.channelIterator.isVisible() && + this.currentPrefix !== (':b ' || '.b ' || '#b ') + ) { + this.messages.typingMsg(this.channelIterator.isMe()); + } + + this.setSize(typed?.length); + + // Early check for standard radio channel key + if (typed && typed.slice(0, 2) === '; ') { + this.channelIterator.set('Radio'); + this.currentPrefix = null; + this.setValue(typed.slice(2)); + this.setState({ buttonContent: this.channelIterator.current() }); + return; + } + + // Is there a value? Is it long enough to be a prefix? + if (!typed || typed.length < 3) { + return; + } + + if (!CHANNEL_REGEX.test(typed)) { + return; + } + + // Is it a valid prefix? + const prefix = typed + .slice(0, 3) + ?.toLowerCase() as keyof typeof RADIO_PREFIXES; + if (!RADIO_PREFIXES[prefix] || prefix === this.currentPrefix) { + return; + } + + // If we're in binary, hide the thinking indicator + if (prefix === (':b ' || '.b ' || '#b ')) { + Byond.sendMessage('thinking', { visible: false }); + } + + this.channelIterator.set('Say'); + this.currentPrefix = prefix; + this.setState({ buttonContent: RADIO_PREFIXES[prefix] }); + this.setValue(typed.slice(3)); + } + + handleKeyDown(event: InfernoKeyboardEvent) { + switch (event.key) { + case KEY.Up: + case KEY.Down: + event.preventDefault(); + this.handleArrowKeys(event.key); + break; + + case KEY.Delete: + case KEY.Backspace: + this.handleBackspaceDelete(); + break; + + case KEY.Enter: + event.preventDefault(); + this.handleEnter(); + break; + + case KEY.Tab: + event.preventDefault(); + this.handleIncrementChannel(); + break; + + case KEY.Escape: + this.handleClose(); + break; + } + } + + handleOpen = (data: ByondOpen) => { + setTimeout(() => { + this.innerRef.current?.focus(); + }, 0); + + const { channel } = data; + // Catches the case where the modal is already open + if (this.channelIterator.isSay()) { + this.channelIterator.set(channel); + } + this.setState({ buttonContent: this.channelIterator.current() }); + + windowOpen(this.channelIterator.current()); + }; + + handleProps = (data: ByondProps) => { + const { maxLength, lightMode } = data; + this.maxLength = maxLength; + this.lightMode = !!lightMode; + }; + + reset() { + this.setValue(''); + this.setSize(); + this.setState({ + buttonContent: this.channelIterator.current(), + }); + } + + setSize(length = 0) { + let newSize: WINDOW_SIZES; + + if (length > LINE_LENGTHS.medium) { + newSize = WINDOW_SIZES.large; + } else if (length <= LINE_LENGTHS.medium && length > LINE_LENGTHS.small) { + newSize = WINDOW_SIZES.medium; + } else { + newSize = WINDOW_SIZES.small; + } + + if (this.state.size !== newSize) { + this.setState({ size: newSize }); + windowSet(newSize); + } + } + + setValue(value: string) { + const textArea = this.innerRef.current; + if (textArea) { + textArea.value = value; + } + } + + render() { + const theme = + (this.lightMode && 'lightMode') || + (this.currentPrefix && RADIO_PREFIXES[this.currentPrefix]) || + this.channelIterator.current(); + + return ( +
+ +
+ +
+ +