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\('