diff --git a/code/datums/components/ling_decoy_brain.dm b/code/datums/components/ling_decoy_brain.dm
new file mode 100644
index 00000000000..7bcb4e38c8f
--- /dev/null
+++ b/code/datums/components/ling_decoy_brain.dm
@@ -0,0 +1,68 @@
+/// Component applied to ling brains to make them into decoy brains, as ling brains are vestigial and don't do anything
+/datum/component/ling_decoy_brain
+ /// The ling this brain is linked to
+ VAR_FINAL/datum/antagonist/changeling/parent_ling
+ /// A talk action that is granted to the ling when this decoy enters an MMI
+ VAR_FINAL/datum/action/changeling/mmi_talk/talk_action
+
+/datum/component/ling_decoy_brain/Initialize(datum/antagonist/changeling/ling)
+ if(!istype(parent, /obj/item/organ/internal/brain))
+ return COMPONENT_INCOMPATIBLE
+ if(isnull(ling))
+ stack_trace("[type] instantiated without a changeling to link to.")
+ return COMPONENT_INCOMPATIBLE
+
+ parent_ling = ling
+ RegisterSignal(parent_ling, COMSIG_QDELETING, PROC_REF(clear_decoy))
+
+/datum/component/ling_decoy_brain/Destroy()
+ UnregisterSignal(parent_ling, COMSIG_QDELETING)
+ parent_ling = null
+ QDEL_NULL(talk_action)
+ return ..()
+
+/datum/component/ling_decoy_brain/RegisterWithParent()
+ var/obj/item/organ/internal/brain/ling_brain = parent
+ ling_brain.organ_flags &= ~ORGAN_VITAL
+ ling_brain.decoy_override = TRUE
+ RegisterSignal(ling_brain, COMSIG_ATOM_ENTERING, PROC_REF(entered_mmi))
+
+/datum/component/ling_decoy_brain/UnregisterFromParent()
+ var/obj/item/organ/internal/brain/ling_brain = parent
+ ling_brain.organ_flags |= ORGAN_VITAL
+ ling_brain.decoy_override = FALSE
+ UnregisterSignal(ling_brain, COMSIG_ATOM_ENTERING, PROC_REF(entered_mmi))
+
+/**
+ * Signal proc for [COMSIG_ATOM_ENTERING], when the brain enters an MMI grant the MMI talk action to the ling
+ *
+ * Unfortunately this is hooked on Entering rather than its own dedicated MMI signal becuase MMI code is a fuck
+ */
+/datum/component/ling_decoy_brain/proc/entered_mmi(obj/item/organ/internal/brain/source, atom/entering, atom/old_loc, ...)
+ SIGNAL_HANDLER
+
+ var/mob/living/the_real_ling = parent_ling.owner.current
+ if(!istype(the_real_ling))
+ return
+
+ if(istype(source.loc, /obj/item/mmi) && talk_action?.owner != the_real_ling)
+ if(isnull(talk_action))
+ talk_action = new() // Not linked to anything, we manage the reference (and don't want it disappearing on us)
+ talk_action.brain_ref = source
+
+ if(the_real_ling.key)
+ to_chat(the_real_ling, span_ghostalert("We detect our decoy brain has been placed within a Man-Machine Interface. \
+ We can use the \"MMI Talk\" action to command it to speak."))
+ else
+ the_real_ling.notify_ghost_cloning("Your decoy brain has been placed in an MMI, re-enter your body to talk via it!", source = the_real_ling, flashwindow = TRUE)
+ talk_action.Grant(the_real_ling)
+
+ else if(talk_action?.owner == the_real_ling)
+ to_chat(the_real_ling, span_ghostalert("We can no longer detect our decoy brain."))
+ talk_action.Remove(the_real_ling)
+
+/// Clear up the decoy if the ling is de-linged
+/datum/component/ling_decoy_brain/proc/clear_decoy(datum/source)
+ SIGNAL_HANDLER
+
+ qdel(src)
diff --git a/code/modules/antagonists/changeling/changeling.dm b/code/modules/antagonists/changeling/changeling.dm
index a42eebcff7f..5c44acd5fc0 100644
--- a/code/modules/antagonists/changeling/changeling.dm
+++ b/code/modules/antagonists/changeling/changeling.dm
@@ -178,11 +178,16 @@
else
RegisterSignal(living_mob, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created))
+ make_brain_decoy(living_mob)
+
+/datum/antagonist/changeling/proc/make_brain_decoy(mob/living/ling)
+ var/obj/item/organ/internal/brain/our_ling_brain = ling.get_organ_slot(ORGAN_SLOT_BRAIN)
+ if(isnull(our_ling_brain) || our_ling_brain.decoy_override)
+ return
+
// Brains are optional for lings.
- var/obj/item/organ/internal/brain/our_ling_brain = living_mob.get_organ_slot(ORGAN_SLOT_BRAIN)
- if(our_ling_brain)
- our_ling_brain.organ_flags &= ~ORGAN_VITAL
- our_ling_brain.decoy_override = TRUE
+ // This is automatically cleared if the ling is.
+ our_ling_brain.AddComponent(/datum/component/ling_decoy_brain, src)
/datum/antagonist/changeling/proc/generate_name()
var/honorific
@@ -226,15 +231,10 @@
QDEL_NULL(lingchemdisplay)
QDEL_NULL(lingstingdisplay)
+ // The old body's brain still remains a decoy, I guess?
+
/datum/antagonist/changeling/on_removal()
remove_changeling_powers(include_innate = TRUE)
- if(!iscarbon(owner.current))
- return
- var/mob/living/carbon/carbon_owner = owner.current
- var/obj/item/organ/internal/brain/not_ling_brain = carbon_owner.get_organ_slot(ORGAN_SLOT_BRAIN)
- if(not_ling_brain && (not_ling_brain.decoy_override != initial(not_ling_brain.decoy_override)))
- not_ling_brain.organ_flags |= ORGAN_VITAL
- not_ling_brain.decoy_override = FALSE
return ..()
/datum/antagonist/changeling/farewell()
@@ -294,14 +294,18 @@
adjust_chemicals((chem_recharge_rate - chem_recharge_slowdown) * delta_time)
/**
- * Signal proc for [COMSIG_LIVING_POST_FULLY_HEAL], getting admin-healed restores our chemicals.
+ * Signal proc for [COMSIG_LIVING_POST_FULLY_HEAL]
*/
-/datum/antagonist/changeling/proc/on_fullhealed(datum/source, heal_flags)
+/datum/antagonist/changeling/proc/on_fullhealed(mob/living/source, heal_flags)
SIGNAL_HANDLER
+ // Aheal restores all chemicals
if(heal_flags & HEAL_ADMIN)
adjust_chemicals(INFINITY)
+ // Makes sure the brain, if recreated, is a decoy as expected
+ make_brain_decoy(source)
+
/**
* Signal proc for [COMSIG_MOB_MIDDLECLICKON] and [COMSIG_MOB_ALTCLICKON].
* Allows the changeling to sting people with a click.
diff --git a/code/modules/antagonists/changeling/powers/mmi_talk.dm b/code/modules/antagonists/changeling/powers/mmi_talk.dm
new file mode 100644
index 00000000000..f68968c223e
--- /dev/null
+++ b/code/modules/antagonists/changeling/powers/mmi_talk.dm
@@ -0,0 +1,140 @@
+/datum/action/changeling/mmi_talk
+ name = "MMI Talk"
+ desc = "Our decoy brain has been implanted into a Man-Machine Interface. \
+ In order to maintain our secrecy, we can speak through the decoy as if a normal brain. \
+ The decoy brain will relay speech it hears to you in purple."
+ button_icon = 'icons/obj/assemblies/assemblies.dmi'
+ button_icon_state = "mmi_off"
+ dna_cost = CHANGELING_POWER_UNOBTAINABLE
+ ignores_fakedeath = TRUE // Can be used while fake dead
+ req_stat = DEAD // Can be used while real dead too
+
+ /**
+ * Reference to the brain we're talking through.
+ *
+ * Set when created via the ling decoy component.
+ * If the brain ends up being qdelled, this action will also be qdelled, and thus this ref is cleared.
+ */
+ VAR_FINAL/obj/item/organ/internal/brain/brain_ref
+
+ /// A map view of the area around the MMI.
+ VAR_FINAL/atom/movable/screen/map_view/mmi_view
+ /// The background for the MMI map view.
+ VAR_FINAL/atom/movable/screen/background/mmi_view_background
+ /// The key that the map view uses.
+ VAR_FINAL/mmi_view_key
+ /// A movement detector that updates the map view when the MMI moves around.
+ VAR_FINAL/datum/movement_detector/update_view_tracker
+
+/datum/action/changeling/mmi_talk/Destroy()
+ brain_ref = null
+ QDEL_NULL(mmi_view)
+ QDEL_NULL(mmi_view_background)
+ QDEL_NULL(update_view_tracker)
+ return ..()
+
+/datum/action/changeling/mmi_talk/Remove(mob/remove_from)
+ . = ..()
+ SStgui.close_uis(src)
+
+/datum/action/changeling/mmi_talk/can_sting(mob/living/user, mob/living/target)
+ . = ..()
+ if(!.)
+ return FALSE
+ // This generally shouldn't happen, but just in case
+ if(isnull(brain_ref))
+ stack_trace("[type] can_sting was called with a null brain!")
+ return FALSE
+ if(!istype(brain_ref.loc, /obj/item/mmi))
+ stack_trace("[type] can_sting was called with a brain not located in an MMI!")
+ return FALSE
+ return TRUE
+
+/datum/action/changeling/mmi_talk/sting_action(mob/living/user, mob/living/target)
+ ..()
+ ui_interact(user)
+ return TRUE
+
+/datum/action/changeling/mmi_talk/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/action/changeling/mmi_talk/ui_status(mob/user, datum/ui_state/state)
+ if(user != owner)
+ return UI_CLOSE
+ return ..()
+
+/datum/action/changeling/mmi_talk/ui_static_data(mob/user)
+ var/list/data = list()
+ data["mmi_view"] = mmi_view_key
+ return data
+
+/datum/action/changeling/mmi_talk/ui_interact(mob/user, datum/tgui/ui)
+ if(isnull(mmi_view_key))
+ // it's worth noting a ling could have multiple of these actions.
+ mmi_view_key = "ling_mmi_[REF(src)]_view"
+ // Generate background
+ mmi_view_background = new()
+ mmi_view_background.assigned_map = mmi_view_key
+ mmi_view_background.del_on_map_removal = FALSE
+ mmi_view_background.fill_rect(1, 1, 5, 5)
+ // Generate map view
+ mmi_view = new()
+ mmi_view.generate_view(mmi_view_key)
+ // Generate movement detector (to update the view on MMI movement)
+ update_view_tracker = new(brain_ref, CALLBACK(src, PROC_REF(update_mmi_view)))
+
+ // Shows the view to the user foremost
+ mmi_view.display_to(user)
+ user.client.register_map_obj(mmi_view_background)
+ update_mmi_view()
+ // Makes the MMI relay heard messages
+ if(!HAS_TRAIT_FROM(brain_ref.loc, TRAIT_HEARING_SENSITIVE, REF(src)))
+ var/obj/item/mmi/mmi = brain_ref.loc
+ mmi.become_hearing_sensitive(REF(src))
+ RegisterSignal(mmi, COMSIG_MOVABLE_HEAR, PROC_REF(relay_hearing))
+ // Actually open the UI
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "LingMMITalk")
+ ui.open()
+
+/datum/action/changeling/mmi_talk/ui_close(mob/user)
+ var/obj/item/mmi/mmi = brain_ref.loc
+ UnregisterSignal(mmi, COMSIG_MOVABLE_HEAR)
+ mmi.lose_hearing_sensitivity(REF(src))
+
+/datum/action/changeling/mmi_talk/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return TRUE
+
+ if(action != "send_mmi_message")
+ return FALSE
+
+ var/obj/item/mmi/mmi = brain_ref.loc
+ if(mmi.brainmob.stat != CONSCIOUS)
+ to_chat(usr, span_warning("Our decoy brain is too damaged to speak."))
+ else
+ // Say will perform input sanitization and such for us
+ mmi.brainmob.say(params["message"], sanitize = TRUE)
+ return TRUE
+
+/// Used in callbacks to update the map view when the MMI moves.
+/datum/action/changeling/mmi_talk/proc/update_mmi_view()
+ mmi_view.vis_contents.Cut()
+ for(var/turf/visible_turf in view(2, get_turf(brain_ref)))
+ mmi_view.vis_contents += visible_turf
+
+/// Signal proc for [COMSIG_MOVABLE_HEAR] to relay stuff the MMI hears to the ling.
+/// Not super good, but it works.
+/datum/action/changeling/mmi_talk/proc/relay_hearing(obj/item/mmi/source, list/hear_args)
+ SIGNAL_HANDLER
+
+ // We can likely already hear them, so do not bother
+ if(can_see(owner, hear_args[HEARING_SPEAKER], 7))
+ return
+
+ var/list/new_args = hear_args.Copy()
+ new_args[HEARING_SPANS] |= "purple"
+ new_args[HEARING_RANGE] = INFINITY // so we can hear it from any distance away
+ owner.Hear(arglist(new_args))
diff --git a/code/modules/antagonists/changeling/powers/regenerate.dm b/code/modules/antagonists/changeling/powers/regenerate.dm
index 6595e331195..a83adade98d 100644
--- a/code/modules/antagonists/changeling/powers/regenerate.dm
+++ b/code/modules/antagonists/changeling/powers/regenerate.dm
@@ -15,22 +15,16 @@
..()
to_chat(user, span_notice("You feel an itching, both inside and outside as your tissues knit and reknit."))
var/mob/living/carbon/carbon_user = user
- if(length(carbon_user.get_missing_limbs()))
+ var/got_limbs_back = length(carbon_user.get_missing_limbs()) >= 1
+ carbon_user.fully_heal(HEAL_BODY)
+ // Occurs after fully heal so the ling themselves can hear the sound effects (if deaf prior)
+ if(got_limbs_back)
playsound(user, 'sound/magic/demon_consume.ogg', 50, TRUE)
carbon_user.visible_message(
span_warning("[user]'s missing limbs reform, making a loud, grotesque sound!"),
span_userdanger("Your limbs regrow, making a loud, crunchy sound and giving you great pain!"),
span_hear("You hear organic matter ripping and tearing!"),
)
-
carbon_user.emote("scream")
- carbon_user.fully_heal(HEAL_BODY)
-
- // Make sure the brain's nonvital
- // Shouldn't be necessary but you can never be certain with lingcode
- var/obj/item/organ/internal/brain/replacement_brain = user.get_organ_slot(ORGAN_SLOT_BRAIN)
- replacement_brain.organ_flags &= ~ORGAN_VITAL
- replacement_brain.decoy_override = TRUE
-
return TRUE
diff --git a/code/modules/mob/living/brain/MMI.dm b/code/modules/mob/living/brain/MMI.dm
index 3dc3d08d399..e182c51f54f 100644
--- a/code/modules/mob/living/brain/MMI.dm
+++ b/code/modules/mob/living/brain/MMI.dm
@@ -57,7 +57,7 @@
if(newbrain.suicided)
to_chat(user, span_warning("[newbrain] is completely useless."))
return
- if(!newbrain.brainmob?.mind || !newbrain.brainmob)
+ if(!newbrain.brainmob)
var/install = tgui_alert(user, "[newbrain] is inactive, slot it in anyway?", "Installing Brain", list("Yes", "No"))
if(install != "Yes")
return
@@ -73,7 +73,7 @@
if(!user.transferItemToLoc(O, src))
return
var/mob/living/brain/B = newbrain.brainmob
- if(!B.key)
+ if(!B.key && !newbrain.decoy_override)
B.notify_ghost_cloning("Someone has put your brain in a MMI!", source = src)
user.visible_message(span_notice("[user] sticks \a [newbrain] into [src]."), span_notice("[src]'s indicator light turn on as you insert [newbrain]."))
@@ -241,14 +241,23 @@
. = ..()
if(radio)
. += span_notice("There is a switch to toggle the radio system [radio.is_on() ? "off" : "on"].[brain ? " It is currently being covered by [brain]." : null]")
- if(brainmob)
- var/mob/living/brain/B = brainmob
- if(!B.key || !B.mind || B.stat == DEAD)
- . += span_warning("\The [src] indicates that the brain is completely unresponsive.")
- else if(!B.client)
- . += span_warning("\The [src] indicates that the brain is currently inactive; it might change.")
+
+ if(!isnull(brain))
+ // It's dead, show it as much
+ if((brain.organ_flags & ORGAN_FAILING) || brainmob?.stat == DEAD)
+ if(brain.suicided || (brainmob && HAS_TRAIT(brainmob, TRAIT_SUICIDED)))
+ . += span_warning("[src] indicator light is red.")
+ else
+ . += span_warning("[src] indicator light is yellow - perhaps you should check the brain for damage.")
+ // If we have a client, OR it's a decoy brain, show as active
+ else if(brain.decoy_override || brainmob?.client)
+ . += span_notice("[src] indicates that the brain is active.")
+ // If we have a brainmob and it has a mind, it may just be DC'd
+ else if(brainmob?.mind)
+ . += span_warning("[src] indicates that the brain is currently inactive; it might change.")
+ // No brainmob, no mind, and not a decoy, it's a dead brain
else
- . += span_notice("\The [src] indicates that the brain is active.")
+ . += span_warning("[src] indicates that the brain is completely unresponsive.")
/obj/item/mmi/relaymove(mob/living/user, direction)
return //so that the MMI won't get a warning about not being able to move if it tries to move
@@ -259,6 +268,10 @@
if(user)
to_chat(user, span_warning("\The [src] indicates that there is no mind present!"))
return FALSE
+ if(brain.decoy_override)
+ if(user)
+ to_chat(user, span_warning("This [name] does not seem to fit!"))
+ return FALSE
if(!B.key || !B.mind)
if(user)
to_chat(user, span_warning("\The [src] indicates that their mind is completely unresponsive!"))
diff --git a/code/modules/mob/living/brain/brain_item.dm b/code/modules/mob/living/brain/brain_item.dm
index ac8f503df45..4bd275b5df1 100644
--- a/code/modules/mob/living/brain/brain_item.dm
+++ b/code/modules/mob/living/brain/brain_item.dm
@@ -54,17 +54,23 @@
brain_owner.update_body_parts()
return
- // Not a ling? Now you get to assume direct control.
if(brainmob)
- if(brain_owner.key)
- brain_owner.ghostize()
+ // If it's a ling decoy brain, nothing to transfer, just throw it out
+ if(decoy_override)
+ if(brainmob?.key)
+ stack_trace("Decoy override brain with a key assigned - This should never happen.")
- if(brainmob.mind)
- brainmob.mind.transfer_to(brain_owner)
+ // Not a ling - assume direct control
else
- brain_owner.key = brainmob.key
+ if(brain_owner.key)
+ brain_owner.ghostize()
- brain_owner.set_suicide(HAS_TRAIT(brainmob, TRAIT_SUICIDED))
+ if(brainmob.mind)
+ brainmob.mind.transfer_to(brain_owner)
+ else
+ brain_owner.key = brainmob.key
+
+ brain_owner.set_suicide(HAS_TRAIT(brainmob, TRAIT_SUICIDED))
QDEL_NULL(brainmob)
else
@@ -126,8 +132,13 @@
/obj/item/organ/internal/brain/proc/transfer_identity(mob/living/L)
name = "[L.name]'s [initial(name)]"
- if(brainmob || decoy_override)
- return
+ if(brainmob)
+ if(!decoy_override)
+ return
+
+ // it's just a dummy, throw it out
+ QDEL_NULL(brainmob)
+
if(!L.mind)
return
brainmob = new(src)
@@ -146,9 +157,10 @@
// Hack, fucked dna needs to follow the brain to prevent memes, so we need to copy over the trait sources and shit
for(var/source in GET_TRAIT_SOURCES(L, TRAIT_BADDNA))
ADD_TRAIT(brainmob, TRAIT_BADDNA, source)
- if(L.mind && L.mind.current)
+
+ if(L.mind && L.mind.current && !decoy_override)
L.mind.transfer_to(brainmob)
- to_chat(brainmob, span_notice("You feel slightly disoriented. That's normal when you're just a brain."))
+ to_chat(brainmob, span_notice("You feel slightly disoriented. That's normal when you're just a brain."))
/obj/item/organ/internal/brain/attackby(obj/item/O, mob/user, params)
user.changeNext_move(CLICK_CD_MELEE)
@@ -214,7 +226,7 @@
if(suicided)
. += span_info("It's started turning slightly grey. They must not have been able to handle the stress of it all.")
return
- if((brainmob && (brainmob.client || brainmob.get_ghost())) || decoy_override)
+ if(brainmob && (decoy_override || brainmob.client || brainmob.get_ghost()))
if(organ_flags & ORGAN_FAILING)
. += span_info("It seems to still have a bit of energy within it, but it's rather damaged... You may be able to restore it with some mannitol.")
else if(damage >= BRAIN_DAMAGE_DEATH*0.5)
@@ -262,13 +274,11 @@
..()
/obj/item/organ/internal/brain/Destroy() //copypasted from MMIs.
- if(brainmob)
- QDEL_NULL(brainmob)
+ QDEL_NULL(brainmob)
QDEL_LIST(traumas)
destroy_all_skillchips()
- if(owner?.mind) //You aren't allowed to return to brains that don't exist
- owner.mind.set_current(null)
+ owner?.mind?.set_current(null) //You aren't allowed to return to brains that don't exist
return ..()
/obj/item/organ/internal/brain/on_life(seconds_per_tick, times_fired)
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index a89d4524170..20ca34d5259 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -161,6 +161,7 @@
#include "leash.dm"
#include "lesserform.dm"
#include "limbsanity.dm"
+#include "ling_decap.dm"
#include "liver.dm"
#include "load_map_security.dm"
#include "lungs.dm"
diff --git a/code/modules/unit_tests/ling_decap.dm b/code/modules/unit_tests/ling_decap.dm
new file mode 100644
index 00000000000..0c964f9a043
--- /dev/null
+++ b/code/modules/unit_tests/ling_decap.dm
@@ -0,0 +1,45 @@
+/// Test lings don't die when decapitated.
+/datum/unit_test/ling_decap
+
+/datum/unit_test/ling_decap/Run()
+ var/mob/living/carbon/human/ling = allocate(/mob/living/carbon/human/consistent)
+ ling.mind_initialize()
+ ling.mind.add_antag_datum(/datum/antagonist/changeling)
+
+ var/obj/item/bodypart/head/noggin = ling.get_bodypart(BODY_ZONE_HEAD)
+ noggin.dismember()
+ TEST_ASSERT_NULL(ling.get_bodypart(BODY_ZONE_HEAD), "Changeling failed to be decapitated.")
+ TEST_ASSERT_NULL(noggin.brainmob.mind, "Changeling's mind was moved to their head after decapitation, but it should have remained in their body.")
+
+ var/obj/item/organ/internal/brain/oldbrain = noggin.brain
+ noggin.drop_organs()
+ TEST_ASSERT_NULL(noggin.brain, "Changeling's head failed to drop its brain.")
+ TEST_ASSERT_NULL(oldbrain.brainmob.mind, "Changeling's mind was moved to their brain after decapitation and organ dropping, but it should have remained in their body.")
+
+ TEST_ASSERT_EQUAL(ling.stat, CONSCIOUS, "Changeling was not conscious after losing their head.")
+
+ // Cleanup
+ qdel(noggin)
+ for(var/obj/item/organ/leftover in ling.loc)
+ qdel(leftover)
+
+/// Tests people get decapitated properly.
+/datum/unit_test/normal_decap
+
+/datum/unit_test/normal_decap/Run()
+ var/mob/living/carbon/human/normal_guy = allocate(/mob/living/carbon/human/consistent)
+ normal_guy.mind_initialize()
+ var/my_guys_mind = normal_guy.mind
+
+ var/obj/item/bodypart/head/noggin = normal_guy.get_bodypart(BODY_ZONE_HEAD)
+ noggin.dismember()
+ TEST_ASSERT_EQUAL(noggin.brainmob.mind, my_guys_mind, "Dummy's mind was not moved to their head after decapitation.")
+
+ var/obj/item/organ/internal/brain/oldbrain = noggin.brain
+ noggin.drop_organs()
+ TEST_ASSERT_EQUAL(oldbrain.brainmob.mind, my_guys_mind, "Dummy's mind was not moved to their brain after being removed from their head.")
+
+ // Cleanup
+ qdel(noggin)
+ for(var/obj/item/organ/leftover in normal_guy.loc)
+ qdel(leftover)
diff --git a/tgstation.dme b/tgstation.dme
index 83b1a3a08e2..5c1c96e21f2 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1094,6 +1094,7 @@
#include "code\datums\components\label.dm"
#include "code\datums\components\leash.dm"
#include "code\datums\components\light_eater.dm"
+#include "code\datums\components\ling_decoy_brain.dm"
#include "code\datums\components\lock_on_cursor.dm"
#include "code\datums\components\magnet.dm"
#include "code\datums\components\manual_blinking.dm"
@@ -2887,6 +2888,7 @@
#include "code\modules\antagonists\changeling\powers\headcrab.dm"
#include "code\modules\antagonists\changeling\powers\lesserform.dm"
#include "code\modules\antagonists\changeling\powers\mimic_voice.dm"
+#include "code\modules\antagonists\changeling\powers\mmi_talk.dm"
#include "code\modules\antagonists\changeling\powers\mutations.dm"
#include "code\modules\antagonists\changeling\powers\panacea.dm"
#include "code\modules\antagonists\changeling\powers\pheromone_receptors.dm"
diff --git a/tgui/packages/tgui/interfaces/LingMMITalk.tsx b/tgui/packages/tgui/interfaces/LingMMITalk.tsx
new file mode 100644
index 00000000000..8fd6e5d6e72
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/LingMMITalk.tsx
@@ -0,0 +1,57 @@
+import { useBackend, useLocalState } from '../backend';
+import { Button, ByondUi, Stack, TextArea } from '../components';
+import { Window } from '../layouts';
+
+type Data = {
+ mmi_view: string;
+};
+
+export const LingMMITalk = (props, context) => {
+ const { data, act } = useBackend(context);
+ const [mmiMessage, setmmiMessage] = useLocalState(
+ context,
+ 'textArea',
+ ''
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};