Skip to content

Commit d68dcb9

Browse files
francinumZephyrTFAKapu1178
authored
json savefiles (#1211)
* json savefiles Co-authored-by: ZephyrTFA <[email protected]> * json * Resolve comments * kill meeee * COMMA * fuck 2 * Update code/modules/client/migrations/reconcile_v44.dm * Update code/modules/client/preferences/_preference.dm * Hard to save a savefile when you don't know who's savefile it is. * Pretend to be a client better. * allergies are murdering my throat I feel like wet concrete * Fixes character swap loading This was clobbering savefiles with garbage. * piss and shit * Mock clients never save on prod Fix Randomize Human Appearance --------- Co-authored-by: ZephyrTFA <[email protected]> Co-authored-by: Kapu1178 <[email protected]>
1 parent 2ada659 commit d68dcb9

19 files changed

+491
-179
lines changed

code/datums/json_savefile.dm

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* A savefile implementation that handles all data using json.
3+
* Also saves it using JSON too, fancy.
4+
* If you pass in a null path, it simply acts as a memory tree instead, and cannot be saved.
5+
*/
6+
/datum/json_savefile
7+
var/path = ""
8+
VAR_PRIVATE/list/tree
9+
/// 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
10+
var/auto_save = FALSE
11+
/// Cooldown that tracks the time between attempts to download the savefile.
12+
COOLDOWN_DECLARE(download_cooldown)
13+
14+
GENERAL_PROTECT_DATUM(/datum/json_savefile)
15+
16+
/datum/json_savefile/New(path)
17+
src.path = path
18+
tree = list()
19+
if(path && fexists(path))
20+
load()
21+
22+
/**
23+
* Gets an entry from the json tree, with an optional default value.
24+
* If no key is specified it throws the entire tree at you instead
25+
*/
26+
/datum/json_savefile/proc/get_entry(key, default_value)
27+
if(!key)
28+
return tree
29+
return (key in tree) ? tree[key] : default_value
30+
31+
/// Sets an entry in the tree to the given value
32+
/datum/json_savefile/proc/set_entry(key, value)
33+
tree[key] = value
34+
if(auto_save)
35+
save()
36+
37+
/// Removes the given key from the tree
38+
/datum/json_savefile/proc/remove_entry(key)
39+
if(key)
40+
tree -= key
41+
if(auto_save)
42+
save()
43+
44+
/// Wipes the entire tree
45+
/datum/json_savefile/proc/wipe()
46+
tree?.Cut()
47+
48+
/datum/json_savefile/proc/load()
49+
if(!path || !fexists(path))
50+
return FALSE
51+
try
52+
tree = json_decode(rustg_file_read(path))
53+
return TRUE
54+
catch(var/exception/err)
55+
stack_trace("failed to load json savefile at '[path]': [err]")
56+
return FALSE
57+
58+
/datum/json_savefile/proc/save()
59+
if(path)
60+
rustg_file_write(json_encode(tree, JSON_PRETTY_PRINT), path)
61+
62+
/datum/json_savefile/serialize_list(list/options, list/semvers)
63+
SHOULD_CALL_PARENT(FALSE)
64+
//SET_SERIALIZATION_SEMVER(semvers, "1.0.0")
65+
return tree.Copy()
66+
67+
/// Traverses the entire dir tree of the given savefile and dynamically assembles the tree from it
68+
/datum/json_savefile/proc/import_byond_savefile(savefile/savefile)
69+
tree.Cut()
70+
var/list/dirs_to_go = list("/" = tree)
71+
while(length(dirs_to_go))
72+
var/dir = dirs_to_go[1]
73+
var/list/region = dirs_to_go[dir]
74+
dirs_to_go.Cut(1, 2)
75+
savefile.cd = dir
76+
for(var/entry in savefile.dir)
77+
var/entry_value
78+
savefile.cd = "[dir]/[entry]"
79+
//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
80+
if (savefile.eof)
81+
region[entry] = list()
82+
dirs_to_go["[dir]/[entry]"] = region[entry]
83+
continue
84+
READ_FILE(savefile, entry_value) //we are cd'ed to the entry, so we don't need to specify a path to read from
85+
region[entry] = entry_value
86+
87+
/* no. not touching this right now.
88+
/// Proc that handles generating a JSON file (prettified if 515 and over!) of a user's preferences and showing it to them.
89+
/// Requester is passed in to the ftp() and tgui_alert() procs, and account_name is just used to generate the filename.
90+
/// 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.
91+
/datum/json_savefile/proc/export_json_to_client(mob/requester, account_name)
92+
if(!istype(requester) || !path)
93+
return
94+
95+
if(!json_export_checks(requester))
96+
return
97+
98+
COOLDOWN_START(src, download_cooldown, (CONFIG_GET(number/seconds_cooldown_for_preferences_export) * (1 SECONDS)))
99+
var/file_name = "[account_name ? "[account_name]_" : ""]preferences_[time2text(world.timeofday, "MMM_DD_YYYY_hh-mm-ss")].json"
100+
var/temporary_file_storage = "data/preferences_export_working_directory/[file_name]"
101+
102+
if(!text2file(json_encode(tree, JSON_PRETTY_PRINT), temporary_file_storage))
103+
tgui_alert(requester, "Failed to export preferences to JSON! You might need to try again later.", "Export Preferences JSON")
104+
return
105+
106+
var/exportable_json = file(temporary_file_storage)
107+
108+
DIRECT_OUTPUT(requester, ftp(exportable_json, file_name))
109+
fdel(temporary_file_storage)
110+
111+
/// Proc that just handles all of the checks for exporting a preferences file, returns TRUE if all checks are passed, FALSE otherwise.
112+
/// Just done like this to make the code in the export_json_to_client() proc a bit cleaner.
113+
/datum/json_savefile/proc/json_export_checks(mob/requester)
114+
if(!COOLDOWN_FINISHED(src, download_cooldown))
115+
tgui_alert(requester, "You must wait [DisplayTimeText(COOLDOWN_TIMELEFT(src, download_cooldown))] before exporting your preferences again!", "Export Preferences JSON")
116+
return FALSE
117+
118+
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")
119+
return TRUE
120+
121+
return FALSE
122+
*/

code/datums/mocking/client.dm

+46
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,49 @@
55

66
/// The view of the client, similar to /client/var/view.
77
var/view = "15x15"
8+
9+
/// View data of the client, similar to /client/var/view_size.
10+
var/datum/view_data/view_size
11+
12+
/// Objects on the screen of the client
13+
var/list/screen = list()
14+
15+
/// The mob the client controls
16+
var/mob/mob
17+
18+
/// The ckey for this mock interface
19+
var/ckey = "mockclient"
20+
21+
/// The key for this mock interface
22+
var/key = "mockclient"
23+
24+
/// client prefs
25+
var/fps
26+
var/hotkeys
27+
var/tgui_say
28+
var/typing_indicators
29+
30+
/datum/client_interface/New()
31+
..()
32+
var/static/mock_client_uid = 0
33+
mock_client_uid++
34+
35+
src.key = "[key]_[mock_client_uid]"
36+
ckey = ckey(key)
37+
38+
#ifdef UNIT_TESTS // otherwise this shit can leak into production servers which is drather bad
39+
GLOB.directory[ckey] = src
40+
#endif
41+
42+
/datum/client_interface/Destroy(force)
43+
GLOB.directory -= ckey
44+
return ..()
45+
46+
/datum/client_interface/proc/IsByondMember()
47+
return FALSE
48+
49+
/datum/client_interface/proc/set_macros()
50+
return
51+
52+
/datum/client_interface/proc/update_ambience_pref()
53+
return

code/game/data_huds.dm

+1-1
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ Security HUDs! Basic mode shows only the job.
257257

258258
/mob/living/carbon/human/proc/sec_hud_set_security_status()
259259
var/perpname = get_face_name(get_id_name(""))
260-
if(perpname && SSdatacore)
260+
if(perpname && SSdatacore.initialized)
261261
var/datum/data/record/security/R = SSdatacore.get_record_by_name(name, DATACORE_RECORDS_SECURITY)
262262
if(R)
263263
var/new_state

code/modules/client/client_procs.dm

+1
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
288288
prefs = GLOB.preferences_datums[ckey]
289289
if(prefs)
290290
prefs.parent = src
291+
prefs.load_savefile() // just to make sure we have the latest data
291292
prefs.apply_all_client_preferences()
292293
else
293294
prefs = new /datum/preferences(src)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/datum/preferences/proc/try_savefile_type_migration()
2+
load_path(parent.ckey, "preferences.sav") // old save file
3+
var/old_path = path
4+
load_path(parent.ckey)
5+
if(!fexists(old_path))
6+
return
7+
var/datum/json_savefile/json_savefile = new(path)
8+
json_savefile.import_byond_savefile(new /savefile(old_path))
9+
json_savefile.save()
10+
return TRUE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/datum/preferences/proc/migrate_med_sec_fancy_job_titles()
2+
var/list/migrate_jobs = list(
3+
"Head of Security" = JOB_SECURITY_MARSHAL,
4+
"Detective" = JOB_DETECTIVE,
5+
"Medical Doctor" = JOB_ACOLYTE,
6+
"Curator" = JOB_ARCHIVIST,
7+
"Cargo Technician" = JOB_DECKHAND,
8+
)
9+
10+
var/list/job_prefs = read_preference(/datum/preference/blob/job_priority)
11+
for(var/job in job_prefs)
12+
if(job in migrate_jobs)
13+
var/old_value = job_prefs[job]
14+
job_prefs -= job
15+
job_prefs[migrate_jobs[job]] = old_value
16+
var/datum/preference/blob/job_priority/actual_datum = GLOB.preference_entries[/datum/preference/blob/job_priority]
17+
write_preference(/datum/preference/blob/job_priority, actual_datum.serialize(job_prefs))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/datum/preferences/proc/reconcile_v44(current_version)
2+
//Due to unsafe edits to the v44 updater, I need to make sure that both states are safely reconciled..
3+
4+
if(current_version < 44)
5+
//If we called the v44 handler in this same session, we have nothing to resolve.
6+
return
7+
8+
var/list/migrate_jobs = list(
9+
// If my logic is correct, this state should never occur, but just to ensure all states are stable.
10+
"Medical Doctor" = JOB_ACOLYTE,
11+
// Pre-Aethering-II Doctor.
12+
"General Practitioner" = JOB_ACOLYTE
13+
)
14+
var/list/job_prefs = read_preference(/datum/preference/blob/job_priority)
15+
for(var/job in job_prefs)
16+
if(job in migrate_jobs)
17+
var/old_value = job_prefs[job]
18+
job_prefs -= job
19+
job_prefs[migrate_jobs[job]] = old_value
20+
var/datum/preference/blob/job_priority/actual_datum = GLOB.preference_entries[/datum/preference/blob/job_priority]
21+
write_preference(/datum/preference/blob/job_priority, actual_datum.serialize(job_prefs))
22+

code/modules/client/preferences.dm

+36-22
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@ GLOBAL_LIST_EMPTY(preferences_datums)
44
var/client/parent
55
//doohickeys for savefiles
66
var/path
7-
var/default_slot = 1 //Holder so it doesn't default to slot 1, rather the last one used
8-
var/max_save_slots = 10
9-
10-
//non-preference stuff
11-
var/muted = 0
7+
/// Whether or not we allow saving/loading. Used for guests, if they're enabled
8+
var/load_and_save = TRUE
9+
/// Ensures that we always load the last used save, QOL
10+
var/default_slot = 1
11+
/// The maximum number of slots we're allowed to contain
12+
var/max_save_slots = 3
13+
14+
/// Bitflags for communications that are muted
15+
var/muted = NONE
16+
/// Last IP that this client has connected from
1217
var/last_ip
18+
/// Last CID that this client has connected from
1319
var/last_id
1420

15-
//game-preferences
16-
var/lastchangelog = "" //Saved changlog filesize to detect if there was a change
21+
/// Cached changelog size, to detect new changelogs since last join
22+
var/lastchangelog = ""
1723

1824
/// Custom keybindings. Map of keybind names to keyboard inputs.
1925
/// For example, by default would have "swap_hands" -> list("X")
@@ -24,7 +30,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
2430
var/list/key_bindings_by_key = list()
2531

2632
var/toggles = TOGGLES_DEFAULT
27-
var/db_flags
33+
var/db_flags = NONE
2834
var/chat_toggles = TOGGLES_DEFAULT_CHAT
2935
var/ghost_form = "ghost"
3036

@@ -52,11 +58,11 @@ GLOBAL_LIST_EMPTY(preferences_datums)
5258
/// A list of instantiated middleware
5359
var/list/datum/preference_middleware/middleware = list()
5460

55-
/// The savefile relating to core preferences, PREFERENCE_PLAYER
56-
var/savefile/game_savefile
61+
/// The json savefile for this datum
62+
var/datum/json_savefile/savefile
5763

5864
/// The savefile relating to character preferences, PREFERENCE_CHARACTER
59-
var/savefile/character_savefile
65+
var/list/character_data
6066

6167
/// A list of keys that have been updated since the last save.
6268
var/list/recently_updated_keys = list()
@@ -89,19 +95,29 @@ GLOBAL_LIST_EMPTY(preferences_datums)
8995
return ..()
9096

9197
/datum/preferences/New(client/C)
98+
if(!C)
99+
CRASH("Attempted to create preferences without a client or mock.")
92100
parent = C
93101

94102
for (var/middleware_type in subtypesof(/datum/preference_middleware))
95103
middleware += new middleware_type(src)
96104

97105
selected_category = locate(/datum/preference_group/category/general) in GLOB.all_pref_groups
98106

99-
if(istype(C))
100-
if(!is_guest_key(C.key))
101-
load_path(C.ckey)
102-
unlock_content = !!C.IsByondMember()
103-
if(unlock_content)
104-
max_save_slots = 15
107+
if(istype(C) || istype(C, /datum/client_interface))
108+
#ifdef UNIT_TESTS
109+
load_and_save = !is_guest_key(parent.key) //Treat them as fully real.
110+
#else
111+
load_and_save = !is_guest_key(parent.key) && !istype(C, /datum/client_interface) // Never save mock clients to disk on prod.
112+
#endif
113+
load_path(parent.ckey)
114+
if(load_and_save && !fexists(path))
115+
try_savefile_type_migration()
116+
unlock_content = !!C.IsByondMember() //TG made this a preference level var, I can do that if you want.
117+
if(unlock_content)
118+
max_save_slots = 15
119+
120+
load_savefile()
105121

106122
// give them default keybinds and update their movement keys
107123
key_bindings = deep_copy_list(GLOB.default_hotkeys)
@@ -486,17 +502,15 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/subscreen)
486502
/datum/preferences/proc/create_character_profiles()
487503
var/list/profiles = list()
488504

489-
var/savefile/savefile = new(path)
490505
for (var/index in 1 to max_save_slots)
491506
// It won't be updated in the savefile yet, so just read the name directly
492507
if (index == default_slot)
493508
profiles += read_preference(/datum/preference/name/real_name)
494509
continue
495510

496-
savefile.cd = "/character[index]"
497-
498-
var/name
499-
READ_FILE(savefile["real_name"], name)
511+
var/tree_key = "character[index]"
512+
var/save_data = savefile.get_entry(tree_key)
513+
var/name = save_data?["real_name"]
500514

501515
if (isnull(name))
502516
profiles += null

0 commit comments

Comments
 (0)