Persistent data framework (#21055)

# Summary

This PR has the goal of adding a base framework for tracking, saving and
otherwise managing persistent data between rounds, written from scratch.

Persistent data can be anything - Most expectedly noticeboards, dirt,
papers, etc.

_Technical details about the PR below are subject to change._

## Estimated scope

The first iteration of the persistence framework should include the
functioning subsystem and the first implementation of a persistent type
using said subsystem.

- [x] Prepare database for persistent data
- [x] Add new features to `/obj/`
- [x] Logic to get and load persistent data into objects for the
subsystem
  - [x] Internal logic for tracking and other
- [x] Implement subsystem
  - [x] Basics, files, integration of subsystem
  - [x] Database queries for different operations
- [x] Methods for registering and de-registering object tracking for
`/obj`
  - [x] Round start logic
  - [x] Round end logic
- [x] Implement first persistent type: Papers on notice boards
- [ ] Code review
  - [x] Scope validation
  - [x] Testing
  - [x] Full changelog
- Merge
- [ ] Setup logging config for new subsystem
- [x] Documentation for developers ([How to add new persistent
types](https://github.com/Aurorastation/Aurora.3/wiki/Persistence))

## Inner workings

The new persistence subsystem has the following concept:
At round start the subsystem reads existing data from the database and
creates objects for them.
During the round objects can be added to the tracking using a register
function (or removed by a de-register function).
At round end the subsystem goes over all it's actively tracked objects:
Create new objects (that don't have a persistent ID from the database),
update objects that changed during the round and expire objects that are
no longer tracked.

The objects in questions can register themselves, de-register themselves
and decide themselves what content should be stored and handle how to
set themselves up during the subsystems init phase.

## Information on the new table

`id` - database generated ID for tracking persistent objects over
multiple rounds, objects without an ID are considered new and created
during the round.
`author_ckey` - With the first implementation unused, but expected to be
required in the future to allow any staff/moderation on persistent data.
Nullable as persistent data also can be things like decals without an
actual owner.
`type` - Type of persistent data used for creating objects at round
start.
`created_at` - Statistical values.
`expire_at` - Used for cleaning the database and setting limits for
persistent data types. It's optional and allows permanent persistent
data (command notice boards?).
`content` - JSON formatted data containing the actual properties of an
individual tracked object (e.g. what's written on a paper, what's its
title).
`x`, `y`, `z` - Coordinates taken and given from/to the object during
save/load. Those can be null for future purposes.

## The goal on how to add new persistent data types

`/obj` received two new methods which need to be overriden in the to-be
added new persistent type:
`persistence_get_content()` and `persistence_apply_content(content, x,
y, z)`.

The first method has the responsibility to provide all custom content of
a new type to the subsystem in the form of an associated list (`["title"
= "Hello, World!", "value" = 1337].

What does that mean?  
In the example of a piece of paper, the method needs to put the papers
title and text content into a an associated list, that list will be
saved by the persistence subsystem.

The second method has the responsibility to apply the previously saved
content when persistent data is initialized at the start of the round:
The associated list needs to be read back and applied to the paper,
additionally coordinates are provided for the object that have been
taken during saving. Note that the coordinates might be null and could
be ignored during setup.

During the round, when a new object is created, like a paper, you need
to call the subsystem and register a track:
`SSpersistence.register_track(your_object_to_track, ckey (optional)`.
The subsystem will be using the methods above when the round ends and
saves the object - and loads it at the start of the round using the
second method. The ckey needs to be given for all persistent that
contains user generated content, e.g. papers.

In case a persistent object gets removed, you need to call
`SSpersistence.deregister_track(your_tracked_object)`. At the end of the
round the object will be removed from the storage.

`/obj` contains the variable (along some other technical vars)
`persistance_initial_expiration_time_days`, which has a default value of
30 days, but can be safely overriden on a per-type basis. This value (in
days) will be used for the persistent data entry expiration date when a
new object of said type is stored in the database.

## PR description changelog

- Added *The goal on how to add new persistent data types*.
- Updates *Estimated scope* format.
- Updates *Inner workings*.
- Final iteration.

*(Grammer and formatting not tracked manually)*
This commit is contained in:
FabianK3
2025-08-17 00:00:12 +02:00
committed by GitHub
parent fb4e695181
commit 36d267d83e
14 changed files with 421 additions and 2 deletions
+13
View File
@@ -0,0 +1,13 @@
CREATE TABLE ss13_persistent_data (
id INT NOT NULL AUTO_INCREMENT,
author_ckey VARCHAR(32) NULL,
type VARCHAR(128) NOT NULL,
created_at DATETIME NOT NULL,
expires_at DATETIME NOT NULL,
content MEDIUMTEXT NOT NULL,
x INT NULL,
y INT NULL,
z INT NULL,
PRIMARY KEY (id),
INDEX idx_expireDate (expires_at)
);
+3
View File
@@ -112,6 +112,7 @@
#include "code\__DEFINES\outfit.dm"
#include "code\__DEFINES\overmap.dm"
#include "code\__DEFINES\path.dm"
#include "code\__DEFINES\persistence.dm"
#include "code\__DEFINES\pipes.dm"
#include "code\__DEFINES\prefs.dm"
#include "code\__DEFINES\procpath.dm"
@@ -267,6 +268,7 @@
#include "code\__HELPERS\logging\subsystems\law.dm"
#include "code\__HELPERS\logging\subsystems\mapfinalization.dm"
#include "code\__HELPERS\logging\subsystems\odyssey.dm"
#include "code\__HELPERS\logging\subsystems\persistence.dm"
#include "code\__HELPERS\logging\subsystems\ZAS.dm"
#include "code\__HELPERS\paths\jps.dm"
#include "code\__HELPERS\paths\path.dm"
@@ -372,6 +374,7 @@
#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"
+4
View File
@@ -111,6 +111,10 @@ 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)
// 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"))
+6
View File
@@ -0,0 +1,6 @@
/*#############################################
Constants for the persistence subsystem
#############################################*/
#define PERSISTENT_DEFAULT_EXPIRATION_DAYS 30 // Default expire timespan for newly created persistent objects
#define PERSISTENT_EXPIRATION_CLEANUP_DELAY_DAYS 30 // Grace period for expired database entries before they get cleaned up.
+2 -1
View File
@@ -227,7 +227,8 @@
#define INIT_ORDER_EARLY_ASSETS 48
#define INIT_ORDER_SPATIAL_GRID 43
#define INIT_ORDER_ECONOMY 40
#define INIT_ORDER_MAPFINALIZE 31 //Asteroid generation, another aurora snowflake, must run before the atoms init
#define INIT_ORDER_MAPFINALIZE 32 // Asteroid generation, another aurora snowflake, must run before the atoms init
#define INIT_ORDER_PERSISTENCE 31 // Persistence subsystem, requires map load
#define INIT_ORDER_ATOMS 30
#define INIT_ORDER_MACHINES 20
#define INIT_ORDER_DEFAULT 0
@@ -0,0 +1,7 @@
/proc/log_subsystem_persistence(text)
#if defined(UNIT_TEST)
LOG_GITHUB_DEBUG("SSPersistence: [text]")
#else
if (GLOB.config?.logsettings["log_subsystems_persistence"])
WRITE_LOG(GLOB.config.logfiles["world_subsystems_persistence_log"], "SSPersistence: [text]")
#endif
+2
View File
@@ -62,6 +62,7 @@ GLOBAL_LIST_EMPTY(gamemode_cache)
"log_subsystems_ghostroles" = TRUE, // Ghost Roles
"log_subsystems_law" = TRUE, // Law
"log_subsystems_cargo" = TRUE, // Cargo
"log_subsystems_persistence" = TRUE, // Persistence
"log_subsystems_documents" = TRUE, // Documents
"log_subsystems_fail2topic" = TRUE, // Fail2Topic
"log_subsystems_mapfinalization" = TRUE, // Map Finalization
@@ -120,6 +121,7 @@ GLOBAL_LIST_EMPTY(gamemode_cache)
"world_subsystems_ghostroles_log" = "subsystems/ghostroles.log",
"world_subsystems_law_log" = "subsystems/law.log",
"world_subsystems_cargo_log" = "subsystems/cargo.log",
"world_subsystems_persistence_log" = "subsystems/persistence.log",
"world_subsystems_documents_log" = "subsystems/documents.log",
"world_subsystems_fail2topic_log" = "subsystems/fail2topic.log",
"world_subsystems_mapfinalization_log" = "subsystems/mapfinalization.log",
+310
View File
@@ -0,0 +1,310 @@
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
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)
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 = null
try
result = json_encode(track.persistence_get_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/content = track_get_content(track)
if (!content)
return
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
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" = content,
"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/content = track_get_content(track)
if (!content)
return
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
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" = content,
"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
+38
View File
@@ -65,7 +65,23 @@
var/list/req_one_access
/* END ACCESS VARS */
/* 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
// Tracking ID of the object used by the persistence subsystem
var/persistence_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
// 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
/* 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)
STOP_PROCESSING(SSprocessing, src)
unbuckle()
QDEL_NULL(talking_atom)
@@ -324,3 +340,25 @@
/// Override this to customize the effects an activated signaler has.
/obj/proc/do_signaler()
return
/*#############################################
PERSISTENT
#############################################*/
/**
* Called by the persistence subsystem to retrieve relevant persistent information to be stored in the database.
* Expected to be overriden by derived objects.
* RETURN: Associated list with custom information (e.g.: ["test" = "abc", "counter" = 123])
*/
/obj/proc/persistence_get_content()
return
/**
* Called by the persistence subsystem to apply persistent data on the created object.
* Expected to be overriden by derived objects.
* PARAMS:
* 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)
return
+7 -1
View File
@@ -7,8 +7,12 @@
anchored = 1
var/notices = 0
/obj/structure/noticeboard/Initialize()
/obj/structure/noticeboard/Initialize(mapload)
if (mapload)
add_papers_from_turf()
. = ..()
/obj/structure/noticeboard/proc/add_papers_from_turf()
for(var/obj/item/I in loc)
if(notices > 4) break
if(istype(I, /obj/item/paper))
@@ -25,6 +29,7 @@
user.drop_from_inventory(attacking_item,src)
notices++
icon_state = "nboard0[notices]" //update sprite
SSpersistence.register_track(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("You reach to pin your paper to the board but hesitate. You are certain your paper will not be seen among the many others already attached."))
@@ -58,6 +63,7 @@
add_fingerprint(usr)
notices--
icon_state = "nboard0[notices]"
SSpersistence.deregister_track(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
+19
View File
@@ -791,6 +791,25 @@
. = ..()
scan_target = WEAKREF(set_scan_target)
/*#############################################
PERSISTENT
#############################################*/
/obj/item/paper/persistence_get_content()
var/list/content = list()
content["title"] = name
content["text"] = info
return content
/obj/item/paper/persistence_apply_content(content, x, y, z)
set_content(content["title"], content["text"])
src.x = x
src.y = y
src.z = z
for(var/obj/object in loc) // Pin to noticeboard
if(istype(object, /obj/structure/noticeboard))
var/obj/structure/noticeboard/notice_board = object
notice_board.add_papers_from_turf()
/*#############################################
+1
View File
@@ -33,6 +33,7 @@
"log_subsystems_ghostroles":1,
"log_subsystems_law":1,
"log_subsystems_cargo":1,
"log_subsystems_persistence":1,
"log_subsystems_discord":0,
"log_subsystems_documents":1,
"log_subsystems_fail2topic":1,
+1
View File
@@ -33,6 +33,7 @@
"world_subsystems_ghostroles_log":"subsystems/ghostroles.log",
"world_subsystems_law_log":"subsystems/law.log",
"world_subsystems_cargo_log":"subsystems/cargo.log",
"world_subsystems_persistence_log":"subsystems/persistence.log",
"world_subsystems_discord_log":"subsystems/discord.log",
"world_subsystems_documents_log":"subsystems/documents.log",
"world_subsystems_fail2topic_log":"subsystems/fail2topic.log",
@@ -0,0 +1,8 @@
author: FabianK3
delete-after: True
changes:
- rscadd: "Added subsystem for persistent data between rounds. The framework includes the subsystem responsible for managing persistent data and provides functionality for objects to be added to said system. See PR and GitHub Wiki documentation for more technical details."
- config: "Added new log type for the persistence subsystem."
- experiment: "Added papers and noticeboards to the persistence subsystem. Papers and their contents (title and text) will now be persistent across rounds when they get pinned to noticeboards. They will loose persistence when they either get unpinned during a round or after no changes within 30 days. Unpinning and re-pinning will keep them persistent but not update the 30 days expiration date; Unpinning, editing and re-pinning them will reset the 30 days expiration date."