diff --git a/citadel.dme b/citadel.dme index 6bf3846e787a..ffff9b3f3480 100644 --- a/citadel.dme +++ b/citadel.dme @@ -361,6 +361,7 @@ #include "code\__HELPERS\_global_objects.dm" #include "code\__HELPERS\_lists_tg.dm" #include "code\__HELPERS\_logging.dm" +#include "code\__HELPERS\admin.dm" #include "code\__HELPERS\animations.dm" #include "code\__HELPERS\areas.dm" #include "code\__HELPERS\atom_movables.dm" @@ -693,6 +694,7 @@ #include "code\datums\EPv2.dm" #include "code\datums\ghost_query.dm" #include "code\datums\hierarchy.dm" +#include "code\datums\http.dm" #include "code\datums\is_abstract.dm" #include "code\datums\material_container.dm" #include "code\datums\mind.dm" @@ -2182,7 +2184,6 @@ #include "code\modules\admin\secrets\random_events\trigger_xenomorph_infestation.dm" #include "code\modules\admin\verbs\admin_set_headshot.dm" #include "code\modules\admin\verbs\adminhelp.dm" -#include "code\modules\admin\verbs\adminhelp_vr.dm" #include "code\modules\admin\verbs\adminjump.dm" #include "code\modules\admin\verbs\adminpm.dm" #include "code\modules\admin\verbs\adminsay.dm" @@ -2223,6 +2224,7 @@ #include "code\modules\admin\verbs\server\admin_reboot.dm" #include "code\modules\admin\view_variables\admin_delete.dm" #include "code\modules\admin\view_variables\color_matrix_editor.dm" +#include "code\modules\admin\view_variables\debug_variable_appearance.dm" #include "code\modules\admin\view_variables\debug_variables.dm" #include "code\modules\admin\view_variables\filteriffic.dm" #include "code\modules\admin\view_variables\get_variables.dm" @@ -2668,6 +2670,7 @@ #include "code\modules\detectivework\tools\storage.dm" #include "code\modules\detectivework\tools\swabs.dm" #include "code\modules\detectivework\tools\uvlight.dm" +#include "code\modules\discord\discord_embed.dm" #include "code\modules\donatorreskins\donatoraccessory.dm" #include "code\modules\donatorreskins\donatorarmor.dm" #include "code\modules\donatorreskins\donatorclothing.dm" diff --git a/code/__DEFINES/_cooldowns.dm b/code/__DEFINES/_cooldowns.dm index 2355b1005b8d..23d3774fc0d9 100644 --- a/code/__DEFINES/_cooldowns.dm +++ b/code/__DEFINES/_cooldowns.dm @@ -62,6 +62,14 @@ #define COOLDOWN_TIMELEFT(cd_source, cd_index) (max(0, cd_source.cd_##cd_index - world.time)) +// kevin... +// the cd indexes arent supposed to start with cd_ +#define COOLDOWN_START_A(cd_source, cd_index, cd_time) (cd_source.cd_index = world.time + (cd_time)) + +//Returns true if the cooldown has run its course, false otherwise +#define COOLDOWN_FINISHED_A(cd_source, cd_index) (cd_source.cd_index <= world.time) + + // INDEXES FOR VAR COOLDOWNS - DO NOT USE UPPERCASE, DO NOT USE cooldown_, APPENDS ADDED AUTOMATICALLY // INDEXES FOR TIMER COOLDOWNS - Must be unique! diff --git a/code/__DEFINES/_lists.dm b/code/__DEFINES/_lists.dm index 0496f1c3ef84..f40fae4a5c9a 100644 --- a/code/__DEFINES/_lists.dm +++ b/code/__DEFINES/_lists.dm @@ -94,3 +94,7 @@ #define VARSET_FROM_LIST_IF(L, V, C...) if(L && L[#V] && (C)) V = L[#V] #define VARSET_TO_LIST(L, V) if(L) L[#V] = V #define VARSET_TO_LIST_IF(L, V, C...) if(L && (C)) L[#V] = V + +// Generic listoflist safe add and removal macros: +///If value is a list, wrap it in a list so it can be used with list add/remove operations +#define LIST_VALUE_WRAP_LISTS(value) (islist(value) ? list(value) : value) diff --git a/code/__DEFINES/_protect.dm b/code/__DEFINES/_protect.dm index 00d66d60dc06..b4b9b612f980 100644 --- a/code/__DEFINES/_protect.dm +++ b/code/__DEFINES/_protect.dm @@ -1,6 +1,5 @@ -/** - * Completely occludes a path from view variable interactions. - */ +///Protects a datum from being VV'd or spawned through admin manipulation +#ifndef TESTING #define VV_PROTECT(Path)\ ##Path/can_vv_get(var_name){\ return FALSE;\ @@ -11,9 +10,19 @@ ##Path/CanProcCall(procname){\ return FALSE;\ }\ +##Path/Read(savefile/savefile){\ + del(src);\ +}\ +##Path/Write(savefile/savefile){\ + return;\ +}\ ##Path/can_vv_mark(){\ return FALSE;\ } +#else +#define VV_PROTECT(Path) +#endif +// we del instead of qdel because for security reasons we must ensure the datum does not exist if Read is called. qdel will not enforce this. /** * Makes a path read-only to view variables. diff --git a/code/__DEFINES/admin/admin.dm b/code/__DEFINES/admin/admin.dm index 96d7f09ed2de..3b9f57a97f04 100644 --- a/code/__DEFINES/admin/admin.dm +++ b/code/__DEFINES/admin/admin.dm @@ -84,19 +84,19 @@ #define ADMIN_FULLMONTY(user) ("[key_name_admin(user)] [ADMIN_FULLMONTY_NONAME(user)]") /atom/proc/Admin_Coordinates_Readable(area_name, admin_jump_ref) - var/turf/T = Safe_COORD_Location() - return T ? "[area_name ? "[get_area_name(T, TRUE)] " : " "]([T.x],[T.y],[T.z])[admin_jump_ref ? " [ADMIN_JMP(T)]" : ""]" : "nonexistent location" + var/turf/turf_at_coords = Safe_COORD_Location() + return turf_at_coords ? "[area_name ? "[get_area_name(turf_at_coords, TRUE)] " : " "]([turf_at_coords.x],[turf_at_coords.y],[turf_at_coords.z])[admin_jump_ref ? " [ADMIN_JMP(turf_at_coords)]" : ""]" : "nonexistent location" /atom/proc/Safe_COORD_Location() - var/atom/A = drop_location() - if(!A) - return // Not a valid atom. - var/turf/T = get_step(A, 0) // Resolve where the thing is. - if(!T) // Incase it's inside a valid drop container, inside another container. ie if a mech picked up a closet and has it inside it's internal storage. - var/atom/last_try = A.loc?.drop_location() // One last try, otherwise fuck it. + var/atom/drop_atom = drop_location() + if(!drop_atom) + return //not a valid atom. + var/turf/drop_turf = get_step(drop_atom, 0) //resolve where the thing is. + if(!drop_turf) //incase it's inside a valid drop container, inside another container. ie if a mech picked up a closet and has it inside its internal storage. + var/atom/last_try = drop_atom.loc?.drop_location() //one last try, otherwise fuck it. if(last_try) - T = get_step(last_try, 0) - return T + drop_turf = get_step(last_try, 0) + return drop_turf /turf/Safe_COORD_Location() return src @@ -104,11 +104,11 @@ /** *! AHELP Commands. */ -#define ADMIN_AHELP_REJECT(user) ("([SPAN_TOOLTIP("Reject and close this ticket.","REJT")])") -#define ADMIN_AHELP_IC(user) ("([SPAN_TOOLTIP("Close this ticket and mark it IC.","IC")])") -#define ADMIN_AHELP_CLOSE(user) ("([SPAN_TOOLTIP("Close this ticket.","CLOSE")])") -#define ADMIN_AHELP_RESOLVE(user) ("([SPAN_TOOLTIP("Close this ticket and mark it as Resolved.","RSLVE")])") -#define ADMIN_AHELP_HANDLE(user) ("([SPAN_TOOLTIP("Alert other Administrators that you're handling this ticket.","HANDLE")])") +#define ADMIN_AHELP_REJECT(user) ("([SPAN_TOOLTIP("Reject and close this ticket.","REJT")])") +#define ADMIN_AHELP_IC(user) ("([SPAN_TOOLTIP("Close this ticket and mark it IC.","IC")])") +#define ADMIN_AHELP_CLOSE(user) ("([SPAN_TOOLTIP("Close this ticket.","CLOSE")])") +#define ADMIN_AHELP_RESOLVE(user) ("([SPAN_TOOLTIP("Close this ticket and mark it as Resolved.","RSLVE")])") +#define ADMIN_AHELP_HANDLE(user) ("([SPAN_TOOLTIP("Alert other Administrators that you're handling this ticket.","HANDLE")])") #define ADMIN_AHELP_FULLMONTY(user) ("[ADMIN_AHELP_REJECT(user)] [ADMIN_AHELP_IC(user)] [ADMIN_AHELP_CLOSE(user)] [ADMIN_AHELP_RESOLVE(user)] [ADMIN_AHELP_HANDLE(user)]") #define AHELP_ACTIVE 1 @@ -118,3 +118,8 @@ // LOG BROWSE TYPES #define BROWSE_ROOT_ALL_LOGS 1 #define BROWSE_ROOT_CURRENT_LOGS 2 + +/// for [/proc/check_asay_links], if there are any actionable refs in the asay message, this index in the return list contains the new message text to be printed +#define ASAY_LINK_NEW_MESSAGE_INDEX "!asay_new_message" +/// for [/proc/check_asay_links], if there are any admin pings in the asay message, this index in the return list contains a list of admins to ping +#define ASAY_LINK_PINGED_ADMINS_INDEX "!pinged_admins" diff --git a/code/__DEFINES/chat.dm b/code/__DEFINES/chat.dm index d6129edd0dd7..5c6e99e837d7 100644 --- a/code/__DEFINES/chat.dm +++ b/code/__DEFINES/chat.dm @@ -9,12 +9,12 @@ #define MESSAGE_TYPE_RADIO "radio" #define MESSAGE_TYPE_INFO "info" #define MESSAGE_TYPE_WARNING "warning" -#define MESSAGE_TYPE_HELPFUL "helpful" #define MESSAGE_TYPE_DEADCHAT "deadchat" #define MESSAGE_TYPE_OOC "ooc" #define MESSAGE_TYPE_ADMINPM "adminpm" #define MESSAGE_TYPE_COMBAT "combat" #define MESSAGE_TYPE_ADMINCHAT "adminchat" +#define MESSAGE_TYPE_PRAYER "prayer" #define MESSAGE_TYPE_MODCHAT "modchat" #define MESSAGE_TYPE_EVENTCHAT "eventchat" #define MESSAGE_TYPE_ADMINLOG "adminlog" diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index 79b5c1694483..a168e0c40bb6 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -21,9 +21,12 @@ #define ismutableappearance(D) (istype(D, /mutable_appearance)) +#define isweakref(D) (istype(D, /datum/weakref)) + #define isimage(D) (istype(D, /image)) -#define isweakref(D) (istype(D, /datum/weakref)) +GLOBAL_VAR_INIT(magic_appearance_detecting_image, new /image) // appearances are awful to detect safely, but this seems to be the best way ~ninjanomnom +#define isappearance(thing) (!isimage(thing) && !ispath(thing) && istype(GLOB.magic_appearance_detecting_image, thing)) //Datums diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm index 0ba4477f7b8b..f71058d02173 100644 --- a/code/__DEFINES/vv.dm +++ b/code/__DEFINES/vv.dm @@ -1,6 +1,8 @@ #define VV_NUM "Number" #define VV_TEXT "Text" -#define VV_MESSAGE "Mutiline Text" +#define VV_MESSAGE "Multiline Text" +#define VV_COLOR "Color" +#define VV_COLOR_MATRIX "Color Matrix" #define VV_ICON "Icon" #define VV_ATOM_REFERENCE "Atom Reference" #define VV_DATUM_REFERENCE "Datum Reference" @@ -16,11 +18,13 @@ #define VV_NEW_TYPE "New Custom Typepath" #define VV_NEW_LIST "New List" #define VV_NULL "NULL" +#define VV_INFINITY "Infinity" #define VV_RESTORE_DEFAULT "Restore to Default" #define VV_MARKED_DATUM "Marked Datum" #define VV_BITFIELD "Bitfield" #define VV_TEXT_LOCATE "Custom Reference Locate" #define VV_PROCCALL_RETVAL "Return Value of Proccall" +#define VV_WEAKREF "Weak Reference Datum" #define VV_MSG_MARKED "
Marked Object" #define VV_MSG_EDITED "
Var Edited" @@ -62,8 +66,7 @@ #define VV_HK_TARGET "target" ///name or index of var for 1 variable targetting hrefs. #define VV_HK_VARNAME "targetvar" -/// to view an appearance virtual object -#define VV_HK_VIEW_APPEARANCE "vv_appearance" +// VV_HK_VIEW_APPEARANCE is handled on vv natively now // vv_do_list() keys #define VV_HK_LIST_ADD "listadd" #define VV_HK_LIST_EDIT "listedit" diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm index 02374d8d8083..86f967dd9625 100644 --- a/code/__HELPERS/_logging.dm +++ b/code/__HELPERS/_logging.dm @@ -304,9 +304,7 @@ GLOBAL_LIST_INIT(testing_global_profiler, list("_PROFILE_NAME" = "Global")) rustg_log_close_all() #endif -/** - * Helper procs for building detailed log lines - */ +/* Helper procs for building detailed log lines */ /proc/key_name(whom, include_link = null, include_name = TRUE, highlight_special_characters = TRUE) var/mob/M var/client/C @@ -332,7 +330,7 @@ GLOBAL_LIST_INIT(testing_global_profiler, list("_PROFILE_NAME" = "Global")) C = GLOB.directory[ckey] if(C) M = C.mob - else if(istype(whom, /datum/mind)) + else if(istype(whom,/datum/mind)) var/datum/mind/mind = whom ckey = mind.ckey if(mind.current) @@ -347,7 +345,7 @@ GLOBAL_LIST_INIT(testing_global_profiler, list("_PROFILE_NAME" = "Global")) if(istype(whom, /atom)) var/atom/A = whom swhom = "[A.name]" - else if(istype(whom, /datum)) + else if(isdatum(whom)) swhom = "[whom]" if(!swhom) @@ -363,11 +361,11 @@ GLOBAL_LIST_INIT(testing_global_profiler, list("_PROFILE_NAME" = "Global")) if(key) if(C?.is_under_stealthmin() && !include_name) if(include_link) - . += "" + . += "" . += "Administrator" else if(include_link) - . += "" + . += "" . += key if(!C) . += "\[DC\]" diff --git a/code/__HELPERS/admin.dm b/code/__HELPERS/admin.dm new file mode 100644 index 000000000000..88c2d1ecb0d6 --- /dev/null +++ b/code/__HELPERS/admin.dm @@ -0,0 +1,9 @@ + +/proc/is_admin(mob/user) + return check_rights(R_ADMIN, 0, user) != 0 + +/// Sends a message in the event that someone attempts to elevate their permissions through invoking a certain proc. +/proc/alert_to_permissions_elevation_attempt(mob/user) + var/message = " has tried to elevate permissions!" + message_admins(key_name_admin(user) + message) + log_admin(key_name(user) + message) diff --git a/code/__HELPERS/chat.dm b/code/__HELPERS/chat.dm index 94982ca90fbf..3927135c7e22 100644 --- a/code/__HELPERS/chat.dm +++ b/code/__HELPERS/chat.dm @@ -1,13 +1,17 @@ -/** +/* + * + * Here's how to use the TGS chat system with configs * - * Here's how to use the chat system with configs + * send2adminchat is a simple function that broadcasts to all admin channels that are designated in TGS * - *? send2adminchat is a simple function that broadcasts to admin channels + * send2chat is a bit verbose but can be very specific * - *? send2chat is a bit verbose but can be very specific + * In TGS3 it will always be sent to all connected designated game chats. + * + * In TGS4+ they use the new tagging system * * The second parameter is a string, this string should be read from a config. - * What this does is dictacte which TGS4 channels can be sent to. + * What this does is dictate which TGS channels can be sent to. * * For example if you have the following channels in tgs4 set up * - Channel 1, Tag: asdf @@ -18,7 +22,7 @@ * * 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: * @@ -31,47 +35,49 @@ * WHERE_TO_SEND_SNIFF_BUTTS * * it will be sent to all connected chats. - * - * In TGS3 it will always be sent to all connected designated game chats. -*/ + */ /** - * Sends a message to TGS chat channels. + * Asynchronously sends a message to TGS chat channels. * - * message - The message to send. + * message - The [/datum/tgs_message_content] 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(datum/tgs_message_content/message, channel_tag, admin_only = FALSE) + set waitfor = 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) /** - * Sends a message to TGS admin chat channels. + * Asynchronously sends a message to TGS admin chat channels. * * category - The category of the mssage. * message - The message to send. */ /proc/send2adminchat(category, message, embed_links = FALSE) + set waitfor = 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``` ") - world.TgsTargetedChatBroadcast("[category] | [message]", TRUE) + world.TgsTargetedChatBroadcast(new /datum/tgs_message_content("[category] | [message]"), TRUE) /// Handles text formatting for item use hints in examine text #define EXAMINE_HINT(text) ("" + text + "") diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm index 4e9ca9fe3625..9172f024a486 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -512,10 +512,15 @@ /proc/SecondsToTicks(seconds) return seconds * 10 -/proc/window_flash(client_or_usr) - if (!client_or_usr) +/// Flash the window of a player +/proc/window_flash(client/flashed_client) + if(ismob(flashed_client)) + var/mob/player_mob = flashed_client + if(player_mob.client) + flashed_client = player_mob.client + if(!flashed_client) return - winset(client_or_usr, "mainwindow", "flash=5") + winset(flashed_client, "mainwindow", "flash=5") /** * Used for the multiz camera console stolen from virgo. diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm index 7fcdc575e3a9..ecbe20b0512a 100644 --- a/code/__HELPERS/text.dm +++ b/code/__HELPERS/text.dm @@ -622,3 +622,9 @@ GLOBAL_LIST_INIT(binary, list("0","1")) /proc/sanitize_css_class_name(name) var/static/regex/regex = new(@"[^a-zA-Z0-9]","g") return replacetext(name, regex, "") + +// return input text if it is a text, else returns default +/proc/sanitize_text(text, default="") + if(istext(text)) + return text + return default diff --git a/code/__HELPERS/time.dm b/code/__HELPERS/time.dm index a20c0a9b1a6e..d83a44ea87f8 100644 --- a/code/__HELPERS/time.dm +++ b/code/__HELPERS/time.dm @@ -2,6 +2,9 @@ GLOBAL_VAR_INIT(startup_year, text2num(time2text(world.time, "YYYY"))) GLOBAL_VAR_INIT(startup_month, text2num(time2text(world.time, "MM"))) GLOBAL_VAR_INIT(startup_day, text2num(time2text(world.time, "DD"))) +///displays the current time into the round, with a lot of extra code just there for ensuring it looks okay after an entire day passes +#define ROUND_TIME(...) ( "[world.time - SSticker.round_start_time > MIDNIGHT_ROLLOVER ? "[round((world.time - SSticker.round_start_time)/MIDNIGHT_ROLLOVER)]:[WORLDTIME2TEXT("hh:mm:ss")]" : WORLDTIME2TEXT("hh:mm:ss")]" ) + #define TimeOfGame (get_game_time()) #define TimeOfTick (TICK_USAGE*0.01*world.tick_lag) diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 33a67bc10f00..6dc612128f3e 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -1426,7 +1426,7 @@ var/list/WALLITEMS = list( /proc/admin_chat_message(message = "Debug Message", color = "#FFFFFF", sender) // Adds TGS3 integration to those fancy verbose round event messages. if(message) - send2irc("Event", message) + send2adminchat("Event", message) if (!config_legacy.chat_webhook_url || !message) return spawn(0) diff --git a/code/_globals/lists/misc.dm b/code/_globals/lists/misc.dm index 72e591d35e54..d696337b8222 100644 --- a/code/_globals/lists/misc.dm +++ b/code/_globals/lists/misc.dm @@ -9,3 +9,19 @@ GLOBAL_LIST_EMPTY(wire_color_directory) // This is an associative list with the // Reference list for disposal sort junctions. Filled up by sorting junction's New() GLOBAL_LIST_EMPTY(tagger_locations) + +// A list of all the special byond lists that need to be handled different by vv +GLOBAL_LIST_INIT(vv_special_lists, init_special_list_names()) + +/proc/init_special_list_names() + var/list/output = list() + var/obj/sacrifice = new + for(var/varname in sacrifice.vars) + var/value = sacrifice.vars[varname] + if(!islist(value)) + if(!isdatum(value) && hascall(value, "Cut")) + output += varname + continue + if(isnull(locate(REF(value)))) + output += varname + return output diff --git a/code/_globals/tgui.dm b/code/_globals/tgui.dm index d3a7fdf55aa6..5376f2c31ab0 100644 --- a/code/_globals/tgui.dm +++ b/code/_globals/tgui.dm @@ -1,3 +1,6 @@ //GLOBAL_DATUM(crew_manifest_tgui, /datum/crew_manifest) GLOBAL_DATUM(changelog_tgui, /datum/changelog) //GLOBAL_DATUM(hotkeys_tgui, /datum/hotkeys_help) + +// tgui color matrix editor +GLOBAL_LIST_INIT(color_vars, list("color")) diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index d9882912bae2..147b3a6efed2 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -4,6 +4,8 @@ /datum/config_entry/string/invoke_youtubedl protection = CONFIG_ENTRY_LOCKED | CONFIG_ENTRY_HIDDEN +/datum/config_entry/flag/show_irc_name + /datum/config_entry/number/client_warn_version default = null min_val = 500 @@ -41,3 +43,36 @@ /// Enable or disable the toast notification when the the instance finishes initializing. /datum/config_entry/flag/toast_notification_on_init + +/datum/config_entry/string/channel_announce_new_game + default = null + +/datum/config_entry/string/channel_announce_end_game + default = null + +/datum/config_entry/string/chat_new_game_notifications + default = null + +/datum/config_entry/flag/debug_admin_hrefs + +/datum/config_entry/number/urgent_ahelp_cooldown + default = 300 + +/datum/config_entry/string/urgent_ahelp_message + default = "This ahelp is urgent!" + +/datum/config_entry/string/ahelp_message + default = "" + +/datum/config_entry/string/urgent_ahelp_user_prompt + default = "There are no admins currently on. Do not press the button below if your ahelp is a joke, a request or a question. Use it only for cases of obvious grief." + +/datum/config_entry/string/urgent_adminhelp_webhook_url + +/datum/config_entry/string/regular_adminhelp_webhook_url + +/datum/config_entry/string/adminhelp_webhook_pfp + +/datum/config_entry/string/adminhelp_webhook_name + +/datum/config_entry/string/adminhelp_ahelp_link diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 85c11386e172..ad799e7bd29c 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -81,15 +81,23 @@ SUBSYSTEM_DEF(ticker) /datum/controller/subsystem/ticker/fire() switch(current_state) if(GAME_STATE_INIT) - // We fire after init finishes - on_mc_init_finish() + if(Master.initializations_finished_with_no_players_logged_in) + start_at = world.time + (CONFIG_GET(number/lobby_countdown) * 10) + for(var/client/C in GLOB.clients) + window_flash(C) //let them know lobby has opened up. + SEND_SOUND(world, sound('sound/misc/server-ready.ogg', volume = 100)) + send2chat(new /datum/tgs_message_content("New round starting on [(LEGACY_MAP_DATUM).station_name]!"), CONFIG_GET(string/channel_announce_new_game)) + to_chat(world, SPAN_NOTICE("Welcome to [(LEGACY_MAP_DATUM).station_name]!")) + to_chat(world, SPAN_NOTICE("Please set up your character and select ready. The round will start in [CONFIG_GET(number/lobby_countdown)] seconds.")) + current_state = GAME_STATE_PREGAME + + fire() if(GAME_STATE_PREGAME) process_pregame() if(GAME_STATE_SETTING_UP) setup() - setup_done = TRUE if(GAME_STATE_PLAYING) round_process() @@ -111,29 +119,19 @@ SUBSYSTEM_DEF(ticker) if(blackbox) blackbox.save_all_data_to_sql() - send2irc("Server", "A round of [mode.name] just ended.") + send2chat(new /datum/tgs_message_content("[GLOB.round_id ? "Round [GLOB.round_id]" : "The round has"] just ended."), CONFIG_GET(string/channel_announce_end_game)) + send2adminchat("Server", "Round just ended.") if(CONFIG_GET(string/chat_roundend_notice_tag)) - var/broadcastmessage = "The round has ended." + var/broadcastmessage = "[GLOB.round_id ? "Round [GLOB.round_id]" : "The round has"] just ended." 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)) SSdbcore.SetRoundEnd() SSpersistence.SavePersistence() if(!SSpersistence.world_saved_count && CONFIG_GET(flag/persistence) && !SSpersistence.world_non_canon) SSpersistence.save_the_world() - -/datum/controller/subsystem/ticker/proc/on_mc_init_finish() - send2irc("Server lobby is loaded and open at byond://[config_legacy.serverurl ? config_legacy.serverurl : (config_legacy.server ? config_legacy.server : "[world.address]:[world.port]")]") - to_chat(world, "Welcome to the pregame lobby!") - to_chat(world, "Please set up your character and select ready. The round will start in [CONFIG_GET(number/lobby_countdown)] seconds.") - SEND_SOUND(world, sound('sound/misc/server-ready.ogg', volume = 100)) - current_state = GAME_STATE_PREGAME - if(Master.initializations_finished_with_no_players_logged_in) - start_at = world.time + (CONFIG_GET(number/lobby_countdown) * 10) - fire() - /datum/controller/subsystem/ticker/proc/process_pregame() var/citest = FALSE #ifdef CITESTING @@ -232,9 +230,11 @@ SUBSYSTEM_DEF(ticker) timeLeft = newtime /datum/controller/subsystem/ticker/proc/setup() - to_chat(world, "Starting game...") + to_chat(world, SPAN_BOLDANNOUNCE("Starting game...")) var/init_start = world.timeofday + CHECK_TICK + //Create and announce mode if(master_mode=="secret") src.hide_mode = 1 @@ -262,11 +262,15 @@ SUBSYSTEM_DEF(ticker) to_chat(world, "Serious error in mode setup! Reverting to pregame lobby.") //Uses setup instead of set up due to computational context. return 0 + CHECK_TICK + SSjob.reset_occupations() src.mode.create_antagonists() src.mode.pre_setup() SSjob.DivideOccupations() // Apparently important for new antagonist system to register specific job antags properly. + CHECK_TICK + if(!src.mode.can_start()) to_chat(world, "Unable to start [mode.name]. Not enough players readied, [config_legacy.player_requirements[mode.config_tag]] players needed. Reverting to pregame lobby.") current_state = GAME_STATE_PREGAME @@ -289,7 +293,6 @@ SUBSYSTEM_DEF(ticker) src.mode.announce() setup_economy() - current_state = GAME_STATE_PLAYING create_characters() //Create player characters and transfer them. collect_minds() equip_characters() @@ -301,54 +304,55 @@ SUBSYSTEM_DEF(ticker) cb.InvokeAsync() LAZYCLEARLIST(round_start_events) - for(var/obj/landmark/L in GLOB.landmarks_list) - // type filtered, we cannot risk runtimes - L.OnRoundstart() + for(var/i in GLOB.landmarks_list) + var/obj/landmark/L = i + if(istype(L)) //we can not runtime here. not in this important of a proc. + L.OnRoundstart() + else + stack_trace("[L] [L.type] found in landmarks list, which isn't a landmark!") - log_world("Game start took [(world.timeofday - init_start)/10]s") round_start_time = world.time + + log_world("Game start took [(world.timeofday - init_start)/10]s") INVOKE_ASYNC(SSdbcore, TYPE_PROC_REF(/datum/controller/subsystem/dbcore, SetRoundStart)) - // TODO Dear God Fix This. Fix all of this. Not just this line, this entire proc. This entire file! - spawn(0)//Forking here so we dont have to wait for this to finish - mode.post_setup() - //Cleanup some stuff - to_chat(world, "Enjoy the game!") - var/startupsound = rand(1,4) - switch(startupsound) - if(1) - SEND_SOUND(world, sound('sound/roundStart/start_up_1.ogg')) - if(2) - SEND_SOUND(world, sound('sound/roundStart/start_up_2.ogg')) - if(3) - SEND_SOUND(world, sound('sound/roundStart/start_up_3.ogg')) - if(4) - SEND_SOUND(world, sound('sound/roundStart/start_up_4.ogg'))//the original sound - //Holiday Round-start stuff ~Carn - Holiday_Game_Start() - - //start_events() //handles random events and space dust. - //new random event system is handled from the MC. - - var/admins_number = 0 - for(var/client/C) - if(C.holder) - admins_number++ - if(admins_number == 0) - send2irc("A round has started with no admins online.") - -/* SSsupply.process() //Start the supply shuttle regenerating points -- TLE // handled in scheduler - master_controller.process() //Start master_controller.process() - lighting_controller.process() //Start processing DynamicAreaLighting updates - */ + to_chat(world, SPAN_NOTICE(SPAN_BOLD("Welcome to [(LEGACY_MAP_DATUM).station_name], enjoy your stay!"))) + // fine to leave this not spawn() + switch(rand(1,4)) + if(1) + SEND_SOUND(world, sound('sound/roundStart/start_up_1.ogg')) + if(2) + SEND_SOUND(world, sound('sound/roundStart/start_up_2.ogg')) + if(3) + SEND_SOUND(world, sound('sound/roundStart/start_up_3.ogg')) + if(4) + SEND_SOUND(world, sound('sound/roundStart/start_up_4.ogg'))//the original sound + current_state = GAME_STATE_PLAYING Master.SetRunLevel(RUNLEVEL_GAME) + // this is horrible and should be moved to ssblackbox if(CONFIG_GET(flag/sql_enabled)) ASYNC // THIS REQUIRES THE ASYNC! statistic_cycle() // Polls population totals regularly and stores them in an SQL DB -- TLE + + Holiday_Game_Start() // before post setup + + PostSetup() + return TRUE +/datum/controller/subsystem/ticker/proc/PostSetup() + set waitfor = FALSE + + // old stuff + mode.post_setup() + + var/list/adm = get_admin_counts() + var/list/allmins = adm["present"] + send2adminchat("Server", "Round [GLOB.round_id ? "#[GLOB.round_id]" : ""] has started[allmins.len ? ".":" with no active admins online!"]") + setup_done = TRUE + //These callbacks will fire after roundstart key transfer /datum/controller/subsystem/ticker/proc/OnRoundstart(datum/callback/cb) if(!HasRoundStarted()) diff --git a/code/datums/datumvars.dm b/code/datums/datumvars.dm index c93c2373e7b9..599f86fbfbc5 100644 --- a/code/datums/datumvars.dm +++ b/code/datums/datumvars.dm @@ -4,7 +4,8 @@ /datum/proc/can_vv_get(var_name) return TRUE -/datum/proc/vv_edit_var(var_name, var_value, mass_edit, raw_edit) //called whenever a var is edited +/// Called when a var is edited with the new value to change to +/datum/proc/vv_edit_var(var_name, var_value, mass_edit, raw_edit) if(var_name == NAMEOF(src, vars) || var_name == NAMEOF(src, parent_type)) return FALSE vars[var_name] = var_value @@ -18,16 +19,21 @@ */ /datum/proc/vv_get_var(var_name, resolve) switch(var_name) - if ("vars") + if (NAMEOF(src, vars)) return debug_variable(var_name, list(), 0, src) return debug_variable(var_name, vars[var_name], 0, src) /datum/proc/can_vv_mark() return TRUE -//please call . = ..() first and append to the result, that way parent items are always at the top and child items are further down -//add separaters by doing . += "---" +/** + * Gets all the dropdown options in the vv menu. + * When overriding, make sure to call . = ..() first and append to the result, that way parent items are always at the top and child items are further down. + * Add separators by doing VV_DROPDOWN_OPTION("", "---") + */ /datum/proc/vv_get_dropdown() + SHOULD_CALL_PARENT(TRUE) + . = list() VV_DROPDOWN_OPTION("", "---") VV_DROPDOWN_OPTION(VV_HK_CALLPROC, "Call Proc") @@ -37,12 +43,14 @@ VV_DROPDOWN_OPTION(VV_HK_ADDCOMPONENT, "Add Component/Element") VV_DROPDOWN_OPTION(VV_HK_MODIFY_TRAITS, "Modify Traits") -//This proc is only called if everything topic-wise is verified. The only verifications that should happen here is things like permission checks! -//href_list is a reference, modifying it in these procs WILL change the rest of the proc in topic.dm of admin/view_variables! -//This proc is for "high level" actions like admin heal/set species/etc/etc. The low level debugging things should go in admin/view_variables/topic_basic.dm incase this runtimes. +/** + * This proc is only called if everything topic-wise is verified. The only verifications that should happen here is things like permission checks! + * href_list is a reference, modifying it in these procs WILL change the rest of the proc in topic.dm of admin/view_variables! + * This proc is for "high level" actions like admin heal/set species/etc/etc. The low level debugging things should go in admin/view_variables/topic_basic.dm in case this runtimes. + */ /datum/proc/vv_do_topic(list/href_list) if(!usr || !usr.client || !usr.client.holder || !check_rights(NONE)) - return FALSE //This is VV, not to be called by anything else. + return FALSE //This is VV, not to be called by anything else. if(href_list[VV_HK_MODIFY_TRAITS]) usr.client.holder.modify_traits(src) return TRUE diff --git a/code/datums/http.dm b/code/datums/http.dm new file mode 100644 index 000000000000..49b183fde6c6 --- /dev/null +++ b/code/datums/http.dm @@ -0,0 +1,82 @@ +/datum/http_request + var/id + var/in_progress = FALSE + + var/method + var/body + var/headers + var/url + /// If present response body will be saved to this file. + var/output_file + + var/_raw_response + +/datum/http_request/proc/prepare(method, url, body = "", list/headers, output_file) + if (!length(headers)) + headers = "" + else + headers = json_encode(headers) + + src.method = method + src.url = url + src.body = body + src.headers = headers + src.output_file = output_file + +/datum/http_request/proc/execute_blocking() + _raw_response = rustg_http_request_blocking(method, url, body, headers, build_options()) + +/datum/http_request/proc/begin_async() + if (in_progress) + CRASH("Attempted to re-use a request object.") + + id = rustg_http_request_async(method, url, body, headers, build_options()) + + if (isnull(text2num(id))) + stack_trace("Proc error: [id]") + _raw_response = "Proc error: [id]" + else + in_progress = TRUE + +/datum/http_request/proc/build_options() + if(output_file) + return json_encode(list("output_filename"=output_file,"body_filename"=null)) + return null + +/datum/http_request/proc/is_complete() + if (isnull(id)) + return TRUE + + if (!in_progress) + return TRUE + + var/r = rustg_http_check_request(id) + + if (r == RUSTG_JOB_NO_RESULTS_YET) + return FALSE + else + _raw_response = r + in_progress = FALSE + return TRUE + +/datum/http_request/proc/into_response() + var/datum/http_response/R = new() + + try + var/list/L = json_decode(_raw_response) + R.status_code = L["status_code"] + R.headers = L["headers"] + R.body = L["body"] + catch + R.errored = TRUE + R.error = _raw_response + + return R + +/datum/http_response + var/status_code + var/body + var/list/headers + + var/errored = FALSE + var/error diff --git a/code/game/antagonist/antagonist_panel.dm b/code/game/antagonist/antagonist_panel.dm index 3f397d29c38d..2d676cf2ba44 100644 --- a/code/game/antagonist/antagonist_panel.dm +++ b/code/game/antagonist/antagonist_panel.dm @@ -31,7 +31,7 @@ if(!M.client) dat += " (logged out)" if(M.stat == DEAD) dat += " (DEAD)" dat += "" - dat += "\[PP]\[PM\]\[TP\]" + dat += "\[PP]\[PM\]\[TP\]" else dat += "[player.ckey] Mob not found!" dat += "" diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm index f5b71ab2dc0c..8e27559e1750 100644 --- a/code/game/gamemodes/game_mode.dm +++ b/code/game/gamemodes/game_mode.dm @@ -371,7 +371,7 @@ var/global/list/additional_antag_types = list() feedback_set("escaped_on_shuttle",escaped_on_shuttle) - send2irc("ROUND END", "A round of [src.name] has ended - [surviving_total] survivors, [ghosts] ghosts.") + send2adminchat("ROUND END", "A round of [src.name] has ended - [surviving_total] survivors, [ghosts] ghosts.") return 0 diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 1a13ff024566..0ebf8d567a00 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -27,109 +27,119 @@ var/global/floorIsLava = 0 ///////////////////////////////////////////////////////////////////////////////////////////////Panels -/datum/admins/proc/show_player_panel(var/mob/M in GLOB.mob_list) +/datum/admins/proc/show_player_panel(mob/player in GLOB.mob_list) set category = "Admin" set name = "Show Player Panel" set desc="Edit player (respawn, ban, heal, etc)" - if(!M) - to_chat(usr, "You seem to be selecting a mob that doesn't exist anymore.") + if(!check_rights(R_ADMIN)) return - if (!istype(src,/datum/admins)) - src = usr.client.holder - if (!istype(src,/datum/admins)) - to_chat(usr, "Error: you are not an admin!") + + log_admin("[key_name(usr)] checked the player panel.") + if(!player) + to_chat(usr, SPAN_WARNING("You seem to be selecting a mob that doesn't exist anymore."), confidential = TRUE) return - var/body = "Options for [M.key]" - body += "Options panel for [M]" - if(M.client) - body += " played by [M.client] " - body += "\[[M.client.holder ? M.client.holder.rank : "Player"]\]" + var/body = "Options for [player.key]" + body += "Options panel for [player]" + if(player.client) + body += " played by [player.client] " + body += "\[[player.client.holder ? player.client.holder.rank : "Player"]\]" - if(istype(M, /mob/new_player)) + if(isnewplayer(player)) body += " Hasn't Entered Game " else - body += " \[Heal\] " - - if(M.client) - body += "
First connection: [M.client.player.player_age] days ago" - body += "
BYOND account created: [M.client.persistent.account_join]" - body += "
BYOND account age (days): [M.client.persistent.account_age]" - - body += {" -

\[ - VV - - TP - - PM - - SM - - [admin_jump_link(M, src)]\]
- Mob type: [M.type]
- Inactivity time: [M.client ? "[M.client.inactivity/600] minutes" : "Logged out"]

- Kick | - Ban | - Jobban | - [is_role_banned_ckey(M.ckey, role = BAN_ROLE_OOC)? "OOC Ban" : "OOC Ban"] | - Notes - "} - - if(M.client) - body += "| Prison | " - var/muted = M.client.prefs.muted - body += {"
Mute: - \[IC | - OOC | - PRAY | - ADMINHELP | - DEADCHAT\] - (toggle all) - "} - - body += {"

- Jump to | - Get | - Send To -

- [check_rights(R_ADMIN|R_MOD,0) ? "Traitor panel | " : "" ] - Narrate to | - Subtle message - "} - - if (M.client) - if(!istype(M, /mob/new_player)) + body += " \[Heal\] " + + if(player.client) + body += "
\[First Seen: [player.client.player.player_age]\]\[Byond account registered on: [player.client.persistent.account_join]\]\[Byond account age (days): [player.client.persistent.account_age]\]" + var/full_version = "Unknown" + if(player.client.byond_version) + full_version = "[player.client.byond_version].[player.client.byond_build ? player.client.byond_build : "xxx"]" + body += "
\[Byond version: [full_version]\]
" + + body += "

\[ " + body += "VV - " + if(player.mind) + body += "TP - " + body += "PM - " + body += "SM - " + body += "FLW - " + + body += "Mob type = [player.type]

" + + body += "Kick | " + // old ban sys is mob level + body += "Ban | " + body += "[is_role_banned_ckey(player.ckey, role = BAN_ROLE_OOC)? "OOC Ban" : "OOC Ban"] | " + body += "Jobban | " + // old note sys requires mob + body += "Notes | Messages | Watchlist | " + if(player.client) + body += "| Prison | " + var/muted = player.client.prefs.muted + body += "
Mute: " + body += "\[IC | " + body += "OOC | " + body += "PRAY | " + body += "ADMINHELP | " + body += "DEADCHAT\]" + body += "(toggle all)" + + body += "

" + body += "Jump to | " + body += "Get | " + body += "Send To" + + body += "

" + body += "Traitor panel | " + body += "Narrate to | " + body += "Subtle message " + + if(player.client) + if(!isnewplayer(player)) body += "

" - body += "Transformation:" - body += "
" + body += "Transformation:
" + + if(isobserver(player)) + body += "Ghost | " + else + body += "Make Ghost | " + + if(ishuman(player) && !issmall(player)) + body += "Human | " + else + body += "Make Human | " //Monkey - if(issmall(M)) - body += "Monkeyized | " + if(issmall(player)) + body += "Monkey | " else - body += "Monkeyize | " + body += "Make Monkey | " //Corgi - if(iscorgi(M)) + if(iscorgi(player)) body += "Corgized | " else - body += "Corgize | " + body += "Corgize | " //AI / Cyborg - if(isAI(M)) + if(isAI(player)) body += "Is an AI " - else if(ishuman(M)) - body += {"Make AI | - Make Robot | - Make Alien + else if(ishuman(player)) + body += {"Make AI | + Make Robot | + Make Alien "} //Simple Animals - if(isanimal(M)) - body += "Re-Animalize | " + if(isanimal(player)) + body += "Re-Animalize | " else - body += "Animalize | " + body += "Animalize | " // DNA2 - Admin Hax - if(M.dna && iscarbon(M)) + if(player.dna && iscarbon(player)) body += "

" body += "DNA Blocks:
" var/bname @@ -139,9 +149,9 @@ var/global/floorIsLava = 0 bname = assigned_blocks[block] body += "" @@ -149,46 +159,44 @@ var/global/floorIsLava = 0 body += {"

Rudimentary transformation:
These transformations only create a new mob type and copy stuff over. They do not take into account MMIs and similar mob-specific things. The buttons in 'Transformations' are preferred, when possible.

- Observer | - \[ Xenos: Larva - Drone - Hunter - Sentinel - Queen \] | - \[ Crew: Human - Unathi - Tajaran - Skrell \] | \[ - Nymph - Diona \] | - \[ slime: Baby, - Adult \] - Monkey | - Cyborg | - Cat | - Runtime | - Corgi | - Ian | - Crab | - Coffee | - \[ Construct: Armoured , - Builder , - Wraith \] - Shade + \[ Xenos: Larva + Drone + Hunter + Sentinel + Queen \] | + \[ Crew: Unathi + Tajaran + Skrell \] | \[ + Nymph + Diona \] | + \[ slime: Baby, + Adult \] + Monkey | + Cyborg | + Cat | + Runtime | + Corgi | + Ian | + Crab | + Coffee | + \[ Construct: Armoured , + Builder , + Wraith \] + Shade
"} - body += {"

- Other actions: -
- Forcesay - "} - if (M.client) - body += {" | - Thunderdome 1 | - Thunderdome 2 | - Thunderdome Admin | - Thunderdome Observer | - "} + + body += "

" + body += "Other actions:" + body += "
" + + if(!isnewplayer(player)) + body += "Forcesay | " + body += "Thunderdome 1 | " + body += "Thunderdome 2 | " + body += "Thunderdome Admin | " + body += "Thunderdome Observer | " + // language toggles body += "

Languages:
" var/f = 1 @@ -196,16 +204,15 @@ var/global/floorIsLava = 0 if(!(L.language_flags & LANGUAGE_INNATE)) if(!f) body += " | " else f = 0 - if(L in M.languages) - body += "[L.name]" + if(L in player.languages) + body += "[L.name]" else - body += "[L.name]" + body += "[L.name]" - body += {"
- - "} + body += "
" + body += "" - usr << browse(body, "window=[M.ckey]_playerpanel;size=550x515") + usr << browse(body, "window=adminplayeropts-[REF(player)];size=550x515") feedback_add_details("admin_verb","SPP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -258,7 +265,7 @@ var/global/floorIsLava = 0 upper_bound = min(upper_bound, note_keys.len) for(var/index = lower_bound, index <= upper_bound, index++) var/t = note_keys[index] - dat += "" + dat += "" dat += "
 12345" if(bname) - var/bstate=M.dna.GetSEState(block) + var/bstate=player.dna.GetSEState(block) var/bcolor="[(bstate)?"#006600":"#ff0000"]" - body += "[bname][block]" + body += "[bname][block]" else body += "[block]" body+="
[t]
[t]

" @@ -266,7 +273,7 @@ var/global/floorIsLava = 0 for(var/index = 1, index <= number_pages, index++) if(index == page) dat += "" - dat += "[index] " + dat += "[index] " if(index == page) dat += "" @@ -317,13 +324,13 @@ var/global/floorIsLava = 0 update_file = 1 dat += "[I.content] by [I.author] ([I.rank]) on [I.timestamp] " if(I.author == usr.key || I.author == "Adminbot" || ishost(usr)) - dat += "Remove" + dat += "Remove" dat += "

" if(update_file) info << infos dat += "
" - dat += "Add Comment
" + dat += "Add Comment
" dat += "" usr << browse(dat, "window=adminplayerinfo;size=480x480") @@ -350,11 +357,11 @@ var/global/floorIsLava = 0
Note that this panel allows full freedom over the news network, there are no constrictions except the few basic ones. Don't break things! "} if(news_network.wanted_issue) - dat+= "
Read Wanted Issue" + dat+= "
Read Wanted Issue" - dat+= {"

Create Feed Channel -
View Feed Channels -
Submit new Feed story + dat+= {"

Create Feed Channel +
View Feed Channels +
Submit new Feed story

Exit "} @@ -363,10 +370,10 @@ var/global/floorIsLava = 0 wanted_already = 1 dat+={"
Feed Security functions:
-
[(wanted_already) ? ("Manage") : ("Publish")] 'Wanted' Issue -
Censor Feed Stories -
Mark Feed Channel with [(LEGACY_MAP_DATUM).company_name] D-Notice (disables and locks the channel. -

The newscaster recognises you as:
[src.admincaster_signature]
+
[(wanted_already) ? ("Manage") : ("Publish")] 'Wanted' Issue +
Censor Feed Stories +
Mark Feed Channel with [(LEGACY_MAP_DATUM).company_name] D-Notice (disables and locks the channel. +

The newscaster recognises you as:
[src.admincaster_signature]
"} if(1) dat+= "Station Feed Channels
" @@ -375,38 +382,38 @@ var/global/floorIsLava = 0 else for(var/datum/feed_channel/CHANNEL in news_network.network_channels) if(CHANNEL.is_admin_channel) - dat+="[CHANNEL.channel_name]
" + dat+="[CHANNEL.channel_name]
" else - dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : null]
" - dat+={"

Refresh -
Back + dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : null]
" + dat+={"

Refresh +
Back "} if(2) dat+={" Creating new Feed Channel... -
Channel Name: [src.admincaster_feed_channel.channel_name]
- Channel Author: [src.admincaster_signature]
- Will Accept Public Feeds: [(src.admincaster_feed_channel.locked) ? ("NO") : ("YES")]

-
Submit

Cancel
+
Channel Name: [src.admincaster_feed_channel.channel_name]
+ Channel Author: [src.admincaster_signature]
+ Will Accept Public Feeds: [(src.admincaster_feed_channel.locked) ? ("NO") : ("YES")]

+
Submit

Cancel
"} if(3) dat+={" Creating new Feed Message... -
Receiving Channel: [src.admincaster_feed_channel.channel_name]
+
Receiving Channel: [src.admincaster_feed_channel.channel_name]
Message Author: [src.admincaster_signature]
- Message Body: [src.admincaster_feed_message.body]
-
Submit

Cancel
+ Message Body: [src.admincaster_feed_message.body]
+
Submit

Cancel
"} if(4) dat+={" Feed story successfully submitted to [src.admincaster_feed_channel.channel_name].

-
Return
+
Return
"} if(5) dat+={" Feed Channel [src.admincaster_feed_channel.channel_name] created successfully.

-
Return
+
Return
"} if(6) dat+="ERROR: Could not submit Feed story to Network.

" @@ -414,7 +421,7 @@ var/global/floorIsLava = 0 dat+="•Invalid receiving channel name.
" if(src.admincaster_feed_message.body == "" || src.admincaster_feed_message.body == "\[REDACTED\]") dat+="•Invalid message body.
" - dat+="
Return
" + dat+="
Return
" if(7) dat+="ERROR: Could not submit Feed Channel to Network.

" if(src.admincaster_feed_channel.channel_name =="" || src.admincaster_feed_channel.channel_name == "\[REDACTED\]") @@ -426,7 +433,7 @@ var/global/floorIsLava = 0 break if(check) dat+="•Channel name already in use.
" - dat+="
Return
" + dat+="
Return
" if(9) dat+="[src.admincaster_feed_channel.channel_name]: \[created by: [src.admincaster_feed_channel.author]\]
" if(src.admincaster_feed_channel.censored) @@ -447,8 +454,8 @@ var/global/floorIsLava = 0 dat+="

" dat+="\[Story by [MESSAGE.author]\]
" dat+={" -

Refresh -
Back +

Refresh +
Back "} if(10) dat+={" @@ -461,8 +468,8 @@ var/global/floorIsLava = 0 dat+="No feed channels found active...
" else for(var/datum/feed_channel/CHANNEL in news_network.network_channels) - dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : null]
" - dat+="
Cancel" + dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : null]
" + dat+="
Cancel" if(11) dat+={" [(LEGACY_MAP_DATUM).company_name] D-Notice Handler
@@ -474,13 +481,13 @@ var/global/floorIsLava = 0 dat+="No feed channels found active...
" else for(var/datum/feed_channel/CHANNEL in news_network.network_channels) - dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : null]
" + dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : null]
" - dat+="
Back" + dat+="
Back" if(12) dat+={" [src.admincaster_feed_channel.channel_name]: \[ created by: [src.admincaster_feed_channel.author] \]
- [(src.admincaster_feed_channel.author=="\[REDACTED\]") ? ("Undo Author censorship") : ("Censor channel Author")]
+ [(src.admincaster_feed_channel.author=="\[REDACTED\]") ? ("Undo Author censorship") : ("Censor channel Author")]
"} if( !length(src.admincaster_feed_channel.messages) ) dat+="No feed messages found in channel...
" @@ -488,13 +495,13 @@ var/global/floorIsLava = 0 for(var/datum/feed_message/MESSAGE in src.admincaster_feed_channel.messages) dat+={" -[MESSAGE.body]
\[Story by [MESSAGE.author]\]
- [(MESSAGE.body == "\[REDACTED\]") ? ("Undo story censorship") : ("Censor story")] - [(MESSAGE.author == "\[REDACTED\]") ? ("Undo Author Censorship") : ("Censor message Author")]
+ [(MESSAGE.body == "\[REDACTED\]") ? ("Undo story censorship") : ("Censor story")] - [(MESSAGE.author == "\[REDACTED\]") ? ("Undo Author Censorship") : ("Censor message Author")]
"} - dat+="
Back" + dat+="
Back" if(13) dat+={" [src.admincaster_feed_channel.channel_name]: \[ created by: [src.admincaster_feed_channel.author] \]
- Channel messages listed below. If you deem them dangerous to the station, you can Bestow a D-Notice upon the channel.
+ Channel messages listed below. If you deem them dangerous to the station, you can Bestow a D-Notice upon the channel.
"} if(src.admincaster_feed_channel.censored) dat+={" @@ -508,7 +515,7 @@ var/global/floorIsLava = 0 for(var/datum/feed_message/MESSAGE in src.admincaster_feed_channel.messages) dat+="-[MESSAGE.body]
\[Story by [MESSAGE.author]\]
" - dat+="
Back" + dat+="
Back" if(14) dat+="Wanted Issue Handler:" var/wanted_already = 0 @@ -520,21 +527,21 @@ var/global/floorIsLava = 0 dat+="
A wanted issue is already in Feed Circulation. You can edit or cancel it below.
" dat+={"
- Criminal Name: [src.admincaster_feed_message.author]
- Description: [src.admincaster_feed_message.body]
+ Criminal Name: [src.admincaster_feed_message.author]
+ Description: [src.admincaster_feed_message.body]
"} if(wanted_already) dat+="Wanted Issue created by: [news_network.wanted_issue.backup_author]
" else dat+="Wanted Issue will be created under prosecutor: [src.admincaster_signature]
" - dat+="
[(wanted_already) ? ("Edit Issue") : ("Submit")]" + dat+="
[(wanted_already) ? ("Edit Issue") : ("Submit")]" if(wanted_already) - dat+="
Take down Issue" - dat+="
Cancel" + dat+="
Take down Issue" + dat+="
Cancel" if(15) dat+={" Wanted issue for [src.admincaster_feed_message.author] is now in Network Circulation.

-
Return
+
Return
"} if(16) dat+="ERROR: Wanted Issue rejected by Network.

" @@ -542,11 +549,11 @@ var/global/floorIsLava = 0 dat+="•Invalid name for person wanted.
" if(src.admincaster_feed_message.body == "" || src.admincaster_feed_message.body == "\[REDACTED\]") dat+="•Invalid description.
" - dat+="
Return
" + dat+="
Return
" if(17) dat+={" Wanted Issue successfully deleted from Circulation
-
Return
+
Return
"} if(18) dat+={" @@ -560,11 +567,11 @@ var/global/floorIsLava = 0 dat+="
" else dat+="None" - dat+="
Back
" + dat+="
Back
" if(19) dat+={" Wanted issue for [src.admincaster_feed_message.author] successfully edited.

-
Return
+
Return
"} else dat+="I'm sorry to break your immersion. This shit's bugged. Report this bug to Agouri, polyxenitopalidou@gmail.com" @@ -584,7 +591,7 @@ var/global/floorIsLava = 0 var/r = t if( findtext(r,"##") ) r = copytext( r, 1, findtext(r,"##") )//removes the description - dat += "[t] (unban)" + dat += "[t] (unban)" dat += "" usr << browse(dat, "window=ban;size=400x400") @@ -593,18 +600,18 @@ var/global/floorIsLava = 0 var/dat = {"
Game Panel

\n - Change Game Mode
+ Change Game Mode
"} if(master_mode == "secret") - dat += "(Force Secret Mode)
" + dat += "(Force Secret Mode)
" dat += {"
- Create Object
- Quick Create Object
- Create Turf
- Create Mob
-
Modify Atmospherics Properties
+ Create Object
+ Quick Create Object
+ Create Turf
+ Create Mob
+
Modify Atmospherics Properties
"} usr << browse(dat, "window=admin2;size=210x280") @@ -618,7 +625,7 @@ var/global/floorIsLava = 0 for(var/datum/admin_secret_category/category in admin_secrets.categories) if(!category.can_view(usr)) continue - dat += "[category.name] " + dat += "[category.name] " dat += "
" // If a category is selected, print its description and then options @@ -629,7 +636,7 @@ var/global/floorIsLava = 0 for(var/datum/admin_secret_item/item in active_category.items) if(!item.can_view(usr)) continue - dat += "[item.name()]
" + dat += "[item.name()]
" dat += "
" var/datum/browser/popup = new(usr, "secrets", "Secrets", 500, 500) @@ -1271,19 +1278,19 @@ var/datum/legacy_announcement/minor/admin_min_announcer = new return "[key_name(C, link, name, highlight_special)]" if(1) //Private Messages - return "[key_name(C, link, name, highlight_special)](?)" + return "[key_name(C, link, name, highlight_special)](?)" if(2) //Admins - var/ref_mob = "\ref[M]" - return "[key_name(C, link, name, highlight_special)](?) (PP) (VV) (SM) ([admin_jump_link(M)]) (CA) (TAKE)" + var/ref_mob = "[REF(M)]" + return "[key_name(C, link, name, highlight_special)](?) (PP) (VV) (SM) ([admin_jump_link(M)]) (CA) (TAKE)" if(3) //Devs - var/ref_mob = "\ref[M]" - return "[key_name(C, link, name, highlight_special)](VV)([admin_jump_link(M)]) (TAKE)" + var/ref_mob = "[REF(M)]" + return "[key_name(C, link, name, highlight_special)](VV)([admin_jump_link(M)]) (TAKE)" if(4) //Event Managers - var/ref_mob = "\ref[M]" - return "[key_name(C, link, name, highlight_special)] (?) (PP) (VV) (SM) ([admin_jump_link(M)]) (TAKE)" + var/ref_mob = "[REF(M)]" + return "[key_name(C, link, name, highlight_special)] (?) (PP) (VV) (SM) ([admin_jump_link(M)]) (TAKE)" /proc/ishost(whom) @@ -1539,3 +1546,17 @@ datum/admins/var/obj/item/paper/admin/faxreply // var to hold fax replies in dead.alpha = 0 dead.original_name = dead.name dead.name = "ghost" + +/// Sends a message to adminchat when anyone with a holder logs in or logs out. +/// Is dependent on admin preferences and configuration settings, which means that this proc can fire without sending a message. +/client/proc/adminGreet(logout = FALSE) + if(!SSticker.HasRoundStarted()) + return + + if(logout /* && CONFIG_GET(flag/announce_admin_logout) */) + message_admins("Admin logout: [key_name(src)]") + return + + if(!logout /* && CONFIG_GET(flag/announce_admin_login) && (prefs.toggles & ANNOUNCE_LOGIN) */ ) + message_admins("Admin login: [key_name(src)]") + return diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index d59d7c798855..9a3bd8a08fbf 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -173,6 +173,8 @@ var/list/admin_verbs_server = list( /datum/admins/proc/toggleAI, /client/proc/cmd_admin_delete, // Delete an instance/object/mob/etc, /client/proc/cmd_debug_del_all, + /client/proc/cmd_debug_del_all_force, + /client/proc/cmd_debug_del_all_hard, /client/proc/cmd_admin_clear_mobs, /datum/admins/proc/adspawn, /datum/admins/proc/adjump, @@ -524,15 +526,6 @@ var/list/admin_verbs_event_manager = list( to_chat(mob, "Invisimin on. You are now as invisible as a ghost.") mob.alpha = max(mob.alpha - 100, 0) -/* -/client/proc/player_panel() - set name = "Player Panel" - set category = "Admin" - if(holder) - holder.player_panel_old() - feedback_add_details("admin_verb","PP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - return -*/ /client/proc/player_panel() set name = "Player Panel" set category = "Admin" @@ -587,24 +580,38 @@ var/list/admin_verbs_event_manager = list( holder.Secrets() feedback_add_details("admin_verb","S") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! +//! do not use /client/proc/findStealthKey(txt) - if(txt) - for(var/P in GLOB.stealthminID) - if(GLOB.stealthminID[P] == txt) - return P - txt = GLOB.stealthminID[ckey] - return txt + return findTrueKey(txt) + +/// Takes a stealthed ckey as input, returns the true key it represents +/proc/findTrueKey(stealth_key) + if(!stealth_key) + return + for(var/potentialKey in GLOB.stealthminID) + if(GLOB.stealthminID[potentialKey] == stealth_key) + return potentialKey + +/// Hands back a stealth ckey to use, guarenteed to be unique +/proc/generateStealthCkey() + var/guess = rand(0, 1000) + var/text_guess + var/valid_found = FALSE + while(valid_found == FALSE) + valid_found = TRUE + text_guess = "@[num2text(guess)]" + // We take a guess at some number, and if it's not in the existing stealthmin list we exit + for(var/key in GLOB.stealthminID) + // If it is in the list tho, we up one number, and redo the loop + if(GLOB.stealthminID[key] == text_guess) + guess += 1 + valid_found = FALSE + break + + return text_guess /client/proc/createStealthKey() - var/num = (rand(0,1000)) - var/i = 0 - while(i == 0) - i = 1 - for(var/P in GLOB.stealthminID) - if(num == GLOB.stealthminID[P]) - num++ - i = 0 - GLOB.stealthminID["[ckey]"] = "@[num2text(num)]" + GLOB.stealthminID["[ckey]"] = generateStealthCkey() /client/proc/stealth() set category = "Admin" diff --git a/code/modules/admin/callproc/callproc.dm b/code/modules/admin/callproc/callproc.dm index 4fd283abc8c1..a3a85c78754e 100644 --- a/code/modules/admin/callproc/callproc.dm +++ b/code/modules/admin/callproc/callproc.dm @@ -1,6 +1,100 @@ +GLOBAL_DATUM_INIT(AdminProcCallHandler, /mob/proccall_handler, new()) +GLOBAL_PROTECT(AdminProcCallHandler) + +/// Used to handle proccalls called indirectly by an admin (e.g. tgs, circuits). +/// Has to be a mob because IsAdminAdvancedProcCall() checks usr, which is a mob variable. +/// So usr is set to this for any proccalls that don't have any usr mob/client to refer to. +/mob/proccall_handler + name = "ProcCall Handler" + desc = "If you are seeing this, tell a coder." + + var/list/callers = list() + + invisibility = INVISIBILITY_ABSTRACT + density = FALSE + +/// Adds a caller. +/mob/proccall_handler/proc/add_caller(caller_name) + callers += caller_name + name = "[initial(name)] ([callers.Join(") (")])" + +/// Removes a caller. +/mob/proccall_handler/proc/remove_caller(caller_name) + callers -= caller_name + name = "[initial(name)] ([callers.Join(") (")])" + +/mob/proccall_handler/Initialize(mapload) + . = ..() + if(GLOB.AdminProcCallHandler && GLOB.AdminProcCallHandler != src) + return INITIALIZE_HINT_QDEL + GLOB.AdminProcCallHandler = src + +/mob/proccall_handler/vv_edit_var(var_name, var_value) + if(GLOB.AdminProcCallHandler != src) + return ..() + return FALSE + +/mob/proccall_handler/vv_do_topic(list/href_list) + if(GLOB.AdminProcCallHandler != src) + return ..() + return FALSE + +/mob/proccall_handler/CanProcCall(procname) + if(GLOB.AdminProcCallHandler != src) + return ..() + return FALSE + +// Shit will break if this is allowed to be deleted +/mob/proccall_handler/Destroy(force) + if(GLOB.AdminProcCallHandler != src) + return ..() + if(!force) + stack_trace("Attempted deletion on [type] - [name], aborting.") + return QDEL_HINT_LETMELIVE + return ..() + +/** + * Handles a userless proccall, used by circuits. + * + * Arguments: + * * user - a string used to identify the user + * * target - the target to proccall on + * * proc - the proc to call + * * arguments - any arguments + */ +/proc/HandleUserlessProcCall(user, datum/target, procname, list/arguments) + if(IsAdminAdvancedProcCall()) + return + var/mob/proccall_handler/handler = GLOB.AdminProcCallHandler + handler.add_caller(user) + var/lastusr = usr + usr = handler + . = WrapAdminProcCall(target, procname, arguments) + usr = lastusr + handler.remove_caller(user) + +/** + * Handles a userless sdql, used by circuits and tgs. + * + * Arguments: + * * user - a string used to identify the user + * * query_text - the query text + */ +/proc/HandleUserlessSDQL(user, query_text) + if(IsAdminAdvancedProcCall()) + return + var/mob/proccall_handler/handler = GLOB.AdminProcCallHandler + handler.add_caller(user) + var/lastusr = usr + usr = handler + . = world.SDQL2_query(query_text, user, user) + usr = lastusr + handler.remove_caller(user) + /client/proc/callproc() set category = "Debug" set name = "Advanced ProcCall" + set desc = "Call a proc on any datum in the server." set waitfor = FALSE callproc_blocking() @@ -20,7 +114,7 @@ return target = value["value"] if(!istype(target)) - to_chat(usr, "Invalid target.") + to_chat(usr, SPAN_DANGER("Invalid target."), confidential = TRUE) return if("No") target = null @@ -40,38 +134,39 @@ if(targetselected) if(!hascall(target, procname)) - to_chat(usr, "Error: callproc(): type [target.type] has no [proctype] named [procpath].") + to_chat(usr, SPAN_WARNING("Error: callproc(): type [target.type] has no [proctype] named [procpath]."), confidential = TRUE) return else procpath = "/[proctype]/[procname]" if(!text2path(procpath)) - to_chat(usr, "Error: callproc(): [procpath] does not exist.") + to_chat(usr, SPAN_WARNING("Error: callproc(): [procpath] does not exist."), confidential = TRUE) return + var/list/lst = get_callproc_args() if(!lst) return if(targetselected) if(!target) - to_chat(usr, "Error: callproc(): owner of proc no longer exists.") + to_chat(usr, "Error: callproc(): owner of proc no longer exists.", confidential = TRUE) return var/msg = "[key_name(src)] called [target]'s [procname]() with [lst.len ? "the arguments [list2params(lst)]":"no arguments"]." log_admin(msg) - message_admins(msg) //Proccall announce removed. + message_admins(msg) //Proccall announce removed. admin_ticket_log(target, msg) returnval = WrapAdminProcCall(target, procname, lst) // Pass the lst as an argument list to the proc else //this currently has no hascall protection. wasn't able to get it working. log_admin("[key_name(src)] called [procname]() with [lst.len ? "the arguments [list2params(lst)]":"no arguments"].") - message_admins("[key_name(src)] called [procname]() with [lst.len ? "the arguments [list2params(lst)]":"no arguments"].") //Proccall announce removed. + message_admins("[key_name(src)] called [procname]() with [lst.len ? "the arguments [list2params(lst)]":"no arguments"].") //Proccall announce removed. returnval = WrapAdminProcCall(GLOBAL_PROC, procname, lst) // Pass the lst as an argument list to the proc - //SSblackbox.record_feedback("tally", "admin_verb", 1, "Advanced ProcCall") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + if(get_retval) - get_retval["VALUE"] = returnval + get_retval += returnval . = get_callproc_returnval(returnval, procname) if(.) - to_chat(usr, .) + to_chat(usr, ., confidential = TRUE) GLOBAL_VAR(AdminProcCaller) GLOBAL_PROTECT(AdminProcCaller) @@ -86,35 +181,47 @@ GLOBAL_PROTECT(LastAdminCalledProc) GLOBAL_LIST_EMPTY(AdminProcCallSpamPrevention) GLOBAL_PROTECT(AdminProcCallSpamPrevention) +/// Wrapper for proccalls where the datum is flagged as vareditted /proc/WrapAdminProcCall(datum/target, procname, list/arguments) if(target && procname == "Del") - to_chat(usr, "Calling Del() is not allowed") + to_chat(usr, "Calling Del() is not allowed", confidential = TRUE) return if(target != GLOBAL_PROC && !target.CanProcCall(procname)) - to_chat(usr, "Proccall on [target.type]/proc/[procname] is disallowed!") + to_chat(usr, "Proccall on [target.type]/proc/[procname] is disallowed!", confidential = TRUE) return var/current_caller = GLOB.AdminProcCaller - var/ckey = usr ? usr.client.ckey : GLOB.AdminProcCaller - if(!ckey) + var/user_identifier = usr ? usr.client?.ckey : GLOB.AdminProcCaller + var/is_remote_handler = usr == GLOB.AdminProcCallHandler + if(is_remote_handler) + user_identifier = GLOB.AdminProcCallHandler.name + + if(!user_identifier) CRASH("WrapAdminProcCall with no ckey: [target] [procname] [english_list(arguments)]") - if(current_caller && current_caller != ckey) - if(!GLOB.AdminProcCallSpamPrevention[ckey]) - to_chat(usr, "Another set of admin called procs are still running, your proc will be run after theirs finish.") - GLOB.AdminProcCallSpamPrevention[ckey] = TRUE + + if(!is_remote_handler && current_caller && current_caller != user_identifier) + if(!GLOB.AdminProcCallSpamPrevention[user_identifier]) + to_chat(usr, SPAN_ADMINNOTICE("Another set of admin called procs are still running, your proc will be run after theirs finish."), confidential = TRUE) + GLOB.AdminProcCallSpamPrevention[user_identifier] = TRUE UNTIL(!GLOB.AdminProcCaller) - to_chat(usr, "Running your proc") - GLOB.AdminProcCallSpamPrevention -= ckey + to_chat(usr, SPAN_ADMINNOTICE("Running your proc")) + GLOB.AdminProcCallSpamPrevention -= user_identifier else UNTIL(!GLOB.AdminProcCaller) + GLOB.LastAdminCalledProc = procname if(target != GLOBAL_PROC) GLOB.LastAdminCalledTargetRef = REF(target) - GLOB.AdminProcCaller = ckey //if this runtimes, too bad for you - ++GLOB.AdminProcCallCount - . = world.WrapAdminProcCall(target, procname, arguments) - if(--GLOB.AdminProcCallCount == 0) - GLOB.AdminProcCaller = null + + if(!is_remote_handler) + GLOB.AdminProcCaller = user_identifier //if this runtimes, too bad for you + ++GLOB.AdminProcCallCount + . = world.WrapAdminProcCall(target, procname, arguments) + GLOB.AdminProcCallCount-- + if(GLOB.AdminProcCallCount == 0) + GLOB.AdminProcCaller = null + else + . = world.WrapAdminProcCall(target, procname, arguments) //adv proc call this, ya nerds /world/proc/WrapAdminProcCall(datum/target, procname, list/arguments) @@ -129,10 +236,10 @@ GLOBAL_PROTECT(AdminProcCallSpamPrevention) #ifdef TESTING return FALSE #else - return usr && usr.client && GLOB.AdminProcCaller == usr.client.ckey + return (GLOB.AdminProcCaller && GLOB.AdminProcCaller == usr?.client?.ckey) || (GLOB.AdminProcCallHandler && usr == GLOB.AdminProcCallHandler) #endif -/client/proc/callproc_datum(datum/A as null|area|mob|obj|turf) +/client/proc/callproc_datum(datum/thing as null|area|mob|obj|turf) set category = "Debug" set name = "Atom ProcCall" set waitfor = 0 @@ -143,33 +250,32 @@ GLOBAL_PROTECT(AdminProcCallSpamPrevention) var/procname = input("Proc name, eg: fake_blood","Proc:", null) as text|null if(!procname) return - if(!hascall(A,procname)) - to_chat(usr, "Error: callproc_datum(): type [A.type] has no proc named [procname].") + if(!hascall(thing, procname)) + to_chat(usr, "Error: callproc_datum(): type [thing.type] has no proc named [procname].", confidential = TRUE) return var/list/lst = get_callproc_args() if(!lst) return - if(!A || !is_valid_src(A)) - to_chat(usr, "Error: callproc_datum(): owner of proc no longer exists.") + if(!thing || !is_valid_src(thing)) + to_chat(usr, SPAN_WARNING("Error: callproc_datum(): owner of proc no longer exists."), confidential = TRUE) return - log_admin("[key_name(src)] called [A]'s [procname]() with [lst.len ? "the arguments [list2params(lst)]":"no arguments"].") - var/msg = "[key_name(src)] called [A]'s [procname]() with [lst.len ? "the arguments [list2params(lst)]":"no arguments"]." + log_admin("[key_name(usr)] called [thing]'s [procname]() with [lst.len ? "the arguments [list2params(lst)]":"no arguments"].") + var/msg = "[key_name(usr)] called [thing]'s [procname]() with [lst.len ? "the arguments [list2params(lst)]":"no arguments"]." message_admins(msg) - admin_ticket_log(A, msg) - //SSblackbox.record_feedback("tally", "admin_verb", 1, "Atom ProcCall") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + admin_ticket_log(thing, msg) - var/returnval = WrapAdminProcCall(A, procname, lst) // Pass the lst as an argument list to the proc + var/returnval = WrapAdminProcCall(thing, procname, lst) // Pass the lst as an argument list to the proc . = get_callproc_returnval(returnval,procname) if(.) - to_chat(usr, .) + to_chat(usr, ., confidential = TRUE) /client/proc/get_callproc_args() var/argnum = input("Number of arguments","Number:",0) as num|null if(isnull(argnum)) return - var/list/retval = list() + . = list() var/list/named_args = list() while(argnum--) var/named_arg = input("Leave blank for positional argument. Positional arguments will be considered as if they were added first.", "Named argument") as text|null @@ -179,19 +285,15 @@ GLOBAL_PROTECT(AdminProcCallSpamPrevention) if(named_arg) named_args[named_arg] = value["value"] else - retval.len++ - retval[retval.len] = value["value"] + . += LIST_VALUE_WRAP_LISTS(value["value"]) if(LAZYLEN(named_args)) - retval += named_args - return retval - -// todo: rework all of vv and proccall yet again because this is shit formatting + . += named_args /client/proc/get_callproc_returnval(returnval,procname) . = "" if(islist(returnval)) var/list/returnedlist = returnval - . = "" + . = "" if(returnedlist.len) var/assoc_check = returnedlist[1] if(istext(assoc_check) && (returnedlist[assoc_check] != null)) @@ -208,9 +310,9 @@ GLOBAL_PROTECT(AdminProcCallSpamPrevention) . += "" else if(istype(returnval, /icon)) var/icon/I = returnval - . += "[procname] returned an icon: [REF(returnval)] - [icon2html(I, src)] [I] ([I.type])" + . += "[procname] returned an icon: [REF(returnval)] - [icon2html(I, src)] [I] ([I.type])" else if(istype(returnval, /datum)) var/datum/D = returnval - . += "[procname] returned a datum: [REF(returnval)] - [D] ([D.type])" + . += "[procname] returned a datum: [REF(returnval)] - [D] ([D.type])" else - . = "[procname] returned: [!isnull(returnval) ? returnval : "null"]" + . = "[procname] returned: [!isnull(returnval) ? html_encode(returnval) : "null"]" diff --git a/code/modules/admin/chat_commands.dm b/code/modules/admin/chat_commands.dm index d64a78cb6894..fc9977c311b8 100644 --- a/code/modules/admin/chat_commands.dm +++ b/code/modules/admin/chat_commands.dm @@ -1,55 +1,45 @@ -#define IRC_STATUS_THROTTLE 5 - -/datum/tgs_chat_command/ircstatus - name = "status" - help_text = "Gets the admincount, playercount, gamemode, and true game mode of the server" +/// Reload admins tgs chat command. Intentionally not validated. +/datum/tgs_chat_command/reload_admins + name = "reload_admins" + help_text = "Forces the server to reload admins." admin_only = TRUE - var/last_irc_status = 0 -/datum/tgs_chat_command/ircstatus/Run(datum/tgs_chat_user/sender, params) - var/rtod = REALTIMEOFDAY - if(rtod - last_irc_status < IRC_STATUS_THROTTLE) - return - last_irc_status = rtod - var/list/adm = get_admin_counts() - 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 +/datum/tgs_chat_command/reload_admins/Run(datum/tgs_chat_user/sender, params) + ReloadAsync() + log_admin("[sender.friendly_name] reloaded admins via chat command.") + message_admins("[sender.friendly_name] reloaded admins via chat command.") + return new /datum/tgs_message_content("Admins reloaded.") -/datum/tgs_chat_command/irccheck - name = "check" - help_text = "Gets the playercount, gamemode, and address of the server" - var/last_irc_check = 0 +/datum/tgs_chat_command/reload_admins/proc/ReloadAsync() + set waitfor = FALSE + load_admins() -/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 = null //CONFIG_GET(string/server) - //return "[round_id ? "Round #[round_id]: " : ""][clients.len] players on [SSmapping.config_legacy.map_name], Mode: [master_mode]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] -- [server ? server : "[world.internet_address]:[world.port]"]" - var/current_state - switch(SSticker.current_state) - if(GAME_STATE_PREGAME) - current_state = "pregame" - if(GAME_STATE_SETTING_UP) - current_state = "starting" - if(GAME_STATE_PLAYING) - current_state = "active" - if(GAME_STATE_FINISHED) - current_state = "finishing" - return "[GLOB.clients.len] players on [(LEGACY_MAP_DATUM).name], Mode: [master_mode]; round [current_state] -- Duration [roundduration2text()] -- [server ? server : "[world.internet_address]:[world.port]"]" +/// subtype tgs chat command with validated admin ranks. Only supports discord. +/datum/tgs_chat_command/validated + ignore_type = /datum/tgs_chat_command/validated + admin_only = TRUE + var/required_rights = NONE //! validate discord userid is linked to a game admin with these flags. + +/// called by tgs +/datum/tgs_chat_command/validated/Run(datum/tgs_chat_user/sender, params) + // TODO: discord <-> ss13 admin linkage + return Validated_Run(sender, params) -/datum/tgs_chat_command/ahelp +/// Called if the sender passes validation checks or if those checks are disabled. +/datum/tgs_chat_command/validated/proc/Validated_Run(datum/tgs_chat_user/sender, params) + RETURN_TYPE(/datum/tgs_message_content) + CRASH("[type] has no implementation for Validated_Run()") + +/datum/tgs_chat_command/validated/ahelp name = "ahelp" help_text = " |list>>" admin_only = TRUE + required_rights = R_ADMIN -/datum/tgs_chat_command/ahelp/Run(datum/tgs_chat_user/sender, params) +/datum/tgs_chat_command/validated/ahelp/Validated_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) @@ -58,31 +48,84 @@ if(AH) target = AH.initiator_ckey else - return "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("Ticket #[id] not found!") + return new /datum/tgs_message_content(TgsPm(target, all_params.Join(" "), sender.friendly_name)) -/datum/tgs_chat_command/namecheck +/datum/tgs_chat_command/validated/namecheck name = "namecheck" help_text = "Returns info on the specified target" admin_only = TRUE + required_rights = R_ADMIN -/datum/tgs_chat_command/namecheck/Run(datum/tgs_chat_user/sender, params) +/datum/tgs_chat_command/validated/namecheck/Validated_Run(datum/tgs_chat_user/sender, params) params = trim(params) if(!params) - return "Insufficient parameters" + return new /datum/tgs_message_content("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 +/datum/tgs_chat_command/validated/adminwho name = "adminwho" help_text = "Lists administrators currently on the server" admin_only = TRUE + required_rights = 0 + +/datum/tgs_chat_command/validated/adminwho/Validated_Run(datum/tgs_chat_user/sender, params) + return new /datum/tgs_message_content(tgsadminwho()) -/datum/tgs_chat_command/adminwho/Run(datum/tgs_chat_user/sender, params) - return ircadminwho() +/datum/tgs_chat_command/validated/sdql + name = "sdql" + help_text = "Runs an SDQL query" + admin_only = TRUE + required_rights = R_DEBUG + +/datum/tgs_chat_command/validated/sdql/Validated_Run(datum/tgs_chat_user/sender, params) + var/list/results = HandleUserlessSDQL(sender.friendly_name, params) + if(!results) + 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 + return new /datum/tgs_message_content("[text_res.Join("\n")][refs ? "\nRefs: [refs.Join(" ")]" : ""]") + +/datum/tgs_chat_command/validated/tgsstatus + name = "status" + help_text = "Gets the admincount, playercount, gamemode, and true game mode of the server" + admin_only = TRUE + required_rights = R_ADMIN + +/datum/tgs_chat_command/validated/tgsstatus/Validated_Run(datum/tgs_chat_user/sender, params) + var/list/adm = get_admin_counts() + 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]. Round has [SSticker.HasRoundStarted() ? "" : "not "]started." + return new /datum/tgs_message_content(status) + +#define IRC_STATUS_THROTTLE 5 + +/datum/tgs_chat_command/irccheck + name = "check" + help_text = "Gets the playercount, gamemode, 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 = null //CONFIG_GET(string/server) + //return "[round_id ? "Round #[round_id]: " : ""][clients.len] players on [SSmapping.config_legacy.map_name], Mode: [master_mode]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] -- [server ? server : "[world.internet_address]:[world.port]"]" + var/current_state + switch(SSticker.current_state) + if(GAME_STATE_PREGAME) + current_state = "pregame" + if(GAME_STATE_SETTING_UP) + current_state = "starting" + if(GAME_STATE_PLAYING) + current_state = "active" + if(GAME_STATE_FINISHED) + current_state = "finishing" + return "[GLOB.clients.len] players on [(LEGACY_MAP_DATUM).name], Mode: [master_mode]; round [current_state] -- Duration [roundduration2text()] -- [server ? server : "[world.internet_address]:[world.port]"]" GLOBAL_LIST(round_end_notifiees) @@ -99,36 +142,6 @@ GLOBAL_LIST(round_end_notifiees) GLOB.round_end_notifiees[sender.mention] = TRUE return "I will notify [sender.mention] when the round ends." -/datum/tgs_chat_command/sdql - name = "sdql" - help_text = "Runs an SDQL query" - admin_only = TRUE - -/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." - 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" - var/list/text_res = results.Copy(1, 3) - var/list/refs = results.len > 3 ? results.Copy(4) : null - . = "[text_res.Join("\n")][refs ? "\nRefs: [refs.Join(" ")]" : ""]" - -/datum/tgs_chat_command/reload_admins - name = "reload_admins" - help_text = "Forces the server to reload admins." - admin_only = TRUE - -/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." - -/datum/tgs_chat_command/reload_admins/proc/ReloadAsync() - set waitfor = FALSE - load_admins() /datum/tgs_chat_command/whitelist name = "whitelist" diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm index be83c2f246b6..2240c5e61bf5 100644 --- a/code/modules/admin/holder2.dm +++ b/code/modules/admin/holder2.dm @@ -5,6 +5,9 @@ GLOBAL_PROTECT(href_token) /datum/admins var/rank = "Temporary Admin" + + var/target + var/name = "nobody's admin datum (no rank)" //Makes for better runtimes var/client/owner = null var/rights = 0 // todo: rework @@ -23,12 +26,22 @@ GLOBAL_PROTECT(href_token) var/datum/filter_editor/filteriffic /datum/admins/New(initial_rank = "Temporary Admin", initial_rights = 0, ckey) - if(!ckey) - log_world("Admin datum created without a ckey argument. Datum has been deleted") - qdel(src) + if(IsAdminAdvancedProcCall()) + alert_to_permissions_elevation_attempt(usr) + if (!target) //only del if this is a true creation (and not just a New() proc call), other wise trialmins/coders could abuse this to deadmin other admins + QDEL_IN(src, 0) + CRASH("Admin proc call creation of admin datum") return + if(!ckey) + QDEL_IN(src, 0) + CRASH("Admin datum created without a ckey") + + target = ckey + name = "[ckey]'s admin datum ([initial_rank])" rank = initial_rank rights = initial_rights + href_token = GenerateToken() + admin_datums[ckey] = src if(rights & R_DEBUG) //grant profile access world.SetConfig("APP/admin", ckey, "role=admin") @@ -37,14 +50,35 @@ GLOBAL_PROTECT(href_token) UNTIL(SSmapping.loaded_station) admincaster_signature = "[(LEGACY_MAP_DATUM).company_name] Officer #[rand(0,9)][rand(0,9)][rand(0,9)]" +/datum/admins/Destroy() + if(IsAdminAdvancedProcCall()) + alert_to_permissions_elevation_attempt(usr) + return QDEL_HINT_LETMELIVE + . = ..() + /datum/admins/proc/associate(client/C) - if(istype(C)) - owner = C - owner.holder = src - owner.add_admin_verbs() //TODO - GLOB.admins |= C + if(IsAdminAdvancedProcCall()) + alert_to_permissions_elevation_attempt(usr) + return + + if(!istype(C)) + return + + if(C?.ckey != target) + var/msg = " has attempted to associate with [target]'s admin datum" + message_admins("[key_name_admin(C)][msg]") + log_admin("[key_name(C)][msg]") + return + + owner = C + owner.holder = src + owner.add_admin_verbs() + GLOB.admins |= C /datum/admins/proc/disassociate() + if(IsAdminAdvancedProcCall()) + alert_to_permissions_elevation_attempt(usr) + return if(owner) GLOB.admins -= owner owner.remove_admin_verbs() @@ -52,12 +86,23 @@ GLOBAL_PROTECT(href_token) owner.holder = null /datum/admins/proc/reassociate() + if(IsAdminAdvancedProcCall()) + alert_to_permissions_elevation_attempt(usr) + return if(owner) GLOB.admins |= owner owner.holder = src owner.deadmin_holder = null owner.add_admin_verbs() +/datum/admins/proc/check_for_rights(rights_required) + if(rights_required && !(rights_required & rights)) + return FALSE + return TRUE + +/datum/admins/vv_edit_var(var_name, var_value) + return FALSE //nice try trialmin + /* checks if usr is an admin with at least ONE of the flags in rights_required. (Note, they don't need all the flags) if rights_required == 0, then it simply checks if they are an admin. @@ -70,7 +115,7 @@ proc/admin_proc() NOTE: It checks usr by default. Supply the "user" argument if you wish to check for a specific mob. */ -/proc/check_rights(rights_required, show_msg = TRUE, var/client/C = usr) +/proc/check_rights(rights_required, show_msg = TRUE, client/C = usr) if(ismob(C)) var/mob/M = C C = M.client @@ -93,22 +138,17 @@ NOTE: It checks usr by default. Supply the "user" argument if you wish to check else return TRUE -/datum/admins/proc/check_for_rights(rights_required) - if(rights_required && !(rights_required & rights)) - return FALSE - return TRUE - //probably a bit iffy - will hopefully figure out a better solution /proc/check_if_greater_rights_than(client/other) - if(usr && usr.client) + if(usr?.client) if(usr.client.holder) if(!other || !other.holder) - return 1 + return TRUE if(usr.client.holder.rights != other.holder.rights) if( (usr.client.holder.rights & other.holder.rights) == other.holder.rights ) - return 1 //we have all the rights they have and more + return TRUE //we have all the rights they have and more to_chat(usr, "Error: Cannot proceed. They have more or equal rights to us.") - return 0 + return FALSE //This proc checks whether subject has at least ONE of the rights specified in rights_required. /proc/check_rights_for(client/subject, rights_required) @@ -144,27 +184,3 @@ NOTE: It checks usr by default. Supply the "user" argument if you wish to check /proc/HrefTokenFormField(forceGlobal = FALSE) return "" -/datum/admins/proc/CheckAdminHref(href, href_list) - return TRUE - /* Disabled - var/auth = href_list["admin_token"] - . = auth && (auth == href_token || auth == GLOB.href_token) - if(.) - return - var/msg = !auth ? "no" : "a bad" - message_admins("[key_name_admin(usr)] clicked an href with [msg] authorization key!") - if(CONFIG_GET(flag/debug_admin_hrefs)) - message_admins("Debug mode enabled, call not blocked. Please ask your coders to review this round's logs.") - log_world("UAH: [href]") - return TRUE - log_admin_private("[key_name(usr)] clicked an href with [msg] authorization key! [href]") - */ - -/datum/admins/vv_edit_var(var_name, var_value) -#ifdef TESTING - return ..() -#else - if(var_name == NAMEOF(src, rank) || var_name == NAMEOF(src, rights)) - return FALSE - return ..() -#endif diff --git a/code/modules/admin/player_panel.dm b/code/modules/admin/player_panel.dm index 60eacb6a16ab..72f1f864b361 100644 --- a/code/modules/admin/player_panel.dm +++ b/code/modules/admin/player_panel.dm @@ -1,8 +1,8 @@ - /datum/admins/proc/player_panel()//The new one - if (!usr.client.holder) + if(!check_rights()) return - var/dat = "Admin Player Panel" + log_admin("[key_name(usr)] checked the player panel.") + var/dat = "Player Panel" //javascript, the part that does most of the work~ dat += {" @@ -38,15 +38,11 @@ } var ltd = tr.getElementsByTagName("td"); var td = ltd\[0\]; - var lsearch = td.getElementsByTagName("b"); + var lsearch = td.getElementsByClassName("filter_data"); var search = lsearch\[0\]; - //var inner_span = li.getElementsByTagName("span")\[1\] //Should only ever contain one element. - //document.write("

"+search.innerText+"
"+filter+"
"+search.innerText.indexOf(filter)) if ( search.innerText.toLowerCase().indexOf(filter) == -1 ) { - //document.write("a"); - //ltr.removeChild(tr); - td.innerHTML = ""; + tr.innerHTML = ""; i--; } }catch(err) { } @@ -58,32 +54,41 @@ var debug = document.getElementById("debug"); locked_tabs = new Array(); - } - function expand(id,job,name,real_name,image,key,ip,antagonist,ref){ + function expand(data_id,target_id){ + + job = document.getElementById(data_id+"_job").textContent + name = document.getElementById(data_id+"_name").textContent + real_name = document.getElementById(data_id+"_rname").textContent + old_names = document.getElementById(data_id+"_prevnames").textContent + key = document.getElementById(data_id+"_key").textContent + ip = document.getElementById(data_id+"_lastip").textContent + antagonist = document.getElementById(data_id+"_isantag").textContent + ref = document.getElementById(data_id+"_ref").textContent clearAll(); - var span = document.getElementById(id); + var span = document.getElementById(target_id); + var ckey = key.toLowerCase().replace(/\[^a-z@0-9\]+/g,""); body = "
"; body += ""; - body += ""+job+" "+name+"
Real name "+real_name+"
Played by "+key+" ("+ip+")
" + body += ""+job+" "+name+"
Real name "+real_name+"
Played by "+key+" ("+ip+")
Old names: "+old_names+"
"; body += "
"; - body += "PP - " - body += "N - " - body += "VV - " - body += "TP - " - body += "PM - " - body += "SM - " - body += "FLW
" + body += "PP - " + body += "N - " + body += "VV - " + body += "TP - " + body += "PM - " + body += "SM - " + body += "FLW
" if(antagonist > 0) - body += "Antagonist"; + body += "Antagonist"; body += "
"; @@ -98,13 +103,13 @@ var id = span.getAttribute("id"); - if(!(id.indexOf("item")==0)) + if(!id || !(id.indexOf("item") == 0)) continue; var pass = 1; for(var j = 0; j < locked_tabs.length; j++){ - if(locked_tabs\[j\]==id){ + if(locked_tabs\[j\] == id){ pass = 0; break; } @@ -133,7 +138,7 @@ var pass = 1; for(var j = 0; j < locked_tabs.length; j++){ - if(locked_tabs\[j\]==id){ + if(locked_tabs\[j\] == id){ pass = 0; break; } @@ -143,9 +148,6 @@ locked_tabs.push(id); var notice_span = document.getElementById(notice_span_id); notice_span.innerHTML = "Locked "; - //link.setAttribute("onClick","attempt('"+id+"','"+link_id+"','"+notice_span_id+"');"); - //document.write("removeFromLocked('"+id+"','"+link_id+"','"+notice_span_id+"')"); - //document.write("aa - "+link.getAttribute("onClick")); } function attempt(ab){ @@ -157,7 +159,7 @@ var index = 0; var pass = 0; for(var j = 0; j < locked_tabs.length; j++){ - if(locked_tabs\[j\]==id){ + if(locked_tabs\[j\] == id){ pass = 1; index = j; break; @@ -168,8 +170,6 @@ locked_tabs\[index\] = ""; var notice_span = document.getElementById(notice_span_id); notice_span.innerHTML = ""; - //var link = document.getElementById(link_id); - //link.setAttribute("onClick","addToLocked('"+id+"','"+link_id+"','"+notice_span_id+"')"); } function selectTextField(){ @@ -194,7 +194,7 @@ Player panel
- Hover over a line to see more information - Check antagonists + Hover over a line to see more information - Check antagonists

@@ -227,13 +227,11 @@ if(isliving(M)) if(iscarbon(M)) //Carbon stuff - if(ishuman(M)) + if(ishuman(M) && M.job) M_job = M.job - else if(isslime(M)) - M_job = "slime" else if(issmall(M)) - M_job = "Monkey" //??? - else if(isalien(M)) + M_job = "Monkey" // isSmall are monkeys (probably) + else if(isalien(M)) //aliens M_job = "Alien" else M_job = "Carbon-based" @@ -251,35 +249,29 @@ else if(isanimal(M)) //simple animals if(iscorgi(M)) M_job = "Corgi" + else if(isslime(M)) + M_job = "slime" else M_job = "Animal" else M_job = "Living" - else if(istype(M,/mob/new_player)) + else if(isnewplayer(M)) M_job = "New player" else if(isobserver(M)) M_job = "Ghost" - M_job = replacetext(M_job, "'", "") - M_job = replacetext(M_job, "\"", "") - M_job = replacetext(M_job, "\\", "") + var/M_key = html_encode(M.key) + var/M_ip_address = isnull(M.lastKnownIP) ? "+localhost+" : M.lastKnownIP + var/M_name = html_encode(M.name) + var/M_rname = html_encode(M.real_name) + var/M_rname_as_key = html_encode(ckey(M.real_name)) // so you can ignore punctuation + if(M_rname == M_rname_as_key) + M_rname_as_key = null - var/M_name = M.name - M_name = replacetext(M_name, "'", "") - M_name = replacetext(M_name, "\"", "") - M_name = replacetext(M_name, "\\", "") - var/M_rname = M.real_name - M_rname = replacetext(M_rname, "'", "") - M_rname = replacetext(M_rname, "\"", "") - M_rname = replacetext(M_rname, "\\", "") - - var/M_key = M.key - M_key = replacetext(M_key, "'", "") - M_key = replacetext(M_key, "\"", "") - M_key = replacetext(M_key, "\\", "") + var/previous_names_string = "" //output for each mob dat += {" @@ -288,9 +280,19 @@ - [M_name] - [M_rname] - [M_key] ([M_job]) + [M_name] - [M_rname] - [M_key] ([M_job]) + + + + + + + + + +
@@ -315,73 +317,6 @@ usr << browse(dat, "window=players;size=600x480") -//The old one -/datum/admins/proc/player_panel_old() - if (!usr.client.holder) - return - - var/dat = "Player Menu" - dat += "" - //add to this if wanting to add back in IP checking - //add if you want to know their ip to the lists below - var/list/mobs = sortmobs() - - for(var/mob/M in mobs) - if(!M.ckey) continue - - dat += "" - if(isAI(M)) - dat += "" - else if(isrobot(M)) - dat += "" - else if(ishuman(M)) - dat += "" - else if(istype(M, /mob/living/silicon/pai)) - dat += "" - else if(istype(M, /mob/new_player)) - dat += "" - else if(isobserver(M)) - dat += "" - else if(issmall(M)) - dat += "" - else if(isalien(M)) - dat += "" - else - dat += "" - - - if(istype(M,/mob/living/carbon/human)) - var/mob/living/carbon/human/H = M - if(H.mind && H.mind.assigned_role) - dat += "" - else - dat += "" - - - dat += {" - - - "} - - if(usr.client) - switch(is_special_character(M)) - if(0) - dat += {""} - if(1) - dat += {""} - if(2) - dat += {""} - else - dat += {""} - - - - dat += "
NameReal NameAssigned JobKeyOptionsPMTraitor?
IP:(IP: [M.lastKnownIP])
[M.name]AICyborg[M.real_name]pAINew PlayerGhostMonkeyAlienUnknown[H.mind.assigned_role]NA[M.key ? (M.client ? M.key : "[M.key] (DC)") : "No key"]XPMTraitor?Traitor?Traitor? N/A
" - - usr << browse(dat, "window=players;size=640x480") - - - /datum/admins/proc/check_antagonists() if (SSticker && SSticker.current_state >= GAME_STATE_PLAYING) var/dat = "Round Status

Round Status

" diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 940978d0e116..aa5e7a39b511 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -1,17 +1,29 @@ +/datum/admins/proc/CheckAdminHref(href, href_list) + var/auth = href_list["admin_token"] + . = auth && (auth == href_token || auth == GLOB.href_token) + if(.) + return + var/msg = !auth ? "no" : "a bad" + message_admins("[key_name_admin(usr)] clicked an href with [msg] authorization key!") + if(CONFIG_GET(flag/debug_admin_hrefs)) + message_admins("Debug mode enabled, call not blocked. Please ask your coders to review this round's logs.") + log_world("UAH: [href]") + return TRUE + log_admin_private("[key_name(usr)] clicked an href with [msg] authorization key! [href]") + /datum/admins/Topic(href, href_list) ..() - if((usr.client != src.owner) || !check_rights(0)) - log_admin("[key_name(usr)] tried to use the admin panel without authorization.") + if(usr.client != src.owner || !check_rights(0)) message_admins("[usr.key] has attempted to override the admin panel!") + log_admin("[key_name(usr)] tried to use the admin panel without authorization.") return - if(SSticker.mode && SSticker.mode.check_antagonists_topic(href, href_list)) - check_antagonists() + if(!CheckAdminHref(href, href_list)) return if(href_list["ahelp"]) - if(!check_rights(R_ADMIN|R_MOD|R_DEBUG)) + if(!check_rights(R_ADMIN|R_MOD, TRUE)) return var/ahelp_ref = href_list["ahelp"] @@ -19,11 +31,15 @@ if(AH) AH.Action(href_list["ahelp_action"]) else - to_chat(usr, "Ticket [ahelp_ref] has been deleted!") + to_chat(usr, "Ticket [ahelp_ref] has been deleted!", confidential = TRUE) else if(href_list["ahelp_tickets"]) GLOB.ahelp_tickets.BrowseTickets(text2num(href_list["ahelp_tickets"])) + if(SSticker.mode && SSticker.mode.check_antagonists_topic(href, href_list)) + check_antagonists() + return + if(href_list["dbsearchckey"] || href_list["dbsearchadmin"]) var/adminckey = href_list["dbsearchadmin"] @@ -363,7 +379,7 @@ /////////////////////////////////////new ban stuff else if(href_list["jobban2"]) -// if(!check_rights(R_BAN)) return + // if(!check_rights(R_BAN)) return var/mob/M = locate(href_list["jobban2"]) if(!ismob(M)) @@ -388,7 +404,7 @@ -Nodrak ************************************WARNING!***********************************/ var/counter = 0 -//Regular jobs + //Regular jobs //Command (Blue) jobs += "" jobs += "" @@ -972,17 +988,9 @@ return else if(href_list["mute"]) - if(!check_rights(R_MOD,0) && !check_rights(R_ADMIN)) return - - var/mob/M = locate(href_list["mute"]) - if(!ismob(M)) return - if(!M.client) return - - var/mute_type = href_list["mute_type"] - if(istext(mute_type)) mute_type = text2num(mute_type) - if(!isnum(mute_type)) return - - cmd_admin_mute(M, mute_type) + if(!check_rights(R_MOD,0) && !check_rights(R_ADMIN)) + return + cmd_admin_mute(href_list["mute"], text2num(href_list["mute_type"])) else if(href_list["c_mode"]) if(!check_rights(R_ADMIN)) return diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm index 3e07dc740a7b..4fc4b23f6d53 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -1,15 +1,19 @@ -/client/var/adminhelptimerid = 0 //a timer id for returning the ahelp verb -/client/var/datum/admin_help/current_ticket //the current ticket the (usually) not-admin client is dealing with - -// -//TICKET MANAGER -// +/// Client var used for returning the ahelp verb +/client/var/adminhelptimerid = 0 +/// Client var used for tracking the ticket the (usually) not-admin client is dealing with +/client/var/datum/admin_help/current_ticket GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) +/** + * # Adminhelp Ticket Manager + */ /datum/admin_help_tickets + /// The set of all active tickets var/list/active_tickets = list() + /// The set of all closed tickets var/list/closed_tickets = list() + /// The set of all resolved tickets var/list/resolved_tickets = list() var/obj/effect/statclick/ticket_list/astatclick = new(null, null, AHELP_ACTIVE) @@ -79,11 +83,11 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) title = "Resolved Tickets" if(!l2b) return - var/list/dat = list("[title]") - dat += "Refresh

" + var/list/dat = list("[title]") + dat += "Refresh

" for(var/I in l2b) var/datum/admin_help/AH = I - dat += "Ticket #[AH.id]: [AH.initiator_key_name]: [AH.name]
" + dat += "[SPAN_ADMINNOTICE("[SPAN_ADMINHELP("Ticket #[AH.id]")]: [AH.initiator_key_name]: [AH.name]")]
" usr << browse(dat.Join(), "window=ahelp_list[state];size=600x480") @@ -91,31 +95,31 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) /datum/admin_help_tickets/proc/stat_data() . = list() var/num_disconnected = 0 - STATPANEL_DATA_CLICK("Active Tickets:", "[active_tickets.len]", "\ref[astatclick]") + STATPANEL_DATA_CLICK("Active Tickets:", "[active_tickets.len]", REF(astatclick)) + astatclick.update("[active_tickets.len]") for(var/I in active_tickets) var/datum/admin_help/AH = I if(AH.initiator) - STATPANEL_DATA_CLICK("#[AH.id]. [AH.initiator_key_name]:", "[AH.statclick.update()]", "\ref[AH.statclick]") - else + STATPANEL_DATA_CLICK("#[AH.id]. [AH.initiator_key_name]:", "[AH.statclick.update()]", REF(AH)) ++num_disconnected if(num_disconnected) - STATPANEL_DATA_CLICK("Disconnected:", "[num_disconnected]", "\ref[astatclick]") - STATPANEL_DATA_CLICK("Closed Tickets:", "[closed_tickets.len]", "\ref[cstatclick]") - STATPANEL_DATA_CLICK("Resolved Tickets:", "[resolved_tickets.len]", "\ref[rstatclick]") + STATPANEL_DATA_CLICK("Disconnected:", "[num_disconnected]", REF(astatclick)) + STATPANEL_DATA_CLICK("Closed Tickets:", "[closed_tickets.len]", REF(cstatclick)) + STATPANEL_DATA_CLICK("Resolved Tickets:", "[resolved_tickets.len]", REF(rstatclick)) //Reassociate still open ticket if one exists /datum/admin_help_tickets/proc/ClientLogin(client/C) C.current_ticket = CKey2ActiveTicket(C.ckey) if(C.current_ticket) - C.current_ticket.AddInteraction("Client reconnected.") C.current_ticket.initiator = C + C.current_ticket.AddInteraction("Client reconnected.") //Dissasociate ticket /datum/admin_help_tickets/proc/ClientLogout(client/C) if(C.current_ticket) - C.current_ticket.AddInteraction("Client disconnected.") - C.current_ticket.initiator = null - C.current_ticket = null + var/datum/admin_help/T = C.current_ticket + T.AddInteraction("Client disconnected.") + T.initiator = null //Get a ticket given a ckey /datum/admin_help_tickets/proc/CKey2ActiveTicket(ckey) @@ -128,45 +132,74 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) //TICKET LIST STATCLICK // +INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) /obj/effect/statclick/ticket_list var/current_state -INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) /obj/effect/statclick/ticket_list/Initialize(mapload, name, state) . = ..() current_state = state /obj/effect/statclick/ticket_list/Click() + if (!usr.client?.holder) + message_admins("[key_name_admin(usr)] non-holder clicked on a ticket list statclick! ([src])") + return + GLOB.ahelp_tickets.BrowseTickets(current_state) -// -//TICKET DATUM -// +#define WEBHOOK_NONE 0 +#define WEBHOOK_URGENT 1 +#define WEBHOOK_NON_URGENT 2 +/** + * # Adminhelp Ticket + */ /datum/admin_help + /// Unique ID of the ticket var/id + /// The current name of the ticket var/name + /// The current state of the ticket var/state = AHELP_ACTIVE - + /// The time at which the ticket was opened var/opened_at + /// The time at which the ticket was closed var/closed_at - - var/client/initiator //semi-misnomer, it's the person who ahelped/was bwoinked + /// Semi-misnomer, it's the person who ahelped/was bwoinked + var/client/initiator + /// The ckey of the initiator var/initiator_ckey + /// The key name of the initiator var/initiator_key_name - - var/list/_interactions //use AddInteraction() or, preferably, admin_ticket_log() - + /// If any admins were online when the ticket was initialized + var/heard_by_no_admins = FALSE + /// The collection of interactions with this ticket. Use AddInteraction() or, preferably, admin_ticket_log() + var/list/ticket_interactions + /// Statclick holder for the ticket var/obj/effect/statclick/ahelp/statclick - + /// Static counter used for generating each ticket ID var/static/ticket_counter = 0 - -//call this on its own to create a ticket, don't manually assign current_ticket -//msg is the title of the ticket: usually the ahelp text -//is_bwoink is TRUE if this ticket was started by an admin PM -/datum/admin_help/New(msg, client/C, is_bwoink) + /// The list of clients currently responding to the opening ticket before it gets a response + var/list/opening_responders + /// Whether this ahelp has sent a webhook or not, and what type + var/webhook_sent = WEBHOOK_NONE + /// List of player interactions + var/list/player_interactions + /// List of admin ckeys that are involved, like through responding + var/list/admins_involved = list() + /// Has the player replied to this ticket yet? + var/player_replied = FALSE + +/** + * Call this on its own to create a ticket, don't manually assign current_ticket + * + * Arguments: + * * msg_raw - The first message of this admin_help: used for the initial title of the ticket + * * is_bwoink - Boolean operator, TRUE if this ticket was started by an admin PM + */ +/datum/admin_help/New(msg_raw, client/C, is_bwoink, urgent = FALSE) //clean the input msg - msg = sanitize(copytext(msg,1,MAX_MESSAGE_LEN)) + var/msg = sanitize(copytext_char(msg_raw, 1, MAX_MESSAGE_LEN)) if(!msg || !C || !C.mob) qdel(src) return @@ -174,50 +207,151 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) id = ++ticket_counter opened_at = world.time - name = msg + name = copytext_char(msg, 1, 100) initiator = C initiator_ckey = initiator.ckey initiator_key_name = key_name(initiator, FALSE, TRUE) - if(initiator.current_ticket) //This is a bug - log_debug(SPAN_DEBUG("Multiple ahelp current_tickets")) + if(initiator.current_ticket) //This is a bug + stack_trace("Multiple ahelp current_tickets") initiator.current_ticket.AddInteraction("Ticket erroneously left open by code") initiator.current_ticket.Close() initiator.current_ticket = src - var/parsed_message = keywords_lookup(msg) + TimeoutVerb() statclick = new(null, src) - _interactions = list() + ticket_interactions = list() + player_interactions = list() if(is_bwoink) - AddInteraction("[key_name_admin(usr)] PM'd [LinkedReplyName()]") - message_admins("Ticket [TicketHref("#[id]")] created") + AddInteraction("[key_name_admin(usr)] PM'd [LinkedReplyName()]", player_message = "[key_name_admin(usr, include_name = FALSE)] PM'd [LinkedReplyName()]") + message_admins("Ticket [TicketHref("#[id]")] created") else - MessageNoRecipient(parsed_message) - - //show it to the person adminhelping too - to_chat(C, "PM to-Admins: [name]") - - //send it to irc if nobody is on and tell us how many were on - var/admin_number_present = send2irc_adminless_only(initiator_ckey, name) - log_admin("Ticket #[id]: [key_name(initiator)]: [name] - heard by [admin_number_present] non-AFK admins who have +BAN.") - if(admin_number_present <= 0) - to_chat(C, "No active admins are online, your adminhelp was sent to the admin irc.") - send2adminchat() + MessageNoRecipient(msg_raw, urgent) + send_message_to_tgs(msg, urgent) GLOB.ahelp_tickets.active_tickets += src +/datum/admin_help/proc/format_embed_discord(message) + var/datum/discord_embed/embed = new() + embed.title = "Ticket #[id]" + embed.description = "" + embed.author = key_name(initiator_ckey) + var/round_state + switch(SSticker.current_state) + if(GAME_STATE_INIT, GAME_STATE_PREGAME, GAME_STATE_SETTING_UP) + round_state = "Round has not started" + if(GAME_STATE_PLAYING) + round_state = "Round is ongoing." + if(GAME_STATE_FINISHED) + round_state = "Round has ended" + var/list/admin_counts = get_admin_counts(R_BAN) + var/stealth_admins = jointext(admin_counts["stealth"], ", ") + var/afk_admins = jointext(admin_counts["afk"], ", ") + var/other_admins = jointext(admin_counts["noflags"], ", ") + var/admin_text = "" + // we cant get an accurate list of living players, living_mob_list contains living mobs, not alive mobs with client + var/player_count = "**Total**: [length(GLOB.clients)]" + // , **Living**: [length(GLOB.alive_player_list)], **Dead**: [length(GLOB.dead_player_list)], **Observers**: [length(GLOB.current_observers_list)] + if(stealth_admins) + admin_text += "**Stealthed**: [stealth_admins]\n" + if(afk_admins) + admin_text += "**AFK**: [afk_admins]\n" + if(other_admins) + admin_text += "**Lacks +BAN**: [other_admins]\n" + embed.fields = list( + "CKEY" = initiator_ckey, + "PLAYERS" = player_count, + "ROUND STATE" = round_state, + "ROUND ID" = GLOB.round_id, + "ROUND TIME" = ROUND_TIME(), + "MESSAGE" = message, + "ADMINS" = admin_text, + ) + if(CONFIG_GET(string/adminhelp_ahelp_link)) + var/ahelp_link = replacetext(CONFIG_GET(string/adminhelp_ahelp_link), "$RID", GLOB.round_id) + ahelp_link = replacetext(ahelp_link, "$TID", id) + embed.url = ahelp_link + return embed + +/datum/admin_help/proc/send_message_to_tgs(message, urgent = FALSE) + var/message_to_send = message + + if(urgent) + var/extra_message = CONFIG_GET(string/urgent_ahelp_message) + to_chat(initiator, SPAN_BOLDWARNING("Notified admins to prioritize your ticket")) + var/datum/discord_embed/embed = format_embed_discord(message) + embed.content = extra_message + embed.footer = "This player requested an admin" + send2adminchat_webhook(embed, urgent = TRUE) + webhook_sent = WEBHOOK_URGENT + //send it to TGS if nobody is on and tell us how many were on + var/admin_number_present = send2tgs_adminless_only(initiator_ckey, "Ticket #[id]: [message_to_send]") + log_admin_private("Ticket #[id]: [key_name(initiator)]: [name] - heard by [admin_number_present] non-AFK admins who have +BAN.") + if(admin_number_present <= 0) + to_chat(initiator, SPAN_NOTICE("No active admins are online, your adminhelp was sent to admins who are available through IRC or Discord."), confidential = TRUE) + heard_by_no_admins = TRUE + var/regular_webhook_url = CONFIG_GET(string/regular_adminhelp_webhook_url) + if(regular_webhook_url && (!urgent || regular_webhook_url != CONFIG_GET(string/urgent_adminhelp_webhook_url))) + var/extra_message = CONFIG_GET(string/ahelp_message) + var/datum/discord_embed/embed = format_embed_discord(message) + embed.content = extra_message + embed.footer = "This player sent an ahelp when no admins are available [urgent? "and also requested an admin": ""]" + send2adminchat_webhook(embed, urgent = FALSE) + webhook_sent = WEBHOOK_NON_URGENT + +/proc/send2adminchat_webhook(message_or_embed, urgent) + var/webhook = CONFIG_GET(string/urgent_adminhelp_webhook_url) + if(!urgent) + webhook = CONFIG_GET(string/regular_adminhelp_webhook_url) + + if(!webhook) + return + var/list/webhook_info = list() + if(istext(message_or_embed)) + var/message_content = replacetext(replacetext(message_or_embed, "\proper", ""), "\improper", "") + message_content = GLOB.has_discord_embeddable_links.Replace(replacetext(message_content, "`", ""), " ```$1``` ") + webhook_info["content"] = message_content + else + var/datum/discord_embed/embed = message_or_embed + webhook_info["embeds"] = list(embed.convert_to_list()) + if(embed.content) + webhook_info["content"] = embed.content + if(CONFIG_GET(string/adminhelp_webhook_name)) + webhook_info["username"] = CONFIG_GET(string/adminhelp_webhook_name) + if(CONFIG_GET(string/adminhelp_webhook_pfp)) + webhook_info["avatar_url"] = CONFIG_GET(string/adminhelp_webhook_pfp) + var/list/headers = list() + headers["Content-Type"] = "application/json" + var/datum/http_request/request = new() + request.prepare(RUSTG_HTTP_METHOD_POST, webhook, json_encode(webhook_info), headers, "tmp/response.json") + request.begin_async() + /datum/admin_help/Destroy() RemoveActive() GLOB.ahelp_tickets.closed_tickets -= src GLOB.ahelp_tickets.resolved_tickets -= src return ..() -/datum/admin_help/proc/AddInteraction(formatted_message) - _interactions += "[gameTimestamp()]: [formatted_message]" +/datum/admin_help/proc/AddInteraction(formatted_message, player_message) + if (!isnull(usr) && usr.ckey != initiator_ckey) + admins_involved |= usr.ckey + if(heard_by_no_admins) + heard_by_no_admins = FALSE + send2adminchat(initiator_ckey, "Ticket #[id]: Answered by [key_name(usr)]") + + ticket_interactions += "[time_stamp()]: [formatted_message]" + if (!isnull(player_message)) + player_interactions += "[time_stamp()]: [player_message]" + +//Removes the ahelp verb and returns it after 2 minutes +/datum/admin_help/proc/TimeoutVerb() + remove_verb(initiator, /client/verb/adminhelp) + initiator.adminhelptimerid = addtimer(CALLBACK(initiator, TYPE_PROC_REF(/client, giveadminhelpverb)), 1200, TIMER_STOPPABLE) //2 minute cooldown of admin helps //private /datum/admin_help/proc/FullMonty(ref_src) + PRIVATE_PROC(TRUE) if(!ref_src) ref_src = "[REF(src)]" . = ADMIN_FULLMONTY_NONAME(initiator.mob) @@ -226,46 +360,67 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) //private /datum/admin_help/proc/ClosureLinks(ref_src) + PRIVATE_PROC(TRUE) if(!ref_src) ref_src = "[REF(src)]" . = ADMIN_AHELP_FULLMONTY(ref_src) //private /datum/admin_help/proc/LinkedReplyName(ref_src) + PRIVATE_PROC(TRUE) if(!ref_src) - ref_src = "\ref[src]" - return "[initiator_key_name]" + ref_src = "[REF(src)]" + return "[initiator_key_name]" //private /datum/admin_help/proc/TicketHref(msg, ref_src, action = "ticket") + PRIVATE_PROC(TRUE) if(!ref_src) - ref_src = "\ref[src]" - return "[msg]" + ref_src = "[REF(src)]" + return "[msg]" //message from the initiator without a target, all admins will see this -//won't bug irc -/datum/admin_help/proc/MessageNoRecipient(msg) - var/ref_src = "\ref[src]" - var/chat_msg = SPAN_ADMINNOTICE("Ticket [TicketHref(SPAN_TOOLTIP("Open the ticket in a new window.","#[id]"), ref_src)]: [SPAN_TOOLTIP("Open a prompt to reply to this ticket.","[LinkedReplyName(ref_src)]")] [FullMonty(ref_src)]: [msg]") +//won't bug irc/discord +/datum/admin_help/proc/MessageNoRecipient(msg, urgent = FALSE) + msg = sanitize(copytext_char(msg, 1, MAX_MESSAGE_LEN)) + var/ref_src = "[REF(src)]" + //Message to be sent to all admins + var/admin_msg = SPAN_ADMINNOTICE(SPAN_ADMINHELP("Ticket [TicketHref(SPAN_TOOLTIP("Open the ticket in a new window.", "#[id]"), ref_src)]: [LinkedReplyName(ref_src)] [FullMonty(ref_src)]: [SPAN_LINKIFY(keywords_lookup(msg))]")) - AddInteraction(SPAN_DANGER("[SPAN_TOOLTIP("Open a prompt to reply to this ticket.","[LinkedReplyName(ref_src)]")]: [msg]")) - //send this msg to all admins + AddInteraction("[LinkedReplyName(ref_src)]: [msg]", player_message = "[LinkedReplyName(ref_src)]: [msg]") + log_admin_private("Ticket #[id]: [key_name(initiator)]: [msg]") + //send this msg to all admins for(var/client/X in GLOB.admins) - // if(X.get_preference_toggle(/datum/client_preference/holder/play_adminhelp_ping)) - // SEND_SOUND(X, sound('sound/effects/adminhelp.ogg')) SEND_SOUND(X, sound('sound/effects/adminhelp.ogg')) window_flash(X) - to_chat(X, chat_msg) + to_chat(X, + type = MESSAGE_TYPE_ADMINPM, + html = admin_msg, + confidential = TRUE) + + //show it to the person adminhelping too + reply_to_admins_notification(msg) + +/// Sends a message to the player that they are replying to admins. +/datum/admin_help/proc/reply_to_admins_notification(message) + to_chat( + initiator, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_NOTICE("PM to-Admins: [SPAN_LINKIFY(message)]"), + confidential = TRUE, + ) + + player_replied = TRUE //Reopen a closed ticket /datum/admin_help/proc/Reopen() if(state == AHELP_ACTIVE) - to_chat(usr, "This ticket is already open.") + to_chat(usr, SPAN_WARNING("This ticket is already open."), confidential = TRUE) return if(GLOB.ahelp_tickets.CKey2ActiveTicket(initiator_ckey)) - to_chat(usr, "This user already has an active ticket, cannot reopen this one.") + to_chat(usr, SPAN_WARNING("This user already has an active ticket, cannot reopen this one."), confidential = TRUE) return statclick = new(null, src) @@ -282,12 +437,11 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) if(initiator) initiator.current_ticket = src - AddInteraction("Reopened by [key_name_admin(usr)]") - if(initiator) - to_chat(initiator, "Ticket [TicketHref("#[id]")] was reopened by [key_name(usr,FALSE,FALSE)].") - var/msg = "Ticket [TicketHref("#[id]")] reopened by [key_name_admin(usr)]." + AddInteraction("Reopened by [key_name_admin(usr)]", player_message = "Ticket reopened!") + var/msg = SPAN_ADMINHELP("Ticket [TicketHref("#[id]")] reopened by [key_name_admin(usr)].") message_admins(msg) - log_admin(msg) + log_admin_private(msg) + feedback_inc("ahelp_reopen") TicketPanel() //can only be done from here, so refresh it @@ -302,37 +456,36 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) initiator.current_ticket = null //Mark open ticket as closed/meme -/datum/admin_help/proc/Close(silent = FALSE) +/datum/admin_help/proc/Close(key_name = key_name_admin(usr), silent = FALSE) if(state != AHELP_ACTIVE) return RemoveActive() state = AHELP_CLOSED GLOB.ahelp_tickets.ListInsert(src) - AddInteraction("Closed by [key_name_admin(usr)].") - if(initiator) - to_chat(initiator, "Ticket [TicketHref("#[id]")] was closed by [key_name(usr,FALSE,FALSE)].") + AddInteraction("Closed by [key_name].", player_message = "Ticket closed!") if(!silent) feedback_inc("ahelp_close") - var/msg = "Ticket [TicketHref("#[id]")] closed by [key_name_admin(usr)]." + var/msg = "Ticket [TicketHref("#[id]")] closed by [key_name]." message_admins(msg) - log_admin(msg) + log_admin_private(msg) //Mark open ticket as resolved/legitimate, returns ahelp verb -/datum/admin_help/proc/Resolve(silent = FALSE) +/datum/admin_help/proc/Resolve(key_name = key_name_admin(usr), silent = FALSE) if(state != AHELP_ACTIVE) return RemoveActive() state = AHELP_RESOLVED GLOB.ahelp_tickets.ListInsert(src) - AddInteraction("Resolved by [key_name_admin(usr)].") - if(initiator) - to_chat(initiator, "Ticket [TicketHref("#[id]")] was marked resolved by [key_name(usr,FALSE,FALSE)].") + addtimer(CALLBACK(initiator, TYPE_PROC_REF(/client, giveadminhelpverb)), 5 SECONDS) + + AddInteraction("Resolved by [key_name].", player_message = "Ticket resolved!") + to_chat(initiator, SPAN_ADMINHELP("Your ticket has been resolved by an admin. The Adminhelp verb will be returned to you shortly."), confidential = TRUE) if(!silent) feedback_inc("ahelp_resolve") - var/msg = "Ticket [TicketHref("#[id]")] resolved by [key_name_admin(usr)]" + var/msg = "Ticket [TicketHref("#[id]")] resolved by [key_name]" message_admins(msg) - log_admin(msg) + log_admin_private(msg) //Close and return ahelp verb, use if ticket is incoherent /datum/admin_help/proc/Reject(key_name = key_name_admin(usr)) @@ -340,19 +493,19 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) return if(initiator) - // if(initiator.get_preference_toggle(/datum/client_preference/holder/play_adminhelp_ping)) - // SEND_SOUND(initiator, sound('sound/effects/adminhelp.ogg')) + initiator.giveadminhelpverb() + SEND_SOUND(initiator, sound('sound/effects/adminhelp.ogg')) - to_chat(initiator, "- AdminHelp Rejected! -") - to_chat(initiator, "Your admin help was rejected.") - to_chat(initiator, "Please try to be calm, clear, and descriptive in admin helps, do not assume the admin has seen any related events, and clearly state the names of anybody you are reporting.") + to_chat(initiator, "- AdminHelp Rejected! -", confidential = TRUE) + to_chat(initiator, "Your admin help was rejected. The adminhelp verb has been returned to you so that you may try again.", confidential = TRUE) + to_chat(initiator, "Please try to be calm, clear, and descriptive in admin helps, do not assume the admin has seen any related events, and clearly state the names of anybody you are reporting.", confidential = TRUE) feedback_inc("ahelp_reject") - var/msg = "Ticket [TicketHref("#[id]")] rejected by [key_name_admin(usr)]" + var/msg = "Ticket [TicketHref("#[id]")] rejected by [key_name]" message_admins(msg) - log_admin(msg) - AddInteraction("Rejected by [key_name_admin(usr)].") + log_admin_private(msg) + AddInteraction("Rejected by [key_name].", player_message = "Ticket rejected!") Close(silent = TRUE) //Resolve ticket with IC Issue message @@ -361,17 +514,16 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) return var/msg = "- AdminHelp marked as IC issue! -
" - msg += "This is something that can be solved ICly, and does not currently require staff intervention.
" - msg += "Your AdminHelp may also be unanswerable due to ongoing events." + msg += "Your issue has been determined by an administrator to be an in character issue and does NOT require administrator intervention at this time. For further resolution you should pursue options that are in character." if(initiator) - to_chat(initiator, msg) + to_chat(initiator, msg, confidential = TRUE) feedback_inc("ahelp_icissue") - msg = "Ticket [TicketHref("#[id]")] marked as IC by [key_name_admin(usr)]" + msg = "Ticket [TicketHref("#[id]")] marked as IC by [key_name]" message_admins(msg) - log_admin(msg) - AddInteraction("Marked as IC issue by [key_name_admin(usr)]") + log_admin_private(msg) + AddInteraction("Marked as IC issue by [key_name]", player_message = "Marked as IC issue!") Resolve(silent = TRUE) //Resolve ticket with IC Issue message @@ -392,35 +544,54 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) //Show the ticket panel /datum/admin_help/proc/TicketPanel() - var/list/dat = list("Ticket #[id]") - var/ref_src = "\ref[src]" + var/list/dat = list("Ticket #[id]") + var/ref_src = "[REF(src)]" dat += "

Admin Help Ticket #[id]: [LinkedReplyName(ref_src)]

" - dat += "State: " - switch(state) - if(AHELP_ACTIVE) - dat += "OPEN" - if(AHELP_RESOLVED) - dat += "RESOLVED" - if(AHELP_CLOSED) - dat += "CLOSED" - else - dat += "UNKNOWN" - dat += "[FOURSPACES][TicketHref("Refresh", ref_src)][FOURSPACES][TicketHref("Re-Title", ref_src, "retitle")]" + dat += "State: [ticket_status()]" + dat += "[FOURSPACES][TicketHref("Refresh", ref_src)][FOURSPACES][TicketHref("Re-Title", ref_src, "retitle")]" if(state != AHELP_ACTIVE) dat += "[FOURSPACES][TicketHref("Reopen", ref_src, "reopen")]" - dat += "

Opened at: [gameTimestamp(wtime = opened_at)] (Approx [(world.time - opened_at) / 600] minutes ago)" + dat += "

Opened at: [gameTimestamp(wtime = opened_at)] (Approx [DisplayTimeText(world.time - opened_at)] ago)" if(closed_at) - dat += "
Closed at: [gameTimestamp(wtime = closed_at)] (Approx [(world.time - closed_at) / 600] minutes ago)" + dat += "
Closed at: [gameTimestamp(wtime = closed_at)] (Approx [DisplayTimeText(world.time - closed_at)] ago)" dat += "

" if(initiator) dat += "Actions: [FullMonty(ref_src)]
" else dat += "DISCONNECTED[FOURSPACES][ClosureLinks(ref_src)]
" dat += "
Log:

" - for(var/I in _interactions) + for(var/I in ticket_interactions) dat += "[I]
" - usr << browse(dat.Join(), "window=ahelp[id];size=620x480") + // Helper for opening directly to player ticket history + dat += "

Player Ticket History:" + dat += "[FOURSPACES]Open" + + // Append any tickets also opened by this user if relevant + var/list/related_tickets = GLOB.ahelp_tickets.TicketsByCKey(initiator_ckey) + if (related_tickets.len > 1) + dat += "
Other Tickets by User
" + for (var/datum/admin_help/related_ticket in related_tickets) + if (related_ticket.id == id) + continue + dat += "[related_ticket.TicketHref("#[related_ticket.id]")] ([related_ticket.ticket_status()]): [related_ticket.name]
" + + usr << browse(dat.Join(), "window=ahelp[id];size=750x480") + +/** + * Renders the current status of the ticket into a displayable string + */ +/datum/admin_help/proc/ticket_status() + switch(state) + if(AHELP_ACTIVE) + return "OPEN" + if(AHELP_RESOLVED) + return "RESOLVED" + if(AHELP_CLOSED) + return "CLOSED" + else + stack_trace("Invalid ticket state: [state]") + return "INVALID, CALL A CODER" /datum/admin_help/proc/Retitle() var/new_title = input(usr, "Enter a title for the ticket", "Rename Ticket", name) as text|null @@ -429,12 +600,25 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) //not saying the original name cause it could be a long ass message var/msg = "Ticket [TicketHref("#[id]")] titled [name] by [key_name_admin(usr)]" message_admins(msg) - log_admin(msg) - TicketPanel() //we have to be here to do this + log_admin_private(msg) + TicketPanel() //we have to be here to do this //Forwarded action from admin/Topic /datum/admin_help/proc/Action(action) testing("Ahelp action: [action]") + if(webhook_sent != WEBHOOK_NONE) + var/datum/discord_embed/embed = new() + embed.title = "Ticket #[id]" + if(CONFIG_GET(string/adminhelp_ahelp_link)) + var/ahelp_link = replacetext(CONFIG_GET(string/adminhelp_ahelp_link), "$RID", GLOB.round_id) + ahelp_link = replacetext(ahelp_link, "$TID", id) + embed.url = ahelp_link + embed.description = "[key_name(usr)] has sent an action to this ticket. Action ID: [action]" + if(webhook_sent == WEBHOOK_URGENT) + send2adminchat_webhook(embed, urgent = TRUE) + if(webhook_sent == WEBHOOK_NON_URGENT || CONFIG_GET(string/regular_adminhelp_webhook_url) != CONFIG_GET(string/urgent_adminhelp_webhook_url)) + send2adminchat_webhook(embed, urgent = FALSE) + webhook_sent = WEBHOOK_NONE switch(action) if("ticket") TicketPanel() @@ -455,6 +639,32 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) if("reopen") Reopen() +/datum/admin_help/proc/player_ticket_panel() + var/list/dat = list("Player Ticket") + dat += "State: " + switch(state) + if(AHELP_ACTIVE) + dat += "OPEN" + if(AHELP_RESOLVED) + dat += "RESOLVED" + if(AHELP_CLOSED) + dat += "CLOSED" + else + dat += "UNKNOWN" + dat += "\n[FOURSPACES]Refresh" + dat += "

Opened at: [gameTimestamp("hh:mm:ss", opened_at)] (Approx [DisplayTimeText(world.time - opened_at)] ago)" + if(closed_at) + dat += "
Closed at: [gameTimestamp("hh:mm:ss", closed_at)] (Approx [DisplayTimeText(world.time - closed_at)] ago)" + dat += "

" + dat += "
Log:

" + for (var/interaction in player_interactions) + dat += "[interaction]
" + + var/datum/browser/player_panel = new(usr, "ahelp[id]", 0, 620, 480) + player_panel.set_content(dat.Join()) + player_panel.open() + + // // TICKET STATCLICK // @@ -465,12 +675,16 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ticket_list) INITIALIZE_IMMEDIATE(/obj/effect/statclick/ahelp) /obj/effect/statclick/ahelp/Initialize(mapload, datum/admin_help/AH) ahelp_datum = AH - return ..(mapload) + . = ..() /obj/effect/statclick/ahelp/update() return ..(ahelp_datum.name) /obj/effect/statclick/ahelp/Click() + if (!usr.client?.holder) + message_admins("[key_name_admin(usr)] non-holder clicked on an ahelp statclick! ([src])") + return + ahelp_datum.TicketPanel() /obj/effect/statclick/ahelp/Destroy() @@ -486,94 +700,158 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ahelp) deltimer(adminhelptimerid) adminhelptimerid = 0 -// Used for methods where input via arg doesn't work -/client/proc/get_adminhelp() - var/msg = input(src, "Please describe your problem concisely and an admin will help as soon as they're able.", "Adminhelp contents") as message|null - adminhelp(msg) +GLOBAL_DATUM_INIT(admin_help_ui_handler, /datum/admin_help_ui_handler, new) -/client/verb/adminhelp(msg as text) - set category = "Admin" - set name = "Adminhelp" +/datum/admin_help_ui_handler + var/list/ahelp_cooldowns = list() - //handle muting and automuting - if(prefs.muted & MUTE_ADMINHELP) - to_chat(src, "Error: Admin-PM: You cannot send adminhelps (Muted).") +/datum/admin_help_ui_handler/ui_state(mob/user) + return GLOB.always_state + +/datum/admin_help_ui_handler/ui_data(mob/user) + . = list() + var/list/admins = get_admin_counts(R_BAN) + .["adminCount"] = length(admins["present"]) + +/datum/admin_help_ui_handler/ui_static_data(mob/user) + . = list() + .["bannedFromUrgentAhelp"] = FALSE + .["urgentAhelpPromptMessage"] = CONFIG_GET(string/urgent_ahelp_user_prompt) + var/webhook_url = CONFIG_GET(string/urgent_adminhelp_webhook_url) + if(webhook_url) + .["urgentAhelpEnabled"] = TRUE + +/datum/admin_help_ui_handler/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Adminhelp") + ui.open() + ui.set_autoupdate(FALSE) + +/datum/admin_help_ui_handler/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) return - if(handle_spam_prevention(msg,MUTE_ADMINHELP)) + var/client/user_client = usr.client + var/message = sanitize_text(trim(params["message"])) + var/urgent = !!params["urgent"] + var/list/admins = get_admin_counts(R_BAN) + if(length(admins["present"]) != 0) + urgent = FALSE + + if(user_client.adminhelptimerid) return - if(!msg) - return + perform_adminhelp(user_client, message, urgent) + ui.close() - msg = sanitize(msg) +/datum/admin_help_ui_handler/proc/perform_adminhelp(client/user_client, message, urgent) + if(!message) + return - //remove out adminhelp verb temporarily to prevent spamming of admins. - remove_verb(src, /client/verb/adminhelp) - adminhelptimerid = addtimer(CALLBACK(src, PROC_REF(giveadminhelpverb)), 2 MINUTES, flags = TIMER_STOPPABLE) + //handle muting and automuting + if(user_client.prefs.muted & MUTE_ADMINHELP) + to_chat(user_client, SPAN_DANGER("Error: Admin-PM: You cannot send adminhelps (Muted)."), confidential = TRUE) + return + if(user_client.handle_spam_prevention(message, MUTE_ADMINHELP)) + return - if(persistent.ligma) - to_chat(usr, "PM to-Admins: [msg]") - log_shadowban("[key_name(src)] AHELP: [msg]") + if (user_client.persistent.ligma) return feedback_add_details("admin_verb","Adminhelp") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - if(current_ticket) - if(alert(usr, "You already have a ticket open. Is this for the same issue?",,"Yes","No") != "No") - if(current_ticket) - current_ticket.MessageNoRecipient(msg) - to_chat(usr, "PM to-Admins: [msg]") - return - else - to_chat(usr, "Ticket not found, creating new one...") + + if(urgent) + if(!COOLDOWN_FINISHED_A(src, ahelp_cooldowns?[user_client.ckey])) + urgent = FALSE // Prevent abuse else - current_ticket.AddInteraction("[key_name_admin(usr)] opened a new ticket.") - current_ticket.Close() + COOLDOWN_START_A(src, ahelp_cooldowns[user_client.ckey], CONFIG_GET(number/urgent_ahelp_cooldown) * (1 SECONDS)) + + if(user_client.current_ticket) + user_client.current_ticket.TimeoutVerb() + if(urgent) + var/sanitized_message = sanitize(copytext_char(message, 1, MAX_MESSAGE_LEN)) + user_client.current_ticket.send_message_to_tgs(sanitized_message, urgent = TRUE) + user_client.current_ticket.MessageNoRecipient(message, urgent) + return - new /datum/admin_help(msg, src, FALSE) + new /datum/admin_help(message, user_client, FALSE, urgent) -//admin proc -/client/proc/cmd_admin_ticket_panel() - set name = "Show Ticket List" - set category = "Admin" +/client/verb/no_tgui_adminhelp(message as message) + set name = "NoTguiAdminhelp" + set hidden = TRUE - if(!check_rights(R_ADMIN|R_MOD|R_DEBUG, TRUE)) + if(adminhelptimerid) return - var/browse_to + message = trim(message) - switch(input("Display which ticket list?") as null|anything in list("Active Tickets", "Closed Tickets", "Resolved Tickets")) - if("Active Tickets") - browse_to = AHELP_ACTIVE - if("Closed Tickets") - browse_to = AHELP_CLOSED - if("Resolved Tickets") - browse_to = AHELP_RESOLVED - else + GLOB.admin_help_ui_handler.perform_adminhelp(src, message, FALSE) + +/client/verb/adminhelp() + set category = "Admin" + set name = "Adminhelp" + GLOB.admin_help_ui_handler.ui_interact(mob) + to_chat(src, SPAN_BOLDNOTICE("Adminhelp failing to open or work? Click here")) + +/client/verb/view_latest_ticket() + set category = "Admin" + set name = "View Latest Ticket" + + if(!current_ticket) + // Check if the client had previous tickets, and show the latest one + var/list/prev_tickets = list() + var/datum/admin_help/last_ticket + // Check all resolved tickets for this player + for(var/datum/admin_help/resolved_ticket in GLOB.ahelp_tickets.resolved_tickets) + if(resolved_ticket.initiator_ckey == ckey) // Initiator is a misnomer, it's always the non-admin player even if an admin bwoinks first + prev_tickets += resolved_ticket + // Check all closed tickets for this player + for(var/datum/admin_help/closed_ticket in GLOB.ahelp_tickets.closed_tickets) + if(closed_ticket.initiator_ckey == ckey) + prev_tickets += closed_ticket + // Take the most recent entry of prev_tickets and open the panel on it + if(LAZYLEN(prev_tickets)) + last_ticket = pop(prev_tickets) + last_ticket.player_ticket_panel() return - GLOB.ahelp_tickets.BrowseTickets(browse_to) + // client had no tickets this round + to_chat(src, SPAN_WARNING("You have not had an ahelp ticket this round.")) + return + + current_ticket.player_ticket_panel() // // LOGGING // -//Use this proc when an admin takes action that may be related to an open ticket on what -//what can be a client, ckey, or mob -/proc/admin_ticket_log(what, message) - var/client/C +/// Use this proc when an admin takes action that may be related to an open ticket on what +/// what can be a client, ckey, or mob +/// player_message: If the message should be shown in the player ticket panel, fill this out +/// log_in_blackbox: Whether or not this message with the blackbox system. +/// If disabled, this message should be logged with a different proc call +/proc/admin_ticket_log(what, message, player_message, log_in_blackbox = TRUE) + var/client/mob_client var/mob/Mob = what if(istype(Mob)) - C = Mob.client + mob_client = Mob.client else - C = what - if(istype(C) && C.current_ticket) - C.current_ticket.AddInteraction(message) - return C.current_ticket - if(istext(what)) //ckey - var/datum/admin_help/AH = GLOB.ahelp_tickets.CKey2ActiveTicket(what) - if(AH) - AH.AddInteraction(message) - return AH + mob_client = what + if(istype(mob_client) && mob_client.current_ticket) + if (isnull(player_message)) + mob_client.current_ticket.AddInteraction(message) + else + mob_client.current_ticket.AddInteraction(message, player_message) + return mob_client.current_ticket + if(istext(what)) //ckey + var/datum/admin_help/active_admin_help = GLOB.ahelp_tickets.CKey2ActiveTicket(what) + if(active_admin_help) + if (isnull(player_message)) + active_admin_help.AddInteraction(message) + else + active_admin_help.AddInteraction(message, player_message) + return active_admin_help // // HELPER PROCS @@ -583,7 +861,7 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ahelp) . = list("total" = list(), "noflags" = list(), "afk" = list(), "stealth" = list(), "present" = list()) for(var/client/X in GLOB.admins) .["total"] += X - if(requiredflags != 0 && !check_rights(rights_required = requiredflags, show_msg = FALSE, C = X)) + if(requiredflags != NONE && !check_rights_for(X, requiredflags)) .["noflags"] += X else if(X.is_afk()) .["afk"] += X @@ -592,28 +870,23 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ahelp) else .["present"] += X -/proc/send2irc_adminless_only(source, msg, requiredflags = R_BAN) - var/list/adm = get_admin_counts() +/proc/send2tgs_adminless_only(source, msg, requiredflags = R_BAN) + var/list/adm = get_admin_counts(requiredflags) var/list/activemins = adm["present"] . = activemins.len if(. <= 0) - var/final = "" + var/message = "" var/list/afkmins = adm["afk"] var/list/stealthmins = adm["stealth"] var/list/powerlessmins = adm["noflags"] var/list/allmins = adm["total"] if(!afkmins.len && !stealthmins.len && !powerlessmins.len) - final = "[msg] - No admins online" + message = "[msg] - No admins online" else - final = "[msg] - All admins stealthed\[[english_list(stealthmins)]\], AFK\[[english_list(afkmins)]\], or lacks +BAN\[[english_list(powerlessmins)]\]! Total: [allmins.len] " - send2irc(source,final) - -/proc/send2irc(msg,msg2) - msg = replacetext(replacetext(msg, "\proper", ""), "\improper", "") - msg2 = replacetext(replacetext(msg2, "\proper", ""), "\improper", "") - world.TgsTargetedChatBroadcast("[msg] | [msg2]", TRUE) + message = "[msg] - All admins stealthed\[[english_list(stealthmins)]\], AFK\[[english_list(afkmins)]\], or lacks +BAN\[[english_list(powerlessmins)]\]! Total: [allmins.len] " + send2adminchat(source, message) -/proc/ircadminwho() +/proc/tgsadminwho() var/list/message = list("Admins: ") var/list/admin_keys = list() for(var/adm in GLOB.admins) @@ -621,14 +894,14 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ahelp) admin_keys += "[C][C.holder.fakekey ? "(Stealth)" : ""][C.is_afk() ? "(AFK)" : ""]" for(var/admin in admin_keys) - if(LAZYLEN(admin_keys) > 1) + if(LAZYLEN(message) > 1) message += ", [admin]" else message += "[admin]" return jointext(message, "") -/proc/keywords_lookup(msg,irc) +/proc/keywords_lookup(msg,external) //This is a list of words which are ignored by the parser when comparing message contents for names. MUST BE IN LOWER CASE! var/list/adminhelp_ignored_words = list("unknown","the","a","an","of","monkey","alien","as", "i") @@ -650,14 +923,14 @@ INITIALIZE_IMMEDIATE(/obj/effect/statclick/ahelp) var/list/L = splittext(string, " ") var/surname_found = 0 //surnames - for(var/i=L.len, i>=1, i--) + for(var/i=L.len, i >= 1, i--) var/word = ckey(L[i]) if(word) surnames[word] = M surname_found = i break //forenames - for(var/i=1, i(?|F) " + founds += "Name: [found.name]([found.real_name]) Key: [found.key] Ckey: [found.ckey] [is_antag ? "(Antag)" : null] " + msg += "[original_word](?|F) " continue msg += "[original_word] " - if(irc) + if(external) if(founds == "") return "Search Failed" else return founds return msg + +/** + * Checks a given message to see if any of the words are something we want to treat specially, as detailed below. + * + * There are 3 cases where a word is something we want to act on + * 1. Admin pings, like @adminckey. Pings the admin in question, text is not clickable + * 2. Datum refs, like @0x2001169 or @mob_23. Clicking on the link opens up the VV for that datum + * 3. Ticket refs, like #3. Displays the status and ahelper in the link, clicking on it brings up the ticket panel for it. + * Returns a list being used as a tuple. Index ASAY_LINK_NEW_MESSAGE_INDEX contains the new message text (with clickable links and such) + * while index ASAY_LINK_PINGED_ADMINS_INDEX contains a list of pinged admin clients, if there are any. + * + * Arguments: + * * msg - the message being scanned + */ +/proc/check_asay_links(msg) + var/list/msglist = splittext(msg, " ") //explode the input msg into a list + var/list/pinged_admins = list() // if we ping any admins, store them here so we can ping them after + var/modified = FALSE // did we find anything? + + var/i = 0 + for(var/word in msglist) + i++ + if(!length(word)) + continue + + switch(word[1]) + if("@") + var/stripped_word = ckey(copytext(word, 2)) + + // first we check if it's a ckey of an admin + var/client/client_check = GLOB.directory[stripped_word] + if(client_check?.holder) + msglist[i] = "[word]" + pinged_admins[stripped_word] = client_check + modified = TRUE + continue + + // then if not, we check if it's a datum ref + + var/word_with_brackets = "\[[stripped_word]\]" // the actual memory address lookups need the bracket wraps + var/datum/datum_check = locate(word_with_brackets) + if(!istype(datum_check)) + continue + msglist[i] = "[word]" + modified = TRUE + + if("#") // check if we're linking a ticket + var/possible_ticket_id = text2num(copytext(word, 2)) + if(!possible_ticket_id) + continue + + var/datum/admin_help/ahelp_check = GLOB.ahelp_tickets?.TicketByID(possible_ticket_id) + if(!ahelp_check) + continue + + var/state_word + switch(ahelp_check.state) + if(AHELP_ACTIVE) + state_word = "Active" + if(AHELP_CLOSED) + state_word = "Closed" + if(AHELP_RESOLVED) + state_word = "Resolved" + + msglist[i]= "[word] ([state_word] | [ahelp_check.initiator_key_name])" + modified = TRUE + + if(modified) + var/list/return_list = list() + return_list[ASAY_LINK_NEW_MESSAGE_INDEX] = jointext(msglist, " ") // without tuples, we must make do! + return_list[ASAY_LINK_PINGED_ADMINS_INDEX] = pinged_admins + return return_list + + +#undef WEBHOOK_URGENT +#undef WEBHOOK_NONE +#undef WEBHOOK_NON_URGENT diff --git a/code/modules/admin/verbs/adminhelp_vr.dm b/code/modules/admin/verbs/adminhelp_vr.dm deleted file mode 100644 index ac6bf1cc1fbb..000000000000 --- a/code/modules/admin/verbs/adminhelp_vr.dm +++ /dev/null @@ -1,16 +0,0 @@ -/datum/admin_help/proc/send2adminchat() - if(!config_legacy.chat_webhook_url) - return - - var/list/adm = get_admin_counts() - var/list/afkmins = adm["afk"] - var/list/allmins = adm["total"] - - spawn(0) //Unreliable world.Exports() - var/query_string = "type=adminhelp" - query_string += "&key=[url_encode(config_legacy.chat_webhook_key)]" - query_string += "&from=[url_encode(key_name(initiator))]" - query_string += "&msg=[url_encode(html_decode(name))]" - query_string += "&admin_number=[allmins.len]" - query_string += "&admin_number_afk=[afkmins.len]" - world.Export("[config_legacy.chat_webhook_url]?[query_string]") diff --git a/code/modules/admin/verbs/adminjump.dm b/code/modules/admin/verbs/adminjump.dm index 919636c37434..be08e0ccac3d 100644 --- a/code/modules/admin/verbs/adminjump.dm +++ b/code/modules/admin/verbs/adminjump.dm @@ -1,52 +1,51 @@ -/client/proc/Jump(var/area/A in GLOB.sortedAreas) +/client/proc/Jump(area/target in GLOB.sortedAreas) set name = "Jump to Area" set desc = "Area to jump to" set category = "Admin" + if(!check_rights(R_ADMIN|R_MOD|R_DEBUG)) return - if(config_legacy.allow_admin_jump) - usr.forceMove(pick(get_area_turfs(A))) + // try not to area jump in walls + var/turf/drop_location + top_level: + for(var/turf/area_turf as anything in get_area_turfs(target)) + if(area_turf.density) + continue + drop_location = area_turf + break top_level + + if(isnull(drop_location)) + to_chat(usr, SPAN_WARNING("No valid drop location found in the area!")) + return - log_admin("[key_name(usr)] jumped to [A]") - message_admins("[key_name_admin(usr)] jumped to [A]", 1) - feedback_add_details("admin_verb","JA") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - else - alert("Admin jumping disabled") + mob.abstract_move(drop_location) + log_admin("[key_name(usr)] jumped to [AREACOORD(drop_location)]") + message_admins("[key_name_admin(usr)] jumped to [AREACOORD(drop_location)]") + feedback_add_details("admin_verb","JA") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/client/proc/jumptoturf(turf/T in world) +/client/proc/jumptoturf(turf/locale in world) set name = "Jump to Turf" set category = "Admin" if(!check_rights(R_ADMIN|R_MOD|R_DEBUG)) return - if(config_legacy.allow_admin_jump) - log_admin("[key_name(usr)] jumped to [T.x],[T.y],[T.z] in [T.loc]") - message_admins("[key_name_admin(usr)] jumped to [T.x],[T.y],[T.z] in [T.loc]", 1) - usr.forceMove(T) - feedback_add_details("admin_verb","JT") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - else - alert("Admin jumping disabled") -/client/proc/jumptomob(var/mob/M in GLOB.mob_list) + log_admin("[key_name(usr)] jumped to [AREACOORD(locale)]") + message_admins("[key_name_admin(usr)] jumped to [AREACOORD(locale)]") + mob.abstract_move(locale) + feedback_add_details("admin_verb","JT") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + +/client/proc/jumptomob(mob/target in GLOB.mob_list) set category = "Admin" set name = "Jump to Mob" set popup_menu = FALSE if(!check_rights(R_ADMIN|R_MOD|R_DEBUG)) return - if(config_legacy.allow_admin_jump) - log_admin("[key_name(usr)] jumped to [key_name(M)]") - message_admins("[key_name_admin(usr)] jumped to [key_name_admin(M)]", 1) - if(src.mob) - var/mob/A = src.mob - var/turf/T = get_turf(M) - if(T && isturf(T)) - feedback_add_details("admin_verb","JM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - A.forceMove(T) - else - to_chat(A, "This mob is not located in the game world.") - else - alert("Admin jumping disabled") + mob.abstract_move(target.loc) + log_admin("[key_name(usr)] jumped to [key_name(target)]") + message_admins("[key_name_admin(usr)] jumped to [ADMIN_LOOKUPFLW(target)] at [AREACOORD(target)]") + feedback_add_details("admin_verb","JM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/proc/jumptocoord(tx as num, ty as num, tz as num) set category = "Admin" @@ -55,19 +54,14 @@ if(!check_rights(R_ADMIN|R_MOD|R_DEBUG)) return - if (config_legacy.allow_admin_jump) - if(src.mob) - var/mob/A = src.mob - var/turf/T = locate(tx, ty, tz) - if(!T) - to_chat(src, "[tx], [ty], [tz] does not exist!") - return - A.forceMove(T) - feedback_add_details("admin_verb","JC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - message_admins("[key_name_admin(usr)] jumped to coordinates [tx], [ty], [tz]") + var/turf/where_we_droppin = locate(tx, ty,tz) + if(isnull(where_we_droppin)) + to_chat(usr, SPAN_WARNING("Invalid coordinates.")) + return - else - alert("Admin jumping disabled") + mob.abstract_move(where_we_droppin) + message_admins("[key_name_admin(usr)] jumped to coordinates [tx], [ty], [tz]") + feedback_add_details("admin_verb","JC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/proc/jumptokey() set category = "Admin" @@ -76,37 +70,48 @@ if(!check_rights(R_ADMIN|R_MOD|R_DEBUG)) return - if(config_legacy.allow_admin_jump) - var/list/keys = list() - for(var/mob/M in GLOB.player_list) - keys += M.client - var/selection = input("Please, select a player!", "Admin Jumping", null, null) as null|anything in sortKey(keys) - if(!selection) - to_chat(src, "No keys found.") - return - var/mob/M = selection:mob - log_admin("[key_name(usr)] jumped to [key_name(M)]") - message_admins("[key_name_admin(usr)] jumped to [key_name_admin(M)]", 1) - usr.forceMove(M.loc) - feedback_add_details("admin_verb","JK") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - else - alert("Admin jumping disabled") + if(!isobserver(mob)) + admin_ghost() -/client/proc/Getmob(var/mob/M in GLOB.mob_list) + var/list/keys = list() + for(var/mob/M in GLOB.player_list) + keys += M.client + var/client/selection = input(usr, "Please, select a player!", "Admin Jumping") as null|anything in sortKey(keys) + if(!selection) + to_chat(usr, "No keys found.", confidential = TRUE) + return + var/mob/M = selection.mob + log_admin("[key_name(usr)] jumped to [key_name(M)]") + message_admins("[key_name_admin(usr)] jumped to [ADMIN_LOOKUPFLW(M)]") + mob.abstract_move(M.loc) + feedback_add_details("admin_verb","JK") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + +/client/proc/Getmob(mob/target in GLOB.mob_list) set category = "Admin" set name = "Get Mob" set desc = "Mob to teleport" if(!check_rights(R_ADMIN|R_MOD|R_DEBUG)) return - if(config_legacy.allow_admin_jump) - log_admin("[key_name(usr)] jumped to [key_name(M)]") - var/msg = "[key_name_admin(usr)] jumped to [key_name_admin(M)]" - message_admins(msg) - admin_ticket_log(M, msg) - M.forceMove(get_turf(usr)) - feedback_add_details("admin_verb","GM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + var/atom/loc = get_turf(mob) + target.admin_teleport(loc) + feedback_add_details("admin_verb","GM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + +/// Proc to hook user-enacted teleporting behavior and keep logging of the event. +/atom/movable/proc/admin_teleport(atom/new_location) + if(isnull(new_location)) + log_admin("[key_name(usr)] teleported [key_name(src)] to nullspace") + moveToNullspace() else - alert("Admin jumping disabled") + var/turf/location = get_turf(new_location) + log_admin("[key_name(usr)] teleported [key_name(src)] to [AREACOORD(location)]") + forceMove(new_location) + +/mob/admin_teleport(atom/new_location) + var/turf/location = get_turf(new_location) + var/msg = "[key_name_admin(usr)] teleported [ADMIN_LOOKUPFLW(src)] to [isnull(new_location) ? "nullspace" : ADMIN_VERBOSEJMP(location)]" + message_admins(msg) + admin_ticket_log(src, msg) + return ..() /client/proc/Getkey() set category = "Admin" @@ -116,41 +121,40 @@ if(!check_rights(R_ADMIN|R_MOD|R_DEBUG)) return - if(config_legacy.allow_admin_jump) - var/list/keys = list() - for(var/mob/M in GLOB.player_list) - keys += M.client - var/selection = input("Please, select a player!", "Admin Jumping", null, null) as null|anything in sortKey(keys) - if(!selection) - return - var/mob/M = selection:mob - - if(!M) - return - log_admin("[key_name(usr)] teleported [key_name(M)]") - var/msg = "[key_name_admin(usr)] teleported [ADMIN_LOOKUPFLW(M)]" - message_admins(msg) - admin_ticket_log(M, msg) - if(M) - M.forceMove(get_turf(usr)) - feedback_add_details("admin_verb","GK") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - else - alert("Admin jumping disabled") + var/list/keys = list() + for(var/mob/M in GLOB.player_list) + keys += M.client + var/client/selection = input(usr, "Please, select a player!", "Admin Jumping") as null|anything in sortKey(keys) + if(!selection) + return + var/mob/M = selection.mob + + if(!M) + return + log_admin("[key_name(usr)] teleported [key_name(M)]") + var/msg = "[key_name_admin(usr)] teleported [ADMIN_LOOKUPFLW(M)]" + message_admins(msg) + admin_ticket_log(M, msg) + if(M) + M.forceMove(get_turf(usr)) + feedback_add_details("admin_verb","GK") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/client/proc/sendmob(var/mob/M in sortmobs()) +/client/proc/sendmob(mob/jumper in sortmobs()) set category = "Admin" set name = "Send Mob" if(!check_rights(R_ADMIN|R_MOD|R_DEBUG)) return - var/area/A = input(usr, "Pick an area.", "Pick an area") in GLOB.sortedAreas - if(A) - if(config_legacy.allow_admin_jump) - M.forceMove(pick(get_area_turfs(A))) - feedback_add_details("admin_verb","SMOB") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - - log_admin("[key_name(usr)] teleported [key_name(M)]") - var/msg = "[key_name_admin(usr)] teleported [ADMIN_LOOKUPFLW(M)]" - message_admins(msg) - admin_ticket_log(M, msg) - else - alert("Admin jumping disabled") + var/area/target_area = tgui_input_list(usr, "Pick an area", "Send Mob", GLOB.sortedAreas) + if(isnull(target_area)) + return + if(!istype(target_area)) + return + var/list/turfs = get_area_turfs(target_area) + if(length(turfs) && jumper.forceMove(pick(turfs))) + log_admin("[key_name(usr)] teleported [key_name(jumper)] to [AREACOORD(jumper)]") + var/msg = "[key_name_admin(usr)] teleported [ADMIN_LOOKUPFLW(jumper)] to [AREACOORD(jumper)]" + message_admins(msg) + admin_ticket_log(jumper, msg) + else + to_chat(usr, "Failed to move mob to a valid location.", confidential = TRUE) + feedback_add_details("admin_verb","SMOB") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm index 7f0f2d1d55ef..be37c0d3ade8 100644 --- a/code/modules/admin/verbs/adminpm.dm +++ b/code/modules/admin/verbs/adminpm.dm @@ -1,16 +1,21 @@ -#define IRCREPLYCOUNT 2 - +#define EXTERNALREPLYCOUNT 2 +#define EXTERNAL_PM_USER "IRCKEY" //allows right clicking mobs to send an admin PM to their client, forwards the selected mob's client to cmd_admin_pm -/client/proc/cmd_admin_pm_context(mob/M in GLOB.mob_list) +/client/proc/cmd_admin_pm_context(mob/target in GLOB.mob_list) set category = null set name = "Admin PM Mob" if(!holder) - to_chat(src, "Error: Admin-PM-Context: Only administrators may use this command.") return - if( !ismob(M) || !M.client ) + if(!ismob(target)) + to_chat( + src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Context: Target mob is not a mob, somehow."), + confidential = TRUE, + ) return - cmd_admin_pm(M.client,null) + cmd_admin_pm(target.client, null) feedback_add_details("admin_verb","Admin PM Mob") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! //shows a list of clients we could send PMs to, then forwards our choice to cmd_admin_pm @@ -18,273 +23,728 @@ set category = "Admin" set name = "Admin PM" if(!holder) - to_chat(src, "Error: Admin-PM-Panel: Only administrators may use this command.") return - var/list/client/targets[0] - for(var/client/T) - if(T.mob) - if(isnewplayer(T.mob)) - targets["(New Player) - [T]"] = T - else if(isobserver(T.mob)) - targets["[T.mob.name](Ghost) - [T]"] = T - else - targets["[T.mob.real_name](as [T.mob.name]) - [T]"] = T + + var/list/targets = list() + for(var/client/client in GLOB.clients) + var/nametag = "" + var/mob/lad = client.mob + var/mob_name = lad?.name + var/real_mob_name = lad?.real_name + if(!lad) + nametag = "(No Mob)" + else if(isnewplayer(lad)) + nametag = "(New Player)" + else if(isobserver(lad)) + nametag = "[mob_name](Ghost)" else - targets["(No Mob) - [T]"] = T - var/target = input(src,"To whom shall we send a message?","Admin PM",null) as null|anything in sortList(targets) - cmd_admin_pm(targets[target],null) + nametag = "[real_mob_name](as [mob_name])" + targets["[nametag] - [client]"] = client + + var/target = input(src,"To whom shall we send a message?", "Admin PM", null) as null|anything in sortList(targets) + if (isnull(target)) + return + cmd_admin_pm(targets[target], null) feedback_add_details("admin_verb","Admin PM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! +/// Replys to some existing ahelp, reply to whom, which can be a client or ckey /client/proc/cmd_ahelp_reply(whom) + if(IsAdminAdvancedProcCall()) + return FALSE + if(prefs.muted & MUTE_ADMINHELP) - to_chat(src, "Error: Admin-PM: You are unable to use admin PM-s (muted).") + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Reply: You are unable to use admin PM-s (muted)."), + confidential = TRUE) return - var/client/C - if(istext(whom)) - if(cmptext(copytext(whom,1,2),"@")) - whom = findStealthKey(whom) - C = GLOB.directory[whom] - else if(istype(whom,/client)) - C = whom - if(!C) + + // We use the ckey here rather then keeping the client to ensure resistance to client logouts mid execution + if(istype(whom, /client)) + var/client/boi = whom + whom = boi.ckey + + var/ambiguious_recipient = disambiguate_client(whom) + if(!istype(ambiguious_recipient, /client)) if(holder) - to_chat(src, "Error: Admin-PM: Client not found.") + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Reply: Client not found."), + confidential = TRUE) return - var/datum/admin_help/AH = C.current_ticket + // Existing client case + var/client/recipient = ambiguious_recipient + + // The ticket our recipient is using + var/datum/admin_help/recipient_ticket = recipient?.current_ticket + // Any past interactions with the recipient ticket + var/datum/admin_help/recipient_interactions = recipient_ticket?.ticket_interactions + // Any opening interactions with the recipient ticket, IE: interactions started before the ticket first receives a response + var/datum/admin_help/opening_interactions = recipient_ticket?.opening_responders + // Our recipient's admin holder, if one exists + var/datum/admins/recipient_holder = recipient?.holder + // The ckey of our recipient + var/recipient_ckey = recipient?.ckey + // Our recipient's fake key, if they are faking their ckey + var/recipient_fake_key = recipient_holder?.fakekey + // Our ckey, with our mob's name if one exists, formatted with a reply link + var/our_linked_name = key_name_admin(src) + // The recipient's ckey, formatted with a reply link + var/recipient_linked_ckey = key_name_admin(recipient, FALSE) + // The recipient's ckey, formatted slightly with html + var/formatted_recipient_ckey = key_name(recipient, FALSE, FALSE) + + var/message_prompt = "Message:" + if(recipient_ticket) + message_admins("[our_linked_name] has started replying to [recipient_linked_ckey]'s admin help.") + // If none's interacted with the ticket yet + if(length(recipient_interactions) == 1) + if(length(opening_interactions)) // Inform the admin that they aren't the first + var/printable_interators = english_list(opening_interactions) + SEND_SOUND(src, sound('sound/machines/buzz-sigh.ogg', volume=30)) + message_prompt += "\n\n**This ticket is already being responded to by: [printable_interators]**" + // add the admin who is currently responding to the list of people responding + LAZYADD(recipient_ticket.opening_responders, src) + + var/request = "Private message to" + if(recipient_fake_key) + request = "[request] an Administrator." + else + request = "[request] [formatted_recipient_ckey]." - if(AH) - message_admins("[key_name_admin(src)] has started replying to [key_name(C, 0, 0)]'s admin help.") - var/msg = input(src,"Message:", "Private message to [key_name(C, 0, 0)]") as message|null - if (!msg) - message_admins("[key_name_admin(src)] has cancelled their reply to [key_name(C, 0, 0)]'s admin help.") + var/message = input(src, message_prompt, request) as message|null + + if(recipient_ticket) + LAZYREMOVE(recipient_ticket.opening_responders, src) + + if (!message) + message_admins("[our_linked_name] has cancelled their reply to [recipient_linked_ckey]'s admin help.") return - cmd_admin_pm(whom, msg, AH) + + if(!recipient) //We lost the client during input, disconnected or relogged. + if(GLOB.directory[recipient_ckey]) // Client has reconnected, lets try to recover + whom = GLOB.directory[recipient_ckey] + else + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Reply: Client not found."), + confidential = TRUE) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = "[SPAN_DANGER("Message not sent:")]
[message]", + confidential = TRUE) + if(recipient_ticket) + recipient_ticket.AddInteraction("No client found, message not sent:
[message]") + return + cmd_admin_pm(whom, message) //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, datum/admin_help/AH) +//Fetching a message if needed. +//whom here is a client, a ckey, or [EXTERNAL_PM_USER] if this is from tgs. message is the default message to send +/client/proc/cmd_admin_pm(whom, message) if(prefs.muted & MUTE_ADMINHELP) - to_chat(src, "Error: Admin-PM: You are unable to use admin PM-s (muted).") + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM: You are unable to use admin PM-s (muted)."), + confidential = TRUE) return - if(!holder && !current_ticket) //no ticket? https://www.youtube.com/watch?v=iHSPf6x1Fdo - to_chat(src, "You can no longer reply to this ticket, please open another one by using the Adminhelp verb if need be.") - to_chat(src, "Message: [msg]") + if(!holder && !current_ticket) //no ticket? https://www.youtube.com/watch?v=iHSPf6x1Fdo + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("You can no longer reply to this ticket, please open another one by using the Adminhelp verb if need be."), + confidential = TRUE) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_NOTICE("Message: [message]"), + confidential = TRUE) return - var/client/recipient - var/irc = 0 - if(istext(whom)) - if(cmptext(copytext(whom,1,2),"@")) - whom = findStealthKey(whom) - if(whom == "IRCKEY") - irc = 1 - else - recipient = GLOB.directory[whom] - else if(istype(whom,/client)) - recipient = whom + // We use the ckey here rather then keeping the client to ensure resistance to client logouts mid execution + if(istype(whom, /client)) + var/client/boi = whom + whom = boi.ckey + + var/message_to_send = request_adminpm_message(disambiguate_client(whom), message) + if(!message_to_send) + return + if(!sends_adminpm_message(disambiguate_client(whom), message_to_send)) + return - if(irc) - if(!ircreplyamount) //to prevent people from spamming irc - return - if(!msg) - msg = input(src,"Message:", "Private message to Administrator") as text|null + notify_adminpm_message(disambiguate_client(whom), message_to_send) + +/// Requests an admin pm message to send +/// message_target here can be either [EXTERNAL_PM_USER], indicating that this message is intended for some external chat channel +/// or a /client, which we will then store info about to ensure logout -> logins are protected as expected +/// Accepts an optional existing message, which will be used in place of asking the recipient assuming all other conditions are met +/// Returns the message to send or null if no message is found +/// Sleeps +/client/proc/request_adminpm_message(ambiguious_recipient, existing_message = null) + if(IsAdminAdvancedProcCall()) + return null + + if(ambiguious_recipient == EXTERNAL_PM_USER) + if(!externalreplyamount) //to prevent people from spamming irc/discord + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Message: External reply cap hit."), + confidential = TRUE) + return null + var/msg = "" + if(existing_message) + msg = existing_message + else + msg = input(src,"Message:", "Private message to Administrator") as message|null if(!msg) - return - if(holder) - to_chat(src, "Error: Use the admin IRC channel, nerd.") - return - + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Message: No message input."), + confidential = TRUE) + return null + if(holder) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Message: Use the admin IRC/Discord channel, nerd."), + confidential = TRUE) + return null + return msg + + if(!istype(ambiguious_recipient, /client)) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Message: Client not found."), + confidential = TRUE) + return null + + var/client/recipient = ambiguious_recipient + // Stored in case client is deleted between this and after the message is input + var/recipient_ckey = recipient?.ckey + // Stored in case client is deleted between this and after the message is input + var/datum/admin_help/recipient_ticket = recipient?.current_ticket + // Our current active ticket + var/datum/admin_help/our_ticket = current_ticket + // If our recipient is an admin, this is their admins datum + var/datum/admins/recipient_holder = recipient?.holder + // If our recipient has a fake name, this is it + var/recipient_fake_key = recipient_holder?.fakekey + // Just the recipient's ckey, formatted for htmlifying stuff + var/recipient_print_key = key_name(recipient, FALSE, FALSE) + + // The message we intend on returning + var/msg = "" + if(existing_message) + msg = existing_message else - if(!recipient) - if(holder) - to_chat(src, "Error: Admin-PM: Client not found.") - to_chat(src, msg) - else - current_ticket.MessageNoRecipient(msg) - return + var/request = "Private message to" + if(recipient_fake_key) + request = "[request] an Administrator." + else + request = "[request] [recipient_print_key]." + //get message text, limit its length.and clean/escape html + msg = input(src,"Message:", request) as message|null + msg = trim(msg) - //get message text, limit it's length.and clean/escape html - if(!msg) - msg = input(src,"Message:", "Private message to [key_name(recipient, 0, 0)]") as message|null + if(!msg) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Message: No message input."), + confidential = TRUE) + return null + + if(recipient) + return msg + // Client has disappeared due to logout + if(GLOB.directory[recipient_ckey]) // Client has reconnected, lets try to recover + recipient = GLOB.directory[recipient_ckey] + return msg + + // We don't tell standard users if a ticket drops because admins have a way to actually see + // Past tickets, and well, admins are the ones who might ban you if you ignore them + if(holder) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Message: Client not found."), + confidential = TRUE) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = "[SPAN_DANGER("Message not sent:")]
[msg]", + confidential = TRUE) + if(recipient_ticket) + recipient_ticket.AddInteraction("No client found, message not sent:
[msg]") + return null + if(our_ticket) + our_ticket.MessageNoRecipient(msg) + return null + +/// Sends a pm message via the tickets system +/// message_target here can be either [EXTERNAL_PM_USER], indicating that this message is intended for some external chat channel +/// or a /client, in which case we send in the standard form +/// send_message is the raw message to send, it will be filtered and treated to ensure we do not break any text handling +/// Returns FALSE if the send failed, TRUE otherwise +/client/proc/sends_adminpm_message(ambiguious_recipient, send_message) + if(IsAdminAdvancedProcCall()) + return FALSE + + send_message = adminpm_filter_text(ambiguious_recipient, send_message) + if(!send_message) + return null + + if (handle_spam_prevention(send_message, MUTE_ADMINHELP)) + // handle_spam_prevention does its own "hey buddy ya fucker up here's what happen" + return FALSE + + var/raw_message = send_message + + if(holder) + send_message = emoji_parse(send_message) + + var/keyword_parsed_msg = keywords_lookup(send_message) + // Stores a bit of html with our ckey, name, and a linkified string to click and rely to us with + var/name_key_with_link = key_name(src, TRUE, TRUE) + + if(ambiguious_recipient == EXTERNAL_PM_USER) + var/datum/admin_help/new_admin_help = admin_ticket_log(src, + "Reply PM from-[name_key_with_link] to External: [keyword_parsed_msg]", + player_message = "Reply PM from-[name_key_with_link] to External: [send_message]") + + new_admin_help.reply_to_admins_notification(raw_message) + + var/new_help_id = new_admin_help?.id + + externalreplyamount-- + + var/category = "Reply: [ckey]" + if(new_admin_help) + category = "#[new_help_id] [category]" + + send2adminchat(category, raw_message) + return TRUE + + if(!istype(ambiguious_recipient, /client)) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Send: Client not found."), + confidential = TRUE) + return FALSE + + var/client/recipient = ambiguious_recipient + var/datum/admins/recipient_holder = recipient.holder + var/datum/admins/our_holder = holder + + // Stores a bit of html that contains the ckey of the recipient, its mob's name if any exist, and a link to reply to them with + var/their_name_with_link = key_name(recipient, TRUE, TRUE) + // Stores a bit of html with our ckey highlighted as a reply link + var/link_to_us = key_name(src, TRUE, FALSE) + // Stores a bit of html with outhe ckey of the recipientr highlighted as a reply link + var/link_to_their = key_name(recipient, TRUE, FALSE) + // Our current ticket, can (supposedly) be null here + var/datum/admin_help/ticket = current_ticket + // The recipient's current ticket, could in theory? maybe? be null here + var/datum/admin_help/recipient_ticket = recipient?.current_ticket + + // If we should do a full on boink, so with the text and extra flair and everything + // We want to always do this so long as WE are an admin, and we're messaging the "loser" of the converstation + var/full_boink = FALSE + // Only admins can perform boinks + if(our_holder) + full_boink = TRUE + // Tickets will only generate for the non admin/admin being boinked. This check is to ensure boinked admins don't send the same + // ADMINISTRAITOR PRIVATE MESSAGE text to their boinker every time they respond + if(recipient_holder && ticket) + full_boink = FALSE + + // If we're gonna boink em, do it now + // It is worth noting this will always generate the target a ticket if they don't already have one (tickets will generate if a player ahelps automatically, outside this logic) + // So past this point, because of our block above here, we can be reasonably guarenteed that the user will have a ticket + if(full_boink) + // Full boinks will always be done to players, so we are not guarenteed that they won't have a ticket + if(!recipient_ticket) + new /datum/admin_help(send_message, recipient, TRUE) + // This action mutates our existing cached ticket information, so we recache + ticket = current_ticket + recipient_ticket = recipient?.current_ticket + + to_chat(recipient, + type = MESSAGE_TYPE_ADMINPM, + html = "-- Administrator private message --", + confidential = TRUE) + + recipient.receive_ahelp( + link_to_us, + SPAN_LINKIFY(send_message), + ) + + to_chat(recipient, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_ADMINSAY("Click on the administrator's name to reply."), + confidential = TRUE) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_NOTICE("Admin PM to-[their_name_with_link]: [SPAN_LINKIFY(send_message)]"), + confidential = TRUE) + + admin_ticket_log(recipient, + "PM From [name_key_with_link]: [keyword_parsed_msg]", + log_in_blackbox = FALSE, + player_message = "PM From [link_to_us]: [send_message]") + + //always play non-admin recipients the adminhelp sound + SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg')) + return TRUE + + // Ok if we're here, either this message is for an admin, or someone somehow figured out how to send a new message as a player + // First case well, first + if(!our_holder && !recipient_holder) //neither are admins + if(!ticket) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Send: Non-admin to non-admin PM communication is forbidden."), + confidential = TRUE) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = "[SPAN_DANGER("Message not sent:")]
[send_message]", + confidential = TRUE) + return FALSE + ticket.MessageNoRecipient(send_message) + return TRUE + + // Ok by this point the recipient has to be an admin, and this is either an admin on admin event, or a player replying to an admin + + // You're replying to a ticket that is closed. Bad move. You must have started replying before the close, and then got input()'d + // Lets be nice and pass this off to a new ticket, as we recomend above + if(!ticket) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Send: Attempted to send a reply to a closed ticket."), + confidential = TRUE) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_NOTICE("Relaying message to a new admin help."), + confidential = TRUE) + GLOB.admin_help_ui_handler.perform_adminhelp(src, raw_message, FALSE) + return FALSE + + // Let's play some music for the admin + SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg')) + + // Admin on admin violence first + if(our_holder) + recipient.receive_ahelp( + name_key_with_link, + SPAN_LINKIFY(keyword_parsed_msg), + "danger", + ) + + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_NOTICE("Admin PM to-[their_name_with_link]: [SPAN_LINKIFY(keyword_parsed_msg)]"), + confidential = TRUE) + + //omg this is dumb, just fill in both their logs + var/interaction_message = "PM from-[name_key_with_link] to-[their_name_with_link]: [keyword_parsed_msg]" + var/player_interaction_message = "PM from-[link_to_us] to-[link_to_their]: [send_message]" + admin_ticket_log(src, + interaction_message, + log_in_blackbox = FALSE, + player_message = player_interaction_message) + if(recipient != src) //reeee + admin_ticket_log(recipient, + interaction_message, + log_in_blackbox = FALSE, + player_message = player_interaction_message) + + return TRUE + + // This is us (a player) trying to talk to the recipient (an admin) + var/replymsg = "Reply PM from-[name_key_with_link]: [SPAN_LINKIFY(keyword_parsed_msg)]" + var/player_replymsg = "Reply PM from-[link_to_us]: [SPAN_LINKIFY(send_message)]" + admin_ticket_log(src, + "[replymsg]", + log_in_blackbox = FALSE, + player_message = player_replymsg) + to_chat(recipient, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("[replymsg]"), + confidential = TRUE) + + ticket.reply_to_admins_notification(send_message) + + return TRUE + +/// Notifies all admins about the existance of an admin pm, then logs the pm +/// message_target here can be either [EXTERNAL_PM_USER], indicating that this message is intended for some external chat channel +/// or a /client, in which case we send in the standard form +/// log_message is the raw message to send, it will be filtered and treated to ensure we do not break any text handling +/client/proc/notify_adminpm_message(ambiguious_recipient, log_message) + if(IsAdminAdvancedProcCall()) + return - if(!msg) - return + // First we filter, because these procs can be called by anyone with debug, and I don't trust that check + // gotta make sure none's fucking about + log_message = adminpm_filter_text(ambiguious_recipient, log_message) + if(!log_message) + return - if(prefs.muted & MUTE_ADMINHELP) - to_chat(src, "Error: Admin-PM: You are unable to use admin PM-s (muted).") - return + var/raw_message = log_message - if(!recipient) - if(holder) - to_chat(src, "Error: Admin-PM: Client not found.") - else - current_ticket.MessageNoRecipient(msg) - return + if(holder) + log_message = emoji_parse(log_message) + + var/keyword_parsed_msg = keywords_lookup(log_message) + // Shows our ckey and the name of any mob we might be possessing + var/our_name = key_name(src) + // Shows our ckey/name embedded inside a clickable link to reply to this message + var/our_linked_ckey = key_name(src, TRUE, FALSE) + // Our current active ticket + var/datum/admin_help/ticket = current_ticket + // Our current ticket id, if one exists + var/ticket_id = ticket?.id + + if(ambiguious_recipient == EXTERNAL_PM_USER) + // Guard against the possibility of a null, since it'll runtime and spit out the contents of what should be a private ticket. + if(ticket) + log_admin_private("PM: Ticket #[ticket_id]: [our_name]->External: [sanitize_text(trim(raw_message))]") + else + log_admin_private("PM: [our_name]->External: [sanitize_text(trim(raw_message))]") + for(var/client/lad in GLOB.admins) + to_chat(lad, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_NOTICE("PM: [our_linked_ckey]->External: [keyword_parsed_msg]"), + confidential = TRUE) + return + if(!istype(ambiguious_recipient, /client)) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Notify: Client not found."), + confidential = TRUE) + return - if (src.handle_spam_prevention(msg,MUTE_ADMINHELP)) + var/client/recipient = ambiguious_recipient + // The key of our recipient + var/recipient_key = recipient?.key + // Shows the recipient's ckey and the name of any mob it might be possessing + var/recipient_name = key_name(recipient) + // Shows the recipient's ckey/name embedded inside a clickable link to reply to this message + var/recipient_linked_ckey = key_name(recipient, TRUE, FALSE) + + window_flash(recipient) + if(ticket) + log_admin_private("PM: Ticket #[ticket_id]: [our_name]->[recipient_name]: [sanitize_text(trim(raw_message))]") + else + log_admin_private("PM: [our_name]->[recipient_name]: [sanitize_text(trim(raw_message))]") + //we don't use message_admins here because the sender/receiver might get it too + for(var/client/lad in GLOB.admins) + if(lad.key == key || lad.key == recipient_key) //check to make sure client/lad isn't the sender or recipient + continue + to_chat(lad, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_NOTICE("PM: [our_linked_ckey]->[recipient_linked_ckey]: [keyword_parsed_msg]") , + confidential = TRUE) + +/// Accepts a message and an ambiguious recipient (some sort of client representative, or [EXTERNAL_PM_USER]) +/// Returns the filtered message if it passes all checks, or null if the send fails +/client/proc/adminpm_filter_text(ambiguious_recipient, message) + if(prefs.muted & MUTE_ADMINHELP) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Filter: You are unable to use admin PM-s (muted)."), + confidential = TRUE) return //clean the message if it's not sent by a high-rank admin - if(!check_rights(R_SERVER|R_DEBUG,0)||irc)//no sending html to the poor bots - msg = trim(sanitize(copytext(msg,1,MAX_MESSAGE_LEN))) - if(!msg) + if(!check_rights(R_SERVER|R_DEBUG, 0) || ambiguious_recipient == EXTERNAL_PM_USER)//no sending html to the poor bots + message = sanitize(copytext_char(message, 1, MAX_MESSAGE_LEN)) + if(!message) + to_chat(src, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_DANGER("Error: Admin-PM-Filter: Your message contained only HTML, it's been sanitized away and the message disregarded."), + confidential = TRUE) return + return message - var/rawmsg = msg - - if(holder) - msg = emoji_parse(msg) - - var/keywordparsedmsg = keywords_lookup(msg) +#define TGS_AHELP_USAGE "Usage: ticket " +/proc/TgsPm(target, message, sender) + var/requested_ckey = ckey(target) + var/ambiguious_target = disambiguate_client(requested_ckey) - if(irc) - to_chat(src, "PM to-Admins: [rawmsg]") - admin_ticket_log(src, "Reply PM from-[key_name(src, TRUE, TRUE)] to IRC: [keywordparsedmsg]") - ircreplyamount-- - send2irc("Reply: [ckey]",rawmsg) + var/client/recipient + // This might seem like hiding a failure condition, but we want to be able to send commands to the ticket without the client being logged in + if(istype(ambiguious_target, /client)) + recipient = ambiguious_target + + // The ticket we want to talk about here. Either the target's active ticket, or the last one it had + var/datum/admin_help/ticket + if(recipient) + ticket = recipient.current_ticket else - if(recipient.holder) - if(holder) //both are admins - to_chat(recipient, "Admin PM from-[key_name(src, recipient, 1)]: [keywordparsedmsg]") - to_chat(src, "Admin PM to-[key_name(recipient, src, 1)]: [keywordparsedmsg]") - - //omg this is dumb, just fill in both their tickets - var/interaction_message = "PM from-[key_name(src, recipient, 1)] to-[key_name(recipient, src, 1)]: [keywordparsedmsg]" - admin_ticket_log(src, interaction_message) - if(recipient != src) //reeee - admin_ticket_log(recipient, interaction_message) - - else //recipient is an admin but sender is not - var/replymsg = "Reply PM from-[key_name(src, recipient, 1)]: [keywordparsedmsg]" - admin_ticket_log(src, replymsg) - to_chat(recipient, replymsg) - to_chat(src, "PM to-Admins: [msg]") - - //play the recieving admin the adminhelp sound (if they have them enabled) - // if(recipient.get_preference_toggle(/datum/client_preference/holder/play_adminhelp_ping)) - // SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg')) - SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg')) + GLOB.ahelp_tickets.CKey2ActiveTicket(requested_ckey) + // The ticket's id + var/ticket_id = ticket?.id - else - if(holder) //sender is an admin but recipient is not. Do BIG RED TEXT - if(!recipient.current_ticket) - new /datum/admin_help(msg, recipient, TRUE) - - to_chat(recipient, "-- Administrator private message --") - to_chat(recipient, "Admin PM from-[key_name(src, recipient, 0)]: [msg]") - to_chat(recipient, "Click on the administrator's name to reply.") - to_chat(src, "Admin PM to-[key_name(recipient, src, 1)]: [msg]") - - admin_ticket_log(recipient, "PM From [key_name_admin(src)]: [keywordparsedmsg]") - - //always play non-admin recipients the adminhelp sound - SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg')) - - //AdminPM popup for ApocStation and anybody else who wants to use it. Set it with POPUP_ADMIN_PM in config_legacy.txt ~Carn - if(config_legacy.popup_admin_pm) - spawn() //so we don't hold the caller proc up - var/sender = src - var/sendername = key - var/reply = input(recipient, msg,"Admin PM from-[sendername]", "") as message|null //show message and await a reply - if(recipient && reply) - if(sender) - recipient.cmd_admin_pm(sender,reply) //sender is still about, let's reply to them - else - adminhelp(reply) //sender has left, adminhelp instead - return - - else //neither are admins - to_chat(src, "Error: Admin-PM: Non-admin to non-admin PM communication is forbidden.") - return - - if(irc) - log_admin("PM: [key_name(src)]->IRC: [rawmsg]") - for(var/client/X in GLOB.admins) - to_chat(X, "PM: [key_name(src, X, 0)]->IRC: [keywordparsedmsg]") - else - log_admin("PM: [key_name(src)]->[key_name(recipient)]: [rawmsg]") - //we don't use message_admins here because the sender/receiver might get it too - for(var/client/X in GLOB.admins) - if(X.key!=key && X.key!=recipient.key) //check client/X is an admin and isn't the sender or recipient - to_chat(X, "PM: [key_name(src, X, 0)]->[key_name(recipient, X, 0)]: [keywordparsedmsg]" ) - -/proc/IrcPm(target,msg,sender) - var/client/C = GLOB.directory[target] - - var/datum/admin_help/ticket = C ? C.current_ticket : GLOB.ahelp_tickets.CKey2ActiveTicket(target) - var/compliant_msg = trim(lowertext(msg)) - var/irc_tagged = "[sender](IRC)" + var/compliant_msg = trim(lowertext(message)) + var/tgs_tagged = "[sender](TGS/External)" var/list/splits = splittext(compliant_msg, " ") - if(splits.len && splits[1] == "ticket") - if(splits.len < 2) - return "Usage: ticket " + var/split_size = length(splits) + + if(split_size && splits[1] == "ticket") + if(split_size < 2) + return TGS_AHELP_USAGE switch(splits[2]) if("close") if(ticket) - ticket.Close(irc_tagged) - return "Ticket #[ticket.id] successfully closed" + ticket.Close(tgs_tagged) + return "Ticket #[ticket_id] successfully closed" if("resolve") if(ticket) - ticket.Resolve(irc_tagged) - return "Ticket #[ticket.id] successfully resolved" + ticket.Resolve(tgs_tagged) + return "Ticket #[ticket_id] successfully resolved" if("icissue") if(ticket) - ticket.ICIssue(irc_tagged) - return "Ticket #[ticket.id] successfully marked as IC issue" + ticket.ICIssue(tgs_tagged) + return "Ticket #[ticket_id] successfully marked as IC issue" if("reject") if(ticket) - ticket.Reject(irc_tagged) - return "Ticket #[ticket.id] successfully rejected" + ticket.Reject(tgs_tagged) + return "Ticket #[ticket_id] successfully rejected" + if("reopen") + if(ticket) + return "Error: [target] already has ticket #[ticket_id] open" + var/ticket_num + // If the passed in command actually has a ticket id arg + if(split_size >= 3) + ticket_num = text2num(splits[3]) + + if(isnull(ticket_num)) + return "Error: No/Invalid ticket id specified. [TGS_AHELP_USAGE]" + + // The active ticket we're trying to reopen, if one exists + var/datum/admin_help/active_ticket = GLOB.ahelp_tickets.TicketByID(ticket_num) + // The ckey of the player to be targeted BY the ticket + // Not the initiator all the time + var/boinked_ckey = active_ticket?.initiator_ckey + + if(!active_ticket) + return "Error: Ticket #[ticket_num] not found" + if(boinked_ckey != target) + return "Error: Ticket #[ticket_num] belongs to [boinked_ckey]" + + active_ticket.Reopen() + return "Ticket #[ticket_num] successfully reopened" + if("list") + var/list/tickets = GLOB.ahelp_tickets.TicketsByCKey(target) + var/tickets_length = length(tickets) + + if(!tickets_length) + return "None" + var/list/printable_tickets = list() + for(var/datum/admin_help/iterated_ticket in tickets) + // The id of the iterated adminhelp + var/iterated_id = iterated_ticket?.id + var/text = "" + if(iterated_ticket == ticket) + text += "Active: " + text += "#[iterated_id]" + printable_tickets += text + return printable_tickets.Join(", ") else - return "Usage: ticket " + return TGS_AHELP_USAGE return "Error: Ticket could not be found" - var/static/stealthkey - var/adminname = "Administrator" - - if(!C) + // Now that we've handled command processing, we can actually send messages to the client + if(!recipient) return "Error: No client" - if(!stealthkey) - stealthkey = GenIrcStealthKey() + var/adminname + if(CONFIG_GET(flag/show_irc_name)) + adminname = tgs_tagged + else + adminname = "Administrator" - msg = sanitize(copytext(msg,1,MAX_MESSAGE_LEN)) - if(!msg) + var/stealthkey = GetTgsStealthKey() + + message = sanitize(copytext_char(message, 1, MAX_MESSAGE_LEN)) + message = emoji_parse(message) + + if(!message) return "Error: No message" - message_admins("IRC message from [sender] to [key_name_admin(C)] : [msg]") - log_admin("IRC PM: [sender] -> [key_name(C)] : [msg]") + // The ckey of our recipient, with a reply link, and their mob if one exists + var/recipient_name_linked = key_name_admin(recipient) + // The ckey of our recipient, with their mob if one exists. No link + var/recipient_name = key_name_admin(recipient) + + message_admins("External message from [sender] to [recipient_name_linked] : [message]") + log_admin_private("External PM: [sender] -> [recipient_name] : [message]") - to_chat(C, "-- Administrator private message --") - to_chat(C, "Admin PM from-[adminname]: [msg]") - to_chat(C, "Click on the administrator's name to reply.") + to_chat(recipient, + type = MESSAGE_TYPE_ADMINPM, + html = "-- Administrator private message --", + confidential = TRUE) - admin_ticket_log(C, "PM From [irc_tagged]: [msg]") + recipient.receive_ahelp( + "[adminname]", + message, + ) - window_flash(C) - //always play non-admin recipients the adminhelp sound - SEND_SOUND(C, sound('sound/effects/adminhelp.ogg')) + to_chat(recipient, + type = MESSAGE_TYPE_ADMINPM, + html = SPAN_ADMINSAY("Click on the administrator's name to reply."), + confidential = TRUE) - C.ircreplyamount = IRCREPLYCOUNT + admin_ticket_log(recipient, "PM From [tgs_tagged]: [message]", log_in_blackbox = FALSE) + window_flash(recipient) + // Nullcheck because we run a winset in window flash and I do not trust byond + if(recipient) + //always play non-admin recipients the adminhelp sound + SEND_SOUND(recipient, 'sound/effects/adminhelp.ogg') + + recipient.externalreplyamount = EXTERNALREPLYCOUNT return "Message Successful" -/proc/GenIrcStealthKey() - var/num = (rand(0,1000)) - var/i = 0 - while(i == 0) - i = 1 - for(var/P in GLOB.stealthminID) - if(num == GLOB.stealthminID[P]) - num++ - i = 0 - var/stealth = "@[num2text(num)]" - GLOB.stealthminID["IRCKEY"] = stealth - return stealth - -#undef IRCREPLYCOUNT +/// Gets TGS's stealth key, generates one if none is found +/proc/GetTgsStealthKey() + var/static/tgsStealthKey + if(tgsStealthKey) + return tgsStealthKey + + tgsStealthKey = generateStealthCkey() + GLOB.stealthminID[EXTERNAL_PM_USER] = tgsStealthKey + return tgsStealthKey + +/// Takes an argument which could be either a ckey, /client, or IRC marker, and returns a client if possible +/// Returns [EXTERNAL_PM_USER] if an IRC marker is detected +/// Otherwise returns null +/proc/disambiguate_client(whom) + if(istype(whom, /client)) + return whom + + if(!istext(whom) || !(length(whom) >= 1)) + return null + + var/searching_ckey = whom + if(whom[1] == "@") + searching_ckey = findTrueKey(whom) + + if(searching_ckey == EXTERNAL_PM_USER) + return EXTERNAL_PM_USER + + return GLOB.directory[searching_ckey] + +/client/proc/receive_ahelp(reply_to, message, span_class = "adminsay") + to_chat( + src, + type = MESSAGE_TYPE_ADMINPM, + html = "Admin PM from-[reply_to]: [message]", + confidential = TRUE, + ) + + current_ticket?.player_replied = FALSE + +#undef EXTERNAL_PM_USER +#undef EXTERNALREPLYCOUNT +#undef TGS_AHELP_USAGE diff --git a/code/modules/admin/verbs/adminsay.dm b/code/modules/admin/verbs/adminsay.dm index 90d09218fca4..628ce62a234c 100644 --- a/code/modules/admin/verbs/adminsay.dm +++ b/code/modules/admin/verbs/adminsay.dm @@ -1,20 +1,38 @@ -/client/proc/cmd_admin_say(msg as text) +/client/proc/cmd_admin_say(message as text) set category = "Special Verbs" set name = "Asay" //Gave this shit a shorter name so you only have to time out "asay" rather than "admin say" to use it --NeoFite set hidden = 1 if(!check_rights(R_ADMIN|R_MOD)) return - msg = emoji_parse(sanitize(msg)) - if(!msg) + message = emoji_parse(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN)) + if(!message) return - log_adminsay(msg,src) - - if(check_rights(R_ADMIN|R_MOD,0)) - for(var/client/C in GLOB.admins) - if((R_ADMIN|R_MOD) & C.holder.rights) - to_chat(C, "" + "ADMIN: " + " [key_name(usr, 1)]([admin_jump_link(mob, src)]): [msg]") + if(findtext(message, "@") || findtext(message, "#")) + var/list/link_results = check_asay_links(message) + if(length(link_results)) + message = link_results[ASAY_LINK_NEW_MESSAGE_INDEX] + link_results[ASAY_LINK_NEW_MESSAGE_INDEX] = null + var/list/pinged_admin_clients = link_results[ASAY_LINK_PINGED_ADMINS_INDEX] + for(var/iter_ckey in pinged_admin_clients) + var/client/iter_admin_client = pinged_admin_clients[iter_ckey] + if(!iter_admin_client?.holder) + continue + window_flash(iter_admin_client) + SEND_SOUND(iter_admin_client.mob, sound('sound/misc/asay_ping.ogg')) + + log_adminsay(message, src) + message = keywords_lookup(message) + message = "[SPAN_ADMINSAY("[SPAN_PREFIX("ADMIN:")] [key_name_admin(usr)] [ADMIN_FLW(mob)]: [message]")]" + + // manual filter + for(var/client/C in GLOB.admins) + if((R_ADMIN|R_MOD) & C.holder.rights) + to_chat(C, + type = MESSAGE_TYPE_ADMINCHAT, + html = message, + confidential = TRUE) feedback_add_details("admin_verb","M") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -30,12 +48,12 @@ if(!check_rights(R_ADMIN|R_MOD|R_SERVER)) return - msg = emoji_parse(sanitize(msg)) - log_modsay(msg,src) - + msg = emoji_parse(copytext_char(sanitize(msg), 1, MAX_MESSAGE_LEN)) if (!msg) return + log_modsay(msg,src) + var/sender_name = key_name(usr, 1) if(check_rights(R_ADMIN, 0)) sender_name = "[sender_name]" diff --git a/code/modules/admin/verbs/atmosdebug.dm b/code/modules/admin/verbs/atmosdebug.dm index b22a7f917a71..0a7f70e1cc69 100644 --- a/code/modules/admin/verbs/atmosdebug.dm +++ b/code/modules/admin/verbs/atmosdebug.dm @@ -10,21 +10,21 @@ if(alert("WARNING: This command should not be run on a live server. Do you want to continue?", "Check Piping", "No", "Yes") == "No") return - to_chat(usr, "Checking for disconnected pipes...") + to_chat(usr, "Checking for disconnected pipes...", confidential = TRUE) //all plumbing - yes, some things might get stated twice, doesn't matter. for (var/obj/machinery/atmospherics/plumbing in GLOB.machines) if (plumbing.nodealert) - to_chat(usr, "Unconnected [plumbing.name] located at [plumbing.x],[plumbing.y],[plumbing.z] ([get_area(plumbing.loc)])") + to_chat(usr, "Unconnected [plumbing.name] located at [ADMIN_VERBOSEJMP(plumbing)]", confidential = TRUE) //Manifolds for (var/obj/machinery/atmospherics/pipe/manifold/pipe in GLOB.machines) if (!pipe.node1 || !pipe.node2 || !pipe.node3) - to_chat(usr, "Unconnected [pipe.name] located at [pipe.x],[pipe.y],[pipe.z] ([get_area(pipe.loc)])") + to_chat(usr, "Unconnected [pipe.name] located at [ADMIN_VERBOSEJMP(pipe)]", confidential = TRUE) //Pipes for (var/obj/machinery/atmospherics/pipe/simple/pipe in GLOB.machines) if (!pipe.node1 || !pipe.node2) - to_chat(usr, "Unconnected [pipe.name] located at [pipe.x],[pipe.y],[pipe.z] ([get_area(pipe.loc)])") + to_chat(usr, "Unconnected [pipe.name] located at [ADMIN_VERBOSEJMP(pipe)]", confidential = TRUE) to_chat(usr, "Checking for overlapping pipes...") next_turf: @@ -36,9 +36,9 @@ for(var/connect_type in pipe.connect_types) connect_types[connect_type] += 1 if(connect_types[1] > 1 || connect_types[2] > 1 || connect_types[3] > 1) - to_chat(usr, "Overlapping pipe ([pipe.name]) located at [T.x],[T.y],[T.z] ([get_area(T)])") + to_chat(usr, "Overlapping pipe ([pipe.name]) located at [ADMIN_VERBOSEJMP(pipe)]", confidential = TRUE) continue next_turf - to_chat(usr, "Done") + to_chat(usr, "Done", confidential = TRUE) /client/proc/powerdebug() set category = "Mapping" @@ -47,14 +47,16 @@ to_chat(src, "Only administrators may use this command.") return feedback_add_details("admin_verb","CPOW") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + var/list/results = list() for (var/datum/powernet/PN in powernets) if (!PN.nodes || !PN.nodes.len) if(PN.cables && (PN.cables.len > 1)) var/obj/structure/cable/C = PN.cables[1] - to_chat(usr, "Powernet with no nodes! (number [PN.number]) - example cable at [C.x], [C.y], [C.z] in area [get_area(C.loc)]") + results += "Powernet with no nodes! (number [PN.number]) - example cable at [ADMIN_VERBOSEJMP(C)]" if (!PN.cables || (PN.cables.len < 10)) if(PN.cables && (PN.cables.len > 1)) var/obj/structure/cable/C = PN.cables[1] - to_chat(usr, "Powernet with fewer than 10 cables! (number [PN.number]) - example cable at [C.x], [C.y], [C.z] in area [get_area(C.loc)]") + results += "Powernet with fewer than 10 cables! (number [PN.number]) - example cable at [ADMIN_VERBOSEJMP(C)]" + to_chat(usr, "[results.Join("\n")]", confidential = TRUE) diff --git a/code/modules/admin/verbs/buildmode.dm b/code/modules/admin/verbs/buildmode.dm index 45da5598369d..98cfbb20b1d0 100644 --- a/code/modules/admin/verbs/buildmode.dm +++ b/code/modules/admin/verbs/buildmode.dm @@ -10,6 +10,7 @@ if(H.cl == M.client) qdel(H) else + message_admins("[key_name_admin(usr)] has entered build mode.") log_admin("[key_name(usr)] has entered build mode.") M.client.buildmode = 1 M.client.show_popup_menus = 0 diff --git a/code/modules/admin/verbs/deadsay.dm b/code/modules/admin/verbs/deadsay.dm index 3b961cd99b50..3734ea7376bb 100644 --- a/code/modules/admin/verbs/deadsay.dm +++ b/code/modules/admin/verbs/deadsay.dm @@ -1,42 +1,44 @@ GLOBAL_LIST_INIT(stealthmin_nicknames, world.file2list("[global.config.directory]/admin_nicknames.txt")) -/client/proc/dsay(msg as text) +/client/proc/dsay(message as text) set category = "Special Verbs" set name = "Dsay" //Gave this shit a shorter name so you only have to time out "dsay" rather than "dead say" to use it --NeoFite set hidden = 1 + if(!src.holder) to_chat(src, "Only administrators may use this command.") return - if(!src.mob) - return + if(prefs.muted & MUTE_DEADCHAT) - to_chat(src, "You cannot send DSAY messages (muted).") + to_chat(usr, SPAN_DANGER("You cannot send DSAY messages (muted)."), confidential = TRUE) return - if(!get_preference_toggle(/datum/game_preference_toggle/chat/dsay)) - to_chat(src, "You have deadchat muted.") + if (handle_spam_prevention(message, MUTE_DEADCHAT)) return - var/stafftype = holder.rank + if(!get_preference_toggle(/datum/game_preference_toggle/chat/dsay)) + to_chat(src, "You have deadchat muted.", confidential = TRUE) + return - msg = sanitize(msg) - log_admin("DSAY: [key_name(src)] : [msg]") + message = copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN) + log_admin("DSAY: [key_name(src)] : [message]") - if (!msg) + if (!message) return - var/key + var/rank_name = holder.rank + var/admin_name = get_public_key() if(is_under_stealthmin() && get_preference_toggle(/datum/game_preference_toggle/admin/obfuscate_stealth_dsay)) - key = pick(GLOB.stealthmin_nicknames) - else - key = get_public_key() + // rank_name = pick(strings("admin_nicknames.json", "ranks", "config")) + admin_name = pick(GLOB.stealthmin_nicknames) + var/name_and_rank = "[SPAN_TOOLTIP(rank_name, "STAFF")] ([admin_name])" - msg = emoji_parse(msg) - - say_dead_direct("[stafftype]([key]) says, \"[msg]\"") + say_dead_direct("[SPAN_PREFIX("DEAD:")] [name_and_rank] says, \"[emoji_parse(message)]\"") feedback_add_details("admin_verb","D") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/proc/get_dead_say() var/msg = input(src, null, "dsay \"text\"") as text|null + if (isnull(msg)) + return dsay(msg) diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm index 80f006814d16..9fca003b2af7 100644 --- a/code/modules/admin/verbs/debug.dm +++ b/code/modules/admin/verbs/debug.dm @@ -3,14 +3,10 @@ set name = "Debug-Game" if(!check_rights(R_DEBUG)) return - if(GLOB.Debug2) - GLOB.Debug2 = 0 - message_admins("[key_name(src)] toggled debugging off.") - log_admin("[key_name(src)] toggled debugging off.") - else - GLOB.Debug2 = 1 - message_admins("[key_name(src)] toggled debugging on.") - log_admin("[key_name(src)] toggled debugging on.") + GLOB.Debug2 = !GLOB.Debug2 + var/message = "toggled debugging [(GLOB.Debug2 ? "ON" : "OFF")]" + message_admins("[key_name_admin(usr)] [message].") + log_admin("[key_name(usr)] [message].") feedback_add_details("admin_verb","DG2") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -93,22 +89,30 @@ usr.show_message(t, 1) feedback_add_details("admin_verb","ASL") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/client/proc/cmd_admin_robotize(var/mob/M in GLOB.mob_list) +/client/proc/cmd_admin_robotize(mob/target in GLOB.mob_list) set category = "Fun" set name = "Make Robot" - if(istype(M, /mob/living/carbon/human)) - log_admin("[key_name(src)] has robotized [M.key].") - spawn(10) - M:Robotize() + if(!check_rights(R_FUN)) return - else - alert("Invalid mob") + if(!SSticker.HasRoundStarted()) + alert(usr, "Wait until the game starts") + return + if(issilicon(target)) + alert(usr, "They are already a cyborg.") + return + if(!ishuman(target)) + alert(usr, "Mob type must be a human.") + return + log_admin("[key_name(usr)] has robotized [target.key].") + INVOKE_ASYNC(target, TYPE_PROC_REF(/mob/living/carbon/human, Robotize)) -/client/proc/cmd_admin_animalize(var/mob/M in GLOB.mob_list) +/client/proc/cmd_admin_animalize(mob/M in GLOB.mob_list) set category = "Fun" set name = "Make Simple Animal" + if(!check_rights(R_FUN)) return + if(!M) alert("That mob doesn't seem to exist, close the panel and try again.") return @@ -118,15 +122,16 @@ return log_admin("[key_name(src)] has animalized [M.key].") - spawn(10) - M.Animalize() + INVOKE_ASYNC(M, TYPE_PROC_REF(/mob, Animalize)) -/client/proc/makepAI(var/turf/T in GLOB.mob_list) +/client/proc/makepAI(turf/T in GLOB.mob_list) set category = "Fun" set name = "Make pAI" set desc = "Specify a location to spawn a pAI device, then specify a key to play that pAI" + if(!check_rights(R_FUN)) return + var/list/available = list() for(var/mob/C in GLOB.mob_list) if(C.key) @@ -149,10 +154,12 @@ paiController.pai_candidates.Remove(candidate) feedback_add_details("admin_verb","MPAI") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/client/proc/cmd_admin_alienize(var/mob/M in GLOB.mob_list) +/client/proc/cmd_admin_alienize(mob/M in GLOB.mob_list) set category = "Fun" set name = "Make Alien" + if(!check_rights(R_FUN)) return + if(ishuman(M)) log_admin("[key_name(src)] has alienized [M.key].") spawn(10) @@ -163,34 +170,111 @@ else alert("Invalid mob") -//TODO: merge the vievars version into this or something maybe mayhaps +/client/proc/poll_type_to_del(search_string) + var/list/types = get_fancy_list_of_atom_types() + if (!isnull(search_string) && search_string != "") + types = filter_fancy_list(types, search_string) + + if(!length(types)) + return + + var/key = input(usr, "Choose an object to delete.", "Delete:") as null|anything in sortList(types) + + if(!key) + return + return types[key] + /client/proc/cmd_debug_del_all(object as text) set category = "Debug" set name = "Del-All" + set desc = "Delete all datums with the specified type." - var/list/matches = get_fancy_list_of_atom_types() - if (!isnull(object) && object!="") - matches = filter_fancy_list(matches, object) + if(!check_rights(R_DEBUG|R_SPAWN)) return - if(matches.len==0) + var/type_to_del = poll_type_to_del(object) + if(!type_to_del) return - var/hsbitem = input(usr, "Choose an object to delete. Use clear-mobs instead on LIVE.", "Delete:") as null|anything in matches - if(hsbitem) - hsbitem = matches[hsbitem] - var/counter = 0 + + var/counter = 0 + for(var/atom/O in world) + if(istype(O, type_to_del)) + counter++ + qdel(O) + CHECK_TICK + log_admin("[key_name(usr)] has force deleted all ([counter]) instances of [type_to_del].") + message_admins("[key_name_admin(usr)] has force deleted all ([counter]) instances of [type_to_del].") + +/client/proc/cmd_debug_del_all_force(object as text) + set category = "Debug" + set name = "Force-Del-All" + set desc = "Forcibly delete all datums with the specified type." + + if(!check_rights(R_DEBUG|R_SPAWN)) return + + var/type_to_del = poll_type_to_del(object) + if(!type_to_del) + return + + var/counter = 0 + for(var/atom/O in world) + if(istype(O, type_to_del)) + counter++ + qdel(O, force = TRUE) + CHECK_TICK + log_admin("[key_name(usr)] has deleted all ([counter]) instances of [type_to_del].") + message_admins("[key_name_admin(usr)] has deleted all ([counter]) instances of [type_to_del].") + +/client/proc/cmd_debug_del_all_hard(object as text) + set category = "Debug" + set name = "Hard-Del-All" + set desc = "Hard delete all datums with the specified type." + + if(!check_rights(R_DEBUG|R_SPAWN)) return + + var/type_to_del = poll_type_to_del(object) + if(!type_to_del) + return + + var/choice = alert(usr, "ARE YOU SURE that you want to hard delete this type? It will cause MASSIVE lag.", "Hoooo lad what happen?", "Yes", "No") + if(choice != "Yes") + return + + choice = alert(usr, "Do you want to pre qdelete the atom? This will speed things up significantly, but may break depending on your level of fuckup.", "How do you even get it that bad", "Yes", "No") + var/should_pre_qdel = TRUE + if(choice == "No") + should_pre_qdel = FALSE + + choice = alert(usr, "Ok one last thing, do you want to yield to the game? or do it all at once. These are hard deletes remember.", "Jesus christ man", "Yield", "Ignore the server") + var/should_check_tick = TRUE + if(choice == "Ignore the server") + should_check_tick = FALSE + + var/counter = 0 + if(should_check_tick) for(var/atom/O in world) - if(istype(O, hsbitem)) + if(istype(O, type_to_del)) counter++ - qdel(O) + if(should_pre_qdel) + qdel(O) + del(O) CHECK_TICK - log_admin("[key_name(src)] has deleted all ([counter]) instances of [hsbitem].") - message_admins("[key_name_admin(src)] has deleted all ([counter]) instances of [hsbitem].") - // SSblackbox.record_feedback("tally", "admin_verb", 1, "Delete All") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + else + for(var/atom/O in world) + if(istype(O, type_to_del)) + counter++ + if(should_pre_qdel) + qdel(O) + del(O) + // CHECK_TICK funny how on tg this check tick is here on the "should not check tick" part + + log_admin("[key_name(usr)] has hard deleted all ([counter]) instances of [type_to_del].") + message_admins("[key_name_admin(usr)] has hard deleted all ([counter]) instances of [type_to_del].") /client/proc/cmd_admin_clear_mobs() set category = "Admin" set name = "Clear Mobs" + if(!check_rights(R_DEBUG|R_SPAWN)) return var/range = input(usr, "Choose a range in tiles FROM your location", "If uncertain, enter 25 or below.") as num if(range >= 50) // ridiculously high alert("Please enter a valid range below 50.") @@ -214,15 +298,19 @@ /client/proc/cmd_debug_make_powernets() set category = "Debug" set name = "Make Powernets" + set desc = "Regenerates all powernets for all cables." + + if(!check_rights(R_DEBUG|R_SERVER)) return SSmachines.makepowernets() - log_admin("[key_name(src)] has remade the powernet. SSmachines.makepowernets() called.") - message_admins("[key_name_admin(src)] has remade the powernets. SSmachines.makepowernets() called.", 0) + log_admin("[key_name(usr)] has remade the powernet. makepowernets() called.") + message_admins("[key_name_admin(usr)] has remade the powernets. makepowernets() called.") feedback_add_details("admin_verb","MPWN") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/proc/cmd_debug_tog_aliens() set category = "Server" set name = "Toggle Aliens" + if(!check_rights(R_DEBUG|R_SERVER)) return config_legacy.aliens_allowed = !config_legacy.aliens_allowed log_admin("[key_name(src)] has turned aliens [config_legacy.aliens_allowed ? "on" : "off"].") message_admins("[key_name_admin(src)] has turned aliens [config_legacy.aliens_allowed ? "on" : "off"].", 0) @@ -294,54 +382,74 @@ else . = lines.Join("\n") -/client/proc/cmd_admin_grantfullaccess(var/mob/M in GLOB.mob_list) +/client/proc/cmd_admin_grantfullaccess(mob/M in GLOB.mob_list) set category = "Admin" set name = "Grant Full Access" + set desc = "Grant full access to a mob." + + if(!check_rights(R_DEBUG)) + return - if (!SSticker) - alert("Wait until the game starts") + if(!SSticker.HasRoundStarted()) + alert(usr, "Wait until the game starts") return - if (istype(M, /mob/living/carbon/human)) + + if(ishuman(M)) var/mob/living/carbon/human/H = M - if (H.wear_id) - var/obj/item/card/id/id = H.wear_id - if(istype(H.wear_id, /obj/item/pda)) - var/obj/item/pda/pda = H.wear_id - id = pda.id - id.icon_state = "gold" - id.access = get_all_accesses().Copy() + var/obj/item/worn = H.wear_id + var/obj/item/card/id/id = null + + if(worn) + id = worn.GetID() + if(id) + if(id == worn) + worn = null + qdel(id) + + id = new /obj/item/card/id/gold() + + id.access = get_all_accesses().Copy() + id.registered_name = H.real_name + id.assignment = "Facility Director" + id.name = "[id.registered_name]'s ID Card ([id.assignment])" + id.update_icon() + + if(worn) + if(istype(worn, /obj/item/storage/wallet)) + var/obj/item/storage/wallet/W = worn + W.front_id = id + id.forceMove(W) + W.update_icon() else - var/obj/item/card/id/id = new/obj/item/card/id(M); - id.icon_state = "gold" - id.access = get_all_accesses().Copy() - id.registered_name = H.real_name - id.assignment = "Facility Director" - id.name = "[id.registered_name]'s ID Card ([id.assignment])" H.equip_to_slot_or_del(id, SLOT_ID_WORN_ID) H.update_inv_wear_id() else alert("Invalid mob") feedback_add_details("admin_verb","GFA") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - log_admin("[key_name(src)] has granted [M.key] full access.") - message_admins("[key_name_admin(usr)] has granted [M.key] full access.", 1) + log_admin("[key_name(usr)] has granted [M.key] full access.") + message_admins(SPAN_ADMINNOTICE("[key_name_admin(usr)] has granted [M.key] full access.")) -/client/proc/cmd_assume_direct_control(var/mob/M in GLOB.mob_list) +/client/proc/cmd_assume_direct_control(mob/M in GLOB.mob_list) set category = "Admin" set name = "Assume direct control" - set desc = "Direct intervention" + set desc = "Assume direct control of a mob." - if(!check_rights(R_DEBUG|R_ADMIN)) return + if(!check_rights(R_ADMIN)) return if(M.ckey) if(alert("This mob is being controlled by [M.ckey]. Are you sure you wish to assume control of it? [M.ckey] will be made a ghost.",,"Yes","No") != "Yes") return - else - var/mob/observer/dead/ghost = new/mob/observer/dead(M,1) - M.transfer_client_to(ghost) - message_admins("[key_name_admin(usr)] assumed direct control of [M].", 1) + + if(!M || QDELETED(M)) + to_chat(usr, SPAN_WARNING("The target mob no longer exists.")) + return + + message_admins(SPAN_ADMINNOTICE("[key_name_admin(usr)] assumed direct control of [M].")) log_admin("[key_name(usr)] assumed direct control of [M].") - var/mob/adminmob = src.mob + var/mob/adminmob = mob + if(M.ckey) + M.ghostize(FALSE) transfer_to(M) - if( isobserver(adminmob) ) + if(isobserver(adminmob)) qdel(adminmob) feedback_add_details("admin_verb","ADC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -603,21 +711,25 @@ /client/proc/cmd_debug_mob_lists() set category = "Debug" set name = "Debug Mob Lists" - set desc = "For when you just gotta know" + set desc = "For when you just gotta know." + + var/chosen_list = input("Which list?", "Select List") in list("Players","Admins","Mobs","Living Mobs","Dead Mobs","Clients") + if(isnull(chosen_list)) + return - switch(input("Which list?") in list("Players","Admins","Mobs","Living Mobs","Dead Mobs", "Clients")) + switch(chosen_list) if("Players") - to_chat(usr, jointext(GLOB.player_list,",")) + to_chat(usr, jointext(GLOB.player_list,","), confidential = TRUE) if("Admins") - to_chat(usr, jointext(GLOB.admins,",")) + to_chat(usr, jointext(GLOB.admins,","), confidential = TRUE) if("Mobs") - to_chat(usr, jointext(GLOB.mob_list,",")) + to_chat(usr, jointext(GLOB.mob_list,","), confidential = TRUE) if("Living Mobs") - to_chat(usr, jointext(living_mob_list,",")) + to_chat(usr, jointext(living_mob_list,","), confidential = TRUE) if("Dead Mobs") - to_chat(usr, jointext(dead_mob_list,",")) + to_chat(usr, jointext(dead_mob_list,","), confidential = TRUE) if("Clients") - to_chat(usr, jointext(GLOB.clients,",")) + to_chat(usr, jointext(GLOB.clients,","), confidential = TRUE) // DNA2 - Admin Hax /client/proc/cmd_admin_toggle_block(var/mob/M,var/block) diff --git a/code/modules/admin/verbs/getlogs.dm b/code/modules/admin/verbs/getlogs.dm index 8ef3899adc52..bd9ecb5cdc7e 100644 --- a/code/modules/admin/verbs/getlogs.dm +++ b/code/modules/admin/verbs/getlogs.dm @@ -31,4 +31,4 @@ src << ftp(file(path)) else return - to_chat(src, "Attempting to send [path], this may take a fair few minutes if the file is very large.") + to_chat(src, "Attempting to send [path], this may take a fair few minutes if the file is very large.", confidential = TRUE) diff --git a/code/modules/admin/verbs/panicbunker.dm b/code/modules/admin/verbs/panicbunker.dm index 80b51f9d3d25..39d39edd3490 100644 --- a/code/modules/admin/verbs/panicbunker.dm +++ b/code/modules/admin/verbs/panicbunker.dm @@ -3,12 +3,12 @@ GLOBAL_LIST_EMPTY(bunker_passthrough) /client/proc/panicbunker() set category = "Server" set name = "Toggle Panic Bunker" - - if(!check_rights(R_ADMIN)) + set desc = "Toggles the panic bunker for the server." + if(!check_rights(R_SERVER)) return if(!CONFIG_GET(flag/sql_enabled)) - to_chat(usr, "The Database is not enabled!") + to_chat(usr, SPAN_ADMINNOTICE("The Database is not enabled!"), confidential = TRUE) return var/now = CONFIG_GET(flag/panic_bunker) @@ -17,7 +17,7 @@ GLOBAL_LIST_EMPTY(bunker_passthrough) CONFIG_SET(flag/panic_bunker, now) log_and_message_admins("[key_name(usr)] has toggled the Panic Bunker, it is now [now? "on" : "off"]") - if(now && (!SSdbcore.Connect())) + if(now && !SSdbcore.Connect()) message_admins("The Database is not connected! Panic bunker will not work until the connection is reestablished.") feedback_add_details("admin_verb","PANIC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -25,8 +25,11 @@ GLOBAL_LIST_EMPTY(bunker_passthrough) set category = "Server" set name = "Add PB Bypass" set desc = "Allows a given ckey to connect despite the panic bunker for a given round." + if(!check_rights(R_ADMIN)) + return + if(!CONFIG_GET(flag/sql_enabled)) - to_chat(usr, "The Database is not enabled!") + to_chat(usr, SPAN_ADMINNOTICE("The Database is not enabled!"), confidential = TRUE) return GLOB.bunker_passthrough |= ckey(ckeytobypass) @@ -34,26 +37,28 @@ GLOBAL_LIST_EMPTY(bunker_passthrough) SSpersistence.SavePanicBunker() //we can do this every time, it's okay log_admin("[key_name(usr)] has added [ckeytobypass] to the current round's bunker bypass list.") message_admins("[key_name_admin(usr)] has added [ckeytobypass] to the current round's bunker bypass list.") - send2irc("Panic Bunker", "[key_name(usr)] has added [ckeytobypass] to the current round's bunker bypass list.") + send2adminchat("Panic Bunker", "[key_name(usr)] has added [ckeytobypass] to the current round's bunker bypass list.") /client/proc/revokebunkerbypass(ckeytobypass as text) set category = "Server" set name = "Revoke PB Bypass" set desc = "Revoke's a ckey's permission to bypass the panic bunker for a given round." + if(!check_rights(R_ADMIN)) + return + if(!CONFIG_GET(flag/sql_enabled)) - to_chat(usr, "The Database is not enabled!") + to_chat(usr, SPAN_ADMINNOTICE("The Database is not enabled!"), confidential = TRUE) return GLOB.bunker_passthrough -= ckey(ckeytobypass) SSpersistence.SavePanicBunker() log_admin("[key_name(usr)] has removed [ckeytobypass] from the current round's bunker bypass list.") message_admins("[key_name_admin(usr)] has removed [ckeytobypass] from the current round's bunker bypass list.") - send2irc("Panic Bunker", "[key_name(usr)] has removed [ckeytobypass] from the current round's bunker bypass list.") + send2adminchat("Panic Bunker", "[key_name(usr)] has removed [ckeytobypass] from the current round's bunker bypass list.") /client/proc/paranoia_logging() set category = "Server" set name = "New Player Warnings" - if(!check_rights(R_ADMIN)) return diff --git a/code/modules/admin/verbs/playsound.dm b/code/modules/admin/verbs/playsound.dm index 1bbd1100a693..b4f0317c74dc 100644 --- a/code/modules/admin/verbs/playsound.dm +++ b/code/modules/admin/verbs/playsound.dm @@ -1,6 +1,7 @@ -/client/proc/play_sound(S as sound) +/client/proc/play_sound(sound as sound) set category = "Fun" set name = "Play Global Sound" + set desc = "Play a sound to all connected players." if(!check_rights(R_SOUNDS)) return @@ -12,25 +13,25 @@ freq = 1 vol = clamp(vol, 1, 100) - var/sound/admin_sound = new() - admin_sound.file = S + var/sound/admin_sound = new + admin_sound.file = sound admin_sound.priority = 250 admin_sound.channel = CHANNEL_ADMIN admin_sound.frequency = freq admin_sound.wait = 1 - admin_sound.repeat = 0 + admin_sound.repeat = FALSE admin_sound.status = SOUND_STREAM admin_sound.volume = vol var/res = alert(usr, "Show the title of this song to the players?",, "Yes","No", "Cancel") switch(res) if("Yes") - to_chat(world, "An admin played: [S]") + to_chat(world, SPAN_BOLDANNOUNCE("An admin played: [sound]"), confidential = TRUE) if("Cancel") return - log_admin("[key_name(src)] played sound [S]") - message_admins("[key_name_admin(src)] played sound [S]") + log_admin("[key_name(src)] played sound [sound]") + message_admins("[key_name_admin(src)] played sound [sound]") for(var/mob/M in GLOB.player_list) if(M.get_preference_toggle(/datum/game_preference_toggle/music/admin)) //if(M.client.prefs.toggles & SOUND_MIDI) @@ -40,15 +41,16 @@ feedback_add_details("admin_verb","PGS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/client/proc/play_local_sound(S as sound) +/client/proc/play_local_sound(sound as sound) set category = "Fun" set name = "Play Local Sound" + set desc = "Plays a sound only you can hear." if(!check_rights(R_SOUNDS)) return - log_admin("[key_name(src)] played a local sound [S]") - message_admins("[key_name_admin(src)] played a local sound [S]") - playsound(get_turf(src.mob), S, 50, 0, 0) + log_admin("[key_name(src)] played a local sound [sound]") + message_admins("[key_name_admin(src)] played a local sound [sound]") + playsound(get_turf(src.mob), sound, 50, FALSE, FALSE) feedback_add_details("admin_verb","PLS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/proc/play_web_sound() @@ -237,14 +239,15 @@ /client/proc/stop_sounds() set category = "Debug" set name = "Stop All Playing Sounds" + set desc = "Stops all playing sounds for EVERYONE." if(!src.holder) return log_admin("[key_name(src)] stopped all currently playing sounds.") message_admins("[key_name_admin(src)] stopped all currently playing sounds.") - for(var/mob/M in GLOB.player_list) - SEND_SOUND(M, sound(null)) - var/client/C = M.client - C?.tgui_panel?.stop_music() - // SSblackbox.record_feedback("tally", "admin_verb", 1, "Stop All Playing Sounds") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + for(var/mob/player as anything in GLOB.player_list) + SEND_SOUND(player, sound(null)) + var/client/player_client = player.client + player_client?.tgui_panel?.stop_music() + S_TIMER_COOLDOWN_RESET(SStimer, CD_INTERNET_SOUND) feedback_add_details("admin_verb","SAPS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/code/modules/admin/verbs/possess.dm b/code/modules/admin/verbs/possess.dm index 4fda1c1a7638..78dcbc43c027 100644 --- a/code/modules/admin/verbs/possess.dm +++ b/code/modules/admin/verbs/possess.dm @@ -1,5 +1,6 @@ /proc/possess(obj/O as obj in world) set name = "Possess Obj" + set desc = "Possess an object." set category = VERB_CATEGORY_OBJECT if(!O.loc) @@ -31,6 +32,7 @@ /proc/release(obj/O as obj in world) set name = "Release Obj" + set desc = "Stop possessing an object." set category = VERB_CATEGORY_OBJECT //usr.loc = get_turf(usr) diff --git a/code/modules/admin/verbs/pray.dm b/code/modules/admin/verbs/pray.dm index 2cb95a1feb10..14478ad5a8c3 100644 --- a/code/modules/admin/verbs/pray.dm +++ b/code/modules/admin/verbs/pray.dm @@ -3,14 +3,11 @@ set category = VERB_CATEGORY_IC set name = "Pray" - msg = sanitize(msg) - if(!msg) - return - msg = copytext_char(sanitize(msg), 1, MAX_MESSAGE_LEN) if(!msg) return // log_prayer("[src.key]/([src.name]): [msg]") + if(usr.client) if(usr.client.prefs.muted & MUTE_PRAY) to_chat(usr, SPAN_DANGER("You cannot pray (muted)."), confidential = TRUE) @@ -22,34 +19,31 @@ var/font_color = "purple" var/prayer_type = "PRAYER" var/deity + //TODO: Unshit this when we have some better job and trait systems. @Zandario - // if(usr.job == JOB_CHAPLAIN) if(usr.mind.assigned_role == CHAPLAIN) - cross.icon_state = "bible" + cross.icon_state = "kingyellow" font_color = "blue" prayer_type = "CHAPLAIN PRAYER" if(GLOB.deity) deity = GLOB.deity - // else if(IS_CULTIST(usr)) - // cross.icon_state = "tome" - // font_color = "red" - // prayer_type = "CULTIST PRAYER" - // deity = "Nar'Sie" + // this fucking sucks + else if(usr.mind.special_role == "Cultist") + cross.icon_state = "tome" + font_color = "red" + prayer_type = "CULTIST PRAYER" + deity = "Nar'Sie" else if(isliving(usr)) - // var/mob/living/L = usr - // if(HAS_TRAIT(L, TRAIT_SPIRITUAL)) - if(usr.mind.isholy == TRUE) + if(usr.mind.isholy) cross.icon_state = "holylight" font_color = "blue" prayer_type = "SPIRITUAL PRAYER" var/msg_tmp = msg - // GLOB.requests.pray(usr.client, msg, usr.mind.assigned_role == CHAPLAIN) - msg = SPAN_ADMINNOTICE("[icon2html(cross, GLOB.admins)][SPAN_BOLD("[prayer_type][deity ? " (to [deity])" : ""]: [ADMIN_FULLMONTY(src)] [ADMIN_SC(src)] [ADMIN_ST(src)]:")] [SPAN_LINKIFY(msg)]") - + msg = SPAN_ADMINNOTICE("[icon2html(cross, GLOB.admins)][prayer_type][deity ? " (to [deity])" : ""]: [ADMIN_FULLMONTY(src)] [ADMIN_SC(src)]: [SPAN_LINKIFY(msg)]") for(var/client/C in GLOB.admins) if((R_ADMIN|R_MOD) & C.holder.rights) - to_chat(C, msg) + to_chat(C, msg, type = MESSAGE_TYPE_PRAYER, confidential = TRUE) SEND_SOUND(C, sound('sound/effects/ding.ogg')) to_chat(usr, SPAN_INFO("You pray to the gods: \"[msg_tmp]\""), confidential = TRUE) diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm index 8dabffe7bfbf..97b67f19b001 100644 --- a/code/modules/admin/verbs/randomverbs.dm +++ b/code/modules/admin/verbs/randomverbs.dm @@ -198,23 +198,8 @@ feedback_add_details("admin_verb","GOD") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -/proc/cmd_admin_mute(mob/M as mob, mute_type, automute = 0) - if(automute) - if(!config_legacy.automute_on) - return - else - if(!usr || !usr.client) - return - if(!usr.client.holder) - to_chat(usr, "Error: cmd_admin_mute: You don't have permission to do this.") - return - if(!M.client) - to_chat(usr, "Error: cmd_admin_mute: This mob doesn't have a client tied to it.") - if(M.client.holder) - to_chat(usr, "Error: cmd_admin_mute: You cannot mute an admin/mod.") - if(!M.client) - return - if(M.client.holder) +/proc/cmd_admin_mute(whom, mute_type, automute = 0) + if(!whom) return var/muteunmute @@ -227,27 +212,53 @@ if(MUTE_ADMINHELP) mute_string = "adminhelp, admin PM and ASAY" if(MUTE_DEADCHAT) mute_string = "deadchat and DSAY" if(MUTE_ALL) mute_string = "everything" - else return + else + return + + var/client/C + if(istype(whom, /client)) + C = whom + else if(istext(whom)) + C = GLOB.directory[whom] + else + return + + var/datum/preferences/P + if(C) + P = C.prefs + else + P = GLOB.preferences_datums[whom] + if(!P) + return + + if(automute) + if(!config_legacy.automute_on) + return + else + if(!check_rights()) + return if(automute) muteunmute = "auto-muted" - M.client.prefs.muted |= 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].", 1) - to_chat(M, "You have been [muteunmute] from [mute_string] by the SPAM AUTOMUTE system. Contact an admin.") + P.muted |= mute_type + log_admin("SPAM AUTOMUTE: [muteunmute] [key_name(whom)] from [mute_string]") + message_admins("SPAM AUTOMUTE: [muteunmute] [key_name_admin(whom)] from [mute_string].") + if(C) + to_chat(C, "You have been [muteunmute] from [mute_string] by the SPAM AUTOMUTE system. Contact an admin.", confidential = TRUE) feedback_add_details("admin_verb","AUTOMUTE") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! return - if(M.client.prefs.muted & mute_type) + if(P.muted & mute_type) muteunmute = "unmuted" - M.client.prefs.muted &= ~mute_type + P.muted &= ~mute_type else muteunmute = "muted" - M.client.prefs.muted |= mute_type + P.muted |= mute_type - log_admin("[key_name(usr)] has [muteunmute] [key_name(M)] from [mute_string]") - message_admins("[key_name_admin(usr)] has [muteunmute] [key_name_admin(M)] from [mute_string].", 1) - to_chat(M, "You have been [muteunmute] from [mute_string].") + log_admin("[key_name(usr)] has [muteunmute] [key_name(whom)] from [mute_string]") + message_admins("[key_name_admin(usr)] has [muteunmute] [key_name_admin(whom)] from [mute_string].") + if(C) + to_chat(C, "You have been [muteunmute] from [mute_string] by [key_name(usr, include_name = FALSE)].", confidential = TRUE) feedback_add_details("admin_verb","MUTE") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /client/proc/cmd_admin_add_random_ai_law() diff --git a/code/modules/admin/view_variables/admin_delete.dm b/code/modules/admin/view_variables/admin_delete.dm index 7157a1cce2bd..f8aefd975c22 100644 --- a/code/modules/admin/view_variables/admin_delete.dm +++ b/code/modules/admin/view_variables/admin_delete.dm @@ -5,15 +5,17 @@ if(istype(A)) var/turf/T = get_turf(A) if(T) - coords = "at [COORD(T)]" - jmp_coords = "at [ADMIN_COORDJMP(T)]" + var/atom/a_loc = A.loc + var/is_turf = isturf(a_loc) + coords = "[is_turf ? "at" : "from [a_loc] at"] [AREACOORD(T)]" + jmp_coords = "[is_turf ? "at" : "from [a_loc] at"] [ADMIN_VERBOSEJMP(T)]" else jmp_coords = coords = "in nullspace" - if (alert(src, "Are you sure you want to delete:\n[D]\n[coords]?", "Confirmation", "Yes", "No") == "Yes") + if (alert(usr, "Are you sure you want to delete:\n[D]\n[coords]?", "Confirmation", "Yes", "No") == "Yes") log_admin("[key_name(usr)] deleted [D] [coords]") message_admins("[key_name_admin(usr)] deleted [D] [jmp_coords]") - //SSblackbox.record_feedback("tally", "admin_verb", 1, "Delete") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + if(isturf(D)) var/turf/T = D T.ScrapeAway() diff --git a/code/modules/admin/view_variables/debug_variable_appearance.dm b/code/modules/admin/view_variables/debug_variable_appearance.dm new file mode 100644 index 000000000000..407e347c4875 --- /dev/null +++ b/code/modules/admin/view_variables/debug_variable_appearance.dm @@ -0,0 +1,107 @@ +/// Shows a header name on top when you investigate an appearance/image +/image/vv_get_header() + . = list() + var/icon_name = "[icon || "null"]
" + . += replacetext(icon_name, "icons/obj", "") // shortens the name. We know the path already. + if(icon) + . += icon_state ? "\"[icon_state]\"" : "(icon_state = null)" + +/// Makes nice short vv names for images +/image/debug_variable_value(name, level, datum/owner, sanitize, display_flags) + var/display_name = "[type]" + if("[src]" != "[type]") // If we have a name var, let's use it. + display_name = "[src] [type]" + + var/display_value + var/list/icon_file_name = splittext("[icon]", "/") + if(length(icon_file_name)) + display_value = icon_file_name[length(icon_file_name)] + else + display_value = "null" + + if(icon_state) + display_value = "[display_value]:[icon_state]" + + var/display_ref = get_vv_link_ref() + return "[display_name] ([display_value]) [display_ref]" + +/// Returns the ref string to use when displaying this image in the vv menu of something else +/image/proc/get_vv_link_ref() + return REF(src) + +// It is endlessly annoying to display /appearance directly for stupid byond reasons, so we copy everything we care about into a holder datum +// That we can override procs on and store other vars on and such. +/mutable_appearance/appearance_mirror + // So people can see where it came from + var/appearance_ref + // vis flags can't be displayed by mutable appearances cause it don't make sense as overlays, but appearances do carry them + // can't use the name either for byond reasons + var/_vis_flags + +#if (MIN_COMPILER_VERSION > 515 || MIN_COMPILER_BUILD > 1643) +#warn vis_flags should now be supported by mutable appearances so we can safely remove the weird copying in this code +#endif +// all alone at the end of the universe +GLOBAL_DATUM_INIT(pluto, /atom/movable, new /atom/movable(null)) + +// arg is actually an appearance, typed as mutable_appearance as closest mirror +/mutable_appearance/appearance_mirror/New(mutable_appearance/appearance_father) + . = ..() // /mutable_appearance/New() copies over all the appearance vars MAs care about by default + // We copy over our appearance onto an atom. This is done so we can read vars carried by but not accessible on appearances + GLOB.pluto.appearance = appearance_father + _vis_flags = GLOB.pluto.vis_flags + appearance_ref = REF(appearance_father) + +// This means if the appearance loses refs before a click it's gone, but that's consistent to other datums so it's fine +// Need to ref the APPEARANCE because we just free on our own, which sorta fucks this operation up you know? +/mutable_appearance/appearance_mirror/get_vv_link_ref() + return appearance_ref + +/mutable_appearance/appearance_mirror/can_vv_get(var_name) + var/static/datum/beloved = new() + if(beloved.vars.Find(var_name)) // If datums have it, get out + return FALSE + +#if (MIN_COMPILER_VERSION >= 515 && MIN_COMPILER_BUILD >= 1643) +#warn X/Y/Z and contents are now fully unviewable on our supported versions, remove the below check +#endif + +// lummy removed these from the the MA/image type +#if (DM_VERSION <= 515 && DM_BUILD < 1643) + // Filtering out the stuff I know we don't care about + if(var_name == NAMEOF(src, x)) + return FALSE + if(var_name == NAMEOF(src, y)) + return FALSE + if(var_name == NAMEOF(src, z)) + return FALSE + #ifndef SPACEMAN_DMM // Spaceman doesn't believe in contents on appearances, sorry lads + if(var_name == NAMEOF(src, contents)) + return FALSE + #endif + if(var_name == NAMEOF(src, loc)) + return FALSE +#endif + // Could make an argument for this but I think they will just confuse people, so yeeet + if(var_name == NAMEOF(src, vis_contents)) + return FALSE + return ..() + +/mutable_appearance/appearance_mirror/vv_get_var(var_name) + // No editing for you + var/value = vars[var_name] + return "
  • (READ ONLY) [var_name] = [_debug_variable_value(var_name, value, 0, src, sanitize = TRUE, display_flags = NONE)]
  • " + +/mutable_appearance/appearance_mirror/vv_get_dropdown() + SHOULD_CALL_PARENT(FALSE) + + . = list() + VV_DROPDOWN_OPTION("", "---") + VV_DROPDOWN_OPTION(VV_HK_CALLPROC, "Call Proc") + VV_DROPDOWN_OPTION(VV_HK_MARK, "Mark Object") + // VV_DROPDOWN_OPTION(VV_HK_TAG, "Tag Datum") + VV_DROPDOWN_OPTION(VV_HK_DELETE, "Delete") + VV_DROPDOWN_OPTION(VV_HK_EXPOSE, "Show VV To Player") + +/proc/get_vv_appearance(mutable_appearance/appearance) // actually appearance yadeeyada + return new /mutable_appearance/appearance_mirror(appearance) diff --git a/code/modules/admin/view_variables/debug_variables.dm b/code/modules/admin/view_variables/debug_variables.dm index 28bdaead151c..17a8c99b0b55 100644 --- a/code/modules/admin/view_variables/debug_variables.dm +++ b/code/modules/admin/view_variables/debug_variables.dm @@ -1,84 +1,121 @@ #define VV_HTML_ENCODE(thing) ( sanitize ? html_encode(thing) : thing ) -/proc/debug_variable(name, value, level, datum/D, sanitize = TRUE) //if D is a list, name will be index, and value will be assoc value. - var/header - if(D) - if(islist(D)) - var/list/D_l = D +/// Get displayed variable in VV variable list +/proc/debug_variable(name, value, level, datum/owner, sanitize = TRUE, display_flags = NONE) //if D is a list, name will be index, and value will be assoc value. + if(owner) + if(islist(owner)) + var/list/list_owner = owner var/index = name if (value) - name = D_l[name] //name is really the index until this line + name = list_owner[name] //name is really the index until this line else - value = D_l[name] - header = "
  • ([VV_HREF_TARGET_1V(D, VV_HK_LIST_EDIT, "E", index)]) ([VV_HREF_TARGET_1V(D, VV_HK_LIST_CHANGE, "C", index)]) ([VV_HREF_TARGET_1V(D, VV_HK_LIST_REMOVE, "-", index)]) " + value = list_owner[name] + . = "
  • ([VV_HREF_TARGET_1V(owner, VV_HK_LIST_EDIT, "E", index)]) ([VV_HREF_TARGET_1V(owner, VV_HK_LIST_CHANGE, "C", index)]) ([VV_HREF_TARGET_1V(owner, VV_HK_LIST_REMOVE, "-", index)]) " else - header = "
  • ([VV_HREF_TARGET_1V(D, VV_HK_BASIC_EDIT, "E", name)]) ([VV_HREF_TARGET_1V(D, VV_HK_BASIC_CHANGE, "C", name)]) ([VV_HREF_TARGET_1V(D, VV_HK_BASIC_MASSEDIT, "M", name)]) " + . = "
  • ([VV_HREF_TARGET_1V(owner, VV_HK_BASIC_EDIT, "E", name)]) ([VV_HREF_TARGET_1V(owner, VV_HK_BASIC_CHANGE, "C", name)]) ([VV_HREF_TARGET_1V(owner, VV_HK_BASIC_MASSEDIT, "M", name)]) " else - header = "
  • " + . = "
  • " - var/item - if (isnull(value)) - item = "[VV_HTML_ENCODE(name)] = null" + var/name_part = VV_HTML_ENCODE(name) + if(level > 0 || islist(owner)) //handling keys in assoc lists + if(istype(name,/datum)) + name_part = "[VV_HTML_ENCODE(name)] [REF(name)]" + else if(islist(name)) + var/list/list_value = name + name_part = " /list ([length(list_value)]) [REF(name)]" - else if(IS_APPEARANCE(value)) - item = "[VV_HTML_ENCODE(name)] [ref(value)] = /appearance" + . = "[.][name_part] = " - else if (istext(value)) - item = "[VV_HTML_ENCODE(name)] = \"[VV_HTML_ENCODE(value)]\"" + var/item = _debug_variable_value(name, value, level, owner, sanitize, display_flags) - else if (isicon(value)) + return "[.][item]
  • " + +// This is split into a separate proc mostly to make errors that happen not break things too much +/proc/_debug_variable_value(name, value, level, datum/owner, sanitize, display_flags) + if(isappearance(value)) + value = get_vv_appearance(value) + + . = "DISPLAY_ERROR: ([value] [REF(value)])" // Make sure this line can never runtime + + if(isnull(value)) + return "null" + + if(istext(value)) + return "\"[VV_HTML_ENCODE(value)]\"" + + if(isicon(value)) #ifdef VARSICON - var/icon/I = icon(value) + var/icon/icon_value = icon(value) var/rnd = rand(1,10000) - var/rname = "tmp[REF(I)][rnd].png" - usr << browse_rsc(I, rname) - item = "[VV_HTML_ENCODE(name)] = ([value]) " + var/rname = "tmp[REF(icon_value)][rnd].png" + usr << browse_rsc(icon_value, rname) + return "([value]) " #else - item = "[VV_HTML_ENCODE(name)] = /icon ([value])" + return "/icon ([value])" #endif - else if (isfile(value)) - item = "[VV_HTML_ENCODE(name)] = '[value]'" + if(isfile(value)) + return "'[value]'" - else if (istype(value, /datum)) - var/datum/DV = value - if ("[DV]" != "[DV.type]") //if the thing as a name var, lets use it. - item = "[VV_HTML_ENCODE(name)] [REF(value)] = [DV] [DV.type]" - else - item = "[VV_HTML_ENCODE(name)] [REF(value)] = [DV.type]" + if(isdatum(value)) + var/datum/datum_value = value + return datum_value.debug_variable_value(name, level, owner, sanitize, display_flags) - else if (islist(value)) - var/list/L = value + if(islist(value) || (name in GLOB.vv_special_lists)) // Some special lists aren't detectable as a list through istype + var/list/list_value = value var/list/items = list() - // don't expand if it's: - // 1. overlays - this info is rarely needing to be accessed unless you're doing overlay debugging - // 2. underlays - ditto - // 3. GLOB - there's a metric ton of lists on global variables and we want to avoid admins needing to download MB's of data instantly - // 4. if the list is too long otherwise - if (L.len > 0 && !(name == "underlays" || name == "overlays" || D == GLOB || L.len > (IS_NORMAL_LIST(L) ? VV_NORMAL_LIST_NO_EXPAND_THRESHOLD : VV_SPECIAL_LIST_NO_EXPAND_THRESHOLD))) - for (var/i in 1 to L.len) - var/key = L[i] + + // This is because some lists either don't count as lists or a locate on their ref will return null + var/link_vars = "Vars=[REF(value)]" + if(name in GLOB.vv_special_lists) + link_vars = "Vars=[REF(owner)];special_varname=[name]" + + if (list_value.len > 0 && list_value.len <= (IS_NORMAL_LIST(list_value) ? VV_NORMAL_LIST_NO_EXPAND_THRESHOLD : VV_SPECIAL_LIST_NO_EXPAND_THRESHOLD)) + for (var/i in 1 to list_value.len) + var/key = list_value[i] var/val - if (IS_NORMAL_LIST(L) && !isnum(key)) - val = L[key] - if (isnull(val)) // we still want to display non-null false values, such as 0 or "" + if (IS_NORMAL_LIST(list_value) && !isnum(key)) + val = list_value[key] + if (isnull(val)) // we still want to display non-null false values, such as 0 or "" val = key key = i items += debug_variable(key, val, level + 1, sanitize = sanitize) - item = "[VV_HTML_ENCODE(name)] = /list ([L.len])
      [items.Join()]
    " + return "/list ([list_value.len])
      [items.Join()]
    " else - item = "[VV_HTML_ENCODE(name)] = /list ([L.len])" + return "/list ([list_value.len])" - else if (name in GLOB.bitfields) + if(name in GLOB.bitfields) var/list/flags = list() for (var/i in GLOB.bitfields[name]) if (value & GLOB.bitfields[name][i]) flags += i - item = "[VV_HTML_ENCODE(name)] = [VV_HTML_ENCODE(jointext(flags, ", "))]" + if(length(flags)) + return "[VV_HTML_ENCODE(jointext(flags, ", "))]" + else + return "NONE" else - item = "[VV_HTML_ENCODE(name)] = [VV_HTML_ENCODE(value)]" + return "[VV_HTML_ENCODE(value)]" + +/datum/proc/debug_variable_value(name, level, datum/owner, sanitize, display_flags) + if("[src]" != "[type]") // If we have a name var, let's use it. + return "[src] [type] [REF(src)]" + else + return "[type] [REF(src)]" + +/datum/weakref/debug_variable_value(name, level, datum/owner, sanitize, display_flags) + . = ..() + return "[.] (Resolve)" - return "[header][item]" +/matrix/debug_variable_value(name, level, datum/owner, sanitize, display_flags) + return {" +
    Command Positions
      + + + + + + +
    [a][d]0
    [b][e]0
    [c][f]1
     
    "} //TODO link to modify_transform wrapper for all matrices #undef VV_HTML_ENCODE diff --git a/code/modules/admin/view_variables/filteriffic.dm b/code/modules/admin/view_variables/filteriffic.dm index 747a7b1a9ab8..baaf79df10a3 100644 --- a/code/modules/admin/view_variables/filteriffic.dm +++ b/code/modules/admin/view_variables/filteriffic.dm @@ -53,7 +53,7 @@ target.change_filter_priority(params["name"], new_priority) . = TRUE if("transition_filter_value") - target.transition_filter(params["name"], 4, params["new_data"]) + target.transition_filter(params["name"], params["new_data"], 4) . = TRUE if("modify_filter_value") var/list/old_filter_data = target.filter_data[params["name"]] @@ -69,7 +69,7 @@ if("modify_color_value") var/new_color = input(usr, "Pick new filter color", "Filteriffic Colors!") as color|null if(new_color) - target.transition_filter(params["name"], 4, list("color" = new_color)) + target.transition_filter(params["name"], list("color" = new_color), 4) . = TRUE if("modify_icon_value") var/icon/new_icon = input("Pick icon:", "Icon") as null|icon @@ -78,7 +78,7 @@ target.update_filters() . = TRUE if("mass_apply") - if(!check_rights(R_FUN)) + if(!check_rights_for(usr.client, R_FUN)) to_chat(usr, SPAN_DANGER("Stay in your lane, jannie.")) return var/target_path = text2path(params["path"]) diff --git a/code/modules/admin/view_variables/get_variables.dm b/code/modules/admin/view_variables/get_variables.dm index d864eebd1254..729803e3d5d1 100644 --- a/code/modules/admin/view_variables/get_variables.dm +++ b/code/modules/admin/view_variables/get_variables.dm @@ -1,4 +1,4 @@ -/proc/vv_get_class(var_name, var_value) +/client/proc/vv_get_class(var_name, var_value) if(isnull(var_value)) . = VV_NULL @@ -11,6 +11,8 @@ else if(istext(var_value)) if(findtext(var_value, "\n")) . = VV_MESSAGE + else if(findtext(var_value, GLOB.is_color)) + . = VV_COLOR else . = VV_TEXT @@ -26,7 +28,10 @@ else if(istype(var_value, /client)) . = VV_CLIENT - else if(istype(var_value, /datum)) + else if(isweakref(var_value)) + . = VV_WEAKREF + + else if(isdatum(var_value)) . = VV_DATUM_REFERENCE else if(ispath(var_value)) @@ -38,7 +43,10 @@ . = VV_TYPE else if(islist(var_value)) - . = VV_LIST + if(var_name in GLOB.color_vars) + . = VV_COLOR_MATRIX + else + . = VV_LIST else if(isfile(var_value)) . = VV_FILE @@ -54,6 +62,8 @@ VV_TEXT, VV_MESSAGE, VV_ICON, + VV_COLOR, + VV_COLOR_MATRIX, VV_ATOM_REFERENCE, VV_DATUM_REFERENCE, VV_MOB_REFERENCE, @@ -67,9 +77,11 @@ VV_NEW_TYPE, VV_NEW_LIST, VV_NULL, + VV_INFINITY, VV_RESTORE_DEFAULT, VV_TEXT_LOCATE, VV_PROCCALL_RETVAL, + VV_WEAKREF, ) var/markstring @@ -178,6 +190,19 @@ return .["value"] = things[value] + if(VV_WEAKREF) + var/type = pick_closest_path(FALSE, get_fancy_list_of_datum_types()) + var/subtypes = vv_subtype_prompt(type) + if(subtypes == null) + .["class"] = null + return + var/list/things = vv_reference_list(type, subtypes) + var/value = input("Select reference:", "Reference", current_value) as null|anything in things + if(!value) + .["class"] = null + return + .["value"] = WEAKREF(things[value]) + if(VV_CLIENT) .["value"] = input("Select reference:", "Reference", current_value) as null|anything in GLOB.clients if(.["value"] == null) @@ -205,7 +230,7 @@ if(VV_PROCCALL_RETVAL) var/list/get_retval = list() callproc_blocking(get_retval) - .["value"] = get_retval["VALUE"] //should have been set in proccall! + .["value"] = get_retval[1] //should have been set in proccall! if(.["value"] == null) .["class"] = null return @@ -250,8 +275,23 @@ .["value"] = newguy if(VV_NEW_LIST) - .["value"] = list() .["type"] = /list + var/list/value = list() + + var/expectation = alert("Would you like to populate the list", "Populate List?", "Yes", "No") + if(!expectation || expectation == "No") + .["value"] = value + return . + + var/list/insert = null + while(TRUE) + insert = vv_get_value(restricted_classes = list(VV_RESTORE_DEFAULT)) + if(!insert["class"]) + break + value += LIST_VALUE_WRAP_LISTS(insert["value"]) + + + .["value"] = value if(VV_TEXT_LOCATE) var/datum/D @@ -263,12 +303,23 @@ if(!D) alert("Invalid ref!") continue - if(!istype(D)) - alert("Not a datum.") - continue if(!D.can_vv_mark()) alert("Datum can not be marked!") continue while(!D) .["type"] = D.type .["value"] = D + + if(VV_COLOR) + .["value"] = input("Enter new color:", "Color", current_value) as color|null + if(.["value"] == null) + .["class"] = null + return + + if(VV_COLOR_MATRIX) + .["value"] = open_color_matrix_editor() + if(.["value"] == COLOR_MATRIX_IDENTITY) //identity is equivalent to null + .["class"] = null + + if(VV_INFINITY) + .["value"] = INFINITY diff --git a/code/modules/admin/view_variables/helpers_LEGACY.dm b/code/modules/admin/view_variables/helpers_LEGACY.dm index faa691443ea8..7d1134a5d3b2 100644 --- a/code/modules/admin/view_variables/helpers_LEGACY.dm +++ b/code/modules/admin/view_variables/helpers_LEGACY.dm @@ -1,53 +1,49 @@ - -/datum/proc/get_view_variables_header_legacy() - return "" - /datum/proc/get_view_variables_options_legacy() return "" /mob/get_view_variables_options_legacy() return ..() + {" - + - - - - - - + + + + + + - - + + - - + + - - - - - + + + + + - + - - + + - - - + + + "} /mob/living/carbon/human/get_view_variables_options_legacy() return ..() + {"/ - - - - - + + + + + "} /obj/get_view_variables_options_legacy() return ..() + {" - + "} diff --git a/code/modules/admin/view_variables/mark_datum.dm b/code/modules/admin/view_variables/mark_datum.dm index b608c57ababb..eeed49566209 100644 --- a/code/modules/admin/view_variables/mark_datum.dm +++ b/code/modules/admin/view_variables/mark_datum.dm @@ -2,11 +2,18 @@ if(!holder) return if(holder.marked_datum) + holder.UnregisterSignal(holder.marked_datum, COMSIG_PARENT_QDELETING) vv_update_display(holder.marked_datum, "marked", "") holder.marked_datum = D + holder.RegisterSignal(holder.marked_datum, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/datum/admins, handle_marked_del)) vv_update_display(D, "marked", VV_MSG_MARKED) /client/proc/mark_datum_mapview(datum/D in world) set category = "Debug" set name = "Mark Object" mark_datum(D) + +/datum/admins/proc/handle_marked_del(datum/source) + SIGNAL_HANDLER + UnregisterSignal(marked_datum, COMSIG_PARENT_QDELETING) + marked_datum = null diff --git a/code/modules/admin/view_variables/mass_edit_variables.dm b/code/modules/admin/view_variables/mass_edit_variables.dm index 86f9f78e0137..29a710377917 100644 --- a/code/modules/admin/view_variables/mass_edit_variables.dm +++ b/code/modules/admin/view_variables/mass_edit_variables.dm @@ -8,7 +8,7 @@ if(!check_rights(R_VAREDIT)) return - if(A && A.type) + if(A?.type) method = vv_subtype_prompt(A.type) src.massmodify_variables(A, var_name, method) @@ -38,7 +38,7 @@ var/var_value = O.vars[variable] if(variable in GLOB.VVckey_edit) - to_chat(src, "It's forbidden to mass-modify ckeys. It'll crash everyone's client you dummy.") + to_chat(src, "It's forbidden to mass-modify ckeys. It'll crash everyone's client you dummy.", confidential = TRUE) return if(variable in GLOB.VVlocked) if(!check_rights(R_DEBUG)) @@ -56,11 +56,11 @@ default = vv_get_class(variable, var_value) if(isnull(default)) - to_chat(src, "Unable to determine variable type.") + to_chat(src, "Unable to determine variable type.", confidential = TRUE) else - to_chat(src, "Variable appears to be [uppertext(default)].") + to_chat(src, "Variable appears to be [uppertext(default)].", confidential = TRUE) - to_chat(src, "Variable contains: [var_value]") + to_chat(src, "Variable contains: [var_value]", confidential = TRUE) if(default == VV_NUM) var/dir_text = "" @@ -75,7 +75,7 @@ dir_text += "WEST" if(dir_text) - to_chat(src, "If a direction, direction is: [dir_text]") + to_chat(src, "If a direction, direction is: [dir_text]", confidential = TRUE) var/value = vv_get_value(default_class = default) var/new_value = value["value"] @@ -97,9 +97,9 @@ switch(class) if(VV_RESTORE_DEFAULT) - to_chat(src, "Finding items...") + to_chat(src, "Finding items...", confidential = TRUE) var/list/items = get_all_of_type(O.type, method) - to_chat(src, "Changing [items.len] items...") + to_chat(src, "Changing [items.len] items...", confidential = TRUE) for(var/thing in items) if (!thing) continue @@ -114,8 +114,8 @@ var/list/varsvars = vv_parse_text(O, new_value) var/pre_processing = new_value var/unique - if (varsvars && varsvars.len) - unique = alert(usr, "Process vars unique to each instance, or same for all?", "Variable Association", "Unique", "Same") + if (varsvars?.len) + unique = tgui_alert(usr, "Process vars unique to each instance, or same for all?", "Variable Association", list("Unique", "Same")) if(unique == "Unique") unique = TRUE else @@ -123,9 +123,9 @@ for(var/V in varsvars) new_value = replacetext(new_value,"\[[V]]","[O.vars[V]]") - to_chat(src, "Finding items...") + to_chat(src, "Finding items...", confidential = TRUE) var/list/items = get_all_of_type(O.type, method) - to_chat(src, "Changing [items.len] items...") + to_chat(src, "Changing [items.len] items...", confidential = TRUE) for(var/thing in items) if (!thing) continue @@ -151,9 +151,9 @@ many = FALSE var/type = value["type"] - to_chat(src, "Finding items...") + to_chat(src, "Finding items...", confidential = TRUE) var/list/items = get_all_of_type(O.type, method) - to_chat(src, "Changing [items.len] items...") + to_chat(src, "Changing [items.len] items...", confidential = TRUE) for(var/thing in items) if (!thing) continue @@ -169,9 +169,9 @@ CHECK_TICK else - to_chat(src, "Finding items...") + to_chat(src, "Finding items...", confidential = TRUE) var/list/items = get_all_of_type(O.type, method) - to_chat(src, "Changing [items.len] items...") + to_chat(src, "Changing [items.len] items...", confidential = TRUE) for(var/thing in items) if (!thing) continue @@ -185,13 +185,13 @@ var/count = rejected+accepted if (!count) - to_chat(src, "No objects found") + to_chat(src, "No objects found", confidential = TRUE) return if (!accepted) - to_chat(src, "Every object rejected your edit") + to_chat(src, "Every object rejected your edit", confidential = TRUE) return if (rejected) - to_chat(src, "[rejected] out of [count] objects rejected your edit") + to_chat(src, "[rejected] out of [count] objects rejected your edit", confidential = TRUE) log_world("### MassVarEdit by [src]: [O.type] (A/R [accepted]/[rejected]) [variable]=[html_encode("[O.vars[variable]]")]([list2params(value)])") log_admin("[key_name(src)] mass modified [original_name]'s [variable] to [O.vars[variable]] ([accepted] objects modified)") diff --git a/code/modules/admin/view_variables/modify_variables.dm b/code/modules/admin/view_variables/modify_variables.dm index 976085f10200..b91870aa5472 100644 --- a/code/modules/admin/view_variables/modify_variables.dm +++ b/code/modules/admin/view_variables/modify_variables.dm @@ -1,10 +1,10 @@ -GLOBAL_LIST_INIT(VVlocked, list("vars", "datum_flags", "client", "mob")) //Requires DEBUG +GLOBAL_LIST_INIT(VVlocked, list("vars", "datum_flags", "client", "mob")) //Requires DEBUG GLOBAL_PROTECT(VVlocked) -GLOBAL_LIST_INIT(VVicon_edit_lock, list("icon", "icon_state", "overlays", "underlays")) //Requires DEBUG or FUN +GLOBAL_LIST_INIT(VVicon_edit_lock, list("icon", "icon_state", "overlays", "underlays")) //Requires DEBUG or FUN GLOBAL_PROTECT(VVicon_edit_lock) -GLOBAL_LIST_INIT(VVckey_edit, list("key", "ckey")) //Requires DEBUG or SPAWN +GLOBAL_LIST_INIT(VVckey_edit, list("key", "ckey")) //Requires DEBUG or SPAWN GLOBAL_PROTECT(VVckey_edit) -GLOBAL_LIST_INIT(VVpixelmovement, list("bound_x", "bound_y", "step_x", "step_y", "step_size", "bound_height", "bound_width", "bounds")) +GLOBAL_LIST_INIT(VVpixelmovement, list("bound_x", "bound_y", "step_x", "step_y", "step_size", "bound_height", "bound_width", "bounds")) //No editing ever. GLOBAL_PROTECT(VVpixelmovement) /client/proc/vv_parse_text(O, new_var) @@ -17,13 +17,13 @@ GLOBAL_PROTECT(VVpixelmovement) //FALSE = no subtypes, strict exact type pathing (or the type doesn't have subtypes) //TRUE = Yes subtypes //NULL = User cancelled at the prompt or invalid type given -/client/proc/vv_subtype_prompt(var/type) +/client/proc/vv_subtype_prompt(type) if (!ispath(type)) return var/list/subtypes = subtypesof(type) if (!subtypes || !subtypes.len) return FALSE - if (subtypes && subtypes.len) + if (subtypes?.len) switch(alert("Strict object type detection?", "Type detection", "Strictly this type","This type and subtypes", "Cancel")) if("Strictly this type") return FALSE @@ -50,14 +50,14 @@ GLOBAL_PROTECT(VVpixelmovement) var/datum/D = thing i++ //try one of 3 methods to shorten the type text: - // fancy type, - // fancy type with the base type removed from the begaining, - // the type with the base type removed from the begaining + // fancy type, + // fancy type with the base type removed from the begaining, + // the type with the base type removed from the begaining var/fancytype = types[D.type] if (findtext(fancytype, types[type])) - fancytype = copytext(fancytype, length(types[type])+1) - var/shorttype = copytext("[D.type]", length("[type]")+1) - if (length(shorttype) > length(fancytype)) + fancytype = copytext(fancytype, length(types[type]) + 1) + var/shorttype = copytext("[D.type]", length("[type]") + 1) + if (length_char(shorttype) > length_char(fancytype)) shorttype = fancytype if (!length(shorttype)) shorttype = "/" @@ -95,15 +95,14 @@ GLOBAL_PROTECT(VVpixelmovement) if (O) L = L.Copy() - L.len++ - L[L.len] = var_value + L += list(var_value) //var_value could be a list switch(alert("Would you like to associate a value with the list entry?",,"Yes","No")) if("Yes") L[var_value] = mod_list_add_ass(O) //hehe if (O) if (O.vv_edit_var(objectvar, L) == FALSE) - to_chat(src, "Your edit was rejected by the object.") + to_chat(src, "Your edit was rejected by the object.", confidential = TRUE) return log_world("### ListVarEdit by [src]: [(O ? O.type : "/list")] [objectvar]: ADDED=[var_value]") log_admin("[key_name(src)] modified [original_name]'s [objectvar]: ADDED=[var_value]") @@ -113,7 +112,7 @@ GLOBAL_PROTECT(VVpixelmovement) if(!check_rights(R_VAREDIT)) return if(!istype(L, /list)) - to_chat(src, "Not a List.") + to_chat(src, "Not a List.", confidential = TRUE) return if(L.len > 1000) @@ -145,7 +144,7 @@ GLOBAL_PROTECT(VVpixelmovement) L = L.Copy() listclearnulls(L) if (!O.vv_edit_var(objectvar, L)) - to_chat(src, "Your edit was rejected by the object.") + to_chat(src, "Your edit was rejected by the object.", confidential = TRUE) return log_world("### ListVarEdit by [src]: [O.type] [objectvar]: CLEAR NULLS") log_admin("[key_name(src)] modified [original_name]'s [objectvar]: CLEAR NULLS") @@ -155,7 +154,7 @@ GLOBAL_PROTECT(VVpixelmovement) if(variable == "(CLEAR DUPES)") L = uniqueList(L) if (!O.vv_edit_var(objectvar, L)) - to_chat(src, "Your edit was rejected by the object.") + to_chat(src, "Your edit was rejected by the object.", confidential = TRUE) return log_world("### ListVarEdit by [src]: [O.type] [objectvar]: CLEAR DUPES") log_admin("[key_name(src)] modified [original_name]'s [objectvar]: CLEAR DUPES") @@ -165,7 +164,7 @@ GLOBAL_PROTECT(VVpixelmovement) if(variable == "(SHUFFLE)") L = shuffle(L) if (!O.vv_edit_var(objectvar, L)) - to_chat(src, "Your edit was rejected by the object.") + to_chat(src, "Your edit was rejected by the object.", confidential = TRUE) return log_world("### ListVarEdit by [src]: [O.type] [objectvar]: SHUFFLE") log_admin("[key_name(src)] modified [original_name]'s [objectvar]: SHUFFLE") @@ -187,7 +186,7 @@ GLOBAL_PROTECT(VVpixelmovement) assoc_key = L[index] var/default var/variable - var/old_assoc_value //EXPERIMENTAL - Keep old associated value while modifying key, if any + var/old_assoc_value //EXPERIMENTAL - Keep old associated value while modifying key, if any if(is_normal_list) if (assoc) variable = L[assoc_key] @@ -202,9 +201,9 @@ GLOBAL_PROTECT(VVpixelmovement) default = vv_get_class(objectvar, variable) - to_chat(src, "Variable appears to be [uppertext(default)].") + to_chat(src, "Variable appears to be [uppertext(default)].", confidential = TRUE) - to_chat(src, "Variable contains: [variable]") + to_chat(src, "Variable contains: [variable]", confidential = TRUE) if(default == VV_NUM) var/dir_text = "" @@ -220,7 +219,7 @@ GLOBAL_PROTECT(VVpixelmovement) dir_text += "WEST" if(dir_text) - to_chat(usr, "If a direction, direction is: [dir_text]") + to_chat(usr, "If a direction, direction is: [dir_text]", confidential = TRUE) var/original_var = variable @@ -248,7 +247,7 @@ GLOBAL_PROTECT(VVpixelmovement) L.Cut(index, index+1) if (O) if (O.vv_edit_var(objectvar, L)) - to_chat(src, "Your edit was rejected by the object.") + to_chat(src, "Your edit was rejected by the object.", confidential = TRUE) return log_world("### ListVarEdit by [src]: [O.type] [objectvar]: REMOVED=[html_encode("[original_var]")]") log_admin("[key_name(src)] modified [original_name]'s [objectvar]: REMOVED=[original_var]") @@ -270,7 +269,7 @@ GLOBAL_PROTECT(VVpixelmovement) L[new_var] = old_assoc_value if (O) if (O.vv_edit_var(objectvar, L) == FALSE) - to_chat(src, "Your edit was rejected by the object.") + to_chat(src, "Your edit was rejected by the object.", confidential = TRUE) return log_world("### ListVarEdit by [src]: [(O ? O.type : "/list")] [objectvar]: [original_var]=[new_var]") log_admin("[key_name(src)] modified [original_name]'s [objectvar]: [original_var]=[new_var]") @@ -298,7 +297,7 @@ GLOBAL_PROTECT(VVpixelmovement) if(param_var_name) if(!(param_var_name in O.vars)) - to_chat(src, "A variable with this name ([param_var_name]) doesn't exist in this datum ([O])") + to_chat(src, "A variable with this name ([param_var_name]) doesn't exist in this datum ([O])", confidential = TRUE) return variable = param_var_name @@ -323,11 +322,11 @@ GLOBAL_PROTECT(VVpixelmovement) var/default = vv_get_class(variable, var_value) if(isnull(default)) - to_chat(src, "Unable to determine variable type.") + to_chat(src, "Unable to determine variable type.", confidential = TRUE) else - to_chat(src, "Variable appears to be [uppertext(default)].") + to_chat(src, "Variable appears to be [uppertext(default)].", confidential = TRUE) - to_chat(src, "Variable contains: [var_value]") + to_chat(src, "Variable contains: [var_value]", confidential = TRUE) if(default == VV_NUM) var/dir_text = "" @@ -342,7 +341,7 @@ GLOBAL_PROTECT(VVpixelmovement) dir_text += "WEST" if(dir_text) - to_chat(src, "If a direction, direction is: [dir_text]") + to_chat(src, "If a direction, direction is: [dir_text]", confidential = TRUE) if(autodetect_class && default != VV_NULL) if (default == VV_TEXT) @@ -379,7 +378,7 @@ GLOBAL_PROTECT(VVpixelmovement) if (O.vv_edit_var(variable, var_new) == FALSE) - to_chat(src, "Your edit was rejected by the object.") + to_chat(src, "Your edit was rejected by the object.", confidential = TRUE) return vv_update_display(O, "varedited", VV_MSG_EDITED) log_world("### VarEdit by [key_name(src)]: [O.type] [variable]=[var_value] => [var_new]") diff --git a/code/modules/admin/view_variables/topic.dm b/code/modules/admin/view_variables/topic.dm index 9917921e1d8b..6288122c784b 100644 --- a/code/modules/admin/view_variables/topic.dm +++ b/code/modules/admin/view_variables/topic.dm @@ -5,59 +5,105 @@ return var/target = GET_VV_TARGET vv_do_basic(target, href_list, href) - if(istype(target, /datum)) + if(isdatum(target)) var/datum/D = target D.vv_do_topic(href_list) else if(islist(target)) vv_do_list(target, href_list) if(href_list["Vars"]) - debug_variables(locate(href_list["Vars"])) + var/datum/vars_target = locate(href_list["Vars"]) + if(href_list["special_varname"]) // Some special vars can't be located even if you have their ref, you have to use this instead + vars_target = vars_target.vars[href_list["special_varname"]] + debug_variables(vars_target) - //~CARN: for renaming mobs (updates their name, real_name, mind.name, their ID/PDA and datacore records). - else if(href_list["rename"]) - if(!check_rights(R_VAREDIT)) return +//Stuff below aren't in dropdowns/etc. - var/mob/M = locate(href_list["rename"]) - if(!istype(M)) - to_chat(usr, "This can only be used on instances of type /mob") - return + if(check_rights(R_VAREDIT)) - var/new_name = sanitize(input(usr,"What would you like to name this mob?","Input a name",M.real_name) as text|null, MAX_NAME_LEN) - if( !new_name || !M ) return + //~CARN: for renaming mobs (updates their name, real_name, mind.name, their ID/PDA and datacore records). - message_admins("Admin [key_name_admin(usr)] renamed [key_name_admin(M)] to [new_name].") - M.fully_replace_character_name(M.real_name,new_name) - href_list["datumrefresh"] = href_list["rename"] + if(href_list["rename"]) + if(!check_rights(NONE)) + return - else if(href_list["varnameedit"] && href_list["datumedit"]) - if(!check_rights(R_VAREDIT)) return + var/mob/M = locate(href_list["rename"]) in GLOB.mob_list + if(!istype(M)) + to_chat(usr, "This can only be used on instances of type /mob", confidential = TRUE) + return - var/D = locate(href_list["datumedit"]) - if(!istype(D,/datum) && !istype(D,/client)) - to_chat(usr, "This can only be used on instances of types /client or /datum") - return + var/new_name = stripped_input(usr,"What would you like to name this mob?","Input a name",M.real_name,MAX_NAME_LEN) - modify_variables(D, href_list["varnameedit"], 1) + if( !new_name || !M ) + return - else if(href_list["varnamechange"] && href_list["datumchange"]) - if(!check_rights(R_VAREDIT)) return + message_admins("Admin [key_name_admin(usr)] renamed [key_name_admin(M)] to [new_name].") + M.fully_replace_character_name(M.real_name,new_name) + vv_update_display(M, "name", new_name) + vv_update_display(M, "real_name", M.real_name || "No real name") - var/D = locate(href_list["datumchange"]) - if(!istype(D,/datum) && !istype(D,/client)) - to_chat(usr, "This can only be used on instances of types /client or /datum") - return + else if(href_list["rotatedatum"]) + if(!check_rights(NONE)) + return + + var/atom/A = locate(href_list["rotatedatum"]) + if(!istype(A)) + to_chat(usr, "This can only be done to instances of type /atom", confidential = TRUE) + return + + switch(href_list["rotatedir"]) + if("right") + A.setDir(turn(A.dir, -45)) + if("left") + A.setDir(turn(A.dir, 45)) + vv_update_display(A, "dir", dir2text(A.dir)) + + else if(href_list["adjustDamage"] && href_list["mobToDamage"]) + if(!check_rights(NONE)) + return + + var/mob/living/L = locate(href_list["mobToDamage"]) in GLOB.mob_list + if(!istype(L)) + return + + var/Text = href_list["adjustDamage"] + + var/amount = input("Deal how much damage to mob? (Negative values here heal)","Adjust [Text]loss",0) as num|null + + if (isnull(amount)) + return + + if(!L) + to_chat(usr, "Mob doesn't exist anymore", confidential = TRUE) + return + + switch(Text) + if("brute") amount > 0? L.take_overall_damage(brute = amount) : L.heal_overall_damage(brute = -amount) + if("fire") amount > 0? L.take_overall_damage(burn = amount) : L.heal_overall_damage(burn = -amount) + if("toxin") L.adjustToxLoss(amount) + if("oxygen")L.adjustOxyLoss(amount) + if("brain") L.adjustBrainLoss(amount) + if("clone") L.adjustCloneLoss(amount) + else + to_chat(usr, "You caused an error. DEBUG: Text:[Text] Mob:[L]", confidential = TRUE) + return - modify_variables(D, href_list["varnamechange"], 0) + if(amount != 0) + var/log_msg = "[key_name(usr)] dealt [amount] amount of [Text] damage to [key_name(L)]" + message_admins("[key_name(usr)] dealt [amount] amount of [Text] damage to [ADMIN_LOOKUPFLW(L)]") + log_admin(log_msg) + admin_ticket_log(L, "[log_msg]") + href_list["datumrefresh"] = href_list["mobToDamage"] - else if(href_list["varnamemass"] && href_list["datummass"]) + // weirdly tg removed this but still use it on atom_vv + if(href_list["varnameedit"] && href_list["datumedit"]) if(!check_rights(R_VAREDIT)) return - var/atom/A = locate(href_list["datummass"]) - if(!istype(A)) - to_chat(usr, "This can only be used on instances of type /atom") + var/D = locate(href_list["datumedit"]) + if(!istype(D,/datum) && !istype(D,/client)) + to_chat(usr, "This can only be used on instances of types /client or /datum") return - cmd_mass_modify_object_variables(A, href_list["varnamemass"]) + modify_variables(D, href_list["varnameedit"], 1) else if(href_list["mob_player_panel"]) if(!check_rights(0)) return @@ -234,30 +280,6 @@ src.cmd_admin_emp(A) href_list["datumrefresh"] = href_list["emp"] - else if(href_list["mark_object"]) - if(!check_rights(0)) return - - var/datum/D = locate(href_list["mark_object"]) - if(!istype(D)) - to_chat(usr, "This can only be done to instances of type /datum") - return - - src.holder.marked_datum = D - href_list["datumrefresh"] = href_list["mark_object"] - - else if(href_list["rotatedatum"]) - if(!check_rights(0)) return - - var/atom/A = locate(href_list["rotatedatum"]) - if(!istype(A)) - to_chat(usr, "This can only be done to instances of type /atom") - return - - switch(href_list["rotatedir"]) - if("right") A.setDir(turn(A.dir, -45)) - if("left") A.setDir(turn(A.dir, 45)) - href_list["datumrefresh"] = href_list["rotatedatum"] - else if(href_list["makemonkey"]) if(!check_rights(R_SPAWN)) return @@ -482,60 +504,8 @@ return M.regenerate_icons() - else if(href_list["adjustDamage"] && href_list["mobToDamage"]) - if(!check_rights(R_DEBUG|R_ADMIN|R_FUN|R_EVENT)) return - - var/mob/living/L = locate(href_list["mobToDamage"]) - if(!istype(L)) return - - var/Text = href_list["adjustDamage"] - - var/amount = input(usr, "Deal how much damage to mob? (Negative values here heal)","Adjust [Text]loss",0) as num - - if(!L) - to_chat(usr, "Mob doesn't exist anymore") - return - - switch(Text) - if("brute") amount > 0? L.take_overall_damage(brute = amount) : L.heal_overall_damage(brute = -amount) - if("fire") amount > 0? L.take_overall_damage(burn = amount) : L.heal_overall_damage(burn = -amount) - if("toxin") L.adjustToxLoss(amount) - if("oxygen")L.adjustOxyLoss(amount) - if("brain") L.adjustBrainLoss(amount) - if("clone") L.adjustCloneLoss(amount) - else - to_chat(usr, "You caused an error. DEBUG: Text:[Text] Mob:[L]") - return - - if(amount != 0) - log_admin("[key_name(usr)] dealt [amount] amount of [Text] damage to [L]") - message_admins("[key_name(usr)] dealt [amount] amount of [Text] damage to [L]") - href_list["datumrefresh"] = href_list["mobToDamage"] - - else if(href_list["expose"]) - if(!check_rights(R_ADMIN, FALSE)) - return - var/thing = locate(href_list["expose"]) - if(!thing) //Do NOT QDELETED check! - return - var/value = vv_get_value(VV_CLIENT) - if (value["class"] != VV_CLIENT) - return - var/client/C = value["value"] - if (!C) - return - var/prompt = tgui_alert(usr, "Do you want to grant [C] access to view this VV window? (they will not be able to edit or change anysrc nor open nested vv windows unless they themselves are an admin)", "Confirm", list("Yes", "No")) - if (prompt != "Yes") - return - if(!thing) - to_chat(usr, SPAN_WARNING("The object you tried to expose to [C] no longer exists (GC'd)")) - return - message_admins("[key_name_admin(usr)] Showed [key_name_admin(C)] a VV window") - log_admin("Admin [key_name(usr)] Showed [key_name(C)] a VV window of a [src]") - to_chat(C, "[is_under_stealthmin() ? "an Administrator" : "[usr.client.key]"] has granted you access to view a View Variables window") - C.debug_variables(thing) - + //Finally, refresh if something modified the list. if(href_list["datumrefresh"]) var/datum/DAT = locate(href_list["datumrefresh"]) - if(istype(DAT, /datum) || istype(DAT, /client) || islist(DAT)) + if(isdatum(DAT) || istype(DAT, /client) || islist(DAT)) debug_variables(DAT) diff --git a/code/modules/admin/view_variables/topic_basic.dm b/code/modules/admin/view_variables/topic_basic.dm index 98a5b3f999c4..fa99bafd8f8c 100644 --- a/code/modules/admin/view_variables/topic_basic.dm +++ b/code/modules/admin/view_variables/topic_basic.dm @@ -21,6 +21,7 @@ var/mob/living/L = target if(istype(L)) vv_update_display(target, "real_name", L.real_name || "No real name") + if(href_list[VV_HK_BASIC_CHANGE]) modify_variables(target, target_var, 0) if(href_list[VV_HK_BASIC_MASSEDIT]) @@ -34,42 +35,45 @@ if (!C) return if(!target) - to_chat(usr, "The object you tried to expose to [C] no longer exists (nulled or hard-deled)") + to_chat(usr, SPAN_WARNING("The object you tried to expose to [C] no longer exists (nulled or hard-deled)"), confidential = TRUE) return message_admins("[key_name_admin(usr)] Showed [key_name_admin(C)] a VV window") log_admin("Admin [key_name(usr)] Showed [key_name(C)] a VV window of a [target]") - to_chat(C, "[is_under_stealthmin() ? "an Administrator" : "[usr.client.key]"] has granted you access to view a View Variables window") + to_chat(C, "[is_under_stealthmin() ? "an Administrator" : "[usr.client.key]"] has granted you access to view a View Variables window", confidential = TRUE) C.debug_variables(target) if(check_rights(R_DEBUG)) if(href_list[VV_HK_DELETE]) usr.client.admin_delete(target) - if (isturf(src)) // show the turf that took its place - usr.client.debug_variables(src) + if (isturf(target)) // show the turf that took its place + usr.client.debug_variables(target) return - if(href_list[VV_HK_VIEW_APPEARANCE]) - var/appearance/A = locate(href_list[VV_HK_VIEW_APPEARANCE]) - if(!A || !IS_APPEARANCE(A)) - to_chat(usr, SPAN_WARNING("Invalid ref: [href_list[VV_HK_VIEW_APPEARANCE]]")) - return - usr.client.debug_variables(A) + if(href_list[VV_HK_MARK]) usr.client.mark_datum(target) if(href_list[VV_HK_ADDCOMPONENT]) if(!check_rights(NONE)) return var/list/names = list() - var/list/componentsubtypes = subtypesof(/datum/component) + var/list/componentsubtypes = sortList(subtypesof(/datum/component), GLOBAL_PROC_REF(cmp_typepaths_asc)) + names += "---Components---" names += componentsubtypes - names += subtypesof(/datum/element) - var/result = tgui_input_list(usr, "Choose a component/element to add", "better know what ur fuckin doin pal", names) - if(!usr || !result || result == "---Components---" || result == "---Elements---") + names += "---Elements---" + names += sortList(subtypesof(/datum/element), GLOBAL_PROC_REF(cmp_typepaths_asc)) + + var/result = tgui_input_list(usr, "Choose a component/element to add", "Add Component", names) + if(isnull(result)) + return + if(!usr || result == "---Components---" || result == "---Elements---") return + if(QDELETED(src)) - to_chat(usr, "That thing doesn't exist anymore!") + to_chat(usr, "That thing doesn't exist anymore!", confidential = TRUE) return + var/list/lst = get_callproc_args() if(!lst) return + lst.Insert(1, result) var/datumname = "error" if(result in componentsubtypes) @@ -78,7 +82,8 @@ else datumname = "element" target._AddElement(lst) - log_admin("[key_name(usr)] has added [result] [datumname] to [key_name(src)].") - message_admins("[key_name_admin(usr)] has added [result] [datumname] to [target] ([ADMIN_VV(target)]).") + log_admin("[key_name(usr)] has added [result] [datumname] to [key_name(target)].") + message_admins(SPAN_NOTICE("[key_name_admin(usr)] has added [result] [datumname] to [key_name_admin(target)].")) + if(href_list[VV_HK_CALLPROC]) usr.client.callproc_datum(target) diff --git a/code/modules/admin/view_variables/view_variables.dm b/code/modules/admin/view_variables/view_variables.dm index ac03d8543b71..71efd2bb27fc 100644 --- a/code/modules/admin/view_variables/view_variables.dm +++ b/code/modules/admin/view_variables/view_variables.dm @@ -1,130 +1,128 @@ -// todo: refactor number.. 4? -// thise is all snowflakey. -/client/proc/debug_variables(datum/D in world) +#define ICON_STATE_CHECKED 1 /// this dmi is checked. We don't check this one anymore. +#define ICON_STATE_NULL 2 /// this dmi has null-named icon_state, allowing it to show a sprite on vv editor. + +/client/proc/debug_variables(datum/thing in world) set category = "Debug" set name = "View Variables" //set src in world var/static/cookieoffset = rand(1, 9999) //to force cookies to reset after the round. - if(!usr.client || !usr.client.holder) //This is usr because admins can call the proc on other clients, even if they're not admins, to show them VVs. - to_chat(usr, "You need to be an administrator to access this.") + if(!usr.client || !usr.client.holder) //This is usr because admins can call the proc on other clients, even if they're not admins, to show them VVs. + to_chat(usr, SPAN_DANGER("You need to be an administrator to access this."), confidential = TRUE) return - if(!D) + if(!thing) return - var/vtype - var/type - var/refid = REF(D) - var/ref = ref(D) - var/list/header - var/title = "#unkw" - var/list/dropdownoptions - // welcome to yanderedev - // but i assure you this is necessary unless we switch(typeid). - // vv refactor #4 when? - if(isdatum(D)) - vtype = VVING_A_DATUM - type = D.type - header = D.vv_get_header() - if(refid == ref) - title = "[D] ([ref]) = [type]" - else - title = "[D] ([refid]/[ref]) = [type]" - dropdownoptions = D.vv_get_dropdown() - dropdownoptions += D.get_view_variables_options_legacy() - header += D.get_view_variables_header_legacy() - else if(islist(D)) - vtype = VVING_A_LIST - type = /list - header = list("/list [ref]") - title = "/list [ref]" - dropdownoptions = list( - "---", - "Add Item" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_LIST_ADD), - "Remove Nulls" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_LIST_ERASE_NULLS), - "Remove Dupes" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_LIST_ERASE_DUPES), - "Set len" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_LIST_SET_LENGTH), - "Shuffle" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_LIST_SHUFFLE), - "Show VV To Player" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_EXPOSE), - "---" - ) - for(var/i in 1 to length(dropdownoptions)) - var/name = dropdownoptions[i] - var/link = dropdownoptions[name] - dropdownoptions[i] = "" - else if(IS_APPEARANCE(D)) - vtype = VVING_A_APPEARANCE - type = /appearance - header = list("virtual appearance [ref]") - title = "virtual appearance [ref]" - dropdownoptions = list() - else - to_chat(usr, "Invalid vtype.") + if(isappearance(thing)) + thing = get_vv_appearance(thing) // this is /mutable_appearance/our_bs_subtype + var/islist = islist(thing) || (!isdatum(thing) && hascall(thing, "Cut")) // Some special lists don't count as lists, but can be detected by if they have list procs + if(!islist && !isdatum(thing)) return + var/title = "" + var/refid = REF(thing) var/icon/sprite var/hash + var/type = islist ? /list : thing.type var/no_icon = FALSE - if(istype(D, /atom)) - sprite = get_flat_icon(D) - if(sprite) - hash = md5(sprite) - src << browse_rsc(sprite, "vv[hash].png") - else + if(isatom(thing)) + sprite = get_flat_icon(thing) + if(!sprite) no_icon = TRUE - var/formatted_type = replacetext("[type]", "/", "/") + else if(isimage(thing)) + // icon_state=null shows first image even if dmi has no icon_state for null name. + // This list remembers which dmi has null icon_state, to determine if icon_state=null should display a sprite + // (NOTE: icon_state="" is correct, but saying null is obvious) + var/static/list/dmi_nullstate_checklist = list() + var/image/image_object = thing + var/icon_filename_text = "[image_object.icon]" // "icon(null)" type can exist. textifying filters it. + if(icon_filename_text) + if(image_object.icon_state) + sprite = icon(image_object.icon, image_object.icon_state) + + else // it means: icon_state="" + if(!dmi_nullstate_checklist[icon_filename_text]) + dmi_nullstate_checklist[icon_filename_text] = ICON_STATE_CHECKED + if("" in icon_states(image_object.icon)) + // this dmi has nullstate. We'll allow "icon_state=null" to show image. + dmi_nullstate_checklist[icon_filename_text] = ICON_STATE_NULL + + if(dmi_nullstate_checklist[icon_filename_text] == ICON_STATE_NULL) + sprite = icon(image_object.icon, image_object.icon_state) var/sprite_text if(sprite) - sprite_text = no_icon? "\[NO ICON\]" : "" + hash = md5(sprite) + src << browse_rsc(sprite, "vv[hash].png") + sprite_text = no_icon ? "\[NO ICON\]" : "" + + title = "[thing] ([REF(thing)]) = [type]" + var/formatted_type = replacetext("[type]", "/", "/") + + var/list/header = islist ? list("/list") : thing.vv_get_header() + + var/ref_line = "@[copytext(refid, 2, -1)]" // get rid of the brackets, add a @ prefix for copy pasting in asay var/marked_line - if(holder && holder.marked_datum && holder.marked_datum == D) + if(holder && holder.marked_datum && holder.marked_datum == thing) marked_line = VV_MSG_MARKED var/varedited_line - if(vtype == VVING_A_DATUM && (D.datum_flags & DF_VAR_EDITED)) + if(!islist && (thing.datum_flags & DF_VAR_EDITED)) varedited_line = VV_MSG_EDITED var/deleted_line - if(vtype == VVING_A_DATUM && D.gc_destroyed) + if(!islist && thing.gc_destroyed) deleted_line = VV_MSG_DELETED + var/list/dropdownoptions + if (islist) + dropdownoptions = list( + "---", + "Add Item" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_LIST_ADD), + "Remove Nulls" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_LIST_ERASE_NULLS), + "Remove Dupes" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_LIST_ERASE_DUPES), + "Set len" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_LIST_SET_LENGTH), + "Shuffle" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_LIST_SHUFFLE), + "Show VV To Player" = VV_HREF_TARGETREF_INTERNAL(refid, VV_HK_EXPOSE), + "---" + ) + for(var/i in 1 to length(dropdownoptions)) + var/name = dropdownoptions[i] + var/link = dropdownoptions[name] + dropdownoptions[i] = "" + else + dropdownoptions = thing.vv_get_dropdown() + dropdownoptions += thing.get_view_variables_options_legacy() // ew + var/list/names = list() + if(!islist) + for(var/varname in thing.vars) + names += varname + + sleep(1 TICKS) + var/list/variable_html = list() - switch(vtype) - if(VVING_A_DATUM) - for(var/V in D.vars) - names += V - if(VVING_A_LIST) - if(VVING_A_APPEARANCE) - for(var/V in global._appearance_var_list) - names += V - sleep(1) - switch(vtype) - if(VVING_A_DATUM) - names = sortList(names) - for(var/V in names) - if(D.can_vv_get(V)) - variable_html += D.vv_get_var(V, TRUE) - if(VVING_A_LIST) - var/list/L = D - for(var/i in 1 to L.len) - var/key = L[i] - var/value - if(IS_NORMAL_LIST(L) && IS_VALID_ASSOC_KEY(key)) - value = L[key] - variable_html += debug_variable(i, value, 0, L) - if(VVING_A_APPEARANCE) - // lol, lmao - for(var/V in names) - variable_html += __appearance_v_debug(D, V) + if(islist) + var/list/list_value = thing + for(var/i in 1 to list_value.len) + var/key = list_value[i] + var/value + if(IS_NORMAL_LIST(list_value) && IS_VALID_ASSOC_KEY(key)) + value = list_value[key] + variable_html += debug_variable(i, value, 0, list_value) + else + names = sortList(names) + for(var/varname in names) + if(thing.can_vv_get(varname)) + variable_html += thing.vv_get_var(varname) var/html = {" + [title] @@ -155,8 +182,8 @@ var ca = document.cookie.split(';'); for(var i=0; i
    [formatted_type] +
    [ref_line] [marked_line] [varedited_line] [deleted_line] @@ -304,5 +332,8 @@ datumrefresh=[refid];[HrefToken()]'>Refresh "} src << browse(html, "window=variables[refid];size=475x650") -/client/proc/vv_update_display(datum/D, span, content) - src << output("[span]:[content]", "variables[REF(D)].browser:replace_span") +/client/proc/vv_update_display(datum/thing, span, content) + src << output("[span]:[content]", "variables[REF(thing)].browser:replace_span") + +#undef ICON_STATE_CHECKED +#undef ICON_STATE_NULL diff --git a/code/modules/client/client.dm b/code/modules/client/client.dm index 7c36b27ebb5c..87acf68e3bff 100644 --- a/code/modules/client/client.dm +++ b/code/modules/client/client.dm @@ -152,8 +152,8 @@ var/last_message = "" ///contins a number of how many times a message identical to last_message was sent. var/last_message_count = 0 - ///Internal counter for clients sending irc relay messages via ahelp to prevent spamming. Set to a number every time an admin reply is sent, decremented for every client send. - var/ircreplyamount = 0 + ///Internal counter for clients sending external (IRC/Discord) relay messages via ahelp to prevent spamming. Set to a number every time an admin reply is sent, decremented for every client send. + var/externalreplyamount = 0 ///////// //OTHER// diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 138744a62a74..c53aeb969aba 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -89,6 +89,10 @@ if(CONFIG_GET(flag/emergency_tgui_logging)) log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]") return + // Route statpanel + if(href_list["statpanel"]) + _statpanel_act(href_list["statpanel"], href_list) + return //? Normal HREF handling go below @@ -99,29 +103,17 @@ if(on_topic_hook(href, href_list, hsrc)) return - // Route statpanel - if(href_list["statpanel"]) - _statpanel_act(href_list["statpanel"], href_list) - return - //Admin PM if(href_list["priv_msg"]) - var/client/C = locate(href_list["priv_msg"]) - if(ismob(C)) //Old stuff can feed-in mobs instead of clients - var/mob/M = C - C = M.client - cmd_admin_pm(C,null) + cmd_admin_pm(href_list["priv_msg"],null) + return + if (href_list["player_ticket_panel"]) + view_latest_ticket() return - // Depricated. go use TGS - if(href_list["irc_msg"]) - if(!holder && received_irc_pm < world.time - 6000) //Worse they can do is spam IRC 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 IRC has responded to you") - return - if(mute_irc) - to_chat(usr, "") - return - send2irc("AHELP", href_list["irc_msg"]) + // TGUIless adminhelp + if(href_list["tguiless_adminhelp"]) + no_tgui_adminhelp(input(src, "Enter your ahelp", "Ahelp") as null|message) return switch(href_list["_src_"]) @@ -326,8 +318,7 @@ if(holder) add_admin_verbs() admin_memo_show() - // to_chat(src, get_message_output("memo")) - // adminGreet() + adminGreet() if(custom_event_msg && custom_event_msg != "") to_chat(src, "

    Custom Event

    ") @@ -431,6 +422,7 @@ if(holder) holder.owner = null GLOB.admins -= src //delete them on the managed one too + handle_admin_logout() active_mousedown_item = null @@ -606,3 +598,32 @@ GLOBAL_VAR_INIT(log_clicks, FALSE) /client/proc/AnnouncePR(announcement) to_chat(src, announcement) +/// Handles any "fluff" or supplementary procedures related to an admin logout event. Should not have anything critically related cleaning up an admin's logout. +/client/proc/handle_admin_logout() + adminGreet(logout = TRUE) + if(length(GLOB.admins) > 0 || !SSticker.IsRoundInProgress()) // We only want to report this stuff if we are currently playing. + return + + var/list/message_to_send = list() + var/static/list/cheesy_messages = null + + if (isnull(cheesy_messages)) + cheesy_messages = list( + "Forever alone :(", + "I have no admins online!", + "I need a hug :(", + "I need someone on me :(", + "I want a man :(", + "I'm all alone :(", + "I'm feeling lonely :(", + "I'm so lonely :(", + "Someone come hold me :(", + "What happened? Where has everyone gone?", + "Where has everyone gone?", + "Why does nobody love me? :(", + ) + + message_to_send += pick(cheesy_messages) + message_to_send += "(No admins online)" + + send2adminchat("Server", jointext(message_to_send, " ")) diff --git a/code/modules/client/spam_prevention.dm b/code/modules/client/spam_prevention.dm index 70e13361b11a..e9ea7a0f926a 100644 --- a/code/modules/client/spam_prevention.dm +++ b/code/modules/client/spam_prevention.dm @@ -7,7 +7,7 @@ if(world.time - last_message_time < spam_delay) spam_alert++ if(spam_alert > 5) - cmd_admin_mute(src.mob, mute_type, 1) + cmd_admin_mute(src, mute_type, 1) else spam_alert = max(0, spam_alert--) last_message_time = world.time diff --git a/code/modules/discord/discord_embed.dm b/code/modules/discord/discord_embed.dm new file mode 100644 index 000000000000..cae7d9ade6ad --- /dev/null +++ b/code/modules/discord/discord_embed.dm @@ -0,0 +1,80 @@ +/// Documentation for the embed object and all of its variables can be found at +/// https://discord.com/developers/docs/resources/channel#embed-object +/// It is recommended to read the documentation on the discord website, as the information below could become outdated in the future. +/datum/discord_embed + /// Title of the embed + var/title + /// The description + var/description + /// The URL that the title + var/url + /// The colour that appears on the top of the embed. This is an integer and is the color code of the embed. + var/color + /// The footer that appears on the embed + var/footer + /// String representing a link to an image + var/image + /// String representing a link to the thumbnail image + var/thumbnail + /// String representing a link to the video + var/video + /// String representing the name of the provider + var/provider + /// String representing the link of the provider + var/provider_url + /// Name of the author of the embed + var/author + /// A key-value string list of fields that should be displayed + var/list/fields + /// Any content that should appear above the embed + var/content + +/datum/discord_embed/proc/convert_to_list() + if(color && !isnum(color)) + CRASH("Color on [type] is not a number! Expected a number, got [color] instead.") + var/list/data_to_list = list() + if(title) + data_to_list["title"] = title + if(description) + var/new_desc = replacetext(replacetext(description, "\proper", ""), "\improper", "") + new_desc = GLOB.has_discord_embeddable_links.Replace(replacetext(new_desc, "`", ""), " ```$1``` ") + data_to_list["description"] = new_desc + if(url) + data_to_list["url"] = url + if(color) + data_to_list["color"] = color + if(footer) + data_to_list["footer"] = list( + "text" = footer, + ) + if(image) + data_to_list["image"] = list( + "url" = image, + ) + if(thumbnail) + data_to_list["thumbnail"] = list( + "url" = thumbnail, + ) + if(video) + data_to_list["video"] = list( + "url" = video, + ) + if(provider) + data_to_list["provider"] = list( + "name" = provider, + "url" = provider_url, + ) + if(author) + data_to_list["author"] = list( + "author" = author, + ) + if(fields) + data_to_list["fields"] = list() + for(var/data as anything in fields) + if(!fields[data]) + continue + data_to_list["fields"] += list(list( + "name" = data, + "value" = GLOB.has_discord_embeddable_links.Replace(replacetext(fields[data], "`", ""), " ```$1``` "), + )) + return data_to_list diff --git a/code/modules/keybindings/keybind/client.dm b/code/modules/keybindings/keybind/client.dm index 84d844e9a476..38185e709c24 100644 --- a/code/modules/keybindings/keybind/client.dm +++ b/code/modules/keybindings/keybind/client.dm @@ -9,7 +9,7 @@ description = "Ask an admin for help." /datum/keybinding/client/admin_help/down(client/user) - user.get_adminhelp() + GLOB.admin_help_ui_handler.ui_interact(user.mob) return TRUE /datum/keybinding/client/screenshot diff --git a/code/modules/mob/living/vv.dm b/code/modules/mob/living/vv.dm index d002ab76e787..06db7b323ce3 100644 --- a/code/modules/mob/living/vv.dm +++ b/code/modules/mob/living/vv.dm @@ -2,14 +2,14 @@ /mob/living/vv_get_header() . = ..() . += {" - [src] -
    [ckey ? ckey : "No ckey"] / [real_name ? real_name : "No real name"] + [src] +
    [ckey ? ckey : "No ckey"] / [real_name ? real_name : "No real name"]
    - BRUTE:[getBruteLoss()] - FIRE:[getFireLoss()] - TOXIN:[getToxLoss()] - OXY:[getOxyLoss()] - CLONE:[getCloneLoss()] - BRAIN:[getBrainLoss()] + BRUTE:[getBruteLoss()] + FIRE:[getFireLoss()] + TOXIN:[getToxLoss()] + OXY:[getOxyLoss()] + CLONE:[getCloneLoss()] + BRAIN:[getBrainLoss()]
    "} diff --git a/code/modules/mob/logout.dm b/code/modules/mob/logout.dm index 6096b0519ac6..811e5000003b 100644 --- a/code/modules/mob/logout.dm +++ b/code/modules/mob/logout.dm @@ -34,13 +34,8 @@ active_storage?.hide(src) update_client_z(null) log_access_out(src) - if(admin_datums[src.ckey]) - if (SSticker && SSticker.current_state == GAME_STATE_PLAYING) //Only report this stuff if we are currently playing. - var/admins_number = GLOB.admins.len - message_admins("Admin logout: [key_name(src)]") - if(admins_number == 0) //Apparently the admin logging out is no longer an admin at this point, so we have to check this towards 0 and not towards 1. Awell. - send2irc("LOGOUT", "[key_name(src)] logged out - no more admins online.") + // admin logout shenanigans moved to client destroy! // unrender rendering systems dispose_rendering() diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index b81b51c2bd56..e4f2599b380e 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -82,9 +82,6 @@ return SUIT_SENSOR_OFF -/proc/is_admin(var/mob/user) - return check_rights(R_ADMIN, 0, user) != 0 - /** * Returns true if the user should have admin AI level access *! TO-BE-DEPRICATED diff --git a/config/entries/general.txt b/config/entries/general.txt index eccbea9f051c..874b9bc061f1 100644 --- a/config/entries/general.txt +++ b/config/entries/general.txt @@ -17,6 +17,9 @@ MINIMAPS_ENABLED ## The default value assumes youtube-dl is in your system PATH # INVOKE_YOUTUBEDL youtube-dl +## Uncomment to show the names of the admin sending a pm from IRC instead of showing as a stealthmin. +#SHOW_IRC_NAME + ## CLIENT VERSION CONTROL ## This allows you to configure the minimum required client version, as well as a warning version, and message for both. ## These trigger for any version below (non-inclusive) the given version, so 510 triggers on 509 or lower. @@ -40,3 +43,50 @@ ALLOW_ADMIN_OOCCOLOR ## Even if this is enabled, a notification will only be sent if there are no clients connected. TOAST_NOTIFICATION_ON_INIT +## Game Chat Message Options +## Various messages to be sent to connected chat channels. +## Uncommenting these will enable them, by default they will be broadcast to Game chat channels on TGS3 or non-admin channels on TGS>=4. +## If using TGS>=4, the string option can be set as one of more chat channel tags (separated by ','s) to limit the message to channels with that tag name (case-sensitive). This will have no effect on TGS3. +## i.e. CHANNEL_ANNOUNCE_NEW_GAME chat_channel_tag + +## Which channel will have a message about a new game starting, message includes the station name. +#CHANNEL_ANNOUNCE_NEW_GAME + +## Which channel will have a message about a new game starting, message includes the round ID of the game that has just ended. +#CHANNEL_ANNOUNCE_END_GAME + +## Ping users who use the `notify` command when a new game starts. +#CHAT_NEW_GAME_NOTIFICATIONS + +## Allow admin hrefs that don't use the new token system, will eventually be removed +#DEBUG_ADMIN_HREFS + +## The URL to the webhook for adminhelps to relay the urgent ahelp message to +## If not set, will disable urgent ahelps. +#URGENT_ADMINHELP_WEBHOOK_URL + +## See above, but for non-urgent ahelps +#REGULAR_ADMINHELP_WEBHOOK_URL + +## The urgent ahelp cooldown for a given player if they're alone on a server and need to send an urgent ahelp. +URGENT_AHELP_COOLDOWN 300 + +## The message that is sent to the discord if an urgent ahelp is sent. Useful for sending a role ping. +#URGENT_AHELP_MESSAGE + +## See above, but for non-urgent ahelps. +#AHELP_MESSAGE + +## The message that the player receives whenever prompted whether they want to send an urgent ahelp or not. +#URGENT_AHELP_USER_PROMPT This'll ping the admins! + +## The link that the title of a ticket can link to. +## If not set, it will link nowhere +## use $RID for the round ID and $TID for the ticket ID +#ADMINHELP_AHELP_LINK + +## The URL for the pfp of the webhook +#ADMINHELP_WEBHOOK_PFP + +## The name of the webhook +#ADMINHELP_WEBHOOK_NAME diff --git a/sound/misc/asay_ping.ogg b/sound/misc/asay_ping.ogg new file mode 100644 index 000000000000..02fb83ad9f9a Binary files /dev/null and b/sound/misc/asay_ping.ogg differ diff --git a/tgui/packages/tgui-panel/chat/constants.js b/tgui/packages/tgui-panel/chat/constants.js index cb32ac7db484..b3aee12a61d2 100644 --- a/tgui/packages/tgui-panel/chat/constants.js +++ b/tgui/packages/tgui-panel/chat/constants.js @@ -26,13 +26,13 @@ export const MESSAGE_TYPE_LOCALCHAT = 'localchat'; export const MESSAGE_TYPE_RADIO = 'radio'; export const MESSAGE_TYPE_INFO = 'info'; export const MESSAGE_TYPE_WARNING = 'warning'; -export const MESSAGE_TYPE_HELPFUL = 'helpful'; export const MESSAGE_TYPE_DEADCHAT = 'deadchat'; export const MESSAGE_TYPE_OOC = 'ooc'; export const MESSAGE_TYPE_ADMINPM = 'adminpm'; export const MESSAGE_TYPE_COMBAT = 'combat'; export const MESSAGE_TYPE_ADMINCHAT = 'adminchat'; export const MESSAGE_TYPE_MODCHAT = 'modchat'; +export const MESSAGE_TYPE_PRAYER = 'prayer'; export const MESSAGE_TYPE_EVENTCHAT = 'eventchat'; export const MESSAGE_TYPE_ADMINLOG = 'adminlog'; export const MESSAGE_TYPE_ATTACKLOG = 'attacklog'; @@ -109,12 +109,6 @@ export const MESSAGE_TYPES = [ description: 'Urist McTraitor has stabbed you with a knife!', selector: '.danger', }, - { - type: MESSAGE_TYPE_HELPFUL, - name: 'Helpful', - description: 'PFC Unga Dunga starts feeding you a pill.', - selector: '.helpful', - }, { type: MESSAGE_TYPE_UNKNOWN, name: 'Unsorted', @@ -135,6 +129,12 @@ export const MESSAGE_TYPES = [ selector: '.mod_channel', admin: true, }, + { + type: MESSAGE_TYPE_PRAYER, + name: 'Prayers', + description: 'Prayers from players', + admin: true, + }, { type: MESSAGE_TYPE_ADMINLOG, name: 'Admin Log', diff --git a/tgui/packages/tgui-panel/styles/components/Notifications.scss b/tgui/packages/tgui-panel/styles/components/Notifications.scss index 6b5160f07839..f3669967fd70 100644 --- a/tgui/packages/tgui-panel/styles/components/Notifications.scss +++ b/tgui/packages/tgui-panel/styles/components/Notifications.scss @@ -5,7 +5,7 @@ .Notifications { position: absolute; - bottom: 1em; + top: 1em; left: 1em; right: 2em; } diff --git a/tgui/packages/tgui-panel/styles/themes/light.scss b/tgui/packages/tgui-panel/styles/themes/light.scss index 41cd8c888b56..27bca3c54382 100644 --- a/tgui/packages/tgui-panel/styles/themes/light.scss +++ b/tgui/packages/tgui-panel/styles/themes/light.scss @@ -22,7 +22,7 @@ $color-fg: #000000, $color-bg: #eeeeee, $color-bg-section: #ffffff, - $color-bg-grad-spread: 0%, + $color-bg-grad-spread: 0% ); // A fat warning to anyone who wants to use this: this only half works. @@ -32,23 +32,34 @@ @include meta.load-css('~tgui/styles/atomic/color.scss'); // Components - @include meta.load-css('~tgui/styles/components/Tabs.scss', $with: ( - 'text-color': rgba(0, 0, 0, 0.5), - 'color-default': rgba(0, 0, 0, 1), - )); + @include meta.load-css( + '~tgui/styles/components/Tabs.scss', + $with: ( + 'text-color': rgba(0, 0, 0, 0.5), + 'color-default': rgba(0, 0, 0, 1), + 'tab-color-selected': rgba(0, 0, 0, 0.125), + 'tab-color-hovered': rgba(0, 0, 0, 0.075) + ) + ); @include meta.load-css('~tgui/styles/components/Section.scss'); - @include meta.load-css('~tgui/styles/components/Button.scss', $with: ( - 'color-default': #bbbbbb, - 'color-disabled': #363636, - 'color-selected': #0668b8, - 'color-caution': #be6209, - 'color-danger': #9a9d00, - 'color-transparent-text': rgba(0, 0, 0, 0.5), - )); - @include meta.load-css('~tgui/styles/components/Input.scss', $with: ( - 'border-color': colors.fg(colors.$label), - 'background-color': #ffffff, - )); + @include meta.load-css( + '~tgui/styles/components/Button.scss', + $with: ( + 'color-default': #bbbbbb, + 'color-disabled': #363636, + 'color-selected': #0668b8, + 'color-caution': #be6209, + 'color-danger': #9a9d00, + 'color-transparent-text': rgba(0, 0, 0, 0.5) + ) + ); + @include meta.load-css( + '~tgui/styles/components/Input.scss', + $with: ( + 'border-color': colors.fg(colors.$label), + 'background-color': #e6e6e6 + ) + ); @include meta.load-css('~tgui/styles/components/NumberInput.scss'); @include meta.load-css('~tgui/styles/components/TextArea.scss'); @include meta.load-css('~tgui/styles/components/Knob.scss'); @@ -56,21 +67,26 @@ @include meta.load-css('~tgui/styles/components/ProgressBar.scss'); // Components specific to tgui-panel - @include meta.load-css('../components/Chat.scss', $with: ( - 'text-color': #000000, - )); + @include meta.load-css( + '../components/Chat.scss', + $with: ('text-color': #000000) + ); // Layouts - @include meta.load-css('~tgui/styles/layouts/Layout.scss', $with: ( - 'scrollbar-color-multiplier': -1, - )); + @include meta.load-css( + '~tgui/styles/layouts/Layout.scss', + $with: ('scrollbar-color-multiplier': -1) + ); @include meta.load-css('~tgui/styles/layouts/Window.scss'); - @include meta.load-css('~tgui/styles/layouts/TitleBar.scss', $with: ( - 'text-color': rgba(0, 0, 0, 0.75), - 'background-color': base.$color-bg, - 'shadow-color-core': rgba(0, 0, 0, 0.25), - )); + @include meta.load-css( + '~tgui/styles/layouts/TitleBar.scss', + $with: ( + 'text-color': rgba(0, 0, 0, 0.75), + 'background-color': base.$color-bg, + 'shadow-color-core': rgba(0, 0, 0, 0.25) + ) + ); - // Goonchat styles + // tgchat styles @include meta.load-css('../goon/chat-light.scss'); } diff --git a/tgui/packages/tgui/interfaces/unused/Adminhelp.tsx b/tgui/packages/tgui/interfaces/Adminhelp.tsx similarity index 66% rename from tgui/packages/tgui/interfaces/unused/Adminhelp.tsx rename to tgui/packages/tgui/interfaces/Adminhelp.tsx index f161358b0088..d02cfebfcc07 100644 --- a/tgui/packages/tgui/interfaces/unused/Adminhelp.tsx +++ b/tgui/packages/tgui/interfaces/Adminhelp.tsx @@ -1,14 +1,21 @@ +import { useBackend, useLocalState } from "../backend"; +import { + Box, + Button, + Input, + NoticeBox, + Stack, + TextArea, +} from "../components"; import { BooleanLike } from "common/react"; -import { useBackend, useLocalState } from "../../backend"; -import { TextArea, Stack, Button, NoticeBox, Input, Box } from "../../components"; -import { Window } from "../../layouts"; +import { Window } from "../layouts"; type AdminhelpData = { - adminCount: number, - urgentAhelpEnabled: BooleanLike, - bannedFromUrgentAhelp: BooleanLike, - urgentAhelpPromptMessage: string, -} + adminCount: number; + urgentAhelpEnabled: BooleanLike; + bannedFromUrgentAhelp: BooleanLike; + urgentAhelpPromptMessage: string; +}; export const Adminhelp = (props, context) => { const { act, data } = useBackend(context); @@ -22,16 +29,14 @@ export const Adminhelp = (props, context) => { const [currentlyInputting, setCurrentlyInputting] = useLocalState(context, "confirm_request", false); const [ahelpMessage, setAhelpMessage] = useLocalState(context, "ahelp_message", ""); - const confirmationText = "alert admins"; + const confirmationText = 'alert admins'; return ( - - + +