diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm
index 5d6c5626d55..999f4762c6e 100644
--- a/code/__defines/misc.dm
+++ b/code/__defines/misc.dm
@@ -79,6 +79,8 @@
#define MAX_NAME_LEN 26
#define MAX_DESC_LEN 128
#define MAX_TEXTFILE_LENGTH 128000 // 512GQ file
+//Maximum length of a MEDIUM_TEXT column type in a mariadb database.
+#define MAX_MEDIUM_TEXT_LEN 65535
// Event defines.
#define EVENT_LEVEL_MUNDANE 1
@@ -300,3 +302,6 @@
//Damage stuff
#define ITEM_HEALTH_NO_DAMAGE -1
+
+///Formats into a readable string neatly an exception's details.
+#define EXCEPTION_TEXT(E) "'[E.name]' ('[E.type]'): '[E.file]':[E.line][length(E.desc)? ":\n'[E.desc]'" : ""]"
\ No newline at end of file
diff --git a/code/__defines/time.dm b/code/__defines/time.dm
index 3d42eb81b50..83001acc8d0 100644
--- a/code/__defines/time.dm
+++ b/code/__defines/time.dm
@@ -22,3 +22,6 @@
#define worldtime2stationtime(time) time2text(roundstart_hour HOURS + time, "hh:mm")
#define round_duration_in_ticks (round_start_time ? world.time - round_start_time : 0)
#define station_time_in_ticks (roundstart_hour HOURS + round_duration_in_ticks)
+
+///Convert a time value from REALTIMEOFDAY into an amount of seconds
+#define REALTIMEOFDAY2SEC(T) ((REALTIMEOFDAY - T) / (1 SECOND))
\ No newline at end of file
diff --git a/code/_macros.dm b/code/_macros.dm
index 3da8159f70b..ce559e6a3b9 100644
--- a/code/_macros.dm
+++ b/code/_macros.dm
@@ -168,6 +168,12 @@
#define SPAN_PINK(X) SPAN_CLASS("font_pink", X)
#define SPAN_PALEPINK(X) SPAN_CLASS("font_palepink", X)
#define SPAN_SINISTER(X) SPAN_CLASS("sinister", X)
+
+//Persistence
+#define SPAN_SERIALIZER(X) SPAN_CLASS("serializer", X)
+#define SPAN_AUTOSAVE(X) SPAN_CLASS("autosave", X)
+#define SPAN_AUTOSAVE_WARN(X) SPAN_CLASS("autosave_warn", X)
+
// placeholders
#define SPAN_GOOD(X) SPAN_GREEN(X)
#define SPAN_NEUTRAL(X) SPAN_BLUE(X)
diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm
index 5a47add0257..aed434653b6 100644
--- a/code/controllers/configuration.dm
+++ b/code/controllers/configuration.dm
@@ -848,7 +848,9 @@ var/global/list/gamemode_cache = list()
config.show_typing_indicator_for_whispers = TRUE
else
- log_misc("Unknown setting in configuration: '[name]'")
+ //Shitty hook to get our extra settings to load until config options code is less dumb
+ if(!load_mod_config(name, value))
+ log_misc("Unknown setting in configuration: '[name]'")
else if(type == "game_options")
if(!value)
@@ -976,7 +978,9 @@ var/global/list/gamemode_cache = list()
config.disable_daycycle = TRUE
else
- log_misc("Unknown setting in configuration: '[name]'")
+ //Shitty hook to get our extra settings to load until config options code is less dumb
+ if(!load_mod_game_options(name, value))
+ log_misc("Unknown setting in configuration: '[name]'")
fps = round(fps)
if(fps <= 0)
@@ -1020,7 +1024,8 @@ var/global/list/gamemode_cache = list()
if ("password")
sqlpass = value
else
- log_misc("Unknown setting in configuration: '[name]'")
+ if(!load_mod_dbconfig(name, value))
+ log_misc("Unknown setting in configuration: '[name]'")
/datum/configuration/proc/pick_mode(mode_name)
// I wish I didn't have to instance the game modes in order to look up
@@ -1042,3 +1047,15 @@ var/global/list/gamemode_cache = list()
var/event_info = safe_file2text(filename, FALSE)
if(event_info)
custom_event_msg = event_info
+
+///Hook stub for loading modpack specific configs. Just override in modpack.
+/datum/configuration/proc/load_mod_config(var/name, var/value)
+ return
+
+///Hook stub for loading modpack specific game_options. Just override in modpack.
+/datum/configuration/proc/load_mod_game_options(var/name, var/value)
+ return
+
+///Hook stub for loading modpack specific dbconfig. Just override in modpack.
+/datum/configuration/proc/load_mod_dbconfig(var/name, var/value)
+ return
\ No newline at end of file
diff --git a/code/controllers/subsystems/air.dm b/code/controllers/subsystems/air.dm
index 5e9105dd1f6..793f85d10fb 100644
--- a/code/controllers/subsystems/air.dm
+++ b/code/controllers/subsystems/air.dm
@@ -441,3 +441,9 @@ Total Unsimulated Turfs: [world.maxx*world.maxy*world.maxz - simulated_turf_coun
active_edges -= E
if(processing_edges)
processing_edges -= E
+
+/datum/controller/subsystem/air/proc/invalidate_all_zones()
+ while(SSair.zones.len)
+ var/zone/zone = SSair.zones[SSair.zones.len]
+ SSair.zones.len--
+ zone.c_invalidate()
\ No newline at end of file
diff --git a/code/controllers/subsystems/machines.dm b/code/controllers/subsystems/machines.dm
index 48d7b425037..45afc55e449 100644
--- a/code/controllers/subsystems/machines.dm
+++ b/code/controllers/subsystems/machines.dm
@@ -215,6 +215,16 @@ if(current_step == this_step || (check_resumed && !resumed)) {\
if (istype(SSmachines.power_objects))
power_objects = SSmachines.power_objects
+///Divides the contents of all pipenets into their individual members.
+/datum/controller/subsystem/machines/proc/temporarily_store_pipenets()
+ if(can_fire)
+ log_warning(("Tried to store pipenets air while the subsystem is running!"))
+ CRASH("Tried to store pipenets air while the subsystem is running!")
+
+ for(var/datum/pipe_network/net in SSmachines.pipenets)
+ for(var/datum/pipeline/line in net.line_members)
+ line.temporarily_store_fluids()
+
#undef SSMACHINES_PIPENETS
#undef SSMACHINES_MACHINERY
#undef SSMACHINES_POWERNETS
diff --git a/code/modules/client/darkmode.css b/code/modules/client/darkmode.css
index 857df3e3138..799146eaba3 100644
--- a/code/modules/client/darkmode.css
+++ b/code/modules/client/darkmode.css
@@ -96,6 +96,11 @@ h1.alert, h2.alert {color: #a4bad6;}
.antagdesc {color: #ff0033; font-size: 125%}
.reflex_shoot {color: #000099; font-style: italic;}
+/*Persistence*/
+.autosave {color: #00ff00; font-weight: bold; font-size: 125%;}
+.autosave_warn {color: #a13434; font-weight: bold; font-size: 125%;}
+.serializer {color: #a523a5; font-weight: bold;}
+
/* General purpose colour classes */
.font_red {color: #ff0000;}
.font_orange {color: #ff7f00;}
diff --git a/code/modules/client/lightmode.css b/code/modules/client/lightmode.css
index 2c8f3c2dca5..9f1e86c9d05 100644
--- a/code/modules/client/lightmode.css
+++ b/code/modules/client/lightmode.css
@@ -92,6 +92,11 @@ h1.alert, h2.alert {color: #000000;}
.antagdesc {color: #ff0033; font-size: 125%}
.reflex_shoot {color: #000099; font-style: italic;}
+/*Persistence*/
+.autosave {color: #104b10; font-weight: bold; font-size: 125%;}
+.autosave_warn {color: #5f2525; font-weight: bold; font-size: 125%;}
+.serializer {color: #791979; font-weight: bold;}
+
/* General purpose colour classes */
.font_red {color: #ff0000;}
.font_orange {color: #ff7f00;}
diff --git a/config/example/config.txt b/config/example/config.txt
index f7edfa0499e..d66bd3b7ae3 100644
--- a/config/example/config.txt
+++ b/config/example/config.txt
@@ -462,3 +462,19 @@ RADIATION_LOWER_LIMIT 0.15
## Uncomment this to show a typing indicator for people writing whispers.
#SHOW_TYPING_INDICATOR_FOR_WHISPERS
+
+################################
+## Persistence Stuff
+################################
+
+## Time in minutes between automated world saves. Default is every 120 minutes.
+#AUTOSAVE_INTERVAL 120
+
+## Uptime in hours after which the next autosave will force a server reboot. Default is 12 Hours. Setting to 0 disables it.
+#AUTOSAVE_AUTO_RESTART 12
+
+## !! - Use With Caution - !!
+## Set what kind of errors will be skipped over during saving/loading if encountered. Default is "NONE".
+## This value is meant to be used for rescuing a broken save, or forcing a broken save to work. As it will most likely cause problems if used constantly.
+## The possible values are: "NONE"->No errors allowed at all, "RECOVERABLE"->Only recoverable errors allowed, "ANY"->Any error, even connection error will be ignored.
+#SAVE_ERROR_TOLERANCE NONE
\ No newline at end of file
diff --git a/mods/persistence/__defines/logging.dm b/mods/persistence/__defines/logging.dm
index 4beafd93132..966ed164106 100644
--- a/mods/persistence/__defines/logging.dm
+++ b/mods/persistence/__defines/logging.dm
@@ -1,3 +1,3 @@
/proc/report_progress_serializer(var/progress_message)
- admin_notice("[progress_message]", R_DEBUG)
- to_world_log(progress_message)
+ admin_notice(SPAN_SERIALIZER(progress_message), R_DEBUG)
+ to_world_log(SPAN_SERIALIZER(progress_message))
diff --git a/mods/persistence/__defines/serializer.dm b/mods/persistence/__defines/serializer.dm
index 3a3763e3e36..2cbbc1c0a82 100644
--- a/mods/persistence/__defines/serializer.dm
+++ b/mods/persistence/__defines/serializer.dm
@@ -1,35 +1,65 @@
-/*
- Serialized type names
-*/
-#define SERIALIZER_TYPE_NULL "NULL"
-#define SERIALIZER_TYPE_VAR "VAR"
-#define SERIALIZER_TYPE_TEXT "TEXT"
-#define SERIALIZER_TYPE_NUM "NUM"
-#define SERIALIZER_TYPE_PATH "PATH"
-#define SERIALIZER_TYPE_FILE "FILE"
-#define SERIALIZER_TYPE_WRAPPER "WRAP"
-#define SERIALIZER_TYPE_LIST "LIST"
-#define SERIALIZER_TYPE_LIST_EMPTY "EMPTY"
-#define SERIALIZER_TYPE_DATUM "OBJ"
-#define SERIALIZER_TYPE_DATUM_FLAT "FLAT_OBJ"
-#define SERIALIZER_TYPE_FLAT_REF "FLAT_REF"
-
-/*
- SQL table names
-*/
-#define SQLS_TABLE_DATUM "thing"
-#define SQLS_TABLE_DATUM_VARS "thing_var"
-#define SQLS_TABLE_LIST_ELEM "list_element"
-#define SQLS_TABLE_Z_LEVELS "z_level"
-#define SQLS_TABLE_AREAS "areas"
-#define SQLS_TABLE_LIMBO "limbo"
-#define SQLS_TABLE_LIMBO_DATUM "limbo_thing"
-#define SQLS_TABLE_LIMBO_DATUM_VARS "limbo_thing_var"
-#define SQLS_TABLE_LIMBO_LIST_ELEM "limbo_list_element"
+///A macro for getting the current save DB name
+#define SQLS_SAVE_DATABASE global.sqldb
+/////////////////////////////////////////////////////////
+// Serialized type names
+/////////////////////////////////////////////////////////
+
+#define SERIALIZER_TYPE_NULL "NULL"
+#define SERIALIZER_TYPE_VAR "VAR"
+#define SERIALIZER_TYPE_TEXT "TEXT"
+#define SERIALIZER_TYPE_NUM "NUM"
+#define SERIALIZER_TYPE_PATH "PATH"
+#define SERIALIZER_TYPE_FILE "FILE"
+#define SERIALIZER_TYPE_WRAPPER "WRAP"
+#define SERIALIZER_TYPE_LIST "LIST"
+#define SERIALIZER_TYPE_LIST_EMPTY "EMPTY"
+#define SERIALIZER_TYPE_DATUM "OBJ"
+#define SERIALIZER_TYPE_DATUM_FLAT "FLAT_OBJ"
+#define SERIALIZER_TYPE_FLAT_REF "FLAT_REF"
+
+/////////////////////////////////////////////////////////
+// SQL table names
+/////////////////////////////////////////////////////////
+
+#define SQLS_TABLE_DATUM "thing"
+#define SQLS_TABLE_DATUM_VARS "thing_var"
+#define SQLS_TABLE_LIST_ELEM "list_element"
+#define SQLS_TABLE_Z_LEVELS "z_level"
+#define SQLS_TABLE_AREAS "areas"
+#define SQLS_TABLE_LIMBO "limbo"
+#define SQLS_TABLE_LIMBO_DATUM "limbo_thing"
+#define SQLS_TABLE_LIMBO_DATUM_VARS "limbo_thing_var"
+#define SQLS_TABLE_LIMBO_LIST_ELEM "limbo_list_element"
+
+/////////////////////////////////////////////////////////
+// SQL Stored Functions Names
+/////////////////////////////////////////////////////////
+
+///Name of the stored function that returns the time we last made a world save from the db.
+#define SQLS_FUNC_GET_LAST_SAVE_TIME "GetLastWorldSaveTime"
+///Log to the table when a world save begins, returns the current save log id.
+#define SQLS_FUNC_LOG_SAVE_WORLD_START "LogSaveWorldStart"
+///Log to the table when a limbo/storage save begins, returns the current save log id.
+#define SQLS_FUNC_LOG_SAVE_STORAGE_START "LogSaveStorageStart"
+///Log to the table when any save ends, returns the current save log id.
+#define SQLS_FUNC_LOG_SAVE_END "LogSaveEnd"
+
+/////////////////////////////////////////////////////////
+// SQL Stored Procedures Names
+/////////////////////////////////////////////////////////
+
+///Delete the current world save from the db, so we can write a newer one. Procedures are executed with CALL, and don't return anything.
+#define SQLS_PROC_CLEAR_WORLD_SAVE "ClearWorldSave"
+
+/////////////////////////////////////////////////////////
+// SQL Helpers
+/////////////////////////////////////////////////////////
+
+///A helper for executing a sql query and throwing the proper exception with a standardized error message. QUERY must be a variable.
#define SQLS_EXECUTE_AND_REPORT_ERROR(QUERY, ERRORMSG)\
if(!QUERY.Execute()){\
- var/errormsg = ERRORMSG + " '[QUERY.ErrorMsg()]'"; \
+ var/errormsg = ERRORMSG + " '[QUERY.ErrorMsg()]'" + "\n'[QUERY.sql]'"; \
to_world_log(errormsg);\
throw new /exception/sql_connection(errormsg, __FILE__, __LINE__); \
}
diff --git a/mods/persistence/_persistence.dme b/mods/persistence/_persistence.dme
index 4afeef26df6..8193a6cebf7 100644
--- a/mods/persistence/_persistence.dme
+++ b/mods/persistence/_persistence.dme
@@ -26,6 +26,10 @@
#include "controllers\subsystems\mapping.dm"
#include "controllers\subsystems\mining.dm"
#include "controllers\subsystems\persistence.dm"
+#include "controllers\subsystems\persistence\persistence_loading.dm"
+#include "controllers\subsystems\persistence\persistence_saving.dm"
+#include "controllers\subsystems\persistence\persistence_stats.dm"
+#include "controllers\subsystems\persistence\persistence_storage.dm"
#include "controllers\subsystems\skills.dm"
#include "controllers\subsystems\initialization\chargen.dm"
#include "controllers\subsystems\initialization\fabrication.dm"
@@ -284,6 +288,7 @@
#include "modules\world_save\serializers\one_off_serializer.dm"
#include "modules\world_save\serializers\sql_serializer.dm"
#include "modules\world_save\serializers\sql_serializer_db.dm"
+#include "modules\world_save\save_testing.dm"
#include "modules\world_save\wrappers\_late_wrapper.dm"
#include "modules\world_save\wrappers\_wrapper.dm"
#include "modules\world_save\wrappers\_wrapper_holder.dm"
diff --git a/mods/persistence/code/__defines/world_save.dm b/mods/persistence/code/__defines/world_save.dm
index 6c8ec2ef582..bc9f9ba1f29 100644
--- a/mods/persistence/code/__defines/world_save.dm
+++ b/mods/persistence/code/__defines/world_save.dm
@@ -23,4 +23,14 @@ else{src.custom_saved |= list(VARNAMES);}
//Helper to place at the end of Initialize of saved objects to make sure they lateinit only if they don't get deleted during init and if they were saved!
#define LATE_INIT_IF_SAVED \
-if(. != INITIALIZE_HINT_QDEL && src.persistent_id){return INITIALIZE_HINT_LATELOAD;}
\ No newline at end of file
+if(. != INITIALIZE_HINT_QDEL && src.persistent_id){return INITIALIZE_HINT_LATELOAD;}
+
+////////////////////////////////////////////
+// Persistence Error Tolerance
+////////////////////////////////////////////
+///Do not tolerate any errors during save/load.
+#define PERSISTENCE_ERROR_TOLERANCE_NONE 0
+///Tolerate only recoverable errors during save/load.
+#define PERSISTENCE_ERROR_TOLERANCE_RECOVERABLE 1
+///Tolerate ANY errors during save/load. This is likely to cause a corrupted save.
+#define PERSISTENCE_ERROR_TOLERANCE_ANY 2
\ No newline at end of file
diff --git a/mods/persistence/code/controllers/configuration.dm b/mods/persistence/code/controllers/configuration.dm
index 5830eedba94..6e22113ebaf 100644
--- a/mods/persistence/code/controllers/configuration.dm
+++ b/mods/persistence/code/controllers/configuration.dm
@@ -1,3 +1,34 @@
/datum/configuration
- var/autosave_interval = 60 MINUTES
- var/autosave_auto_reset = 12 HOURS
\ No newline at end of file
+ var/autosave_interval = 2 HOURS
+ var/autosave_auto_restart = 12 HOURS
+ var/save_error_tolerance = PERSISTENCE_ERROR_TOLERANCE_NONE
+
+/datum/configuration/load_mod_config(name, value)
+ . = ..()
+ switch(name)
+ if("autosave_interval")
+ autosave_interval = text2num(value) MINUTES
+ . = TRUE
+ if("autosave_auto_restart")
+ autosave_auto_restart = text2num(value) HOURS
+ . = TRUE
+ if("save_error_tolerance")
+ value = lowertext(value)
+ switch(value)
+ if("any")
+ save_error_tolerance = PERSISTENCE_ERROR_TOLERANCE_ANY
+ if("recoverable")
+ save_error_tolerance = PERSISTENCE_ERROR_TOLERANCE_RECOVERABLE
+ if("none")
+ save_error_tolerance = PERSISTENCE_ERROR_TOLERANCE_NONE
+ else
+ log_misc("Bad value for '[name]' : '[value]'! (Expected 'any', 'recoverable' or 'none')")
+ . = TRUE
+
+///Hook to add persistence settings meant to be in the game_options file if any.
+/datum/configuration/load_mod_game_options(name, value)
+ . = ..()
+
+///Hook to add persistence settings meant to be in the dbconfig file if any.
+/datum/configuration/load_mod_dbconfig(name, value)
+ . = ..()
\ No newline at end of file
diff --git a/mods/persistence/controllers/subsystems/autosave.dm b/mods/persistence/controllers/subsystems/autosave.dm
index 87394bb5876..31151d28413 100644
--- a/mods/persistence/controllers/subsystems/autosave.dm
+++ b/mods/persistence/controllers/subsystems/autosave.dm
@@ -10,9 +10,9 @@ SUBSYSTEM_DEF(autosave)
var/last_save // world.time of the last save
var/autosave_interval // Time between autosaves.
-/datum/controller/subsystem/autosave/Initialize()
+/datum/controller/subsystem/autosave/Initialize(start_timeofday)
. = ..()
- last_save = world.time
+ last_save = world.time
autosave_interval = config.autosave_interval // To prevent saving upon start.
/datum/controller/subsystem/autosave/stat_entry()
@@ -22,53 +22,73 @@ SUBSYSTEM_DEF(autosave)
else if(saving)
msg = "Currently Saving..."
else
- msg = "Next Autosave in [ round(((last_save + autosave_interval) - world.time) / (1 MINUTE), 0.1)] Minutes."
+ msg = "Next Autosave in [round(((last_save + autosave_interval) - world.time) / (1 MINUTE), 0.1)] Minutes."
..(msg)
/datum/controller/subsystem/autosave/fire()
AnnounceSave()
- if(last_save + autosave_interval <= world.time)
+ if((last_save + autosave_interval) <= world.time)
Save()
/datum/controller/subsystem/autosave/proc/Save(var/check_for_restart = TRUE)
if(saving)
- message_admins(SPAN_DANGER("Attempted to save while already saving!"))
- else
- saves += 1
- to_world("Beginning save! Server will unpause when save is complete.")
+ message_admins(SPAN_DANGER("Attempted autosave while already making an autosave!"))
+ return
+ var/exception/last_except = null
+ var/restart_after_save = (config.autosave_auto_restart > 0) && (world.time >= config.autosave_auto_restart)
+ saves += 1
+ saving = TRUE
- var/reset_after_save = config.autosave_auto_reset > 0 && world.time >= config.autosave_auto_reset
+ try
+ //Announce saving start!
+ to_world(SPAN_AUTOSAVE("Beginning autosave! Server will pause until complete."))
+ if(check_for_restart && restart_after_save)
+ to_world(SPAN_AUTOSAVE_WARN("Server is restarting after this autosave!"))
+ sleep(5)
- if(check_for_restart && reset_after_save)
- to_world("Server is resetting after this save!")
+ //Begin actual save
+ SSpersistence.SaveWorld(name)
- saving = 1
- for(var/datum/controller/subsystem/S in Master.subsystems)
- S.disable()
- SSpersistence.SaveWorld()
- for(var/datum/controller/subsystem/S in Master.subsystems)
- S.enable()
- saving = 0
- to_world("World save complete!")
+ catch(var/exception/e)
+ //Prevent exception going upwards the stack if we fail, so we don't keep saving over and over, and don't break saving afterwards
+ log_warning("datum/controller/subsystem/autosave/proc/Save() was interrupted by an exception!")
+ last_except = e
+
+ //Set the next save timer + indicate we're done saving
+ saving = FALSE
+ last_save = world.time
- if(check_for_restart && reset_after_save)
- to_world("Server is going down NOW!")
- world.Reboot()
+ //Before telling the user we've had success throw any exceptions we've hit
+ if(!isnull(last_except))
+ throw last_except //Kick us out after we've cleaned up our state and report the exception.
- last_save = world.time
+ //Otherwise, everything is going fine
+ to_world(SPAN_AUTOSAVE("Autosave complete!"))
+
+ if(check_for_restart && restart_after_save)
+ to_world(SPAN_AUTOSAVE_WARN("Server is going down NOW!"))
+ sleep(1 SECOND)
+ world.Reboot()
/datum/controller/subsystem/autosave/proc/AnnounceSave()
- var/minutes = (last_save + autosave_interval - world.time) / (1 MINUTE)
+ var/minutes_left = (last_save + autosave_interval - world.time) / (1 MINUTE)
- if(!announced && minutes <= 5)
- to_world("Autosave in 5 minutes!")
- if((world.time + minutes MINUTES) >= config.autosave_auto_reset)
- to_world("The server will reboot after this save!")
+ if(!announced && minutes_left <= 5)
+ to_world(SPAN_AUTOSAVE("Autosave in 5 minutes!"))
+ if((world.time + minutes_left MINUTES) >= config.autosave_auto_restart)
+ to_world(SPAN_AUTOSAVE("The server will reboot after this save!"))
announced = 1
- if(announced == 1 && minutes <= 1)
- to_world("Autosave in 1 minute!")
- if((world.time + minutes MINUTES) >= config.autosave_auto_reset)
- to_world("The server will reboot after this save!")
+ if(announced == 1 && minutes_left <= 1)
+ to_world(SPAN_AUTOSAVE("Autosave in 1 minute!"))
+ if((world.time + minutes_left MINUTES) >= config.autosave_auto_restart)
+ to_world(SPAN_AUTOSAVE("The server will reboot after this save!"))
announced = 2
- if(announced == 2 && minutes >= 6)
- announced = 0
\ No newline at end of file
+ if(announced == 2 && minutes_left >= 6)
+ announced = 0
+
+///Adds the given amount of time to the next autosave timer.
+/datum/controller/subsystem/autosave/proc/DelayNextSave(var/delay)
+ //Prevent bad things from happening if we somehow trigger another save at the same time
+ if(saving)
+ CRASH("Cannot change the time to next save while we're currently saving!")
+ last_save += delay
\ No newline at end of file
diff --git a/mods/persistence/controllers/subsystems/mapping.dm b/mods/persistence/controllers/subsystems/mapping.dm
index 1352c8d1923..3585f30b12a 100644
--- a/mods/persistence/controllers/subsystems/mapping.dm
+++ b/mods/persistence/controllers/subsystems/mapping.dm
@@ -17,3 +17,19 @@
/datum/controller/subsystem/mapping/proc/Save()
SSpersistence.SaveWorld()
+
+///Work around for saved level and stuff not restoring properly after save load. Would be simpler if we'd just modify the base class.
+/datum/controller/subsystem/mapping/register_level_data(var/datum/level_data/LD)
+ if(!(. = ..()))
+ return .
+ if(LD.level_flags & ZLEVEL_SAVED)
+ SSpersistence.saved_levels |= LD.level_z
+ if(LD.level_flags & ZLEVEL_MINING)
+ SSmapping.mining_levels |= LD.level_z
+ return .
+/datum/controller/subsystem/mapping/unregister_level_data(var/datum/level_data/LD)
+ if(!(. = ..()))
+ return .
+ SSpersistence.saved_levels -= LD.level_z
+ SSmapping.mining_levels -= LD.level_z
+ return .
diff --git a/mods/persistence/controllers/subsystems/persistence.dm b/mods/persistence/controllers/subsystems/persistence.dm
index 11e237f6e67..75c415fec5d 100644
--- a/mods/persistence/controllers/subsystems/persistence.dm
+++ b/mods/persistence/controllers/subsystems/persistence.dm
@@ -1,679 +1,292 @@
+/*
+ #TODO: Save system overhaul:
+ * Have saving sleeps/stoplag() every bunch of turfs, so connection to clients doesn't cut so easily.
+ * Unify the sql serializer stuff, so it's less redundant. (We don't need 2 different serializer objects.)
+ * Don't iterate the world 3 times over for no reasons. ( "for(var/atom/thing in somearea)" )
+ * Don't access area's contents, behind the scene it just accesses world contents and is massively slow.
+ * Don't concatenate strings more than once!! Use a list if you must, but just adding to a string is a hundred time slower
+ than just putting strings in a list and running jointext() on them.
+ * Serialize turfs in chunks, and commit after each chunks so we have a coherent set of turfs to work with.
+*/
+
+///Init priority for the persistence subsystem
#define SS_INIT_PERSISTENCE 18.5
+///Comparison function for sorting the list of atoms that took the longest to save.
/proc/cmp_serialization_stats_dsc(var/datum/serialization_stat/S1, var/datum/serialization_stat/S2)
return S2.time_spent - S1.time_spent
+///Retally all area power on round start
+/hook/roundstart/proc/retally_all_power()
+ for(var/area/A)
+ A.retally_power()
+ return TRUE
+
+/**
+ * Override of the persistence subsystem for handling saving the entire world.
+ */
/datum/controller/subsystem/persistence
- name = "Persistence"
+ name = "Persistence"
init_order = SS_INIT_PERSISTENCE
- flags = SS_NO_FIRE
+ flags = SS_NO_FIRE
- var/save_exists = FALSE // Whether or not a save exists in the DB
- var/in_loaded_world = FALSE // Whether or not we're in a world that was loaded.
+ // *** Config ***
+ ///Whether or not rent will be required for created sectors.
+ var/rent_enabled = FALSE
+ ///If set, we'll do everything we can to generate a valid save even if some things do runtime.
+ var/error_tolerance = PERSISTENCE_ERROR_TOLERANCE_NONE
- var/list/saved_areas = list()
- var/list/saved_levels = list() // Saved levels are saved entirely and optimized with get_base_turf()
- var/list/saved_extensions = list() // Extensions mark themselves to be saved on world save.
-
- var/rent_enabled = FALSE // Whether or not rent will be required for created sectors.
-
- var/serializer/sql/serializer = new() // The serializer impl for actually saving.
- var/serializer/sql/one_off/one_off = new() // The serializer impl for one off serialization/deserialization.
-
- var/list/limbo_removals = list() // Objects which will be removed from limbo on the next save. Format is list(limbo_key, limbo_type)
- var/list/limbo_refs = list() // Objects which are deserialized out of limbo don't have their refs in the database immediately, so we add them here until the next save
- // Format is p_id -> ref
+ // *** State ***
+ ///Whether or not a save exists in the DB. Value is cached when a save did exist at runtime for performance reasons.
+ var/save_exists = FALSE
+ ///Whether or not we're currently running a loaded save.
+ var/in_loaded_world = FALSE
+ ///Whether we're currently loading a world save.
var/loading_world = FALSE
+ ///The time when the currently loaded world save was made if any. NULL means we never loaded a save.
+ var/loaded_save_time
+ ///Save log entry index from the database for the currently running save. Used to fill in the saving result into the db.
+ var/save_log_id
+ ///Holds the previous entering allowed state while we're running a save.
+ var/was_entering_allowed
+
+ ///Text to log into the database log for the completion/failure of the save
+ var/save_complete_text = ""
+ ///Span class to use for displaying the completion/failure message into chat.
+ var/save_complete_span_class = "danger"
+ ///Amount of z-levels saved
+ var/nb_saved_z_levels = 0
+ ///Amount of atoms saved
+ var/nb_saved_atoms = 0
+
+ //#FIXME: Not sure if keeping a separate list of those here is a very good idea?
+ var/list/saved_areas = list()
+ /// Saved levels are saved entirely and optimized with get_base_turf()
+ var/list/saved_levels = list()
+ /// Extensions mark themselves to be saved on world save.
+ var/list/saved_extensions = list()
+
+ //#FIXME: The idea for having a generic serializer class was that we wouldn't initiate the exact subtype in the var definition.
+ // So that you could just interchangeably use another kind of serializer that saves to json or xml or whatever instead of sql seamlessly.
+ // This implementation completely nullifies the whole point of having a serializer class currently?
+ /// The serializer impl for actually saving.
+ var/serializer/sql/serializer = new()
+ /// The serializer impl for one off serialization/deserialization.
+ var/serializer/sql/one_off/one_off = new()
+
+ //#FIXME: Ideally, this shouldn't be handled by the server. The database could cross-reference atoms that were in limbo with those already in the world,
+ // and just clear their limbo entry. It would thousands of time faster.
+ /// Objects which will be removed from limbo on the next save. Format is list(limbo_key, limbo_type)
+ var/list/limbo_removals = list()
+ /// Objects which are deserialized out of limbo don't have their refs in the database immediately, so we add them here until the next save. Format is p_id -> ref
+ var/list/limbo_refs = list()
+
+ /// Some wrapped objects need special behavior post-load. This list is cleared post-atom Init.
+ var/list/late_wrappers = list()
+
+/////////////////////////////////////////////////////////////////
+// Accessors
+/////////////////////////////////////////////////////////////////
+
+///Prints a description of the tables on the connected database into the chat for the usr calling this proc.
+/datum/controller/subsystem/persistence/proc/PrintDBStatus()
+ return SQLS_Print_DB_STATUS()
- var/list/late_wrappers = list() // Some wrapped objects need special behavior post-load. This list is cleared post-atom Init.
-
+///Returns true if a save already exists on the DB
/datum/controller/subsystem/persistence/proc/SaveExists()
if(!save_exists)
save_exists = establish_save_db_connection() && serializer.save_exists()
in_loaded_world = save_exists
return save_exists
-/datum/controller/subsystem/persistence/proc/SaveWorld()
- // Collect the z-levels we're saving and get the turfs!
- to_world_log("Saving [LAZYLEN(SSpersistence.saved_levels)] z-levels. World size max ([world.maxx],[world.maxy])")
- sleep(5)
- serializer._before_serialize()
+///Timestamp of when the currently loaded save was made.
+/datum/controller/subsystem/persistence/proc/LoadedSaveTimestamp()
+ if(!in_loaded_world)
+ return
+ if(!length(loaded_save_time))
+ loaded_save_time = serializer.last_loaded_save_time()
+ return loaded_save_time
- var/start = world.timeofday
+///Sets the level of error tolerance to use during saves/loads. Default is PERSISTENCE_ERROR_TOLERANCE_NONE.
+/datum/controller/subsystem/persistence/proc/SetErrorTolerance(var/tolerance_level = PERSISTENCE_ERROR_TOLERANCE_NONE)
+ if(tolerance_level < PERSISTENCE_ERROR_TOLERANCE_NONE || tolerance_level > PERSISTENCE_ERROR_TOLERANCE_ANY)
+ CRASH("Invalid error tolerance value.")
+ return (error_tolerance = tolerance_level)
- // Launch events
+/////////////////////////////////////////////////////////////////
+//
+/////////////////////////////////////////////////////////////////
- RAISE_EVENT(/decl/observ/world_saving_start_event, src)
- try
- //
- // PREPARATION SECTIONS
- //
- var/reallow = 0
- if(config.enter_allowed) reallow = 1
- config.enter_allowed = 0
- // Prepare atmosphere for saving.
- SSair.can_fire = FALSE
- if (SSair.state != SS_IDLE)
- report_progress_serializer("ZAS Rebuild initiated. Waiting for current air tick to complete before continuing.")
- sleep(5)
- while (SSair.state != SS_IDLE)
- stoplag()
-
- // Prepare all atmospheres to save.
- report_progress_serializer("Storing pipenet air..")
- sleep(5)
- var/time_start_pipenet = REALTIMEOFDAY
- for(var/datum/pipe_network/net in SSmachines.pipenets)
- for(var/datum/pipeline/line in net.line_members)
- line.temporarily_store_fluids()
- report_progress_serializer("Pipenet air stored in [(REALTIMEOFDAY - time_start_pipenet) / (1 SECOND)]s")
- sleep(5)
+//#TODO: Don't cache those on the persistence ss. It's an extra place to look for what levels have that flag.
+/datum/controller/subsystem/persistence/proc/AddSavedLevel(var/z)
+ saved_levels |= z
- report_progress_serializer("Invalidating all airzones..")
- sleep(5)
- var/time_start_airzones = REALTIMEOFDAY
- while (SSair.zones.len)
- var/zone/zone = SSair.zones[SSair.zones.len]
- SSair.zones.len--
- zone.c_invalidate()
- report_progress_serializer("Invalidated in [(REALTIMEOFDAY - time_start_airzones) / (1 SECOND)]s")
- sleep(5)
+/datum/controller/subsystem/persistence/proc/RemoveSavedLevel(var/z)
+ saved_levels -= z
- report_progress_serializer("Removing queued limbo objects..")
- sleep(5)
+/datum/controller/subsystem/persistence/proc/AddSavedArea(var/area/A)
+ saved_areas |= A
- var/time_start_limbo_removal = REALTIMEOFDAY
- for(var/list/queued in limbo_removals)
- one_off.RemoveFromLimbo(queued[1], queued[2])
- limbo_removals -= list(queued)
+/datum/controller/subsystem/persistence/proc/RemoveSavedArea(var/area/A)
+ saved_areas -= A
- limbo_refs.Cut()
- report_progress_serializer("Done removing queued limbo objects in [(REALTIMEOFDAY - time_start_limbo_removal) / (1 SECOND)]s.")
- sleep(5)
+/////////////////////////////////////////////////////////////////
+// Save/Load
+/////////////////////////////////////////////////////////////////
- report_progress_serializer("Adding limbo players minds to limbo..")
- sleep(5)
- var/time_start_limbo_minds = REALTIMEOFDAY
- // Find all the minds gameworld and add any player characters to the limbo list.
- for(var/datum/mind/char_mind in global.player_minds)
- var/mob/current_mob = char_mind.current
- if(!current_mob || !char_mind.key || istype(char_mind.current, /mob/new_player) || !char_mind.finished_chargen)
- // Just in case, delete this character from limbo.
- one_off.RemoveFromLimbo(char_mind.unique_id, LIMBO_MIND)
- continue
- if(QDELETED(current_mob))
- continue
- // Check to see if the mobs are already being saved.
- if(current_mob.in_saved_location())
- continue
- one_off.AddToLimbo(list(current_mob, char_mind), char_mind.unique_id, LIMBO_MIND, char_mind.key, current_mob.real_name, TRUE)
- report_progress_serializer("Done adding player minds to limbo in [(REALTIMEOFDAY - time_start_limbo_minds) / (1 SECOND)]s.")
- sleep(5)
+///Causes a world save to be started.
+/datum/controller/subsystem/persistence/proc/SaveWorld(var/save_initiator)
+ //Make sure we log who started the save.
+ if(!save_initiator && ismob(usr))
+ save_initiator = usr.ckey
- report_progress_serializer("Wiping previous save..")
- sleep(5)
- var/time_start_wipe = REALTIMEOFDAY
- // Wipe the previous save.
- serializer.WipeSave()
- report_progress_serializer("Done wiping previous save in [(REALTIMEOFDAY - time_start_wipe) / (1 SECOND)]s.")
- sleep(5)
+ var/start = REALTIMEOFDAY
+ var/exception/last_except
- //
- // ACTUAL SAVING SECTION
- //
+ //Prevent entering
+ _block_entering()
- report_progress_serializer("Preparing z-levels for save..")
- sleep(5)
- var/time_start_zprepare = REALTIMEOFDAY
- // This will prepare z_level translations.
- var/list/z_transform = list()
- var/new_z_index = 1
- // First we find the highest non-dynamic z_level.
- for(var/z in SSmapping.player_levels) //#FIXME: That logic is flawed. We got levels that aren't dynamic and aren't station levels!!!!
- if(z in saved_levels)
- new_z_index = max(new_z_index, z)
-
- // Now we go through our saved levels and remap all of those.
- for(var/z in saved_levels)
- var/datum/persistence/load_cache/z_level/z_level = new()
- var/datum/level_data/LD = SSmapping.levels_by_z[z]
- z_level.default_turf = get_base_turf(z)
- z_level.index = z
- z_level.level_data_subtype = LD.type
- if(z in SSmapping.player_levels) //#FIXME: That logic is flawed. We got levels that aren't dynamic and aren't station levels!!!!
- z_level.dynamic = FALSE
- z_level.new_index = z
- else
- new_z_index++
- z_level.dynamic = TRUE
- z_level.new_index = new_z_index
- z_transform["[z]"] = z_level
-
- // Go through all of our saved areas and save those, too.
- for(var/area/A in saved_areas)
- for(var/turf/T in A)
- if("[T.z]" in z_transform)
- continue
- // Turf exists in an area outside of saved_levels.
- // In this case, we'll remap.
- var/datum/persistence/load_cache/z_level/z_level = new()
- z_level.default_turf = get_base_turf(T.z)
- z_level.index = T.z
- z_level.dynamic = TRUE
- var/datum/level_data/LD = SSmapping.levels_by_z[T.z]
- z_level.level_data_subtype = LD.type
- if("[T.z]" in global.overmap_sectors)
- var/obj/effect/overmap = global.overmap_sectors["[T.z]"]
- z_level.metadata = "[overmap.x],[overmap.y]"
- new_z_index++
- z_level.new_index = new_z_index
- z_transform["[T.z]"] = z_level
-
-
- // Now we rebuild our z_level metadata list into the serializer for it to remap everything for us.
- for(var/z in z_transform)
- var/datum/persistence/load_cache/z_level/z_level = z_transform[z]
- serializer.z_map["[z_level.index]"] = z_level.new_index
- new_z_index++
- serializer.z_index = new_z_index
-
- report_progress_serializer("Z-levels prepared for save in [(REALTIMEOFDAY - time_start_zprepare) / (1 SECOND)]s.")
- sleep(5)
+ //Saving start event
+ RAISE_EVENT(/decl/observ/world_saving_start_event, src)
- report_progress_serializer("Saving z-level turfs..")
- sleep(5)
- var/time_start_zsave = REALTIMEOFDAY
- // This will save all the turfs/world.
- var/index = 1
- var/progress = 0
- var/max_progress = length(saved_levels)
-
- for(var/z in saved_levels)
- var/default_turf = get_base_turf(z)
- var/datum/persistence/load_cache/z_level/z_level = z_transform["[z]"]
-
- var/last_area_type
- var/last_area_name
- var/area_turf_count = 0
-
- // We iterate horizontally, since saved turfs 'in' area contents are iterated over in the same way.
- for(var/y in 1 to world.maxy)
- for(var/x in 1 to world.maxx)
- // Get the thing to serialize and serialize it.
- var/turf/T = locate(x,y,z)
- var/area/TA = T.loc
-
- if(last_area_type != TA.type || last_area_name != TA.name)
- if(area_turf_count > 0)
- z_level.areas += list(list("[last_area_type]", sanitize_sql(last_area_name), area_turf_count))
- last_area_type = TA.type
- last_area_name = TA.name
- area_turf_count = 1
- else
- area_turf_count++
-
- // These if statements checks to see if we should save this turf.
-
- //Ignore non-saved areas
- if(istype(TA) && (TA.area_flags & AREA_FLAG_IS_NOT_PERSISTENT))
- continue
-
- // Turfs not saved become their default_turf after deserialization.
- if(!istype(T) || !LAZYLEN(T.contents))
- continue
-
- //Save anything else
- if(istype(T, default_turf) || !T.should_save)
- var/should_skip = TRUE
- for(var/atom/A as anything in T.contents)
- if(A.should_save())
- should_skip = FALSE
- break // We found a thing that's worth saving.
- if(should_skip)
- continue // Skip this tile. Not worth saving.
- serializer.Serialize(T, null, z)
-
- // Don't save every single tile.
- // Batch them up to save time.
- if(index % 128 == 0)
- serializer.Commit()
- index = 1
- else
- index++
- if(last_area_type)
- z_level.areas += list(list("[last_area_type]", sanitize_sql(last_area_name), area_turf_count))
-
- serializer.Commit() // cleanup leftovers.
- serializer.CommitRefUpdates()
- ++progress
- report_progress_serializer("Working.. [(progress * 100) / max_progress]%")
- sleep(3)
+ // Do preparation first
+ _before_save(save_initiator)
- index = 1
- report_progress_serializer("Z-levels turfs saved in [(REALTIMEOFDAY - time_start_zsave) / (1 SECOND)]s.")
- sleep(5)
+ try
+ //Prepare z levels structure for saving
+ var/list/z_transform = _prepare_zlevels_indexing()
- report_progress_serializer("Saving z-level areas..")
- sleep(5)
- var/time_start_zarea = REALTIMEOFDAY
- // Repeat much of the above code in order to save areas marked to be saved that are not in a saved z-level.
- var/list/area_chunks = list()
- for(var/area/A in saved_areas)
- var/datum/persistence/load_cache/area_chunk/area_chunk = new()
- area_chunk.area_type = A.type
- area_chunk.name = A.name
- for(var/turf/T in A)
- if(T.z in saved_levels)
- continue
- var/turf/default_turf = get_base_turf(T.z)
- if(!istype(T) || istype(T, default_turf))
- if(!istype(T) || !T.contents || !length(T.contents) || !T.should_save)
- continue
- var/should_skip = TRUE
- for(var/atom/AM as anything in T.contents)
- if(AM.should_save())
- should_skip = FALSE
- break // We found a thing that's worth saving.
- if(should_skip)
- continue // Skip this tile. Not worth saving.
-
- var/new_z = serializer.z_map["[T.z]"]
- if(new_z)
- area_chunk.turfs += "[T.x],[T.y],[new_z]"
- serializer.Serialize(T, null, T.z)
-
- // Don't save every single tile.
- // Batch them up to save time.
- if(index % 128 == 0)
- serializer.Commit()
- index = 1
- else
- index++
-
- if(length(area_chunk.turfs))
- area_chunks += area_chunk
-
- serializer.Commit() // cleanup leftovers.
-
- // Insert our z-level remaps.
- serializer.save_z_level_remaps(z_transform)
- if(length(area_chunks))
- serializer.save_area_chunks(area_chunks)
- serializer.Commit()
- serializer.CommitRefUpdates()
-
- report_progress_serializer("Z-levels areas saved in [(REALTIMEOFDAY - time_start_zarea) / (1 SECOND)]s.")
- sleep(5)
+ //Save all individual turfs marked for saving
+ _save_turfs(z_transform)
+
+ //Save area related stuff
+ _save_areas(z_transform)
- report_progress_serializer("Saving extensions...")
// Now save all the extensions which have marked themselves to be saved.
// As with areas, we create a dummy wrapper holder to hold these during load etc.
- var/datum/wrapper_holder/extension_wrapper_holder = new(saved_extensions)
- var/time_start_extensions = REALTIMEOFDAY
- serializer.Serialize(extension_wrapper_holder)
- serializer.Commit()
-
- report_progress_serializer("Extensions saved in [(REALTIMEOFDAY - time_start_extensions) / (1 SECOND)]s.")
- sleep(5)
+ _save_extensions()
// Save escrow accounts which are normally held on the SSmoney_accounts subsystem
- if(length(SSmoney_accounts.all_escrow_accounts))
- report_progress_serializer("Saving escrow accounts...")
- var/datum/wrapper_holder/escrow_holder/e_holder = new(SSmoney_accounts.all_escrow_accounts.Copy())
- var/time_start_escrow = REALTIMEOFDAY
-
- serializer.Serialize(e_holder)
- serializer.Commit()
-
- report_progress_serializer("Escrow accounts saved in [(REALTIMEOFDAY - time_start_escrow) / (1 SECOND)]s.")
+ _save_bank_accounts()
- //
- // CLEANUP SECTION
- //
- // Clear the refmaps/do other cleanup to end the save.
- serializer.Clear()
- // Clear the custom saved list used to keep list refs intact
- global.custom_saved_lists.Cut()
- // Let people back in
- if(reallow) config.enter_allowed = 1
- catch (var/exception/e)
- to_world_log("Save failed on line [e.line], file [e.file] with message: '[e]'.")
-
- to_world("Save complete! Took [(world.timeofday-start)/ (1 SECOND)]s to save world.")
- saved_extensions.Cut() // Make extensions re-report if they want to be saved again.
- serializer._after_serialize()
+ catch(var/exception/e)
+ //If exceptions end up in here, then we must interrupt saving completely.
+ //Exceptions should be filtered in the sub-procs depending on severity and the error tolerance threshold set.
+ save_complete_span_class = "danger"
+ save_complete_text = "SAVE FAILED: [EXCEPTION_TEXT(e)]"
+ . = FALSE
+ last_except = e
+
+ //Set our success text if we didn't hit any exceptions
+ if(!length(save_complete_text))
+ save_complete_span_class = "serializer"
+ save_complete_text = "Save complete! Took [REALTIMEOFDAY2SEC(start)]s to save world."
+ . = TRUE
+
+ //Handle post-save cleanup and such
+ _after_save()
// Launch event for anything that needs to do cleanup post save.
RAISE_EVENT_REPEAT(/decl/observ/world_saving_finish_event, src)
- //Print out detailed statistics on what time was spent on what types
- var/list/saved_types_stats = list()
- global.serialization_time_spent_type = sortTim(global.serialization_time_spent_type, /proc/cmp_serialization_stats_dsc, 1)
- for(var/key in global.serialization_time_spent_type)
- var/datum/serialization_stat/statistics = global.serialization_time_spent_type[key]
- saved_types_stats += "\t[statistics.time_spent / (1 SECOND)] second(s)\t[statistics.nb_instances]\tinstance(s)\t\t'[key]'"
- to_world_log("Time spent per type:\n[jointext(saved_types_stats, "\n")]")
- to_world_log("Total time spent doing saved variables lookups: [global.get_saved_variables_lookup_time_total / (1 SECOND)] second(s).")
+ //Resume all subsystems
+ _resume_subsystems()
+ // Reallow people in
+ _restore_entering()
- // Reboot air subsystem.
- SSair.reboot()
+ //Throw any exception, so it's a bit more obvious to people looking at the runtime log that it actually runtimed and failed
+ if(last_except)
+ throw last_except
+///Load the last saved world.
/datum/controller/subsystem/persistence/proc/LoadWorld()
- serializer._before_deserialize()
+ var/time_total = REALTIMEOFDAY
+ var/exception/first_except
+ loading_world = TRUE
+
try
- // Loads all data in as part of a version.
- if(!establish_save_db_connection())
- CRASH("SSPersistence: Couldn't establish DB connection!")
- to_world_log("Loading [serializer.count_saved_datums()] things from world save.")
+ //Establish connection and etc..
+ _before_load()
// We start by loading the cache. This will load everything from SQL into an object structure
// and is much faster than live-querying for information.
- serializer.resolver.load_cache()
-
- // Begin deserializing the world.
- var/start = world.timeofday
- loading_world = TRUE
-
- // Start with rebuilding the z-levels.
-
- var/list/unmapped_z = list()
- var/list/mapped_z = list()
- var/list/mapped_indices = list() //Indices reserved by mapped zlevels
-
- var/min_mapped_index
- var/mapped_offset = 0
- ///Sort z-levels on whether they got a mapped z indice, or not
- for(var/datum/persistence/load_cache/z_level/z_level in serializer.resolver.z_levels)
- if(z_level.dynamic)
- unmapped_z |= z_level
- else
- mapped_z |= z_level
- mapped_indices[num2text(z_level.index)] = z_level.level_data_subtype
-
- if(!min_mapped_index || z_level.index < min_mapped_index)
- min_mapped_index = z_level.index
-
- // If mapping changes have resulted in more levels existing then during save, offset the mapped levels.
- mapped_offset = max(0, world.maxz + 1 - min_mapped_index)
-
- //Then handle assigning z levels
- for(var/datum/persistence/load_cache/z_level/z_level in mapped_z)
- //If the world z isn't at the index we're loading this level at, increment
- z_level.new_index = z_level.index + mapped_offset
- for(var/z_incr = 1 to max(z_level.new_index - world.maxz, 0))
-
- // map_indices is indexed by the database value, so we need to re-adjust.
- var/index_key = num2text(world.maxz - mapped_offset)
- if(index_key in mapped_indices)
- SSmapping.increment_world_z_size(text2path(mapped_indices[index_key]) || /datum/level_data/space, TRUE)
- else
- SSmapping.increment_world_z_size(/datum/level_data/space, TRUE)
- //If we have any unmapped z levels to place, use the empty space between
- if(length(unmapped_z))
- var/datum/persistence/load_cache/z_level/unmapped = unmapped_z[unmapped_z.len]
- unmapped_z.len--
- unmapped.new_index = world.maxz
- serializer.z_map["[unmapped.index]"] = unmapped.new_index
- report_progress_serializer("Mapping Save Z ([unmapped.index]) to World Z ([unmapped.new_index]) with default turf ([unmapped.default_turf]).")
-
- report_progress_serializer("Mapping Save Z ([z_level.index]) to World Z ([z_level.new_index]) with default turf ([z_level.default_turf]) and level data [z_level.level_data_subtype].")
- serializer.z_map[num2text(z_level.index)] = z_level.new_index
-
- //If any unmapped left, add them
- for(var/datum/persistence/load_cache/z_level/z_level in unmapped_z)
- SSmapping.increment_world_z_size(/datum/level_data/space)
- z_level.new_index = world.maxz
- serializer.z_map[num2text(z_level.index)] = z_level.new_index
- report_progress_serializer("Mapping Save Z ([z_level.index]) to World Z ([z_level.new_index]) with default turf ([z_level.default_turf]).")
- report_progress_serializer("Z-Levels loaded!")
-
- // This is a sort-of hack. We're going to go back and edit all of the thing_references to their new Z from the z_levels we just modified.
- for(var/thing_id in serializer.resolver.things)
- var/datum/persistence/load_cache/thing/thing = serializer.resolver.things[thing_id]
- thing.z = serializer.z_map["[thing.z]"]
- report_progress_serializer("Dynamic z-levels populated!")
-
- // Now we're going to load the actual data from the save.
- var/list/turfs_loaded = list()
- to_world_log("Things: [LAZYLEN(serializer.resolver.things)]")
- for(var/TKEY in serializer.resolver.things)
- try
- var/datum/persistence/load_cache/thing/T = serializer.resolver.things[TKEY]
- if(ispath(T.thing_type, /datum/wrapper_holder)) // Special handling for wrapper holders since they don't have another reference.
- serializer.DeserializeDatum(T)
- continue
- if(!T.x || !T.y || !T.z)
- continue // This isn't a turf or a wrapper holder. We can skip it.
- serializer.DeserializeDatum(T)
- turfs_loaded["([T.x], [T.y], [T.z])"] = TRUE
- catch(var/exception/E)
- to_world_log("Failed to load! [E.name], [E.file] [E.line], [E.desc]")
- CHECK_TICK
- to_world_log("Load complete! Took [(world.timeofday-start)/10]s to load [length(serializer.resolver.things)] things. Loaded [LAZYLEN(turfs_loaded)] turfs.")
- in_loaded_world = LAZYLEN(turfs_loaded) > 0
-
- report_progress_serializer("Adding default turfs and areas...")
- start = world.timeofday
-
- for(var/datum/persistence/load_cache/z_level/z_level in serializer.resolver.z_levels)
- var/change_turf = z_level.default_turf && !ispath(z_level.default_turf, /turf/space)
-
- // Create the areas in the z-level if they don't already exist.
- // Areas are added to the area dictionary in area/New()
- for(var/list/area_chunk in z_level.areas)
- var/area/area_instance = global.area_dictionary["[area_chunk[1]], [area_chunk[2]]"]
- if(!area_instance)
- var/new_type = text2path(area_chunk[1])
- new new_type(null, area_chunk[2])
- // The areas are split into horizontal chunks with the area type and name corresponding to a certain amount of tiles in a row.
- var/chunk_index = 1
- var/list/current_area_chunk
- var/area/current_area
- var/turf_count = 1
- if(z_level.areas.len)
- current_area_chunk = z_level.areas[chunk_index]
- current_area = global.area_dictionary["[current_area_chunk[1]], [current_area_chunk[2]]"]
-
- for(var/turf/T in block(locate(1, 1, z_level.new_index), locate(world.maxx, world.maxy, z_level.new_index)))
- if(current_area)
- current_area.contents += T
- turf_count++
- if(turf_count > current_area_chunk[3])
- chunk_index++
- // All the chunks are done. Most likely we're on the last tile of the z-level but just in case allow the loop
- // to continue.
- if(chunk_index > z_level.areas.len)
- current_area = null
- current_area_chunk = null
- else
- current_area_chunk = z_level.areas[chunk_index]
- current_area = global.area_dictionary["[current_area_chunk[1]], [current_area_chunk[2]]"]
- turf_count = 1
- if(change_turf && !turfs_loaded["([T.x], [T.y], [T.z])"])
- T.ChangeTurf(z_level.default_turf)
- report_progress_serializer("Default turfs and areas complete! Took [(world.timeofday-start)/10]s.")
-
- report_progress_serializer("Adding other areas...")
- start = world.timeofday
- for(var/datum/persistence/load_cache/area_chunk/area_chunk in serializer.resolver.area_chunks)
- var/area/new_area = global.area_dictionary["[area_chunk.area_type], [area_chunk.name]"]
- if(!new_area)
- new area_chunk.area_type(null, area_chunk.name)
-
- for(var/turf_chunk in area_chunk.turfs)
- var/list/coords = splittext(turf_chunk, ",")
- // Adjust to new index.
- coords[3] = serializer.z_map[coords[3]]
- var/turf/T = locate(text2num(coords[1]), text2num(coords[2]), coords[3])
- new_area.contents += T
-
- report_progress_serializer("Adding other areas complete! Took [(world.timeofday-start)/10]s.")
-
- // Cleanup the cache. It uses a *lot* of memory.
- for(var/id in serializer.reverse_map)
- var/datum/T = serializer.reverse_map[id]
- T.after_deserialize()
- for(var/id in serializer.reverse_list_map)
- var/list/_list = serializer.reverse_list_map[id]
- //#FIXME: If the elements in the list are numbers, this will be even slower than it is right now.
- // Since it'll runtime if a number is out of range of the list.
- for(var/element in _list)
- var/datum/T = element
- if(istype(T, /datum))
- T.after_deserialize()
- var/datum/TE
- try
- TE = _list[element] //#FIXME: We really need to get rid of this awful way to check list types.
- catch
- continue
- try
- if(istype(TE, /datum))
- TE.after_deserialize()
- catch(var/exception/E)
- to_world_log("TE after_deserialize! [E.name], [E.file] [E.line], [E.desc]")
+ serializer.resolver.load_cache() //This is entirely unrecoverable if it throws and exception.
+ report_progress_serializer("Cached DB data in [REALTIMEOFDAY2SEC(time_total)]s.")
+ sleep(5)
- serializer.resolver.clear_cache()
- serializer.CommitRefUpdates() // Clean up any leftovers.
- serializer.Clear()
+ //Properly restore and assign z-level structure.
+ _restore_zlevel_structure()
- // Clean up limbo by removing any characters present in the gameworld. This may occur if the server does not save after
- // a player enters limbo.
+ //Now we're going to load the actual data from the save.
+ var/list/turfs_loaded = _deserialize_turfs()
- // TODO: Generalize this for other things in limbo.
- for(var/datum/mind/char_mind in global.player_minds)
- one_off.RemoveFromLimbo(char_mind.unique_id, LIMBO_MIND)
+ //Make sure all turfs we didn't load have the right base type and area
+ _setup_default_turfs(turfs_loaded)
- // Tell the atoms subsystem to not populate parts.
- //if(turfs_loaded)
- //SSatoms.adjust_init_arguments = TRUE
- catch(var/exception/e)
- to_world_log("Load failed on line [e.line], file [e.file] with message: '[e]'.")
+ //Apply the right area to any unsaved and saved turfs within it's area.
+ _apply_area_chunks()
- serializer._after_deserialize()
- loading_world = FALSE
+ //Try to run after_deserialize on the loaded atoms
+ _run_after_deserialize()
-/datum/controller/subsystem/persistence/proc/clear_late_wrapper_queue()
- if(!length(late_wrappers))
- return
- var/new_db_connection = FALSE
- if(!check_save_db_connection())
- if(!establish_save_db_connection())
- CRASH("SSPersistence: Couldn't establish DB connection while clearing wrapper queue!")
- new_db_connection = TRUE
- for(var/datum/wrapper/late/L as anything in late_wrappers)
- L.on_late_load()
-
- late_wrappers.Cut()
- if(new_db_connection)
- close_save_db_connection()
+ //Sync references
+ try
+ serializer.CommitRefUpdates()
+ catch(var/exception/e_commit_ref)
+ //If refs fails, it's pretty bad. So filter it as a critical exception.
+ _handle_critical_load_exception(e_commit_ref, "running CommitRefUpdates()")
-/datum/controller/subsystem/persistence/proc/AddSavedLevel(var/z)
- saved_levels |= z
+ //Make sure objects loaded onto the world that are still in the limbo table are removed
+ _update_limbo_state()
-/datum/controller/subsystem/persistence/proc/RemoveSavedLevel(var/z)
- saved_levels -= z
+ catch(var/exception/e_load)
+ //Don't return in here, we need to let the code try to run cleanup below!
+ to_world_log("Load failed: [EXCEPTION_TEXT(e_load)].")
+ first_except = e_load
-/datum/controller/subsystem/persistence/proc/AddSavedArea(var/area/A)
- saved_areas |= A
+ //Now attempt cleanup. This should be attempted even after an exception!!
+ var/time_cleanup = REALTIMEOFDAY
+ try
+ serializer.Clear()
+ serializer.resolver.clear_cache()
+ serializer._after_deserialize()
+ catch(var/exception/e_cleanup)
+ to_world_log("Load cleanup failed: [EXCEPTION_TEXT(e_cleanup)]")
+ if(!first_except)
+ first_except = e_cleanup
+ report_progress_serializer("Cache cleanup done in [REALTIMEOFDAY2SEC(time_cleanup)]s!")
+
+ //Be sure to set this to false even on exceptions
+ loading_world = FALSE
+ report_progress_serializer("Saved world load completed in [REALTIMEOFDAY2SEC(time_total)] seconds.[first_except? SPAN_RED("Some errors were encountered!!") : ""]")
-/datum/controller/subsystem/persistence/proc/RemoveSavedArea(var/area/A)
- saved_areas -= A
-
-/hook/roundstart/proc/retally_all_power()
- for(var/area/A)
- A.retally_power()
- return TRUE
+ //Throw any exception that were allowed, so it's a bit more obvious to people looking at the runtime log that it actually runtimed and failed
+ if(first_except)
+ throw first_except
+/////////////////////////////////////////////////////////////////
+// Base Persistence Subsystem Overrides
+/////////////////////////////////////////////////////////////////
/datum/controller/subsystem/persistence/Shutdown()
return
-
/datum/controller/subsystem/persistence/track_value()
return
-
/datum/controller/subsystem/persistence/is_tracking()
return
-
/datum/controller/subsystem/persistence/forget_value()
return
-
/datum/controller/subsystem/persistence/show_info(mob/user)
to_chat(user, SPAN_INFO("Disabled with persistence modpack (how ironic)..."))
return
-/datum/controller/subsystem/persistence/proc/AddToLimbo(var/list/things, var/key, var/limbo_type, var/metadata, var/metadata2, var/modify = TRUE)
- var/new_db_connection = FALSE
- if(!check_save_db_connection())
- if(!establish_save_db_connection())
- CRASH("SSPersistence: Couldn't establish DB connection during Limbo Addition!")
- new_db_connection = TRUE
- . = one_off.AddToLimbo(things, key, limbo_type, metadata, metadata2, modify)
- if(.) // Clear it from the queued removals.
- for(var/list/queued in limbo_removals)
- if(queued[1] == sanitize_sql(key) && queued[2] == limbo_type)
- limbo_removals -= list(queued)
- if(new_db_connection)
- close_save_db_connection()
-
-/datum/controller/subsystem/persistence/proc/RemoveFromLimbo(var/limbo_key, var/limbo_type)
- var/new_db_connection = FALSE
- if(!check_save_db_connection())
- if(!establish_save_db_connection())
- CRASH("SSPersistence: Couldn't establish DB connection during Limbo Removal!")
- . = one_off.RemoveFromLimbo(limbo_key, limbo_type)
- if(new_db_connection)
- close_save_db_connection()
-
-/datum/controller/subsystem/persistence/proc/DeserializeOneOff(var/limbo_key, var/limbo_type, var/remove_after = TRUE)
- var/new_db_connection = FALSE
- if(!check_save_db_connection())
- if(!establish_save_db_connection())
- CRASH("SSPersistence: Couldn't establish DB connection during Limbo Deserialization!")
- new_db_connection = TRUE
- . = one_off.DeserializeOneOff(limbo_key, limbo_type, remove_after)
- if(remove_after)
- limbo_removals += list(list(sanitize_sql(limbo_key), limbo_type))
- if(new_db_connection)
- close_save_db_connection()
-
-// Get an object from its p_id via ref tracking. This will not always work if an object is asynchronously deserialized from others.
-// This is also quite slow - if you're trying to locate many objects at once, it's best to use a single query for multiple objects.
-/datum/controller/subsystem/persistence/proc/get_object_from_p_id(var/target_p_id)
-
- // Check to see if the object has been deserialized from limbo and not yet added to the normal tables.
- if(target_p_id in limbo_refs)
- var/datum/existing = locate(limbo_refs[target_p_id])
- if(existing && !QDELETED(existing) && existing.persistent_id == target_p_id)
- return existing
- limbo_refs -= target_p_id
-
- // If it was in limbo_refs we shouldn't find it in the normal tables, but we'll check anyway.
- var/new_db_connection = FALSE
- if(!check_save_db_connection())
- if(!establish_save_db_connection())
- CRASH("SSPersistence: Couldn't establish DB connection during Object Lookup!")
- new_db_connection = TRUE
- var/DBQuery/world_query = dbcon_save.NewQuery("SELECT `p_id`, `ref` FROM `[SQLS_TABLE_DATUM]` WHERE `p_id` = \"[target_p_id]\";")
- SQLS_EXECUTE_AND_REPORT_ERROR(world_query, "OBTAINING OBJECT FROM P_ID FAILED:")
-
- while(world_query.NextRow())
- var/list/items = world_query.GetRowData()
- var/datum/existing = locate(items["ref"])
- if(existing && !QDELETED(existing) && existing.persistent_id == items["p_id"])
- . = existing
- break
-
- if(new_db_connection)
- close_save_db_connection()
-
-/datum/controller/subsystem/persistence/proc/print_db_status()
- return SQLS_Print_DB_STATUS()
+//Display to any server staff the timestamp of the currently loaded save in the status panel.
+/mob/Stat()
+ ..()
+ . = (is_client_active(10 MINUTES))
+ if(!.)
+ return
-//Stats datum
-/datum/serialization_stat
- var/time_spent = 0
- var/nb_instances = 0
-/datum/serialization_stat/New(var/_time_spent = 0, var/_nb_instances = 0)
- . = ..()
- time_spent = _time_spent
- nb_instances = _nb_instances
\ No newline at end of file
+ if(statpanel("Status"))
+ if((check_rights(R_DEBUG, FALSE, client) || check_rights(R_SERVER, FALSE, client) || check_rights(R_ADMIN, FALSE, client)))
+ stat("Loaded Save", (SSpersistence.in_loaded_world? "[SSpersistence.LoadedSaveTimestamp()]": "NONE"))
\ No newline at end of file
diff --git a/mods/persistence/controllers/subsystems/persistence/persistence_loading.dm b/mods/persistence/controllers/subsystems/persistence/persistence_loading.dm
new file mode 100644
index 00000000000..e1e94d581fe
--- /dev/null
+++ b/mods/persistence/controllers/subsystems/persistence/persistence_loading.dm
@@ -0,0 +1,297 @@
+//Text helper to avoid copy-pasta
+#define __PRINT_STRING_LIST_DETAIL(ID, L) "'[id]'[islist(_list)? ", ref:\ref[_list],length:[length(_list)]" : ""]"
+#define __PRINT_KEY_DETAIL(KEY) "'[KEY]'(ref:\ref[KEY])([KEY.type])"
+#define __PRINT_VALUE_DETAIL(VAL) "'[VAL]'(ref:\ref[VAL])([VAL.type])"
+
+///Call in a catch block for critical/typically unrecoverable errors during load. Filters out the kind of exceptions we let through or not.
+/datum/controller/subsystem/persistence/proc/_handle_critical_load_exception(var/exception/E, var/code_location)
+ if(error_tolerance < PERSISTENCE_ERROR_TOLERANCE_ANY)
+ throw E
+ else
+ log_warning(EXCEPTION_TEXT(E))
+ log_warning("Error tolerance set to 'any', proceeding with load despite critical error in '[code_location]'!")
+
+///Call in a catch block for recoverable or non-critical errors during load. Filters out the kind of exceptions we let through or not.
+/datum/controller/subsystem/persistence/proc/_handle_recoverable_load_exception(var/exception/E, var/code_location)
+ if(error_tolerance < PERSISTENCE_ERROR_TOLERANCE_RECOVERABLE)
+ throw E
+ else
+ log_warning(EXCEPTION_TEXT(E))
+ log_warning("Error tolerance set to 'critical-only', proceeding with load despite error in '[code_location]'!")
+
+// Get an object from its p_id via ref tracking. This will not always work if an object is asynchronously deserialized from others.
+// This is also quite slow - if you're trying to locate many objects at once, it's best to use a single query for multiple objects.
+/datum/controller/subsystem/persistence/proc/get_object_from_p_id(var/target_p_id)
+//#TODO: This could be sped up by changing the db structure to use indexes and using stored procedures.
+
+ // Check to see if the object has been deserialized from limbo and not yet added to the normal tables.
+ if(target_p_id in limbo_refs)
+ var/datum/existing = locate(limbo_refs[target_p_id])
+ if(existing && !QDELETED(existing) && existing.persistent_id == target_p_id)
+ return existing
+ limbo_refs -= target_p_id
+
+ // If it was in limbo_refs we shouldn't find it in the normal tables, but we'll check anyway.
+ var/new_db_connection = FALSE
+ if(!check_save_db_connection())
+ if(!establish_save_db_connection())
+ CRASH("SSPersistence: Couldn't establish DB connection during Object Lookup!")
+ new_db_connection = TRUE
+ var/DBQuery/world_query = dbcon_save.NewQuery("SELECT `p_id`, `ref` FROM `[SQLS_TABLE_DATUM]` WHERE `p_id` = \"[target_p_id]\";")
+ SQLS_EXECUTE_AND_REPORT_ERROR(world_query, "OBTAINING OBJECT FROM P_ID FAILED:")
+
+ while(world_query.NextRow())
+ var/list/items = world_query.GetRowData()
+ var/datum/existing = locate(items["ref"])
+ if(existing && !QDELETED(existing) && existing.persistent_id == items["p_id"])
+ . = existing
+ break
+
+ if(new_db_connection)
+ close_save_db_connection()
+
+/datum/controller/subsystem/persistence/proc/clear_late_wrapper_queue()
+ if(!length(late_wrappers))
+ return
+ //#TODO: Move db handling to serializer stuff.
+ var/new_db_connection = FALSE
+ if(!check_save_db_connection())
+ if(!establish_save_db_connection())
+ CRASH("SSPersistence: Couldn't establish DB connection while clearing wrapper queue!")
+ new_db_connection = TRUE
+ for(var/datum/wrapper/late/L as anything in late_wrappers)
+ L.on_late_load()
+
+ late_wrappers.Cut()
+ if(new_db_connection)
+ close_save_db_connection() //#TODO: Move db handling to serializer stuff.
+
+///Handles setting up db connections and etc..
+/datum/controller/subsystem/persistence/proc/_before_load()
+ try
+ //Establish connection mainly
+ serializer._before_deserialize()
+
+ // Loads all data in as part of a version.
+ report_progress_serializer("Loading last save, `[serializer.last_loaded_save_time()]`, with [serializer.count_saved_datums()] atoms to load.")
+ catch(var/exception/e)
+ _handle_critical_load_exception(e, "establishing db connection before load")
+
+///Assign the right z-level index to the right level.
+/datum/controller/subsystem/persistence/proc/_restore_zlevel_structure()
+ // Start with rebuilding the z-levels.
+ var/list/unmapped_z = list()
+ var/list/mapped_z = list()
+ var/list/mapped_indices = list() //Indices reserved by mapped zlevels
+ var/mapped_offset = 0
+ var/time_start = REALTIMEOFDAY
+ var/min_mapped_index
+
+ //#TODO: The dynamic thing is a bit cruddy. We could avoid a lot of that by just handling turfs separately. And associating them to static level id, z stack ids and areas ids.
+ ///Sort z-levels on whether they got a mapped z indice, or not
+ for(var/datum/persistence/load_cache/z_level/z_level in serializer.resolver.z_levels)
+ if(z_level.dynamic)
+ unmapped_z |= z_level
+ else
+ mapped_z |= z_level
+ mapped_indices[num2text(z_level.index)] = z_level.level_data_subtype
+
+ if(!min_mapped_index || z_level.index < min_mapped_index)
+ min_mapped_index = z_level.index
+
+ // If mapping changes have resulted in more levels existing then during save, offset the mapped levels.
+ mapped_offset = max(0, world.maxz + 1 - min_mapped_index)
+
+ //Then handle assigning z levels
+ for(var/datum/persistence/load_cache/z_level/z_level in mapped_z)
+ //If the world z isn't at the index we're loading this level at, increment
+ z_level.new_index = z_level.index + mapped_offset
+ for(var/z_incr = 1 to max(z_level.new_index - world.maxz, 0))
+
+ // mapped_indices is indexed by the database value, so we need to re-adjust.
+ var/index_key = num2text(z_level.index)
+ if(index_key in mapped_indices)
+ var/lvldat_path = mapped_indices[index_key]
+ if(length(mapped_indices[index_key]) && !ispath(lvldat_path))
+ CRASH("Loaded level_data path '[mapped_indices[index_key]]' resolved to null type!")
+ SSmapping.increment_world_z_size(lvldat_path, TRUE)
+ else
+ SSmapping.increment_world_z_size(/datum/level_data/space, TRUE)
+ //If we have any unmapped z levels to place, use the empty space between
+ if(length(unmapped_z))
+ var/datum/persistence/load_cache/z_level/unmapped = unmapped_z[unmapped_z.len]
+ unmapped_z.len--
+ unmapped.new_index = world.maxz
+ serializer.z_map["[unmapped.index]"] = unmapped.new_index
+ report_progress_serializer("Mapping Save Z ([unmapped.index]) to World Z ([unmapped.new_index]) with default turf ([unmapped.default_turf]).")
+
+ report_progress_serializer("Mapping original Z ([z_level.index]) to new Z ([z_level.new_index]) with default turf ([z_level.default_turf]) and level data [z_level.level_data_subtype].")
+ serializer.z_map[num2text(z_level.index)] = z_level.new_index
+
+ //If any unmapped left, add them
+ for(var/datum/persistence/load_cache/z_level/z_level in unmapped_z)
+ SSmapping.increment_world_z_size(/datum/level_data/space)
+ z_level.new_index = world.maxz
+ serializer.z_map[num2text(z_level.index)] = z_level.new_index
+ report_progress_serializer("Mapping original Z ([z_level.index]) to new Z ([z_level.new_index]) with default turf ([z_level.default_turf]).")
+ report_progress_serializer("Z-Levels loaded!")
+
+ // This is a sort-of hack. We're going to go back and edit all of the thing_references to their new Z from the z_levels we just modified.
+ for(var/thing_id in serializer.resolver.things)
+ var/datum/persistence/load_cache/thing/thing = serializer.resolver.things[thing_id]
+ thing.z = serializer.z_map["[thing.z]"]
+ report_progress_serializer("Dynamic z-levels populated!")
+
+ report_progress_serializer("Restored z-level structure in [REALTIMEOFDAY2SEC(time_start)]s.")
+ sleep(5)
+
+///Runs after deserialize on all the loaded atoms.
+/datum/controller/subsystem/persistence/proc/_run_after_deserialize()
+ //Run after_deserialize on all atoms in the map.
+ for(var/id in serializer.reverse_map)
+ var/datum/T
+ try
+ T = serializer.reverse_map[id]
+ T.after_deserialize()
+ catch(var/exception/e)
+ _handle_recoverable_load_exception(e, "while running after_deserialize() on PID: '[id]'[!isnull(T)? ", '[T]'(\ref[T])([T.type])" : ""]")
+
+ //Since datums used as list value and list key are stored in another list, run after_deserialize() on them too
+ for(var/id in serializer.reverse_list_map)
+ var/list/_list
+ try
+ _list = serializer.reverse_list_map[id]
+ //#FIXME: If the keys in the list are numbers, this will be even slower than it is right now.
+ // Since it'll runtime if a number is out of range of the list.
+ for(var/key in _list)
+ var/datum/K = key
+ if(istype(K, /datum))
+ try
+ K.after_deserialize()
+ catch(var/exception/e_list_key)
+ _handle_recoverable_load_exception(e_list_key, "while running after_deserialize() on key [__PRINT_KEY_DETAIL(K)], of list: [__PRINT_STRING_LIST_DETAIL(id, _list)]")
+
+ var/datum/V
+ try
+ V = _list[key] //#FIXME: We really need to get rid of this awful way to check list types.
+ catch
+ continue
+ if(istype(V, /datum))
+ try
+ V.after_deserialize()
+ catch(var/exception/e_list_value)
+ _handle_recoverable_load_exception(e_list_value, "while running after_deserialize() on value [__PRINT_VALUE_DETAIL(V)], for key [__PRINT_KEY_DETAIL(K)], of list: [__PRINT_STRING_LIST_DETAIL(id, _list)]")
+ catch(var/exception/e_list)
+ //Catch any sort of bad index error
+ _handle_recoverable_load_exception(e_list, "while running after_deserialize() on elements of list: [__PRINT_STRING_LIST_DETAIL(id, _list)]")
+
+///Clean up limbo by removing any characters present in the gameworld. This may occur if the server does not save after
+///a player enters limbo.
+/datum/controller/subsystem/persistence/proc/_update_limbo_state()
+ // TODO: Generalize this for other things in limbo.
+ for(var/datum/mind/char_mind in global.player_minds)
+ try
+ one_off.RemoveFromLimbo(char_mind.unique_id, LIMBO_MIND)
+ catch(var/exception/e)
+ _handle_recoverable_load_exception(e, "while updating off-world storage state for player '[char_mind.key]'")
+
+///Deserialize cached top level wrapper datum/turf exclusively from the db cache.
+/datum/controller/subsystem/persistence/proc/_deserialize_turfs()
+ var/list/turfs_loaded = list()
+ var/time_start = REALTIMEOFDAY
+
+ report_progress_serializer("Deserializing [LAZYLEN(serializer.resolver.things)] cached atoms...")
+ sleep(5)
+
+ for(var/TKEY in serializer.resolver.things)
+ var/datum/persistence/load_cache/thing/T
+ try
+ T = serializer.resolver.things[TKEY]
+ if(ispath(T.thing_type, /datum/wrapper_holder)) // Special handling for wrapper holders since they don't have another reference.
+ serializer.DeserializeDatum(T)
+ continue
+ if(!T.x || !T.y || !T.z)
+ continue // This isn't a turf or a wrapper holder. We can skip it.
+ serializer.DeserializeDatum(T)
+ turfs_loaded["([T.x], [T.y], [T.z])"] = TRUE
+ catch(var/exception/E)
+ to_world_log("Failed to load turf '[T]'!: [EXCEPTION_TEXT(E)]")
+ CHECK_TICK
+
+ in_loaded_world = LAZYLEN(turfs_loaded) > 0
+ . = turfs_loaded
+ report_progress_serializer("Deserialized [LAZYLEN(turfs_loaded)] turfs and their contents in [REALTIMEOFDAY2SEC(time_start)]s.")
+ sleep(5)
+
+/// TODO
+/datum/controller/subsystem/persistence/proc/_setup_default_turfs(var/list/turfs_loaded)
+ var/time_start = REALTIMEOFDAY
+ for(var/datum/persistence/load_cache/z_level/z_level in serializer.resolver.z_levels)
+ var/change_turf = z_level.default_turf && !ispath(z_level.default_turf, /turf/space)
+
+ // Create the areas in the z-level if they don't already exist.
+ // Areas are added to the area dictionary in area/New()
+ for(var/list/area_chunk in z_level.areas)
+ var/area/area_instance = global.area_dictionary["[area_chunk[1]], [area_chunk[2]]"]
+ if(!area_instance)
+ var/new_type = text2path(area_chunk[1])
+ new new_type(null, area_chunk[2])
+ // The areas are split into horizontal chunks with the area type and name corresponding to a certain amount of tiles in a row.
+ var/chunk_index = 1
+ var/list/current_area_chunk
+ var/area/current_area
+ var/turf_count = 1
+ if(length(z_level.areas))
+ current_area_chunk = z_level.areas[chunk_index]
+ current_area = global.area_dictionary["[current_area_chunk[1]], [current_area_chunk[2]]"]
+
+ for(var/turf/T in block(locate(1, 1, z_level.new_index), locate(world.maxx, world.maxy, z_level.new_index)))
+ try
+ if(current_area)
+ current_area.contents += T //#FIXME: It's dangerous to do it like this. Use loc on the turf, not the area's contents "list".
+ turf_count++
+ if(turf_count > current_area_chunk[3])
+ chunk_index++
+ // All the chunks are done. Most likely we're on the last tile of the z-level but just in case allow the loop
+ // to continue.
+ if(chunk_index > z_level.areas.len)
+ current_area = null
+ current_area_chunk = null
+ else
+ current_area_chunk = z_level.areas[chunk_index]
+ current_area = global.area_dictionary["[current_area_chunk[1]], [current_area_chunk[2]]"]
+ turf_count = 1
+ if(change_turf && !turfs_loaded["([T.x], [T.y], [T.z])"])
+ T.ChangeTurf(z_level.default_turf)
+ catch(var/exception/e_changeturf)
+ _handle_recoverable_load_exception(e_changeturf, "changing base turf/area")
+
+ report_progress_serializer("Applied default turfs and areas in [REALTIMEOFDAY2SEC(time_start)]s!")
+ sleep(5)
+
+///Applies areas to both loaded and default turfs inside the regions they cover.
+/datum/controller/subsystem/persistence/proc/_apply_area_chunks()
+ report_progress_serializer("Applying area chunks...")
+ var/time_start = REALTIMEOFDAY
+ for(var/datum/persistence/load_cache/area_chunk/area_chunk in serializer.resolver.area_chunks)
+ try
+ var/area/new_area = global.area_dictionary["[area_chunk.area_type], [area_chunk.name]"]
+ if(!new_area)
+ new area_chunk.area_type(null, area_chunk.name)
+
+ for(var/turf_chunk in area_chunk.turfs)
+ var/list/coords = splittext(turf_chunk, ",")
+ // Adjust to new index.
+ coords[3] = serializer.z_map[coords[3]]
+ var/turf/T = locate(text2num(coords[1]), text2num(coords[2]), coords[3])
+ new_area.contents += T //#FIXME: Accessing contents directly is dangerous. It's better to set loc instead.
+ catch(var/exception/e)
+ //Keep going if we're tolerating critical exceptions
+ _handle_critical_load_exception(e, "applying area for area chunk '[area_chunk?.name]'")
+
+ report_progress_serializer("Applied area chunks completed! Took [REALTIMEOFDAY2SEC(time_start)]s.")
+ sleep(5)
+
+#undef __PRINT_STRING_LIST_DETAIL
+#undef __PRINT_KEY_DETAIL
+#undef __PRINT_VALUE_DETAIL
\ No newline at end of file
diff --git a/mods/persistence/controllers/subsystems/persistence/persistence_saving.dm b/mods/persistence/controllers/subsystems/persistence/persistence_saving.dm
new file mode 100644
index 00000000000..293397f348c
--- /dev/null
+++ b/mods/persistence/controllers/subsystems/persistence/persistence_saving.dm
@@ -0,0 +1,427 @@
+///If this returns true, we should always skip saving this turf!
+///Ignore non-saved areas and turfs with no contents.
+#define __SHOULD_SKIP_TURF(T) ((!istype(T) || !length(T.contents)) || (istype(T.loc, /area) && (T.loc:area_flags & AREA_FLAG_IS_NOT_PERSISTENT)))
+
+/////////////////////////////////////////////////////////////////
+// Utility
+/////////////////////////////////////////////////////////////////
+
+///Keeps the previous state of 'enter_allowed' and set it to false. Returns TRUE if entering was currently allowed.
+/datum/controller/subsystem/persistence/proc/_block_entering()
+ was_entering_allowed = config.enter_allowed
+ config.enter_allowed = FALSE
+ return was_entering_allowed
+
+///Restore the previous state of 'enter_allowed'. Returns the restored value of 'enter_allowed'.
+/datum/controller/subsystem/persistence/proc/_restore_entering()
+ . = (config.enter_allowed = was_entering_allowed)
+ was_entering_allowed = FALSE
+
+///Handle pausing all subsystems before save
+/datum/controller/subsystem/persistence/proc/_pause_subsystems()
+ //Turn off all the subsystems we don't need messing things up during saving.
+ for(var/datum/controller/subsystem/S in Master.subsystems)
+ S.disable()
+
+ //Wait on SSair to complete it's tick.
+ if (SSair.state != SS_IDLE)
+ report_progress_serializer("ZAS Rebuild initiated. Waiting for current air tick to complete before continuing.")
+ while (SSair.state != SS_IDLE)
+ stoplag()
+
+///Handles resuming all subsystems post-save
+/datum/controller/subsystem/persistence/proc/_resume_subsystems()
+ // Reboot air subsystem before mass enabling all of them.
+ SSair.reboot()
+ //Resune subsystems
+ for(var/datum/controller/subsystem/S in Master.subsystems)
+ S.enable()
+
+/////////////////////////////////////////////////////////////////
+// Exception Filtering
+/////////////////////////////////////////////////////////////////
+
+///Call in a catch block for critical/typically unrecoverable errors during save. Filters out the kind of exceptions we let through or not.
+/datum/controller/subsystem/persistence/proc/_handle_critical_save_exception(var/exception/E, var/code_location)
+ if(error_tolerance < PERSISTENCE_ERROR_TOLERANCE_ANY)
+ throw E
+ else
+ log_warning(EXCEPTION_TEXT(E))
+ log_warning("Error tolerance set to 'any', proceeding with save despite critical error in '[code_location]'!")
+
+///Call in a catch block for recoverable or non-critical errors during save. Filters out the kind of exceptions we let through or not.
+/datum/controller/subsystem/persistence/proc/_handle_recoverable_save_exception(var/exception/E, var/code_location)
+ if(error_tolerance < PERSISTENCE_ERROR_TOLERANCE_RECOVERABLE)
+ throw E
+ else
+ log_warning(EXCEPTION_TEXT(E))
+ log_warning("Error tolerance set to 'critical-only', proceeding with save despite error in '[code_location]'!")
+
+/////////////////////////////////////////////////////////////////
+// Saving Steps
+/////////////////////////////////////////////////////////////////
+
+///Make atmos store it's air values so we can properly save them per atmos atom.
+/datum/controller/subsystem/persistence/proc/prepare_atmos_for_save()
+ var/time_start = REALTIMEOFDAY
+ SSmachines.temporarily_store_pipenets()
+ report_progress_serializer("Pipenet air stored in [REALTIMEOFDAY2SEC(time_start)]s")
+
+ time_start = REALTIMEOFDAY
+ SSair.invalidate_all_zones()
+ report_progress_serializer("Invalidated in [REALTIMEOFDAY2SEC(time_start)]s")
+
+///Update the list of things in limbo and not in limbo, so they don't get saved in 2 different states.
+/datum/controller/subsystem/persistence/proc/prepare_limbo_for_save()
+ var/time_start = REALTIMEOFDAY
+ for(var/list/queued in limbo_removals)
+ one_off.RemoveFromLimbo(queued[1], queued[2])
+ limbo_removals -= list(queued)
+ limbo_refs.Cut()
+ report_progress_serializer("Removed queued limbo objects in [REALTIMEOFDAY2SEC(time_start)]s.")
+
+ // Find all the minds gameworld and add any player characters to the limbo list.
+ time_start = REALTIMEOFDAY
+ for(var/datum/mind/char_mind in global.player_minds)
+ var/mob/current_mob = char_mind.current
+ if(!current_mob || !char_mind.key || istype(char_mind.current, /mob/new_player) || !char_mind.finished_chargen)
+ // Just in case, delete this character from limbo.
+ one_off.RemoveFromLimbo(char_mind.unique_id, LIMBO_MIND)
+ continue
+ if(QDELETED(current_mob))
+ continue
+ // Check to see if the mobs are already being saved.
+ if(current_mob.in_saved_location())
+ continue
+ one_off.AddToLimbo(list(current_mob, char_mind), char_mind.unique_id, LIMBO_MIND, char_mind.key, current_mob.real_name, TRUE)
+ report_progress_serializer("Added player minds to limbo in [REALTIMEOFDAY2SEC(time_start)]s.")
+
+/datum/controller/subsystem/persistence/proc/_prepare_zlevels_indexing()
+ var/time_start_zprepare = REALTIMEOFDAY
+ // This will prepare z_level translations.
+ var/list/z_transform = list()
+ var/new_z_index = 1
+
+ report_progress_serializer("Preparing z-levels for save..")
+ sleep(5)
+ try
+ // First we find the highest non-dynamic z_level.
+ for(var/z in SSmapping.player_levels) //#FIXME: That logic is flawed. We got levels that aren't dynamic and aren't station levels!!!!
+ if(z in saved_levels)
+ new_z_index = max(new_z_index, z)
+
+ // Now we go through our saved levels and remap all of those.
+ for(var/z in saved_levels)
+ var/datum/persistence/load_cache/z_level/z_level = new()
+ var/datum/level_data/LD = SSmapping.levels_by_z[z]
+ z_level.default_turf = get_base_turf(z)
+ z_level.index = z
+ z_level.level_data_subtype = LD.type
+ if(z in SSmapping.player_levels) //#FIXME: That logic is flawed. We got levels that aren't dynamic and aren't station levels!!!!
+ z_level.dynamic = FALSE
+ z_level.new_index = z
+ else
+ new_z_index++
+ z_level.dynamic = TRUE
+ z_level.new_index = new_z_index
+ z_transform["[z]"] = z_level
+
+ // Go through all of our saved areas and save those, too.
+ for(var/area/A in saved_areas)
+ for(var/turf/T in A)
+ if("[T.z]" in z_transform)
+ continue
+ // Turf exists in an area outside of saved_levels.
+ // In this case, we'll remap.
+ var/datum/persistence/load_cache/z_level/z_level = new()
+ z_level.default_turf = get_base_turf(T.z)
+ z_level.index = T.z
+ z_level.dynamic = TRUE
+ var/datum/level_data/LD = SSmapping.levels_by_z[T.z]
+ z_level.level_data_subtype = LD.type
+ if("[T.z]" in global.overmap_sectors)
+ var/obj/effect/overmap = global.overmap_sectors["[T.z]"]
+ z_level.metadata = "[overmap.x],[overmap.y]"
+ new_z_index++
+ z_level.new_index = new_z_index
+ z_transform["[T.z]"] = z_level
+
+ // Now we rebuild our z_level metadata list into the serializer for it to remap everything for us.
+ for(var/z in z_transform)
+ var/datum/persistence/load_cache/z_level/z_level = z_transform[z]
+ serializer.z_map["[z_level.index]"] = z_level.new_index
+ new_z_index++
+ serializer.z_index = new_z_index
+
+ report_progress_serializer("Z-levels prepared for save in [REALTIMEOFDAY2SEC(time_start_zprepare)]s.")
+ sleep(5)
+
+ catch(var/exception/e)
+ //Critical because If z-indexes are messed up, it can corrupt the whole save.
+ _handle_critical_save_exception(e, "_prepare_zlevels_indexing()")
+
+ return z_transform
+
+///Saves all the turfs marked for saving in the world.
+/datum/controller/subsystem/persistence/proc/_save_turfs(var/list/z_transform)
+ report_progress_serializer("Saving z-level turfs..")
+ sleep(5)
+
+ var/time_start_zsave = REALTIMEOFDAY
+ ///Amount of turfs waiting for a commit
+ var/nb_turfs_queued = 1
+ ///The total count of zlevels we're saving
+ var/total_zlevels = length(saved_levels)
+
+ //Reset our status vars
+ nb_saved_z_levels = 0
+ nb_saved_atoms = 0
+
+ //!!!!!!!!!!!!!!!!!!!!!!!!
+ //!! - HOT CODE BELOW - !!
+ //!!!!!!!!!!!!!!!!!!!!!!!!
+ for(var/z in saved_levels)
+ var/datum/persistence/load_cache/z_level/z_level = z_transform["[z]"]
+ var/last_area_type
+ var/last_area_name
+ var/default_turf = get_base_turf(z)
+ var/area_turf_count = 0
+
+ try
+ // We iterate horizontally, since saved turfs 'in' area contents are iterated over in the same way.
+ for(var/y in 1 to world.maxy)
+ for(var/x in 1 to world.maxx)
+ try
+ // Get the thing to serialize and serialize it.
+ var/turf/T = locate(x,y,z)
+ var/area/TA = T.loc
+
+ if(last_area_type != TA.type || last_area_name != TA.name)
+ if(area_turf_count > 0)
+ z_level.areas += list(list("[last_area_type]", sanitize_sql(last_area_name), area_turf_count))
+ last_area_type = TA.type
+ last_area_name = TA.name
+ area_turf_count = 1
+ else
+ area_turf_count++
+
+ var/should_skip = __SHOULD_SKIP_TURF(T)
+ // These if statements checks to see if we should save this turf.
+ if(!should_skip && istype(T, default_turf) || !T.should_save)
+ for(var/atom/A as anything in T.contents)
+ if(A.should_save())
+ should_skip = FALSE
+ break // We found a thing that's worth saving.
+ if(should_skip)
+ continue //Turfs not saved become their default_turf after deserialization.
+
+ //If we got through the filter save
+ serializer.Serialize(T, null, z)
+
+ catch(var/exception/e_turf)
+ _handle_recoverable_save_exception(e_turf, "saving a turf") //Allow a turf to fail to save when allowed, that's minimal damage.
+
+ // Don't commit every single tile.
+ // Batch them up to save time.
+ if(nb_turfs_queued % 128 == 0)
+ try
+ serializer.Commit()
+ nb_turfs_queued = 1
+ catch(var/exception/e_turf_commit)
+ nb_turfs_queued = 1
+ _handle_critical_save_exception(e_turf_commit, "pushing turf commit") //Failing a commit is pretty bad since they're all batched together.
+ else
+ nb_turfs_queued++
+
+ if(last_area_type)
+ z_level.areas += list(list("[last_area_type]", sanitize_sql(last_area_name), area_turf_count))
+ catch(var/exception/e_zlvl)
+ _handle_recoverable_save_exception(e_zlvl, "saving a z level") //A z-level failing is manageable.
+
+ //Ref update commit + flush commit
+ try
+ serializer.Commit() // cleanup leftovers.
+ serializer.CommitRefUpdates()
+ catch(var/exception/e_ref_commit)
+ _handle_critical_save_exception(e_ref_commit, "pushing a ref update commit") //Failing ref updates is really bad.
+
+ ++nb_saved_z_levels
+ report_progress_serializer("Working.. [CEILING((nb_saved_z_levels * 100) / total_zlevels)]%")
+ sleep(3)
+
+ nb_turfs_queued = 1
+ report_progress_serializer("Z-levels turfs saved in [REALTIMEOFDAY2SEC(time_start_zsave)]s.")
+ sleep(5)
+
+///Saves area stuff
+/datum/controller/subsystem/persistence/proc/_save_areas(var/list/z_transform)
+ var/time_start_zarea = REALTIMEOFDAY
+ var/list/area_chunks = list()
+ var/nb_turfs_queued = 1
+
+ //#FIXME: This block of code is deranged. It's making us iterate over all turfs in the world again just to save area stuff?
+ // We should do all turf-related ops in a single spot, not copy paste code like this.
+
+ // Repeat much of the above code in order to save areas marked to be saved that are not in a saved z-level.
+ for(var/area/A in saved_areas)
+ var/datum/persistence/load_cache/area_chunk/area_chunk = new()
+ area_chunk.area_type = A.type
+ area_chunk.name = A.name
+
+ for(var/turf/T in A) //#FIXME: This actually iterates through ALL TURFS IN THE WORLD. Area contents is broken and slow.
+ if(T.z in saved_levels) //#FIXME: We're already going through saved zlevels above..
+ continue
+ //#FIXME: This is a copy paste of the code prior and it has some differences with it too, which will cause mismatches between areas and turfs saving.
+ var/turf/default_turf = get_base_turf(T.z)
+ if(!istype(T) || istype(T, default_turf))
+ if(!istype(T) || !T.contents || !length(T.contents) || !T.should_save)
+ continue
+ var/should_skip = TRUE
+ for(var/atom/AM as anything in T.contents)
+ if(AM.should_save())
+ should_skip = FALSE
+ break // We found a thing that's worth saving.
+ if(should_skip)
+ continue // Skip this tile. Not worth saving.
+
+ var/new_z = serializer.z_map["[T.z]"] //#FIXME: String concat is extremely slow.
+ if(new_z)
+ area_chunk.turfs += "[T.x],[T.y],[new_z]" //#FIXME: String concat is extremely slow.
+ serializer.Serialize(T, null, T.z)
+
+ // Don't save every single tile.
+ // Batch them up to save time.
+ if(nb_turfs_queued % 128 == 0)
+ serializer.Commit()
+ nb_turfs_queued = 1
+ else
+ nb_turfs_queued++
+
+ if(length(area_chunk.turfs))
+ area_chunks += area_chunk
+
+ serializer.Commit() // cleanup leftovers.
+
+ try
+ // Insert our z-level remaps.
+ serializer.save_z_level_remaps(z_transform)
+ if(length(area_chunks))
+ serializer.save_area_chunks(area_chunks)
+ serializer.Commit()
+ serializer.CommitRefUpdates()
+ catch(var/exception/e_commit)
+ _handle_critical_save_exception(e_commit, "area saving turf ref commit")
+
+ report_progress_serializer("Z-levels areas saved in [REALTIMEOFDAY2SEC(time_start_zarea)]s.")
+ sleep(5)
+
+///Saves extension wrapper stuff
+/datum/controller/subsystem/persistence/proc/_save_extensions()
+ var/datum/wrapper_holder/extension_wrapper_holder = new(saved_extensions)
+ var/time_start_extensions = REALTIMEOFDAY
+
+ try
+ serializer.Serialize(extension_wrapper_holder)
+ catch(var/exception/e_serial)
+ _handle_recoverable_save_exception(e_serial, "extension serialization")
+
+ try
+ serializer.Commit()
+ catch(var/exception/e_commit)
+ _handle_critical_save_exception(e_commit, "extension commit") //If commit fails, we corrupted our commit cache so not good
+
+ report_progress_serializer("Saved extensions in [REALTIMEOFDAY2SEC(time_start_extensions)]s.")
+ sleep(5)
+
+///Save bank account stuff
+/datum/controller/subsystem/persistence/proc/_save_bank_accounts()
+ if(!length(SSmoney_accounts.all_escrow_accounts))
+ return
+ var/datum/wrapper_holder/escrow_holder/e_holder = new(SSmoney_accounts.all_escrow_accounts.Copy())
+ var/time_start_escrow = REALTIMEOFDAY
+
+ try
+ serializer.Serialize(e_holder)
+ catch(var/exception/e_serial)
+ _handle_recoverable_save_exception(e_serial, "bank account serialization")
+
+ try
+ serializer.Commit()
+ catch(var/exception/e_commit)
+ _handle_critical_save_exception(e_commit, "bank account commit") //If commit fails, we corrupted our commit cache so not good
+
+ report_progress_serializer("Escrow accounts saved in [REALTIMEOFDAY2SEC(time_start_escrow)]s.")
+ sleep(5)
+
+/////////////////////////////////////////////////////////////////
+// Pre/Post Save
+/////////////////////////////////////////////////////////////////
+
+///Run the pre-save stuff
+/datum/controller/subsystem/persistence/proc/_before_save(var/save_initiator)
+ //Clear any previous log entry ids we got from the db before
+ save_log_id = null
+
+ // Collect the z-levels we're saving and get the turfs!
+ to_world_log("Saving [LAZYLEN(SSpersistence.saved_levels)] z-levels. World size max ([world.maxx],[world.maxy])")
+ sleep(5)
+
+ //Disable all subsystems
+ try
+ _pause_subsystems()
+ catch(var/exception/e_ss)
+ _handle_recoverable_save_exception(e_ss, "_pause_subsystems()")
+ sleep(5)
+
+ // Prepare all atmospheres to save.
+ try
+ prepare_atmos_for_save()
+ catch(var/exception/e_atmos)
+ _handle_recoverable_save_exception(e_atmos, "prepare_atmos_for_save()")
+ sleep(5)
+
+ //Let the serializer know we're preparing a save
+ // Wipe the previous save + add log entry
+ try
+ var/time_start_wipe = REALTIMEOFDAY
+ save_log_id = serializer.PreWorldSave(save_initiator)
+ report_progress_serializer("Wiped previous save in [REALTIMEOFDAY2SEC(time_start_wipe)]s.")
+ catch(var/exception/e_presave)
+ if(istype(e_presave, /exception/sql_connection))
+ _handle_critical_save_exception(e_presave, "serializer.PreWorldSave()") //db queries errors during wipe are unrecoverable and WILL break further duplicate INSERTS..
+ else
+ _handle_recoverable_save_exception(e_presave, "serializer.PreWorldSave()")
+ sleep(5)
+
+ //Clear limbo stuff after we've connected to the db!
+ try
+ prepare_limbo_for_save()
+ catch(var/exception/e_limbo)
+ _handle_recoverable_save_exception(e_limbo, "prepare_limbo_for_save()")
+ sleep(5)
+
+///Runs the post-saving stuff
+/datum/controller/subsystem/persistence/proc/_after_save()
+ try
+ //Do post-save cleanup and logging
+ serializer.PostWorldSave(save_log_id, nb_saved_z_levels, nb_saved_atoms, save_complete_text)
+ saved_extensions.Cut() // Make extensions re-report if they want to be saved again.
+ // Clear the custom saved list used to keep list refs intact
+ global.custom_saved_lists.Cut()
+
+ //Print out detailed statistics on what time was spent on what types
+ var/list/saved_types_stats = list()
+ global.serialization_time_spent_type = sortTim(global.serialization_time_spent_type, /proc/cmp_serialization_stats_dsc, 1)
+ for(var/key in global.serialization_time_spent_type)
+ var/datum/serialization_stat/statistics = global.serialization_time_spent_type[key]
+ saved_types_stats += "\t[statistics.time_spent / (1 SECOND)] second(s)\t[statistics.nb_instances]\tinstance(s)\t\t'[key]'"
+
+ to_world(SPAN_CLASS(save_complete_span_class, save_complete_text))
+ to_world_log(save_complete_text)
+ to_world_log("Time spent per type:\n[jointext(saved_types_stats, "\n")]")
+ to_world_log("Total time spent doing saved variables lookups: [global.get_saved_variables_lookup_time_total / (1 SECOND)] second(s).")
+
+ catch(var/exception/e)
+ _handle_recoverable_save_exception(e, "_after_save()") //Anything post-save is recoverable
+
+#undef __SHOULD_SKIP_TURF
diff --git a/mods/persistence/controllers/subsystems/persistence/persistence_stats.dm b/mods/persistence/controllers/subsystems/persistence/persistence_stats.dm
new file mode 100644
index 00000000000..c16f279563c
--- /dev/null
+++ b/mods/persistence/controllers/subsystems/persistence/persistence_stats.dm
@@ -0,0 +1,13 @@
+/////////////////////////////////
+// Save statistics
+/////////////////////////////////
+
+///Stats on how long a specific type of atom took to save in total, and how many instances of that type there are.
+/datum/serialization_stat
+ var/time_spent = 0
+ var/nb_instances = 0
+
+/datum/serialization_stat/New(var/_time_spent = 0, var/_nb_instances = 0)
+ . = ..()
+ time_spent = _time_spent
+ nb_instances = _nb_instances
diff --git a/mods/persistence/controllers/subsystems/persistence/persistence_storage.dm b/mods/persistence/controllers/subsystems/persistence/persistence_storage.dm
new file mode 100644
index 00000000000..f316da279b7
--- /dev/null
+++ b/mods/persistence/controllers/subsystems/persistence/persistence_storage.dm
@@ -0,0 +1,61 @@
+///Add something to the off-world save.
+/datum/controller/subsystem/persistence/proc/AddToLimbo(var/list/things, var/key, var/limbo_type, var/metadata, var/metadata2, var/modify = TRUE, var/initiator)
+ //Make sure we log who started the save.
+ if(!initiator && ismob(usr))
+ initiator = usr.ckey
+
+ //#TODO: Move db handling to serializer stuff.
+ var/new_db_connection = FALSE
+ if(!check_save_db_connection())
+ if(!establish_save_db_connection())
+ CRASH("SSPersistence: Couldn't establish DB connection during Limbo Addition!")
+ new_db_connection = TRUE
+
+ //Log transaction
+ var/limbo_log_id = one_off.PreStorageSave(initiator)
+ //#TODO: Use a single serializer for both limbo and world save
+ . = one_off.AddToLimbo(things, key, limbo_type, metadata, metadata2, modify)
+ if(.) // Clear it from the queued removals.
+ for(var/list/queued in limbo_removals)
+ if(queued[1] == sanitize_sql(key) && queued[2] == limbo_type)
+ limbo_removals -= list(queued)
+
+ one_off.PostStorageSave(limbo_log_id, 0, length(things), "Addition [limbo_type] [key]")
+ if(new_db_connection)
+ close_save_db_connection() //#TODO: Move db handling to serializer stuff.
+
+///Remove something from the off-world save.
+/datum/controller/subsystem/persistence/proc/RemoveFromLimbo(var/limbo_key, var/limbo_type, var/initiator)
+ //#TODO: Move db handling to serializer stuff.
+ var/new_db_connection = FALSE
+ if(!check_save_db_connection())
+ if(!establish_save_db_connection())
+ CRASH("SSPersistence: Couldn't establish DB connection during Limbo Removal!")
+
+ //Log transaction
+ var/limbo_log_id = one_off.PreStorageSave(initiator)
+ //#TODO: Use a single serializer for both limbo and world save
+ . = one_off.RemoveFromLimbo(limbo_key, limbo_type)
+ one_off.PostStorageSave(limbo_log_id, 0, ., "Removal [limbo_type]")
+
+ //#TODO: Move db handling to serializer stuff.
+ if(new_db_connection)
+ close_save_db_connection()
+
+///Load something from the off-world save.
+/datum/controller/subsystem/persistence/proc/LoadFromLimbo(var/limbo_key, var/limbo_type, var/remove_after = TRUE)
+ //#TODO: Move db handling to serializer stuff.
+ var/new_db_connection = FALSE
+ if(!check_save_db_connection())
+ if(!establish_save_db_connection())
+ CRASH("SSPersistence: Couldn't establish DB connection during Limbo Deserialization!")
+ new_db_connection = TRUE
+
+ //#TODO: Use a single serializer for both limbo and world save
+ . = one_off.LoadFromLimbo(limbo_key, limbo_type, remove_after)
+ if(remove_after)
+ limbo_removals += list(list(sanitize_sql(limbo_key), limbo_type))
+
+ //#TODO: Move db handling to serializer stuff.
+ if(new_db_connection)
+ close_save_db_connection()
diff --git a/mods/persistence/game/machinery/cryopod.dm b/mods/persistence/game/machinery/cryopod.dm
index 1bee3ddf7d4..62652e19eb8 100644
--- a/mods/persistence/game/machinery/cryopod.dm
+++ b/mods/persistence/game/machinery/cryopod.dm
@@ -3,6 +3,8 @@
/obj/machinery/cryopod
var/obj/item/radio/intercom/old_intercom
var/despawning = FALSE
+ ///The ckey of who put the occupant in the machine if not the occupant themselves
+ var/tmp/who_put_me_in
/obj/machinery/cryopod/Initialize()
old_intercom = locate() in src
@@ -30,6 +32,7 @@
src.occupant = occupant
if(!occupant)
SetName(initial(name))
+ who_put_me_in = null
return
if(occupant.client)
@@ -42,6 +45,9 @@
SetName("[name] ([occupant])")
icon_state = occupied_icon_state
+ if(ismob(usr))
+ var/mob/M = usr
+ who_put_me_in = M.ckey
/obj/machinery/cryopod/verb/self_eject()
set name = "Self-eject Pod"
@@ -66,6 +72,7 @@
add_fingerprint(usr)
SetName(initial(name))
+ who_put_me_in = null
return
// Players shoved into this will be removed from the game and added to limbo to be deserialized later.
@@ -131,7 +138,7 @@
H.home_spawn = src
var/datum/mind/occupant_mind = occupant.mind
if(occupant_mind)
- var/success = SSpersistence.AddToLimbo(list(occupant, occupant_mind), occupant_mind.unique_id, LIMBO_MIND, occupant_mind.key, occupant_mind.current.real_name, TRUE)
+ var/success = SSpersistence.AddToLimbo(list(occupant, occupant_mind), occupant_mind.unique_id, LIMBO_MIND, occupant_mind.key, occupant_mind.current.real_name, TRUE, (who_put_me_in || occupant.ckey))
if(!success)
log_and_message_admins("\The cryopod at ([x], [y], [z]) failed to despawn the occupant [occupant]!")
to_chat(occupant, SPAN_WARNING("Something has gone wrong while saving your character. Contact an admin!"))
@@ -141,9 +148,11 @@
else
despawning = FALSE
return
- if(occupant.ckey)
+ if(occupant.ckey && occupant.client)
var/mob/new_player/new_player = new()
- new_player.ckey = occupant.ckey
+ new_player.ckey = occupant.ckey
+ new_player.client.eye = new_player.client.mob //Do this so we don't hear what's going on around the pod after cryo.
+ new_player.client.perspective = MOB_PERSPECTIVE
despawning = FALSE
// Delete the mob.
diff --git a/mods/persistence/modules/admin/verbs/admin.dm b/mods/persistence/modules/admin/verbs/admin.dm
index fcf21e9c981..d707589ef37 100644
--- a/mods/persistence/modules/admin/verbs/admin.dm
+++ b/mods/persistence/modules/admin/verbs/admin.dm
@@ -7,6 +7,7 @@ var/global/list/persistence_admin_verbs = list(
/client/proc/remove_character,
/client/proc/lock_server_and_kick_players,
/client/proc/clear_named_character_from_limbo,
+ /client/proc/change_serialization_error_tolerance,
)
/client/proc/save_server()
@@ -40,7 +41,7 @@ var/global/list/persistence_admin_verbs = list(
return
for(var/datum/mind/M in global.player_minds)
if(M.key == target_ckey)
- SSpersistence.RemoveFromLimbo(M.unique_id, LIMBO_MIND)
+ SSpersistence.RemoveFromLimbo(M.unique_id, LIMBO_MIND, ckey)
qdel(M)
/client/proc/database_status()
@@ -50,7 +51,7 @@ var/global/list/persistence_admin_verbs = list(
if(!check_rights(R_ADMIN))
return
- SSpersistence.print_db_status()
+ SSpersistence.PrintDBStatus()
/client/proc/database_reconect()
set category = "Server"
@@ -60,7 +61,7 @@ var/global/list/persistence_admin_verbs = list(
if(!check_rights(R_ADMIN))
return
SQLS_Force_Reconnect()
-
+
/client/proc/lock_server_and_kick_players()
set category = "Server"
set desc = "Lock entering the server, kick all non-admin players, and prevent them from re-joining"
@@ -91,17 +92,19 @@ var/global/list/persistence_admin_verbs = list(
//////////////////////////////////////////////////////////////////////
// Limbo Character Verbs
//////////////////////////////////////////////////////////////////////
+
+//#FIXME: Try to get all that SQL out of here and call a proc on the serializer directly instead.
/client/proc/clear_named_character_from_limbo()
set category = "Server"
set desc = "Force delete from the database the limbo mob for a given character real_name. Meant to be used to clear character names being in-use even if there isn't an active character tied to it."
set name = "Delete Limbo Character"
if(!check_rights(R_ADMIN))
return
-
- var/choice = alert(usr,
- "USE WITH CAUTION! Will delete the limbo character entry in the database, so the associated name can be used by a new character. THIS WILL PERMENANTLY DELETE ANY CRYOED CHARACTER WITH THE GIVEN NAME IF THERE WAS ANY. Use only in last resort.",
- "Delete named character",
- "Proceed",
+
+ var/choice = alert(usr,
+ "USE WITH CAUTION! Will delete the limbo character entry in the database, so the associated name can be used by a new character. THIS WILL PERMENANTLY DELETE ANY CRYOED CHARACTER WITH THE GIVEN NAME IF THERE WAS ANY. Use only in last resort.",
+ "Delete named character",
+ "Proceed",
"Cancel")
if(choice == "Cancel")
to_chat(usr, SPAN_INFO("Action Aborted"))
@@ -127,25 +130,25 @@ var/global/list/persistence_admin_verbs = list(
if(length(row))
LAZYADD(entries, "name:'[row["metadata2"]]' ckey:'[row["metadata"]]' pid:'[row["p_ids"]]'")
LAZYADD(mind_ids, row["key"])
-
+
if(!length(entries))
to_chat(usr, SPAN_WARNING("No matching characters found in the database. Aborting."))
if(should_close_connection)
close_save_db_connection()
- return
+ return
to_chat(usr, SPAN_INFO("The command will delete the following:\n[jointext(entries,"\n")]"))
//Ask again
- choice = alert(usr,
- "Really delete [length(entries)] character\s from the database?",
- "Delete named character",
- "Cancel",
+ choice = alert(usr,
+ "Really delete [length(entries)] character\s from the database?",
+ "Delete named character",
+ "Cancel",
"Ok")
if(choice == "Cancel")
to_chat(usr, SPAN_INFO("Action Aborted"))
if(should_close_connection)
close_save_db_connection()
- return
+ return
//Do the deleting
for(var/mindid in mind_ids)
@@ -154,3 +157,68 @@ var/global/list/persistence_admin_verbs = list(
if(should_close_connection)
close_save_db_connection()
to_chat(usr, SPAN_INFO("Successfully deleted all entries for [char_name]!"))
+
+//////////////////////////////////////////////////////////////////////
+// Save Error Handling
+//////////////////////////////////////////////////////////////////////
+#define TOLERANCE_ALL_ERRORS "Any Errors"
+#define TOLERANCE_RECOVERABLE_ERRORS "Recoverable Errors"
+#define TOLERANCE_NONE "None"
+
+///Allow changing error tolerance by admins in order to salvage a save that wouldn't go through because of some localised error.
+/client/proc/change_serialization_error_tolerance()
+ set category = "Server"
+ set desc = "Allow more or less error tolerance for serializtion errors. Meant to be used as last resort to force the server to save despite runtimes."
+ set name = "Change Save Error Tolerance"
+ if(!check_rights(R_ADMIN) || !check_rights(R_SERVER))
+ return
+
+ var/previous_tolerance
+ //Get current setting, and turn it to a string
+ switch(global.SSpersistence.error_tolerance)
+ if(PERSISTENCE_ERROR_TOLERANCE_NONE)
+ previous_tolerance = TOLERANCE_NONE
+ if(PERSISTENCE_ERROR_TOLERANCE_RECOVERABLE)
+ previous_tolerance = TOLERANCE_RECOVERABLE_ERRORS
+ if(PERSISTENCE_ERROR_TOLERANCE_ANY)
+ previous_tolerance = TOLERANCE_ALL_ERRORS
+ else
+ CRASH("Had bad current save error tolerance value!")
+
+ ///Options for the possible error tolerance
+ var/static/tolerance_options = list(
+ TOLERANCE_ALL_ERRORS,
+ TOLERANCE_RECOVERABLE_ERRORS,
+ TOLERANCE_NONE,
+ "Cancel"
+ )
+ ///Message displayed to users
+ var/static/user_message = {"
+!! USE WITH CAUTION !! - Ensure there is a BACKUP of the last save first as this could likely cause DATA LOSS, or SAVE CORRUPTION!!
+Select what kind of errors will be TOLERATED (A \"Non-recoverable\" error is usually DB queries failing for instance):
+"}
+ ///Show the dialog
+ var/choice = input(
+ usr,
+ user_message,
+ "Change Save Error Tolerance",
+ previous_tolerance) as anything in tolerance_options
+
+ var/new_tolerance
+ switch(choice)
+ if(TOLERANCE_ALL_ERRORS)
+ new_tolerance = PERSISTENCE_ERROR_TOLERANCE_ANY
+ if(TOLERANCE_RECOVERABLE_ERRORS)
+ new_tolerance = PERSISTENCE_ERROR_TOLERANCE_RECOVERABLE
+ if(TOLERANCE_NONE)
+ new_tolerance = PERSISTENCE_ERROR_TOLERANCE_NONE
+ else
+ to_chat(usr, SPAN_INFO("Save error tolerance unchanged."))
+ return
+
+ SSpersistence.SetErrorTolerance(new_tolerance)
+ to_chat(usr, SPAN_INFO("Save error tolerance changed to: '[choice]'!"))
+
+#undef TOLERANCE_ALL_ERRORS
+#undef TOLERANCE_RECOVERABLE_ERRORS
+#undef TOLERANCE_NONE
\ No newline at end of file
diff --git a/mods/persistence/modules/chargen/launch_pod.dm b/mods/persistence/modules/chargen/launch_pod.dm
index 37de28d6178..f4881e412cd 100644
--- a/mods/persistence/modules/chargen/launch_pod.dm
+++ b/mods/persistence/modules/chargen/launch_pod.dm
@@ -76,7 +76,7 @@
SSchargen.release_spawn_pod(get_area(src))
// Add the mob to limbo for safety. Mark for removal on the next save.
- SSpersistence.AddToLimbo(list(user, user.mind), user.mind.unique_id, LIMBO_MIND, user.mind.key, user.mind.current.real_name, TRUE)
+ SSpersistence.AddToLimbo(list(user, user.mind), user.mind.unique_id, LIMBO_MIND, user.mind.key, user.mind.current.real_name, TRUE, user.mind.key)
SSpersistence.limbo_removals += list(list(sanitize_sql(user.mind.unique_id), LIMBO_MIND))
for(var/turf/T in global.latejoin_cryo_locations)
diff --git a/mods/persistence/modules/mob/death.dm b/mods/persistence/modules/mob/death.dm
index 78c595a50f5..f172690d291 100644
--- a/mods/persistence/modules/mob/death.dm
+++ b/mods/persistence/modules/mob/death.dm
@@ -39,6 +39,6 @@
player.ckey = ckey
// Permanently remove the player from the limbo list so that the mind datum is removed from the database at next save.
- SSpersistence.RemoveFromLimbo(mind.unique_id, LIMBO_MIND)
+ SSpersistence.RemoveFromLimbo(mind.unique_id, LIMBO_MIND, player.ckey)
QDEL_NULL(mind)
qdel_self()
diff --git a/mods/persistence/modules/mob/new_player.dm b/mods/persistence/modules/mob/new_player.dm
index 542d0e8c15d..4793d28f3c6 100644
--- a/mods/persistence/modules/mob/new_player.dm
+++ b/mods/persistence/modules/mob/new_player.dm
@@ -124,7 +124,7 @@
if(char_query.NextRow())
var/list/char_items = char_query.GetRowData()
var/char_key = char_items["key"]
- SSpersistence.RemoveFromLimbo(char_key, LIMBO_MIND)
+ SSpersistence.RemoveFromLimbo(char_key, LIMBO_MIND, ckey)
to_chat(src, SPAN_NOTICE("Character Delete Completed."))
else
to_chat(src, SPAN_NOTICE("Delete Failed! Contact a developer."))
@@ -234,7 +234,7 @@
to_world_log("CHARACTER DESERIALIZATION FAILED: [char_query.ErrorMsg()].")
if(char_query.NextRow())
var/list/char_items = char_query.GetRowData()
- var/list/deserialized = SSpersistence.DeserializeOneOff(char_items["key"], LIMBO_MIND)
+ var/list/deserialized = SSpersistence.LoadFromLimbo(char_items["key"], LIMBO_MIND)
var/datum/mind/target_mind
for(var/thing in deserialized)
if(istype(thing, /datum/mind))
diff --git a/mods/persistence/modules/multiz/level_data.dm b/mods/persistence/modules/multiz/level_data.dm
index 81344599f0e..939e6441198 100644
--- a/mods/persistence/modules/multiz/level_data.dm
+++ b/mods/persistence/modules/multiz/level_data.dm
@@ -22,9 +22,6 @@ SAVED_VAR(/datum/level_data, transition_turf_type)
SAVED_VAR(/datum/level_data, exterior_atmos_temp)
SAVED_VAR(/datum/level_data, exterior_atmosphere)
SAVED_VAR(/datum/level_data, connected_levels)
-/datum/level_data/setup_level_data()
- if(level_flags & ZLEVEL_SAVED)
- SSpersistence.saved_levels |= level_z
- if(level_flags & ZLEVEL_MINING)
- SSmapping.mining_levels |= level_z
- . = ..()
\ No newline at end of file
+
+//Planetoid stuff
+SAVED_VAR(/datum/level_data/planetoid, parent_planetoid)
diff --git a/mods/persistence/modules/overmap/sectors.dm b/mods/persistence/modules/overmap/sectors.dm
index f558b797518..7d5200287a6 100644
--- a/mods/persistence/modules/overmap/sectors.dm
+++ b/mods/persistence/modules/overmap/sectors.dm
@@ -3,7 +3,7 @@
// If the area or z-level is saved, the overmap effect will be saved.
var/atom/old_loc // Where the ship was prior to saving. Used to relocate the ship following saving, not on load.
- //#FIXME: Don't store this here.
+ //#FIXME: Don't store this here. Those are meant to be strictly just temporary objects just to show a position on the overmap!
var/rent_amount = 15000 // The amount of rent per period.
var/paid_rent = 0 // The rent paid so far.
var/rent_period = 14 DAYS // Time between rent payments.
@@ -14,7 +14,7 @@
events_repository.register(/decl/observ/world_saving_start_event, SSpersistence, src, .proc/on_saving_start)
events_repository.register(/decl/observ/world_saving_finish_event, SSpersistence, src, .proc/on_saving_end)
if(!last_due)
- last_due = world.realtime
+ last_due = world.realtime //#FIXME: Use REALTIMEOFDAY since world.realtime doesn't handle midnight rollover
/obj/effect/overmap/visitable/Destroy()
. = ..()
@@ -87,6 +87,11 @@ SAVED_VAR(/datum/planetoid_data, flora)
SAVED_VAR(/datum/planetoid_data, fauna)
SAVED_VAR_AS_TYPE(/datum/planetoid_data, strata)
+/datum/planetoid_data/after_deserialize()
+ . = ..()
+ if(!LAZYACCESS(SSmapping.planetoid_data_by_id, id))
+ setup_planetoid()
+
//Save picked engravings
SAVED_VAR(/datum/xenoarch_engraving_flavor, picked_actors)
diff --git a/mods/persistence/modules/world_save/_persistence.dm b/mods/persistence/modules/world_save/_persistence.dm
index 0cbc99d269f..31a693073e5 100644
--- a/mods/persistence/modules/world_save/_persistence.dm
+++ b/mods/persistence/modules/world_save/_persistence.dm
@@ -22,7 +22,7 @@ SAVED_VAR(/datum, custom_saved)
* Called right after the entity has been saved.
*/
/datum/proc/after_save()
- custom_saved_lists |= list(custom_saved)
+ custom_saved_lists |= list(custom_saved) //This make sure the ref to the list is kept alive long enough for the save to be able to write it.
#ifndef SAVE_DEBUG
custom_saved = null //Clear it since its no longer needed
#endif
diff --git a/mods/persistence/modules/world_save/save_testing.dm b/mods/persistence/modules/world_save/save_testing.dm
new file mode 100644
index 00000000000..9bccfd8349a
--- /dev/null
+++ b/mods/persistence/modules/world_save/save_testing.dm
@@ -0,0 +1,29 @@
+//Tools for implementing save db tests
+/obj/debug
+ is_spawnable_type = FALSE //Prevent unrelated unit tests from creating those.
+
+///Object meant to be saved and cause an error
+/obj/debug/serialization
+ should_save = TRUE
+/obj/debug/serialization/Initialize(mapload)
+ . = ..()
+ message_staff("Debug object `[type]` was spawned! This will cause a crash in any saves it's part of!! Make sure to delete before making any live server save!")
+
+///Object that when saved will cause an error in it's before_save() proc.
+/obj/debug/serialization/error_on_save
+/obj/debug/serialization/error_on_save/before_save()
+ . = ..()
+ CRASH("[type]: Crashing before_save()!")
+
+///Object that when loaded will cause an error in it's Initialize() proc.
+/obj/debug/serialization/error_on_load_init
+/obj/debug/serialization/error_on_load_init/Initialize(mapload)
+ . = ..()
+ if(persistent_id)
+ CRASH("[type]: Crashing Initialize()!")
+
+///Test object that always crash during after_deserialize().
+/obj/debug/serialization/error_on_after_load
+/obj/debug/serialization/error_on_after_load/after_deserialize()
+ . = ..()
+ CRASH("[type]: Crashing after_deserialize()!")
diff --git a/mods/persistence/modules/world_save/saved_vars/saved_skipped.dm b/mods/persistence/modules/world_save/saved_vars/saved_skipped.dm
index 89d581ec2f0..1dd3dd19a76 100644
--- a/mods/persistence/modules/world_save/saved_vars/saved_skipped.dm
+++ b/mods/persistence/modules/world_save/saved_vars/saved_skipped.dm
@@ -93,4 +93,16 @@
//This object is doing a bunch of nasty things, like initializing during new, and moving to nullspace, don't try saving it.
/obj/abstract/weather_system
+ should_save = FALSE
+
+///////////////////////////////////////////////////
+// Turfs
+///////////////////////////////////////////////////
+
+//Mimic edges try to access adjacent levels's level data, and their own level's level_data when being created. But we can't guarantee they exist yet.
+/turf/exterior/mimic_edge
+ should_save = FALSE
+/turf/unsimulated/mimic_edge
+ should_save = FALSE
+/turf/simulated/mimic_edge
should_save = FALSE
\ No newline at end of file
diff --git a/mods/persistence/modules/world_save/serializers/_serializer.dm b/mods/persistence/modules/world_save/serializers/_serializer.dm
index c99f5b6267a..1dfb3d391bb 100644
--- a/mods/persistence/modules/world_save/serializers/_serializer.dm
+++ b/mods/persistence/modules/world_save/serializers/_serializer.dm
@@ -26,7 +26,7 @@
wrappers[initial(Wd.wrapper_for)] = W
/serializer/proc/get_wrapper(var/D)
- for(var/wrapper_type in wrappers)
+ for(var/wrapper_type in wrappers)
if(istype(D, wrapper_type))
return wrappers[wrapper_type]
@@ -50,7 +50,7 @@
/serializer/proc/DeserializeList(var/raw_list)
/serializer/proc/QueryAndDeserializeDatum(var/object_id, var/reference_only = FALSE)
- var/datum/existing = reverse_map["[object_id]"]
+ var/datum/existing = reverse_map["[object_id]"]
if(!isnull(existing))
return existing
// We check to see if this is a reference only var so that if things are missing from the resolver, this doesn't fail silently.
@@ -85,6 +85,10 @@
/serializer/proc/save_exists()
return FALSE
+///Stub for obtaining the last save timestamp
+/serializer/proc/last_loaded_save_time()
+ return
+
/serializer/proc/save_z_level_remaps()
return FALSE
@@ -102,4 +106,4 @@
return
/serializer/proc/count_saved_datums()
- return
\ No newline at end of file
+ return
\ No newline at end of file
diff --git a/mods/persistence/modules/world_save/serializers/one_off_serializer.dm b/mods/persistence/modules/world_save/serializers/one_off_serializer.dm
index bd1857d6876..cbedb12beb5 100644
--- a/mods/persistence/modules/world_save/serializers/one_off_serializer.dm
+++ b/mods/persistence/modules/world_save/serializers/one_off_serializer.dm
@@ -16,6 +16,20 @@
if(istype(E) && E.should_save(one_off = TRUE))
extension_wrapper_holder.wrapped |= E
+///Do pre world saving stuff. Returns the current save log entry id.
+/serializer/sql/one_off/proc/PreStorageSave(var/save_initiator)
+ //Logs the save into the table
+ var/DBQuery/query = dbcon_save.NewQuery("SELECT `[SQLS_FUNC_LOG_SAVE_STORAGE_START]`('[sanitize_sql(sanitize(save_initiator, MAX_LNAME_LEN))]');")
+ SQLS_EXECUTE_AND_REPORT_ERROR(query, "UNABLE TO LOG STORAGE SAVE START:")
+ if(query.NextRow())
+ . = query.item[1]
+
+/serializer/sql/one_off/proc/PostStorageSave(var/log_id, var/nb_saved_lvl, var/nb_saved_atoms, var/result_text)
+ var/DBQuery/query = dbcon_save.NewQuery("SELECT `[SQLS_FUNC_LOG_SAVE_END]`('[log_id]','[nb_saved_lvl]','[nb_saved_atoms]','[sanitize_sql(sanitize(result_text, MAX_MEDIUM_TEXT_LEN))]');")
+ SQLS_EXECUTE_AND_REPORT_ERROR(query, "UNABLE TO LOG STORAGE SAVE END ('[query.sql]'):")
+ if(query.NextRow())
+ . = query.item[1]
+
/serializer/sql/one_off/Commit(limbo_assoc)
if(!establish_save_db_connection())
CRASH("One-Off Serializer: Couldn't establish DB connection!")
@@ -38,7 +52,7 @@
try
SQLS_EXECUTE_AND_REPORT_ERROR(query, "LIMBO ELEMENT SERIALIZATION FAILED:")
catch(var/exception/E)
- log_warning("Caught exception when issuing query :\n[raw_statement]")
+ log_warning("Caught exception when issuing query :\n[raw_statement]") //#FIXME: query.sql does the same thing
throw E
catch (var/exception/e)
@@ -46,7 +60,7 @@
last_except = e //Throw it after we clean up
else
to_world_log("Limbo Serializer Failed")
- to_world_log(e)
+ to_world_log(e) //#FIXME: This isn't going to print the exception's text...
thing_inserts.Cut(1)
var_inserts.Cut(1)
@@ -220,9 +234,11 @@
limbo_assoc = limbo_items["limbo_assoc"]
else
return // The object wasn't in limbo to begin with.
+
var/DBQuery/delete_query
delete_query = dbcon_save.NewQuery("DELETE FROM `[SQLS_TABLE_LIMBO_DATUM]` WHERE `limbo_assoc` = '[limbo_assoc]';")
SQLS_EXECUTE_AND_REPORT_ERROR(delete_query, "LIMBO DELETION OF THING(S) FAILED:")
+ . = delete_query.RowsAffected() //Return the amount of datums removed
delete_query = dbcon_save.NewQuery("DELETE FROM `[SQLS_TABLE_LIMBO_DATUM_VARS]` WHERE `limbo_assoc` = '[limbo_assoc]';")
SQLS_EXECUTE_AND_REPORT_ERROR(delete_query, "LIMBO DELETION OF VAR(S) FAILED:")
@@ -234,7 +250,7 @@
SQLS_EXECUTE_AND_REPORT_ERROR(delete_query, "LIMBO DELETION FROM LIMBO TABLE FAILED:")
-/serializer/sql/one_off/proc/DeserializeOneOff(var/limbo_key, var/limbo_type)
+/serializer/sql/one_off/proc/LoadFromLimbo(var/limbo_key, var/limbo_type)
var/DBQuery/limbo_query = dbcon_save.NewQuery("SELECT `p_ids` FROM `[SQLS_TABLE_LIMBO]` WHERE `key` = '[limbo_key]' AND `type` = '[limbo_type]';")
SQLS_EXECUTE_AND_REPORT_ERROR(limbo_query, "DESERIALIZE ONE-OFF FAILED:")
var/list/limbo_p_ids = list()
diff --git a/mods/persistence/modules/world_save/serializers/sql_serializer.dm b/mods/persistence/modules/world_save/serializers/sql_serializer.dm
index 49d469efd47..abc9c02df91 100644
--- a/mods/persistence/modules/world_save/serializers/sql_serializer.dm
+++ b/mods/persistence/modules/world_save/serializers/sql_serializer.dm
@@ -564,7 +564,7 @@ var/global/list/serialization_time_spent_type
last_except = e //Throw it after we clean up
else
to_world_log("World Serializer Failed")
- to_world_log(e)
+ to_world_log(e) //#FIXME: This doesn't print the exception's contents
thing_inserts.Cut(1)
var_inserts.Cut(1)
@@ -604,23 +604,42 @@ var/global/list/serialization_time_spent_type
list_index = 1
flattener.Clear()
-// Deletes all saves from the database.
-/serializer/sql/proc/WipeSave()
- var/DBQuery/query = dbcon_save.NewQuery("TRUNCATE TABLE `[SQLS_TABLE_DATUM]`;")
- SQLS_EXECUTE_AND_REPORT_ERROR(query, "UNABLE TO WIPE PREVIOUS SAVE:")
- query = dbcon_save.NewQuery("TRUNCATE TABLE `[SQLS_TABLE_DATUM_VARS]`;")
- SQLS_EXECUTE_AND_REPORT_ERROR(query, "UNABLE TO WIPE PREVIOUS SAVE:")
- query = dbcon_save.NewQuery("TRUNCATE TABLE `[SQLS_TABLE_LIST_ELEM]`;")
- SQLS_EXECUTE_AND_REPORT_ERROR(query, "UNABLE TO WIPE PREVIOUS SAVE:")
- query = dbcon_save.NewQuery("TRUNCATE TABLE `[SQLS_TABLE_Z_LEVELS]`;")
- SQLS_EXECUTE_AND_REPORT_ERROR(query, "UNABLE TO WIPE PREVIOUS SAVE:")
- query = dbcon_save.NewQuery("TRUNCATE TABLE `[SQLS_TABLE_AREAS]`;")
+///Do pre world saving stuff. Returns the current save log entry id.
+/serializer/sql/proc/PreWorldSave(var/save_initiator)
+ _before_serialize()
+ //Ask the db to clear the old world save
+ var/DBQuery/query = dbcon_save.NewQuery("CALL `[SQLS_PROC_CLEAR_WORLD_SAVE]`();")
SQLS_EXECUTE_AND_REPORT_ERROR(query, "UNABLE TO WIPE PREVIOUS SAVE:")
+
+ //Logs the world save into the table
+ query = dbcon_save.NewQuery("SELECT `[SQLS_FUNC_LOG_SAVE_WORLD_START]`('[sanitize_sql(sanitize(save_initiator, MAX_LNAME_LEN))]');")
+ SQLS_EXECUTE_AND_REPORT_ERROR(query, "UNABLE TO LOG WORLD SAVE START:")
+ if(query.NextRow())
+ . = query.item[1]
+ Clear()
+
+/serializer/sql/proc/PostWorldSave(var/save_entry_id, var/nb_saved_lvl, var/nb_saved_atoms, var/result_text)
+ var/actual_query = "SELECT `[SQLS_FUNC_LOG_SAVE_END]`('[save_entry_id]','[nb_saved_lvl]','[nb_saved_atoms]','[sanitize_sql(sanitize(result_text, MAX_MEDIUM_TEXT_LEN))]');"
+ var/DBQuery/query = dbcon_save.NewQuery(actual_query)
+ SQLS_EXECUTE_AND_REPORT_ERROR(query, "UNABLE TO LOG WORLD SAVE END ('[actual_query]'):")
+ if(query.NextRow())
+ . = query.item[1]
+ _after_serialize()
Clear()
/serializer/sql/save_exists()
return count_saved_datums() > 0
+///Returns the timestamp of when the currently loaded save was completed.
+/serializer/sql/last_loaded_save_time()
+ if(!establish_save_db_connection())
+ CRASH("Couldn't get last world save timestamp, connection failed!")
+ //Get the last logged world save from the db
+ var/DBQuery/query = dbcon_save.NewQuery("SELECT `[SQLS_FUNC_GET_LAST_SAVE_TIME]`();")
+ SQLS_EXECUTE_AND_REPORT_ERROR(query, "UNABLE TO GET THE TIMESTAMP FOR THE LAST WORLD SAVE:")
+ if(query.NextRow())
+ return query.item[1]
+
/serializer/sql/save_z_level_remaps(var/list/z_transform)
var/list/z_inserts = list()
var/z_insert_index = 1
diff --git a/mods/persistence/modules/world_save/serializers/sql_serializer_db.dm b/mods/persistence/modules/world_save/serializers/sql_serializer_db.dm
index 5d805bbea1c..bf038ee01db 100644
--- a/mods/persistence/modules/world_save/serializers/sql_serializer_db.dm
+++ b/mods/persistence/modules/world_save/serializers/sql_serializer_db.dm
@@ -13,15 +13,6 @@ var/global/DBConnection/dbcon_save
/proc/check_save_db_connection()
return global.dbcon_save && global.dbcon_save.IsConnected()
- // if(!global.dbcon_save || !global.dbcon_save.IsConnected())
- // return FALSE
- // var/DBQuery/dbq = global.dbcon_save.NewQuery("SELECT DATABASE();")
- // . = dbq.Execute()
- // if(!.)
- // to_world_log("check_save_db_connection: failed after execution : '[dbq.ErrorMsg()]'")
- // return FALSE
- // if(dbq.NextRow())
- // to_world_log("check_save_db_connection: Test query returned '[dbq.item[1]]'")
/proc/close_save_db_connection()
if(global.dbcon_save && global.dbcon_save.IsConnected())
diff --git a/sql/migrate/V101__Save_Logging.sql b/sql/migrate/V101__Save_Logging.sql
new file mode 100644
index 00000000000..15ac90058a4
--- /dev/null
+++ b/sql/migrate/V101__Save_Logging.sql
@@ -0,0 +1,106 @@
+-- --------------------------------------------------------
+-- Host: 127.0.0.1
+-- Server version: 10.11.2-MariaDB - mariadb.org binary distribution
+-- Server OS: Win64
+-- HeidiSQL Version: 12.5.0.6677
+-- --------------------------------------------------------
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET NAMES utf8 */;
+/*!50503 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+-- Dumping structure for procedure outreach13.ClearWorldSave
+DELIMITER //
+CREATE PROCEDURE `ClearWorldSave`()
+ MODIFIES SQL DATA
+ SQL SECURITY INVOKER
+ COMMENT 'Delete all the data from the previous world save before we make a new one.'
+BEGIN
+ DELETE FROM list_element;
+ DELETE FROM thing;
+ DELETE FROM thing_var;
+ DELETE FROM z_level;
+ ## TODO: Add code for handling selectively removing entries when save system update comes in
+END//
+DELIMITER ;
+
+-- Dumping structure for function outreach13.GetLastWorldSaveTime
+DELIMITER //
+CREATE FUNCTION `GetLastWorldSaveTime`() RETURNS datetime
+ READS SQL DATA
+ SQL SECURITY INVOKER
+ COMMENT 'Returns the DATETIME of when the last world save logged was.'
+BEGIN
+ RETURN (SELECT save_logging.time_end FROM save_logging WHERE save_logging.save_type = 'WORLD' LIMIT 1);
+END//
+DELIMITER ;
+
+-- Dumping structure for function outreach13.LogSaveEnd
+DELIMITER //
+CREATE FUNCTION `LogSaveEnd`(`MyId` INT,
+ `MyNbSavedLevels` INT,
+ `MyNbSavedAtoms` INT,
+ `MyResult` LONGTEXT
+) RETURNS int(11)
+ MODIFIES SQL DATA
+ SQL SECURITY INVOKER
+ COMMENT 'Marks the save end time in the logging table. Returns the save log entry index.'
+BEGIN
+ UPDATE save_logging
+ SET time_end = CURRENT_TIMESTAMP(), save_result = MyResult, nb_saved_levels = MyNbSavedLevels, nb_saved_atoms = MyNbSavedAtoms
+ WHERE `id` = MyId;
+ RETURN MyId;
+END//
+DELIMITER ;
+
+-- Dumping structure for function outreach13.LogSaveStorageStart
+DELIMITER //
+CREATE FUNCTION `LogSaveStorageStart`(`Initiator` VARCHAR(64)
+) RETURNS int(11)
+ MODIFIES SQL DATA
+ SQL SECURITY INVOKER
+ COMMENT 'Adds an entry to the save log for this save storage/limbo save. Returns the index in the save log this save session log is at.'
+BEGIN
+ INSERT INTO save_logging (save_logging.save_initiator, save_logging.save_type) VALUES (Initiator, 'STORAGE');
+ RETURN (SELECT LAST_INSERT_ID() FROM save_logging LIMIT 1);
+END//
+DELIMITER ;
+
+-- Dumping structure for function outreach13.LogSaveWorldStart
+DELIMITER //
+CREATE FUNCTION `LogSaveWorldStart`(`Initiator` VARCHAR(64)
+) RETURNS int(11)
+ MODIFIES SQL DATA
+ SQL SECURITY INVOKER
+ COMMENT 'Adds an entry to the save log for this world save session. Returns the index in the save log this save session log is at.'
+BEGIN
+ INSERT INTO save_logging (save_logging.save_initiator, save_logging.save_type) VALUES (Initiator, 'WORLD');
+ RETURN (SELECT LAST_INSERT_ID() FROM save_logging LIMIT 1);
+END//
+DELIMITER ;
+
+-- Dumping structure for table outreach13.save_logging
+CREATE TABLE IF NOT EXISTS `save_logging` (
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `time_start` datetime DEFAULT current_timestamp() COMMENT 'The time the save started at.',
+ `time_end` datetime DEFAULT NULL COMMENT 'The time the save completed at.',
+ `save_initiator` varchar(128) DEFAULT NULL COMMENT 'The ckey or description of who started the save!',
+ `save_type` enum('WORLD','STORAGE') NOT NULL COMMENT 'Whether the save was a world save, or was just for saving a set of specific atoms.',
+ `save_result` longtext DEFAULT NULL COMMENT 'What the result of the save was.',
+ `nb_saved_levels` int(10) unsigned DEFAULT NULL,
+ `nb_saved_atoms` int(10) unsigned DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Keeps a record of all the save writes done to this save database. So it''s easier to see where something could have went wrong.';
+
+-- Data exporting was unselected.
+
+/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */;
+/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
+/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */;
diff --git a/test/check-paths.sh b/test/check-paths.sh
index 32b8e7654ad..22c5ac90004 100755
--- a/test/check-paths.sh
+++ b/test/check-paths.sh
@@ -37,7 +37,7 @@ exactly 1 "world.log<< uses" 'world.log<<|world.log[[:space:]]<<'
exactly 18 "<< uses" '(?> uses" '>>(?!>)' -P
exactly 0 "incorrect indentations" '^( {4,})' -P
-exactly 43 "text2path uses" 'text2path'
+exactly 42 "text2path uses" 'text2path'
exactly 5 "update_icon() override" '/update_icon\((.*)\)' -P
exactly 0 "goto uses" 'goto '
exactly 7 "atom/New uses" '^/(obj|atom|area|mob|turf).*/New\('