Files
Aurora.3/code/game/objects/objs.dm
T
FabianK3andGitHub 36d267d83e 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)*
2025-08-16 22:00:12 +00:00

365 lines
11 KiB
Plaintext

/obj
layer = OBJ_LAYER
animate_movement = 2
var/list/matter //Used to store information about the contents of the object.
var/recyclable = FALSE //Whether the object can be recycled (eaten) by something like the Autolathe
var/w_class // Size of the object.
var/list/origin_tech = null //Used by R&D to determine what research bonuses it grants.
var/unacidable = 0 //universal "unacidabliness" var, here so you can use it in any obj.
var/obj_flags //Special flags such as whether or not this object can be rotated.
var/throwforce = 1
var/list/attack_verb //Used in attackby() to say how something was attacked "[x] has been [z.attack_verb] by [y] with [z]"
var/sharp = 0 // whether this object cuts
var/edge = FALSE // whether this object is more likely to dismember
var/in_use = 0 // If we have a user using us, this will be set on. We will check if the user has stopped using us, and thus stop updating and LAGGING EVERYTHING!
var/damtype = DAMAGE_BRUTE
var/force = 0
var/armor_penetration = 0
var/noslice = 0 // To make it not able to slice things.
var/being_shocked = 0
var/icon_species_tag = ""//If set, this holds the 3-letter shortname of a species, used for species-specific worn icons
var/icon_auto_adapt = 0//If 1, this item will automatically change its species tag to match the wearer's species.
//requires that the wearer's species is listed in icon_supported_species_tags
/**
* A list of strings used with icon_auto_adapt, a list of species which have differing appearances for this item,
* based on the specie short name
*/
var/list/icon_supported_species_tags
///If `TRUE`, will use the `icon_species_tag` var for rendering this item in the left/right hand
var/icon_species_in_hand = FALSE
var/equip_slot = 0
///Played when the item is used, for example tools
var/usesound
var/toolspeed = 1
var/surgerysound
/* START BUCKLING VARS */
var/list/can_buckle
var/buckle_movable = 0
var/buckle_dir = 0
var/buckle_lying = -1 //bed-like behavior, forces mob.lying = buckle_lying if != -1
var/buckle_require_restraints = 0 //require people to be handcuffed before being able to buckle. eg: pipes
var/atom/movable/buckled = null
/**
* Stores the original layer of a buckled atom.
*
* Set in `/obj/proc/buckle` when the atom's layer is adjusted.
*
* Used in `/unbuckle()` to restore the original layer.
*/
var/buckled_original_layer = null
var/buckle_delay = 0 //How much extra time to buckle someone to this object.
/* END BUCKLING VARS */
/* START ACCESS VARS */
var/list/req_access
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)
return ..()
/obj/Topic(href, href_list, var/datum/ui_state/state = GLOB.default_state)
if(..())
return 1
// In the far future no checks are made in an overriding Topic() beyond if(..()) return
// Instead any such checks are made in CanUseTopic()
if(CanUseTopic(usr, state, href_list) == STATUS_INTERACTIVE)
CouldUseTopic(usr)
return 0
CouldNotUseTopic(usr)
return 1
/obj/CanUseTopic(var/mob/user, var/datum/ui_state/state)
if(user.CanUseObjTopic(src))
return ..()
to_chat(user, SPAN_DANGER("[icon2html(src, user)]Access denied!"))
return STATUS_CLOSE
/mob/living/silicon/CanUseObjTopic(var/obj/O)
var/id = src.GetIdCard()
return O.check_access(id)
/mob/proc/CanUseObjTopic()
return 1
/obj/proc/CouldUseTopic(var/mob/user)
user.AddTopicPrint(src)
/mob/proc/AddTopicPrint(var/obj/target)
target.add_hiddenprint(src)
/mob/living/AddTopicPrint(var/obj/target)
if(Adjacent(target))
target.add_fingerprint(src)
else
target.add_hiddenprint(src)
/mob/living/silicon/ai/AddTopicPrint(var/obj/target)
target.add_hiddenprint(src)
/obj/proc/CouldNotUseTopic(var/mob/user)
// Nada
/obj/proc/ai_can_interact(var/mob/user)
if(!Adjacent(user) && within_jamming_range(src, FALSE)) // if not adjacent to it, it uses wireless signal
to_chat(user, SPAN_WARNING("Something in the area of \the [src] is blocking the remote signal!"))
return FALSE
return TRUE
/obj/item/proc/is_used_on(obj/O, mob/user)
/obj/assume_air(datum/gas_mixture/giver)
if(loc)
return loc.assume_air(giver)
else
return null
/obj/remove_air(amount)
if(loc)
return loc.remove_air(amount)
else
return null
/obj/return_air()
if(loc)
return loc.return_air()
/obj/proc/updateUsrDialog()
if(in_use)
var/is_in_use = 0
var/list/nearby = viewers(1, src)
for(var/mob/M in nearby)
if ((M.client && M.machine == src))
is_in_use = 1
src.attack_hand(M)
if (istype(usr, /mob/living/silicon/ai) || istype(usr, /mob/living/silicon/robot))
if (!(usr in nearby))
if (usr.client && usr.machine==src) // && M.machine == src is omitted because if we triggered this by using the dialog, it doesn't matter if our machine changed in between triggering it and this - the dialog is probably still supposed to refresh.
is_in_use = 1
src.attack_ai(usr)
in_use = is_in_use
/obj/proc/updateDialog()
// Check that people are actually using the machine. If not, don't update anymore.
if(in_use)
var/list/nearby = viewers(1, src)
var/is_in_use = 0
for(var/mob/M in nearby)
if ((M.client && M.machine == src))
is_in_use = 1
src.interact(M)
var/ai_in_use = AutoUpdateAI(src)
if(!ai_in_use && !is_in_use)
in_use = 0
/obj/attack_ghost(mob/user)
ui_interact(user)
..()
/obj/proc/interact(mob/user)
return
/mob/proc/unset_machine()
src.machine = null
/mob/proc/set_machine(var/obj/O)
if(src.machine)
unset_machine()
src.machine = O
if(istype(O))
O.in_use = 1
/obj/item/proc/updateSelfDialog()
var/mob/M = src.loc
if(istype(M) && M.client && M.machine == src)
src.attack_self(M)
/obj/proc/hide(var/hide)
set_invisibility(hide ? INVISIBILITY_MAXIMUM : initial(invisibility))
level = hide ? 1 : initial(level)
/obj/proc/hides_under_flooring()
return level == 1
/obj/proc/hear_talk(mob/M as mob, text, verb, datum/language/speaking)
if(talking_atom)
talking_atom.catchMessage(text, M)
/obj/proc/see_emote(mob/M as mob, text, var/emote_type)
return
/obj/proc/tesla_act(var/power, var/melt = FALSE)
if(melt)
visible_message(SPAN_DANGER("\The [src] melts down until ashes are left!"))
new /obj/effect/decal/cleanable/ash(loc)
qdel(src)
return
being_shocked = 1
var/power_bounced = power / 2
tesla_zap(src, 3, power_bounced)
addtimer(CALLBACK(src, PROC_REF(reset_shocked)), 10)
/obj/proc/reset_shocked()
being_shocked = 0
/obj/show_message(msg, type, alt, alt_type)//Message, type of message (1 or 2), alternative message, alt message type (1 or 2)
return
//To be called from things that spill objects on the floor.
//Makes an object move around randomly for a couple of tiles
/obj/proc/tumble(var/dist)
if (dist >= 1)
spawn()
dist += rand(0,1)
for(var/i = 1, i <= dist, i++)
if(src)
step(src, pick(NORTH,SOUTH,EAST,WEST))
sleep(rand(2,4))
/**
* Sets the `icon_species_tag` on the `/obj` based on the wearer specie, which is
* then used by the icon generator to select the correct overlay of the object
*
* * wearer - A `/mob/living/carbon/human` to adapt the object to the specie of
*
* Returns `TRUE` on successful adaptation, `FALSE` otherwise
*/
/obj/proc/auto_adapt_species(mob/living/carbon/human/wearer)
SHOULD_NOT_SLEEP(TRUE)
if(icon_auto_adapt)
icon_species_tag = ""
if(wearer && length(icon_supported_species_tags))
if(wearer.species.short_name in icon_supported_species_tags)
icon_species_tag = wearer.species.short_name
return TRUE
return FALSE
//This function should be called on an item when it is:
//Built, autolathed, protolathed, crafted or constructed. At runtime, by players or machines
//It should NOT be called on things that:
//spawn at roundstart, are adminspawned, arrive on shuttles, spawned from vendors, removed from fridges and containers, etc
//This is useful for setting special behaviour for built items that shouldn't apply to those spawned at roundstart
/obj/proc/Created()
return
/obj/proc/rotate(var/mob/user, var/anchored_ignore = FALSE)
if(use_check_and_message(user))
return
if(anchored && !anchored_ignore)
to_chat(user, SPAN_WARNING("\The [src] is bolted down to the floor!"))
return
set_dir(turn(dir, 90))
update_icon()
return TRUE
/obj/AltClick(var/mob/user)
if(obj_flags & OBJ_FLAG_ROTATABLE)
rotate(user)
return
if(obj_flags & OBJ_FLAG_ROTATABLE_ANCHORED)
rotate(user, TRUE)
return
..()
/obj/get_examine_text(mob/user, distance, is_adjacent, infix, suffix, get_extended = FALSE)
. = ..()
if((obj_flags & OBJ_FLAG_ROTATABLE) || (obj_flags & OBJ_FLAG_ROTATABLE_ANCHORED))
. += SPAN_SUBTLE("Can be rotated with alt-click.")
if(contaminated)
. += SPAN_ALIEN("\The [src] has been contaminated!")
// whether mobs can unequip and drop items into us or not
/obj/proc/can_hold_dropped_items()
return TRUE
/obj/proc/damage_flags()
. = 0
if(has_edge(src))
. |= DAMAGE_FLAG_EDGE
if(is_sharp(src))
. |= DAMAGE_FLAG_SHARP
if(damtype == DAMAGE_BURN)
. |= DAMAGE_FLAG_LASER
/obj/proc/set_pixel_offsets()
return
//wash an object
/obj/proc/clean()
clean_blood()
color = initial(color)
/obj/proc/output_spoken_message(var/message, var/message_verb = "transmits", var/display_overhead = TRUE, var/overhead_time = 2 SECONDS)
audible_message("\The <b>[src.name]</b> [message_verb], \"[message]\"")
if(display_overhead)
var/list/hearers = get_hearers_in_view(7, src)
var/list/clients_in_hearers = list()
for(var/mob/mob in hearers)
if(mob.client)
clients_in_hearers += mob.client
if(length(clients_in_hearers))
langchat_speech(message, hearers, GLOB.all_languages, skip_language_check = TRUE)
/// 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