Skip to content

Commit bfb3ca2

Browse files
Adds a per-level persistence system.
1 parent 7f780a5 commit bfb3ca2

File tree

95 files changed

+1446
-531
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+1446
-531
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_NO_LEGACY_PERSISTENCE 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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Handled elsewhere, do not let them load like vars.
2+
var/global/list/_forbid_field_load = list(
3+
(nameof(/datum::type)) = TRUE,
4+
(nameof(/atom::loc)) = TRUE
5+
)

code/__defines/serde.dm

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#define SERDE_HINT_FINISHED 1
2+
#define SERDE_HINT_POSTINIT 2
3+
4+
#define SERDE_REAGENT_LIST "_reagent_list"
5+
#define SERDE_REAGENT_VOLUME "_reagent_volume"
6+
7+
#define SERIALIZE_VALUE(V, T, VAL) .[nameof(T::V)] = VAL;
8+
#define SERIALIZE(V, T) SERIALIZE_VALUE(V, T, V)
9+
#define SERIALIZE_IF_MODIFIED(V, T) if(V != initial(V)) { SERIALIZE_VALUE(V, T, V) }
10+
#define SERIALIZE_TYPE_IF_MODIFIED(V, T) if(V != initial(V)) { SERIALIZE_VALUE(V, T, "[V]") }
11+
#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) }
12+
#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) }
13+
#define SERIALIZE_REAGENTS(V, T, I) if(istype(V, /datum/reagents)) { \
14+
.[I + SERDE_REAGENT_VOLUME] = V.maximum_volume; \
15+
if(V.total_volume) { \
16+
var/list/__compiled_reagents = list(); \
17+
for(var/decl/material/R in V.liquid_volumes) { \
18+
__compiled_reagents[++__compiled_reagents.len] = list(R.uid, V.liquid_volumes[R], (MAT_PHASE_LIQUID)); \
19+
} \
20+
for(var/decl/material/R in V.solid_volumes) { \
21+
__compiled_reagents[++__compiled_reagents.len] = list(R.uid, V.solid_volumes[R], (MAT_PHASE_SOLID)); \
22+
} \
23+
.[I + SERDE_REAGENT_LIST] = __compiled_reagents; \
24+
} else { \
25+
.[I + SERDE_REAGENT_LIST] = list(); \
26+
} \
27+
} else { \
28+
.[I + SERDE_REAGENT_LIST] = list(); \
29+
.[I + SERDE_REAGENT_VOLUME] = 0; \
30+
}
31+
32+
#define DESERIALIZE_REAGENTS(V, I) if(((I + SERDE_REAGENT_LIST) in __deserialization_payload) && ((I + SERDE_REAGENT_VOLUME) in __deserialization_payload)) { \
33+
V = list((SERDE_REAGENT_VOLUME) = __deserialization_payload[I + SERDE_REAGENT_VOLUME], (SERDE_REAGENT_LIST) = __deserialization_payload[I + SERDE_REAGENT_LIST]); \
34+
}
35+
36+
#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; }
37+
#define DESERIALIZE_TYPE(V) if(istext(V)) { V = text2path(V); } else if(!ispath(V)) { V = null; }
38+
#define DESERIALIZE_DECL(V) if(istext(V) || ispath(V)) { V = RESOLVE_TO_DECL(V); } else { V = null; }
39+
40+
// List cast is to avoid OpenDream complaining about V typically being typed as a reagents datum, but holding a list for serde.
41+
#define FINALIZE_REAGENTS_SERDE_BODY(V) try { \
42+
if((SERDE_REAGENT_LIST in V) && (SERDE_REAGENT_VOLUME in V)) { \
43+
var/list/LV = V; \
44+
var/__serde_volume = LV[SERDE_REAGENT_VOLUME]; \
45+
if(__serde_volume <= 0) { \
46+
V = null; \
47+
} else { \
48+
var/list/__serde_reagents = LV[SERDE_REAGENT_LIST]; \
49+
V = new /datum/reagents(__serde_volume, src); \
50+
for(var/list/entry in __serde_reagents) { \
51+
V.add_reagent(RESOLVE_TO_DECL(entry[1]), entry[2], phase = entry[3]); \
52+
} \
53+
} \
54+
} else { \
55+
V = null; \
56+
} \
57+
} catch(var/exception/E) { \
58+
log_error("Exception while finalizing reagents load for [type]: [EXCEPTION_TEXT(E)]"); \
59+
V = null; \
60+
}
61+
62+
#define FINALIZE_REAGENTS_SERDE(V) if(islist(V)) { FINALIZE_REAGENTS_SERDE_BODY(V); }
63+
#define FINALIZE_REAGENTS_SERDE_AND_RETURN(V) if(islist(V)) { FINALIZE_REAGENTS_SERDE_BODY(V); return; }

code/__defines/subsystems.dm

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@
1919
// Subsystems shutdown in the reverse of the order they initialize in
2020
// The numbers just define the ordering, they are meaningless otherwise.
2121

22-
#define SS_INIT_INPUT 22
23-
#define SS_INIT_EARLY 21
24-
#define SS_INIT_WEBHOOKS 20
25-
#define SS_INIT_MODPACKS 19
26-
#define SS_INIT_SECRETS 18
27-
#define SS_INIT_GARBAGE 17
22+
#define SS_INIT_INPUT 23
23+
#define SS_INIT_EARLY 22
24+
#define SS_INIT_WEBHOOKS 21
25+
#define SS_INIT_MODPACKS 20
26+
#define SS_INIT_SECRETS 19
27+
#define SS_INIT_GARBAGE 18
28+
#define SS_INIT_SERDE 17
2829
#define SS_INIT_MATERIALS 16
2930
#define SS_INIT_PLANTS 15
3031
#define SS_INIT_LORE 14

code/_helpers/serde.dm

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/proc/instantiate_serialized_data(load_z, requestor, list/instance_map, entries_decay_at, entry_decay_weight)
2+
3+
var/list/nested_instances = list()
4+
var/list/instanced_areas = list()
5+
var/list/created_data = list()
6+
7+
LAZYINITLIST(instance_map)
8+
9+
to_world_log("Finalising load of [length(instance_map)] instance\s for level '[requestor]'.")
10+
for(var/uid in instance_map)
11+
12+
var/list/instance_data = instance_map[uid]
13+
try
14+
15+
var/raw_load_path = instance_data[nameof(/datum::type)]
16+
var/load_path = ispath(raw_load_path, /datum) ? raw_load_path : text2path(raw_load_path)
17+
if(!ispath(load_path, /datum))
18+
error("[requestor]: attempted to load persistent instance with invalid or non-/datum type '[raw_load_path]'")
19+
continue
20+
21+
var/datum/created_instance
22+
23+
// Instance is a /datum.
24+
// Just pass the data in and assume the datum type knows what to do with it.
25+
if(!ispath(load_path, /atom) && ispath(load_path, /datum))
26+
created_instance = new load_path(instance_data)
27+
created_data += created_instance
28+
else
29+
var/list/spawn_data = instance_data[nameof(/atom/movable::loc)]
30+
if(spawn_data)
31+
32+
if(isnull(spawn_data) || length(spawn_data) < 3)
33+
error("[requestor]: attempted to load persistent instance with malformed loc.")
34+
continue
35+
36+
// Instance has a world coordinate.
37+
if(islist(spawn_data))
38+
var/turf/spawn_loc = locate(spawn_data[1], spawn_data[2], isnull(load_z) ? spawn_data[3] : load_z)
39+
if(!istype(spawn_loc))
40+
error("[requestor]: attempted to load persistent instance but could not find spawn loc.")
41+
continue
42+
if(ispath(load_path, /turf))
43+
if(spawn_loc.type == load_path)
44+
created_instance = spawn_loc
45+
else
46+
created_instance = spawn_loc.ChangeTurf(load_path)
47+
48+
// TODO: Areas will need bespoke handling for non-subtype-related persistence (blueprint renaming etc).
49+
else if(ispath(load_path, /area))
50+
var/area/area = instanced_areas[load_path]
51+
if(!area)
52+
area = new load_path(null)
53+
instanced_areas[load_path] = area
54+
ChangeArea(spawn_loc, area)
55+
56+
else if(ispath(load_path, /atom))
57+
created_instance = new load_path(spawn_loc)
58+
spawn_loc._contents_were_modified = TRUE // ensure
59+
else
60+
error("[requestor]: attempted to instantiate unimplemented path '[load_path]'.")
61+
continue
62+
63+
// Instance is inside another instance; implies/requires /atom/movable
64+
else if(istext(spawn_data))
65+
if(!ispath(load_path, /atom/movable))
66+
error("[requestor]: tried to spawn non-movable [load_path] inside an instance.")
67+
continue
68+
created_instance = new load_path
69+
nested_instances[created_instance] = spawn_data
70+
71+
else
72+
error("[requestor]: attempted to load persistent instance with malformed loc.")
73+
continue
74+
75+
else
76+
// Should we just go ahead and do this to create atoms in nullspace?
77+
// Would we ever want to track an atom in nullspace via level persistence?
78+
error("[requestor]: attempted to load non-/datum persistent instance with no spawn loc.")
79+
80+
if(istype(created_instance))
81+
LAZYSET(., uid, created_instance)
82+
if(isatom(created_instance))
83+
var/atom/atom = created_instance
84+
atom.__deserialization_payload = instance_data
85+
SSatoms.deserialized_atoms[uid] = atom
86+
if(!isnull(entries_decay_at) && !isnull(entry_decay_weight))
87+
created_instance.HandlePersistentDecay(entries_decay_at, entry_decay_weight)
88+
89+
catch(var/exception/E)
90+
log_error("Exception during persistent instance load - [islist(instance_data) ? json_encode(instance_data) : "no instance data"]: [EXCEPTION_TEXT(E)]")
91+
92+
// Atoms use SSatoms for this, datums don't go through SSatoms so need to do it here.
93+
for(var/datum/instance in created_data)
94+
instance.DeserializePostInit(.)
95+
96+
// Resolve any loc references to instances.
97+
for(var/atom/movable/atom as anything in nested_instances)
98+
var/nested_atom_id = nested_instances[atom]
99+
var/atom/nested_atom = .[nested_atom_id]
100+
if(!istype(nested_atom))
101+
error("[requestor]: could not resolve instance ref [nested_atom_id] to instance.")
102+
continue
103+
atom.forceMove(nested_atom)
104+
nested_atom.contents_were_modified()
105+
106+
// Now that everything is loaded and placed, clear out anything that should not be present on the turfs we've loaded.
107+
for(var/uid in SSatoms.deserialized_atoms)
108+
var/turf/turf = SSatoms.deserialized_atoms[uid]
109+
if(!istype(turf))
110+
continue
111+
for(var/atom/thing in turf)
112+
if(!thing.simulated)
113+
continue
114+
if(!isnull(thing.__deserialization_payload))
115+
continue
116+
qdel(thing)
117+
118+
to_world_log("[requestor] loaded [length(.)] persistent instance\s.")
119+
120+
/proc/apply_serde_message_decay(_message, _age, _decay_weight, _decay_at)
121+
var/static/list/decayed_chars = list(".",",","-","'","\\","/","\"",":",";")
122+
if(_age < _decay_at || isnull(_message))
123+
return _message
124+
. = ""
125+
for(var/i = 1 to length(_message))
126+
var/char = copytext(_message, i, i + 1)
127+
if(prob(round(_age * _decay_weight)))
128+
if(prob(99))
129+
. += pick(decayed_chars)
130+
else
131+
. += char

code/controllers/subsystems/atoms.dm

Lines changed: 36 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+
/// An associative list of UIDs to 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,20 @@ 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/uid = deserialized_atoms[index++]
40+
var/atom/instance = deserialized_atoms[uid]
41+
if(instance.Preload(deserialized_atoms) == SERDE_HINT_POSTINIT)
42+
postinit_serde_atoms += instance
43+
CHECK_TICK
44+
report_progress("Deserialized [index-1] atom\s.")
45+
index = 1
46+
47+
var/list/mapload_arg = list(TRUE)
3548
// Things can add to the end of this list while we iterate, so we can't use a for loop.
3649
while(index <= length(created_atoms))
3750
// Don't remove from this list while we run, that's expensive.
@@ -49,10 +62,10 @@ SUBSYSTEM_DEF(atoms)
4962
else
5063
InitAtom(A, mapload_arg)
5164
CHECK_TICK
52-
53-
report_progress("Initialized [index] atom\s")
5465
created_atoms.Cut()
5566

67+
report_progress("Initialized [index-1] atom\s.")
68+
5669
atom_init_stage = INITIALIZATION_INNEW_REGULAR
5770

5871
if(length(late_loaders))
@@ -65,6 +78,25 @@ SUBSYSTEM_DEF(atoms)
6578
report_progress("Late initialized [index] atom\s")
6679
late_loaders.Cut()
6780

81+
if(length(postinit_serde_atoms))
82+
index = 1
83+
while(index <= length(postinit_serde_atoms))
84+
var/atom/instance = postinit_serde_atoms[index++]
85+
instance.DeserializePostInit(deserialized_atoms)
86+
CHECK_TICK
87+
postinit_serde_atoms.Cut()
88+
89+
// Clear out the serde payloads now that everything should be tidied away.
90+
if(length(deserialized_atoms))
91+
index = 1
92+
while(index <= length(deserialized_atoms))
93+
var/uid = deserialized_atoms[index++]
94+
var/atom/instance = deserialized_atoms[uid]
95+
if(istype(instance))
96+
instance.__deserialization_payload = null
97+
CHECK_TICK
98+
deserialized_atoms.Cut()
99+
68100
/datum/controller/subsystem/atoms/proc/InitAtom(atom/A, list/arguments)
69101
var/the_type = A.type
70102
if(QDELING(A))

code/controllers/subsystems/mapping.dm

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,36 @@ 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/datum/level_data/level in levels_by_z)
157+
if(level.preload_persistent_data())
158+
preloaded_levels += level
159+
160+
// Now actually load the serde data into the map.
161+
for(var/datum/level_data/level as anything in preloaded_levels)
162+
level.load_persistent_data()
163+
164+
// Clear our reference data for GC
165+
// This might not be needed but it saves refs floating around I guess.
166+
for(var/key in level_persistence_ref_map)
167+
var/list/stale_data = global.level_persistence_ref_map[key]
168+
stale_data.Cut()
169+
170+
global.level_persistence_ref_map.Cut()
171+
172+
// With levels set up and serde complete (and levels flagged) we can do any remaining level generation.
173+
for(var/datum/level_data/level in levels_by_z)
174+
level.generate_level()
175+
176+
global.using_map.finalize_map_generation()
177+
152178
// Generate turbolifts last, since away sites may have elevators to generate too.
153179
for(var/obj/abstract/turbolift_spawner/turbolift as anything in turbolifts_to_initialize)
154180
turbolift.build_turbolift()
155181

156-
global.using_map.finalize_map_generation()
157-
158182
. = ..()
159183

160184
/datum/controller/subsystem/mapping/proc/setup_data_for_levels(min_z = 1, max_z = world.maxz)

0 commit comments

Comments
 (0)