JSON Savefiles | Player Saves use JSON (#70492)

<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may
not be viewable. -->
<!-- You can view Contributing.MD for a detailed description of the pull
request process. -->
TODO:

- [x] DOCUMENT SHIT
- [x] UPDATE DOCUMENTATION

## About The Pull Request

Adds a new datum, which is intended to be a replacement for the stock
savefile type, json_savefile
As you can imagine, this is essentially just a wrapper around a json
file for reading/writing/manipulation that is intended to be a dropin
replacement for savefiles
It also have the ability to import stock savefiles and parse them into a
json tree

<!-- Describe The Pull Request. Please be sure every change is
documented or this can delay review and even discourage maintainers from
merging your PR! -->

## Why It's Good For The Game

Permission obtained from MSO and Mothblocks.

<!-- Argue for the merits of your changes and how they benefit the game,
especially if they are controversial and/or far reaching. If you can't
actually explain WHY what you are doing will improve the game, then it
probably isn't good for the game in the first place. -->

## Changelog

<!-- If your PR modifies aspects of the game that can be concretely
observed by players or admins you should add a changelog. If your change
does NOT meet this description, remove this section. Be sure to properly
mark your PRs to prevent unnecessary GBP loss. You can read up on GBP
and it's effects on PRs in the tgstation guides for contributors. Please
note that maintainers freely reserve the right to remove and add tags
should they deem it appropriate. You can attempt to finagle the system
all you want, but it's best to shoot for clear communication right off
the bat. -->
Not player facing, tested locally exhaustively to ensure it doesnt break
shit
🆑
/🆑

<!-- Both 🆑's are required for the changelog to work! You can put
your name to the right of the first 🆑 if you want to overwrite your
GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the
icon ingame) and delete the unneeded ones. Despite some of the tags,
changelogs should generally represent how a player might be affected by
the changes rather than a summary of the PR's contents. -->

Co-authored-by: Kyle Spier-Swenson <kyleshome@gmail.com>
Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
Co-authored-by: san7890 <the@san7890.com>
This commit is contained in:
Zephyr
2022-11-17 21:45:18 -08:00
committed by GitHub
co-authored by Kyle Spier-Swenson Mothblocks san7890
parent 6b6151e16d
commit 47ba13033f
16 changed files with 309 additions and 148 deletions
+2
View File
@@ -0,0 +1,2 @@
/// Checks if the given target is either a client or a mock client
#define IS_CLIENT_OR_MOCK(target) (istype(target, /client) || istype(target, /datum/client_interface))
+76
View File
@@ -0,0 +1,76 @@
/**
* A savefile implementation that handles all data using json.
* Also saves it using JSON too, fancy.
*/
/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
GENERAL_PROTECT_DATUM(/datum/json_savefile)
/datum/json_savefile/New(path)
src.path = path
tree = list()
if(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(!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()
rustg_file_write(json_encode(tree), path)
/// 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
+27
View File
@@ -6,8 +6,35 @@
/// The view of the client, similar to /client/var/view.
var/view = "15x15"
/// View data of the client, similar to /client/var/view_size.
var/datum/view_data/view_size
/// Objects on the screen of the client
var/list/screen = list()
/// The mob the client controls
var/mob/mob
/// The ckey for this mock interface
var/ckey = "mockclient"
/// The key for this mock interface
var/key = "mockclient"
/// client prefs
var/fps
var/hotkeys
var/tgui_say
var/typing_indicators
/datum/client_interface/proc/IsByondMember()
return FALSE
/datum/client_interface/New(key)
..()
if(key)
src.key = key
ckey = ckey(key)
/datum/client_interface/proc/set_macros()
return
+19 -16
View File
@@ -66,11 +66,11 @@ GLOBAL_LIST_EMPTY(preferences_datums)
/// A list of instantiated middleware
var/list/datum/preference_middleware/middleware = list()
/// The savefile relating to core preferences, PREFERENCE_PLAYER
var/savefile/game_savefile
/// The json savefile for this datum
var/datum/json_savefile/savefile
/// The savefile relating to character preferences, PREFERENCE_CHARACTER
var/savefile/character_savefile
var/list/character_data
/// A list of keys that have been updated since the last save.
var/list/recently_updated_keys = list()
@@ -88,18 +88,23 @@ GLOBAL_LIST_EMPTY(preferences_datums)
value_cache = null
return ..()
/datum/preferences/New(client/C)
parent = C
/datum/preferences/New(client/parent)
src.parent = parent
for (var/middleware_type in subtypesof(/datum/preference_middleware))
middleware += new middleware_type(src)
if(istype(C))
if(!is_guest_key(C.key))
load_path(C.ckey)
unlock_content = !!C.IsByondMember()
if(IS_CLIENT_OR_MOCK(parent))
if(!is_guest_key(parent.key))
load_path(parent.ckey)
if(!fexists(path))
try_savefile_type_migration()
unlock_content = !!parent.IsByondMember()
if(unlock_content)
max_save_slots = 8
else
CRASH("attempted to create a preferences datum without a client or mock!")
load_savefile()
// give them default keybinds and update their movement keys
key_bindings = deep_copy_list(GLOB.default_hotkeys)
@@ -112,9 +117,9 @@ GLOBAL_LIST_EMPTY(preferences_datums)
return
//we couldn't load character data so just randomize the character appearance + name
randomise_appearance_prefs() //let's create a random character then - rather than a fat, bald and naked man.
if(C)
if(parent)
apply_all_client_preferences()
C.set_macros()
parent.set_macros()
if(!loaded_preferences_successfully)
save_preferences()
@@ -370,17 +375,15 @@ GLOBAL_LIST_EMPTY(preferences_datums)
/datum/preferences/proc/create_character_profiles()
var/list/profiles = list()
var/savefile/savefile = new(path)
for (var/index in 1 to max_save_slots)
// It won't be updated in the savefile yet, so just read the name directly
if (index == default_slot)
profiles += read_preference(/datum/preference/name/real_name)
continue
savefile.cd = "/character[index]"
var/name
READ_FILE(savefile["real_name"], name)
var/tree_key = "character[index]"
var/save_data = savefile.get_entry(tree_key)
var/name = save_data?["real_name"]
if (isnull(name))
profiles += null
+15 -24
View File
@@ -156,13 +156,13 @@ GLOBAL_LIST_INIT(preference_entries_by_key, init_preference_entries_by_key())
/// Given a savefile, return either the saved data or an acceptable default.
/// This will write to the savefile if a value was not found with the new value.
/datum/preference/proc/read(savefile/savefile, datum/preferences/preferences)
/datum/preference/proc/read(list/save_data, datum/preferences/preferences)
SHOULD_NOT_OVERRIDE(TRUE)
var/value
if (!isnull(savefile))
READ_FILE(savefile[savefile_key], value)
if (!isnull(save_data))
value = save_data[savefile_key]
if (isnull(value))
return null
@@ -172,14 +172,14 @@ GLOBAL_LIST_INIT(preference_entries_by_key, init_preference_entries_by_key())
/// Given a savefile, writes the inputted value.
/// Returns TRUE for a successful application.
/// Return FALSE if it is invalid.
/datum/preference/proc/write(savefile/savefile, value)
/datum/preference/proc/write(list/save_data, value)
SHOULD_NOT_OVERRIDE(TRUE)
if (!is_valid(value))
return FALSE
if (!isnull(savefile))
WRITE_FILE(savefile[savefile_key], serialize(value))
if (!isnull(save_data))
save_data[savefile_key] = serialize(value)
return TRUE
@@ -205,31 +205,22 @@ GLOBAL_LIST_INIT(preference_entries_by_key, init_preference_entries_by_key())
CRASH("`apply_to_human()` was not implemented for [type]!")
/// Returns which savefile to use for a given savefile identifier
/datum/preferences/proc/get_savefile_for_savefile_identifier(savefile_identifier)
RETURN_TYPE(/savefile)
/datum/preferences/proc/get_save_data_for_savefile_identifier(savefile_identifier)
RETURN_TYPE(/list)
if (!parent)
return null
if(!savefile)
CRASH("Attempted to get the savedata for [savefile_identifier] of [parent] without a savefile. This should have been handled by load_preferences()")
// Both of these will cache savefiles, but only for a tick.
// This is because storing a savefile will lock it, causing later issues down the line.
// Do not change them to addtimer, since the timer SS might not be running at this time.
switch (savefile_identifier)
if (PREFERENCE_CHARACTER)
if (!character_savefile)
character_savefile = new /savefile(path)
character_savefile.cd = "/character[default_slot]"
spawn (1)
character_savefile = null
return character_savefile
return savefile.get_entry("character[default_slot]")
if (PREFERENCE_PLAYER)
if (!game_savefile)
game_savefile = new /savefile(path)
game_savefile.cd = "/"
spawn (1)
game_savefile = null
return game_savefile
return savefile.get_entry()
else
CRASH("Unknown savefile identifier [savefile_identifier]")
@@ -252,7 +243,7 @@ GLOBAL_LIST_INIT(preference_entries_by_key, init_preference_entries_by_key())
if (preference_type in value_cache)
return value_cache[preference_type]
var/value = preference_entry.read(get_savefile_for_savefile_identifier(preference_entry.savefile_identifier), src)
var/value = preference_entry.read(get_save_data_for_savefile_identifier(preference_entry.savefile_identifier), src)
if (isnull(value))
value = preference_entry.create_informed_default_value(src)
if (write_preference(preference_entry, value))
@@ -266,9 +257,9 @@ GLOBAL_LIST_INIT(preference_entries_by_key, init_preference_entries_by_key())
/// Returns TRUE for a successful preference application.
/// Returns FALSE if it is invalid.
/datum/preferences/proc/write_preference(datum/preference/preference, preference_value)
var/savefile = get_savefile_for_savefile_identifier(preference.savefile_identifier)
var/save_data = get_save_data_for_savefile_identifier(preference.savefile_identifier)
var/new_value = preference.deserialize(preference_value, src)
var/success = preference.write(savefile, new_value)
var/success = preference.write(save_data, new_value)
if (success)
value_cache[preference.type] = new_value
return success
@@ -2,9 +2,7 @@
/// PR #62733 changed this to allow all characters to use body type.
/// This migration moves binary-gendered characters over to the "use gender" body type
/// so that old characters are preserved.
/datum/preferences/proc/migrate_body_types(savefile/savefile)
var/current_gender
READ_FILE(savefile["gender"], current_gender)
/datum/preferences/proc/migrate_body_types(list/save_data)
var/current_gender = save_data["gender"]
if (current_gender == MALE || current_gender == FEMALE)
WRITE_FILE(savefile["body_type"], "Use gender")
save_data["body_type"] = "Use gender"
@@ -0,0 +1,10 @@
/datum/preferences/proc/try_savefile_type_migration()
load_path(parent.ckey, "preferences.sav") // old save file
var/old_path = path
load_path(parent.ckey)
if(!fexists(old_path))
return
var/datum/json_savefile/json_savefile = new(path)
json_savefile.import_byond_savefile(new /savefile(old_path))
json_savefile.save()
return TRUE
+65 -97
View File
@@ -23,15 +23,13 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
Failing all that, the standard sanity checks are performed. They simply check the data is suitable, reverting to
initial() values if necessary.
*/
/datum/preferences/proc/savefile_needs_update(savefile/S)
var/savefile_version
READ_FILE(S["version"], savefile_version)
if(savefile_version < SAVEFILE_VERSION_MIN)
S.dir.Cut()
/datum/preferences/proc/save_data_needs_update(list/save_data)
if(!save_data) // empty list, either savefile isnt loaded or its a new char
return -1
if(save_data["version"] < SAVEFILE_VERSION_MIN)
return -2
if(savefile_version < SAVEFILE_VERSION_MAX)
return savefile_version
if(save_data["version"] < SAVEFILE_VERSION_MAX)
return save_data["version"]
return -1
//should these procs get fairly long
@@ -41,7 +39,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
//This only really meant to avoid annoying frequent players
//if your savefile is 3 months out of date, then 'tough shit'.
/datum/preferences/proc/update_preferences(current_version, savefile/S)
/datum/preferences/proc/update_preferences(current_version, datum/json_savefile/S)
if(current_version < 34)
write_preference(/datum/preference/toggle/auto_fit_viewport, TRUE)
@@ -93,12 +91,12 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
if (current_version < 41)
migrate_preferences_to_tgui_prefs_menu()
/datum/preferences/proc/update_character(current_version, savefile/savefile)
/datum/preferences/proc/update_character(current_version, list/save_data)
if (current_version < 41)
migrate_character_to_tgui_prefs_menu()
if (current_version < 42)
migrate_body_types(savefile)
migrate_body_types(save_data)
if (current_version < 43)
migrate_legacy_sound_toggles(savefile)
@@ -141,50 +139,48 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
var/datum/keybinding/conflicted = item
to_chat(parent, span_danger("[conflicted.category]: [conflicted.full_name] needs updating"))
/datum/preferences/proc/load_path(ckey,filename="preferences.sav")
/datum/preferences/proc/load_path(ckey, filename="preferences.json")
if(!ckey)
return
path = "data/player_saves/[ckey[1]]/[ckey]/[filename]"
/datum/preferences/proc/load_preferences()
/datum/preferences/proc/load_savefile()
if(!path)
return FALSE
if(!fexists(path))
return FALSE
CRASH("Attempted to load savefile without first loading a path!")
savefile = new /datum/json_savefile(path)
var/savefile/S = new /savefile(path)
if(!S)
return FALSE
S.cd = "/"
/datum/preferences/proc/load_preferences()
if(!savefile)
stack_trace("Attempted to load the preferences of [parent] without a savefile; did you forget to call load_savefile?")
load_savefile()
if(!savefile)
stack_trace("Failed to load the savefile for [parent] after manually calling load_savefile; something is very wrong.")
return FALSE
var/needs_update = savefile_needs_update(S)
var/needs_update = save_data_needs_update(savefile.get_entry())
if(needs_update == -2) //fatal, can't load any data
var/bacpath = "[path].updatebac" //todo: if the savefile version is higher then the server, check the backup, and give the player a prompt to load the backup
if (fexists(bacpath))
fdel(bacpath) //only keep 1 version of backup
fcopy(S, bacpath) //byond helpfully lets you use a savefile for the first arg.
fcopy(savefile.path, bacpath) //byond helpfully lets you use a savefile for the first arg.
return FALSE
apply_all_client_preferences()
//general preferences
READ_FILE(S["lastchangelog"], lastchangelog)
READ_FILE(S["be_special"] , be_special)
READ_FILE(S["default_slot"], default_slot)
READ_FILE(S["chat_toggles"], chat_toggles)
READ_FILE(S["toggles"], toggles)
READ_FILE(S["ignoring"], ignoring)
lastchangelog = savefile.get_entry("lastchangelog")
be_special = savefile.get_entry("be_special")
default_slot = savefile.get_entry("default_slot")
chat_toggles = savefile.get_entry("chat_toggles")
toggles = savefile.get_entry("toggles")
ignoring = savefile.get_entry("ignoring")
// OOC commendations
READ_FILE(S["hearted_until"], hearted_until)
hearted_until = savefile.get_entry("hearted_until")
if(hearted_until > world.realtime)
hearted = TRUE
//favorite outfits
READ_FILE(S["favorite_outfits"], favorite_outfits)
favorite_outfits = savefile.get_entry("favorite_outfits")
var/list/parsed_favs = list()
for(var/typetext in favorite_outfits)
@@ -194,15 +190,15 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
favorite_outfits = unique_list(parsed_favs)
// Custom hotkeys
READ_FILE(S["key_bindings"], key_bindings)
key_bindings = savefile.get_entry("key_bindings")
//try to fix any outdated data if necessary
if(needs_update >= 0)
var/bacpath = "[path].updatebac" //todo: if the savefile version is higher then the server, check the backup, and give the player a prompt to load the backup
if (fexists(bacpath))
fdel(bacpath) //only keep 1 version of backup
fcopy(S, bacpath) //byond helpfully lets you use a savefile for the first arg.
update_preferences(needs_update, S) //needs_update = savefile_version if we need an update (positive integer)
fcopy(savefile.path, bacpath) //byond helpfully lets you use a savefile for the first arg.
update_preferences(needs_update, savefile) //needs_update = savefile_version if we need an update (positive integer)
check_keybindings() // this apparently fails every time and overwrites any unloaded prefs with the default values, so don't load anything after this line or it won't actually save
key_bindings_by_key = get_key_bindings_by_key(key_bindings)
@@ -219,7 +215,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
var/old_default_slot = default_slot
var/old_max_save_slots = max_save_slots
for (var/slot in S.dir) //but first, update all current character slots.
for (var/slot in savefile.get_entry()) //but first, update all current character slots.
if (copytext(slot, 1, 10) != "character")
continue
var/slotnum = text2num(copytext(slot, 10))
@@ -236,14 +232,9 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
return TRUE
/datum/preferences/proc/save_preferences()
if(!path)
return FALSE
var/savefile/S = new /savefile(path)
if(!S)
return FALSE
S.cd = "/"
WRITE_FILE(S["version"] , SAVEFILE_VERSION_MAX) //updates (or failing that the sanity checks) will ensure data is not invalid at load. Assume up-to-date
if(!savefile)
CRASH("Attempted to save the preferences of [parent] without a savefile. This should have been handled by load_preferences()")
savefile.set_entry("version", SAVEFILE_VERSION_MAX) //updates (or failing that the sanity checks) will ensure data is not invalid at load. Assume up-to-date
for (var/preference_type in GLOB.preference_entries)
var/datum/preference/preference = GLOB.preference_entries[preference_type]
@@ -258,41 +249,31 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
if (preference_type in value_cache)
write_preference(preference, preference.serialize(value_cache[preference_type]))
//general preferences
WRITE_FILE(S["lastchangelog"], lastchangelog)
WRITE_FILE(S["be_special"], be_special)
WRITE_FILE(S["default_slot"], default_slot)
WRITE_FILE(S["toggles"], toggles)
WRITE_FILE(S["chat_toggles"], chat_toggles)
WRITE_FILE(S["ignoring"], ignoring)
WRITE_FILE(S["key_bindings"], key_bindings)
WRITE_FILE(S["hearted_until"], (hearted_until > world.realtime ? hearted_until : null))
WRITE_FILE(S["favorite_outfits"], favorite_outfits)
savefile.set_entry("lastchangelog", lastchangelog)
savefile.set_entry("be_special", be_special)
savefile.set_entry("default_slot", default_slot)
savefile.set_entry("toggles", toggles)
savefile.set_entry("chat_toggles", chat_toggles)
savefile.set_entry("ignoring", ignoring)
savefile.set_entry("key_bindings", key_bindings)
savefile.set_entry("hearted_until", (hearted_until > world.realtime ? hearted_until : null))
savefile.set_entry("favorite_outfits", favorite_outfits)
savefile.save()
return TRUE
/datum/preferences/proc/load_character(slot)
SHOULD_NOT_SLEEP(TRUE)
if(!path)
return FALSE
if(!fexists(path))
return FALSE
character_savefile = null
var/savefile/S = new /savefile(path)
if(!S)
return FALSE
S.cd = "/"
if(!slot)
slot = default_slot
slot = sanitize_integer(slot, 1, max_save_slots, initial(default_slot))
if(slot != default_slot)
default_slot = slot
WRITE_FILE(S["default_slot"] , slot)
savefile.set_entry("default_slot", slot)
S.cd = "/character[slot]"
var/needs_update = savefile_needs_update(S)
var/tree_key = "character[slot]"
var/list/save_data = savefile.get_entry(tree_key)
var/needs_update = save_data_needs_update(save_data)
if(needs_update == -2) //fatal, can't load any data
return FALSE
@@ -306,22 +287,23 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
read_preference(preference_type)
//Character
READ_FILE(S["randomise"], randomise)
randomise = save_data?["randomise"]
//Load prefs
READ_FILE(S["job_preferences"], job_preferences)
job_preferences = save_data?["job_preferences"]
//Quirks
READ_FILE(S["all_quirks"], all_quirks)
all_quirks = save_data?["all_quirks"]
//try to fix any outdated data if necessary
//preference updating will handle saving the updated data for us.
if(needs_update >= 0)
update_character(needs_update, S) //needs_update == savefile_version if we need an update (positive integer)
update_character(needs_update, save_data) //needs_update == savefile_version if we need an update (positive integer)
//Sanitize
randomise = SANITIZE_LIST(randomise)
job_preferences = SANITIZE_LIST(job_preferences)
all_quirks = SANITIZE_LIST(all_quirks)
//Validate job prefs
for(var/j in job_preferences)
@@ -338,10 +320,10 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
if(!path)
return FALSE
var/savefile/S = new /savefile(path)
if(!S)
return FALSE
S.cd = "/character[default_slot]"
var/tree_key = "character[default_slot]"
if(!(tree_key in savefile.get_entry()))
savefile.set_entry(tree_key, list())
var/save_data = savefile.get_entry(tree_key)
for (var/datum/preference/preference as anything in get_preferences_in_priority_order())
if (preference.savefile_identifier != PREFERENCE_CHARACTER)
@@ -355,7 +337,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
if (preference.type in value_cache)
write_preference(preference, preference.serialize(value_cache[preference.type]))
WRITE_FILE(S["version"] , SAVEFILE_VERSION_MAX) //load_character will sanitize any bad data, so assume up-to-date.)
save_data["version"] = SAVEFILE_VERSION_MAX //load_character will sanitize any bad data, so assume up-to-date.
// This is the version when the random security department was removed.
// When the minimum is higher than that version, it's impossible for someone to have the "Random" department.
@@ -364,13 +346,13 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
#endif
//Character
WRITE_FILE(S["randomise"] , randomise)
save_data["randomise"] = randomise
//Write prefs
WRITE_FILE(S["job_preferences"] , job_preferences)
save_data["job_preferences"] = job_preferences
//Quirks
WRITE_FILE(S["all_quirks"] , all_quirks)
save_data["all_quirks"] = all_quirks
return TRUE
@@ -392,17 +374,3 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
#undef SAVEFILE_VERSION_MAX
#undef SAVEFILE_VERSION_MIN
#ifdef TESTING
//DEBUG
//Some crude tools for testing savefiles
//path is the savefile path
/client/verb/savefile_export(path as text)
var/savefile/S = new /savefile(path)
S.ExportText("/",file("[path].txt"))
//path is the savefile path
/client/verb/savefile_import(path as text)
var/savefile/S = new /savefile(path)
S.ImportText("/",file("[path].txt"))
#endif
@@ -233,7 +233,7 @@
/// Fully randomizes everything according to the given flags.
/mob/living/carbon/human/proc/randomize_human_appearance(randomize_flags = ALL)
var/datum/preferences/preferences = new
var/datum/preferences/preferences = new(new /datum/client_interface)
for (var/datum/preference/preference as anything in get_preferences_in_priority_order())
if (!preference.included_in_randomization_flags(randomize_flags))
+1
View File
@@ -117,6 +117,7 @@
#include "hydroponics_self_mutations.dm"
#include "hydroponics_validate_genes.dm"
#include "inhands.dm"
#include "json_savefile_importing.dm"
#include "keybinding_init.dm"
#include "knockoff_component.dm"
#include "limbsanity.dm"
+1 -2
View File
@@ -8,8 +8,7 @@
var/datum/client_interface/client = new
human.mock_client = client
client.prefs = new
client.prefs = new(client)
client.prefs.write_preference(GLOB.preference_entries[/datum/preference/name/real_name], "Prefs Biddle")
@@ -0,0 +1,83 @@
/**
* The job of this unit test is to ensure that save files are correctly imported from BYOND to JSON.
* It's a rather convoluted process and so this test ensures that something didn't fuck up somewhere.
*/
/datum/unit_test/json_savefiles
var/savefile/test_savefile
var/datum/json_savefile/json_savefile
var/list/basic_list
var/list/assoc_list
var/var_string
/datum/unit_test/json_savefiles/proc/setup()
var/path_byond_file = "data/json_savefile_test.sav"
var/path_json_file = "data/json_savefile_test.json"
if(fexists(path_byond_file))
fdel(path_byond_file)
if(fexists(path_json_file))
fdel(path_json_file)
test_savefile = new /savefile(path_byond_file)
json_savefile = new /datum/json_savefile(path_json_file)
var_string = random_nukecode()
basic_list = list(rand(), rand(), rand(), "3", "6", "null", "\proper house")
assoc_list = list("2" = rand(), "4" = "3", "341" = "15134123", "\[22\]\[\]\[\[")
test_savefile["basic_list"] << basic_list
test_savefile["assoc_list"] << assoc_list
test_savefile["null_value"] << null
test_savefile["empty_list"] << list()
test_savefile.cd = "/v1/v2"
test_savefile["var_string"] << var_string
/datum/unit_test/json_savefiles/Run()
setup()
// first, we import the file to json
json_savefile.import_byond_savefile(test_savefile)
// now we seperate out the different values
var/byond_basic_list = json_encode(basic_list)
var/json_basic_list = json_encode(json_savefile.get_entry("basic_list"))
TEST_ASSERT_EQUAL(byond_basic_list, json_basic_list, "didn't convert basic list correctly")
var/byond_assoc_list = json_encode(assoc_list)
var/json_assoc_list = json_encode(json_savefile.get_entry("assoc_list"))
TEST_ASSERT_EQUAL(byond_assoc_list, json_assoc_list, "didn't convert associative list correctly")
var/null_value = json_savefile.get_entry("null_value")
var/default_value = json_savefile.get_entry("this_key_doesnt_exist", "defval")
TEST_ASSERT_NULL(null_value, "read an invalid value for what should be null")
TEST_ASSERT_EQUAL(default_value, "defval", "didn't grab the default value for a non existant key")
var/empty_list = json_savefile.get_entry("empty_list")
if(!istype(empty_list, /list))
TEST_FAIL("empty_list was not a list")
else
if(length(empty_list))
TEST_FAIL("empty_list was not empty")
var/empty_list_check_default = json_savefile.get_entry("empty_list", "123")
TEST_ASSERT_NOTEQUAL(empty_list_check_default, "123", "grabbed the default value for a key when key exists in tree")
// Now we check to ensure dir traversal is working as intended
// we are expecting v1 -> v2 -> var_string
var/dir_v1 = json_savefile.get_entry("v1")
var/dir_v2 = dir_v1?["v2"]
var/dir_string = dir_v2?["var_string"]
TEST_ASSERT_EQUAL(dir_string, var_string, "didn't traverse dirs correctly")
var/runtime_check_string = random_nukecode()
json_savefile.auto_save = TRUE
json_savefile.set_entry("runtime_saving", runtime_check_string)
var/runtime_read = json_savefile.get_entry("runtime_saving")
TEST_ASSERT_EQUAL(runtime_check_string, runtime_read, "wrote and read the same key but got different values")
json_savefile.wipe()
runtime_read = json_savefile.get_entry("runtime_saving")
TEST_ASSERT_NULL(runtime_read, "wiped the tree but data remained")
json_savefile.load()
runtime_read = json_savefile.get_entry("runtime_saving")
TEST_ASSERT_EQUAL(runtime_check_string, runtime_read, "saved and read the same key but got different values, auto save didn't work as expected")
+1 -1
View File
@@ -16,7 +16,7 @@
/datum/unit_test/nuke_cinematic/Run()
var/obj/machinery/nuclearbomb/syndicate/nuke = allocate(/obj/machinery/nuclearbomb/syndicate)
var/mob/living/carbon/human/nuked = allocate(/mob/living/carbon/human)
var/datum/client_interface/mock_client = new()
var/datum/client_interface/mock_client = new
nuked.mock_client = mock_client
mock_client.mob = nuked
+1 -1
View File
@@ -2,7 +2,7 @@
/datum/unit_test/preferences_implement_everything
/datum/unit_test/preferences_implement_everything/Run()
var/datum/preferences/preferences = new
var/datum/preferences/preferences = new(new /datum/client_interface)
var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)
for (var/preference_type in GLOB.preference_entries)
@@ -58,7 +58,7 @@
var/mob/dead/new_player/new_player = allocate(/mob/dead/new_player)
var/datum/client_interface/mock_client = new
mock_client.prefs = new
mock_client.prefs = new(mock_client)
var/write_success = mock_client.prefs.write_preference(
GLOB.preference_entries[/datum/preference/choiced/security_department],
SECURITY_OFFICER_DEPARTMENTS_TO_NAMES[preference],
+3
View File
@@ -52,6 +52,7 @@
#include "code\__DEFINES\chat.dm"
#include "code\__DEFINES\chat_filter.dm"
#include "code\__DEFINES\cleaning.dm"
#include "code\__DEFINES\client.dm"
#include "code\__DEFINES\clothing.dm"
#include "code\__DEFINES\colors.dm"
#include "code\__DEFINES\combat.dm"
@@ -622,6 +623,7 @@
#include "code\datums\hotkeys_help.dm"
#include "code\datums\http.dm"
#include "code\datums\hud.dm"
#include "code\datums\json_savefile.dm"
#include "code\datums\map_config.dm"
#include "code\datums\minigames_menu.dm"
#include "code\datums\mood.dm"
@@ -2773,6 +2775,7 @@
#include "code\modules\client\preferences\middleware\random.dm"
#include "code\modules\client\preferences\middleware\species.dm"
#include "code\modules\client\preferences\migrations\body_type_migration.dm"
#include "code\modules\client\preferences\migrations\convert_to_json_savefile.dm"
#include "code\modules\client\preferences\migrations\legacy_sound_toggles_migration.dm"
#include "code\modules\client\preferences\migrations\tgui_prefs_migration.dm"
#include "code\modules\client\preferences\species_features\basic.dm"