Skip to content

Commit 1802990

Browse files
Adding a limited persistence system for doing serde based on level_id.
1 parent f041131 commit 1802990

File tree

10 files changed

+257
-4
lines changed

10 files changed

+257
-4
lines changed

code/controllers/subsystems/atoms.dm

Lines changed: 9 additions & 0 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,6 +31,13 @@ SUBSYSTEM_DEF(atoms)
2931

3032
atom_init_stage = INITIALIZATION_INNEW_MAPLOAD
3133

34+
// Preload any atoms that have deserialized during the initial load process prior to flush.
35+
while(length(deserialized_atoms))
36+
var/atom/A = deserialized_atoms[deserialized_atoms.len]
37+
deserialized_atoms.len--
38+
A.Preload()
39+
CHECK_TICK
40+
3241
var/list/mapload_arg = list(TRUE)
3342

3443
var/index = 1

code/controllers/subsystems/initialization/persistence.dm

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
SUBSYSTEM_DEF(persistence)
22
name = "Persistence"
33
init_order = SS_INIT_MISC_LATE
4-
flags = SS_NO_FIRE | SS_NEEDS_SHUTDOWN
4+
flags = SS_NEEDS_SHUTDOWN
5+
wait = 15 MINUTES
56

6-
var/elevator_fall_path = "data/elevator_falls_tracking.txt"
7-
var/elevator_fall_shifts = -1 // This is snowflake, but oh well.
8-
var/list/tracking_values = list()
7+
VAR_PRIVATE/elevator_fall_path = "data/elevator_falls_tracking.txt"
8+
VAR_PRIVATE/elevator_fall_shifts = -1 // This is snowflake, but oh well.
9+
VAR_PRIVATE/list/tracking_values = list()
10+
VAR_PRIVATE/_persistent_save_running = FALSE
911

1012
/datum/controller/subsystem/persistence/Initialize()
1113
. = ..()
@@ -34,6 +36,30 @@ SUBSYSTEM_DEF(persistence)
3436
fdel(elevator_fall_path)
3537
text2file("[elevator_fall_shifts]", elevator_fall_path)
3638

39+
// Handle level data shutdown.
40+
start_persistent_level_save()
41+
while(_persistent_save_running)
42+
sleep(1)
43+
44+
/datum/controller/subsystem/persistence/fire(resumed)
45+
. = ..()
46+
start_persistent_level_save()
47+
48+
/datum/controller/subsystem/persistence/proc/start_persistent_level_save()
49+
// Is this actually desirable? People moving around or modifying
50+
// atoms across save could result in inconsistent data.
51+
set waitfor = FALSE
52+
if(_persistent_save_running)
53+
return // debounce
54+
_persistent_save_running = TRUE // used to avoid shutting down mid-write
55+
try
56+
for(var/z = 1 to length(SSmapping.levels_by_z))
57+
var/datum/level_data/level = SSmapping.levels_by_z[z]
58+
level.save_persistent_data()
59+
catch(var/exception/E)
60+
error("Exception when running persistent level save: [EXCEPTION_TEXT(E)]")
61+
_persistent_save_running = FALSE
62+
3763
/datum/controller/subsystem/persistence/proc/track_value(var/atom/value, var/track_type)
3864

3965
var/turf/T = get_turf(value)

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/game/atoms_serde.dm

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
PreloadData()
12+
catch(var/exception/E)
13+
PRINT_STACK_TRACE("Exception when preloading [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/PreloadData()
19+
SHOULD_CALL_PARENT(TRUE)
20+
SHOULD_NOT_SLEEP(TRUE)
21+
for(var/data_key in __init_deserialization_payload)
22+
if(data_key in vars)
23+
try
24+
vars[data_key] = __init_deserialization_payload[data_key]
25+
catch(var/exception/E)
26+
error("Failed to write [data_key] to [type] vars: [E]")
27+
__init_deserialization_payload = null
28+
29+
// Used for saving atoms via the level persistence system.
30+
// Returns an assoc list of var name to var value.
31+
// Expected format is:
32+
// list("atom field" = "atom value", "so on" = "so forth"))
33+
// If serializing an atom reference, use "\ref[atom]" as a GUID.
34+
// Hardcoded/non-var keys:
35+
// - "type" = type (or a different serde type)
36+
// - "location" = list(loc.x, loc.y)
37+
/atom/proc/Serialize()
38+
return

code/modules/multiz/level_data.dm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@
232232

233233
///Prepare level for being used. Setup borders, lateral z connections, ambient lighting, atmosphere, etc..
234234
/datum/level_data/proc/setup_level_data(var/skip_gen = FALSE)
235+
235236
if(_level_setup_completed)
236237
log_debug("level_data for [src], on z [level_z], had setup_level_data called more than once!")
237238
return //Since we can defer setup, make sure we only setup once
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/decl/level_persistence_handler/proc/get_data_path(location, map, level)
2+
return "[location]/[map]/[level]"
3+
4+
/decl/level_persistence_handler/proc/save_data(datum/level_data/level_data, location, map, level)
5+
return // Unimplemented, so return null to indicate a failure
6+
7+
/decl/level_persistence_handler/proc/load_data(location, map, level)
8+
return // Unimplemented, so return null to indicate a failure
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/decl/level_persistence_handler/json/get_data_path(location, map, level)
2+
return "[..()].json"
3+
4+
/decl/level_persistence_handler/json/save_data(datum/level_data/level_data, location, map, level)
5+
var/save_file = get_data_path(location, map, level)
6+
try
7+
var/list/save_data = level_data.get_persistent_data()
8+
if(!length(save_data))
9+
return
10+
11+
var/write_data = json_encode(save_data)
12+
var/write_file = file(save_file)
13+
14+
// Do a backup (at the end to avoid overwriting then throwing an exception)
15+
if(fexists(save_file))
16+
var/backup_contents = file2text(save_file)
17+
var/backup_file = file("[save_file].backup")
18+
to_file(backup_file, backup_contents)
19+
// Clear old file to avoid appending data.
20+
fdel(save_file)
21+
22+
// Finally, write out our new json.
23+
to_file(write_file, write_data)
24+
25+
catch(var/exception/E)
26+
error("Exception when saving persistent level data to [save_file]: [EXCEPTION_TEXT(E)]")
27+
return null
28+
return 1 // Return a non-null value just to show we didn't throw an exception.
29+
30+
/decl/level_persistence_handler/json/load_data(location, map, level)
31+
var/load_file = get_data_path(location, map, level)
32+
try
33+
if(fexists(load_file)) // done separately to avoid generating an error for levels with no saved data
34+
var/loaded_json = safe_file2text(load_file)
35+
if(loaded_json)
36+
return json_decode(loaded_json) // do not cache this giant blob pls
37+
catch(var/exception/E)
38+
error("Exception when loading persistent level data from [load_file]: [EXCEPTION_TEXT(E)]")
39+
return null
40+
return 1 // Return a non-null value just to show we didn't throw an exception.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
var/global/list/level_persistence_ref_map = list()
2+
/datum/level_data
3+
/// String pointing to a base directory on the filesystem for use in serde.
4+
/// If set, will automatically suffix map path and level name.
5+
/// Leave null to opt out of any persistence for this level.
6+
var/persistent_data_location
7+
/// Decl handler, mostly forcing myself to keep this general so it can be
8+
// optimized with a DB or something down the track.
9+
var/persistence_handler = /decl/level_persistence_handler/json
10+
11+
/datum/level_data/proc/get_persistent_data()
12+
. = list()
13+
var/list/atoms_to_save = get_persistent_atoms()
14+
if(!length(atoms_to_save))
15+
return
16+
for(var/atom/thing as anything in get_persistent_atoms())
17+
var/serialized_atom = thing.Serialize()
18+
if(length(serialized_atom))
19+
.["\ref[thing]"] = serialized_atom
20+
21+
// Returns a linear list of atoms that we are interested in saving.
22+
/datum/level_data/proc/get_persistent_atoms()
23+
return
24+
25+
// First load all the raw data into memory so every reference is populated.
26+
/datum/level_data/proc/preload_persistent_data()
27+
28+
// Don't bother if we aren't configured for it at all.
29+
if(!persistence_handler || !persistent_data_location)
30+
return FALSE
31+
32+
// Basic sanity check.
33+
if(!level_id)
34+
persistent_data_location = null
35+
PRINT_STACK_TRACE("Level data [type] tried to initialize persistent data but had no level_id.")
36+
return FALSE
37+
38+
// Atoms on a map are expected to be returned as an associative list with some specific text keys.
39+
var/decl/level_persistence_handler/load_handler = GET_DECL(persistence_handler)
40+
var/list/loaded_data = load_handler?.load_data(persistent_data_location, global.using_map.path, ckey(level_id))
41+
if(islist(loaded_data) && length(loaded_data))
42+
var/list/atom_map = list()
43+
global.level_persistence_ref_map[level_id] = atom_map
44+
for(var/uid in loaded_data)
45+
atom_map[uid] = loaded_data[uid]
46+
return TRUE
47+
return FALSE
48+
49+
// Now create the atoms and register them in the global map. Note that levels with no level_id
50+
// or no persistence handling set will not reach this proc.
51+
/datum/level_data/proc/load_persistent_data()
52+
var/load_count = 0
53+
var/list/atom_map = global.level_persistence_ref_map[level_id]
54+
for(var/uid in atom_map)
55+
56+
var/list/atom_data = atom_map[uid]
57+
58+
// TODO: datum handling? Or expect atoms to create datums internally during serde?
59+
var/atom_type = text2path(atom_data["type"])
60+
if(!ispath(atom_type, /atom))
61+
error("[level_id]: attempted to load persistent atom with invalid or non-atom type.")
62+
continue
63+
64+
// TODO: maybe we can hook DMMS for loading individual atoms?
65+
// TODO: if we can write our serde out as .TGM we won't need bespoke handling at all.
66+
67+
var/atom/created_atom
68+
var/list/atom_coords = atom_data["location"]
69+
if(atom_coords)
70+
if(!islist(atom_coords) || length(atom_coords) < 2)
71+
error("[level_id]: attempted to load persistent atom with malformed coordinates.")
72+
continue
73+
var/turf/spawn_loc = locate(atom_coords[1], atom_coords[2], level_z)
74+
if(!istype(spawn_loc))
75+
error("[level_id]: attempted to load persistent atom but could not find spawn loc.")
76+
continue
77+
if(ispath(atom_type, /turf))
78+
created_atom = spawn_loc.ChangeTurf(atom_type)
79+
else if(ispath(atom_type, /area))
80+
// VERY BAD, TODO
81+
ChangeArea(spawn_loc, new atom_type)
82+
created_atom = get_area(spawn_loc)
83+
else
84+
created_atom = new atom_type(spawn_loc)
85+
86+
else if(ispath(atom_type, /turf) || ispath(atom_type, /area))
87+
error("[level_id]: attempted to load persistent turf or area with no corresponding coordinate.")
88+
continue
89+
else
90+
created_atom = new atom_type
91+
92+
load_count++
93+
created_atom.__init_deserialization_payload = atom_data["data"]
94+
atom_map[uid] = created_atom
95+
SSatoms.deserialized_atoms += created_atom
96+
97+
to_world_log("[level_id]/[name] loaded [load_count] persistent atom\s.")
98+
return !!load_count
99+
100+
// Write any data out if we need to.
101+
/datum/level_data/proc/save_persistent_data()
102+
// TODO: block any changes to persistent data structures while save is running?
103+
if(persistence_handler && persistent_data_location && level_id)
104+
var/decl/level_persistence_handler/save_handler = GET_DECL(persistence_handler)
105+
save_handler?.save_data(src, persistent_data_location, global.using_map.path, ckey(level_id))

maps/tradeship/tradeship_levels.dm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
/datum/level_data/main_level/tradeship
1414
abstract_type = /datum/level_data/main_level/tradeship
15+
persistent_data_location = "data/level_data"
1516

1617
/datum/level_data/main_level/tradeship/basement
1718
name = "Tradeship Basement Deck"

nebula.dme

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,7 @@
756756
#include "code\game\atoms_movable_grabs.dm"
757757
#include "code\game\atoms_movable_interactions.dm"
758758
#include "code\game\atoms_movable_overlay.dm"
759+
#include "code\game\atoms_serde.dm"
759760
#include "code\game\atoms_temperature.dm"
760761
#include "code\game\base_turf.dm"
761762
#include "code\game\movietitles.dm"
@@ -3239,6 +3240,9 @@
32393240
#include "code\modules\multiz\hoist.dm"
32403241
#include "code\modules\multiz\ladder.dm"
32413242
#include "code\modules\multiz\level_data.dm"
3243+
#include "code\modules\multiz\level_persistence_handler.dm"
3244+
#include "code\modules\multiz\level_persistence_handler_json.dm"
3245+
#include "code\modules\multiz\level_persistence_serialization.dm"
32423246
#include "code\modules\multiz\map_data.dm"
32433247
#include "code\modules\multiz\mobile_ladder.dm"
32443248
#include "code\modules\multiz\movement.dm"

0 commit comments

Comments
 (0)