diff --git a/code/game/objects/items/stacks/stack_recipes.dm b/code/game/objects/items/stacks/stack_recipes.dm
index 3111bec60512..6c8e61b22d11 100644
--- a/code/game/objects/items/stacks/stack_recipes.dm
+++ b/code/game/objects/items/stacks/stack_recipes.dm
@@ -340,6 +340,7 @@ var/list/datum/stack_recipe/metal_recipes = list (
), 2),
new/datum/stack_recipe("table parts", /obj/item/weapon/table_parts, 2 ),
new/datum/stack_recipe("rack parts", /obj/item/weapon/rack_parts, ),
+ new/datum/stack_recipe("crate shelf parts", /obj/item/weapon/rack_parts/shelf, 3 ),
new/datum/stack_recipe("filing cabinet", /obj/structure/filingcabinet/filingcabinet, 2, one_per_turf = 1, time = 15 ),
new/datum/stack_recipe("closet", /obj/structure/closet/basic, 2, one_per_turf = 1, time = 15 ),
new/datum/stack_recipe("metal crate", /obj/structure/closet/crate/basic, 2, one_per_turf = 1, time = 15 ),
diff --git a/code/game/objects/items/weapons/table_rack_parts.dm b/code/game/objects/items/weapons/table_rack_parts.dm
index 8af26d0e3291..ad6f4ba19bc1 100644
--- a/code/game/objects/items/weapons/table_rack_parts.dm
+++ b/code/game/objects/items/weapons/table_rack_parts.dm
@@ -234,3 +234,16 @@
R.add_fingerprint(user)
user.drop_item(src, force_drop = 1)
qdel(src)
+
+/obj/item/weapon/rack_parts/shelf
+ name = "crate shelf parts"
+ desc = "Parts of a shelf."
+ icon_state = "crate_shelf_parts"
+ starting_materials = list(MAT_IRON = 11250)
+ sheet_amount = 3
+
+/obj/item/weapon/rack_parts/shelf/attack_self(mob/user)
+ var/obj/structure/rack/crate_shelf/C = new /obj/structure/rack/crate_shelf(user.loc)
+ C.add_fingerprint(user)
+ user.drop_item(src, force_drop = 1)
+ qdel(src)
diff --git a/code/game/objects/structures/crates_lockers/crates.dm b/code/game/objects/structures/crates_lockers/crates.dm
index 27381e31c521..713d1e643f6f 100644
--- a/code/game/objects/structures/crates_lockers/crates.dm
+++ b/code/game/objects/structures/crates_lockers/crates.dm
@@ -532,6 +532,8 @@
/obj/structure/closet/crate/attack_hand(var/mob/user)
if(!Adjacent(user))
return
+ if(istype(src.loc, /obj/structure/rack/crate_shelf))
+ return // No opening crates in shelves!!
add_fingerprint(user)
if(opened)
close()
@@ -560,6 +562,22 @@
else
..()
+/obj/structure/closet/crate/MouseDrop(atom/drop_atom, src_location, over_location)
+ . = ..()
+ var/mob/living/user = usr
+ if(!isliving(user))
+ return // Ghosts busted.
+ if(!isturf(user.loc) || user.incapacitated() || user.resting)
+ return // If the user is in a weird state, don't bother trying.
+ if(get_dist(drop_atom, src) != 1 || get_dist(drop_atom, user) != 1)
+ return // Check whether the crate is exactly 1 tile from the shelf and the user.
+ if(isturf(drop_atom) && istype(loc, /obj/structure/rack/crate_shelf) && user.Adjacent(drop_atom))
+ var/obj/structure/rack/crate_shelf/shelf = loc
+ return shelf.unload(src, user, drop_atom) // If we're being dropped onto a turf, and we're inside of a crate shelf, unload.
+ if(istype(drop_atom, /obj/structure/rack/crate_shelf) && isturf(loc) && user.Adjacent(src))
+ var/obj/structure/rack/crate_shelf/shelf = drop_atom
+ return shelf.load(src, user) // If we're being dropped onto a crate shelf, and we're in a turf, load.
+
/obj/structure/closet/crate/secure/proc/togglelock(atom/A)
if(istype(A,/mob))
var/mob/user = A
diff --git a/code/game/objects/structures/crateshelf.dm b/code/game/objects/structures/crateshelf.dm
new file mode 100644
index 000000000000..0dafd4208c7b
--- /dev/null
+++ b/code/game/objects/structures/crateshelf.dm
@@ -0,0 +1,139 @@
+#define DEFAULT_SHELF_CAPACITY 3 // Default capacity of the shelf
+#define DEFAULT_SHELF_USE_DELAY 1 SECONDS // Default interaction delay of the shelf
+#define DEFAULT_SHELF_VERTICAL_OFFSET 11 // Vertical pixel offset of shelving-related things.
+
+/obj/structure/rack/crate_shelf
+ name = "crate shelf"
+ desc = "It's a shelf! For storing crates!"
+ icon = 'icons/obj/objects.dmi'
+ icon_state = "shelf_base"
+ density = TRUE
+ anchored = TRUE
+ health = 50 // A bit stronger than a regular rack
+ parts = /obj/item/weapon/rack_parts/shelf
+
+ var/capacity = DEFAULT_SHELF_CAPACITY
+ var/use_delay = DEFAULT_SHELF_USE_DELAY
+ var/list/shelf_contents
+
+/obj/structure/rack/crate_shelf/tall
+ capacity = 12
+
+/obj/structure/rack/crate_shelf/New()
+ . = ..()
+ var/mutable_appearance/base = mutable_appearance(icon = 'icons/obj/objects.dmi', icon_state = "shelf_overlay", layer = BELOW_OBJ_LAYER + 0.01, plane = FLOAT_PLANE)
+ base.plane = FLOAT_PLANE
+ overlays += base
+ shelf_contents = new/list(capacity) // Initialize our shelf's contents list, this will be used later.
+ var/stack_layer // This is used to generate the sprite layering of the shelf pieces.
+ var/stack_offset // This is used to generate the vertical offset of the shelf pieces.
+ var/stack_plane = FLOAT_PLANE
+ for(var/i in 1 to (capacity - 1))
+ if(i >= 3) // If we're at or above three, we'll be on the way to going off the tile we're on. This allows mobs to be below the shelf when this happens.
+ stack_plane = HUMAN_PLANE
+ stack_layer = BELOW_OBJ_LAYER + (0.02 * i) - 0.01 // Make each shelf piece render above the last, but below the crate that should be on it.
+ stack_offset = DEFAULT_SHELF_VERTICAL_OFFSET * i // Make each shelf piece physically above the last.
+ var/mutable_appearance/nextshelf = mutable_appearance(icon = 'icons/obj/objects.dmi', icon_state = "shelf_stack", layer = stack_layer, plane = stack_plane)
+ stack_layer += 0.2
+ nextshelf.pixel_y = stack_offset
+ overlays += nextshelf
+ var/mutable_appearance/nextshelf_olay = mutable_appearance(icon = 'icons/obj/objects.dmi', icon_state = "shelf_overlay", layer = stack_layer, plane = stack_plane)
+ nextshelf_olay.pixel_y = stack_offset
+ overlays += nextshelf_olay
+ return
+
+/obj/structure/rack/crate_shelf/Destroy()
+ QDEL_LIST(shelf_contents)
+ return ..()
+
+/obj/structure/rack/crate_shelf/examine(mob/user)
+ . = ..()
+ . += "There are some bolts holding [src] together."
+ if(shelf_contents.Find(null)) // If there's an empty space in the shelf, let the examiner know.
+ . += "You could drag a crate into [src]."
+ if(contents.len) // If there are any crates in the shelf, let the examiner know.
+ . += "You could drag a crate out of [src]."
+ . += "[src] contains:"
+ for(var/obj/structure/closet/crate/crate in shelf_contents)
+ . += "[crate]"
+
+/obj/structure/rack/crate_shelf/attackby(obj/item/weapon/W as obj, mob/living/user, params)
+ if(W.is_wrench(user) && can_disassemble())
+ W.playtoolsound(src, 50)
+ destroy(TRUE)
+
+/obj/structure/rack/crate_shelf/proc/relay_container_resist_act(mob/living/user, obj/structure/closet/crate)
+ to_chat(user, "You begin attempting to knock [crate] out of [src].")
+ if(do_after(user, 30 SECONDS, target = crate))
+ if(!user || user.stat != CONSCIOUS || user.loc != crate || crate.loc != src)
+ return // If the user is in a strange condition, return early.
+ visible_message("[crate] falls off of [src]!",
+ "You manage to knock [crate] free of [src].",
+ "\The [src] is full!")
+ return FALSE
+ if(do_after(user, use_delay, target = crate))
+ if(shelf_contents[next_free] != null)
+ return FALSE // Something has been added to the shelf while we were waiting, abort!
+ if(crate.opened) // If the crate is open, try to close it.
+ if(!crate.close())
+ return FALSE // If we fail to close it, don't load it into the shelf.
+ shelf_contents[next_free] = crate // Insert a reference to the crate into the free slot.
+ crate.forceMove(src) // Insert the crate into the shelf.
+ crate.pixel_y = DEFAULT_SHELF_VERTICAL_OFFSET * (next_free - 1) // Adjust the vertical offset of the crate to look like it's on the shelf.
+ crate.plane = FLOAT_PLANE
+ if(next_free >= 3) // If we're at or above three, we'll be on the way to going off the tile we're on. This allows mobs to be below the crate when this happens.
+ crate.plane = HUMAN_PLANE
+ crate.layer = BELOW_OBJ_LAYER + 0.02 * (next_free - 1) // Adjust the layer of the crate to look like it's in the shelf.
+ handle_visuals()
+ return TRUE
+ return FALSE // If the do_after() is interrupted, return FALSE!
+
+/obj/structure/rack/crate_shelf/proc/unload(obj/structure/closet/crate/crate, mob/user, turf/unload_turf)
+ if(!unload_turf)
+ unload_turf = get_turf(user) // If a turf somehow isn't passed into the proc, put it at the user's feet.
+ if(unload_turf.density)
+ return
+ if(locate(/obj/structure/closet/crate) in unload_turf)
+ to_chat(user,"There is already a crate here.")
+ return
+ if(do_after(user, use_delay, target = crate))
+ if(!shelf_contents.Find(crate))
+ return FALSE // If something has happened to the crate while we were waiting, abort!
+ crate.plane = initial(crate.plane)
+ crate.layer = initial(crate.layer) // Reset the crate back to having the default layer, otherwise we might get strange interactions.
+ crate.pixel_y = initial(crate.pixel_y) // Reset the crate back to having no offset, otherwise it will be floating.
+ crate.forceMove(unload_turf)
+ shelf_contents[shelf_contents.Find(crate)] = null // We do this instead of removing it from the list to preserve the order of the shelf.
+ handle_visuals()
+ return TRUE
+ return FALSE // If the do_after() is interrupted, return FALSE!
+
+/obj/structure/rack/crate_shelf/destroy(dropParts = TRUE)
+ var/turf/dump_turf = get_turf(src)
+ for(var/obj/structure/closet/crate/crate in shelf_contents)
+ crate.plane = initial(crate.plane)
+ crate.layer = initial(crate.layer) // Reset the crates back to default visual state
+ crate.pixel_y = initial(crate.pixel_y)
+ crate.forceMove(dump_turf)
+ step(crate, pick(cardinal)) // Shuffle the crates around as though they've fallen down.
+ if(prob(5)) // Open the crate!
+ if(crate.open())
+ crate.visible_message("[crate]'s lid falls open!")
+ shelf_contents[shelf_contents.Find(crate)] = null
+ return ..()
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 5a188fb4fe90..fbc94d855f6e 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -1017,6 +1017,11 @@ Thanks.
var/obj/structure/closet/C = L.loc
if(C.opened)
return //Door's open... wait, why are you in it's contents then?
+ if(istype(C.loc, /obj/structure/rack/crate_shelf) && istype(C,/obj/structure/closet/crate))
+ var/obj/structure/closet/crate/R = C
+ var/obj/structure/rack/crate_shelf/CS = C.loc
+ CS.relay_container_resist_act(src,R)
+ return
if(!istype(C.loc, /obj/item/delivery/large)) //Wouldn't want to interrupt escaping being wrapped over the next few trivial checks
if(istype(C, /obj/structure/closet/secure_closet))
var/obj/structure/closet/secure_closet/SC = L.loc
diff --git a/icons/obj/items.dmi b/icons/obj/items.dmi
index 618059aba3f6..46c0a16fc211 100644
Binary files a/icons/obj/items.dmi and b/icons/obj/items.dmi differ
diff --git a/icons/obj/objects.dmi b/icons/obj/objects.dmi
index 8cc9e4ab5179..b2a1f9ad027d 100644
Binary files a/icons/obj/objects.dmi and b/icons/obj/objects.dmi differ
diff --git a/vgstation13.dme b/vgstation13.dme
index 6e590e96921b..d0d09a6264c3 100644
--- a/vgstation13.dme
+++ b/vgstation13.dme
@@ -1243,6 +1243,7 @@
#include "code\game\objects\structures\catwalk.dm"
#include "code\game\objects\structures\clock.dm"
#include "code\game\objects\structures\coatrack.dm"
+#include "code\game\objects\structures\crateshelf.dm"
#include "code\game\objects\structures\curtains.dm"
#include "code\game\objects\structures\displaycase.dm"
#include "code\game\objects\structures\docking_port.dm"