diff --git a/code/__DEFINES/crafting.dm b/code/__DEFINES/crafting.dm
index cb7930e9d1f..e71004f7f53 100644
--- a/code/__DEFINES/crafting.dm
+++ b/code/__DEFINES/crafting.dm
@@ -6,6 +6,8 @@
#define CRAFTING_MACHINERY_USE 0
///If the structure is only "used" i.e. it checks to see if it's nearby and allows crafting, but doesn't delete it
#define CRAFTING_STRUCTURE_USE 0
+///If the ingredient is only "used" i.e. it checks to see if it's nearby and allows crafting, but doesn't delete it
+#define CRAFTING_INGREDIENT_USE 0
//stack recipe placement check types
/// Checks if there is an object of the result type in any of the cardinal directions
diff --git a/code/__DEFINES/dcs/signals/signals_area.dm b/code/__DEFINES/dcs/signals/signals_area.dm
index b136f46467f..5636a4c35a0 100644
--- a/code/__DEFINES/dcs/signals/signals_area.dm
+++ b/code/__DEFINES/dcs/signals/signals_area.dm
@@ -38,3 +38,9 @@
///From base of area/update_beauty()
#define COMSIG_AREA_BEAUTY_UPDATED "area_beauty_updated"
+
+/// From base of turf/change_area(area/old_area)
+#define COMSIG_AREA_TURF_ADDED "area_turf_added"
+
+/// From base of turf/change_area(area/new_area)
+#define COMSIG_AREA_TURF_REMOVED "area_turf_removed"
diff --git a/code/__DEFINES/dcs/signals/signals_lattice.dm b/code/__DEFINES/dcs/signals/signals_lattice.dm
new file mode 100644
index 00000000000..fe45ad5cf52
--- /dev/null
+++ b/code/__DEFINES/dcs/signals/signals_lattice.dm
@@ -0,0 +1,4 @@
+//From base of /obj/structure/lattice/proc/replace_with_catwalk() : (list/post_replacement_callbacks)
+/// `post_replacement_callbacks` is a list that signal handlers can mutate to append `/datum/callback` objects.
+/// They will be called with the new catwalk after it has been initialized.
+#define COMSIG_LATTICE_PRE_REPLACE_WITH_CATWALK "lattice_pre_replace_with_catwalk"
diff --git a/code/__DEFINES/dcs/signals/signals_shuttle.dm b/code/__DEFINES/dcs/signals/signals_shuttle.dm
index 336117fb868..69aa13c3900 100644
--- a/code/__DEFINES/dcs/signals/signals_shuttle.dm
+++ b/code/__DEFINES/dcs/signals/signals_shuttle.dm
@@ -1,3 +1,4 @@
+
// Shuttle signals. this file is empty because shuttle code is ancient, feel free to
// add more signals where its appropriate to have them
@@ -5,3 +6,6 @@
#define COMSIG_SHUTTLE_SHOULD_MOVE "shuttle_should_move"
/// Return this when the shuttle move should be blocked.
#define BLOCK_SHUTTLE_MOVE (1<<0)
+
+//from base of /proc/expand_shuttle() : (list/turfs)
+#define COMSIG_SHUTTLE_EXPANDED "shuttle_expanded"
diff --git a/code/__DEFINES/dcs/signals/signals_turf.dm b/code/__DEFINES/dcs/signals/signals_turf.dm
index 321fb503cbf..fb16d8c6b35 100644
--- a/code/__DEFINES/dcs/signals/signals_turf.dm
+++ b/code/__DEFINES/dcs/signals/signals_turf.dm
@@ -43,3 +43,14 @@
///Called when turf no longer blocks light from passing through
#define COMSIG_TURF_NO_LONGER_BLOCK_LIGHT "turf_no_longer_block_light"
+
+///from /turf/proc/attempt_lattice_replacement() : (list/post_successful_replacement_callbacks)
+/// `post_successful_replacement_callbacks` is a list that signal handlers can mutate to append `/datum/callback` objects.
+/// They will be called with the new catwalk if it is actually created.
+#define COMSIG_TURF_ATTEMPT_LATTICE_REPLACEMENT "turf_replaced_with_lattice"
+
+///from /turf/proc/change_area() : (area/old_area)
+#define COMSIG_TURF_AREA_CHANGED "turf_area_changed"
+
+///from /proc/create_shuttle() and /obj/docking_port/mobile/proc/add_turf() : (obj/docking_port/mobile/shuttle)
+#define COMSIG_TURF_ADDED_TO_SHUTTLE "turf_added_to_shuttle"
diff --git a/code/__DEFINES/research/techweb_nodes.dm b/code/__DEFINES/research/techweb_nodes.dm
index f27225f1fed..c44a3f4b71a 100644
--- a/code/__DEFINES/research/techweb_nodes.dm
+++ b/code/__DEFINES/research/techweb_nodes.dm
@@ -107,6 +107,7 @@
#define TECHWEB_NODE_SANITATION "sanitation"
#define TECHWEB_NODE_SEC_EQUIP "sec_equip"
#define TECHWEB_NODE_SELECTION "selection"
+#define TECHWEB_NODE_SHUTTLE_ENG "shuttle_eng"
#define TECHWEB_NODE_SPEC_ENG "spec_eng"
#define TECHWEB_NODE_STICKY_ADVANCED "sticky_advanced"
#define TECHWEB_NODE_SURGERY "surgery"
diff --git a/code/__DEFINES/shuttles.dm b/code/__DEFINES/shuttles.dm
index 12f15ab1e68..8a31acfc087 100644
--- a/code/__DEFINES/shuttles.dm
+++ b/code/__DEFINES/shuttles.dm
@@ -130,3 +130,21 @@
#define HIJACK_STAGE_3 3
#define HIJACK_STAGE_4 4
#define HIJACK_COMPLETED 5
+
+// Custom shuttle construction errors
+#define ORIGIN_NOT_ON_SHUTTLE 1 << 0
+
+// The following two errors occur in mutually exclusive contexts, so they can share the same bitflag
+#define TOO_MANY_SHUTTLES 1 << 1
+#define FRAME_NOT_ADJACENT_TO_LINKED_SHUTTLE 1 << 1
+
+#define ABOVE_MAX_SHUTTLE_SIZE 1 << 2
+#define CUSTOM_AREA_NOT_COMPLETELY_CONTAINED 1 << 3
+#define INTERSECTS_NON_WHITELISTED_AREA 1 << 4
+
+// Custom shuttle engine defines
+#define CUSTOM_ENGINE_COEFF_MIN 0.5
+#define CUSTOM_ENGINE_COEFF_MAX 10
+
+#define CUSTOM_ENGINE_POWER_MULTIPLIER 25
+#define CUSTOM_ENGINE_POWER_TURF_COUNT_OFFSET 5
diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm
index db594d235c8..1c081f14c54 100644
--- a/code/__DEFINES/traits/declarations.dm
+++ b/code/__DEFINES/traits/declarations.dm
@@ -1438,4 +1438,18 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Prevents fishing achievement from being granted by catching one of these
#define TRAIT_NO_FISHING_ACHIEVEMENT "no_fishing_achievement"
+/**
+ * This trait is given to turfs that have had shuttle frame parts built on them, but are not yet part of a shuttle.
+ * When attempting custom shuttle creation, a flood fill algorithm
+ * checks for turfs with this trait to determine the turfs
+ * that will constitute the created shuttle.
+ */
+#define TRAIT_SHUTTLE_CONSTRUCTION_TURF "shuttle_construction_turf"
+
+// Trait given to areas with a shuttle construction turf in them
+#define TRAIT_HAS_SHUTTLE_CONSTRUCTION_TURF "has_shuttle_construction_turf"
+
+///A trait given to users as a mutex to prevent repeated unresolved attempts to christen a shuttle
+#define TRAIT_ATTEMPTING_CHRISTENING "attempting_christening"
+
// END TRAIT DEFINES
diff --git a/code/__DEFINES/turfs.dm b/code/__DEFINES/turfs.dm
index ac4f62f10ee..f66edbed8ad 100644
--- a/code/__DEFINES/turfs.dm
+++ b/code/__DEFINES/turfs.dm
@@ -5,6 +5,7 @@
#define CHANGETURF_INHERIT_AIR (1<<4) // Inherit air from previous turf. Implies CHANGETURF_IGNORE_AIR
#define CHANGETURF_RECALC_ADJACENT (1<<5) //Immediately recalc adjacent atmos turfs instead of queuing.
#define CHANGETURF_TRAPDOOR_INDUCED (1<<6) // Caused by a trapdoor, for trapdoor to know that this changeturf was caused by itself
+#define CHANGETURF_GENERATE_SHUTTLE_CEILING (1<<7) // Generate a shuttle ceiling on the above turf
#define IS_OPAQUE_TURF(turf) (turf.directional_opacity == ALL_CARDINALS)
diff --git a/code/__HELPERS/areas.dm b/code/__HELPERS/areas.dm
index 8e818e0e7f4..2395bcbde95 100644
--- a/code/__HELPERS/areas.dm
+++ b/code/__HELPERS/areas.dm
@@ -1,4 +1,6 @@
#define BP_MAX_ROOM_SIZE 300
+#define EXTRA_ROOM_CHECK_SKIP 1
+#define EXTRA_ROOM_CHECK_FAIL 2
GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(list(
/area/station/engineering/main,
@@ -13,8 +15,9 @@ GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(list(
// Returns an associative list of turf|dirs pairs
// The dirs are connected turfs in the same space
// break_if_found is a typecache of turf/area types to return false if found
+// extra_check is an optional callback to invoke on each turf checked, and can specify whether to skip processing the turf or return false
// Please keep this proc type agnostic. If you need to restrict it do it elsewhere or add an arg.
-/proc/detect_room(turf/origin, list/break_if_found = list(), max_size=INFINITY)
+/proc/detect_room(turf/origin, list/break_if_found = list(), max_size=INFINITY, datum/callback/extra_check)
if(origin.blocks_air)
return list(origin)
@@ -35,6 +38,11 @@ GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(list(
continue
checked_turfs[sourceT] |= dir
checked_turfs[checkT] |= REVERSE_DIR(dir)
+ switch(extra_check?.Invoke(checkT))
+ if(EXTRA_ROOM_CHECK_SKIP)
+ continue
+ if(EXTRA_ROOM_CHECK_FAIL)
+ return FALSE
.[sourceT] |= dir
.[checkT] |= REVERSE_DIR(dir)
if(break_if_found[checkT.type] || break_if_found[checkT.loc.type])
@@ -81,6 +89,28 @@ GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(list(
return
counter += 1 //increment by one so the next loop will start at the next position in the list
+/proc/set_turfs_to_area(list/turf/turfs, area/new_area, list/area/affected_areas = list())
+ for(var/turf/the_turf as anything in turfs)
+ var/area/old_area = the_turf.loc
+
+ //keep rack of all areas affected by turf changes
+ affected_areas[old_area.name] = old_area
+
+ //move the turf to its new area and unregister it from the old one
+ the_turf.change_area(old_area, new_area)
+
+ //inform atoms on the turf that their area has changed
+ for(var/atom/stuff as anything in the_turf)
+ //unregister the stuff from its old area
+ SEND_SIGNAL(stuff, COMSIG_EXIT_AREA, old_area)
+
+ //register the stuff to its new area. special exception for apc as its not registered to this signal
+ if(istype(stuff, /obj/machinery/power/apc))
+ var/obj/machinery/power/apc/area_apc = stuff
+ area_apc.assign_to_area()
+ else
+ SEND_SIGNAL(stuff, COMSIG_ENTER_AREA, new_area)
+
/proc/create_area(mob/creator, new_area_type = /area)
// Passed into the above proc as list/break_if_found
var/static/list/area_or_turf_fail_types = typecacheof(list(
@@ -135,8 +165,10 @@ GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(list(
if(!str)
return
newA = new area_choice
+ newA.AddComponent(/datum/component/custom_area)
newA.setup(str)
newA.default_gravity = oldA.default_gravity
+ GLOB.custom_areas[newA] = TRUE
require_area_resort() //new area registered. resort the names
else
newA = area_choice
@@ -151,26 +183,7 @@ GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(list(
* we use this to keep track of what areas are affected by the blueprints & what machinery of these areas needs to be reconfigured accordingly
*/
var/list/area/affected_areas = list()
- for(var/turf/the_turf as anything in turfs)
- var/area/old_area = the_turf.loc
-
- //keep rack of all areas affected by turf changes
- affected_areas[old_area.name] = old_area
-
- //move the turf to its new area and unregister it from the old one
- the_turf.change_area(old_area, newA)
-
- //inform atoms on the turf that their area has changed
- for(var/atom/stuff as anything in the_turf)
- //unregister the stuff from its old area
- SEND_SIGNAL(stuff, COMSIG_EXIT_AREA, old_area)
-
- //register the stuff to its new area. special exception for apc as its not registered to this signal
- if(istype(stuff, /obj/machinery/power/apc))
- var/obj/machinery/power/apc/area_apc = stuff
- area_apc.assign_to_area()
- else
- SEND_SIGNAL(stuff, COMSIG_ENTER_AREA, newA)
+ set_turfs_to_area(turfs, newA, affected_areas)
newA.reg_in_areas_in_z()
diff --git a/code/__HELPERS/pronouns.dm b/code/__HELPERS/pronouns.dm
index 28bb4c11662..6568909c964 100644
--- a/code/__HELPERS/pronouns.dm
+++ b/code/__HELPERS/pronouns.dm
@@ -57,6 +57,9 @@
/datum/proc/p_es(temp_gender)
return "es"
+/datum/proc/p_themselves(temp_gender)
+ return "itself"
+
/datum/proc/plural_s(pluralize)
switch(copytext_char(pluralize, -2))
if ("ss")
@@ -178,6 +181,19 @@
if(temp_gender != PLURAL && temp_gender != NEUTER)
return "es"
+/client/p_themselves(temp_gender)
+ if(!temp_gender)
+ temp_gender = gender
+ switch(temp_gender)
+ if(FEMALE)
+ return "herself"
+ if(MALE)
+ return "himself"
+ if(PLURAL)
+ return "themselves"
+ else
+ return "itself"
+
//mobs(and atoms but atoms don't really matter write your own proc overrides) also have gender!
/mob/p_they(temp_gender)
if(!temp_gender)
@@ -271,6 +287,19 @@
if(temp_gender != PLURAL)
return "es"
+/mob/p_themselves(temp_gender)
+ if(!temp_gender)
+ temp_gender = gender
+ switch(temp_gender)
+ if(FEMALE)
+ return "herself"
+ if(MALE)
+ return "himself"
+ if(PLURAL)
+ return "themselves"
+ else
+ return "itself"
+
//humans need special handling, because they can have their gender hidden
/mob/living/carbon/human/p_they(temp_gender)
var/obscured = check_obscured_slots()
@@ -342,6 +371,13 @@
temp_gender = PLURAL
return ..()
+/mob/living/carbon/human/p_themselves(temp_gender)
+ var/obscured = check_obscured_slots()
+ var/skipface = (wear_mask && (wear_mask.flags_inv & HIDEFACE)) || (head && (head.flags_inv & HIDEFACE))
+ if((obscured & ITEM_SLOT_ICLOTHING) && skipface)
+ temp_gender = PLURAL
+ return ..()
+
//clothing need special handling due to pairs of items, ie gloves vs a singular glove, shoes, ect.
/obj/item/clothing/p_they(temp_gender)
if(!temp_gender)
@@ -411,6 +447,19 @@
if(temp_gender != PLURAL)
return "es"
+/obj/item/clothing/p_themselves(temp_gender)
+ if(!temp_gender)
+ temp_gender = gender
+ switch(temp_gender)
+ if(FEMALE)
+ return "herself"
+ if(MALE)
+ return "himself"
+ if(PLURAL)
+ return "themselves"
+ else
+ return "itself"
+
/datum/mind/p_they(temp_gender)
return current?.p_they(temp_gender) || ..()
@@ -440,3 +489,6 @@
/datum/mind/p_es(temp_gender)
return current?.p_es(temp_gender) || ..()
+
+/datum/mind/p_themselves(temp_gender)
+ return current?.p_themselves(temp_gender) || ..()
diff --git a/code/__HELPERS/spatial_info.dm b/code/__HELPERS/spatial_info.dm
index 050c569cbdf..3125f380902 100644
--- a/code/__HELPERS/spatial_info.dm
+++ b/code/__HELPERS/spatial_info.dm
@@ -443,6 +443,16 @@
)
list_clear_nulls(.)
+///Returns a list of all turfs that are adjacent to the center atom's turf, clear the list of nulls at the end.
+/proc/get_adjacent_turfs(atom/center)
+ . = list(
+ get_step(center, NORTH),
+ get_step(center, SOUTH),
+ get_step(center, EAST),
+ get_step(center, WEST)
+ )
+ list_clear_nulls(.)
+
///Checks if the mob provided (must_be_alone) is alone in an area
/proc/alone_in_area(area/the_area, mob/must_be_alone, check_type = /mob/living/carbon)
var/area/our_area = get_area(the_area)
diff --git a/code/__HELPERS/string_lists.dm b/code/__HELPERS/string_lists.dm
index 99ce28fba1d..eae730213f7 100644
--- a/code/__HELPERS/string_lists.dm
+++ b/code/__HELPERS/string_lists.dm
@@ -22,6 +22,7 @@ GLOBAL_LIST_EMPTY(string_lists)
stack_trace("The baseturfs list of [baseturf_holder] at [baseturf_holder.x], [baseturf_holder.y], [baseturf_holder.x] is [length(values)], it should never be this long, investigate. I've set baseturfs to a flashing wall as a visual queue")
baseturf_holder.ChangeTurf(/turf/closed/indestructible/baseturfs_ded, list(/turf/closed/indestructible/baseturfs_ded), flags = CHANGETURF_FORCEOP)
return string_list(list(/turf/closed/indestructible/baseturfs_ded)) //I want this reported god damn it
+
return string_list(values)
/turf/closed/indestructible/baseturfs_ded
diff --git a/code/_globalvars/lists/mapping.dm b/code/_globalvars/lists/mapping.dm
index 78680d4b216..35a18fffdd3 100644
--- a/code/_globalvars/lists/mapping.dm
+++ b/code/_globalvars/lists/mapping.dm
@@ -147,6 +147,8 @@ GLOBAL_LIST_EMPTY(areas)
GLOBAL_LIST_EMPTY(sortedAreas)
/// An association from typepath to area instance. Only includes areas with `unique` set.
GLOBAL_LIST_EMPTY_TYPED(areas_by_type, /area)
+/// A list of player-created areas.
+GLOBAL_LIST_EMPTY_TYPED(custom_areas, /area)
GLOBAL_LIST_EMPTY(all_abstract_markers)
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index 820cf66fa69..9151dcb5bc7 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -88,6 +88,9 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_WAS_RENAMED" = TRAIT_WAS_RENAMED,
"TRAIT_WEATHER_IMMUNE" = TRAIT_WEATHER_IMMUNE,
),
+ /area = list(
+ "TRAIT_HAS_SHUTTLE_CONSTRUCTION_TURF" = TRAIT_HAS_SHUTTLE_CONSTRUCTION_TURF,
+ ),
/datum/controller/subsystem/economy = list(
"TRAIT_MARKET_CRASHING" = TRAIT_MARKET_CRASHING,
),
@@ -157,6 +160,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_ANTIMAGIC" = TRAIT_ANTIMAGIC,
"TRAIT_ANTIMAGIC_NO_SELFBLOCK" = TRAIT_ANTIMAGIC_NO_SELFBLOCK,
"TRAIT_ANXIOUS" = TRAIT_ANXIOUS,
+ "TRAIT_ATTEMPTING_CHRISTENING" = TRAIT_ATTEMPTING_CHRISTENING,
"TRAIT_BADDNA" = TRAIT_BADDNA,
"TRAIT_BADTOUCH" = TRAIT_BADTOUCH,
"TRAIT_BALD" = TRAIT_BALD,
@@ -740,6 +744,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_IMMERSE_STOPPED" = TRAIT_IMMERSE_STOPPED,
"TRAIT_LAVA_STOPPED" = TRAIT_LAVA_STOPPED,
"TRAIT_NO_TERRAFORM" = TRAIT_NO_TERRAFORM,
+ "TRAIT_SHUTTLE_CONSTRUCTION_TURF" = TRAIT_SHUTTLE_CONSTRUCTION_TURF,
"TRAIT_SPINNING_WEB_TURF" = TRAIT_SPINNING_WEB_TURF,
"TRAIT_TURF_IGNORE_SLIPPERY" = TRAIT_TURF_IGNORE_SLIPPERY,
"TRAIT_TURF_IGNORE_SLOWDOWN" = TRAIT_TURF_IGNORE_SLOWDOWN,
diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm
index e2dd97935e0..0a9cd40bddd 100644
--- a/code/controllers/configuration/entries/game_options.dm
+++ b/code/controllers/configuration/entries/game_options.dm
@@ -494,3 +494,12 @@
integer = FALSE
default = 1
min_val = 0.05
+
+
+//Custom Shuttles
+//Shuttle size limiter
+/datum/config_entry/number/max_shuttle_count
+ default = 6
+
+/datum/config_entry/number/max_shuttle_size
+ default = 250
diff --git a/code/controllers/subsystem/shuttle.dm b/code/controllers/subsystem/shuttle.dm
index 71d9ed0189e..060e50e4159 100644
--- a/code/controllers/subsystem/shuttle.dm
+++ b/code/controllers/subsystem/shuttle.dm
@@ -17,6 +17,8 @@ SUBSYSTEM_DEF(shuttle)
var/list/mobile_docking_ports = list()
/// A list of all the stationary docking ports.
var/list/stationary_docking_ports = list()
+ /// A list of all the custom shuttles.
+ var/list/custom_shuttles = list()
/// A list of all the beacons that can be docked to.
var/list/beacon_list = list()
/// A list of all the transit docking ports.
diff --git a/code/datums/components/connect_containers.dm b/code/datums/components/connect_containers.dm
index 22efc634359..59eabcf6658 100644
--- a/code/datums/components/connect_containers.dm
+++ b/code/datums/components/connect_containers.dm
@@ -33,7 +33,7 @@
/datum/component/connect_containers/proc/set_tracked(atom/movable/new_tracked)
if(tracked)
UnregisterSignal(tracked, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
- unregister_signals(tracked.loc)
+ unregister_signals(tracked)
tracked = new_tracked
if(!tracked)
return
diff --git a/code/datums/components/connect_inventory.dm b/code/datums/components/connect_inventory.dm
new file mode 100644
index 00000000000..c2e688171b4
--- /dev/null
+++ b/code/datums/components/connect_inventory.dm
@@ -0,0 +1,59 @@
+/// Behaves similar to connect_loc_behalf, but hooks into signals on items in the user's inventory
+/datum/component/connect_inventory
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+
+ var/list/connections
+
+ var/mob/living/tracked
+
+ var/allowed_slots
+
+/datum/component/connect_inventory/Initialize(mob/living/tracked, connections, allowed_slots = ALL)
+ . = ..()
+ if(!istype(tracked))
+ return COMPONENT_INCOMPATIBLE
+ src.connections = connections
+ src.tracked = tracked
+ src.allowed_slots = allowed_slots
+
+/datum/component/connect_inventory/RegisterWithParent()
+ RegisterSignal(tracked, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(on_equipped_item))
+ RegisterSignal(tracked, COMSIG_QDELETING, PROC_REF(handle_tracked_qdel))
+ update_signals()
+
+/datum/component/connect_inventory/UnregisterFromParent()
+ unregister_signals()
+ UnregisterSignal(tracked, list(COMSIG_MOB_EQUIPPED_ITEM, COMSIG_MOB_UNEQUIPPED_ITEM))
+
+/datum/component/connect_inventory/proc/handle_tracked_qdel()
+ SIGNAL_HANDLER
+ qdel(src)
+
+/datum/component/connect_inventory/proc/update_signals()
+ unregister_signals()
+
+ for(var/obj/item/item as anything in tracked.get_equipped_items(INCLUDE_POCKETS | INCLUDE_HELD))
+ if(!(allowed_slots & tracked.get_slot_by_item(item)))
+ continue
+ RegisterSignal(item, COMSIG_ITEM_DROPPED, PROC_REF(on_unequipped_item))
+ for(var/signal in connections)
+ parent.RegisterSignal(item, signal, connections[signal])
+
+/datum/component/connect_inventory/proc/unregister_signals()
+ for(var/obj/item/item as anything in tracked.get_equipped_items(INCLUDE_POCKETS | INCLUDE_HELD))
+ UnregisterSignal(item, COMSIG_ITEM_DROPPED)
+ parent.UnregisterSignal(item, connections)
+
+/datum/component/connect_inventory/proc/on_equipped_item(datum/source, obj/item/equipped, slot)
+ SIGNAL_HANDLER
+ if(!(allowed_slots & slot))
+ return
+ // This handler has to be registered on the component itself because users may have their own COMSIG_ITEM_DROPPED handler for the equipped item
+ RegisterSignal(equipped, COMSIG_ITEM_DROPPED, PROC_REF(on_unequipped_item))
+ for(var/signal in connections)
+ parent.RegisterSignal(equipped, signal, connections[signal])
+
+/datum/component/connect_inventory/proc/on_unequipped_item(obj/item/unequipped)
+ SIGNAL_HANDLER
+ UnregisterSignal(unequipped, COMSIG_ITEM_DROPPED)
+ parent.UnregisterSignal(unequipped, connections)
diff --git a/code/datums/components/connect_range.dm b/code/datums/components/connect_range.dm
index af8ec247eb2..2a9225523d3 100644
--- a/code/datums/components/connect_range.dm
+++ b/code/datums/components/connect_range.dm
@@ -75,14 +75,13 @@
turfs = list()
return
- if(ismovable(target.loc))
+ var/loc_is_movable = ismovable(target.loc)
+
+ if(loc_is_movable)
if(!works_in_containers)
unregister_signals(old_loc, turfs)
turfs = list()
return
- //Keep track of possible movement of all movables the target is in.
- for(var/atom/movable/container as anything in get_nested_locs(target))
- RegisterSignal(container, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
//Only register/unregister turf signals if it's moved to a new turf.
if(current_turf == get_turf(old_loc))
@@ -91,6 +90,10 @@
var/list/old_turfs = turfs
turfs = RANGE_TURFS(range, current_turf)
unregister_signals(old_loc, old_turfs - turfs)
+ if(loc_is_movable)
+ //Keep track of possible movement of all movables the target is in.
+ for(var/atom/movable/container as anything in get_nested_locs(target))
+ RegisterSignal(container, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
for(var/turf/target_turf as anything in turfs - old_turfs)
for(var/signal in connections)
parent.RegisterSignal(target_turf, signal, connections[signal])
diff --git a/code/datums/components/crafting/tools.dm b/code/datums/components/crafting/tools.dm
index d1d303daf80..a1e1fd1ab76 100644
--- a/code/datums/components/crafting/tools.dm
+++ b/code/datums/components/crafting/tools.dm
@@ -78,3 +78,34 @@
reqs = list(/obj/item/stack/sheet/bone = 1)
time = 2 SECONDS
category = CAT_TOOLS
+
+/datum/crafting_recipe/shuttle_blueprints
+ name = "Crude Shuttle Blueprints"
+ result = /obj/item/shuttle_blueprints/crude
+ reqs = list(
+ /obj/item/paper = 1,
+ /obj/item/toy/crayon = CRAFTING_INGREDIENT_USE,
+ )
+ steps = list(
+ "You must use either a a blue crayon, a rainbow crayon, or a spray can.",
+ "The crayon or spray can you use must have at least 10 uses remaining."
+ )
+ time = 10 SECONDS
+ category = CAT_TOOLS
+
+/datum/crafting_recipe/shuttle_blueprints/check_requirements(mob/user, list/collected_requirements)
+ var/list/crayons = collected_requirements[/obj/item/toy/crayon]
+ for(var/obj/item/toy/crayon/crayon as anything in crayons)
+ if(!is_type_in_list(crayon, list(/obj/item/toy/crayon/blue, /obj/item/toy/crayon/rainbow, /obj/item/toy/crayon/spraycan)))
+ continue
+ if(!crayon.check_empty(user, 10))
+ return TRUE
+
+/datum/crafting_recipe/shuttle_blueprints/on_craft_completion(mob/user, atom/result)
+ var/static/list/valid_types = list(/obj/item/toy/crayon/blue, /obj/item/toy/crayon/rainbow, /obj/item/toy/crayon/spraycan)
+ for(var/valid_type in valid_types)
+ var/obj/item/toy/crayon/crayon = locate(valid_type) in range(1)
+ if(!crayon)
+ continue
+ if(crayon.use_charges(user, 10))
+ return
diff --git a/code/datums/components/custom_area.dm b/code/datums/components/custom_area.dm
new file mode 100644
index 00000000000..f46a168bb86
--- /dev/null
+++ b/code/datums/components/custom_area.dm
@@ -0,0 +1,23 @@
+/**
+ * Container for data necessary to track custom areas.
+ * Currently this is just the areas this area was created on top of, in case the area is used to create a custom shuttle.
+ */
+/datum/component/custom_area
+ var/list/previous_areas = list()
+
+/datum/component/custom_area/Initialize(...)
+ if(!isarea(parent))
+ return COMPONENT_INCOMPATIBLE
+
+/datum/component/custom_area/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_AREA_TURF_ADDED, PROC_REF(on_turf_added))
+ RegisterSignal(parent, COMSIG_AREA_TURF_REMOVED, PROC_REF(on_turf_removed))
+
+/datum/component/custom_area/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_AREA_TURF_ADDED, COMSIG_AREA_TURF_REMOVED))
+
+/datum/component/custom_area/proc/on_turf_added(area/source, turf/turf, area/old_area)
+ previous_areas[turf] = old_area
+
+/datum/component/custom_area/proc/on_turf_removed(area/source, turf/turf)
+ previous_areas -= turf
diff --git a/code/datums/elements/shuttle_construction_lattice.dm b/code/datums/elements/shuttle_construction_lattice.dm
new file mode 100644
index 00000000000..0c89b7fe110
--- /dev/null
+++ b/code/datums/elements/shuttle_construction_lattice.dm
@@ -0,0 +1,55 @@
+/// Element used to specify that a lattice is part of an incomplete shuttle frame
+/datum/element/shuttle_construction_lattice
+ element_flags = ELEMENT_DETACH_ON_HOST_DESTROY
+ var/list/lattices_by_turf = list()
+
+/datum/element/shuttle_construction_lattice/Attach(obj/structure/lattice/target)
+ . = ..()
+ if(!istype(target))
+ return ELEMENT_INCOMPATIBLE
+ var/turf/target_turf = target.loc
+ if(!istype(target_turf))
+ return ELEMENT_INCOMPATIBLE
+ target_turf.AddElementTrait(TRAIT_SHUTTLE_CONSTRUCTION_TURF, REF(target), eletype = /datum/element/shuttle_construction_turf)
+ lattices_by_turf[target_turf] = target
+ RegisterSignal(target, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined))
+ RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
+ RegisterSignal(target, COMSIG_LATTICE_PRE_REPLACE_WITH_CATWALK, PROC_REF(on_replacing_with_catwalk))
+ RegisterSignal(target_turf, COMSIG_TURF_ADDED_TO_SHUTTLE, PROC_REF(on_turf_added_to_shuttle))
+
+/datum/element/shuttle_construction_lattice/Detach(obj/source, ...)
+ . = ..()
+ var/turf/source_turf = source.loc
+ if(istype(source_turf))
+ REMOVE_TRAIT(source_turf, TRAIT_SHUTTLE_CONSTRUCTION_TURF, REF(source))
+ lattices_by_turf -= source_turf
+ UnregisterSignal(source_turf, COMSIG_TURF_ADDED_TO_SHUTTLE)
+ UnregisterSignal(source, list(COMSIG_ATOM_EXAMINE, COMSIG_MOVABLE_MOVED, COMSIG_LATTICE_PRE_REPLACE_WITH_CATWALK))
+
+/datum/element/shuttle_construction_lattice/proc/on_examined(obj/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ examine_list += span_notice("Cutting this [source.name] will ruin the treatment that makes it suitable for shuttle construction.")
+
+/datum/element/shuttle_construction_lattice/proc/on_moved(obj/source, atom/old_loc)
+ SIGNAL_HANDLER
+ var/trait_source = REF(source)
+ if(isturf(old_loc))
+ REMOVE_TRAIT(old_loc, TRAIT_SHUTTLE_CONSTRUCTION_TURF, trait_source)
+ UnregisterSignal(old_loc, COMSIG_TURF_ADDED_TO_SHUTTLE)
+ lattices_by_turf -= old_loc
+ var/turf/new_turf = source.loc
+ if(istype(new_turf))
+ new_turf.AddElementTrait(TRAIT_SHUTTLE_CONSTRUCTION_TURF, trait_source, /datum/element/shuttle_construction_turf)
+ RegisterSignal(new_turf, COMSIG_TURF_ADDED_TO_SHUTTLE, PROC_REF(on_turf_added_to_shuttle))
+ lattices_by_turf[new_turf] = source
+
+/datum/element/shuttle_construction_lattice/proc/on_replacing_with_catwalk(obj/source, list/callbacks)
+ SIGNAL_HANDLER
+ callbacks += CALLBACK(src, PROC_REF(register_catwalk))
+
+/datum/element/shuttle_construction_lattice/proc/register_catwalk(obj/structure/lattice/catwalk/new_catwalk)
+ new_catwalk.AddElement(/datum/element/shuttle_construction_lattice)
+
+/datum/element/shuttle_construction_lattice/proc/on_turf_added_to_shuttle(turf/source)
+ var/obj/structure/lattice/turf_lattice = lattices_by_turf[source]
+ turf_lattice?.RemoveElement(/datum/element/shuttle_construction_lattice)
diff --git a/code/datums/elements/shuttle_construction_turf.dm b/code/datums/elements/shuttle_construction_turf.dm
new file mode 100644
index 00000000000..3c345e5dbf6
--- /dev/null
+++ b/code/datums/elements/shuttle_construction_turf.dm
@@ -0,0 +1,43 @@
+/// Element used to track the conditions for a turf being part of an incomplete shuttle frame
+/datum/element/shuttle_construction_turf
+ element_flags = ELEMENT_DETACH_ON_HOST_DESTROY
+
+/datum/element/shuttle_construction_turf/Attach(turf/target)
+ . = ..()
+ if(!istype(target))
+ return ELEMENT_INCOMPATIBLE
+ RegisterSignal(target, COMSIG_TURF_CHANGE, PROC_REF(pre_turf_changed))
+ RegisterSignal(target, COMSIG_TURF_ATTEMPT_LATTICE_REPLACEMENT, PROC_REF(pre_lattice_replacement))
+ RegisterSignal(target, COMSIG_TURF_ADDED_TO_SHUTTLE, PROC_REF(on_turF_added_to_shuttle))
+ ADD_TRAIT((get_area(target)), TRAIT_HAS_SHUTTLE_CONSTRUCTION_TURF, REF(target))
+
+/datum/element/shuttle_construction_turf/Detach(turf/source, ...)
+ . = ..()
+ UnregisterSignal(source, list(COMSIG_TURF_CHANGE, COMSIG_TURF_ATTEMPT_LATTICE_REPLACEMENT, COMSIG_TURF_ADDED_TO_SHUTTLE, SIGNAL_REMOVETRAIT(TRAIT_SHUTTLE_CONSTRUCTION_TURF)))
+ REMOVE_TRAIT((get_area(source)), TRAIT_HAS_SHUTTLE_CONSTRUCTION_TURF, REF(source))
+
+///Changing or destroying the turf detaches the element, also we need to reapply the traits since they don't get passed down.
+/datum/element/shuttle_construction_turf/proc/pre_turf_changed(turf/source, path, list/new_baseturfs, flags, list/post_change_callbacks)
+ SIGNAL_HANDLER
+ var/list/old_trait_sources = GET_TRAIT_SOURCES(source, TRAIT_SHUTTLE_CONSTRUCTION_TURF)
+ old_trait_sources = old_trait_sources.Copy()
+ post_change_callbacks += CALLBACK(src, PROC_REF(post_turf_changed), old_trait_sources)
+
+/datum/element/shuttle_construction_turf/proc/pre_lattice_replacement(turf/source, list/post_successful_replacement_callbacks)
+ SIGNAL_HANDLER
+ post_successful_replacement_callbacks += CALLBACK(src, PROC_REF(register_lattice))
+
+/datum/element/shuttle_construction_turf/proc/post_turf_changed(list/trait_sources, turf/new_turf)
+ if(isfloorturf(new_turf) || iswallturf(new_turf))
+ trait_sources |= ELEMENT_TRAIT(type)
+ else
+ trait_sources -= ELEMENT_TRAIT(type)
+ if(length(trait_sources))
+ for(var/source in trait_sources)
+ new_turf.AddElementTrait(TRAIT_SHUTTLE_CONSTRUCTION_TURF, source, type)
+
+/datum/element/shuttle_construction_turf/proc/register_lattice(obj/structure/lattice/new_lattice)
+ new_lattice.AddElement(/datum/element/shuttle_construction_lattice)
+
+/datum/element/shuttle_construction_turf/proc/on_turF_added_to_shuttle(turf/source)
+ REMOVE_TRAIT(source, TRAIT_SHUTTLE_CONSTRUCTION_TURF, ELEMENT_TRAIT(type))
diff --git a/code/datums/shuttles/emergency.dm b/code/datums/shuttles/emergency.dm
index 70161d2622b..75e1d56f6e1 100644
--- a/code/datums/shuttles/emergency.dm
+++ b/code/datums/shuttles/emergency.dm
@@ -53,12 +53,6 @@
who_can_purchase = list(ACCESS_CAPTAIN, ACCESS_CE)
occupancy_limit = "Flexible"
-/datum/map_template/shuttle/emergency/construction/post_load()
- . = ..()
- //enable buying engines from cargo
- var/datum/supply_pack/P = SSshuttle.supply_packs[/datum/supply_pack/engineering/shuttle_engine]
- P.special_enabled = TRUE
-
/datum/map_template/shuttle/emergency/asteroid
suffix = "asteroid"
name = "Asteroid Station Emergency Shuttle"
diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm
index de2b9ebb6c2..5ef0f578120 100644
--- a/code/game/area/areas.dm
+++ b/code/game/area/areas.dm
@@ -355,6 +355,8 @@ GLOBAL_LIST_EMPTY(teleportlocs)
//just for sanity sake cause why not
if(!isnull(GLOB.areas))
GLOB.areas -= src
+ if(!isnull(GLOB.custom_areas))
+ GLOB.custom_areas -= src
//machinery cleanup
STOP_PROCESSING(SSobj, src)
QDEL_NULL(alarm_manager)
diff --git a/code/game/area/areas/shuttles.dm b/code/game/area/areas/shuttles.dm
index f93f0ca4c5a..c82f04e7027 100644
--- a/code/game/area/areas/shuttles.dm
+++ b/code/game/area/areas/shuttles.dm
@@ -21,10 +21,17 @@
. = ..()
if(ispath(added_layer, /turf/open/floor/plating))
new_baseturfs.Add(/turf/baseturf_skipover/shuttle)
+ . |= CHANGETURF_GENERATE_SHUTTLE_CEILING
else if(ispath(new_baseturfs[1], /turf/open/floor/plating))
new_baseturfs.Insert(1, /turf/baseturf_skipover/shuttle)
+ . |= CHANGETURF_GENERATE_SHUTTLE_CEILING
-////////////////////////////Multi-area shuttles////////////////////////////
+////////////////////////////Custom Shuttles////////////////////////////
+
+/area/shuttle/custom
+ requires_power = TRUE
+
+////////////////////////////Multi-area shuttles//////////////////////////////
////////////////////////////Syndicate infiltrator////////////////////////////
diff --git a/code/game/objects/items/circuitboards/computer_circuitboards.dm b/code/game/objects/items/circuitboards/computer_circuitboards.dm
index 82d96329540..e77e36a0dac 100644
--- a/code/game/objects/items/circuitboards/computer_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/computer_circuitboards.dm
@@ -650,3 +650,24 @@
/obj/item/circuitboard/computer/exodrone_console
name = "Exploration Drone Control Console"
build_path = /obj/machinery/computer/exodrone_control_console
+
+/obj/item/circuitboard/computer/shuttle
+ var/shuttle_id
+
+/obj/item/circuitboard/computer/shuttle/configure_machine(obj/machinery/machine)
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_id ? SSshuttle.getShuttle(shuttle_id) : SSshuttle.get_containing_shuttle(machine)
+ if(!shuttle)
+ var/on_shuttle_frame = HAS_TRAIT((get_turf(machine)), TRAIT_SHUTTLE_CONSTRUCTION_TURF)
+ machine.say(on_shuttle_frame ? "Console will automatically link on shuttle completion." : "No shuttle available for linking.")
+ else if(!istype(shuttle))
+ machine.say("Cannot link to this kind of shuttle!")
+ else
+ machine.connect_to_shuttle(TRUE, shuttle)
+
+/obj/item/circuitboard/computer/shuttle/flight_control
+ name = "Shuttle Flight Control (Computer Board)"
+ build_path = /obj/machinery/computer/shuttle/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/robot/items/storage.dm b/code/game/objects/items/robot/items/storage.dm
index 4d90c28de6d..e0310d62584 100644
--- a/code/game/objects/items/robot/items/storage.dm
+++ b/code/game/objects/items/robot/items/storage.dm
@@ -251,10 +251,11 @@
///Apparatus to allow Engineering/Sabo borgs to manipulate any material sheets.
/obj/item/borg/apparatus/sheet_manipulator
name = "material manipulation apparatus"
- desc = "An apparatus for carrying, deploying, and manipulating sheets of material. The device can also carry custom floor tiles."
+ desc = "An apparatus for carrying, deploying, and manipulating sheets of material. The device can also carry custom floor tiles and shuttle frame rods."
icon_state = "borg_stack_apparatus"
storable = list(/obj/item/stack/sheet,
- /obj/item/stack/tile)
+ /obj/item/stack/tile,
+ /obj/item/stack/rods/shuttle)
/obj/item/borg/apparatus/sheet_manipulator/Initialize(mapload)
update_appearance()
diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm
index f448ec0ed59..a6702259316 100644
--- a/code/game/objects/items/robot/robot_upgrades.dm
+++ b/code/game/objects/items/robot/robot_upgrades.dm
@@ -803,6 +803,16 @@
items_to_add = list(/obj/item/storage/bag/plants/cyborg, /obj/item/borg/cyborg_omnitool/botany, /obj/item/plant_analyzer)
+/obj/item/borg/upgrade/shuttle_blueprints
+ name = "Engineering Cyborg Shuttle Blueprint Database"
+ desc = "An upgrade to the engineering model cyborg allowing for the construction and expansion of shuttles."
+ icon_state = "module_engineer"
+ require_model = TRUE
+ model_type = list(/obj/item/robot_model/engineering, /obj/item/robot_model/saboteur)
+ model_flags = BORG_MODEL_ENGINEERING
+
+ items_to_add = list(/obj/item/shuttle_blueprints/borg)
+
///This isn't an upgrade or part of the same path, but I'm gonna just stick it here because it's a tool used on cyborgs.
//A reusable tool that can bring borgs back to life. They gotta be repaired first, though.
diff --git a/code/game/objects/items/stacks/rods.dm b/code/game/objects/items/stacks/rods.dm
index 6523a52308c..127f39bfb81 100644
--- a/code/game/objects/items/stacks/rods.dm
+++ b/code/game/objects/items/stacks/rods.dm
@@ -145,3 +145,22 @@ GLOBAL_LIST_INIT(rod_recipes, list ( \
/obj/item/stack/rods/lava/thirty
amount = 30
+
+/obj/item/stack/rods/shuttle
+ name = "shuttle frame rods"
+ desc = "Treated, specialized iron rods suitable for the construction of shuttle frames or the expansion of existing shuttles."
+ singular_name = "shuttle frame rod"
+ mats_per_unit = list(/datum/material/iron=HALF_SHEET_MATERIAL_AMOUNT, /datum/material/titanium=SMALL_MATERIAL_AMOUNT)
+ merge_type = /obj/item/stack/rods/shuttle
+
+/obj/item/stack/rods/shuttle/five
+ amount = 5
+
+/obj/item/stack/rods/shuttle/ten
+ amount = 10
+
+/obj/item/stack/rods/shuttle/twentyfive
+ amount = 25
+
+/obj/item/stack/rods/shuttle/fifty
+ amount = 50
diff --git a/code/game/objects/items/stacks/sheets/mineral.dm b/code/game/objects/items/stacks/sheets/mineral.dm
index 4b9990ea04e..14a9b5dcfb7 100644
--- a/code/game/objects/items/stacks/sheets/mineral.dm
+++ b/code/game/objects/items/stacks/sheets/mineral.dm
@@ -296,6 +296,26 @@ GLOBAL_LIST_INIT(titanium_recipes, list ( \
. = ..()
. += GLOB.titanium_recipes
+/obj/item/stack/sheet/mineral/titanium/attackby(obj/item/W, mob/user, params)
+ add_fingerprint(user)
+ if(istype(W, /obj/item/stack/rods))
+ var/obj/item/stack/rods/old_rods = W
+ if(old_rods.merge_type != /obj/item/stack/rods)
+ to_chat(user, span_warning("You can't craft shuttle frame rods with this type of rod!"))
+ if (old_rods.get_amount() >= 5 && get_amount() >= 1)
+ var/obj/item/stack/rods/shuttle/five/new_rods = new (get_turf(user))
+ if(!QDELETED(new_rods))
+ new_rods.add_fingerprint(user)
+ var/replace = user.get_inactive_held_item() == src
+ old_rods.use(5)
+ use(1)
+ if(QDELETED(src) && replace && !QDELETED(new_rods))
+ user.put_in_hands(new_rods)
+ else
+ to_chat(user, span_warning("You need five rods and one sheet of titanium to make shuttle frame rods!"))
+ return
+ return ..()
+
/obj/item/stack/sheet/mineral/titanium/fifty
amount = 50
diff --git a/code/game/objects/structures/lattice.dm b/code/game/objects/structures/lattice.dm
index f22ef3f7e2b..40bc738e2fb 100644
--- a/code/game/objects/structures/lattice.dm
+++ b/code/game/objects/structures/lattice.dm
@@ -76,16 +76,14 @@
/obj/structure/lattice/rcd_act(mob/user, obj/item/construction/rcd/the_rcd, list/rcd_data)
if(rcd_data["[RCD_DESIGN_MODE]"] == RCD_TURF)
var/design_structure = rcd_data["[RCD_DESIGN_PATH]"]
- if(design_structure == /turf/open/floor/plating)
+ if(design_structure == /turf/open/floor/plating/rcd)
var/turf/T = src.loc
if(isgroundlessturf(T))
T.place_on_top(/turf/open/floor/plating, flags = CHANGETURF_INHERIT_AIR)
qdel(src)
return TRUE
if(design_structure == /obj/structure/lattice/catwalk)
- var/turf/turf = loc
- qdel(src)
- new /obj/structure/lattice/catwalk(turf)
+ replace_with_catwalk()
return TRUE
return FALSE
@@ -93,6 +91,15 @@
if(current_size >= STAGE_FOUR)
deconstruct()
+/obj/structure/lattice/proc/replace_with_catwalk()
+ var/list/post_replacement_callbacks = list()
+ SEND_SIGNAL(src, COMSIG_LATTICE_PRE_REPLACE_WITH_CATWALK, post_replacement_callbacks)
+ var/turf/turf = loc
+ qdel(src)
+ var/new_catwalk = new /obj/structure/lattice/catwalk(turf)
+ for(var/datum/callback/callback as anything in post_replacement_callbacks)
+ callback.Invoke(new_catwalk)
+
/obj/structure/lattice/catwalk
name = "catwalk"
desc = "A catwalk for easier EVA maneuvering and cable placement."
diff --git a/code/game/shuttle_engines.dm b/code/game/shuttle_engines.dm
index 726b7274834..f8d2a9db058 100644
--- a/code/game/shuttle_engines.dm
+++ b/code/game/shuttle_engines.dm
@@ -26,6 +26,8 @@
///The mobile ship we are connected to.
var/datum/weakref/connected_ship_ref
+ var/static/list/connections = list(COMSIG_TURF_ADDED_TO_SHUTTLE = PROC_REF(on_turf_added_to_shuttle))
+
/datum/armor/power_shuttle_engine
melee = 100
bullet = 10
@@ -35,6 +37,7 @@
/obj/machinery/power/shuttle_engine/Initialize(mapload)
. = ..()
+ AddComponent(/datum/component/simple_rotation)
register_context()
/obj/machinery/power/shuttle_engine/connect_to_shuttle(mapload, obj/docking_port/mobile/port, obj/docking_port/stationary/dock)
@@ -83,6 +86,7 @@
port.engine_list -= src
port.current_engine_power -= initial(engine_power)
connected_ship_ref = null
+ RemoveElement(/datum/element/connect_loc, connections)
//Ugh this is a lot of copypasta from emitters, welding need some boilerplate reduction
/obj/machinery/power/shuttle_engine/can_be_unfasten_wrench(mob/user, silent)
@@ -97,11 +101,17 @@
if(. == SUCCESSFUL_UNFASTEN)
if(anchored)
connect_to_shuttle(port = SSshuttle.get_containing_shuttle(src)) //connect to a new ship, if needed
+ if(!connected_ship_ref?.resolve())
+ AddElement(/datum/element/connect_loc, connections)
engine_state = ENGINE_WRENCHED
else
unsync_ship() //not part of the ship anymore
engine_state = ENGINE_UNWRENCHED
+/obj/machinery/power/shuttle_engine/proc/on_turf_added_to_shuttle(turf/source, obj/docking_port/mobile/port)
+ SIGNAL_HANDLER
+ connect_to_shuttle(port = port)
+
/obj/machinery/power/shuttle_engine/wrench_act(mob/living/user, obj/item/tool)
. = ..()
default_unfasten_wrench(user, tool)
diff --git a/code/game/turfs/baseturfs.dm b/code/game/turfs/baseturfs.dm
index 29a288b1b59..832b7c1def5 100644
--- a/code/game/turfs/baseturfs.dm
+++ b/code/game/turfs/baseturfs.dm
@@ -171,3 +171,13 @@
var/floor_position = baseturfs.Find(floor)
if(floor_position != 0)
insert_baseturf(floor_position + 1, roof)
+
+/// Places a baseturf below a searched for baseturf.
+/turf/proc/stack_below_baseturf(search_type, stack_type)
+ if(!islist(baseturfs))
+ baseturfs = list(baseturfs)
+ var/search_position = baseturfs.Find(search_type)
+ if(search_position != 0)
+ insert_baseturf(search_position, stack_type)
+ else if(type == search_type)
+ insert_baseturf(turf_type = stack_type)
diff --git a/code/game/turfs/change_turf.dm b/code/game/turfs/change_turf.dm
index dc2c257ad38..9e00c034678 100644
--- a/code/game/turfs/change_turf.dm
+++ b/code/game/turfs/change_turf.dm
@@ -128,6 +128,16 @@ GLOBAL_LIST_INIT(blacklisted_automated_baseturfs, typecacheof(list(
if(!(flags & CHANGETURF_DEFER_CHANGE))
new_turf.AfterChange(flags, old_type)
+ if(flags & CHANGETURF_GENERATE_SHUTTLE_CEILING)
+ var/turf/above = get_step_multiz(src, UP)
+ if(above)
+ if(!(istype(above, /turf/open/floor/engine/hull/ceiling) || above.depth_to_find_baseturf(/turf/open/floor/engine/hull/ceiling)))
+ if(istype(above, /turf/open/openspace) || istype(above, /turf/open/space/openspace))
+ above.place_on_top(/turf/open/floor/engine/hull/ceiling)
+ else
+ above.stack_ontop_of_baseturf(/turf/open/openspace, /turf/open/floor/engine/hull/ceiling)
+ above.stack_ontop_of_baseturf(/turf/open/space/openspace, /turf/open/floor/engine/hull/ceiling)
+
new_turf.blueprint_data = old_bp
new_turf.rcd_memory = old_rcd_memory
new_turf.explosion_throw_details = old_explosion_throw_details
@@ -284,8 +294,12 @@ GLOBAL_LIST_INIT(blacklisted_automated_baseturfs, typecacheof(list(
/// Attempts to replace a tile with lattice. Amount is the amount of tiles to scrape away.
/turf/proc/attempt_lattice_replacement(amount = 2)
if(lattice_underneath)
+ var/list/successful_replacement_callbacks = list()
+ SEND_SIGNAL(src, COMSIG_TURF_ATTEMPT_LATTICE_REPLACEMENT, successful_replacement_callbacks)
var/turf/new_turf = ScrapeAway(amount, flags = CHANGETURF_INHERIT_AIR)
if(!istype(new_turf, /turf/open/floor))
- new /obj/structure/lattice(src)
+ var/new_lattice = new /obj/structure/lattice(src)
+ for(var/datum/callback/callback as anything in successful_replacement_callbacks)
+ callback.Invoke(new_lattice)
else
ScrapeAway(amount, flags = CHANGETURF_INHERIT_AIR)
diff --git a/code/game/turfs/open/_open.dm b/code/game/turfs/open/_open.dm
index 8f99f494eeb..6e0d91dbcad 100644
--- a/code/game/turfs/open/_open.dm
+++ b/code/game/turfs/open/_open.dm
@@ -434,10 +434,9 @@
if(catwalk_bait)
if(used_rods.use(1))
- qdel(catwalk_bait)
to_chat(user, span_notice("You construct a catwalk."))
playsound(src, 'sound/items/weapons/genhit.ogg', 50, TRUE)
- new /obj/structure/lattice/catwalk(src)
+ catwalk_bait.replace_with_catwalk()
else
to_chat(user, span_warning("You need two rods to build a catwalk!"))
return
@@ -445,7 +444,9 @@
if(used_rods.use(1))
to_chat(user, span_notice("You construct a lattice."))
playsound(src, 'sound/items/weapons/genhit.ogg', 50, TRUE)
- new /obj/structure/lattice(src)
+ var/obj/structure/lattice/new_lattice = new (src)
+ if(istype(used_rods, /obj/item/stack/rods/shuttle) && !istype(loc, /area/shuttle))
+ new_lattice.AddElement(/datum/element/shuttle_construction_lattice)
else
to_chat(user, span_warning("You need one rod to build a lattice."))
@@ -462,6 +463,8 @@
playsound(src, 'sound/items/weapons/genhit.ogg', 50, TRUE)
var/turf/open/floor/plating/new_plating = place_on_top(/turf/open/floor/plating, flags = CHANGETURF_INHERIT_AIR)
+ if(istype(loc, /area/shuttle))
+ new_plating.insert_baseturf(turf_type = /turf/baseturf_skipover/shuttle)
if(lattice)
qdel(lattice)
else
diff --git a/code/game/turfs/open/floor/hull.dm b/code/game/turfs/open/floor/hull.dm
index 6836c29b960..2aec1f97fd3 100644
--- a/code/game/turfs/open/floor/hull.dm
+++ b/code/game/turfs/open/floor/hull.dm
@@ -8,7 +8,6 @@
/turf/open/floor/engine/hull/ceiling
name = "shuttle ceiling plating"
- var/old_turf_type
/turf/open/floor/engine/hull/ceiling/Initialize(mapload)
. = ..()
@@ -20,10 +19,6 @@
///datum/unit_test/mapping_nearstation_test.dm SHOULD fail this case automatically
//this is just here so the mapper responsible can easily see where the issues are directly on the map.
-/turf/open/floor/engine/hull/ceiling/AfterChange(flags, oldType)
- . = ..()
- old_turf_type = oldType
-
/turf/open/floor/engine/hull/reinforced
name = "exterior reinforced hull plating"
desc = "Extremely sturdy exterior hull plating that separates you from the uncaring vacuum of space."
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 7e839e9d709..c5a2f8fe423 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -244,6 +244,9 @@ GLOBAL_LIST_EMPTY(station_turfs)
old_area.turfs_to_uncontain_by_zlevel[z] += src
new_area.turfs_by_zlevel[z] += src
new_area.contents += src
+ SEND_SIGNAL(src, COMSIG_TURF_AREA_CHANGED, old_area)
+ SEND_SIGNAL(new_area, COMSIG_AREA_TURF_ADDED, src, old_area)
+ SEND_SIGNAL(old_area, COMSIG_AREA_TURF_REMOVED, src, new_area)
//changes to make after turf has moved
on_change_area(old_area, new_area)
diff --git a/code/modules/cargo/goodies.dm b/code/modules/cargo/goodies.dm
index ee48e48e403..3b3aec6680c 100644
--- a/code/modules/cargo/goodies.dm
+++ b/code/modules/cargo/goodies.dm
@@ -366,3 +366,16 @@
desc = "Many people consider mice to be vermin, or dirty lab animals for experimentation, or a culinary delicacy. That's why we're not asking any questions, here."
cost = PAYCHECK_CREW * 1.5
contains = list(/obj/item/pet_carrier/small/mouse)
+
+/datum/supply_pack/goody/shuttle_construction_kit
+ name = "Shuttle Construction Starter Kit"
+ desc = "Contains a set of shuttle blueprints, and the circuitboards necessary for constructing your own shuttle. \
+ Well at least the ones you can't source yourself without Science's help."
+ cost = PAYCHECK_COMMAND * 12 //You assistants with shipwrighting ambitions can do a couple bounties, can't you?
+ access_view = ACCESS_AUX_BASE //Engineers have it, QM can give it to whoever, and scientists can just research the tech.
+ contains = list(
+ /obj/item/shuttle_blueprints,
+ /obj/item/circuitboard/computer/shuttle/flight_control,
+ /obj/item/circuitboard/computer/shuttle/docker,
+ /obj/item/circuitboard/machine/engine/propulsion = 2,
+ )
diff --git a/code/modules/cargo/packs/engineering.dm b/code/modules/cargo/packs/engineering.dm
index dd376ec2017..0173fd69e7f 100644
--- a/code/modules/cargo/packs/engineering.dm
+++ b/code/modules/cargo/packs/engineering.dm
@@ -106,7 +106,6 @@
contains = list(/obj/machinery/power/shuttle_engine/propulsion/burst/cargo)
crate_name = "shuttle engine crate"
crate_type = /obj/structure/closet/crate/secure/engineering
- special = TRUE
/datum/supply_pack/engineering/tools
name = "Toolbox Crate"
diff --git a/code/modules/hallucination/_hallucination.dm b/code/modules/hallucination/_hallucination.dm
index b3f33fc3191..df3bbe74757 100644
--- a/code/modules/hallucination/_hallucination.dm
+++ b/code/modules/hallucination/_hallucination.dm
@@ -107,6 +107,8 @@
var/image_layer = MOB_LAYER
/// The plane of the image
var/image_plane = GAME_PLANE
+ /// Should this image holder persist if there are no seers for it?
+ var/persist_without_seers = FALSE
/obj/effect/client_image_holder/Initialize(mapload, list/mobs_which_see_us)
. = ..()
@@ -121,10 +123,7 @@
who_sees_us = list()
for(var/mob/seer as anything in mobs_which_see_us)
- RegisterSignal(seer, COMSIG_MOB_LOGIN, PROC_REF(show_image_to))
- RegisterSignal(seer, COMSIG_QDELETING, PROC_REF(remove_seer))
- who_sees_us += seer
- show_image_to(seer)
+ add_seer(seer)
/obj/effect/client_image_holder/Destroy(force)
if(shown_image)
@@ -141,6 +140,12 @@
return
SET_PLANE(shown_image, PLANE_TO_TRUE(shown_image.plane), new_turf)
+/obj/effect/client_image_holder/proc/add_seer(mob/new_seer)
+ RegisterSignal(new_seer, COMSIG_MOB_LOGIN, PROC_REF(show_image_to))
+ RegisterSignal(new_seer, COMSIG_QDELETING, PROC_REF(remove_seer))
+ who_sees_us += new_seer
+ show_image_to(new_seer)
+
/// Signal proc to clean up references if people who see us are deleted.
/obj/effect/client_image_holder/proc/remove_seer(mob/source)
SIGNAL_HANDLER
@@ -150,7 +155,7 @@
who_sees_us -= source
// No reason to exist, anymore
- if(!QDELETED(src) && !length(who_sees_us))
+ if(!QDELETED(src) && !length(who_sees_us) && !persist_without_seers)
qdel(src)
/// Generates the image which we take on.
diff --git a/code/modules/research/designs/comp_board_designs.dm b/code/modules/research/designs/comp_board_designs.dm
index 6db2a6bb24e..50ed6e43f61 100644
--- a/code/modules/research/designs/comp_board_designs.dm
+++ b/code/modules/research/designs/comp_board_designs.dm
@@ -395,3 +395,19 @@
RND_CATEGORY_COMPUTER + RND_SUBCATEGORY_COMPUTER_RECORDS
)
departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+
+/datum/design/board/shuttle
+ category = list("Computer Boards", "Shuttle Machinery")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO
+
+/datum/design/board/shuttle/flight_control
+ name = "Computer Design (Shuttle Flight Controls)"
+ 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
+
+/datum/design/board/shuttle/shuttle_docker
+ name = "Computer Design (Shuttle Navigation Computer)"
+ 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
diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm
index 6364025924b..5fb67b7356e 100644
--- a/code/modules/research/designs/machine_designs.dm
+++ b/code/modules/research/designs/machine_designs.dm
@@ -1357,3 +1357,13 @@
RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_CARGO
)
departmental_flags = DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_ENGINEERING
+
+/datum/design/board/propulsion_engine
+ name = "Propulsion Engine Board"
+ desc = "The circuit for a propulsion engine."
+ id = "propulsion_engine"
+ build_path = /obj/item/circuitboard/machine/engine/propulsion
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING
diff --git a/code/modules/research/designs/mechfabricator_designs.dm b/code/modules/research/designs/mechfabricator_designs.dm
index c06eb40204e..7c288770240 100644
--- a/code/modules/research/designs/mechfabricator_designs.dm
+++ b/code/modules/research/designs/mechfabricator_designs.dm
@@ -1705,6 +1705,20 @@
RND_CATEGORY_MECHFAB_CYBORG_MODULES + RND_SUBCATEGORY_MECHFAB_CYBORG_MODULES_SERVICE
)
+/datum/design/borg_upgrade_shuttle_blueprints
+ name = "Engineering Shuttle Blueprints"
+ id = "borg_upgrade_engineering_shuttle_blueprints"
+ build_type = MECHFAB
+ build_path = /obj/item/borg/upgrade/shuttle_blueprints
+ materials = list(
+ /datum/material/iron = SHEET_MATERIAL_AMOUNT*5,
+ /datum/material/glass = SHEET_MATERIAL_AMOUNT*2.5,
+ )
+ construction_time = 4 SECONDS
+ category = list(
+ RND_CATEGORY_MECHFAB_CYBORG_MODULES + RND_SUBCATEGORY_MECHFAB_CYBORG_MODULES_ENGINEERING
+ )
+
/datum/design/borg_upgrade_expand
name = "Expand Module"
id = "borg_upgrade_expand"
diff --git a/code/modules/research/designs/misc_designs.dm b/code/modules/research/designs/misc_designs.dm
index c7441285fc3..6221444ab9d 100644
--- a/code/modules/research/designs/misc_designs.dm
+++ b/code/modules/research/designs/misc_designs.dm
@@ -1116,6 +1116,17 @@
)
departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_CARGO
+/datum/design/shuttle_rods
+ name = "Shuttle Frame Rods"
+ id = "shuttlerods"
+ build_type = PROTOLATHE
+ materials = list(/datum/material/iron =HALF_SHEET_MATERIAL_AMOUNT, /datum/material/titanium = SMALL_MATERIAL_AMOUNT)
+ build_path = /obj/item/stack/rods/shuttle
+ category = list(
+ RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING
+
// Experimental designs
/datum/design/polymorph_belt
diff --git a/code/modules/research/designs/tool_designs.dm b/code/modules/research/designs/tool_designs.dm
index 901bc304dc1..fdd96513848 100644
--- a/code/modules/research/designs/tool_designs.dm
+++ b/code/modules/research/designs/tool_designs.dm
@@ -425,3 +425,13 @@
RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_ENGINEERING_ADVANCED
)
departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING
+
+/datum/design/shuttle_blueprints
+ name = "Shuttle Blueprints"
+ desc = "Blueprints suitable for constructing shuttles"
+ id = "shuttle_blueprints"
+ build_type = PROTOLATHE
+ materials = list(/datum/material/plastic = HALF_SHEET_MATERIAL_AMOUNT)
+ build_path = /obj/item/shuttle_blueprints
+ category = list(RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_ENGINEERING)
+ departmental_flags = DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_SCIENCE
diff --git a/code/modules/research/techweb/nodes/engi_nodes.dm b/code/modules/research/techweb/nodes/engi_nodes.dm
index 75c9459771c..69f653d8db8 100644
--- a/code/modules/research/techweb/nodes/engi_nodes.dm
+++ b/code/modules/research/techweb/nodes/engi_nodes.dm
@@ -182,6 +182,22 @@
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_1_POINTS)
announce_channels = list(RADIO_CHANNEL_ENGINEERING)
+/datum/techweb_node/shuttle_engineering
+ id = TECHWEB_NODE_SHUTTLE_ENG
+ display_name = "Shuttle Engineering"
+ description = "Materials and equipment for constructing shuttles"
+ prereq_ids = list(TECHWEB_NODE_ENERGY_MANIPULATION, TECHWEB_NODE_APPLIED_BLUESPACE)
+ design_ids = list(
+ "borg_upgrade_engineering_shuttle_blueprints",
+ "propulsion_engine",
+ "shuttle_blueprints",
+ "shuttle_control",
+ "shuttle_docker",
+ "shuttlerods",
+ )
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_1_POINTS)
+ announce_channels = list(RADIO_CHANNEL_ENGINEERING, RADIO_CHANNEL_SCIENCE, RADIO_CHANNEL_SUPPLY)
+
/datum/techweb_node/holographics
id = TECHWEB_NODE_HOLOGRAPHICS
display_name = "Holographics"
diff --git a/code/modules/shuttle/mobile_port/mobile_port.dm b/code/modules/shuttle/mobile_port/mobile_port.dm
index 8679c1f7cf7..b98528d2e08 100644
--- a/code/modules/shuttle/mobile_port/mobile_port.dm
+++ b/code/modules/shuttle/mobile_port/mobile_port.dm
@@ -56,7 +56,12 @@
///List of shuttle events that can run or are running
var/list/datum/shuttle_event/event_list = list()
-/obj/docking_port/mobile/Initialize(mapload)
+ var/list/underlying_areas_by_turf = list()
+
+ ///How many turfs this shuttle has. Used to check against max shuttle size when expanding expandable shuttles.
+ var/turf_count = 0
+
+/obj/docking_port/mobile/Initialize(mapload, list/areas)
. = ..()
if(!shuttle_id)
@@ -71,12 +76,17 @@
shuttle_id = "[tmp_id]_[counter]"
name = "[tmp_name] [counter]"
- var/list/all_turfs = return_ordered_turfs(x, y, z, dir)
- for(var/i in 1 to all_turfs.len)
- var/turf/curT = all_turfs[i]
- var/area/cur_area = curT.loc
- if(istype(cur_area, area_type))
- shuttle_areas[cur_area] = TRUE
+ if(areas)
+ for(var/area/area as anything in areas)
+ shuttle_areas[area] = TRUE
+ else
+ var/list/all_turfs = return_ordered_turfs(x, y, z, dir)
+ for(var/i in 1 to all_turfs.len)
+ var/turf/curT = all_turfs[i]
+ var/area/cur_area = curT.loc
+ if(istype(cur_area, area_type))
+ turf_count++
+ shuttle_areas[cur_area] = TRUE
#ifdef TESTING
highlight("#0f0")
@@ -111,28 +121,28 @@
if(!length(shuttle_areas))
CRASH("Attempted to calculate a docking port's information without a template before it was assigned any areas!")
// no template given, use shuttle_areas to calculate width and height
- var/min_x = -1
- var/min_y = -1
- var/max_x = WORLDMAXX_CUTOFF
- var/max_y = WORLDMAXY_CUTOFF
+ var/min_x = WORLDMAXX_CUTOFF
+ var/min_y = WORLDMAXY_CUTOFF
+ var/max_x = -1
+ var/max_y = -1
for(var/area/shuttle_area as anything in shuttle_areas)
for (var/list/zlevel_turfs as anything in shuttle_area.get_zlevel_turf_lists())
for(var/turf/turf as anything in zlevel_turfs)
- min_x = max(turf.x, min_x)
- max_x = min(turf.x, max_x)
- min_y = max(turf.y, min_y)
- max_y = min(turf.y, max_y)
+ min_x = min(turf.x, min_x)
+ max_x = max(turf.x, max_x)
+ min_y = min(turf.y, min_y)
+ max_y = max(turf.y, max_y)
CHECK_TICK
- if(min_x == -1 || max_x == WORLDMAXX_CUTOFF)
+ if(min_x == WORLDMAXX_CUTOFF || max_x == -1)
CRASH("Failed to locate shuttle boundaries when iterating through shuttle areas, somehow.")
- if(min_y == -1 || max_y == WORLDMAXY_CUTOFF)
+ if(min_y == WORLDMAXY_CUTOFF || max_y == -1)
CRASH("Failed to locate shuttle boundaries when iterating through shuttle areas, somehow.")
width = (max_x - min_x) + 1
height = (max_y - min_y) + 1
- port_x_offset = min_x - x
- port_y_offset = min_y - y
+ port_x_offset = x - min_x + 1
+ port_y_offset = y - min_y + 1
if(dir in list(EAST, WEST))
src.width = height
@@ -157,13 +167,19 @@
#undef WORLDMAXX_CUTOFF
#undef WORLDMAXY_CUTOFF
+/obj/docking_port/mobile/is_in_shuttle_bounds(atom/A)
+ . = ..()
+ if(. && !shuttle_areas[get_area(A)])
+ return FALSE
+
/**
* Actions to be taken after shuttle is loaded but before it has been moved out of transit z-level to its final location
*
* Arguments:
* * replace - TRUE if this shuttle is replacing an existing one. FALSE by default.
+ * * custom - TRUE if this shuttle should be added to the custom shuttle list. FALSE by default.
*/
-/obj/docking_port/mobile/register(replace = FALSE)
+/obj/docking_port/mobile/register(replace = FALSE, custom = FALSE)
. = ..()
if(!shuttle_id)
shuttle_id = "shuttle"
@@ -185,6 +201,9 @@
SSshuttle.mobile_docking_ports += src
+ if(custom)
+ SSshuttle.custom_shuttles += src
+
/**
* Actions to be taken after shuttle is loaded and has been moved to its final location
*
@@ -197,6 +216,7 @@
/obj/docking_port/mobile/unregister()
. = ..()
SSshuttle.mobile_docking_ports -= src
+ SSshuttle.custom_shuttles -= src
diff --git a/code/modules/shuttle/mobile_port/shuttle_move.dm b/code/modules/shuttle/mobile_port/shuttle_move.dm
index b7e125826dc..5c748bc2cfd 100644
--- a/code/modules/shuttle/mobile_port/shuttle_move.dm
+++ b/code/modules/shuttle/mobile_port/shuttle_move.dm
@@ -17,11 +17,11 @@
var/obj/docking_port/stationary/old_dock = get_docked()
- // The area that gets placed under where the shuttle moved from
- var/underlying_area_type = SHUTTLE_DEFAULT_UNDERLYING_AREA
+ // The area that gets placed under shuttle turfs that do not have their own area to place down
+ var/fallback_area_type = SHUTTLE_DEFAULT_UNDERLYING_AREA
if(old_dock) //Dock overwrites
- underlying_area_type = old_dock.area_type
+ fallback_area_type = old_dock.area_type
/**************************************************************************************************************
Both lists are associative with a turf:bitflag structure. (new_turfs bitflag space unused currently)
@@ -33,11 +33,11 @@
CHECK_TICK
/**************************************************************************************************************/
- // The underlying old area is the area assumed to be under the shuttle's starting location
+ // The fallback area is the area for shuttle turfs that have no area underneath them
// If it no longer/has never existed it will be created
- var/area/underlying_old_area = GLOB.areas_by_type[underlying_area_type]
- if(!underlying_old_area)
- underlying_old_area = new underlying_area_type(null)
+ var/area/fallback_area = GLOB.areas_by_type[fallback_area_type]
+ if(!fallback_area)
+ fallback_area = new fallback_area_type(null)
var/rotation = 0
if(new_dock.dir != dir) //Even when the dirs are the same rotation is coming out as not 0 for some reason
@@ -51,8 +51,9 @@
var/list/moved_atoms = list() //Everything not a turf that gets moved in the shuttle
var/list/areas_to_move = list() //unique assoc list of areas on turfs being moved
+ var/list/underlying_areas = list() //unique assoc list of areas beneath turfs being moved
- . = preflight_check(old_turfs, new_turfs, areas_to_move, rotation)
+ . = preflight_check(old_turfs, new_turfs, areas_to_move, underlying_areas, rotation)
if(.)
remove_ripples()
return
@@ -81,11 +82,11 @@
// Moving to the new location will trample the ripples there at the exact
// same time any mobs there are trampled, to avoid any discrepancy where
// the ripples go away before it is safe.
- takeoff(old_turfs, new_turfs, moved_atoms, rotation, movement_direction, old_dock, underlying_old_area)
+ takeoff(old_turfs, new_turfs, moved_atoms, rotation, movement_direction, old_dock, fallback_area)
CHECK_TICK
- cleanup_runway(new_dock, old_turfs, new_turfs, areas_to_move, moved_atoms, rotation, movement_direction, underlying_old_area)
+ cleanup_runway(new_dock, old_turfs, new_turfs, areas_to_move, underlying_areas, moved_atoms, rotation, movement_direction, fallback_area)
CHECK_TICK
@@ -103,7 +104,7 @@
remove_ripples()
return DOCKING_SUCCESS
-/obj/docking_port/mobile/proc/preflight_check(list/old_turfs, list/new_turfs, list/areas_to_move, rotation)
+/obj/docking_port/mobile/proc/preflight_check(list/old_turfs, list/new_turfs, list/areas_to_move, list/underlying_areas, rotation)
for(var/i in 1 to length(old_turfs))
CHECK_TICK
var/turf/oldT = old_turfs[i]
@@ -126,11 +127,14 @@
move_mode = newT.toShuttleMove(oldT, move_mode, src) //turfs
if(move_mode & MOVE_AREA)
+ var/area/underlying_area = underlying_areas_by_turf[oldT]
+ if(underlying_area)
+ underlying_areas[underlying_area] = TRUE
areas_to_move[old_area] = TRUE
old_turfs[oldT] = move_mode
-/obj/docking_port/mobile/proc/takeoff(list/old_turfs, list/new_turfs, list/moved_atoms, rotation, movement_direction, old_dock, area/underlying_old_area)
+/obj/docking_port/mobile/proc/takeoff(list/old_turfs, list/new_turfs, list/moved_atoms, rotation, movement_direction, old_dock, area/fallback_area)
for(var/i in 1 to old_turfs.len)
var/turf/oldT = old_turfs[i]
var/turf/newT = new_turfs[i]
@@ -141,7 +145,7 @@
if(move_mode & MOVE_AREA)
var/area/shuttle_area = oldT.loc
- shuttle_area.onShuttleMove(oldT, newT, underlying_old_area) //areas
+ shuttle_area.onShuttleMove(oldT, newT, src, fallback_area) //areas
if(move_mode & MOVE_CONTENTS)
for(var/k in oldT)
@@ -152,8 +156,12 @@
moved_atoms[moving_atom] = oldT
-/obj/docking_port/mobile/proc/cleanup_runway(obj/docking_port/stationary/new_dock, list/old_turfs, list/new_turfs, list/areas_to_move, list/moved_atoms, rotation, movement_direction, area/underlying_old_area)
- underlying_old_area.afterShuttleMove(0)
+/obj/docking_port/mobile/proc/cleanup_runway(obj/docking_port/stationary/new_dock, list/old_turfs, list/new_turfs, list/areas_to_move, list/underlying_areas, list/moved_atoms, rotation, movement_direction, area/fallback_area)
+ fallback_area.afterShuttleMove(0)
+ for(var/i in 1 to underlying_areas.len)
+ CHECK_TICK
+ var/area/underlying_area = underlying_areas[i]
+ underlying_area.afterShuttleMove(0)
// Parallax handling
// This needs to be done before the atom after move
@@ -167,24 +175,28 @@
for(var/i in 1 to old_turfs.len)
CHECK_TICK
- if(!(old_turfs[old_turfs[i]] & MOVE_TURF))
- continue
var/turf/old_turf = old_turfs[i]
+ var/old_move_mode = old_turfs[old_turf]
+ var/turf/old_ceiling = get_step_multiz(old_turf, UP)
+ if(old_ceiling) // check if a ceiling was generated previously
+ // remove old ceiling
+ if(istype(old_ceiling, /turf/open/floor/engine/hull/ceiling))
+ old_ceiling.ScrapeAway()
+ else
+ old_ceiling.remove_baseturfs_from_typecache(list(/turf/open/floor/engine/hull/ceiling = TRUE))
+ if(!(old_move_mode & MOVE_TURF))
+ continue
var/turf/new_turf = new_turfs[i]
new_turf.afterShuttleMove(old_turf, rotation) //turfs
var/turf/new_ceiling = get_step_multiz(new_turf, UP) // check if a ceiling is needed
if(new_ceiling)
// generate ceiling
- if(istype(new_ceiling, /turf/open/openspace)) // why is this needed? because we have 2 different typepaths for openspace
- new_ceiling.ChangeTurf(/turf/open/floor/engine/hull/ceiling, list(/turf/open/openspace))
- else if (istype(new_ceiling, /turf/open/space/openspace))
- new_ceiling.ChangeTurf(/turf/open/floor/engine/hull/ceiling, list(/turf/open/space/openspace))
- var/turf/old_ceiling = get_step_multiz(old_turf, UP)
- if(old_ceiling && istype(old_ceiling, /turf/open/floor/engine/hull/ceiling)) // check if a ceiling was generated previously
- // remove old ceiling
- var/turf/open/floor/engine/hull/ceiling/old_shuttle_ceiling = old_ceiling
- old_shuttle_ceiling.ChangeTurf(old_shuttle_ceiling.old_turf_type)
-
+ if(!(istype(new_ceiling, /turf/open/floor/engine/hull/ceiling) || new_ceiling.depth_to_find_baseturf(/turf/open/floor/engine/hull/ceiling)))
+ if(istype(new_ceiling, /turf/open/openspace) || istype(new_ceiling, /turf/open/space/openspace))
+ new_ceiling.place_on_top(/turf/open/floor/engine/hull/ceiling)
+ else
+ new_ceiling.stack_ontop_of_baseturf(/turf/open/openspace, /turf/open/floor/engine/hull/ceiling)
+ new_ceiling.stack_ontop_of_baseturf(/turf/open/space/openspace, /turf/open/floor/engine/hull/ceiling)
for(var/i in 1 to moved_atoms.len)
CHECK_TICK
var/atom/movable/moved_object = moved_atoms[i]
@@ -195,13 +207,18 @@
// lateShuttleMove (There had better be a really good reason for additional stages beyond this)
- underlying_old_area.lateShuttleMove()
+ fallback_area.lateShuttleMove()
for(var/i in 1 to areas_to_move.len)
CHECK_TICK
var/area/internal_area = areas_to_move[i]
internal_area.lateShuttleMove()
+ for(var/i in 1 to underlying_areas.len)
+ CHECK_TICK
+ var/area/underlying_area = underlying_areas[i]
+ underlying_area.lateShuttleMove()
+
for(var/i in 1 to old_turfs.len)
CHECK_TICK
if(!(old_turfs[old_turfs[i]] & MOVE_CONTENTS | MOVE_TURF))
diff --git a/code/modules/shuttle/mobile_port/shuttle_move_callbacks.dm b/code/modules/shuttle/mobile_port/shuttle_move_callbacks.dm
index 4dc5e8142b0..07d9cf65669 100644
--- a/code/modules/shuttle/mobile_port/shuttle_move_callbacks.dm
+++ b/code/modules/shuttle/mobile_port/shuttle_move_callbacks.dm
@@ -89,6 +89,11 @@ All ShuttleMove procs go here
/////////////////////////////////////////////////////////////////////////////////////
+// Return the move_move (based on the old), without any side effects.
+// This is for checking what would be moved if src is on a shuttle being moved.
+/atom/movable/proc/hypotheticalShuttleMove(rotation, move_mode, obj/docking_port/mobile/moving_dock)
+ return move_mode
+
// Called on every atom in shuttle turf contents before anything has been moved
// returns the new move_mode (based on the old)
// WARNING: Do not leave turf contents in beforeShuttleMove or dock() will runtime
@@ -141,16 +146,19 @@ All ShuttleMove procs go here
return MOVE_AREA
// Called on areas to move their turf between areas
-/area/proc/onShuttleMove(turf/oldT, turf/newT, area/underlying_old_area)
+/area/proc/onShuttleMove(turf/oldT, turf/newT, obj/docking_port/mobile/shuttle, area/fallback_area)
if(newT == oldT) // In case of in place shuttle rotation shenanigans.
return TRUE
- oldT.change_area(src, underlying_old_area)
+ var/area/underlying_area = shuttle.underlying_areas_by_turf[oldT]
+ oldT.change_area(src, underlying_area || fallback_area)
+ shuttle.underlying_areas_by_turf -= oldT
//The old turf has now been given back to the area that turf originaly belonged to
var/area/old_dest_area = newT.loc
parallax_movedir = old_dest_area.parallax_movedir
newT.change_area(old_dest_area, src)
+ shuttle.underlying_areas_by_turf[newT] = old_dest_area
return TRUE
// Called on areas after everything has been moved
@@ -218,6 +226,11 @@ All ShuttleMove procs go here
break
+/obj/machinery/camera/hypotheticalShuttleMove(rotation, move_mode, obj/docking_port/mobile/moving_dock)
+ . = ..()
+ if(. & MOVE_AREA)
+ . |= MOVE_CONTENTS
+
/obj/machinery/camera/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock)
. = ..()
if(. & MOVE_AREA)
@@ -323,11 +336,21 @@ All ShuttleMove procs go here
/************************************Structure move procs************************************/
+/obj/structure/grille/hypotheticalShuttleMove(rotation, move_mode, obj/docking_port/mobile/moving_dock)
+ . = ..()
+ if(. & MOVE_AREA)
+ . |= MOVE_CONTENTS
+
/obj/structure/grille/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock)
. = ..()
if(. & MOVE_AREA)
. |= MOVE_CONTENTS
+/obj/structure/lattice/hypotheticalShuttleMove(rotation, move_mode, obj/docking_port/mobile/moving_dock)
+ . = ..()
+ if(. & MOVE_AREA)
+ . |= MOVE_CONTENTS
+
/obj/structure/lattice/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock)
. = ..()
if(. & MOVE_AREA)
@@ -342,6 +365,11 @@ All ShuttleMove procs go here
Connect_cable(TRUE)
propagate_if_no_network()
+/obj/machinery/power/shuttle_engine/hypotheticalShuttleMove(move_mode)
+ . = ..()
+ if(. & MOVE_AREA)
+ . |= MOVE_CONTENTS
+
/obj/machinery/power/shuttle_engine/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock)
. = ..()
if(. & MOVE_AREA)
@@ -365,6 +393,11 @@ All ShuttleMove procs go here
/************************************Misc move procs************************************/
+/obj/docking_port/mobile/hypotheticalShuttleMove(rotation, move_mode, obj/docking_port/mobile/moving_dock)
+ . = ..()
+ if(moving_dock == src)
+ . |= MOVE_CONTENTS
+
/obj/docking_port/mobile/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock)
. = ..()
if(moving_dock == src)
diff --git a/code/modules/shuttle/mobile_port/variants/assault_pod.dm b/code/modules/shuttle/mobile_port/variants/assault_pod.dm
index 75baee5c392..aa1b7ea5259 100644
--- a/code/modules/shuttle/mobile_port/variants/assault_pod.dm
+++ b/code/modules/shuttle/mobile_port/variants/assault_pod.dm
@@ -7,7 +7,7 @@
return ..()
-/obj/docking_port/mobile/assault_pod/initiate_docking(obj/docking_port/stationary/S1)
+/obj/docking_port/mobile/assault_pod/initiate_docking(obj/docking_port/stationary/S1, force=FALSE)
. = ..()
if(!istype(S1, /obj/docking_port/stationary/transit))
playsound(get_turf(src.loc), 'sound/effects/explosion/explosion1.ogg',50,TRUE)
diff --git a/code/modules/shuttle/mobile_port/variants/custom/blueprints.dm b/code/modules/shuttle/mobile_port/variants/custom/blueprints.dm
new file mode 100644
index 00000000000..5185d268b8e
--- /dev/null
+++ b/code/modules/shuttle/mobile_port/variants/custom/blueprints.dm
@@ -0,0 +1,1051 @@
+GLOBAL_LIST_INIT(shuttle_construction_area_whitelist, list(/area/space, /area/lavaland, /area/icemoon, /area/station/asteroid))
+
+/obj/effect/client_image_holder/shuttle_construction_visualization
+ image_icon = 'icons/effects/alphacolors.dmi'
+ image_state = "transparent"
+ image_layer = ABOVE_NORMAL_TURF_LAYER
+ image_plane = ABOVE_GAME_PLANE
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ persist_without_seers = TRUE
+
+/datum/proximity_monitor/advanced/shuttle_construction_visualizer
+ edge_is_a_field = TRUE
+ var/mob/user
+ var/list/image_holders = list()
+ var/obj/item/shuttle_blueprints/parent
+
+/datum/proximity_monitor/advanced/shuttle_construction_visualizer/New(atom/_host, range, _ignore_if_not_on_turf)
+ . = ..()
+ parent = _host
+
+/datum/proximity_monitor/advanced/shuttle_construction_visualizer/Destroy()
+ . = ..()
+ parent = null
+ QDEL_LIST_ASSOC_VAL(image_holders)
+
+/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/set_user(mob/new_user)
+ if(user)
+ for(var/turf in image_holders)
+ var/obj/effect/client_image_holder/holder = image_holders[turf]
+ holder.remove_seer(user)
+ UnregisterSignal(user, list(COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT))
+ var/client/client = user.client
+ if(client)
+ UnregisterSignal(client, list(COMSIG_VIEW_SET, COMSIG_CLIENT_SET_EYE))
+ if(new_user)
+ user = new_user
+ for(var/turf in image_holders)
+ var/obj/effect/client_image_holder/holder = image_holders[turf]
+ holder.add_seer(user)
+ RegisterSignal(user, COMSIG_MOB_LOGIN, PROC_REF(on_user_login))
+ RegisterSignal(user, COMSIG_MOB_LOGOUT, PROC_REF(unregister_client))
+ var/client/client = user.client
+ if(client)
+ register_client(client)
+ else
+ unregister_client()
+
+/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/register_client(client/client)
+ var/atom/eye = client.eye
+ if(eye)
+ set_host(client.eye)
+ var/list/view_size = getviewsize(client.view)
+ set_range(CEILING(max(view_size[1], view_size[2])/2, 1)+1)
+ RegisterSignal(client, COMSIG_VIEW_SET, PROC_REF(on_view_set))
+ RegisterSignal(client, COMSIG_CLIENT_SET_EYE, PROC_REF(on_set_eye))
+
+/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/unregister_client()
+ SIGNAL_HANDLER
+ set_host(parent)
+ set_range(0)
+
+/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/on_user_login(mob/source)
+ SIGNAL_HANDLER
+ register_client(source.client)
+
+/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/on_view_set(datum/source, new_view)
+ SIGNAL_HANDLER
+ var/list/view_size = getviewsize(new_view)
+ set_range(CEILING(max(view_size[1], view_size[2])/2, 1)+1)
+
+/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/on_set_eye(datum/source, atom/old_eye, atom/new_eye)
+ SIGNAL_HANDLER
+ set_host(new_eye)
+
+/datum/proximity_monitor/advanced/shuttle_construction_visualizer/setup_field_turf(turf/target)
+ var/obj/effect/client_image_holder/shuttle_construction_visualization/holder = new(target, list())
+ image_holders[target] = holder
+ evaluate_turf_overlay(holder, target)
+ RegisterSignals(target, list(
+ COMSIG_TURF_CHANGE,
+ COMSIG_TURF_AREA_CHANGED,
+ SIGNAL_ADDTRAIT(TRAIT_SHUTTLE_CONSTRUCTION_TURF),
+ SIGNAL_REMOVETRAIT(TRAIT_SHUTTLE_CONSTRUCTION_TURF)
+ ), PROC_REF(on_turf_updated))
+ if(user)
+ holder.add_seer(user)
+
+/datum/proximity_monitor/advanced/shuttle_construction_visualizer/cleanup_field_turf(turf/target)
+ qdel(image_holders[target])
+ image_holders -= target
+ UnregisterSignal(target, list(
+ COMSIG_TURF_CHANGE,
+ COMSIG_TURF_AREA_CHANGED,
+ SIGNAL_ADDTRAIT(TRAIT_SHUTTLE_CONSTRUCTION_TURF),
+ SIGNAL_REMOVETRAIT(TRAIT_SHUTTLE_CONSTRUCTION_TURF)
+ ))
+
+/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/on_turf_updated(turf/source)
+ SIGNAL_HANDLER
+ evaluate_turf_overlay(image_holders[source], source)
+
+/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/evaluate_turf_overlay(obj/effect/client_image_holder/holder, turf/target)
+ var/area/turf_area = target.loc
+ if(HAS_TRAIT(target, TRAIT_SHUTTLE_CONSTRUCTION_TURF))
+ if(is_type_in_list(turf_area, GLOB.shuttle_construction_area_whitelist) || GLOB.custom_areas[turf_area])
+ holder.image_state = "green"
+ else
+ holder.image_state = "red"
+ else
+ if(GLOB.custom_areas[turf_area] && HAS_TRAIT(turf_area, TRAIT_HAS_SHUTTLE_CONSTRUCTION_TURF))
+ holder.image_state = "red"
+ else
+ holder.image_state = "transparent"
+ holder.regenerate_image()
+
+/obj/item/shuttle_blueprints
+ name = "shuttle blueprints"
+ desc = "A blank sheet of synthetic engineering-grade paper."
+ icon = 'icons/obj/scrolls.dmi'
+ icon_state = "shuttle_blueprints0"
+ base_icon_state = "shuttle_blueprints"
+ inhand_icon_state = "blueprints"
+ attack_verb_continuous = list("attacks", "baps", "hits")
+ attack_verb_simple = list("attack", "bap", "hit")
+ interaction_flags_atom = parent_type::interaction_flags_atom | INTERACT_ATOM_ALLOW_USER_LOCATION | INTERACT_ATOM_IGNORE_MOBILITY
+
+ var/base_desc = "A blank sheet of synthetic engineering-grade paper."
+ var/linked_desc = "A sheet of synthetic engineering-grade paper with shuttle schematics printed on it."
+
+ //A weakref to the mobile docking port of the shuttle these blueprints are linked to, if any.
+ var/datum/weakref/shuttle_ref
+
+ //Whether the holder can visualize shuttle frames (and any turfs preventing them from becoming complete shuttles)
+ var/visualize_frame_turfs = FALSE
+
+ var/offset_x = 0
+ var/offset_y = 0
+
+ var/datum/proximity_monitor/advanced/shuttle_construction_visualizer/prox_monitor
+
+/obj/item/shuttle_blueprints/Initialize(mapload)
+ . = ..()
+ prox_monitor = new(src, 0, FALSE)
+ update_appearance()
+
+/obj/item/shuttle_blueprints/equipped(mob/user, slot, initial)
+ . = ..()
+ var/static/list/connections = list(COMSIG_ITEM_PRE_ATTACK = PROC_REF(christen_check))
+ if(slot == ITEM_SLOT_HANDS)
+ AddComponent(/datum/component/connect_inventory, user, connections, allowed_slots = ITEM_SLOT_HANDS)
+
+/obj/item/shuttle_blueprints/dropped(mob/user, silent)
+ . = ..()
+ stop_visualizing(user)
+ qdel(GetComponent(/datum/component/connect_inventory))
+
+/obj/item/shuttle_blueprints/proc/christen_check(obj/item/reagent_containers/cup/glass/bottle/source, atom/attacked, mob/living/user)
+ SIGNAL_HANDLER
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve()
+ if(!istype(shuttle))
+ return
+ if(!iswallturf(attacked))
+ return
+ if(!istype(source))
+ return
+ if(SSshuttle.get_containing_shuttle(attacked) != shuttle)
+ return
+ if(HAS_TRAIT_FROM(user, TRAIT_ATTEMPTING_CHRISTENING, REF(shuttle)))
+ return
+ . = COMPONENT_CANCEL_ATTACK_CHAIN
+ if(shuttle.master_blueprint?.resolve() != src)
+ to_chat(user, span_warning("Only the master blueprint for \the [shuttle] grants you the right to rechristen [shuttle.p_them()]!"))
+ return
+ var/turf/user_turf = get_turf(user)
+ if(user_turf && isshuttleturf(user_turf))
+ to_chat(user, span_warning("You can't rechristen \the [shuttle] from inside of [shuttle.p_them()]!"))
+ return
+ if(!source.isGlass)
+ to_chat(user, span_warning("You can't break [source] against [attacked]!"))
+ return
+ if(source.reagents.total_volume < CHEMICAL_QUANTISATION_LEVEL)
+ to_chat(user, span_warning("You should put some christening fluid in [source]!"))
+ return
+ INVOKE_ASYNC(src, PROC_REF(christen), user, shuttle, attacked, user.active_hand_index)
+
+/obj/item/shuttle_blueprints/proc/christen(mob/living/user, obj/docking_port/mobile/custom/shuttle, atom/attacked, hand)
+ var/trait_source = REF(shuttle)
+ ADD_TRAIT(user, TRAIT_ATTEMPTING_CHRISTENING, trait_source)
+ var/new_name = reject_bad_name(tgui_input_text(user, "What would you like to rechristen \the [shuttle] as?", "Shuttle Rechristening", max_length = 128), allow_numbers = TRUE, strict = TRUE, cap_after_symbols = FALSE)
+ if(QDELETED(user))
+ return
+ REMOVE_TRAIT(user, TRAIT_ATTEMPTING_CHRISTENING, trait_source)
+ if(!new_name)
+ user.balloon_alert(user, "cancelled")
+ return
+ new_name = apply_text_macros(new_name)
+ var/obj/item/hitting_implement = (locate(/obj/item/reagent_containers/cup/glass/bottle) in user.held_items) || user.get_item_for_held_index(hand)
+ if(!user.CanReach(attacked, hitting_implement))
+ user.balloon_alert(user, "out of range!")
+ return
+ var/obj/item/reagent_containers/cup/glass/bottle/bottle = hitting_implement
+ bottle = istype(bottle) && bottle.isGlass && bottle
+ var/shuttle_exists = !QDELETED(shuttle)
+ if(!shuttle_exists)
+ to_chat(user, span_warning("Wasn't there supposed to be a shuttle here?"))
+ var/has_blueprints = !QDELETED(src) && (src in user.gather_belongings())
+ var/turf/attacked_turf = attacked
+ var/is_closed_turf = isclosedturf(attacked_turf)
+ var/is_shuttle_turf = isshuttleturf(attacked_turf)
+ var/off_shuttle = SSshuttle.get_containing_shuttle(get_turf(user)) != shuttle
+ var/has_bottle = istype(bottle)
+ var/bottle_empty = bottle && bottle.reagents.total_volume < CHEMICAL_QUANTISATION_LEVEL
+ var/is_clumsy = HAS_TRAIT(user, TRAIT_CLUMSY)
+ var/clumsy_check = is_clumsy && prob(25)
+ if(!(has_blueprints && is_closed_turf && is_shuttle_turf && off_shuttle && has_bottle && !clumsy_check && !bottle_empty))
+ var/msg = "[user] tries to christen the shuttle with [hitting_implement ? "[hitting_implement.get_examine_name()]" : "[user.p_their()] empty fist"]"
+ var/self_msg = "You try to christen the shuttle with [hitting_implement ? "[hitting_implement.get_examine_name()]" : "your empty fist"]"
+ if(!is_closed_turf || clumsy_check)
+ msg += ", but miss[user.p_es()], hitting [user.p_themselves()] instead"
+ self_msg += ", but miss, hitting yourself instead"
+ msg += "!"
+ self_msg += "!"
+ if(is_clumsy)
+ msg += " What a klutz!"
+ var/old_combat_mode = user.combat_mode
+ user.set_combat_mode(TRUE)
+ // Stupid code to bypass pacifism checks, since unintentional hits like this one shouldn't be blocked by pacifism
+ var/list/pacifism_sources = GET_TRAIT_SOURCES(user, TRAIT_PACIFISM)
+ REMOVE_TRAIT(user, TRAIT_PACIFISM, pacifism_sources)
+ if(hitting_implement)
+ hitting_implement.attack(user, user)
+ bottle?.smash(user, user) // Because smashing a bottle isn't part of its main melee attack chain
+ else
+ user.resolve_unarmed_attack(user)
+ if(length(pacifism_sources))
+ ADD_TRAIT(user, TRAIT_PACIFISM, pacifism_sources)
+ user.set_combat_mode(old_combat_mode)
+ else
+ if(!shuttle_exists)
+ msg += ", but there was no shuttle to christen!"
+ self_msg += ", but there was no shuttle to christen!"
+ else if(!off_shuttle)
+ msg += ", but you're suppossed to do that outside of [shuttle.p_them()]!"
+ self_msg += ", but you're suppossed to do that outside of [shuttle.p_them()]!"
+ else if(!is_shuttle_turf)
+ msg += ", but miss[user.p_es()], hitting [attacked] instead!"
+ self_msg += ", but miss, hitting [attacked] instead!"
+ else if(!has_bottle)
+ if(hitting_implement)
+ msg += " - which isn't a glass bottle!"
+ self_msg += " - which isn't a glass bottle!"
+ else
+ msg += "! That's not how you're supposed to do it!"
+ self_msg += "! Aren't you supposed to use a glass bottle?"
+ else if(bottle_empty)
+ msg += ", but there's no christening fluid in [bottle]!"
+ self_msg += ", but there's no christening fluid in [bottle]!"
+ else if(!has_blueprints)
+ msg += ", but [user.p_have()] no right to do so!"
+ msg += ", but have no right to do so!"
+ user.do_attack_animation(attacked, used_item = hitting_implement)
+ if(hitting_implement && !bottle)
+ if(hitting_implement.force)
+ playsound(hitting_implement, 'sound/items/weapons/smash.ogg', hitting_implement.get_clamped_volume(), TRUE, hitting_implement.stealthy_audio ? SILENCED_SOUND_EXTRARANGE : -1, falloff_distance = 0)
+ else
+ playsound(hitting_implement, 'sound/items/weapons/tap.ogg', hitting_implement.get_clamped_volume(), TRUE, -1)
+ else if(bottle)
+ bottle.smash(attacked, user)
+ else
+ playsound(user, 'sound/effects/bang.ogg', 50, TRUE)
+ user.visible_message(span_warning(msg), span_warning(self_msg))
+ else
+ user.visible_message(
+ span_notice("[user] christens the shuttle as \the [new_name] with [hitting_implement.get_examine_name()]\
+ [(bottle.reagents.total_volume < 30) ? "" : ", though the dearth of christening fluid makes for an unimpressive display"]."),
+ span_notice("You christen the shuttle as \the [new_name] with [hitting_implement.get_examine_name()].")
+ )
+ user.do_attack_animation(attacked, used_item = bottle)
+ bottle.smash(attacked, user)
+ shuttle.name = new_name
+ rename_area(shuttle.default_area, new_name)
+
+/obj/item/shuttle_blueprints/proc/start_visualizing(mob/user)
+ visualize_frame_turfs = TRUE
+ RegisterSignal(user, SIGNAL_ADDTRAIT(TRAIT_USER_SCOPED), PROC_REF(stop_visualizing))
+ prox_monitor.set_user(user)
+ prox_monitor.recalculate_field()
+
+/obj/item/shuttle_blueprints/proc/stop_visualizing(mob/user)
+ SIGNAL_HANDLER
+ visualize_frame_turfs = FALSE
+ UnregisterSignal(user, SIGNAL_ADDTRAIT(TRAIT_USER_SCOPED))
+ prox_monitor.set_user(null)
+
+/obj/item/shuttle_blueprints/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "ShuttleBlueprints", name)
+ ui.open()
+ RegisterSignal(user, COMSIG_ENTER_AREA, PROC_REF(on_user_enter_area))
+
+/obj/item/shuttle_blueprints/ui_close(mob/user)
+ . = ..()
+ UnregisterSignal(user, COMSIG_ENTER_AREA)
+
+/obj/item/shuttle_blueprints/proc/on_user_enter_area(mob/source, area/new_area)
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve()
+ if(shuttle && shuttle.default_area == new_area)
+ update_static_data(source)
+
+/obj/item/shuttle_blueprints/ui_state(mob/user)
+ return GLOB.hands_state
+
+/obj/item/shuttle_blueprints/ui_static_data(mob/user)
+ var/list/data = list()
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve()
+ if(get_area(user) == shuttle?.default_area && length(shuttle?.shuttle_areas) > 1)
+ var/list/neighboring_areas = list()
+ var/room_turfs = detect_room(get_turf(user), max_size = CONFIG_GET(number/max_shuttle_size), extra_check = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(custom_shuttle_room_check), shuttle, neighboring_areas))
+ if(room_turfs)
+ data["apcInMergeRegion"] = shuttle.default_area.apc && room_turfs[get_turf(shuttle.default_area.apc)]
+ var/list/area_refs = list()
+ for(var/area/neighboring_area in neighboring_areas)
+ area_refs[REF(neighboring_area)] = neighboring_area.name
+ data["neighboringAreas"] = area_refs
+ return data
+
+/obj/item/shuttle_blueprints/ui_data(mob/user)
+ var/data = list()
+ var/turf/current_turf = get_turf(user)
+ var/obj/docking_port/mobile/custom/linked_shuttle = shuttle_ref?.resolve()
+ var/linked_to_shuttle = istype(linked_shuttle)
+ data["linkedShuttle"] = linked_to_shuttle && shuttle_ref.reference
+ data["visualizing"] = visualize_frame_turfs
+ data["onShuttleFrame"] = HAS_TRAIT(current_turf, TRAIT_SHUTTLE_CONSTRUCTION_TURF)
+ if(!linked_to_shuttle)
+ var/obj/docking_port/mobile/custom/loc_shuttle = SSshuttle.get_containing_shuttle(current_turf)
+ data["tooManyShuttles"] = length(SSshuttle.custom_shuttles) >= CONFIG_GET(number/max_shuttle_count)
+ data["onCustomShuttle"] = istype(loc_shuttle)
+ data["masterExists"] = loc_shuttle?.master_blueprint?.resolve()
+ else
+ var/obj/item/shuttle_blueprints/master = linked_shuttle.master_blueprint?.resolve()
+ data["masterExists"] = master
+ data["isMaster"] = master == src
+ var/area/current_area = get_area(current_turf)
+ var/area/default_area = linked_shuttle.default_area
+ data["onShuttle"] = linked_shuttle.shuttle_areas[current_area]
+ data["inDefaultArea"] = default_area == current_area
+ data["currentArea"] = list(name = current_area.name, ref = REF(current_area))
+ data["defaultApc"] = !!default_area.apc
+ var/list/apcs = list()
+ for(var/area/area as anything in linked_shuttle.shuttle_areas - default_area)
+ apcs[REF(area)] = !!area.apc
+ data["apcs"] = apcs
+ data["idle"] = linked_shuttle.mode == SHUTTLE_IDLE
+ return data
+
+/obj/item/shuttle_blueprints/proc/link_to_shuttle(obj/docking_port/mobile/custom/shuttle, is_master = FALSE)
+ shuttle_ref = WEAKREF(shuttle)
+ if(is_master)
+ shuttle.master_blueprint = WEAKREF(src)
+ RegisterSignal(shuttle, COMSIG_QDELETING, PROC_REF(on_shuttle_deleted))
+ update_appearance()
+
+/obj/item/shuttle_blueprints/proc/on_shuttle_deleted()
+ SIGNAL_HANDLER
+ unlink(removing = TRUE)
+
+/obj/item/shuttle_blueprints/proc/unlink(removing = FALSE)
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref.resolve()
+ if(!QDELETED(shuttle))
+ UnregisterSignal(shuttle, COMSIG_QDELETING)
+ shuttle_ref = null
+ update_appearance()
+
+/obj/item/shuttle_blueprints/update_name(updates)
+ . = ..()
+ var/obj/docking_port/mobile/shuttle = shuttle_ref?.resolve()
+ if(shuttle)
+ name = get_linked_name(shuttle)
+ else
+ name = initial(name)
+
+/obj/item/shuttle_blueprints/proc/get_linked_name(obj/docking_port/mobile/shuttle)
+ return "\improper [shuttle.name] blueprints"
+
+/obj/item/shuttle_blueprints/update_desc(updates)
+ . = ..()
+ desc = shuttle_ref?.resolve() ? linked_desc : base_desc
+
+/obj/item/shuttle_blueprints/update_icon_state()
+ . = ..()
+ icon_state = "[base_icon_state][!!shuttle_ref]"
+
+/obj/item/shuttle_blueprints/vv_edit_var(vname, vval)
+ . = ..()
+ if(!.)
+ return
+ if(vname == NAMEOF(src, desc))
+ if(shuttle_ref?.resolve())
+ linked_desc = vval
+ else
+ base_desc = vval
+ update_desc()
+
+/obj/item/shuttle_blueprints/examine(mob/user)
+ . = ..()
+ . += get_shuttle_tip()
+
+/obj/item/shuttle_blueprints/proc/get_shuttle_tip()
+ . = list()
+ if(!shuttle_ref)
+ . += span_notice("It can be used to construct a custom shuttle.")
+ return
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref.resolve()
+ if(!shuttle)
+ . += span_notice("It has the plans for a shuttle that no longer exists. It can be reused to construct a new shuttle.")
+ else
+ . += span_notice("It has the plans for \the [shuttle] on it, and can be used to expand [shuttle.p_them()] or modify [shuttle.p_their()] areas.")
+ if(shuttle.master_blueprint.resolve() == src)
+ . += span_notice("This is the master blueprint for \the [shuttle]. You can copy it to a blank set of blueprints, or to an engineering cyborg with a shuttle database module installed.")
+
+/obj/item/shuttle_blueprints/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
+ . = ..()
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve()
+ if(!istype(shuttle))
+ return
+ if(shuttle.master_blueprint?.resolve() != src)
+ return
+ if(istype(interacting_with, /obj/item/shuttle_blueprints))
+ var/obj/item/shuttle_blueprints/other_blueprints = interacting_with
+ var/obj/docking_port/mobile/other_shuttle = other_blueprints.shuttle_ref?.resolve()
+ if(istype(other_shuttle))
+ return
+ balloon_alert(user, "copying blueprints...")
+ if(!do_after(user, 5 SECONDS, other_blueprints))
+ balloon_alert(user, "interrupted!")
+ return ITEM_INTERACT_FAILURE
+ other_blueprints.link_to_shuttle(shuttle)
+ balloon_alert(user, "copied")
+ return ITEM_INTERACT_SUCCESS
+ if(istype(interacting_with, /mob/living/silicon/robot))
+ var/mob/living/silicon/robot/borg = interacting_with
+ var/obj/item/shuttle_blueprints/borg/other_blueprints = (locate() in borg.model.modules) || (locate() in borg.held_items)
+ if(!other_blueprints)
+ return
+ if(other_blueprints.shuttles.Find(shuttle_ref))
+ balloon_alert(user, "already has these blueprints!")
+ balloon_alert(user, "copying blueprints...")
+ if(!do_after(user, 5 SECONDS, borg))
+ balloon_alert(user, "interrupted")
+ return ITEM_INTERACT_FAILURE
+ if(QDELETED(other_blueprints))
+ return ITEM_INTERACT_FAILURE
+ other_blueprints.shuttles |= shuttle_ref
+ balloon_alert(user, "copied")
+ return ITEM_INTERACT_SUCCESS
+
+/obj/item/shuttle_blueprints/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+ switch(action)
+ if("toggleVisualization")
+ if(visualize_frame_turfs)
+ stop_visualizing(usr)
+ else
+ start_visualizing(usr)
+ return TRUE
+ if("tryBuildShuttle")
+ var/shuttle_dir = params["dir"]
+ if(!(shuttle_dir in GLOB.cardinals))
+ return TRUE
+ var/list/shuttle_turfs = list()
+ var/list/shuttle_areas = list()
+ var/turf/shuttle_origin = get_turf(usr)
+ var/check_status = shuttle_build_check(shuttle_origin, shuttle_turfs, shuttle_areas)
+ if(check_status & ORIGIN_NOT_ON_SHUTTLE)
+ balloon_alert(usr, "not on shuttle frame!")
+ return TRUE
+ if(check_status & TOO_MANY_SHUTTLES)
+ balloon_alert(usr, "too many shuttles exist!")
+ return TRUE
+ if(check_status & ABOVE_MAX_SHUTTLE_SIZE)
+ balloon_alert(usr, "frame too big!")
+ return TRUE
+ if(check_status & CUSTOM_AREA_NOT_COMPLETELY_CONTAINED)
+ balloon_alert(usr, "frame must completely enclose custom areas!")
+ return TRUE
+ if(check_status & INTERSECTS_NON_WHITELISTED_AREA)
+ balloon_alert(usr, "frame overlaps disallowed areas!")
+ return TRUE
+ var/obj/docking_port/mobile/custom/shuttle = create_shuttle(
+ usr,
+ shuttle_origin,
+ shuttle_turfs,
+ shuttle_areas,
+ shuttle_dir,
+ name = "\improper Unnamed Shuttle",
+ id = "custom_[length(SSshuttle.custom_shuttles)+1]"
+ )
+ link_to_shuttle(shuttle, TRUE)
+ return TRUE
+ if("tryLinkShuttle")
+ if(shuttle_ref?.resolve())
+ balloon_alert(usr, "already linked!")
+ return TRUE
+ var/obj/docking_port/mobile/custom/shuttle = SSshuttle.get_containing_shuttle(usr)
+ if(!shuttle)
+ balloon_alert(usr, "not on shuttle!")
+ return TRUE
+ if(!istype(shuttle))
+ balloon_alert(usr, "incompatible shuttle type!")
+ return TRUE
+ var/obj/item/shuttle_blueprints/master = shuttle.master_blueprint?.resolve()
+ if(master && (master != src))
+ balloon_alert(usr, "master blueprint already exists!")
+ return TRUE
+ link_to_shuttle(shuttle, TRUE)
+ return TRUE
+ if("promoteToMaster")
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve()
+ if(!shuttle)
+ balloon_alert(usr, "not linked!")
+ return TRUE
+ var/obj/item/shuttle_blueprints/master = shuttle.master_blueprint?.resolve()
+ if(master)
+ balloon_alert(usr, "master blueprint already exists!")
+ return TRUE
+ shuttle.master_blueprint = WEAKREF(src)
+ return TRUE
+ if("createNewArea")
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve()
+ if(!shuttle)
+ balloon_alert(usr, "not linked!")
+ return TRUE
+ var/area_name = params["name"]
+ if(!area_name)
+ balloon_alert(usr, "no name given!")
+ return TRUE
+ var/area/current_area = get_area(usr)
+ var/area/default_area = shuttle.default_area
+ if(current_area != default_area)
+ balloon_alert(usr, "must be in default area!")
+ return TRUE
+ var/list/turfs = detect_room(get_turf(usr), max_size = CONFIG_GET(number/max_shuttle_size), extra_check = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(custom_shuttle_room_check), shuttle, null))
+ if(!length(turfs))
+ balloon_alert(usr, "invalid room!")
+ return TRUE
+ var/area/shuttle/custom/new_area = new()
+ new_area.name = area_name
+ shuttle.shuttle_areas[new_area] = TRUE
+ set_turfs_to_area(turfs, new_area)
+ new_area.reg_in_areas_in_z()
+ new_area.create_area_lighting_objects()
+ for(var/obj/machinery/door/firedoor/firelock as anything in default_area.firedoors)
+ firelock.CalculateAffectingAreas()
+ new_area.power_change()
+ return TRUE
+ if("releaseArea")
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve()
+ if(!shuttle)
+ balloon_alert(usr, "not linked!")
+ return TRUE
+ var/area/current_area = get_area(usr)
+ if(!shuttle.shuttle_areas[current_area])
+ balloon_alert(usr, "not on shuttle!")
+ return TRUE
+ var/area/default_area = shuttle.default_area
+ if(current_area == default_area)
+ balloon_alert(usr, "can't release default area!")
+ return TRUE
+ var/obj/machinery/power/apc/current_area_apc = current_area.apc
+ var/obj/machinery/power/apc/default_area_apc = default_area.apc
+ if(current_area_apc && default_area_apc)
+ balloon_alert(usr, "remove the apc first!")
+ return TRUE
+ var/list/turfs = current_area.get_turfs_by_zlevel(shuttle.z)
+ set_turfs_to_area(turfs, default_area)
+ for(var/obj/machinery/door/firedoor/firelock as anything in default_area.firedoors)
+ firelock.CalculateAffectingAreas()
+ shuttle.shuttle_areas -= current_area
+ qdel(current_area)
+ return TRUE
+ if("mergeIntoArea")
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve()
+ if(!shuttle)
+ balloon_alert(usr, "not linked!")
+ return TRUE
+ var/area/current_area = get_area(usr)
+ var/area/default_area = shuttle.default_area
+ if(current_area != default_area)
+ balloon_alert(usr, "must be in default area!")
+ return TRUE
+ var/area/merge_area = locate(params["area"])
+ if(!istype(merge_area))
+ return TRUE
+ if(!shuttle.shuttle_areas[merge_area])
+ return TRUE
+ if(merge_area == default_area)
+ return TRUE
+ var/list/actual_adjacent_areas = list()
+ var/list/turfs = detect_room(get_turf(usr), max_size = CONFIG_GET(number/max_shuttle_size), extra_check = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(custom_shuttle_room_check), shuttle, actual_adjacent_areas))
+ if(!length(turfs))
+ balloon_alert(usr, "invalid room!")
+ return TRUE
+ if(!actual_adjacent_areas[merge_area])
+ balloon_alert(usr, "selected area not connected to room!")
+ return TRUE
+ if(merge_area.apc && default_area.apc && turfs[get_turf(default_area.apc)])
+ balloon_alert(usr, "remove the apc first!")
+ return TRUE
+ set_turfs_to_area(turfs, merge_area)
+ for(var/obj/machinery/door/firedoor/firelock as anything in merge_area.firedoors + default_area.firedoors)
+ firelock.CalculateAffectingAreas()
+ return TRUE
+ if("renameArea")
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve()
+ if(!shuttle)
+ balloon_alert(usr, "not linked!")
+ return TRUE
+ var/area/current_area = get_area(usr)
+ if(!shuttle.shuttle_areas[current_area])
+ balloon_alert(usr, "not on shuttle!")
+ return TRUE
+ var/area/default_area = shuttle.default_area
+ if(current_area == default_area)
+ balloon_alert(usr, "can't rename default area!")
+ return TRUE
+ var/new_name = params["name"]
+ if(!new_name)
+ balloon_alert(usr, "no name given!")
+ return TRUE
+ rename_area(current_area, new_name)
+ return TRUE
+ if("expandWithFrame")
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve()
+ if(!shuttle)
+ balloon_alert(usr, "not linked!")
+ return TRUE
+ var/list/turfs = list()
+ var/list/areas = list()
+ var/turf/origin = get_turf(usr)
+ var/check_status = shuttle_expand_check(origin, shuttle, turfs, areas)
+ if(check_status & ORIGIN_NOT_ON_SHUTTLE)
+ balloon_alert(usr, "not on shuttle frame!")
+ return TRUE
+ if(check_status & FRAME_NOT_ADJACENT_TO_LINKED_SHUTTLE)
+ balloon_alert(usr, "not connected to linked shuttle!")
+ return TRUE
+ if(check_status & ABOVE_MAX_SHUTTLE_SIZE)
+ balloon_alert(usr, "frame too big!")
+ return TRUE
+ if(check_status & CUSTOM_AREA_NOT_COMPLETELY_CONTAINED)
+ balloon_alert(usr, "frame must completely enclose custom areas!")
+ return TRUE
+ if(check_status & INTERSECTS_NON_WHITELISTED_AREA)
+ balloon_alert(usr, "frame overlaps disallowed areas!")
+ return TRUE
+ expand_shuttle(usr, shuttle, turfs, areas)
+ return TRUE
+ if("cleanupEmptyTurfs")
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve()
+ if(!shuttle)
+ balloon_alert(usr, "not linked!")
+ return TRUE
+ var/obj/item/shuttle_blueprints/master = shuttle.master_blueprint?.resolve()
+ if(master && master != src)
+ balloon_alert(usr, "not master blueprints!")
+ var/shuttle_z = shuttle.z
+ var/bounds_need_recalculation
+ var/docking_port_needs_relocated
+ var/list/bounds = shuttle.return_coords()
+ var/x0 = bounds[1]
+ var/y0 = bounds[2]
+ var/x1 = bounds[3]
+ var/y1 = bounds[4]
+ for(var/area/area as anything in shuttle.shuttle_areas)
+ var/list/turfs = area.get_turfs_by_zlevel(shuttle_z)
+ turfs = turfs.Copy()
+ for(var/turf/turf as anything in turfs)
+ var/move_mode = turf.fromShuttleMove(move_mode = MOVE_AREA)
+ if(move_mode & (MOVE_TURF | MOVE_CONTENTS))
+ continue
+ for(var/atom/movable/movable as anything in turf.contents)
+ //CHECK_TICK
+ if(movable.loc != turf)
+ continue
+ if(movable == shuttle)
+ continue
+ move_mode = movable.hypotheticalShuttleMove(0, move_mode, shuttle)
+ if(move_mode & (MOVE_TURF | MOVE_CONTENTS))
+ continue
+ if(shuttle.loc == turf)
+ docking_port_needs_relocated = TRUE
+ bounds_need_recalculation = TRUE
+ var/area/new_area = shuttle.underlying_areas_by_turf[turf]
+ if(!istype(new_area))
+ new_area = GLOB.areas_by_type[SHUTTLE_DEFAULT_UNDERLYING_AREA]
+ if(!istype(new_area))
+ new_area = new SHUTTLE_DEFAULT_UNDERLYING_AREA(null)
+ shuttle.underlying_areas_by_turf -= turf
+ shuttle.turf_count--
+ turfs -= turf
+ turf.change_area(area, new_area)
+ if(bounds_need_recalculation)
+ continue
+ if(turf.x == x0 || turf.x == x1 || turf.y == y0 || turf.y == y1)
+ bounds_need_recalculation = TRUE
+ if((area != shuttle.default_area) && !length(turfs))
+ shuttle.shuttle_areas -= area
+ qdel(area)
+ if(!shuttle.turf_count)
+ qdel(shuttle.default_area)
+ qdel(shuttle)
+ return
+ if(docking_port_needs_relocated)
+ shuttle.forceMove(pick(shuttle.underlying_areas_by_turf))
+ if(bounds_need_recalculation)
+ QDEL_NULL(shuttle.assigned_transit)
+ shuttle.calculate_docking_port_information()
+
+/obj/item/shuttle_blueprints/crude
+ name = "crude shuttle blueprints"
+ desc = "This is just a sheet of paper thoroughly covered in what could either be crayon or spraypaint."
+ icon_state = "shuttle_blueprints_crude0"
+ base_icon_state = "shuttle_blueprints_crude"
+ base_desc = "This is just a sheet of paper thoroughly covered in what could either be crayon or spraypaint."
+ linked_desc = "This is just a crude doodle of a shuttle drawn on a background of what could either be crayon or spraypaint."
+
+/obj/item/shuttle_blueprints/borg
+ name = "shuttle blueprint database"
+ desc = "A module designed to store the plans for one or more shuttles."
+ icon = 'icons/obj/items_cyborg.dmi'
+ icon_state = "shuttle_database0"
+ base_icon_state = "shuttle_database"
+ damtype = BURN // In case fantasy affixes or adminbus end up making this actually capable of hurting someone.
+ attack_verb_continuous = list("attacks", "scans", "analyzes")
+ attack_verb_simple = list("attack", "scan", "analyze")
+ base_desc = "A module designed to store the plans for one or more shuttles."
+ linked_desc = "A module designed to store the plans for one or more shuttles."
+ var/list/shuttles = list()
+
+/obj/item/shuttle_blueprints/borg/get_shuttle_tip()
+ . = list()
+ if(!shuttle_ref)
+ if(!length(shuttles))
+ . += span_notice("It does not have any shuttle plans stored.")
+ else
+ . += span_notice("It does not currently have a shuttle plan loaded.")
+ . += span_notice("In this mode, it can be used to construct a custom shuttle.")
+ return
+ var/obj/docking_port/mobile/custom/shuttle = shuttle_ref.resolve()
+ if(!shuttle)
+ . += span_notice("The currently loaded plans are for a shuttle that no longer exists. It will default to shuttle construction mode.")
+ else
+ . += span_notice("It has the plans for \the [shuttle] currently loaded, and can be used to expand [shuttle.p_them()] or modify [shuttle.p_their()] areas.")
+ if(shuttle.master_blueprint.resolve() == src)
+ . += span_notice("This is the master blueprint for \the [shuttle]. You can copy it to a blank set of blueprints, or to another engineering cyborg with a shuttle database module installed.")
+
+/obj/item/shuttle_blueprints/borg/unlink(removing)
+ if(removing)
+ shuttles -= shuttle_ref
+ ..()
+
+/obj/item/shuttle_blueprints/borg/ui_data(mob/user)
+ var/list/data = ..()
+ var/list/shuttle_data = list()
+ var/list/shuttle_name_count = list()
+ for(var/datum/weakref/shuttle_weakref as anything in shuttles)
+ var/obj/docking_port/mobile/shuttle = shuttle_weakref.resolve()
+ if(!shuttle)
+ continue
+ var/shuttle_name = shuttle.name
+ if(!shuttle_name_count[shuttle_name])
+ shuttle_name_count[shuttle_name] = 0
+ var/ref = shuttle_weakref.reference
+ shuttle_data[ref] = shuttle_name
+ if(shuttle_name_count[shuttle_name]++ > 1)
+ shuttle_data[ref] += " ([shuttle_name_count[shuttle_name]])"
+ if(length(shuttle_data))
+ data["shuttles"] = shuttle_data
+ return data
+
+/obj/item/shuttle_blueprints/borg/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+ switch(action)
+ if("switchShuttle")
+ var/ref = params["ref"]
+ var/obj/docking_port/mobile/shuttle = locate(ref)
+ if(!shuttle)
+ return TRUE
+ if(shuttles.Find(WEAKREF(shuttle)))
+ link_to_shuttle(shuttle)
+ return TRUE
+ if("unsetShuttle")
+ unlink()
+ return TRUE
+
+/obj/item/shuttle_blueprints/borg/link_to_shuttle(obj/docking_port/mobile/custom/shuttle, is_master)
+ . = ..()
+ shuttles |= WEAKREF(shuttle)
+
+/obj/item/shuttle_blueprints/borg/get_linked_name(obj/docking_port/mobile/shuttle)
+ name = "shuttle blueprint database ([shuttle.name])"
+
+/proc/custom_shuttle_room_check(obj/docking_port/mobile/custom/shuttle, list/neighboring_areas = list(), turf/check_turf)
+ if(SSshuttle.get_containing_shuttle(check_turf) != shuttle)
+ return EXTRA_ROOM_CHECK_SKIP
+ var/area/check_area = check_turf.loc
+ if(check_area != shuttle.default_area)
+ neighboring_areas[check_area] = TRUE
+ return EXTRA_ROOM_CHECK_SKIP
+ var/move_mode = MOVE_AREA
+ move_mode = check_turf.fromShuttleMove(move_mode = move_mode)
+ if(move_mode & (MOVE_CONTENTS | MOVE_TURF))
+ return
+ for(var/atom/movable/movable as anything in check_turf.contents)
+ CHECK_TICK
+ if(movable.loc != check_turf)
+ continue
+ move_mode = movable.hypotheticalShuttleMove(0, move_mode, shuttle)
+ if(!(move_mode & (MOVE_CONTENTS | MOVE_TURF)))
+ return EXTRA_ROOM_CHECK_SKIP
+
+/proc/get_connected_shuttle_frame_turfs(turf/starting_turf, max_size = INFINITY, list/adjacent_shuttles)
+ . = list()
+ var/list/check_turfs = list(starting_turf)
+ while(length(check_turfs))
+ var/turf/check_turf = check_turfs[1]
+ check_turfs.Cut(1,2)
+ .[check_turf] = TRUE
+ if(length(.) > max_size)
+ return
+ for(var/check_dir in GLOB.cardinals)
+ var/turf/neighbor = get_step(check_turf, check_dir)
+ if(neighbor)
+ if(HAS_TRAIT(neighbor, TRAIT_SHUTTLE_CONSTRUCTION_TURF) && !.[neighbor])
+ check_turfs[neighbor] = TRUE
+ else if(islist(adjacent_shuttles))
+ var/obj/docking_port/mobile/custom/shuttle = SSshuttle.get_containing_shuttle(neighbor)
+ if(istype(shuttle))
+ adjacent_shuttles |= shuttle
+
+/proc/shuttle_build_check(turf/origin, list/turfs, list/areas)
+ var/z = origin.z
+ var/skip_flood_fill = !!length(turfs)
+ if(skip_flood_fill && !(turfs[origin]))
+ . |= ORIGIN_NOT_ON_SHUTTLE
+ if(length(SSshuttle.custom_shuttles) >= CONFIG_GET(number/max_shuttle_count))
+ . |= TOO_MANY_SHUTTLES
+ var/max_turfs = CONFIG_GET(number/max_shuttle_size)
+ if(!skip_flood_fill)
+ turfs += get_connected_shuttle_frame_turfs(origin, max_turfs)
+ var/turf_count = length(turfs)
+ if(!skip_flood_fill && !(turfs[origin]))
+ . |= ORIGIN_NOT_ON_SHUTTLE
+ if(turf_count > max_turfs)
+ . |= ABOVE_MAX_SHUTTLE_SIZE
+ . |= shuttle_area_check(turfs.Copy(), areas, z)
+
+/proc/shuttle_expand_check(turf/origin, obj/docking_port/mobile/shuttle, list/turfs, list/areas)
+ var/z = origin.z
+ var/skip_flood_fill = !!length(turfs)
+ if(skip_flood_fill && !(turfs[origin]))
+ . |= ORIGIN_NOT_ON_SHUTTLE
+ var/max_turfs = CONFIG_GET(number/max_shuttle_size) - shuttle.turf_count
+ var/list/adjacent_shuttles = list()
+ if(!skip_flood_fill)
+ turfs += get_connected_shuttle_frame_turfs(origin, max_turfs, adjacent_shuttles)
+ var/turf_count = length(turfs)
+ if(!skip_flood_fill && !(turfs[origin]))
+ . |= ORIGIN_NOT_ON_SHUTTLE
+ if(turf_count > max_turfs)
+ . |= ABOVE_MAX_SHUTTLE_SIZE
+ if(!adjacent_shuttles.Find(shuttle))
+ . |= FRAME_NOT_ADJACENT_TO_LINKED_SHUTTLE
+ . |= shuttle_area_check(turfs.Copy(), areas, z)
+
+/*
+ * Check to see if the following conditions are met:
+ * 1. Any custom areas that overlap with the the region defined by `turf` are contained entirely within that region
+ * 2. All other turfs in the region are within whitelisted areas
+ */
+/proc/shuttle_area_check(list/turfs, list/areas, z)
+ for(var/area/custom_area as anything in GLOB.custom_areas)
+ var/list/area_turfs = custom_area.get_turfs_by_zlevel(z)
+ var/turf_count = length(area_turfs)
+ if(!turf_count)
+ continue
+ var/list/turfs_not_in_frame = area_turfs - turfs
+ var/turfs_not_in_frame_count = length(turfs_not_in_frame)
+ if(turfs_not_in_frame_count == turf_count)
+ continue
+ areas[custom_area] = area_turfs - turfs_not_in_frame
+ if(turfs_not_in_frame_count)
+ . |= CUSTOM_AREA_NOT_COMPLETELY_CONTAINED
+ turfs -= area_turfs
+ while(length(turfs))
+ var/turf/checked_turf = pick(turfs)
+ var/area/checked_area = checked_turf.loc
+ var/list/area_turfs = checked_area.get_turfs_by_zlevel(z)
+ if(!is_type_in_list(checked_area, GLOB.shuttle_construction_area_whitelist))
+ . |= INTERSECTS_NON_WHITELISTED_AREA
+ turfs -= area_turfs
+
+/proc/convert_areas_to_shuttle_areas(list/turfs, list/in_areas, list/out_areas, list/underlying_areas, area_type = /area/shuttle/custom)
+ for(var/area/area as anything in in_areas)
+ var/area/new_area = new area_type()
+ new_area.setup(area.name)
+ out_areas += new_area
+ var/list/area_turfs = in_areas[area]
+ var/datum/component/custom_area/custom_area = area.GetComponent(/datum/component/custom_area)
+ if(custom_area)
+ underlying_areas += (custom_area.previous_areas & area_turfs)
+ set_turfs_to_area(area_turfs, new_area)
+ turfs -= area_turfs
+ new_area.reg_in_areas_in_z()
+ new_area.create_area_lighting_objects()
+ new_area.power_change()
+ for(var/obj/machinery/door/firedoor/firelock as anything in area.firedoors)
+ firelock.CalculateAffectingAreas()
+ if(!area.has_contained_turfs())
+ qdel(area)
+
+/proc/create_shuttle(mob/user, turf/origin, list/turfs, list/areas, shuttle_dir, port_dir = NORTH, area_type = /area/shuttle/custom, docking_port_type = /obj/docking_port/mobile/custom, obj/docking_port/stationary/dock_at, name, id, replace, custom = TRUE, force)
+ if(!ispath(docking_port_type, /obj/docking_port/mobile))
+ CRASH("docking_port_type must be /obj/docking_port/mobile or a subpath")
+ if(!ispath(area_type, /area/shuttle))
+ CRASH("area_type must be /area/shuttle or a subpath")
+ var/list/default_area_turfs = turfs.Copy()
+ // Convert each custom area into a shuttle area, then remove the affected turfs from the list of turfs to add to the default area
+ var/list/shuttle_areas = list()
+ var/list/underlying_areas = list()
+ convert_areas_to_shuttle_areas(default_area_turfs, areas, shuttle_areas, underlying_areas, area_type)
+ for(var/turf/turf as anything in default_area_turfs)
+ underlying_areas[turf] = turf.loc
+
+ // Merge the remaining frame turfs into a default shuttle area
+ var/list/affected_areas = list()
+ var/area/default_area = new area_type()
+ default_area.setup(name)
+ set_turfs_to_area(default_area_turfs, default_area, affected_areas)
+ default_area.reg_in_areas_in_z()
+ default_area.create_area_lighting_objects()
+ default_area.power_change()
+ for(var/area_name in affected_areas)
+ var/area/merged_area = affected_areas[area_name]
+ for(var/obj/machinery/door/firedoor/firelock as anything in merged_area.firedoors)
+ firelock.CalculateAffectingAreas()
+ if(!merged_area.has_contained_turfs())
+ qdel(merged_area)
+ shuttle_areas.Insert(1, default_area)
+
+ var/obj/docking_port/mobile/mobile_port = new docking_port_type(origin, shuttle_areas)
+ mobile_port.underlying_areas_by_turf += underlying_areas
+ mobile_port.name = name
+ mobile_port.shuttle_id = id
+ mobile_port.port_direction = REVERSE_DIR(shuttle_dir)
+ mobile_port.dir = port_dir
+ mobile_port.calculate_docking_port_information()
+ mobile_port.turf_count = length(turfs)
+
+ for(var/turf/turf as anything in turfs)
+ turf.stack_below_baseturf(/turf/open/floor/plating, /turf/baseturf_skipover/shuttle)
+ SEND_SIGNAL(turf, COMSIG_TURF_ADDED_TO_SHUTTLE, mobile_port)
+ if(!turf.depth_to_find_baseturf(/turf/baseturf_skipover/shuttle))
+ continue
+ var/turf/new_ceiling = get_step_multiz(turf, UP) // check if a ceiling is needed
+ if(new_ceiling)
+ // generate ceiling
+ if(!(istype(new_ceiling, /turf/open/floor/engine/hull/ceiling) || new_ceiling.depth_to_find_baseturf(/turf/open/floor/engine/hull/ceiling)))
+ if(istype(new_ceiling, /turf/open/openspace) || istype(new_ceiling, /turf/open/space/openspace))
+ new_ceiling.place_on_top(/turf/open/floor/engine/hull/ceiling)
+ else
+ new_ceiling.stack_ontop_of_baseturf(/turf/open/openspace, /turf/open/floor/engine/hull/ceiling)
+ new_ceiling.stack_ontop_of_baseturf(/turf/open/space/openspace, /turf/open/floor/engine/hull/ceiling)
+
+ if(!istype(dock_at))
+ dock_at = new(origin)
+ var/area/origin_area = get_area(origin)
+ dock_at.name = origin_area.name
+ dock_at.dir = port_dir
+
+ mobile_port.register(replace, custom)
+
+ message_admins("[key_name(user)] has created a shuttle at [ADMIN_VERBOSEJMP(origin)].")
+ log_shuttle("[key_name(user)] has created a shuttle at [get_area(origin)].")
+
+ return mobile_port
+
+/proc/expand_shuttle(mob/user, obj/docking_port/mobile/shuttle, list/turfs, list/areas)
+ var/list/default_area_turfs = turfs.Copy()
+ // Convert each custom area into a shuttle area, then remove the affected turfs from the list of turfs to add to the default area
+ var/list/shuttle_areas = list()
+ var/list/underlying_areas = list()
+ convert_areas_to_shuttle_areas(default_area_turfs, areas, shuttle_areas, underlying_areas, shuttle.area_type)
+ for(var/turf/turf as anything in default_area_turfs)
+ underlying_areas[turf] = turf.loc
+
+ var/list/affected_areas = list()
+ var/area/default_area = shuttle.shuttle_areas[1]
+ set_turfs_to_area(default_area_turfs, default_area, affected_areas)
+ default_area.power_change()
+
+ for(var/area_name in affected_areas)
+ var/area/merged_area = affected_areas[area_name]
+ for(var/obj/machinery/door/firedoor/firelock as anything in merged_area.firedoors)
+ firelock.CalculateAffectingAreas()
+ if(!merged_area.has_contained_turfs())
+ qdel(merged_area)
+
+ for(var/area/shuttle_area as anything in shuttle_areas)
+ shuttle.shuttle_areas[shuttle_area] = TRUE
+
+ var/list/bounds = shuttle.return_coords()
+ var/x0 = bounds[1]
+ var/y0 = bounds[2]
+ var/x1 = bounds[3]
+ var/y1 = bounds[4]
+ var/bounds_need_recalculation
+ for(var/turf/turf as anything in turfs)
+ turf.stack_below_baseturf(/turf/open/floor/plating, /turf/baseturf_skipover/shuttle)
+ SEND_SIGNAL(turf, COMSIG_TURF_ADDED_TO_SHUTTLE, shuttle)
+ if(turf.depth_to_find_baseturf(/turf/baseturf_skipover/shuttle))
+ var/turf/new_ceiling = get_step_multiz(turf, UP) // check if a ceiling is needed
+ if(new_ceiling)
+ // generate ceiling
+ if(!(istype(new_ceiling, /turf/open/floor/engine/hull/ceiling) || new_ceiling.depth_to_find_baseturf(/turf/open/floor/engine/hull/ceiling)))
+ if(istype(new_ceiling, /turf/open/openspace) || istype(new_ceiling, /turf/open/space/openspace))
+ new_ceiling.place_on_top(/turf/open/floor/engine/hull/ceiling)
+ else
+ new_ceiling.stack_ontop_of_baseturf(/turf/open/openspace, /turf/open/floor/engine/hull/ceiling)
+ new_ceiling.stack_ontop_of_baseturf(/turf/open/space/openspace, /turf/open/floor/engine/hull/ceiling)
+ if(bounds_need_recalculation)
+ continue
+ if(!(ISINRANGE(turf.x, x0, x1) && ISINRANGE(turf.y, y0, y1)))
+ bounds_need_recalculation = TRUE
+
+ shuttle.turf_count += length(turfs)
+ shuttle.underlying_areas_by_turf += underlying_areas
+ SEND_SIGNAL(shuttle, COMSIG_SHUTTLE_EXPANDED, turfs)
+ if(bounds_need_recalculation)
+ QDEL_NULL(shuttle.assigned_transit)
+ shuttle.calculate_docking_port_information()
+ shuttle.initiate_docking(shuttle.get_docked(), force = TRUE)
+
+ message_admins("[key_name(user)] has expanded [shuttle] at [ADMIN_VERBOSEJMP(user)].")
+ log_shuttle("[key_name(user)] expanded [shuttle] at [get_area(user)].")
diff --git a/code/modules/shuttle/mobile_port/variants/custom/custom.dm b/code/modules/shuttle/mobile_port/variants/custom/custom.dm
new file mode 100644
index 00000000000..1a5a1df71a1
--- /dev/null
+++ b/code/modules/shuttle/mobile_port/variants/custom/custom.dm
@@ -0,0 +1,19 @@
+/obj/docking_port/mobile/custom
+ name = "custom shuttle"
+ shuttle_id = "custom"
+ var/datum/weakref/master_blueprint
+ var/area/default_area
+ var/datum/weakref/control_console
+ var/datum/weakref/navigation_console
+
+/obj/docking_port/mobile/custom/Initialize(mapload, list/areas)
+ . = ..()
+ default_area = areas[1]
+
+/obj/docking_port/mobile/custom/canMove()
+ return ..() && (current_engine_power > 0)
+
+/obj/docking_port/mobile/custom/get_engine_coeff()
+ var/thrust_ratio = (current_engine_power * CUSTOM_ENGINE_POWER_MULTIPLIER)/(turf_count + CUSTOM_ENGINE_POWER_TURF_COUNT_OFFSET)
+ var/calculated_multiplier = 2*(1-(NUM_E ** -thrust_ratio))
+ return clamp(1/calculated_multiplier, CUSTOM_ENGINE_COEFF_MIN, CUSTOM_ENGINE_COEFF_MAX)
diff --git a/code/modules/shuttle/mobile_port/variants/custom/custom_consoles.dm b/code/modules/shuttle/mobile_port/variants/custom/custom_consoles.dm
new file mode 100644
index 00000000000..c3ec5a05414
--- /dev/null
+++ b/code/modules/shuttle/mobile_port/variants/custom/custom_consoles.dm
@@ -0,0 +1,174 @@
+GLOBAL_LIST_INIT(custom_shuttle_station_area_whitelist, list(/area/station/asteroid))
+
+/obj/machinery/computer/shuttle/custom_shuttle
+ desc = "A shuttle control computer."
+ icon_screen = "shuttle"
+ icon_keyboard = "tech_key"
+ shuttleId = ""
+ light_color = LIGHT_COLOR_CYAN
+ req_access = list()
+ interaction_flags_machine = INTERACT_MACHINE_ALLOW_SILICON
+ possible_destinations = "whiteship_home;"
+ var/static/list/connections = list(COMSIG_TURF_ADDED_TO_SHUTTLE = PROC_REF(on_loc_added_to_shuttle))
+
+/obj/machinery/computer/shuttle/custom_shuttle/on_construction(mob/user)
+ circuit.configure_machine(src)
+ if(!shuttleId)
+ AddElement(/datum/element/connect_loc, connections)
+
+/obj/machinery/computer/shuttle/custom_shuttle/proc/on_loc_added_to_shuttle(turf/source, obj/docking_port/mobile/custom/port)
+ SIGNAL_HANDLER
+ if(!istype(port))
+ say("Cannot link to this kind of shuttle!")
+ else
+ if(connect_to_shuttle(TRUE, port))
+ RemoveElement(/datum/element/connect_loc, connections)
+
+/obj/machinery/computer/shuttle/custom_shuttle/connect_to_shuttle(mapload, obj/docking_port/mobile/port, obj/docking_port/stationary/dock)
+ var/obj/docking_port/mobile/custom/custom_port = port
+ if(istype(custom_port))
+ if(custom_port.control_console?.resolve())
+ say("Control console already present!")
+ return FALSE
+ . = ..()
+ if(!.)
+ return
+ if(istype(custom_port))
+ custom_port.control_console = WEAKREF(src)
+ name = "[port.name] console"
+
+/obj/machinery/computer/shuttle/custom_shuttle/proc/linkShuttle(new_id)
+ if(shuttleId=="")
+ shuttleId = new_id
+ possible_destinations = "whiteship_home;shuttle[new_id]_custom;"
+ return TRUE
+ return FALSE
+
+
+//docking cam
+/obj/machinery/computer/camera_advanced/shuttle_docker/custom
+ lock_override = NONE
+ jump_to_ports = list("whiteship_home" = 1)
+ designate_time = 100
+ circuit = /obj/item/circuitboard/computer/shuttle/docker
+ var/static/list/connections = list(COMSIG_TURF_ADDED_TO_SHUTTLE = PROC_REF(on_loc_added_to_shuttle))
+
+/obj/machinery/computer/camera_advanced/shuttle_docker/custom/on_construction(mob/user)
+ circuit.configure_machine(src)
+ if(!shuttleId)
+ AddElement(/datum/element/connect_loc, connections)
+
+/obj/machinery/computer/camera_advanced/shuttle_docker/custom/proc/on_loc_added_to_shuttle(turf/source, obj/docking_port/mobile/custom/port)
+ SIGNAL_HANDLER
+ if(!istype(port))
+ say("Cannot link to this kind of shuttle!")
+ else
+ if(connect_to_shuttle(TRUE, port))
+ RemoveElement(/datum/element/connect_loc, connections)
+
+/obj/machinery/computer/camera_advanced/shuttle_docker/custom/Initialize(mapload)
+ . = ..()
+ 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/connect_to_shuttle(mapload, obj/docking_port/mobile/port, obj/docking_port/stationary/dock)
+ if(shuttleId) //We normally should only be connecting newly-created consoles to shuttles, but just in case...
+ var/obj/docking_port/mobile/old_shuttle = SSshuttle.getShuttle(shuttleId)
+ if(old_shuttle)
+ UnregisterSignal(old_shuttle, COMSIG_SHUTTLE_EXPANDED)
+ var/obj/docking_port/mobile/custom/custom_port = port
+ if(istype(custom_port))
+ if(custom_port.navigation_console?.resolve())
+ say("Navigation console already present!")
+ return FALSE
+ . = ..()
+ if(!.)
+ return
+ name = "[port.name] navigation computer"
+ if(istype(custom_port))
+ custom_port.navigation_console = WEAKREF(src)
+ RegisterSignal(port, COMSIG_SHUTTLE_EXPANDED, PROC_REF(on_shuttle_expanded))
+ recalculate_eye_view(port)
+
+/obj/machinery/computer/camera_advanced/shuttle_docker/custom/proc/on_shuttle_expanded(obj/docking_port/mobile/source, list/turfs)
+ SIGNAL_HANDLER
+ recalculate_eye_view(source)
+ if(my_port)
+ var/here_x = source.x
+ var/here_y = source.y
+ var/there_x = my_port.x
+ var/there_y = my_port.y
+ var/target_z = my_port.z
+ var/rotation = angle2dir_cardinal(dir2angle(my_port.dir) - dir2angle(source.dir))
+ var/docked = my_port.get_docked() == source
+ for(var/turf/turf as anything in turfs)
+ var/turf/checked_turf
+ if(docked)
+ checked_turf = turf
+ else
+ var/offset_x = turf.x - here_x
+ var/offset_y = turf.y - here_y
+ var/target_x = there_x
+ var/target_y = there_y
+ switch(rotation)
+ if(NORTH)
+ target_x += offset_x
+ target_y += offset_y
+ if(SOUTH)
+ target_x -= offset_x
+ target_y -= offset_y
+ if(EAST)
+ target_x -= offset_y
+ target_y += offset_x
+ if(WEST)
+ target_x += offset_y
+ target_y -= offset_x
+ checked_turf = locate(target_x, target_y, target_z)
+ if(checkLandingTurf(checked_turf) != SHUTTLE_DOCKER_LANDING_CLEAR)
+ if(docked)
+ my_port.unregister()
+ my_port.delete_after = TRUE
+ my_port.shuttle_id = null
+ my_port.name = "Old [my_port.name]"
+ my_port = null
+ else
+ QDEL_NULL(my_port)
+ break
+
+/obj/machinery/computer/camera_advanced/shuttle_docker/custom/proc/recalculate_eye_view(obj/docking_port/mobile/shuttle)
+ var/bigger_shuttle_dimension = max(shuttle.width, shuttle.height)
+ var/list/viewsize = getviewsize(world.view)
+ var/smaller_view_dimension = min(viewsize[1], viewsize[2])
+ var/new_view_range = max(bigger_shuttle_dimension - smaller_view_dimension, 0)
+ if(new_view_range != view_range)
+ view_range = new_view_range
+
+/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/checkLandingTurf(turf/T, list/overlappers)
+ . = ..()
+ var/area/area = get_area(T)
+ if(!is_type_in_list(area, GLOB.custom_shuttle_station_area_whitelist) && is_type_in_list(area, GLOB.the_station_areas))
+ return SHUTTLE_DOCKER_BLOCKED
+
+/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)
+ if(shuttleId=="")
+ shuttleId = new_id
+ return TRUE
+ return FALSE
diff --git a/code/modules/shuttle/mobile_port/variants/supply.dm b/code/modules/shuttle/mobile_port/variants/supply.dm
index c45d4285523..54f7548aff2 100644
--- a/code/modules/shuttle/mobile_port/variants/supply.dm
+++ b/code/modules/shuttle/mobile_port/variants/supply.dm
@@ -119,7 +119,7 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list(
message_admins("Blacklisted item found on in-transit Cargo Shuttle. See cargo logs for more details.")
SSshuttle.centcom_message = "Contraband found on Cargo Shuttle. This has been returned via drop pod."
-/obj/docking_port/mobile/supply/initiate_docking()
+/obj/docking_port/mobile/supply/initiate_docking(obj/docking_port/stationary/new_dock, force=FALSE)
if(getDockedId() == "cargo_away") // Buy when we leave home.
buy()
create_mail()
diff --git a/code/modules/shuttle/shuttle_consoles/navigation_computer.dm b/code/modules/shuttle/shuttle_consoles/navigation_computer.dm
index 3e9bf0cbe3b..033c6309aae 100644
--- a/code/modules/shuttle/shuttle_consoles/navigation_computer.dm
+++ b/code/modules/shuttle/shuttle_consoles/navigation_computer.dm
@@ -24,7 +24,7 @@
var/x_offset = 0
///y offset for where the camera eye will spawn. Starts from the shuttle's docking port
var/y_offset = 0
- var/list/whitelist_turfs = list(/turf/open/space, /turf/open/floor/plating, /turf/open/lava, /turf/open/openspace)
+ var/list/whitelist_turfs = list(/turf/open/space, /turf/open/floor/plating, /turf/open/lava, /turf/open/openspace, /turf/open/misc)
var/see_hidden = FALSE
var/designate_time = 0
var/turf/designating_target_loc
diff --git a/code/modules/shuttle/shuttle_consoles/shuttle_console.dm b/code/modules/shuttle/shuttle_consoles/shuttle_console.dm
index e269e360ba4..cf5ccb2c1c4 100644
--- a/code/modules/shuttle/shuttle_consoles/shuttle_console.dm
+++ b/code/modules/shuttle/shuttle_consoles/shuttle_console.dm
@@ -239,6 +239,7 @@
possible_destinations = replacetext(replacetextEx(possible_destinations, "[shuttleId]_custom", ""), ";;", ";")
shuttleId = port.shuttle_id
possible_destinations += ";[port.shuttle_id]_custom"
+ return TRUE
#undef SHUTTLE_CONSOLE_ACCESSDENIED
#undef SHUTTLE_CONSOLE_ENDGAME
diff --git a/config/game_options.txt b/config/game_options.txt
index 93f060c3759..97799dd8656 100644
--- a/config/game_options.txt
+++ b/config/game_options.txt
@@ -560,3 +560,7 @@ MAX_POSITIVE_QUIRKS 6
# If the value is lower than 1, it'll tend to even out the odds
# If higher than 1, it'll lean toward common spawns even more.
RANDOM_LOOT_WEIGHT_MODIFIER 1
+
+## 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 00000000000..ef35786a2b2
Binary files /dev/null and b/icons/mob/actions/actions_shuttle.dmi differ
diff --git a/icons/obj/items_cyborg.dmi b/icons/obj/items_cyborg.dmi
index e3014997c04..17a65770979 100644
Binary files a/icons/obj/items_cyborg.dmi and b/icons/obj/items_cyborg.dmi differ
diff --git a/icons/obj/scrolls.dmi b/icons/obj/scrolls.dmi
index ae488cab2f7..84805f239d9 100644
Binary files a/icons/obj/scrolls.dmi and b/icons/obj/scrolls.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 27a7d558e7d..5b6e4d22c1f 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -330,6 +330,7 @@
#include "code\__DEFINES\dcs\signals\signals_janitor.dm"
#include "code\__DEFINES\dcs\signals\signals_key.dm"
#include "code\__DEFINES\dcs\signals\signals_ladder.dm"
+#include "code\__DEFINES\dcs\signals\signals_lattice.dm"
#include "code\__DEFINES\dcs\signals\signals_lazy_templates.dm"
#include "code\__DEFINES\dcs\signals\signals_leash.dm"
#include "code\__DEFINES\dcs\signals\signals_lift.dm"
@@ -1098,6 +1099,7 @@
#include "code\datums\components\combo_attacks.dm"
#include "code\datums\components\combustible_flooder.dm"
#include "code\datums\components\connect_containers.dm"
+#include "code\datums\components\connect_inventory.dm"
#include "code\datums\components\connect_loc_behalf.dm"
#include "code\datums\components\connect_mob_behalf.dm"
#include "code\datums\components\connect_range.dm"
@@ -1110,6 +1112,7 @@
#include "code\datums\components\cult_ritual_item.dm"
#include "code\datums\components\curse_of_hunger.dm"
#include "code\datums\components\curse_of_polymorph.dm"
+#include "code\datums\components\custom_area.dm"
#include "code\datums\components\customizable_reagent_holder.dm"
#include "code\datums\components\damage_aura.dm"
#include "code\datums\components\damage_chain.dm"
@@ -1575,6 +1578,8 @@
#include "code\datums\elements\selfknockback.dm"
#include "code\datums\elements\series.dm"
#include "code\datums\elements\shiny_bait.dm"
+#include "code\datums\elements\shuttle_construction_lattice.dm"
+#include "code\datums\elements\shuttle_construction_turf.dm"
#include "code\datums\elements\sideway_movement.dm"
#include "code\datums\elements\simple_flying.dm"
#include "code\datums\elements\skill_reward.dm"
@@ -5981,6 +5986,9 @@
#include "code\modules\shuttle\mobile_port\variants\ferry.dm"
#include "code\modules\shuttle\mobile_port\variants\infiltrator.dm"
#include "code\modules\shuttle\mobile_port\variants\supply.dm"
+#include "code\modules\shuttle\mobile_port\variants\custom\blueprints.dm"
+#include "code\modules\shuttle\mobile_port\variants\custom\custom.dm"
+#include "code\modules\shuttle\mobile_port\variants\custom\custom_consoles.dm"
#include "code\modules\shuttle\mobile_port\variants\emergency\emergency.dm"
#include "code\modules\shuttle\mobile_port\variants\emergency\emergency_console.dm"
#include "code\modules\shuttle\mobile_port\variants\emergency\emergency_types.dm"
diff --git a/tgui/packages/tgui/constants.ts b/tgui/packages/tgui/constants.ts
index 0d58cb5cbe3..1b55c62c945 100644
--- a/tgui/packages/tgui/constants.ts
+++ b/tgui/packages/tgui/constants.ts
@@ -69,6 +69,21 @@ export const CSS_COLORS = [
'yellow',
] as const;
+export enum Direction {
+ NONE = 0,
+ NORTH = 1,
+ SOUTH = 2,
+ EAST = 4,
+ WEST = 8,
+ NORTHEAST = NORTH | EAST,
+ NORTHWEST = NORTH | WEST,
+ SOUTHEAST = SOUTH | EAST,
+ SOUTHWEST = SOUTH | WEST,
+ VERTICAL = NORTH | SOUTH,
+ HORIZONTAL = EAST | WEST,
+ ALL = NORTH | SOUTH | EAST | WEST,
+}
+
export type CssColor = (typeof CSS_COLORS)[number];
/* IF YOU CHANGE THIS KEEP IT IN SYNC WITH CHAT CSS */
diff --git a/tgui/packages/tgui/interfaces/ShuttleBlueprints.tsx b/tgui/packages/tgui/interfaces/ShuttleBlueprints.tsx
new file mode 100644
index 00000000000..c2bcca2e852
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ShuttleBlueprints.tsx
@@ -0,0 +1,422 @@
+import { ReactNode, useState } from 'react';
+import {
+ Box,
+ Button,
+ Dropdown,
+ Input,
+ Section,
+ Stack,
+ Tooltip,
+} from 'tgui-core/components';
+import { BooleanLike } from 'tgui-core/react';
+
+import { useBackend } from '../backend';
+import { Direction } from '../constants';
+import { Window } from '../layouts';
+
+type AreaData = { name: string; ref: string };
+
+type VisualizationToggleProps = { visualizing: BooleanLike };
+
+type ShuttleConstructionUnieuqData = {
+ linkedShuttle: 0;
+ tooManyShuttles: BooleanLike;
+ onCustomShuttle: BooleanLike;
+};
+
+type ShuttleConfigurationUniqueData = {
+ linkedShuttle: string;
+ onShuttle: BooleanLike;
+ inDefaultArea: BooleanLike;
+ currentArea: AreaData;
+ defaultApc: BooleanLike;
+ apcInMergeRegion: BooleanLike;
+ apcs: Record;
+ neighboringAreas: Record;
+ idle: BooleanLike;
+};
+
+type ShuttleBlueprintsData = {
+ shuttles?: Record;
+ visualizing: BooleanLike;
+ onShuttleFrame: BooleanLike;
+ masterExists: BooleanLike;
+ isMaster: BooleanLike;
+} & (ShuttleConstructionUnieuqData | ShuttleConfigurationUniqueData);
+
+type DirectionPadProps = {
+ title: string;
+ tooltip?: ReactNode;
+ enabledDirections: Direction;
+ selectedDirection: Direction;
+ onSelect: (direction: Direction) => void;
+};
+
+const directionData: [Direction, string][] = [
+ [Direction.NORTH, 'up'],
+ [Direction.SOUTH, 'down'],
+ [Direction.EAST, 'right'],
+ [Direction.WEST, 'left'],
+];
+
+const DirectionPad = (props: DirectionPadProps) => {
+ const { title, tooltip, enabledDirections, selectedDirection, onSelect } =
+ props;
+ const [north, south, east, west] = directionData.map(
+ ([direction, icon_suffix], i) => (
+
+
+ ),
+ );
+ const titleNode = (
+
+ {title}
+
+ );
+ return (
+ {titleNode} : titleNode
+ }
+ >
+
+ {north}
+
+
+ {west}
+
+ {east}
+
+
+ {south}
+
+
+ );
+};
+
+const VisualizationToggle = (props: VisualizationToggleProps) => {
+ const { visualizing } = props;
+ const { act } = useBackend();
+ return (
+
+
+ Visualization:
+
+
+ );
+};
+
+const ShuttleConstruction = () => {
+ const [shuttleDirection, setShuttleDirection] = useState(
+ Direction.NORTH,
+ );
+ const { act, data } = useBackend();
+ if (data.linkedShuttle !== 0) {
+ throw new Error('type guard failure - linkedShuttle must be 0');
+ }
+ const {
+ onShuttleFrame,
+ visualizing,
+ tooManyShuttles,
+ onCustomShuttle,
+ masterExists,
+ } = data;
+ return (
+
+
+ setShuttleDirection(dir)}
+ />
+
+
+
+
+
+
+
+
+
+
+ act('tryBuildShuttle', { dir: shuttleDirection })
+ }
+ >
+ Build New Shuttle
+
+
+
+ act('tryLinkShuttle')}
+ >
+ Connect To Existing Shuttle
+
+
+
+
+
+
+
+ );
+};
+
+const ShuttleConfiguration = () => {
+ const [name, setName] = useState('');
+ const [mergeArea = { name: '', ref: '' }, setMergeArea] =
+ useState();
+ const { act, data } = useBackend();
+ if (data.linkedShuttle === 0) {
+ throw new Error('type guard failure - linkedShuttle must be non-zero');
+ }
+ const {
+ visualizing,
+ onShuttleFrame,
+ onShuttle,
+ inDefaultArea,
+ currentArea = { name: '', ref: '' },
+ neighboringAreas = {},
+ apcs = {},
+ defaultApc,
+ apcInMergeRegion,
+ idle,
+ isMaster,
+ } = data;
+ const { name: currentAreaName, ref: currentAreaRef } = currentArea;
+ const { name: mergeAreaName, ref: mergeAreaRef } = mergeArea;
+ const removalApcConflict = defaultApc && apcs[currentAreaRef];
+ const mergeApcConflict = apcInMergeRegion && apcs[mergeAreaRef];
+ return (
+
+
+ Current Area:
+
+ {onShuttle
+ ? inDefaultArea
+ ? 'Default Area'
+ : currentAreaName
+ : 'Not on Shuttle'}
+
+
+
+ setName(value)}
+ />
+
+
+ act('createNewArea', { name: name })}
+ >
+ Create New Area
+
+
+
+ act('renameArea', { name: name })}
+ >
+ Rename Current Area
+
+
+
+
+
+
+
+ {
+ return {
+ displayText: name,
+ value: ref,
+ };
+ })}
+ selected={mergeAreaName}
+ onSelected={(value) =>
+ setMergeArea({ name: neighboringAreas[value], ref: value })
+ }
+ />
+
+
+ act('mergeIntoArea', { area: mergeAreaRef })}
+ >
+ Expand Area
+
+
+
+
+
+
+
+ act('expandWithFrame')}
+ >
+ Expand With Shuttle Frame
+
+
+
+
+
+
+
+
+ act('releaseArea')}
+ >
+ Remove Area
+
+
+
+ act('cleanupEmptyTurfs')}
+ >
+ Clean Up Empty Space
+
+
+
+ );
+};
+
+export const ShuttleBlueprints = (props) => {
+ const { act, data } = useBackend();
+ const { linkedShuttle, shuttles, masterExists, isMaster } = data;
+ return (
+
+
+
+ {shuttles && (
+ {
+ return { displayText: name, value: ref };
+ },
+ ),
+ ]}
+ selected={linkedShuttle ? shuttles[linkedShuttle] : 'None'}
+ onSelected={(value) => {
+ if (value === 0) {
+ act('unsetShuttle');
+ } else {
+ act('switchShuttle', { ref: value });
+ }
+ }}
+ />
+ )}
+ {!!linkedShuttle && !masterExists && !isMaster && (
+ act('promoteToMaster')}>
+ Promote To Master Blueprint
+
+ )}
+ >
+ }
+ >
+ {linkedShuttle ? : }
+
+
+
+ );
+};