diff --git a/code/controllers/master_controller.dm b/code/controllers/master_controller.dm
index 682b0fb3d03..62209a1014c 100644
--- a/code/controllers/master_controller.dm
+++ b/code/controllers/master_controller.dm
@@ -76,5 +76,12 @@ datum/controller/game_controller/proc/setup_objects()
//Set up spawn points.
populate_spawn_points()
+ admin_notice("Initializing turbolifts", R_DEBUG)
+ for(var/thing in turbolifts)
+ if(!deleted(thing))
+ var/obj/turbolift_map_holder/lift = thing
+ lift.initialize()
+ sleep(-1)
+
admin_notice("Initializations complete.", R_DEBUG)
sleep(-1)
diff --git a/code/modules/materials/materials.dm b/code/modules/materials/materials.dm
index fb3af280150..e032ae48930 100644
--- a/code/modules/materials/materials.dm
+++ b/code/modules/materials/materials.dm
@@ -586,6 +586,12 @@ var/list/name_to_material
hardness = 500
weight = 500
+// Likewise.
+/material/alienalloy/elevatorium
+ name = "elevatorium"
+ display_name = "elevator panelling"
+ icon_colour = "#666666"
+
/material/wood
name = "wood"
stack_type = /obj/item/stack/material/wood
diff --git a/code/modules/shuttles/shuttle_emergency.dm b/code/modules/shuttles/shuttle_emergency.dm
index 3238d9b2f40..bd2eaaa770d 100644
--- a/code/modules/shuttles/shuttle_emergency.dm
+++ b/code/modules/shuttles/shuttle_emergency.dm
@@ -31,7 +31,7 @@
if (emergency_shuttle.evac)
priority_announcement.Announce(replacetext(replacetext(using_map.emergency_shuttle_leaving_dock, "%dock_name%", "[using_map.dock_name]"), "%ETD%", "[estimated_time] minute\s"))
else
- priority_announcement.Announce(replacetext(replacetext(using_map.shuttle_leaving_dock, "%dock_name%", "[using_map.dock_name]"), "%ETD%", "[estimated_time] minute\s"))
+ priority_announcement.Announce(replacetext(replacetext(using_map.shuttle_leaving_dock, "%dock_name%", "[using_map.dock_name]"), "%ETA%", "[estimated_time] minute\s"))
/datum/shuttle/ferry/emergency/can_launch(var/user)
if (istype(user, /obj/machinery/computer/shuttle_control/emergency))
diff --git a/code/modules/turbolift/_turbolift.dm b/code/modules/turbolift/_turbolift.dm
new file mode 100644
index 00000000000..088bf39c8c4
--- /dev/null
+++ b/code/modules/turbolift/_turbolift.dm
@@ -0,0 +1,12 @@
+/*
+ * Turbolifts! Sort of like multishuttles-lite.
+ *
+ * How-to: Map /obj/turbolift_map_holder in at the bottom of the shaft, give it a depth
+ * value equivalent to the number of floors it should span (inclusive of the first),
+ * and at runtime it will update the map, set areas and create control panels and
+ * wifi-set doors appropriate to itself. You will save time at init if you map the
+ * elevator shaft in properly before runtime, but ultimately you're just avoiding a
+ * bunch of ChangeTurf() calls.
+ */
+
+var/list/turbolifts = list()
\ No newline at end of file
diff --git a/code/modules/turbolift/turbolift.dm b/code/modules/turbolift/turbolift.dm
new file mode 100644
index 00000000000..7d2f3c11a83
--- /dev/null
+++ b/code/modules/turbolift/turbolift.dm
@@ -0,0 +1,122 @@
+// Lift master datum. One per turbolift.
+/datum/turbolift
+ var/datum/turbolift_floor/target_floor // Where are we going?
+ var/datum/turbolift_floor/current_floor // Where is the lift currently?
+ var/list/doors = list() // Doors inside the lift structure.
+ var/list/queued_floors = list() // Where are we moving to next?
+ var/list/floors = list() // All floors in this system.
+ var/move_delay = 30 // Time between floor changes.
+ var/floor_wait_delay = 85 // Time to wait at floor stops.
+ var/obj/structure/lift/panel/control_panel_interior // Lift control panel.
+ var/doors_closing = 0 // Whether doors are in the process of closing
+
+ var/tmp/moving_upwards
+ var/tmp/busy
+
+/datum/turbolift/proc/emergency_stop()
+ queued_floors.Cut()
+ target_floor = null
+ open_doors()
+
+/datum/turbolift/proc/doors_are_open(var/datum/turbolift_floor/use_floor = current_floor)
+ for(var/obj/machinery/door/airlock/door in (use_floor ? (doors + use_floor.doors) : doors))
+ if(!door.density)
+ return 1
+ return 0
+
+/datum/turbolift/proc/open_doors(var/datum/turbolift_floor/use_floor = current_floor)
+ for(var/obj/machinery/door/airlock/door in (use_floor ? (doors + use_floor.doors) : doors))
+ //door.command("open")
+ spawn(0)
+ door.open()
+ return
+
+/datum/turbolift/proc/close_doors(var/datum/turbolift_floor/use_floor = current_floor)
+ for(var/obj/machinery/door/airlock/door in (use_floor ? (doors + use_floor.doors) : doors))
+ //door.command("close")
+ spawn(0)
+ door.close()
+ return
+
+/datum/turbolift/proc/do_move()
+
+ var/current_floor_index = floors.Find(current_floor)
+
+ if(!target_floor)
+ if(!queued_floors || !queued_floors.len)
+ return 0
+ target_floor = queued_floors[1]
+ queued_floors -= target_floor
+ if(current_floor_index < floors.Find(target_floor))
+ moving_upwards = 1
+ else
+ moving_upwards = 0
+
+ if(doors_are_open())
+ if(!doors_closing)
+ close_doors()
+ doors_closing = 1
+ return 1
+ else // We failed to close the doors - probably, someone is blocking them; stop trying to move
+ doors_closing = 0
+ open_doors()
+ control_panel_interior.audible_message("\The [current_floor.ext_panel] buzzes loudly.")
+ playsound(control_panel_interior.loc, "sound/machines/buzz-two.ogg", 50, 1)
+ return 0
+
+ doors_closing = 0 // The doors weren't open, so they are done closing
+
+ var/area/turbolift/origin = locate(current_floor.area_ref)
+
+ if(target_floor == current_floor)
+
+ playsound(control_panel_interior.loc, origin.arrival_sound, 50, 1)
+ target_floor.arrived(src)
+ target_floor = null
+
+ sleep(15)
+ control_panel_interior.visible_message("The elevator announces, \"[origin.lift_announce_str]\"")
+ sleep(floor_wait_delay)
+
+ return 1
+
+ // Work out where we're headed.
+ var/datum/turbolift_floor/next_floor
+ if(moving_upwards)
+ next_floor = floors[current_floor_index+1]
+ else
+ next_floor = floors[current_floor_index-1]
+
+ var/area/turbolift/destination = locate(next_floor.area_ref)
+
+ if(!istype(origin) || !istype(destination) || (origin == destination))
+ return 0
+
+ for(var/turf/T in destination)
+ for(var/atom/movable/AM in T)
+ if(istype(AM, /mob/living))
+ var/mob/living/M = AM
+ M.gib()
+ else if(AM.simulated)
+ qdel(AM)
+
+ origin.move_contents_to(destination)
+
+ if((locate(/obj/machinery/power) in destination) || (locate(/obj/structure/cable) in destination))
+ makepowernets()
+
+ current_floor = next_floor
+ control_panel_interior.visible_message("The elevator [moving_upwards ? "rises" : "descends"] smoothly.")
+
+ return 1
+
+/datum/turbolift/proc/queue_move_to(var/datum/turbolift_floor/floor)
+ if(!floor || !(floor in floors) || (floor in queued_floors))
+ return // STOP PRESSING THE BUTTON.
+ floor.pending_move(src)
+ queued_floors |= floor
+ turbolift_controller.lift_is_moving(src)
+
+// TODO: dummy machine ('lift mechanism') in powered area for functionality/blackout checks.
+/datum/turbolift/proc/is_functional()
+ return 1
\ No newline at end of file
diff --git a/code/modules/turbolift/turbolift_areas.dm b/code/modules/turbolift/turbolift_areas.dm
new file mode 100644
index 00000000000..dbf568aec2e
--- /dev/null
+++ b/code/modules/turbolift/turbolift_areas.dm
@@ -0,0 +1,11 @@
+// Used for creating the exchange areas.
+/area/turbolift
+ name = "Turbolift"
+ base_turf = /turf/simulated/open
+ requires_power = 0
+ sound_env = SMALL_ENCLOSED
+
+ var/lift_floor_label = null
+ var/lift_floor_name = null
+ var/lift_announce_str = "Ding!"
+ var/arrival_sound = 'sound/machines/ding.ogg'
diff --git a/code/modules/turbolift/turbolift_console.dm b/code/modules/turbolift/turbolift_console.dm
new file mode 100644
index 00000000000..4cbee6a7737
--- /dev/null
+++ b/code/modules/turbolift/turbolift_console.dm
@@ -0,0 +1,155 @@
+// Base type, do not use.
+/obj/structure/lift
+ name = "turbolift control component"
+ icon = 'icons/obj/turbolift.dmi'
+ anchored = 1
+ density = 0
+ layer = 4
+
+ var/datum/turbolift/lift
+
+/obj/structure/lift/set_dir(var/newdir)
+ . = ..()
+ pixel_x = 0
+ pixel_y = 0
+ if(dir & NORTH)
+ pixel_y = -32
+ else if(dir & SOUTH)
+ pixel_y = 32
+ else if(dir & EAST)
+ pixel_x = -32
+ else if(dir & WEST)
+ pixel_x = 32
+
+/obj/structure/lift/proc/pressed(var/mob/user)
+ if(!istype(user, /mob/living/silicon))
+ if(user.a_intent == I_HURT)
+ user.visible_message("\The [user] hammers on the lift button!")
+ else
+ user.visible_message("\The [user] presses the lift button.")
+
+
+/obj/structure/lift/New(var/newloc, var/datum/turbolift/_lift)
+ lift = _lift
+ return ..(newloc)
+
+/obj/structure/lift/attack_ai(var/mob/user)
+ return attack_hand(user)
+
+/obj/structure/lift/attack_generic(var/mob/user)
+ return attack_hand(user)
+
+/obj/structure/lift/attack_hand(var/mob/user)
+ return interact(user)
+
+/obj/structure/lift/interact(var/mob/user)
+ if(!lift.is_functional())
+ return 0
+ return 1
+// End base.
+
+// Button. No HTML interface, just calls the associated lift to its floor.
+/obj/structure/lift/button
+ name = "elevator button"
+ desc = "A call button for an elevator. Be sure to hit it three hundred times."
+ icon_state = "button"
+ var/light_up = FALSE
+ var/datum/turbolift_floor/floor
+
+/obj/structure/lift/button/Destroy()
+ if(floor && floor.ext_panel == src)
+ floor.ext_panel = null
+ floor = null
+ return ..()
+
+/obj/structure/lift/button/proc/reset()
+ light_up = FALSE
+ update_icon()
+
+/obj/structure/lift/button/interact(var/mob/user)
+ if(!..())
+ return
+ light_up()
+ pressed(user)
+ if(floor == lift.current_floor)
+ lift.open_doors()
+ spawn(3)
+ reset()
+ return
+ lift.queue_move_to(floor)
+
+/obj/structure/lift/button/proc/light_up()
+ light_up = TRUE
+ update_icon()
+
+/obj/structure/lift/button/update_icon()
+ if(light_up)
+ icon_state = "button_lit"
+ else
+ icon_state = initial(icon_state)
+
+// End button.
+
+// Panel. Lists floors (HTML), moves with the elevator, schedules a move to a given floor.
+/obj/structure/lift/panel
+ name = "elevator control panel"
+ icon_state = "panel"
+
+
+/obj/structure/lift/panel/attack_ghost(var/mob/user)
+ return interact(user)
+
+/obj/structure/lift/panel/interact(var/mob/user)
+ if(!..())
+ return
+
+ var/dat = list()
+ dat += "
Lift panel
"
+
+ //the floors list stores levels in order of increasing Z
+ //therefore, to display upper levels at the top of the menu and
+ //lower levels at the bottom, we need to go through the list in reverse
+ for(var/i in lift.floors.len to 1 step -1)
+ var/datum/turbolift_floor/floor = lift.floors[i]
+ var/label = floor.label? floor.label : "Level #[i]"
+ dat += ""
+ dat += "[label]: [floor.name]
"
+
+ dat += "
"
+ if(lift.doors_are_open())
+ dat += "Close Doors
"
+ else
+ dat += "Open Doors
"
+ dat += "Emergency Stop"
+ dat += "
"
+
+ var/datum/browser/popup = new(user, "turbolift_panel", "Lift Panel", 230, 260)
+ popup.set_content(jointext(dat, null))
+ popup.open()
+ return
+
+/obj/structure/lift/panel/Topic(href, href_list)
+ . = ..()
+ if(.)
+ return
+
+ var/panel_interact
+ if(href_list["move_to_floor"])
+ lift.queue_move_to(locate(href_list["move_to_floor"]))
+ panel_interact = 1
+ if(href_list["open_doors"])
+ panel_interact = 1
+ lift.open_doors()
+ if(href_list["close_doors"])
+ panel_interact = 1
+ lift.close_doors()
+ if(href_list["emergency_stop"])
+ panel_interact = 1
+ lift.emergency_stop()
+
+ if(panel_interact)
+ pressed(usr)
+
+ return 0
+
+// End panel.
\ No newline at end of file
diff --git a/code/modules/turbolift/turbolift_door.dm b/code/modules/turbolift/turbolift_door.dm
new file mode 100644
index 00000000000..79bf1d0c51a
--- /dev/null
+++ b/code/modules/turbolift/turbolift_door.dm
@@ -0,0 +1,44 @@
+/obj/machinery/door/airlock/lift
+ name = "Elevator Door"
+ desc = "Ding."
+ req_access = list(access_maint_tunnels)
+ opacity = 0
+ autoclose = 0
+ glass = 1
+ icon = 'icons/obj/doors/doorlift.dmi'
+
+ var/datum/turbolift/lift
+ var/datum/turbolift_floor/floor
+
+/obj/machinery/door/airlock/lift/Destroy()
+ if(lift)
+ lift.doors -= src
+ if(floor)
+ floor.doors -= src
+ return ..()
+
+/obj/machinery/door/airlock/lift/bumpopen(var/mob/user)
+ return // No accidental sprinting into open elevator shafts.
+
+/obj/machinery/door/airlock/lift/allowed(mob/M)
+ return FALSE //only the lift machinery is allowed to operate this door
+
+/obj/machinery/door/airlock/lift/close(var/forced=0)
+ for(var/turf/turf in locs)
+ for(var/mob/living/LM in turf)
+ if(LM.mob_size <= MOB_TINY)
+ var/moved = 0
+ for(dir in shuffle(cardinal.Copy()))
+ var/dest = get_step(LM,dir)
+ if(!(locate(/obj/machinery/door/airlock/lift) in dest))
+ if(LM.Move(dest))
+ moved = 1
+ LM.visible_message("\The [LM] scurries away from the closing doors.")
+ break
+ if(!moved) // nowhere to go....
+ LM.gib()
+ else // the mob is too big to just move, so we need to give up what we're doing
+ audible_message("\The [src]'s motors grind as they quickly reverse direction, unable to safely close.")
+ cur_command = null // the door will just keep trying otherwise
+ return 0
+ return ..()
\ No newline at end of file
diff --git a/code/modules/turbolift/turbolift_floor.dm b/code/modules/turbolift/turbolift_floor.dm
new file mode 100644
index 00000000000..7761f092843
--- /dev/null
+++ b/code/modules/turbolift/turbolift_floor.dm
@@ -0,0 +1,33 @@
+// Simple holder for each floor in the lift.
+/datum/turbolift_floor
+ var/area_ref
+ var/label
+ var/name
+ var/announce_str
+ var/arrival_sound
+
+ var/list/doors = list()
+ var/obj/structure/lift/button/ext_panel
+
+/datum/turbolift_floor/proc/set_area_ref(var/ref)
+ var/area/turbolift/A = locate(ref)
+ if(!istype(A))
+ log_debug("Turbolift floor area was of the wrong type: ref=[ref]")
+ return
+
+ area_ref = ref
+ label = A.lift_floor_label
+ name = A.lift_floor_name ? A.lift_floor_name : A.name
+ announce_str = A.lift_announce_str
+ arrival_sound = A.arrival_sound
+
+//called when a lift has queued this floor as a destination
+/datum/turbolift_floor/proc/pending_move(var/datum/turbolift/lift)
+ if(ext_panel)
+ ext_panel.light_up()
+
+//called when a lift arrives at this floor
+/datum/turbolift_floor/proc/arrived(var/datum/turbolift/lift)
+ lift.open_doors(src)
+ if(ext_panel)
+ ext_panel.reset()
\ No newline at end of file
diff --git a/code/modules/turbolift/turbolift_map.dm b/code/modules/turbolift/turbolift_map.dm
new file mode 100644
index 00000000000..2e2498f88db
--- /dev/null
+++ b/code/modules/turbolift/turbolift_map.dm
@@ -0,0 +1,228 @@
+// Map object.
+/obj/turbolift_map_holder
+ name = "turbolift map placeholder"
+ icon = 'icons/obj/turbolift_preview_3x3.dmi'
+ dir = SOUTH // Direction of the holder determines the placement of the lift control panel and doors.
+ var/depth = 1 // Number of floors to generate, including the initial floor.
+ var/lift_size_x = 2 // Number of turfs on each axis to generate in addition to the first
+ var/lift_size_y = 2 // ie. a 3x3 lift would have a value of 2 in each of these variables.
+
+ // Various turf and door types used when generating the turbolift floors.
+ var/wall_type = /turf/simulated/wall/elevator
+ var/floor_type = /turf/simulated/floor/tiled/dark
+ var/door_type = /obj/machinery/door/airlock/lift
+
+ var/list/areas_to_use = list()
+
+/obj/turbolift_map_holder/Destroy()
+ turbolifts -= src
+ return ..()
+
+/obj/turbolift_map_holder/New()
+ turbolifts += src
+ ..()
+
+/obj/turbolift_map_holder/initialize()
+
+ // Create our system controller.
+ var/datum/turbolift/lift = new()
+
+ // Holder values since we're moving this object to null ASAP.
+ var/ux = x
+ var/uy = y
+ var/uz = z
+ var/udir = dir
+ forceMove(null)
+
+ // These modifiers are used in relation to the origin
+ // to place the system control panels and doors.
+ var/int_panel_x
+ var/int_panel_y
+ var/ext_panel_x
+ var/ext_panel_y
+ var/door_x1
+ var/door_y1
+ var/door_x2
+ var/door_y2
+ var/light_x1
+ var/light_x2
+ var/light_y1
+ var/light_y2
+
+ var/az = 1
+ var/ex = (ux+lift_size_x)
+ var/ey = (uy+lift_size_y)
+ var/ez = (uz+(depth-1))
+
+ switch(dir)
+
+ if(NORTH)
+
+ int_panel_x = ux + Floor(lift_size_x/2)
+ int_panel_y = uy + 1
+ ext_panel_x = ux
+ ext_panel_y = ey + 2
+
+ door_x1 = ux + 1
+ door_y1 = ey
+ door_x2 = ex - 1
+ door_y2 = ey + 1
+
+ light_x1 = ux + 1
+ light_y1 = uy + 1
+ light_x2 = ux + lift_size_x - 1
+ light_y2 = uy + 1
+
+ if(SOUTH)
+
+ int_panel_x = ux + Floor(lift_size_x/2)
+ int_panel_y = ey - 1
+ ext_panel_x = ex
+ ext_panel_y = uy - 2
+
+ door_x1 = ux + 1
+ door_y1 = uy - 1
+ door_x2 = ex - 1
+ door_y2 = uy
+
+ light_x1 = ux + 1
+ light_y1 = uy + 2
+ light_x2 = ux + lift_size_x - 1
+ light_y2 = uy + lift_size_y - 1
+
+ if(EAST)
+
+ int_panel_x = ux+1
+ int_panel_y = uy + Floor(lift_size_y/2)
+ ext_panel_x = ex+2
+ ext_panel_y = ey
+
+ door_x1 = ex
+ door_y1 = uy + 1
+ door_x2 = ex + 1
+ door_y2 = ey - 1
+
+ light_x1 = ux + 1
+ light_y1 = uy + 1
+ light_x2 = ux + 1
+ light_y2 = uy + lift_size_x - 1
+
+ if(WEST)
+
+ int_panel_x = ex-1
+ int_panel_y = uy + Floor(lift_size_y/2)
+ ext_panel_x = ux-2
+ ext_panel_y = uy
+
+ door_x1 = ux - 1
+ door_y1 = uy + 1
+ door_x2 = ux
+ door_y2 = ey - 1
+
+ light_x1 = ux + lift_size_x - 1
+ light_y1 = uy + 1
+ light_x2 = ux + lift_size_x - 1
+ light_y2 = uy + lift_size_y - 1
+
+ // Generate each floor and store it in the controller datum.
+ for(var/cz = uz;cz<=ez;cz++)
+
+ var/datum/turbolift_floor/cfloor = new()
+ lift.floors += cfloor
+
+ var/list/floor_turfs = list()
+ // Update the appropriate turfs.
+ for(var/tx = ux to ex)
+ for(var/ty = uy to ey)
+
+ var/turf/checking = locate(tx,ty,cz)
+
+ if(!istype(checking))
+ log_debug("[name] cannot find a component turf at [tx],[ty] on floor [cz]. Aborting.")
+ qdel(src)
+ return
+
+ // Update path appropriately if needed.
+ var/swap_to = /turf/simulated/open
+ if(cz == uz) // Elevator.
+ if((tx == ux || ty == uy || tx == ex || ty == ey) && !(tx >= door_x1 && tx <= door_x2 && ty >= door_y1 && ty <= door_y2))
+ swap_to = wall_type
+ else
+ swap_to = floor_type
+
+ if(checking.type != swap_to)
+ checking.ChangeTurf(swap_to)
+ // Let's make absolutely sure that we have the right turf.
+ checking = locate(tx,ty,cz)
+
+ // Clear out contents.
+ for(var/atom/movable/thing in checking.contents)
+ if(thing.simulated)
+ qdel(thing)
+
+ if(tx >= ux && tx <= ex && ty >= uy && ty <= ey)
+ floor_turfs += checking
+
+ // Place exterior doors.
+ for(var/tx = door_x1 to door_x2)
+ for(var/ty = door_y1 to door_y2)
+ var/turf/checking = locate(tx,ty,cz)
+ var/internal = 1
+ if(!(checking in floor_turfs))
+ internal = 0
+ if(checking.type != floor_type)
+ checking.ChangeTurf(floor_type)
+ checking = locate(tx,ty,cz)
+ for(var/atom/movable/thing in checking.contents)
+ if(thing.simulated)
+ qdel(thing)
+ if(checking.type == floor_type) // Don't build over empty space on lower levels.
+ var/obj/machinery/door/airlock/lift/newdoor = new door_type(checking)
+ if(internal)
+ lift.doors += newdoor
+ newdoor.lift = cfloor
+ else
+ cfloor.doors += newdoor
+ newdoor.floor = cfloor
+
+ // Place exterior control panel.
+ var/turf/placing = locate(ext_panel_x, ext_panel_y, cz)
+ var/obj/structure/lift/button/panel_ext = new(placing, lift)
+ panel_ext.floor = cfloor
+ panel_ext.set_dir(udir)
+ cfloor.ext_panel = panel_ext
+
+ // Place lights
+ var/turf/placing1 = locate(light_x1, light_y1, cz)
+ var/turf/placing2 = locate(light_x2, light_y2, cz)
+ var/obj/machinery/light/light1 = new(placing1, light)
+ var/obj/machinery/light/light2 = new(placing2, light)
+ if(udir == NORTH || udir == SOUTH)
+ light1.set_dir(WEST)
+ light2.set_dir(EAST)
+ else
+ light1.set_dir(SOUTH)
+ light2.set_dir(NORTH)
+
+ // Update area.
+ if(az > areas_to_use.len)
+ log_debug("Insufficient defined areas in turbolift datum, aborting.")
+ qdel(src)
+ return
+
+ var/area_path = areas_to_use[az]
+ for(var/thing in floor_turfs)
+ new area_path(thing)
+ var/area/A = locate(area_path)
+ cfloor.set_area_ref("\ref[A]")
+ az++
+
+ // Place lift panel.
+ var/turf/T = locate(int_panel_x, int_panel_y, uz)
+ lift.control_panel_interior = new(T, lift)
+ lift.control_panel_interior.set_dir(udir)
+ lift.current_floor = lift.floors[uz]
+
+ lift.open_doors()
+
+ qdel(src) // We're done.
diff --git a/code/modules/turbolift/turbolift_process.dm b/code/modules/turbolift/turbolift_process.dm
new file mode 100644
index 00000000000..83ce1b9faf0
--- /dev/null
+++ b/code/modules/turbolift/turbolift_process.dm
@@ -0,0 +1,35 @@
+var/datum/controller/process/turbolift/turbolift_controller
+
+/datum/controller/process/turbolift
+ var/list/moving_lifts = list()
+
+/datum/controller/process/turbolift/New()
+ ..()
+ turbolift_controller = src
+
+/datum/controller/process/turbolift/setup()
+ name = "turbolift controller"
+ schedule_interval = 10
+
+/datum/controller/process/turbolift/doWork()
+ for(var/liftref in moving_lifts)
+ if(world.time < moving_lifts[liftref])
+ continue
+ var/datum/turbolift/lift = locate(liftref)
+ if(lift.busy)
+ continue
+ spawn(0)
+ lift.busy = 1
+ if(!lift.do_move())
+ moving_lifts[liftref] = null
+ moving_lifts -= liftref
+ if(lift.target_floor)
+ lift.target_floor.ext_panel.reset()
+ lift.target_floor = null
+ else
+ lift_is_moving(lift)
+ lift.busy = 0
+ SCHECK
+
+/datum/controller/process/turbolift/proc/lift_is_moving(var/datum/turbolift/lift)
+ moving_lifts["\ref[lift]"] = world.time + lift.move_delay
diff --git a/code/modules/turbolift/turbolift_turfs.dm b/code/modules/turbolift/turbolift_turfs.dm
new file mode 100644
index 00000000000..632160efaa1
--- /dev/null
+++ b/code/modules/turbolift/turbolift_turfs.dm
@@ -0,0 +1,2 @@
+/turf/simulated/wall/elevator/New(var/newloc)
+ ..(newloc,"elevatorium")
diff --git a/icons/obj/doors/doorlift.dmi b/icons/obj/doors/doorlift.dmi
new file mode 100644
index 00000000000..5617a988f78
Binary files /dev/null and b/icons/obj/doors/doorlift.dmi differ
diff --git a/icons/obj/turbolift.dmi b/icons/obj/turbolift.dmi
new file mode 100644
index 00000000000..cffc8aa140c
Binary files /dev/null and b/icons/obj/turbolift.dmi differ
diff --git a/icons/obj/turbolift_preview_3x3.dmi b/icons/obj/turbolift_preview_3x3.dmi
new file mode 100644
index 00000000000..f0f1ec491e1
Binary files /dev/null and b/icons/obj/turbolift_preview_3x3.dmi differ
diff --git a/icons/obj/turbolift_preview_5x5.dmi b/icons/obj/turbolift_preview_5x5.dmi
new file mode 100644
index 00000000000..e054a6b9aac
Binary files /dev/null and b/icons/obj/turbolift_preview_5x5.dmi differ
diff --git a/maps/example/example-1.dmm b/maps/example/example-1.dmm
index 6c5d1ead2d9..26c7e4bd46f 100644
--- a/maps/example/example-1.dmm
+++ b/maps/example/example-1.dmm
@@ -36,6 +36,7 @@
"J" = (/obj/machinery/light,/obj/effect/floor_decal/corner/white/diagonal,/turf/simulated/floor/tiled/dark,/area/aisat)
"K" = (/obj/structure/cable/yellow,/obj/structure/cable/yellow{d1 = 16; d2 = 0; icon_state = "16-0"},/turf/simulated/floor/plating,/area/aisat)
"L" = (/turf/simulated/floor/tiled/red,/area/aisat)
+"M" = (/obj/turbolift_map_holder/example{dir = 1; icon = 'icons/obj/turbolift_preview_5x5.dmi'},/turf/simulated/floor/tiled/red,/area/aisat)
(1,1,1) = {"
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
@@ -61,13 +62,13 @@ aaaaaaaabhhFFFhGhhhhhEbaaaaaaaaa
aaaaaaaabnbFFFhnhhhhhEbaaaaaaaaa
aaaaaaaabHbhhhhIhhhhhEbaaaaaaaaa
aaaaaaaabbbhJhhnhhJhhKbaaaaaaaaa
-aaaaaaaabbbbbLLLLLbbbbbaaaaaaaaa
+aaaaaaaabbbbbbnnnbbbbbbaaaaaaaaa
aaaaaaaaaaaabLLLLLbaaaaaaaaaaaaa
aaaaaaaaaaaabLLLLLbaaaaaaaaaaaaa
aaaaaaaaaaaabLLLLLbaaaaaaaaaaaaa
aaaaaaaaaaaabLLLLLbaaaaaaaaaaaaa
+aaaaaaaaaaaabMLLLLbaaaaaaaaaaaaa
aaaaaaaaaaaabbbbbbbaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
"}
diff --git a/maps/example/example-2.dmm b/maps/example/example-2.dmm
index f108e93b91a..9c0fd272dcc 100644
--- a/maps/example/example-2.dmm
+++ b/maps/example/example-2.dmm
@@ -57,7 +57,8 @@ aaaaaaaaccciiiioiiiiiDcaaaaaaaaa
aaaaaaaacEciiiioiiiiiDcaaaaaaaaa
aaaaaaaacEciiiiFiiiiiDcaaaaaaaaa
aaaaaaaaciiiGiioiiGiiHcaaaaaaaaa
-aaaaaaaacccccEEEEEcccccaaaaaaaaa
+aaaaaaaaccccccoooccccccaaaaaaaaa
+aaaaaaaaaaaacEEEEEcaaaaaaaaaaaaa
aaaaaaaaaaaacEEEEEcaaaaaaaaaaaaa
aaaaaaaaaaaacEEEEEcaaaaaaaaaaaaa
aaaaaaaaaaaacEEEEEcaaaaaaaaaaaaa
@@ -65,5 +66,4 @@ aaaaaaaaaaaacEEEEEcaaaaaaaaaaaaa
aaaaaaaaaaaacccccccaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
"}
diff --git a/maps/example/example.dm b/maps/example/example.dm
index d5e18ea3665..a103a326257 100644
--- a/maps/example/example.dm
+++ b/maps/example/example.dm
@@ -4,6 +4,8 @@
#include "example-2.dmm"
#include "example_defines.dm"
+ #include "example_elevator.dm"
+ #include "example_areas.dm"
#define USING_MAP_DATUM /datum/map/example
diff --git a/maps/example/example_areas.dm b/maps/example/example_areas.dm
new file mode 100644
index 00000000000..8e01e14055c
--- /dev/null
+++ b/maps/example/example_areas.dm
@@ -0,0 +1,13 @@
+// Elevator areas.
+/area/turbolift/example_top
+ name = "lift (top floor)"
+ lift_floor_label = "Floor 2"
+ lift_floor_name = "Top Floor"
+ lift_announce_str = "Arriving at Top Floor."
+
+/area/turbolift/example_ground
+ name = "lift (ground floor)"
+ lift_floor_label = "Floor 1"
+ lift_floor_name = "First Floor"
+ lift_announce_str = "Arriving at First Floor."
+ base_turf = /turf/simulated/floor
\ No newline at end of file
diff --git a/maps/example/example_elevator.dm b/maps/example/example_elevator.dm
new file mode 100644
index 00000000000..4e279f3df7c
--- /dev/null
+++ b/maps/example/example_elevator.dm
@@ -0,0 +1,11 @@
+/obj/turbolift_map_holder/example
+ name = "Example turbolift map placeholder"
+ depth = 2
+ lift_size_x = 4
+ lift_size_y = 4
+ icon = 'icons/obj/turbolift_preview_5x5.dmi'
+
+ areas_to_use = list(
+ /area/turbolift/example_ground,
+ /area/turbolift/example_top
+ )
\ No newline at end of file
diff --git a/polaris.dme b/polaris.dme
index 45b45aaa9ec..d34d6834fd2 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -2055,6 +2055,15 @@
#include "code\modules\tables\rack.dm"
#include "code\modules\tables\tables.dm"
#include "code\modules\tables\update_triggers.dm"
+#include "code\modules\turbolift\_turbolift.dm"
+#include "code\modules\turbolift\turbolift.dm"
+#include "code\modules\turbolift\turbolift_areas.dm"
+#include "code\modules\turbolift\turbolift_console.dm"
+#include "code\modules\turbolift\turbolift_door.dm"
+#include "code\modules\turbolift\turbolift_floor.dm"
+#include "code\modules\turbolift\turbolift_map.dm"
+#include "code\modules\turbolift\turbolift_process.dm"
+#include "code\modules\turbolift\turbolift_turfs.dm"
#include "code\modules\vehicles\cargo_train.dm"
#include "code\modules\vehicles\train.dm"
#include "code\modules\vehicles\vehicle.dm"