Files
Bubberstation/code/datums/json_savefile.dm
SkyratBot 3ecc9f859d [MIRROR] Allows Export of your Preferences JSON File [MDB IGNORE] (#20894)
* 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.
/🆑

* Allows Export of your Preferences JSON File

---------

Co-authored-by: san7890 <the@san7890.com>
2023-05-02 15:27:19 -04:00

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