diff --git a/code/__HELPERS/chat.dm b/code/__HELPERS/chat.dm index 57824b6286..a45bbe23d5 100644 --- a/code/__HELPERS/chat.dm +++ b/code/__HELPERS/chat.dm @@ -18,7 +18,7 @@ For example if you have the following channels in tgs4 set up and you make the call: -send2chat("I sniff butts", CONFIG_GET(string/where_to_send_sniff_butts)) +send2chat(new /datum/tgs_message_content("I sniff butts"), CONFIG_GET(string/where_to_send_sniff_butts)) and the config option is set like: @@ -40,23 +40,22 @@ In TGS3 it will always be sent to all connected designated game chats. * * message - The message to send. * channel_tag - Required. If "", the message with be sent to all connected (Game-type for TGS3) channels. Otherwise, it will be sent to TGS4 channels with that tag (Delimited by ','s). + * admin_only - Determines if this communication can only be sent to admin only channels. */ -/proc/send2chat(message, channel_tag) +/proc/send2chat(message, channel_tag, admin_only = FALSE) if(channel_tag == null || !world.TgsAvailable()) return var/datum/tgs_version/version = world.TgsVersion() if(channel_tag == "" || version.suite == 3) - world.TgsTargetedChatBroadcast(message, FALSE) + world.TgsTargetedChatBroadcast(message, admin_only) return - var/list/channels_to_use = list() for(var/I in world.TgsChatChannelInfo()) var/datum/tgs_chat_channel/channel = I var/list/applicable_tags = splittext(channel.custom_tag, ",") - if(channel_tag in applicable_tags) + if((!admin_only || channel.is_admin_channel) && (channel_tag in applicable_tags)) channels_to_use += channel - if(channels_to_use.len) world.TgsChatBroadcast(message, channels_to_use) @@ -69,6 +68,9 @@ In TGS3 it will always be sent to all connected designated game chats. /proc/send2adminchat(category, message, embed_links = FALSE) category = replacetext(replacetext(category, "\proper", ""), "\improper", "") message = replacetext(replacetext(message, "\proper", ""), "\improper", "") - // if(!embed_links) - // message = GLOB.has_discord_embeddable_links.Replace(replacetext(message, "`", ""), " ```$1``` ") + if(!embed_links) + message = GLOB.has_discord_embeddable_links.Replace(replacetext(message, "`", ""), " ```$1``` ") world.TgsTargetedChatBroadcast("[category] | [message]", TRUE) + +/// Handles text formatting for item use hints in examine text +#define EXAMINE_HINT(text) ("" + text + "") diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm index 142e3b44ab..325233b7be 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -275,7 +275,7 @@ if(CONFIG_GET(string/chat_reboot_role)) broadcastmessage += "\n\n<@&[CONFIG_GET(string/chat_reboot_role)]>, the server will reboot shortly!" - send2chat(broadcastmessage, CONFIG_GET(string/chat_roundend_notice_tag)) + send2chat(new /datum/tgs_message_content(broadcastmessage), CONFIG_GET(string/chat_roundend_notice_tag)) CHECK_TICK diff --git a/code/_globalvars/regexes.dm b/code/_globalvars/regexes.dm index bd252b68ce..934296fe1b 100644 --- a/code/_globalvars/regexes.dm +++ b/code/_globalvars/regexes.dm @@ -5,3 +5,19 @@ GLOBAL_DATUM_INIT(is_website, /regex, regex("http|www.|\[a-z0-9_-]+.(com|org|net GLOBAL_DATUM_INIT(is_email, /regex, regex("\[a-z0-9_-]+@\[a-z0-9_-]+.\[a-z0-9_-]+", "i")) GLOBAL_DATUM_INIT(is_alphanumeric, /regex, regex("\[a-z0-9]+", "i")) GLOBAL_DATUM_INIT(is_punctuation, /regex, regex("\[.!?]+", "i")) +GLOBAL_DATUM_INIT(is_color, /regex, regex("^#\[0-9a-fA-F]{6}$")) +GLOBAL_DATUM_INIT(is_alpha_color, /regex, regex("^#\[0-9a-fA-F]{8}$")) + +//finds text strings recognized as links on discord. Mainly used to stop embedding. +GLOBAL_DATUM_INIT(has_discord_embeddable_links, /regex, regex("(https?://\[^\\s|<\]{2,})")) + +//All < and > characters +GLOBAL_DATUM_INIT(angular_brackets, /regex, regex(@"[<>]", "g")) + +//All characters between < a > inclusive of the bracket +GLOBAL_DATUM_INIT(html_tags, /regex, regex(@"<.*?>", "g")) + +//All characters forbidden by filenames: ", \, \n, \t, /, ?, %, *, :, |, <, >, .. +GLOBAL_DATUM_INIT(filename_forbidden_chars, /regex, regex(@{""|[\\\n\t/?%*:|<>]|\.\."}, "g")) +GLOBAL_PROTECT(filename_forbidden_chars) +// had to use the OR operator for quotes instead of putting them in the character class because it breaks the syntax highlighting otherwise. diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 2417210f8f..fb889dd0cb 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -163,7 +163,7 @@ SUBSYSTEM_DEF(ticker) for(var/client/C in GLOB.clients) window_flash(C, ignorepref = TRUE) //let them know lobby has opened up. to_chat(world, "Welcome to [station_name()]!") - send2chat("New round starting on [SSmapping.config.map_name]!", CONFIG_GET(string/chat_announce_new_game)) + send2chat(new /datum/tgs_message_content("New round starting on [SSmapping.config.map_name]!"), CONFIG_GET(string/chat_announce_new_game)) current_state = GAME_STATE_PREGAME //SPLURT EDIT - Bring back old panel //Everyone who wants to be an observer is now spawned @@ -348,7 +348,7 @@ SUBSYSTEM_DEF(ticker) var/list/allmins = adm["present"] send2adminchat("Server", "Round [GLOB.round_id ? "#[GLOB.round_id]:" : "of"] [hide_mode ? "secret":"[mode.name]"] has started[allmins.len ? ".":" with no active admins online!"]") if(CONFIG_GET(string/new_round_ping)) - send2chat("<@&[CONFIG_GET(string/new_round_ping)]> | A new round has started on [SSmapping.config.map_name]!", CONFIG_GET(string/chat_announce_new_game)) + send2chat(new /datum/tgs_message_content("<@&[CONFIG_GET(string/new_round_ping)]> | A new round has started on [SSmapping.config.map_name]!"), CONFIG_GET(string/chat_announce_new_game)) setup_done = TRUE for(var/i in GLOB.start_landmarks_list) diff --git a/code/modules/admin/chat_commands.dm b/code/modules/admin/chat_commands.dm index afe7367517..ae19984f15 100644 --- a/code/modules/admin/chat_commands.dm +++ b/code/modules/admin/chat_commands.dm @@ -15,22 +15,8 @@ var/list/allmins = adm["total"] var/status = "Admins: [allmins.len] (Active: [english_list(adm["present"])] AFK: [english_list(adm["afk"])] Stealth: [english_list(adm["stealth"])] Skipped: [english_list(adm["noflags"])]). " status += "Players: [GLOB.clients.len] (Active: [get_active_player_count(0,1,0)]). Mode: [SSticker.mode ? SSticker.mode.name : "Not started"]." - return status + return new /datum/tgs_message_content(status) -/datum/tgs_chat_command/irccheck - name = "check" - help_text = "Gets the playercount, round info, and address of the server" - var/last_irc_check = 0 - -/datum/tgs_chat_command/irccheck/Run(datum/tgs_chat_user/sender, params) - var/rtod = REALTIMEOFDAY - if(rtod - last_irc_check < IRC_STATUS_THROTTLE) - return - last_irc_check = rtod - var/server = CONFIG_GET(string/server) - return "[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.config.map_name]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active for [DisplayTimeText(world.time - SSticker.round_start_time, 1)]" : "Finishing") : "Starting"] -- [server ? server : "" - //CIT CHANGE obfuscates the gamemode for TGS bot commands on discord by removing Mode:[GLOB.master_mode] - //sandstorm change, says how long the round has been on for /datum/tgs_chat_command/ahelp name = "ahelp" help_text = " |list>>" @@ -39,7 +25,7 @@ /datum/tgs_chat_command/ahelp/Run(datum/tgs_chat_user/sender, params) var/list/all_params = splittext(params, " ") if(all_params.len < 2) - return "Insufficient parameters" + return new /datum/tgs_message_content("Insufficient parameters") var/target = all_params[1] all_params.Cut(1, 2) var/id = text2num(target) @@ -48,10 +34,10 @@ if(AH) target = AH.initiator_ckey else - return "Ticket #[id] not found!" + return new /datum/tgs_message_content("Ticket #[id] not found!") var/res = IrcPm(target, all_params.Join(" "), sender.friendly_name) if(res != "Message Successful") - return res + return new /datum/tgs_message_content(res) /datum/tgs_chat_command/namecheck name = "namecheck" @@ -64,7 +50,7 @@ return "Insufficient parameters" log_admin("Chat Name Check: [sender.friendly_name] on [params]") message_admins("Name checking [params] from [sender.friendly_name]") - return keywords_lookup(params, 1) + return new /datum/tgs_message_content(keywords_lookup(params, 1)) /datum/tgs_chat_command/adminwho name = "adminwho" @@ -72,7 +58,7 @@ admin_only = TRUE /datum/tgs_chat_command/adminwho/Run(datum/tgs_chat_user/sender, params) - return ircadminwho() + return new /datum/tgs_message_content(ircadminwho()) GLOBAL_LIST(round_end_notifiees) @@ -83,10 +69,10 @@ GLOBAL_LIST(round_end_notifiees) /datum/tgs_chat_command/notify/Run(datum/tgs_chat_user/sender, params) if(!SSticker.IsRoundInProgress() && SSticker.HasRoundStarted()) - return "[sender.mention], the round has already ended!" + return new /datum/tgs_message_content("[sender.mention], the round has already ended!") LAZYINITLIST(GLOB.round_end_notifiees) GLOB.round_end_notifiees[sender.mention] = TRUE - return "I will notify [sender.mention] when the round ends." + return new /datum/tgs_message_content("I will notify [sender.mention] when the round ends.") /datum/tgs_chat_command/sdql name = "sdql" @@ -95,12 +81,12 @@ GLOBAL_LIST(round_end_notifiees) /datum/tgs_chat_command/sdql/Run(datum/tgs_chat_user/sender, params) if(GLOB.AdminProcCaller) - return "Unable to run query, another admin proc call is in progress. Try again later." + return new /datum/tgs_message_content("Unable to run query, another admin proc call is in progress. Try again later.") GLOB.AdminProcCaller = "CHAT_[sender.friendly_name]" //_ won't show up in ckeys so it'll never match with a real admin var/list/results = world.SDQL2_query(params, GLOB.AdminProcCaller, GLOB.AdminProcCaller) GLOB.AdminProcCaller = null if(!results) - return "Query produced no output" + return new /datum/tgs_message_content("Query produced no output") var/list/text_res = results.Copy(1, 3) var/list/refs = results.len > 3 ? results.Copy(4) : null if(refs) @@ -112,7 +98,7 @@ GLOBAL_LIST(round_end_notifiees) else L += "[ref]" refs = L - . = "[text_res.Join("\n")][refs ? "\nRefs: [refs.Join(" ")]" : ""]" + . = new /datum/tgs_message_content("[text_res.Join("\n")][refs ? "\nRefs: [refs.Join(" ")]" : ""]") /datum/tgs_chat_command/reload_admins name = "reload_admins" @@ -122,7 +108,7 @@ GLOBAL_LIST(round_end_notifiees) /datum/tgs_chat_command/reload_admins/Run(datum/tgs_chat_user/sender, params) ReloadAsync() log_admin("[sender.friendly_name] reloaded admins via chat command.") - return "Admins reloaded." + return new /datum/tgs_message_content("Admins reloaded.") /datum/tgs_chat_command/reload_admins/proc/ReloadAsync() set waitfor = FALSE @@ -135,14 +121,14 @@ GLOBAL_LIST(round_end_notifiees) /datum/tgs_chat_command/addbunkerbypass/Run(datum/tgs_chat_user/sender, params) if(!CONFIG_GET(flag/sql_enabled)) - return "The Database is not enabled!" + return new /datum/tgs_message_content("The Database is not enabled!") GLOB.bunker_passthrough |= ckey(params) GLOB.bunker_passthrough[ckey(params)] = world.realtime SSpersistence.SavePanicBunker() //we can do this every time, it's okay log_admin("[sender.friendly_name] has added [params] to the current round's bunker bypass list.") message_admins("[sender.friendly_name] has added [params] to the current round's bunker bypass list.") - return "[params] has been added to the current round's bunker bypass list." + return new /datum/tgs_message_content("[params] has been added to the current round's bunker bypass list.") // More (silly) chat commands citadel added. /datum/tgs_chat_command/wheelofsalt @@ -159,21 +145,21 @@ GLOBAL_LIST(round_end_notifiees) saltresult += " `\[REDACTED\]` gets some salt this time too" else saltresult += "[saltprimarysubject] [saltsecondarysubject]" - return "[saltresult]!" + return new /datum/tgs_message_content("[saltresult]!") /datum/tgs_chat_command/valentine name = "valentine" help_text = "Get a random flirt line." /datum/tgs_chat_command/valentine/Run(datum/tgs_chat_user/sender, params) - return "[pick(GLOB.flirts)]" + return new /datum/tgs_message_content("[pick(GLOB.flirts)]") /datum/tgs_chat_command/despacito name = "despacito" //someone please high effort this sometime and make it a full on ytdl search help_text = "This is so sad." /datum/tgs_chat_command/despacito/Run() - return "https://www.youtube.com/watch?v=kJQP7kiw5Fk" + return new /datum/tgs_message_content("https://www.youtube.com/watch?v=kJQP7kiw5Fk") /datum/tgs_chat_command/polly name = "polly" @@ -187,7 +173,7 @@ GLOBAL_LIST(round_end_notifiees) else var/json_file = file("data/npc_saves/Poly.json") if(!fexists(json_file)) - return "**BAWWWWWK!** LEAVE THE HEADSET! ***BAWKKKKK!!***" + return new /datum/tgs_message_content("**BAWWWWWK!** LEAVE THE HEADSET! ***BAWKKKKK!!***") var/list/json = json_decode(file2text(json_file)) speech_buffer = json["phrases"] - return "[pick(speech_buffer)]" + return new /datum/tgs_message_content("[pick(speech_buffer)]") diff --git a/code/modules/discord/tgs_commands.dm b/code/modules/discord/tgs_commands.dm new file mode 100644 index 0000000000..49b26eb005 --- /dev/null +++ b/code/modules/discord/tgs_commands.dm @@ -0,0 +1,33 @@ +/datum/tgs_chat_command/tgscheck + name = "check" + help_text = "Gets the playercount, gamemode, and address of the server" + +/datum/tgs_chat_command/tgscheck/Run(datum/tgs_chat_user/sender, params) + var/server = CONFIG_GET(string/server) + return new /datum/tgs_message_content("[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.config.map_name]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] -- [server ? server : "[world.internet_address]:[world.port]"]") + +/datum/tgs_chat_command/gameversion + name = "gameversion" + help_text = "Gets the version details from the show-server-revision verb, basically" + +/datum/tgs_chat_command/gameversion/Run(datum/tgs_chat_user/sender, params) + var/list/msg = list("") + msg += "BYOND Server Version: [world.byond_version].[world.byond_build] (Compiled with: [DM_VERSION].[DM_BUILD])\n" + + if (!GLOB.revdata) + msg += "No revision information found." + else + msg += "Revision [copytext_char(GLOB.revdata.commit, 1, 9)]" + if (GLOB.revdata.date) + msg += " compiled on '[GLOB.revdata.date]'" + + if(GLOB.revdata.originmastercommit) + msg += ", from origin commit: <[CONFIG_GET(string/githuburl)]/commit/[GLOB.revdata.originmastercommit]>" + + if(GLOB.revdata.testmerge.len) + msg += "\n" + for(var/datum/tgs_revision_information/test_merge/PR as anything in GLOB.revdata.testmerge) + msg += "PR #[PR.number] at [copytext_char(PR.head_commit, 1, 9)] [PR.title].\n" + if (PR.url) + msg += "<[PR.url]>\n" + return new /datum/tgs_message_content(msg.Join("")) diff --git a/code/modules/mob/dead/observer/respawn.dm b/code/modules/mob/dead/observer/respawn.dm index e191525dd5..bf7ed9658f 100644 --- a/code/modules/mob/dead/observer/respawn.dm +++ b/code/modules/mob/dead/observer/respawn.dm @@ -40,8 +40,8 @@ message_admins("[key_name_admin(src)] gave [key_name_admin(O)] a full respawn and sent them back to the lobby.") log_admin("[key_name(src)] gave [key_name(O)] a full respawn and sent them back to the lobby.") to_chat(O, "You have been given a full respawn.") - O.do_respawn(FALSE) - O.client.prefs.dnr_triggered = FALSE + if(O.do_respawn(FALSE)) + player.prefs.dnr_triggered = FALSE else if(istype(M, /mob/dead/new_player)) var/mob/dead/new_player/NP = M var/confirm = alert(src, "Remove [NP]'s respawn restrictions?", "Remove Restrictions", "Yes", "No") @@ -160,7 +160,7 @@ */ /mob/dead/observer/proc/do_respawn(penalize) if(!client) - return + return FALSE if(isnull(penalize)) penalize = client.prefs.respawn_restrictions_active client.prefs.respawn_restrictions_active = penalize @@ -172,6 +172,7 @@ to_chat(N, "You have been respawned to the lobby. \ Remember to take heed of rules regarding round knowledge - notably, that ALL past lives are forgotten. \ Any character you join as has NO knowledge of round events unless specified otherwise by an admin.") + return TRUE /** * Actual proc that removes us and puts us back on lobby diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index b0cea6783f..be33694b37 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -826,7 +826,7 @@ /mob/living/carbon/human/proc/piggyback(mob/living/carbon/target) if(can_piggyback(target)) visible_message("[target] starts to climb onto [src]...") - if(do_after(target, 1.5 SECONDS, src, extra_checks = CALLBACK(src, PROC_REF(can_piggyback), target))) + if(do_after(target, 1.5 SECONDS, src, IGNORE_INCAPACITATED, extra_checks = CALLBACK(src, PROC_REF(can_piggyback), target))) if(can_piggyback(target)) if(target.incapacitated(FALSE, TRUE) || incapacitated(FALSE, TRUE)) target.visible_message("[target] can't hang onto [src]!") diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm index 0c24dd36fa..516ffa1085 100644 --- a/code/modules/mob/living/simple_animal/parrot.dm +++ b/code/modules/mob/living/simple_animal/parrot.dm @@ -932,7 +932,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list( /mob/living/simple_animal/parrot/Polly/say(message, bubble_type,var/list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null) . = ..() if(. && !client && prob(1) && prob(1) && CONFIG_GET(string/chat_squawk_tag)) //Only the one true bird may speak across dimensions. - send2chat("A stray squawk is heard... \"[message]\"", CONFIG_GET(string/chat_squawk_tag)) + send2chat(new /datum/tgs_message_content("A stray squawk is heard... \"[message]\""), CONFIG_GET(string/chat_squawk_tag)) /mob/living/simple_animal/parrot/Polly/BiologicalLife(delta_time, times_fired) if(!(. = ..())) diff --git a/modular_sand/code/modules/tgs/chat_commands.dm b/modular_sand/code/modules/tgs/chat_commands.dm index c87de31a3d..6c8611306f 100644 --- a/modular_sand/code/modules/tgs/chat_commands.dm +++ b/modular_sand/code/modules/tgs/chat_commands.dm @@ -3,7 +3,7 @@ help_text = "Lists everyone currently on the server" /datum/tgs_chat_command/who/Run(datum/tgs_chat_user/sender, params) - return tgswho() + return new /datum/tgs_message_content(tgswho()) /datum/tgs_chat_command/awho name = "awho" @@ -11,7 +11,7 @@ admin_only = TRUE /datum/tgs_chat_command/awho/Run(datum/tgs_chat_user/sender, params) - return tgsadminwho() + return new /datum/tgs_message_content(tgsadminwho()) /datum/tgs_chat_command/restart name = "restart" @@ -19,7 +19,7 @@ admin_only = TRUE /datum/tgs_chat_command/restart/Run(datum/tgs_chat_user/sender) - . = "Restarting." + . = new /datum/tgs_message_content("Restarting.") to_chat(world, span_boldwarning("Server restart - Initialized by [sender.friendly_name] on Discord.")) send2adminchat("Server", "[sender.friendly_name] forced a restart.") addtimer(CALLBACK(src, world.TgsEndProcess()), 1 SECONDS) diff --git a/tgstation.dme b/tgstation.dme index 96976371fc..3fd3374b9b 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2190,6 +2190,7 @@ #include "code\modules\detectivework\detective_work.dm" #include "code\modules\detectivework\evidence.dm" #include "code\modules\detectivework\scanner.dm" +#include "code\modules\discord\tgs_commands.dm" #include "code\modules\economy\_economy.dm" #include "code\modules\economy\account.dm" #include "code\modules\economy\paystand.dm" diff --git a/tgui/packages/tgui/interfaces/MobInteraction.tsx b/tgui/packages/tgui/interfaces/MobInteraction.tsx index f37d550dbe..f48a90f1a2 100644 --- a/tgui/packages/tgui/interfaces/MobInteraction.tsx +++ b/tgui/packages/tgui/interfaces/MobInteraction.tsx @@ -1,5 +1,6 @@ import { filter, map, sortBy } from 'common/collections'; import { flow } from 'common/fp'; +import { clamp } from 'common/math'; import { createSearch } from 'common/string'; import { useBackend, useLocalState } from '../backend'; import { BlockQuote, Button, Flex, LabeledList, Icon, Input, Section, Table, Tabs, Stack, ProgressBar, Divider } from '../components'; @@ -91,6 +92,10 @@ export const MobInteraction = (props, context) => { theirLust, theirMaxLust, } = data; + const [ + searchText, + setSearchText, + ] = useLocalState(context, 'searchText', ''); const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0); return ( @@ -98,46 +103,52 @@ export const MobInteraction = (props, context) => { width={430} height={700} resizable> - -
+ +
- - -
- You...
- {selfAttributes.map(attribute => ( -
- {attribute}
-
- ))} -
-
- {!isTargetSelf ? ( - +
+ +
- They...
- {theirAttributes.map(attribute => ( + You...
+ {selfAttributes.map(attribute => (
{attribute}
))}
- ) : (null)} -
- - - - - {(!isTargetSelf && !isNaN(theirLust)) ? ( - - + {!isTargetSelf ? ( + +
+ They...
+ {theirAttributes.map(attribute => ( +
+ {attribute}
+
+ ))} +
+
+ ) : (null)} +
+
+
+ + + - ) : (null)} - + {(!isTargetSelf && !isNaN(theirLust)) ? ( + + + + ) : (null)} + +
-
+
setTabIndex(0)}> Interactions @@ -152,6 +163,19 @@ export const MobInteraction = (props, context) => { Preferences + + + + + + setSearchText(value)} /> + + {tabIndex === 0 && ( ) || tabIndex === 1 && ( @@ -175,63 +199,51 @@ const InteractionsTab = (props, context) => { ] = useLocalState(context, 'searchText', ''); const interactions = sortInteractions(data.interactions, searchText) || []; return ( - - - - - - - - setSearchText(value)} /> - - -
-
- { - interactions.length ? ( - interactions.map((interaction) => ( - - - )))} - - - )) - ) : ( -
- { - searchText ? ( - "No matching results." - ) : ( - "No interactions available." - ) - } -
- ) - } -
+
+ + { + interactions.length ? ( + interactions.map((interaction) => ( + + + )))} + + + )) + ) : ( +
+ { + searchText ? ( + "No matching results." + ) : ( + "No interactions available." + ) + } +
+ ) + } +
+
); }; @@ -250,6 +262,20 @@ export const sortInteractions = (interactions, searchText = '') => { ])(interactions); }; +/** + * Genital sorter! + */ +export const sortGenitals = (genitals, searchText = '') => { + const testSearch = createSearch(searchText, + genital => genital.name); + return flow([ + // Optional search term + searchText && filter(testSearch), + // Slightly expensive, but way better than sorting in BYOND + sortBy(genital => genital.name), + ])(genitals); +}; + // Self explanatory const ModeToIcon = { "Always visible": "eye", @@ -271,65 +297,77 @@ const ModeToIcon = { */ const GenitalTab = (props, context) => { const { act, data } = useBackend(context); - const genitals = data.genitals || []; + const [ + searchText, + setSearchText, + ] = useLocalState(context, 'searchText', ''); + const genitals = sortGenitals(data.genitals, searchText) || []; return ( - genitals.length ? ( - - {genitals.map(genital => ( -
- - - Visibility
- {genital.possible_choices.map(choice => ( +
+ {genitals.length ? ( + + {genitals.map(genital => ( +
+
+ + Visibility
+ {genital.possible_choices.map(choice => ( +
-
- ))} -
- ) : ( -
- You don't seem to have any genitals... - Or any that you could modify. -
- ) +
+ ))} + + ) : ( +
+ { + searchText ? ( + "No matching results." + ) : ( + "You don't seem to have any genitals...\ + Or any that you could modify." + ) + } +
+ )} +
); }; @@ -344,143 +382,145 @@ const CharacterPrefsTab = (props, context) => { extreme_harm, } = data; return ( - - - -
); }; @@ -514,307 +554,309 @@ const ContentPreferencesTab = (props, context) => { cum_onto_pref, } = data; return ( - - -
+
+ + +
+
); };