diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm
index e4a2e6521a..a860a8cd3f 100644
--- a/code/__HELPERS/roundend.dm
+++ b/code/__HELPERS/roundend.dm
@@ -238,8 +238,11 @@
CHECK_TICK
SSdbcore.SetRoundEnd()
//Collects persistence features
- if(mode.allow_persistence_save)
- SSpersistence.CollectData()
+ if(mode.station_was_nuked)
+ SSpersistence.station_was_destroyed = TRUE
+ if(!mode.allow_persistence_save)
+ SSpersistence.station_persistence_save_disabled = TRUE
+ SSpersistence.CollectData()
//stop collecting feedback during grifftime
SSblackbox.Seal()
diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm
index 67e4a97894..dc8e9e1859 100644
--- a/code/controllers/configuration/entries/game_options.dm
+++ b/code/controllers/configuration/entries/game_options.dm
@@ -566,3 +566,20 @@
config_entry_value = 0.333
min_val = 0
integer = FALSE
+
+/// Amount of dirtyness tiles need to spawn dirt.
+/datum/config_entry/number/turf_dirt_threshold
+ config_entry_value = 100
+ min_val = 1
+ integer = TRUE
+
+/// Alpha dirt starts at
+/datum/config_entry/number/dirt_alpha_starting
+ config_entry_value = 127
+ max_val = 255
+ min_val = 0
+ integer = TRUE
+
+/// Dirtyness multiplier for making turfs dirty
+/datum/config_entry/number/turf_dirty_multiplier
+ config_entry_value = 1
diff --git a/code/controllers/configuration/entries/persistence.dm b/code/controllers/configuration/entries/persistence.dm
new file mode 100644
index 0000000000..2444034b86
--- /dev/null
+++ b/code/controllers/configuration/entries/persistence.dm
@@ -0,0 +1,20 @@
+/// Whether or not to use the persistence system for cleanable objects
+/datum/config_entry/flag/persistent_debris
+ config_entry_value = FALSE
+
+/// Whether or not to nuke all roundstart debris that isn't due to persistence if the above is true
+/datum/config_entry/flag/persistent_debris_only
+ config_entry_value = TRUE
+
+/// Max amount of objects to store, total
+/datum/config_entry/number/persistent_debris_global_max
+ config_entry_value = 10000
+ integer = TRUE
+
+/// Max amount of objects to store per type
+/datum/config_entry/number/persistent_debris_type_max
+ config_entry_value = 2000
+ integer = TRUE
+
+/// Wipe dirty stuff on nuke
+/datum/config_entry/flag/persistent_debris_wipe_on_nuke
diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm
index 2ea3b29c91..90908bbde0 100644
--- a/code/controllers/subsystem/mapping.dm
+++ b/code/controllers/subsystem/mapping.dm
@@ -41,6 +41,8 @@ SUBSYSTEM_DEF(mapping)
var/datum/space_level/transit
var/datum/space_level/empty_space
var/num_of_res_levels = 1
+ /// Lookup for zlevel to station z. text = num.
+ var/list/z_to_station_z_index
var/stat_map_name = "Loading..."
@@ -259,6 +261,16 @@ SUBSYSTEM_DEF(mapping)
INIT_ANNOUNCE("Loaded [name] in [(REALTIMEOFDAY - start_time)/10]s!")
return parsed_maps
+/datum/controller/subsystem/mapping/proc/setup_station_z_index()
+ z_to_station_z_index = list()
+ var/sz = 1
+ var/cz = station_start
+ if(islist(config.map_file))
+ for(var/map in config.map_file)
+ z_to_station_z_index["[cz++]"] = sz++
+ else
+ z_to_station_z_index["[station_start]"] = 1
+
/datum/controller/subsystem/mapping/proc/loadWorld()
//if any of these fail, something has gone horribly, HORRIBLY, wrong
var/list/FailedZs = list()
@@ -271,6 +283,8 @@ SUBSYSTEM_DEF(mapping)
INIT_ANNOUNCE("Loading [config.map_name]...")
LoadGroup(FailedZs, "Station", config.map_path, config.map_file, config.traits, ZTRAITS_STATION, FALSE, config.orientation)
+ setup_station_z_index()
+
if(SSdbcore.Connect())
var/datum/DBQuery/query_round_map_name = SSdbcore.NewQuery("UPDATE [format_table_name("round")] SET map_name = '[config.map_name]' WHERE id = [GLOB.round_id]")
query_round_map_name.Execute()
diff --git a/code/controllers/subsystem/persistence.dm b/code/controllers/subsystem/persistence/_persistence.dm
similarity index 51%
rename from code/controllers/subsystem/persistence.dm
rename to code/controllers/subsystem/persistence/_persistence.dm
index e39242aac3..741d1dd72a 100644
--- a/code/controllers/subsystem/persistence.dm
+++ b/code/controllers/subsystem/persistence/_persistence.dm
@@ -5,18 +5,14 @@ SUBSYSTEM_DEF(persistence)
name = "Persistence"
init_order = INIT_ORDER_PERSISTENCE
flags = SS_NO_FIRE
- var/list/satchel_blacklist = list() //this is a typecache
- var/list/new_secret_satchels = list() //these are objects
- var/list/old_secret_satchels = list()
+
+ /// Marks if the station got horribly destroyed
+ var/station_was_destroyed = FALSE
+ /// Marks if persistence save should be disabled
+ var/station_persistence_save_disabled = FALSE
var/list/obj/structure/chisel_message/chisel_messages = list()
var/list/saved_messages = list()
- var/list/saved_modes = list(1,2,3)
- var/list/saved_dynamic_rules = list(list(),list(),list())
- var/list/saved_storytellers = list("foo","bar","baz")
- var/list/average_dynamic_threat = 50
- var/list/saved_maps
- var/list/saved_trophies = list()
var/list/spawned_objects = list()
var/list/antag_rep = list()
var/list/antag_rep_change = list()
@@ -28,62 +24,81 @@ SUBSYSTEM_DEF(persistence)
var/list/paintings = list()
/datum/controller/subsystem/persistence/Initialize()
- LoadSatchels()
- LoadPoly()
- LoadChiselMessages()
- LoadTrophies()
- LoadRecentModes()
- LoadRecentStorytellers()
- LoadRecentRulesets()
- LoadRecentMaps()
- LoadPhotoPersistence()
+ LoadServerPersistence()
+ LoadGamePersistence()
+ var/map_persistence_path = get_map_persistence_path()
+ if(map_persistence_path)
+ LoadMapPersistence()
+ return ..()
+
+/**
+ * Gets the persistence path of the current map.
+ */
+/datum/controller/subsystem/persistence/proc/get_map_persistence_path()
+ ASSERT(SSmapping.config)
+ if(!SSmapping.config.persistence_key || (SSmapping.config.persistence_key == "NO_PERSIST"))
+ return null
+ return "data/persistence/[ckey(SSmapping.config.persistence_key)]"
+
+/datum/controller/subsystem/persistence/proc/CollectData()
+ SaveServerPersistence()
+ if(station_persistence_save_disabled)
+ return
+ SaveGamePersistence()
+ var/map_persistence_path = get_map_persistence_path()
+ if(map_persistence_path)
+ SaveMapPersistence()
+
+/**
+ * Loads persistent data relevant to the server: Configurations, past gamemodes, votes, antag rep, etc
+ */
+/datum/controller/subsystem/persistence/proc/LoadServerPersistence()
for(var/client/C in GLOB.clients)
LoadSavedVote(C.ckey)
if(CONFIG_GET(flag/use_antag_rep))
LoadAntagReputation()
LoadRandomizedRecipes()
- LoadPanicBunker()
- return ..()
-/datum/controller/subsystem/persistence/proc/LoadSatchels()
- var/placed_satchel = 0
- var/path
+/**
+ * Saves persistent data relevant to the server: Configurations, past gamemodes, votes, antag rep, etc
+ */
+/datum/controller/subsystem/persistence/proc/SaveServerPersistence()
+ if(CONFIG_GET(flag/use_antag_rep))
+ CollectAntagReputation()
+ SaveRandomizedRecipes()
- var/json_file = file("data/npc_saves/SecretSatchels[SSmapping.config.map_name].json")
- var/list/json = list()
- if(fexists(json_file))
- json = json_decode(file2text(json_file))
+/**
+ * Loads persistent data relevant to the game in general: Photos, etc
+ *
+ * Legacy map persistence systems also use this.
+ */
+/datum/controller/subsystem/persistence/proc/LoadGamePersistence()
+ LoadChiselMessages()
+ LoadPhotoPersistence()
+ LoadPaintings()
- old_secret_satchels = json["data"]
- var/obj/item/storage/backpack/satchel/flat/F
- if(old_secret_satchels && old_secret_satchels.len >= 10) //guards against low drop pools assuring that one player cannot reliably find his own gear.
- var/pos = rand(1, old_secret_satchels.len)
- F = new()
- old_secret_satchels.Cut(pos, pos+1 % old_secret_satchels.len)
- F.x = old_secret_satchels[pos]["x"]
- F.y = old_secret_satchels[pos]["y"]
- F.z = SSmapping.station_start
- path = text2path(old_secret_satchels[pos]["saved_obj"])
+/**
+ * Saves persistent data relevant to the game in general: Photos, etc
+ *
+ * Legacy map persistence systems also use this.
+ */
+/datum/controller/subsystem/persistence/proc/SaveGamePersistence()
+ CollectChiselMessages()
+ SavePhotoPersistence() //THIS IS PERSISTENCE, NOT THE LOGGING PORTION.
+ SavePaintings()
+ SaveScars()
- if(F)
- if(isfloorturf(F.loc) && !isplatingturf(F.loc))
- F.hide(1)
- if(ispath(path))
- var/spawned_item = new path(F)
- spawned_objects[spawned_item] = TRUE
- placed_satchel++
- var/free_satchels = 0
- for(var/turf/T in shuffle(block(locate(TRANSITIONEDGE,TRANSITIONEDGE,SSmapping.station_start), locate(world.maxx-TRANSITIONEDGE,world.maxy-TRANSITIONEDGE,SSmapping.station_start)))) //Nontrivially expensive but it's roundstart only
- if(isfloorturf(T) && !isplatingturf(T))
- new /obj/item/storage/backpack/satchel/flat/secret(T)
- free_satchels++
- if((free_satchels + placed_satchel) == 10) //ten tiles, more than enough to kill anything that moves
- break
+/**
+ * Loads persistent data relevant to the current map: Objects, etc.
+ */
+/datum/controller/subsystem/persistence/proc/LoadMapPersistence()
+ return
-/datum/controller/subsystem/persistence/proc/LoadPoly()
- for(var/mob/living/simple_animal/parrot/Poly/P in GLOB.alive_mob_list)
- twitterize(P.speech_buffer, "polytalk")
- break //Who's been duping the bird?!
+/**
+ * Saves persistent data relevant to the current map: Objects, etc.
+ */
+/datum/controller/subsystem/persistence/proc/SaveMapPersistence()
+ return
/datum/controller/subsystem/persistence/proc/LoadChiselMessages()
var/list/saved_messages = list()
@@ -130,63 +145,6 @@ SUBSYSTEM_DEF(persistence)
log_world("Loaded [saved_messages.len] engraved messages on map [SSmapping.config.map_name]")
-/datum/controller/subsystem/persistence/proc/LoadTrophies()
- if(fexists("data/npc_saves/TrophyItems.sav")) //legacy compatability to convert old format to new
- var/savefile/S = new /savefile("data/npc_saves/TrophyItems.sav")
- var/saved_json
- S >> saved_json
- if(!saved_json)
- return
- saved_trophies = json_decode(saved_json)
- fdel("data/npc_saves/TrophyItems.sav")
- else
- var/json_file = file("data/npc_saves/TrophyItems.json")
- if(!fexists(json_file))
- return
- var/list/json = json_decode(file2text(json_file))
- if(!json)
- return
- saved_trophies = json["data"]
- SetUpTrophies(saved_trophies.Copy())
-
-/datum/controller/subsystem/persistence/proc/LoadRecentModes()
- var/json_file = file("data/RecentModes.json")
- if(!fexists(json_file))
- return
- var/list/json = json_decode(file2text(json_file))
- if(!json)
- return
- saved_modes = json["data"]
-
-/datum/controller/subsystem/persistence/proc/LoadRecentRulesets()
- var/json_file = file("data/RecentRulesets.json")
- if(!fexists(json_file))
- return
- var/list/json = json_decode(file2text(json_file))
- if(!json)
- return
- saved_dynamic_rules = json["data"]
-
-/datum/controller/subsystem/persistence/proc/LoadRecentStorytellers()
- var/json_file = file("data/RecentStorytellers.json")
- if(!fexists(json_file))
- return
- var/list/json = json_decode(file2text(json_file))
- if(!json)
- return
- saved_storytellers = json["data"]
- if(saved_storytellers.len > 3)
- average_dynamic_threat = saved_storytellers[4]
- saved_storytellers.len = 3
-
-/datum/controller/subsystem/persistence/proc/LoadRecentMaps()
- var/json_file = file("data/RecentMaps.json")
- if(!fexists(json_file))
- return
- var/list/json = json_decode(file2text(json_file))
- if(!json)
- return
- saved_maps = json["maps"]
/datum/controller/subsystem/persistence/proc/LoadAntagReputation()
var/json = file2text(FILE_ANTAG_REP)
@@ -207,59 +165,6 @@ SUBSYSTEM_DEF(persistence)
return
saved_votes[ckey] = json["data"]
-/datum/controller/subsystem/persistence/proc/SetUpTrophies(list/trophy_items)
- for(var/A in GLOB.trophy_cases)
- var/obj/structure/displaycase/trophy/T = A
- if (T.showpiece)
- continue
- T.added_roundstart = TRUE
-
- var/trophy_data = pick_n_take(trophy_items)
-
- if(!islist(trophy_data))
- continue
-
- var/list/chosen_trophy = trophy_data
-
- if(!chosen_trophy || isemptylist(chosen_trophy)) //Malformed
- continue
-
- var/path = text2path(chosen_trophy["path"]) //If the item no longer exist, this returns null
- if(!path)
- continue
-
- T.showpiece = new /obj/item/showpiece_dummy(T, path)
- T.trophy_message = chosen_trophy["message"]
- T.placer_key = chosen_trophy["placer_key"]
- T.update_icon()
-
-/datum/controller/subsystem/persistence/proc/CollectData()
- CollectChiselMessages()
- CollectSecretSatchels()
- CollectTrophies()
- CollectRoundtype()
- if(istype(SSticker.mode, /datum/game_mode/dynamic))
- var/datum/game_mode/dynamic/mode = SSticker.mode
- CollectStoryteller(mode)
- CollectRulesets(mode)
- RecordMaps()
- SavePhotoPersistence() //THIS IS PERSISTENCE, NOT THE LOGGING PORTION.
- if(CONFIG_GET(flag/use_antag_rep))
- CollectAntagReputation()
- SaveRandomizedRecipes()
- SavePanicBunker()
- SavePaintings()
- SaveScars()
-
-/datum/controller/subsystem/persistence/proc/LoadPanicBunker()
- var/bunker_path = file("data/bunker_passthrough.json")
- if(fexists(bunker_path))
- var/list/json = json_decode(file2text(bunker_path))
- GLOB.bunker_passthrough = json["data"]
- for(var/ckey in GLOB.bunker_passthrough)
- if(daysSince(GLOB.bunker_passthrough[ckey]) >= CONFIG_GET(number/max_bunker_days))
- GLOB.bunker_passthrough -= ckey
-
/datum/controller/subsystem/persistence/proc/GetPhotoAlbums()
var/album_path = file("data/photo_albums.json")
if(fexists(album_path))
@@ -329,35 +234,6 @@ SUBSYSTEM_DEF(persistence)
WRITE_FILE(frame_path, frame_json)
-/datum/controller/subsystem/persistence/proc/CollectSecretSatchels()
- satchel_blacklist = typecacheof(list(/obj/item/stack/tile/plasteel, /obj/item/crowbar))
- var/list/satchels_to_add = list()
- for(var/A in new_secret_satchels)
- var/obj/item/storage/backpack/satchel/flat/F = A
- if(QDELETED(F) || F.z != SSmapping.station_start || F.invisibility != INVISIBILITY_MAXIMUM)
- continue
- var/list/savable_obj = list()
- for(var/obj/O in F)
- if(is_type_in_typecache(O, satchel_blacklist) || (O.flags_1 & ADMIN_SPAWNED_1))
- continue
- if(O.persistence_replacement)
- savable_obj += O.persistence_replacement
- else
- savable_obj += O.type
- if(isemptylist(savable_obj))
- continue
- var/list/data = list()
- data["x"] = F.x
- data["y"] = F.y
- data["saved_obj"] = pick(savable_obj)
- satchels_to_add += list(data)
-
- var/json_file = file("data/npc_saves/SecretSatchels[SSmapping.config.map_name].json")
- var/list/file_data = list()
- fdel(json_file)
- file_data["data"] = old_secret_satchels + satchels_to_add
- WRITE_FILE(json_file, json_encode(file_data))
-
/datum/controller/subsystem/persistence/proc/CollectChiselMessages()
var/json_file = file("data/npc_saves/ChiselMessages[SSmapping.config.map_name].json")
@@ -373,84 +249,6 @@ SUBSYSTEM_DEF(persistence)
/datum/controller/subsystem/persistence/proc/SaveChiselMessage(obj/structure/chisel_message/M)
saved_messages += list(M.pack()) // dm eats one list
-
-/datum/controller/subsystem/persistence/proc/CollectTrophies()
- var/json_file = file("data/npc_saves/TrophyItems.json")
- var/list/file_data = list()
- file_data["data"] = remove_duplicate_trophies(saved_trophies)
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(file_data))
-
-/datum/controller/subsystem/persistence/proc/SavePanicBunker()
- var/json_file = file("data/bunker_passthrough.json")
- var/list/file_data = list()
- file_data["data"] = GLOB.bunker_passthrough
- fdel(json_file)
- WRITE_FILE(json_file,json_encode(file_data))
-
-/datum/controller/subsystem/persistence/proc/remove_duplicate_trophies(list/trophies)
- var/list/ukeys = list()
- . = list()
- for(var/trophy in trophies)
- var/tkey = "[trophy["path"]]-[trophy["message"]]"
- if(ukeys[tkey])
- continue
- else
- . += list(trophy)
- ukeys[tkey] = TRUE
-
-/datum/controller/subsystem/persistence/proc/SaveTrophy(obj/structure/displaycase/trophy/T)
- if(!T.added_roundstart && T.showpiece)
- var/list/data = list()
- data["path"] = T.showpiece.type
- data["message"] = T.trophy_message
- data["placer_key"] = T.placer_key
- saved_trophies += list(data)
-
-/datum/controller/subsystem/persistence/proc/CollectRoundtype()
- saved_modes[3] = saved_modes[2]
- saved_modes[2] = saved_modes[1]
- saved_modes[1] = SSticker.mode.config_tag
- var/json_file = file("data/RecentModes.json")
- var/list/file_data = list()
- file_data["data"] = saved_modes
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(file_data))
-
-/datum/controller/subsystem/persistence/proc/CollectStoryteller(var/datum/game_mode/dynamic/mode)
- saved_storytellers.len = 3
- saved_storytellers[3] = saved_storytellers[2]
- saved_storytellers[2] = saved_storytellers[1]
- saved_storytellers[1] = mode.storyteller.name
- average_dynamic_threat = (mode.max_threat + average_dynamic_threat) / 2
- var/json_file = file("data/RecentStorytellers.json")
- var/list/file_data = list()
- file_data["data"] = saved_storytellers + average_dynamic_threat
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(file_data))
-
-/datum/controller/subsystem/persistence/proc/CollectRulesets(var/datum/game_mode/dynamic/mode)
- saved_dynamic_rules[3] = saved_dynamic_rules[2]
- saved_dynamic_rules[2] = saved_dynamic_rules[1]
- saved_dynamic_rules[1] = list()
- for(var/r in mode.executed_rules)
- var/datum/dynamic_ruleset/rule = r
- saved_dynamic_rules[1] += rule.config_tag
- var/json_file = file("data/RecentRulesets.json")
- var/list/file_data = list()
- file_data["data"] = saved_dynamic_rules
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(file_data))
-
-/datum/controller/subsystem/persistence/proc/RecordMaps()
- saved_maps = saved_maps?.len ? list("[SSmapping.config.map_name]") | saved_maps : list("[SSmapping.config.map_name]")
- var/json_file = file("data/RecentMaps.json")
- var/list/file_data = list()
- file_data["maps"] = saved_maps
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(file_data))
-
-
/datum/controller/subsystem/persistence/proc/CollectAntagReputation()
var/ANTAG_REP_MAXIMUM = CONFIG_GET(number/antag_rep_maximum)
diff --git a/code/controllers/subsystem/persistence/cleanable_debris.dm b/code/controllers/subsystem/persistence/cleanable_debris.dm
new file mode 100644
index 0000000000..a0817c460b
--- /dev/null
+++ b/code/controllers/subsystem/persistence/cleanable_debris.dm
@@ -0,0 +1,177 @@
+/**
+ * Persistence for cleanable debris.
+ */
+/datum/controller/subsystem/persistence
+ /// tracks if we already loaded debris. Unlike everything else, this can actually be a major problem if some badmin procs it twice.
+ var/loaded_debris = FALSE
+
+/datum/controller/subsystem/persistence/LoadMapPersistence()
+ . = ..()
+ if(CONFIG_GET(flag/persistent_debris))
+ LoadMapDebris()
+
+/datum/controller/subsystem/persistence/SaveMapPersistence()
+ . = ..()
+ if(CONFIG_GET(flag/persistent_debris))
+ SaveMapDebris()
+
+/datum/controller/subsystem/persistence/proc/LoadMapDebris()
+ if(CONFIG_GET(flag/persistent_debris_only))
+ wipe_existing_debris()
+ if(!fexists("[get_map_persistence_path()]/debris.json"))
+ return
+ if(loaded_debris)
+ return
+ loaded_debris = TRUE
+ var/list/allowed_turf_typecache = typecacheof(/turf/open) - typecacheof(/turf/open/space)
+ var/list/allowed_z_cache = list()
+ for(var/z in SSmapping.levels_by_trait(ZTRAIT_STATION))
+ allowed_z_cache[num2text(z)] = TRUE
+ var/list/data = json_decode(file2text("[get_map_persistence_path()]/debris.json"))
+ var/list/z_lookup = list()
+ var/loaded = 0
+ var/list/loaded_by_type = list()
+ var/nopath = 0
+ var/badloc = 0
+ var/noturf = 0
+ /// reverse it
+ for(var/z in SSmapping.z_to_station_z_index)
+ var/sz = SSmapping.z_to_station_z_index[z]
+ z_lookup[num2text(sz)] = text2num(z)
+ for(var/z in data)
+ var/actual_z = z_lookup[z]
+ var/list/L1 = data[z]
+ for(var/x in L1)
+ var/list/L2 = data[z][x]
+ for(var/y in L2)
+ var/turf/tile = locate(text2num(x), text2num(y), actual_z)
+ if(!tile)
+ noturf++
+ continue
+ var/list/objects = data[z][x][y]
+ for(var/_L in objects)
+ var/list/objdata
+ var/path
+ if(islist(_L))
+ objdata = _L
+ path = text2path(objdata["__PATH__"])
+ else
+ path = text2path(_L)
+ objdata = objects[_L]
+ if(!path)
+ nopath++
+ continue
+ if(!IsValidDebrisLocation(tile, allowed_turf_typecache, allowed_z_cache, path, TRUE))
+ badloc++
+ continue
+ var/obj/effect/decal/cleanable/instantiated = new path(tile)
+ loaded_by_type[path] += 1
+ loaded++
+ if(objdata)
+ instantiated.PersistenceLoad(objdata)
+ var/list/bytype = list()
+ for(var/path in loaded_by_type)
+ bytype += "[path] - [loaded_by_type[path]]"
+ subsystem_log(
+ {"Debris loading completed:
+ Errors:
+ No path: [nopath]
+ Invalid location: [badloc]
+ No turf on map: [noturf]
+ Total loaded: [loaded]
+ By type:
+ [bytype.Join("\n")]"}
+ )
+
+/datum/controller/subsystem/persistence/proc/SaveMapDebris()
+ if(fexists("[get_map_persistence_path()]/debris.json"))
+ fdel("[get_map_persistence_path()]/debris.json")
+ if(CONFIG_GET(flag/persistent_debris_wipe_on_nuke) && station_was_destroyed)
+ return // local janitor cheers on nukeop team to save some work
+ var/list/data = list()
+ var/list/z_lookup = SSmapping.z_to_station_z_index
+ var/list/debris = RelevantPersistentDebris()
+ var/obj/effect/decal/cleanable/saving
+ var/global_max = CONFIG_GET(number/persistent_debris_global_max)
+ var/type_max = CONFIG_GET(number/persistent_debris_type_max)
+ var/stored = 0
+ var/list/stored_by_type = list()
+ for(var/i in debris)
+ saving = i
+ var/list/serializing = list()
+ var/path = saving.PersistenceSave(serializing)
+ if(!path)
+ continue
+ if(stored_by_type[path] > type_max)
+ continue
+ var/text_z = num2text(z_lookup[num2text(saving.z)])
+ var/text_y = num2text(saving.y)
+ var/text_x = num2text(saving.x)
+ LAZYINITLIST(data[text_z])
+ LAZYINITLIST(data[text_z][text_x])
+ LAZYINITLIST(data[text_z][text_x][text_y])
+ if(saving.persistence_allow_stacking)
+ serializing["__PATH__"] = path
+ data[text_z][text_x][text_y] += list(serializing)
+ else
+ data[text_z][text_x][text_y][path] = serializing
+ stored++
+ if(stored > global_max)
+ var/w = "Persistent debris saving globally aborted due to global max >= [global_max]. Either janitors never do their jobs or something is wrong."
+ message_admins(w)
+ subsystem_log(w)
+ return
+ stored_by_type[path] = stored_by_type[path]? stored_by_type[path] + 1 : 1
+ if(stored_by_type[path] > type_max)
+ var/w = "Persistent debris saving aborted for type [path] due to type max >= [global_max]. Either janitors never do their jobs or something is wrong."
+ message_admins(w)
+ subsystem_log(w)
+
+ var/list/bytype = list()
+ for(var/path in stored_by_type)
+ bytype += "[path] - [stored_by_type[path]]"
+ subsystem_log(
+ {"Debris saving completed:
+ Total: [stored]
+ By type:
+ [bytype.Join("\n")]"}
+ )
+ WRITE_FILE(file("[get_map_persistence_path()]/debris.json"), json_encode(data))
+
+/datum/controller/subsystem/persistence/proc/IsValidDebrisLocation(turf/tile, list/allowed_typecache, list/allowed_zcache, obj/effect/decal/cleanable/type, loading = FALSE)
+ if(!allowed_typecache[tile.type])
+ return FALSE
+ var/area/A = tile.loc
+ if(!A.persistent_debris_allowed)
+ return FALSE
+ if(!allowed_zcache[num2text(tile.z)])
+ return FALSE
+ if(loading)
+ if(!initial(type.persistence_allow_stacking))
+ var/obj/effect/decal/cleanable/C = locate(type) in tile
+ if(!QDELETED(C))
+ return FALSE
+ // Saving verifies allow stacking in the save proc.
+ for(var/obj/structure/window/W in tile)
+ if(W.fulltile)
+ return FALSE
+ return TRUE
+
+/datum/controller/subsystem/persistence/proc/wipe_existing_debris()
+ var/list/existing = RelevantPersistentDebris()
+ QDEL_LIST(existing)
+
+/datum/controller/subsystem/persistence/proc/RelevantPersistentDebris()
+ var/list/allowed_turf_typecache = typecacheof(/turf/open) - typecacheof(/turf/open/space)
+ var/list/allowed_z_cache = list()
+ for(var/z in SSmapping.levels_by_trait(ZTRAIT_STATION))
+ allowed_z_cache[num2text(z)] = TRUE
+ . = list()
+ for(var/obj/effect/decal/cleanable/C in world)
+ if(!C.loc || QDELETED(C))
+ continue
+ if(!C.persistent)
+ continue
+ if(!IsValidDebrisLocation(C.loc, allowed_turf_typecache, allowed_z_cache, C.type, FALSE))
+ continue
+ . += C
diff --git a/code/controllers/subsystem/persistence/panic_bunker.dm b/code/controllers/subsystem/persistence/panic_bunker.dm
new file mode 100644
index 0000000000..93ada2835c
--- /dev/null
+++ b/code/controllers/subsystem/persistence/panic_bunker.dm
@@ -0,0 +1,26 @@
+/**
+ * Persists panic bunker whitelisting for a configured period of time
+ */
+/datum/controller/subsystem/persistence/LoadServerPersistence()
+ . = ..()
+ LoadPanicBunker()
+
+/datum/controller/subsystem/persistence/SaveServerPersistence()
+ . = ..()
+ SavePanicBunker()
+
+/datum/controller/subsystem/persistence/proc/LoadPanicBunker()
+ var/bunker_path = file("data/bunker_passthrough.json")
+ if(fexists(bunker_path))
+ var/list/json = json_decode(file2text(bunker_path))
+ GLOB.bunker_passthrough = json["data"]
+ for(var/ckey in GLOB.bunker_passthrough)
+ if(daysSince(GLOB.bunker_passthrough[ckey]) >= CONFIG_GET(number/max_bunker_days))
+ GLOB.bunker_passthrough -= ckey
+
+/datum/controller/subsystem/persistence/proc/SavePanicBunker()
+ var/json_file = file("data/bunker_passthrough.json")
+ var/list/file_data = list()
+ file_data["data"] = GLOB.bunker_passthrough
+ fdel(json_file)
+ WRITE_FILE(json_file,json_encode(file_data))
diff --git a/code/controllers/subsystem/persistence/poly_parrot.dm b/code/controllers/subsystem/persistence/poly_parrot.dm
new file mode 100644
index 0000000000..64743e9623
--- /dev/null
+++ b/code/controllers/subsystem/persistence/poly_parrot.dm
@@ -0,0 +1,11 @@
+/**
+ * Persists poly messages across rounds
+ */
+/datum/controller/subsystem/persistence/LoadGamePersistence()
+ . = ..()
+ LoadPoly()
+
+/datum/controller/subsystem/persistence/proc/LoadPoly()
+ for(var/mob/living/simple_animal/parrot/Poly/P in GLOB.alive_mob_list)
+ twitterize(P.speech_buffer, "polytalk")
+ break //Who's been duping the bird?!
diff --git a/code/controllers/subsystem/persistence/recent_votes_etc.dm b/code/controllers/subsystem/persistence/recent_votes_etc.dm
new file mode 100644
index 0000000000..f1b902d6ab
--- /dev/null
+++ b/code/controllers/subsystem/persistence/recent_votes_etc.dm
@@ -0,0 +1,107 @@
+/**
+ * Stores recently played gamemodes, maps, etc.
+ */
+/datum/controller/subsystem/persistence
+ var/list/saved_modes = list(1,2,3)
+ var/list/saved_dynamic_rules = list(list(),list(),list())
+ var/list/saved_storytellers = list("foo","bar","baz")
+ var/list/average_dynamic_threat = 50
+ var/list/saved_maps
+
+/datum/controller/subsystem/persistence/SaveServerPersistence()
+ . = ..()
+ CollectRoundtype()
+ if(istype(SSticker.mode, /datum/game_mode/dynamic))
+ var/datum/game_mode/dynamic/mode = SSticker.mode
+ CollectStoryteller(mode)
+ CollectRulesets(mode)
+ RecordMaps()
+
+/datum/controller/subsystem/persistence/LoadServerPersistence()
+ . = ..()
+ LoadRecentModes()
+ LoadRecentStorytellers()
+ LoadRecentRulesets()
+ LoadRecentMaps()
+
+/datum/controller/subsystem/persistence/proc/CollectRoundtype()
+ saved_modes[3] = saved_modes[2]
+ saved_modes[2] = saved_modes[1]
+ saved_modes[1] = SSticker.mode.config_tag
+ var/json_file = file("data/RecentModes.json")
+ var/list/file_data = list()
+ file_data["data"] = saved_modes
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
+
+/datum/controller/subsystem/persistence/proc/CollectStoryteller(var/datum/game_mode/dynamic/mode)
+ saved_storytellers.len = 3
+ saved_storytellers[3] = saved_storytellers[2]
+ saved_storytellers[2] = saved_storytellers[1]
+ saved_storytellers[1] = mode.storyteller.name
+ average_dynamic_threat = (mode.max_threat + average_dynamic_threat) / 2
+ var/json_file = file("data/RecentStorytellers.json")
+ var/list/file_data = list()
+ file_data["data"] = saved_storytellers + average_dynamic_threat
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
+
+/datum/controller/subsystem/persistence/proc/CollectRulesets(var/datum/game_mode/dynamic/mode)
+ saved_dynamic_rules[3] = saved_dynamic_rules[2]
+ saved_dynamic_rules[2] = saved_dynamic_rules[1]
+ saved_dynamic_rules[1] = list()
+ for(var/r in mode.executed_rules)
+ var/datum/dynamic_ruleset/rule = r
+ saved_dynamic_rules[1] += rule.config_tag
+ var/json_file = file("data/RecentRulesets.json")
+ var/list/file_data = list()
+ file_data["data"] = saved_dynamic_rules
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
+
+/datum/controller/subsystem/persistence/proc/RecordMaps()
+ saved_maps = saved_maps?.len ? list("[SSmapping.config.map_name]") | saved_maps : list("[SSmapping.config.map_name]")
+ var/json_file = file("data/RecentMaps.json")
+ var/list/file_data = list()
+ file_data["maps"] = saved_maps
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
+
+/datum/controller/subsystem/persistence/proc/LoadRecentModes()
+ var/json_file = file("data/RecentModes.json")
+ if(!fexists(json_file))
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+ saved_modes = json["data"]
+
+/datum/controller/subsystem/persistence/proc/LoadRecentRulesets()
+ var/json_file = file("data/RecentRulesets.json")
+ if(!fexists(json_file))
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+ saved_dynamic_rules = json["data"]
+
+/datum/controller/subsystem/persistence/proc/LoadRecentStorytellers()
+ var/json_file = file("data/RecentStorytellers.json")
+ if(!fexists(json_file))
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+ saved_storytellers = json["data"]
+ if(saved_storytellers.len > 3)
+ average_dynamic_threat = saved_storytellers[4]
+ saved_storytellers.len = 3
+
+/datum/controller/subsystem/persistence/proc/LoadRecentMaps()
+ var/json_file = file("data/RecentMaps.json")
+ if(!fexists(json_file))
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+ saved_maps = json["maps"]
diff --git a/code/controllers/subsystem/persistence/secret_satchels.dm b/code/controllers/subsystem/persistence/secret_satchels.dm
new file mode 100644
index 0000000000..fa2a445317
--- /dev/null
+++ b/code/controllers/subsystem/persistence/secret_satchels.dm
@@ -0,0 +1,79 @@
+/**
+ * Secret satchel persistence - allows storing of items in underfloor satchels that's loaded later.
+ */
+/datum/controller/subsystem/persistence
+ var/list/satchel_blacklist = list() //this is a typecache
+ var/list/new_secret_satchels = list() //these are objects
+ var/list/old_secret_satchels = list()
+
+/datum/controller/subsystem/persistence/LoadGamePersistence()
+ . = ..()
+ LoadSatchels()
+
+/datum/controller/subsystem/persistence/SaveGamePersistence()
+ . = ..()
+ CollectSecretSatchels()
+
+/datum/controller/subsystem/persistence/proc/LoadSatchels()
+ var/placed_satchel = 0
+ var/path
+
+ var/json_file = file("data/npc_saves/SecretSatchels[SSmapping.config.map_name].json")
+ var/list/json = list()
+ if(fexists(json_file))
+ json = json_decode(file2text(json_file))
+
+ old_secret_satchels = json["data"]
+ var/obj/item/storage/backpack/satchel/flat/F
+ if(old_secret_satchels && old_secret_satchels.len >= 10) //guards against low drop pools assuring that one player cannot reliably find his own gear.
+ var/pos = rand(1, old_secret_satchels.len)
+ F = new()
+ old_secret_satchels.Cut(pos, pos+1 % old_secret_satchels.len)
+ F.x = old_secret_satchels[pos]["x"]
+ F.y = old_secret_satchels[pos]["y"]
+ F.z = SSmapping.station_start
+ path = text2path(old_secret_satchels[pos]["saved_obj"])
+
+ if(F)
+ if(isfloorturf(F.loc) && !isplatingturf(F.loc))
+ F.hide(1)
+ if(ispath(path))
+ var/spawned_item = new path(F)
+ spawned_objects[spawned_item] = TRUE
+ placed_satchel++
+ var/free_satchels = 0
+ for(var/turf/T in shuffle(block(locate(TRANSITIONEDGE,TRANSITIONEDGE,SSmapping.station_start), locate(world.maxx-TRANSITIONEDGE,world.maxy-TRANSITIONEDGE,SSmapping.station_start)))) //Nontrivially expensive but it's roundstart only
+ if(isfloorturf(T) && !isplatingturf(T))
+ new /obj/item/storage/backpack/satchel/flat/secret(T)
+ free_satchels++
+ if((free_satchels + placed_satchel) == 10) //ten tiles, more than enough to kill anything that moves
+ break
+
+/datum/controller/subsystem/persistence/proc/CollectSecretSatchels()
+ satchel_blacklist = typecacheof(list(/obj/item/stack/tile/plasteel, /obj/item/crowbar))
+ var/list/satchels_to_add = list()
+ for(var/A in new_secret_satchels)
+ var/obj/item/storage/backpack/satchel/flat/F = A
+ if(QDELETED(F) || F.z != SSmapping.station_start || F.invisibility != INVISIBILITY_MAXIMUM)
+ continue
+ var/list/savable_obj = list()
+ for(var/obj/O in F)
+ if(is_type_in_typecache(O, satchel_blacklist) || (O.flags_1 & ADMIN_SPAWNED_1))
+ continue
+ if(O.persistence_replacement)
+ savable_obj += O.persistence_replacement
+ else
+ savable_obj += O.type
+ if(isemptylist(savable_obj))
+ continue
+ var/list/data = list()
+ data["x"] = F.x
+ data["y"] = F.y
+ data["saved_obj"] = pick(savable_obj)
+ satchels_to_add += list(data)
+
+ var/json_file = file("data/npc_saves/SecretSatchels[SSmapping.config.map_name].json")
+ var/list/file_data = list()
+ fdel(json_file)
+ file_data["data"] = old_secret_satchels + satchels_to_add
+ WRITE_FILE(json_file, json_encode(file_data))
diff --git a/code/controllers/subsystem/persistence/trophies.dm b/code/controllers/subsystem/persistence/trophies.dm
new file mode 100644
index 0000000000..770ca99125
--- /dev/null
+++ b/code/controllers/subsystem/persistence/trophies.dm
@@ -0,0 +1,84 @@
+/**
+ * Stores trophies in curator display cases
+ */
+/datum/controller/subsystem/persistence
+ var/list/saved_trophies = list()
+
+/datum/controller/subsystem/persistence/LoadGamePersistence()
+ . = ..()
+ LoadTrophies()
+
+/datum/controller/subsystem/persistence/SaveGamePersistence()
+ . = ..()
+ CollectTrophies()
+
+/datum/controller/subsystem/persistence/proc/LoadTrophies()
+ if(fexists("data/npc_saves/TrophyItems.sav")) //legacy compatability to convert old format to new
+ var/savefile/S = new /savefile("data/npc_saves/TrophyItems.sav")
+ var/saved_json
+ S >> saved_json
+ if(!saved_json)
+ return
+ saved_trophies = json_decode(saved_json)
+ fdel("data/npc_saves/TrophyItems.sav")
+ else
+ var/json_file = file("data/npc_saves/TrophyItems.json")
+ if(!fexists(json_file))
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+ saved_trophies = json["data"]
+ SetUpTrophies(saved_trophies.Copy())
+
+/datum/controller/subsystem/persistence/proc/SetUpTrophies(list/trophy_items)
+ for(var/A in GLOB.trophy_cases)
+ var/obj/structure/displaycase/trophy/T = A
+ if (T.showpiece)
+ continue
+ T.added_roundstart = TRUE
+
+ var/trophy_data = pick_n_take(trophy_items)
+
+ if(!islist(trophy_data))
+ continue
+
+ var/list/chosen_trophy = trophy_data
+
+ if(!chosen_trophy || isemptylist(chosen_trophy)) //Malformed
+ continue
+
+ var/path = text2path(chosen_trophy["path"]) //If the item no longer exist, this returns null
+ if(!path)
+ continue
+
+ T.showpiece = new /obj/item/showpiece_dummy(T, path)
+ T.trophy_message = chosen_trophy["message"]
+ T.placer_key = chosen_trophy["placer_key"]
+ T.update_icon()
+
+/datum/controller/subsystem/persistence/proc/CollectTrophies()
+ var/json_file = file("data/npc_saves/TrophyItems.json")
+ var/list/file_data = list()
+ file_data["data"] = remove_duplicate_trophies(saved_trophies)
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
+
+/datum/controller/subsystem/persistence/proc/remove_duplicate_trophies(list/trophies)
+ var/list/ukeys = list()
+ . = list()
+ for(var/trophy in trophies)
+ var/tkey = "[trophy["path"]]-[trophy["message"]]"
+ if(ukeys[tkey])
+ continue
+ else
+ . += list(trophy)
+ ukeys[tkey] = TRUE
+
+/datum/controller/subsystem/persistence/proc/SaveTrophy(obj/structure/displaycase/trophy/T)
+ if(!T.added_roundstart && T.showpiece)
+ var/list/data = list()
+ data["path"] = T.showpiece.type
+ data["message"] = T.trophy_message
+ data["placer_key"] = T.placer_key
+ saved_trophies += list(data)
diff --git a/code/game/area/Space_Station_13_areas.dm b/code/game/area/Space_Station_13_areas.dm
index edd2047bd6..2c2cc46344 100644
--- a/code/game/area/Space_Station_13_areas.dm
+++ b/code/game/area/Space_Station_13_areas.dm
@@ -41,6 +41,7 @@ NOTE: there are two lists of areas in the end of this file: centcom and station
power_environ = FALSE
valid_territory = FALSE
outdoors = TRUE
+ persistent_debris_allowed = FALSE
ambientsounds = SPACE
blob_allowed = FALSE //Eating up space doesn't count for victory as a blob.
considered_hull_exterior = TRUE
diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm
index b9415f3cf8..ff177898cb 100644
--- a/code/game/area/areas.dm
+++ b/code/game/area/areas.dm
@@ -24,6 +24,10 @@
var/clockwork_warp_allowed = TRUE
/// Message to display when the clockwork warp fails
var/clockwork_warp_fail = "The structure there is too dense for warping to pierce. (This is normal in high-security areas.)"
+ /// Persistent debris alowed
+ var/persistent_debris_allowed = TRUE
+ /// Dirty flooring allowed
+ var/dirt_buildup_allowed = TRUE
/// If mining tunnel generation is allowed in this area
var/tunnel_allowed = FALSE
diff --git a/code/game/objects/effects/decals/cleanable.dm b/code/game/objects/effects/decals/cleanable.dm
index 34996177af..5a6d5e54e9 100644
--- a/code/game/objects/effects/decals/cleanable.dm
+++ b/code/game/objects/effects/decals/cleanable.dm
@@ -1,6 +1,13 @@
/obj/effect/decal/cleanable
gender = PLURAL
layer = ABOVE_NORMAL_TURF_LAYER
+ /// Is this kind of cleanable decal persistent
+ var/persistent = FALSE
+ /// Can we stack multiple in one tile?
+ var/persistence_allow_stacking = FALSE
+ /// Are we deleted by turf changes?
+ var/wiped_by_floor_change = FALSE
+
var/list/random_icon_states = null
var/blood_state = "" //I'm sorry but cleanable/blood code is ass, and so is blood_DNA
var/bloodiness = 0 //0-100, amount of blood in this decal, used for making footprints and affecting the alpha of bloody footprints
@@ -29,9 +36,21 @@
addtimer(CALLBACK(src, /datum.proc/_AddElement, list(/datum/element/beauty, beauty)), 0)
+/**
+ * A data list is passed into this.
+ * This should return null to skip saving, or the type of data to save. Type must be /cleanable.
+ */
+/obj/effect/decal/cleanable/proc/PersistenceSave(list/data)
+ return type
+
+/**
+ * Loads from a data list.
+ */
+/obj/effect/decal/cleanable/proc/PersistenceLoad(list/data)
+ return
+
/obj/effect/decal/cleanable/proc/replace_decal(obj/effect/decal/cleanable/C) // Returns true if we should give up in favor of the pre-existing decal
- if(mergeable_decal)
- qdel(C)
+ return mergeable_decal
/obj/effect/decal/cleanable/attackby(obj/item/W, mob/user, params)
if(istype(W, /obj/item/reagent_containers/glass) || istype(W, /obj/item/reagent_containers/food/drinks))
diff --git a/code/game/objects/effects/decals/cleanable/gibs.dm b/code/game/objects/effects/decals/cleanable/gibs.dm
index 7df0153ddc..4b63cbecc8 100644
--- a/code/game/objects/effects/decals/cleanable/gibs.dm
+++ b/code/game/objects/effects/decals/cleanable/gibs.dm
@@ -6,6 +6,7 @@
random_icon_states = list("gib1", "gib2", "gib3", "gib4", "gib5", "gib6")
mergeable_decal = FALSE
bloodiness = 0 //This isn't supposed to be bloody.
+ persistent = TRUE
var/body_colors = "#e3ba84" //a default color just in case.
var/gibs_reagent_id = /datum/reagent/liquidgibs
var/gibs_bloodtype = "A+"
@@ -31,6 +32,10 @@
flesh.color = body_colors
add_overlay(flesh)
+/obj/effect/decal/cleanable/blood/gibs/PersistenceSave(list/data)
+ . = ..()
+ return /obj/effect/decal/cleanable/blood/gibs/old
+
/obj/effect/decal/cleanable/blood/gibs/ex_act(severity, target)
return
@@ -159,6 +164,10 @@
. = ..()
update_icon()
+/obj/effect/decal/cleanable/blood/gibs/slime/PersistenceSave(list/data)
+ . = ..()
+ return type
+
/obj/effect/decal/cleanable/blood/gibs/slime/update_icon()
add_atom_colour(body_colors, FIXED_COLOUR_PRIORITY)
cut_overlays()
@@ -198,6 +207,10 @@
. = ..()
update_icon()
+/obj/effect/decal/cleanable/blood/gibs/synth/PersistenceSave(list/data)
+ . = ..()
+ return type
+
//IPCs
/obj/effect/decal/cleanable/blood/gibs/ipc
desc = "They look sharp yet oozing."
@@ -209,6 +222,10 @@
. = ..()
update_icon()
+/obj/effect/decal/cleanable/blood/gibs/ipc/PersistenceSave(list/data)
+ . = ..()
+ return type
+
/obj/effect/decal/cleanable/blood/gibs/ipc/update_icon()
add_atom_colour(blood_DNA_to_color(), FIXED_COLOUR_PRIORITY)
cut_overlays()
diff --git a/code/game/objects/effects/decals/cleanable/humans.dm b/code/game/objects/effects/decals/cleanable/humans.dm
index fa16a95faf..6d051b6c8c 100644
--- a/code/game/objects/effects/decals/cleanable/humans.dm
+++ b/code/game/objects/effects/decals/cleanable/humans.dm
@@ -8,12 +8,18 @@
bloodiness = BLOOD_AMOUNT_PER_DECAL
color = BLOOD_COLOR_HUMAN //default so we don't have white splotches everywhere.
beauty = -100
+ persistent = TRUE
+ var/fixed_color = FALSE
/obj/effect/decal/cleanable/blood/replace_decal(obj/effect/decal/cleanable/blood/C)
+ if(C.fixed_color)
+ qdel(C)
+ return FALSE
if (C.blood_DNA)
blood_DNA |= C.blood_DNA
+ qdel(C)
update_icon()
- ..()
+ return FALSE
/obj/effect/decal/cleanable/blood/transfer_blood_dna()
..()
@@ -24,7 +30,22 @@
update_icon()
/obj/effect/decal/cleanable/blood/update_icon()
- color = blood_DNA_to_color()
+ . = ..()
+ if(!fixed_color)
+ add_atom_colour(blood_DNA_to_color(), FIXED_COLOUR_PRIORITY)
+
+/obj/effect/decal/cleanable/blood/PersistenceSave(list/data)
+ . = ..()
+ data["color"] = color
+
+/obj/effect/decal/cleanable/blood/PersistenceLoad(list/data)
+ . = ..()
+ if(data["color"])
+ fixed_color = TRUE
+ add_atom_colour(data["color"], FIXED_COLOUR_PRIORITY)
+ name = "dried blood"
+ desc = "Looks like it's been here a while. Eew"
+ bloodiness = 0
/obj/effect/decal/cleanable/blood/old
name = "dried blood"
@@ -38,6 +59,7 @@
/obj/effect/decal/cleanable/blood/splats
random_icon_states = list("gibbl1", "gibbl2", "gibbl3", "gibbl4", "gibbl5")
+ persistence_allow_stacking = TRUE
/obj/effect/decal/cleanable/blood/splatter
random_icon_states = list("splatter1", "splatter2", "splatter3", "splatter4", "splatter5")
@@ -48,16 +70,43 @@
random_icon_states = null
beauty = -50
+/obj/effect/decal/cleanable/blood/tracks/PersistenceSave(list/data)
+ . = ..()
+ data["dir"] = dir
+
+/obj/effect/decal/cleanable/blood/tracks/PersistenceLoad(list/data)
+ . = ..()
+ if(data["dir"])
+ setDir(text2num(data["dir"]))
+
/obj/effect/decal/cleanable/trail_holder //not a child of blood on purpose
name = "blood"
icon_state = "ltrails_1"
desc = "Your instincts say you shouldn't be following these."
random_icon_states = null
beauty = -50
+ persistent = TRUE
+ persistence_allow_stacking = TRUE
var/list/existing_dirs = list()
+ var/fixed_color = FALSE
+
+/obj/effect/decal/cleanable/trail_holder/PersistenceSave(list/data)
+ . = ..()
+ data["dir"] = dir
+ data["color"] = color
+
+/obj/effect/decal/cleanable/trail_holder/PersistenceLoad(list/data)
+ . = ..()
+ if(data["dir"])
+ setDir(text2num(data["dir"]))
+ if(data["color"])
+ fixed_color = TRUE
+ add_atom_colour(data["color"], FIXED_COLOUR_PRIORITY)
/obj/effect/decal/cleanable/trail_holder/update_icon()
- color = blood_DNA_to_color()
+ . = ..()
+ if(!fixed_color)
+ add_atom_colour(blood_DNA_to_color(), FIXED_COLOUR_PRIORITY)
/obj/effect/cleanable/trail_holder/Initialize()
. = ..()
@@ -84,8 +133,19 @@
var/entered_dirs = 0
var/exited_dirs = 0
blood_state = BLOOD_STATE_BLOOD //the icon state to load images from
+ persistent = TRUE
+ persistence_allow_stacking = TRUE
var/list/shoe_types = list()
+/obj/effect/decal/cleanable/blood/footprints/PersistenceSave(list/data)
+ . = ..()
+ data["dir"] = dir
+
+/obj/effect/decal/cleanable/blood/footprints/PersistenceLoad(list/data)
+ . = ..()
+ if(data["dir"])
+ setDir(text2num(data["dir"]))
+
/obj/effect/decal/cleanable/blood/footprints/Crossed(atom/movable/O)
if(ishuman(O))
var/mob/living/carbon/human/H = O
@@ -143,7 +203,7 @@
return
if(color != C.color)
return
- ..()
+ return ..()
/obj/effect/decal/cleanable/blood/footprints/can_bloodcrawl_in()
if((blood_state != BLOOD_STATE_OIL) && (blood_state != BLOOD_STATE_NOT_BLOODY))
diff --git a/code/game/objects/effects/decals/cleanable/misc.dm b/code/game/objects/effects/decals/cleanable/misc.dm
index 044c5c6e86..0f7d7c185d 100644
--- a/code/game/objects/effects/decals/cleanable/misc.dm
+++ b/code/game/objects/effects/decals/cleanable/misc.dm
@@ -12,6 +12,8 @@
icon_state = "ash"
mergeable_decal = FALSE
beauty = -50
+ persistent = TRUE
+ persistence_allow_stacking = TRUE
/obj/effect/decal/cleanable/ash/Initialize()
. = ..()
@@ -38,6 +40,8 @@
icon = 'icons/obj/shards.dmi'
icon_state = "tiny"
beauty = -100
+ mergeable_decal = TRUE
+ persistent = TRUE
/obj/effect/decal/cleanable/glass/Initialize()
. = ..()
@@ -53,10 +57,33 @@
name = "dirt"
desc = "Someone should clean that up."
icon_state = "dirt"
+ alpha = 127
canSmoothWith = list(/obj/effect/decal/cleanable/dirt, /turf/closed/wall, /obj/structure/falsewall)
smooth = SMOOTH_FALSE
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
beauty = -75
+ mergeable_decal = TRUE
+ persistent = TRUE
+ wiped_by_floor_change = TRUE
+
+/obj/effect/decal/cleanable/dirt/Initialize(mapload)
+ . = ..()
+ alpha = CONFIG_GET(number/dirt_alpha_starting)
+
+/obj/effect/decal/cleanable/dirt/proc/dirty(strength = 1)
+ if(alpha < 255)
+ alpha += strength
+ if(alpha > 255)
+ alpha = 255
+
+/obj/effect/decal/cleanable/dirt/PersistenceSave(list/data)
+ . = ..()
+ data["alpha"] = alpha
+
+/obj/effect/decal/cleanable/dirt/PersistenceLoad(list/data)
+ . = ..()
+ if(data["alpha"])
+ alpha = text2num(data["alpha"])
/obj/effect/decal/cleanable/dirt/Initialize()
. = ..()
@@ -94,6 +121,8 @@
light_color = LIGHT_COLOR_GREEN
icon_state = "greenglow"
beauty = -300
+ mergeable_decal = TRUE
+ persistent = TRUE
/obj/effect/decal/cleanable/greenglow/Initialize(mapload)
. = ..()
@@ -122,6 +151,8 @@
icon_state = "molten"
mergeable_decal = FALSE
beauty = -150
+ persistent = TRUE
+ persistence_allow_stacking = TRUE
/obj/effect/decal/cleanable/molten_object/large
name = "big gooey grey mass"
@@ -136,6 +167,7 @@
icon_state = "vomit_1"
random_icon_states = list("vomit_1", "vomit_2", "vomit_3", "vomit_4")
beauty = -150
+ persistent = TRUE
/obj/effect/decal/cleanable/vomit/on_attack_hand(mob/user, act_intent = user.a_intent, unarmed_attack_flags)
if(ishuman(user))
@@ -151,6 +183,10 @@
reagents.trans_to(H, reagents.total_volume)
qdel(src)
+/obj/effect/decal/cleanable/vomit/PersistenceSave(list/data)
+ . = ..()
+ return /obj/effect/decal/cleanable/vomit/old
+
/obj/effect/decal/cleanable/vomit/old
name = "crusty dried vomit"
desc = "You try not to look at the chunks, and fail."
@@ -166,12 +202,16 @@
icon = 'icons/effects/tomatodecal.dmi'
random_icon_states = list("tomato_floor1", "tomato_floor2", "tomato_floor3")
beauty = -100
+ mergeable_decal = TRUE
+ persistent = TRUE
/obj/effect/decal/cleanable/plant_smudge
name = "plant smudge"
gender = NEUTER
icon = 'icons/effects/tomatodecal.dmi'
random_icon_states = list("smashed_plant")
+ mergeable_decal = TRUE
+ persistent = TRUE
/obj/effect/decal/cleanable/egg_smudge
name = "smashed egg"
@@ -179,6 +219,8 @@
gender = NEUTER
icon = 'icons/effects/tomatodecal.dmi'
random_icon_states = list("smashed_egg1", "smashed_egg2", "smashed_egg3")
+ mergeable_decal = TRUE
+ persistent = TRUE
/obj/effect/decal/cleanable/pie_smudge //honk
name = "smashed pie"
@@ -186,6 +228,8 @@
gender = NEUTER
icon = 'icons/effects/tomatodecal.dmi'
random_icon_states = list("smashed_pie")
+ mergeable_decal = TRUE
+ persistent = TRUE
/obj/effect/decal/cleanable/chem_pile
name = "chemical pile"
@@ -193,6 +237,8 @@
gender = NEUTER
icon = 'icons/obj/objects.dmi'
icon_state = "ash"
+ mergeable_decal = TRUE
+ persistent = TRUE
/obj/effect/decal/cleanable/shreds
name = "shreds"
@@ -200,6 +246,8 @@
icon_state = "shreds"
gender = PLURAL
mergeable_decal = FALSE
+ mergeable_decal = TRUE
+ persistent = TRUE
/obj/effect/decal/cleanable/shreds/ex_act(severity, target)
if(severity == 1) //so shreds created during an explosion aren't deleted by the explosion.
@@ -222,6 +270,8 @@
desc = "The herpes of arts and crafts."
icon = 'icons/effects/atmospherics.dmi'
gender = NEUTER
+ mergeable_decal = TRUE
+ persistent = TRUE
/obj/effect/decal/cleanable/glitter/pink
name = "pink glitter"
@@ -247,3 +297,5 @@
icon = 'icons/effects/blood.dmi'
icon_state = "xfloor1"
random_icon_states = list("xfloor1", "xfloor2", "xfloor3", "xfloor4", "xfloor5", "xfloor6", "xfloor7")
+ mergeable_decal = TRUE
+ persistent = TRUE
diff --git a/code/game/objects/effects/decals/cleanable/robots.dm b/code/game/objects/effects/decals/cleanable/robots.dm
index 2765330977..8f8f27c3c6 100644
--- a/code/game/objects/effects/decals/cleanable/robots.dm
+++ b/code/game/objects/effects/decals/cleanable/robots.dm
@@ -11,6 +11,7 @@
bloodiness = BLOOD_AMOUNT_PER_DECAL
mergeable_decal = FALSE
beauty = -50
+ persistent = TRUE
/obj/effect/decal/cleanable/robot_debris/Initialize(mapload, list/datum/disease/diseases)
. = ..()
@@ -52,6 +53,7 @@
blood_state = BLOOD_STATE_OIL
bloodiness = BLOOD_AMOUNT_PER_DECAL
beauty = -100
+ persistent = TRUE
/obj/effect/decal/cleanable/oil/Initialize()
. = ..()
@@ -61,8 +63,10 @@
/obj/effect/decal/cleanable/oil/streak
random_icon_states = list("streak1", "streak2", "streak3", "streak4", "streak5")
beauty = -50
+ persistent = TRUE
/obj/effect/decal/cleanable/oil/slippery
+ persistent = FALSE
/obj/effect/decal/cleanable/oil/slippery/Initialize()
AddComponent(/datum/component/slippery, 80, (NO_SLIP_WHEN_WALKING | SLIDE))
diff --git a/code/game/objects/effects/decals/crayon.dm b/code/game/objects/effects/decals/crayon.dm
index 8cfdad0432..387b8c167d 100644
--- a/code/game/objects/effects/decals/crayon.dm
+++ b/code/game/objects/effects/decals/crayon.dm
@@ -5,6 +5,8 @@
icon_state = "rune1"
plane = ABOVE_WALL_PLANE //makes the graffiti visible over a wall.
gender = NEUTER
+ persistent = TRUE
+ persistence_allow_stacking = TRUE
mergeable_decal = FALSE
var/do_icon_rotate = TRUE
var/rotation = 0
@@ -29,3 +31,35 @@
if(main)
paint_colour = main
add_atom_colour(paint_colour, FIXED_COLOUR_PRIORITY)
+
+/obj/effect/decal/cleanable/crayon/PersistenceSave(list/data)
+ . = ..()
+ if(icon != initial(icon)) // no support for alticons yet, awful system anyways
+ return null
+ data["icon_state"] = icon_state
+ data["paint_color"] = paint_colour
+ if(do_icon_rotate)
+ data["rotation"] = rotation
+ data["name"] = name
+ if(pixel_x != initial(pixel_x))
+ data["pixel_x"] = pixel_x
+ if(pixel_y != initial(pixel_y))
+ data["pixel_y"] = pixel_y
+
+/obj/effect/decal/cleanable/crayon/PersistenceLoad(list/data)
+ . = ..()
+ if(data["name"])
+ name = data["name"]
+ if(do_icon_rotate && data["rotation"])
+ var/matrix/M = matrix()
+ M.Turn(text2num(data["rotation"]))
+ transform = M
+ if(data["paint_color"])
+ paint_colour = data["paint_color"]
+ add_atom_colour(paint_colour, FIXED_COLOUR_PRIORITY)
+ if(data["icon_state"])
+ icon_state = data["icon_state"]
+ if(data["pixel_x"])
+ pixel_x = data["pixel_x"]
+ if(data["pixel_y"])
+ pixel_y = data["pixel_y"]
diff --git a/code/game/turfs/open.dm b/code/game/turfs/open.dm
index 493e88d442..7e00b1cf63 100644
--- a/code/game/turfs/open.dm
+++ b/code/game/turfs/open.dm
@@ -1,5 +1,12 @@
/turf/open
plane = FLOOR_PLANE
+ /// Does dirt buildup happen on us?
+ var/dirt_buildup_allowed = FALSE
+ /// Dirt level.
+ var/dirtyness = 0
+ /// Dirt level to spawn dirt. Null to use config.
+ var/dirt_spawn_threshold
+ /// Slowdown applied to mobs on us.
var/slowdown = 0 //negative for faster, positive for slower
var/postdig_icon_change = FALSE
diff --git a/code/game/turfs/simulated/chasm.dm b/code/game/turfs/simulated/chasm.dm
index 5e8f55feed..9490d728fe 100644
--- a/code/game/turfs/simulated/chasm.dm
+++ b/code/game/turfs/simulated/chasm.dm
@@ -9,6 +9,7 @@
canSmoothWith = list(/turf/open/floor/fakepit, /turf/open/chasm)
density = TRUE //This will prevent hostile mobs from pathing into chasms, while the canpass override will still let it function like an open turf
bullet_bounce_sound = null //abandon all hope ye who enter
+ dirt_buildup_allowed = FALSE
/turf/open/chasm/Initialize()
. = ..()
diff --git a/code/game/turfs/simulated/dirtystation.dm b/code/game/turfs/simulated/dirtystation.dm
index e29d75a248..add8467ceb 100644
--- a/code/game/turfs/simulated/dirtystation.dm
+++ b/code/game/turfs/simulated/dirtystation.dm
@@ -8,6 +8,9 @@
//Making the station dirty, one tile at a time. Called by master controller's setup_objects
/turf/open/floor/proc/MakeDirty()
+ if(CONFIG_GET(flag/persistent_debris_only))
+ return
+
if(prob(66)) //fastest possible exit 2/3 of the time
return
@@ -38,7 +41,7 @@
return
//Construction zones. Blood, sweat, and oil. Oh, and dirt.
- var/static/list/engine_dirt_areas = typecacheof(list(/area/engine,
+ var/static/list/engine_dirt_areas = typecacheof(list(/area/engine,
/area/crew_quarters/heads/chief,
/area/ruin/space/derelict/assembly_line,
/area/science/robotics,
diff --git a/code/game/turfs/simulated/floor.dm b/code/game/turfs/simulated/floor.dm
index ddec9750d9..6a4ddea63b 100644
--- a/code/game/turfs/simulated/floor.dm
+++ b/code/game/turfs/simulated/floor.dm
@@ -5,6 +5,7 @@
name = "floor"
icon = 'icons/turf/floors.dmi'
baseturfs = /turf/open/floor/plating
+ dirt_buildup_allowed = TRUE
footstep = FOOTSTEP_FLOOR
barefootstep = FOOTSTEP_HARD_BAREFOOT
@@ -137,6 +138,9 @@
burnt = 1
/turf/open/floor/proc/make_plating()
+ for(var/obj/effect/decal/cleanable/C in src)
+ if(C.wiped_by_floor_change)
+ qdel(C)
return ScrapeAway(flags = CHANGETURF_INHERIT_AIR)
/turf/open/floor/ChangeTurf(path, new_baseturf, flags)
diff --git a/code/game/turfs/simulated/floor/plating.dm b/code/game/turfs/simulated/floor/plating.dm
index 35ee06a584..ad3cf86864 100644
--- a/code/game/turfs/simulated/floor/plating.dm
+++ b/code/game/turfs/simulated/floor/plating.dm
@@ -61,6 +61,9 @@
to_chat(user, "You begin reinforcing the floor...")
if(do_after(user, 30, target = src))
if (R.get_amount() >= 2 && !istype(src, /turf/open/floor/engine))
+ for(var/obj/effect/decal/cleanable/decal in src)
+ if(decal.wiped_by_floor_change)
+ qdel(decal)
PlaceOnTop(/turf/open/floor/engine, flags = CHANGETURF_INHERIT_AIR)
playsound(src, 'sound/items/deconstruct.ogg', 80, 1)
R.use(2)
@@ -78,6 +81,9 @@
to_chat(user, "You begin adding glass to the floor...")
if(do_after(user, 5, target = src))
if (G.get_amount() >= 2 && !istype(src, /turf/open/transparent/glass))
+ for(var/obj/effect/decal/cleanable/decal in src)
+ if(decal.wiped_by_floor_change)
+ qdel(decal)
PlaceOnTop(/turf/open/transparent/glass, flags = CHANGETURF_INHERIT_AIR)
playsound(src, 'sound/items/deconstruct.ogg', 80, 1)
G.use(2)
@@ -95,6 +101,9 @@
to_chat(user, "You begin adding reinforced glass to the floor...")
if(do_after(user, 10, target = src))
if (RG.get_amount() >= 2 && !istype(src, /turf/open/transparent/glass/reinforced))
+ for(var/obj/effect/decal/cleanable/decal in src)
+ if(decal.wiped_by_floor_change)
+ qdel(decal)
PlaceOnTop(/turf/open/transparent/glass/reinforced, flags = CHANGETURF_INHERIT_AIR)
playsound(src, 'sound/items/deconstruct.ogg', 80, 1)
RG.use(2)
@@ -110,6 +119,9 @@
var/obj/item/stack/tile/W = C
if(!W.use(1))
return
+ for(var/obj/effect/decal/cleanable/decal in src)
+ if(decal.wiped_by_floor_change)
+ qdel(decal)
if(istype(W, /obj/item/stack/tile/material))
var/turf/newturf = PlaceOnTop(/turf/open/floor/material, flags = CHANGETURF_INHERIT_AIR)
newturf.set_custom_materials(W.custom_materials)
diff --git a/code/game/turfs/simulated/floor/reinf_floor.dm b/code/game/turfs/simulated/floor/reinf_floor.dm
index 42b4707b50..f8c6ff1ce2 100644
--- a/code/game/turfs/simulated/floor/reinf_floor.dm
+++ b/code/game/turfs/simulated/floor/reinf_floor.dm
@@ -43,6 +43,9 @@
return TRUE
if(floor_tile)
new floor_tile(src, 2)
+ for(var/obj/effect/decal/cleanable/C in src)
+ if(C.wiped_by_floor_change)
+ qdel(C)
ScrapeAway(flags = CHANGETURF_INHERIT_AIR)
return TRUE
diff --git a/code/game/turfs/simulated/lava.dm b/code/game/turfs/simulated/lava.dm
index 2f223f2c40..e7471478fb 100644
--- a/code/game/turfs/simulated/lava.dm
+++ b/code/game/turfs/simulated/lava.dm
@@ -6,6 +6,7 @@
gender = PLURAL //"That's some lava."
baseturfs = /turf/open/lava //lava all the way down
slowdown = 2
+ dirt_buildup_allowed = FALSE
light_range = 2
light_power = 0.75
diff --git a/code/game/turfs/simulated/reebe_void.dm b/code/game/turfs/simulated/reebe_void.dm
index 14aa02065d..3f3bf31f9b 100644
--- a/code/game/turfs/simulated/reebe_void.dm
+++ b/code/game/turfs/simulated/reebe_void.dm
@@ -6,6 +6,7 @@
planetary_atmos = TRUE
bullet_bounce_sound = null //forever falling
tiled_dirt = FALSE
+ dirt_buildup_allowed = FALSE
/turf/open/indestructible/reebe_void/Initialize(mapload)
. = ..()
diff --git a/code/game/turfs/space/space.dm b/code/game/turfs/space/space.dm
index 765c88842e..9c9639e57c 100644
--- a/code/game/turfs/space/space.dm
+++ b/code/game/turfs/space/space.dm
@@ -3,6 +3,7 @@
icon_state = "0"
name = "\proper space"
intact = 0
+ dirt_buildup_allowed = FALSE
temperature = TCMB
thermal_conductivity = OPEN_HEAT_TRANSFER_COEFFICIENT
diff --git a/code/modules/antagonists/clockcult/clock_structures/ratvar_the_clockwork_justicar.dm b/code/modules/antagonists/clockcult/clock_structures/ratvar_the_clockwork_justicar.dm
index 73ae89a19b..28901ea0b2 100644
--- a/code/modules/antagonists/clockcult/clock_structures/ratvar_the_clockwork_justicar.dm
+++ b/code/modules/antagonists/clockcult/clock_structures/ratvar_the_clockwork_justicar.dm
@@ -30,6 +30,7 @@
var/mutable_appearance/alert_overlay = mutable_appearance('icons/effects/clockwork_effects.dmi', "ratvar_alert")
notify_ghosts("The Justiciar's light calls to you! Reach out to Ratvar in [get_area_name(src)] to be granted a shell to spread his glory!", null, source = src, alert_overlay = alert_overlay)
INVOKE_ASYNC(SSshuttle.emergency, /obj/docking_port/mobile/emergency.proc/request, null, 10, null, FALSE, 0)
+ SSpersistence.station_was_destroyed = TRUE
/obj/structure/destructible/clockwork/massive/ratvar/Destroy()
GLOB.ratvar_awakens--
diff --git a/code/modules/mapping/map_config.dm b/code/modules/mapping/map_config.dm
index d18ca1e1e3..1f4b558e21 100644
--- a/code/modules/mapping/map_config.dm
+++ b/code/modules/mapping/map_config.dm
@@ -18,6 +18,8 @@
var/map_name = "Box Station"
var/map_path = "map_files/BoxStation"
var/map_file = "BoxStation.dmm"
+ /// Persistence key: Defaults to ckey(map_name). If set to "NO_PERSIST", this map will have NO persistence.
+ var/persistence_key
var/traits = null
var/space_ruin_levels = 4
@@ -99,6 +101,16 @@
map_path = json["map_path"]
map_file = json["map_file"]
+
+ persistence_key = ckey(map_name)
+
+ var/json_persistence_key = json["persistence_key"]
+ if(json_persistence_key)
+ if(json_persistence_key == "NO_PERSIST")
+ persistence_key = null
+ else
+ persistence_key = json_persistence_key
+
// "map_file": "BoxStation.dmm"
if (istext(map_file))
if (!fexists("_maps/[map_path]/[map_file]"))
diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm
index 341aa6a30f..a82151cc1d 100644
--- a/code/modules/mob/living/carbon/human/human_defines.dm
+++ b/code/modules/mob/living/carbon/human/human_defines.dm
@@ -15,6 +15,7 @@
block_parry_data = /datum/block_parry_data/unarmed/human
default_block_parry_data = /datum/block_parry_data/unarmed/human
+ causes_dirt_buildup_on_floor = TRUE
//Hair colour and style
var/hair_color = "000"
diff --git a/code/modules/mob/living/carbon/human/human_movement.dm b/code/modules/mob/living/carbon/human/human_movement.dm
index 509633c0d5..440400f889 100644
--- a/code/modules/mob/living/carbon/human/human_movement.dm
+++ b/code/modules/mob/living/carbon/human/human_movement.dm
@@ -100,3 +100,8 @@
if(dna.species.space_move(src))
return TRUE
return ..()
+
+/mob/living/carbon/human/dirt_buildup(strength)
+ if(!shoes || !(shoes.body_parts_covered & FEET))
+ return // barefoot advantage
+ return ..()
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index d4ccc63b34..742d4d8624 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -63,6 +63,8 @@
//Allows mobs to move through dense areas without restriction. For instance, in space or out of holder objects.
var/incorporeal_move = FALSE //FALSE is off, INCORPOREAL_MOVE_BASIC is normal, INCORPOREAL_MOVE_SHADOW is for ninjas
//and INCORPOREAL_MOVE_JAUNT is blocked by holy water/salt
+ /// Do we make floors dirty as we move?
+ var/causes_dirt_buildup_on_floor = FALSE
var/list/roundstart_quirks = list()
diff --git a/code/modules/mob/living/living_movement.dm b/code/modules/mob/living/living_movement.dm
index 0cdfe9dbf2..9e093d391e 100644
--- a/code/modules/mob/living/living_movement.dm
+++ b/code/modules/mob/living/living_movement.dm
@@ -101,7 +101,30 @@
if(lying && !buckled && prob(getBruteLoss()*200/maxHealth))
makeTrail(newloc, T, old_direction)
+ if(causes_dirt_buildup_on_floor && (movement_type & GROUND))
+ dirt_buildup()
+/**
+ * Attempts to make the floor dirty.
+ */
+/mob/living/proc/dirt_buildup(strength = 1)
+ var/turf/open/T = loc
+ if(!istype(T) || !T.dirt_buildup_allowed)
+ return
+ var/area/A = T.loc
+ if(!A.dirt_buildup_allowed)
+ return
+ var/multiplier = CONFIG_GET(number/turf_dirty_multiplier)
+ strength *= multiplier
+ var/obj/effect/decal/cleanable/dirt/D = locate() in T
+ if(D)
+ D.dirty(strength)
+ else
+ T.dirtyness += strength
+ if(T.dirtyness >= (isnull(T.dirt_spawn_threshold)? CONFIG_GET(number/turf_dirt_threshold) : T.dirt_spawn_threshold))
+ D = new /obj/effect/decal/cleanable/dirt(T)
+ D.dirty(T.dirt_spawn_threshold - T.dirtyness)
+ T.dirtyness = 0 // reset.
/mob/living/Move_Pulled(atom/A)
. = ..()
diff --git a/code/modules/power/singularity/narsie.dm b/code/modules/power/singularity/narsie.dm
index 8bf20fddc6..5b5292a020 100644
--- a/code/modules/power/singularity/narsie.dm
+++ b/code/modules/power/singularity/narsie.dm
@@ -78,6 +78,7 @@
set_security_level("delta")
SSshuttle.registerHostileEnvironment(src)
SSshuttle.lockdown = TRUE
+ SSpersistence.station_was_destroyed = TRUE
sleep(600)
if(resolved == FALSE)
resolved = TRUE
diff --git a/config/config.txt b/config/config.txt
index 72df74beb1..c7bb36af0e 100644
--- a/config/config.txt
+++ b/config/config.txt
@@ -11,6 +11,7 @@ $include dynamic_config.txt
$include plushies/defines.txt
$include job_threats.txt
$include policy.txt
+$include persistence.txt
$include respawns.txt
# You can use the @ character at the beginning of a config option to lock it from being edited in-game
diff --git a/config/game_options.txt b/config/game_options.txt
index bc5fdf940e..3c53d9fecb 100644
--- a/config/game_options.txt
+++ b/config/game_options.txt
@@ -668,4 +668,10 @@ ALLOW_CUSTOM_SKINTONES
## Enables the FoV component, which hides objects and mobs behind the parent from their sight, unless they turn around, duh.
## Camera mobs, AIs, ghosts and some other are of course exempt from this. This also doesn't influence simplemob AI, for the best.
-#USE_FIELD_OF_VISION
\ No newline at end of file
+#USE_FIELD_OF_VISION
+
+## Default turf threshold to get dirt
+TURF_DIRT_THRESHOLD 100
+
+## Default alpha of dirt on spawn
+DIRT_ALPHA_STARTING 127
diff --git a/config/persistence.txt b/config/persistence.txt
new file mode 100644
index 0000000000..7d65a150e2
--- /dev/null
+++ b/config/persistence.txt
@@ -0,0 +1,14 @@
+### Whether or not cleanable object persistence is on
+PERSISTENT_DEBRIS
+
+### Whether or not ONLY persistent cleanable objects should be spawned, for supported objects
+PERSISTENT_DEBRIS_ONLY
+
+### Max objects to store, total
+PERSISTENT_DEBRIS_GLOBAL_MAX 10000
+
+### Max objects to store per type, total
+PERSISTENT_DEBRIS_TYPE_MAX 2000
+
+### Wipe objects on nuke for the above
+PERSISTENT_DEBRIS_WIPE_ON_NUKE
diff --git a/tgstation.dme b/tgstation.dme
index 04cadaef0e..3d2c19866a 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -292,6 +292,7 @@
#include "code\controllers\configuration\entries\fail2topic.dm"
#include "code\controllers\configuration\entries\game_options.dm"
#include "code\controllers\configuration\entries\general.dm"
+#include "code\controllers\configuration\entries\persistence.dm"
#include "code\controllers\configuration\entries\plushies.dm"
#include "code\controllers\configuration\entries\policy.dm"
#include "code\controllers\configuration\entries\resources.dm"
@@ -340,7 +341,6 @@
#include "code\controllers\subsystem\pai.dm"
#include "code\controllers\subsystem\parallax.dm"
#include "code\controllers\subsystem\pathfinder.dm"
-#include "code\controllers\subsystem\persistence.dm"
#include "code\controllers\subsystem\profiler.dm"
#include "code\controllers\subsystem\radiation.dm"
#include "code\controllers\subsystem\radio.dm"
@@ -363,6 +363,13 @@
#include "code\controllers\subsystem\vis_overlays.dm"
#include "code\controllers\subsystem\vore.dm"
#include "code\controllers\subsystem\vote.dm"
+#include "code\controllers\subsystem\persistence\_persistence.dm"
+#include "code\controllers\subsystem\persistence\cleanable_debris.dm"
+#include "code\controllers\subsystem\persistence\panic_bunker.dm"
+#include "code\controllers\subsystem\persistence\poly_parrot.dm"
+#include "code\controllers\subsystem\persistence\recent_votes_etc.dm"
+#include "code\controllers\subsystem\persistence\secret_satchels.dm"
+#include "code\controllers\subsystem\persistence\trophies.dm"
#include "code\controllers\subsystem\processing\chemistry.dm"
#include "code\controllers\subsystem\processing\circuit.dm"
#include "code\controllers\subsystem\processing\fastprocess.dm"