diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm
index 4d5a639708f0..06d5c69bf2ae 100644
--- a/code/__defines/misc.dm
+++ b/code/__defines/misc.dm
@@ -87,27 +87,28 @@
#define EVENT_LEVEL_MAJOR 3
//Area flags, possibly more to come
-#define AREA_FLAG_RAD_SHIELDED BITFLAG(1) // Shielded from radiation, clearly.
-#define AREA_FLAG_EXTERNAL BITFLAG(2) // External as in exposed to space, not outside in a nice, green, forest.
-#define AREA_FLAG_ION_SHIELDED BITFLAG(3) // Shielded from ionospheric anomalies.
-#define AREA_FLAG_IS_NOT_PERSISTENT BITFLAG(4) // SSpersistence will not track values from this area.
-#define AREA_FLAG_IS_BACKGROUND BITFLAG(5) // Blueprints can create areas on top of these areas. Cannot edit the name of or delete these areas.
-#define AREA_FLAG_MAINTENANCE BITFLAG(6) // Area is a maintenance area.
-#define AREA_FLAG_SHUTTLE BITFLAG(7) // Area is a shuttle area.
-#define AREA_FLAG_HALLWAY BITFLAG(8) // Area is a public hallway suitable for event selection
-#define AREA_FLAG_PRISON BITFLAG(9) // Area is a prison for the purposes of brigging objectives.
-#define AREA_FLAG_HOLY BITFLAG(10) // Area is holy for the purposes of marking turfs as cult-resistant.
-#define AREA_FLAG_SECURITY BITFLAG(11) // Area is security for the purposes of newscaster init.
-#define AREA_FLAG_HIDE_FROM_HOLOMAP BITFLAG(12) // if we shouldn't be drawn on station holomaps
+#define AREA_FLAG_RAD_SHIELDED BITFLAG(1) // Shielded from radiation, clearly.
+#define AREA_FLAG_EXTERNAL BITFLAG(2) // External as in exposed to space, not outside in a nice, green, forest.
+#define AREA_FLAG_ION_SHIELDED BITFLAG(3) // Shielded from ionospheric anomalies.
+#define AREA_FLAG_NO_LEGACY_PERSISTENCE BITFLAG(4) // SSpersistence will not track values from this area.
+#define AREA_FLAG_IS_BACKGROUND BITFLAG(5) // Blueprints can create areas on top of these areas. Cannot edit the name of or delete these areas.
+#define AREA_FLAG_MAINTENANCE BITFLAG(6) // Area is a maintenance area.
+#define AREA_FLAG_SHUTTLE BITFLAG(7) // Area is a shuttle area.
+#define AREA_FLAG_HALLWAY BITFLAG(8) // Area is a public hallway suitable for event selection
+#define AREA_FLAG_PRISON BITFLAG(9) // Area is a prison for the purposes of brigging objectives.
+#define AREA_FLAG_HOLY BITFLAG(10) // Area is holy for the purposes of marking turfs as cult-resistant.
+#define AREA_FLAG_SECURITY BITFLAG(11) // Area is security for the purposes of newscaster init.
+#define AREA_FLAG_HIDE_FROM_HOLOMAP BITFLAG(12) // if we shouldn't be drawn on station holomaps
+#define AREA_FLAG_ALLOW_LEVEL_PERSISTENCE BITFLAG(13) // Whether or not this area should pass changed turfs to SSpersistence.
//Map template flags
-#define TEMPLATE_FLAG_ALLOW_DUPLICATES BITFLAG(0) // Lets multiple copies of the template to be spawned
-#define TEMPLATE_FLAG_SPAWN_GUARANTEED BITFLAG(1) // Makes it ignore away site budget and just spawn (only for away sites)
-#define TEMPLATE_FLAG_CLEAR_CONTENTS BITFLAG(2) // if it should destroy objects it spawns on top of
-#define TEMPLATE_FLAG_NO_RUINS BITFLAG(3) // if it should forbid ruins from spawning on top of it
-#define TEMPLATE_FLAG_NO_RADS BITFLAG(4) // Removes all radiation from the template after spawning.
-#define TEMPLATE_FLAG_TEST_DUPLICATES BITFLAG(5) // Makes unit testing attempt to spawn mutliple copies of this template. Assumes unit testing is spawning at least one copy.
-#define TEMPLATE_FLAG_GENERIC_REPEATABLE BITFLAG(6) // Template can be picked repeatedly for the same level gen run.
+#define TEMPLATE_FLAG_ALLOW_DUPLICATES BITFLAG(0) // Lets multiple copies of the template to be spawned
+#define TEMPLATE_FLAG_SPAWN_GUARANTEED BITFLAG(1) // Makes it ignore away site budget and just spawn (only for away sites)
+#define TEMPLATE_FLAG_CLEAR_CONTENTS BITFLAG(2) // if it should destroy objects it spawns on top of
+#define TEMPLATE_FLAG_NO_RUINS BITFLAG(3) // if it should forbid ruins from spawning on top of it
+#define TEMPLATE_FLAG_NO_RADS BITFLAG(4) // Removes all radiation from the template after spawning.
+#define TEMPLATE_FLAG_TEST_DUPLICATES BITFLAG(5) // Makes unit testing attempt to spawn mutliple copies of this template. Assumes unit testing is spawning at least one copy.
+#define TEMPLATE_FLAG_GENERIC_REPEATABLE BITFLAG(6) // Template can be picked repeatedly for the same level gen run.
// Convoluted setup so defines can be supplied by Bay12 main server compile script.
// Should still work fine for people jamming the icons into their repo.
diff --git a/code/__defines/persistence.dm b/code/__defines/persistence.dm
new file mode 100644
index 000000000000..bbd7ffbbf0c0
--- /dev/null
+++ b/code/__defines/persistence.dm
@@ -0,0 +1,5 @@
+// Handled elsewhere, do not let them load like vars.
+var/global/list/_forbid_field_load = list(
+ (nameof(/datum::type)) = TRUE,
+ (nameof(/atom::loc)) = TRUE
+)
diff --git a/code/__defines/serde.dm b/code/__defines/serde.dm
new file mode 100644
index 000000000000..a1e84e3fe30c
--- /dev/null
+++ b/code/__defines/serde.dm
@@ -0,0 +1,64 @@
+#define SERDE_HINT_FINISHED 1
+#define SERDE_HINT_POSTINIT 2
+
+#define SERDE_REAGENT_LIST "_reagent_list"
+#define SERDE_REAGENT_VOLUME "_reagent_volume"
+
+#define SERIALIZE_VALUE(V, T, VAL) .[nameof(T::V)] = VAL;
+#define SERIALIZE(V, T) SERIALIZE_VALUE(V, T, V)
+#define SERIALIZE_IF_MODIFIED(V, T) if(V != initial(V)) { SERIALIZE_VALUE(V, T, V) }
+#define SERIALIZE_TYPE_IF_MODIFIED(V, T) if(V != initial(V)) { SERIALIZE_VALUE(V, T, "[V]") }
+#define SERIALIZE_DECL_IF_MODIFIED(V, T) if((isnull(V) && !isnull(initial(V))) || ((istext(V) || istype(V, /decl) || ispath(V, /decl)) && !DECLS_ARE_EQUIVALENT(V, initial(V)))) { var/decl/__D = RESOLVE_TO_DECL(V); SERIALIZE_VALUE(V, T, __D?.uid) }
+#define SERIALIZE_DECL_LIST(V, T) if(islist(V)) { var/list/__decl_uids = list(); for(var/decl/__decl in V) { __decl_uids += __decl.uid }; SERIALIZE_VALUE(V, T, __decl_uids) }
+#define SERIALIZE_REAGENTS(V, T, I) if(istype(V, /datum/reagents)) { \
+ .[I + SERDE_REAGENT_VOLUME] = UNLINT(V.maximum_volume); \
+ if(UNLINT(V.total_volume)) { \
+ var/list/__compiled_reagents = list(); \
+ for(var/decl/material/R in UNLINT(V.liquid_volumes)) { \
+ __compiled_reagents[++__compiled_reagents.len] = list(R.uid, UNLINT(V.liquid_volumes[R]), (MAT_PHASE_LIQUID)); \
+ } \
+ for(var/decl/material/R in UNLINT(V.solid_volumes)) { \
+ __compiled_reagents[++__compiled_reagents.len] = list(R.uid, UNLINT(V.solid_volumes[R]), (MAT_PHASE_SOLID)); \
+ } \
+ .[I + SERDE_REAGENT_LIST] = __compiled_reagents; \
+ } else { \
+ .[I + SERDE_REAGENT_LIST] = list(); \
+ } \
+} else { \
+ .[I + SERDE_REAGENT_LIST] = list(); \
+ .[I + SERDE_REAGENT_VOLUME] = 0; \
+}
+
+#define DESERIALIZE_REAGENTS(V, I) if(((I + SERDE_REAGENT_LIST) in __deserialization_payload) && ((I + SERDE_REAGENT_VOLUME) in __deserialization_payload)) { \
+ V = list((SERDE_REAGENT_VOLUME) = __deserialization_payload[I + SERDE_REAGENT_VOLUME], (SERDE_REAGENT_LIST) = __deserialization_payload[I + SERDE_REAGENT_LIST]); \
+}
+
+#define DESERIALIZE_DECL_TO_TYPE(V) if(istext(V) || ispath(V, /decl) || istype(V, /decl)) { var/decl/__D = RESOLVE_TO_DECL(V); V = __D?.type; } else { V = null; }
+#define DESERIALIZE_TYPE(V) if(istext(V)) { V = text2path(V); } else if(!ispath(V)) { V = null; }
+#define DESERIALIZE_DECL(V) if(istext(V) || ispath(V)) { V = RESOLVE_TO_DECL(V); } else { V = null; }
+
+// List cast is to avoid OpenDream complaining about V typically being typed as a reagents datum, but holding a list for serde.
+#define FINALIZE_REAGENTS_SERDE_BODY(V) try { \
+ if((SERDE_REAGENT_LIST in V) && (SERDE_REAGENT_VOLUME in V)) { \
+ var/list/LV = V; \
+ var/__serde_volume = LV[SERDE_REAGENT_VOLUME]; \
+ if(__serde_volume <= 0) { \
+ V = null; \
+ } else { \
+ var/list/__serde_reagents = LV[SERDE_REAGENT_LIST]; \
+ V = new /datum/reagents(__serde_volume, src); \
+ for(var/list/entry in __serde_reagents) { \
+ V.add_reagent(RESOLVE_TO_DECL(entry[1]), entry[2], phase = entry[3], defer_update = TRUE); \
+ } \
+ V.handle_update(); \
+ } \
+ } else { \
+ V = null; \
+ } \
+} catch(var/exception/E) { \
+ log_error("Exception while finalizing reagents load for [type]: [EXCEPTION_TEXT(E)]"); \
+ V = null; \
+}
+
+#define FINALIZE_REAGENTS_SERDE(V) if(islist(V)) { FINALIZE_REAGENTS_SERDE_BODY(V); }
+#define FINALIZE_REAGENTS_SERDE_AND_RETURN(V) if(islist(V)) { FINALIZE_REAGENTS_SERDE_BODY(V); return; }
diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm
index 127f3b2d1149..87f4b099d0f5 100644
--- a/code/__defines/subsystems.dm
+++ b/code/__defines/subsystems.dm
@@ -19,12 +19,13 @@
// Subsystems shutdown in the reverse of the order they initialize in
// The numbers just define the ordering, they are meaningless otherwise.
-#define SS_INIT_INPUT 22
-#define SS_INIT_EARLY 21
-#define SS_INIT_WEBHOOKS 20
-#define SS_INIT_MODPACKS 19
-#define SS_INIT_SECRETS 18
-#define SS_INIT_GARBAGE 17
+#define SS_INIT_INPUT 23
+#define SS_INIT_EARLY 22
+#define SS_INIT_WEBHOOKS 21
+#define SS_INIT_MODPACKS 20
+#define SS_INIT_SECRETS 19
+#define SS_INIT_GARBAGE 18
+#define SS_INIT_SERDE 17
#define SS_INIT_MATERIALS 16
#define SS_INIT_PLANTS 15
#define SS_INIT_LORE 14
diff --git a/code/_helpers/serde.dm b/code/_helpers/serde.dm
new file mode 100644
index 000000000000..9d50f3fae347
--- /dev/null
+++ b/code/_helpers/serde.dm
@@ -0,0 +1,131 @@
+/proc/instantiate_serialized_data(load_z, requestor, list/instance_map, entries_decay_at, entry_decay_weight)
+
+ var/list/nested_instances = list()
+ var/list/instanced_areas = list()
+ var/list/created_data = list()
+
+ LAZYINITLIST(instance_map)
+
+ to_world_log("Finalising load of [length(instance_map)] instance\s for level '[requestor]'.")
+ for(var/uid in instance_map)
+
+ var/list/instance_data = instance_map[uid]
+ try
+
+ var/raw_load_path = instance_data[nameof(/datum::type)]
+ var/load_path = ispath(raw_load_path, /datum) ? raw_load_path : text2path(raw_load_path)
+ if(!ispath(load_path, /datum))
+ error("[requestor]: attempted to load persistent instance with invalid or non-/datum type '[raw_load_path]'")
+ continue
+
+ var/datum/created_instance
+
+ // Instance is a /datum.
+ // Just pass the data in and assume the datum type knows what to do with it.
+ if(!ispath(load_path, /atom) && ispath(load_path, /datum))
+ created_instance = new load_path(instance_data)
+ created_data += created_instance
+ else
+ var/list/spawn_data = instance_data[nameof(/atom/movable::loc)]
+ if(spawn_data)
+
+ if(isnull(spawn_data) || length(spawn_data) < 3)
+ error("[requestor]: attempted to load persistent instance with malformed loc.")
+ continue
+
+ // Instance has a world coordinate.
+ if(islist(spawn_data))
+ var/turf/spawn_loc = locate(spawn_data[1], spawn_data[2], isnull(load_z) ? spawn_data[3] : load_z)
+ if(!istype(spawn_loc))
+ error("[requestor]: attempted to load persistent instance but could not find spawn loc.")
+ continue
+ if(ispath(load_path, /turf))
+ if(spawn_loc.type == load_path)
+ created_instance = spawn_loc
+ else
+ created_instance = spawn_loc.ChangeTurf(load_path)
+
+ // TODO: Areas will need bespoke handling for non-subtype-related persistence (blueprint renaming etc).
+ else if(ispath(load_path, /area))
+ var/area/area = instanced_areas[load_path]
+ if(!area)
+ area = new load_path(null)
+ instanced_areas[load_path] = area
+ ChangeArea(spawn_loc, area)
+
+ else if(ispath(load_path, /atom))
+ created_instance = new load_path(spawn_loc)
+ spawn_loc._contents_were_modified = TRUE // ensure
+ else
+ error("[requestor]: attempted to instantiate unimplemented path '[load_path]'.")
+ continue
+
+ // Instance is inside another instance; implies/requires /atom/movable
+ else if(istext(spawn_data))
+ if(!ispath(load_path, /atom/movable))
+ error("[requestor]: tried to spawn non-movable [load_path] inside an instance.")
+ continue
+ created_instance = new load_path
+ nested_instances[created_instance] = spawn_data
+
+ else
+ error("[requestor]: attempted to load persistent instance with malformed loc.")
+ continue
+
+ else
+ // Should we just go ahead and do this to create atoms in nullspace?
+ // Would we ever want to track an atom in nullspace via level persistence?
+ error("[requestor]: attempted to load non-/datum persistent instance with no spawn loc.")
+
+ if(istype(created_instance))
+ LAZYSET(., uid, created_instance)
+ if(isatom(created_instance))
+ var/atom/atom = created_instance
+ atom.__deserialization_payload = instance_data
+ SSatoms.deserialized_atoms[uid] = atom
+ if(!isnull(entries_decay_at) && !isnull(entry_decay_weight))
+ created_instance.HandlePersistentDecay(entries_decay_at, entry_decay_weight)
+
+ catch(var/exception/E)
+ log_error("Exception during persistent instance load - [islist(instance_data) ? json_encode(instance_data) : "no instance data"]: [EXCEPTION_TEXT(E)]")
+
+ // Atoms use SSatoms for this, datums don't go through SSatoms so need to do it here.
+ for(var/datum/instance in created_data)
+ instance.DeserializePostInit(.)
+
+ // Resolve any loc references to instances.
+ for(var/atom/movable/atom as anything in nested_instances)
+ var/nested_atom_id = nested_instances[atom]
+ var/atom/nested_atom = .[nested_atom_id]
+ if(!istype(nested_atom))
+ error("[requestor]: could not resolve instance ref [nested_atom_id] to instance.")
+ continue
+ atom.forceMove(nested_atom)
+ nested_atom.contents_were_modified()
+
+ // Now that everything is loaded and placed, clear out anything that should not be present on the turfs we've loaded.
+ for(var/uid in SSatoms.deserialized_atoms)
+ var/turf/turf = SSatoms.deserialized_atoms[uid]
+ if(!istype(turf))
+ continue
+ for(var/atom/thing in turf)
+ if(!thing.simulated)
+ continue
+ if(!isnull(thing.__deserialization_payload))
+ continue
+ qdel(thing)
+
+ to_world_log("[requestor] loaded [length(.)] persistent instance\s.")
+
+/proc/apply_serde_message_decay(_message, _age, _decay_weight, _decay_at)
+ var/static/list/decayed_chars = list(".",",","-","'","\\","/","\"",":",";")
+ if(_age < _decay_at || isnull(_message))
+ return _message
+ . = ""
+ for(var/i = 1 to length(_message))
+ var/char = copytext(_message, i, i + 1)
+ if(prob(round(_age * _decay_weight)))
+ if(prob(99))
+ . += pick(decayed_chars)
+ else
+ . += char
diff --git a/code/controllers/subsystems/atoms.dm b/code/controllers/subsystems/atoms.dm
index baf065bd0fd3..68e2940bcb41 100644
--- a/code/controllers/subsystems/atoms.dm
+++ b/code/controllers/subsystems/atoms.dm
@@ -11,6 +11,8 @@ SUBSYSTEM_DEF(atoms)
var/atom_init_stage = INITIALIZATION_INSSATOMS
var/old_init_stage
+ /// An associative list of UIDs to atoms that were deserialized prior to flush.
+ var/list/deserialized_atoms = list()
/// A non-associative list of lists, with the format list(list(atom, list(Initialize arguments))).
var/list/created_atoms = list()
/// A non-associative list of lists, with the format list(list(atom, list(LateInitialize arguments))).
@@ -29,9 +31,20 @@ SUBSYSTEM_DEF(atoms)
atom_init_stage = INITIALIZATION_INNEW_MAPLOAD
- var/list/mapload_arg = list(TRUE)
-
+ // Preload any atoms that have deserialized during the initial load process prior to flush.
var/index = 1
+ var/list/postinit_serde_atoms = list()
+ if(length(deserialized_atoms))
+ while(index <= length(deserialized_atoms))
+ var/uid = deserialized_atoms[index++]
+ var/atom/instance = deserialized_atoms[uid]
+ if(instance.Preload(deserialized_atoms) == SERDE_HINT_POSTINIT)
+ postinit_serde_atoms += instance
+ CHECK_TICK
+ report_progress("Deserialized [index-1] atom\s.")
+ index = 1
+
+ var/list/mapload_arg = list(TRUE)
// Things can add to the end of this list while we iterate, so we can't use a for loop.
while(index <= length(created_atoms))
// Don't remove from this list while we run, that's expensive.
@@ -49,10 +62,10 @@ SUBSYSTEM_DEF(atoms)
else
InitAtom(A, mapload_arg)
CHECK_TICK
-
- report_progress("Initialized [index] atom\s")
created_atoms.Cut()
+ report_progress("Initialized [index-1] atom\s.")
+
atom_init_stage = INITIALIZATION_INNEW_REGULAR
if(length(late_loaders))
@@ -65,6 +78,25 @@ SUBSYSTEM_DEF(atoms)
report_progress("Late initialized [index] atom\s")
late_loaders.Cut()
+ if(length(postinit_serde_atoms))
+ index = 1
+ while(index <= length(postinit_serde_atoms))
+ var/atom/instance = postinit_serde_atoms[index++]
+ instance.DeserializePostInit(deserialized_atoms)
+ CHECK_TICK
+ postinit_serde_atoms.Cut()
+
+ // Clear out the serde payloads now that everything should be tidied away.
+ if(length(deserialized_atoms))
+ index = 1
+ while(index <= length(deserialized_atoms))
+ var/uid = deserialized_atoms[index++]
+ var/atom/instance = deserialized_atoms[uid]
+ if(istype(instance))
+ instance.__deserialization_payload = null
+ CHECK_TICK
+ deserialized_atoms.Cut()
+
/datum/controller/subsystem/atoms/proc/InitAtom(atom/A, list/arguments)
var/the_type = A.type
if(QDELING(A))
diff --git a/code/controllers/subsystems/initialization/persistence.dm b/code/controllers/subsystems/initialization/persistence.dm
deleted file mode 100644
index c1119f8ca859..000000000000
--- a/code/controllers/subsystems/initialization/persistence.dm
+++ /dev/null
@@ -1,78 +0,0 @@
-SUBSYSTEM_DEF(persistence)
- name = "Persistence"
- init_order = SS_INIT_MISC_LATE
- flags = SS_NO_FIRE | SS_NEEDS_SHUTDOWN
-
- var/elevator_fall_path = "data/elevator_falls_tracking.txt"
- var/elevator_fall_shifts = -1 // This is snowflake, but oh well.
- var/list/tracking_values = list()
-
-/datum/controller/subsystem/persistence/Initialize()
- . = ..()
-
- decls_repository.get_decls_of_subtype(/decl/persistence_handler) // Initialize()s persistence categories.
-
- // Begin snowflake.
- var/elevator_file = safe_file2text(elevator_fall_path, FALSE)
- if(elevator_file)
- elevator_fall_shifts = text2num(elevator_file)
- else
- elevator_fall_shifts = initial(elevator_fall_shifts)
- if(isnull(elevator_fall_shifts))
- elevator_fall_shifts = initial(elevator_fall_shifts)
- elevator_fall_shifts++
- // End snowflake.
-
-/datum/controller/subsystem/persistence/Shutdown()
- var/list/all_persistence_datums = decls_repository.get_decls_of_subtype(/decl/persistence_handler)
- for(var/thing in all_persistence_datums)
- var/decl/persistence_handler/P = all_persistence_datums[thing]
- P.Shutdown()
-
- // Refer to snowflake above.
- if(fexists(elevator_fall_path))
- fdel(elevator_fall_path)
- text2file("[elevator_fall_shifts]", elevator_fall_path)
-
-/datum/controller/subsystem/persistence/proc/track_value(var/atom/value, var/track_type)
-
- var/turf/T = get_turf(value)
- if(!T)
- return
-
- var/area/A = get_area(T)
- if(!A || (A.area_flags & AREA_FLAG_IS_NOT_PERSISTENT))
- return
-
- var/datum/level_data/level = SSmapping.levels_by_z[T.z]
- if(!istype(level) || !level.permit_persistence)
- return
-
- if(!tracking_values[track_type])
- tracking_values[track_type] = list()
- tracking_values[track_type] |= value
-
-/datum/controller/subsystem/persistence/proc/is_tracking(var/atom/value, var/track_type)
- . = (value in tracking_values[track_type])
-
-/datum/controller/subsystem/persistence/proc/forget_value(var/atom/value, var/track_type)
- if(tracking_values[track_type])
- tracking_values[track_type] -= value
-
-/datum/controller/subsystem/persistence/proc/show_info(var/mob/user)
-
- if(!check_rights(R_INVESTIGATE, C = user))
- return
-
- var/list/dat = list("
")
- var/can_modify = check_rights(R_ADMIN, 0, user)
- var/list/all_persistence_datums = decls_repository.get_decls_of_subtype(/decl/persistence_handler)
- for(var/thing in all_persistence_datums)
- var/decl/persistence_handler/P = all_persistence_datums[thing]
- if(P.has_admin_data)
- dat += P.GetAdminSummary(user, can_modify)
- dat += "
"
-
- var/datum/browser/popup = new(user, "admin_persistence", "Persistence Data")
- popup.set_content(jointext(dat, null))
- popup.open()
diff --git a/code/controllers/subsystems/mapping.dm b/code/controllers/subsystems/mapping.dm
index bcf8a6922368..a03221cd513b 100644
--- a/code/controllers/subsystems/mapping.dm
+++ b/code/controllers/subsystems/mapping.dm
@@ -149,10 +149,31 @@ SUBSYSTEM_DEF(mapping)
setup_data_for_levels(min_z = old_maxz + 1)
+ // Now that levels are in place, preload any associated persistent data.
+ // This is to avoid dependencies on other atoms or any other weird ordering
+ // problems like we used to get with old DMMS and SSatoms.
+ var/list/preloaded_levels = list()
+ for(var/datum/level_data/level in levels_by_z)
+ if(level.preload_persistent_data())
+ preloaded_levels += level
+
+ // Now actually load the serde data into the map.
+ for(var/datum/level_data/level as anything in preloaded_levels)
+ level.load_persistent_data()
+
+ // Clear our reference data for GC
+ // This might not be needed but it saves refs floating around I guess.
+ for(var/key in level_persistence_ref_map)
+ var/list/stale_data = global.level_persistence_ref_map[key]
+ stale_data.Cut()
+
+ global.level_persistence_ref_map.Cut()
+
// Generate turbolifts last, since away sites may have elevators to generate too.
for(var/obj/abstract/turbolift_spawner/turbolift as anything in turbolifts_to_initialize)
turbolift.build_turbolift()
+ // With levels set up and serde complete (and levels flagged) we can do any remaining level generation.
global.using_map.finalize_map_generation()
. = ..()
diff --git a/code/controllers/subsystems/persistence.dm b/code/controllers/subsystems/persistence.dm
new file mode 100644
index 000000000000..28f36ae855f0
--- /dev/null
+++ b/code/controllers/subsystems/persistence.dm
@@ -0,0 +1,146 @@
+/datum/admins/proc/force_persistence_save_verb()
+ set name = "Force Early Level Save"
+ set category = "Admin"
+ set desc = "Forces an early level save run by SSpersistence."
+ if(!SSpersistence)
+ return
+ if(UNLINT(SSpersistence._persistent_save_running))
+ to_chat(usr, SPAN_WARNING("There is already a level save running. Please wait for it to finish."))
+ return
+ log_admin("[key_name(usr)] has started an early level save.")
+ message_admins("[key_name(usr)] has started an early level save.")
+ SSpersistence.start_persistent_level_save()
+
+SUBSYSTEM_DEF(persistence)
+ name = "Persistence"
+ init_order = SS_INIT_SERDE
+ flags = SS_NEEDS_SHUTDOWN
+ wait = 60 MINUTES
+
+ VAR_PRIVATE/const/ELEVATOR_FALL_PATH = "data/elevator_falls_tracking.txt"
+ var/elevator_fall_shifts = -1 // This is snowflake, but oh well.
+ var/list/tracking_values = list()
+ VAR_PRIVATE/_persistent_save_running = FALSE
+ var/const/save_warning_period = 30 SECONDS // How long to warn about an upcoming world save so people can get to safety, etc
+ var/initial_save_skip_period // Set in Initialize()
+ var/showing_warning = FALSE
+
+/datum/controller/subsystem/persistence/Initialize()
+ . = ..()
+ initial_save_skip_period = max(0, (wait - 10 MINUTES)) // Skip initial fire(), typically there's no need to save immediately after roundstart
+ decls_repository.get_decls_of_subtype(/decl/persistence_handler) // Initialize()s persistence categories.
+
+ // Begin snowflake.
+ var/elevator_file = safe_file2text(ELEVATOR_FALL_PATH, FALSE)
+ if(elevator_file)
+ elevator_fall_shifts = text2num(elevator_file)
+ else
+ elevator_fall_shifts = initial(elevator_fall_shifts)
+ if(isnull(elevator_fall_shifts))
+ elevator_fall_shifts = initial(elevator_fall_shifts)
+ elevator_fall_shifts++
+ // End snowflake.
+
+/datum/controller/subsystem/persistence/Shutdown()
+ var/list/all_persistence_datums = decls_repository.get_decls_of_subtype(/decl/persistence_handler)
+ for(var/thing in all_persistence_datums)
+ var/decl/persistence_handler/P = all_persistence_datums[thing]
+ P.Shutdown()
+
+ // Refer to snowflake above.
+ if(fexists(ELEVATOR_FALL_PATH))
+ fdel(ELEVATOR_FALL_PATH)
+ text2file("[elevator_fall_shifts]", ELEVATOR_FALL_PATH)
+
+ // Handle level data shutdown.
+ start_persistent_level_save()
+ while(_persistent_save_running)
+ sleep(1)
+
+/datum/controller/subsystem/persistence/fire(resumed)
+ if(world.time <= initial_save_skip_period)
+ return
+ do_save_with_warning()
+
+/datum/controller/subsystem/persistence/proc/do_save_with_warning()
+ set waitfor = FALSE
+ if(showing_warning)
+ return // debounce
+ showing_warning = TRUE
+ if(save_warning_period > 0)
+ var/remaining_delay = save_warning_period
+ while(remaining_delay > 10 SECONDS)
+ to_world(SPAN_DANGER("World save will begin in [round(remaining_delay/10)] second\s! Prepare for a server freeze!"))
+ remaining_delay -= 10 SECONDS
+ sleep(10 SECONDS)
+ if(remaining_delay > 0)
+ to_world(SPAN_DANGER("World save will begin in [round(remaining_delay/10)] second\s! Prepare for a server freeze!"))
+ sleep(remaining_delay)
+
+ to_world(SPAN_DANGER("Starting world save!"))
+ sleep(1 SECOND)
+ showing_warning = FALSE
+ start_persistent_level_save()
+ to_world(SPAN_DANGER("Saved the world! Thank you for your patience, please go about your business."))
+
+/datum/controller/subsystem/persistence/proc/start_persistent_level_save()
+ if(_persistent_save_running)
+ return // debounce
+ _persistent_save_running = TRUE // used to avoid shutting down mid-write
+
+ var/started_run = REALTIMEOFDAY
+ report_progress("Starting persistent level save.")
+ // TODO: suspend all subsystems while the save is running
+ // TODO: prevent player input somehow?
+ try
+ for(var/z = 1 to length(SSmapping.levels_by_z))
+ var/datum/level_data/level = SSmapping.levels_by_z[z]
+ level.save_persistent_data()
+ catch(var/exception/E)
+ error("Exception when running persistent level save: [EXCEPTION_TEXT(E)]")
+ // TODO: re-enable all subsystems
+ report_progress("Persistent level save finished in [(REALTIMEOFDAY-started_run)/10] second\s.")
+ _persistent_save_running = FALSE
+
+/datum/controller/subsystem/persistence/proc/track_value(var/atom/value, var/track_type)
+
+ var/turf/T = get_turf(value)
+ if(!T)
+ return
+
+ var/area/A = get_area(T)
+ if(!A || (A.area_flags & AREA_FLAG_NO_LEGACY_PERSISTENCE))
+ return
+
+ var/datum/level_data/level = SSmapping.levels_by_z[T.z]
+ if(!istype(level) || !level.permit_legacy_persistence)
+ return
+
+ if(!tracking_values[track_type])
+ tracking_values[track_type] = list()
+ tracking_values[track_type] |= value
+
+/datum/controller/subsystem/persistence/proc/is_tracking(var/atom/value, var/track_type)
+ . = (value in tracking_values[track_type])
+
+/datum/controller/subsystem/persistence/proc/forget_value(var/atom/value, var/track_type)
+ if(tracking_values[track_type])
+ tracking_values[track_type] -= value
+
+/datum/controller/subsystem/persistence/proc/show_info(var/mob/user)
+
+ if(!check_rights(R_INVESTIGATE, C = user))
+ return
+
+ var/list/dat = list("")
+ var/can_modify = check_rights(R_ADMIN, 0, user)
+ var/list/all_persistence_datums = decls_repository.get_decls_of_subtype(/decl/persistence_handler)
+ for(var/thing in all_persistence_datums)
+ var/decl/persistence_handler/P = all_persistence_datums[thing]
+ if(P.has_admin_data)
+ dat += P.GetAdminSummary(user, can_modify)
+ dat += "
"
+
+ var/datum/browser/popup = new(user, "admin_persistence", "Persistence Data")
+ popup.set_content(jointext(dat, null))
+ popup.open()
diff --git a/code/datums/datum.dm b/code/datums/datum.dm
index bbc6bd7a5f2c..30e00f5a6573 100644
--- a/code/datums/datum.dm
+++ b/code/datums/datum.dm
@@ -9,6 +9,8 @@
var/list/active_timers
/// Used to avoid unnecessary refstring creation in Destroy().
var/tmp/has_state_machine = FALSE
+ /// Var for holding a unique-to-this-run identifier for a serialized datum.
+ VAR_PRIVATE/tmp/__run_uid
#ifdef REFTRACKING_ENABLED
var/tmp/running_find_references
diff --git a/code/datums/datum_serde.dm b/code/datums/datum_serde.dm
new file mode 100644
index 000000000000..cafee1161d64
--- /dev/null
+++ b/code/datums/datum_serde.dm
@@ -0,0 +1,36 @@
+// Used for saving instances via the level persistence system.
+// Returns an assoc list of var name to var value.
+// Expected format is:
+// list("field" = "value", "so on" = "so forth"))
+// Using a var name (via nameof() or manually) will automatically load the var to the field in Deserialize.
+// If serializing an instance reference, use get_run_uid() to get a UID.
+/datum/proc/Serialize()
+ SHOULD_CALL_PARENT(TRUE)
+ . = list((nameof(/datum::type)) = GetSerializedType())
+
+/datum/proc/GetSerializedType()
+ return type
+
+/datum/proc/GetPossiblySerializableInstances()
+ return list(src)
+
+// A proc for checking preconditions on an instance to determine if it should bother serializing at all.
+/datum/proc/ShouldSerialize(_age)
+ SHOULD_CALL_PARENT(TRUE)
+ return TRUE
+
+// Returns a UID for this instance, used for serde across rounds.
+// Probably-kind-of a GUID but only for this run.
+/datum/proc/get_run_uid()
+ if(isnull(__run_uid))
+ __run_uid = "\ref[src]-[sequential_id(type)]" // Staple seq_id on there in case of \ref reuse.
+ return __run_uid
+
+// Called after Initialize()/LateInitialize() on all non-atom datums, and if an atom returns SERDE_HINT_POSTINIT to Deserialize().
+/datum/proc/DeserializePostInit(list/instance_map)
+ return
+
+// Apply cross-round degradation (graffiti decaying, etc) prior to Deserialize() and Initialize()
+// Typically this means modifying __deserialization_payload
+/datum/proc/HandlePersistentDecay(entries_decay_at, entry_decay_weight)
+ return
diff --git a/code/game/area/area_space.dm b/code/game/area/area_space.dm
index 5c2f8c444715..b8cfc6e116d5 100644
--- a/code/game/area/area_space.dm
+++ b/code/game/area/area_space.dm
@@ -10,7 +10,7 @@ var/global/area/space_area
power_equip = 0
power_environ = 0
has_gravity = 0
- area_flags = AREA_FLAG_EXTERNAL | AREA_FLAG_IS_NOT_PERSISTENT | AREA_FLAG_IS_BACKGROUND | AREA_FLAG_HIDE_FROM_HOLOMAP
+ area_flags = AREA_FLAG_EXTERNAL | AREA_FLAG_NO_LEGACY_PERSISTENCE | AREA_FLAG_IS_BACKGROUND | AREA_FLAG_HIDE_FROM_HOLOMAP
ambience = list('sound/ambience/ambispace1.ogg','sound/ambience/ambispace2.ogg','sound/ambience/ambispace3.ogg','sound/ambience/ambispace4.ogg','sound/ambience/ambispace5.ogg')
is_outside = OUTSIDE_YES
diff --git a/code/game/area/areas_serde.dm b/code/game/area/areas_serde.dm
new file mode 100644
index 000000000000..d2b8270006c5
--- /dev/null
+++ b/code/game/area/areas_serde.dm
@@ -0,0 +1,3 @@
+/area/ShouldSerialize(_age)
+ SHOULD_CALL_PARENT(FALSE)
+ return FALSE // This is specifically if this area instance should serialize, not if the contents should.
diff --git a/code/game/atoms_movable_serde.dm b/code/game/atoms_movable_serde.dm
new file mode 100644
index 000000000000..e47ee52da8fd
--- /dev/null
+++ b/code/game/atoms_movable_serde.dm
@@ -0,0 +1,13 @@
+/atom/movable/Serialize()
+ . = ..()
+ if(isturf(loc))
+ SERIALIZE_VALUE(loc, /atom/movable, list(loc.x, loc.y, loc.z))
+ // The below does not handle cases where the nested instance is not itself persistent.
+ // In this case, if the instance tried to serialize while inside a non-persistent instance, it would
+ // throw a runtime on subsequent loads due to having a UID as a loc that does not map to a loaded instance.
+ else if(isatom(loc))
+ SERIALIZE_VALUE(loc, /atom/movable, loc.get_run_uid())
+
+/atom/movable/Deserialize(list/instance_map)
+ . = ..()
+ contents_were_modified()
diff --git a/code/game/atoms_serde.dm b/code/game/atoms_serde.dm
new file mode 100644
index 000000000000..e8212228384e
--- /dev/null
+++ b/code/game/atoms_serde.dm
@@ -0,0 +1,81 @@
+/atom
+ /// Var for holding serde information when this atom was loaded from a persistent source.
+ var/__deserialization_payload
+
+/atom/Serialize()
+ . = ..()
+ if(current_health != get_max_health())
+ SERIALIZE(current_health, /atom)
+ SERIALIZE_IF_MODIFIED(max_health, /atom)
+ SERIALIZE_IF_MODIFIED(dir, /atom)
+ if(ATOM_IS_TEMPERATURE_SENSITIVE(src))
+ SERIALIZE_IF_MODIFIED(temperature, /atom)
+ if(istype(reagents))
+ SERIALIZE_REAGENTS(reagents, /atom, "atom")
+ SERIALIZE_DECL_IF_MODIFIED(material, /atom)
+ SERIALIZE_DECL_IF_MODIFIED(reinf_material, /atom)
+ SERIALIZE_IF_MODIFIED(paint_color, /atom)
+ SERIALIZE_IF_MODIFIED(pixel_x, /atom)
+ SERIALIZE_IF_MODIFIED(pixel_y, /atom)
+ SERIALIZE_IF_MODIFIED(default_pixel_x, /atom)
+ SERIALIZE_IF_MODIFIED(default_pixel_y, /atom)
+
+// Keeping this in code for reference, but a large number of atoms generate
+// name and desc at runtime, so not storing this in serde by default.
+/*
+ SERIALIZE_IF_MODIFIED(name, /atom)
+ SERIALIZE_IF_MODIFIED(desc, /atom)
+*/
+ // TODO: serialize forensics
+
+/atom/proc/Deserialize(list/instance_map)
+ SHOULD_CALL_PARENT(TRUE)
+ SHOULD_NOT_SLEEP(TRUE)
+ for(var/data_key in __deserialization_payload)
+ if(data_key in vars)
+ try
+ if(!global._forbid_field_load[data_key] && (data_key in vars))
+ vars[data_key] = __deserialization_payload[data_key]
+ else
+ PreloadKey(data_key, __deserialization_payload[data_key])
+ catch(var/exception/E)
+ error("Failed to write [data_key] to [type] vars: [E]")
+ DESERIALIZE_REAGENTS(reagents, "atom") // Handled in initialize_reagents()
+ DESERIALIZE_DECL_TO_TYPE(material)
+ DESERIALIZE_DECL_TO_TYPE(reinf_material)
+ return SERDE_HINT_FINISHED
+
+/atom/ShouldSerialize(_age)
+ return ..() && simulated
+
+/atom/GetPossiblySerializableInstances()
+ . = ..()
+ var/list/contained = get_contained_external_atoms()
+ if(length(contained))
+ . |= contained
+
+/atom/Exited(atom/movable/atom, atom/newloc)
+ . = ..()
+ if(simulated && atom.ShouldSerialize())
+ contents_were_modified()
+
+/atom/Entered(atom/movable/atom, atom/old_loc)
+ . = ..()
+ if(simulated && atom.ShouldSerialize())
+ contents_were_modified()
+
+// Called when an instance is being preloaded with information from deserialization.
+/atom/proc/Preload(list/instance_map)
+ SHOULD_CALL_PARENT(TRUE)
+ SHOULD_NOT_SLEEP(TRUE)
+ var/turf/turf = get_turf(src)
+ if(__deserialization_payload)
+ try
+ . = Deserialize(instance_map)
+ catch(var/exception/E)
+ PRINT_STACK_TRACE("Exception when deserializing [type] at ([turf?.x || "NULL"],[turf?.y || "NULL"],[turf?.z || "NULL"]): [E]")
+ else
+ PRINT_STACK_TRACE("[type] at ([turf?.x || "NULL"],[turf?.y || "NULL"],[turf?.z || "NULL"]) tried to preload with no deserialization payload.")
+
+/atom/proc/PreloadKey(data_key, payload)
+ return
diff --git a/code/game/objects/__objs.dm b/code/game/objects/__objs.dm
index 123678e0cee3..feebd9293847 100644
--- a/code/game/objects/__objs.dm
+++ b/code/game/objects/__objs.dm
@@ -27,17 +27,20 @@
var/directional_offset
/obj/Initialize(mapload)
- //Health should be set to max_health only if it's null.
. = ..()
create_matter()
//Only apply directional offsets if the mappers haven't set any offsets already
if(!pixel_x && !pixel_y && !pixel_w && !pixel_z)
update_directional_offset()
+
+ //Health should be set to max_health only if it's null.
+ var/_max_health = get_max_health()
if(isnull(current_health) || current_health == INFINITY)
- current_health = get_max_health()
- else
- current_health = min(current_health, get_max_health())
- if(!isnull(chem_volume) && chem_volume >= 0) // 0-volume holders perserved for legacy code reasons. Ideally shouldn't exist if <= 0
+ current_health = _max_health
+ current_health = min(current_health, _max_health)
+
+ // Initialize our reagents if they've been preloaded or we have a chem_volume
+ if((!isnull(chem_volume) && chem_volume >= 0) || islist(reagents))
initialize_reagents()
/obj/object_shaken()
@@ -264,15 +267,21 @@
return TRUE
/**
- * Init starting reagents and/or reagent var. Called if chem_volume > 0 in /obj/Initialize()
- * populate: If set to true, we expect map load/admin spawned reagents to be set.
+ * Init starting reagents and/or reagent var. Called in /obj/Initialize() if volume is above 0.
+ * Skips populate_initialize() if reagents is null, or if it is a list, ie. we are pending deserialization.
*/
-/obj/proc/initialize_reagents(var/populate = TRUE)
+/obj/proc/initialize_reagents()
SHOULD_CALL_PARENT(TRUE)
- if(REAGENT_TOTAL_VOLUME(reagents) > 0)
+ // Check if this is getting called twice, or we created reagents somewhere in Initialize() (bad juju)
+ if(istype(reagents))
log_warning("\The [src] possibly is initializing its reagents more than once!")
- create_or_update_reagents(chem_volume)
- if(populate)
+ // If preloaded from serde, handle expected list structure.
+ // Returns if preload is successful to skip populate_reagents() call.
+ FINALIZE_REAGENTS_SERDE_AND_RETURN(reagents)
+ // Standard non-serde reagent init behavior after this point.
+ if(chem_volume > 0)
+ create_or_update_reagents(chem_volume)
+ if(istype(reagents))
populate_reagents()
/**
diff --git a/code/game/objects/effects/_effect.dm b/code/game/objects/effects/_effect.dm
index d7b2823e0d7d..c90056be1c7f 100644
--- a/code/game/objects/effects/_effect.dm
+++ b/code/game/objects/effects/_effect.dm
@@ -1,8 +1,13 @@
/obj/effect
abstract_type = /obj/effect
+/obj/effect/ShouldSerialize(_age)
+ SHOULD_CALL_PARENT(FALSE)
+ return FALSE // Typically no. Specific subtypes should reimplement this (vomit etc)
+
/obj/effect/can_be_grabbed(var/mob/grabber, var/target_zone)
return FALSE
/obj/effect/try_make_grab(mob/living/user, defer_hand = FALSE)
return FALSE
+
diff --git a/code/game/objects/effects/decals/Cleanable/humans.dm b/code/game/objects/effects/decals/Cleanable/humans.dm
index 421c415cb24f..48a282d2fdde 100644
--- a/code/game/objects/effects/decals/Cleanable/humans.dm
+++ b/code/game/objects/effects/decals/Cleanable/humans.dm
@@ -12,13 +12,13 @@
random_icon_states = list("mfloor1", "mfloor2", "mfloor3", "mfloor4", "mfloor5", "mfloor6", "mfloor7", "dir_splatter_1", "dir_splatter_2")
blood_DNA = list()
generic_filth = TRUE
- persistent = TRUE
+ use_legacy_persistence = TRUE
appearance_flags = NO_CLIENT_COLOR
cleanable_scent = "blood"
scent_descriptor = "odour"
var/base_icon = 'icons/effects/blood.dmi'
- var/basecolor=COLOR_BLOOD_HUMAN // Color when wet.
+ var/basecolor = COLOR_BLOOD_HUMAN // Color when wet.
var/amount = 5
//for 1 unit of depth in puddle (amount var)
var/time_to_dry = 5 MINUTES
@@ -29,6 +29,18 @@
var/list/blood_data
var/chemical = /decl/material/liquid/blood
+/obj/effect/decal/cleanable/blood/Serialize()
+ . = ..()
+ if(!generic_filth) // Generic filth is serialized to a type without these vars, so deserializing them will cause errors.
+ SERIALIZE_IF_MODIFIED(fluorescent, /obj/effect/decal/cleanable/blood)
+ SERIALIZE_IF_MODIFIED(basecolor, /obj/effect/decal/cleanable/blood)
+ SERIALIZE_IF_MODIFIED(drytime, /obj/effect/decal/cleanable/blood)
+ SERIALIZE_DECL_IF_MODIFIED(chemical, /obj/effect/decal/cleanable/blood)
+
+/obj/effect/decal/cleanable/blood/Deserialize(list/instance_map)
+ . = ..()
+ DESERIALIZE_DECL_TO_TYPE(chemical)
+
/obj/effect/decal/cleanable/blood/reveal_blood()
if(ispath(chemical, /decl/material/liquid/blood) && !fluorescent)
fluorescent = FLUORESCENT_GLOWS
@@ -277,7 +289,7 @@
icon = 'icons/effects/blood.dmi'
icon_state = "mucus"
generic_filth = TRUE
- persistent = TRUE
+ use_legacy_persistence = TRUE
#undef BLOOD_SIZE_SMALL
#undef BLOOD_SIZE_MEDIUM
diff --git a/code/game/objects/effects/decals/Cleanable/misc.dm b/code/game/objects/effects/decals/Cleanable/misc.dm
index 634c454322af..9b96d6939412 100644
--- a/code/game/objects/effects/decals/Cleanable/misc.dm
+++ b/code/game/objects/effects/decals/Cleanable/misc.dm
@@ -37,13 +37,13 @@
return TRUE
/obj/effect/decal/cleanable/flour
- name = "flour"
- desc = "It's still good. Four second rule!"
- gender = PLURAL
- icon = 'icons/effects/effects.dmi'
- icon_state = "flour"
- persistent = TRUE
- sweepable = TRUE
+ name = "flour"
+ desc = "It's still good. Four second rule!"
+ gender = PLURAL
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "flour"
+ use_legacy_persistence = TRUE
+ sweepable = TRUE
/obj/effect/decal/cleanable/cobweb
name = "cobweb"
@@ -55,13 +55,13 @@
sweepable = TRUE
/obj/effect/decal/cleanable/molten_item
- name = "gooey grey mass"
- desc = "It looks like a melted... something."
- icon = 'icons/effects/molten_item.dmi'
- icon_state = "molten"
- persistent = TRUE
- generic_filth = TRUE
- weather_sensitive = FALSE
+ name = "gooey grey mass"
+ desc = "It looks like a melted... something."
+ icon = 'icons/effects/molten_item.dmi'
+ icon_state = "molten"
+ use_legacy_persistence = TRUE
+ generic_filth = TRUE
+ weather_sensitive = FALSE
/obj/effect/decal/cleanable/cobweb2
name = "cobweb"
@@ -74,14 +74,14 @@
//Vomit (sorry)
/obj/effect/decal/cleanable/vomit
- name = "vomit"
- desc = "Gosh, how unpleasant."
- gender = PLURAL
- icon = 'icons/effects/vomit.dmi'
- icon_state = "vomit_1"
- persistent = TRUE
- generic_filth = TRUE
- chem_volume = 30
+ name = "vomit"
+ desc = "Gosh, how unpleasant."
+ gender = PLURAL
+ icon = 'icons/effects/vomit.dmi'
+ icon_state = "vomit_1"
+ use_legacy_persistence = TRUE
+ generic_filth = TRUE
+ chem_volume = 30
/obj/effect/decal/cleanable/vomit/Initialize(ml, _age)
random_icon_states = icon_states(icon)
@@ -106,40 +106,40 @@
walker.add_walking_contaminant(reagents, rand(2, 3))
/obj/effect/decal/cleanable/tomato_smudge
- name = "tomato smudge"
- desc = "It's red."
- icon = 'icons/effects/tomatodecal.dmi'
- icon_state = "tomato_floor1"
- random_icon_states = list("tomato_floor1", "tomato_floor2", "tomato_floor3")
- persistent = TRUE
- generic_filth = TRUE
+ name = "tomato smudge"
+ desc = "It's red."
+ icon = 'icons/effects/tomatodecal.dmi'
+ icon_state = "tomato_floor1"
+ random_icon_states = list("tomato_floor1", "tomato_floor2", "tomato_floor3")
+ use_legacy_persistence = TRUE
+ generic_filth = TRUE
/obj/effect/decal/cleanable/egg_smudge
- name = "smashed egg"
- desc = "Seems like this one won't hatch."
- icon = 'icons/effects/tomatodecal.dmi'
- icon_state = "smashed_egg1"
- random_icon_states = list("smashed_egg1", "smashed_egg2", "smashed_egg3")
- persistent = TRUE
- generic_filth = TRUE
+ name = "smashed egg"
+ desc = "Seems like this one won't hatch."
+ icon = 'icons/effects/tomatodecal.dmi'
+ icon_state = "smashed_egg1"
+ random_icon_states = list("smashed_egg1", "smashed_egg2", "smashed_egg3")
+ use_legacy_persistence = TRUE
+ generic_filth = TRUE
/obj/effect/decal/cleanable/pie_smudge //honk
- name = "smashed pie"
- desc = "It's pie cream from a cream pie."
- icon = 'icons/effects/tomatodecal.dmi'
- icon_state = "smashed_pie"
- random_icon_states = list("smashed_pie")
- persistent = TRUE
- generic_filth = TRUE
+ name = "smashed pie"
+ desc = "It's pie cream from a cream pie."
+ icon = 'icons/effects/tomatodecal.dmi'
+ icon_state = "smashed_pie"
+ random_icon_states = list("smashed_pie")
+ use_legacy_persistence = TRUE
+ generic_filth = TRUE
/obj/effect/decal/cleanable/fruit_smudge
- name = "smudge"
- desc = "Some kind of fruit smear."
- icon = 'icons/effects/blood.dmi'
- icon_state = "mfloor1"
- random_icon_states = list("mfloor1", "mfloor2", "mfloor3", "mfloor4", "mfloor5", "mfloor6", "mfloor7")
- persistent = TRUE
- generic_filth = TRUE
+ name = "smudge"
+ desc = "Some kind of fruit smear."
+ icon = 'icons/effects/blood.dmi'
+ icon_state = "mfloor1"
+ random_icon_states = list("mfloor1", "mfloor2", "mfloor3", "mfloor4", "mfloor5", "mfloor6", "mfloor7")
+ use_legacy_persistence = TRUE
+ generic_filth = TRUE
/obj/effect/decal/cleanable/champagne
name = "champagne"
diff --git a/code/game/objects/effects/decals/cleanable.dm b/code/game/objects/effects/decals/cleanable.dm
index 036251e4ea38..aa0e391befb5 100644
--- a/code/game/objects/effects/decals/cleanable.dm
+++ b/code/game/objects/effects/decals/cleanable.dm
@@ -6,9 +6,8 @@
var/burnable = TRUE
var/sweepable = FALSE
var/weather_sensitive = TRUE
- var/persistent = FALSE
+ var/use_legacy_persistence = FALSE
var/generic_filth = FALSE
- var/age = 0
var/list/random_icon_states
var/image/hud_overlay/hud_overlay
var/cleanable_scent
@@ -16,14 +15,30 @@
var/scent_intensity = /decl/scent_intensity/normal
var/scent_descriptor = "smell"
var/scent_range = 2
+ var/have_randomized_icon_state = FALSE
+
+/obj/effect/decal/cleanable/ShouldSerialize(_age)
+ return ..() && use_legacy_persistence
+
+/obj/effect/decal/cleanable/GetSerializedType()
+ return generic_filth ? /obj/effect/decal/cleanable/filth : ..()
+
+/obj/effect/decal/cleanable/Serialize()
+ . = ..()
+ SERIALIZE_IF_MODIFIED(icon_state, /atom)
+
+/obj/effect/decal/cleanable/Deserialize(list/instance_map)
+ . = ..()
+ have_randomized_icon_state = TRUE
/obj/effect/decal/cleanable/Initialize(var/ml, var/_age)
- if(random_icon_states && length(src.random_icon_states) > 0)
- src.icon_state = pick(src.random_icon_states)
+ if(!have_randomized_icon_state && length(random_icon_states))
+ icon_state = pick(random_icon_states)
+ have_randomized_icon_state = TRUE
if(!ml)
if(!isnull(_age))
age = _age
- if(persistent)
+ if(use_legacy_persistence)
SSpersistence.track_value(src, /decl/persistence_handler/filth)
. = ..()
@@ -43,7 +58,7 @@
/obj/effect/decal/cleanable/Destroy()
if(weather_sensitive)
SSweather_atoms.weather_atoms -= src
- if(persistent)
+ if(use_legacy_persistence)
SSpersistence.forget_value(src, /decl/persistence_handler/filth)
. = ..()
diff --git a/code/game/objects/effects/decals/decal.dm b/code/game/objects/effects/decals/decal.dm
index e7f36574fa55..7c4a8c775cb6 100644
--- a/code/game/objects/effects/decals/decal.dm
+++ b/code/game/objects/effects/decals/decal.dm
@@ -1,5 +1,6 @@
/obj/effect/decal
layer = DECAL_LAYER
+ var/age = 0
/obj/effect/decal/fall_damage()
return 0
@@ -11,4 +12,4 @@
. = !throwing ? ..() : FALSE
/obj/effect/decal/get_examine_prefix()
- return null
\ No newline at end of file
+ return null
diff --git a/code/game/objects/effects/decals/decal_serde.dm b/code/game/objects/effects/decals/decal_serde.dm
new file mode 100644
index 000000000000..d598f384dcc4
--- /dev/null
+++ b/code/game/objects/effects/decals/decal_serde.dm
@@ -0,0 +1,10 @@
+/obj/effect/decal/Serialize()
+ . = ..()
+ SERIALIZE_IF_MODIFIED(age, /obj/effect/decal)
+
+/obj/effect/decal/Deserialize(list/instance_map)
+ . = ..()
+ age++
+
+/obj/effect/decal/ShouldSerialize(_age)
+ return simulated && (isnull(_age) || age < _age)
diff --git a/code/game/objects/effects/dirty_floor.dm b/code/game/objects/effects/dirty_floor.dm
index 8c2df8b9f8b0..833ddaf2b0a4 100644
--- a/code/game/objects/effects/dirty_floor.dm
+++ b/code/game/objects/effects/dirty_floor.dm
@@ -5,10 +5,14 @@
icon = 'icons/effects/effects.dmi'
icon_state = "dirt"
mouse_opacity = MOUSE_OPACITY_UNCLICKABLE
- persistent = TRUE
+ use_legacy_persistence = TRUE
alpha = 0
var/dirt_amount = 0
+/obj/effect/decal/cleanable/dirt/Serialize()
+ . = ..()
+ SERIALIZE_IF_MODIFIED(dirt_amount, /obj/effect/decal/cleanable/dirt)
+
/obj/effect/decal/cleanable/dirt/lava_act()
qdel(src)
return TRUE
@@ -21,10 +25,14 @@
/obj/effect/decal/cleanable/dirt/visible
dirt_amount = 60
- persistent = FALSE // This is a subtype for mapping.
+ use_legacy_persistence = FALSE // This is a subtype for mapping.
/obj/effect/decal/cleanable/dirt/Initialize()
. = ..()
+ for(var/obj/effect/decal/cleanable/dirt/other in loc)
+ if(other != src)
+ other.dirt_amount += dirt_amount
+ return INITIALIZE_HINT_QDEL
verbs.Cut()
update_icon()
diff --git a/code/game/objects/items/_item_serde.dm b/code/game/objects/items/_item_serde.dm
new file mode 100644
index 000000000000..c4aba6ca9327
--- /dev/null
+++ b/code/game/objects/items/_item_serde.dm
@@ -0,0 +1,3 @@
+/obj/item/Serialize()
+ . = ..()
+ SERIALIZE_IF_MODIFIED(paint_verb, /obj/item)
diff --git a/code/game/objects/items/books/_book_serde.dm b/code/game/objects/items/books/_book_serde.dm
new file mode 100644
index 000000000000..25251936e1d9
--- /dev/null
+++ b/code/game/objects/items/books/_book_serde.dm
@@ -0,0 +1,31 @@
+/obj/item/book/Serialize()
+ . = ..()
+ SERIALIZE_IF_MODIFIED(last_modified_ckey, /obj/item/book)
+ SERIALIZE_IF_MODIFIED(dat, /obj/item/book)
+ SERIALIZE_IF_MODIFIED(title, /obj/item/book)
+ SERIALIZE_IF_MODIFIED(author, /obj/item/book)
+ SERIALIZE_IF_MODIFIED(icon_state, /atom)
+
+/obj/item/book/Deserialize()
+ . = ..()
+ SSpersistence.track_value(src, /decl/persistence_handler/book)
+
+/obj/item/book/GetPossiblySerializableInstances()
+ . = ..()
+ if(istype(loc, /obj/structure/bookcase))
+ LAZYDISTINCTADD(., loc)
+
+/obj/item/book/Deserialize(list/instance_map)
+ ..()
+ return SERDE_HINT_POSTINIT
+
+/obj/item/book/DeserializePostInit(list/instance_map)
+ . = ..()
+ var/area/area = get_area(src)
+ if(!area || (area.area_flags & AREA_FLAG_NO_LEGACY_PERSISTENCE))
+ forceMove(null)
+ if(isnull(loc))
+ if(length(global.station_bookcases))
+ forceMove(pick(global.station_bookcases))
+ else
+ forceMove(get_random_spawn_turf(SPAWN_FLAG_PERSISTENCE_CAN_SPAWN))
diff --git a/code/game/objects/items/stacks/stack_serde.dm b/code/game/objects/items/stacks/stack_serde.dm
new file mode 100644
index 000000000000..53702ee76722
--- /dev/null
+++ b/code/game/objects/items/stacks/stack_serde.dm
@@ -0,0 +1,3 @@
+/obj/item/stack/Serialize()
+ . = ..()
+ SERIALIZE_IF_MODIFIED(amount, /obj/item/stack)
diff --git a/code/game/objects/items/trash_serde.dm b/code/game/objects/items/trash_serde.dm
new file mode 100644
index 000000000000..ad791d2e8d39
--- /dev/null
+++ b/code/game/objects/items/trash_serde.dm
@@ -0,0 +1,24 @@
+/obj/item/trash/Serialize()
+ . = ..()
+ SERIALIZE_IF_MODIFIED(age, /obj/item/trash)
+
+/obj/item/trash/ShouldSerialize(_age)
+ return ..() && (isnull(_age) || age < _age)
+
+/obj/item/trash/Deserialize(list/instance_map)
+ ..()
+ return SERDE_HINT_POSTINIT
+
+/obj/item/trash/DeserializePostInit(list/instance_map)
+ . = ..()
+ for(var/obj/item/trash/thing in loc)
+ if(thing != src && thing.type == type)
+ qdel(src)
+ return
+ var/too_much_trash = 0
+ for(var/obj/item/trash/trash in loc)
+ if(trash == src)
+ too_much_trash++
+ if(too_much_trash >= 5)
+ qdel(src)
+ return
diff --git a/code/game/objects/items/weapons/storage/med_pouch.dm b/code/game/objects/items/weapons/storage/med_pouch.dm
index 0bb25ee3ac04..1d7cc0f7c27b 100644
--- a/code/game/objects/items/weapons/storage/med_pouch.dm
+++ b/code/game/objects/items/weapons/storage/med_pouch.dm
@@ -195,16 +195,23 @@ Single Use Emergency Pouches
chem_volume = 15
abstract_type = /obj/item/chems/pill/pouch_pill
var/_reagent_name
+ var/_reagent_volume
/obj/item/chems/pill/pouch_pill/Initialize(ml, material_key)
. = ..()
- if(!REAGENT_TOTAL_VOLUME(reagents))
+ if(!istype(reagents) || !REAGENT_TOTAL_VOLUME(reagents))
log_warning("[log_info_line(src)] was deleted for containing no reagents during init!")
return INITIALIZE_HINT_QDEL
- if(reagents?.get_primary_reagent_name() && !_reagent_name)
- _reagent_name = "emergency [reagents.get_primary_reagent_name()] pill ([REAGENT_TOTAL_VOLUME(reagents)]u)"
- if(_reagent_name)
- SetName(_reagent_name)
+ if(isnull(_reagent_name))
+ _reagent_name = reagents.get_primary_reagent_name()
+ _reagent_volume = REAGENT_TOTAL_VOLUME(reagents)
+ if(_reagent_name && _reagent_volume)
+ SetName("emergency [_reagent_name] pill ([_reagent_volume]u)")
+
+/obj/item/chems/pill/pouch_pill/Serialize()
+ . = ..()
+ SERIALIZE_IF_MODIFIED(_reagent_name, /obj/item/chems/pill/pouch_pill)
+ SERIALIZE_IF_MODIFIED(_reagent_volume, /obj/item/chems/pill/pouch_pill)
/obj/item/chems/pill/pouch_pill/stabilizer/populate_reagents()
add_to_reagents(/decl/material/liquid/stabilizer, REAGENT_MAXIMUM_VOLUME(reagents))
diff --git a/code/game/objects/structures/__structure.dm b/code/game/objects/structures/__structure.dm
index be2c2102ae50..27239ec9127b 100644
--- a/code/game/objects/structures/__structure.dm
+++ b/code/game/objects/structures/__structure.dm
@@ -8,7 +8,6 @@
/// Multiplier for degree of comfort offered to mobs buckled to this furniture.
var/user_comfort = 0 // TODO: extremely uncomfortable chairs
-
var/structure_flags
var/last_damage_message
var/hitsound = 'sound/weapons/Genhit.ogg'
@@ -16,7 +15,6 @@
var/parts_amount
var/footstep_type
var/mob_offset
-
var/paint_verb
/obj/structure/get_color()
diff --git a/code/game/objects/structures/_structure_serde.dm b/code/game/objects/structures/_structure_serde.dm
new file mode 100644
index 000000000000..1f69c1294225
--- /dev/null
+++ b/code/game/objects/structures/_structure_serde.dm
@@ -0,0 +1,3 @@
+/obj/structure/Serialize()
+ . = ..()
+ SERIALIZE_IF_MODIFIED(paint_verb, /obj/structure)
diff --git a/code/game/objects/structures/drying_rack.dm b/code/game/objects/structures/drying_rack.dm
index 3feffa51de95..6f116ab4451f 100644
--- a/code/game/objects/structures/drying_rack.dm
+++ b/code/game/objects/structures/drying_rack.dm
@@ -8,6 +8,16 @@
material_alteration = MAT_FLAG_ALTERATION_COLOR | MAT_FLAG_ALTERATION_NAME | MAT_FLAG_ALTERATION_DESC
var/obj/item/drying
+/obj/structure/drying_rack/Initialize(ml, _mat, _reinf_mat)
+ . = ..()
+ // This is mostly for serde.
+ for(var/obj/item/thing in get_contained_external_atoms())
+ if(!drying && thing.is_dryable())
+ drying = thing
+ update_icon()
+ else
+ thing.dropInto(loc)
+
/obj/structure/drying_rack/ebony
material = /decl/material/solid/organic/wood/ebony
color = /decl/material/solid/organic/wood/ebony::color
diff --git a/code/game/objects/structures/flora/plant_serde.dm b/code/game/objects/structures/flora/plant_serde.dm
new file mode 100644
index 000000000000..26cfbedac5e9
--- /dev/null
+++ b/code/game/objects/structures/flora/plant_serde.dm
@@ -0,0 +1,7 @@
+/obj/structure/flora/plant/ShouldSerialize(_age)
+ return plant?.roundstart && ..(_age)
+
+/obj/structure/flora/plant/Serialize()
+ . = ..()
+ if(plant && plant.name != initial(plant))
+ .[nameof(/obj/structure/flora/plant::plant)] = plant.name
diff --git a/code/game/turfs/floors/floor_height.dm b/code/game/turfs/floors/floor_height.dm
index 8968d6020a5a..43057b4dc0cc 100644
--- a/code/game/turfs/floors/floor_height.dm
+++ b/code/game/turfs/floors/floor_height.dm
@@ -19,4 +19,5 @@
for(var/atom/movable/thing in contents)
thing.on_turf_height_change(new_height)
+ state_was_modified()
return TRUE
diff --git a/code/game/turfs/floors/floor_icon.dm b/code/game/turfs/floors/floor_icon.dm
index 32574263fd61..0f684b9a5a47 100644
--- a/code/game/turfs/floors/floor_icon.dm
+++ b/code/game/turfs/floors/floor_icon.dm
@@ -158,6 +158,7 @@
_floor_broken = new_broken
if(!skip_update)
queue_icon_update()
+ state_was_modified()
return TRUE
return FALSE
@@ -172,6 +173,7 @@
_floor_burned = new_burned
if(!skip_update)
queue_icon_update()
+ state_was_modified()
return TRUE
return FALSE
diff --git a/code/game/turfs/floors/floor_layers.dm b/code/game/turfs/floors/floor_layers.dm
index 8fb98f91fa6a..24e1629b664c 100644
--- a/code/game/turfs/floors/floor_layers.dm
+++ b/code/game/turfs/floors/floor_layers.dm
@@ -55,21 +55,25 @@
remove_flooring(_flooring, TRUE, place_product)
if(!skip_update)
update_from_flooring()
+ state_was_modified()
return TRUE
/turf/floor/proc/remove_flooring(var/decl/flooring/flooring, skip_update, place_product)
// Remove floor layers one by one.
_topmost_flooring = null
+
if(islist(flooring))
for(var/floor in UNLINT(flooring))
if(remove_flooring(floor, TRUE, place_product))
. = TRUE
- if(. && !skip_update)
- set_floor_broken(skip_update = TRUE)
- set_floor_burned(skip_update = TRUE)
- update_from_flooring()
- return
+ if(.)
+ state_was_modified()
+ if(!skip_update)
+ set_floor_broken(skip_update = TRUE)
+ set_floor_burned(skip_update = TRUE)
+ update_from_flooring()
+ return
// Validate our input.
flooring = RESOLVE_TO_DECL(flooring)
@@ -85,6 +89,8 @@
else if(_flooring == flooring)
_flooring = null
+ state_was_modified()
+
// If the turf was not the topmost turf, then we don't really need to care about it.
if(!was_topmost)
return
@@ -140,7 +146,7 @@
_flooring = RESOLVE_TO_DECL(newflooring)
else
return FALSE
-
+ state_was_modified()
if(!skip_update)
update_from_flooring()
return TRUE
@@ -162,6 +168,7 @@
for(var/floor in UNLINT(newflooring))
if(add_flooring(floor, skip_update = FALSE))
. = TRUE
+ state_was_modified()
if(!skip_update)
set_floor_broken(skip_update = TRUE)
set_floor_burned(skip_update = TRUE)
@@ -189,6 +196,8 @@
_flooring = list(_flooring)
_flooring |= newflooring
+ state_was_modified()
+
// Update for the new top layer.
if(!skip_update)
set_floor_broken(skip_update = TRUE)
diff --git a/code/game/turfs/floors/floor_materials.dm b/code/game/turfs/floors/floor_materials.dm
index 213deaca53b1..5a7ff3459fd2 100644
--- a/code/game/turfs/floors/floor_materials.dm
+++ b/code/game/turfs/floors/floor_materials.dm
@@ -11,8 +11,10 @@
material = get_default_material()
. = TRUE
- if(. && !skip_update)
- queue_icon_update()
+ if(.)
+ state_was_modified()
+ if(!skip_update)
+ queue_icon_update()
/turf/floor/get_material()
var/decl/flooring/flooring = get_topmost_flooring()
diff --git a/code/game/turfs/floors/floor_serde.dm b/code/game/turfs/floors/floor_serde.dm
new file mode 100644
index 000000000000..9e88c47f2a83
--- /dev/null
+++ b/code/game/turfs/floors/floor_serde.dm
@@ -0,0 +1,30 @@
+/turf/floor/Serialize()
+ . = ..()
+
+ SERIALIZE_IF_MODIFIED(_floor_broken, /turf/floor)
+ SERIALIZE_IF_MODIFIED(_floor_burned, /turf/floor)
+ SERIALIZE_IF_MODIFIED(height, /turf/floor)
+ SERIALIZE_DECL_IF_MODIFIED(_base_flooring, /turf/floor)
+
+ var/initial_flooring = initial(_flooring)
+ if(isnull(_flooring) && !isnull(initial_flooring))
+ .[nameof(/turf/floor::_flooring)] = json_encode(list())
+ else if((ispath(_flooring) || istype(_flooring, /decl)) && (!ispath(initial_flooring) || !DECLS_ARE_EQUIVALENT(_flooring, initial_flooring)))
+ var/decl/flooring/flooring = RESOLVE_TO_DECL(_flooring)
+ if(istype(flooring))
+ .[nameof(/turf/floor::_flooring)] = json_encode(list(flooring.uid))
+ else if(islist(_flooring))
+ var/list/flooring_uids
+ for(var/floor in _flooring)
+ var/decl/flooring/floor_decl = RESOLVE_TO_DECL(floor)
+ if(istype(floor_decl))
+ LAZYADD(flooring_uids, floor_decl.uid)
+ if(!istext(initial_flooring) || !(flooring_uids ~= cached_json_decode(initial_flooring)))
+ .[nameof(/turf/floor::_flooring)] = json_encode(flooring_uids)
+
+/turf/floor/Deserialize(list/instance_map)
+ . = ..()
+ fill_reagent_type = null // Assume any fluids on this turf were serialized and will be deserialized on /turf/Deserialize()
+ DESERIALIZE_DECL_TO_TYPE(_base_flooring)
+ // _flooring is expected as a JSON list in base floor
+ // Initialize(), so no additional deserializing needed here.
diff --git a/code/game/turfs/space/space.dm b/code/game/turfs/space/space.dm
index 0c4d7107c015..ff76d205ad23 100644
--- a/code/game/turfs/space/space.dm
+++ b/code/game/turfs/space/space.dm
@@ -23,6 +23,7 @@
SHOULD_CALL_PARENT(FALSE)
atom_flags |= ATOM_FLAG_INITIALIZED
+ _earliest_type ||= type
AMBIENCE_QUEUE_TURF(src)
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index d2b367402d54..df7d8c2b68ad 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -101,6 +101,7 @@
/turf/Initialize(mapload, ...)
. = null && ..() // This weird construct is to shut up the 'parent proc not called' warning without disabling the lint for child types. We explicitly return an init hint so this won't change behavior.
+ _earliest_type ||= type
color = null
// atom/Initialize has been copied here for performance (or at least the bits of it that turfs use has been)
@@ -116,6 +117,10 @@
else
luminosity = 1
+ // Reagents got deserialized, set them up. Do not return as we want to finish turf init.
+ // we don't care about volume because turfs always create a maximum volume holder on reagent add.
+ FINALIZE_REAGENTS_SERDE(reagents)
+
AMBIENCE_QUEUE_TURF(src)
if (opacity)
@@ -139,6 +144,7 @@
if(flooded)
set_flooded(flooded, TRUE, skip_vis_contents_update = TRUE, mapload = mapload)
+
update_vis_contents()
if(simulated)
@@ -627,6 +633,7 @@
if(is_outside == new_outside)
return FALSE
+ state_was_modified()
is_outside = new_outside
update_external_atmos_participation()
AMBIENCE_QUEUE_TURF(src)
diff --git a/code/game/turfs/turf_changing.dm b/code/game/turfs/turf_changing.dm
index ee3311a90cb5..0dfa901cf988 100644
--- a/code/game/turfs/turf_changing.dm
+++ b/code/game/turfs/turf_changing.dm
@@ -55,6 +55,7 @@
// Track a number of old values for the purposes of raising
// state change events after changing the turf to the new type.
+ var/old_earliest_type = _earliest_type
var/old_fire = fire
var/old_above = above
var/old_opacity = opacity
@@ -201,6 +202,9 @@
for(var/atom/movable/thing in changed_turf.get_contained_external_atoms())
thing.fall()
+ changed_turf._earliest_type = old_earliest_type
+ changed_turf.state_was_modified()
+
/turf/proc/transport_properties_from(turf/other, transport_air)
if(transport_air && can_inherit_air && (other.zone || other.air))
if(!air)
diff --git a/code/game/turfs/turf_fluids.dm b/code/game/turfs/turf_fluids.dm
index 21227d330c38..aafbb2bb1931 100644
--- a/code/game/turfs/turf_fluids.dm
+++ b/code/game/turfs/turf_fluids.dm
@@ -179,6 +179,8 @@
if(!(. = ..()))
return
+ state_was_modified()
+
if(REAGENT_TOTAL_LIQUID_VOLUME(reagents) < FLUID_SLURRY)
dump_solid_reagents()
@@ -204,7 +206,7 @@
for(var/checkdir in global.cardinal)
var/turf/neighbor = get_step_resolving_mimic(src, checkdir)
- if(REAGENT_TOTAL_VOLUME(neighbor?.reagents) > FLUID_QDEL_POINT)
+ if(istype(neighbor) && (islist(neighbor.reagents) || (istype(neighbor.reagents) && REAGENT_TOTAL_VOLUME(neighbor.reagents) > FLUID_QDEL_POINT)))
ADD_ACTIVE_FLUID(neighbor)
/turf/proc/dump_solid_reagents(datum/reagents/solids)
diff --git a/code/game/turfs/turf_serde.dm b/code/game/turfs/turf_serde.dm
new file mode 100644
index 000000000000..d8f7515f2b84
--- /dev/null
+++ b/code/game/turfs/turf_serde.dm
@@ -0,0 +1,60 @@
+/turf
+ var/_earliest_type
+ var/_state_was_modified
+ var/_contents_were_modified
+
+/turf/ShouldSerialize(_age)
+ if(type == _earliest_type && !_state_was_modified && !_contents_were_modified)
+ return FALSE
+ var/area/area = get_area(src)
+ if(!(area?.area_flags & AREA_FLAG_ALLOW_LEVEL_PERSISTENCE))
+ return FALSE
+ return ..(_age)
+
+/turf/Serialize()
+ . = ..()
+ SERIALIZE_VALUE(loc, /atom/movable, list(x, y, z))
+ SERIALIZE_IF_MODIFIED(is_outside, /turf)
+
+/turf/Deserialize(list/instance_map)
+ . = ..()
+ state_was_modified()
+
+/turf/proc/state_was_modified()
+ if(!simulated || _state_was_modified)
+ return
+ _state_was_modified = TRUE
+ update_level_persistence_tracking()
+
+/atom/proc/contents_were_modified()
+ var/turf/turf = get_turf(src)
+ turf?.contents_were_modified()
+
+/turf/contents_were_modified()
+ if(!simulated || _contents_were_modified)
+ return
+ _contents_were_modified = TRUE
+ update_level_persistence_tracking()
+
+/turf/proc/update_level_persistence_tracking()
+ var/area/area = get_area(src)
+ if(!(area?.area_flags & AREA_FLAG_ALLOW_LEVEL_PERSISTENCE))
+ return
+ var/datum/level_data/level = SSmapping.levels_by_z[z]
+ if(!istype(level) || !level.is_persistent())
+ return
+ var/list/coord = json_encode(list(x, y))
+ LAZYSET(level.changed_turfs, coord, TRUE)
+
+/turf/proc/UnpackSerializableInstances()
+ // Get all recursively nested instances on this turf.
+ var/list/instances_to_unpack = list(src)
+ while(length(instances_to_unpack))
+ var/datum/instance = instances_to_unpack[1]
+ instances_to_unpack.Cut(1, 2)
+ if(instance in .)
+ continue
+ LAZYADD(., instance)
+ var/list/packed_instances = instance.GetPossiblySerializableInstances()
+ if(length(packed_instances))
+ instances_to_unpack |= packed_instances
diff --git a/code/game/turfs/walls/wall_material.dm b/code/game/turfs/walls/wall_material.dm
index a71c306e0412..f82b5d0be9a0 100644
--- a/code/game/turfs/walls/wall_material.dm
+++ b/code/game/turfs/walls/wall_material.dm
@@ -25,5 +25,7 @@
girder_material = new_girder_material
. = TRUE
- if(. && !skip_update)
- update_material()
+ if(.)
+ state_was_modified()
+ if(!skip_update)
+ queue_icon_update()
diff --git a/code/game/turfs/walls/wall_natural_ramps.dm b/code/game/turfs/walls/wall_natural_ramps.dm
index c36d31b5bd87..76526f13e7ad 100644
--- a/code/game/turfs/walls/wall_natural_ramps.dm
+++ b/code/game/turfs/walls/wall_natural_ramps.dm
@@ -1,5 +1,9 @@
/turf/wall/natural/proc/make_ramp(var/mob/user, var/new_slope, var/skip_icon_update = FALSE)
+ if(ramp_slope_direction == new_slope)
+ return
+
+ state_was_modified()
ramp_slope_direction = new_slope
QDEL_NULL_LIST(engravings)
diff --git a/code/game/turfs/walls/wall_natural_subtypes.dm b/code/game/turfs/walls/wall_natural_subtypes.dm
index cba963b8e9cc..dc072ac25193 100644
--- a/code/game/turfs/walls/wall_natural_subtypes.dm
+++ b/code/game/turfs/walls/wall_natural_subtypes.dm
@@ -1,6 +1,10 @@
/turf/wall/natural/random
reinf_material = null
+// We want to avoid spawning random ores in Initialize() by serializing a subtype that does that.
+/turf/wall/natural/random/GetSerializedType()
+ return /turf/wall/natural
+
/turf/wall/natural/random/proc/get_weighted_mineral_list()
if(strata_override)
var/decl/strata/strata_info = GET_DECL(strata_override)
@@ -30,9 +34,15 @@
/turf/wall/natural/random/volcanic
strata_override = /decl/strata/igneous
+/turf/wall/natural/random/volcanic/GetSerializedType()
+ return /turf/wall/natural/volcanic
+
/turf/wall/natural/random/high_chance/volcanic
strata_override = /decl/strata/igneous
+/turf/wall/natural/random/high_chance/volcanic/GetSerializedType()
+ return /turf/wall/natural/volcanic
+
/turf/wall/natural/ice
strata_override = /decl/strata/permafrost
floor_type = /turf/floor/ice
@@ -41,41 +51,53 @@
strata_override = /decl/strata/permafrost
floor_type = /turf/floor/ice
+/turf/wall/natural/random/ice/GetSerializedType()
+ return /turf/wall/natural/ice
+
/turf/wall/natural/random/high_chance/ice
strata_override = /decl/strata/permafrost
floor_type = /turf/floor/ice
+/turf/wall/natural/random/high_chance/ice/GetSerializedType()
+ return /turf/wall/natural/ice
+
/turf/wall/natural/dirt
material = /decl/material/solid/soil
color = "#41311b"
floor_type = /turf/floor/dirt
-#define MATERIAL_NATURAL_TURFS(ID, MAT) \
-/turf/floor/rock/##ID { \
- color = /decl/material/##MAT::color; \
- material = /decl/material/##MAT \
-} \
-/turf/floor/rock/##ID/sand { \
- name = "sand"; \
- icon = 'icons/turf/flooring/sand.dmi'; \
- icon_state = "sand0"; \
- color = "#ae9e66"; \
- _flooring = /decl/flooring/sand; \
-} \
-/turf/wall/natural/##ID { \
- material = /decl/material/##MAT; \
- color = /decl/material/##MAT::color; \
- floor_type = /turf/floor/rock/##ID; \
-} \
-/turf/wall/natural/random/##ID { \
- material = /decl/material/##MAT; \
- color = /decl/material/##MAT::color; \
- floor_type = /turf/floor/rock/##ID; \
-} \
-/turf/wall/natural/random/high_chance/##ID { \
- material = /decl/material/##MAT; \
- color = /decl/material/##MAT::color; \
- floor_type = /turf/floor/rock/##ID \
+#define MATERIAL_NATURAL_TURFS(ID, MAT) \
+/turf/floor/rock/##ID { \
+ color = /decl/material/##MAT::color; \
+ material = /decl/material/##MAT \
+} \
+/turf/floor/rock/##ID/sand { \
+ name = "sand"; \
+ icon = 'icons/turf/flooring/sand.dmi'; \
+ icon_state = "sand0"; \
+ color = "#ae9e66"; \
+ _flooring = /decl/flooring/sand; \
+} \
+/turf/wall/natural/##ID { \
+ material = /decl/material/##MAT; \
+ color = /decl/material/##MAT::color; \
+ floor_type = /turf/floor/rock/##ID; \
+} \
+/turf/wall/natural/random/##ID { \
+ material = /decl/material/##MAT; \
+ color = /decl/material/##MAT::color; \
+ floor_type = /turf/floor/rock/##ID; \
+} \
+/turf/wall/natural/random/##ID/GetSerializedType() { \
+ return /turf/wall/natural/##ID; \
+} \
+/turf/wall/natural/random/high_chance/##ID { \
+ material = /decl/material/##MAT; \
+ color = /decl/material/##MAT::color; \
+ floor_type = /turf/floor/rock/##ID \
+} \
+/turf/wall/natural/random/high_chance/##ID/GetSerializedType() { \
+ return /turf/wall/natural/##ID; \
}
MATERIAL_NATURAL_TURFS(sandstone, solid/stone/sandstone)
MATERIAL_NATURAL_TURFS(basalt, solid/stone/basalt)
diff --git a/code/game/turfs/walls/wall_serde.dm b/code/game/turfs/walls/wall_serde.dm
new file mode 100644
index 000000000000..53d737fffa2c
--- /dev/null
+++ b/code/game/turfs/walls/wall_serde.dm
@@ -0,0 +1,20 @@
+/turf/wall/Serialize()
+ . = ..()
+
+ SERIALIZE_DECL_IF_MODIFIED(material, /turf/wall)
+ SERIALIZE_DECL_IF_MODIFIED(girder_material, /turf/wall)
+ SERIALIZE_DECL_IF_MODIFIED(shutter_material, /turf/wall)
+
+ SERIALIZE_IF_MODIFIED(shutter_state, /turf/wall)
+ SERIALIZE_IF_MODIFIED(stripe_color, /turf/wall)
+ SERIALIZE_IF_MODIFIED(damage, /turf/wall)
+ SERIALIZE_IF_MODIFIED(can_open, /turf/wall)
+
+/turf/wall/Deserialize(list/instance_map)
+ . = ..()
+ DESERIALIZE_DECL_TO_TYPE(girder_material)
+ DESERIALIZE_DECL_TO_TYPE(shutter_material)
+
+/turf/wall/natural/Serialize()
+ . = ..()
+ SERIALIZE_IF_MODIFIED(ramp_slope_direction, /turf/wall/natural)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index e2f496827b76..d4d3e91d6e5f 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -157,7 +157,8 @@ var/global/list/admin_verbs_server = list(
/datum/admins/proc/removeserverwhitelist,
/datum/admins/proc/panicbunker,
/datum/admins/proc/addbunkerbypass,
- /datum/admins/proc/revokebunkerbypass
+ /datum/admins/proc/revokebunkerbypass,
+ /datum/admins/proc/force_persistence_save_verb
)
var/global/list/admin_verbs_debug = list(
/datum/admins/proc/jump_to_fluid_source,
diff --git a/code/modules/banners/_banner_frame.dm b/code/modules/banners/_banner_frame.dm
index 745de870f6ba..fab17e290c66 100644
--- a/code/modules/banners/_banner_frame.dm
+++ b/code/modules/banners/_banner_frame.dm
@@ -21,12 +21,19 @@
var/obj/item/banner/banner
var/accepts_banner_type = /obj/item/banner
+// Avoiding random generation behavior on subtypes.
+// We don't serialize a reference to the banner item anyway.
+/obj/structure/banner_frame/GetSerializedType()
+ return /obj/structure/banner_frame
+
/obj/structure/banner_frame/set_dir(ndir)
return ..(force_south_facing ? SOUTH : ndir)
/obj/structure/banner_frame/Initialize(ml, _mat, _reinf_mat)
if(ispath(banner))
set_banner(new banner(src))
+ else if(isnull(banner))
+ set_banner(locate(/obj/item/banner) in src)
. = ..()
update_icon()
diff --git a/code/modules/banners/sign_post.dm b/code/modules/banners/sign_post.dm
index d4b4a0c45a18..f41ebb0157e4 100644
--- a/code/modules/banners/sign_post.dm
+++ b/code/modules/banners/sign_post.dm
@@ -9,12 +9,18 @@
icon_state = "sign_preview"
density = TRUE
+/obj/structure/banner_frame/sign/GetSerializedType()
+ return /obj/structure/banner_frame/sign
+
/obj/structure/banner_frame/sign/wall
base_icon_state = "sign_hanging"
icon_state = "sign_hanging_preview"
force_south_facing = FALSE
density = FALSE
+/obj/structure/banner_frame/sign/wall/GetSerializedType()
+ return /obj/structure/banner_frame/sign/wall
+
/obj/structure/banner_frame/sign/random/Initialize(ml, _mat, _reinf_mat)
material = pick(decls_repository.get_decls_of_subtype(/decl/material/solid/organic/wood))
..()
diff --git a/code/modules/crafting/working/quern.dm b/code/modules/crafting/working/quern.dm
index 3ff1832811c7..aa76aafb4034 100644
--- a/code/modules/crafting/working/quern.dm
+++ b/code/modules/crafting/working/quern.dm
@@ -15,8 +15,8 @@
var/tmp/possible_transfer_amounts = @"[10,25,50,100,500]"
/obj/structure/working/quern/Initialize()
- . = ..()
atom_flags |= ATOM_FLAG_OPEN_CONTAINER
+ . = ..()
/obj/structure/working/quern/try_start_working(mob/user)
diff --git a/code/modules/fluids/_fluid.dm b/code/modules/fluids/_fluid.dm
index 8dd93caf8988..8e2449f56fc1 100644
--- a/code/modules/fluids/_fluid.dm
+++ b/code/modules/fluids/_fluid.dm
@@ -117,7 +117,7 @@ var/global/list/_fluid_edge_mask_cache = list()
var/list/connections
for(var/checkdir in global.alldirs)
var/turf/neighbor = get_step_resolving_mimic(loc, checkdir)
- if(!neighbor || neighbor.density || REAGENT_TOTAL_VOLUME(neighbor?.reagents) > FLUID_PUDDLE)
+ if(!neighbor || neighbor.density || !istype(neighbor?.reagents) || REAGENT_TOTAL_VOLUME(neighbor?.reagents) > FLUID_PUDDLE)
LAZYADD(connections, checkdir)
else
LAZYADD(ignored, checkdir)
diff --git a/code/modules/maps/reader.dm b/code/modules/maps/reader.dm
index 71bedba36393..9f7dff608187 100644
--- a/code/modules/maps/reader.dm
+++ b/code/modules/maps/reader.dm
@@ -10,6 +10,7 @@ var/global/dmm_suite/preloader/_preloader = new
/datum/map_load_metadata
var/bounds
var/list/atoms_to_initialise
+ var/list/turfs_to_mark_modified
/dmm_suite
// /"([a-zA-Z]+)" = \(((?:.|\n)*?)\)\n(?!\t)|\((\d+),(\d+),(\d+)\) = \{"\n*([a-zA-Z\n]*)\n?"\}/g
@@ -64,12 +65,19 @@ var/global/dmm_suite/preloader/_preloader = new
initialized_areas_by_type = initialized_areas_by_type || list()
if(!(world.area in initialized_areas_by_type))
initialized_areas_by_type[world.area] = locate(world.area)
- . = load_map_impl(dmm_file, x_offset, y_offset, z_offset, cropMap, measureOnly, no_changeturf, clear_contents, lower_crop_x, upper_crop_x, lower_crop_y, upper_crop_y, initialized_areas_by_type, level_data_type)
+
+ var/datum/map_load_metadata/M = load_map_impl(dmm_file, x_offset, y_offset, z_offset, cropMap, measureOnly, no_changeturf, clear_contents, lower_crop_x, upper_crop_x, lower_crop_y, upper_crop_y, initialized_areas_by_type, level_data_type)
+
+ if(length(M.turfs_to_mark_modified))
+ for(var/turf/turf in M.turfs_to_mark_modified)
+ turf.state_was_modified()
+
#ifdef TESTING
if(turfsSkipped)
testing("Skipped loading [turfsSkipped] default turfs")
#endif
Master.StopLoadingMap()
+ return M
/dmm_suite/proc/load_map_impl(dmm_file, x_offset, y_offset, z_offset, cropMap, measureOnly, no_changeturf, clear_contents, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY, initialized_areas_by_type, level_data_type = /datum/level_data/space)
var/tfile = dmm_file//the map file we're creating
@@ -91,6 +99,7 @@ var/global/dmm_suite/preloader/_preloader = new
var/list/atoms_to_initialise = list()
var/list/atoms_to_delete = list()
+ var/list/turfs_to_mark_modified = list()
while(dmmRegex.Find(tfile, stored_index))
stored_index = dmmRegex.next
@@ -180,7 +189,11 @@ var/global/dmm_suite/preloader/_preloader = new
throw EXCEPTION("Undefined model key in DMM.")
var/datum/grid_load_metadata/M = parse_grid(grid_models[model_key], model_key, xcrd, ycrd, zcrd, no_changeturf || zexpansion, clear_contents, initialized_areas_by_type)
if (M)
- atoms_to_initialise += M.atoms_to_initialise
+ if(length(M.atoms_to_initialise))
+ atoms_to_initialise += M.atoms_to_initialise
+ if(length(M.turfs_to_mark_modified))
+ turfs_to_mark_modified += M.turfs_to_mark_modified
+
atoms_to_delete += M.atoms_to_delete
#ifdef TESTING
else
@@ -205,6 +218,7 @@ var/global/dmm_suite/preloader/_preloader = new
var/datum/map_load_metadata/M = new
M.bounds = bounds
M.atoms_to_initialise = atoms_to_initialise
+ M.turfs_to_mark_modified = turfs_to_mark_modified
return M
/**
@@ -228,6 +242,7 @@ var/global/dmm_suite/preloader/_preloader = new
/datum/grid_load_metadata
var/list/atoms_to_initialise
var/list/atoms_to_delete
+ var/list/turfs_to_mark_modified
/dmm_suite/proc/parse_grid(model as text, model_key as text, xcrd as num,ycrd as num,zcrd as num, no_changeturf as num, clear_contents as num, initialized_areas_by_type)
/*Method parse_grid()
@@ -345,23 +360,28 @@ var/global/dmm_suite/preloader/_preloader = new
SSatoms.map_loader_begin()
//since we've switched off autoinitialisation, record atoms to initialise later
var/list/atoms_to_initialise = list()
+ var/list/turfs_to_mark_modified = list()
//instanciate the first /turf
var/turf/T
if(members[first_turf_index] != /turf/template_noop)
is_not_noop = TRUE
- T = instance_atom(members[first_turf_index],members_attributes[first_turf_index],crds,no_changeturf)
+ T = instance_atom(members[first_turf_index], members_attributes[first_turf_index], crds, no_changeturf)
atoms_to_initialise += T
+ if(isturf(T))
+ turfs_to_mark_modified += T
if(T)
//if others /turf are presents, simulates the underlays piling effect
index = first_turf_index + 1
while(index < length(members)) // Last item is an /area
var/underlay = T.appearance
- T = instance_atom(members[index],members_attributes[index],crds,no_changeturf)//instance new turf
+ T = instance_atom(members[index], members_attributes[index], crds, no_changeturf)//instance new turf
T.underlays += underlay
index++
atoms_to_initialise += T
+ if(isturf(T))
+ turfs_to_mark_modified += T
if (clear_contents && is_not_noop && length(crds.contents))
for (var/atom/movable/pre_existing as anything in crds)
@@ -373,13 +393,18 @@ var/global/dmm_suite/preloader/_preloader = new
//finally instance all remainings objects/mobs
for(index in 1 to first_turf_index-1)
- atoms_to_initialise += instance_atom(members[index],members_attributes[index],crds,no_changeturf)
+ var/atom/thing = instance_atom(members[index], members_attributes[index], crds, no_changeturf)
+ atoms_to_initialise += thing
+ if(isturf(thing))
+ turfs_to_mark_modified += thing
+
//Restore initialization to the previous valsue
SSatoms.map_loader_stop()
var/datum/grid_load_metadata/M = new
M.atoms_to_initialise = atoms_to_initialise
M.atoms_to_delete = atoms_to_delete
+ M.turfs_to_mark_modified = turfs_to_mark_modified
return M
////////////////
@@ -387,12 +412,15 @@ var/global/dmm_suite/preloader/_preloader = new
////////////////
//Instance an atom at (x,y,z) and gives it the variables in attributes
-/dmm_suite/proc/instance_atom(path,list/attributes, turf/crds, no_changeturf)
+/dmm_suite/proc/instance_atom(path, list/attributes, turf/crds, no_changeturf)
global._preloader.setup(attributes, path)
if(crds)
- if(!no_changeturf && ispath(path, /turf))
- . = crds.ChangeTurf(path, FALSE, TRUE)
+ if(ispath(path, /turf))
+ if(no_changeturf)
+ . = new path(crds)
+ else
+ . = crds.ChangeTurf(path, FALSE, TRUE)
else
. = new path(crds)// preloader called from atom/New
diff --git a/code/modules/mob/grab/grab_object.dm b/code/modules/mob/grab/grab_object.dm
index f4861ede315f..c4a5015aa4d5 100644
--- a/code/modules/mob/grab/grab_object.dm
+++ b/code/modules/mob/grab/grab_object.dm
@@ -77,6 +77,10 @@
if(affecting_mob && assailant?.check_intent(I_FLAG_HARM))
upgrade(TRUE)
+/obj/item/grab/ShouldSerialize(_age)
+ SHOULD_CALL_PARENT(FALSE)
+ return FALSE
+
/obj/item/grab/mob_can_unequip(mob/user, slot, disable_warning = FALSE, dropping = FALSE)
if(dropping)
return TRUE
diff --git a/code/modules/mob/living/human/human.dm b/code/modules/mob/living/human/human.dm
index 8f21c27edd60..0af52080fbb1 100644
--- a/code/modules/mob/living/human/human.dm
+++ b/code/modules/mob/living/human/human.dm
@@ -9,7 +9,10 @@
/mob/living/human/Initialize(mapload, species_uid, datum/mob_snapshot/supplied_appearance)
+ // Health is dynamically calculated from organ state, so no point keeping a
+ // serialized or modified value, it will be recalculated almost immediately.
current_health = get_max_health()
+ species_uid ||= species // Pass our current species in as an arg (in case of serde)
reset_hud_overlays()
var/list/newargs = args.Copy(2)
setup_human(arglist(newargs))
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 36a2184fdf0c..43c86171d045 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -1,6 +1,8 @@
/mob/living/Initialize()
- current_health = get_max_health()
+ if(isnull(current_health) || current_health == INFINITY)
+ current_health = get_max_health()
+
original_fingerprint_seed = sequential_id(/mob)
fingerprint = md5(num2text(original_fingerprint_seed))
original_genetic_seed = sequential_id(/mob)
diff --git a/code/modules/mob/living/simple_animal/_simple_animal.dm b/code/modules/mob/living/simple_animal/_simple_animal.dm
index ab53e5630e3f..ce219e75bbd9 100644
--- a/code/modules/mob/living/simple_animal/_simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/_simple_animal.dm
@@ -1,4 +1,3 @@
-
/mob/living/simple_animal
name = "animal"
max_health = 20
@@ -114,6 +113,11 @@
/mob/living/simple_animal/Initialize()
. = ..()
+ // Deserialize any JSON payload for our overlays.
+ if(istext(draw_visible_overlays))
+ draw_visible_overlays = cached_json_decode(draw_visible_overlays)
+ if(!islist(draw_visible_overlays))
+ draw_visible_overlays = null
if(isnull(draw_visible_overlays))
var/list/defaults = get_default_animal_colours()
draw_visible_overlays = defaults?.Copy() // do not mutate static list
diff --git a/code/modules/mob/living/simple_animal/passive/deer.dm b/code/modules/mob/living/simple_animal/passive/deer.dm
index c9590c54714d..cdb5d4895cbe 100644
--- a/code/modules/mob/living/simple_animal/passive/deer.dm
+++ b/code/modules/mob/living/simple_animal/passive/deer.dm
@@ -96,10 +96,10 @@
desc = "A fleet-footed forest animal known for a love of vtubers."
/mob/living/simple_animal/passive/deer/sparkle/Initialize()
- draw_visible_overlays = list(
+ draw_visible_overlays ||= list(
"base" = get_random_colour(),
"markings" = get_random_colour(TRUE),
"socks" = get_random_colour()
)
- eye_color = get_random_colour(TRUE)
+ eye_color ||= get_random_colour(TRUE)
. = ..()
diff --git a/code/modules/mob/living/simple_animal/passive/fox.dm b/code/modules/mob/living/simple_animal/passive/fox.dm
index 09567e7abf19..d8f8ad225271 100644
--- a/code/modules/mob/living/simple_animal/passive/fox.dm
+++ b/code/modules/mob/living/simple_animal/passive/fox.dm
@@ -81,11 +81,11 @@
desc = "A cunning and graceful predatory mammal, known for being really into hardstyle."
/mob/living/simple_animal/passive/fox/sparkle/Initialize()
- draw_visible_overlays = list(
+ draw_visible_overlays ||= list(
"base" = get_random_colour(),
"markings" = get_random_colour(TRUE),
"socks" = get_random_colour()
)
- eye_color = get_random_colour(TRUE)
+ eye_color ||= get_random_colour(TRUE)
. = ..()
diff --git a/code/modules/mob/living/simple_animal/passive/rabbit.dm b/code/modules/mob/living/simple_animal/passive/rabbit.dm
index 4fe5386ebc36..32676d4ab40d 100644
--- a/code/modules/mob/living/simple_animal/passive/rabbit.dm
+++ b/code/modules/mob/living/simple_animal/passive/rabbit.dm
@@ -60,10 +60,10 @@
desc = "A hopping mammal with long ears and a love for raves."
/mob/living/simple_animal/passive/rabbit/sparkle/Initialize()
- draw_visible_overlays = list(
+ draw_visible_overlays ||= list(
"base" = get_random_colour(),
"markings" = get_random_colour(TRUE),
"socks" = get_random_colour()
)
- eye_color = get_random_colour(TRUE)
+ eye_color ||= get_random_colour(TRUE)
. = ..()
diff --git a/code/modules/mob/living/simple_animal/passive/wolf.dm b/code/modules/mob/living/simple_animal/passive/wolf.dm
index aae33697bfb8..cbe5c004b259 100644
--- a/code/modules/mob/living/simple_animal/passive/wolf.dm
+++ b/code/modules/mob/living/simple_animal/passive/wolf.dm
@@ -35,10 +35,10 @@
desc = "A predatory canine commonly known to watch speedruns and take party drugs."
/mob/living/simple_animal/passive/wolf/sparkle/Initialize()
- draw_visible_overlays = list(
+ draw_visible_overlays ||= list(
"base" = get_random_colour(),
"markings" = get_random_colour(TRUE),
"socks" = get_random_colour()
)
- eye_color = get_random_colour(TRUE)
+ eye_color ||= get_random_colour(TRUE)
. = ..()
diff --git a/code/modules/mob/living/simple_animal/simple_animal_serde.dm b/code/modules/mob/living/simple_animal/simple_animal_serde.dm
new file mode 100644
index 000000000000..3474b7c868d9
--- /dev/null
+++ b/code/modules/mob/living/simple_animal/simple_animal_serde.dm
@@ -0,0 +1,35 @@
+// Very basic serde for simple animals for things like the Shaded Hills submap.
+/mob/living/simple_animal/ShouldSerialize(_age)
+ return simulated
+
+/mob/living/simple_animal/GetPossiblySerializableInstances()
+ return list(src)
+
+/mob/living/simple_animal/Serialize()
+ . = ..()
+
+ SERIALIZE_IF_MODIFIED(name, /mob/living/simple_animal)
+ SERIALIZE_IF_MODIFIED(desc, /mob/living/simple_animal)
+ SERIALIZE_IF_MODIFIED(icon_state, /mob/living/simple_animal)
+
+ SERIALIZE_IF_MODIFIED(purge, /mob/living/simple_animal)
+ SERIALIZE_IF_MODIFIED(in_stasis, /mob/living/simple_animal)
+ SERIALIZE_IF_MODIFIED(eye_color, /mob/living/simple_animal)
+ SERIALIZE_IF_MODIFIED(brute_damage, /mob/living/simple_animal)
+ SERIALIZE_IF_MODIFIED(burn_damage, /mob/living/simple_animal)
+ SERIALIZE_IF_MODIFIED(gene_damage, /mob/living/simple_animal)
+
+ var/list/defaults = get_default_animal_colours()
+ var/changed_from_defaults = length(defaults) != length(draw_visible_overlays)
+ if(!changed_from_defaults && islist(draw_visible_overlays))
+ for(var/animal_color in defaults)
+ if(!(animal_color in draw_visible_overlays) || defaults[animal_color] != draw_visible_overlays[animal_color])
+ changed_from_defaults = TRUE
+ break
+ if(!changed_from_defaults)
+ for(var/animal_color in draw_visible_overlays)
+ if(!(animal_color in defaults) || defaults[animal_color] != draw_visible_overlays[animal_color])
+ changed_from_defaults = TRUE
+ break
+ if(changed_from_defaults)
+ SERIALIZE_VALUE(draw_visible_overlays, /mob/living/simple_animal, json_encode(draw_visible_overlays))
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index dadb2de1b99d..d1a23de71358 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -1603,4 +1603,3 @@ var/global/const/ACTION_DANGER_ALL = 2
// Returns true if the mob is cloaked, otherwise false
/mob/proc/is_cloaked()
return FALSE
-
diff --git a/code/modules/mob/mob_serde.dm b/code/modules/mob/mob_serde.dm
new file mode 100644
index 000000000000..d6bb36e8c970
--- /dev/null
+++ b/code/modules/mob/mob_serde.dm
@@ -0,0 +1,8 @@
+// Prevent all mob serde for the time being.
+// Equipment handling and the like needs a lot of work to implement.
+/mob/ShouldSerialize(_age)
+ SHOULD_CALL_PARENT(FALSE)
+ return FALSE
+
+/mob/GetPossiblySerializableInstances()
+ return null
diff --git a/code/modules/mob/mob_snapshot.dm b/code/modules/mob/mob_snapshot.dm
index 01398160314a..c876a47857c6 100644
--- a/code/modules/mob/mob_snapshot.dm
+++ b/code/modules/mob/mob_snapshot.dm
@@ -19,16 +19,19 @@
/datum/mob_snapshot/New(mob/living/donor, genetic_info_only = FALSE)
- real_name = donor?.real_name || "unknown"
- eye_color = donor?.get_eye_colour() || COLOR_BLACK
- blood_type = donor?.get_blood_type()
- unique_enzymes = donor?.get_unique_enzymes()
- skin_color = donor?.get_skin_colour()
- skin_tone = donor?.get_skin_tone()
- fingerprint = donor?.get_full_print(ignore_blockers = TRUE)
+ if(!istype(donor))
+ return
- root_species = donor?.get_species() || decls_repository.get_decl_by_id(global.using_map.default_species)
- root_bodytype = donor?.get_bodytype() || root_species.default_bodytype
+ real_name = donor.real_name || "unknown"
+ eye_color = donor.get_eye_colour() || COLOR_BLACK
+ blood_type = donor.get_blood_type()
+ unique_enzymes = donor.get_unique_enzymes()
+ skin_color = donor.get_skin_colour()
+ skin_tone = donor.get_skin_tone()
+ fingerprint = donor.get_full_print(ignore_blockers = TRUE)
+
+ root_species = donor.get_species() || decls_repository.get_decl_by_id(global.using_map.default_species)
+ root_bodytype = donor.get_bodytype() || root_species.default_bodytype
for(var/obj/item/organ/external/limb in donor?.get_external_organs())
// Discard anything not relating to our core/original bodytype and species.
@@ -113,4 +116,5 @@
return TRUE
/mob/proc/get_mob_snapshot(check_dna = FALSE)
+ RETURN_TYPE(/datum/mob_snapshot)
return (!check_dna || has_genetic_information()) ? new /datum/mob_snapshot(src, genetic_info_only = check_dna) : null
diff --git a/code/modules/multiz/level_data.dm b/code/modules/multiz/level_data.dm
index d291f1194227..dffc97367227 100644
--- a/code/modules/multiz/level_data.dm
+++ b/code/modules/multiz/level_data.dm
@@ -170,9 +170,6 @@
/// Note that this is more or less unnecessary if you are using a mapped area that doesn't stretch to the edge of the level.
var/template_edge_padding = 15
- // Whether or not this level permits things like graffiti and filth to persist across rounds.
- var/permit_persistence = FALSE
-
// Submap loading values, passed back via getters like get_subtemplate_budget().
/// A point budget to spend on subtemplates (see template costs)
var/subtemplate_budget = 0
@@ -181,6 +178,9 @@
/// A specific area to use when determining where to place subtemplates.
var/subtemplate_area = null
+ /// Set to TRUE if this level persistence data, FALSE if not. Check is only done if _has_serde_data is null.
+ VAR_PRIVATE/_has_serde_data = null
+
/datum/level_data/New(var/_z_level, var/defer_level_setup = FALSE)
. = ..()
level_z = _z_level
@@ -232,6 +232,7 @@
///Prepare level for being used. Setup borders, lateral z connections, ambient lighting, atmosphere, etc..
/datum/level_data/proc/setup_level_data(var/skip_gen = FALSE)
+
if(_level_setup_completed)
log_debug("level_data for [src], on z [level_z], had setup_level_data called more than once!")
return //Since we can defer setup, make sure we only setup once
@@ -444,24 +445,35 @@
if(base_area)
return list(base_area)
+// If we do serde, that implies we don't want to apply another layer of procgen over what's already saved.
+// Specific levels should override this proc as needed for generation independent of serde.
+/datum/level_data/proc/should_generate_level()
+ if(!is_persistent())
+ return TRUE
+ if(!get_config_value(/decl/config/toggle/roundstart_level_generation) || !(get_subtemplate_budget() || length(level_generators)))
+ return FALSE
+ if(isnull(_has_serde_data))
+ var/decl/serialization_handler/handler = RESOLVE_TO_DECL(persistence_handler)
+ _has_serde_data = fexists(handler.get_data_path(persistent_data_location, global.using_map.path, ckey(level_id)))
+ return !_has_serde_data
+
///Called when setting up the level. Apply generators and anything that modifies the turfs of the level.
/datum/level_data/proc/generate_level()
-
- if(!get_config_value(/decl/config/toggle/roundstart_level_generation))
+ if(!should_generate_level())
+ report_progress("Skipping [_has_serde_data ? "post-serde " : ""]level generation for [level_id].")
return
-
var/origx = level_inner_min_x
var/origy = level_inner_min_y
var/endx = level_inner_min_x + level_inner_width
var/endy = level_inner_min_y + level_inner_height
-
// Run level generators.
for(var/gen_type in level_generators)
+ report_progress("Placing [gen_type] on [level_id]...")
new gen_type(origx, origy, level_z, endx, endy, FALSE, TRUE, get_base_area_instance())
-
// Place points of interest.
var/budget = get_subtemplate_budget()
if(budget)
+ report_progress("Placing subtemplates on [level_id]...")
spawn_subtemplates(budget, get_subtemplate_category(), get_subtemplate_blacklist(), get_subtemplate_whitelist())
///Apply the parent entity's map generators. (Planets generally)
@@ -741,14 +753,14 @@ INITIALIZE_IMMEDIATE(/obj/abstract/level_data_spawner)
/datum/level_data/main_level
level_flags = (ZLEVEL_STATION|ZLEVEL_CONTACT|ZLEVEL_PLAYER)
- permit_persistence = TRUE
+ permit_legacy_persistence = TRUE
/datum/level_data/admin_level
level_flags = (ZLEVEL_ADMIN|ZLEVEL_SEALED)
/datum/level_data/player_level
level_flags = (ZLEVEL_CONTACT|ZLEVEL_PLAYER)
- permit_persistence = TRUE
+ permit_legacy_persistence = TRUE
/datum/level_data/unit_test
level_flags = (ZLEVEL_CONTACT|ZLEVEL_PLAYER|ZLEVEL_SEALED)
diff --git a/code/modules/multiz/level_persistence_handler.dm b/code/modules/multiz/level_persistence_handler.dm
new file mode 100644
index 000000000000..39337f5c246d
--- /dev/null
+++ b/code/modules/multiz/level_persistence_handler.dm
@@ -0,0 +1,14 @@
+/decl/serialization_handler/proc/get_data_path(location, map, level)
+ return "[location]/[map]/[level]"
+
+/decl/serialization_handler/proc/save_level_data(datum/level_data/level_data, location, map, level)
+ return // Unimplemented, so return null to indicate a failure
+
+/decl/serialization_handler/proc/save_data_to_file(filepath, save_data, report_id)
+ return // Unimplemented, so return null to indicate a failure
+
+/decl/serialization_handler/proc/load_level_data(location, map, level)
+ return // Unimplemented, so return null to indicate a failure
+
+/decl/serialization_handler/proc/load_data_from_file(filepath)
+ return // Unimplemented, so return null to indicate a failure
diff --git a/code/modules/multiz/level_persistence_handler_json.dm b/code/modules/multiz/level_persistence_handler_json.dm
new file mode 100644
index 000000000000..bebf101a2b78
--- /dev/null
+++ b/code/modules/multiz/level_persistence_handler_json.dm
@@ -0,0 +1,46 @@
+/decl/serialization_handler/json/get_data_path(location, map, level)
+ return "[..()].json"
+
+/decl/serialization_handler/json/save_level_data(datum/level_data/level_data, location, map, level)
+ return save_data_to_file(get_data_path(location, map, level), level_data.get_persistent_data(), level_data.level_id)
+
+/decl/serialization_handler/json/save_data_to_file(filepath, save_data, report_id)
+ try
+ if(!length(save_data))
+ return 1
+
+ var/write_data = json_encode(save_data)
+ var/write_file = file(filepath)
+
+ // Do a backup (at the end to avoid overwriting then throwing an exception)
+ if(fexists(filepath))
+ var/backup_contents = file2text(filepath)
+ var/backup_file = file("[filepath].[time2text(REALTIMEOFDAY, "YY-MM-DD_hh-mm")].backup")
+ to_file(backup_file, backup_contents)
+ // Clear old file to avoid appending data.
+ // TODO: remove old backups? Leave as an exercise for the admin?
+ fdel(filepath)
+
+ // Finally, write out our new json.
+ to_file(write_file, write_data)
+ report_progress("Saved [length(save_data)] record\s for [report_id].")
+
+ catch(var/exception/E)
+ error("Exception when saving persistent level data to [filepath]: [EXCEPTION_TEXT(E)]")
+ return null
+
+ return 1 // Return a non-null value just to show we didn't throw an exception.
+
+/decl/serialization_handler/json/load_level_data(location, map, level)
+ return load_data_from_file(get_data_path(location, map, level))
+
+/decl/serialization_handler/json/load_data_from_file(filepath)
+ try
+ if(fexists(filepath)) // done separately to avoid generating an error for levels with no saved data
+ var/loaded_json = safe_file2text(filepath)
+ if(loaded_json)
+ return json_decode(loaded_json) // do not cache this giant blob pls
+ catch(var/exception/E)
+ error("Exception when loading persistent level data from [filepath]: [EXCEPTION_TEXT(E)]")
+ return null
+ return 1 // Return a non-null value just to show we didn't throw an exception.
\ No newline at end of file
diff --git a/code/modules/multiz/level_persistence_serialization.dm b/code/modules/multiz/level_persistence_serialization.dm
new file mode 100644
index 000000000000..a72affa8dede
--- /dev/null
+++ b/code/modules/multiz/level_persistence_serialization.dm
@@ -0,0 +1,99 @@
+// General flow of level persistence:
+// Saving:
+// - SSpersistence periodically iterates the z-level list, finds levels that want to serde, and calls save_persistent_data()
+// - Levels return a list of instances to get_persistent_instances(), instances have Serialize() called and return a list of modified fields.
+// - Fields are serialized (to JSON with the default handler) and written to disk.
+// Loading:
+// - SSmapping initializes and calls preload_persistent_data() and load_persistent_data() on relevant /datum/level_data z-level objects.
+// - load_persistent_data() creates the base instances and (for atoms) sets __init_deserialisation_payload with the data loaded from tile.
+// - SSatoms flush calls Preload() on all deserialized atoms which pre-populates vars on the atom.
+// - Ssatoms proceeds to Initialize() atoms as normal.
+
+var/global/list/level_persistence_ref_map = list()
+/datum/level_data
+ /// String indicating a location for use in serde. Typically a filepath,
+ /// but not strictly required to be. Implementation is on the handler.
+ /// If set, will automatically suffix map path and level name.
+ /// Leave null to opt out of any persistence for this level.
+ // Example setting would be:
+ // persistent_data_location = "data/level_data"
+ VAR_PROTECTED/persistent_data_location
+ /// Decl handler, mostly forcing myself to keep this general so it can be optimized with a DB or something down the track.
+ VAR_PRIVATE/persistence_handler = /decl/serialization_handler/json
+ /// 2D list of coordinates for turfs to serialize.
+ var/list/changed_turfs
+ /// Legacy bool. Whether or not this level permits things like graffiti and filth to persist across rounds.
+ var/permit_legacy_persistence = FALSE
+
+/datum/level_data/proc/is_persistent()
+ return !isnull(persistent_data_location) && !isnull(persistence_handler) && !isnull(level_id)
+
+/datum/level_data/proc/get_persistent_data()
+ . = list()
+ var/list/instances_to_save = get_persistent_instances()
+ if(!length(instances_to_save))
+ return
+ for(var/datum/thing as anything in get_persistent_instances())
+ var/serialized_instance = thing.Serialize()
+ if(length(serialized_instance))
+ .[thing.get_run_uid()] = serialized_instance
+
+// Returns a linear list of instances that we are interested in saving.
+/datum/level_data/proc/get_persistent_instances()
+ for(var/coord in changed_turfs)
+
+ var/list/coord_list = cached_json_decode(coord)
+ if(!islist(coord_list) || length(coord_list) < 2)
+ changed_turfs -= coord
+ continue
+
+ var/turf/turf = locate(coord_list[1], coord_list[2], level_z)
+ if(!istype(turf) || !turf.ShouldSerialize())
+ continue
+
+ for(var/datum/instance in turf.UnpackSerializableInstances())
+ if(instance.ShouldSerialize())
+ LAZYDISTINCTADD(., instance)
+
+// First load all the raw data into memory so every reference is populated.
+/datum/level_data/proc/preload_persistent_data()
+
+ // Basic sanity check.
+ if(persistent_data_location && !level_id)
+ persistent_data_location = null
+ PRINT_STACK_TRACE("Level data [type] tried to initialize persistent data but had no level_id.")
+ return FALSE
+
+ // Don't bother if we aren't configured for it at all.
+ if(!is_persistent())
+ return FALSE
+
+ // Atoms on a map are expected to be returned as an associative list with some specific text keys.
+ try
+ var/decl/serialization_handler/load_handler = GET_DECL(persistence_handler)
+ var/list/loaded_data = load_handler?.load_level_data(persistent_data_location, global.using_map.path, ckey(level_id))
+ if(islist(loaded_data) && length(loaded_data))
+ var/list/instance_map = list()
+ global.level_persistence_ref_map[level_id] = instance_map
+ for(var/uid in loaded_data)
+ instance_map[uid] = loaded_data[uid]
+ return TRUE
+ catch(var/exception/E)
+ PRINT_STACK_TRACE("Exception during '[level_id]' preload: [EXCEPTION_TEXT(E)]")
+
+ return FALSE
+
+// Now create the instances and register them in the global map. Note that levels
+// with no level_id or no persistence handling set will not reach this proc.
+// Returns TRUE if it loaded anything; this may imply not needing to run level
+// generation for this level (random maps, etc)
+/datum/level_data/proc/load_persistent_data()
+ _has_serde_data = length(instantiate_serialized_data(level_z, "[level_id]/[name]", global.level_persistence_ref_map[level_id])) > 0
+ return _has_serde_data
+
+// Write any data out if we need to.
+/datum/level_data/proc/save_persistent_data()
+ // TODO: block any changes to persistent data structures while save is running?
+ if(is_persistent())
+ var/decl/serialization_handler/save_handler = GET_DECL(persistence_handler)
+ save_handler?.save_level_data(src, persistent_data_location, global.using_map.path, ckey(level_id))
diff --git a/code/modules/multiz/turf_mimic_edge.dm b/code/modules/multiz/turf_mimic_edge.dm
index 512b1f7fd629..fb33c094c7c0 100644
--- a/code/modules/multiz/turf_mimic_edge.dm
+++ b/code/modules/multiz/turf_mimic_edge.dm
@@ -27,7 +27,6 @@
////////////////////////////////
// Simulated Mimic Edges
////////////////////////////////
-
///Simulated turf meant to replicate the appearence of another.
/turf/mimic_edge
name = MIMIC_EDGE_NAME
@@ -61,6 +60,13 @@
QDEL_NULL(click_eater) //Make sure we get rid of it if the turf is somehow replaced by map gen to prevent them accumulating.
return ..()
+/turf/mimic_edge/ShouldSerialize(_age)
+ SHOULD_CALL_PARENT(FALSE)
+ return FALSE
+
+/turf/mimic_edge/GetPossiblySerializableInstances()
+ return null
+
/turf/mimic_edge/Crossed(atom/movable/O)
. = ..()
if(isobserver(O))
diff --git a/code/modules/organs/internal/lungs.dm b/code/modules/organs/internal/lungs.dm
index fa5cbde82863..aeea20125422 100644
--- a/code/modules/organs/internal/lungs.dm
+++ b/code/modules/organs/internal/lungs.dm
@@ -34,10 +34,20 @@
QDEL_NULL(inhaled)
. = ..()
-/obj/item/organ/internal/lungs/initialize_reagents(populate)
+/obj/item/organ/internal/lungs/Serialize()
+ . = ..()
+ SERIALIZE_REAGENTS(inhaled, /obj/item/organ/internal/lungs, "inhaled")
+
+/obj/item/organ/internal/lungs/Deserialize(list/instance_map)
+ . = ..()
+ DESERIALIZE_REAGENTS(inhaled, "inhaled")
+
+/obj/item/organ/internal/lungs/initialize_reagents()
+ FINALIZE_REAGENTS_SERDE(inhaled)
if(!inhaled)
- inhaled = new/datum/reagents/metabolism(240, (owner || src), CHEM_INHALE)
- REAGENT_SET_ATOM(inhaled, src)
+ inhaled = new/datum/reagents/metabolism(240, src, CHEM_INHALE)
+ var/owner_atom = owner || src
+ REAGENT_SET_ATOM(inhaled, owner_atom)
. = ..()
/obj/item/organ/internal/lungs/do_install(mob/living/human/target, obj/item/organ/external/affected, in_place)
diff --git a/code/modules/organs/internal/stomach.dm b/code/modules/organs/internal/stomach.dm
index 5c98aeea901d..52016ae3967b 100644
--- a/code/modules/organs/internal/stomach.dm
+++ b/code/modules/organs/internal/stomach.dm
@@ -12,6 +12,14 @@
QDEL_NULL(ingested)
. = ..()
+/obj/item/organ/internal/stomach/Serialize()
+ . = ..()
+ SERIALIZE_REAGENTS(ingested, /obj/item/organ/internal/stomach, "ingested")
+
+/obj/item/organ/internal/stomach/Deserialize(list/instance_map)
+ . = ..()
+ DESERIALIZE_REAGENTS(ingested, "ingested")
+
/obj/item/organ/internal/stomach/set_species(species_uid)
if(species?.gluttonous)
verbs -= /obj/item/organ/internal/stomach/proc/throw_up
@@ -21,10 +29,12 @@
if(species && !stomach_capacity)
stomach_capacity = species.stomach_capacity
-/obj/item/organ/internal/stomach/initialize_reagents(populate)
+/obj/item/organ/internal/stomach/initialize_reagents()
+ FINALIZE_REAGENTS_SERDE(ingested)
if(!ingested)
- ingested = new/datum/reagents/metabolism(240, (owner || src), CHEM_INGEST)
- REAGENT_SET_ATOM(ingested, src)
+ ingested = new/datum/reagents/metabolism(240, src, CHEM_INGEST)
+ var/owner_atom = owner || src
+ REAGENT_SET_ATOM(ingested, owner_atom)
. = ..()
/obj/item/organ/internal/stomach/do_install()
diff --git a/code/modules/paperwork/paper.dm b/code/modules/paperwork/_paper.dm
similarity index 99%
rename from code/modules/paperwork/paper.dm
rename to code/modules/paperwork/_paper.dm
index 034c88c3ae93..af411daf7cd4 100644
--- a/code/modules/paperwork/paper.dm
+++ b/code/modules/paperwork/_paper.dm
@@ -43,7 +43,7 @@
/obj/item/paper/Initialize(mapload, material_key, var/_text, var/_title, var/list/md = null)
. = ..(mapload, material_key)
- set_content(_text ? _text : info, _title)
+ set_content(_text ? _text : info, _title ? _title : name)
if(md)
LAZYDISTINCTADD(metadata, md) //Merge them
if(!mapload && persist_on_init)
diff --git a/code/modules/paperwork/_paper_serde.dm b/code/modules/paperwork/_paper_serde.dm
new file mode 100644
index 000000000000..8fbfe734b9bb
--- /dev/null
+++ b/code/modules/paperwork/_paper_serde.dm
@@ -0,0 +1,30 @@
+/obj/item/paper/Serialize()
+ . = ..()
+ SERIALIZE_IF_MODIFIED(age, /obj/item/paper)
+ SERIALIZE_IF_MODIFIED(is_crumpled, /obj/item/paper)
+ SERIALIZE_IF_MODIFIED(last_modified_ckey, /obj/item/paper)
+ SERIALIZE_IF_MODIFIED(name, /obj/item/paper)
+ SERIALIZE_IF_MODIFIED(info, /obj/item/paper)
+
+/obj/item/paper/Deserialize()
+ . = ..()
+ SSpersistence.track_value(src, /decl/persistence_handler/paper)
+
+/obj/item/paper/ShouldSerialize(_age)
+ return ..() && (isnull(_age) || age < _age)
+
+/obj/item/paper/GetPossiblySerializableInstances()
+ . = ..()
+ if(istype(loc, /obj/structure/noticeboard))
+ LAZYDISTINCTADD(., loc)
+
+// If it's old enough we start to trim down any textual information and scramble strings.
+#define SERDE_MESSAGE nameof(/obj/item/paper::info)
+/obj/item/paper/HandlePersistentDecay(entries_decay_at, entry_decay_weight)
+ __deserialization_payload[SERDE_MESSAGE] = apply_serde_message_decay(
+ __deserialization_payload[SERDE_MESSAGE],
+ __deserialization_payload[nameof(/obj/item/paper::age)],
+ entry_decay_weight,
+ entries_decay_at
+ )
+#undef SERDE_MESSAGE
diff --git a/code/modules/persistence/filth.dm b/code/modules/persistence/filth.dm
index ab432d17a7b1..07af39cfc8d9 100644
--- a/code/modules/persistence/filth.dm
+++ b/code/modules/persistence/filth.dm
@@ -1,12 +1,12 @@
/obj/effect/decal/cleanable/filth
- name = "filth"
- desc = "Disgusting. Someone from last shift didn't do their job properly."
- icon = 'icons/effects/blood.dmi'
- icon_state = "mfloor1"
- random_icon_states = list("mfloor1", "mfloor2", "mfloor3", "mfloor4", "mfloor5", "mfloor6", "mfloor7")
- color = "#464f33"
- persistent = TRUE
- anchored = TRUE
+ name = "filth"
+ desc = "Disgusting. Someone from last shift didn't do their job properly."
+ icon = 'icons/effects/blood.dmi'
+ icon_state = "mfloor1"
+ random_icon_states = list("mfloor1", "mfloor2", "mfloor3", "mfloor4", "mfloor5", "mfloor6", "mfloor7")
+ color = "#464f33"
+ use_legacy_persistence = TRUE
+ anchored = TRUE
/obj/effect/decal/cleanable/filth/Initialize()
. = ..()
diff --git a/code/modules/persistence/graffiti.dm b/code/modules/persistence/graffiti.dm
index cb65ce6ca0c3..6c6515ec6ba8 100644
--- a/code/modules/persistence/graffiti.dm
+++ b/code/modules/persistence/graffiti.dm
@@ -10,10 +10,39 @@
anchored = TRUE
var/message
- var/graffiti_age = 0
var/author = "unknown"
+/obj/effect/decal/writing/Serialize()
+ . = ..()
+ SERIALIZE_IF_MODIFIED(message, /obj/effect/decal/writing)
+ SERIALIZE_IF_MODIFIED(author, /obj/effect/decal/writing)
+
+// If it's old enough we start to trim down any textual information and scramble strings.
+#define SERDE_MESSAGE nameof(/obj/effect/decal/writing::message)
+/obj/effect/decal/writing/HandlePersistentDecay(entries_decay_at, entry_decay_weight)
+ var/original_message = __deserialization_payload[SERDE_MESSAGE]
+ var/decayed_message = apply_serde_message_decay(
+ __deserialization_payload[SERDE_MESSAGE],
+ __deserialization_payload[nameof(/obj/effect/decal::age)],
+ entry_decay_weight,
+ entries_decay_at
+ )
+ to_world_log("decayed graffifi: [original_message] -> [decayed_message]")
+ __deserialization_payload[SERDE_MESSAGE] = decayed_message
+#undef SERDE_MESSAGE
+
/obj/effect/decal/writing/Initialize(mapload, var/_age, var/_message, var/_author)
+
+ var/turf/checking_turf = loc
+ if(istype(checking_turf) && !checking_turf.can_engrave())
+ return INITIALIZE_HINT_QDEL
+
+ var/too_much_graffiti = 0
+ for(var/obj/effect/decal/writing/writing in loc)
+ too_much_graffiti++
+ if(too_much_graffiti >= 5)
+ return INITIALIZE_HINT_QDEL
+
var/list/random_icon_states = get_states_in_icon(icon)
for(var/obj/effect/decal/writing/writing in loc)
random_icon_states -= writing.icon_state
@@ -22,9 +51,10 @@
SSpersistence.track_value(src, /decl/persistence_handler/graffiti)
. = ..(mapload)
if(!isnull(_age))
- graffiti_age = _age
- message = _message
- if(!isnull(author))
+ age = _age
+ if(_message && !message)
+ message = _message
+ if(_author && !author)
author = _author
/obj/effect/decal/writing/Destroy()
diff --git a/code/modules/persistence/noticeboards.dm b/code/modules/persistence/noticeboards.dm
index 9ec2db2954f9..02daf6cc11c2 100644
--- a/code/modules/persistence/noticeboards.dm
+++ b/code/modules/persistence/noticeboards.dm
@@ -19,10 +19,15 @@
// Grab any mapped notices.
if(ml)
- for(var/obj/item/paper/note in get_turf(src))
+ for(var/obj/item/paper/note in contents)
add_paper(note, skip_icon_update = TRUE)
if(LAZYLEN(notices) >= max_notices)
break
+ if(LAZYLEN(notices) < max_notices)
+ for(var/obj/item/paper/note in get_turf(src))
+ add_paper(note, skip_icon_update = TRUE)
+ if(LAZYLEN(notices) >= max_notices)
+ break
// Automatically place noticeboards that aren't mapped to specific positions.
if(default_pixel_x == 0 && default_pixel_y == 0)
diff --git a/code/modules/persistence/persistence_datum.dm b/code/modules/persistence/persistence_datum.dm
index 08acd039c57a..ac7b5973c3dc 100644
--- a/code/modules/persistence/persistence_datum.dm
+++ b/code/modules/persistence/persistence_datum.dm
@@ -11,6 +11,9 @@
var/has_admin_data // If set, shows up on the admin persistence panel.
var/ignore_area_flags = FALSE // Set to TRUE to skip area flag checks such as nonpersistent areas.
var/ignore_invalid_loc = FALSE // Set to TRUE to skip checking for a non-null station turf for the entry.
+ var/list/legacy_map_values // A list of legacy keys to new keys.
+ var/legacy_type
+ var/serialization_handler = /decl/serialization_handler/json // Which serialization handler to use for load/save
/decl/persistence_handler/proc/SetFilename()
if(name)
@@ -18,80 +21,22 @@
if(!isnull(entries_decay_at) && !isnull(entries_expire_at))
entries_decay_at = floor(entries_expire_at * entries_decay_at)
-/decl/persistence_handler/proc/GetValidTurf(var/turf/T, var/list/tokens)
- if(T && isStationLevel(T.z) && CheckTurfContents(T, tokens))
- return T
-
-/decl/persistence_handler/proc/CheckTurfContents(var/turf/T, var/list/tokens)
- return TRUE
-
-/decl/persistence_handler/proc/CheckTokenSanity(var/list/tokens)
- if(!islist(tokens))
- return FALSE
- if(isnull(tokens["x"]) || isnull(tokens["y"]) || isnull(tokens["z"]))
- return FALSE
- if(!isnull(entries_expire_at))
- if(isnull(tokens["age"]))
- return FALSE
- if(tokens["age"] > entries_expire_at)
- return FALSE
- return TRUE
-
-/decl/persistence_handler/proc/CreateEntryInstance(var/turf/creating, var/list/tokens)
- return
-
-/decl/persistence_handler/proc/ProcessAndApplyTokens(var/list/tokens)
-
- // If it's old enough we start to trim down any textual information and scramble strings.
- if(tokens["message"] && !isnull(entries_decay_at) && !isnull(entry_decay_weight))
- var/_n = tokens["age"]
- var/_message = tokens["message"]
- if(_n >= entries_decay_at)
- var/decayed_message = ""
- for(var/i = 1 to length(_message))
- var/char = copytext(_message, i, i + 1)
- if(prob(round(_n * entry_decay_weight)))
- if(prob(99))
- decayed_message += pick(".",",","-","'","\\","/","\"",":",";")
- else
- decayed_message += char
- _message = decayed_message
- if(length(_message))
- tokens["message"] = _message
- else
- return
-
- . = GetValidTurf(locate(tokens["x"], tokens["y"], tokens["z"]), tokens)
- if(.)
- . = CreateEntryInstance(., tokens)
-
/decl/persistence_handler/proc/IsValidEntry(var/atom/entry)
if(!istype(entry))
return FALSE
- if(!isnull(entries_expire_at) && GetEntryAge(entry) >= entries_expire_at)
+ if(!entry.ShouldSerialize(entries_expire_at))
return FALSE
var/turf/T = get_turf(entry)
if(!ignore_invalid_loc && (!T || !isStationLevel(T.z)))
return FALSE
var/area/A = get_area(T)
- if(!ignore_area_flags && (!A || (A.area_flags & AREA_FLAG_IS_NOT_PERSISTENT)))
+ if(!ignore_area_flags && (!A || (A.area_flags & AREA_FLAG_NO_LEGACY_PERSISTENCE)))
return FALSE
return TRUE
/decl/persistence_handler/proc/GetEntryAge(var/atom/entry)
return 0
-/decl/persistence_handler/proc/CompileEntry(var/atom/entry)
- var/turf/T = get_turf(entry)
- . = list()
- .["x"] = T?.x || 0
- .["y"] = T?.y || 0
- .["z"] = T?.z || 0
- .["age"] = GetEntryAge(entry)
-
-/decl/persistence_handler/proc/FinalizeTokens(var/list/tokens)
- . = tokens || list()
-
/decl/persistence_handler/Initialize()
SetFilename()
@@ -101,39 +46,67 @@
if(!fexists(filename))
return
- var/list/entries = cached_json_decode(safe_file2text(filename, FALSE))
+ var/decl/serialization_handler/handler = GET_DECL(serialization_handler)
+ var/list/entries = handler.load_data_from_file(filename)
+
if(!length(entries))
return
- var/list/encoding_flag = entries[1]
- if(encoding_flag && ("url_encoded" in encoding_flag))
- entries -= encoding_flag
- for(var/list/entry in entries)
- for(var/i in 1 to entry.len)
- var/item = entry[i]
- var/decoded_value = (istext(entry[item]) ? url_decode(entry[item]) : entry[item])
- var/decoded_key = url_decode(item)
- entry[i] = decoded_key
- entry[decoded_key] = decoded_value
-
- for(var/list/entry in entries)
- entry = FinalizeTokens(entry)
- if(CheckTokenSanity(entry))
- ProcessAndApplyTokens(entry)
+ // Check for old-style persistence data and generate a key.
+ if(length(entries) && !istext(entries[1]))
+ try
+ // Save a backup of the old file just in case we cook it.
+ fcopy(filename, "[filename]-legacy.[time2text(REALTIMEOFDAY, "YY-MM-DD_hh-mm")].backup")
+ catch(var/exception/e)
+ log_error("Exception during saving backup of legacy file [filename]: [EXCEPTION_TEXT(e)]")
+
+ // Update the data to match the expected format of the new system.
+ var/list/entries_with_key = list()
+ var/i = 1
+ for(var/entry in entries)
+ entries_with_key[num2text(i)] = UpdateFromLegacyFormat(entry)
+ entries = entries_with_key
+
+ instantiate_serialized_data(null, name, entries, entries_decay_at, entry_decay_weight)
/decl/persistence_handler/proc/Shutdown()
var/list/entries = list()
- for(var/thing in SSpersistence.tracking_values[type])
+ for(var/atom/thing in SSpersistence.tracking_values[type])
if(IsValidEntry(thing))
- entries += list(CompileEntry(thing))
-
- if(fexists(filename))
- fdel(filename)
- to_file(file(filename), json_encode(entries))
+ var/list/things_to_serialize = thing.GetPossiblySerializableInstances()
+ for(var/datum/subthing in things_to_serialize)
+ entries[subthing.get_run_uid()] = subthing.Serialize()
+ var/decl/serialization_handler/handler = GET_DECL(serialization_handler)
+ handler.save_data_to_file(filename, entries, name)
/decl/persistence_handler/proc/RemoveValue(var/atom/value)
qdel(value)
+/decl/persistence_handler/proc/UpdateFromLegacyFormat(list/_entry)
+
+ // Convert any old values to the new indices.
+ for(var/map_key in legacy_map_values)
+ if(map_key in _entry)
+ var/value = _entry[map_key]
+ _entry -= map_key
+ _entry[legacy_map_values[map_key]] = value
+
+ // Convert entry coords into new format.
+ if(("x" in _entry) || ("y" in _entry) || ("z" in _entry))
+ _entry["loc"] = list(
+ _entry["x"] || 1,
+ _entry["y"] || 1,
+ _entry["z"] || 1
+ )
+ _entry -= "x"
+ _entry -= "y"
+ _entry -= "z"
+
+ if(legacy_type && !(nameof(/datum::type) in _entry))
+ _entry[nameof(/datum::type)] = legacy_type
+
+ return _entry
+
/decl/persistence_handler/proc/GetAdminSummary(var/mob/user, var/can_modify)
. = list("| [capitalize(name)] |
")
. += "
|
"
diff --git a/code/modules/persistence/persistence_datum_book.dm b/code/modules/persistence/persistence_datum_book.dm
index 839f0ed29a63..9b96bca8dff5 100644
--- a/code/modules/persistence/persistence_datum_book.dm
+++ b/code/modules/persistence/persistence_datum_book.dm
@@ -3,28 +3,13 @@
has_admin_data = TRUE
ignore_area_flags = TRUE
ignore_invalid_loc = TRUE
-
-/decl/persistence_handler/book/CreateEntryInstance(var/turf/creating, var/list/tokens)
-
- var/book_type = tokens["book_type"]
- if(book_type)
- book_type = text2path(book_type)
- if(!ispath(book_type))
- book_type = /obj/item/book
-
- var/obj/item/book/book = new book_type(creating)
- book.dat = tokens["message"]
- book.title = tokens["title"]
- book.author = tokens["writer"]
- book.icon_state = tokens["icon_state"]
- book.last_modified_ckey = tokens["author"] || "unknown"
- book.unique = TRUE
- book.SetName(book.title)
- var/obj/structure/bookcase/case = locate() in creating
- if(case)
- book.forceMove(case)
- case.update_icon()
- return book
+ legacy_type = /obj/item/book
+ legacy_map_values = list(
+ "author" = nameof(/obj/item/book::last_modified_ckey),
+ "writer" = nameof(/obj/item/book::author),
+ "book_type" = nameof(/obj/item/book::type),
+ "message" = nameof(/obj/item/book::dat)
+ )
/decl/persistence_handler/book/IsValidEntry(var/atom/entry)
. = ..()
@@ -32,23 +17,6 @@
var/obj/item/book/book = entry
. = istype(book) && book.dat && book.last_modified_ckey
-/decl/persistence_handler/book/CompileEntry(var/atom/entry)
- . = ..()
-
- var/obj/item/book/book = entry
- .["author"] = book.last_modified_ckey || ""
- .["message"] = book.dat || "dat"
- .["title"] = book.title || "Untitled"
- .["writer"] = book.author || "unknown"
- .["icon_state"] = book.icon_state || "book"
- .["book_type"] = "[book.type]"
-
- var/turf/T = get_turf(entry)
- if(!T || !isStationLevel(T.z))
- .["x"] = 0
- .["y"] = 0
- .["z"] = 0
-
/decl/persistence_handler/book/RemoveValue(var/atom/movable/value)
var/obj/structure/bookcase/bookcase = value.loc
if(istype(bookcase))
@@ -57,24 +25,6 @@
bookcase.update_icon()
..()
-/decl/persistence_handler/book/GetValidTurf(var/turf/T, var/list/tokens)
-
- if(T)
- var/area/A = get_area(T)
- if(!A || (A.area_flags & AREA_FLAG_IS_NOT_PERSISTENT))
- T = null
-
- if(!T)
- if(length(global.station_bookcases))
- T = get_turf(pick(global.station_bookcases))
- else
- T = get_random_spawn_turf(SPAWN_FLAG_PERSISTENCE_CAN_SPAWN)
-
- . = ..()
-
-/decl/persistence_handler/book/GetEntryAge(var/atom/entry)
- . = -1
-
/decl/persistence_handler/book/GetAdminDataStringFor(var/thing, var/can_modify, var/mob/user)
var/obj/item/book/book = thing
if(can_modify)
diff --git a/code/modules/persistence/persistence_datum_filth.dm b/code/modules/persistence/persistence_datum_filth.dm
index 9f877efeab0d..249535eed99b 100644
--- a/code/modules/persistence/persistence_datum_filth.dm
+++ b/code/modules/persistence/persistence_datum_filth.dm
@@ -1,44 +1,11 @@
/decl/persistence_handler/filth
name = "filth"
entries_expire_at = 5
+ legacy_type = /obj/effect/decal/cleanable/filth
+ legacy_map_values = list(
+ "path" = nameof(/obj/effect/decal/cleanable::type),
+ "filthiness" = nameof(/obj/effect/decal/cleanable/dirt::dirt_amount)
+ )
/decl/persistence_handler/filth/IsValidEntry(var/atom/entry)
. = ..() && entry.invisibility == 0
-
-/decl/persistence_handler/filth/CheckTokenSanity(var/list/tokens)
- return ..() && ispath(tokens["path"])
-
-/decl/persistence_handler/filth/CheckTurfContents(var/turf/T, var/list/tokens)
- return !(locate(tokens["path"]) in T)
-
-/decl/persistence_handler/filth/FinalizeTokens(var/list/tokens)
- . = ..()
- if(.["path"] && !ispath(.["path"]))
- .["path"] = text2path(.["path"])
- if(isnull(.["filthiness"]))
- .["filthiness"] = 0
-
-/decl/persistence_handler/filth/CreateEntryInstance(var/turf/creating, var/list/tokens)
- var/_path = tokens["path"]
- var/obj/effect/decal/cleanable/dirt/dirt = new _path(creating, tokens["age"]+1)
- if(istype(dirt))
- dirt.dirt_amount = tokens["filthiness"]
- dirt.update_icon()
- return dirt
-
-/decl/persistence_handler/filth/GetEntryAge(var/atom/entry)
- var/obj/effect/decal/cleanable/filth = entry
- return filth.age
-
-/decl/persistence_handler/filth/proc/GetEntryPath(var/atom/entry)
- var/obj/effect/decal/cleanable/filth = entry
- return filth.generic_filth ? /obj/effect/decal/cleanable/filth : filth.type
-
-/decl/persistence_handler/filth/CompileEntry(var/atom/entry)
- . = ..()
- .["path"] = "[GetEntryPath(entry)]"
- if(istype(entry, /obj/effect/decal/cleanable/dirt))
- var/obj/effect/decal/cleanable/dirt/dirt = entry
- .["filthiness"] = dirt.dirt_amount
- else
- .["filthiness"] = 0
diff --git a/code/modules/persistence/persistence_datum_filth_trash.dm b/code/modules/persistence/persistence_datum_filth_trash.dm
index 8dd10f4806d3..dab2fcfed354 100644
--- a/code/modules/persistence/persistence_datum_filth_trash.dm
+++ b/code/modules/persistence/persistence_datum_filth_trash.dm
@@ -1,17 +1,3 @@
/decl/persistence_handler/filth/trash
name = "trash"
-
-/decl/persistence_handler/filth/trash/CheckTurfContents(var/turf/T, var/list/tokens)
- var/too_much_trash = 0
- for(var/obj/item/trash/trash in T)
- too_much_trash++
- if(too_much_trash >= 5)
- return FALSE
- return TRUE
-
-/decl/persistence_handler/filth/trash/GetEntryAge(var/atom/entry)
- var/obj/item/trash/trash = entry
- return trash.age
-
-/decl/persistence_handler/filth/trash/GetEntryPath(var/atom/entry)
- return entry.type
+ legacy_type = /obj/random/trash
diff --git a/code/modules/persistence/persistence_datum_graffiti.dm b/code/modules/persistence/persistence_datum_graffiti.dm
index 0bc733ab2f07..3e30ab2c4ecb 100644
--- a/code/modules/persistence/persistence_datum_graffiti.dm
+++ b/code/modules/persistence/persistence_datum_graffiti.dm
@@ -2,22 +2,7 @@
name = "graffiti"
entries_expire_at = 50
has_admin_data = TRUE
-
-/decl/persistence_handler/graffiti/GetValidTurf(var/turf/T, var/list/tokens)
- var/turf/checking_turf = ..()
- if(istype(checking_turf) && checking_turf.can_engrave())
- return checking_turf
-
-/decl/persistence_handler/graffiti/CheckTurfContents(var/turf/T, var/list/tokens)
- var/too_much_graffiti = 0
- for(var/obj/effect/decal/writing/writing in .)
- too_much_graffiti++
- if(too_much_graffiti >= 5)
- return FALSE
- return TRUE
-
-/decl/persistence_handler/graffiti/CreateEntryInstance(var/turf/creating, var/list/tokens)
- return new /obj/effect/decal/writing(creating, tokens["age"]+1, tokens["message"], tokens["author"])
+ legacy_type = /obj/effect/decal/writing
/decl/persistence_handler/graffiti/IsValidEntry(var/atom/entry)
. = ..()
@@ -25,16 +10,6 @@
var/turf/T = entry.loc
. = T.can_engrave()
-/decl/persistence_handler/graffiti/GetEntryAge(var/atom/entry)
- var/obj/effect/decal/writing/save_graffiti = entry
- return save_graffiti.graffiti_age
-
-/decl/persistence_handler/graffiti/CompileEntry(var/atom/entry)
- . = ..()
- var/obj/effect/decal/writing/save_graffiti = entry
- .["author"] = save_graffiti.author || "unknown"
- .["message"] = save_graffiti.message
-
/decl/persistence_handler/graffiti/GetAdminDataStringFor(var/thing, var/can_modify, var/mob/user)
var/obj/effect/decal/writing/save_graffiti = thing
if(can_modify)
diff --git a/code/modules/persistence/persistence_datum_paper.dm b/code/modules/persistence/persistence_datum_paper.dm
index 94749f01deda..a14cb7d79878 100644
--- a/code/modules/persistence/persistence_datum_paper.dm
+++ b/code/modules/persistence/persistence_datum_paper.dm
@@ -2,41 +2,12 @@
name = "paper"
entries_expire_at = 50
has_admin_data = TRUE
- var/paper_type = /obj/item/paper
-
-/decl/persistence_handler/paper/CreateEntryInstance(var/turf/creating, var/list/tokens)
-
- var/obj/item/paper/paper = new paper_type(creating)
- paper.set_content(tokens["message"], tokens["title"])
- paper.last_modified_ckey = tokens["author"]
-
- if("has_noticeboard" in tokens)
- var/obj/structure/noticeboard/board = locate() in creating
- if(!board)
- var/decl/material/mat = decls_repository.get_decl_by_id_or_var(tokens["noticeboard_material"], /decl/material, "name")
- board = new(creating, (mat?.type || /decl/material/solid/organic/wood/oak))
- if("noticeboard_direction" in tokens)
- board.set_dir(tokens["noticeboard_direction"])
- if(LAZYLEN(board.notices) < board.max_notices)
- board.add_paper(paper)
-
- return paper
-
-/decl/persistence_handler/paper/GetEntryAge(var/atom/entry)
- var/obj/item/paper/paper = entry
- return paper.age
-
-/decl/persistence_handler/paper/CompileEntry(var/atom/entry)
- . = ..()
- var/obj/item/paper/paper = entry
- .["author"] = paper.last_modified_ckey || "unknown"
- .["message"] = paper.info || ""
- .["title"] = paper.name || "paper"
- var/obj/structure/noticeboard/board = entry.loc
- if(istype(board))
- .["has_noticeboard"] = TRUE
- .["noticeboard_direction"] = board.dir
- .["noticeboard_material"] = board.material.uid
+ legacy_map_values = list(
+ "author" = nameof(/obj/item/paper::last_modified_ckey),
+ "message" = nameof(/obj/item/paper::info),
+ "title" = nameof(/obj/item/paper::name)
+ )
+ legacy_type = /obj/item/paper
/decl/persistence_handler/paper/GetAdminDataStringFor(var/thing, var/can_modify, var/mob/user)
var/obj/item/paper/paper = thing
diff --git a/code/modules/persistence/persistence_datum_paper_sticky.dm b/code/modules/persistence/persistence_datum_paper_sticky.dm
index 4b6f849ee0b2..2eeee9e7a371 100644
--- a/code/modules/persistence/persistence_datum_paper_sticky.dm
+++ b/code/modules/persistence/persistence_datum_paper_sticky.dm
@@ -1,19 +1,3 @@
/decl/persistence_handler/paper/sticky
name = "stickynotes"
- paper_type = /obj/item/paper/sticky
-
-/decl/persistence_handler/paper/sticky/CreateEntryInstance(var/turf/creating, var/list/tokens)
- var/atom/paper = ..()
- if(paper)
- paper.default_pixel_x = tokens["offset_x"]
- paper.default_pixel_y = tokens["offset_y"]
- paper.reset_offsets(0)
- paper.color = tokens["color"]
- return paper
-
-/decl/persistence_handler/paper/sticky/CompileEntry(var/atom/entry)
- . = ..()
- var/obj/item/paper/sticky/paper = entry
- .["offset_x"] = paper.pixel_x
- .["offset_y"] = paper.pixel_y
- .["color"] = paper.color
+ legacy_type = /obj/item/paper/sticky
diff --git a/code/modules/random_map/random_map.dm b/code/modules/random_map/random_map.dm
index 3604f7ab33d9..10c390dfcd47 100644
--- a/code/modules/random_map/random_map.dm
+++ b/code/modules/random_map/random_map.dm
@@ -159,7 +159,6 @@ var/global/list/map_count = list()
if(!origin_x) origin_x = 1
if(!origin_y) origin_y = 1
if(!origin_z) origin_z = 1
-
for(var/x in 1 to limit_x)
for(var/y in 1 to limit_y)
CHECK_TICK
diff --git a/code/modules/reagents/Chemistry-Holder.dm b/code/modules/reagents/Chemistry-Holder.dm
index a9c135a0b29c..be8dfdb98859 100644
--- a/code/modules/reagents/Chemistry-Holder.dm
+++ b/code/modules/reagents/Chemistry-Holder.dm
@@ -73,13 +73,18 @@ var/global/datum/reagents/sink/infinite_reagent_sink = new
VAR_PRIVATE/list/solid_volumes // This should be taken as powders/flakes, rather than large solid pieces of material.
VAR_PRIVATE/list/reagent_data
VAR_PRIVATE/atom/my_atom
- VAR_PRIVATE/cached_color
- VAR_PRIVATE/primary_reagent
- VAR_PRIVATE/primary_solid
- VAR_PRIVATE/primary_liquid
- VAR_PRIVATE/total_volume = 0
- VAR_PRIVATE/total_liquid_volume // Used to determine when to create fluids in the world and the like.
VAR_PRIVATE/maximum_volume = 120
+ VAR_PRIVATE/tmp/cached_color
+ VAR_PRIVATE/tmp/primary_reagent
+ VAR_PRIVATE/tmp/primary_solid
+ VAR_PRIVATE/tmp/primary_liquid
+ VAR_PRIVATE/tmp/total_volume = 0
+ VAR_PRIVATE/tmp/total_liquid_volume // Used to determine when to create fluids in the world and the like.
+
+// Reagent serde is handled per atom.
+/datum/reagents/ShouldSerialize(_age)
+ SHOULD_CALL_PARENT(FALSE)
+ return FALSE
/datum/reagents/New(var/maximum_volume = 120, var/atom/my_atom)
src.maximum_volume = maximum_volume
@@ -986,30 +991,18 @@ var/global/datum/reagents/sink/infinite_reagent_sink = new
. = FONT_COLORED(get_color(), .)
/* Atom reagent creation - use it all the time */
-/atom/proc/create_reagents(var/max_vol)
- if(istype(reagents))
- log_debug("Attempted to create a new reagents holder when already referencing one: [log_info_line(src)]")
- REAGENT_SET_MAX_VOL(reagents, max(REAGENT_MAXIMUM_VOLUME(reagents), max_vol))
- else if(!reagents)
- reagents = new/datum/reagents(max_vol, src)
- else
- return
- return reagents
-
-/atom/proc/create_or_update_reagents(_vol, override_volume)
- if(isnull(reagents))
- return create_reagents(_vol)
- if(istype(reagents))
- if(override_volume)
- REAGENT_SET_MAX_VOL(reagents, _vol) // should we remove excess reagents here?
- else
- REAGENT_SET_MAX_VOL(reagents, max(REAGENT_MAXIMUM_VOLUME(reagents), _vol))
+/atom/proc/create_or_update_reagents(vol, override_volume)
+ if(islist(reagents))
+ return // We are pending serde, this will be handled in initialize_reagents().
+ else if(istype(reagents))
+ var/use_max_vol = override_volume ? vol : max(vol, REAGENT_MAXIMUM_VOLUME(reagents))
+ REAGENT_SET_MAX_VOL(reagents, use_max_vol)
reagents.update_total()
- return reagents
+ else if(isnull(reagents))
+ reagents = new /datum/reagents(vol, src)
+ return reagents
/// Infinite reagent sink: nothing is ever actually added to it, useful for complex, filtered deletion of reagents without holder churn.
-/datum/reagents/sink
-
/datum/reagents/sink/add_reagent(var/decl/material/reagent, amount, data, safety, defer_update, phase)
amount = CHEMS_QUANTIZE(min(amount, REAGENTS_FREE_SPACE(src)))
if(amount <= 0)
diff --git a/code/modules/reagents/dispenser/cartridge.dm b/code/modules/reagents/dispenser/cartridge.dm
index 35300fc4310a..daee57f2381d 100644
--- a/code/modules/reagents/dispenser/cartridge.dm
+++ b/code/modules/reagents/dispenser/cartridge.dm
@@ -11,10 +11,14 @@
possible_transfer_amounts = @"[50,100]"
var/_reagent_label
-/obj/item/chems/chem_disp_cartridge/Initialize()
+/obj/item/chems/chem_disp_cartridge/Serialize()
+ . = ..()
+ SERIALIZE_IF_MODIFIED(_reagent_label, /obj/item/chems/chem_disp_cartridge)
+
+/obj/item/chems/chem_disp_cartridge/Initialize(ml, material_key)
. = ..()
var/decl/material/primary_reagent = istype(reagents) && reagents.get_primary_reagent_decl()
- if(primary_reagent && !_reagent_label)
+ if(primary_reagent && isnull(_reagent_label))
_reagent_label = primary_reagent.name
if(_reagent_label)
setLabel(_reagent_label)
diff --git a/code/modules/reagents/reagent_containers.dm b/code/modules/reagents/reagent_containers.dm
index 89c9ecc84b34..847d7996b244 100644
--- a/code/modules/reagents/reagent_containers.dm
+++ b/code/modules/reagents/reagent_containers.dm
@@ -54,7 +54,7 @@
/obj/item/chems/update_name()
. = ..() // handles material, etc
var/newname = name
- if(presentation_flags & PRESENTATION_FLAG_NAME)
+ if(istype(reagents) && (presentation_flags & PRESENTATION_FLAG_NAME))
var/decl/material/primary = reagents?.get_primary_reagent_decl()
if(primary)
newname += " of [primary.get_presentation_name(src)]"
diff --git a/code/modules/tools/tool_serde.dm b/code/modules/tools/tool_serde.dm
new file mode 100644
index 000000000000..f94e251715b2
--- /dev/null
+++ b/code/modules/tools/tool_serde.dm
@@ -0,0 +1,9 @@
+/obj/item/tool/Serialize()
+ . = ..()
+ SERIALIZE_DECL_IF_MODIFIED(handle_material, /obj/item/tool)
+ SERIALIZE_DECL_IF_MODIFIED(binding_material, /obj/item/tool)
+
+/obj/item/tool/Deserialize(list/instance_map)
+ . = ..()
+ DESERIALIZE_DECL_TO_TYPE(handle_material)
+ DESERIALIZE_DECL_TO_TYPE(binding_material)
diff --git a/maps/exodus/exodus_shuttles.dm b/maps/exodus/exodus_shuttles.dm
index 9691d2c27ff0..c7abc4523d25 100644
--- a/maps/exodus/exodus_shuttles.dm
+++ b/maps/exodus/exodus_shuttles.dm
@@ -20,7 +20,7 @@
} \
/area/shuttle/escape_pod_##NUMBER { \
name = "Escape Pod " + #NUMBER; \
- area_flags = AREA_FLAG_RAD_SHIELDED | AREA_FLAG_ION_SHIELDED | AREA_FLAG_IS_NOT_PERSISTENT; \
+ area_flags = AREA_FLAG_RAD_SHIELDED | AREA_FLAG_ION_SHIELDED | AREA_FLAG_NO_LEGACY_PERSISTENCE; \
}
ESCAPE_POD(1)
@@ -38,7 +38,7 @@ ESCAPE_POD(4)
current_location = "nav_exodus_research_pod_dock"
/area/ship/exodus_pod_research
name = "Research Pod"
- area_flags = AREA_FLAG_RAD_SHIELDED | AREA_FLAG_ION_SHIELDED | AREA_FLAG_IS_NOT_PERSISTENT
+ area_flags = AREA_FLAG_RAD_SHIELDED | AREA_FLAG_ION_SHIELDED | AREA_FLAG_NO_LEGACY_PERSISTENCE
/obj/effect/overmap/visitable/ship/landable/pod/research
name = "Exodus Research Pod"
shuttle = "Research Pod"
@@ -57,7 +57,7 @@ ESCAPE_POD(4)
current_location = "nav_exodus_mining_pod_dock"
/area/ship/exodus_pod_mining
name = "Mining Pod"
- area_flags = AREA_FLAG_RAD_SHIELDED | AREA_FLAG_ION_SHIELDED | AREA_FLAG_IS_NOT_PERSISTENT
+ area_flags = AREA_FLAG_RAD_SHIELDED | AREA_FLAG_ION_SHIELDED | AREA_FLAG_NO_LEGACY_PERSISTENCE
/obj/effect/overmap/visitable/ship/landable/pod/mining
name = "Exodus Mining Pod"
shuttle = "Mining Pod"
@@ -76,7 +76,7 @@ ESCAPE_POD(4)
current_location = "nav_exodus_engineering_pod_dock"
/area/ship/exodus_pod_engineering
name = "Engineering Pod"
- area_flags = AREA_FLAG_RAD_SHIELDED | AREA_FLAG_ION_SHIELDED | AREA_FLAG_IS_NOT_PERSISTENT
+ area_flags = AREA_FLAG_RAD_SHIELDED | AREA_FLAG_ION_SHIELDED | AREA_FLAG_NO_LEGACY_PERSISTENCE
/obj/effect/overmap/visitable/ship/landable/pod/engineering
name = "Exodus Engineering Pod"
shuttle = "Engineering Pod"
diff --git a/maps/shaded_hills/levels/_levels.dm b/maps/shaded_hills/levels/_levels.dm
index dc2d59ddcc52..411714f57c56 100644
--- a/maps/shaded_hills/levels/_levels.dm
+++ b/maps/shaded_hills/levels/_levels.dm
@@ -30,7 +30,7 @@
level_id = "shaded_hills_grassland"
level_generators = list(
/datum/random_map/automata/cave_system/shaded_hills,
- /datum/random_map/noise/ore/poor,
+ /datum/random_map/noise/ore/poor/shaded_hills,
/datum/random_map/noise/forage/shaded_hills/grassland
)
connected_levels = list(
@@ -163,7 +163,7 @@
subtemplate_area = /area/shaded_hills/caves/deep/poi
level_generators = list(
/datum/random_map/automata/cave_system/shaded_hills,
- /datum/random_map/noise/ore/rich
+ /datum/random_map/noise/ore/rich/shaded_hills
)
base_turf = /turf/floor/rock/basalt
diff --git a/maps/shaded_hills/levels/random_map.dm b/maps/shaded_hills/levels/random_map.dm
index d51ed84765b3..5c3098b3c2c7 100644
--- a/maps/shaded_hills/levels/random_map.dm
+++ b/maps/shaded_hills/levels/random_map.dm
@@ -11,6 +11,13 @@
smooth_single_tiles = TRUE
target_turf_type = /turf/unsimulated/mask
+/datum/random_map/noise/forage/shaded_hills
+ abstract_type = /datum/random_map/noise/forage/shaded_hills
+
+/datum/random_map/noise/ore/poor/shaded_hills
+
+/datum/random_map/noise/ore/rich/shaded_hills
+
/datum/random_map/noise/shaded_hills/swamp
descriptor = "Shaded Hills swamp"
@@ -33,10 +40,6 @@
return /turf/floor/grass/wild
return /turf/floor/grass
-// TODO
-/datum/random_map/noise/forage/shaded_hills
- abstract_type = /datum/random_map/noise/forage/shaded_hills
-
/datum/random_map/noise/forage/shaded_hills/grassland/New()
forage["grass"] |= list(
"yarrow",
diff --git a/mods/content/fantasy/submaps/grassland/_grassland.dm b/mods/content/fantasy/submaps/grassland/_grassland.dm
index 19b70b9367f0..e2860756be59 100644
--- a/mods/content/fantasy/submaps/grassland/_grassland.dm
+++ b/mods/content/fantasy/submaps/grassland/_grassland.dm
@@ -4,4 +4,4 @@
/datum/map_template/fantasy/cavern
abstract_type = /datum/map_template/fantasy/cavern
- template_categories = list(MAP_TEMPLATE_CATEGORY_FANTASY_CAVERNS)
\ No newline at end of file
+ template_categories = list(MAP_TEMPLATE_CATEGORY_FANTASY_CAVERNS)
diff --git a/mods/content/fantasy/submaps/swamp/_swamp.dm b/mods/content/fantasy/submaps/swamp/_swamp.dm
index 6a3c2054ea9a..5b3401cfd6f8 100644
--- a/mods/content/fantasy/submaps/swamp/_swamp.dm
+++ b/mods/content/fantasy/submaps/swamp/_swamp.dm
@@ -1,3 +1,3 @@
/datum/map_template/fantasy/swamp
abstract_type = /datum/map_template/fantasy/swamp
- template_categories = list(MAP_TEMPLATE_CATEGORY_FANTASY_SWAMP)
\ No newline at end of file
+ template_categories = list(MAP_TEMPLATE_CATEGORY_FANTASY_SWAMP)
diff --git a/nebula.dme b/nebula.dme
index 67c56ab53fb0..036102ca7d81 100644
--- a/nebula.dme
+++ b/nebula.dme
@@ -80,6 +80,7 @@
#include "code\__defines\organs.dm"
#include "code\__defines\overmap.dm"
#include "code\__defines\paperwork.dm"
+#include "code\__defines\persistence.dm"
#include "code\__defines\power.dm"
#include "code\__defines\proc_presets.dm"
#include "code\__defines\qdel.dm"
@@ -87,6 +88,7 @@
#include "code\__defines\reactions.dm"
#include "code\__defines\reagent_data_fields.dm"
#include "code\__defines\research.dm"
+#include "code\__defines\serde.dm"
#include "code\__defines\shields.dm"
#include "code\__defines\shuttle.dm"
#include "code\__defines\skills.dm"
@@ -151,6 +153,7 @@
#include "code\_helpers\profiling.dm"
#include "code\_helpers\radio.dm"
#include "code\_helpers\sanitize_values.dm"
+#include "code\_helpers\serde.dm"
#include "code\_helpers\storage.dm"
#include "code\_helpers\text.dm"
#include "code\_helpers\time.dm"
@@ -296,6 +299,7 @@
#include "code\controllers\subsystems\overlays.dm"
#include "code\controllers\subsystems\overmap.dm"
#include "code\controllers\subsystems\pathfinding.dm"
+#include "code\controllers\subsystems\persistence.dm"
#include "code\controllers\subsystems\radiation.dm"
#include "code\controllers\subsystems\shuttle.dm"
#include "code\controllers\subsystems\skybox.dm"
@@ -324,7 +328,6 @@
#include "code\controllers\subsystems\initialization\materials.dm"
#include "code\controllers\subsystems\initialization\misc.dm"
#include "code\controllers\subsystems\initialization\modpacks.dm"
-#include "code\controllers\subsystems\initialization\persistence.dm"
#include "code\controllers\subsystems\initialization\robots.dm"
#include "code\controllers\subsystems\initialization\secrets.dm"
#include "code\controllers\subsystems\initialization\webhooks.dm"
@@ -352,6 +355,7 @@
#include "code\datums\category.dm"
#include "code\datums\cinematic.dm"
#include "code\datums\datum.dm"
+#include "code\datums\datum_serde.dm"
#include "code\datums\footsteps.dm"
#include "code\datums\hierarchy.dm"
#include "code\datums\local_network.dm"
@@ -756,6 +760,8 @@
#include "code\game\atoms_movable_grabs.dm"
#include "code\game\atoms_movable_interactions.dm"
#include "code\game\atoms_movable_overlay.dm"
+#include "code\game\atoms_movable_serde.dm"
+#include "code\game\atoms_serde.dm"
#include "code\game\atoms_temperature.dm"
#include "code\game\base_turf.dm"
#include "code\game\movietitles.dm"
@@ -780,6 +786,7 @@
#include "code\game\area\area_power.dm"
#include "code\game\area\area_space.dm"
#include "code\game\area\areas.dm"
+#include "code\game\area\areas_serde.dm"
#include "code\game\gamemodes\game_mode.dm"
#include "code\game\gamemodes\game_mode_latespawn.dm"
#include "code\game\gamemodes\calamity\calamity.dm"
@@ -1044,6 +1051,7 @@
#include "code\game\objects\effects\decals\cleanable.dm"
#include "code\game\objects\effects\decals\crayon.dm"
#include "code\game\objects\effects\decals\decal.dm"
+#include "code\game\objects\effects\decals\decal_serde.dm"
#include "code\game\objects\effects\decals\misc.dm"
#include "code\game\objects\effects\decals\warning_stripes.dm"
#include "code\game\objects\effects\decals\Cleanable\humans.dm"
@@ -1073,6 +1081,7 @@
#include "code\game\objects\items\_item_materials.dm"
#include "code\game\objects\items\_item_melting.dm"
#include "code\game\objects\items\_item_reagents.dm"
+#include "code\game\objects\items\_item_serde.dm"
#include "code\game\objects\items\_item_sharpness.dm"
#include "code\game\objects\items\blackout.dm"
#include "code\game\objects\items\blueprints.dm"
@@ -1108,6 +1117,7 @@
#include "code\game\objects\items\toys.dm"
#include "code\game\objects\items\training_dummy.dm"
#include "code\game\objects\items\trash.dm"
+#include "code\game\objects\items\trash_serde.dm"
#include "code\game\objects\items\umbrella.dm"
#include "code\game\objects\items\waterskin.dm"
#include "code\game\objects\items\artifice\chain.dm"
@@ -1124,6 +1134,7 @@
#include "code\game\objects\items\blades\swords_one_handed.dm"
#include "code\game\objects\items\blades\swords_two_handed.dm"
#include "code\game\objects\items\books\_book.dm"
+#include "code\game\objects\items\books\_book_serde.dm"
#include "code\game\objects\items\books\fluff\_fluff.dm"
#include "code\game\objects\items\books\fluff\science.dm"
#include "code\game\objects\items\books\manuals\_manual.dm"
@@ -1248,6 +1259,7 @@
#include "code\game\objects\items\stacks\nanopaste.dm"
#include "code\game\objects\items\stacks\rods.dm"
#include "code\game\objects\items\stacks\stack.dm"
+#include "code\game\objects\items\stacks\stack_serde.dm"
#include "code\game\objects\items\stacks\telecrystal.dm"
#include "code\game\objects\items\stacks\medical\_medical.dm"
#include "code\game\objects\items\stacks\medical\medical_bandage.dm"
@@ -1403,6 +1415,7 @@
#include "code\game\objects\structures\_structure_interactions.dm"
#include "code\game\objects\structures\_structure_lock.dm"
#include "code\game\objects\structures\_structure_materials.dm"
+#include "code\game\objects\structures\_structure_serde.dm"
#include "code\game\objects\structures\ai_decoy.dm"
#include "code\game\objects\structures\armor_stand.dm"
#include "code\game\objects\structures\barricade.dm"
@@ -1539,6 +1552,7 @@
#include "code\game\objects\structures\flora\bush.dm"
#include "code\game\objects\structures\flora\grass.dm"
#include "code\game\objects\structures\flora\plant.dm"
+#include "code\game\objects\structures\flora\plant_serde.dm"
#include "code\game\objects\structures\flora\potted.dm"
#include "code\game\objects\structures\flora\stump.dm"
#include "code\game\objects\structures\flora\tree.dm"
@@ -1564,6 +1578,7 @@
#include "code\game\turfs\turf_material.dm"
#include "code\game\turfs\turf_navigation.dm"
#include "code\game\turfs\turf_ramps.dm"
+#include "code\game\turfs\turf_serde.dm"
#include "code\game\turfs\flooring\_flooring.dm"
#include "code\game\turfs\flooring\_flooring_decals.dm"
#include "code\game\turfs\flooring\flooring_carpet.dm"
@@ -1591,6 +1606,7 @@
#include "code\game\turfs\floors\floor_icon.dm"
#include "code\game\turfs\floors\floor_layers.dm"
#include "code\game\turfs\floors\floor_materials.dm"
+#include "code\game\turfs\floors\floor_serde.dm"
#include "code\game\turfs\floors\subtypes\floor_carpet.dm"
#include "code\game\turfs\floors\subtypes\floor_circuit.dm"
#include "code\game\turfs\floors\subtypes\floor_concrete.dm"
@@ -1628,6 +1644,7 @@
#include "code\game\turfs\walls\wall_natural_ramps.dm"
#include "code\game\turfs\walls\wall_natural_subtypes.dm"
#include "code\game\turfs\walls\wall_natural_xenoarch.dm"
+#include "code\game\turfs\walls\wall_serde.dm"
#include "code\game\turfs\walls\wall_types.dm"
#include "code\game\turfs\walls\wall_wattle.dm"
#include "code\game\verbs\byond_membership.dm"
@@ -2800,6 +2817,7 @@
#include "code\modules\mob\mob_layering.dm"
#include "code\modules\mob\mob_movement.dm"
#include "code\modules\mob\mob_organs.dm"
+#include "code\modules\mob\mob_serde.dm"
#include "code\modules\mob\mob_snapshot.dm"
#include "code\modules\mob\mob_status.dm"
#include "code\modules\mob\mob_temperature.dm"
@@ -2990,6 +3008,7 @@
#include "code\modules\mob\living\simple_animal\natural_weapons.dm"
#include "code\modules\mob\living\simple_animal\simple_animal_codex.dm"
#include "code\modules\mob\living\simple_animal\simple_animal_damage.dm"
+#include "code\modules\mob\living\simple_animal\simple_animal_serde.dm"
#include "code\modules\mob\living\simple_animal\alien\alien.dm"
#include "code\modules\mob\living\simple_animal\aquatic\_aquatic.dm"
#include "code\modules\mob\living\simple_animal\aquatic\_aquatic_hostile.dm"
@@ -3243,6 +3262,9 @@
#include "code\modules\multiz\hoist.dm"
#include "code\modules\multiz\ladder.dm"
#include "code\modules\multiz\level_data.dm"
+#include "code\modules\multiz\level_persistence_handler.dm"
+#include "code\modules\multiz\level_persistence_handler_json.dm"
+#include "code\modules\multiz\level_persistence_serialization.dm"
#include "code\modules\multiz\map_data.dm"
#include "code\modules\multiz\mobile_ladder.dm"
#include "code\modules\multiz\movement.dm"
@@ -3359,6 +3381,8 @@
#include "code\modules\overmap\ships\machines\fusion_thruster.dm"
#include "code\modules\overmap\ships\machines\gas_thruster.dm"
#include "code\modules\overmap\ships\machines\ion_thruster.dm"
+#include "code\modules\paperwork\_paper.dm"
+#include "code\modules\paperwork\_paper_serde.dm"
#include "code\modules\paperwork\adminpaper.dm"
#include "code\modules\paperwork\bodyscan_paper.dm"
#include "code\modules\paperwork\carbonpaper.dm"
@@ -3368,7 +3392,6 @@
#include "code\modules\paperwork\folders.dm"
#include "code\modules\paperwork\handlabeler.dm"
#include "code\modules\paperwork\helpers.dm"
-#include "code\modules\paperwork\paper.dm"
#include "code\modules\paperwork\paper_bundle.dm"
#include "code\modules\paperwork\paper_plane.dm"
#include "code\modules\paperwork\paper_sticky.dm"
@@ -3858,6 +3881,7 @@
#include "code\modules\synthesized_instruments\real_instruments\Trumpet\trumpet.dm"
#include "code\modules\synthesized_instruments\real_instruments\Violin\violin.dm"
#include "code\modules\tools\tool.dm"
+#include "code\modules\tools\tool_serde.dm"
#include "code\modules\tools\archetypes\_tool_defines.dm"
#include "code\modules\tools\archetypes\tool_archetype.dm"
#include "code\modules\tools\archetypes\tool_archetype_definition_pen.dm"