Files
Bubberstation/code/modules/client/verbs/ooc.dm
T
san7890 912e843f53 Allows Export of your Preferences JSON File (#75014)
## About The Pull Request

Hey there,

This was spoken about in #70492 (specifically
https://github.com/tgstation/tgstation/pull/70492#issuecomment-1278069607),
and I have been waiting for this to be implemented for some time. It
never got implemented, so I decided to code it myself.

Basically, **if the server host doesn't disable it**, you are free to
export your JSONs as a player, right from the stat-panel. It's a pretty
JSON on 515 versions, too!

It's right here:


![image](https://user-images.githubusercontent.com/34697715/235251447-1c977718-51fd-4025-8d89-c60bffc379ec.png)

Here's what the prettified JSON looks like on 515.


![image](https://user-images.githubusercontent.com/34697715/235321061-4a217e26-c082-4bba-b54a-2c780defda0a.png)

There's a cooldown (default to 10 seconds) between exporting your
preferences.

#### Why is this config?

It's because in the past, a server host could always just file-share the
.sav or .json or whatever to the player, but they would have to do the
explicit option of actually bothering to make the files accessible to
the player. In that same line of logic, the server operator will have to
explicitly make the files accessible. This is mostly because I'm not
sure how good `ftp()` is at being a player function and wanted to have
some sort of cap/control somehow in case an exploit vector is detected
or it's just plain spammed by bots, so we'll just leave it up to the
direct providers of this data to elect if they wish to provide the data
or not.
## Why It's Good For The Game

Players don't have to log into Server A to remember what hairstyle they
loved using when they want to swap to Server B! That's amazing actually.
I always forget what ponytail my character has, and it'll be nice to
have the hairstyle in a readily accessible place (after I prettify the
JSON for myself).

It's also more convenient for server hosts to make player data like this
accessible if they really want to, too.

If we ever add an _import_ feature in the future (which would have to be
done with a LOT of care), this will also be useful. I wouldn't advise it
though having taken a precursory look at how much goes into it while
trying to ascertain the scope of this PR.
## Changelog
🆑
qol: The game now supports export of your preferences into a JSON file!
The verb (export-preferences) should now be available in the OOC tab of
your stat-panel if enabled by server operators.
server: Exporting player preferences is controlled by a configuration
option, 'FORBID_PREFERENCES_EXPORT'. If you do not wish to let clients
access the ftp() function to their own preferences file (probably for
bandwidth reasons?) you should uncomment this or add it to your config
somehow.
config: Server operators are also able to set the cooldown between
requests to download the JSON Preferences file via the
'SECONDS_COOLDOWN_FOR_PREFERENCES_EXPORT' config option.
/🆑
2023-05-02 08:22:44 -07:00

461 lines
17 KiB
Plaintext

GLOBAL_VAR_INIT(OOC_COLOR, null)//If this is null, use the CSS for OOC. Otherwise, use a custom colour.
GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8")
///talking in OOC uses this
/client/verb/ooc(msg as text)
set name = "OOC" //Gave this shit a shorter name so you only have to time out "ooc" rather than "ooc message" to use it --NeoFite
set category = "OOC"
if(GLOB.say_disabled) //This is here to try to identify lag problems
to_chat(usr, span_danger("Speech is currently admin-disabled."))
return
if(!mob)
return
VALIDATE_CLIENT(src)
if(!holder)
if(!GLOB.ooc_allowed)
to_chat(src, span_danger("OOC is globally muted."))
return
if(!GLOB.dooc_allowed && (mob.stat == DEAD))
to_chat(usr, span_danger("OOC for dead mobs has been turned off."))
return
if(prefs.muted & MUTE_OOC)
to_chat(src, span_danger("You cannot use OOC (muted)."))
return
if(is_banned_from(ckey, "OOC"))
to_chat(src, span_danger("You have been banned from OOC."))
return
if(QDELETED(src))
return
msg = trim(copytext_char(sanitize(msg), 1, MAX_MESSAGE_LEN))
var/raw_msg = msg
var/list/filter_result = is_ooc_filtered(msg)
if (!CAN_BYPASS_FILTER(usr) && filter_result)
REPORT_CHAT_FILTER_TO_USER(usr, filter_result)
log_filter("OOC", msg, filter_result)
return
// Protect filter bypassers from themselves.
// Demote hard filter results to soft filter results if necessary due to the danger of accidentally speaking in OOC.
var/list/soft_filter_result = filter_result || is_soft_ooc_filtered(msg)
if (soft_filter_result)
if(tgui_alert(usr,"Your message contains \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\". \"[soft_filter_result[CHAT_FILTER_INDEX_REASON]]\", Are you sure you want to say it?", "Soft Blocked Word", list("Yes", "No")) != "Yes")
return
message_admins("[ADMIN_LOOKUPFLW(usr)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term. Message: \"[msg]\"")
log_admin_private("[key_name(usr)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term. Message: \"[msg]\"")
if(!msg)
return
msg = emoji_parse(msg)
if(SSticker.HasRoundStarted() && (msg[1] in list(".",";",":","#") || findtext_char(msg, "say", 1, 5)))
if(tgui_alert(usr,"Your message \"[raw_msg]\" looks like it was meant for in game communication, say it in OOC?", "Meant for OOC?", list("Yes", "No")) != "Yes")
return
if(!holder)
if(handle_spam_prevention(msg,MUTE_OOC))
return
if(findtext(msg, "byond://"))
to_chat(src, span_boldannounce("<B>Advertising other servers is not allowed.</B>"))
log_admin("[key_name(src)] has attempted to advertise in OOC: [msg]")
message_admins("[key_name_admin(src)] has attempted to advertise in OOC: [msg]")
return
if(!(prefs.chat_toggles & CHAT_OOC))
to_chat(src, span_danger("You have OOC muted."))
return
mob.log_talk(raw_msg, LOG_OOC)
var/keyname = key
if(prefs.unlock_content)
if(prefs.toggles & MEMBER_PUBLIC)
keyname = "<font color='[prefs.read_preference(/datum/preference/color/ooc_color) || GLOB.normal_ooc_colour]'>[icon2html('icons/ui_icons/chat/member_content.dmi', world, "blag")][keyname]</font>"
if(prefs.hearted)
var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/chat)
keyname = "[sheet.icon_tag("emoji-heart")][keyname]"
//The linkify span classes and linkify=TRUE below make ooc text get clickable chat href links if you pass in something resembling a url
for(var/client/receiver as anything in GLOB.clients)
if(!receiver.prefs) // Client being created or deleted. Despite all, this can be null.
continue
if(!(receiver.prefs.chat_toggles & CHAT_OOC))
continue
if(holder?.fakekey in receiver.prefs.ignoring)
continue
var/avoid_highlight = receiver == src
if(holder)
if(!holder.fakekey || receiver.holder)
if(check_rights_for(src, R_ADMIN))
var/ooc_color = prefs.read_preference(/datum/preference/color/ooc_color)
to_chat(receiver, span_adminooc("[CONFIG_GET(flag/allow_admin_ooccolor) && ooc_color ? "<font color=[ooc_color]>" :"" ][span_prefix("OOC:")] <EM>[keyname][holder.fakekey ? "/([holder.fakekey])" : ""]:</EM> <span class='message linkify'>[msg]</span>"), avoid_highlighting = avoid_highlight)
else
to_chat(receiver, span_adminobserverooc(span_prefix("OOC:</span> <EM>[keyname][holder.fakekey ? "/([holder.fakekey])" : ""]:</EM> <span class='message linkify'>[msg]")), avoid_highlighting = avoid_highlight)
else
if(GLOB.OOC_COLOR)
to_chat(receiver, "<span class='oocplain'><font color='[GLOB.OOC_COLOR]'><b>[span_prefix("OOC:")] <EM>[holder.fakekey ? holder.fakekey : key]:</EM> <span class='message linkify'>[msg]</span></b></font></span>", avoid_highlighting = avoid_highlight)
else
to_chat(receiver, span_ooc(span_prefix("OOC:</span> <EM>[holder.fakekey ? holder.fakekey : key]:</EM> <span class='message linkify'>[msg]")), avoid_highlighting = avoid_highlight)
else if(!(key in receiver.prefs.ignoring))
if(GLOB.OOC_COLOR)
to_chat(receiver, "<span class='oocplain'><font color='[GLOB.OOC_COLOR]'><b>[span_prefix("OOC:")] <EM>[keyname]:</EM> <span class='message linkify'>[msg]</span></b></font></span>", avoid_highlighting = avoid_highlight)
else
to_chat(receiver, span_ooc(span_prefix("OOC:</span> <EM>[keyname]:</EM> <span class='message linkify'>[msg]")), avoid_highlighting = avoid_highlight)
/proc/toggle_ooc(toggle = null)
if(toggle != null) //if we're specifically en/disabling ooc
if(toggle != GLOB.ooc_allowed)
GLOB.ooc_allowed = toggle
else
return
else //otherwise just toggle it
GLOB.ooc_allowed = !GLOB.ooc_allowed
to_chat(world, "<span class='oocplain'><B>The OOC channel has been globally [GLOB.ooc_allowed ? "enabled" : "disabled"].</B></span>")
/proc/toggle_dooc(toggle = null)
if(toggle != null)
if(toggle != GLOB.dooc_allowed)
GLOB.dooc_allowed = toggle
else
return
else
GLOB.dooc_allowed = !GLOB.dooc_allowed
/client/proc/set_ooc()
set name = "Set Player OOC Color"
set desc = "Modifies player OOC Color"
set category = "Server"
if(IsAdminAdvancedProcCall())
return
var/newColor = input(src, "Please select the new player OOC color.", "OOC color") as color|null
if(isnull(newColor))
return
if(!check_rights(R_FUN))
message_admins("[usr.key] has attempted to use the Set Player OOC Color verb!")
log_admin("[key_name(usr)] tried to set player ooc color without authorization.")
return
var/new_color = sanitize_color(newColor)
message_admins("[key_name_admin(usr)] has set the players' ooc color to [new_color].")
log_admin("[key_name_admin(usr)] has set the player ooc color to [new_color].")
GLOB.OOC_COLOR = new_color
/client/proc/reset_ooc()
set name = "Reset Player OOC Color"
set desc = "Returns player OOC Color to default"
set category = "Server"
if(IsAdminAdvancedProcCall())
return
if(tgui_alert(usr, "Are you sure you want to reset the OOC color of all players?", "Reset Player OOC Color", list("Yes", "No")) != "Yes")
return
if(!check_rights(R_FUN))
message_admins("[usr.key] has attempted to use the Reset Player OOC Color verb!")
log_admin("[key_name(usr)] tried to reset player ooc color without authorization.")
return
message_admins("[key_name_admin(usr)] has reset the players' ooc color.")
log_admin("[key_name_admin(usr)] has reset player ooc color.")
GLOB.OOC_COLOR = null
//Checks admin notice
/client/verb/admin_notice()
set name = "Adminnotice"
set category = "Admin"
set desc ="Check the admin notice if it has been set"
if(GLOB.admin_notice)
to_chat(src, "[span_boldnotice("Admin Notice:")]\n \t [GLOB.admin_notice]")
else
to_chat(src, span_notice("There are no admin notices at the moment."))
/client/verb/motd()
set name = "MOTD"
set category = "OOC"
set desc ="Check the Message of the Day"
var/motd = global.config.motd
if(motd)
to_chat(src, "<span class='infoplain'><div class=\"motd\">[motd]</div></span>", handle_whitespace=FALSE)
else
to_chat(src, span_notice("The Message of the Day has not been set."))
/client/proc/self_notes()
set name = "View Admin Remarks"
set category = "OOC"
set desc = "View the notes that admins have written about you"
if(!CONFIG_GET(flag/see_own_notes))
to_chat(usr, span_notice("Sorry, that function is not enabled on this server."))
return
browse_messages(null, usr.ckey, null, TRUE)
/client/proc/self_playtime()
set name = "View tracked playtime"
set category = "OOC"
set desc = "View the amount of playtime for roles the server has tracked."
if(!CONFIG_GET(flag/use_exp_tracking))
to_chat(usr, span_notice("Sorry, tracking is currently disabled."))
return
new /datum/job_report_menu(src, usr)
// Ignore verb
/client/verb/select_ignore()
set name = "Ignore"
set category = "OOC"
set desc ="Ignore a player's messages on the OOC channel"
// Make a list to choose players from
var/list/players = list()
// Use keys and fakekeys for the same purpose
var/displayed_key = ""
// Try to add every player who's online to the list
for(var/client/C in GLOB.clients)
// Don't add ourself
if(C == src)
continue
// Don't add players we've already ignored if they're not using a fakekey
if((C.key in prefs.ignoring) && !C.holder?.fakekey)
continue
// Don't add players using a fakekey we've already ignored
if(C.holder?.fakekey in prefs.ignoring)
continue
// Use the player's fakekey if they're using one
if(C.holder?.fakekey)
displayed_key = C.holder.fakekey
// Use the player's key if they're not using a fakekey
else
displayed_key = C.key
// Check if both we and the player are ghosts and they're not using a fakekey
if(isobserver(mob) && isobserver(C.mob) && !C.holder?.fakekey)
// Show us if the player is a ghost or not after their displayed key
// Add the player's displayed key to the list
players["[displayed_key](ghost)"] = displayed_key
// Add the player's displayed key to the list if we or the player aren't a ghost or they're using a fakekey
else
players[displayed_key] = displayed_key
// Check if the list is empty
if(!length(players))
// Express that there are no players we can ignore in chat
to_chat(src, "<span class='infoplain'>There are no other players you can ignore!</span>")
// Stop running
return
// Sort the list
players = sort_list(players)
// Request the player to ignore
var/selection = tgui_input_list(src, "Select a player", "Ignore", players)
// Stop running if we didn't receieve a valid selection
if(isnull(selection) || !(selection in players))
return
// Store the selected player
selection = players[selection]
// Check if the selected player is on our ignore list
if(selection in prefs.ignoring)
// Express that the selected player is already on our ignore list in chat
to_chat(src, "<span class='infoplain'>You are already ignoring [selection]!</span>")
// Stop running
return
// Add the selected player to our ignore list
prefs.ignoring.Add(selection)
// Save our preferences
prefs.save_preferences()
// Express that we've ignored the selected player in chat
to_chat(src, "<span class='infoplain'>You are now ignoring [selection] on the OOC channel.</span>")
// Unignore verb
/client/verb/select_unignore()
set name = "Unignore"
set category = "OOC"
set desc = "Stop ignoring a player's messages on the OOC channel"
// Check if we've ignored any players
if(!length(prefs.ignoring))
// Express that we haven't ignored any players in chat
to_chat(src, "<span class='infoplain'>You haven't ignored any players!</span>")
// Stop running
return
// Request the player to unignore
var/selection = tgui_input_list(src, "Select a player", "Unignore", prefs.ignoring)
// Stop running if we didn't receive a selection
if(isnull(selection))
return
// Check if the selected player is not on our ignore list
if(!(selection in prefs.ignoring))
// Express that the selected player is not on our ignore list in chat
to_chat(src, "<span class='infoplain'>You are not ignoring [selection]!</span>")
// Stop running
return
// Remove the selected player from our ignore list
prefs.ignoring.Remove(selection)
// Save our preferences
prefs.save_preferences()
// Express that we've unignored the selected player in chat
to_chat(src, "<span class='infoplain'>You are no longer ignoring [selection] on the OOC channel.</span>")
/client/proc/show_previous_roundend_report()
set name = "Your Last Round"
set category = "OOC"
set desc = "View the last round end report you've seen"
SSticker.show_roundend_report(src, report_type = PERSONAL_LAST_ROUND)
/client/proc/show_servers_last_roundend_report()
set name = "Server's Last Round"
set category = "OOC"
set desc = "View the last round end report from this server"
SSticker.show_roundend_report(src, report_type = SERVER_LAST_ROUND)
/client/verb/fit_viewport()
set name = "Fit Viewport"
set category = "OOC"
set desc = "Fit the width of the map window to match the viewport"
// Fetch aspect ratio
var/view_size = getviewsize(view)
var/aspect_ratio = view_size[1] / view_size[2]
// Calculate desired pixel width using window size and aspect ratio
var/list/sizes = params2list(winget(src, "mainwindow.split;mapwindow", "size"))
// Client closed the window? Some other error? This is unexpected behaviour, let's
// CRASH with some info.
if(!sizes["mapwindow.size"])
CRASH("sizes does not contain mapwindow.size key. This means a winget failed to return what we wanted. --- sizes var: [sizes] --- sizes length: [length(sizes)]")
var/list/map_size = splittext(sizes["mapwindow.size"], "x")
// Gets the type of zoom we're currently using from our view datum
// If it's 0 we do our pixel calculations based off the size of the mapwindow
// If it's not, we already know how big we want our window to be, since zoom is the exact pixel ratio of the map
var/zoom_value = src.view_size?.zoom || 0
var/desired_width = 0
if(zoom_value)
desired_width = round(view_size[1] * zoom_value * world.icon_size)
else
// Looks like we expect mapwindow.size to be "ixj" where i and j are numbers.
// If we don't get our expected 2 outputs, let's give some useful error info.
if(length(map_size) != 2)
CRASH("map_size of incorrect length --- map_size var: [map_size] --- map_size length: [length(map_size)]")
var/height = text2num(map_size[2])
desired_width = round(height * aspect_ratio)
if (text2num(map_size[1]) == desired_width)
// Nothing to do
return
var/split_size = splittext(sizes["mainwindow.split.size"], "x")
var/split_width = text2num(split_size[1])
// Avoid auto-resizing the statpanel and chat into nothing.
desired_width = min(desired_width, split_width - 300)
// Calculate and apply a best estimate
// +4 pixels are for the width of the splitter's handle
var/pct = 100 * (desired_width + 4) / split_width
winset(src, "mainwindow.split", "splitter=[pct]")
// Apply an ever-lowering offset until we finish or fail
var/delta
for(var/safety in 1 to 10)
var/after_size = winget(src, "mapwindow", "size")
map_size = splittext(after_size, "x")
var/got_width = text2num(map_size[1])
if (got_width == desired_width)
// success
return
else if (isnull(delta))
// calculate a probable delta value based on the difference
delta = 100 * (desired_width - got_width) / split_width
else if ((delta > 0 && got_width > desired_width) || (delta < 0 && got_width < desired_width))
// if we overshot, halve the delta and reverse direction
delta = -delta/2
pct += delta
winset(src, "mainwindow.split", "splitter=[pct]")
/// Attempt to automatically fit the viewport, assuming the user wants it
/client/proc/attempt_auto_fit_viewport()
if (!prefs.read_preference(/datum/preference/toggle/auto_fit_viewport))
return
if(fully_created)
INVOKE_ASYNC(src, VERB_REF(fit_viewport))
else //Delayed to avoid wingets from Login calls.
addtimer(CALLBACK(src, VERB_REF(fit_viewport), 1 SECONDS))
/client/verb/policy()
set name = "Show Policy"
set desc = "Show special server rules related to your current character."
set category = "OOC"
//Collect keywords
var/list/keywords = mob.get_policy_keywords()
var/header = get_policy(POLICY_VERB_HEADER)
var/list/policytext = list(header,"<hr>")
var/anything = FALSE
for(var/keyword in keywords)
var/p = get_policy(keyword)
if(p)
policytext += p
policytext += "<hr>"
anything = TRUE
if(!anything)
policytext += "No related rules found."
usr << browse(policytext.Join(""),"window=policy")
/client/verb/fix_stat_panel()
set name = "Fix Stat Panel"
set hidden = TRUE
init_verbs()
/client/proc/export_preferences()
set name = "Export Preferences"
set desc = "Export your current preferences to a file."
set category = "OOC"
ASSERT(prefs, "User attempted to export preferences while preferences were null!") // what the fuck
prefs.savefile.export_json_to_client(usr, ckey)