mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-04 22:11:27 +00:00
* 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:  Here's what the prettified JSON looks like on 515.  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. /🆑 * Allows Export of your Preferences JSON File --------- Co-authored-by: san7890 <the@san7890.com>
125 lines
4.8 KiB
Plaintext
125 lines
4.8 KiB
Plaintext
/**
|
|
* A savefile implementation that handles all data using json.
|
|
* Also saves it using JSON too, fancy.
|
|
* If you pass in a null path, it simply acts as a memory tree instead, and cannot be saved.
|
|
*/
|
|
/datum/json_savefile
|
|
var/path = ""
|
|
VAR_PRIVATE/list/tree
|
|
/// If this is set to true, calling set_entry or remove_entry will automatically call save(), this does not catch modifying a sub-tree, nor do I know how to do that
|
|
var/auto_save = FALSE
|
|
/// Cooldown that tracks the time between attempts to download the savefile.
|
|
COOLDOWN_DECLARE(download_cooldown)
|
|
|
|
GENERAL_PROTECT_DATUM(/datum/json_savefile)
|
|
|
|
/datum/json_savefile/New(path)
|
|
src.path = path
|
|
tree = list()
|
|
if(path && fexists(path))
|
|
load()
|
|
|
|
/**
|
|
* Gets an entry from the json tree, with an optional default value.
|
|
* If no key is specified it throws the entire tree at you instead
|
|
*/
|
|
/datum/json_savefile/proc/get_entry(key, default_value)
|
|
if(!key)
|
|
return tree
|
|
return (key in tree) ? tree[key] : default_value
|
|
|
|
/// Sets an entry in the tree to the given value
|
|
/datum/json_savefile/proc/set_entry(key, value)
|
|
tree[key] = value
|
|
if(auto_save)
|
|
save()
|
|
|
|
/// Removes the given key from the tree
|
|
/datum/json_savefile/proc/remove_entry(key)
|
|
if(key)
|
|
tree -= key
|
|
if(auto_save)
|
|
save()
|
|
|
|
/// Wipes the entire tree
|
|
/datum/json_savefile/proc/wipe()
|
|
tree?.Cut()
|
|
|
|
/datum/json_savefile/proc/load()
|
|
if(!path || !fexists(path))
|
|
return FALSE
|
|
try
|
|
tree = json_decode(rustg_file_read(path))
|
|
return TRUE
|
|
catch(var/exception/err)
|
|
stack_trace("failed to load json savefile at '[path]': [err]")
|
|
return FALSE
|
|
|
|
/datum/json_savefile/proc/save()
|
|
if(path)
|
|
rustg_file_write(json_encode(tree), path)
|
|
|
|
/datum/json_savefile/serialize_list(list/options)
|
|
return tree.Copy()
|
|
|
|
/// Traverses the entire dir tree of the given savefile and dynamically assembles the tree from it
|
|
/datum/json_savefile/proc/import_byond_savefile(savefile/savefile)
|
|
tree.Cut()
|
|
var/list/dirs_to_go = list("/" = tree)
|
|
while(length(dirs_to_go))
|
|
var/dir = dirs_to_go[1]
|
|
var/list/region = dirs_to_go[dir]
|
|
dirs_to_go.Cut(1, 2)
|
|
savefile.cd = dir
|
|
for(var/entry in savefile.dir)
|
|
var/entry_value
|
|
savefile.cd = "[dir]/[entry]"
|
|
//eof refers to the path you are cd'ed into, not the savefile as a whole. being false right after cding into an entry means this entry has no buffer, which only happens with nested save file directories
|
|
if (savefile.eof)
|
|
region[entry] = list()
|
|
dirs_to_go["[dir]/[entry]"] = region[entry]
|
|
continue
|
|
READ_FILE(savefile, entry_value) //we are cd'ed to the entry, so we don't need to specify a path to read from
|
|
region[entry] = entry_value
|
|
|
|
/// Proc that handles generating a JSON file (prettified if 515 and over!) of a user's preferences and showing it to them.
|
|
/// Requester is passed in to the ftp() and tgui_alert() procs, and account_name is just used to generate the filename.
|
|
/// We don't _need_ to pass in account_name since this is reliant on the json_savefile datum already knowing what we correspond to, but it's here to help people keep track of their stuff.
|
|
/datum/json_savefile/proc/export_json_to_client(mob/requester, account_name)
|
|
if(!istype(requester) || !path)
|
|
return
|
|
|
|
if(!json_export_checks(requester))
|
|
return
|
|
|
|
COOLDOWN_START(src, download_cooldown, (CONFIG_GET(number/seconds_cooldown_for_preferences_export) * (1 SECONDS)))
|
|
var/file_name = "[account_name ? "[account_name]_" : ""]preferences_[time2text(world.timeofday, "MMM_DD_YYYY_hh-mm-ss")].json"
|
|
var/temporary_file_storage = "data/preferences_export_working_directory/[file_name]"
|
|
|
|
#if DM_VERSION >= 515
|
|
if(!text2file(json_encode(tree, JSON_PRETTY_PRINT), temporary_file_storage))
|
|
tgui_alert(requester, "Failed to export preferences to JSON! You might need to try again later.", "Export Preferences JSON")
|
|
return
|
|
#else
|
|
if(!text2file(json_encode(tree), temporary_file_storage))
|
|
tgui_alert(requester, "Failed to export preferences to JSON! You might need to try again later.", "Export Preferences JSON")
|
|
return
|
|
#endif
|
|
|
|
var/exportable_json = file(temporary_file_storage)
|
|
|
|
DIRECT_OUTPUT(requester, ftp(exportable_json, file_name))
|
|
fdel(temporary_file_storage)
|
|
|
|
/// Proc that just handles all of the checks for exporting a preferences file, returns TRUE if all checks are passed, FALSE otherwise.
|
|
/// Just done like this to make the code in the export_json_to_client() proc a bit cleaner.
|
|
/datum/json_savefile/proc/json_export_checks(mob/requester)
|
|
if(!COOLDOWN_FINISHED(src, download_cooldown))
|
|
tgui_alert(requester, "You must wait [DisplayTimeText(COOLDOWN_TIMELEFT(src, download_cooldown))] before exporting your preferences again!", "Export Preferences JSON")
|
|
return FALSE
|
|
|
|
if(tgui_alert(requester, "Are you sure you want to export your preferences as a JSON file? This will save to a file on your computer.", "Export Preferences JSON", list("Cancel", "Yes")) == "Yes")
|
|
return TRUE
|
|
|
|
return FALSE
|