Skip to content

Commit 3a905e3

Browse files
Implementing z-level persistence.
1 parent e5a542b commit 3a905e3

32 files changed

+717
-62
lines changed

code/__defines/misc.dm

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -87,27 +87,28 @@
8787
#define EVENT_LEVEL_MAJOR 3
8888

8989
//Area flags, possibly more to come
90-
#define AREA_FLAG_RAD_SHIELDED BITFLAG(1) // Shielded from radiation, clearly.
91-
#define AREA_FLAG_EXTERNAL BITFLAG(2) // External as in exposed to space, not outside in a nice, green, forest.
92-
#define AREA_FLAG_ION_SHIELDED BITFLAG(3) // Shielded from ionospheric anomalies.
93-
#define AREA_FLAG_IS_NOT_PERSISTENT BITFLAG(4) // SSpersistence will not track values from this area.
94-
#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.
95-
#define AREA_FLAG_MAINTENANCE BITFLAG(6) // Area is a maintenance area.
96-
#define AREA_FLAG_SHUTTLE BITFLAG(7) // Area is a shuttle area.
97-
#define AREA_FLAG_HALLWAY BITFLAG(8) // Area is a public hallway suitable for event selection
98-
#define AREA_FLAG_PRISON BITFLAG(9) // Area is a prison for the purposes of brigging objectives.
99-
#define AREA_FLAG_HOLY BITFLAG(10) // Area is holy for the purposes of marking turfs as cult-resistant.
100-
#define AREA_FLAG_SECURITY BITFLAG(11) // Area is security for the purposes of newscaster init.
101-
#define AREA_FLAG_HIDE_FROM_HOLOMAP BITFLAG(12) // if we shouldn't be drawn on station holomaps
90+
#define AREA_FLAG_RAD_SHIELDED BITFLAG(1) // Shielded from radiation, clearly.
91+
#define AREA_FLAG_EXTERNAL BITFLAG(2) // External as in exposed to space, not outside in a nice, green, forest.
92+
#define AREA_FLAG_ION_SHIELDED BITFLAG(3) // Shielded from ionospheric anomalies.
93+
#define AREA_FLAG_IS_NOT_PERSISTENT BITFLAG(4) // SSpersistence will not track values from this area.
94+
#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.
95+
#define AREA_FLAG_MAINTENANCE BITFLAG(6) // Area is a maintenance area.
96+
#define AREA_FLAG_SHUTTLE BITFLAG(7) // Area is a shuttle area.
97+
#define AREA_FLAG_HALLWAY BITFLAG(8) // Area is a public hallway suitable for event selection
98+
#define AREA_FLAG_PRISON BITFLAG(9) // Area is a prison for the purposes of brigging objectives.
99+
#define AREA_FLAG_HOLY BITFLAG(10) // Area is holy for the purposes of marking turfs as cult-resistant.
100+
#define AREA_FLAG_SECURITY BITFLAG(11) // Area is security for the purposes of newscaster init.
101+
#define AREA_FLAG_HIDE_FROM_HOLOMAP BITFLAG(12) // if we shouldn't be drawn on station holomaps
102+
#define AREA_FLAG_ALLOW_LEVEL_PERSISTENCE BITFLAG(13) // Whether or not this area should pass changed turfs to SSpersistence.
102103

103104
//Map template flags
104-
#define TEMPLATE_FLAG_ALLOW_DUPLICATES BITFLAG(0) // Lets multiple copies of the template to be spawned
105-
#define TEMPLATE_FLAG_SPAWN_GUARANTEED BITFLAG(1) // Makes it ignore away site budget and just spawn (only for away sites)
106-
#define TEMPLATE_FLAG_CLEAR_CONTENTS BITFLAG(2) // if it should destroy objects it spawns on top of
107-
#define TEMPLATE_FLAG_NO_RUINS BITFLAG(3) // if it should forbid ruins from spawning on top of it
108-
#define TEMPLATE_FLAG_NO_RADS BITFLAG(4) // Removes all radiation from the template after spawning.
109-
#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.
110-
#define TEMPLATE_FLAG_GENERIC_REPEATABLE BITFLAG(6) // Template can be picked repeatedly for the same level gen run.
105+
#define TEMPLATE_FLAG_ALLOW_DUPLICATES BITFLAG(0) // Lets multiple copies of the template to be spawned
106+
#define TEMPLATE_FLAG_SPAWN_GUARANTEED BITFLAG(1) // Makes it ignore away site budget and just spawn (only for away sites)
107+
#define TEMPLATE_FLAG_CLEAR_CONTENTS BITFLAG(2) // if it should destroy objects it spawns on top of
108+
#define TEMPLATE_FLAG_NO_RUINS BITFLAG(3) // if it should forbid ruins from spawning on top of it
109+
#define TEMPLATE_FLAG_NO_RADS BITFLAG(4) // Removes all radiation from the template after spawning.
110+
#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.
111+
#define TEMPLATE_FLAG_GENERIC_REPEATABLE BITFLAG(6) // Template can be picked repeatedly for the same level gen run.
111112

112113
// Convoluted setup so defines can be supplied by Bay12 main server compile script.
113114
// Should still work fine for people jamming the icons into their repo.

code/__defines/persistence.dm

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#define SERDE_TYPE "type"
2+
#define SERDE_LOC "loc"
3+
4+
#define SERDE_NAME "name"
5+
#define SERDE_DESC "desc"
6+
#define SERDE_COLOR "color"
7+
#define SERDE_ICON_STATE "icon_state"
8+
9+
// Handled elsewhere, do not let them load like vars.
10+
var/global/list/_forbid_field_load = list(
11+
(SERDE_TYPE) = TRUE,
12+
(SERDE_LOC) = TRUE
13+
)

code/__defines/serde.dm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#define SERDE_HINT_FINISHED 1
2+
#define SERDE_HINT_POSTINIT 2

code/controllers/subsystems/atoms.dm

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ SUBSYSTEM_DEF(atoms)
1111
var/atom_init_stage = INITIALIZATION_INSSATOMS
1212
var/old_init_stage
1313

14+
/// A linear list of atoms that were deserialized prior to flush.
15+
var/list/deserialized_atoms = list()
1416
/// A non-associative list of lists, with the format list(list(atom, list(Initialize arguments))).
1517
var/list/created_atoms = list()
1618
/// A non-associative list of lists, with the format list(list(atom, list(LateInitialize arguments))).
@@ -29,9 +31,21 @@ SUBSYSTEM_DEF(atoms)
2931

3032
atom_init_stage = INITIALIZATION_INNEW_MAPLOAD
3133

32-
var/list/mapload_arg = list(TRUE)
33-
34+
// Preload any atoms that have deserialized during the initial load process prior to flush.
3435
var/index = 1
36+
var/list/postinit_serde_atoms = list()
37+
if(length(deserialized_atoms))
38+
while(index <= length(deserialized_atoms))
39+
var/atom/A = deserialized_atoms[index++]
40+
var/res = A.Preload()
41+
if(res == SERDE_HINT_POSTINIT)
42+
postinit_serde_atoms += res
43+
CHECK_TICK
44+
deserialized_atoms.Cut()
45+
report_progress("Deserialized [index-1] atom\s.")
46+
index = 1
47+
48+
var/list/mapload_arg = list(TRUE)
3549
// Things can add to the end of this list while we iterate, so we can't use a for loop.
3650
while(index <= length(created_atoms))
3751
// Don't remove from this list while we run, that's expensive.
@@ -49,10 +63,10 @@ SUBSYSTEM_DEF(atoms)
4963
else
5064
InitAtom(A, mapload_arg)
5165
CHECK_TICK
52-
53-
report_progress("Initialized [index] atom\s")
5466
created_atoms.Cut()
5567

68+
report_progress("Initialized [index-1] atom\s.")
69+
5670
atom_init_stage = INITIALIZATION_INNEW_REGULAR
5771

5872
if(length(late_loaders))
@@ -65,6 +79,14 @@ SUBSYSTEM_DEF(atoms)
6579
report_progress("Late initialized [index] atom\s")
6680
late_loaders.Cut()
6781

82+
if(length(postinit_serde_atoms))
83+
index = 1
84+
while(index <= length(postinit_serde_atoms))
85+
var/atom/A = postinit_serde_atoms[index++]
86+
A.PostInitLoad()
87+
CHECK_TICK
88+
postinit_serde_atoms.Cut()
89+
6890
/datum/controller/subsystem/atoms/proc/InitAtom(atom/A, list/arguments)
6991
var/the_type = A.type
7092
if(QDELING(A))

code/controllers/subsystems/mapping.dm

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,27 @@ SUBSYSTEM_DEF(mapping)
149149

150150
setup_data_for_levels(min_z = old_maxz + 1)
151151

152+
// Now that levels are in place, preload any associated persistent data.
153+
// This is to avoid dependencies on other atoms or any other weird ordering
154+
// problems like we used to get with old DMMS and SSatoms.
155+
var/list/preloaded_levels = list()
156+
for(var/z = 1 to length(levels_by_z))
157+
var/datum/level_data/level = levels_by_z[z]
158+
if(level.preload_persistent_data())
159+
preloaded_levels += level
160+
161+
// Now actually load the serde data into the map.
162+
for(var/datum/level_data/level as anything in preloaded_levels)
163+
level.load_persistent_data()
164+
165+
// Clear our reference data for GC
166+
// This might not be needed but it saves refs floating around I guess.
167+
for(var/key in level_persistence_ref_map)
168+
var/list/stale_data = global.level_persistence_ref_map[key]
169+
stale_data.Cut()
170+
171+
global.level_persistence_ref_map.Cut()
172+
152173
// Generate turbolifts last, since away sites may have elevators to generate too.
153174
for(var/obj/abstract/turbolift_spawner/turbolift as anything in turbolifts_to_initialize)
154175
turbolift.build_turbolift()

code/controllers/subsystems/initialization/persistence.dm renamed to code/controllers/subsystems/persistence.dm

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
1+
/datum/admins/proc/force_persistence_save_verb()
2+
set name = "Force Early Level Save"
3+
set category = "Admin"
4+
set desc = "Forces an early level save run by SSpersistence."
5+
if(!SSpersistence)
6+
return
7+
if(UNLINT(SSpersistence._persistent_save_running))
8+
to_chat(usr, SPAN_WARNING("There is already a level save running. Please wait for it to finish."))
9+
return
10+
log_admin("[key_name(usr)] has started an early level save.")
11+
message_admins("[key_name(usr)] has started an early level save.")
12+
SSpersistence.start_persistent_level_save()
13+
114
SUBSYSTEM_DEF(persistence)
215
name = "Persistence"
316
init_order = SS_INIT_MISC_LATE
4-
flags = SS_NO_FIRE | SS_NEEDS_SHUTDOWN
17+
flags = SS_NEEDS_SHUTDOWN
18+
wait = 60 MINUTES
519

6-
var/elevator_fall_path = "data/elevator_falls_tracking.txt"
20+
VAR_PRIVATE/const/ELEVATOR_FALL_PATH = "data/elevator_falls_tracking.txt"
721
var/elevator_fall_shifts = -1 // This is snowflake, but oh well.
822
var/list/tracking_values = list()
23+
VAR_PRIVATE/_persistent_save_running = FALSE
924

1025
/datum/controller/subsystem/persistence/Initialize()
1126
. = ..()
1227

1328
decls_repository.get_decls_of_subtype(/decl/persistence_handler) // Initialize()s persistence categories.
1429

1530
// Begin snowflake.
16-
var/elevator_file = safe_file2text(elevator_fall_path, FALSE)
31+
var/elevator_file = safe_file2text(ELEVATOR_FALL_PATH, FALSE)
1732
if(elevator_file)
1833
elevator_fall_shifts = text2num(elevator_file)
1934
else
@@ -30,9 +45,37 @@ SUBSYSTEM_DEF(persistence)
3045
P.Shutdown()
3146

3247
// Refer to snowflake above.
33-
if(fexists(elevator_fall_path))
34-
fdel(elevator_fall_path)
35-
text2file("[elevator_fall_shifts]", elevator_fall_path)
48+
if(fexists(ELEVATOR_FALL_PATH))
49+
fdel(ELEVATOR_FALL_PATH)
50+
text2file("[elevator_fall_shifts]", ELEVATOR_FALL_PATH)
51+
52+
// Handle level data shutdown.
53+
start_persistent_level_save()
54+
while(_persistent_save_running)
55+
sleep(1)
56+
57+
/datum/controller/subsystem/persistence/fire(resumed)
58+
start_persistent_level_save()
59+
60+
/datum/controller/subsystem/persistence/proc/start_persistent_level_save()
61+
set waitfor = FALSE
62+
if(_persistent_save_running)
63+
return // debounce
64+
_persistent_save_running = TRUE // used to avoid shutting down mid-write
65+
66+
var/started_run = world.time
67+
report_progress("Starting persistent level save.")
68+
// TODO: suspend all subsystems while the save is running
69+
// TODO: prevent player input somehow?
70+
try
71+
for(var/z = 1 to length(SSmapping.levels_by_z))
72+
var/datum/level_data/level = SSmapping.levels_by_z[z]
73+
level.save_persistent_data()
74+
catch(var/exception/E)
75+
error("Exception when running persistent level save: [EXCEPTION_TEXT(E)]")
76+
// TODO: re-enable all subsystems
77+
report_progress("Persistent level save finished in [(world.time-started_run)/10] second\s.")
78+
_persistent_save_running = FALSE
3679

3780
/datum/controller/subsystem/persistence/proc/track_value(var/atom/value, var/track_type)
3881

@@ -45,7 +88,7 @@ SUBSYSTEM_DEF(persistence)
4588
return
4689

4790
var/datum/level_data/level = SSmapping.levels_by_z[T.z]
48-
if(!istype(level) || !level.permit_persistence)
91+
if(!istype(level) || !level.permit_legacy_persistence)
4992
return
5093

5194
if(!tracking_values[track_type])

code/datums/datum.dm

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
var/list/active_timers
1010
/// Used to avoid unnecessary refstring creation in Destroy().
1111
var/tmp/has_state_machine = FALSE
12+
/// Var for holding a unique-to-this-run identifier for a serialized datum.
13+
VAR_PRIVATE/__run_uid
1214

1315
#ifdef REFTRACKING_ENABLED
1416
var/tmp/running_find_references
@@ -114,3 +116,22 @@
114116
*/
115117
/datum/proc/PopulateClone(var/datum/clone)
116118
return clone
119+
120+
// Used for saving instances via the level persistence system.
121+
// Returns an assoc list of var name to var value.
122+
// Expected format is:
123+
// list("field" = "value", "so on" = "so forth"))
124+
// If serializing an instance reference, use get_run_uid() to get a UID.
125+
// Hardcoded/non-var keys:
126+
// - (SERDE_TYPE) = type (or a different serde type)
127+
// - (SERDE_LOC) = list(loc.x, loc.y)
128+
/datum/proc/Serialize()
129+
SHOULD_CALL_PARENT(TRUE)
130+
. = list((SERDE_TYPE) = type)
131+
132+
// Returns a UID for this instance, used for serde across rounds.
133+
// Probably-kind-of a GUID but only for this run.
134+
/datum/proc/get_run_uid()
135+
if(isnull(__run_uid))
136+
__run_uid = "\ref[src]-[sequential_id(type)]" // Staple seq_id on there in case of \ref reuse.
137+
return __run_uid

code/datums/repositories/decls.dm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@
2020

2121
var/global/repository/decls/decls_repository = new
2222

23+
/proc/decls_are_equivalent(decl/first, decl/second)
24+
first = RESOLVE_TO_DECL(first)
25+
second = RESOLVE_TO_DECL(second)
26+
return second == first
27+
28+
/proc/resolve_decl_uid_list(list/decl_uids)
29+
for(var/uid in decl_uids)
30+
var/decl/decl = decls_repository.get_decl_by_id(uid)
31+
if(istype(decl))
32+
LAZYADD(., decl)
33+
2334
/repository/decls
2435
var/list/fetched_decls = list()
2536
var/list/fetched_decl_ids = list()

code/game/atoms_serde.dm

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/atom
2+
/// Var for holding serde information when this atom was loaded from a persistent source.
3+
var/__init_deserialization_payload
4+
5+
// Called when an atom is being preloaded with information from deserialization.
6+
/atom/proc/Preload()
7+
SHOULD_CALL_PARENT(TRUE)
8+
SHOULD_NOT_SLEEP(TRUE)
9+
if(__init_deserialization_payload)
10+
try
11+
. = Deserialize()
12+
catch(var/exception/E)
13+
PRINT_STACK_TRACE("Exception when deserializing [type]: [E]")
14+
__init_deserialization_payload = null
15+
else
16+
PRINT_STACK_TRACE("[type] tried to preload with no deserialization payload.")
17+
18+
/atom/proc/PreloadKey(data_key, payload)
19+
return
20+
21+
/atom/proc/Deserialize()
22+
SHOULD_CALL_PARENT(TRUE)
23+
SHOULD_NOT_SLEEP(TRUE)
24+
for(var/data_key in __init_deserialization_payload)
25+
if(data_key in vars)
26+
try
27+
if(!global._forbid_field_load[data_key] && (data_key in vars))
28+
vars[data_key] = __init_deserialization_payload[data_key]
29+
else
30+
PreloadKey(data_key, __init_deserialization_payload[data_key])
31+
catch(var/exception/E)
32+
error("Failed to write [data_key] to [type] vars: [E]")
33+
return SERDE_HINT_FINISHED
34+
35+
// Called after Initialize()/LateInitialize() if an atom returns SERDE_HINT_POSTINIT to Deserialize().
36+
/atom/proc/PostInitLoad()
37+
return
38+
39+
/atom/movable/Serialize()
40+
. = ..()
41+
if(isturf(loc))
42+
.[SERDE_LOC] = list(loc.x, loc.y)
43+
// The below does not handle cases where the nested instance is not itself persistent.
44+
// In this case, if the instance tried to serialize while inside a non-persistent instance, it would
45+
// throw a runtime on subsequent loads due to having a UID as a loc that does not map to a loaded instance.
46+
else if(isatom(loc))
47+
.[SERDE_LOC] = loc.get_run_uid()

code/game/turfs/floors/_floor.dm

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434

3535
set_turf_materials(floor_material, skip_update = TRUE)
3636

37+
if(istext(_flooring))
38+
_flooring = resolve_decl_uid_list(cached_json_decode(_flooring))
39+
if(!length(_flooring))
40+
_flooring = null
41+
3742
if(!floortype && (ispath(_flooring) || islist(_flooring)))
3843
floortype = _flooring
3944
else
@@ -49,7 +54,6 @@
4954
if(ml) // We skipped the update above to avoid updating our neighbors, but we need to update ourselves.
5055
lazy_update_icon()
5156

52-
5357
/turf/floor/ChangeTurf(turf/N, tell_universe, force_lighting_update, keep_air, update_open_turfs_above, keep_height)
5458
if(is_processing)
5559
STOP_PROCESSING(SSobj, src)

0 commit comments

Comments
 (0)