diff --git a/_maps/RandomRuins/SpaceRuins/crashedship.dmm b/_maps/RandomRuins/SpaceRuins/crashedship.dmm
index 63e6770e42..beb93b319e 100644
--- a/_maps/RandomRuins/SpaceRuins/crashedship.dmm
+++ b/_maps/RandomRuins/SpaceRuins/crashedship.dmm
@@ -2372,7 +2372,7 @@
},
/area/awaymission/BMPship/Midship)
"hh" = (
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/turf/open/floor/plating/airless,
/area/awaymission/BMPship/Midship)
"hi" = (
diff --git a/_maps/RandomZLevels/away_mission/Academy.dmm b/_maps/RandomZLevels/away_mission/Academy.dmm
index 37b0098fd2..9b61f50600 100644
--- a/_maps/RandomZLevels/away_mission/Academy.dmm
+++ b/_maps/RandomZLevels/away_mission/Academy.dmm
@@ -2710,7 +2710,7 @@
/obj/structure/cable{
icon_state = "4-8"
},
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/obj/effect/turf_decal/tile/green,
/obj/effect/turf_decal/tile/green{
dir = 4
diff --git a/_maps/RandomZLevels/away_mission/SnowCabin.dmm b/_maps/RandomZLevels/away_mission/SnowCabin.dmm
index 844a134d27..fbb83899fd 100644
--- a/_maps/RandomZLevels/away_mission/SnowCabin.dmm
+++ b/_maps/RandomZLevels/away_mission/SnowCabin.dmm
@@ -312,7 +312,7 @@
/obj/structure/janitorialcart,
/obj/item/mop,
/obj/item/reagent_containers/glass/bucket,
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/obj/effect/decal/cleanable/dirt,
/turf/open/floor/plating,
/area/awaymission/cabin)
diff --git a/_maps/map_files/BoxStation/BoxStation.dmm b/_maps/map_files/BoxStation/BoxStation.dmm
index 3e87af5427..cb257efeba 100644
--- a/_maps/map_files/BoxStation/BoxStation.dmm
+++ b/_maps/map_files/BoxStation/BoxStation.dmm
@@ -45265,7 +45265,7 @@
/area/maintenance/aft)
"cfq" = (
/obj/structure/mopbucket,
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/turf/open/floor/plating,
/area/maintenance/aft)
"cfr" = (
diff --git a/_maps/map_files/CogStation/CogStation.dmm b/_maps/map_files/CogStation/CogStation.dmm
index 7350c713ad..482ddc3f22 100644
--- a/_maps/map_files/CogStation/CogStation.dmm
+++ b/_maps/map_files/CogStation/CogStation.dmm
@@ -3643,7 +3643,7 @@
lootcount = 2;
name = "2maintenance loot spawner"
},
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/turf/open/floor/plating,
/area/construction)
"ajb" = (
@@ -4579,7 +4579,7 @@
/turf/open/floor/plasteel,
/area/security/processing)
"alk" = (
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/turf/open/floor/plating,
/area/construction/secondary)
"all" = (
@@ -23340,19 +23340,19 @@
/obj/effect/turf_decal/stripes/line{
dir = 8
},
-/obj/item/caution{
+/obj/item/clothing/suit/caution{
pixel_x = 6;
pixel_y = 3
},
-/obj/item/caution{
+/obj/item/clothing/suit/caution{
pixel_x = 6;
pixel_y = 3
},
-/obj/item/caution{
+/obj/item/clothing/suit/caution{
pixel_x = 6;
pixel_y = 3
},
-/obj/item/caution{
+/obj/item/clothing/suit/caution{
pixel_x = 6;
pixel_y = 3
},
@@ -26810,7 +26810,7 @@
/area/quartermaster/sorting)
"bgP" = (
/obj/item/analyzer,
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/turf/open/floor/plating,
/area/maintenance/disposal)
"bgQ" = (
@@ -33797,7 +33797,7 @@
/turf/open/floor/plasteel/dark,
/area/chapel/office)
"bvU" = (
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/obj/effect/decal/cleanable/dirt,
/obj/structure/sign/warning/biohazard{
pixel_x = -32
@@ -38795,7 +38795,7 @@
/turf/open/floor/plasteel/freezer,
/area/crew_quarters/toilet/restrooms)
"bGv" = (
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/turf/open/floor/plasteel,
/area/science/server/compcore)
"bGw" = (
@@ -40999,7 +40999,7 @@
dir = 1;
pixel_y = -26
},
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/obj/item/shovel,
/obj/item/stack/tile/plasteel{
pixel_x = 10;
@@ -51446,8 +51446,8 @@
/area/quartermaster/warehouse)
"cgt" = (
/obj/structure/rack,
-/obj/item/caution,
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
+/obj/item/clothing/suit/caution,
/obj/effect/decal/cleanable/dirt,
/turf/open/floor/plasteel,
/area/quartermaster/warehouse)
@@ -61826,7 +61826,7 @@
/obj/effect/turf_decal/tile/green{
dir = 8
},
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/obj/machinery/atmospherics/pipe/simple/cyan/hidden{
dir = 4
},
@@ -62925,7 +62925,7 @@
/obj/effect/turf_decal/tile/green{
dir = 4
},
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/obj/machinery/atmospherics/pipe/manifold4w/orange/hidden,
/obj/structure/cable{
icon_state = "4-8"
diff --git a/_maps/map_files/LambdaStation/dorms.dmm b/_maps/map_files/LambdaStation/dorms.dmm
index a38d2b9e1d..b7adb4a518 100644
--- a/_maps/map_files/LambdaStation/dorms.dmm
+++ b/_maps/map_files/LambdaStation/dorms.dmm
@@ -10679,7 +10679,7 @@
light_color = "#e8eaff"
},
/obj/machinery/vending/wardrobe/jani_wardrobe{
- products = list(/obj/item/clothing/under/rank/civilian/janitor = 2, /obj/item/cartridge/janitor = 2, /obj/item/clothing/gloves/color/black = 2, /obj/item/clothing/head/soft/purple = 2, /obj/item/paint/paint_remover = 2, /obj/item/melee/flyswatter = 2, /obj/item/flashlight = 2, /obj/item/caution = 10, /obj/item/holosign_creator = 2, /obj/item/lightreplacer = 2, /obj/item/soap/nanotrasen = 2, /obj/item/storage/bag/trash = 2, /obj/item/clothing/shoes/galoshes = 2, /obj/item/watertank/janitor = 2, /obj/item/storage/belt/janitor = 2)
+ products = list(/obj/item/clothing/under/rank/civilian/janitor = 2, /obj/item/cartridge/janitor = 2, /obj/item/clothing/gloves/color/black = 2, /obj/item/clothing/head/soft/purple = 2, /obj/item/paint/paint_remover = 2, /obj/item/melee/flyswatter = 2, /obj/item/flashlight = 2, /obj/item/clothing/suit/caution = 10, /obj/item/holosign_creator = 2, /obj/item/lightreplacer = 2, /obj/item/soap/nanotrasen = 2, /obj/item/storage/bag/trash = 2, /obj/item/clothing/shoes/galoshes = 2, /obj/item/watertank/janitor = 2, /obj/item/storage/belt/janitor = 2)
},
/turf/open/floor/plating,
/area/janitor)
@@ -13637,7 +13637,7 @@
/turf/open/floor/plasteel,
/area/hallway/primary/central)
"IX" = (
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/obj/effect/decal/cleanable/dirt,
/obj/effect/decal/cleanable/dirt,
/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden/layer3{
diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm
index ee47b2edab..c2953916e0 100644
--- a/_maps/map_files/MetaStation/MetaStation.dmm
+++ b/_maps/map_files/MetaStation/MetaStation.dmm
@@ -12681,7 +12681,7 @@
/turf/open/floor/wood,
/area/crew_quarters/dorms)
"axL" = (
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/obj/effect/turf_decal/stripes/line{
dir = 1
},
@@ -39921,9 +39921,6 @@
/turf/open/floor/plasteel/dark,
/area/bridge)
"bzU" = (
-/obj/structure/chair/comfy/teal{
- dir = 4
- },
/obj/structure/chair/comfy/black{
dir = 4
},
@@ -65296,9 +65293,6 @@
/turf/open/floor/plasteel,
/area/medical/genetics)
"czd" = (
-/obj/machinery/light_switch{
- pixel_x = 23
- },
/obj/machinery/atmospherics/pipe/simple/supply/hidden{
dir = 4
},
diff --git a/_maps/map_files/PubbyStation/PubbyStation.dmm b/_maps/map_files/PubbyStation/PubbyStation.dmm
index 0356dd1d07..d367ed82b1 100644
--- a/_maps/map_files/PubbyStation/PubbyStation.dmm
+++ b/_maps/map_files/PubbyStation/PubbyStation.dmm
@@ -24603,7 +24603,7 @@
/turf/open/floor/plating,
/area/maintenance/department/cargo)
"beS" = (
-/obj/item/caution,
+/obj/item/clothing/suit/caution,
/turf/open/floor/plating,
/area/maintenance/department/cargo)
"beU" = (
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/_globalvars/lists/flavor_misc.dm b/code/_globalvars/lists/flavor_misc.dm
index 0444e42e91..7fad1690e6 100644
--- a/code/_globalvars/lists/flavor_misc.dm
+++ b/code/_globalvars/lists/flavor_misc.dm
@@ -275,7 +275,7 @@ GLOBAL_LIST_INIT(wisdoms, world.file2list("strings/wisdoms.txt"))
//LANGUAGE CHARACTER CUSTOMIZATION
GLOBAL_LIST_INIT(speech_verbs, list("default","says","gibbers", "states", "chitters", "chimpers", "declares", "bellows", "buzzes" ,"beeps", "chirps", "clicks", "hisses" ,"poofs" , "puffs", "rattles", "mewls" ,"barks", "blorbles", "squeaks", "squawks", "flutters", "warbles"))
-GLOBAL_LIST_INIT(roundstart_tongues, list("default","human tongue" = /obj/item/organ/tongue, "lizard tongue" = /obj/item/organ/tongue/lizard, "skeleton tongue" = /obj/item/organ/tongue/bone, "fly tongue" = /obj/item/organ/tongue/fly, "ipc tongue" = /obj/item/organ/tongue/robot/ipc))
+GLOBAL_LIST_INIT(roundstart_tongues, list("default","human tongue" = /obj/item/organ/tongue, "lizard tongue" = /obj/item/organ/tongue/lizard, "skeleton tongue" = /obj/item/organ/tongue/bone, "fly tongue" = /obj/item/organ/tongue/fly, "ipc tongue" = /obj/item/organ/tongue/robot/ipc, "xeno tongue" = /obj/item/organ/tongue/alien))
//SPECIES BODYPART LISTS
//locked parts are those that your picked species requires to have
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/garbage.dm b/code/controllers/subsystem/garbage.dm
index 51d56545b6..da3bb24a2f 100644
--- a/code/controllers/subsystem/garbage.dm
+++ b/code/controllers/subsystem/garbage.dm
@@ -115,18 +115,22 @@ SUBSYSTEM_DEF(garbage)
lastlevel = level
- for (var/refID in queue)
- if (!refID)
+ //We do this rather then for(var/refID in queue) because that sort of for loop copies the whole list.
+ //Normally this isn't expensive, but the gc queue can grow to 40k items, and that gets costly/causes overrun.
+ for (var/i in 1 to length(queue))
+ var/list/L = queue[i]
+ if (length(L) < 2)
count++
if (MC_TICK_CHECK)
return
continue
- var/GCd_at_time = queue[refID]
+ var/GCd_at_time = L[1]
if(GCd_at_time > cut_off_time)
break // Everything else is newer, skip them
count++
+ var/refID = L[2]
var/datum/D
D = locate(refID)
@@ -202,10 +206,7 @@ SUBSYSTEM_DEF(garbage)
D.gc_destroyed = gctime
var/list/queue = queues[level]
- if (queue[refid])
- queue -= refid // Removing any previous references that were GC'd so that the current object will be at the end of the list.
-
- queue[refid] = gctime
+ queue[++queue.len] = list(gctime, refid) // not += for byond reasons
#ifdef LEGACY_REFERENCE_TRACKING
/datum/controller/subsystem/garbage/proc/add_type_to_findref(type)
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/datums/helper_datums/teleport.dm b/code/datums/helper_datums/teleport.dm
index 5e9371f754..79c12c7fea 100644
--- a/code/datums/helper_datums/teleport.dm
+++ b/code/datums/helper_datums/teleport.dm
@@ -85,6 +85,15 @@
var/mob/M = teleatom
M.cancel_camera()
+ var/static/list/bread_cache = typecacheof(/obj/item/reagent_containers/food/snacks/store/bread)
+ var/list/breadlist = typecache_filter_list(teleatom.GetAllContents(), bread_cache)
+ if(breadlist.len && (channel == TELEPORT_CHANNEL_BLUESPACE || channel == TELEPORT_CHANNEL_QUANTUM))
+ for(var/obj/item/reagent_containers/food/snacks/store/bread/bread in breadlist)
+ bread.bread_teleport()
+ else if(istype(teleatom, /obj/item/reagent_containers/food/snacks/store/bread))
+ var/obj/item/reagent_containers/food/snacks/store/bread/bread = teleatom
+ bread.bread_teleport()
+
return TRUE
/proc/tele_play_specials(atom/movable/teleatom, atom/location, datum/effect_system/effect, sound)
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/machinery/computer/atmos_control.dm b/code/game/machinery/computer/atmos_control.dm
index 4ba8d9f3d3..75a181b922 100644
--- a/code/game/machinery/computer/atmos_control.dm
+++ b/code/game/machinery/computer/atmos_control.dm
@@ -6,6 +6,7 @@
name = "gas sensor"
icon = 'icons/obj/stationobjs.dmi'
icon_state = "gsensor1"
+ armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 0)
var/on = TRUE
diff --git a/code/game/machinery/porta_turret/portable_turret.dm b/code/game/machinery/porta_turret/portable_turret.dm
index 34745a65f6..29883a073a 100644
--- a/code/game/machinery/porta_turret/portable_turret.dm
+++ b/code/game/machinery/porta_turret/portable_turret.dm
@@ -774,6 +774,9 @@
stun_projectile_sound = 'sound/weapons/gunshot_smg.ogg'
armor = list("melee" = 50, "bullet" = 30, "laser" = 30, "energy" = 30, "bomb" = 80, "bio" = 0, "rad" = 0, "fire" = 90, "acid" = 90)
+/obj/machinery/porta_turret/syndicate/pod/toolbox
+ max_integrity = 100
+
/obj/machinery/porta_turret/syndicate/shuttle/target(atom/movable/target)
if(target)
setDir(get_dir(base, target))//even if you can't shoot, follow the target
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/objects/items/devices/traitordevices.dm b/code/game/objects/items/devices/traitordevices.dm
index eb444cdb12..70edd3f43d 100644
--- a/code/game/objects/items/devices/traitordevices.dm
+++ b/code/game/objects/items/devices/traitordevices.dm
@@ -264,6 +264,30 @@ effective or pretty fucking useless.
GLOB.active_jammers -= src
update_icon()
+/*portable turret*/
+/obj/item/storage/toolbox/emergency/turret
+ desc = "You feel a strange urge to hit this with a wrench."
+
+/obj/item/storage/toolbox/emergency/turret/PopulateContents()
+ new /obj/item/screwdriver(src)
+ new /obj/item/wrench(src)
+ new /obj/item/weldingtool(src)
+ new /obj/item/crowbar(src)
+ new /obj/item/analyzer(src)
+ new /obj/item/wirecutters(src)
+
+/obj/item/storage/toolbox/emergency/turret/attackby(obj/item/I, mob/living/user, params)
+ if(I.tool_behaviour == TOOL_WRENCH && user.a_intent == INTENT_HARM)
+ user.visible_message("[user] bashes [src] with [I]!", \
+ "You bash [src] with [I]!", null, COMBAT_MESSAGE_RANGE)
+ playsound(src, "sound/items/drill_use.ogg", 80, TRUE, -1)
+ var/obj/machinery/porta_turret/syndicate/pod/toolbox/turret = new(get_turf(loc))
+ turret.faction = list("[REF(user)]")
+ qdel(src)
+ return
+
+ ..()
+
/obj/item/headsetupgrader
name = "headset upgrader"
desc = "A tool that can be used to upgrade a normal headset to be able to protect from flashbangs."
diff --git a/code/game/objects/items/grenades/plastic.dm b/code/game/objects/items/grenades/plastic.dm
index 014487332c..69f89d0150 100644
--- a/code/game/objects/items/grenades/plastic.dm
+++ b/code/game/objects/items/grenades/plastic.dm
@@ -19,6 +19,7 @@
var/boom_sizes = list(0, 0, 3)
var/can_attach_mob = FALSE
var/full_damage_on_mobs = FALSE
+ var/can_gib_mobs = FALSE
/obj/item/grenade/plastic/Initialize()
. = ..()
@@ -72,7 +73,7 @@
explosion(get_step(T, aim_dir), boom_sizes[1], boom_sizes[2], boom_sizes[3])
else
explosion(location, boom_sizes[1], boom_sizes[2], boom_sizes[3])
- if(ismob(target))
+ if(ismob(target) && can_gib_mobs)
var/mob/M = target
M.gib()
qdel(src)
diff --git a/code/game/objects/items/miscellaneous.dm b/code/game/objects/items/miscellaneous.dm
index 913d73bdad..224c4ffb9b 100644
--- a/code/game/objects/items/miscellaneous.dm
+++ b/code/game/objects/items/miscellaneous.dm
@@ -1,17 +1,3 @@
-/obj/item/caution
- desc = "Caution! Wet Floor!"
- name = "wet floor sign"
- icon = 'icons/obj/janitor.dmi'
- icon_state = "caution"
- lefthand_file = 'icons/mob/inhands/equipment/custodial_lefthand.dmi'
- righthand_file = 'icons/mob/inhands/equipment/custodial_righthand.dmi'
- force = 1
- throwforce = 3
- throw_speed = 2
- throw_range = 5
- w_class = WEIGHT_CLASS_SMALL
- attack_verb = list("warned", "cautioned", "smashed")
-
/obj/item/choice_beacon
name = "choice beacon"
desc = "Hey, why are you viewing this?!! Please let Centcom know about this odd occurance."
diff --git a/code/game/objects/items/robot/robot_parts.dm b/code/game/objects/items/robot/robot_parts.dm
index a9cd4c33a7..e4d1dd8516 100644
--- a/code/game/objects/items/robot/robot_parts.dm
+++ b/code/game/objects/items/robot/robot_parts.dm
@@ -392,6 +392,7 @@
created_name = new_name
else
created_name = ""
+ log_game("[key_name(usr)] renamed [src] ([REF(src)]) to [created_name].")
else if(href_list["Master"])
forced_ai = select_active_ai(usr)
diff --git a/code/game/objects/items/stacks/sheets/sheet_types.dm b/code/game/objects/items/stacks/sheets/sheet_types.dm
index c0fd65bd28..9f126f492b 100644
--- a/code/game/objects/items/stacks/sheets/sheet_types.dm
+++ b/code/game/objects/items/stacks/sheets/sheet_types.dm
@@ -793,7 +793,7 @@ GLOBAL_LIST_INIT(plastic_recipes, list(
new /datum/stack_recipe("shower curtain", /obj/structure/curtain, 10, time = 10, one_per_turf = 1, on_floor = 1), \
new /datum/stack_recipe("duct", /obj/item/stack/ducts,1), \
new /datum/stack_recipe("laser pointer case", /obj/item/glasswork/glass_base/laserpointer_shell, 30), \
- new /datum/stack_recipe("wet floor sign", /obj/item/caution, 2)))
+ new /datum/stack_recipe("wet floor sign", /obj/item/clothing/suit/caution, 2)))
/obj/item/stack/sheet/plastic
name = "plastic"
diff --git a/code/game/objects/items/storage/toolbox.dm b/code/game/objects/items/storage/toolbox.dm
index 73246276c2..7a067257cb 100644
--- a/code/game/objects/items/storage/toolbox.dm
+++ b/code/game/objects/items/storage/toolbox.dm
@@ -103,6 +103,39 @@ GLOBAL_LIST_EMPTY(rubber_toolbox_icons)
/obj/item/storage/toolbox/mechanical/old/heirloom/PopulateContents()
return
+/obj/item/storage/toolbox/mechanical/old/clean // the assistant traitor toolbox, damage scales with TC inside
+ name = "toolbox"
+ desc = "An old, blue toolbox. It menaces with a sickening miasma of robust energies. You sure about this, Brain?"
+ icon_state = "toolbox_blue_clean"
+ force = 19
+ throwforce = 22
+ wound_bonus = 0
+ bare_wound_bonus = 10
+
+/obj/item/storage/toolbox/mechanical/old/clean/proc/calc_damage()
+ var/power = 0
+ for (var/obj/item/stack/telecrystal/TC in GetAllContents())
+ power += TC.amount
+ force = 19 + power
+ throwforce = 22 + power
+
+/obj/item/storage/toolbox/mechanical/old/clean/attack(mob/target, mob/living/user)
+ calc_damage() // one damage for one telecrystal equals about thirty seven(?) damage if you pour ALL your tc
+ ..()
+
+/obj/item/storage/toolbox/mechanical/old/clean/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
+ calc_damage()
+ ..()
+
+/obj/item/storage/toolbox/mechanical/old/clean/PopulateContents()
+ new /obj/item/screwdriver(src)
+ new /obj/item/wrench(src)
+ new /obj/item/weldingtool(src)
+ new /obj/item/crowbar(src)
+ new /obj/item/wirecutters(src)
+ new /obj/item/multitool(src)
+ new /obj/item/clothing/gloves/color/yellow(src)
+
/obj/item/storage/toolbox/electrical
name = "electrical toolbox"
icon_state = "yellow"
diff --git a/code/game/objects/structures/crates_lockers/closets/job_closets.dm b/code/game/objects/structures/crates_lockers/closets/job_closets.dm
index 21b75e8af3..3af0d0278a 100644
--- a/code/game/objects/structures/crates_lockers/closets/job_closets.dm
+++ b/code/game/objects/structures/crates_lockers/closets/job_closets.dm
@@ -70,7 +70,7 @@
new /obj/item/melee/flyswatter(src)
new /obj/item/flashlight(src)
for(var/i in 1 to 3)
- new /obj/item/caution(src)
+ new /obj/item/clothing/suit/caution(src)
new /obj/item/holosign_creator(src)
new /obj/item/lightreplacer(src)
new /obj/item/soap(src)
diff --git a/code/game/objects/structures/janicart.dm b/code/game/objects/structures/janicart.dm
index 10a9f2afd4..0c99188c49 100644
--- a/code/game/objects/structures/janicart.dm
+++ b/code/game/objects/structures/janicart.dm
@@ -73,7 +73,7 @@
l.janicart_insert(user,src)
else
to_chat(user, fail_msg)
- else if(istype(I, /obj/item/caution))
+ else if(istype(I, /obj/item/clothing/suit/caution))
if(signs < max_signs)
put_in_cart(I, user)
signs++
@@ -104,7 +104,7 @@
items += list("Spray bottle" = image(icon = myspray.icon, icon_state = myspray.icon_state))
if(myreplacer)
items += list("Light replacer" = image(icon = myreplacer.icon, icon_state = myreplacer.icon_state))
- var/obj/item/caution/sign = locate() in src
+ var/obj/item/clothing/suit/caution/sign = locate() in src
if(sign)
items += list("Sign" = image(icon = sign.icon, icon_state = sign.icon_state))
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/blob/blob/blobs/resource.dm b/code/modules/antagonists/blob/blob/blobs/resource.dm
index 76aecf717d..2ed9744327 100644
--- a/code/modules/antagonists/blob/blob/blobs/resource.dm
+++ b/code/modules/antagonists/blob/blob/blobs/resource.dm
@@ -26,7 +26,7 @@
return
flick("blob_resource_glow", src)
if(overmind)
- overmind.add_points(1)
+ overmind.add_points(2)
resource_delay = world.time + 40 + overmind.resource_blobs.len * 2.5 //4 seconds plus a quarter second for each resource blob the overmind has
else
resource_delay = world.time + 40
diff --git a/code/modules/antagonists/blob/blob/powers.dm b/code/modules/antagonists/blob/blob/powers.dm
index dff8e22552..cd53f624a6 100644
--- a/code/modules/antagonists/blob/blob/powers.dm
+++ b/code/modules/antagonists/blob/blob/powers.dm
@@ -147,7 +147,7 @@
set category = "Blob"
set name = "Create Factory Blob (60)"
set desc = "Create a spore tower that will spawn spores to harass your enemies."
- createSpecial(60, /obj/structure/blob/factory, 7, 1)
+ createSpecial(60, /obj/structure/blob/factory, 5, 1)
/mob/camera/blob/verb/create_blobbernaut()
set category = "Blob"
@@ -195,6 +195,7 @@
to_chat(blobber, "The [blobstrain.name] reagent [blobstrain.shortdesc ? "[blobstrain.shortdesc]" : "[blobstrain.description]"]")
else
to_chat(src, "You could not conjure a sentience for your blobbernaut. Your points have been refunded. Try again later.")
+ B.naut = FALSE
add_points(40)
/mob/camera/blob/verb/relocate_core()
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/antagonists/morph/morph.dm b/code/modules/antagonists/morph/morph.dm
index e91feda006..5844aad02b 100644
--- a/code/modules/antagonists/morph/morph.dm
+++ b/code/modules/antagonists/morph/morph.dm
@@ -42,7 +42,9 @@
/obj/screen,
/obj/singularity,
/mob/living/simple_animal/hostile/morph,
- /obj/effect))
+ /obj/effect,
+ /mob/camera
+ ))
var/playstyle_string = "You are a morph, an abomination of science created primarily with changeling cells. \
You may take the form of anything nearby by shift-clicking it. This process will alert any nearby \
diff --git a/code/modules/cargo/console.dm b/code/modules/cargo/console.dm
index f5a8d21278..5a976b0abb 100644
--- a/code/modules/cargo/console.dm
+++ b/code/modules/cargo/console.dm
@@ -179,6 +179,10 @@
if((pack.hidden && !(obj_flags & EMAGGED)) || (pack.contraband && !contraband) || pack.DropPodOnly)
return
+ if(self_paid && !pack.can_private_buy)
+ say("This cannot be bought privately.")
+ return
+
var/name = "*None Provided*"
var/rank = "*None Provided*"
var/ckey = usr.ckey
diff --git a/code/modules/cargo/packs/service.dm b/code/modules/cargo/packs/service.dm
index 0c7c63f867..2bb3934ac7 100644
--- a/code/modules/cargo/packs/service.dm
+++ b/code/modules/cargo/packs/service.dm
@@ -261,9 +261,9 @@
/obj/item/reagent_containers/glass/bucket,
/obj/item/reagent_containers/glass/bucket,
/obj/item/mop,
- /obj/item/caution,
- /obj/item/caution,
- /obj/item/caution,
+ /obj/item/clothing/suit/caution,
+ /obj/item/clothing/suit/caution,
+ /obj/item/clothing/suit/caution,
/obj/item/storage/bag/trash,
/obj/item/reagent_containers/spray/cleaner,
/obj/item/reagent_containers/rag,
diff --git a/code/modules/clothing/suits/miscellaneous.dm b/code/modules/clothing/suits/miscellaneous.dm
index 149f17f4d6..6dfcd72c10 100644
--- a/code/modules/clothing/suits/miscellaneous.dm
+++ b/code/modules/clothing/suits/miscellaneous.dm
@@ -541,6 +541,21 @@
flags_inv = HIDEGLOVES|HIDESHOES|HIDEJUMPSUIT
allowed = list(/obj/item/clothing/mask/facehugger/toy)
+/obj/item/clothing/suit/caution
+ name = "wet floor sign"
+ desc = "Caution! Wet Floor!"
+ icon_state = "caution"
+ lefthand_file = 'icons/mob/inhands/equipment/custodial_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/equipment/custodial_righthand.dmi'
+ force = 1
+ throwforce = 3
+ throw_speed = 2
+ throw_range = 5
+ w_class = WEIGHT_CLASS_SMALL
+ body_parts_covered = CHEST|GROIN
+ attack_verb = list("warned", "cautioned", "smashed")
+ armor = list("melee" = 5, "bullet" = 0, "laser" = 0,"energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 0, "acid" = 0)
+
// WINTER COATS
/obj/item/clothing/suit/hooded/wintercoat
diff --git a/code/modules/food_and_drinks/food/snacks_bread.dm b/code/modules/food_and_drinks/food/snacks_bread.dm
index 0d7c715654..3b133d8675 100644
--- a/code/modules/food_and_drinks/food/snacks_bread.dm
+++ b/code/modules/food_and_drinks/food/snacks_bread.dm
@@ -7,6 +7,12 @@
foodtype = GRAIN
dunkable = TRUE
+/obj/item/reagent_containers/food/snacks/store/bread/proc/bread_teleport()
+ // you did WHAT?
+ new /mob/living/simple_animal/hostile/bread(get_turf(src))
+ visible_message("[src] begins to deform and grow grotesque tumors!")
+ qdel(src)
+
/obj/item/reagent_containers/food/snacks/breadslice
icon = 'icons/obj/food/burgerbread.dmi'
bitesize = 2
@@ -228,3 +234,25 @@
/obj/item/reagent_containers/food/snacks/butterdog/ComponentInitialize()
. = ..()
AddComponent(/datum/component/slippery, 80)
+
+/obj/item/reagent_containers/food/snacks/store/bread/tumor_bread
+ name = "dead tumor bread"
+ desc = "It's still within its expiration date, right?."
+ icon_state = "tumorbread"
+ slice_path = /obj/item/reagent_containers/food/snacks/breadslice/tumor_bread
+ list_reagents = list(/datum/reagent/consumable/nutriment = 10, /datum/reagent/toxin = 10)
+ foodtype = GROSS | GRAIN
+ tastes = list("wheat and tumors" = 10)
+
+//teleporting tumor bread kills it
+/obj/item/reagent_containers/food/snacks/store/bread/tumor_bread/bread_teleport()
+ visible_message(src, "[src] explodes in a horrible mess of tumor and flour!")
+ qdel(src)
+
+/obj/item/reagent_containers/food/snacks/breadslice/tumor_bread
+ name = "tumor bread slice"
+ desc = "A slice of bread filled with tumors!"
+ icon_state = "tumorbreadslice"
+ filling_color = "#B2D72C"
+ list_reagents = list(/datum/reagent/consumable/nutriment = 2, /datum/reagent/toxin = 2)
+ foodtype = GROSS | GRAIN
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/dead/new_player/sprite_accessories/wings.dm b/code/modules/mob/dead/new_player/sprite_accessories/wings.dm
index fb71bb483d..37ef2a7d19 100644
--- a/code/modules/mob/dead/new_player/sprite_accessories/wings.dm
+++ b/code/modules/mob/dead/new_player/sprite_accessories/wings.dm
@@ -138,6 +138,18 @@
name = "White Fly"
icon_state = "whitefly"
+/datum/sprite_accessory/deco_wings/oakworm
+ name = "Oak Worm"
+ icon_state = "oakworm"
+
+/datum/sprite_accessory/deco_wings/witchwing
+ name = "Witch Wing"
+ icon_state = "witchwing"
+
+/datum/sprite_accessory/deco_wings/jungle
+ name = "Jungle"
+ icon_state = "jungle"
+
//INSECT WINGS
/datum/sprite_accessory/insect_wings
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/carbon/human/species_types/xeno.dm b/code/modules/mob/living/carbon/human/species_types/xeno.dm
index 2757c624e7..f88ccc876f 100644
--- a/code/modules/mob/living/carbon/human/species_types/xeno.dm
+++ b/code/modules/mob/living/carbon/human/species_types/xeno.dm
@@ -5,7 +5,7 @@
say_mod = "hisses"
default_color = "00FF00"
species_traits = list(MUTCOLORS,EYECOLOR,LIPS,CAN_SCAR)
- mutant_bodyparts = list("xenotail"="Xenomorph Tail","xenohead"="Standard","xenodorsal"="Standard", "mam_body_markings" = "Xeno","mcolor" = "0F0","mcolor2" = "0F0","mcolor3" = "0F0","taur" = "None", "legs" = "Digitigrade")
+ mutant_bodyparts = list("xenotail"="Xenomorph Tail","xenohead"="Standard","xenodorsal"="Standard", "mam_body_markings" = "Xeno","mcolor" = "0F0","mcolor2" = "0F0","mcolor3" = "0F0","taur" = "None", "legs" = "Digitigrade","deco_wings"= "None")
attack_verb = "slash"
attack_sound = 'sound/weapons/slash.ogg'
miss_sound = 'sound/weapons/slashmiss.ogg'
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/mob/living/simple_animal/hostile/bread.dm b/code/modules/mob/living/simple_animal/hostile/bread.dm
new file mode 100644
index 0000000000..317827028d
--- /dev/null
+++ b/code/modules/mob/living/simple_animal/hostile/bread.dm
@@ -0,0 +1,69 @@
+//funny reference to the video 'Expiration Date'
+
+/mob/living/simple_animal/hostile/bread
+ name = "tumor bread"
+ desc = "I have done nothing but teleport bread for three days."
+ icon_state = "tumorbread"
+ health = 1
+ maxHealth = 1
+ turns_per_move = 5 //this isn't player speed =|
+ speed = 2 //this is player speed
+ melee_damage_lower = 1
+ melee_damage_upper = 2
+ obj_damage = 0
+ loot = list(/obj/item/reagent_containers/food/snacks/store/bread/tumor_bread)
+ atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
+ minbodytemp = 270
+ maxbodytemp = INFINITY
+ pass_flags = PASSTABLE | PASSGRILLE | PASSMOB
+ mob_size = MOB_SIZE_TINY
+ response_help_continuous = "pokes"
+ response_help_simple = "poke"
+ response_disarm_continuous = "shoos"
+ response_disarm_simple = "shoo"
+ response_harm_continuous = "punches"
+ response_harm_simple = "punch"
+ speak_emote = list("growls")
+ mouse_opacity = 2
+ density = TRUE
+ ventcrawler = VENTCRAWLER_ALWAYS
+ verb_say = "growls"
+ verb_ask = "growls inquisitively"
+ verb_exclaim = "growls loudly"
+ verb_yell = "growls loudly"
+ del_on_death = TRUE
+
+/mob/living/simple_animal/bread/hostile/Initialize()
+ . = ..()
+ var/area/A = get_area(src)
+ if(A)
+ notify_ghosts("A tumor bread has been created in \the [A.name].", source = src, action=NOTIFY_ATTACK, flashwindow = FALSE, ignore_dnr_observers = TRUE)
+
+/mob/living/simple_animal/hostile/bread/attack_ghost(mob/user)
+ if(key) //please stop using src. without a good reason.
+ return
+ if(CONFIG_GET(flag/use_age_restriction_for_jobs))
+ if(!isnum(user.client.player_age))
+ return
+ if(isobserver(user))
+ var/mob/dead/observer/O = user
+ if(!O.can_reenter_round())
+ return
+ if(!SSticker.mode)
+ to_chat(user, "Can't become a tumor bread before the game has started.")
+ return
+ var/be_bread = alert("Become a tumor bread? (Warning, You can no longer be cloned!)",,"Yes","No")
+ if(be_bread == "No" || QDELETED(src) || !isobserver(user))
+ return
+ if(key)
+ to_chat(user, "Someone else already took this tumor bread.")
+ return
+ sentience_act()
+ user.transfer_ckey(src, FALSE)
+ density = TRUE
+
+/mob/living/simple_animal/hostile/bread/ex_act()
+ return
+
+/mob/living/simple_animal/hostile/bread/start_pulling()
+ return FALSE
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index 5fa6a0c22d..e9b97b3597 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -354,7 +354,7 @@
/mob/living/simple_animal/proc/drop_loot()
if(loot.len)
for(var/i in loot)
- new i(loc)
+ new i(loc, dir = src.dir)
/mob/living/simple_animal/death(gibbed)
movement_type &= ~FLYING
diff --git a/code/modules/paperwork/pen.dm b/code/modules/paperwork/pen.dm
index 13890ae69e..8863a098fc 100644
--- a/code/modules/paperwork/pen.dm
+++ b/code/modules/paperwork/pen.dm
@@ -182,8 +182,9 @@
if(..())
if(reagents.total_volume)
if(M.reagents)
+ log_combat(user, M, "injected with sleepypen", src, reagents.log_list())
reagents.reaction(M, INJECT)
- reagents.trans_to(M, reagents.total_volume)
+ reagents.trans_to(M, reagents.total_volume, log = "sleepypen inject")
/obj/item/pen/sleepy/Initialize()
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/code/modules/uplink/uplink_items/uplink_roles.dm b/code/modules/uplink/uplink_items/uplink_roles.dm
index b384106396..4edbe2f2c7 100644
--- a/code/modules/uplink/uplink_items/uplink_roles.dm
+++ b/code/modules/uplink/uplink_items/uplink_roles.dm
@@ -142,6 +142,14 @@
cost = 4 // Has syndie tools + gloves + a robust weapon
restricted_roles = list("Assistant", "Curator") //Curator due to this being made of gold - It fits the theme
+/datum/uplink_item/role_restricted/oldtoolboxclean // this is the fourth item relating to toolboxes to be placed into this godforsaken bloated uplink
+ name = "Ancient Toolbox"
+ desc = "An iconic toolbox design notorious with Assistants everywhere, this design was especially made to become more robust the more telecrystals it has inside it! Tools and insulated gloves included."
+ item = /obj/item/storage/toolbox/mechanical/old/clean
+ cost = 2 // with eighteen telecrystals you are still weaker than a desword and without any of its defenses -- the scary part comes from collaborating with allies for a fifty eight force toolbox oh fuck
+ restricted_roles = list("Assistant")
+ surplus = 0
+
/datum/uplink_item/role_restricted/mimery
name = "Guide to Advanced Mimery Series"
desc = "The classical two part series on how to further hone your mime skills. Upon studying the series, the user should be able to make 3x1 invisible walls, and shoot bullets out of their fingers. \
@@ -242,3 +250,11 @@
cost = 2
item = /obj/item/clothing/shoes/magboots/crushing
restricted_roles = list("Chief Engineer", "Station Engineer", "Atmospheric Technician")
+
+/datum/uplink_item/role_restricted/turretbox
+ name = "Disposable Sentry Gun"
+ desc = "A disposable sentry gun deployment system cleverly disguised as a toolbox, apply wrench for functionality."
+ item = /obj/item/storage/toolbox/emergency/turret
+ cost = 11
+ restricted_roles = list("Station Engineer")
+
diff --git a/code/modules/vending/wardrobes.dm b/code/modules/vending/wardrobes.dm
index d10f06b257..0b47c7218f 100644
--- a/code/modules/vending/wardrobes.dm
+++ b/code/modules/vending/wardrobes.dm
@@ -375,7 +375,7 @@
/obj/item/paint/paint_remover = 2,
/obj/item/melee/flyswatter = 2,
/obj/item/flashlight = 2,
- /obj/item/caution = 8,
+ /obj/item/clothing/suit/caution = 8,
/obj/item/holosign_creator = 2,
/obj/item/lightreplacer = 2,
/obj/item/soap = 2,
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/html/changelogs/AutoChangeLog-pr-13834.yml b/html/changelogs/AutoChangeLog-pr-13834.yml
new file mode 100644
index 0000000000..46cedff5d5
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13834.yml
@@ -0,0 +1,4 @@
+author: "zeroisthebiggay"
+delete-after: True
+changes:
+ - rscadd: "Traitor assistants can now purchase the patented POGBox! Put TC into it for even higher damage!"
diff --git a/html/changelogs/AutoChangeLog-pr-13871.yml b/html/changelogs/AutoChangeLog-pr-13871.yml
new file mode 100644
index 0000000000..f667abe816
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13871.yml
@@ -0,0 +1,5 @@
+author: "NT Cleaning Crews On Break"
+delete-after: True
+changes:
+ - rscadd: "Most kinds of dirt, grime, and debris are now persistent. Get to work, jannies."
+ - rscadd: "Dirt can now be removed by tile replacements. Other cleanable decals can't, though."
diff --git a/html/changelogs/AutoChangeLog-pr-13931.yml b/html/changelogs/AutoChangeLog-pr-13931.yml
new file mode 100644
index 0000000000..fd903f2d6e
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13931.yml
@@ -0,0 +1,4 @@
+author: "shellspeed1"
+delete-after: True
+changes:
+ - rscadd: "Adds the disposable sentry gun from tg for 11tc each."
diff --git a/html/changelogs/AutoChangeLog-pr-13967.yml b/html/changelogs/AutoChangeLog-pr-13967.yml
new file mode 100644
index 0000000000..d2e580b531
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13967.yml
@@ -0,0 +1,6 @@
+author: "keronshb"
+delete-after: True
+changes:
+ - balance: "Blob Resource Tower to 2 points per instead of 1 point per."
+ - balance: "Blob Factory Towers can be placed 5 tiles apart instead of 7."
+ - bugfix: "Fixes Blobbernaut Factories consuming Factories if no naut is chosen."
diff --git a/html/changelogs/AutoChangeLog-pr-13976.yml b/html/changelogs/AutoChangeLog-pr-13976.yml
new file mode 100644
index 0000000000..dac85f8e9a
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13976.yml
@@ -0,0 +1,4 @@
+author: "timothyteakettle"
+delete-after: True
+changes:
+ - rscadd: "you can teleport bread"
diff --git a/html/changelogs/AutoChangeLog-pr-13978.yml b/html/changelogs/AutoChangeLog-pr-13978.yml
new file mode 100644
index 0000000000..94c19edec4
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13978.yml
@@ -0,0 +1,4 @@
+author: "silicons"
+delete-after: True
+changes:
+ - balance: "c4 can no longer gib mobs"
diff --git a/html/changelogs/AutoChangeLog-pr-13980.yml b/html/changelogs/AutoChangeLog-pr-13980.yml
new file mode 100644
index 0000000000..6838ba48a2
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13980.yml
@@ -0,0 +1,4 @@
+author: "MrJWhit"
+delete-after: True
+changes:
+ - rscadd: "Makes gas sensors fireproof."
diff --git a/html/changelogs/AutoChangeLog-pr-13983.yml b/html/changelogs/AutoChangeLog-pr-13983.yml
new file mode 100644
index 0000000000..ba0bf62e6a
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13983.yml
@@ -0,0 +1,6 @@
+author: "shellspeed1"
+delete-after: True
+changes:
+ - rscadd: "Xenomorph hybrids can now select wings
+~~add: Xenomorph hybrids can now speak xenomorph~~"
+ - rscadd: "Xenomorph tongues are available for customization."
diff --git a/html/changelogs/AutoChangeLog-pr-13988.yml b/html/changelogs/AutoChangeLog-pr-13988.yml
new file mode 100644
index 0000000000..262e5f804f
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13988.yml
@@ -0,0 +1,4 @@
+author: "DeltaFire15"
+delete-after: True
+changes:
+ - bugfix: "Cargo packs marked as 'no private buying' now actually register as such."
diff --git a/html/changelogs/AutoChangeLog-pr-13996.yml b/html/changelogs/AutoChangeLog-pr-13996.yml
new file mode 100644
index 0000000000..5bea075460
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13996.yml
@@ -0,0 +1,4 @@
+author: "MrJWhit"
+delete-after: True
+changes:
+ - tweak: "Bugfix of a morph becoming an AI eye"
diff --git a/html/changelogs/AutoChangeLog-pr-13997.yml b/html/changelogs/AutoChangeLog-pr-13997.yml
new file mode 100644
index 0000000000..354f66010a
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-13997.yml
@@ -0,0 +1,4 @@
+author: "MrJWhit"
+delete-after: True
+changes:
+ - tweak: "Porting garbage collection tweak from /tg/"
diff --git a/html/changelogs/AutoChangeLog-pr-14001.yml b/html/changelogs/AutoChangeLog-pr-14001.yml
new file mode 100644
index 0000000000..31c9e560ab
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-14001.yml
@@ -0,0 +1,4 @@
+author: "MrJWhit"
+delete-after: True
+changes:
+ - tweak: "Fixes two chairs on one table"
diff --git a/icons/mob/animal.dmi b/icons/mob/animal.dmi
index 8db32b304b..68e6170969 100644
Binary files a/icons/mob/animal.dmi and b/icons/mob/animal.dmi differ
diff --git a/icons/mob/clothing/suit.dmi b/icons/mob/clothing/suit.dmi
index d4bb1224cc..21d2f872f1 100644
Binary files a/icons/mob/clothing/suit.dmi and b/icons/mob/clothing/suit.dmi differ
diff --git a/icons/obj/clothing/suits.dmi b/icons/obj/clothing/suits.dmi
index c12720bc8b..935e9ead02 100644
Binary files a/icons/obj/clothing/suits.dmi and b/icons/obj/clothing/suits.dmi differ
diff --git a/icons/obj/food/burgerbread.dmi b/icons/obj/food/burgerbread.dmi
index 7ef3a7d418..c5bf88ab6e 100644
Binary files a/icons/obj/food/burgerbread.dmi and b/icons/obj/food/burgerbread.dmi differ
diff --git a/icons/obj/storage.dmi b/icons/obj/storage.dmi
index 1f1709a10c..b0e542f618 100644
Binary files a/icons/obj/storage.dmi and b/icons/obj/storage.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 71abfdea8e..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"
@@ -2716,6 +2723,7 @@
#include "code\modules\mob\living\simple_animal\hostile\banana_spider.dm"
#include "code\modules\mob\living\simple_animal\hostile\bear.dm"
#include "code\modules\mob\living\simple_animal\hostile\bees.dm"
+#include "code\modules\mob\living\simple_animal\hostile\bread.dm"
#include "code\modules\mob\living\simple_animal\hostile\carp.dm"
#include "code\modules\mob\living\simple_animal\hostile\cat_butcher.dm"
#include "code\modules\mob\living\simple_animal\hostile\dark_wizard.dm"
diff --git a/tgui/packages/tgui-dev-server/package.json b/tgui/packages/tgui-dev-server/package.json
index f1ab17a264..f72ac63d1f 100644
--- a/tgui/packages/tgui-dev-server/package.json
+++ b/tgui/packages/tgui-dev-server/package.json
@@ -3,7 +3,7 @@
"name": "tgui-dev-server",
"version": "4.2.0",
"dependencies": {
- "axios": "^0.19.2",
+ "axios": "^0.21.1",
"common": "workspace:*",
"esm": "^3.2.25",
"glob": "^7.1.4",
diff --git a/tgui/packages/tgui/interfaces/Photocopier.js b/tgui/packages/tgui/interfaces/Photocopier.js
index fc79571e8c..0e461c403e 100644
--- a/tgui/packages/tgui/interfaces/Photocopier.js
+++ b/tgui/packages/tgui/interfaces/Photocopier.js
@@ -57,7 +57,6 @@ const Toner = (props, context) => {
title="Toner"
buttons={