mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-09 16:12:17 +00:00
Co-authored-by: ShadowLarkens <shadowlarkens@gmail.com> Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
325 lines
12 KiB
Plaintext
325 lines
12 KiB
Plaintext
#define SAVEFILE_VERSION_MIN 8
|
|
#define SAVEFILE_VERSION_MAX 16
|
|
|
|
/*
|
|
SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn
|
|
This proc checks if the current directory of the savefile S needs updating
|
|
It is to be used by the load_character and load_preferences procs.
|
|
(S.cd == "/" is preferences, S.cd == "/character[integer]" is a character slot, etc)
|
|
|
|
if the current directory's version is below SAVEFILE_VERSION_MIN it will simply wipe everything in that directory
|
|
(if we're at root "/" then it'll just wipe the entire savefile, for instance.)
|
|
|
|
if its version is below SAVEFILE_VERSION_MAX but above the minimum, it will load data but later call the
|
|
respective update_preferences() or update_character() proc.
|
|
Those procs allow coders to specify format changes so users do not lose their setups and have to redo them again.
|
|
|
|
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/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"]) // special case: if there is no version key, such as in character slots before v12
|
|
return -3
|
|
if(save_data["version"] < SAVEFILE_VERSION_MIN)
|
|
return -2
|
|
if(save_data["version"] < SAVEFILE_VERSION_MAX)
|
|
return save_data["version"]
|
|
return -1
|
|
|
|
//should these procs get fairly long
|
|
//just increase SAVEFILE_VERSION_MIN so it's not as far behind
|
|
//SAVEFILE_VERSION_MAX and then delete any obsolete if clauses
|
|
//from these procs.
|
|
//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, datum/json_savefile/S)
|
|
// Migration from BYOND savefiles to JSON: Important milemark.
|
|
// if(current_version < 11)
|
|
|
|
// Migration for client preferences
|
|
if(current_version < 13)
|
|
log_debug("[client_ckey] preferences migrating from [current_version] to v13....")
|
|
to_chat(client, span_danger("Migrating savefile from version [current_version] to v13..."))
|
|
|
|
migration_13_preferences(S)
|
|
|
|
log_debug("[client_ckey] preferences successfully migrated from [current_version] to v13.")
|
|
to_chat(client, span_danger("v13 savefile migration complete."))
|
|
|
|
// Migration for nifs
|
|
if(current_version < 14)
|
|
log_debug("[client_ckey] preferences migrating from [current_version] to v14....")
|
|
to_chat(client, span_danger("Migrating savefile from version [current_version] to v14..."))
|
|
|
|
migration_14_nifs(S)
|
|
|
|
log_debug("[client_ckey] preferences successfully migrated from [current_version] to v14.")
|
|
to_chat(client, span_danger("v14 savefile migration complete."))
|
|
|
|
// Migration for nifs, again, to get rid of the /device path
|
|
if(current_version < 15)
|
|
log_debug("[client_ckey] preferences migrating from [current_version] to v15....")
|
|
to_chat(client, span_danger("Migrating savefile from version [current_version] to v15..."))
|
|
|
|
migration_15_nif_path(S)
|
|
|
|
log_debug("[client_ckey] preferences successfully migrated from [current_version] to v15.")
|
|
to_chat(client, span_danger("v15 savefile migration complete."))
|
|
|
|
// Migration for colors
|
|
if(current_version < 16)
|
|
log_debug("[client_ckey] preferences migrating from [current_version] to v16....")
|
|
to_chat(client, span_danger("Migrating savefile from version [current_version] to v16..."))
|
|
|
|
migration_16_colors(S)
|
|
|
|
log_debug("[client_ckey] preferences successfully migrated from [current_version] to v16.")
|
|
to_chat(client, span_danger("v16 savefile migration complete."))
|
|
|
|
|
|
/datum/preferences/proc/update_character(current_version, list/save_data)
|
|
// Migration from BYOND savefiles to JSON: Important milemark.
|
|
if(current_version == -3)
|
|
// Add a version field inside each character
|
|
save_data["version"] = SAVEFILE_VERSION_MAX
|
|
|
|
/// Migrates from byond savefile to json savefile
|
|
/datum/preferences/proc/try_savefile_type_migration()
|
|
log_debug("[client_ckey] preferences migrating from savefile to JSON...")
|
|
to_chat(client, span_danger("Savefile migration to JSON in progress..."))
|
|
|
|
load_path(client.ckey, "preferences.sav") // old save file
|
|
var/old_path = path
|
|
load_path(client.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()
|
|
|
|
log_debug("[client_ckey] preferences successfully migrated from savefile to JSON.")
|
|
to_chat(client, span_danger("Savefile migration to JSON is complete."))
|
|
|
|
return TRUE
|
|
|
|
/datum/preferences/proc/load_path(ckey, filename = "preferences.json")
|
|
if(!ckey || !load_and_save)
|
|
return
|
|
path = "data/player_saves/[copytext(ckey,1,2)]/[ckey]/[filename]"
|
|
|
|
/datum/preferences/proc/load_savefile()
|
|
if(load_and_save && !path)
|
|
CRASH("Attempted to load savefile without first loading a path!")
|
|
savefile = new /datum/json_savefile(load_and_save ? path : null)
|
|
|
|
// General preferences, have to be preloaded
|
|
/datum/preferences/proc/load_early_prefs()
|
|
lastchangelog = savefile.get_entry("lastchangelog", lastchangelog) // CHOMPAdd
|
|
default_slot = savefile.get_entry("default_slot", default_slot)
|
|
lastnews = savefile.get_entry("lastnews", lastnews)
|
|
lastlorenews = savefile.get_entry("lastlorenews", lastlorenews)
|
|
|
|
/datum/preferences/proc/sanitize_early_prefs()
|
|
lastchangelog = sanitize_text(lastchangelog, initial(lastchangelog)) // CHOMPAdd
|
|
default_slot = sanitize_integer(default_slot, 1, CONFIG_GET(number/character_slots), initial(default_slot))
|
|
lastnews = sanitize_text(lastnews, initial(lastnews))
|
|
lastlorenews = sanitize_text(lastlorenews, initial(lastlorenews))
|
|
|
|
/datum/preferences/proc/save_early_prefs()
|
|
savefile.set_entry("lastchangelog", lastchangelog) // CHOMPAdd
|
|
savefile.set_entry("default_slot", default_slot)
|
|
savefile.set_entry("lastnews", lastnews)
|
|
savefile.set_entry("lastlorenews", lastlorenews)
|
|
|
|
/datum/preferences/proc/load_preferences()
|
|
if(!savefile)
|
|
stack_trace("Attempted to load the preferences of [client] without a savefile; did you forget to call load_savefile?")
|
|
load_savefile()
|
|
if(!savefile)
|
|
stack_trace("Failed to load the savefile for [client] after manually calling load_savefile; something is very wrong.")
|
|
return FALSE
|
|
|
|
var/needs_update = save_data_needs_update(savefile.get_entry())
|
|
if(load_and_save && (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(savefile.path, bacpath) //byond helpfully lets you use a savefile for the first arg.
|
|
return FALSE
|
|
|
|
apply_all_client_preferences()
|
|
|
|
load_early_prefs()
|
|
sanitize_early_prefs()
|
|
|
|
//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(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)
|
|
|
|
// Load general prefs after applying migrations
|
|
player_setup.load_preferences(savefile)
|
|
|
|
//save the updated version
|
|
var/old_default_slot = default_slot
|
|
// var/old_max_save_slots = max_save_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))
|
|
if (!slotnum)
|
|
continue
|
|
// max_save_slots = max(max_save_slots, slotnum) //so we can still update byond member slots after they lose memeber status
|
|
default_slot = slotnum
|
|
if(load_character())
|
|
save_character()
|
|
default_slot = old_default_slot
|
|
// max_save_slots = old_max_save_slots
|
|
save_preferences()
|
|
else
|
|
// Load general prefs
|
|
player_setup.load_preferences(savefile)
|
|
|
|
return TRUE
|
|
|
|
/datum/preferences/proc/save_preferences()
|
|
if(!savefile)
|
|
CRASH("Attempted to save the preferences of [client] 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
|
|
|
|
player_setup.save_preferences(savefile)
|
|
|
|
for(var/preference_type in GLOB.preference_entries)
|
|
var/datum/preference/preference = GLOB.preference_entries[preference_type]
|
|
if(preference.savefile_identifier != PREFERENCE_PLAYER)
|
|
continue
|
|
|
|
if(!(preference.type in recently_updated_keys))
|
|
continue
|
|
|
|
recently_updated_keys -= preference.type
|
|
|
|
if(preference_type in value_cache)
|
|
write_preference(preference, preference.pref_serialize(value_cache[preference_type]))
|
|
|
|
save_early_prefs()
|
|
savefile.save()
|
|
|
|
return TRUE
|
|
|
|
/datum/preferences/proc/reset_slot()
|
|
var/bacpath = "[path].resetbac"
|
|
if(fexists(bacpath))
|
|
fdel(bacpath) //only keep 1 version of backup
|
|
fcopy(savefile.path, bacpath) //byond helpfully lets you use a savefile for the first arg.
|
|
|
|
savefile.remove_entry("character[default_slot]")
|
|
default_slot = 1
|
|
|
|
clear_character_previews()
|
|
|
|
// Load slot 1 character
|
|
load_character()
|
|
// And save them immediately, in case we load an empty slot
|
|
save_character()
|
|
save_preferences()
|
|
return TRUE
|
|
|
|
/datum/preferences/proc/load_character(slot)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
if(!slot)
|
|
slot = default_slot
|
|
|
|
slot = sanitize_integer(slot, 1, CONFIG_GET(number/character_slots), initial(default_slot))
|
|
if(slot != default_slot)
|
|
default_slot = slot
|
|
savefile.set_entry("default_slot", slot)
|
|
|
|
var/list/save_data = savefile.get_entry("character[slot]") // This is allowed to be null and will give a -1 in needs_update
|
|
|
|
var/needs_update = save_data_needs_update(save_data)
|
|
if(needs_update == -2) //fatal, can't load any data
|
|
return FALSE
|
|
|
|
// Read everything into cache (pre-migrations, as migrations should have access to deserialized data)
|
|
// Uses priority order as some values may rely on others for creating default values
|
|
for(var/datum/preference/preference as anything in get_preferences_in_priority_order())
|
|
if(preference.savefile_identifier != PREFERENCE_CHARACTER)
|
|
continue
|
|
|
|
value_cache -= preference.type
|
|
read_preference(preference.type)
|
|
|
|
// It has to be a list or load_character freaks out
|
|
if(!save_data)
|
|
player_setup.load_character(list())
|
|
else
|
|
player_setup.load_character(save_data)
|
|
|
|
//try to fix any outdated data if necessary
|
|
//preference updating will handle saving the updated data for us.
|
|
if(needs_update >= 0 || needs_update == -3)
|
|
update_character(needs_update, save_data) //needs_update == savefile_version if we need an update (positive integer
|
|
|
|
clear_character_previews()
|
|
return TRUE
|
|
|
|
/datum/preferences/proc/save_character()
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
if(!savefile)
|
|
return FALSE
|
|
|
|
var/tree_key = "character[default_slot]"
|
|
var/first_save = FALSE
|
|
if(!(tree_key in savefile.get_entry()))
|
|
savefile.set_entry(tree_key, list())
|
|
first_save = TRUE
|
|
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 && !first_save)
|
|
continue
|
|
|
|
if(!(preference.type in recently_updated_keys) && !first_save)
|
|
continue
|
|
|
|
recently_updated_keys -= preference.type
|
|
|
|
if(preference.type in value_cache)
|
|
write_preference(preference, preference.pref_serialize(value_cache[preference.type]))
|
|
|
|
save_data["version"] = SAVEFILE_VERSION_MAX //load_character will sanitize any bad data, so assume up-to-date.
|
|
player_setup.save_character(save_data)
|
|
|
|
return TRUE
|
|
|
|
/datum/preferences/proc/overwrite_character(slot)
|
|
if(!savefile)
|
|
return FALSE
|
|
if(!slot)
|
|
slot = default_slot
|
|
|
|
// This basically just changes default_slot without loading the correct data, so the next save call will overwrite
|
|
// the slot
|
|
slot = sanitize_integer(slot, 1, CONFIG_GET(number/character_slots), initial(default_slot))
|
|
if(slot != default_slot)
|
|
default_slot = slot
|
|
nif_path = nif_durability = nif_savedata = null //VOREStation Add - Don't copy NIF
|
|
savefile.set_entry("default_slot", slot)
|
|
|
|
return TRUE
|
|
|
|
/datum/preferences/proc/sanitize_preferences()
|
|
player_setup.sanitize_setup()
|
|
return TRUE
|
|
|
|
#undef SAVEFILE_VERSION_MAX
|
|
#undef SAVEFILE_VERSION_MIN
|