diff --git a/code/__DEFINES/shuttles.dm b/code/__DEFINES/shuttles.dm
index 36ecebb699..80983b34be 100644
--- a/code/__DEFINES/shuttles.dm
+++ b/code/__DEFINES/shuttles.dm
@@ -7,6 +7,8 @@
#define SHUTTLE_STRANDED "stranded"
#define SHUTTLE_ESCAPE "escape"
#define SHUTTLE_ENDGAME "endgame: game over"
+#define SHUTTLE_RECHARGING "recharging"
+#define SHUTTLE_PREARRIVAL "landing"
#define EMERGENCY_IDLE_OR_RECALLED (SSshuttle.emergency && ((SSshuttle.emergency.mode == SHUTTLE_IDLE) || (SSshuttle.emergency.mode == SHUTTLE_RECALL)))
#define EMERGENCY_ESCAPED_OR_ENDGAMED (SSshuttle.emergency && ((SSshuttle.emergency.mode == SHUTTLE_ESCAPE) || (SSshuttle.emergency.mode == SHUTTLE_ENDGAME)))
diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm
index 7e7d939c39..435a35c05e 100644
--- a/code/controllers/configuration/entries/game_options.dm
+++ b/code/controllers/configuration/entries/game_options.dm
@@ -521,3 +521,10 @@
* Camera mobs, AIs, ghosts and some other are of course exempt from this. This also doesn't influence simplemob AI, for the best.
*/
/datum/config_entry/flag/use_field_of_vision
+
+//Shuttle size limiter
+/datum/config_entry/number/max_shuttle_count
+ config_entry_value = 6
+
+/datum/config_entry/number/max_shuttle_size
+ config_entry_value = 250
diff --git a/code/game/area/areas/shuttles.dm b/code/game/area/areas/shuttles.dm
index cec0067e4b..b0d8a71cfc 100644
--- a/code/game/area/areas/shuttles.dm
+++ b/code/game/area/areas/shuttles.dm
@@ -103,6 +103,10 @@
/area/shuttle/custom
name = "Custom player shuttle"
+/area/shuttle/custom/powered
+ name = "Custom Powered player shuttle"
+ requires_power = FALSE
+
/area/shuttle/arrival
name = "Arrival Shuttle"
unique = TRUE // SSjob refers to this area for latejoiners
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index a83eaed9d3..e2ac541a51 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -846,6 +846,9 @@
/atom/proc/GenerateTag()
return
+/atom/proc/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock, idnum, override=FALSE)
+ return
+
// Generic logging helper
/atom/proc/log_message(message, message_type, color=null, log_globally=TRUE)
if(!log_globally)
diff --git a/code/game/machinery/computer/camera.dm b/code/game/machinery/computer/camera.dm
index 450967050c..2a166c2cdc 100644
--- a/code/game/machinery/computer/camera.dm
+++ b/code/game/machinery/computer/camera.dm
@@ -35,6 +35,11 @@
M.unset_machine() //to properly reset the view of the users if the console is deleted.
return ..()
+/obj/machinery/computer/security/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock, idnum, override=FALSE)
+ for(var/i in network)
+ network -= i
+ network += "[idnum][i]"
+
/obj/machinery/computer/security/can_interact(mob/user)
if((!hasSiliconAccessInArea(user) && !Adjacent(user)) || is_blind(user) || !in_view_range(user, src))
return FALSE
diff --git a/code/game/machinery/shuttle/custom_shuttle.dm b/code/game/machinery/shuttle/custom_shuttle.dm
new file mode 100644
index 0000000000..7c83cade69
--- /dev/null
+++ b/code/game/machinery/shuttle/custom_shuttle.dm
@@ -0,0 +1,33 @@
+/obj/machinery/shuttle
+ name = "shuttle component"
+ desc = "Something for shuttles."
+ density = TRUE
+ obj_integrity = 250
+ max_integrity = 250
+ icon = 'icons/turf/shuttle.dmi'
+ icon_state = "burst_plasma"
+ idle_power_usage = 150
+ circuit = /obj/item/circuitboard/machine/shuttle/engine
+ var/icon_state_closed = "burst_plasma"
+ var/icon_state_open = "burst_plasma_open"
+ var/icon_state_off = "burst_plasma_off"
+
+/obj/machinery/shuttle/Initialize()
+ . = ..()
+ GLOB.custom_shuttle_machines += src
+
+/obj/machinery/shuttle/Destroy()
+ . = ..()
+ GLOB.custom_shuttle_machines -= src
+
+/obj/machinery/shuttle/attackby(obj/item/I, mob/living/user, params)
+ if(default_deconstruction_screwdriver(user, icon_state_open, icon_state_closed, I))
+ return
+ if(default_pry_open(I))
+ return
+ if(panel_open)
+ if(default_change_direction_wrench(user, I))
+ return
+ if(default_deconstruction_crowbar(I))
+ return
+ return ..()
diff --git a/code/game/machinery/shuttle/shuttle_engine.dm b/code/game/machinery/shuttle/shuttle_engine.dm
new file mode 100644
index 0000000000..bdc16c565b
--- /dev/null
+++ b/code/game/machinery/shuttle/shuttle_engine.dm
@@ -0,0 +1,138 @@
+//-----------------------------------------------
+//-------------Engine Thrusters------------------
+//-----------------------------------------------
+
+#define ENGINE_HEAT_TARGET 600
+#define ENGINE_HEATING_POWER 5000000
+
+/obj/machinery/shuttle/engine
+ name = "shuttle thruster"
+ desc = "A thruster for shuttles."
+ density = TRUE
+ obj_integrity = 250
+ max_integrity = 250
+ icon = 'icons/turf/shuttle.dmi'
+ icon_state = "burst_plasma"
+ idle_power_usage = 150
+ circuit = /obj/item/circuitboard/machine/shuttle/engine
+ var/thrust = 0
+ var/fuel_use = 0
+ var/bluespace_capable = TRUE
+ var/cooldown = 0
+ var/thruster_active = FALSE
+ var/datum/weakref/attached_heater
+
+/obj/machinery/shuttle/engine/plasma
+ name = "plasma thruster"
+ desc = "A thruster that burns plasma stored in an adjacent plasma thruster heater."
+ icon_state = "burst_plasma"
+ icon_state_off = "burst_plasma_off"
+
+ idle_power_usage = 0
+ circuit = /obj/item/circuitboard/machine/shuttle/engine/plasma
+ thrust = 25
+ fuel_use = 0.24
+ bluespace_capable = FALSE
+ cooldown = 45
+
+/obj/machinery/shuttle/engine/void
+ name = "void thruster"
+ desc = "A thruster using technology to breach voidspace for propulsion."
+ icon_state = "burst_void"
+ icon_state_off = "burst_void"
+ icon_state_closed = "burst_void"
+ icon_state_open = "burst_void_open"
+ idle_power_usage = 0
+ circuit = /obj/item/circuitboard/machine/shuttle/engine/void
+ thrust = 400
+ fuel_use = 0
+ bluespace_capable = TRUE
+ cooldown = 90
+
+/obj/machinery/shuttle/engine/Initialize()
+ . = ..()
+ check_setup()
+
+/obj/machinery/shuttle/engine/on_construction()
+ . = ..()
+ check_setup()
+
+/obj/machinery/shuttle/engine/proc/check_setup()
+ var/heater_turf
+ switch(dir)
+ if(NORTH)
+ heater_turf = get_offset_target_turf(src, 0, 1)
+ if(SOUTH)
+ heater_turf = get_offset_target_turf(src, 0, -1)
+ if(EAST)
+ heater_turf = get_offset_target_turf(src, 1, 0)
+ if(WEST)
+ heater_turf = get_offset_target_turf(src, -1, 0)
+ if(!heater_turf)
+ attached_heater = null
+ update_engine()
+ return
+ attached_heater = null
+ for(var/obj/machinery/atmospherics/components/unary/shuttle/heater/as_heater in heater_turf)
+ if(as_heater.dir != dir)
+ continue
+ if(as_heater.panel_open)
+ continue
+ if(!as_heater.anchored)
+ continue
+ attached_heater = WEAKREF(as_heater)
+ break
+ update_engine()
+ return
+
+/obj/machinery/shuttle/engine/proc/update_engine()
+ if(!attached_heater)
+ icon_state = icon_state_off
+ thruster_active = FALSE
+ return
+ var/obj/machinery/atmospherics/components/unary/shuttle/heater/resolved_heater = attached_heater.resolve()
+ if(panel_open)
+ thruster_active = FALSE
+ else if(resolved_heater?.hasFuel(1))
+ icon_state = icon_state_closed
+ thruster_active = TRUE
+ else
+ thruster_active = FALSE
+ icon_state = icon_state_off
+ return
+
+/obj/machinery/shuttle/engine/void/update_engine()
+ if(panel_open)
+ thruster_active = FALSE
+ return
+ thruster_active = TRUE
+ icon_state = icon_state_closed
+ return
+
+//Thanks to spaceheater.dm for inspiration :)
+/obj/machinery/shuttle/engine/proc/fireEngine()
+ var/turf/heatTurf = loc
+ if(!heatTurf)
+ return
+ var/datum/gas_mixture/env = heatTurf.return_air()
+ var/heat_cap = env.heat_capacity()
+ var/req_power = abs(env.return_temperature() - ENGINE_HEAT_TARGET) * heat_cap
+ req_power = min(req_power, ENGINE_HEATING_POWER)
+ var/deltaTemperature = req_power / heat_cap
+ if(deltaTemperature < 0)
+ return
+ env.temperature += deltaTemperature
+ air_update_turf()
+
+/obj/machinery/shuttle/engine/attackby(obj/item/I, mob/living/user, params)
+ check_setup()
+ if(default_deconstruction_screwdriver(user, icon_state_open, icon_state_closed, I))
+ return
+ if(default_pry_open(I))
+ return
+ if(panel_open)
+ if(default_change_direction_wrench(user, I))
+ return
+ if(default_deconstruction_crowbar(I))
+ return
+ return ..()
diff --git a/code/game/machinery/shuttle/shuttle_heater.dm b/code/game/machinery/shuttle/shuttle_heater.dm
new file mode 100644
index 0000000000..3c36e53b5a
--- /dev/null
+++ b/code/game/machinery/shuttle/shuttle_heater.dm
@@ -0,0 +1,132 @@
+//-----------------------------------------------
+//--------------Engine Heaters-------------------
+//This uses atmospherics, much like a thermomachine,
+//but instead of changing temp, it stores plasma and uses
+//it for the engine.
+//-----------------------------------------------
+/obj/machinery/atmospherics/components/unary/shuttle
+ name = "shuttle atmospherics device"
+ desc = "This does something to do with shuttle atmospherics"
+ icon_state = "heater"
+ icon = 'icons/turf/shuttle.dmi'
+
+/obj/machinery/atmospherics/components/unary/shuttle/heater
+ name = "engine heater"
+ desc = "Directs energy into compressed particles in order to power an attached thruster."
+ icon_state = "heater_pipe"
+ var/icon_state_closed = "heater_pipe"
+ var/icon_state_open = "heater_pipe_open"
+ var/icon_state_off = "heater_pipe"
+ idle_power_usage = 50
+ circuit = /obj/item/circuitboard/machine/shuttle/heater
+
+ density = TRUE
+ max_integrity = 400
+ armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 100, "bomb" = 0, "bio" = 100, "rad" = 100, "fire" = 100, "acid" = 30)
+ layer = OBJ_LAYER
+ showpipe = TRUE
+
+ pipe_flags = PIPING_ONE_PER_TURF | PIPING_DEFAULT_LAYER_ONLY
+
+ var/gas_type = /datum/gas/plasma
+ var/efficiency_multiplier = 1
+ var/gas_capacity = 0
+
+/obj/machinery/atmospherics/components/unary/shuttle/heater/New()
+ . = ..()
+ GLOB.custom_shuttle_machines += src
+ SetInitDirections()
+ update_adjacent_engines()
+ updateGasStats()
+
+/obj/machinery/atmospherics/components/unary/shuttle/heater/Destroy()
+ . = ..()
+ update_adjacent_engines()
+ GLOB.custom_shuttle_machines -= src
+
+/obj/machinery/atmospherics/components/unary/shuttle/heater/on_construction()
+ ..(dir, dir)
+ SetInitDirections()
+ update_adjacent_engines()
+
+/obj/machinery/atmospherics/components/unary/shuttle/heater/default_change_direction_wrench(mob/user, obj/item/I)
+ if(!..())
+ return FALSE
+ SetInitDirections()
+ var/obj/machinery/atmospherics/node = nodes[1]
+ if(node)
+ node.disconnect(src)
+ nodes[1] = null
+ if(!parents[1])
+ return
+ nullifyPipenet(parents[1])
+
+ atmosinit()
+ node = nodes[1]
+ if(node)
+ node.atmosinit()
+ node.addMember(src)
+ build_network()
+ return TRUE
+
+/obj/machinery/atmospherics/components/unary/shuttle/heater/RefreshParts()
+ var/cap = 0
+ var/eff = 0
+ for(var/obj/item/stock_parts/matter_bin/M in component_parts)
+ cap += M.rating
+ for(var/obj/item/stock_parts/micro_laser/L in component_parts)
+ eff += L.rating
+ gas_capacity = 5000 * ((cap - 1) ** 2) + 1000
+ efficiency_multiplier = round(((eff / 2) / 2.8) ** 2, 0.1)
+ updateGasStats()
+
+/obj/machinery/atmospherics/components/unary/shuttle/heater/examine(mob/user)
+ . = ..()
+ var/datum/gas_mixture/air_contents = airs[1]
+ . += "The engine heater's gas dial reads [air_contents.return_volume()] liters in internal tank.
"
+
+/obj/machinery/atmospherics/components/unary/shuttle/heater/proc/updateGasStats()
+ var/datum/gas_mixture/air_contents = airs[1]
+ if(!air_contents)
+ return
+ air_contents.volume = gas_capacity
+ air_contents.temperature = T20C
+
+/obj/machinery/atmospherics/components/unary/shuttle/heater/proc/hasFuel(var/required)
+ var/datum/gas_mixture/air_contents = airs[1]
+ var/moles = air_contents.total_moles()
+ return moles >= required
+
+/obj/machinery/atmospherics/components/unary/shuttle/heater/proc/consumeFuel(var/amount)
+ var/datum/gas_mixture/air_contents = airs[1]
+ air_contents.remove(amount)
+ return
+
+/obj/machinery/atmospherics/components/unary/shuttle/heater/attackby(obj/item/I, mob/living/user, params)
+ update_adjacent_engines()
+ if(default_deconstruction_screwdriver(user, icon_state_open, icon_state_closed, I))
+ return
+ if(default_pry_open(I))
+ return
+ if(panel_open)
+ if(default_change_direction_wrench(user, I))
+ return
+ if(default_deconstruction_crowbar(I))
+ return
+ return ..()
+
+/obj/machinery/atmospherics/components/unary/shuttle/heater/proc/update_adjacent_engines()
+ var/engine_turf
+ switch(dir)
+ if(NORTH)
+ engine_turf = get_offset_target_turf(src, 0, -1)
+ if(SOUTH)
+ engine_turf = get_offset_target_turf(src, 0, 1)
+ if(EAST)
+ engine_turf = get_offset_target_turf(src, -1, 0)
+ if(WEST)
+ engine_turf = get_offset_target_turf(src, 1, 0)
+ if(!engine_turf)
+ return
+ for(var/obj/machinery/shuttle/engine/E in engine_turf)
+ E.check_setup()
diff --git a/code/game/machinery/status_display.dm b/code/game/machinery/status_display.dm
index 02bb07b8ed..3bb67e2a1c 100644
--- a/code/game/machinery/status_display.dm
+++ b/code/game/machinery/status_display.dm
@@ -290,7 +290,7 @@
if("shuttle_id")
update()
-/obj/machinery/status_display/shuttle/proc/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock, idnum, override)
+/obj/machinery/status_display/shuttle/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock, idnum, override)
if (port && (shuttle_id == initial(shuttle_id) || override))
shuttle_id = port.id
update()
diff --git a/code/game/objects/items/circuitboards/computer_circuitboards.dm b/code/game/objects/items/circuitboards/computer_circuitboards.dm
index 1b378dcfb1..ae579054b1 100644
--- a/code/game/objects/items/circuitboards/computer_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/computer_circuitboards.dm
@@ -379,3 +379,11 @@
/obj/item/circuitboard/computer/nanite_cloud_controller
name = "Nanite Cloud Control (Computer Board)"
build_path = /obj/machinery/computer/nanite_cloud_controller
+
+/obj/item/circuitboard/computer/shuttle/flight_control
+ name = "Shuttle Flight Control (Computer Board)"
+ build_path = /obj/machinery/computer/custom_shuttle
+
+/obj/item/circuitboard/computer/shuttle/docker
+ name = "Shuttle Navigation Computer (Computer Board)"
+ build_path = /obj/machinery/computer/camera_advanced/shuttle_docker/custom
diff --git a/code/game/objects/items/circuitboards/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machine_circuitboards.dm
index b90fd1c1af..ea839ae12d 100644
--- a/code/game/objects/items/circuitboards/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machine_circuitboards.dm
@@ -1102,3 +1102,28 @@
/obj/item/stock_parts/micro_laser = 2,
/obj/item/stock_parts/scanning_module = 2
)
+
+/obj/item/circuitboard/machine/shuttle/engine
+ name = "Thruster (Machine Board)"
+ build_path = /obj/machinery/shuttle/engine
+ req_components = list()
+
+/obj/item/circuitboard/machine/shuttle/engine/plasma
+ name = "Plasma Thruster (Machine Board)"
+ build_path = /obj/machinery/shuttle/engine/plasma
+ req_components = list(/obj/item/stock_parts/capacitor = 2,
+ /obj/item/stack/cable_coil = 5,
+ /obj/item/stock_parts/micro_laser = 1)
+
+/obj/item/circuitboard/machine/shuttle/engine/void
+ name = "Void Thruster (Machine Board)"
+ build_path = /obj/machinery/shuttle/engine/void
+ req_components = list(/obj/item/stock_parts/capacitor/quadratic = 2,
+ /obj/item/stack/cable_coil = 5,
+ /obj/item/stock_parts/micro_laser/quadultra = 1)
+
+/obj/item/circuitboard/machine/shuttle/heater
+ name = "Electronic Engine Heater (Machine Board)"
+ build_path = /obj/machinery/atmospherics/components/unary/shuttle/heater
+ req_components = list(/obj/item/stock_parts/micro_laser = 2,
+ /obj/item/stock_parts/matter_bin = 1)
diff --git a/code/modules/research/designs/electronics_designs.dm b/code/modules/research/designs/electronics_designs.dm
index 4e9238c4c7..806e2ef75c 100644
--- a/code/modules/research/designs/electronics_designs.dm
+++ b/code/modules/research/designs/electronics_designs.dm
@@ -167,4 +167,34 @@
id = "xenobio_slimeadv"
build_path = /obj/item/disk/xenobio_console_upgrade/slimeadv
+/datum/design/board/shuttle/engine/plasma
+ name = "Machine Design (Plasma Thruster Board)"
+ desc = "The circuit board for a plasma thruster."
+ id = "engine_plasma"
+ build_path = /obj/item/circuitboard/machine/shuttle/engine/plasma
+ category = list ("Shuttle Machinery")
+ departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING | DEPARTMENTAL_FLAG_SCIENCE
+/datum/design/board/shuttle/engine/void
+ name = "Machine Design (Void Thruster Board)"
+ desc = "The circuit board for a void thruster."
+ id = "engine_void"
+ build_path = /obj/item/circuitboard/machine/shuttle/engine/void
+ category = list ("Shuttle Machinery")
+ departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING | DEPARTMENTAL_FLAG_SCIENCE
+
+/datum/design/board/shuttle/engine/heater
+ name = "Machine Design (Engine Heater Board)"
+ desc = "The circuit board for an engine heater."
+ id = "engine_heater"
+ build_path = /obj/item/circuitboard/machine/shuttle/heater
+ category = list ("Shuttle Machinery")
+ departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING | DEPARTMENTAL_FLAG_SCIENCE
+
+/obj/item/circuitboard/computer/shuttle/flight_control
+ name = "Shuttle Flight Control (Computer Board)"
+ build_path = /obj/machinery/computer/custom_shuttle
+
+/obj/item/circuitboard/computer/shuttle/docker
+ name = "Shuttle Navigation Computer (Computer Board)"
+ build_path = /obj/machinery/computer/camera_advanced/shuttle_docker/custom
diff --git a/code/modules/research/designs/misc_designs.dm b/code/modules/research/designs/misc_designs.dm
index 9b44264b07..c59ee3ec61 100644
--- a/code/modules/research/designs/misc_designs.dm
+++ b/code/modules/research/designs/misc_designs.dm
@@ -760,4 +760,34 @@
materials = list(/datum/material/iron = 1000)
build_path = /obj/item/tank/internals/emergency_oxygen/engi/empty
category = list("Equipment")
- departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING
\ No newline at end of file
+ departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING
+
+/datum/design/shuttle_speed_upgrade
+ name = "Shuttle Route Optimisation Upgrade"
+ desc = "A disk that allows for calculating shorter routes when inserted into a flight control console."
+ id = "disk_shuttle_route"
+ build_type = PROTOLATHE
+ materials = list(/datum/material/iron = 1000, /datum/material/glass = 1000)
+ build_path = /obj/item/shuttle_route_optimisation
+ category = list("Equipment")
+ departmental_flags = DEPARTMENTAL_FLAG_SCIENCE | DEPARTMENTAL_FLAG_ENGINEERING
+
+/datum/design/shuttle_speed_upgrade_hyper
+ name = "Shuttle Bluespace Hyperlane Optimisation Upgrade"
+ desc = "A disk that allows for calculating shorter routes when inserted into a flight control console. This one abuses bluespace hyperlanes for increased efficiency."
+ id = "disk_shuttle_route_hyper"
+ build_type = PROTOLATHE
+ materials = list(/datum/material/iron = 1000, /datum/material/glass = 1000)
+ build_path = /obj/item/shuttle_route_optimisation/hyperlane
+ category = list("Equipment")
+ departmental_flags = DEPARTMENTAL_FLAG_SCIENCE | DEPARTMENTAL_FLAG_ENGINEERING
+
+/datum/design/shuttle_speed_upgrade_void
+ name = "Shuttle Voidspace Optimisation Upgrade"
+ desc = "A disk that allows for calculating shorter routes when inserted into a flight control console. This one access voidspace for increased efficiency."
+ id = "disk_shuttle_route_void"
+ build_type = PROTOLATHE
+ materials = list(/datum/material/iron = 1000, /datum/material/glass = 1000)
+ build_path = /obj/item/shuttle_route_optimisation/void
+ category = list("Equipment")
+ departmental_flags = DEPARTMENTAL_FLAG_SCIENCE | DEPARTMENTAL_FLAG_ENGINEERING
\ No newline at end of file
diff --git a/code/modules/research/designs/tool_designs.dm b/code/modules/research/designs/tool_designs.dm
index e500166e76..551d6fa0e3 100644
--- a/code/modules/research/designs/tool_designs.dm
+++ b/code/modules/research/designs/tool_designs.dm
@@ -42,6 +42,16 @@
category = list("Tool Designs")
departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING
+/datum/design/shuttlecreator
+ name = "Rapid Shuttle Designator"
+ desc = "An advanced device capable of defining areas for use in the creation of shuttles"
+ id = "shuttle_creator"
+ build_path = /obj/item/shuttle_creator
+ build_type = PROTOLATHE
+ materials = list(/datum/material/iron = 8000, /datum/material/titanium = 5000, /datum/material/bluespace = 5000)
+ category = list("Tool Designs")
+ departmental_flags = DEPARTMENTAL_FLAG_SCIENCE | DEPARTMENTAL_FLAG_ENGINEERING
+
/datum/design/handdrill
name = "Hand Drill"
desc = "A small electric hand drill with an interchangeable screwdriver and bolt bit"
diff --git a/code/modules/research/techweb/nodes/bluespace_nodes.dm b/code/modules/research/techweb/nodes/bluespace_nodes.dm
index 85142cd9b8..09dd4cdd06 100644
--- a/code/modules/research/techweb/nodes/bluespace_nodes.dm
+++ b/code/modules/research/techweb/nodes/bluespace_nodes.dm
@@ -63,3 +63,36 @@
prereq_ids = list("bluespace_warping", "syndicate_basic")
design_ids = list("desynchronizer")
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
+
+/////////////////////////shuttle tech/////////////////////////
+/datum/techweb_node/basic_shuttle_tech
+ id = "basic_shuttle"
+ display_name = "Basic Shuttle Research"
+ description = "Research the technology required to create and use basic shuttles."
+ prereq_ids = list("bluespace_travel", "adv_engi")
+ design_ids = list("shuttle_creator", "engine_plasma", "engine_heater", "shuttle_control", "shuttle_docker")
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 5000)
+
+/datum/techweb_node/shuttle_route_upgrade
+ id = "shuttle_route_upgrade"
+ display_name = "Route Optimisation Upgrade"
+ description = "Research into bluespace tunnelling, allowing us to reduce flight times by up to 20%!"
+ prereq_ids = list("basic_shuttle")
+ design_ids = list("disk_shuttle_route")
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
+
+/datum/techweb_node/shuttle_route_upgrade_hyper
+ id = "shuttle_route_upgrade_hyper"
+ display_name = "Hyperlane Optimisation Upgrade"
+ description = "Research into bluespace hyperlane, allowing us to reduce flight times by up to 40%!"
+ prereq_ids = list("shuttle_route_upgrade", "micro_bluespace")
+ design_ids = list("disk_shuttle_route_hyper")
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 5000)
+
+/datum/techweb_node/shuttle_route_upgrade_void
+ id = "shuttle_route_upgrade_void"
+ display_name = "Nullspace Breaching Upgrade"
+ description = "Research into voidspace tunnelling, allowing us to significantly reduce flight times."
+ prereq_ids = list("shuttle_route_upgrade_hyper", "alientech")
+ design_ids = list("disk_shuttle_route_void", "engine_void")
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 12500)
diff --git a/code/modules/shuttle/computer.dm b/code/modules/shuttle/computer.dm
index d2b13fc71a..75bf55a5a3 100644
--- a/code/modules/shuttle/computer.dm
+++ b/code/modules/shuttle/computer.dm
@@ -71,6 +71,6 @@
to_chat(user, "You fried the consoles ID checking system.")
return TRUE
-/obj/machinery/computer/shuttle/proc/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock, idnum, override=FALSE)
+/obj/machinery/computer/shuttle/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock, idnum, override=FALSE)
if(port && (shuttleId == initial(shuttleId) || override))
shuttleId = port.id
\ No newline at end of file
diff --git a/code/modules/shuttle/custom_shuttle.dm b/code/modules/shuttle/custom_shuttle.dm
new file mode 100644
index 0000000000..6e06e3fefe
--- /dev/null
+++ b/code/modules/shuttle/custom_shuttle.dm
@@ -0,0 +1,268 @@
+#define Z_DIST 500
+#define CUSTOM_ENGINES_START_TIME 65
+#define CALCULATE_STATS_COOLDOWN 2
+
+/obj/machinery/computer/custom_shuttle
+ name = "nanotrasen shuttle flight controller"
+ desc = "A terminal used to fly shuttles defined by the Shuttle Zoning Designator"
+ circuit = /obj/item/circuitboard/computer/shuttle/flight_control
+ icon_screen = "shuttle"
+ icon_keyboard = "tech_key"
+ light_color = LIGHT_COLOR_CYAN
+ req_access = list( )
+ var/shuttleId
+ var/possible_destinations = "whiteship_home"
+ var/admin_controlled
+ var/no_destination_swap = 0
+ var/calculated_mass = 0
+ var/calculated_dforce = 0
+ var/calculated_speed = 0
+ var/calculated_engine_count = 0
+ var/calculated_consumption = 0
+ var/calculated_cooldown = 0
+ var/calculated_non_operational_thrusters = 0
+ var/calculated_fuel_less_thrusters = 0
+ var/target_fuel_cost = 0
+ var/targetLocation
+ var/datum/browser/popup
+
+ var/stat_calc_cooldown = 0
+
+ //Upgrades
+ var/distance_multiplier = 1
+
+/obj/machinery/computer/custom_shuttle/examine(mob/user)
+ . = ..()
+ . += distance_multiplier < 1 ? "Bluespace shortcut module installed. Route is [distance_multiplier]x the original length." : ""
+
+/obj/machinery/computer/custom_shuttle/ui_interact(mob/user)
+ var/list/options = params2list(possible_destinations)
+ var/obj/docking_port/mobile/M = SSshuttle.getShuttle(shuttleId)
+ var/dat = "[M ? "Current Location : [M.getStatusText()]" : "Shuttle link required."]
"
+ if(M)
+ dat += "Run Flight Calculations
"
+ dat += "Shuttle Data
"
+ dat += "Shuttle Mass: [calculated_mass/10]tons
"
+ dat += "Engine Force: [calculated_dforce]kN ([calculated_engine_count] engines)
"
+ dat += "Sublight Speed: [calculated_speed]ms-1
"
+ dat += calculated_speed < 1 ? "INSUFFICIENT ENGINE POWER
" : ""
+ dat += calculated_non_operational_thrusters > 0 ? "Warning: [calculated_non_operational_thrusters] thrusters offline.
" : ""
+ dat += "Fuel Consumption: [calculated_consumption]units per distance
"
+ dat += "Engine Cooldown: [calculated_cooldown]s
"
+ var/destination_found
+ for(var/obj/docking_port/stationary/S in SSshuttle.stationary)
+ if(!options.Find(S.id))
+ continue
+ if(!M.check_dock(S, silent=TRUE))
+ continue
+ if(calculated_speed == 0)
+ break
+ destination_found = TRUE
+ var/dist = round(calculateDistance(S))
+ dat += "Target [S.name] (Dist: [dist] | Fuel Cost: [round(dist * calculated_consumption)] | Time: [round(dist / calculated_speed)])
"
+ if(!destination_found)
+ dat += "No valid destinations
"
+ dat += "
[targetLocation ? "Target Location : [targetLocation]" : "No Target Location"]"
+ dat += "
Initate Flight
"
+ dat += "Close"
+
+ popup = new(user, "computer", M ? M.name : "shuttle", 350, 450)
+ popup.set_content("[dat]")
+ popup.set_title_image(usr.browse_rsc_icon(src.icon, src.icon_state))
+ popup.open()
+
+/obj/machinery/computer/custom_shuttle/Topic(href, href_list)
+ if(..())
+ return
+ usr.set_machine(src)
+ src.add_fingerprint(usr)
+ if(!allowed(usr))
+ to_chat(usr, "Access denied.")
+ return
+
+ if(href_list["calculate"])
+ calculateStats()
+ ui_interact(usr)
+ return
+ var/obj/docking_port/mobile/M = SSshuttle.getShuttle(shuttleId)
+ if(!M)
+ return
+ if(M.launch_status == ENDGAME_LAUNCHED)
+ return
+ if(href_list["setloc"])
+ SetTargetLocation(href_list["setloc"])
+ ui_interact(usr)
+ return
+ else if(href_list["fly"])
+ Fly()
+ ui_interact(usr)
+ return
+
+/obj/machinery/computer/custom_shuttle/proc/calculateDistance(var/obj/docking_port/stationary/port)
+ var/deltaX = port.x - x
+ var/deltaY = port.y - y
+ var/deltaZ = (port.z - z) * Z_DIST
+ return sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) * distance_multiplier
+
+/obj/machinery/computer/custom_shuttle/proc/linkShuttle(var/new_id)
+ shuttleId = new_id
+ possible_destinations = "whiteship_home;shuttle[new_id]_custom"
+
+/obj/machinery/computer/custom_shuttle/proc/calculateStats(var/useFuel = FALSE, var/dist = 0, var/ignore_cooldown = FALSE)
+ if(!ignore_cooldown && stat_calc_cooldown >= world.time)
+ to_chat(usr, "You are using this too fast, please slow down")
+ return
+ stat_calc_cooldown = world.time + CALCULATE_STATS_COOLDOWN
+ var/obj/docking_port/mobile/M = SSshuttle.getShuttle(shuttleId)
+ if(!M)
+ return FALSE
+ //Reset data
+ calculated_mass = 0
+ calculated_dforce = 0
+ calculated_speed = 0
+ calculated_engine_count = 0
+ calculated_consumption = 0
+ calculated_cooldown = 0
+ calculated_fuel_less_thrusters = 0
+ calculated_non_operational_thrusters = 0
+ //Calculate all the data
+ var/list/areas = M.shuttle_areas
+ for(var/shuttleArea in areas)
+ calculated_mass += length(get_area_turfs(shuttleArea))
+ for(var/obj/machinery/shuttle/engine/E in shuttleArea)
+ E.check_setup()
+ if(!E.thruster_active) //Skipover thrusters with no valid heater
+ calculated_non_operational_thrusters ++
+ continue
+ if(E.attached_heater)
+ var/obj/machinery/atmospherics/components/unary/shuttle/heater/resolvedHeater = E.attached_heater.resolve()
+ if(resolvedHeater && !resolvedHeater.hasFuel(dist * E.fuel_use) && useFuel)
+ calculated_fuel_less_thrusters ++
+ continue
+ calculated_engine_count++
+ calculated_dforce += E.thrust
+ calculated_consumption += E.fuel_use
+ calculated_cooldown = max(calculated_cooldown, E.cooldown)
+ //This should really be accelleration, but its a 2d spessman game so who cares
+ if(calculated_mass == 0)
+ return FALSE
+ calculated_speed = (calculated_dforce*1000) / (calculated_mass*100)
+ return TRUE
+
+/obj/machinery/computer/custom_shuttle/proc/consumeFuel(var/dist)
+ var/obj/docking_port/mobile/M = SSshuttle.getShuttle(shuttleId)
+ if(!M)
+ return FALSE
+ //Calculate all the data
+ for(var/obj/machinery/shuttle/engine/shuttle_machine in GLOB.custom_shuttle_machines)
+ shuttle_machine.check_setup()
+ if(!shuttle_machine.thruster_active)
+ continue
+ if(get_area(M) != get_area(shuttle_machine))
+ continue
+ if(shuttle_machine.attached_heater)
+ var/obj/machinery/atmospherics/components/unary/shuttle/heater/resolvedHeater = shuttle_machine.attached_heater.resolve()
+ if(resolvedHeater && !resolvedHeater.hasFuel(dist * shuttle_machine.fuel_use))
+ continue
+ resolvedHeater?.consumeFuel(dist * shuttle_machine.fuel_use)
+ shuttle_machine.fireEngine()
+
+/obj/machinery/computer/custom_shuttle/proc/SetTargetLocation(var/newTarget)
+ if(!(newTarget in params2list(possible_destinations)))
+ log_admin("[usr] attempted to href dock exploit on [src] with target location \"[newTarget]\"")
+ message_admins("[usr] just attempted to href dock exploit on [src] with target location \"[newTarget]\"")
+ return
+ targetLocation = newTarget
+ say("Shuttle route calculated.")
+ return
+
+/obj/machinery/computer/custom_shuttle/proc/Fly()
+ if(!targetLocation)
+ return
+ var/obj/docking_port/mobile/linkedShuttle = SSshuttle.getShuttle(shuttleId)
+ if(!linkedShuttle)
+ return
+ if(linkedShuttle.mode != SHUTTLE_IDLE)
+ return
+ if(!calculateStats(TRUE, 0, TRUE))
+ return
+ if(calculated_fuel_less_thrusters > 0)
+ say("Warning, [calculated_fuel_less_thrusters] do not have enough fuel for this journey, engine output may be limitted.")
+ if(calculated_speed < 1)
+ say("Insufficient engine power, shuttle requires [calculated_mass / 10]kN of thrust.")
+ return
+ var/obj/docking_port/stationary/targetPort = SSshuttle.getDock(targetLocation)
+ if(!targetPort)
+ return
+ var/dist = calculateDistance(targetPort)
+ var/time = min(max(round(dist / calculated_speed), 10), 90)
+ linkedShuttle.callTime = time * 10
+ linkedShuttle.rechargeTime = calculated_cooldown
+ //We need to find the direction of this console to the port
+ linkedShuttle.port_direction = angle2dir(dir2angle(dir) - (dir2angle(linkedShuttle.dir)) + 180)
+ linkedShuttle.preferred_direction = NORTH
+ linkedShuttle.ignitionTime = CUSTOM_ENGINES_START_TIME
+ linkedShuttle.count_engines()
+ linkedShuttle.hyperspace_sound(HYPERSPACE_WARMUP)
+ var/throwForce = clamp((calculated_speed / 2) - 5, 0, 10)
+ linkedShuttle.movement_force = list("KNOCKDOWN" = calculated_speed > 5 ? 3 : 0, "THROW" = throwForce)
+ if(!(targetLocation in params2list(possible_destinations)))
+ log_admin("[usr] attempted to launch a shuttle that has been affected by href dock exploit on [src] with target location \"[targetLocation]\"")
+ message_admins("[usr] attempted to launch a shuttle that has been affected by href dock exploit on [src] with target location \"[targetLocation]\"")
+ return
+ switch(SSshuttle.moveShuttle(shuttleId, targetLocation, 1))
+ if(0)
+ consumeFuel(dist)
+ say("Shuttle departing. Please stand away from the doors.")
+ if(1)
+ to_chat(usr, "Invalid shuttle requested.")
+ else
+ to_chat(usr, "Unable to comply.")
+ return
+
+/obj/machinery/computer/custom_shuttle/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock, idnum, override=FALSE)
+ if(port && (shuttleId == initial(shuttleId) || override))
+ linkShuttle(port.id)
+
+//Custom shuttle docker locations
+/obj/machinery/computer/camera_advanced/shuttle_docker/custom
+ name = "Shuttle Navigation Computer"
+ desc = "Used to designate a precise transit location for private ships."
+ lock_override = NONE
+ whitelist_turfs = list(/turf/open/space,
+ /turf/open/lava,
+ /turf/open/floor/plating/beach,
+ /turf/open/floor/plating/ashplanet,
+ /turf/open/floor/plating/asteroid,
+ /turf/open/floor/plating/lavaland_baseturf)
+ jumpto_ports = list("whiteship_home" = 1)
+ view_range = 12
+ designate_time = 100
+ circuit = /obj/item/circuitboard/computer/shuttle/docker
+
+/obj/machinery/computer/camera_advanced/shuttle_docker/custom/Initialize()
+ . = ..()
+ GLOB.jam_on_wardec += src
+
+/obj/machinery/computer/camera_advanced/shuttle_docker/custom/Destroy()
+ GLOB.jam_on_wardec -= src
+ return ..()
+
+/obj/machinery/computer/camera_advanced/shuttle_docker/custom/placeLandingSpot()
+ if(!shuttleId)
+ return //Only way this would happen is if someone else delinks the console while in use somehow
+ var/obj/docking_port/mobile/M = SSshuttle.getShuttle(shuttleId)
+ if(M?.mode != SHUTTLE_IDLE)
+ to_chat(usr, "You cannot target locations while in transit.")
+ return
+ ..()
+
+/obj/machinery/computer/camera_advanced/shuttle_docker/custom/attack_hand(mob/user)
+ if(!shuttleId)
+ to_chat(user, "You must link the console to a shuttle first.")
+ return
+ return ..()
+
+/obj/machinery/computer/camera_advanced/shuttle_docker/custom/proc/linkShuttle(var/new_id)
+ shuttleId = new_id
+ shuttlePortId = "shuttle[new_id]_custom"
diff --git a/code/modules/shuttle/navigation_computer.dm b/code/modules/shuttle/navigation_computer.dm
index 69eb4a6be4..4f53e5d9d0 100644
--- a/code/modules/shuttle/navigation_computer.dm
+++ b/code/modules/shuttle/navigation_computer.dm
@@ -253,7 +253,7 @@
current_user.client.images -= remove_images
current_user.client.images += add_images
-/obj/machinery/computer/camera_advanced/shuttle_docker/proc/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock, idnum, override=FALSE)
+/obj/machinery/computer/camera_advanced/shuttle_docker/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock, idnum, override=FALSE)
if(port && (shuttleId == initial(shuttleId) || override))
shuttleId = port.id
shuttlePortId = "[port.id]_custom"
diff --git a/code/modules/shuttle/shuttle.dm b/code/modules/shuttle/shuttle.dm
index 665361af49..77fb37b65c 100644
--- a/code/modules/shuttle/shuttle.dm
+++ b/code/modules/shuttle/shuttle.dm
@@ -11,17 +11,31 @@
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
anchored = TRUE
-//
+ /// The identifier of the port or ship.
+ /// This will be used in numerous other places like the console,
+ /// stationary ports and whatnot to tell them your ship's mobile
+ /// port can be used in these places, or the docking port is compatible, etc.
var/id
- // this should point -away- from the dockingport door, ie towards the ship
+ ///Common standard is for this to point -away- from the dockingport door, ie towards the ship
dir = NORTH
- var/width = 0 //size of covered area, perpendicular to dir
- var/height = 0 //size of covered area, parallel to dir
- var/dwidth = 0 //position relative to covered area, perpendicular to dir
- var/dheight = 0 //position relative to covered area, parallel to dir
+ ///size of covered area, perpendicular to dir. You shouldn't modify this for mobile dockingports, set automatically.
+ var/width = 0
+ ///size of covered area, parallel to dir. You shouldn't modify this for mobile dockingports, set automatically.
+ var/height = 0
+ ///position relative to covered area, perpendicular to dir. You shouldn't modify this for mobile dockingports, set automatically.
+ var/dwidth = 0
+ ///position relative to covered area, parallel to dir. You shouldn't modify this for mobile dockingports, set automatically.
+ var/dheight = 0
var/area_type
- var/hidden = FALSE //are we invisible to shuttle navigation computers?
+ ///are we invisible to shuttle navigation computers?
+ var/hidden = FALSE
+
+ ///Delete this port after ship fly off.
+ var/delete_after = FALSE
+
+/obj/docking_port/proc/get_save_vars()
+ return list("pixel_x", "pixel_y", "dir", "name", "req_access", "req_access_txt", "piping_layer", "color", "icon_state", "pipe_color", "amount", "width", "height", "dwidth", "dheight")
//these objects are indestructible
/obj/docking_port/Destroy(force)
@@ -155,8 +169,6 @@
/obj/docking_port/stationary
name = "dock"
- area_type = SHUTTLE_DEFAULT_UNDERLYING_AREA
-
var/last_dock_time
var/datum/map_template/shuttle/roundstart_template
@@ -169,6 +181,9 @@
id = "[SSshuttle.stationary.len]"
if(name == "dock")
name = "dock[SSshuttle.stationary.len]"
+ if(!area_type)
+ var/area/place = get_area(src)
+ area_type = place?.type // We might be created in nullspace
if(mapload)
for(var/turf/T in return_turfs())
@@ -183,6 +198,13 @@
SSshuttle.stationary -= src
. = ..()
+/obj/docking_port/stationary/Moved(atom/oldloc, dir, forced)
+ . = ..()
+ if(area_type) // We already have one
+ return
+ var/area/newarea = get_area(src)
+ area_type = newarea?.type
+
/obj/docking_port/stationary/proc/load_roundstart()
if(json_key)
var/sid = SSmapping.config.shuttles[json_key]
@@ -196,9 +218,6 @@
if(!roundstart_template)
CRASH("Invalid path ([roundstart_template]) passed to docking port.")
- if(roundstart_template)
- SSshuttle.manipulator.action_load(roundstart_template, src)
-
//returns first-found touching shuttleport
/obj/docking_port/stationary/get_docked()
. = locate(/obj/docking_port/mobile) in loc
@@ -235,16 +254,25 @@
var/list/shuttle_areas
- var/timer //used as a timer (if you want time left to complete move, use timeLeft proc)
+ ///used as a timer (if you want time left to complete move, use timeLeft proc)
+ var/timer
var/last_timer_length
+ ///current shuttle mode
+ var/mode = SHUTTLE_IDLE
+ ///time spent in transit (deciseconds). Should not be lower then 10 seconds without editing the animation of the hyperspace ripples.
+ var/callTime = 100
+ /// time spent "starting the engines". Also rate limits how often we try to reserve transit space if its ever full of transiting shuttles.
+ var/ignitionTime = 55
+ /// time spent after arrival before being able to begin ignition
+ var/rechargeTime = 0
+ /// time spent after transit 'landing' before actually arriving
+ var/prearrivalTime = 0
- var/mode = SHUTTLE_IDLE //current shuttle mode
- var/callTime = 100 //time spent in transit (deciseconds). Should not be lower then 10 seconds without editing the animation of the hyperspace ripples.
- var/ignitionTime = 55 // time spent "starting the engines". Also rate limits how often we try to reserve transit space if its ever full of transiting shuttles.
-
- // The direction the shuttle prefers to travel in
+ /// The direction the shuttle prefers to travel in, ie what direction
+ /// the animation will cause it to appear to be traveling in
var/preferred_direction = NORTH
- // And the angle from the front of the shuttle to the port
+ /// relative direction of the docking port from the front of the shuttle
+ /// NORTH is towards front, EAST would be starboard side, WEST port, etc.
var/port_direction = NORTH
var/obj/docking_port/stationary/destination
@@ -254,13 +282,16 @@
var/launch_status = NOLAUNCH
- var/list/movement_force = list("KNOCKDOWN" = 3, "THROW" = 2)
+ ///Whether or not you want your ship to knock people down, and also whether it will throw them several tiles upon launching.
+ var/list/movement_force = list("KNOCKDOWN" = 3, "THROW" = 0)
var/list/ripples = list()
- var/engine_coeff = 1 //current engine coeff
- var/current_engines = 0 //current engine power
- var/initial_engines = 0 //initial engine power
- var/can_move_docking_ports = FALSE //if this shuttle can move docking ports other than the one it is docked at
+ var/engine_coeff = 1
+ var/current_engines = 0
+ var/initial_engines = 0
+ var/list/engine_list = list()
+ ///if this shuttle can move docking ports other than the one it is docked at
+ var/can_move_docking_ports = FALSE
var/list/hidden_turfs = list()
/obj/docking_port/mobile/proc/register()
@@ -308,14 +339,12 @@
id = "[id][idnum]"
if(name == initial(name))
name = "[name] [idnum]"
- for(var/i in shuttle_areas)
- var/area/place = i
- for(var/obj/machinery/computer/shuttle/comp in place)
- comp.connect_to_shuttle(src, dock, idnum)
- for(var/obj/machinery/computer/camera_advanced/shuttle_docker/comp in place)
- comp.connect_to_shuttle(src, dock, idnum)
- for(var/obj/machinery/status_display/shuttle/sd in place)
- sd.connect_to_shuttle(src, dock, idnum)
+ for(var/place in shuttle_areas)
+ var/area/area = place
+ area.connect_to_shuttle(src, dock, idnum, FALSE)
+ for(var/each in place)
+ var/atom/atom = each
+ atom.connect_to_shuttle(src, dock, idnum, FALSE)
//this is a hook for custom behaviour. Maybe at some point we could add checks to see if engines are intact
@@ -423,7 +452,10 @@
if(initiate_docking(S1) != DOCKING_SUCCESS)
WARNING("shuttle \"[id]\" could not enter transit space. Docked at [S0 ? S0.id : "null"]. Transit dock [S1 ? S1.id : "null"].")
else
- previous = S0
+ if(S0.delete_after)
+ qdel(S0, TRUE)
+ else
+ previous = S0
else
WARNING("shuttle \"[id]\" could not enter transit space. S0=[S0 ? S0.id : "null"] S1=[S1 ? S1.id : "null"]")
@@ -536,7 +568,11 @@
// If we can't dock or we don't have a transit slot, wait for 20 ds,
// then try again
switch(mode)
- if(SHUTTLE_CALL)
+ if(SHUTTLE_CALL, SHUTTLE_PREARRIVAL)
+ if(prearrivalTime && mode != SHUTTLE_PREARRIVAL)
+ mode = SHUTTLE_PREARRIVAL
+ setTimer(prearrivalTime)
+ return
var/error = initiate_docking(destination, preferred_direction)
if(error && error & (DOCKING_NULL_DESTINATION | DOCKING_NULL_SOURCE))
var/msg = "A mobile dock in transit exited initiate_docking() with an error. This is most likely a mapping problem: Error: [error], ([src]) ([previous][ADMIN_JMP(previous)] -> [destination][ADMIN_JMP(destination)])"
@@ -547,6 +583,10 @@
else if(error)
setTimer(20)
return
+ if(rechargeTime)
+ mode = SHUTTLE_RECHARGING
+ setTimer(rechargeTime)
+ return
if(SHUTTLE_RECALL)
if(initiate_docking(previous) != DOCKING_SUCCESS)
setTimer(20)
@@ -649,6 +689,10 @@
return "ESC"
if(SHUTTLE_STRANDED)
return "ERR"
+ if(SHUTTLE_RECHARGING)
+ return "RCH"
+ if(SHUTTLE_PREARRIVAL)
+ return "LDN"
return ""
// returns 5-letter timer string, used by status screens and mob status panel
@@ -660,14 +704,14 @@
if(timeleft > 1 HOURS)
return "--:--"
else if(timeleft > 0)
- return "[add_leading(num2text((timeleft / 60) % 60), 2, "0")]:[add_leading(num2text(timeleft % 60), 2, "0")]"
+ return "[add_leading(num2text((timeleft / 60) % 60), 2, "0")]:[add_leading(num2text(timeleft % 60), 2, " ")]"
else
return "00:00"
/obj/docking_port/mobile/proc/getStatusText()
var/obj/docking_port/stationary/dockedAt = get_docked()
-
+ var/docked_at = dockedAt?.name || "unknown"
if(istype(dockedAt, /obj/docking_port/stationary/transit))
if (timeLeft() > 1 HOURS)
return "hyperspace"
@@ -678,8 +722,10 @@
else
dst = destination
. = "transit towards [dst?.name || "unknown location"] ([getTimerStr()])"
+ else if(mode == SHUTTLE_RECHARGING)
+ return "[docked_at], recharging [getTimerStr()]"
else
- return dockedAt?.name || "unknown"
+ return docked_at
/obj/docking_port/mobile/proc/getDbgStatusText()
@@ -711,19 +757,47 @@
return null
/obj/docking_port/mobile/proc/hyperspace_sound(phase, list/areas)
- var/s
+ var/selected_sound
switch(phase)
if(HYPERSPACE_WARMUP)
- s = 'sound/effects/hyperspace_begin.ogg'
+ selected_sound = "hyperspace_begin"
if(HYPERSPACE_LAUNCH)
- s = 'sound/effects/hyperspace_progress.ogg'
+ selected_sound = "hyperspace_progress"
if(HYPERSPACE_END)
- s = 'sound/effects/hyperspace_end.ogg'
+ selected_sound = "hyperspace_end"
else
CRASH("Invalid hyperspace sound phase: [phase]")
- for(var/A in areas)
- for(var/obj/machinery/door/E in A) //dumb, I know, but playing it on the engines doesn't do it justice
- playsound(E, s, 100, FALSE, max(width, height) - world.view)
+ // This previously was played from each door at max volume, and was one of the worst things I had ever seen.
+ // Now it's instead played from the nearest engine if close, or the first engine in the list if far since it doesn't really matter.
+ // Or a door if for some reason the shuttle has no engine, fuck oh hi daniel fuck it
+ var/range = (engine_coeff * max(width, height))
+ var/long_range = range * 2.5
+ var/atom/distant_source
+ if(LAZYLEN(engine_list))
+ distant_source = engine_list[1]
+ else
+ for(var/A in areas)
+ distant_source = locate(/obj/machinery/door) in A
+ if(distant_source)
+ break
+
+ if(distant_source)
+ for(var/mob/M in SSmobs.clients_by_zlevel[z])
+ var/dist_far = get_dist(M, distant_source)
+ if(dist_far <= long_range && dist_far > range)
+ M.playsound_local(distant_source, "sound/effects/[selected_sound]_distance.ogg", 100, falloff = 20)
+ else if(dist_far <= range)
+ var/source
+ if(engine_list.len == 0)
+ source = distant_source
+ else
+ var/closest_dist = 10000
+ for(var/obj/O in engine_list)
+ var/dist_near = get_dist(M, O)
+ if(dist_near < closest_dist)
+ source = O
+ closest_dist = dist_near
+ M.playsound_local(source, "sound/effects/[selected_sound].ogg", 100, falloff = range / 2)
// Losing all initial engines should get you 2
// Adding another set of engines at 0.5 time
@@ -743,7 +817,12 @@
var/area/shuttle/areaInstance = thing
for(var/obj/structure/shuttle/engine/E in areaInstance.contents)
if(!QDELETED(E))
+ engine_list += E
. += E.engine_power
+ for(var/obj/machinery/shuttle/engine/E in areaInstance.contents)
+ if(!QDELETED(E))
+ engine_list += E
+ . += E.thruster_active ? 1 : 0
// Double initial engines to get to 0.5 minimum
// Lose all initial engines to get to 2
@@ -768,7 +847,7 @@
/obj/docking_port/mobile/proc/in_flight()
switch(mode)
- if(SHUTTLE_CALL,SHUTTLE_RECALL)
+ if(SHUTTLE_CALL,SHUTTLE_RECALL,SHUTTLE_PREARRIVAL)
return TRUE
if(SHUTTLE_IDLE,SHUTTLE_IGNITING)
return FALSE
diff --git a/code/modules/shuttle/shuttle_creation/shuttle_creator.dm b/code/modules/shuttle/shuttle_creation/shuttle_creator.dm
new file mode 100644
index 0000000000..f5a11db60f
--- /dev/null
+++ b/code/modules/shuttle/shuttle_creation/shuttle_creator.dm
@@ -0,0 +1,370 @@
+#define SHUTTLE_CREATOR_MAX_SIZE CONFIG_GET(number/max_shuttle_size)
+#define CUSTOM_SHUTTLE_LIMIT CONFIG_GET(number/max_shuttle_count)
+#define CARDINAL_DIRECTIONS_X list(1, 0, -1, 0)
+#define CARDINAL_DIRECTIONS_Y list(0, 1, 0, -1)
+
+GLOBAL_VAR_INIT(custom_shuttle_count, 0) //The amount of custom shuttles created to prevent creating hundreds
+GLOBAL_LIST_EMPTY(custom_shuttle_machines) //Machines that require updating (Heaters, engines)
+
+//============ Shuttle Creator Object ============
+/obj/item/shuttle_creator
+ name = "Rapid Shuttle Designator"
+ icon = 'icons/obj/tools.dmi'
+ icon_state = "rsd"
+ lefthand_file = 'icons/mob/inhands/equipment/tools_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/equipment/tools_righthand.dmi'
+ desc = "A device used to define the area required for custom ships. Uses bluespace crystals to create bluespace-capable ships."
+ density = FALSE
+ anchored = FALSE
+ flags_1 = CONDUCT_1
+ item_flags = NOBLUDGEON
+ force = 0
+ throwforce = 8
+ throw_speed = 3
+ throw_range = 5
+ w_class = WEIGHT_CLASS_NORMAL
+ req_access_txt = "11"
+ armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50)
+ resistance_flags = FIRE_PROOF
+ var/ready = TRUE
+ //pre-designation
+ var/override_max_shuttles = FALSE
+ var/obj/machinery/computer/camera_advanced/shuttle_creator/internal_shuttle_creator
+ //During designation
+ var/overwritten_area = /area/space
+ var/list/loggedTurfs = list()
+ var/loggedOldArea
+ var/recorded_shuttle_area
+ var/datum/shuttle_creator_overlay_holder/overlay_holder
+ //After designation
+ var/linkedShuttleId
+
+/obj/item/shuttle_creator/Initialize()
+ . = ..()
+ internal_shuttle_creator = new()
+ internal_shuttle_creator.owner_rsd = src
+ overlay_holder = new()
+
+/obj/item/shuttle_creator/Destroy()
+ . = ..()
+ if(internal_shuttle_creator)
+ internal_shuttle_creator.owner_rsd = null
+ QDEL_NULL(internal_shuttle_creator)
+ if(overlay_holder)
+ QDEL_NULL(overlay_holder)
+
+/obj/item/shuttle_creator/attack_self(mob/user)
+ ..()
+ if(linkedShuttleId)
+ return
+ if(GLOB.custom_shuttle_count > CUSTOM_SHUTTLE_LIMIT && !override_max_shuttles)
+ to_chat(user, "Too many shuttles have been created.")
+ message_admins("[ADMIN_FLW(user)] attempted to create a shuttle, however [CUSTOM_SHUTTLE_LIMIT] have already been created.")
+ return
+ if(!internal_shuttle_creator)
+ return
+ overlay_holder.add_client(user.client)
+ internal_shuttle_creator.attack_hand(user)
+
+/obj/item/shuttle_creator/afterattack(atom/target, mob/user, proximity_flag)
+ . = ..()
+ if(!ready)
+ to_chat(user, "You need to define a shuttle area first.")
+ return
+ if(!proximity_flag)
+ return
+ if(istype(target, /obj/machinery/computer/custom_shuttle))
+ if(!linkedShuttleId)
+ to_chat(user, "Error, no defined shuttle linked to device")
+ return
+ var/obj/machinery/computer/custom_shuttle/console = target
+ console.linkShuttle(linkedShuttleId)
+ to_chat(user, "Console linked successfully!")
+ return
+ else if(istype(target, /obj/machinery/computer/camera_advanced/shuttle_docker/custom))
+ if(!linkedShuttleId)
+ to_chat(user, "Error, no defined shuttle linked to device")
+ return
+ var/obj/machinery/computer/camera_advanced/shuttle_docker/custom/console = target
+ console.linkShuttle(linkedShuttleId)
+ to_chat(user, "Console linked successfully!")
+ return
+ to_chat(user, "The [src] bleeps. Select an airlock to create a docking port, or a valid machine to link.")
+ return
+
+//=========== shuttle designation actions ============
+/obj/item/shuttle_creator/proc/calculate_bounds(obj/docking_port/mobile/port)
+ if(!port || !istype(port, /obj/docking_port/mobile))
+ return FALSE
+ //Heights is the distance away from the port
+ //width is the distance perpendicular to the port
+ var/minX = INFINITY
+ var/maxX = 0
+ var/minY = INFINITY
+ var/maxY = 0
+ for(var/turf/T in loggedTurfs)
+ minX = min(T.x, minX)
+ maxX = max(T.x, maxX)
+ minY = min(T.y, minY)
+ maxY = max(T.y, maxY)
+ //Make sure shuttle was actually found.
+ if(maxX == INFINITY || maxY == INFINITY)
+ return FALSE
+ minX--
+ minY--
+ var/width = maxX - minX
+ var/height = maxY - minY
+ var/offset_x = port.x - minX
+ var/offset_y = port.y - minY
+ switch(port.dir) //Source: code/datums/shuttles.dm line 77 (14/03/2020) :)
+ if(NORTH)
+ port.width = width
+ port.height = height
+ port.dwidth = offset_x - 1
+ port.dheight = offset_y - 1
+ if(EAST)
+ port.width = height
+ port.height = width
+ port.dwidth = height - offset_y
+ port.dheight = offset_x - 1
+ if(SOUTH)
+ port.width = width
+ port.height = height
+ port.dwidth = width - offset_x
+ port.dheight = height - offset_y
+ if(WEST)
+ port.width = height
+ port.height = width
+ port.dwidth = offset_y - 1
+ port.dheight = width - offset_x
+ return TRUE
+
+//Go through all the all_turfs and check which direction doesn't have the shuttle
+/obj/item/shuttle_creator/proc/getNonShuttleDirection(turf/targetTurf)
+ var/position = null
+ if(!(get_offset_target_turf(targetTurf, 0, 1) in loggedTurfs))
+ if(position != null)
+ return null
+ position = NORTH
+ if(!(get_offset_target_turf(targetTurf, 0, -1) in loggedTurfs))
+ if(position != null)
+ return null
+ position = SOUTH
+ if(!(get_offset_target_turf(targetTurf, 1, 0) in loggedTurfs))
+ if(position != null)
+ return null
+ position = EAST
+ if(!(get_offset_target_turf(targetTurf, -1, 0) in loggedTurfs))
+ if(position != null)
+ return null
+ position = WEST
+ return position
+
+/obj/item/shuttle_creator/proc/invertDir(var/input_dir)
+ if(input_dir == NORTH)
+ return SOUTH
+ else if(input_dir == SOUTH)
+ return NORTH
+ else if(input_dir == EAST)
+ return WEST
+ else if(input_dir == WEST)
+ return EAST
+ return null
+
+/obj/item/shuttle_creator/proc/shuttle_create_docking_port(atom/target, mob/user)
+
+ if(loggedTurfs.len == 0 || !recorded_shuttle_area)
+ to_chat(user, "Invalid shuttle, restarting bluespace systems...")
+ return FALSE
+
+ var/datum/map_template/shuttle/new_shuttle = new /datum/map_template/shuttle()
+
+ var/obj/docking_port/mobile/port = new /obj/docking_port/mobile(get_turf(target))
+ var/obj/docking_port/stationary/stationary_port = new /obj/docking_port/stationary(get_turf(target))
+ port.callTime = 50
+ port.dir = 1 //Point away from space.
+ port.id = "custom_[GLOB.custom_shuttle_count]"
+ linkedShuttleId = port.id
+ port.ignitionTime = 25
+ port.name = "Custom Shuttle"
+ port.port_direction = 2
+ port.preferred_direction = 4
+ port.area_type = recorded_shuttle_area
+
+ stationary_port.area_type = overwritten_area
+
+ var/portDirection = getNonShuttleDirection(get_turf(port))
+ var/invertedDir = invertDir(portDirection)
+ if(!portDirection || !invertedDir)
+ to_chat(usr, "Shuttle creation aborted, docking airlock must be on an external wall. Please select a new airlock.")
+ port.Destroy()
+ stationary_port.Destroy()
+ linkedShuttleId = null
+ return FALSE
+ port.dir = invertedDir
+ port.port_direction = portDirection
+
+ if(!calculate_bounds(port))
+ to_chat(usr, "Bluespace calculations failed, please select a new airlock.")
+ port.Destroy()
+ stationary_port.Destroy()
+ linkedShuttleId = null
+ return FALSE
+
+ port.shuttle_areas = list()
+ //var/list/all_turfs = port.return_ordered_turfs(port.x, port.y, port.z, port.dir)
+ var/list/all_turfs = loggedTurfs
+ for(var/i in 1 to all_turfs.len)
+ var/turf/curT = all_turfs[i]
+ var/area/cur_area = curT.loc
+ //Add the area to the shuttle <3
+ if(istype(cur_area, recorded_shuttle_area))
+ if(istype(curT, /turf/open/space))
+ continue
+ if(length(curT.baseturfs) < 2)
+ continue
+ //Add the shuttle base shit to the shuttle
+ curT.baseturfs.Insert(3, /turf/baseturf_skipover/shuttle)
+ port.shuttle_areas[cur_area] = TRUE
+
+ port.linkup(new_shuttle, stationary_port)
+
+ port.movement_force = list("KNOCKDOWN" = 0, "THROW" = 0)
+ port.initiate_docking(stationary_port)
+
+ port.mode = SHUTTLE_IDLE
+ port.timer = 0
+
+ port.register()
+
+ icon_state = "rsd_used"
+
+ //Clear highlights
+ overlay_holder.clear_highlights()
+ GLOB.custom_shuttle_count ++
+ message_admins("[ADMIN_LOOKUPFLW(user)] created a new shuttle with a [src] at [ADMIN_VERBOSEJMP(user)] ([GLOB.custom_shuttle_count] custom shuttles, limit is [CUSTOM_SHUTTLE_LIMIT])")
+ log_game("[key_name(user)] created a new shuttle with a [src] at [AREACOORD(user)] ([GLOB.custom_shuttle_count] custom shuttles, limit is [CUSTOM_SHUTTLE_LIMIT])")
+ return TRUE
+
+/obj/item/shuttle_creator/proc/create_shuttle_area(mob/user)
+ //Check to see if the user can make a new area to prevent spamming
+ if(user)
+ if(user.create_area_cooldown >= world.time)
+ to_chat(user, "Smoke vents from the [src], maybe you should let it cooldown before using it again.")
+ return FALSE
+ user.create_area_cooldown = world.time + 10
+ if(!loggedTurfs)
+ return FALSE
+ if(!check_area(loggedTurfs, FALSE)) //Makes sure nothing (Shuttles) has moved into the area during creation
+ return FALSE
+ //Create the new area
+ var/area/shuttle/custom/powered/newS
+ var/area/oldA = loggedOldArea
+ var/str = stripped_input(user, "Shuttle Name:", "Blueprint Editing", "", MAX_NAME_LEN)
+ if(!str || !length(str))
+ return FALSE
+ if(length(str) > 50)
+ to_chat(user, "The provided ship name is too long, blares the [src]")
+ return FALSE
+ newS = new /area/shuttle/custom/powered()
+ newS.setup(str)
+ newS.set_dynamic_lighting()
+ //Shuttles always have gravity
+ newS.has_gravity = TRUE
+ newS.requires_power = TRUE
+ //Record the area for use when creating the docking port
+ recorded_shuttle_area = newS
+
+ for(var/i in 1 to loggedTurfs.len)
+ var/turf/turf_holder = loggedTurfs[i]
+ var/area/old_area = turf_holder.loc
+ newS.contents += turf_holder
+ turf_holder.change_area(old_area, newS)
+
+ newS.reg_in_areas_in_z()
+
+ var/list/firedoors = oldA.firedoors
+ for(var/door in firedoors)
+ var/obj/machinery/door/firedoor/FD = door
+ FD.CalculateAffectingAreas()
+ return TRUE
+
+//Checks an area to ensure that the turfs provided are valid to be made into a shuttle
+/obj/item/shuttle_creator/proc/check_area(list/turfs, addingTurfs = TRUE)
+ if(!turfs)
+ to_chat(usr, "Shuttles must be created in an airtight space, ensure that the shuttle is airtight, including corners.")
+ return FALSE
+ if(turfs.len + (addingTurfs ? loggedTurfs.len : 0) > SHUTTLE_CREATOR_MAX_SIZE)
+ to_chat(usr, "The [src]'s internal cooling system wizzes violently and a message appears on the screen, \"Caution, this device can only handle the creation of shuttles up to [SHUTTLE_CREATOR_MAX_SIZE] units in size. Please reduce your shuttle by [turfs.len-SHUTTLE_CREATOR_MAX_SIZE]. Sorry for the inconvinience\"")
+ return FALSE
+ //Check to see if it's a valid shuttle
+ for(var/i in 1 to turfs.len)
+ var/area/place = get_area(turfs[i])
+ //If any of the turfs are on station / not in space, a shuttle cannot be forced there
+ if(!place)
+ to_chat(usr, "You can't seem to overpower the bluespace harmonics in this location, try somewhere else.")
+ return FALSE
+ if(istype(place, /area/space))
+ overwritten_area = /area/space
+ else if(istype(place, /area/lavaland/surface/outdoors))
+ overwritten_area = /area/lavaland/surface/outdoors
+ else
+ to_chat(usr, "Caution, shuttle must not use any material connected to the station. Your shuttle is currenly overlapping with [place.name]")
+ return FALSE
+ //Finally, check to see if the area is actually attached
+ if(!LAZYLEN(loggedTurfs))
+ return TRUE
+ for(var/turf/T in turfs)
+ if(turf_connected_to_saved_turfs(T))
+ return TRUE
+ CHECK_TICK
+ to_chat(usr, "Caution, new areas of the shuttle must be connected to the other areas of the shuttle.")
+ return FALSE
+
+/obj/item/shuttle_creator/proc/turf_connected_to_saved_turfs(turf/T)
+ for(var/i in 1 to 4)
+ var/turf/adjacentT = get_offset_target_turf(T, CARDINAL_DIRECTIONS_X[i], CARDINAL_DIRECTIONS_Y[i])
+ if(adjacentT in loggedTurfs)
+ return TRUE
+ return FALSE
+
+/obj/item/shuttle_creator/proc/turf_in_list(turf/T)
+ return loggedTurfs.Find(T)
+
+/obj/item/shuttle_creator/proc/add_single_turf(turf/T)
+ if(!check_area(list(T)))
+ return FALSE
+ loggedTurfs |= T
+ loggedOldArea = get_area(T)
+ overlay_holder.highlight_turf(T)
+
+/obj/item/shuttle_creator/proc/add_saved_area(mob/user)
+ var/static/area_or_turf_fail_types = typecacheof(list(
+ /turf/open/space,
+ /area/shuttle
+ ))
+ //Detect the turfs connected in the curerrent enclosed area
+ var/list/turfs = detect_room(get_turf(user), area_or_turf_fail_types)
+ if(!check_area(turfs))
+ return FALSE
+ loggedOldArea = get_area(get_turf(user))
+ loggedTurfs |= turfs
+ overlay_holder.highlight_area(turfs)
+ //TODO READD THIS SHIT: icon_state = "rsd_used"
+ to_chat(user, "You add the area into the buffer of the [src], you made add more areas or select an airlock to act as a docking port to complete the shuttle.")
+ return turfs
+
+/obj/item/shuttle_creator/proc/remove_single_turf(turf/T)
+ if(!turf_in_list(T))
+ return
+ loggedTurfs -= T
+ loggedOldArea = get_area(T)
+ overlay_holder.unhighlight_turf(T)
+
+/obj/item/shuttle_creator/proc/reset_saved_area()
+ overlay_holder.clear_highlights()
+ loggedTurfs.Cut()
+ to_chat(usr, "You reset the area buffer on the [src].")
+
+#undef CARDINAL_DIRECTIONS_X
+#undef CARDINAL_DIRECTIONS_Y
diff --git a/code/modules/shuttle/shuttle_creation/shuttle_creator_actions.dm b/code/modules/shuttle/shuttle_creation/shuttle_creator_actions.dm
new file mode 100644
index 0000000000..f50b3bf59f
--- /dev/null
+++ b/code/modules/shuttle/shuttle_creation/shuttle_creator_actions.dm
@@ -0,0 +1,101 @@
+//============ Actions ============
+/datum/action/innate/shuttle_creator
+ icon_icon = 'icons/mob/actions/actions_shuttle.dmi'
+ var/mob/living/C
+ var/mob/camera/aiEye/remote/shuttle_creation/remote_eye
+ var/obj/item/shuttle_creator/shuttle_creator
+
+/datum/action/innate/shuttle_creator/Activate()
+ if(!target)
+ return TRUE
+ C = owner
+ remote_eye = C.remote_control
+ var/obj/machinery/computer/camera_advanced/shuttle_creator/internal_console = target
+ shuttle_creator = internal_console.owner_rsd
+
+//Add an area
+/datum/action/innate/shuttle_creator/designate_area
+ name = "Designate Room"
+ button_icon_state = "designate_area"
+
+/datum/action/innate/shuttle_creator/designate_area/Activate()
+ if(..())
+ return
+ shuttle_creator.add_saved_area(remote_eye)
+
+//Add a single turf
+/datum/action/innate/shuttle_creator/designate_turf
+ name = "Designate Turf"
+ button_icon_state = "designate_turf"
+
+/datum/action/innate/shuttle_creator/designate_turf/Activate()
+ if(..())
+ return
+ var/turf/T = get_turf(remote_eye)
+ if(istype(T, /turf/open/space))
+ var/connectors_exist = FALSE
+ for(var/obj/structure/lattice/lattice in T)
+ connectors_exist = TRUE
+ break
+ if(!connectors_exist)
+ to_chat(usr, "This turf requires support, build some catwalks or lattices.")
+ return
+ if(!shuttle_creator.check_area(list(T)))
+ return
+ if(shuttle_creator.turf_in_list(T))
+ return
+ shuttle_creator.add_single_turf(T)
+
+//Clear a single entire area
+/datum/action/innate/shuttle_creator/clear_turf
+ name = "Clear Turf"
+ button_icon_state = "clear_turf"
+
+/datum/action/innate/shuttle_creator/clear_turf/Activate()
+ if(..())
+ return
+ shuttle_creator.remove_single_turf(get_turf(remote_eye))
+
+//Clear the entire area
+/datum/action/innate/shuttle_creator/reset
+ name = "Reset Buffer"
+ button_icon_state = "clear_area"
+
+/datum/action/innate/shuttle_creator/reset/Activate()
+ if(..())
+ return
+ shuttle_creator.reset_saved_area()
+
+//Finish the shuttle
+/datum/action/innate/shuttle_creator/airlock
+ name = "Select Docking Airlock"
+ button_icon_state = "select_airlock"
+
+/datum/action/innate/shuttle_creator/airlock/Activate()
+ if(..())
+ return
+ var/turf/T = get_turf(remote_eye)
+ for(var/obj/machinery/door/airlock/A in T)
+ if(get_area(A) != shuttle_creator.loggedOldArea)
+ to_chat(C, "Caution, airlock must be on the shuttle to function as a dock.")
+ return
+ if(shuttle_creator.linkedShuttleId)
+ return
+ if(GLOB.custom_shuttle_count > CUSTOM_SHUTTLE_LIMIT)
+ to_chat(C, "Shuttle limit reached, sorry.")
+ return
+ if(shuttle_creator.loggedTurfs.len > SHUTTLE_CREATOR_MAX_SIZE)
+ to_chat(C, "This shuttle is too large!")
+ return
+ if(!shuttle_creator.getNonShuttleDirection(T))
+ to_chat(C, "Docking port must be on an external wall, with only 1 side exposed to space.")
+ return
+ if(!shuttle_creator.create_shuttle_area(C))
+ return
+ if(shuttle_creator.shuttle_create_docking_port(A, C))
+ to_chat(C, "Shuttle created!")
+ //Remove eye control
+ var/obj/machinery/computer/camera_advanced/shuttle_creator/internal_console = target
+ internal_console.remove_eye_control()
+ qdel(internal_console)
+ return
diff --git a/code/modules/shuttle/shuttle_creation/shuttle_creator_console.dm b/code/modules/shuttle/shuttle_creation/shuttle_creator_console.dm
new file mode 100644
index 0000000000..6945c93427
--- /dev/null
+++ b/code/modules/shuttle/shuttle_creation/shuttle_creator_console.dm
@@ -0,0 +1,93 @@
+//============The internal camera console used for designating the area=============
+/obj/machinery/computer/camera_advanced/shuttle_creator
+ name = "internal shuttle creator console"
+ desc = "You should not have access to this, please report this as a bug"
+ networks = list()
+ var/obj/item/shuttle_creator/owner_rsd
+ var/datum/action/innate/shuttle_creator/designate_area/area_action = new
+ var/datum/action/innate/shuttle_creator/designate_turf/turf_action = new
+ var/datum/action/innate/shuttle_creator/clear_turf/clear_turf_action = new
+ var/datum/action/innate/shuttle_creator/reset/reset_action = new
+ var/datum/action/innate/shuttle_creator/airlock/airlock_action = new
+
+/obj/machinery/computer/camera_advanced/shuttle_creator/check_eye(mob/user)
+ if(user.eye_blind || user.incapacitated())
+ user.unset_machine()
+
+/obj/machinery/computer/camera_advanced/shuttle_creator/CreateEye()
+ eyeobj = new /mob/camera/aiEye/remote/shuttle_creation(get_turf(owner_rsd))
+ eyeobj.origin = src
+ eyeobj.use_static = USE_STATIC_NONE
+
+/obj/machinery/computer/camera_advanced/shuttle_creator/is_operational()
+ return TRUE
+
+/obj/machinery/computer/camera_advanced/shuttle_creator/can_interact(mob/user)
+ if(!isliving(user))
+ return FALSE
+ var/mob/living/L = user
+ if(L.incapacitated())
+ return FALSE
+ return TRUE
+
+/obj/machinery/computer/camera_advanced/shuttle_creator/GrantActions(mob/living/user)
+ ..(user)
+ eyeobj.invisibility = SEE_INVISIBLE_LIVING
+ if(area_action)
+ area_action.target = src
+ area_action.Grant(user)
+ actions += area_action
+ if(turf_action)
+ turf_action.target = src
+ turf_action.Grant(user)
+ actions += turf_action
+ if(clear_turf_action)
+ clear_turf_action.target = src
+ clear_turf_action.Grant(user)
+ actions += clear_turf_action
+ if(reset_action)
+ reset_action.target = src
+ reset_action.Grant(user)
+ actions += reset_action
+ if(airlock_action)
+ airlock_action.target = src
+ airlock_action.Grant(user)
+ actions += airlock_action
+
+/obj/machinery/computer/camera_advanced/shuttle_creator/remove_eye_control(mob/living/user)
+ . = ..()
+ owner_rsd.overlay_holder.remove_client()
+ eyeobj.invisibility = INVISIBILITY_MAXIMUM
+ if(user.client)
+ user.client.images -= eyeobj.user_image
+
+/obj/machinery/computer/camera_advanced/shuttle_creator/attack_hand(mob/user)
+ if(!is_operational()) //you cant use broken machine you chumbis
+ return
+ if(current_user)
+ to_chat(user, "The console is already in use!")
+ return
+ var/mob/living/L = user
+ if(!can_use(user))
+ return
+ if(!eyeobj)
+ CreateEye()
+ if(!eyeobj.eye_initialized)
+ var/camera_location = get_turf(owner_rsd)
+ if(camera_location)
+ eyeobj.eye_initialized = TRUE
+ give_eye_control(L)
+ eyeobj.setLoc(camera_location)
+ var/mob/camera/aiEye/remote/shuttle_creation/shuttle_eye = eyeobj
+ shuttle_eye.source_turf = get_turf(user)
+ else
+ user.unset_machine()
+ else
+ var/camera_location = get_turf(owner_rsd)
+ var/mob/camera/aiEye/remote/shuttle_creation/eye = eyeobj
+ give_eye_control(L)
+ if(camera_location)
+ eye.source_turf = camera_location
+ eyeobj.setLoc(camera_location)
+ else
+ eyeobj.setLoc(eyeobj.loc)
diff --git a/code/modules/shuttle/shuttle_creation/shuttle_creator_eye.dm b/code/modules/shuttle/shuttle_creation/shuttle_creator_eye.dm
new file mode 100644
index 0000000000..ff96e2ae65
--- /dev/null
+++ b/code/modules/shuttle/shuttle_creation/shuttle_creator_eye.dm
@@ -0,0 +1,54 @@
+//===============Camera Eye================
+/mob/camera/aiEye/remote/shuttle_creation
+ name = "shuttle holo-drone"
+ icon = 'icons/obj/mining.dmi'
+ icon_state = "construction_drone"
+ visible_icon = FALSE
+ acceleration = 0
+ var/turf/source_turf
+ var/max_range = 12
+
+/mob/camera/aiEye/remote/shuttle_creation/Initialize()
+ . = ..()
+ setLoc(get_turf(source_turf))
+
+/mob/camera/aiEye/remote/shuttle_creation/update_remote_sight(mob/living/user)
+ user.sight = BLIND|SEE_TURFS
+ user.lighting_alpha = LIGHTING_PLANE_ALPHA_INVISIBLE
+ user.sync_lighting_plane_alpha()
+ return TRUE
+
+/mob/camera/aiEye/remote/shuttle_creation/relaymove(mob/user, direct)
+ dir = direct //This camera eye is visible as a drone, and needs to keep the dir updated
+ var/initial = initial(sprint)
+ var/max_sprint = 50
+
+ if(cooldown && cooldown < world.timeofday) // 3 seconds
+ sprint = initial
+
+ for(var/i = 0; i < max(sprint, initial); i += 20)
+ var/turf/step = get_turf(get_step(src, direct))
+ if(step && can_move_to(step))
+ setLoc(step)
+
+ cooldown = world.timeofday + 5
+ if(acceleration)
+ sprint = min(sprint + 0.5, max_sprint)
+ else
+ sprint = initial
+
+/mob/camera/aiEye/remote/shuttle_creation/proc/can_move_to(var/turf/T)
+ var/origin_x = source_turf.x
+ var/origin_y = source_turf.y
+ var/change_X = abs(origin_x - T.x)
+ var/change_Y = abs(origin_y - T.y)
+ return (change_X < max_range && change_Y < max_range)
+
+/mob/camera/aiEye/remote/shuttle_creation/setLoc(T)
+ ..()
+ if(eye_user?.client)
+ eye_user.client.images -= user_image
+ var/image/I = image(icon, loc, icon_state, FLY_LAYER, dir)
+ I.plane = MASSIVE_OBJ_LAYER
+ user_image = I
+ eye_user.client.images += user_image
diff --git a/code/modules/shuttle/shuttle_creation/shuttle_creator_overlay.dm b/code/modules/shuttle/shuttle_creation/shuttle_creator_overlay.dm
new file mode 100644
index 0000000000..919b1f0221
--- /dev/null
+++ b/code/modules/shuttle/shuttle_creation/shuttle_creator_overlay.dm
@@ -0,0 +1,52 @@
+/*
+ * Manages the overlays for the shuttle creator drone.
+*/
+
+/datum/shuttle_creator_overlay_holder
+ var/client/holder
+ var/list/images = list()
+ var/list/turfs = list()
+
+/datum/shuttle_creator_overlay_holder/proc/add_client(client/C)
+ holder = C
+ holder.images += images
+
+/datum/shuttle_creator_overlay_holder/proc/remove_client()
+ holder.images -= images
+ holder = null
+
+/datum/shuttle_creator_overlay_holder/proc/clear_highlights()
+ if(holder)
+ holder.images -= images
+ images.Cut()
+ turfs.Cut()
+
+/datum/shuttle_creator_overlay_holder/proc/create_hightlight(turf/T)
+ if(T in turfs)
+ return
+ var/image/I = image('icons/turf/overlays.dmi', T, "greenOverlay")
+ I.plane = ABOVE_LIGHTING_PLANE
+ images += I
+ holder.images += I
+ turfs += T
+
+/datum/shuttle_creator_overlay_holder/proc/remove_hightlight(turf/T)
+ if(!(T in turfs))
+ return
+ turfs -= T
+ holder.images -= images
+ for(var/image/I in images)
+ if(get_turf(I) != T)
+ continue
+ images -= I
+ holder.images += images
+
+/datum/shuttle_creator_overlay_holder/proc/highlight_area(list/turfs)
+ for(var/turf/T in turfs)
+ highlight_turf(T)
+
+/datum/shuttle_creator_overlay_holder/proc/highlight_turf(turf/T)
+ create_hightlight(T)
+
+/datum/shuttle_creator_overlay_holder/proc/unhighlight_turf(turf/T)
+ remove_hightlight(T)
diff --git a/code/modules/shuttle/shuttle_creation/shuttle_upgrades.dm b/code/modules/shuttle/shuttle_creation/shuttle_upgrades.dm
new file mode 100644
index 0000000000..b014b7bbf8
--- /dev/null
+++ b/code/modules/shuttle/shuttle_creation/shuttle_upgrades.dm
@@ -0,0 +1,39 @@
+/obj/item/shuttle_route_optimisation
+ name = "Route Optimisation Upgrade"
+ desc = "Used on a custom shuttle control console to calculate more efficient routes."
+ icon = 'icons/obj/module.dmi'
+ icon_state = "shuttledisk"
+ force = 0
+ throwforce = 8
+ throw_speed = 3
+ throw_range = 5
+ density = FALSE
+ anchored = FALSE
+ item_flags = NOBLUDGEON
+ var/upgrade_amount = 0.8
+
+/obj/item/shuttle_route_optimisation/hyperlane
+ name = "Bluespace Hyperlane Calculator"
+ desc = "Used on a custom shuttle control console to allow for the following of bluespace hyperlanes, increasing the efficiency of the shuttle."
+ icon_state = "shuttledisk_better"
+ upgrade_amount = 0.6
+
+/obj/item/shuttle_route_optimisation/void
+ name = "Voidspace Route Calculator"
+ desc = "Used on a custom shuttle control console to allow it to navigate into voidspace, making the routes almost instant."
+ icon_state = "shuttledisk_void"
+ upgrade_amount = 0.2
+
+/obj/item/shuttle_route_optimisation/attack_obj(obj/O, mob/living/user)
+ . = ..()
+ if(!istype(O, /obj/machinery/computer))
+ return
+ if(!istype(O, /obj/machinery/computer/custom_shuttle))
+ to_chat(user, "This upgrade only works on a custom shuttle flight console.")
+ return
+ if (!user.transferItemToLoc(src, get_turf(O)))
+ return
+ var/obj/machinery/computer/custom_shuttle/link_comp = O
+ link_comp.distance_multiplier = clamp(link_comp.distance_multiplier, 0, upgrade_amount)
+ playsound(src, 'sound/machines/terminal_insert_disc.ogg', 50, 0)
+ to_chat(usr, "You insert the disk into the flight computer, allowing for routes to be [upgrade_amount]x the original distance.")
diff --git a/icons/mob/actions/actions_shuttle.dmi b/icons/mob/actions/actions_shuttle.dmi
new file mode 100644
index 0000000000..ef35786a2b
Binary files /dev/null and b/icons/mob/actions/actions_shuttle.dmi differ
diff --git a/icons/obj/tools.dmi b/icons/obj/tools.dmi
index 90eea65cb3..f1a8cf3c02 100644
Binary files a/icons/obj/tools.dmi and b/icons/obj/tools.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 39f79a0b71..a3b5c105b4 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -828,6 +828,9 @@
#include "code\game\machinery\porta_turret\portable_turret.dm"
#include "code\game\machinery\porta_turret\portable_turret_construct.dm"
#include "code\game\machinery\porta_turret\portable_turret_cover.dm"
+#include "code\game\machinery\shuttle\custom_shuttle.dm"
+#include "code\game\machinery\shuttle\shuttle_engine.dm"
+#include "code\game\machinery\shuttle\shuttle_heater.dm"
#include "code\game\machinery\telecomms\broadcasting.dm"
#include "code\game\machinery\telecomms\machine_interactions.dm"
#include "code\game\machinery\telecomms\telecomunications.dm"
@@ -3139,6 +3142,7 @@
#include "code\modules\shuttle\arrivals.dm"
#include "code\modules\shuttle\assault_pod.dm"
#include "code\modules\shuttle\computer.dm"
+#include "code\modules\shuttle\custom_shuttle.dm"
#include "code\modules\shuttle\docking.dm"
#include "code\modules\shuttle\elevator.dm"
#include "code\modules\shuttle\emergency.dm"
@@ -3155,6 +3159,12 @@
#include "code\modules\shuttle\supply.dm"
#include "code\modules\shuttle\syndicate.dm"
#include "code\modules\shuttle\white_ship.dm"
+#include "code\modules\shuttle\shuttle_creation\shuttle_creator.dm"
+#include "code\modules\shuttle\shuttle_creation\shuttle_creator_actions.dm"
+#include "code\modules\shuttle\shuttle_creation\shuttle_creator_console.dm"
+#include "code\modules\shuttle\shuttle_creation\shuttle_creator_eye.dm"
+#include "code\modules\shuttle\shuttle_creation\shuttle_creator_overlay.dm"
+#include "code\modules\shuttle\shuttle_creation\shuttle_upgrades.dm"
#include "code\modules\spells\spell.dm"
#include "code\modules\spells\spell_types\aimed.dm"
#include "code\modules\spells\spell_types\area_teleport.dm"