diff --git a/code/ATMOSPHERICS/atmospherics.dm b/code/ATMOSPHERICS/atmospherics.dm
index 7acf465b3e..b5d4df814e 100644
--- a/code/ATMOSPHERICS/atmospherics.dm
+++ b/code/ATMOSPHERICS/atmospherics.dm
@@ -29,6 +29,8 @@ Pipelines + Other Objects -> Pipe network
var/pipe_color
var/global/datum/pipe_icon_manager/icon_manager
+ var/obj/machinery/atmospherics/node1
+ var/obj/machinery/atmospherics/node2
/obj/machinery/atmospherics/New()
if(!icon_manager)
diff --git a/code/ATMOSPHERICS/components/binary_devices/binary_atmos_base.dm b/code/ATMOSPHERICS/components/binary_devices/binary_atmos_base.dm
index 37985034b1..2ccfe0f2e8 100644
--- a/code/ATMOSPHERICS/components/binary_devices/binary_atmos_base.dm
+++ b/code/ATMOSPHERICS/components/binary_devices/binary_atmos_base.dm
@@ -6,9 +6,6 @@ obj/machinery/atmospherics/binary
var/datum/gas_mixture/air1
var/datum/gas_mixture/air2
- var/obj/machinery/atmospherics/node1
- var/obj/machinery/atmospherics/node2
-
var/datum/pipe_network/network1
var/datum/pipe_network/network2
diff --git a/code/ATMOSPHERICS/components/binary_devices/pipeturbine.dm b/code/ATMOSPHERICS/components/binary_devices/pipeturbine.dm
index 0eb342479d..5768def1ec 100644
--- a/code/ATMOSPHERICS/components/binary_devices/pipeturbine.dm
+++ b/code/ATMOSPHERICS/components/binary_devices/pipeturbine.dm
@@ -17,9 +17,6 @@
var/dP = 0
- var/obj/machinery/atmospherics/node1
- var/obj/machinery/atmospherics/node2
-
var/datum/pipe_network/network1
var/datum/pipe_network/network2
diff --git a/code/ATMOSPHERICS/components/trinary_devices/trinary_base.dm b/code/ATMOSPHERICS/components/trinary_devices/trinary_base.dm
index 3f5d66f262..fa066d978f 100644
--- a/code/ATMOSPHERICS/components/trinary_devices/trinary_base.dm
+++ b/code/ATMOSPHERICS/components/trinary_devices/trinary_base.dm
@@ -7,8 +7,6 @@ obj/machinery/atmospherics/trinary
var/datum/gas_mixture/air2
var/datum/gas_mixture/air3
- var/obj/machinery/atmospherics/node1
- var/obj/machinery/atmospherics/node2
var/obj/machinery/atmospherics/node3
var/datum/pipe_network/network1
diff --git a/code/ATMOSPHERICS/components/tvalve.dm b/code/ATMOSPHERICS/components/tvalve.dm
index 963a4f5009..f93970a93f 100644
--- a/code/ATMOSPHERICS/components/tvalve.dm
+++ b/code/ATMOSPHERICS/components/tvalve.dm
@@ -12,8 +12,6 @@
var/state = 0 // 0 = go straight, 1 = go to side
// like a trinary component, node1 is input, node2 is side output, node3 is straight output
- var/obj/machinery/atmospherics/node1
- var/obj/machinery/atmospherics/node2
var/obj/machinery/atmospherics/node3
var/datum/pipe_network/network_node1
diff --git a/code/ATMOSPHERICS/components/valve.dm b/code/ATMOSPHERICS/components/valve.dm
index 6e43afd78e..8969523db4 100644
--- a/code/ATMOSPHERICS/components/valve.dm
+++ b/code/ATMOSPHERICS/components/valve.dm
@@ -12,8 +12,6 @@
var/open = 0
var/openDuringInit = 0
- var/obj/machinery/atmospherics/node1
- var/obj/machinery/atmospherics/node2
var/datum/pipe_network/network_node1
var/datum/pipe_network/network_node2
diff --git a/code/ATMOSPHERICS/pipes.dm b/code/ATMOSPHERICS/pipes.dm
index c445988c53..105aaa8c8b 100644
--- a/code/ATMOSPHERICS/pipes.dm
+++ b/code/ATMOSPHERICS/pipes.dm
@@ -153,9 +153,6 @@
dir = SOUTH
initialize_directions = SOUTH|NORTH
- var/obj/machinery/atmospherics/node1
- var/obj/machinery/atmospherics/node2
-
var/minimum_temperature_difference = 300
var/thermal_conductivity = 0 //WALL_HEAT_TRANSFER_COEFFICIENT No
@@ -427,8 +424,6 @@
dir = SOUTH
initialize_directions = EAST|NORTH|WEST
- var/obj/machinery/atmospherics/node1
- var/obj/machinery/atmospherics/node2
var/obj/machinery/atmospherics/node3
level = 1
@@ -687,8 +682,6 @@
dir = SOUTH
initialize_directions = NORTH|SOUTH|EAST|WEST
- var/obj/machinery/atmospherics/node1
- var/obj/machinery/atmospherics/node2
var/obj/machinery/atmospherics/node3
var/obj/machinery/atmospherics/node4
@@ -1074,8 +1067,6 @@
initialize_directions = SOUTH
density = 1
- var/obj/machinery/atmospherics/node1
-
/obj/machinery/atmospherics/pipe/tank/New()
icon_state = "air"
initialize_directions = dir
@@ -1238,8 +1229,6 @@
var/build_killswitch = 1
- var/obj/machinery/atmospherics/node1
-
/obj/machinery/atmospherics/pipe/vent/New()
initialize_directions = dir
..()
diff --git a/code/_macros.dm b/code/_macros.dm
index 06376205d1..f03b83d4a0 100644
--- a/code/_macros.dm
+++ b/code/_macros.dm
@@ -42,3 +42,5 @@
#define isxeno(A) istype(A, /mob/living/simple_animal/xeno)
#define RANDOM_BLOOD_TYPE pick(4;"O-", 36;"O+", 3;"A-", 28;"A+", 1;"B-", 20;"B+", 1;"AB-", 5;"AB+")
+
+#define to_chat(target, message) target << message
\ No newline at end of file
diff --git a/code/modules/mob/living/carbon/metroid/metroid.dm b/code/modules/mob/living/carbon/metroid/metroid.dm
index b7a66b6842..3c0bfbdbad 100644
--- a/code/modules/mob/living/carbon/metroid/metroid.dm
+++ b/code/modules/mob/living/carbon/metroid/metroid.dm
@@ -381,8 +381,3 @@
powerlevel = 10
adjustToxLoss(-10)
nutrition = max(nutrition, get_max_nutrition())
-
-/mob/living/carbon/slime/cannot_use_vents()
- if(Victim)
- return "You cannot ventcrawl while feeding."
- ..()
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index ba5b8fd24c..02de918890 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -636,126 +636,10 @@ default behaviour is:
resting = !resting
src << "You are now [resting ? "resting" : "getting up"]"
-/mob/living/proc/is_allowed_vent_crawl_item(var/obj/item/carried_item)
- return (get_inventory_slot(carried_item) == 0)
-
-/mob/living/simple_animal/spiderbot/is_allowed_vent_crawl_item(var/obj/item/carried_item)
- if(carried_item == held_item)
- return 0
- return ..()
-
-//called when the mob receives a bright flash
-/mob/living/flash_eyes(intensity = FLASH_PROTECTION_MODERATE, override_blindness_check = FALSE, affect_silicon = FALSE, visual = FALSE, type = /obj/screen/fullscreen/flash)
- if(override_blindness_check || !(disabilities & BLIND))
- overlay_fullscreen("flash", type)
- spawn(25)
- if(src)
- clear_fullscreen("flash", 25)
- return 1
-
-/mob/living/proc/handle_ventcrawl(var/obj/machinery/atmospherics/unary/vent_pump/vent_found = null, var/ignore_items = 0) // -- TLE -- Merged by Carn
- if(stat)
- src << "You must be conscious to do this!"
- return
- if(lying)
- src << "You can't vent crawl while you're stunned!"
- return
- var/special_fail_msg = cannot_use_vents()
- if(special_fail_msg)
- src << "[special_fail_msg]"
- return
-
- if(vent_found) // one was passed in, probably from vent/AltClick()
- if(vent_found.welded)
- src << "That vent is welded shut."
- return
- if(!vent_found.Adjacent(src))
- return // don't even acknowledge that
- else
- for(var/obj/machinery/atmospherics/unary/vent_pump/v in range(1,src))
- if(!v.welded)
- if(v.Adjacent(src))
- vent_found = v
- if(!vent_found)
- src << "You'll need a non-welded vent to crawl into!"
- return
-
- if(!vent_found.network || !vent_found.network.normal_members.len)
- src << "This vent is not connected to anything."
- return
-
- var/list/vents = list()
- for(var/obj/machinery/atmospherics/unary/vent_pump/temp_vent in vent_found.network.normal_members)
- if(temp_vent.welded)
- continue
- if(temp_vent in loc)
- continue
- var/turf/T = get_turf(temp_vent)
-
- if(!T || T.z != loc.z)
- continue
-
- var/i = 1
- var/index = "[T.loc.name]\[[i]\]"
- while(index in vents)
- i++
- index = "[T.loc.name]\[[i]\]"
- vents[index] = temp_vent
- if(!vents.len)
- src << "\red There are no available vents to travel to, they could be welded."
- return
-
- var/obj/selection = input("Select a destination.", "Duct System") as null|anything in sortAssoc(vents)
- if(!selection) return
-
- if(!vent_found.Adjacent(src))
- src << "Never mind, you left."
- return
-
- if(!ignore_items)
- var/list/badItems = list()
- for(var/obj/item/carried_item in contents)//If the monkey got on objects.
- if(!is_allowed_vent_crawl_item(carried_item))
- badItems += carried_item.name
- if(badItems.len)
- src << "Your [english_list(badItems)] prevent[badItems.len == 1 ? "s" : ""] you from ventcrawling."
- return
-
- if(isslime(src))
- var/mob/living/carbon/slime/S = src
- if(S.Victim)
- src << "\red You'll have to let [S.Victim] go or finish eating \him first."
- return
-
- var/obj/machinery/atmospherics/unary/vent_pump/target_vent = vents[selection]
- if(!target_vent)
- return
-
- for(var/mob/O in viewers(src, null))
- O.show_message(text("[src] scrambles into the ventillation ducts!"), 1)
- loc = target_vent
-
- var/travel_time = round(get_dist(loc, target_vent.loc) / 2)
-
- spawn(travel_time)
-
- if(!target_vent) return
- for(var/mob/O in hearers(target_vent,null))
- O.show_message("You hear something squeezing through the ventilation ducts.",2)
-
- sleep(travel_time)
-
- if(!target_vent) return
- if(target_vent.welded) //the vent can be welded while alien scrolled through the list or travelled.
- target_vent = vent_found //travel back. No additional time required.
- src << "\red The vent you were heading to appears to be welded."
- loc = target_vent.loc
- var/area/new_area = get_area(loc)
- if(new_area)
- new_area.Entered(src)
-
/mob/living/proc/cannot_use_vents()
- return "You can't fit into that vent."
+ if(mob_size > MOB_SMALL)
+ return "You can't fit into that vent."
+ return null
/mob/living/proc/has_brain()
return 1
diff --git a/code/modules/mob/living/living_powers.dm b/code/modules/mob/living/living_powers.dm
index b4310d77b5..4fb4c1bcbd 100644
--- a/code/modules/mob/living/living_powers.dm
+++ b/code/modules/mob/living/living_powers.dm
@@ -1,13 +1,3 @@
-/mob/living/proc/ventcrawl()
- set name = "Crawl through Vent"
- set desc = "Enter an air vent and crawl through the pipe system."
- set category = "Abilities"
-
- if(stat == DEAD || paralysis || weakened || stunned || restrained())
- return
-
- handle_ventcrawl()
-
/mob/living/proc/hide()
set name = "Hide"
set desc = "Allows to hide beneath tables or certain items. Toggled on or off."
diff --git a/code/modules/multiz/pipes.dm b/code/modules/multiz/pipes.dm
index 85e23b6797..1cb5b9b4da 100644
--- a/code/modules/multiz/pipes.dm
+++ b/code/modules/multiz/pipes.dm
@@ -13,9 +13,6 @@ obj/machinery/atmospherics/pipe/zpipe
dir = SOUTH
initialize_directions = SOUTH
- var/obj/machinery/atmospherics/node1 //connection on the same Z
- var/obj/machinery/atmospherics/node2 //connection on the other Z
-
var/minimum_temperature_difference = 300
var/thermal_conductivity = 0 //WALL_HEAT_TRANSFER_COEFFICIENT No
diff --git a/code/modules/ventcrawl/ventcrawl.dm b/code/modules/ventcrawl/ventcrawl.dm
new file mode 100644
index 0000000000..cc9cd50359
--- /dev/null
+++ b/code/modules/ventcrawl/ventcrawl.dm
@@ -0,0 +1,211 @@
+var/list/ventcrawl_machinery = list(
+ /obj/machinery/atmospherics/unary/vent_pump,
+ /obj/machinery/atmospherics/unary/vent_scrubber
+ )
+
+// Vent crawling whitelisted items, whoo
+/mob/living/var/list/can_enter_vent_with = list(
+ /obj/item/weapon/implant,
+ /obj/item/device/radio/borg,
+ /obj/item/weapon/holder,
+ /obj/machinery/camera,
+ /mob/living/simple_animal/borer,
+ )
+
+/mob/living/var/list/icon/pipes_shown = list()
+/mob/living/var/last_played_vent
+/mob/living/var/is_ventcrawling = 0
+/mob/var/next_play_vent = 0
+
+/mob/living/proc/can_ventcrawl()
+ return 0
+
+/mob/living/Login()
+ . = ..()
+ //login during ventcrawl
+ if(is_ventcrawling && istype(loc, /obj/machinery/atmospherics)) //attach us back into the pipes
+ remove_ventcrawl()
+ add_ventcrawl(loc)
+
+/mob/living/carbon/slime/can_ventcrawl()
+ if(Victim)
+ src << "You cannot ventcrawl while feeding."
+ return 0
+ return 1
+
+/mob/living/proc/is_allowed_vent_crawl_item(var/obj/item/carried_item)
+ if(carried_item == ability_master)
+ return 1
+ for(var/type in can_enter_vent_with)
+ if(istype(carried_item, can_enter_vent_with))
+ return get_inventory_slot(carried_item) == 0
+ return 0
+
+/mob/living/carbon/is_allowed_vent_crawl_item(var/obj/item/carried_item)
+ if(carried_item in internal_organs)
+ return 1
+ return ..()
+
+/mob/living/carbon/human/is_allowed_vent_crawl_item(var/obj/item/carried_item)
+ if(carried_item in organs)
+ return 1
+ return ..()
+
+/mob/living/simple_animal/spiderbot/is_allowed_vent_crawl_item(var/obj/item/carried_item)
+ if(carried_item == held_item)
+ return 1
+ return ..()
+
+/mob/living/proc/ventcrawl_carry()
+ for(var/atom/A in src.contents)
+ if(!is_allowed_vent_crawl_item(A))
+ src << "You can't be carrying that when vent crawling! - [A.name]"
+ return 0
+ return 1
+
+/mob/living/AltClickOn(var/atom/A)
+ if(is_type_in_list(A,ventcrawl_machinery) && src.can_ventcrawl())
+ src.handle_ventcrawl(A)
+ return 1
+ return ..()
+
+/mob/living/carbon/human/can_ventcrawl()
+ return issmall(src)
+
+/mob/proc/start_ventcrawl()
+ var/atom/pipe
+ var/list/pipes = list()
+ for(var/obj/machinery/atmospherics/unary/U in range(1))
+ if(is_type_in_list(U,ventcrawl_machinery) && Adjacent(U))
+ pipes |= U
+ if(!pipes || !pipes.len)
+ to_chat(src, "There are no pipes that you can ventcrawl into within range!")
+ return
+ if(pipes.len == 1)
+ pipe = pipes[1]
+ else
+ pipe = input("Crawl Through Vent", "Pick a pipe") as null|anything in pipes
+ if(canmove && pipe)
+ return pipe
+
+/mob/living/carbon/slime/can_ventcrawl()
+ return 1
+
+/mob/living/simple_animal/borer/can_ventcrawl()
+ return 1
+
+/mob/living/simple_animal/borer/ventcrawl_carry()
+ return 1
+
+/mob/living/simple_animal/mouse/can_ventcrawl()
+ return 1
+
+/mob/living/simple_animal/spiderbot/can_ventcrawl()
+ return 1
+
+/mob/living/carbon/alien/can_ventcrawl()
+ return 1
+
+/mob/living/carbon/alien/ventcrawl_carry()
+ return 1
+
+/mob/living/var/ventcrawl_layer = 3
+
+/mob/living/proc/handle_ventcrawl(var/atom/clicked_on)
+
+ if(!stat)
+ if(!lying)
+
+ var/obj/machinery/atmospherics/unary/vent_found
+
+ if(clicked_on && Adjacent(clicked_on))
+ vent_found = clicked_on
+ if(!istype(vent_found) || !vent_found.can_crawl_through())
+ vent_found = null
+
+ if(!vent_found)
+ for(var/obj/machinery/atmospherics/machine in range(1,src))
+ if(is_type_in_list(machine, ventcrawl_machinery))
+ vent_found = machine
+
+ if(!vent_found || !vent_found.can_crawl_through())
+ vent_found = null
+
+ if(vent_found)
+ break
+
+ if(vent_found)
+ if(vent_found.network && (vent_found.network.normal_members.len || vent_found.network.line_members.len))
+
+ to_chat(src, "You begin climbing into the ventilation system...")
+ if(vent_found.air_contents && !issilicon(src))
+
+ switch(vent_found.air_contents.temperature)
+ if(0 to BODYTEMP_COLD_DAMAGE_LIMIT)
+ to_chat(src, "You feel a painful freeze coming from the vent!")
+ if(BODYTEMP_COLD_DAMAGE_LIMIT to T0C)
+ to_chat(src, "You feel an icy chill coming from the vent.")
+ if(T0C + 40 to BODYTEMP_HEAT_DAMAGE_LIMIT)
+ to_chat(src, "You feel a hot wash coming from the vent.")
+ if(BODYTEMP_HEAT_DAMAGE_LIMIT to INFINITY)
+ to_chat(src, "You feel a searing heat coming from the vent!")
+
+ switch(vent_found.air_contents.return_pressure())
+ if(0 to HAZARD_LOW_PRESSURE)
+ to_chat(src, "You feel a rushing draw pulling you into the vent!")
+ if(HAZARD_LOW_PRESSURE to WARNING_LOW_PRESSURE)
+ to_chat(src, "You feel a strong drag pulling you into the vent.")
+ if(WARNING_HIGH_PRESSURE to HAZARD_HIGH_PRESSURE)
+ to_chat(src, "You feel a strong current pushing you away from the vent.")
+ if(HAZARD_HIGH_PRESSURE to INFINITY)
+ to_chat(src, "You feel a roaring wind pushing you away from the vent!")
+
+ if(!do_after(src, 45, vent_found, 1, 1))
+ return
+
+ if(!client)
+ return
+
+ if(!ventcrawl_carry())
+ return
+
+ visible_message("[src] scrambles into the ventilation ducts!", "You climb into the ventilation system.")
+
+ forceMove(vent_found)
+ add_ventcrawl(vent_found)
+
+ else
+ to_chat(src, "This vent is not connected to anything.")
+
+ else
+ to_chat(src, "You must be standing on or beside an air vent to enter it.")
+
+ else
+ to_chat(src, "You can't vent crawl while you're stunned!")
+
+ else
+ to_chat(src, "You must be conscious to do this!")
+ return
+
+/mob/living/proc/add_ventcrawl(obj/machinery/atmospherics/starting_machine)
+ is_ventcrawling = 1
+ //candrop = 0
+ var/datum/pipe_network/network = starting_machine.return_network(starting_machine)
+ if(!network)
+ return
+ for(var/datum/pipeline/pipeline in network.line_members)
+ for(var/obj/machinery/atmospherics/A in (pipeline.members || pipeline.edges))
+ if(!A.pipe_image)
+ A.pipe_image = image(A, A.loc, layer = 20, dir = A.dir)
+ pipes_shown += A.pipe_image
+ client.images += A.pipe_image
+
+/mob/living/proc/remove_ventcrawl()
+ is_ventcrawling = 0
+ //candrop = 1
+ if(client)
+ for(var/image/current_image in pipes_shown)
+ client.images -= current_image
+ client.eye = src
+
+ pipes_shown.len = 0
\ No newline at end of file
diff --git a/code/modules/ventcrawl/ventcrawl_atmospherics.dm b/code/modules/ventcrawl/ventcrawl_atmospherics.dm
new file mode 100644
index 0000000000..7989f03315
--- /dev/null
+++ b/code/modules/ventcrawl/ventcrawl_atmospherics.dm
@@ -0,0 +1,86 @@
+/obj/machinery/atmospherics/var/image/pipe_image
+
+/obj/machinery/atmospherics/Destroy()
+ for(var/mob/living/M in src) //ventcrawling is serious business
+ M.remove_ventcrawl()
+ M.forceMove(get_turf(src))
+ if(pipe_image)
+ for(var/mob/living/M in player_list)
+ if(M.client)
+ M.client.images -= pipe_image
+ M.pipes_shown -= pipe_image
+ pipe_image = null
+ . = ..()
+
+/obj/machinery/atmospherics/ex_act(severity)
+ for(var/atom/movable/A in src) //ventcrawling is serious business
+ A.ex_act(severity)
+ . = ..()
+
+/obj/machinery/atmospherics/Entered(atom/movable/Obj)
+ if(istype(Obj, /mob/living))
+ var/mob/living/L = Obj
+ L.ventcrawl_layer = layer
+ . = ..()
+
+/obj/machinery/atmospherics/relaymove(mob/living/user, direction)
+ if(!(direction & initialize_directions)) //can't go in a way we aren't connecting to
+ return
+ ventcrawl_to(user,findConnecting(direction, user.ventcrawl_layer),direction)
+
+/obj/machinery/atmospherics/proc/ventcrawl_to(var/mob/living/user, var/obj/machinery/atmospherics/target_move, var/direction)
+ if(target_move)
+ if(is_type_in_list(target_move, ventcrawl_machinery) && target_move.can_crawl_through())
+ user.remove_ventcrawl()
+ user.forceMove(target_move.loc) //handles entering and so on
+ user.visible_message("You hear something squeezing through the ducts.", "You climb out the ventilation system.")
+ else if(target_move.can_crawl_through())
+ if(target_move.return_network(target_move) != return_network(src))
+ user.remove_ventcrawl()
+ user.add_ventcrawl(target_move)
+ user.forceMove(target_move)
+ user.client.eye = target_move //if we don't do this, Byond only updates the eye every tick - required for smooth movement
+ if(world.time > user.next_play_vent)
+ user.next_play_vent = world.time+30
+ playsound(src, 'sound/machines/ventcrawl.ogg', 50, 1, -3)
+ else
+ if((direction & initialize_directions) || is_type_in_list(src, ventcrawl_machinery) && src.can_crawl_through()) //if we move in a way the pipe can connect, but doesn't - or we're in a vent
+ user.remove_ventcrawl()
+ user.forceMove(src.loc)
+ user.visible_message("You hear something squeezing through the pipes.", "You climb out the ventilation system.")
+ user.canmove = 0
+ spawn(1)
+ user.canmove = 1
+
+/obj/machinery/atmospherics/proc/can_crawl_through()
+ return 1
+
+/obj/machinery/atmospherics/proc/findConnecting(var/direction)
+ for(var/obj/machinery/atmospherics/target in get_step(src,direction))
+ if(target.initialize_directions & get_dir(target,src))
+ if(isConnectable(target) && target.isConnectable(src))
+ return target
+
+/obj/machinery/atmospherics/proc/isConnectable(var/obj/machinery/atmospherics/target)
+ return (target == node1 || target == node2)
+
+/obj/machinery/atmospherics/pipe/manifold/isConnectable(var/obj/machinery/atmospherics/target)
+ return (target == node3 || ..())
+
+obj/machinery/atmospherics/trinary/isConnectable(var/obj/machinery/atmospherics/target)
+ return (target == node3 || ..())
+
+/obj/machinery/atmospherics/pipe/manifold4w/isConnectable(var/obj/machinery/atmospherics/target)
+ return (target == node3 || target == node4 || ..())
+
+/obj/machinery/atmospherics/tvalve/isConnectable(var/obj/machinery/atmospherics/target)
+ return (target == node3 || ..())
+
+/obj/machinery/atmospherics/pipe/cap/isConnectable(var/obj/machinery/atmospherics/target)
+ return (target == node || ..())
+
+/obj/machinery/atmospherics/portables_connector/isConnectable(var/obj/machinery/atmospherics/target)
+ return (target == node || ..())
+
+/obj/machinery/atmospherics/unary/isConnectable(var/obj/machinery/atmospherics/target)
+ return (target == node || ..())
diff --git a/code/modules/ventcrawl/ventcrawl_multiz.dm b/code/modules/ventcrawl/ventcrawl_multiz.dm
new file mode 100644
index 0000000000..87ef8f9ba7
--- /dev/null
+++ b/code/modules/ventcrawl/ventcrawl_multiz.dm
@@ -0,0 +1,24 @@
+/obj/machinery/atmospherics/pipe/zpipe/up/verb/ventcrawl_move_up()
+ set name = "Ventcrawl Upwards"
+ set desc = "Climb up through a pipe."
+ set category = "Abilities"
+ set src = usr.loc
+ var/obj/machinery/atmospherics/target = check_ventcrawl(GetAbove(loc))
+ if(target) ventcrawl_to(usr, target, UP)
+
+/obj/machinery/atmospherics/pipe/zpipe/down/verb/ventcrawl_move_down()
+ set name = "Ventcrawl Downwards"
+ set desc = "Climb down through a pipe."
+ set category = "Abilities"
+ set src = usr.loc
+ var/obj/machinery/atmospherics/target = check_ventcrawl(GetBelow(loc))
+ if(target) ventcrawl_to(usr, target, DOWN)
+
+/obj/machinery/atmospherics/pipe/zpipe/proc/check_ventcrawl(var/turf/target)
+ if(!istype(target))
+ return
+ if(node1 in target)
+ return node1
+ if(node2 in target)
+ return node2
+ return
\ No newline at end of file
diff --git a/code/modules/ventcrawl/ventcrawl_verb.dm b/code/modules/ventcrawl/ventcrawl_verb.dm
new file mode 100644
index 0000000000..44bcab4a5c
--- /dev/null
+++ b/code/modules/ventcrawl/ventcrawl_verb.dm
@@ -0,0 +1,9 @@
+/mob/living/proc/ventcrawl()
+ set name = "Crawl through Vent"
+ set desc = "Enter an air vent and crawl through the pipe system."
+ set category = "Abilities"
+ if(incapacitated() || restrained())
+ return
+ var/pipe = start_ventcrawl()
+ if(pipe)
+ handle_ventcrawl()
diff --git a/html/changelogs/zuhayr-vent.yml b/html/changelogs/zuhayr-vent.yml
new file mode 100644
index 0000000000..fec2fd7d99
--- /dev/null
+++ b/html/changelogs/zuhayr-vent.yml
@@ -0,0 +1,5 @@
+author: Zuhayr
+delete-after: True
+changes:
+ - rscadd: "Added /vg/ direct-action ventcrawling. You will now crawl through the actual pipe network, a step at a time. Have fun."
+
\ No newline at end of file
diff --git a/polaris.dme b/polaris.dme
index b1728a349e..b11ecda08d 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -1996,6 +1996,10 @@
#include "code\modules\vehicles\cargo_train.dm"
#include "code\modules\vehicles\train.dm"
#include "code\modules\vehicles\vehicle.dm"
+#include "code\modules\ventcrawl\ventcrawl.dm"
+#include "code\modules\ventcrawl\ventcrawl_atmospherics.dm"
+#include "code\modules\ventcrawl\ventcrawl_multiz.dm"
+#include "code\modules\ventcrawl\ventcrawl_verb.dm"
#include "code\modules\virus2\admin.dm"
#include "code\modules\virus2\analyser.dm"
#include "code\modules\virus2\antibodies.dm"
diff --git a/sound/machines/ventcrawl.ogg b/sound/machines/ventcrawl.ogg
new file mode 100644
index 0000000000..2df8b5c898
Binary files /dev/null and b/sound/machines/ventcrawl.ogg differ