diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9cf858121b9..a5befac1a0d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -92,7 +92,7 @@ **/persistence @Arrow768 @NonQueueingMatt @FabianK3 /code/__DEFINES/persistence.dm @Arrow768 @NonQueueingMatt @FabianK3 /code/__HELPERS/logging/subsystems/persistence.dm @Arrow768 @NonQueueingMatt @FabianK3 -/code/controllers/subsystems/persistence.dm @Arrow768 @NonQueueingMatt @FabianK3 +/code/controllers/subsystems/persistence/ @Arrow768 @NonQueueingMatt @FabianK3 # Sprites, sammy /icons/ @nauticall diff --git a/.github/labeler.yml b/.github/labeler.yml index a477063ea7e..eb4391fc426 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,6 +1,6 @@ # Folder Rules Database: -- SQL/** +- '**/*.sql' '🗺️ Mapping - Away Ship/Away Site': - maps/away/** diff --git a/SQL/migrate-2023/V019__persistence_rename.sql b/SQL/migrate-2023/V019__persistence_rename.sql new file mode 100644 index 00000000000..d9964245247 --- /dev/null +++ b/SQL/migrate-2023/V019__persistence_rename.sql @@ -0,0 +1 @@ +RENAME TABLE ss13_persistent_data TO ss13_persistent_objects; diff --git a/aurorastation.dme b/aurorastation.dme index 3250b7fbe0a..21ea9ff4797 100644 --- a/aurorastation.dme +++ b/aurorastation.dme @@ -379,7 +379,6 @@ #include "code\controllers\subsystems\overlays.dm" #include "code\controllers\subsystems\pai.dm" #include "code\controllers\subsystems\pathfinder.dm" -#include "code\controllers\subsystems\persistence.dm" #include "code\controllers\subsystems\ping.dm" #include "code\controllers\subsystems\plants.dm" #include "code\controllers\subsystems\profiler.dm" @@ -431,6 +430,10 @@ #include "code\controllers\subsystems\movement\movement.dm" #include "code\controllers\subsystems\movement\movement_types.dm" #include "code\controllers\subsystems\movement\spacedrift.dm" +#include "code\controllers\subsystems\persistence\persistence.dm" +#include "code\controllers\subsystems\persistence\persistence_objects.dm" +#include "code\controllers\subsystems\persistence\persistence_objects_public.dm" +#include "code\controllers\subsystems\persistence\persistence_objects_sql.dm" #include "code\controllers\subsystems\processing\airflow.dm" #include "code\controllers\subsystems\processing\calamity.dm" #include "code\controllers\subsystems\processing\disease.dm" diff --git a/code/__DEFINES/global.dm b/code/__DEFINES/global.dm index 646262ae1b3..2e53035bd83 100644 --- a/code/__DEFINES/global.dm +++ b/code/__DEFINES/global.dm @@ -111,9 +111,9 @@ GLOBAL_VAR(custom_event_msg) GLOBAL_DATUM(dbcon, /DBConnection) GLOBAL_PROTECT(dbcon) -// Persistence subsystem track register - List of all persistent data tracks managed by the subsystem. -GLOBAL_LIST_EMPTY(persistence_register) -GLOBAL_PROTECT(persistence_register) +// Persistence subsystem object track register - List of all persistent objects tracked by the subsystem. +GLOBAL_LIST_EMPTY(persistence_object_track_register) +GLOBAL_PROTECT(persistence_object_track_register) // Added for Xenoarchaeology, might be useful for other stuff. GLOBAL_LIST_INIT(alphabet_uppercase, list("A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z")) diff --git a/code/__HELPERS/logging/subsystems/persistence.dm b/code/__HELPERS/logging/subsystems/persistence.dm index 76575df3200..cdb00b43133 100644 --- a/code/__HELPERS/logging/subsystems/persistence.dm +++ b/code/__HELPERS/logging/subsystems/persistence.dm @@ -5,3 +5,15 @@ if (GLOB.config?.logsettings["log_subsystems_persistence"]) WRITE_LOG(GLOB.config.logfiles["world_subsystems_persistence_log"], "SSPersistence: [text]") #endif + +/proc/log_subsystem_persistence_info(text) + log_subsystem_persistence("INFO: [text]") + +/proc/log_subsystem_persistence_warning(text) + log_subsystem_persistence("WARNING: [text]") + +/proc/log_subsystem_persistence_error(text) + log_subsystem_persistence("ERROR: [text]") + +/proc/log_subsystem_persistence_panic(text) + log_subsystem_persistence("PANIC: [text]") diff --git a/code/controllers/subsystems/persistence.dm b/code/controllers/subsystems/persistence.dm deleted file mode 100644 index 20f7e9a19da..00000000000 --- a/code/controllers/subsystems/persistence.dm +++ /dev/null @@ -1,317 +0,0 @@ -SUBSYSTEM_DEF(persistence) - name = "Persistence" - init_order = INIT_ORDER_PERSISTENCE - flags = SS_NO_FIRE - -/*############################################# - Internal methods -#############################################*/ - -/** - * Initialization of the persistence subsystem. Initialization includes loading all persistent data and spawning the related objects. - */ -/datum/controller/subsystem/persistence/Initialize() - . = ..() - try - if(!GLOB.config.sql_enabled) - log_subsystem_persistence("SQL configuration not enabled! Persistence subsystem requires SQL.") - return SS_INIT_SUCCESS - - GLOB.persistence_register = list() - - if(!SSdbcore.Connect()) - log_subsystem_persistence("SQL ERROR during persistence subsystem init. Failed to connect.") - return SS_INIT_FAILURE - else - // Delete all persistent objects in the database that have expired and have passed the cleanup grace period (PERSISTENT_EXPIRATION_CLEANUP_DELAY_DAYS) - database_clean_entries() - - // Retrieve all persistent data that is not expired - var/list/persistent_data = database_get_active_entries() - log_subsystem_persistence("Init: Retrieved [persistent_data.len] entries for instancing this round.") - - // Instantiate all remaining entries based of their type - // Assign persistence related vars found in /obj, apply content and add to live tracking list. - for (var/data in persistent_data) - CHECK_TICK - var/typepath = text2path(data["type"]) - if (!ispath(typepath)) // Type checking - continue - // Note that the object here is instantiated without init args. - // Objects that require init args should fall back to INITALIZE_HINT_LATELOAD during Init, - // as this will give the subsystem the chance to apply the content and - // the object to continue with init logic after the subsystem is done in LateInitialize. - var/obj/instance = new typepath() - instance.persistence_track_id = data["id"] - track_apply_content(instance, data["content"], data["x"], data["y"], data["z"]) - register_track(instance, data["author_ckey"]) - - return SS_INIT_SUCCESS - catch(var/exception/e) - log_subsystem_persistence("Panic: Exception during subsystem initialize: [e]") - return SS_INIT_FAILURE - -/** - * Shutdown of the persistence subsystem. Adds new persistent objects, removes no longer existing persistent objects and updates changed persistent objects in the database. - */ -/datum/controller/subsystem/persistence/Shutdown() - try - if(!GLOB.config.sql_enabled) - log_subsystem_persistence("SQL configuration not enabled! Panic - Cannot save current round track changes!") - return - - // Subsystem shutdown: - // Create new persistent records for objects that have been created in the round - // Update tracked objects that have an ID (already existing from previous rounds) - // Delete persistent records that no longer exist in the registry (removed during the round) - - // Run checks on each track that might prevent further persistence - for (var/obj/track in GLOB.persistence_register) - CHECK_TICK - var/turf/T = get_turf(track) - if(!T || !is_station_level(T.z)) // The persistence system only supports objects from the main map levels for multiple reasons, e.g. Z level value, mapping support - deregister_track(track) - if(astype(track, /obj/item)?.in_inventory) // Objects that are held by players won't become persistent - deregister_track(track) - - var/created = 0 - var/updated = 0 - var/expired = 0 - - // Get already stored data before saving new tracks so we can compare what has been updated or removed during the round. - var/list/existing_data = database_get_active_entries() - - for (var/obj/track in GLOB.persistence_register) - CHECK_TICK - if (track.persistence_track_id == 0) - // Tracked object has no ID meaning it is new, create a new persistent record for it - database_add_entry(track) - created++ - - // Find tracks that have been removed during the round by trying to find the track by database ID - // If we find the track, we need to check if it requires an update instead - for (var/record in existing_data) - var/found = FALSE - for (var/obj/track in GLOB.persistence_register) - CHECK_TICK - if (record["id"] == track.persistence_track_id) - // A track with the same ID has been found in the register, it still exists, check if we need to update it instead - found = TRUE // Prevent expiration of track - var/changed = FALSE - var/turf/T = get_turf(track) - if (T && T.x != record["x"]) - changed = TRUE - else if (T && T.y != record["y"]) - changed = TRUE - else if (T && T.z != record["z"]) - changed = TRUE - else if (track_get_content(track) != record["content"]) - changed = TRUE - if (changed) - database_update_entry(track) - updated++ - break // Track found (and perhaps updated), break off loop search as it won't need to be deleted anyways - if (!found) - // No track with the same ID has been found in the register, remove it from the database (expire) - database_expire_entry(record["id"]) - expired++ - - log_subsystem_persistence("Shutdown: Tried to create [created], update [updated] and expire [expired] tracks.") - catch(var/exception/e) - log_subsystem_persistence("Panic: Exception during subsystem shutdown: [e]") - return - -/** - * Generates StatEntry. Returns information about currently tracked objects. - */ -/datum/controller/subsystem/persistence/stat_entry(msg) - msg = ("Global register tracks: [GLOB.persistence_register.len]") - return ..() - -/** - * Safely get JSON persistent content of track. - * RETURN: JSON formatted content of track or null if an exception occured. - */ -/datum/controller/subsystem/persistence/proc/track_get_content(var/obj/track) - var/result = json_encode(list()) - try - var/list/content = track.persistence_get_content() - if(content && content.len) - result = json_encode(content) - catch(var/exception/e) - log_subsystem_persistence("Track: Failed to get/encode track content: [e]") - return result - -/** - * Safely apply persistent content to track. - * PARAMS: - * track = Object to apply content to. - * json = Custom persistent content JSON to be applied. - * x,y,z = x-y-z coordinates of object, can be null. - */ -/datum/controller/subsystem/persistence/proc/track_apply_content(var/obj/track, var/json, var/x, var/y, var/z) - try - track.persistence_apply_content(json_decode(json), x, y, z) - catch(var/exception/e) - log_subsystem_persistence("Track: Failed to apply/decode track content: [e]") - -/** - * Run cleanup on the persistence entries in the database. - * Cleanup includes all entries that have expired and have passed the clean up grace period (PERSISTENT_EXPIRATION_CLEANUP_DELAY_DAYS). - */ -/datum/controller/subsystem/persistence/proc/database_clean_entries() - if(!SSdbcore.Connect()) - log_subsystem_persistence("SQL ERROR during persistence database_clean_entries. Failed to connect.") - else - var/datum/db_query/cleanup_query = SSdbcore.NewQuery( - "DELETE FROM ss13_persistent_data WHERE DATE_ADD(expires_at, INTERVAL :grace_period_days DAY) <= NOW()", - list("grace_period_days" = PERSISTENT_EXPIRATION_CLEANUP_DELAY_DAYS) - ) - - cleanup_query.SetFailCallback(CALLBACK(PROC_REF(database_clean_entries_callback_failure))) - cleanup_query.SetSuccessCallback(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel))) - cleanup_query.ExecuteNoSleep(TRUE) - -/datum/controller/subsystem/persistence/proc/database_clean_entries_callback_failure(var/datum/db_query/cleanup_query) - if (cleanup_query.ErrorMsg()) - log_subsystem_persistence("SQL ERROR during persistence database_clean_entries. " + cleanup_query.ErrorMsg()) - qdel(cleanup_query) - -/** - * Retrieve persistent data entries that haven't expired. - * RETURN: List of JSON, with ID, author_ckey, type, content, x, y, z - */ -/datum/controller/subsystem/persistence/proc/database_get_active_entries() - if(!SSdbcore.Connect()) - log_subsystem_persistence("SQL ERROR during persistence database_get_active_entries. Failed to connect.") - else - var/datum/db_query/get_query = SSdbcore.NewQuery( - "SELECT id, author_ckey, type, content, x, y, z FROM ss13_persistent_data WHERE NOW() < expires_at" - ) - get_query.Execute() - - var/list/results = list() - if (get_query.ErrorMsg()) - log_subsystem_persistence("SQL ERROR during persistence database_get_active_entries. " + get_query.ErrorMsg()) - return - else - while (get_query.NextRow()) - CHECK_TICK - var/list/entry = list( - "id" = get_query.item[1], - "author_ckey" = get_query.item[2], - "type" = get_query.item[3], - "content" = get_query.item[4], - "x" = get_query.item[5], - "y" = get_query.item[6], - "z" = get_query.item[7] - ) - results += list(entry) - qdel(get_query) - return results - -/** - * Adds a persistent data record to the database. - */ -/datum/controller/subsystem/persistence/proc/database_add_entry(var/obj/track) - if(!SSdbcore.Connect()) - log_subsystem_persistence("SQL ERROR during persistence database_add_entry. Failed to connect.") - else - var/turf/T = get_turf(track) - if(!T) - return - - var/datum/db_query/insert_query = SSdbcore.NewQuery( - "INSERT INTO ss13_persistent_data (author_ckey, type, created_at, expires_at, content, x, y, z) \ - VALUES (:author_ckey, :type, NOW(), DATE_ADD(NOW(), INTERVAL :expire_in_days DAY), :content, :x, :y, :z)", - list( - "author_ckey" = track.persistence_author_ckey, - "type" = "[track.type]", - "expire_in_days" = track.persistance_expiration_time_days, - "content" = track_get_content(track), - "x" = T.x, - "y" = T.y, - "z" = T.z - ) - ) - insert_query.Execute() - - if (insert_query.ErrorMsg()) - log_subsystem_persistence("SQL ERROR during persistence database_add_entry. " + insert_query.ErrorMsg()) - qdel(insert_query) - -/** - * Updates a persistent data record in the database. - */ -/datum/controller/subsystem/persistence/proc/database_update_entry(var/obj/track) - if(!SSdbcore.Connect()) - log_subsystem_persistence("SQL ERROR during persistence database_update_entry. Failed to connect.") - else - var/turf/T = get_turf(track) - if(!T) - return - - var/datum/db_query/update_query = SSdbcore.NewQuery( - "UPDATE ss13_persistent_data SET author_ckey=:author_ckey, expires_at=DATE_ADD(NOW(), INTERVAL :expire_in_days DAY), content=:content, x=:x, y=:y, z=:z WHERE id = :id", - list( - "author_ckey" = track.persistence_author_ckey, - "expire_in_days" = track.persistance_expiration_time_days, - "content" = track_get_content(track), - "x" = T.x, - "y" = T.y, - "z" = T.z, - "id" = track.persistence_track_id - ) - ) - update_query.Execute() - - if (update_query.ErrorMsg()) - log_subsystem_persistence("SQL ERROR during persistence database_update_entry. " + update_query.ErrorMsg()) - qdel(update_query) - -/** - * Expire a persistent data record in the database by setting it's expiration date to now. - */ -/datum/controller/subsystem/persistence/proc/database_expire_entry(var/track_id) - if(!SSdbcore.Connect()) - log_subsystem_persistence("SQL ERROR during persistence database_expire_entry. Failed to connect.") - else - var/datum/db_query/expire_query = SSdbcore.NewQuery( - "UPDATE ss13_persistent_data SET expires_at=NOW() WHERE id = :id", - list("id" = track_id) - ) - expire_query.Execute() - - if (expire_query.ErrorMsg()) - log_subsystem_persistence("SQL ERROR during persistence database_expire_entry. " + expire_query.ErrorMsg()) - qdel(expire_query) - -/*############################################# - Public methods -#############################################*/ - -/** - * Adds the given object to the list of tracked objects. At shutdown the tracked object will be either created or updated in the database. - * The ckey is an optional argument and is used for tracking user generated content by adding an author to the persistent data. - */ -/datum/controller/subsystem/persistence/proc/register_track(var/obj/new_track, var/ckey) - if(new_track.persistence_track_active) // Prevent multiple registers per object and removes the need to check the register if it's already in there - return - - var/turf/T = get_turf(new_track) - if(!T || !is_station_level(T.z)) // The persistence system only supports objects from the main map levels for multiple reasons, e.g. Z level value, mapping support - return - - new_track.persistence_track_active = TRUE - new_track.persistence_author_ckey = ckey - GLOB.persistence_register += new_track - -/** - * Removes the given object from the list of tracked objects. At shutdown the tracked object will be remove from the database. - */ -/datum/controller/subsystem/persistence/proc/deregister_track(var/obj/old_track) - if(!old_track.persistence_track_active) // Prevent multiple deregisters per object and removes the need to check the register if it's not in there - return - - old_track.persistence_track_active = FALSE - GLOB.persistence_register -= old_track diff --git a/code/controllers/subsystems/persistence/persistence.dm b/code/controllers/subsystems/persistence/persistence.dm new file mode 100644 index 00000000000..5448d95507c --- /dev/null +++ b/code/controllers/subsystems/persistence/persistence.dm @@ -0,0 +1,85 @@ +/* + * Persistence subsystem + * Subsytem for managing any form of persistent content across rounds. + * + * This subsystem consists of multiple partial files, following the structure: + * - persistence.dm - Subsystem definition and generic code. + * - persistence_objects.dm - Persistent objects related code. + * - persistence_objects_sql.dm - Persistent objects database code. + * - persistence_objects_public.dm - Persistent objects public procs. + */ + +SUBSYSTEM_DEF(persistence) + name = "Persistence" + init_order = INIT_ORDER_PERSISTENCE // The order is tied with the init and maploading subsystem. + flags = SS_NO_FIRE // This subsystem has no continues workload, it's init and shutdown only. + +/** + * Subsystem info stub message generation. + */ +/datum/controller/subsystem/persistence/stat_entry(msg) + msg = ("Tracked object register: [length(GLOB.persistence_object_track_register)]") + return ..() + +/** + * Helper method to check and log database connection. + * RETURN: True if connection is scuccessful, false if not. + * PARAMS: + * action = Custom string of the action being performed written to log. + */ +/datum/controller/subsystem/persistence/proc/databaseCheckConnection(action = "unlabeled action") + PRIVATE_PROC(TRUE) + if(!SSdbcore.Connect()) + log_subsystem_persistence_error("SQL error during [action], connection failed.") + return FALSE + return TRUE + +/** + * Helper method to check the SQL query result and log possible errors. + * RETURN: True if no error occured, false if an error was found. + */ +/datum/controller/subsystem/persistence/proc/databaseCheckQueryResult(datum/db_query/query, action = "unlabeled action") + PRIVATE_PROC(TRUE) + if (!query) + log_subsystem_persistence_error("SQL error during [action], in addition query object provided to check was null.") + return FALSE + if (query.ErrorMsg()) + log_subsystem_persistence_error("SQL error during [action]. " + query.ErrorMsg()) + return FALSE + return TRUE + +/** + * Initialization of the persistence subsystem. + * Includes generic startup checks and init of the different persistent data types. + */ +/datum/controller/subsystem/persistence/Initialize() + . = ..() + if(!GLOB.config.sql_enabled) + log_subsystem_persistence_warning("SQL configuration not enabled. Persistence subsystem requires SQL. Skipping init.") + return SS_INIT_SUCCESS + + if(!databaseCheckConnection("subsystem init")) + return SS_INIT_FAILURE + + try + objectsInitialize() + catch(var/exception/e) + log_subsystem_persistence_panic("Unhandled exception during persistent objects initialization: [e]") + return SS_INIT_FAILURE + + return SS_INIT_SUCCESS + +/** + * Shutdown of the persistence subsystem. + * The shutdown consists of finalization steps for each persistent data type. + */ +/datum/controller/subsystem/persistence/Shutdown() + if(!databaseCheckConnection("subsystem shutdown")) + log_subsystem_persistence_panic("SQL error during persistence subsystem shutdown. Cannot finalise persistence of the round.") + return + + try + objectsFinalize() + catch(var/exception/e) + log_subsystem_persistence_panic("Unhandled exception during persistent objects finalization: [e]") + return diff --git a/code/controllers/subsystems/persistence/persistence_objects.dm b/code/controllers/subsystems/persistence/persistence_objects.dm new file mode 100644 index 00000000000..5e0d8a59e73 --- /dev/null +++ b/code/controllers/subsystems/persistence/persistence_objects.dm @@ -0,0 +1,124 @@ +/** + * Initializes persistent objects. + * This includes cleaning up expired objects from the database and instanciating all active tracks. + */ +/datum/controller/subsystem/persistence/proc/objectsInitialize() + PRIVATE_PROC(TRUE) + GLOB.persistence_object_track_register = list() + + // Delete all persistent objects in the database that have expired and have passed the cleanup grace period (PERSISTENT_EXPIRATION_CLEANUP_DELAY_DAYS) + objectsDatabaseCleanEntries() + + // Retrieve all persistent data that is not expired + var/list/persistent_data = objectsDatabaseGetActiveEntries() + log_subsystem_persistence_info("Persistent objects: Retrieved [length(persistent_data)] entries for instancing this round.") + + // Instantiate all remaining entries based of their type + // Assign persistence related vars found in /obj, apply content and add to live tracking list. + for (var/data in persistent_data) + CHECK_TICK + var/typepath = text2path(data["type"]) + if (!ispath(typepath)) // Type checking + continue + // Note that the object here is instantiated without init args. + // Objects that require init args should fall back to INITALIZE_HINT_LATELOAD during Init, + // as this will give the subsystem the chance to apply the content and + // the object to continue with init logic after the subsystem is done in LateInitialize. + var/obj/instance = new typepath() + instance.persistent_objects_track_id = data["id"] + objectsApplyTrackContent(instance, data["content"], data["x"], data["y"], data["z"]) + objectsRegisterTrack(instance, data["author_ckey"]) + +/** + * Finalize persistent object tracking. + * Adds new persistent objects, removes no longer existing persistent objects and updates changed persistent objects in the database. + */ +/datum/controller/subsystem/persistence/proc/objectsFinalize() + PRIVATE_PROC(TRUE) + + // Subsystem shutdown: + // Create new persistent records for objects that have been created in the round + // Update tracked objects that have an ID (already existing from previous rounds) + // Delete persistent records that no longer exist in the registry (removed during the round) + + // Run checks on each track that might prevent further persistence + for (var/obj/track as anything in GLOB.persistence_object_track_register) + CHECK_TICK + var/turf/T = get_turf(track) + if(!T || !is_station_level(T.z)) // The persistence system only supports objects from the main map levels for multiple reasons, e.g. Z level value, mapping support + objectsDeregisterTrack(track) + if(astype(track, /obj/item)?.in_inventory) // Objects that are held by players won't become persistent + objectsDeregisterTrack(track) + + var/created = 0 + var/updated = 0 + var/expired = 0 + + // Get already stored data before saving new tracks so we can compare what has been updated or removed during the round. + var/list/existing_data = objectsDatabaseGetActiveEntries() + + for (var/obj/track as anything in GLOB.persistence_object_track_register) + CHECK_TICK + if (track.persistent_objects_track_id == 0) + // Tracked object has no ID meaning it is new, create a new persistent record for it + objectsDatabaseAddEntry(track) + created++ + + // Find tracks that have been removed during the round by trying to find the track by database ID + // If we find the track, we need to check if it requires an update instead + for (var/record in existing_data) + var/found = FALSE + for (var/obj/track as anything in GLOB.persistence_object_track_register) + CHECK_TICK + if (record["id"] == track.persistent_objects_track_id) + // A track with the same ID has been found in the register, it still exists, check if we need to update it instead + found = TRUE // Prevent expiration of track + var/changed = FALSE + var/turf/T = get_turf(track) + if (T && T.x != record["x"]) + changed = TRUE + else if (T && T.y != record["y"]) + changed = TRUE + else if (T && T.z != record["z"]) + changed = TRUE + else if (objectsGetTrackContent(track) != record["content"]) + changed = TRUE + if (changed) + objectsDatabaseUpdateEntry(track) + updated++ + break // Track found (and perhaps updated), break off loop search as it won't need to be deleted anyways + if (!found) + // No track with the same ID has been found in the register, remove it from the database (expire) + objectsDatabaseExpireEntry(record["id"]) + expired++ + + log_subsystem_persistence_info("Persistent objects: Created [created], updated [updated] and expired [expired] tracks.") + +/** + * Safely get JSON persistent content of track. + * RETURN: JSON formatted content of track or null if an exception occured. + */ +/datum/controller/subsystem/persistence/proc/objectsGetTrackContent(obj/track) + PRIVATE_PROC(TRUE) + var/result = json_encode(list()) + try + var/list/content = track.persistent_objects_get_content() + if(length(content)) + result = json_encode(content) + catch(var/exception/e) + log_subsystem_persistence_error("Error during json serialization for persistent object. Failed to get/encode track content: [e]") + return result + +/** + * Safely apply persistent content to track. + * PARAMS: + * track = Object to apply content to. + * json = Custom persistent content JSON to be applied. + * x,y,z = x-y-z coordinates of object, can be null. + */ +/datum/controller/subsystem/persistence/proc/objectsApplyTrackContent(obj/track, json, x, y, z) + PRIVATE_PROC(TRUE) + try + track.persistent_objects_apply_content(json_decode(json), x, y, z) + catch(var/exception/e) + log_subsystem_persistence_error("Error during json deserialization for persistent object. Failed to apply/decode track content: [e]") diff --git a/code/controllers/subsystems/persistence/persistence_objects_public.dm b/code/controllers/subsystems/persistence/persistence_objects_public.dm new file mode 100644 index 00000000000..2a3452694b1 --- /dev/null +++ b/code/controllers/subsystems/persistence/persistence_objects_public.dm @@ -0,0 +1,28 @@ +/* This partial file contains all procs that are meant for public access. */ +/* Other procs should be made private. */ + +/** + * Adds the given object to the list of tracked objects. At shutdown the tracked object will be either created or updated in the database. + * The ckey is an optional argument and is used for tracking user generated content by adding an author to the persistent data. + */ +/datum/controller/subsystem/persistence/proc/objectsRegisterTrack(obj/new_track, ckey) + if(new_track.persistent_objects_track_active) // Prevent multiple registers per object and removes the need to check the register if it's already in there + return + + var/turf/T = get_turf(new_track) + if(!T || !is_station_level(T.z)) // The persistence system only supports objects from the main map levels for multiple reasons, e.g. Z level value, mapping support + return + + new_track.persistent_objects_track_active = TRUE + new_track.persistent_objects_author_ckey = ckey + GLOB.persistence_object_track_register += new_track + +/** + * Removes the given object from the list of tracked objects. At shutdown the tracked object will be remove from the database. + */ +/datum/controller/subsystem/persistence/proc/objectsDeregisterTrack(obj/old_track) + if(!old_track.persistent_objects_track_active) // Prevent multiple deregisters per object and removes the need to check the register if it's not in there + return + + old_track.persistent_objects_track_active = FALSE + GLOB.persistence_object_track_register -= old_track diff --git a/code/controllers/subsystems/persistence/persistence_objects_sql.dm b/code/controllers/subsystems/persistence/persistence_objects_sql.dm new file mode 100644 index 00000000000..5152183170f --- /dev/null +++ b/code/controllers/subsystems/persistence/persistence_objects_sql.dm @@ -0,0 +1,130 @@ +/** + * Run cleanup on the persistence entries in the database. + * Cleanup includes all entries that have expired and have passed the clean up grace period (PERSISTENT_EXPIRATION_CLEANUP_DELAY_DAYS). + */ +/datum/controller/subsystem/persistence/proc/objectsDatabaseCleanEntries() + PRIVATE_PROC(TRUE) + if(!databaseCheckConnection("objectsDatabaseCleanEntries")) + return + + var/datum/db_query/cleanup_query = SSdbcore.NewQuery( + "DELETE FROM ss13_persistent_objects WHERE DATE_ADD(expires_at, INTERVAL :grace_period_days DAY) <= NOW()", + list("grace_period_days" = PERSISTENT_EXPIRATION_CLEANUP_DELAY_DAYS) + ) + + cleanup_query.SetFailCallback(CALLBACK(PROC_REF(objectsDatabaseCleanEntries_CallbackFailure))) + cleanup_query.SetSuccessCallback(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel))) + cleanup_query.ExecuteNoSleep(TRUE) + +/datum/controller/subsystem/persistence/proc/objectsDatabaseCleanEntries_CallbackFailure(datum/db_query/cleanup_query) + databaseCheckQueryResult(cleanup_query, "objectsDatabaseCleanEntries") + qdel(cleanup_query) + +/** + * Retrieve persistent data entries that haven't expired. + * RETURN: List of JSON, with ID, author_ckey, type, content, x, y, z + */ +/datum/controller/subsystem/persistence/proc/objectsDatabaseGetActiveEntries() + PRIVATE_PROC(TRUE) + if(!databaseCheckConnection("objectsDatabaseGetActiveEntries")) + return + + var/datum/db_query/get_query = SSdbcore.NewQuery( + "SELECT id, author_ckey, type, content, x, y, z FROM ss13_persistent_objects WHERE NOW() < expires_at" + ) + get_query.Execute() + + var/list/results = list() + if (!databaseCheckQueryResult(get_query, "objectsDatabaseGetActiveEntries")) + return + else + while (get_query.NextRow()) + CHECK_TICK + var/list/entry = list( + "id" = get_query.item[1], + "author_ckey" = get_query.item[2], + "type" = get_query.item[3], + "content" = get_query.item[4], + "x" = get_query.item[5], + "y" = get_query.item[6], + "z" = get_query.item[7] + ) + results += list(entry) + qdel(get_query) + return results + +/** + * Adds a persistent data record to the database. + */ +/datum/controller/subsystem/persistence/proc/objectsDatabaseAddEntry(obj/track) + PRIVATE_PROC(TRUE) + if(!databaseCheckConnection("objectsDatabaseAddEntry")) + return + + var/turf/T = get_turf(track) + if(!T) + return + + var/datum/db_query/insert_query = SSdbcore.NewQuery( + "INSERT INTO ss13_persistent_objects (author_ckey, type, created_at, expires_at, content, x, y, z) \ + VALUES (:author_ckey, :type, NOW(), DATE_ADD(NOW(), INTERVAL :expire_in_days DAY), :content, :x, :y, :z)", + list( + "author_ckey" = track.persistent_objects_author_ckey, + "type" = "[track.type]", + "expire_in_days" = track.persistant_objects_expiration_time_days, + "content" = objectsGetTrackContent(track), + "x" = T.x, + "y" = T.y, + "z" = T.z + ) + ) + insert_query.Execute() + + databaseCheckQueryResult(insert_query, "objectsDatabaseAddEntry") + qdel(insert_query) + +/** + * Updates a persistent data record in the database. + */ +/datum/controller/subsystem/persistence/proc/objectsDatabaseUpdateEntry(obj/track) + PRIVATE_PROC(TRUE) + if(!databaseCheckConnection("objectsDatabaseUpdateEntry")) + return + + var/turf/T = get_turf(track) + if(!T) + return + + var/datum/db_query/update_query = SSdbcore.NewQuery( + "UPDATE ss13_persistent_objects SET author_ckey=:author_ckey, expires_at=DATE_ADD(NOW(), INTERVAL :expire_in_days DAY), content=:content, x=:x, y=:y, z=:z WHERE id = :id", + list( + "author_ckey" = track.persistent_objects_author_ckey, + "expire_in_days" = track.persistant_objects_expiration_time_days, + "content" = objectsGetTrackContent(track), + "x" = T.x, + "y" = T.y, + "z" = T.z, + "id" = track.persistent_objects_track_id + ) + ) + update_query.Execute() + + databaseCheckQueryResult(update_query, "objectsDatabaseUpdateEntry") + qdel(update_query) + +/** + * Expire a persistent data record in the database by setting it's expiration date to now. + */ +/datum/controller/subsystem/persistence/proc/objectsDatabaseExpireEntry(track_id) + PRIVATE_PROC(TRUE) + if(!databaseCheckConnection("objectsDatabaseExpireEntry")) + return + + var/datum/db_query/expire_query = SSdbcore.NewQuery( + "UPDATE ss13_persistent_objects SET expires_at=NOW() WHERE id = :id", + list("id" = track_id) + ) + expire_query.Execute() + + databaseCheckQueryResult(expire_query, "objectsDatabaseExpireEntry") + qdel(expire_query) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 18a404d3283..d21219b2609 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -507,7 +507,7 @@ return if(in_storage) // Items getting moved into storages (lunchboxes, backpacks) triggers the dropped handler and requires no persistency as a result - SSpersistence.deregister_track(src) + SSpersistence.objectsDeregisterTrack(src) return // Trash-like items should become only persistent when they are not dropped in an area flagged with AREA_FLAG_PREVENT_PERSISTENT_TRASH @@ -515,12 +515,12 @@ if(T) var/area/A = get_area(T) if(A && !(A.area_flags & AREA_FLAG_PREVENT_PERSISTENT_TRASH)) - persistance_expiration_time_days = 3 // Ensure expiration date is set to prevent long term trash - SSpersistence.register_track(src, usr == null ? null : ckey(usr.key)) + persistant_objects_expiration_time_days = 3 // Ensure expiration date is set to prevent long term trash + SSpersistence.objectsRegisterTrack(src, usr == null ? null : ckey(usr.key)) return // Fallback - No persistency - SSpersistence.deregister_track(src) + SSpersistence.objectsDeregisterTrack(src) /obj/item/proc/remove_item_verbs(mob/user) if(ismech(user)) //very snowflake, but necessary due to how mechs work diff --git a/code/game/objects/items/trash.dm b/code/game/objects/items/trash.dm index 1053d9d7eb3..9c3739d9d4d 100644 --- a/code/game/objects/items/trash.dm +++ b/code/game/objects/items/trash.dm @@ -15,7 +15,7 @@ /obj/item/trash/attack(mob/living/target_mob, mob/living/user, target_zone) return -/obj/item/trash/persistence_get_content() +/obj/item/trash/persistent_objects_get_content() var/list/content = list() content["name"] = name content["desc"] = desc @@ -26,7 +26,7 @@ content["pickup_sound"] = pickup_sound return content -/obj/item/trash/persistence_apply_content(content, x, y, z) +/obj/item/trash/persistent_objects_apply_content(content, x, y, z) name = content["name"] desc = content["desc"] icon = file(content["icon"]) diff --git a/code/game/objects/items/weapons/material/ashtray.dm b/code/game/objects/items/weapons/material/ashtray.dm index 5cc2b014055..cc109d66ba6 100644 --- a/code/game/objects/items/weapons/material/ashtray.dm +++ b/code/game/objects/items/weapons/material/ashtray.dm @@ -11,19 +11,19 @@ /obj/item/material/ashtray/Initialize(newloc, material_key) . = ..() - persistance_expiration_time_days = rand(7, 180) // Imagine they get stolen, lost or break... + persistant_objects_expiration_time_days = rand(7, 180) // Imagine they get stolen, lost or break... max_butts = round(material.hardness/10) //This is arbitrary but whatever. randpixel_xy() update_icon() - SSpersistence.register_track(src, null) + SSpersistence.objectsRegisterTrack(src, null) -/obj/item/material/ashtray/persistence_get_content() +/obj/item/material/ashtray/persistent_objects_get_content() var/list/content = list() content["fill_count"] = length(contents) content["material"] = material.name return content -/obj/item/material/ashtray/persistence_apply_content(content, x, y, z) +/obj/item/material/ashtray/persistent_objects_apply_content(content, x, y, z) src.x = x src.y = y src.z = z @@ -89,7 +89,7 @@ attacking_item.forceMove(src) if(istype(attacking_item, /obj/item/trash/cigbutt)) - SSpersistence.deregister_track(attacking_item) // Ashtray will handle the persistent contents in it itself + SSpersistence.objectsDeregisterTrack(attacking_item) // Ashtray will handle the persistent contents in it itself if (istype(attacking_item,/obj/item/clothing/mask/smokable/cigarette)) var/obj/item/clothing/mask/smokable/cigarette/cig = attacking_item diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index 74d916317ee..f112445f28b 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -81,21 +81,21 @@ /* START PERSISTENCE VARS */ // State check if the subsystem is tracking the object, used for easy state checking without iterating the register - var/persistence_track_active = FALSE + var/persistent_objects_track_active = FALSE // Tracking ID of the object used by the persistence subsystem - var/persistence_track_id = 0 + var/persistent_objects_track_id = 0 // Author ckey of the object used in persistence subsystem // Note: Not every type can have an author, like generated dirt for example // Additionally, the ckey is only an indicator, for example: A player could pin a paper without having written it // This should be considered for any moderation purpose - var/persistence_author_ckey = null + var/persistent_objects_author_ckey = null // Expiration time used when saving/updating a persistent type, this can be changed depending on the use case by assigning a new value - var/persistance_expiration_time_days = PERSISTENT_DEFAULT_EXPIRATION_DAYS + var/persistant_objects_expiration_time_days = PERSISTENT_DEFAULT_EXPIRATION_DAYS /* END PERSISTENCE VARS */ /obj/Destroy() - if(persistence_track_active) // Prevent hard deletion of references in the persistence register by removing it preemptively - SSpersistence.deregister_track(src) + if(persistent_objects_track_active) // Prevent hard deletion of references in the persistence register by removing it preemptively + SSpersistence.objectsDeregisterTrack(src) STOP_PROCESSING(SSprocessing, src) unbuckle() QDEL_NULL(talking_atom) @@ -388,7 +388,7 @@ * Expected to be overriden by derived objects. * RETURN: Associated list with custom information (e.g.: ["test" = "abc", "counter" = 123]) */ -/obj/proc/persistence_get_content() +/obj/proc/persistent_objects_get_content() return /** @@ -398,5 +398,5 @@ * content = Associated list with custom information (e.g.: ["test" = "abc", "counter" = 123]). * x,y,z = x-y-z coordinates of object, can be null. */ -/obj/proc/persistence_apply_content(content, x, y, z) +/obj/proc/persistent_objects_apply_content(content, x, y, z) return diff --git a/code/game/objects/structures/inflatable.dm b/code/game/objects/structures/inflatable.dm index 945279cdbc1..5f817db0cc5 100644 --- a/code/game/objects/structures/inflatable.dm +++ b/code/game/objects/structures/inflatable.dm @@ -293,7 +293,7 @@ icon_state = "folded_wall-torn" persistency_considered_trash = TRUE -/obj/item/inflatable/torn/persistence_apply_content(content, x, y, z) +/obj/item/inflatable/torn/persistent_objects_apply_content(content, x, y, z) src.x = x src.y = y src.z = z @@ -308,7 +308,7 @@ icon_state = "folded_door-torn" persistency_considered_trash = TRUE -/obj/item/inflatable/door/torn/persistence_apply_content(content, x, y, z) +/obj/item/inflatable/door/torn/persistent_objects_apply_content(content, x, y, z) src.x = x src.y = y src.z = z diff --git a/code/game/objects/structures/noticeboard.dm b/code/game/objects/structures/noticeboard.dm index 18bac5a9725..2f7e84d50e9 100644 --- a/code/game/objects/structures/noticeboard.dm +++ b/code/game/objects/structures/noticeboard.dm @@ -36,7 +36,7 @@ user.drop_from_inventory(attacking_item, src) notices++ update_icon() - SSpersistence.register_track(attacking_item, ckey(usr.key)) // Add paper to persistent tracker + SSpersistence.objectsRegisterTrack(attacking_item, ckey(usr.key)) // Add paper to persistent tracker to_chat(user, SPAN_NOTICE("You pin the paper to the noticeboard.")) else to_chat(user, SPAN_NOTICE("\The [src] is already full of papers and can not fit another.")) @@ -71,7 +71,7 @@ add_fingerprint(usr) notices-- update_icon() - SSpersistence.deregister_track(P) // Remove paper from persistent tracker + SSpersistence.objectsDeregisterTrack(P) // Remove paper from persistent tracker if(href_list["write"]) if((usr.stat || usr.restrained())) //For when a player is handcuffed while they have the notice window open return diff --git a/code/modules/paperwork/paper.dm b/code/modules/paperwork/paper.dm index 96c8739b7e9..6df1ae160c5 100644 --- a/code/modules/paperwork/paper.dm +++ b/code/modules/paperwork/paper.dm @@ -822,13 +822,13 @@ PERSISTENT #############################################*/ -/obj/item/paper/persistence_get_content() +/obj/item/paper/persistent_objects_get_content() var/list/content = list() content["title"] = name content["text"] = info return content -/obj/item/paper/persistence_apply_content(content, x, y, z) +/obj/item/paper/persistent_objects_apply_content(content, x, y, z) set_content(content["title"], content["text"]) src.x = x src.y = y @@ -865,14 +865,14 @@ icon_state = info ? "stickynote_words" : "stickynote" -/obj/item/paper/stickynotes/persistence_get_content() +/obj/item/paper/stickynotes/persistent_objects_get_content() var/list/content = ..() content["color"] = color content["pixel_x"] = pixel_x content["pixel_y"] = pixel_y return content -/obj/item/paper/stickynotes/persistence_apply_content(content, x, y, z) +/obj/item/paper/stickynotes/persistent_objects_apply_content(content, x, y, z) src.name = content["title"] src.info = content["text"] src.color = content["color"] @@ -883,7 +883,7 @@ src.z = z /obj/item/paper/stickynotes/pickup() - SSpersistence.deregister_track(src) + SSpersistence.objectsDeregisterTrack(src) ..() /obj/item/paper/stickynotes/afterattack(var/A, mob/user, var/prox, var/params) @@ -900,7 +900,7 @@ if(!params || !prox) return - SSpersistence.register_track(src, ckey(user.key)) + SSpersistence.objectsRegisterTrack(src, ckey(user.key)) user.drop_from_inventory(src,source_turf) if(params) //Parallels taped paper placement method, and avoids seeing stickynotes through walls var/list/mouse_control = mouse_safe_xy(params) diff --git a/code/modules/reagents/reagent_containers/food/drinks/bottle.dm b/code/modules/reagents/reagent_containers/food/drinks/bottle.dm index a5f7d9c2f12..a1f91c735fd 100644 --- a/code/modules/reagents/reagent_containers/food/drinks/bottle.dm +++ b/code/modules/reagents/reagent_containers/food/drinks/bottle.dm @@ -249,7 +249,7 @@ w_class = WEIGHT_CLASS_SMALL persistency_considered_trash = TRUE -/obj/item/broken_bottle/persistence_apply_content(content, x, y, z) +/obj/item/broken_bottle/persistent_objects_apply_content(content, x, y, z) src.x = x src.y = y src.z = z diff --git a/html/changelogs/fabiank3-persistence-refactoring.yml b/html/changelogs/fabiank3-persistence-refactoring.yml new file mode 100644 index 00000000000..3cb8fa783d8 --- /dev/null +++ b/html/changelogs/fabiank3-persistence-refactoring.yml @@ -0,0 +1,6 @@ +author: FabianK3 + +delete-after: True + +changes: + - code_imp: "Overhauled the current persistence subsystem for upcoming additions. No changes to current mechanics."