diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm
index 24b8b041aebd..8ea0a76c5d77 100644
--- a/code/controllers/configuration/entries/game_options.dm
+++ b/code/controllers/configuration/entries/game_options.dm
@@ -401,3 +401,10 @@
/datum/config_entry/number/engine_type
config_entry_value = 3
+
+//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/controllers/subsystem/shuttle.dm b/code/controllers/subsystem/shuttle.dm
index c2c4b9f35ced..252aa7bd95f8 100644
--- a/code/controllers/subsystem/shuttle.dm
+++ b/code/controllers/subsystem/shuttle.dm
@@ -8,6 +8,7 @@ SUBSYSTEM_DEF(shuttle)
var/list/mobile = list()
var/list/stationary = list()
+ var/list/beacons = list()
var/list/transit = list()
var/list/transit_requesters = list()
diff --git a/code/game/area/areas/shuttles.dm b/code/game/area/areas/shuttles.dm
index 15b626434753..8ff9140009a3 100644
--- a/code/game/area/areas/shuttles.dm
+++ b/code/game/area/areas/shuttles.dm
@@ -106,6 +106,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/machinery/shuttle/custom_shuttle.dm b/code/game/machinery/shuttle/custom_shuttle.dm
new file mode 100644
index 000000000000..7c83cade69f3
--- /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 000000000000..98e324b3d020
--- /dev/null
+++ b/code/game/machinery/shuttle/shuttle_engine.dm
@@ -0,0 +1,140 @@
+//-----------------------------------------------
+//-------------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 = 0
+ if(!heat_cap == 0)
+ deltaTemperature = req_power / heat_cap
+ if(deltaTemperature < 0)
+ return
+ env.set_temperature(env.return_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 000000000000..80c46fde7374
--- /dev/null
+++ b/code/game/machinery/shuttle/shuttle_heater.dm
@@ -0,0 +1,134 @@
+//-----------------------------------------------
+//--------------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.get_moles(gas_type)] moles of gas.
" //This probably has issues [air_contents.get_moles]
+
+/obj/machinery/atmospherics/components/unary/shuttle/heater/proc/updateGasStats()
+ var/datum/gas_mixture/air_contents = airs[1]
+ if(!air_contents)
+ return
+ air_contents.set_volume(gas_capacity)
+ air_contents.set_temperature(T20C)
+ if(gas_type)
+ air_contents.set_moles(gas_type)
+
+/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/objects/items/circuitboards/computer_circuitboards.dm b/code/game/objects/items/circuitboards/computer_circuitboards.dm
index 81284c9b9cd9..d8ab95c987e1 100644
--- a/code/game/objects/items/circuitboards/computer_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/computer_circuitboards.dm
@@ -377,6 +377,14 @@
name = "circuit board (Xenobiology Console)"
icon_state = "science"
build_path = /obj/machinery/computer/camera_advanced/xenobio
+
+/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
//Security
diff --git a/code/game/objects/items/circuitboards/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machine_circuitboards.dm
index 0b66104d6cd6..b81ed386e172 100644
--- a/code/game/objects/items/circuitboards/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machine_circuitboards.dm
@@ -1230,3 +1230,28 @@
/obj/item/stack/sheet/mineral/silver = 1)
needs_anchored = FALSE
+
+/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/game/objects/items/shuttle_creator.dm b/code/game/objects/items/shuttle_creator.dm
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/code/game/objects/items/shuttle_upgrades.dm b/code/game/objects/items/shuttle_upgrades.dm
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm
index 96ba894f1e8e..2797bcc65051 100644
--- a/code/modules/mob/mob_defines.dm
+++ b/code/modules/mob/mob_defines.dm
@@ -219,3 +219,5 @@
/// A mock client, provided by tests and friends
var/datum/client_interface/mock_client
+
+ var/create_area_cooldown
diff --git a/code/modules/research/designs/comp_board_designs.dm b/code/modules/research/designs/comp_board_designs.dm
index 83400fc18f2b..c6cda870418c 100644
--- a/code/modules/research/designs/comp_board_designs.dm
+++ b/code/modules/research/designs/comp_board_designs.dm
@@ -285,3 +285,19 @@
build_path = /obj/item/circuitboard/computer/nanite_cloud_controller
category = list("Computer Boards")
departmental_flags = DEPARTMENTAL_FLAG_SCIENCE
+
+/datum/design/board/shuttle/flight_control
+ name = "Computer Design (Shuttle Flight Controller)"
+ desc = "Allows for the construction of circuit boards used to build a console that enables shuttle flight"
+ id = "shuttle_control"
+ build_path = /obj/item/circuitboard/computer/shuttle/flight_control
+ category = list("Computer Boards", "Shuttle Machinery")
+ departmental_flags = DEPARTMENTAL_FLAG_SCIENCE | DEPARTMENTAL_FLAG_ENGINEERING
+
+/datum/design/board/shuttle/shuttle_docker
+ name = "Computer Design (Shuttle Zoning Designator)"
+ desc = "Allows for the construction of circuit boards used to build a console that enables the targetting of custom flight locations"
+ id = "shuttle_docker"
+ build_path = /obj/item/circuitboard/computer/shuttle/docker
+ category = list("Computer Boards", "Shuttle Machinery")
+ departmental_flags = DEPARTMENTAL_FLAG_SCIENCE | DEPARTMENTAL_FLAG_ENGINEERING
diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm
index 0360b7dbfa97..a21214d5e40f 100644
--- a/code/modules/research/designs/machine_designs.dm
+++ b/code/modules/research/designs/machine_designs.dm
@@ -2,6 +2,38 @@
//////////////MISC Boards///////////////
////////////////////////////////////////
+/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
+
+/datum/design/board/spaceship_navigation_beacon
+ name = "Machine Design (Bluespace Navigation Gigabeacon)"
+ desc = "The circuit board for a Bluespace Navigation Gigabeacon."
+ id = "spaceship_navigation_beacon"
+ build_path = /obj/item/circuitboard/machine/spaceship_navigation_beacon
+ category = list ("Shuttle Machinery")
+ departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING | DEPARTMENTAL_FLAG_SCIENCE
+
/datum/design/board/smes
name = "Machine Design (SMES Board)"
desc = "The circuit board for an SMES."
diff --git a/code/modules/research/designs/misc_designs.dm b/code/modules/research/designs/misc_designs.dm
index 99924c91b004..d50cd0f7d6b4 100644
--- a/code/modules/research/designs/misc_designs.dm
+++ b/code/modules/research/designs/misc_designs.dm
@@ -3,6 +3,36 @@
/////////////////HUDs////////////////////
/////////////////////////////////////////
+/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
+
/datum/design/health_hud
name = "Health Scanner HUD"
desc = "A heads-up display that scans the humans in view and provides accurate data about their health status."
diff --git a/code/modules/research/designs/tool_designs.dm b/code/modules/research/designs/tool_designs.dm
index ffc14a6bb941..4a983f4a3619 100644
--- a/code/modules/research/designs/tool_designs.dm
+++ b/code/modules/research/designs/tool_designs.dm
@@ -23,6 +23,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/exwelder
name = "Experimental Welding Tool"
desc = "An experimental welder capable of self-fuel generation."
diff --git a/code/modules/research/machinery/circuit_imprinter.dm b/code/modules/research/machinery/circuit_imprinter.dm
index 948dad61db94..8415564f6d09 100644
--- a/code/modules/research/machinery/circuit_imprinter.dm
+++ b/code/modules/research/machinery/circuit_imprinter.dm
@@ -14,7 +14,8 @@
"Subspace Telecomms",
"Research Machinery",
"Misc. Machinery",
- "Computer Parts"
+ "Computer Parts",
+ "Shuttle Machinery"
)
production_animation = "circuit_imprinter_ani"
allowed_buildtypes = IMPRINTER
diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm
index 10a89987c13b..5a2caf65a5b5 100644
--- a/code/modules/research/techweb/all_nodes.dm
+++ b/code/modules/research/techweb/all_nodes.dm
@@ -272,6 +272,43 @@
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
export_price = 5000
+/////////////////////////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 = 10000)
+ export_price = 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)
+ export_price = 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)
+ export_price = 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)
+ export_price = 5000
+
/////////////////////////robotics tech/////////////////////////
/datum/techweb_node/robotics
id = "robotics"
diff --git a/code/modules/research/techweb/layout.dm b/code/modules/research/techweb/layout.dm
index 13caacb682b4..a2e777139efe 100644
--- a/code/modules/research/techweb/layout.dm
+++ b/code/modules/research/techweb/layout.dm
@@ -8,505 +8,525 @@
// In addition, hovering over a node will show the coordinates if do_node_drag is set
/datum/techweb_node/base
- ui_x = 32
- ui_y = -128
+ ui_x = 32
+ ui_y = -128
/datum/techweb_node/mmi
- ui_x = -32
- ui_y = 96
+ ui_x = -32
+ ui_y = 96
/datum/techweb_node/cyborg
- ui_x = -96
- ui_y = 96
+ ui_x = -96
+ ui_y = 96
/datum/techweb_node/mech
- ui_x = -448
- ui_y = 0
+ ui_x = -448
+ ui_y = 0
/datum/techweb_node/mech_tools
- ui_x = -448
- ui_y = -64
+ ui_x = -448
+ ui_y = -64
/datum/techweb_node/basic_tools
- ui_x = -96
- ui_y = -384
+ ui_x = -96
+ ui_y = -384
/datum/techweb_node/biotech
- ui_x = 160
- ui_y = -128
+ ui_x = 160
+ ui_y = -128
/datum/techweb_node/datatheory
- ui_x = 160
- ui_y = -256
+ ui_x = 160
+ ui_y = -256
/datum/techweb_node/engineering
- ui_x = 32
- ui_y = -256
+ ui_x = 32
+ ui_y = -256
/datum/techweb_node/bluespace_basic
- ui_x = -96
- ui_y = -192
+ ui_x = -96
+ ui_y = -192
/datum/techweb_node/robotics
- ui_x = -160
- ui_y = 96
+ ui_x = -160
+ ui_y = 96
/datum/techweb_node/emp_basic
- ui_x = -96
- ui_y = -128
+ ui_x = -96
+ ui_y = -128
/datum/techweb_node/clown
- ui_x = 256
- ui_y = -576
+ ui_x = 256
+ ui_y = -576
/datum/techweb_node/sec_basic
- ui_x = -512
- ui_y = -672
+ ui_x = -512
+ ui_y = -672
/datum/techweb_node/odysseus
- ui_x = 352
- ui_y = -384
+ ui_x = 352
+ ui_y = -384
-/datum/techweb_node/spacepod_basic
- ui_x = -160
- ui_y = -448
+/datum/techweb_node/spacevehicle_basic
+ ui_x = -32
+ ui_y = -736
/datum/techweb_node/adv_biotech
- ui_x = 288
- ui_y = -96
+ ui_x = 288
+ ui_y = -96
/datum/techweb_node/bio_process
- ui_x = 224
- ui_y = -224
+ ui_x = 224
+ ui_y = -224
/datum/techweb_node/imp_wt_surgery
- ui_x = 224
- ui_y = 64
+ ui_x = 224
+ ui_y = 64
/datum/techweb_node/neural_programming
- ui_x = 544
- ui_y = -160
+ ui_x = 544
+ ui_y = -160
/datum/techweb_node/cloning
- ui_x = 288
- ui_y = -256
+ ui_x = 288
+ ui_y = -256
/datum/techweb_node/cryotech
- ui_x = 160
- ui_y = -192
+ ui_x = 160
+ ui_y = -192
/datum/techweb_node/subdermal_implants
- ui_x = 288
- ui_y = -32
+ ui_x = 288
+ ui_y = -32
/datum/techweb_node/botany
- ui_x = 320
- ui_y = -224
+ ui_x = 320
+ ui_y = -224
/datum/techweb_node/nanite_bio
- ui_x = 480
- ui_y = -288
+ ui_x = 480
+ ui_y = -288
/datum/techweb_node/alientech
- ui_x = 288
- ui_y = 320
+ ui_x = 288
+ ui_y = 320
/datum/techweb_node/high_efficiency
- ui_x = -32
- ui_y = -320
+ ui_x = -32
+ ui_y = -320
/datum/techweb_node/comptech
- ui_x = 192
- ui_y = -320
+ ui_x = 192
+ ui_y = -320
/datum/techweb_node/cyber_implants
- ui_x = 352
- ui_y = -32
+ ui_x = 352
+ ui_y = -32
/datum/techweb_node/nanite_base
- ui_x = 480
- ui_y = -224
+ ui_x = 480
+ ui_y = -224
/datum/techweb_node/adv_engi
- ui_x = -32
- ui_y = -256
+ ui_x = -32
+ ui_y = -256
/datum/techweb_node/adv_power
- ui_x = -128
- ui_y = -320
+ ui_x = -128
+ ui_y = -320
/datum/techweb_node/practical_bluespace
- ui_x = -192
- ui_y = -224
+ ui_x = -192
+ ui_y = -224
/datum/techweb_node/basic_plasma
- ui_x = 32
- ui_y = -320
+ ui_x = 32
+ ui_y = -320
/datum/techweb_node/cyborg_upg_util
- ui_x = -96
- ui_y = 160
+ ui_x = -96
+ ui_y = 160
/datum/techweb_node/basic_mining
- ui_x = 96
- ui_y = -384
+ ui_x = 96
+ ui_y = -384
/datum/techweb_node/weaponry
- ui_x = -576
- ui_y = -416
+ ui_x = -576
+ ui_y = -416
/datum/techweb_node/smartmine
- ui_x = -512
- ui_y = -800
+ ui_x = -512
+ ui_y = -800
/datum/techweb_node/nanite_mesh
- ui_x = 416
- ui_y = -288
+ ui_x = 416
+ ui_y = -288
/datum/techweb_node/spacepod_lock
- ui_x = -224
- ui_y = -448
+ ui_x = 32
+ ui_y = -768
/datum/techweb_node/telecomms
- ui_x = 256
- ui_y = -320
+ ui_x = 256
+ ui_y = -320
/datum/techweb_node/adv_robotics
- ui_x = -160
- ui_y = 160
+ ui_x = -160
+ ui_y = 160
/datum/techweb_node/ai
- ui_x = 608
- ui_y = -96
+ ui_x = 608
+ ui_y = -96
/datum/techweb_node/emp_adv
- ui_x = -160
- ui_y = -128
+ ui_x = -160
+ ui_y = -128
/datum/techweb_node/integrated_HUDs
- ui_x = -96
- ui_y = -64
+ ui_x = -96
+ ui_y = -64
/datum/techweb_node/electric_weapons
- ui_x = -480
- ui_y = -480
+ ui_x = -480
+ ui_y = -480
/datum/techweb_node/camera_theory
- ui_x = 96
- ui_y = -320
+ ui_x = 96
+ ui_y = -320
/datum/techweb_node/landmine
- ui_x = -512
- ui_y = -736
+ ui_x = -512
+ ui_y = -736
+
+/datum/techweb_node/basic_shuttle_tech
+ ui_x = -96
+ ui_y = -736
+
+/datum/techweb_node/shuttle_route_upgrade
+ ui_x = -160
+ ui_y = -736
/datum/techweb_node/spacepod_disabler
- ui_x = -64
- ui_y = -576
+ ui_x = 32
+ ui_y = -704
/datum/techweb_node/spacepod_pseat
- ui_x = -160
- ui_y = -512
-
-/datum/techweb_node/xenology
- ui_x = 480
- ui_y = -96
+ ui_x = 32
+ ui_y = -832
/datum/techweb_node/cyborg_upg_med
- ui_x = 352
- ui_y = -160
-
-/datum/techweb_node/cyborg_upg_surgkit
- ui_x = 416
- ui_y = -160
+ ui_x = 352
+ ui_y = -160
/datum/techweb_node/cyber_organs
- ui_x = 352
- ui_y = -96
+ ui_x = 352
+ ui_y = -96
/datum/techweb_node/medical_weapons
- ui_x = -480
- ui_y = -544
+ ui_x = -480
+ ui_y = -544
/datum/techweb_node/med_mech_tools
- ui_x = 288
- ui_y = -160
+ ui_x = 288
+ ui_y = -160
/datum/techweb_node/alien_bio
- ui_x = 224
- ui_y = 256
+ ui_x = 224
+ ui_y = 256
/datum/techweb_node/adv_surgery
- ui_x = 288
- ui_y = 64
+ ui_x = 288
+ ui_y = 64
/datum/techweb_node/posibrain
- ui_x = 608
- ui_y = -160
+ ui_x = 608
+ ui_y = -160
/datum/techweb_node/adv_cyber_implants
- ui_x = 416
- ui_y = -32
+ ui_x = 416
+ ui_y = -32
/datum/techweb_node/nanite_synaptic
- ui_x = 576
- ui_y = -352
+ ui_x = 576
+ ui_y = -352
/datum/techweb_node/nanite_neural
- ui_x = 512
- ui_y = -352
+ ui_x = 512
+ ui_y = -352
/datum/techweb_node/nanite_harmonic
- ui_x = 448
- ui_y = -352
+ ui_x = 448
+ ui_y = -352
/datum/techweb_node/alien_surgery
- ui_x = 352
- ui_y = 256
+ ui_x = 352
+ ui_y = 256
+
+/datum/techweb_node/shuttle_route_upgrade_void
+ ui_x = 352
+ ui_y = 320
/datum/techweb_node/nanite_hazard
- ui_x = 288
- ui_y = 256
+ ui_x = 288
+ ui_y = 256
/datum/techweb_node/alien_engi
- ui_x = 224
- ui_y = 320
+ ui_x = 224
+ ui_y = 320
/datum/techweb_node/micro_bluespace
- ui_x = -320
- ui_y = -224
+ ui_x = -320
+ ui_y = -224
/datum/techweb_node/combat_cyber_implants
- ui_x = 480
- ui_y = -32
+ ui_x = 480
+ ui_y = -32
/datum/techweb_node/spacepod_storage
- ui_x = -160
- ui_y = -576
+ ui_x = 96
+ ui_y = -832
/datum/techweb_node/spacepod_lockbuster
- ui_x = -224
- ui_y = -512
+ ui_x = 96
+ ui_y = -768
/datum/techweb_node/spacepod_iarmor
- ui_x = -224
- ui_y = -576
+ ui_x = 160
+ ui_y = -832
/datum/techweb_node/computer_hardware_basic
- ui_x = 256
- ui_y = -448
+ ui_x = 256
+ ui_y = -448
/datum/techweb_node/computer_board_gaming
- ui_x = 192
- ui_y = -384
+ ui_x = 192
+ ui_y = -384
/datum/techweb_node/comp_recordkeeping
- ui_x = 256
- ui_y = -384
+ ui_x = 256
+ ui_y = -384
/datum/techweb_node/nanite_smart
- ui_x = 544
- ui_y = -288
+ ui_x = 544
+ ui_y = -288
/datum/techweb_node/anomaly
- ui_x = -224
- ui_y = -288
+ ui_x = -224
+ ui_y = -288
/datum/techweb_node/NVGtech
- ui_x = -160
- ui_y = -64
+ ui_x = -160
+ ui_y = -64
/datum/techweb_node/adv_mining
- ui_x = 96
- ui_y = -448
+ ui_x = 96
+ ui_y = -448
/datum/techweb_node/janitor
- ui_x = -320
- ui_y = -160
+ ui_x = -320
+ ui_y = -160
/datum/techweb_node/exp_tools
- ui_x = -96
- ui_y = -448
+ ui_x = -96
+ ui_y = -448
/datum/techweb_node/rcd_upgrade
- ui_x = -32
- ui_y = -448
+ ui_x = -32
+ ui_y = -448
/datum/techweb_node/adv_weaponry
- ui_x = -576
- ui_y = -576
+ ui_x = -576
+ ui_y = -576
/datum/techweb_node/advmine
- ui_x = -448
- ui_y = -800
+ ui_x = -448
+ ui_y = -800
/datum/techweb_node/radioactive_weapons
- ui_x = -704
- ui_y = -736
+ ui_x = -704
+ ui_y = -736
/datum/techweb_node/syndicate_basic
- ui_x = -384
- ui_y = -672
+ ui_x = -384
+ ui_y = -672
/datum/techweb_node/bluespace_power
- ui_x = -192
- ui_y = -352
+ ui_x = -192
+ ui_y = -352
/datum/techweb_node/mech_tesla
- ui_x = -576
- ui_y = -256
+ ui_x = -576
+ ui_y = -256
/datum/techweb_node/bluespace_travel
- ui_x = -256
- ui_y = -224
+ ui_x = -256
+ ui_y = -224
/datum/techweb_node/adv_plasma
- ui_x = 32
- ui_y = -384
+ ui_x = 32
+ ui_y = -384
/datum/techweb_node/spacepod_ka
- ui_x = -64
- ui_y = -512
+ ui_x = 32
+ ui_y = -640
/datum/techweb_node/ballistic_weapons
- ui_x = -736
- ui_y = -416
+ ui_x = -736
+ ui_y = -416
/datum/techweb_node/gygax
- ui_x = -672
- ui_y = -160
+ ui_x = -672
+ ui_y = -160
/datum/techweb_node/phazon
- ui_x = -736
- ui_y = -128
+ ui_x = -736
+ ui_y = -128
/datum/techweb_node/adv_mecha
- ui_x = -576
- ui_y = -96
+ ui_x = -576
+ ui_y = -96
/datum/techweb_node/emp_super
- ui_x = -224
- ui_y = -128
+ ui_x = -224
+ ui_y = -128
/datum/techweb_node/mech_ion
- ui_x = -672
- ui_y = -256
+ ui_x = -672
+ ui_y = -256
/datum/techweb_node/spacepod_lasers
- ui_x = 0
- ui_y = -576
+ ui_x = 96
+ ui_y = -704
+
+/datum/techweb_node/shuttle_route_upgrade_hyper
+ ui_x = -224
+ ui_y = -736
+
+/datum/techweb_node/cyborg_upg_surgkit
+ ui_x = 416
+ ui_y = -160
/datum/techweb_node/cyber_organs_upgraded
- ui_x = 416
- ui_y = -96
+ ui_x = 416
+ ui_y = -96
/datum/techweb_node/exp_surgery
- ui_x = 352
- ui_y = 64
+ ui_x = 352
+ ui_y = 64
/datum/techweb_node/nanite_combat
- ui_x = -320
- ui_y = -672
+ ui_x = -320
+ ui_y = -672
/datum/techweb_node/advanced_bluespace
- ui_x = -384
- ui_y = -160
+ ui_x = -384
+ ui_y = -160
/datum/techweb_node/mech_teleporter
- ui_x = -416
- ui_y = -256
+ ui_x = -416
+ ui_y = -256
/datum/techweb_node/mech_diamond_drill
- ui_x = 128
- ui_y = -512
+ ui_x = 96
+ ui_y = -512
/datum/techweb_node/spacepod_advmining
- ui_x = 0
- ui_y = -512
+ ui_x = 96
+ ui_y = -640
+
+/datum/techweb_node/adv_rcd_upgrade
+ ui_x = 32
+ ui_y = -448
/datum/techweb_node/beam_weapons
- ui_x = -640
- ui_y = -736
+ ui_x = -640
+ ui_y = -736
/datum/techweb_node/explosive_weapons
- ui_x = -736
- ui_y = -576
+ ui_x = -736
+ ui_y = -576
/datum/techweb_node/exotic_ammo
- ui_x = -704
- ui_y = -672
+ ui_x = -704
+ ui_y = -672
/datum/techweb_node/gravity_gun
- ui_x = -320
- ui_y = -288
+ ui_x = -320
+ ui_y = -288
/datum/techweb_node/durand
- ui_x = -672
- ui_y = -96
+ ui_x = -672
+ ui_y = -96
/datum/techweb_node/unregulated_bluespace
- ui_x = -384
- ui_y = -608
+ ui_x = -384
+ ui_y = -608
/datum/techweb_node/syndicate_surgery
- ui_x = -320
- ui_y = -608
+ ui_x = -320
+ ui_y = -608
/datum/techweb_node/mech_modules
- ui_x = -640
- ui_y = -32
+ ui_x = -640
+ ui_y = -32
/datum/techweb_node/mech_wormhole_gen
- ui_x = -416
- ui_y = -320
+ ui_x = -416
+ ui_y = -320
/datum/techweb_node/spacepod_advplasmacutter
- ui_x = 64
- ui_y = -512
+ ui_x = 160
+ ui_y = -640
/datum/techweb_node/mech_scattershot
- ui_x = -800
- ui_y = -352
+ ui_x = -800
+ ui_y = -352
/datum/techweb_node/mech_carbine
- ui_x = -800
- ui_y = -416
+ ui_x = -800
+ ui_y = -416
/datum/techweb_node/mech_lmg
- ui_x = -736
- ui_y = -352
+ ui_x = -736
+ ui_y = -352
/datum/techweb_node/adv_mecha_tools
- ui_x = -576
- ui_y = -32
+ ui_x = -576
+ ui_y = -32
+
+/datum/techweb_node/xenology
+ ui_x = 480
+ ui_y = -96
/datum/techweb_node/adv_beam_weapons
- ui_x = -640
- ui_y = -832
+ ui_x = -640
+ ui_y = -832
/datum/techweb_node/mech_laser
- ui_x = -704
- ui_y = -896
+ ui_x = -704
+ ui_y = -896
/datum/techweb_node/mech_disabler
- ui_x = -448
- ui_y = -224
+ ui_x = -448
+ ui_y = -224
/datum/techweb_node/mech_grenade_launcher
- ui_x = -800
- ui_y = -512
+ ui_x = -800
+ ui_y = -512
/datum/techweb_node/mech_missile_rack
- ui_x = -800
- ui_y = -640
+ ui_x = -800
+ ui_y = -640
/datum/techweb_node/clusterbang_launcher
- ui_x = -800
- ui_y = -576
+ ui_x = -800
+ ui_y = -576
/datum/techweb_node/mech_laser_heavy
- ui_x = -576
- ui_y = -896
+ ui_x = -576
+ ui_y = -896
/datum/techweb_node/mech_xray
- ui_x = -640
- ui_y = -896
\ No newline at end of file
+ ui_x = -640
+ ui_y = -896
diff --git a/code/modules/shuttle/custom_shuttle.dm b/code/modules/shuttle/custom_shuttle.dm
new file mode 100644
index 000000000000..03cb5cfbfe74
--- /dev/null
+++ b/code/modules/shuttle/custom_shuttle.dm
@@ -0,0 +1,272 @@
+#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/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
+ 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 Zoning Designator"
+ 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(new_id)
+ shuttleId = new_id
+ shuttlePortId = "shuttle[new_id]_custom"
+
+ //Take info from connected port and calculate amendments
+ var/obj/docking_port/mobile/M = SSshuttle.getShuttle(new_id)
+ var/list/shuttlebounds = M.return_coords()
+ view_range = min(round(max(M.width, M.height)*0.5), 15)
+ x_offset = round((shuttlebounds[1] + shuttlebounds[3])*0.5) - M.x
+ y_offset = round((shuttlebounds[2] + shuttlebounds[4])*0.5) - M.y
diff --git a/code/modules/shuttle/navigation_computer.dm b/code/modules/shuttle/navigation_computer.dm
index e5225d3fcbab..bebc478c2a1a 100644
--- a/code/modules/shuttle/navigation_computer.dm
+++ b/code/modules/shuttle/navigation_computer.dm
@@ -11,9 +11,11 @@
var/list/jumpto_ports = list() //hashset of ports to jump to and ignore for collision purposes
var/obj/docking_port/stationary/my_port //the custom docking port placed by this console
var/obj/docking_port/mobile/shuttle_port //the mobile docking port of the connected shuttle
+ var/list/locked_traits = list(ZTRAIT_RESERVED, ZTRAIT_CENTCOM, ZTRAIT_AWAY, ZTRAIT_REEBE) //traits forbided for custom docking
var/view_range = 0
var/x_offset = 0
var/y_offset = 0
+ var/list/whitelist_turfs = list(/turf/open/space, /turf/open/floor/plating, /turf/open/lava)
var/space_turfs_only = TRUE
var/see_hidden = FALSE
var/designate_time = 0
@@ -29,6 +31,7 @@
var/obj/docking_port/stationary/S = V
if(jumpto_ports[S.id])
z_lock |= S.z
+ whitelist_turfs = typecacheof(whitelist_turfs)
/obj/machinery/computer/camera_advanced/shuttle_docker/Destroy()
. = ..()
@@ -162,7 +165,7 @@
if(current_user.client)
current_user.client.images += the_eye.placed_images
to_chat(current_user, span_notice("Transit location designated"))
- return
+ return TRUE
/obj/machinery/computer/camera_advanced/shuttle_docker/proc/canDesignateTarget()
if(!designating_target_loc || !current_user || (eyeobj.loc != designating_target_loc) || (stat & (NOPOWER|BROKEN)) )
@@ -190,7 +193,7 @@
var/turf/eyeturf = get_turf(the_eye)
if(!eyeturf)
return SHUTTLE_DOCKER_BLOCKED
- if(z_lock.len && !(eyeturf.z in z_lock))
+ if(!eyeturf.z || SSmapping.level_has_any_trait(eyeturf.z, locked_traits))
return SHUTTLE_DOCKER_BLOCKED
. = SHUTTLE_DOCKER_LANDING_CLEAR
@@ -228,12 +231,17 @@
if(hidden_turf_info)
. = SHUTTLE_DOCKER_BLOCKED_BY_HIDDEN_PORT
+ if(length(whitelist_turfs))
+ var/turf_type = hidden_turf_info ? hidden_turf_info[2] : T.type
+ if(!is_type_in_typecache(turf_type, whitelist_turfs))
+ return SHUTTLE_DOCKER_BLOCKED
+/*
if(space_turfs_only)
var/turf_type = hidden_turf_info ? hidden_turf_info[2] : T.type
var/area/A = get_area(T)
if(!ispath(turf_type, /turf/open/space) && !ispath(A.type, /area/icemoon))
return SHUTTLE_DOCKER_BLOCKED
-
+*/
// Checking for overlapping dock boundaries
for(var/i in 1 to overlappers.len)
var/obj/docking_port/port = overlappers[i]
@@ -324,13 +332,25 @@
var/list/L = list()
for(var/V in SSshuttle.stationary)
if(!V)
+ stack_trace("SSshuttle.stationary have null entry!")
continue
var/obj/docking_port/stationary/S = V
if(console.z_lock.len && !(S.z in console.z_lock))
continue
if(console.jumpto_ports[S.id])
- L[S.name] = S
+ L["([L.len])[S.name]"] = S
+ for(var/V in SSshuttle.beacons)
+ if(!V)
+ stack_trace("SSshuttle.beacons have null entry!")
+ continue
+ var/obj/machinery/spaceship_navigation_beacon/nav_beacon = V
+ if(!nav_beacon.z || SSmapping.level_has_any_trait(nav_beacon.z, console.locked_traits))
+ break
+ if(!nav_beacon.locked)
+ L["([L.len]) [nav_beacon.name] located: [nav_beacon.x] [nav_beacon.y] [nav_beacon.z]"] = nav_beacon
+ else
+ L["([L.len]) [nav_beacon.name] locked"] = null
playsound(console, 'sound/machines/terminal_prompt.ogg', 25, 0)
var/selected = input("Choose location to jump to", "Locations", null) as null|anything in L
if(QDELETED(src) || QDELETED(target) || !isliving(target))
@@ -345,4 +365,4 @@
C.overlay_fullscreen("flash", /obj/screen/fullscreen/flash/static)
C.clear_fullscreen("flash", 3)
else
- playsound(console, 'sound/machines/terminal_prompt_deny.ogg', 25, 0)
\ No newline at end of file
+ playsound(console, 'sound/machines/terminal_prompt_deny.ogg', 25, 0)
diff --git a/code/modules/shuttle/shuttle.dm b/code/modules/shuttle/shuttle.dm
index 11aa65c70b88..8b59f0e7fcc9 100644
--- a/code/modules/shuttle/shuttle.dm
+++ b/code/modules/shuttle/shuttle.dm
@@ -826,12 +826,17 @@
/obj/docking_port/mobile/proc/count_engines()
. = 0
+ engine_list.Cut()
for(var/thing in shuttle_areas)
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
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 000000000000..d4bd4178117b
--- /dev/null
+++ b/code/modules/shuttle/shuttle_creation/shuttle_creator.dm
@@ -0,0 +1,401 @@
+#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
+ var/ignore_max_shuttle_size = FALSE
+ var/ignore_area = FALSE
+ //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)
+ select_preferred_direction(user)
+ 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 = EAST
+ port.preferred_direction = 4
+ port.area_type = recorded_shuttle_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 = initial(icon_state) + "_used"
+
+ //Select shuttle fly direction.
+ select_preferred_direction(user)
+
+ //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
+ //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
+//Yogs Start: Runs the name through the petty filter. If they trip it, it will cause the shuttle creation to fail, messages the admins, and put the RSD on cooldown.
+ if(isnotpretty(str))
+ to_chat(user, "Nanotrasen prohibited words are in use in this shuttle name, blares the [src] in a slightly offended tone.")
+ message_admins("[ADMIN_LOOKUPFLW(user)] attempted to created a new shuttle with a [src] at [ADMIN_VERBOSEJMP(user)], but failed because of not passing the pretty filter")
+ user.create_area_cooldown = world.time + 10
+ return FALSE
+//Yogs End
+ 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
+
+//Select shuttle fly direction.
+/obj/item/shuttle_creator/proc/select_preferred_direction(mob/user)
+ var/obj/docking_port/mobile/port = SSshuttle.getShuttle(linkedShuttleId)
+ if(!port || !istype(port, /obj/docking_port/mobile))
+ return FALSE
+ var/static/list/choice = list("NORTH" = NORTH, "SOUTH" = SOUTH, "EAST" = EAST, "WEST" = WEST)
+ var/Pdir = input(user, "Shuttle Fly Direction:", "Blueprint Editing", "NORTH") as null|anything in list("NORTH", "SOUTH", "EAST", "WEST")
+ if(Pdir)
+ port.preferred_direction = choice[Pdir]
+
+//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)
+ 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 > SHUTTLE_CREATOR_MAX_SIZE && !ignore_max_shuttle_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 if(ignore_area)
+ overwritten_area = place
+ 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].")
+
+//Yogs: Admin RSD for bussing and messing around. I am not liable for any crashed servers this may cause. This one removes the size and area limits, allowing you to turn the entire station into a shuttle if you want.
+/obj/item/shuttle_creator/admin
+ name = "Admin Rapid Shuttle Designator"
+ icon_state = "arsd"
+ desc = "An experimental RSD with the size and area limits disabled. It is covered in warning of what not to do."
+ override_max_shuttles = TRUE
+ ignore_max_shuttle_size = TRUE
+ ignore_area = TRUE
+
+#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 000000000000..0cf06721aff7
--- /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 && !shuttle_creator.ignore_max_shuttle_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(C)
+ 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 000000000000..6945c934279f
--- /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 000000000000..4eea6bfe8856
--- /dev/null
+++ b/code/modules/shuttle/shuttle_creation/shuttle_creator_eye.dm
@@ -0,0 +1,55 @@
+//===============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))
+ icon_state = "construction_drone"
+
+/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 000000000000..919b1f02211e
--- /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 000000000000..c64f643b6dc7
--- /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) //I have no clue what clamp even does, but I think this should work
+ 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/code/modules/shuttle/spaceship_navigation_beacon.dm b/code/modules/shuttle/spaceship_navigation_beacon.dm
new file mode 100644
index 000000000000..89e02920a732
--- /dev/null
+++ b/code/modules/shuttle/spaceship_navigation_beacon.dm
@@ -0,0 +1,63 @@
+/obj/item/circuitboard/machine/spaceship_navigation_beacon
+ name = "Bluespace Navigation Gigabeacon (Machine Board)"
+ build_path = /obj/machinery/spaceship_navigation_beacon
+ req_components = list()
+
+
+/obj/machinery/spaceship_navigation_beacon
+ name = "Bluespace Navigation Gigabeacon"
+ desc = "A device that creates a bluespace anchor that allow ships jump near to it."
+ icon = 'icons/obj/abductor.dmi'
+ icon_state = "core"
+ use_power = IDLE_POWER_USE
+ idle_power_usage = 0
+ density = TRUE
+ circuit = /obj/item/circuitboard/machine/spaceship_navigation_beacon
+
+ var/locked = FALSE //Locked beacons don't allow to jump to it.
+
+
+/obj/machinery/spaceship_navigation_beacon/Initialize()
+ . = ..()
+ SSshuttle.beacons |= src
+
+obj/machinery/spaceship_navigation_beacon/emp_act()
+ locked = TRUE
+
+/obj/machinery/spaceship_navigation_beacon/Destroy()
+ SSshuttle.beacons -= src
+ return ..()
+
+// update the icon_state
+/obj/machinery/spaceship_navigation_beacon/update_icon()
+ if(powered())
+ icon_state = "core"
+ else
+ icon_state = "core-open"
+
+/obj/machinery/spaceship_navigation_beacon/power_change()
+ . = ..()
+ update_icon()
+
+/obj/machinery/spaceship_navigation_beacon/multitool_act(mob/living/user, obj/item/multitool/I)
+ if(panel_open)
+ var/new_name = "Beacon_[input("Enter the custom name for this beacon", "It be Beacon ..your input..") as text]"
+ if(new_name && Adjacent(user))
+ name = new_name
+ to_chat(user, "You change beacon name to [name].")
+ else
+ locked =!locked
+ to_chat(user, "You [locked ? "" : "un"]lock [src].")
+ return TRUE
+
+/obj/machinery/spaceship_navigation_beacon/examine()
+ .=..()
+ . += "Status: [locked ? "LOCKED" : "Stable"] "
+
+/obj/machinery/spaceship_navigation_beacon/attackby(obj/item/W, mob/user, params)
+ if(default_deconstruction_screwdriver(user, "core-open", "core", W))
+ return
+ if(default_deconstruction_crowbar(W))
+ return
+
+ return ..()
diff --git a/config/config.txt b/config/config.txt
index e2f736b11a2e..964dfb7c07a6 100644
--- a/config/config.txt
+++ b/config/config.txt
@@ -445,3 +445,7 @@ CENTCOM_BAN_DB https://centcom.melonmesa.com/ban/search
## Uncomment to disable hard deletes entirely, even things that explicitly request. This is not recommended unless you have a need for it during events or other high pop times where performance is key.
#DISABLE_ALL_HARD_DELETES
+
+## Custom shuttle spam prevention. Changine these numbers allows you to change the maxsize and amount of custom shuttles.
+MAX_SHUTTLE_COUNT 6
+MAX_SHUTTLE_SIZE 300
diff --git a/icons/mob/actions/actions_shuttle.dmi b/icons/mob/actions/actions_shuttle.dmi
new file mode 100644
index 000000000000..399f50941504
Binary files /dev/null and b/icons/mob/actions/actions_shuttle.dmi differ
diff --git a/icons/obj/module.dmi b/icons/obj/module.dmi
index c0523d86fe1f..a26cfca979c6 100644
Binary files a/icons/obj/module.dmi and b/icons/obj/module.dmi differ
diff --git a/icons/obj/tools.dmi b/icons/obj/tools.dmi
index ff31df768418..e21b6cfb6cfe 100644
Binary files a/icons/obj/tools.dmi and b/icons/obj/tools.dmi differ
diff --git a/icons/turf/shuttle.dmi b/icons/turf/shuttle.dmi
index a09cb7a84737..7c96dcf1b4b4 100644
Binary files a/icons/turf/shuttle.dmi and b/icons/turf/shuttle.dmi differ
diff --git a/yogstation.dme b/yogstation.dme
index b002e518256d..1e3be75185d6 100644
--- a/yogstation.dme
+++ b/yogstation.dme
@@ -811,6 +811,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"
@@ -981,6 +984,8 @@
#include "code\game\objects\items\sharpener.dm"
#include "code\game\objects\items\shields.dm"
#include "code\game\objects\items\shooting_range.dm"
+#include "code\game\objects\items\shuttle_creator.dm"
+#include "code\game\objects\items\shuttle_upgrades.dm"
#include "code\game\objects\items\signs.dm"
#include "code\game\objects\items\singularityhammer.dm"
#include "code\game\objects\items\stunbaton.dm"
@@ -2909,6 +2914,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"
@@ -2920,10 +2926,17 @@
#include "code\modules\shuttle\ripple.dm"
#include "code\modules\shuttle\shuttle.dm"
#include "code\modules\shuttle\shuttle_rotate.dm"
+#include "code\modules\shuttle\spaceship_navigation_beacon.dm"
#include "code\modules\shuttle\special.dm"
#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"
diff --git a/yogstation/code/modules/research/techweb/all_nodes.dm b/yogstation/code/modules/research/techweb/all_nodes.dm
index d5229ff2ff74..6136be1c7d39 100644
--- a/yogstation/code/modules/research/techweb/all_nodes.dm
+++ b/yogstation/code/modules/research/techweb/all_nodes.dm
@@ -13,14 +13,16 @@
/datum/techweb_node/basic_tools
prereq_ids = list("base")
-/datum/techweb_node/spacepod_basic
- id = "spacepod_basic"
+
+/////////////////////////space vehicle tech/////////////////////////
+/datum/techweb_node/spacevehicle_basic
+ id = "spacevehicle_basic"
display_name = "Spacepod Construction"
description = "Basic stuff to construct Spacepods. Don't crash your first spacepod into the station, especially while going more than 10 m/s."
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
export_price = 2500
prereq_ids = list("base")
- design_ids = list("podcore", "podarmor_civ", "podarmor_dark", "spacepod_main")
+ design_ids = list("podcore", "podarmor_civ", "podarmor_dark", "spacepod_main", "spaceship_navigation_beacon")
/datum/techweb_node/spacepod_lock
id = "spacepod_lock"
@@ -28,7 +30,7 @@
description = "Keeps greytiders out of your spacepods."
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2750)
export_price = 2750
- prereq_ids = list("spacepod_basic", "engineering")
+ prereq_ids = list("spacevehicle_basic", "engineering")
design_ids = list("podlock_keyed", "podkey", "podmisc_tracker")
/datum/techweb_node/spacepod_disabler
@@ -37,7 +39,7 @@
description = "For a bit of pew pew space battles"
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 3500)
export_price = 3500
- prereq_ids = list("spacepod_basic", "weaponry")
+ prereq_ids = list("spacevehicle_basic", "weaponry")
design_ids = list("podgun_disabler")
/datum/techweb_node/spacepod_lasers
@@ -82,7 +84,7 @@
description = "For bringing along victims as you fly off into the far reaches of space"
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 3750)
export_price = 3750
- prereq_ids = list("spacepod_basic", "adv_engi")
+ prereq_ids = list("spacevehicle_basic", "adv_engi")
design_ids = list("podcargo_seat")
/datum/techweb_node/spacepod_storage
@@ -112,6 +114,43 @@
prereq_ids = list("spacepod_storage", "high_efficiency")
design_ids = list("podarmor_industiral", "podarmor_sec", "podarmor_gold")
+/datum/techweb_node/basic_shuttle_tech
+ id = "basic_shuttle_tech"
+ display_name = "Basic Shuttle Research"
+ description = "Research the technology required to create and use basic shuttles."
+ prereq_ids = list("bluespace_travel", "adv_engi", "spacevehicle_basic")
+ design_ids = list("shuttle_creator", "engine_plasma", "engine_heater", "shuttle_control", "shuttle_docker")
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 5000)
+ export_price = 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("spacevehicle_basic")
+ design_ids = list("disk_shuttle_route")
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
+ export_price = 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)
+ export_price = 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("engine_void", "disk_shuttle_route_void")
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
+ export_price = 2500
+
+
/datum/techweb_node/syndicate_surgery
id = "syndicate_surgery"
display_name = "Syndicate Surgery"