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 ( + + + + + + + + + +