diff --git a/code/game/objects/items/stacks/sheets/sheet_types.dm b/code/game/objects/items/stacks/sheets/sheet_types.dm
index 2a9e5c38a9d53..f835bb776256d 100644
--- a/code/game/objects/items/stacks/sheets/sheet_types.dm
+++ b/code/game/objects/items/stacks/sheets/sheet_types.dm
@@ -41,6 +41,7 @@ GLOBAL_LIST_INIT(metal_recipes, list ( \
)),
null, \
new/datum/stack_recipe("rack parts", /obj/item/rack_parts), \
+ new/datum/stack_recipe("crate shelf parts", /obj/item/rack_parts/shelf), \
new/datum/stack_recipe_list("closets", list(
new/datum/stack_recipe("closet", /obj/structure/closet, 2, time = 15, one_per_turf = TRUE, on_floor = TRUE),
new/datum/stack_recipe("emergency closet", /obj/structure/closet/emcloset/empty, 2, time = 15, one_per_turf = TRUE, on_floor = TRUE),
diff --git a/code/game/objects/structures/crates_lockers/crates.dm b/code/game/objects/structures/crates_lockers/crates.dm
index bc0d92a3d2073..e2430be999b65 100644
--- a/code/game/objects/structures/crates_lockers/crates.dm
+++ b/code/game/objects/structures/crates_lockers/crates.dm
@@ -45,11 +45,27 @@
. += "manifest"
/obj/structure/closet/crate/attack_hand(mob/user)
- . = ..()
- if(.)
- return
+ if(istype(src.loc, /obj/structure/crate_shelf))
+ return FALSE // No opening crates in shelves!!
if(manifest)
tear_manifest(user)
+ return ..()
+
+/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.body_position == LYING_DOWN)
+ 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(istype(drop_atom, /turf/open) && istype(loc, /obj/structure/crate_shelf) && user.Adjacent(drop_atom))
+ var/obj/structure/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/crate_shelf) && isturf(loc) && user.Adjacent(src))
+ var/obj/structure/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/open(mob/living/user, force = FALSE)
. = ..()
diff --git a/code/game/objects/structures/crateshelf.dm b/code/game/objects/structures/crateshelf.dm
new file mode 100644
index 0000000000000..1ede60f12e22a
--- /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 10 // Vertical pixel offset of shelving-related things. Set to 10 by default due to this leaving more of the crate on-screen to be clicked.
+
+/obj/structure/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
+ max_integrity = 50 // Not hard to break
+
+ var/capacity = DEFAULT_SHELF_CAPACITY
+ var/use_delay = DEFAULT_SHELF_USE_DELAY
+ var/list/shelf_contents
+
+/obj/structure/crate_shelf/tall
+ capacity = 12
+
+/obj/structure/crate_shelf/Initialize()
+ . = ..()
+ 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.
+ for(var/i in 1 to (capacity - 1))
+ 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.
+ overlays += image(icon = 'icons/obj/objects.dmi', icon_state = "shelf_stack", layer = stack_layer, pixel_y = stack_offset)
+ return
+
+/obj/structure/crate_shelf/Destroy()
+ QDEL_LIST(shelf_contents)
+ return ..()
+
+/obj/structure/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)
+ . += " [icon2html(crate, user)] [crate]"
+
+/obj/structure/crate_shelf/attackby(obj/item/item, mob/living/user, params)
+ if (item.tool_behaviour == TOOL_WRENCH && !(flags_1&NODECONSTRUCT_1))
+ item.play_tool_sound(src)
+ if(do_after(user, 3 SECONDS, target = src))
+ deconstruct(TRUE)
+ return TRUE
+ return ..()
+
+/obj/structure/crate_shelf/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].",
+ "[crate]'s lid falls open!")
+ else // If we somehow fail to open the crate, just break it instead!
+ crate.visible_message("[crate] falls apart!")
+ crate.deconstruct()
+ if(3) // Break that crate!
+ crate.visible_message("[crate] falls apart!")
+ crate.deconstruct()
+ shelf_contents[shelf_contents.Find(crate)] = null
+ if(!(flags_1&NODECONSTRUCT_1))
+ density = FALSE
+ var/obj/item/rack_parts/shelf/newparts = new(loc)
+ transfer_fingerprints_to(newparts)
+ return ..()
+
+/obj/item/rack_parts/shelf
+ name = "crate shelf parts"
+ desc = "Parts of a shelf."
+ construction_type = /obj/structure/crate_shelf
diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm
index 3bf44bdfc0d8c..16189291abc02 100644
--- a/code/game/objects/structures/tables_racks.dm
+++ b/code/game/objects/structures/tables_racks.dm
@@ -735,6 +735,7 @@
flags_1 = CONDUCT_1
custom_materials = list(/datum/material/iron=2000)
var/building = FALSE
+ var/obj/construction_type = /obj/structure/rack
/obj/item/rack_parts/attackby(obj/item/W, mob/user, params)
if (W.tool_behaviour == TOOL_WRENCH)
@@ -744,14 +745,17 @@
. = ..()
/obj/item/rack_parts/attack_self(mob/user)
+ if(locate(construction_type) in get_turf(user))
+ balloon_alert(user, "no room!")
+ return
if(building)
return
building = TRUE
- to_chat(user, "You start constructing a rack...")
+ to_chat(user, "You start assembling [src]...")
if(do_after(user, 50, target = user, progress=TRUE))
if(!user.temporarilyRemoveItemFromInventory(src))
return
- var/obj/structure/rack/R = new /obj/structure/rack(user.loc)
+ var/obj/structure/R = new construction_type(user.loc)
user.visible_message("[user] assembles \a [R].\
", "You assemble \a [R].")
R.add_fingerprint(user)
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index e2fb89b9cb3b6..b3305f725e0e5 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -342,7 +342,8 @@ GLOBAL_LIST_EMPTY(created_baseturf_lists)
return FALSE
//There's a lot of QDELETED() calls here if someone can figure out how to optimize this but not runtime when something gets deleted by a Bump/CanPass/Cross call, lemme know or go ahead and fix this mess - kevinz000
-/turf/Enter(atom/movable/mover, atom/oldloc)
+// Test if a movable can enter this turf. Send no_side_effects = TRUE to prevent bumping.
+/turf/Enter(atom/movable/mover, atom/oldloc, no_side_effects = FALSE)
// Do not call ..()
// Byond's default turf/Enter() doesn't have the behaviour we want with Bump()
// By default byond will call Bump() on the first dense object in contents
@@ -356,6 +357,8 @@ GLOBAL_LIST_EMPTY(created_baseturf_lists)
if(thing == mover || thing == mover.loc) // Multi tile objects and moving out of other objects
continue
if(!thing.Cross(mover))
+ if(no_side_effects)
+ return FALSE
if(QDELETED(mover)) //Mover deleted from Cross/CanPass, do not proceed.
return FALSE
if((mover.movement_type & PHASING))
diff --git a/icons/obj/objects.dmi b/icons/obj/objects.dmi
index d779a15bc717e..ff211d21d5c20 100644
Binary files a/icons/obj/objects.dmi and b/icons/obj/objects.dmi differ
diff --git a/shiptest.dme b/shiptest.dme
index 437d49302d1d7..772345db1bb20 100644
--- a/shiptest.dme
+++ b/shiptest.dme
@@ -1309,6 +1309,7 @@
#include "code\game\objects\structures\barsigns.dm"
#include "code\game\objects\structures\bedsheet_bin.dm"
#include "code\game\objects\structures\catwalk.dm"
+#include "code\game\objects\structures\crateshelf.dm"
#include "code\game\objects\structures\curtains.dm"
#include "code\game\objects\structures\destructible_structures.dm"
#include "code\game\objects\structures\displaycase.dm"