diff --git a/code/__DEFINES/~skyrat_defines/signals.dm b/code/__DEFINES/~skyrat_defines/signals.dm
index 7ccaad2e592240..bb3e678bbf9b48 100644
--- a/code/__DEFINES/~skyrat_defines/signals.dm
+++ b/code/__DEFINES/~skyrat_defines/signals.dm
@@ -87,3 +87,24 @@
/// Whenever we need to get the soul of the mob inside of the soulcatcher.
#define COMSIG_SOULCATCHER_SCAN_BODY "soulcatcher_scan_body"
+
+/// Whenever we need to change the current room of a soulcatcher soul.
+#define COMSIG_CARRIER_MOB_CHANGE_ROOM "carrier_mob_change_room"
+
+/// Whenever we need to toggle the senses of a soulcatcher soul.
+#define COMSIG_CARRIER_MOB_TOGGLE_SENSE "carrier_mob_toggle_sense"
+
+/// Whenever we need to rename a soulcatcher soul.
+#define COMSIG_CARRIER_MOB_RENAME "carrier_mob_rename"
+
+/// Whenever we need to reset the name of a soulcatcher soul.
+#define COMSIG_CARRIER_MOB_RESET_NAME "carrier_mob_reset_name"
+
+/// Whenever we need to check if our soulcatcher soul is able to internally hear/see?
+#define COMSIG_CARRIER_MOB_CHECK_INTERNAL_SENSES "carrier_mob_internal_senses"
+
+/// Whenever we need to refresh the internal appearance of a soulcatcher soul.area
+#define COMSIG_CARRIER_MOB_REFRESH_APPEARANCE "carrier_mob_refresh_appearance"
+
+/// Whenever we need the soulcatcher soul to communicate something.
+#define COMSIG_CARRIER_MOB_SAY "carrier_mob_communicate"
diff --git a/code/__DEFINES/~skyrat_defines/traits.dm b/code/__DEFINES/~skyrat_defines/traits.dm
index f49d0bbe413348..3a9a180fbee074 100644
--- a/code/__DEFINES/~skyrat_defines/traits.dm
+++ b/code/__DEFINES/~skyrat_defines/traits.dm
@@ -3,3 +3,4 @@
#define SLIPPERY_MIN 5
/// The maximum amount of tiles a TRAIT_SLIPPERY haver will slide on slip
#define SLIPPERY_MAX 9
+
diff --git a/code/__DEFINES/~skyrat_defines/traits/declarations.dm b/code/__DEFINES/~skyrat_defines/traits/declarations.dm
index c005247fb552df..15a532cbd28c5c 100644
--- a/code/__DEFINES/~skyrat_defines/traits/declarations.dm
+++ b/code/__DEFINES/~skyrat_defines/traits/declarations.dm
@@ -109,6 +109,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Trait that was granted by a NIFSoft
#define TRAIT_NIFSOFT "nifsoft"
+/// Trait that was granted by a soulcatcher
+#define TRAIT_CARRIER "soulcatcher"
+
/// Trait given to a piece of eyewear that allows the user to use NIFSoft HUDs
#define TRAIT_NIFSOFT_HUD_GRANTER "nifsoft_hud_granter"
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index d11de3d00d09e8..35d8b079721f91 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -631,6 +631,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_NEVERBONER" = TRAIT_NEVERBONER,
"TRAIT_NIFSOFT" = TRAIT_NIFSOFT,
"TRAIT_NIFSOFT_HUD_GRANTER" = TRAIT_NIFSOFT_HUD_GRANTER,
+ "TRAIT_CARRIER" = TRAIT_CARRIER,
"TRAIT_NO_HUSK" = TRAIT_NO_HUSK,
"TRAIT_NORUNNING" = TRAIT_NORUNNING,
"TRAIT_NUMBED" = TRAIT_NUMBED,
diff --git a/modular_skyrat/modules/carriers/code/carrier_component.dm b/modular_skyrat/modules/carriers/code/carrier_component.dm
new file mode 100644
index 00000000000000..a7225f1833837a
--- /dev/null
+++ b/modular_skyrat/modules/carriers/code/carrier_component.dm
@@ -0,0 +1,386 @@
+///Global list containing any and all soulcatchers
+GLOBAL_LIST_EMPTY(soulcatchers)
+
+#define SOULCATCHER_DEFAULT_COLOR "#75D5E1"
+#define SOULCATCHER_WARNING_MESSAGE "You have entered a soulcatcher, do not share any information you have received while a ghost. If you have died within the round, you do not know your identity until your body has been scanned, standard blackout policy also applies."
+
+/**
+ * Carrier Component
+ *
+ * This component functions as a bridge between the `carrier_room` attached to itself and the parented datum.
+ * It handles the creation of new carrier rooms, TGUI, and relaying messages to the parent datum.
+ * If the component is deleted, any carrier rooms inside of `carrier_rooms` will be deleted.
+ */
+/datum/component/carrier
+ /// What is the name of the carrier?
+ var/name = "carrier"
+ /// What rooms are linked to this carrier
+ var/list/carrier_rooms = list()
+ /// What carrier room are verbs sending messages to?
+ var/datum/carrier_room/targeted_carrier_room
+ /// What theme are we using for our carrier UI?
+ var/ui_theme = "default"
+ /// Do we want to ask the user permission before the mob enters?
+ var/require_approval = TRUE
+ /// Are are the mobs inside able to emote/speak as the parent?
+ var/communicate_as_parent = FALSE
+ /// Is this carrier going to stay within the possesion of one mob within it's lifespan?
+ var/single_owner = FALSE
+
+ /// What is the max number of people we can keep in this carrier? If this is set to `FALSE` we don't have a limit
+ var/max_mobs = FALSE
+ /// What is the path of user component do we want to give to our mob? This needs to be `/datum/component/carrier_user` or a subtype.
+ var/component_to_give = /datum/component/carrier_user
+ /// What 16x16 chat icon do we want our carrier to display in chat messages?
+ var/chat_icon = "nif-soulcatcher"
+ /// What is the type of room that we want to create?
+ var/type_of_room_to_create = /datum/carrier_room
+
+/datum/component/carrier/New()
+ . = ..()
+ if(!parent)
+ return COMPONENT_INCOMPATIBLE
+
+ create_room()
+ targeted_carrier_room = carrier_rooms[1]
+
+ if(!single_owner)
+ return TRUE
+
+ update_targeted_carrier() // Give them the verbs if the soulcatcher is unlikely to change hands
+
+/datum/component/carrier/Destroy(force, ...)
+ targeted_carrier_room = null
+ for(var/datum/carrier_room as anything in carrier_rooms)
+ carrier_rooms -= carrier_room
+ qdel(carrier_room)
+
+ if(!single_owner)
+ return ..()
+
+ var/mob/living/holder = get_current_holder()
+ if(!holder)
+ return FALSE
+
+ return ..()
+
+/// Updates the target carrier component for the carrier me/emote verb to send messages to.
+/datum/component/carrier/proc/update_targeted_carrier(mob/living/target_mob, inside_carrier = FALSE)
+ var/mob/living/holder = get_current_holder()
+ if(istype(target_mob))
+ holder = target_mob
+
+ if(!holder)
+ return FALSE
+
+ var/datum/component/carrier_communicator/communicator_component = holder.GetComponent(/datum/component/carrier_communicator)
+ if(!istype(communicator_component))
+ communicator_component = holder.AddComponent(/datum/component/carrier_communicator)
+
+ communicator_component.target_carrier = WEAKREF(src)
+ return communicator_component
+
+/**
+ * Creates a `/datum/carrier_room` and adds it to the `carrier_rooms` list.
+ *
+ * Arguments
+ * * target_name - The name that we want to assign to the created room.
+ * * target_desc - The description that we want to assign to the created room.
+ */
+/datum/component/carrier/proc/create_room(target_name, target_desc)
+ var/datum/carrier_room/created_room = new type_of_room_to_create(src)
+ if(target_name)
+ created_room.name = target_name
+ if(target_desc)
+ created_room.room_description = target_desc
+
+ carrier_rooms += created_room
+ created_room.master_carrier = WEAKREF(src)
+
+/// Tries to find out who is currently using the carrier, returns the holder. If no holder can be found, returns FALSE
+/datum/component/carrier/proc/get_current_holder()
+ var/mob/living/holder
+ var/obj/item/parent_item = parent
+ if(!istype(parent_item))
+ return FALSE
+
+ holder = parent_item.loc
+ if(!istype(holder))
+ return FALSE
+
+ return holder
+
+/// Recieves a message from a carrier room.
+/datum/component/carrier/proc/recieve_message(message_to_recieve)
+ if(!message_to_recieve)
+ return FALSE
+
+ var/mob/living/carrier_owner = get_current_holder()
+ if(!istype(carrier_owner))
+ return FALSE
+
+ to_chat(carrier_owner, message_to_recieve)
+ return TRUE
+
+/// Attempts to ping the current user of the carrier, asking them if `joiner_name` is allowed in. If they are, the proc returns `TRUE`, otherwise returns FALSE
+/datum/component/carrier/proc/get_approval(joiner_name)
+ if(!require_approval)
+ return TRUE
+
+ var/mob/living/carrier_owner = get_current_holder()
+ if(!carrier_owner)
+ return FALSE
+
+ if(tgui_alert(carrier_owner, "Do you wish to allow [joiner_name] into your soulcatcher?", name, list("Yes", "No"), autofocus = FALSE) != "Yes")
+ return FALSE
+
+ return TRUE
+
+/// Returns a list containing all of the mobs currently present within a carrier.
+/datum/component/carrier/proc/get_current_mobs()
+ var/list/current_inhabitants = list()
+ for(var/datum/carrier_room/room as anything in carrier_rooms)
+ for(var/mob/living/inhabitant as anything in room.current_mobs)
+ current_inhabitants += inhabitant
+
+ return current_inhabitants
+
+/// Checks the total number of mobs present and compares it with `max_mobs` returns `TRUE` if there is room (or no limit), otherwise returns `FALSE`
+/datum/component/carrier/proc/check_for_vacancy()
+ if(!max_mobs)
+ return TRUE
+
+ if(length(get_current_mobs()) >= max_mobs)
+ return FALSE
+
+ return TRUE
+
+/datum/component/carrier/proc/get_open_rooms()
+ var/list/datum/carrier_room/room_list = list()
+ for(var/datum/carrier_room/room as anything in carrier_rooms)
+ if(!check_for_vacancy())
+ continue
+
+ room_list += room
+
+ return room_list
+
+/// Transfers a soul from a carrier room to another carrier room. Returns `FALSE` if the target room or target soul cannot be found.
+/datum/component/carrier/proc/transfer_mob(mob/living/target_soul, datum/carrier_room/target_room)
+ if(!(target_soul in get_current_mobs()) || !target_room)
+ return FALSE
+
+ var/datum/component/carrier/target_master_carrier = target_room.master_carrier.resolve()
+ if(!target_master_carrier)
+ target_room.master_carrier = null
+ return FALSE
+
+ else if(target_master_carrier != src)
+ target_soul.forceMove(target_master_carrier.parent)
+
+ var/datum/component/carrier_user/carrier_component = target_soul.GetComponent(/datum/component/carrier_user)
+ var/datum/carrier_room/original_room = carrier_component?.current_room?.resolve()
+ if(!istype(carrier_component) || !istype(original_room))
+ return FALSE // Don't transfer someone that isn't already inside of a carrier.
+
+ original_room.current_mobs -= target_soul
+ var/datum/weakref/room_ref = WEAKREF(target_room)
+ carrier_component.current_room = room_ref
+ target_room.current_mobs += target_soul
+
+ to_chat(target_soul, span_cyan("you've been transferred to [target_room]!"))
+ to_chat(target_soul, span_notice(target_room.room_description))
+
+ return TRUE
+
+/// Adds `mob_to_add` into the parent carrier, giving them the carrier component and moving their mob into the room. Returns the component added, if successful
+/datum/component/carrier/proc/add_mob(mob/living/mob_to_add, datum/carrier_room/target_room)
+ if(!istype(mob_to_add))
+ return FALSE
+
+ var/datum/component/carrier_user/carrier_component = mob_to_add.AddComponent(component_to_give)
+ if(!carrier_component)
+ return FALSE
+
+ if(!istype(target_room))
+ target_room = carrier_rooms[1] // Put them in the first room we can find if none is provided.
+
+ carrier_component.current_room = target_room
+ var/datum/component/carrier_communicator/communicator_component = update_targeted_carrier(mob_to_add)
+ communicator_component.carried_mob = TRUE
+
+ return carrier_component
+
+/**
+ * carrier Room
+ *
+ * This datum is where souls are sent to when joining soulcatchers.
+ * It handles sending messages to souls from the outside along with adding new souls, transfering, and removing souls.
+ *
+ */
+/datum/carrier_room
+ /// What is the name of the room?
+ var/name = "Carrier room"
+ /// What is the description of the room?
+ var/room_description = "it feels roomy in here."
+ /// What souls are currently inside of the room?
+ var/list/current_mobs = list()
+ /// Weakref for the master carrier datum
+ var/datum/weakref/master_carrier
+ /// What is the name of the person sending the messages?
+ var/outside_voice = "Host"
+ /// Can the room be joined at all?
+ var/joinable = TRUE
+ /// What is the color of chat messages sent by the room?
+ var/room_color = SOULCATCHER_DEFAULT_COLOR
+
+/// Adds a mob into the carrier
+/datum/carrier_room/proc/add_mob(mob/living/mob_to_add)
+ if(!mob_to_add)
+ return FALSE
+
+ var/datum/component/carrier/parent_carrier = master_carrier.resolve()
+ var/datum/parent_object = parent_carrier.parent
+ if(!parent_object)
+ return FALSE
+
+ var/datum/component/carrier_user/carrier_component = parent_carrier.add_mob(mob_to_add, src)
+ if(!carrier_component)
+ return FALSE
+ current_mobs += mob_to_add
+ carrier_component.current_room = WEAKREF(src)
+ mob_to_add.forceMove(parent_carrier.parent)
+
+ to_chat(mob_to_add, span_cyan("You find yourself now inside of: [name]"))
+ to_chat(mob_to_add, span_notice(room_description))
+
+ var/atom/parent_atom = parent_object
+ if(istype(parent_atom))
+ var/turf/carrier_turf = get_turf(parent_carrier.parent)
+ var/message_to_log = "[key_name(mob_to_add)] entered [src] inside of [parent_atom] at [loc_name(carrier_turf)]"
+ parent_atom.log_message(message_to_log, LOG_GAME)
+ mob_to_add.log_message(message_to_log, LOG_GAME)
+
+ return TRUE
+
+/// Removes a mob from a carrier room, leaving it as a ghost. Returns `FALSE` if the `mob_to_remove` cannot be found, otherwise returns `TRUE` after a successful deletion.
+/datum/carrier_room/proc/remove_mob(mob/living/mob_to_remove)
+ if(!mob_to_remove || !(mob_to_remove in current_mobs))
+ return FALSE
+
+ var/datum/component/carrier_user/carrier_component = mob_to_remove.GetComponent(/datum/component/carrier_user)
+ if(carrier_component)
+ qdel(carrier_component)
+
+ current_mobs -= mob_to_remove
+
+ var/mob/living/soulcatcher_soul/soul_to_remove = mob_to_remove
+ if(istype(soul_to_remove))
+ soul_to_remove.return_to_body()
+ qdel(soul_to_remove)
+
+ return TRUE
+
+ var/datum/component/carrier/parent_carrier = master_carrier.resolve()
+ if(!parent_carrier)
+ master_carrier = null
+ return FALSE
+ else if(!parent_carrier.parent)
+ return FALSE
+
+ var/turf/current_tile = get_turf(parent_carrier.parent)
+ mob_to_remove.forceMove(current_tile)
+
+ return TRUE
+
+/**
+ * Sends a message or emote to all of the souls currently located inside of the carrier room. Returns `FALSE` if a message cannot be sent, otherwise returns `TRUE`.
+ *
+ * Arguments
+ * * message_to_send - The message we want to send to the occupants of the room
+ * * sender_name - The person that is sending the message. This is not required.
+ * * sender_mob - The person that is sending the message. This is not required.
+ * * emote - Is the message sent an emote or not?
+ */
+/datum/carrier_room/proc/send_message(message_to_send, sender_name, mob/living/sender_mob, emote = FALSE)
+ if(!message_to_send) //Why say nothing?
+ return FALSE
+
+ var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/chat)
+ var/master_resolved = master_carrier.resolve()
+ if(!master_resolved)
+ master_carrier = null
+ return FALSE
+
+ var/datum/component/carrier/parent_carrier = master_resolved
+ var/tag = sheet.icon_tag(parent_carrier.chat_icon)
+ var/soulcatcher_icon = ""
+
+ if(tag)
+ soulcatcher_icon = tag
+
+ var/datum/component/carrier_user/user_component
+ if(sender_mob && istype(sender_mob))
+ user_component = sender_mob.GetComponent(/datum/component/carrier_user)
+ if(!istype(user_component))
+ return FALSE
+ else
+ sender_mob = "soulcatcher host"
+
+ if(istype(user_component) && user_component.communicating_externally)
+ var/obj/item/parent_object = parent_carrier.parent
+ if(!istype(parent_object))
+ return FALSE
+
+ var/temp_name = parent_object.name
+ parent_object.name = "[parent_object.name] [soulcatcher_icon]"
+
+ if(emote)
+ parent_object.manual_emote(html_decode(message_to_send))
+ log_emote("[sender_mob] in [name] carrier room emoted: [message_to_send], as an external object")
+ else
+ parent_object.say(html_decode(message_to_send))
+ log_say("[sender_mob] in [name] carrier room said: [message_to_send], as an external object")
+
+ parent_object.name = temp_name
+ return TRUE
+
+ var/first_room_name_word = splittext(name, " ")
+ var/message = ""
+ var/owner_message = ""
+ if(!emote)
+ message = "\ [soulcatcher_icon] [sender_name] says, \"[message_to_send]\""
+ owner_message = "\ ([first_room_name_word[1]]) [soulcatcher_icon] [sender_name] says, \"[message_to_send]\""
+ log_say("[sender_mob] in [name] carrier room said: [message_to_send]")
+ else
+ message = "\ [soulcatcher_icon] [sender_name] [message_to_send]"
+ owner_message = "\ ([first_room_name_word[1]]) [soulcatcher_icon] [sender_name] [message_to_send]"
+ log_emote("[sender_mob] in [name] carrier room emoted: [message_to_send]")
+
+ for(var/mob/living/soul as anything in current_mobs)
+ var/message_eligible = SEND_SIGNAL(soul, COMSIG_CARRIER_MOB_CHECK_INTERNAL_SENSES, emote)
+ if(!message_eligible)
+ continue
+
+ to_chat(soul, message)
+
+ relay_message_to_carrier(owner_message)
+ return TRUE
+
+/// Relays a message sent from the send_message proc to the parent carrier datum
+/datum/carrier_room/proc/relay_message_to_carrier(message)
+ if(!message)
+ return FALSE
+
+ var/datum/component/carrier/recepient_carrier = master_carrier.resolve()
+ if(!recepient_carrier)
+ return FALSE // This really isn't good.
+
+ recepient_carrier.recieve_message(message)
+ return TRUE
+
+/datum/carrier_room/Destroy(force, ...)
+ for(var/mob/living/occupant as anything in current_mobs)
+ remove_mob(occupant)
+
+ return ..()
diff --git a/modular_skyrat/modules/carriers/code/carrier_tgui.dm b/modular_skyrat/modules/carriers/code/carrier_tgui.dm
new file mode 100644
index 00000000000000..0a59d19c2bfb76
--- /dev/null
+++ b/modular_skyrat/modules/carriers/code/carrier_tgui.dm
@@ -0,0 +1,367 @@
+/datum/component/carrier/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(usr, src, ui)
+
+ if(!ui)
+ ui = new(usr, src, "Soulcatcher", name)
+ ui.open()
+
+/datum/component/carrier/soulcatcher/nifsoft/ui_state(mob/user)
+ return GLOB.conscious_state
+
+/datum/component/carrier/ui_data(mob/user)
+ var/list/data = list()
+
+ var/datum/component/carrier/soulcatcher/soulcatcher_carrier = src
+ if(istype(soulcatcher_carrier))
+ data["removable"] = soulcatcher_carrier.removable
+ data["ghost_joinable"] = soulcatcher_carrier.ghost_joinable
+
+ data["require_approval"] = require_approval
+ data["theme"] = ui_theme
+ data["communicate_as_parent"] = communicate_as_parent
+ data["current_mob_count"] = length(get_current_mobs())
+ data["max_mobs"] = max_mobs
+
+ var/carrier_targeted = FALSE
+ var/datum/component/carrier_communicator/communicator = user.GetComponent(/datum/component/carrier_communicator)
+ if(istype(communicator) && communicator?.target_carrier?.resolve())
+ var/datum/component/carrier/held_carrier = communicator.target_carrier.resolve()
+ carrier_targeted = (held_carrier == src)
+ data["carrier_targeted"] = carrier_targeted
+
+ data["current_rooms"] = list()
+ for(var/datum/carrier_room/room in carrier_rooms)
+ var/currently_targeted = (room == targeted_carrier_room)
+
+ var/list/room_data = list(
+ "name" = html_decode(room.name),
+ "description" = html_decode(room.room_description),
+ "reference" = REF(room),
+ "joinable" = room.joinable,
+ "color" = room.room_color,
+ "currently_targeted" = currently_targeted,
+ )
+
+ for(var/mob/living/soul in room.current_mobs)
+ var/datum/component/carrier_user/soul_component = soul.GetComponent(/datum/component/carrier_user)
+ if(!soul_component)
+ continue
+
+ var/mob/living/soulcatcher_soul/soul_to_check = soul // So that we can check if a body scan is needed if we are working with a soul
+ var/list/soul_list = list(
+ "name" = soul_component.name,
+ "description" = soul_component.desc,
+ "reference" = REF(soul),
+ "internal_hearing" = soul_component.internal_hearing,
+ "internal_sight" = soul_component.internal_sight,
+ "outside_hearing" = soul_component.outside_hearing,
+ "outside_sight" = soul_component.outside_sight,
+ "able_to_emote" = soul_component.able_to_emote,
+ "able_to_speak" = soul_component.able_to_speak,
+ "able_to_rename" = soul_component.able_to_rename,
+ "ooc_notes" = soul_component.ooc_notes,
+ "able_to_speak_as_container" = soul_component.able_to_speak_as_container,
+ "able_to_emote_as_container" = soul_component.able_to_emote_as_container,
+ "scan_needed" = soul_to_check?.body_scan_needed,
+ "is_soul" = istype(soul_to_check),
+ )
+ room_data["souls"] += list(soul_list)
+
+ data["current_rooms"] += list(room_data)
+
+ return data
+
+/datum/component/carrier/ui_static_data(mob/user)
+ var/list/data = list()
+
+ data["current_vessel"] = parent
+
+ return data
+
+/datum/component/carrier/ui_act(action, list/params)
+ . = ..()
+ if(.)
+ return
+
+ var/datum/component/carrier/soulcatcher/soulcatcher_carrier = src
+ var/datum/carrier_room/target_room
+ if(params["room_ref"])
+ target_room = locate(params["room_ref"]) in carrier_rooms
+ if(!target_room)
+ return FALSE
+
+ var/mob/living/target_mob
+ if(params["target_mob"])
+ target_mob = locate(params["target_mob"]) in target_room.current_mobs
+ if(!target_mob)
+ return FALSE
+
+ switch(action)
+ if("delete_room")
+ if(length(carrier_rooms) <= 1)
+ return FALSE
+
+ carrier_rooms -= target_room
+ targeted_carrier_room = carrier_rooms[1]
+ qdel(target_room)
+ return TRUE
+
+ if("change_targeted_room")
+ targeted_carrier_room = target_room
+ return TRUE
+
+ if("change_targeted_carrier")
+ update_targeted_carrier()
+ return TRUE
+
+ if("create_room")
+ create_room()
+ return TRUE
+
+ if("rename_room")
+ var/new_room_name = tgui_input_text(usr,"Choose a new name for the room", name, target_room.name)
+ if(!new_room_name)
+ return FALSE
+
+ target_room.name = new_room_name
+ return TRUE
+
+ if("redescribe_room")
+ var/new_room_desc = tgui_input_text(usr,"Choose a new description for the room", name, target_room.room_description, multiline = TRUE)
+ if(!new_room_desc)
+ return FALSE
+
+ target_room.room_description = new_room_desc
+ return TRUE
+
+ if("toggle_joinable_room")
+ if(!istype(soulcatcher_carrier))
+ return FALSE
+
+ target_room.joinable = !target_room.joinable
+ return TRUE
+
+ if("toggle_joinable")
+ if(!istype(soulcatcher_carrier))
+ return FALSE
+
+ soulcatcher_carrier.ghost_joinable = !soulcatcher_carrier.ghost_joinable
+ return TRUE
+
+ if("toggle_approval")
+ require_approval = !require_approval
+ return TRUE
+
+ if("modify_name")
+ var/new_name = tgui_input_text(usr,"Choose a new name to send messages as", name, target_room.outside_voice)
+ if(!new_name)
+ return FALSE
+
+ target_room.outside_voice = new_name
+ return TRUE
+
+ if("remove_mob")
+ target_room.remove_mob(target_mob)
+ return TRUE
+
+ if("transfer_mob")
+ var/list/available_rooms = carrier_rooms.Copy()
+ available_rooms -= target_room
+
+ if(ishuman(usr))
+ var/mob/living/carbon/human/human_user = usr
+ for(var/obj/item/carrier_holder/holder in human_user.contents)
+ var/datum/component/carrier/holder_carrier = holder.GetComponent(/datum/component/carrier)
+ if(!istype(holder_carrier))
+ continue
+
+ available_rooms += holder_carrier.get_open_rooms()
+
+ for(var/obj/item/held_item in human_user.get_all_gear())
+ if(parent == held_item)
+ continue
+
+ var/datum/component/carrier/carrier_component = held_item.GetComponent(/datum/component/carrier)
+ if(!carrier_component)
+ continue
+
+ available_rooms += carrier_component.get_open_rooms()
+
+ var/datum/carrier_room/transfer_room = tgui_input_list(usr, "Choose a room to transfer to", name, available_rooms)
+ if(!(transfer_room in available_rooms))
+ return FALSE
+
+ transfer_mob(target_mob, transfer_room)
+ return TRUE
+
+ if("change_room_color")
+ var/new_room_color = input(usr, "", "Choose Color", SOULCATCHER_DEFAULT_COLOR) as color
+ if(!new_room_color)
+ return FALSE
+
+ target_room.room_color = new_room_color
+
+ if("toggle_soul_sense")
+ var/sense_to_change = params["sense_to_change"]
+ if(!sense_to_change)
+ return FALSE
+
+ SEND_SIGNAL(target_mob, COMSIG_CARRIER_MOB_TOGGLE_SENSE, sense_to_change)
+ return TRUE
+
+ if("change_name")
+ var/new_name = tgui_input_text(usr, "Enter a new name for [target_mob]", "Soulcatcher", target_mob)
+ if(!new_name)
+ return FALSE
+
+ return SEND_SIGNAL(target_mob, COMSIG_CARRIER_MOB_RENAME, new_name)
+
+ if("reset_name")
+ if(tgui_alert(usr, "Do you wish to reset [target_mob]'s name to default?", "Soulcatcher", list("Yes", "No")) != "Yes")
+ return FALSE
+
+ return SEND_SIGNAL(target_mob, COMSIG_CARRIER_MOB_RESET_NAME)
+
+ if("send_message")
+ var/message_to_send = ""
+ var/emote_sent = params["emote"]
+ var/message_sender = target_room.outside_voice
+ if(params["narration"])
+ message_sender = null
+
+ message_to_send = tgui_input_text(usr, "Input the message you want to send", name, multiline = TRUE)
+
+ if(!message_to_send)
+ return FALSE
+
+ target_room.send_message(message_to_send, message_sender, emote = emote_sent)
+ return TRUE
+
+
+ if("delete_self")
+ if(!istype(soulcatcher_carrier))
+ return FALSE
+
+ if(tgui_alert(usr, "Are you sure you want to detach the soulcatcher?", parent, list("Yes", "No")) != "Yes")
+ return FALSE
+
+ soulcatcher_carrier.remove_self()
+ return TRUE
+
+/datum/component/carrier_user/New()
+ . = ..()
+ var/mob/living/parent_mob = parent
+ if(!istype(parent_mob))
+ return COMPONENT_INCOMPATIBLE
+
+ return TRUE
+
+/datum/component/carrier_user/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(usr, src, ui)
+ if(!ui)
+ ui = new(usr, src, "SoulcatcherUser")
+ ui.open()
+
+/datum/component/carrier_user/ui_state(mob/user)
+ return GLOB.conscious_state
+
+/datum/component/carrier_user/ui_data(mob/user)
+ var/list/data = list()
+
+ var/mob/living/parent_mob = parent
+ if(!istype(parent_mob))
+ return FALSE //uhoh
+
+ var/datum/component/carrier_communicator/communicator = parent.GetComponent(/datum/component/carrier_communicator)
+ data["targeted"] = communicator?.carried_mob
+
+ var/mob/living/soulcatcher_soul/user_soul = parent_mob
+ data["user_data"] = list(
+ "name" = name,
+ "description" = desc,
+ "reference" = REF(parent_mob),
+ "internal_hearing" = internal_hearing,
+ "internal_sight" = internal_sight,
+ "outside_hearing" = outside_hearing,
+ "outside_sight" = outside_sight,
+ "able_to_emote" = able_to_emote,
+ "able_to_speak" = able_to_speak,
+ "able_to_rename" = able_to_rename,
+ "able_to_speak_as_container" = able_to_speak_as_container,
+ "able_to_emote_as_container" = able_to_emote_as_container,
+ "communicating_externally" = communicating_externally,
+ "ooc_notes" = ooc_notes,
+ "scan_needed" = user_soul?.body_scan_needed,
+ )
+
+ var/datum/carrier_room/current_carrier_room = current_room.resolve()
+ if(!current_carrier_room)
+ current_room = null
+ return FALSE
+
+ data["current_room"] = list(
+ "name" = html_decode(current_carrier_room.name),
+ "description" = html_decode(current_carrier_room.room_description),
+ "reference" = REF(current_carrier_room),
+ "color" = current_carrier_room.room_color,
+ "owner" = current_carrier_room.outside_voice,
+ )
+
+ var/datum/component/carrier/master_carrier = current_carrier_room.master_carrier.resolve()
+ if(!master_carrier)
+ current_carrier_room.master_carrier = null
+
+ var/datum/component/carrier/soulcatcher/master_soulcatcher
+ if(istype(master_soulcatcher))
+ data["communicate_as_parent"] = master_soulcatcher.communicate_as_parent
+
+ for(var/mob/living/soul in current_carrier_room.current_mobs)
+ if(soul == user_soul)
+ continue
+
+ var/datum/component/carrier_user/soul_component = soul.GetComponent(/datum/component/carrier_user)
+ if(!soul_component)
+ continue
+
+ var/list/soul_list = list(
+ "name" = soul_component.name,
+ "description" = soul_component.desc,
+ "ooc_notes" = soul_component.ooc_notes,
+ "reference" = REF(soul),
+ )
+ data["souls"] += list(soul_list)
+
+ return data
+
+/datum/component/carrier_user/ui_act(action, list/params)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("change_name")
+ var/new_name = tgui_input_text(usr, "Enter a new name", "Soulcatcher", name)
+ if(!new_name)
+ return FALSE
+
+ change_name(new_name = new_name)
+ return TRUE
+
+ if("reset_name")
+ if(tgui_alert(usr, "Do you wish to reset your name to default?", "Soulcatcher", list("Yes", "No")) != "Yes")
+ return FALSE
+
+ reset_name()
+ return TRUE
+
+ if("toggle_external_communication")
+ communicating_externally = !communicating_externally
+ return TRUE
+
+ if("toggle_target")
+ var/datum/component/carrier_communicator/communicator = parent.GetComponent(/datum/component/carrier_communicator)
+ if(!istype(communicator))
+ return FALSE
+
+ communicator.carried_mob = !communicator.carried_mob
+ return TRUE
diff --git a/modular_skyrat/modules/carriers/code/carrier_user_component.dm b/modular_skyrat/modules/carriers/code/carrier_user_component.dm
new file mode 100644
index 00000000000000..075b9d3c2fda31
--- /dev/null
+++ b/modular_skyrat/modules/carriers/code/carrier_user_component.dm
@@ -0,0 +1,285 @@
+/// The component given to carrier inhabitants
+/datum/component/carrier_user
+ /// What is the name of our mob?
+ var/name
+ /// What does our mob look like?
+ var/desc = "It's a mob."
+ /// What are the ooc notes for the mob?
+ var/ooc_notes = ""
+
+ /// What is the weakref of the carrier room are we currently in?
+ var/datum/weakref/current_room
+
+ /// Is the mob able to see things in the outside world?
+ var/outside_sight = TRUE
+ /// Is the mob able to hear things from the outside world?
+ var/outside_hearing = TRUE
+ /// Is the mob able to "see" things from inside of the carrier?
+ var/internal_sight = TRUE
+ /// Is the mob able to "hear" things from inside of the carrier?
+ var/internal_hearing = TRUE
+ /// Is the mob able to emote inside the carrier room?
+ var/able_to_emote = TRUE
+ /// Is the mob able to speak inside the carrier room?
+ var/able_to_speak = TRUE
+ /// Is the mob able to change their own name?
+ var/able_to_rename = TRUE
+ /// Is the mob able to speak as the object it is inside?
+ var/able_to_speak_as_container = TRUE
+ /// Is the mob able to emote as the object it is inside?
+ var/able_to_emote_as_container = TRUE
+ /// Are emote's and Say's done through the container the mob is in?
+ var/communicating_externally = FALSE
+
+ /// Is the action to control the HUD given to the mob?
+ var/hud_action_given = TRUE
+ /// The coresponding action used to pull up the HUD
+ var/datum/action/innate/carrier_user/carrier_action
+ /// Is the action to leave given to the mob?
+ var/leave_action_given = TRUE
+ /// The coresponding action used to leave the carrier
+ var/datum/action/innate/leave_carrier/leave_action
+
+/datum/component/carrier_user/New()
+ . = ..()
+ var/mob/living/parent_mob = parent
+ if(!istype(parent_mob))
+ return COMPONENT_INCOMPATIBLE
+
+ if(hud_action_given)
+ carrier_action = new
+ carrier_action.Grant(parent_mob)
+ carrier_action.carrier_user_component = WEAKREF(src)
+
+ if(leave_action_given)
+ leave_action = new
+ leave_action.Grant(parent_mob)
+
+ if(!outside_sight)
+ outside_sight = TRUE
+ toggle_sense(sense_to_toggle = "outside_sight")
+
+ if(!outside_hearing)
+ outside_hearing = TRUE
+ toggle_sense(sense_to_toggle = "outside_hearing")
+
+ refresh_mob_appearance()
+
+ return TRUE
+
+/datum/component/carrier_user/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_CARRIER_MOB_TOGGLE_SENSE, PROC_REF(toggle_sense))
+ RegisterSignal(parent, COMSIG_CARRIER_MOB_RENAME, PROC_REF(change_name))
+ RegisterSignal(parent, COMSIG_CARRIER_MOB_RESET_NAME, PROC_REF(reset_name))
+ RegisterSignal(parent, COMSIG_CARRIER_MOB_CHANGE_ROOM, PROC_REF(set_room))
+ RegisterSignal(parent, COMSIG_CARRIER_MOB_CHECK_INTERNAL_SENSES, PROC_REF(check_internal_senses))
+ RegisterSignal(parent, COMSIG_CARRIER_MOB_REFRESH_APPEARANCE, PROC_REF(refresh_mob_appearance))
+
+/// Configures the settings of the carrier user to be in accordance with the parent mob
+/datum/component/carrier_user/proc/refresh_mob_appearance(datum/source)
+ SIGNAL_HANDLER
+
+ var/mob/living/parent_mob = parent
+ if(!istype(parent_mob) || !istype(parent_mob.mind))
+ return FALSE
+
+ name = parent_mob.mind.name
+
+ var/datum/preferences/preferences = parent_mob?.client?.prefs
+ if(!preferences)
+ return FALSE
+
+ ooc_notes = preferences.read_preference(/datum/preference/text/ooc_notes)
+ desc = preferences.read_preference(/datum/preference/text/flavor_text)
+
+/// What do we want to do when a mob tries to say something into the carrier?
+/datum/component/carrier_user/proc/say(message_to_say)
+ var/mob/living/parent_mob = parent
+ if(!istype(parent_mob))
+ return FALSE
+
+ if(!can_communicate())
+ to_chat(parent, span_warning("You are unable to speak!"))
+ return FALSE
+
+ if(!message_to_say)
+ return FALSE
+
+ var/datum/carrier_room/room = current_room.resolve()
+ if(!room) // uhoh.
+ current_room = null
+ return FALSE
+
+ room.send_message(message_to_say, name, parent_mob, FALSE)
+ return TRUE
+
+/// What do we want to do when a mob tries to do a `me` emote?
+/datum/component/carrier_user/proc/me_verb(message_to_say)
+ var/mob/living/parent_mob = parent
+ if(!istype(parent_mob))
+ return FALSE
+
+ if(!can_communicate(TRUE))
+ to_chat(parent, span_warning("You are unable to speak!"))
+ return FALSE
+
+ if(!message_to_say)
+ return FALSE
+
+ var/datum/carrier_room/room = current_room.resolve()
+ if(!room) // uhoh.
+ current_room = null
+ return FALSE
+
+ room.send_message(message_to_say, name, parent_mob, TRUE)
+ return TRUE
+
+/// Modifies the sense of the parent mob based on the variable `sense_to_toggle`. Returns the state of the modified variable
+/datum/component/carrier_user/proc/toggle_sense(datum/source, sense_to_toggle)
+ SIGNAL_HANDLER
+ var/status = FALSE
+ var/mob/living/parent_mob = parent
+ if(!istype(parent_mob))
+ return FALSE
+
+ switch(sense_to_toggle)
+ if("external_hearing")
+ outside_hearing = !outside_hearing
+ if(outside_hearing)
+ REMOVE_TRAIT(parent_mob, TRAIT_DEAF, TRAIT_CARRIER)
+ else
+ ADD_TRAIT(parent_mob, TRAIT_DEAF, TRAIT_CARRIER)
+
+ status = outside_hearing
+
+ if("external_sight")
+ outside_sight = !outside_sight
+ if(outside_sight)
+ parent_mob.cure_blind(TRAIT_CARRIER)
+ else
+ parent_mob.become_blind(TRAIT_CARRIER)
+
+ status = outside_sight
+
+ if("hearing")
+ internal_hearing = !internal_hearing
+ status = internal_hearing
+
+ if("sight")
+ internal_sight = !internal_sight
+ status = internal_sight
+
+ if("able_to_emote")
+ able_to_emote = !able_to_emote
+ status = able_to_emote
+
+ if("able_to_speak")
+ able_to_speak = !able_to_speak
+ status = able_to_speak
+
+ if("able_to_rename")
+ able_to_rename = !able_to_rename
+ status = able_to_rename
+
+ if("able_to_emote_as_container")
+ able_to_emote_as_container = !able_to_emote_as_container
+ status = able_to_emote_as_container
+
+ if("able_to_speak_as_container")
+ able_to_speak_as_container = !able_to_speak_as_container
+ status = able_to_speak_as_container
+
+ return status
+
+/// Changes the name show on the component based off `new_name`. Returns `TRUE` if the name has been changed, otherwise returns `FALSE`.
+/datum/component/carrier_user/proc/change_name(datum/source, new_name)
+ SIGNAL_HANDLER
+ var/mob/living/parent_mob = parent
+ if(!new_name || !istype(parent_mob) || !able_to_rename)
+ return FALSE
+
+ var/mob/living/soulcatcher_soul/soul_mob = parent
+ if(istype(soul_mob) && (soul_mob.round_participant && soul_mob.body_scan_needed))
+ return FALSE
+
+ name = new_name
+ return TRUE
+
+/// Attempts to reset the mob's name to it's name in prefs. Returns `TRUE` if the name is reset, otherwise returns `FALSE`.
+/datum/component/carrier_user/proc/reset_name(datum/source)
+ SIGNAL_HANDLER
+ var/mob/living/parent_mob = parent
+ if(!parent_mob?.mind?.name || !change_name(new_name = parent_mob.mind.name))
+ return FALSE
+
+ return TRUE
+
+/// Is the carrier mob able to communicate? Returns `TRUE` if they can, otherwise returns `FALSE`
+/datum/component/carrier_user/proc/can_communicate(emote = FALSE)
+ if(communicating_externally)
+ if((emote && !able_to_emote_as_container) || (!emote && !able_to_speak_as_container))
+ return FALSE
+
+ if((emote && !able_to_emote) || (!emote && !able_to_speak))
+ return FALSE
+
+ return TRUE
+
+//// Is the carrier mob able to witness a message? `Emote` determines if the message is an emote or not.
+/datum/component/carrier_user/proc/check_internal_senses(datum/source, emote = FALSE)
+ SIGNAL_HANDLER
+ if(emote)
+ return internal_sight
+
+ return internal_hearing
+
+/// Sets the current room of the carrier component based off of `room_to_set`
+/datum/component/carrier_user/proc/set_room(datum/source, datum/carrier_room/room_to_set)
+ SIGNAL_HANDLER
+ if(!istype(room_to_set))
+ return FALSE
+
+ current_room = room_to_set
+
+/datum/component/carrier_user/Destroy(force, silent)
+ if(!outside_hearing)
+ toggle_sense("external_hearing")
+
+ if(!outside_sight)
+ toggle_sense("external_sight")
+
+ if(carrier_action)
+ QDEL_NULL(carrier_action)
+
+ if(leave_action)
+ QDEL_NULL(leave_action)
+
+ return ..()
+
+/datum/component/carrier_user/UnregisterFromParent()
+ UnregisterSignal(parent, list(
+ COMSIG_CARRIER_MOB_TOGGLE_SENSE,
+ COMSIG_CARRIER_MOB_RENAME,
+ COMSIG_CARRIER_MOB_RESET_NAME,
+ COMSIG_CARRIER_MOB_CHANGE_ROOM,
+ COMSIG_CARRIER_MOB_CHECK_INTERNAL_SENSES,
+ COMSIG_CARRIER_MOB_REFRESH_APPEARANCE,
+ ))
+
+/datum/action/innate/carrier_user
+ name = "carrier"
+ background_icon = 'modular_skyrat/master_files/icons/mob/actions/action_backgrounds.dmi'
+ background_icon_state = "android"
+ button_icon = 'modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi'
+ button_icon_state = "soulcatcher"
+ /// What carrier user component are we bringing up the menu for?
+ var/datum/weakref/carrier_user_component
+
+/datum/action/innate/carrier_user/Activate()
+ . = ..()
+ var/datum/component/carrier_user/user_component = carrier_user_component.resolve()
+ if(!user_component)
+ carrier_user_component = null
+ return FALSE
+
+ user_component.ui_interact(owner)
diff --git a/modular_skyrat/modules/carriers/code/carrier_verbs.dm b/modular_skyrat/modules/carriers/code/carrier_verbs.dm
new file mode 100644
index 00000000000000..5a8b0eb63339ef
--- /dev/null
+++ b/modular_skyrat/modules/carriers/code/carrier_verbs.dm
@@ -0,0 +1,92 @@
+/datum/component/carrier_communicator
+ /// What carrier room is the parent mob currently trying to communicate with?
+ var/datum/weakref/target_carrier
+ /// Is the mob trying to communicate with the carrier they are inside?
+ var/carried_mob = FALSE
+
+/datum/component/carrier_communicator/New()
+ . = ..()
+ var/mob/living/parent_mob = parent
+ if(!istype(parent_mob))
+ return COMPONENT_INCOMPATIBLE
+
+ add_verb(parent_mob, list(/mob/living/proc/carrier_say, /mob/living/proc/carrier_emote))
+
+/datum/component/carrier_communicator/Destroy(force)
+ var/mob/living/holder = parent
+ if(!istype(holder))
+ return FALSE
+
+ remove_verb(holder, list(/mob/living/proc/carrier_say, /mob/living/proc/carrier_emote))
+ return ..()
+
+/// Prompts the parent mob to send a say message to the soulcatcher. Returns False if no soulcatcher or message could be found.
+/mob/living/proc/carrier_say()
+ set name = "Carrier Say"
+ set category = "IC"
+ set desc = "Send a Say message to your currently targeted carrier room."
+
+ var/datum/carrier_room/room_to_send_to = get_current_carrier_room()
+ if(!istype(room_to_send_to))
+ to_chat(src, span_warning("You do not have a carrier you can send messages to!"))
+ return FALSE
+
+ var/message_to_send = tgui_input_text(usr, "Input the message you want to send", "Carrier", multiline = TRUE)
+ if(!message_to_send)
+ return FALSE
+
+ var/message_sender = room_to_send_to.outside_voice
+ var/datum/component/carrier_user/carrier_user_component = GetComponent(/datum/component/carrier_user)
+ var/datum/component/carrier_communicator/communicator_component = GetComponent(/datum/component/carrier_communicator)
+ if(istype(carrier_user_component) && istype(communicator_component) && communicator_component.carried_mob)
+ message_sender = carrier_user_component.name
+
+ room_to_send_to.send_message(message_to_send, message_sender)
+ return TRUE
+
+/// Prompts the parent mob to send a emote to the soulcatcher. Returns False if no soulcatcher or emote could be found.
+/mob/living/proc/carrier_emote()
+ set name = "Carrier Me"
+ set category = "IC"
+ set desc = "Send a emote to your currently targeted carrier room."
+
+ var/datum/carrier_room/room_to_send_to = get_current_carrier_room()
+ if(!istype(room_to_send_to))
+ to_chat(src, span_warning("You do not have a carrier you can send emotes to!"))
+ return FALSE
+
+ var/message_to_send = tgui_input_text(usr, "Input the emote you want to send", "Soulcatcher", multiline = TRUE)
+ if(!message_to_send)
+ return FALSE
+
+ var/message_sender = room_to_send_to.outside_voice
+ var/datum/component/carrier_user/carrier_user_component = GetComponent(/datum/component/carrier_user)
+ var/datum/component/carrier_communicator/communicator_component = GetComponent(/datum/component/carrier_communicator)
+ if(istype(carrier_user_component) && istype(communicator_component) && communicator_component.carried_mob)
+ message_sender = carrier_user_component.name
+
+ room_to_send_to.send_message(message_to_send, message_sender, TRUE)
+ return TRUE
+
+/// Attempts to find and return the current carrier room the mob is using.
+/mob/living/proc/get_current_carrier_room()
+ var/datum/component/carrier_communicator/communicator_component = GetComponent(/datum/component/carrier_communicator)
+ if(!communicator_component)
+ return FALSE
+
+ var/datum/component/carrier/master_carrier = communicator_component.target_carrier?.resolve()
+ if(!istype(master_carrier))
+ return FALSE
+
+ var/datum/carrier_room/target_room = master_carrier.targeted_carrier_room
+ var/datum/component/carrier_user/carrier_user_component = GetComponent(/datum/component/carrier_user)
+ if(communicator_component.carried_mob && istype(carrier_user_component))
+ target_room = carrier_user_component.current_room.resolve()
+ if(!istype(target_room))
+ return FALSE
+
+ if(!Adjacent(master_carrier.parent))
+ communicator_component.target_carrier = null
+ return FALSE
+
+ return target_room
diff --git a/modular_skyrat/modules/modular_implants/code/nifsofts/soulcatcher.dm b/modular_skyrat/modules/modular_implants/code/nifsofts/soulcatcher.dm
index 716e7e9513c94d..08399f55174266 100644
--- a/modular_skyrat/modules/modular_implants/code/nifsofts/soulcatcher.dm
+++ b/modular_skyrat/modules/modular_implants/code/nifsofts/soulcatcher.dm
@@ -15,9 +15,9 @@
/// What action to bring up the soulcatcher is linked with this NIFSoft?
var/datum/action/innate/soulcatcher/soulcatcher_action
/// a list containing saved soulcatcher rooms
- var/list/saved_soulcatcher_rooms = list()
+ var/list/saved_carrier_rooms = list()
/// The item we are using to store the souls
- var/obj/item/soulcatcher_holder/soul_holder
+ var/obj/item/carrier_holder/holder
/datum/nifsoft/soulcatcher/New()
. = ..()
@@ -25,15 +25,15 @@
soulcatcher_action.Grant(linked_mob)
soulcatcher_action.parent_nifsoft = WEAKREF(src)
- soul_holder = new(linked_mob)
- var/datum/component/soulcatcher/new_soulcatcher = soul_holder.AddComponent(/datum/component/soulcatcher/nifsoft)
- soul_holder.name = linked_mob.name
+ holder = new(linked_mob)
+ var/datum/component/carrier/soulcatcher/new_soulcatcher = holder.AddComponent(/datum/component/carrier/soulcatcher/nifsoft)
+ holder.name = "[linked_mob.name]'s soulcatcher"
- for(var/room in saved_soulcatcher_rooms)
- new_soulcatcher.create_room(room, saved_soulcatcher_rooms[room])
+ for(var/room in saved_carrier_rooms)
+ new_soulcatcher.create_room(room, saved_carrier_rooms[room])
- if(length(new_soulcatcher.soulcatcher_rooms) > 1) //We don't need the default room anymore.
- new_soulcatcher.soulcatcher_rooms -= new_soulcatcher.soulcatcher_rooms[1]
+ if(length(new_soulcatcher.carrier_rooms) > 1) //We don't need the default room anymore.
+ new_soulcatcher.carrier_rooms -= new_soulcatcher.carrier_rooms[1]
new_soulcatcher.name = "[linked_mob]"
@@ -46,7 +46,7 @@
if(!linked_soulcatcher)
return FALSE
- var/datum/component/soulcatcher/current_soulcatcher = linked_soulcatcher.resolve()
+ var/datum/component/carrier/current_soulcatcher = linked_soulcatcher.resolve()
if(!current_soulcatcher)
return FALSE
@@ -65,11 +65,11 @@
qdel(soulcatcher_action)
if(linked_soulcatcher)
- var/datum/component/soulcatcher/current_soulcatcher = linked_soulcatcher.resolve()
+ var/datum/component/carrier/current_soulcatcher = linked_soulcatcher.resolve()
if(current_soulcatcher)
qdel(current_soulcatcher)
- qdel(soul_holder)
+ qdel(holder)
return ..()
@@ -79,7 +79,7 @@
if(!persistence)
return FALSE
- saved_soulcatcher_rooms = params2list(persistence.nif_soulcatcher_rooms)
+ saved_carrier_rooms = params2list(persistence.nif_carrier_rooms)
return TRUE
/datum/nifsoft/soulcatcher/save_persistence_data(datum/modular_persistence/persistence)
@@ -88,11 +88,11 @@
return FALSE
var/list/room_list = list()
- var/datum/component/soulcatcher/current_soulcatcher = linked_soulcatcher.resolve()
- for(var/datum/soulcatcher_room/room in current_soulcatcher.soulcatcher_rooms)
+ var/datum/component/carrier/current_soulcatcher = linked_soulcatcher.resolve()
+ for(var/datum/carrier_room/room in current_soulcatcher.carrier_rooms)
room_list[room.name] = room.room_description
- persistence.nif_soulcatcher_rooms = list2params(room_list)
+ persistence.nif_carrier_rooms = list2params(room_list)
return TRUE
/datum/nifsoft/soulcatcher/update_theme()
@@ -103,7 +103,7 @@
if(isnull(linked_soulcatcher))
return FALSE
- var/datum/component/soulcatcher/current_soulcatcher = linked_soulcatcher.resolve()
+ var/datum/component/carrier/current_soulcatcher = linked_soulcatcher.resolve()
if(!istype(current_soulcatcher))
stack_trace("[src] ([REF(src)]) tried to update its theme when it was missing a linked_soulcatcher component!")
return FALSE
@@ -111,7 +111,7 @@
/datum/modular_persistence
///A param string containing soulcatcher rooms
- var/nif_soulcatcher_rooms = ""
+ var/nif_carrier_rooms = ""
/datum/action/innate/soulcatcher
name = "Soulcatcher"
@@ -131,7 +131,7 @@
soulcatcher_nifsoft.activate()
/// This is the object we use if we give a mob soulcatcher. Having the souls directly parented could cause issues.
-/obj/item/soulcatcher_holder
+/obj/item/carrier_holder
name = "Soul Holder"
desc = "You probably shouldn't be seeing this..."
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/attachable_soulcatcher.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/attachable_soulcatcher.dm
index cd84179e083898..0d2de8f71218ca 100644
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/attachable_soulcatcher.dm
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/attachable_soulcatcher.dm
@@ -1,19 +1,19 @@
-/datum/component/soulcatcher/small_device
- max_souls = 1
+/datum/component/carrier/soulcatcher/small_device
+ max_mobs = 1
-/datum/component/soulcatcher/attachable_soulcatcher
- max_souls = 1
+/datum/component/carrier/soulcatcher/attachable
+ max_mobs = 1
communicate_as_parent = TRUE
removable = TRUE
-/datum/component/soulcatcher/attachable_soulcatcher/New()
+/datum/component/carrier/soulcatcher/attachable/New()
. = ..()
var/obj/item/parent_item = parent
if(!istype(parent_item))
return COMPONENT_INCOMPATIBLE
name = parent_item.name
- var/datum/soulcatcher_room/first_room = soulcatcher_rooms[1]
+ var/datum/carrier_room/first_room = carrier_rooms[1]
first_room.name = parent_item.name
first_room.room_description = parent_item.desc
@@ -22,34 +22,32 @@
RegisterSignal(parent, COMSIG_PREQDELETED, PROC_REF(remove_self))
/// Adds text to the examine text of the parent item, explaining that the item can be used to enable the use of NIFSoft HUDs
-/datum/component/soulcatcher/attachable_soulcatcher/proc/on_examine(datum/source, mob/user, list/examine_text)
+/datum/component/carrier/soulcatcher/attachable/proc/on_examine(datum/source, mob/user, list/examine_text)
SIGNAL_HANDLER
examine_text += span_cyan("[source] has a soulcatcher attached to it, Ctrl+Shift+Click to use it.")
-/datum/component/soulcatcher/attachable_soulcatcher/proc/bring_up_ui(datum/source, mob/user)
+/datum/component/carrier/soulcatcher/attachable/proc/bring_up_ui(datum/source, mob/user)
SIGNAL_HANDLER
INVOKE_ASYNC(src, PROC_REF(ui_interact), user)
-/datum/component/soulcatcher/attachable_soulcatcher/Destroy(force)
+/datum/component/carrier/soulcatcher/attachable/Destroy(force)
UnregisterSignal(parent, COMSIG_ATOM_EXAMINE)
UnregisterSignal(parent, COMSIG_CLICK_CTRL_SHIFT)
UnregisterSignal(parent, COMSIG_PREQDELETED)
return ..()
-/datum/component/soulcatcher/attachable_soulcatcher/remove_self()
+/datum/component/carrier/soulcatcher/attachable/remove_self()
var/obj/item/parent_item = parent
var/turf/drop_turf = get_turf(parent_item)
var/obj/item/attachable_soulcatcher/dropped_item = new (drop_turf)
- var/datum/component/soulcatcher/dropped_soulcatcher = dropped_item.GetComponent(/datum/component/soulcatcher)
- var/datum/soulcatcher_room/target_room = dropped_soulcatcher.soulcatcher_rooms[1]
- var/list/current_souls = get_current_souls()
+ var/datum/component/carrier/dropped_soulcatcher = dropped_item.GetComponent(/datum/component/carrier)
+ var/datum/carrier_room/target_room = dropped_soulcatcher.carrier_rooms[1]
+ var/list/current_mobs = get_current_mobs()
- if(current_souls) // If we have souls inside of here, they should be transferred to the new object
- for(var/mob/living/soulcatcher_soul/soul as anything in current_souls)
- var/datum/soulcatcher_room/current_room = soul.current_room.resolve()
- if(istype(current_room))
- current_room.transfer_soul(soul, target_room)
+ if(current_mobs) // If we have souls inside of here, they should be transferred to the new object
+ for(var/mob/living/soul as anything in current_mobs)
+ transfer_mob(soul, target_room)
return ..()
@@ -72,11 +70,11 @@
/obj/item/disk/nuclear, // Woah there
)
/// What soulcathcer component is currnetly linked to this object?
- var/datum/component/soulcatcher/small_device/linked_soulcatcher
+ var/datum/component/carrier/soulcatcher/small_device/linked_soulcatcher
/obj/item/attachable_soulcatcher/Initialize(mapload)
. = ..()
- linked_soulcatcher = AddComponent(/datum/component/soulcatcher/small_device)
+ linked_soulcatcher = AddComponent(/datum/component/carrier/soulcatcher/small_device)
linked_soulcatcher.name = name
/obj/item/attachable_soulcatcher/attack_self(mob/user, modifiers)
@@ -87,7 +85,7 @@
if(!proximity_flag || !istype(target_item))
return FALSE
- if(target_item.GetComponent(/datum/component/soulcatcher))
+ if(target_item.GetComponent(/datum/component/carrier))
balloon_alert(user, "already attached!")
return FALSE
@@ -95,17 +93,14 @@
balloon_alert(user, "incompatible!")
return FALSE
- var/datum/component/soulcatcher/new_soulcatcher = target_item.AddComponent(/datum/component/soulcatcher/attachable_soulcatcher)
+ var/datum/component/carrier/soulcatcher/attachable/new_soulcatcher = target_item.AddComponent(/datum/component/carrier/soulcatcher/attachable)
playsound(target_item.loc, 'sound/weapons/circsawhit.ogg', 50, vary = TRUE)
- var/datum/soulcatcher_room/target_room = new_soulcatcher.soulcatcher_rooms[1]
- var/list/current_souls = linked_soulcatcher.get_current_souls()
- if(current_souls)
- for(var/mob/living/soulcatcher_soul/soul as anything in current_souls)
- var/datum/soulcatcher_room/current_room = soul.current_room.resolve()
- if(istype(current_room))
- current_room.transfer_soul(soul, target_room)
- current_room.transfer_soul(soul, target_room)
+ var/datum/carrier_room/target_room = new_soulcatcher.carrier_rooms[1]
+ var/list/current_mobs = linked_soulcatcher.get_current_mobs()
+ if(current_mobs)
+ for(var/mob/living/soul as anything in current_mobs)
+ linked_soulcatcher.transfer_mob(soul, target_room)
if(destroy_on_use)
qdel(src)
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/ghost.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/ghost.dm
new file mode 100644
index 00000000000000..28ccc7cdec264f
--- /dev/null
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/ghost.dm
@@ -0,0 +1,89 @@
+/datum/action/innate/join_soulcatcher
+ name = "Enter Soulcatcher"
+ background_icon = 'modular_skyrat/master_files/icons/mob/actions/action_backgrounds.dmi'
+ background_icon_state = "android"
+ button_icon = 'modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi'
+ button_icon_state = "soulcatcher_enter"
+
+/datum/action/innate/join_soulcatcher/Activate()
+ . = ..()
+ var/mob/dead/observer/joining_soul = owner
+ if(!joining_soul)
+ return FALSE
+
+ joining_soul.join_soulcatcher()
+
+/mob/dead/observer/verb/join_soulcatcher()
+ set name = "Enter Soulcatcher"
+ set category = "Ghost"
+
+ var/list/joinable_soulcatchers = list()
+ var/list/rooms_to_join = list()
+
+ for(var/datum/component/carrier/soulcatcher/soulcatcher in GLOB.soulcatchers)
+ if(!soulcatcher.ghost_joinable || !isobj(soulcatcher.parent) || !soulcatcher.check_for_vacancy())
+ continue
+
+ var/list/carrier_rooms = soulcatcher.get_open_rooms(TRUE)
+ if(!length(carrier_rooms))
+ continue
+
+ var/obj/item/soulcatcher_parent = soulcatcher.parent
+ if(soulcatcher.name != soulcatcher_parent.name)
+ soulcatcher.name = soulcatcher_parent.name
+
+ joinable_soulcatchers += soulcatcher
+ rooms_to_join += carrier_rooms
+
+ if(!length(joinable_soulcatchers) || !length(rooms_to_join))
+ to_chat(src, span_warning("No soulcatchers are joinable."))
+ return FALSE
+
+ var/datum/component/carrier/soulcatcher/soulcatcher_to_join = tgui_input_list(src, "Choose a soulcatcher to join", "Enter a soulcatcher", joinable_soulcatchers)
+ if(!soulcatcher_to_join || !(soulcatcher_to_join in joinable_soulcatchers))
+ return FALSE
+
+ rooms_to_join = soulcatcher_to_join.get_open_rooms(TRUE)
+ var/datum/carrier_room/soulcatcher/room_to_join = tgui_input_list(src, "Choose a room to enter", "Enter a room", rooms_to_join)
+ if(!room_to_join)
+ to_chat(src, span_warning("There no rooms that you can join."))
+ return FALSE
+
+ if(soulcatcher_to_join.require_approval)
+ var/ghost_name = name
+ if(mind?.current)
+ ghost_name = "unknown"
+
+ if(!soulcatcher_to_join.get_approval(ghost_name))
+ to_chat(src, span_warning("The owner of [soulcatcher_to_join.name] declined your request to join."))
+ return FALSE
+
+ room_to_join.add_soul_from_ghost(src)
+ return TRUE
+
+/mob/grab_ghost(force)
+ SEND_SIGNAL(src, COMSIG_SOULCATCHER_CHECK_SOUL)
+ return ..()
+
+/mob/get_ghost(even_if_they_cant_reenter, ghosts_with_clients)
+ if(GetComponent(/datum/component/previous_body)) //Is the soul currently within a soulcatcher?
+ return TRUE
+
+ return ..()
+
+/mob/dead/observer/Login()
+ . = ..()
+ var/datum/preferences/preferences = client?.prefs
+ var/soulcatcher_action_given
+
+ if(preferences)
+ soulcatcher_action_given = preferences.read_preference(/datum/preference/toggle/soulcatcher_join_action)
+
+ if(!soulcatcher_action_given)
+ return
+
+ if(locate(/datum/action/innate/join_soulcatcher) in actions)
+ return
+
+ var/datum/action/innate/join_soulcatcher/new_join_action = new(src)
+ new_join_action.Grant(src)
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/handheld_soulcatcher.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/handheld_soulcatcher.dm
index 767f396057e6f0..3ef62e68ac2b4b 100644
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/handheld_soulcatcher.dm
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/handheld_soulcatcher.dm
@@ -12,7 +12,7 @@
slot_flags = ITEM_SLOT_BELT
obj_flags = UNIQUE_RENAME
/// What soulcatcher datum is associated with this item?
- var/datum/component/soulcatcher/linked_soulcatcher
+ var/datum/component/carrier/soulcatcher/linked_soulcatcher
/// The cooldown for the RSD on scanning a body if the ghost refuses. This is here to prevent spamming.
COOLDOWN_DECLARE(rsd_scan_cooldown)
@@ -26,7 +26,7 @@
/obj/item/handheld_soulcatcher/New(loc, ...)
. = ..()
- linked_soulcatcher = AddComponent(/datum/component/soulcatcher)
+ linked_soulcatcher = AddComponent(/datum/component/carrier/soulcatcher)
linked_soulcatcher.name = name
/obj/item/handheld_soulcatcher/Destroy(force)
@@ -59,7 +59,7 @@
to_chat(user, span_warning("You are unable to get the soul of [target_mob]!"))
return FALSE
- var/datum/soulcatcher_room/target_room = tgui_input_list(user, "Choose a room to send [target_mob]'s soul to.", name, linked_soulcatcher.soulcatcher_rooms, timeout = 30 SECONDS)
+ var/datum/carrier_room/soulcatcher/target_room = tgui_input_list(user, "Choose a room to send [target_mob]'s soul to.", name, linked_soulcatcher.carrier_rooms, timeout = 30 SECONDS)
if(!target_room)
return FALSE
@@ -82,7 +82,7 @@
linked_soulcatcher.scan_body(target_mob, user)
return TRUE
- var/datum/soulcatcher_room/target_room = tgui_input_list(user, "Choose a room to send [target_mob]'s soul to.", name, linked_soulcatcher.soulcatcher_rooms, timeout = 30 SECONDS)
+ var/datum/carrier_room/target_room = tgui_input_list(user, "Choose a room to send [target_mob]'s soul to.", name, linked_soulcatcher.carrier_rooms, timeout = 30 SECONDS)
if(!target_room)
return FALSE
@@ -97,7 +97,7 @@
if(!target_mob.mind)
return FALSE
- target_room.add_soul(target_mob.mind, TRUE)
+ target_room.add_soul_from_mind(target_mob.mind, FALSE)
playsound(src, 'modular_skyrat/modules/modular_implants/sounds/default_good.ogg', 50, FALSE, ignore_walls = FALSE)
visible_message(span_notice("[src] beeps: [target_mob]'s mind transfer is now complete."))
@@ -129,9 +129,9 @@
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
var/list/soul_list = list()
- for(var/datum/soulcatcher_room/room as anything in linked_soulcatcher.soulcatcher_rooms)
- for(var/mob/living/soulcatcher_soul/soul as anything in room.current_souls)
- if(!soul.round_participant || soul.body_scan_needed)
+ for(var/datum/carrier_room/room as anything in linked_soulcatcher.carrier_rooms)
+ for(var/mob/living/soulcatcher_soul/soul as anything in room.current_mobs)
+ if(!istype(soul) || !soul.round_participant || soul.body_scan_needed)
continue
soul_list += soul
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_body_component.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_body_component.dm
index 241991b61f80a0..bb48dbb2e72f26 100644
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_body_component.dm
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_body_component.dm
@@ -35,13 +35,9 @@
return FALSE
to_chat(target_soul, span_cyan("Your body has scanned, revealing your true identity."))
- target_soul.name = source_mob.real_name
target_soul.body_scan_needed = FALSE
- var/datum/preferences/preferences = target_soul.client?.prefs
- if(preferences)
- target_soul.soul_desc = preferences.read_preference(/datum/preference/text/flavor_text)
-
+ SEND_SIGNAL(target_soul, COMSIG_CARRIER_MOB_REFRESH_APPEARANCE)
return TRUE
/// Attempts to destroy the component. If `restore_mind` is true, it will attempt to place the mind back inside of the body and delete the soulcatcher soul.
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_component.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_component.dm
index 8c14385430a24a..a2f9a35b70c70f 100644
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_component.dm
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_component.dm
@@ -1,134 +1,42 @@
-///Global list containing any and all soulcatchers
-GLOBAL_LIST_EMPTY(soulcatchers)
-
-#define SOULCATCHER_DEFAULT_COLOR "#75D5E1"
-#define SOULCATCHER_WARNING_MESSAGE "You have entered a soulcatcher, do not share any information you have received while a ghost. If you have died within the round, you do not know your identity until your body has been scanned, standard blackout policy also applies."
-
-/**
- * Soulcatcher Component
- *
- * This component functions as a bridge between the `soulcatcher_room` attached to itself and the parented datum.
- * It handles the creation of new soulcatcher rooms, TGUI, and relaying messages to the parent datum.
- * If the component is deleted, any soulcatcher rooms inside of `soulcatcher_rooms` will be deleted.
- */
-/datum/component/soulcatcher
- /// What is the name of the soulcatcher?
- var/name = "soulcatcher"
- /// What rooms are linked to this soulcatcher
- var/list/soulcatcher_rooms = list()
- /// What soulcatcher room are verbs sending messages to?
- var/datum/soulcatcher_room/targeted_soulcatcher_room
- /// What theme are we using for our soulcatcher UI?
- var/ui_theme = "default"
-
+/datum/component/carrier/soulcatcher
/// Are ghosts currently able to join this soulcatcher?
var/ghost_joinable = TRUE
- /// Do we want to ask the user permission before the ghost joins?
- var/require_approval = TRUE
- /// What is the max number of people we can keep in this soulcatcher? If this is set to `FALSE` we don't have a limit
- var/max_souls = FALSE
- /// Are are the souls inside able to emote/speak as the parent?
- var/communicate_as_parent = FALSE
/// Is the soulcatcher removable from the parent object?
var/removable = FALSE
-/datum/component/soulcatcher/New()
- . = ..()
- if(!parent)
- return COMPONENT_INCOMPATIBLE
+ type_of_room_to_create = /datum/carrier_room/soulcatcher
- create_room()
- targeted_soulcatcher_room = soulcatcher_rooms[1]
+/datum/component/carrier/soulcatcher/New()
+ . = ..()
GLOB.soulcatchers += src
- var/obj/item/soulcatcher_holder/soul_holder = parent
- if(istype(soul_holder) && ismob(soul_holder.loc))
- var/mob/living/soulcatcher_owner = soul_holder.loc
- add_verb(soulcatcher_owner, list(
- /mob/living/proc/soulcatcher_say,
- /mob/living/proc/soulcatcher_emote,
- ))
-
-/datum/component/soulcatcher/Destroy(force, ...)
+/datum/component/carrier/soulcatcher/Destroy(force, ...)
GLOB.soulcatchers -= src
-
- targeted_soulcatcher_room = null
- for(var/datum/soulcatcher_room as anything in soulcatcher_rooms)
- soulcatcher_rooms -= soulcatcher_room
- qdel(soulcatcher_room)
-
- var/mob/living/soulcatcher_owner = parent
- var/obj/item/organ/internal/cyberimp/brain/nif/parent_nif = parent
- if(istype(parent_nif))
- soulcatcher_owner = parent_nif.linked_mob
-
- if(istype(soulcatcher_owner))
- remove_verb(soulcatcher_owner, list(
- /mob/living/proc/soulcatcher_say,
- /mob/living/proc/soulcatcher_emote,
- ))
-
return ..()
-/**
- * Creates a `/datum/soulcatcher_room` and adds it to the `soulcatcher_rooms` list.
- *
- * Arguments
- * * target_name - The name that we want to assign to the created room.
- * * target_desc - The description that we want to assign to the created room.
- */
-/datum/component/soulcatcher/proc/create_room(target_name = "Default Room", target_desc = "An orange platform suspended in space orbited by reflective cubes of various sizes. There really isn't much here at the moment.")
- var/datum/soulcatcher_room/created_room = new(src)
- created_room.name = target_name
- created_room.room_description = target_desc
- soulcatcher_rooms += created_room
-
- created_room.master_soulcatcher = WEAKREF(src)
-
-/// Tries to find out who is currently using the soulcatcher, returns the holder. If no holder can be found, returns FALSE
-/datum/component/soulcatcher/proc/get_current_holder()
- var/mob/living/holder
-
- if(!istype(parent, /obj/item))
- return FALSE
-
- var/obj/item/parent_item = parent
- holder = parent_item.loc
+/datum/component/carrier/soulcatcher/nifsoft
+ single_owner = TRUE
- if(!istype(holder))
- return FALSE
-
- return holder
-
-/// Recieves a message from a soulcatcher room.
-/datum/component/soulcatcher/proc/recieve_message(message_to_recieve)
- if(!message_to_recieve)
- return FALSE
-
- var/mob/living/soulcatcher_owner = get_current_holder()
- if(!soulcatcher_owner)
+/// Attempts to remove the carrier from the attached object
+/datum/component/carrier/soulcatcher/proc/remove_self()
+ if(!removable)
return FALSE
- to_chat(soulcatcher_owner, message_to_recieve)
- return TRUE
-
-/// Attempts to ping the current user of the soulcatcher, asking them if `joiner_name` is allowed in. If they are, the proc returns `TRUE`, otherwise returns FALSE
-/datum/component/soulcatcher/proc/get_approval(joiner_name)
- if(!require_approval)
- return TRUE
+ qdel(src)
- var/mob/living/soulcatcher_owner = get_current_holder()
+/// Returns a list of all of the rooms that a soul can join/transfer into. `ghost_join` checks if the room is accessible to ghosts.
+/datum/component/carrier/soulcatcher/get_open_rooms(ghost_join = FALSE)
+ var/list/datum/carrier_room/room_list = list()
+ for(var/datum/carrier_room/room as anything in carrier_rooms)
+ if((ghost_join && !room.joinable) || !check_for_vacancy())
+ continue
- if(!soulcatcher_owner)
- return FALSE
+ room_list += room
- if(tgui_alert(soulcatcher_owner, "Do you wish to allow [joiner_name] into your soulcatcher?", name, list("Yes", "No"), autofocus = FALSE) != "Yes")
- return FALSE
-
- return TRUE
+ return room_list
/// Attempts to scan the body for the `previous_body component`, returns FALSE if the body is unable to be scanned, otherwise returns TRUE
-/datum/component/soulcatcher/proc/scan_body(mob/living/parent_body, mob/living/user)
+/datum/component/carrier/soulcatcher/proc/scan_body(mob/living/parent_body, mob/living/user)
if(!parent_body || !user)
return FALSE
@@ -144,57 +52,14 @@ GLOBAL_LIST_EMPTY(soulcatchers)
return TRUE
-/// Returns a list containing all of the souls currently present within a soulcatcher.
-/datum/component/soulcatcher/proc/get_current_souls()
- var/list/current_souls = list()
- for(var/datum/soulcatcher_room/room as anything in soulcatcher_rooms)
- for(var/mob/living/soulcatcher_soul as anything in room.current_souls)
- current_souls += soulcatcher_soul
-
- return current_souls
-
-/// Checks the total number of souls present and compares it with `max_souls` returns `TRUE` if there is room (or no limit), otherwise returns `FALSE`
-/datum/component/soulcatcher/proc/check_for_vacancy()
- if(!max_souls)
- return TRUE
-
- if(length(get_current_souls()) >= max_souls)
- return FALSE
-
- return TRUE
-
-/// Attempts to remove the soulcatcher from the attached object
-/datum/component/soulcatcher/proc/remove_self()
- if(!removable)
- return FALSE
-
- qdel(src)
-
-/**
- * Soulcatcher Room
- *
- * This datum is where souls are sent to when joining soulcatchers.
- * It handles sending messages to souls from the outside along with adding new souls, transfering, and removing souls.
- *
- */
-/datum/soulcatcher_room
+/datum/carrier_room/soulcatcher
/// What is the name of the room?
- var/name = "Default Room"
+ name = "Default Room"
/// What is the description of the room?
- var/room_description = "An orange platform suspended in space orbited by reflective cubes of various sizes. There really isn't much here at the moment."
- /// What souls are currently inside of the room?
- var/list/current_souls = list()
- /// Weakref for the master soulcatcher datum
- var/datum/weakref/master_soulcatcher
- /// What is the name of the person sending the messages?
- var/outside_voice = "Host"
- /// Can the room be joined at all?
- var/joinable = TRUE
- /// What is the color of chat messages sent by the room?
- var/room_color = SOULCATCHER_DEFAULT_COLOR
+ room_description = "An orange platform suspended in space orbited by reflective cubes of various sizes. There really isn't much here at the moment."
/// Attemps to add a ghost to the soulcatcher room.
-/datum/soulcatcher_room/proc/add_soul_from_ghost(mob/dead/observer/ghost)
+/datum/carrier_room/soulcatcher/proc/add_soul_from_ghost(mob/dead/observer/ghost)
if(!ghost || !ghost.ckey)
return FALSE
@@ -203,268 +68,53 @@ GLOBAL_LIST_EMPTY(soulcatchers)
ghost.mind.name = ghost.name
ghost.mind.active = TRUE
- if(!add_soul(ghost.mind))
+ if(!add_soul_from_mind(ghost.mind))
return FALSE
return TRUE
/// Converts a mind into a soul and adds the resulting soul to the room.
-/datum/soulcatcher_room/proc/add_soul(datum/mind/mind_to_add)
+/datum/carrier_room/proc/add_soul_from_mind(datum/mind/mind_to_add, hide_participant_identity = TRUE)
if(!mind_to_add)
return FALSE
- var/datum/component/soulcatcher/parent_soulcatcher = master_soulcatcher.resolve()
+ var/datum/component/carrier/parent_soulcatcher = master_carrier.resolve()
var/datum/parent_object = parent_soulcatcher.parent
if(!parent_object)
return FALSE
var/mob/living/soulcatcher_soul/new_soul = new(parent_object)
- new_soul.name = mind_to_add.name
-
if(mind_to_add.current)
var/datum/component/previous_body/body_component = mind_to_add.current.AddComponent(/datum/component/previous_body)
body_component.soulcatcher_soul = WEAKREF(new_soul)
new_soul.round_participant = TRUE
new_soul.body_scan_needed = TRUE
-
new_soul.previous_body = WEAKREF(mind_to_add.current)
- new_soul.name = pick(GLOB.last_names) //Until the body is discovered, the soul is a new person.
- new_soul.soul_desc = "[new_soul] lacks a discernible form."
- mind_to_add.transfer_to(new_soul, TRUE)
- current_souls += new_soul
- new_soul.current_room = WEAKREF(src)
+ var/datum/component/carrier_user/soul_component = parent_soulcatcher.add_mob(new_soul, src)
+ if(!soul_component)
+ return FALSE
- var/datum/preferences/preferences = new_soul.client?.prefs
- if(preferences)
- new_soul.ooc_notes = preferences.read_preference(/datum/preference/text/ooc_notes)
- if(!new_soul.body_scan_needed)
- new_soul.soul_desc = preferences.read_preference(/datum/preference/text/flavor_text)
+ if(hide_participant_identity && new_soul.round_participant)
+ soul_component.name = pick(GLOB.last_names) //Until the body is discovered, the soul is a new person.
+ soul_component.desc = "[new_soul] lacks a discernible form."
+
+ mind_to_add.transfer_to(new_soul, TRUE)
+ current_mobs += new_soul
+ soul_component.current_room = WEAKREF(src)
to_chat(new_soul, span_cyan("You find yourself now inside of: [name]"))
to_chat(new_soul, span_notice(room_description))
to_chat(new_soul, span_doyourjobidiot("You have entered a soulcatcher, do not share any information you have received while a ghost. If you have died within the round, you do not know your identity until your body has been scanned, standard blackout policy also applies."))
- to_chat(new_soul, span_notice("While inside of a soulcatcher, you are able to speak and emote by using the normal hotkeys and verbs, unless disabled by the owner."))
+ to_chat(new_soul, span_notice("While inside of [src], you are able to speak and emote by using the normal hotkeys and verbs, unless disabled by the owner."))
to_chat(new_soul, span_notice("You may use the leave soulcatcher verb to leave the soulcatcher and return to your body at any time."))
var/atom/parent_atom = parent_object
if(istype(parent_atom))
var/turf/soulcatcher_turf = get_turf(parent_soulcatcher.parent)
- var/message_to_log = "[key_name(new_soul)] joined [src] inside of [parent_atom] at [loc_name(soulcatcher_turf)]"
+ var/message_to_log = "[key_name(new_soul)] entered [src] inside of [parent_atom] at [loc_name(soulcatcher_turf)]"
parent_atom.log_message(message_to_log, LOG_GAME)
new_soul.log_message(message_to_log, LOG_GAME)
return TRUE
-
-/// Removes a soul from a soulcatcher room, leaving it as a ghost. Returns `FALSE` if the `soul_to_remove` cannot be found, otherwise returns `TRUE` after a successful deletion.
-/datum/soulcatcher_room/proc/remove_soul(mob/living/soulcatcher_soul/soul_to_remove)
- if(!soul_to_remove || !(soul_to_remove in current_souls))
- return FALSE
-
- current_souls -= soul_to_remove
- soul_to_remove.current_room = null
-
- soul_to_remove.return_to_body()
- qdel(soul_to_remove)
-
- return TRUE
-
-/// Transfers a soul from a soulcatcher room to another soulcatcher room. Returns `FALSE` if the target room or target soul cannot be found.
-/datum/soulcatcher_room/proc/transfer_soul(mob/living/soulcatcher_soul/target_soul, datum/soulcatcher_room/target_room)
- if(!(target_soul in current_souls) || !target_room)
- return FALSE
-
- var/datum/component/soulcatcher/target_master_soulcatcher = target_room.master_soulcatcher.resolve()
- if(target_master_soulcatcher != master_soulcatcher.resolve())
- target_soul.forceMove(target_master_soulcatcher.parent)
-
- target_soul.current_room = WEAKREF(target_room)
- current_souls -= target_soul
- target_room.current_souls += target_soul
-
- to_chat(target_soul, span_cyan("you've been transferred to [target_room]!"))
- to_chat(target_soul, span_notice(target_room.room_description))
-
- return TRUE
-
-/**
- * Sends a message or emote to all of the souls currently located inside of the soulcatcher room. Returns `FALSE` if a message cannot be sent, otherwise returns `TRUE`.
- *
- * Arguments
- * * message_to_send - The message we want to send to the occupants of the room
- * * message_sender - The person that is sending the message. This is not required.
- * * emote - Is the message sent an emote or not?
- */
-/datum/soulcatcher_room/proc/send_message(message_to_send, message_sender, emote = FALSE)
- if(!message_to_send) //Why say nothing?
- return FALSE
-
- var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/chat)
- var/tag = sheet.icon_tag("nif-soulcatcher")
- var/soulcatcher_icon = ""
-
- if(tag)
- soulcatcher_icon = tag
-
- var/mob/living/soulcatcher_soul/soul_sender = message_sender
- if(istype(soul_sender) && soul_sender.communicating_externally)
- var/master_resolved = master_soulcatcher.resolve()
- if(!master_resolved)
- return FALSE
- var/datum/component/soulcatcher/parent_soulcatcher = master_resolved
- var/obj/item/parent_object = parent_soulcatcher.parent
- if(!istype(parent_object))
- return FALSE
-
- var/temp_name = parent_object.name
- parent_object.name = "[parent_object.name] [soulcatcher_icon]"
-
- if(emote)
- parent_object.manual_emote(html_decode(message_to_send))
- log_emote("[soul_sender] in [name] soulcatcher room emoted: [message_to_send], as an external object")
- else
- parent_object.say(html_decode(message_to_send))
- log_say("[soul_sender] in [name] soulcatcher room said: [message_to_send], as an external object")
-
- parent_object.name = temp_name
- return TRUE
-
- var/sender_name = ""
- if(message_sender)
- sender_name = "[message_sender] "
-
- var/first_room_name_word = splittext(name, " ")
- var/message = ""
- var/owner_message = ""
- if(!emote)
- message = "\ [soulcatcher_icon] [sender_name]says, \"[message_to_send]\""
- owner_message = "\ ([first_room_name_word[1]]) [soulcatcher_icon] [sender_name]says, \"[message_to_send]\""
- log_say("[sender_name] in [name] soulcatcher room said: [message_to_send]")
- else
- message = "\ [soulcatcher_icon] [sender_name][message_to_send]"
- owner_message = "\ ([first_room_name_word[1]]) [soulcatcher_icon] [sender_name][message_to_send]"
- log_emote("[sender_name] in [name] soulcatcher room emoted: [message_to_send]")
-
- for(var/mob/living/soulcatcher_soul/soul as anything in current_souls)
- if((emote && !soul.internal_sight) || (!emote && !soul.internal_hearing))
- continue
-
- to_chat(soul, message)
-
- relay_message_to_soulcatcher(owner_message)
- return TRUE
-
-/// Relays a message sent from the send_message proc to the parent soulcatcher datum
-/datum/soulcatcher_room/proc/relay_message_to_soulcatcher(message)
- if(!message)
- return FALSE
-
- var/datum/component/soulcatcher/recepient_soulcatcher = master_soulcatcher.resolve()
- recepient_soulcatcher.recieve_message(message)
- return TRUE
-
-/datum/soulcatcher_room/Destroy(force, ...)
- for(var/mob/living/soulcatcher_soul/soul as anything in current_souls)
- remove_soul(soul)
-
- return ..()
-
-/datum/action/innate/join_soulcatcher
- name = "Enter Soulcatcher"
- background_icon = 'modular_skyrat/master_files/icons/mob/actions/action_backgrounds.dmi'
- background_icon_state = "android"
- button_icon = 'modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi'
- button_icon_state = "soulcatcher_enter"
-
-/datum/action/innate/join_soulcatcher/Activate()
- . = ..()
- var/mob/dead/observer/joining_soul = owner
- if(!joining_soul)
- return FALSE
-
- joining_soul.join_soulcatcher()
-
-/mob/dead/observer/verb/join_soulcatcher()
- set name = "Enter Soulcatcher"
- set category = "Ghost"
-
- var/list/joinable_soulcatchers = list()
- for(var/datum/component/soulcatcher/soulcatcher in GLOB.soulcatchers)
- if(!soulcatcher.ghost_joinable || !isobj(soulcatcher.parent) || !soulcatcher.check_for_vacancy())
- continue
-
- var/obj/item/soulcatcher_parent = soulcatcher.parent
- if(soulcatcher.name != soulcatcher_parent.name)
- soulcatcher.name = soulcatcher_parent.name
-
- joinable_soulcatchers += soulcatcher
-
- if(!length(joinable_soulcatchers))
- to_chat(src, span_warning("No soulcatchers are joinable."))
- return FALSE
-
- var/datum/component/soulcatcher/soulcatcher_to_join = tgui_input_list(src, "Choose a soulcatcher to join", "Enter a soulcatcher", joinable_soulcatchers)
- if(!soulcatcher_to_join || !(soulcatcher_to_join in joinable_soulcatchers))
- return FALSE
-
- var/list/rooms_to_join = list()
- for(var/datum/soulcatcher_room/room in soulcatcher_to_join.soulcatcher_rooms)
- if(!room.joinable)
- continue
-
- rooms_to_join += room
-
- var/datum/soulcatcher_room/room_to_join
- if(length(rooms_to_join) < 1)
- to_chat(src, span_warning("There no rooms that you can join."))
- return FALSE
-
- if(length(rooms_to_join) == 1)
- room_to_join = rooms_to_join[1]
-
- else
- room_to_join = tgui_input_list(src, "Choose a room to enter", "Enter a room", rooms_to_join)
-
- if(!room_to_join)
- to_chat(src, span_warning("There no rooms that you can join."))
- return FALSE
-
- if(soulcatcher_to_join.require_approval)
- var/ghost_name = name
- if(mind?.current)
- ghost_name = "unknown"
-
- if(!soulcatcher_to_join.get_approval(ghost_name))
- to_chat(src, span_warning("The owner of [soulcatcher_to_join.name] declined your request to join."))
- return FALSE
-
- room_to_join.add_soul_from_ghost(src)
- return TRUE
-
-/mob/grab_ghost(force)
- SEND_SIGNAL(src, COMSIG_SOULCATCHER_CHECK_SOUL)
- return ..()
-
-/mob/get_ghost(even_if_they_cant_reenter, ghosts_with_clients)
- if(GetComponent(/datum/component/previous_body)) //Is the soul currently within a soulcatcher?
- return TRUE
-
- return ..()
-
-/mob/dead/observer/Login()
- . = ..()
- var/datum/preferences/preferences = client?.prefs
- var/soulcatcher_action_given
-
- if(preferences)
- soulcatcher_action_given = preferences.read_preference(/datum/preference/toggle/soulcatcher_join_action)
-
- if(!soulcatcher_action_given)
- return
-
- if(locate(/datum/action/innate/join_soulcatcher) in actions)
- return
-
- var/datum/action/innate/join_soulcatcher/new_join_action = new(src)
- new_join_action.Grant(src)
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_mob.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_mob.dm
index 5127fdfe8d46d3..7ac643b363dd67 100644
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_mob.dm
+++ b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_mob.dm
@@ -1,92 +1,12 @@
/mob/living/soulcatcher_soul
- /// What does our soul look like?
- var/soul_desc = "It's a soul."
- /// What are the ooc notes for the soul?
- var/ooc_notes = ""
-
- /// Assuming we died inside of the round? What is our previous body?
- var/datum/weakref/previous_body
- /// What is the weakref of the soulcatcher room are we currently in?
- var/datum/weakref/current_room
-
- /// Is the soul able to see things in the outside world?
- var/outside_sight = TRUE
- /// Is the soul able to hear things from the outside world?
- var/outside_hearing = TRUE
- /// Is the soul able to "see" things from inside of the soulcatcher?
- var/internal_sight = TRUE
- /// Is the soul able to "hear" things from inside of the soulcatcher?
- var/internal_hearing = TRUE
- /// Is the soul able to emote inside the soulcatcher room?
- var/able_to_emote = TRUE
- /// Is the soul able to speak inside the soulcatcher room?
- var/able_to_speak = TRUE
- /// Is the soul able to change their own name?
- var/able_to_rename = TRUE
- /// Is the soul able to speak as the object it is inside?
- var/able_to_speak_as_container = TRUE
- /// Is the soul able to emote as the object it is inside?
- var/able_to_emote_as_container = TRUE
- /// Are emote's and Say's done through the container the mob is in?
- var/communicating_externally = FALSE
-
/// Is the soul able to leave the soulcatcher?
var/able_to_leave = TRUE
/// Did the soul live within the round? This is checked if we want to transfer the soul to another body.
var/round_participant = FALSE
/// Does the body need scanned?
var/body_scan_needed = FALSE
-
-/mob/living/soulcatcher_soul/Initialize(mapload)
- . = ..()
- if(!outside_sight)
- become_blind(NO_EYES)
-
- if(!outside_hearing)
- ADD_TRAIT(src, TRAIT_DEAF, INNATE_TRAIT)
-
- var/datum/action/innate/leave_soulcatcher/leave_action = new(src)
- leave_action.Grant(src)
-
- var/datum/action/innate/soulcatcher_user/soulcatcher_action = new(src)
- soulcatcher_action.Grant(src)
- var/datum/component/soulcatcher_user/user_component = AddComponent(/datum/component/soulcatcher_user)
- soulcatcher_action.soulcatcher_user_component = WEAKREF(user_component)
-
-/// Toggles whether or not the soul inside the soulcatcher can see the outside world. Returns the state of the `outside_sight` variable.
-/mob/living/soulcatcher_soul/proc/toggle_sight()
- outside_sight = !outside_sight
- if(outside_sight)
- cure_blind(NO_EYES)
- else
- become_blind(NO_EYES)
-
- return outside_sight
-
-/// Toggles whether or not the soul inside the soulcatcher can see the outside world. Returns the state of the `outside_hearing` variable.
-/mob/living/soulcatcher_soul/proc/toggle_hearing()
- outside_hearing = !outside_hearing
- if(outside_hearing)
- REMOVE_TRAIT(src, TRAIT_DEAF, INNATE_TRAIT)
- else
- ADD_TRAIT(src, TRAIT_DEAF, INNATE_TRAIT)
-
- return outside_hearing
-
-/// Changes the soul's name based off `new_name`. Returns `TRUE` if the name has been changed, otherwise returns `FALSE`.
-/mob/living/soulcatcher_soul/proc/change_name(new_name)
- if(!new_name || (round_participant && body_scan_needed))
- return FALSE
-
- name = new_name
- return TRUE
-
-/// Attempts to reset the soul's name to it's name in prefs. Returns `TRUE` if the name is reset, otherwise returns `FALSE`.
-/mob/living/soulcatcher_soul/proc/reset_name()
- if(!mind?.name || change_name(mind.name))
- return FALSE
-
- return TRUE
+ /// Assuming we died inside of the round? What is our previous body?
+ var/datum/weakref/previous_body
/// Checks if the mob wants to leave the soulcatcher. If they do and are able to leave, they are booted out.
/mob/living/soulcatcher_soul/verb/leave_soulcatcher()
@@ -113,35 +33,25 @@
/mob/living/soulcatcher_soul/say(message, bubble_type, list/spans, sanitize, datum/language/language, ignore_spam, forced, filterproof, message_range, datum/saymode/saymode)
message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
- if(!message || message == "")
- return
-
- if((!able_to_speak && !communicating_externally) || (!able_to_speak_as_container && communicating_externally))
- to_chat(src, span_warning("You are unable to speak!"))
+ if(!message)
return FALSE
- var/datum/soulcatcher_room/room = current_room.resolve()
- if(!room)
+ var/datum/component/carrier_user/soul_component = GetComponent(/datum/component/carrier_user)
+ if(!soul_component)
return FALSE
- room.send_message(message, src, FALSE)
- return TRUE
+ return soul_component.say(message)
/mob/living/soulcatcher_soul/me_verb(message as text)
message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
if(!message)
return FALSE
- if((!able_to_emote && !communicating_externally) || (!able_to_emote_as_container && communicating_externally))
- to_chat(src, span_warning("You are unable to emote!"))
- return FALSE
-
- var/datum/soulcatcher_room/room = current_room.resolve()
- if(!room)
+ var/datum/component/carrier_user/soul_component = GetComponent(/datum/component/carrier_user)
+ if(!soul_component)
return FALSE
- room.send_message(message, src, TRUE)
- return TRUE
+ return soul_component.me_verb(message)
/mob/living/soulcatcher_soul/subtle()
set hidden = TRUE
@@ -180,46 +90,30 @@
/mob/living/soulcatcher_soul/Destroy()
log_message("[key_name(src)] has exited a soulcatcher.", LOG_GAME)
- if(current_room)
- var/datum/soulcatcher_room/room = current_room.resolve()
+ var/datum/component/carrier_user/soul_component = GetComponent(/datum/component/carrier_user)
+ if(soul_component && soul_component.current_room)
+ var/datum/carrier_room/room = soul_component.current_room.resolve()
if(room)
- room.current_souls -= src
+ room.current_mobs -= src
- current_room = null
+ soul_component.current_room = null
return ..()
/datum/emote/living
mob_type_blacklist_typecache = list(/mob/living/brain, /mob/living/soulcatcher_soul)
-/datum/action/innate/leave_soulcatcher
+/datum/action/innate/leave_carrier
name = "Leave Soulcatcher"
background_icon = 'modular_skyrat/master_files/icons/mob/actions/action_backgrounds.dmi'
background_icon_state = "android"
button_icon = 'modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi'
button_icon_state = "soulcatcher_exit"
-/datum/action/innate/leave_soulcatcher/Activate()
+/datum/action/innate/leave_carrier/Activate()
. = ..()
var/mob/living/soulcatcher_soul/parent_soul = owner
if(!parent_soul)
return FALSE
parent_soul.leave_soulcatcher()
-
-/datum/action/innate/soulcatcher_user
- name = "Soulcatcher"
- background_icon = 'modular_skyrat/master_files/icons/mob/actions/action_backgrounds.dmi'
- background_icon_state = "android"
- button_icon = 'modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi'
- button_icon_state = "soulcatcher"
- /// What soulcatcher user component are we bringing up the menu for?
- var/datum/weakref/soulcatcher_user_component
-
-/datum/action/innate/soulcatcher_user/Activate()
- . = ..()
- var/datum/component/soulcatcher_user/user_component = soulcatcher_user_component.resolve()
- if(!user_component)
- return FALSE
-
- user_component.ui_interact(owner)
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_tgui.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_tgui.dm
deleted file mode 100644
index 6233f906fb342a..00000000000000
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_tgui.dm
+++ /dev/null
@@ -1,343 +0,0 @@
-/datum/component/soulcatcher/ui_interact(mob/user, datum/tgui/ui)
- ui = SStgui.try_update_ui(usr, src, ui)
-
- if(!ui)
- ui = new(usr, src, "Soulcatcher", name)
- ui.open()
-
-/datum/component/soulcatcher/nifsoft/ui_state(mob/user)
- return GLOB.conscious_state
-
-/datum/component/soulcatcher/ui_data(mob/user)
- var/list/data = list()
-
- data["ghost_joinable"] = ghost_joinable
- data["require_approval"] = require_approval
- data["theme"] = ui_theme
- data["communicate_as_parent"] = communicate_as_parent
- data["current_soul_count"] = length(get_current_souls())
- data["max_souls"] = max_souls
- data["removable"] = removable
-
- data["current_rooms"] = list()
- for(var/datum/soulcatcher_room/room in soulcatcher_rooms)
- var/currently_targeted = (room == targeted_soulcatcher_room)
-
- var/list/room_data = list(
- "name" = html_decode(room.name),
- "description" = html_decode(room.room_description),
- "reference" = REF(room),
- "joinable" = room.joinable,
- "color" = room.room_color,
- "currently_targeted" = currently_targeted,
- )
-
- for(var/mob/living/soulcatcher_soul/soul in room.current_souls)
- var/list/soul_list = list(
- "name" = soul.name,
- "description" = soul.soul_desc,
- "reference" = REF(soul),
- "internal_hearing" = soul.internal_hearing,
- "internal_sight" = soul.internal_sight,
- "outside_hearing" = soul.outside_hearing,
- "outside_sight" = soul.outside_sight,
- "able_to_emote" = soul.able_to_emote,
- "able_to_speak" = soul.able_to_speak,
- "able_to_rename" = soul.able_to_rename,
- "ooc_notes" = soul.ooc_notes,
- "scan_needed" = soul.body_scan_needed,
- "able_to_speak_as_container" = soul.able_to_speak_as_container,
- "able_to_emote_as_container" = soul.able_to_emote_as_container,
- )
- room_data["souls"] += list(soul_list)
-
- data["current_rooms"] += list(room_data)
-
- return data
-
-/datum/component/soulcatcher/ui_static_data(mob/user)
- var/list/data = list()
-
- data["current_vessel"] = parent
-
- return data
-
-/datum/component/soulcatcher/ui_act(action, list/params)
- . = ..()
- if(.)
- return
-
- var/datum/soulcatcher_room/target_room
- if(params["room_ref"])
- target_room = locate(params["room_ref"]) in soulcatcher_rooms
- if(!target_room)
- return FALSE
-
- var/mob/living/soulcatcher_soul/target_soul
- if(params["target_soul"])
- target_soul = locate(params["target_soul"]) in target_room.current_souls
- if(!target_soul)
- return FALSE
-
- switch(action)
- if("delete_room")
- if(length(soulcatcher_rooms) <= 1)
- return FALSE
-
- soulcatcher_rooms -= target_room
- targeted_soulcatcher_room = soulcatcher_rooms[1]
- qdel(target_room)
- return TRUE
-
- if("change_targeted_room")
- targeted_soulcatcher_room = target_room
- return TRUE
-
- if("create_room")
- create_room()
- return TRUE
-
- if("rename_room")
- var/new_room_name = tgui_input_text(usr,"Choose a new name for the room", name, target_room.name)
- if(!new_room_name)
- return FALSE
-
- target_room.name = new_room_name
- return TRUE
-
- if("redescribe_room")
- var/new_room_desc = tgui_input_text(usr,"Choose a new description for the room", name, target_room.room_description, multiline = TRUE)
- if(!new_room_desc)
- return FALSE
-
- target_room.room_description = new_room_desc
- return TRUE
-
- if("toggle_joinable_room")
- target_room.joinable = !target_room.joinable
- return TRUE
-
- if("toggle_joinable")
- ghost_joinable = !ghost_joinable
- return TRUE
-
- if("toggle_approval")
- require_approval = !require_approval
- return TRUE
-
- if("modify_name")
- var/new_name = tgui_input_text(usr,"Choose a new name to send messages as", name, target_room.outside_voice, multiline = TRUE)
- if(!new_name)
- return FALSE
-
- target_room.outside_voice = new_name
- return TRUE
-
- if("remove_soul")
- target_room.remove_soul(target_soul)
- return TRUE
-
- if("transfer_soul")
- var/list/available_rooms = soulcatcher_rooms.Copy()
- available_rooms -= target_room
-
- if(ishuman(usr))
- var/mob/living/carbon/human/human_user = usr
- var/datum/nifsoft/soulcatcher/soulcatcher_nifsoft = human_user.find_nifsoft(/datum/nifsoft/soulcatcher)
- if(soulcatcher_nifsoft && (parent != soulcatcher_nifsoft.parent_nif.resolve()))
- var/datum/component/soulcatcher/nifsoft_soulcatcher = soulcatcher_nifsoft.linked_soulcatcher.resolve()
- if(istype(nifsoft_soulcatcher))
- available_rooms.Add(nifsoft_soulcatcher.soulcatcher_rooms)
-
- for(var/obj/item/held_item in human_user.held_items)
- if(parent == held_item)
- continue
-
- var/datum/component/soulcatcher/soulcatcher_component = held_item.GetComponent(/datum/component/soulcatcher)
- if(!soulcatcher_component || !soulcatcher_component.check_for_vacancy())
- continue
-
- for(var/datum/soulcatcher_room/room in soulcatcher_component.soulcatcher_rooms)
- available_rooms += room
-
- var/datum/soulcatcher_room/transfer_room = tgui_input_list(usr, "Choose a room to transfer to", name, available_rooms)
- if(!(transfer_room in available_rooms))
- return FALSE
-
- target_room.transfer_soul(target_soul, transfer_room)
- return TRUE
-
- if("change_room_color")
- var/new_room_color = input(usr, "", "Choose Color", SOULCATCHER_DEFAULT_COLOR) as color
- if(!new_room_color)
- return FALSE
-
- target_room.room_color = new_room_color
-
- if("toggle_soul_outside_sense")
- if(params["sense_to_change"] == "hearing")
- target_soul.toggle_hearing()
- else
- target_soul.toggle_sight()
-
- return TRUE
-
- if("toggle_soul_sense")
- if(params["sense_to_change"] == "hearing")
- target_soul.internal_hearing = !target_soul.internal_hearing
- else
- target_soul.internal_sight = !target_soul.internal_sight
-
- return TRUE
-
- if("toggle_soul_communication")
- if(params["communication_type"] == "emote")
- target_soul.able_to_emote = !target_soul.able_to_emote
- else
- target_soul.able_to_speak = !target_soul.able_to_speak
-
- return TRUE
-
- if("toggle_soul_external_communication")
- if(params["communication_type"] == "emote")
- target_soul.able_to_emote_as_container = !target_soul.able_to_emote_as_container
- else
- target_soul.able_to_speak_as_container = !target_soul.able_to_speak_as_container
-
- return TRUE
-
- if("toggle_soul_renaming")
- target_soul.able_to_rename = !target_soul.able_to_rename
- return TRUE
-
- if("change_name")
- var/new_name = tgui_input_text(usr, "Enter a new name for [target_soul]", "Soulcatcher", target_soul)
- if(!new_name)
- return FALSE
-
- target_soul.change_name(new_name)
- return TRUE
-
- if("reset_name")
- if(tgui_alert(usr, "Do you wish to reset [target_soul]'s name to default?", "Soulcatcher", list("Yes", "No")) != "Yes")
- return FALSE
-
- target_soul.reset_name()
-
- if("send_message")
- var/message_to_send = ""
- var/emote = params["emote"]
- var/message_sender = target_room.outside_voice
- if(params["narration"])
- message_sender = FALSE
-
- message_to_send = tgui_input_text(usr, "Input the message you want to send", name, multiline = TRUE)
-
- if(!message_to_send)
- return FALSE
-
- target_room.send_message(message_to_send, message_sender, emote)
- return TRUE
-
- if("delete_self")
- if(tgui_alert(usr, "Are you sure you want to detach the soulcatcher?", parent, list("Yes", "No")) != "Yes")
- return FALSE
-
- remove_self()
- return TRUE
-
-/datum/component/soulcatcher_user/New()
- . = ..()
- var/mob/living/soulcatcher_soul/parent_soul = parent
- if(!istype(parent_soul))
- return COMPONENT_INCOMPATIBLE
-
- return TRUE
-
-/datum/component/soulcatcher_user/ui_interact(mob/user, datum/tgui/ui)
- ui = SStgui.try_update_ui(usr, src, ui)
- if(!ui)
- ui = new(usr, src, "SoulcatcherUser")
- ui.open()
-
-/datum/component/soulcatcher_user/ui_state(mob/user)
- return GLOB.conscious_state
-
-/datum/component/soulcatcher_user/ui_data(mob/user)
- var/list/data = list()
-
- var/mob/living/soulcatcher_soul/user_soul = parent
- if(!istype(user_soul))
- return FALSE //uhoh
-
- data["user_data"] = list(
- "name" = user_soul.name,
- "description" = user_soul.soul_desc,
- "reference" = REF(user_soul),
- "internal_hearing" = user_soul.internal_hearing,
- "internal_sight" = user_soul.internal_sight,
- "outside_hearing" = user_soul.outside_hearing,
- "outside_sight" = user_soul.outside_sight,
- "able_to_emote" = user_soul.able_to_emote,
- "able_to_speak" = user_soul.able_to_speak,
- "able_to_rename" = user_soul.able_to_rename,
- "able_to_speak_as_container" = user_soul.able_to_speak_as_container,
- "able_to_emote_as_container" = user_soul.able_to_emote_as_container,
- "communicating_externally" = user_soul.communicating_externally,
- "ooc_notes" = user_soul.ooc_notes,
- "scan_needed" = user_soul.body_scan_needed,
- )
-
- var/datum/soulcatcher_room/current_room = user_soul.current_room.resolve()
- data["current_room"] = list(
- "name" = html_decode(current_room.name),
- "description" = html_decode(current_room.room_description),
- "reference" = REF(current_room),
- "color" = current_room.room_color,
- "owner" = current_room.outside_voice,
- )
-
- var/datum/component/soulcatcher/master_soulcatcher = current_room.master_soulcatcher.resolve()
- data["communicate_as_parent"] = master_soulcatcher.communicate_as_parent
-
- for(var/mob/living/soulcatcher_soul/soul in current_room.current_souls)
- if(soul == user_soul)
- continue
-
- var/list/soul_list = list(
- "name" = soul.name,
- "description" = soul.soul_desc,
- "ooc_notes" = soul.ooc_notes,
- "reference" = REF(soul),
- )
- data["souls"] += list(soul_list)
-
- return data
-
-/datum/component/soulcatcher_user/ui_act(action, list/params)
- . = ..()
- if(.)
- return
-
- var/mob/living/soulcatcher_soul/user_soul = parent
- if(!istype(user_soul))
- return FALSE
-
- switch(action)
- if("change_name")
- var/new_name = tgui_input_text(usr, "Enter a new name", "Soulcatcher", user_soul.name)
- if(!new_name)
- return FALSE
-
- user_soul.change_name(new_name)
- return TRUE
-
- if("reset_name")
- if(tgui_alert(usr, "Do you wish to reset your name to default?", "Soulcatcher", list("Yes", "No")) != "Yes")
- return FALSE
-
- user_soul.reset_name()
-
- if("toggle_external_communication")
- user_soul.communicating_externally = !user_soul.communicating_externally
- return TRUE
diff --git a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_verbs.dm b/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_verbs.dm
deleted file mode 100644
index b251a03a3770d3..00000000000000
--- a/modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_verbs.dm
+++ /dev/null
@@ -1,58 +0,0 @@
-/// Prompts the parent mob to send a say message to the soulcatcher. Returns False if no soulcatcher or message could be found.
-/mob/living/proc/soulcatcher_say()
- set name = "Soul Say"
- set category = "IC"
- set desc = "Send a Say message to your currently targeted soulcatcher room."
- var/datum/component/soulcatcher/target_soulcatcher = find_soulcatcher()
- if(!target_soulcatcher || !target_soulcatcher.targeted_soulcatcher_room)
- return FALSE
-
- var/message_to_send = tgui_input_text(usr, "Input the message you want to send", "Soulcatcher", multiline = TRUE)
- if(!message_to_send)
- return FALSE
-
- target_soulcatcher.targeted_soulcatcher_room.send_message(message_to_send, target_soulcatcher.targeted_soulcatcher_room.outside_voice)
- return TRUE
-
-/// Prompts the parent mob to send a emote to the soulcatcher. Returns False if no soulcatcher or emote could be found.
-/mob/living/proc/soulcatcher_emote()
- set name = "Soul Me"
- set category = "IC"
- set desc = "Send a emote to your currently targeted soulcatcher room."
- var/datum/component/soulcatcher/target_soulcatcher = find_soulcatcher()
- if(!target_soulcatcher || !target_soulcatcher.targeted_soulcatcher_room)
- return FALSE
-
- var/message_to_send = tgui_input_text(usr, "Input the emote you want to send", "Soulcatcher", multiline = TRUE)
- if(!message_to_send)
- return FALSE
-
- target_soulcatcher.targeted_soulcatcher_room.send_message(message_to_send, target_soulcatcher.targeted_soulcatcher_room.outside_voice, TRUE)
- return TRUE
-
-/// Attempts to find and return the soulcatcher the parent mob is currently using. If none can be found, returns `FALSE`
-/mob/living/proc/find_soulcatcher()
- var/obj/item/soulcatcher_holder/soul_holder = locate(/obj/item/soulcatcher_holder) in contents
- if(!soul_holder)
- return FALSE
-
- var/datum/component/soulcatcher/target_soulcatcher = soul_holder.GetComponent(/datum/component/soulcatcher)
- if(!target_soulcatcher)
- return FALSE
-
- return target_soulcatcher
-
-/mob/living/carbon/human/find_soulcatcher()
- . = ..()
- if(.) // No need to go searching further if we've already found one.
- return .
-
- var/datum/nifsoft/soulcatcher/souclatcher_nifsoft = find_nifsoft(/datum/nifsoft/soulcatcher)
- if(!souclatcher_nifsoft)
- return FALSE
-
- var/datum/component/soulcatcher/target_soulcatcher = souclatcher_nifsoft.linked_soulcatcher.resolve()
- if(!target_soulcatcher)
- return FALSE
-
- return target_soulcatcher
diff --git a/modular_skyrat/modules/soulstone_changes/readme.md b/modular_skyrat/modules/soulstone_changes/readme.md
index 0185eb05827f68..5f3e05acf5eaf0 100644
--- a/modular_skyrat/modules/soulstone_changes/readme.md
+++ b/modular_skyrat/modules/soulstone_changes/readme.md
@@ -13,7 +13,7 @@ Makes soulstone no longer permakill people, and makes construct's souls return t
### TG Proc/File Changes
-- code/modules/antagonist/wizard/equipment/soulstone.dm > /obj/item/soulstone/proc/transfer_soul()
+- code/modules/antagonist/wizard/equipment/soulstone.dm > /obj/item/soulstone/proc/transfer_mob()
- code/modules/antagonist/wizard/equipment/soulstone.dm > /obj/item/soulstone/proc/init_shade()
- code/modules/antagonist/wizard/equipment/soulstone.dm > /obj/item/soulstone/proc/getCultGhost()
diff --git a/tgstation.dme b/tgstation.dme
index fb091b404151e0..aae8926b5764da 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -6812,6 +6812,10 @@
#include "modular_skyrat\modules\cargo\code\items\gbp_punchcard.dm"
#include "modular_skyrat\modules\cargo\code\items\improvedRCD.dm"
#include "modular_skyrat\modules\cargo_teleporter\code\cargo_teleporter.dm"
+#include "modular_skyrat\modules\carriers\code\carrier_component.dm"
+#include "modular_skyrat\modules\carriers\code\carrier_tgui.dm"
+#include "modular_skyrat\modules\carriers\code\carrier_user_component.dm"
+#include "modular_skyrat\modules\carriers\code\carrier_verbs.dm"
#include "modular_skyrat\modules\cell_component\code\cell_component.dm"
#include "modular_skyrat\modules\cellguns\code\cellgun_cells.dm"
#include "modular_skyrat\modules\cellguns\code\cellguns.dm"
@@ -7568,12 +7572,11 @@
#include "modular_skyrat\modules\modular_implants\code\nifsofts\soulcatcher.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\base_types\action_granter.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\attachable_soulcatcher.dm"
+#include "modular_skyrat\modules\modular_implants\code\soulcatcher\ghost.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\handheld_soulcatcher.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_body_component.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_component.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_mob.dm"
-#include "modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_tgui.dm"
-#include "modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_verbs.dm"
#include "modular_skyrat\modules\modular_items\code\bags.dm"
#include "modular_skyrat\modules\modular_items\code\cash.dm"
#include "modular_skyrat\modules\modular_items\code\ciggies.dm"
diff --git a/tgui/packages/tgui/interfaces/Soulcatcher.jsx b/tgui/packages/tgui/interfaces/Soulcatcher.jsx
index f797d5b2225136..30706467ced1ab 100644
--- a/tgui/packages/tgui/interfaces/Soulcatcher.jsx
+++ b/tgui/packages/tgui/interfaces/Soulcatcher.jsx
@@ -19,11 +19,12 @@ export const Soulcatcher = (props) => {
require_approval,
current_rooms = [],
ghost_joinable,
- current_soul_count,
- max_souls,
+ current_mob_count,
+ max_mobs,
removable,
communicate_as_parent,
theme,
+ carrier_targeted,
} = data;
return (
@@ -156,23 +157,23 @@ export const Soulcatcher = (props) => {
- {room.souls.map((soul) => (
-
+ {room.souls.map((mob) => (
+
- {soul.scan_needed ? (
+ {mob.scan_needed ? (
<> >
) : (
<>