diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
index dafb5f834a1..c3d0f0dc4c8 100644
--- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm
+++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
@@ -53711,6 +53711,7 @@
},
/obj/effect/turf_decal/delivery,
/obj/machinery/bluespace_vendor/north,
+/obj/effect/mapping_helpers/trapdoor_placer,
/turf/open/floor/iron,
/area/hallway/primary/central)
"vHp" = (
@@ -54657,7 +54658,14 @@
/area/security/brig)
"wit" = (
/obj/structure/table,
-/obj/machinery/recharger,
+/obj/machinery/recharger{
+ pixel_x = 5;
+ pixel_y = 2
+ },
+/obj/item/trapdoor_remote/preloaded{
+ pixel_x = -5;
+ pixel_y = 2
+ },
/turf/open/floor/iron,
/area/command/heads_quarters/hop)
"wiA" = (
diff --git a/_maps/map_files/IceBoxStation/IcemoonUnderground_Above.dmm b/_maps/map_files/IceBoxStation/IcemoonUnderground_Above.dmm
index 0235d0465bb..b5f4a6d5d37 100644
--- a/_maps/map_files/IceBoxStation/IcemoonUnderground_Above.dmm
+++ b/_maps/map_files/IceBoxStation/IcemoonUnderground_Above.dmm
@@ -2916,6 +2916,9 @@
/obj/effect/spawner/structure/window/reinforced,
/turf/open/floor/plating,
/area/mine/laborcamp/security)
+"sT" = (
+/turf/open/floor/plating/snowed/smoothed/icemoon,
+/area/mine/storage)
"sX" = (
/obj/structure/rack,
/obj/effect/spawner/lootdrop/maintenance/two,
@@ -32018,9 +32021,9 @@ ak
ak
ak
ak
-ak
-ak
-ak
+AF
+sT
+AF
ak
ak
ak
@@ -32275,9 +32278,9 @@ ak
ak
ak
ak
-ak
-ak
-ak
+sT
+NH
+sT
ak
ak
ak
@@ -32532,9 +32535,9 @@ ak
ak
ak
ak
-ak
-ak
-ak
+AF
+sT
+AF
ak
ak
ak
@@ -32789,9 +32792,9 @@ ak
ak
ak
ak
-ak
-ak
-ak
+NH
+NH
+NH
ak
ak
ak
@@ -33045,10 +33048,10 @@ ak
ak
ak
ak
-ak
-ak
-ak
-ak
+NH
+NH
+NH
+NH
ak
ak
ak
@@ -33302,10 +33305,10 @@ ak
ak
ak
ak
-ak
-ak
-ak
-ak
+NH
+NH
+NH
+NH
ak
ak
ak
@@ -33558,12 +33561,12 @@ ak
ak
ak
ak
-ak
-ak
-ak
-ak
-ak
-ak
+NH
+NH
+NH
+NH
+NH
+NH
ak
ak
ak
@@ -33818,9 +33821,9 @@ NH
NH
NH
NH
-ak
-ak
-ak
+NH
+NH
+NH
ak
ak
ak
@@ -34076,8 +34079,8 @@ NH
NH
NH
NH
-ak
-ak
+NH
+NH
ak
ak
ak
diff --git a/_maps/map_files/tramstation/tramstation.dmm b/_maps/map_files/tramstation/tramstation.dmm
index 0c82f2a37eb..ca8c20383ce 100644
--- a/_maps/map_files/tramstation/tramstation.dmm
+++ b/_maps/map_files/tramstation/tramstation.dmm
@@ -9034,6 +9034,9 @@
pixel_y = -34;
req_access_txt = "28"
},
+/obj/item/trapdoor_remote/preloaded{
+ pixel_x = -8
+ },
/turf/open/floor/wood,
/area/command/heads_quarters/hop)
"aCS" = (
@@ -42336,6 +42339,7 @@
"mjK" = (
/obj/effect/turf_decal/delivery,
/obj/effect/landmark/start/hangover,
+/obj/effect/mapping_helpers/trapdoor_placer,
/turf/open/floor/iron,
/area/hallway/secondary/command)
"mjQ" = (
diff --git a/code/__DEFINES/combat.dm b/code/__DEFINES/combat.dm
index 4348653babb..a8c59583256 100644
--- a/code/__DEFINES/combat.dm
+++ b/code/__DEFINES/combat.dm
@@ -129,6 +129,7 @@
#define ATTACK_EFFECT_BOOP "boop" //Honk
//the define for visible message range in combat
+#define SAMETILE_MESSAGE_RANGE 1
#define COMBAT_MESSAGE_RANGE 3
#define DEFAULT_MESSAGE_RANGE 7
diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 6288faf0c7d..248772f6cba 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -34,6 +34,10 @@
#define CANCEL_PRE_RANDOM_EVENT (1<<0)
/// a person somewhere has thrown something : (mob/living/carbon/carbon_thrower, target)
#define COMSIG_GLOB_CARBON_THROW_THING "!throw_thing"
+/// a trapdoor remote has sent out a signal to link with a trapdoor
+#define COMSIG_GLOB_TRAPDOOR_LINK "!trapdoor_link"
+ ///successfully linked to a trapdoor!
+ #define LINKED_UP (1<<0)
/// an obj/item is created! (obj/item/created_item)
#define COMSIG_GLOB_NEW_ITEM "!new_item"
@@ -382,6 +386,9 @@
///from /turf/open/temperature_expose(datum/gas_mixture/air, exposed_temperature)
#define COMSIG_TURF_EXPOSE "turf_expose"
+///from /datum/element/decal/Detach(): (description, cleanable, directional, mutable_appearance/pic)
+#define COMSIG_TURF_DECAL_DETACHED "turf_decal_detached"
+
// /atom/movable signals
///from base of atom/movable/Moved(): (/atom)
diff --git a/code/datums/components/trapdoor.dm b/code/datums/components/trapdoor.dm
new file mode 100644
index 00000000000..8ab65607b07
--- /dev/null
+++ b/code/datums/components/trapdoor.dm
@@ -0,0 +1,269 @@
+
+///makes this file more legible
+#define IS_OPEN(parent) isgroundlessturf(parent)
+///distance a trapdoor will accept a link request.
+#define TRAPDOOR_LINKING_SEARCH_RANGE 7
+
+/**
+ * ## trapdoor component!
+ *
+ * component attached to floors to turn them into trapdoors, a constructable trap that when signalled drops people to the level below.
+ * assembly code at the bottom of this file
+ */
+/datum/component/trapdoor
+ ///assembly tied to this trapdoor
+ var/obj/item/assembly/trapdoor/assembly
+ ///path of the turf this should change into when the assembly is pulsed. needed for openspace trapdoors knowing what to turn back into
+ var/trapdoor_turf_path
+
+/datum/component/trapdoor/Initialize(starts_open, trapdoor_turf_path, assembly)
+ if(!isopenturf(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.assembly = assembly
+ if(IS_OPEN(parent))
+ openspace_trapdoor_setup(trapdoor_turf_path, assembly)
+ else
+ tile_trapdoor_setup(trapdoor_turf_path, assembly)
+
+ if(starts_open)
+ try_opening()
+
+///initializing as an opened trapdoor, we need to trust that we were given the data by a closed trapdoor
+/datum/component/trapdoor/proc/openspace_trapdoor_setup(trapdoor_turf_path)
+ src.trapdoor_turf_path = trapdoor_turf_path
+
+///initializing as a closed trapdoor, we need to take data from the tile we're on to give it to the open state to store
+/datum/component/trapdoor/proc/tile_trapdoor_setup(trapdoor_turf_path)
+ src.trapdoor_turf_path = parent.type
+ if(assembly && assembly.stored_decals.len)
+ reapply_all_decals()
+
+/datum/component/trapdoor/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_TURF_CHANGE, .proc/turf_changed_pre)
+ if(!src.assembly)
+ RegisterSignal(SSdcs, COMSIG_GLOB_TRAPDOOR_LINK, .proc/on_link_requested)
+ else
+ RegisterSignal(assembly, COMSIG_ASSEMBLY_PULSED, .proc/toggle_trapdoor)
+
+/datum/component/trapdoor/UnregisterFromParent()
+ . = ..()
+ UnregisterSignal(SSdcs, COMSIG_GLOB_TRAPDOOR_LINK)
+ UnregisterSignal(assembly, COMSIG_ASSEMBLY_PULSED)
+ UnregisterSignal(parent, COMSIG_TURF_CHANGE)
+
+/datum/component/trapdoor/proc/decal_detached(datum/source, description, cleanable, directional, pic)
+ SIGNAL_HANDLER
+ ///so it adds the list to the list, not appending it to the end. thank you byond, very cool.
+ assembly.stored_decals += list(list(description, cleanable, directional, pic))
+
+/**
+ * ## reapply_all_decals
+ *
+ * changing turfs does not bring over decals, so we must perform a little bit of element reapplication.
+ */
+/datum/component/trapdoor/proc/reapply_all_decals()
+ for(var/list/element_data as anything in assembly.stored_decals)
+ apply_decal(element_data[1], element_data[2], element_data[3], element_data[4])
+ assembly.stored_decals = list()
+
+/// small proc that takes passed arguments and drops it into a new element
+/datum/component/trapdoor/proc/apply_decal(description, cleanable, directional, pic)
+ parent.AddElement(/datum/element/decal, _description = description, _cleanable = cleanable, _dir = directional, _pic = pic)
+
+///called by linking remotes to tie an assembly to the trapdoor
+/datum/component/trapdoor/proc/on_link_requested(datum/source, obj/item/assembly/trapdoor/assembly)
+ if(get_dist(parent, assembly) > TRAPDOOR_LINKING_SEARCH_RANGE)
+ return
+ . = LINKED_UP
+ src.assembly = assembly
+ assembly.linked = TRUE
+ UnregisterSignal(SSdcs, COMSIG_GLOB_TRAPDOOR_LINK)
+ RegisterSignal(assembly, COMSIG_ASSEMBLY_PULSED, .proc/toggle_trapdoor)
+
+///signal called by our assembly being pulsed
+/datum/component/trapdoor/proc/toggle_trapdoor(datum/source)
+ SIGNAL_HANDLER
+ if(!IS_OPEN(parent))
+ try_opening()
+ else
+ try_closing()
+
+///signal called by turf changing
+/datum/component/trapdoor/proc/turf_changed_pre(datum/source, path, new_baseturfs, flags, post_change_callbacks)
+ SIGNAL_HANDLER
+ var/turf/open/dying_trapdoor = parent
+ if((!IS_OPEN(dying_trapdoor) && !IS_OPEN(path)) || path == /turf/open/floor/plating) //not a process of the trapdoor, so this trapdoor has been destroyed
+ dying_trapdoor.visible_message("The trapdoor mechanism in [dying_trapdoor] is broken!")
+ if(assembly)
+ assembly.linked = FALSE
+ assembly.stored_decals.Cut()
+ assembly = null
+ return
+ post_change_callbacks += CALLBACK(assembly, /obj/item/assembly/trapdoor.proc/carry_over_trapdoor, trapdoor_turf_path)
+
+/**
+ * ## carry_over_trapdoor
+ *
+ * applies the trapdoor to the new turf (created by the last trapdoor)
+ * apparently callbacks with arguments on invoke and the callback itself have the callback args go first. interesting!
+ */
+/obj/item/assembly/trapdoor/proc/carry_over_trapdoor(trapdoor_turf_path, turf/new_turf)
+ new_turf.AddComponent(/datum/component/trapdoor, FALSE, trapdoor_turf_path, src)
+
+/**
+ * ## try_opening
+ *
+ * small proc for opening the turf into openspace
+ * there are no checks for opening a trapdoor, but closed has some
+ */
+/datum/component/trapdoor/proc/try_opening()
+ var/turf/open/trapdoor_turf = parent
+ ///we want to save this turf's decals as they were right before deletion, so this is the point where we begin listening
+ if(assembly)
+ RegisterSignal(parent, COMSIG_TURF_DECAL_DETACHED, .proc/decal_detached)
+ playsound(trapdoor_turf, 'sound/machines/trapdoor/trapdoor_open.ogg', 50)
+ trapdoor_turf.visible_message("[trapdoor_turf] swings open!")
+ trapdoor_turf.ChangeTurf(/turf/open/openspace, flags = CHANGETURF_INHERIT_AIR)
+
+/**
+ * ## try_closing
+ *
+ * small proc for closing the turf back into what it should be
+ * trapdoor can be blocked by building things on the openspace turf
+ */
+/datum/component/trapdoor/proc/try_closing()
+ var/turf/open/trapdoor_turf = parent
+ var/obj/structure/lattice/blocking = locate() in trapdoor_turf.contents
+ if(blocking)
+ trapdoor_turf.visible_message("The trapdoor mechanism in [trapdoor_turf] tries to shut, but is jammed by [blocking]!")
+ return
+ playsound(trapdoor_turf, 'sound/machines/trapdoor/trapdoor_shut.ogg', 50)
+ trapdoor_turf.visible_message("The trapdoor mechanism in [trapdoor_turf] swings shut!")
+ trapdoor_turf.ChangeTurf(trapdoor_turf_path, flags = CHANGETURF_INHERIT_AIR)
+
+#undef IS_OPEN
+
+/obj/item/assembly/trapdoor
+ name = "trapdoor controller"
+ desc = "A sinister-looking controller for a trapdoor."
+ icon_state = "trapdoor"
+ ///if the trapdoor isn't linked it will try to link on pulse, this shouldn't be spammable
+ COOLDOWN_DECLARE(search_cooldown)
+ ///trapdoor link cooldown time here!
+ var/search_cooldown_time = 10 SECONDS
+ ///if true, a trapdoor in the world has a reference to this assembly and is listening for when it is pulsed.
+ var/linked = FALSE
+ /**
+ * list of lists that are arguments for readding decals when the linked trapdoor comes back. pain.
+ *
+ * we are storing this data FOR the trapdoor component we are linked to. kinda like a multitool.
+ * format: list(list(element's description, element's cleanable, element's directional, element's pic))
+ * the list will be filled with all the data of the deleting elements (when ChangeTurf is called) only when the trapdoor begins to open.
+ * so any other case the elements will be changed but not recorded.
+ */
+ var/list/stored_decals = list()
+
+
+/obj/item/assembly/trapdoor/pulsed(radio)
+ . = ..()
+ if(linked)
+ return
+ if(!COOLDOWN_FINISHED(src, search_cooldown))
+ visible_message("[src] cannot attempt another trapdoor linkup so soon!")
+ return
+ attempt_link_up()
+ COOLDOWN_START(src, search_cooldown, search_cooldown_time)
+
+/obj/item/assembly/trapdoor/proc/attempt_link_up()
+ var/turf/assembly_turf = get_turf(src)
+ if(!COOLDOWN_FINISHED(src, search_cooldown))
+ var/timeleft = DisplayTimeText(COOLDOWN_TIMELEFT(src, search_cooldown))
+ assembly_turf.visible_message("[src] is on cooldown! Please wait [timeleft].", vision_distance = SAMETILE_MESSAGE_RANGE)
+ return
+ if(SEND_GLOBAL_SIGNAL(COMSIG_GLOB_TRAPDOOR_LINK, src) & LINKED_UP)
+ playsound(assembly_turf, 'sound/machines/chime.ogg', 50, TRUE)
+ assembly_turf.visible_message("[src] has linked up to a nearby trapdoor! \
+ You may now use it to check where the trapdoor is... be careful!", vision_distance = SAMETILE_MESSAGE_RANGE)
+ else
+ playsound(assembly_turf, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
+ assembly_turf.visible_message("[src] has failed to find a trapdoor nearby to link to.", vision_distance = SAMETILE_MESSAGE_RANGE)
+
+/**
+ * ## trapdoor remotes!
+ *
+ * Item that accepts the assembly for the internals and helps link/activate it.
+ * This base type is an empty shell that needs the assembly added to it first to work.
+ */
+/obj/item/trapdoor_remote
+ name = "trapdoor remote"
+ desc = "A small machine that interfaces with a trapdoor controller for easy use."
+ icon = 'icons/obj/device.dmi'
+ icon_state = "trapdoor_remote"
+ COOLDOWN_DECLARE(trapdoor_cooldown)
+ var/trapdoor_cooldown_time = 2 SECONDS
+ var/obj/item/assembly/trapdoor/internals
+
+/obj/item/trapdoor_remote/examine(mob/user)
+ . = ..()
+ if(!internals)
+ . += "[src] has no internals! It needs a trapdoor controller to function."
+ return
+ . += "The internals can be removed with a screwdriver."
+ if(!internals.linked)
+ . += "[src] is not linked to a trapdoor."
+ return
+ . += "[src] is linked to a trapdoor."
+ if(!COOLDOWN_FINISHED(src, trapdoor_cooldown))
+ . += "It is on a short cooldown."
+
+/obj/item/trapdoor_remote/screwdriver_act(mob/living/user, obj/item/tool)
+ . = ..()
+ if(!internals)
+ to_chat(user, "[src] has no internals!")
+ return
+ to_chat(user, "You pop [internals] out of [src].")
+ internals.forceMove(get_turf(src))
+ internals = null
+
+/obj/item/trapdoor_remote/attackby(obj/item/assembly/trapdoor/assembly, mob/living/user, params)
+ . = ..()
+ if(. || !istype(assembly))
+ return
+ if(internals)
+ to_chat(user, "[src] already has internals!")
+ return
+ to_chat(user, "You add [assembly] to [src].")
+ internals = assembly
+ assembly.forceMove(src)
+
+/obj/item/trapdoor_remote/attack_self(mob/user, modifiers)
+ . = ..()
+ if(.)
+ return
+ if(!internals)
+ to_chat(user, "[src] has no internals!")
+ return
+ if(!internals.linked)
+ to_chat(user, "You activate [src].")
+ internals.pulsed()
+ return
+ if(!COOLDOWN_FINISHED(src, trapdoor_cooldown))
+ to_chat(user, "[src] is on a short cooldown.")
+ return
+ to_chat(user, "You activate [src].")
+ playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
+ icon_state = "trapdoor_pressed"
+ addtimer(VARSET_CALLBACK(src, icon_state, initial(icon_state)), trapdoor_cooldown_time)
+ COOLDOWN_START(src, trapdoor_cooldown, trapdoor_cooldown_time)
+ internals.pulsed()
+
+#undef TRAPDOOR_LINKING_SEARCH_RANGE
+
+///subtype with internals already included. If you're giving a department a roundstart trapdoor, this is what you want
+/obj/item/trapdoor_remote/preloaded
+
+/obj/item/trapdoor_remote/preloaded/Initialize()
+ . = ..()
+ internals = new(src)
diff --git a/code/datums/elements/decal.dm b/code/datums/elements/decal.dm
index 500a0264267..cce7ddfd188 100644
--- a/code/datums/elements/decal.dm
+++ b/code/datums/elements/decal.dm
@@ -59,10 +59,12 @@
/datum/element/decal/Attach(atom/target, _icon, _icon_state, _dir, _cleanable=FALSE, _color, _layer=TURF_LAYER, _description, _alpha=255, mutable_appearance/_pic)
. = ..()
- if(!isatom(target) || !generate_appearance(_icon, _icon_state, _dir, _layer, _color, _alpha, target))
+ if(!isatom(target))
return ELEMENT_INCOMPATIBLE
if(_pic)
pic = _pic
+ else if(!generate_appearance(_icon, _icon_state, _dir, _layer, _color, _alpha, target))
+ return ELEMENT_INCOMPATIBLE
description = _description
cleanable = _cleanable
directional = _dir
@@ -83,6 +85,14 @@
RegisterSignal(target, COMSIG_TURF_ON_SHUTTLE_MOVE, .proc/shuttle_move_react,TRUE)
+/**
+ * ## generate_appearance
+ *
+ * If the decal was not given an appearance, it will generate one based on the other given arguments.
+ * element won't be compatible if it cannot do either
+ * all args are fed into creating an image, they are byond vars for images you'll recognize in the byond docs
+ * (except source, source is the object whose appearance we're copying.)
+ */
/datum/element/decal/proc/generate_appearance(_icon, _icon_state, _dir, _layer, _color, _alpha, source)
if(!_icon || !_icon_state)
return FALSE
@@ -97,6 +107,7 @@
source.update_appearance()
if(isitem(source))
INVOKE_ASYNC(source, /obj/item/.proc/update_slot_icon)
+ SEND_SIGNAL(source, COMSIG_TURF_DECAL_DETACHED, description, cleanable, directional, pic)
return ..()
/datum/element/decal/proc/late_update_icon(atom/source)
diff --git a/code/game/objects/effects/temporary_visuals/temporary_visual.dm b/code/game/objects/effects/temporary_visuals/temporary_visual.dm
index ee6ef355dd0..a4a6fcf9d91 100644
--- a/code/game/objects/effects/temporary_visuals/temporary_visual.dm
+++ b/code/game/objects/effects/temporary_visuals/temporary_visual.dm
@@ -1,11 +1,14 @@
-//temporary visual effects
+///temporary visual effects
/obj/effect/temp_visual
icon_state = "nothing"
anchored = TRUE
layer = ABOVE_MOB_LAYER
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
- var/duration = 10 //in deciseconds
+ ///time, in deciseconds, that this object will exist
+ var/duration = 10
+ ///if true, will pick a random direction when created.
var/randomdir = TRUE
+ ///id of the deletion timer
var/timerid
/obj/effect/temp_visual/Initialize()
diff --git a/code/modules/mapping/mapping_helpers.dm b/code/modules/mapping/mapping_helpers.dm
index 6a6b45879bc..5786007f64a 100644
--- a/code/modules/mapping/mapping_helpers.dm
+++ b/code/modules/mapping/mapping_helpers.dm
@@ -490,3 +490,21 @@ INITIALIZE_IMMEDIATE(/obj/effect/mapping_helpers/no_lava)
icon_cache[url] = I
query_in_progress = FALSE
return I
+
+/**
+ * ## trapdoor placer!
+ *
+ * This places an unlinked trapdoor in the tile its on (so someone with a remote needs to link it up first)
+ * Admins may spawn this in the round for additional trapdoors if they so desire
+ * if YOU want to learn more about trapdoors, read about the component at trapdoor.dm
+ * note: this is not a turf subtype because the trapdoor needs the type of the turf to turn back into
+ */
+/obj/effect/mapping_helpers/trapdoor_placer
+ name = "trapdoor placer"
+ late = TRUE
+ icon_state = "trapdoor"
+
+/obj/effect/mapping_helpers/trapdoor_placer/LateInitialize()
+ var/turf/component_target = get_turf(src)
+ component_target.AddComponent(/datum/component/trapdoor, starts_open = FALSE)
+ qdel(src)
diff --git a/icons/effects/mapping_helpers.dmi b/icons/effects/mapping_helpers.dmi
index e141a97dbd2..b4f0d5d11f0 100644
Binary files a/icons/effects/mapping_helpers.dmi and b/icons/effects/mapping_helpers.dmi differ
diff --git a/icons/obj/assemblies/new_assemblies.dmi b/icons/obj/assemblies/new_assemblies.dmi
index ebbf78692a5..469d4d16d57 100644
Binary files a/icons/obj/assemblies/new_assemblies.dmi and b/icons/obj/assemblies/new_assemblies.dmi differ
diff --git a/icons/obj/device.dmi b/icons/obj/device.dmi
index 85936c1e34a..7b70285723b 100644
Binary files a/icons/obj/device.dmi and b/icons/obj/device.dmi differ
diff --git a/sound/machines/trapdoor/trapdoor_open.ogg b/sound/machines/trapdoor/trapdoor_open.ogg
new file mode 100644
index 00000000000..f64212b9117
Binary files /dev/null and b/sound/machines/trapdoor/trapdoor_open.ogg differ
diff --git a/sound/machines/trapdoor/trapdoor_shut.ogg b/sound/machines/trapdoor/trapdoor_shut.ogg
new file mode 100644
index 00000000000..21021c5f8c1
Binary files /dev/null and b/sound/machines/trapdoor/trapdoor_shut.ogg differ
diff --git a/tgstation.dme b/tgstation.dme
index a84c421c03b..c27e000c821 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -567,6 +567,7 @@
#include "code\datums\components\technoshy.dm"
#include "code\datums\components\tether.dm"
#include "code\datums\components\thermite.dm"
+#include "code\datums\components\trapdoor.dm"
#include "code\datums\components\twohanded.dm"
#include "code\datums\components\udder.dm"
#include "code\datums\components\uplink.dm"