diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 259bcf07500..306a00811d1 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -389,7 +389,8 @@
#define COMSIG_MOB_SWAP_HANDS "mob_swap_hands"
#define COMPONENT_BLOCK_SWAP (1<<0)
-
+#define COMSIG_MOB_AUTOMUTE_CHECK "automute_check"
+ #define WAIVE_AUTOMUTE_CHECK (1<<0)
// /mob/living signals
diff --git a/code/__DEFINES/misc_defines.dm b/code/__DEFINES/misc_defines.dm
index fd6abbb1f44..932dcead2f7 100644
--- a/code/__DEFINES/misc_defines.dm
+++ b/code/__DEFINES/misc_defines.dm
@@ -540,3 +540,12 @@
#define REFLECTABILITY_NEVER 0
#define REFLECTABILITY_PHYSICAL 1
#define REFLECTABILITY_ENERGY 2
+
+// Deadchat control defines
+
+/// Will execute a single command after the cooldown based on player votes.
+#define DEADCHAT_DEMOCRACY_MODE (1<<0)
+/// Allows each player to do a single command every cooldown.
+#define DEADCHAT_ANARCHY_MODE (1<<1)
+/// Mutes the democracy mode messages send to orbiters at the end of each cycle. Useful for when the cooldown is so low it'd get spammy.
+#define MUTE_DEADCHAT_DEMOCRACY_MESSAGES (1<<2)
diff --git a/code/datums/components/deadchat_control.dm b/code/datums/components/deadchat_control.dm
new file mode 100644
index 00000000000..8608af566d0
--- /dev/null
+++ b/code/datums/components/deadchat_control.dm
@@ -0,0 +1,251 @@
+
+/**
+ * Deadchat Plays Things - The Componenting
+ *
+ * Allows deadchat to control stuff and things by typing commands into chat.
+ * These commands will then trigger callbacks to execute procs!
+ */
+/datum/component/deadchat_control
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+
+ /// The id for the DEADCHAT_DEMOCRACY_MODE looping vote timer.
+ var/timerid
+ /// Assoc list of key-chat command string, value-callback pairs. list("right" = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(_step), src, EAST))
+ var/list/datum/callback/inputs = list()
+ /// Assoc list of ckey:value pairings. In DEADCHAT_DEMOCRACY_MODE, value is the player's vote. In DEADCHAT_ANARCHY_MODE, value is world.time when their cooldown expires.
+ var/list/ckey_to_cooldown = list()
+ /// List of everything orbitting this component's parent.
+ var/orbiters = list()
+ /// A bitfield containing the mode which this component uses (DEADCHAT_DEMOCRACY_MODE or DEADCHAT_ANARCHY_MODE) and other settings)
+ var/deadchat_mode = DEADCHAT_DEMOCRACY_MODE
+ /// In DEADCHAT_DEMOCRACY_MODE, this is how long players have to vote on an input. In DEADCHAT_ANARCHY_MODE, this is how long between inputs for each unique player.
+ var/input_cooldown
+ ///Set to true if a point of interest was created for an object, and needs to be removed if deadchat control is removed. Needed for preventing objects from having two points of interest.
+ var/generated_point_of_interest = FALSE
+ /// Callback invoked when this component is Destroy()ed to allow the parent to return to a non-deadchat controlled state.
+ var/datum/callback/on_removal
+
+/datum/component/deadchat_control/Initialize(_deadchat_mode, _inputs, _input_cooldown = 12 SECONDS, _on_removal)
+ if(!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+ RegisterSignal(parent, COMSIG_ATOM_ORBIT_BEGIN, PROC_REF(orbit_begin))
+ RegisterSignal(parent, COMSIG_ATOM_ORBIT_STOP, PROC_REF(orbit_stop))
+ RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine))
+ deadchat_mode = _deadchat_mode
+ inputs = _inputs
+ input_cooldown = _input_cooldown
+ on_removal = _on_removal
+ if(deadchat_mode & DEADCHAT_DEMOCRACY_MODE)
+ if(deadchat_mode & DEADCHAT_ANARCHY_MODE) // Choose one, please.
+ stack_trace("deadchat_control component added to [parent.type] with both democracy and anarchy modes enabled.")
+ timerid = addtimer(CALLBACK(src, PROC_REF(democracy_loop)), input_cooldown, TIMER_STOPPABLE | TIMER_LOOP)
+
+ var/list/input_names = list()
+ for(var/item in inputs)
+ input_names |= item
+ notify_ghosts("[parent] is now deadchat controllable! Possible commands are: [english_list(input_names)]", source = parent, action = NOTIFY_FOLLOW, title="Deadchat control!")
+ if(!ismob(parent) && !(parent in GLOB.poi_list))
+ GLOB.poi_list |= parent
+ generated_point_of_interest = TRUE
+ message_admins("[parent] has been given deadchat control in [deadchat_mode == DEADCHAT_ANARCHY_MODE ? "anarchy" : "democracy"] mode with a cooldown of [input_cooldown SECONDS] second\s.")
+
+/datum/component/deadchat_control/Destroy(force, silent)
+ var/message = "[parent] is no longer controllable."
+ for(var/mob/dead/observer/M in orbiters)
+ to_chat(M, message)
+ on_removal?.Invoke()
+ inputs = null
+ orbiters = null
+ ckey_to_cooldown = null
+ if(generated_point_of_interest)
+ GLOB.poi_list -= parent
+ return ..()
+
+/datum/component/deadchat_control/proc/deadchat_react(mob/source, message)
+ SIGNAL_HANDLER // COMSIG_MOB_DEADSAY
+
+ message = lowertext(message)
+
+ if(!inputs[message])
+ return
+
+ if(deadchat_mode & DEADCHAT_ANARCHY_MODE)
+ if(!source || !source.ckey)
+ return
+ var/cooldown = ckey_to_cooldown[source.ckey] - world.time
+ if(cooldown > 0)
+ to_chat(source, "Your deadchat control inputs are still on cooldown for another [CEILING(cooldown * 0.1, 1)] second\s.")
+ return MOB_DEADSAY_SIGNAL_INTERCEPT
+ ckey_to_cooldown[source.ckey] = world.time + input_cooldown
+ addtimer(CALLBACK(src, PROC_REF(end_cooldown), source.ckey), input_cooldown)
+ inputs[message].Invoke()
+ to_chat(source, "\"[message]\" input accepted. You are now on cooldown for [input_cooldown * 0.1] second\s.")
+ return MOB_DEADSAY_SIGNAL_INTERCEPT
+
+ if(deadchat_mode & DEADCHAT_DEMOCRACY_MODE)
+ ckey_to_cooldown[source.ckey] = message
+ to_chat(source, "You have voted for \"[message]\".")
+ return MOB_DEADSAY_SIGNAL_INTERCEPT
+
+/datum/component/deadchat_control/proc/democracy_loop()
+ if(QDELETED(parent) || !(deadchat_mode & DEADCHAT_DEMOCRACY_MODE))
+ deltimer(timerid)
+ return
+ var/result = count_democracy_votes()
+ if(!isnull(result))
+ inputs[result].Invoke()
+ if(!(deadchat_mode & MUTE_DEADCHAT_DEMOCRACY_MESSAGES))
+ var/message = "[parent] has done action [result]!
New vote started. It will end in [input_cooldown * 0.1] second\s."
+ for(var/mob/dead/observer/M in orbiters)
+ to_chat(M, message)
+ else if(!(deadchat_mode & MUTE_DEADCHAT_DEMOCRACY_MESSAGES))
+ var/message = "No votes were cast this cycle."
+ for(var/mob/dead/observer/M in orbiters)
+ to_chat(M, message)
+
+/datum/component/deadchat_control/proc/count_democracy_votes()
+ if(!length(ckey_to_cooldown))
+ return
+ var/list/votes = list()
+ for(var/command in inputs)
+ votes["[command]"] = 0
+ for(var/vote in ckey_to_cooldown)
+ votes[ckey_to_cooldown[vote]]++
+ ckey_to_cooldown.Remove(vote)
+
+ // Solve which had most votes.
+ var/prev_value = 0
+ var/result
+ for(var/vote in votes)
+ if(votes[vote] > prev_value)
+ prev_value = votes[vote]
+ result = vote
+
+ if(result in inputs)
+ return result
+
+/datum/component/deadchat_control/vv_edit_var(var_name, var_value)
+ . = ..()
+ if(!.)
+ return
+ if(var_name != NAMEOF(src, deadchat_mode))
+ return
+ ckey_to_cooldown = list()
+ if(var_value == DEADCHAT_DEMOCRACY_MODE)
+ timerid = addtimer(CALLBACK(src, PROC_REF(democracy_loop)), input_cooldown, TIMER_STOPPABLE | TIMER_LOOP)
+ else
+ deltimer(timerid)
+
+/datum/component/deadchat_control/proc/orbit_begin(atom/source, atom/orbiter)
+ SIGNAL_HANDLER // COMSIG_ATOM_ORBIT_BEGIN
+
+ if(isobserver(orbiter))
+ var/mob/dead/observer/O = orbiter
+ if(O.client && !(O.client.prefs.toggles & PREFTOGGLE_CHAT_DEAD))
+ to_chat(O, "You have deadchat muted, and as such will not receive messages related to, nor be able to participate in, controlling this object.")
+ to_chat(O, "If you would like to participate, unmute deadchat and follow this object again.")
+ return
+
+ RegisterSignal(orbiter, COMSIG_MOB_DEADSAY, PROC_REF(deadchat_react))
+ RegisterSignal(orbiter, COMSIG_MOB_AUTOMUTE_CHECK, PROC_REF(waive_automute))
+ orbiters |= orbiter
+
+
+/datum/component/deadchat_control/proc/orbit_stop(atom/source, atom/orbiter)
+ SIGNAL_HANDLER // COMSIG_ATOM_ORBIT_STOP
+
+ if(orbiter in orbiters)
+ UnregisterSignal(orbiter, list(
+ COMSIG_MOB_DEADSAY,
+ COMSIG_MOB_AUTOMUTE_CHECK,
+ ))
+ orbiters -= orbiter
+
+/**
+ * Prevents messages used to control the parent from counting towards the automute threshold for repeated identical messages.
+ *
+ * Arguments:
+ * - [speaker][/client]: The mob that is trying to speak.
+ * - [client][/client]: The client that is trying to speak.
+ * - message: The message that the speaker is trying to say.
+ * - mute_type: Which type of mute the message counts towards.
+ */
+/datum/component/deadchat_control/proc/waive_automute(mob/speaker, client/client, message, mute_type)
+ SIGNAL_HANDLER // COMSIG_MOB_AUTOMUTE_CHECK
+ if(mute_type == MUTE_DEADCHAT && inputs[lowertext(message)])
+ return WAIVE_AUTOMUTE_CHECK
+ return NONE
+
+
+/// Informs any examiners to the inputs available as part of deadchat control, as well as the current operating mode and cooldowns.
+/datum/component/deadchat_control/proc/on_examine(atom/A, mob/user, list/examine_list)
+ SIGNAL_HANDLER // COMSIG_PARENT_EXAMINE
+
+ if(!isobserver(user))
+ return
+
+ examine_list += "[A.p_theyre(TRUE)] currently under deadchat control using the [(deadchat_mode & DEADCHAT_DEMOCRACY_MODE) ? "democracy" : "anarchy"] ruleset!"
+
+ if(user.client && !(user.client.prefs.toggles & PREFTOGGLE_CHAT_DEAD))
+ examine_list += "As you have deadchat disabled, you will not see vote messages, nor be able to participate in voting."
+ return
+
+ if(deadchat_mode & DEADCHAT_DEMOCRACY_MODE)
+ examine_list += "Type a command into chat to vote on an action. This happens once every [input_cooldown * 0.1] second\s."
+ else if(deadchat_mode & DEADCHAT_ANARCHY_MODE)
+ examine_list += "Type a command into chat to perform. You may do this once every [input_cooldown * 0.1] second\s."
+
+ var/extended_examine = "Command list:"
+
+ for(var/possible_input in inputs)
+ extended_examine += " [possible_input]"
+
+ extended_examine += "."
+
+ examine_list += extended_examine
+
+/// Removes the ghost from the ckey_to_cooldown list and lets them know they are free to submit a command for the parent again.
+/datum/component/deadchat_control/proc/end_cooldown(ghost_ckey)
+ ckey_to_cooldown -= ghost_ckey
+ var/mob/ghost = get_mob_by_ckey(ghost_ckey)
+ if(!ghost || isliving(ghost))
+ return
+ to_chat(ghost, "Your deadchat control inputs for [parent] ([ghost_follow_link(parent, ghost)]) are no longer on cooldown.")
+
+/// Dummy to call since we can't proc reference builtins
+/datum/component/deadchat_control/proc/_step(ref, dir)
+ step(ref, dir)
+
+/**
+ * Deadchat Moves Things
+ *
+ * A special variant of the deadchat_control component that comes pre-baked with all the hottest inputs for a spicy
+ * singularity or vomit goose.
+ */
+/datum/component/deadchat_control/cardinal_movement/Initialize(_deadchat_mode, _inputs, _input_cooldown, _on_removal)
+ if(!ismovable(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ _inputs["up"] = CALLBACK(src, PROC_REF(_step), parent, NORTH)
+ _inputs["down"] = CALLBACK(src, PROC_REF(_step), parent, SOUTH)
+ _inputs["left"] = CALLBACK(src, PROC_REF(_step), parent, WEST)
+ _inputs["right"] = CALLBACK(src, PROC_REF(_step), parent, EAST)
+
+ return ..()
+
+/**
+ * Deadchat Moves Things
+ *
+ * A special variant of the deadchat_control component that comes pre-baked with all the hottest inputs for spicy
+ * immovable rod.
+ */
+/datum/component/deadchat_control/immovable_rod/Initialize(_deadchat_mode, _inputs, _input_cooldown, _on_removal)
+ if(!istype(parent, /obj/effect/immovablerod))
+ return COMPONENT_INCOMPATIBLE
+
+ _inputs["up"] = CALLBACK(parent, TYPE_PROC_REF(/obj/effect/immovablerod, walk_in_direction), NORTH)
+ _inputs["down"] = CALLBACK(parent, TYPE_PROC_REF(/obj/effect/immovablerod, walk_in_direction), SOUTH)
+ _inputs["left"] = CALLBACK(parent, TYPE_PROC_REF(/obj/effect/immovablerod, walk_in_direction), WEST)
+ _inputs["right"] = CALLBACK(parent, TYPE_PROC_REF(/obj/effect/immovablerod, walk_in_direction), EAST)
+
+ return ..()
diff --git a/code/datums/datumvars.dm b/code/datums/datumvars.dm
index f31d0f014f8..80d22911c58 100644
--- a/code/datums/datumvars.dm
+++ b/code/datums/datumvars.dm
@@ -1236,6 +1236,63 @@
var/datum/ui_module/colour_matrix_tester/CMT = new(target=target)
CMT.ui_interact(usr)
+ if(href_list["grantdeadchatcontrol"])
+ if(!check_rights(R_EVENT))
+ return
+
+ var/atom/movable/A = locateUID(href_list["grantdeadchatcontrol"])
+ if(!istype(A))
+ return
+
+ if(!GLOB.dsay_enabled)
+ // TODO verify what happens when deadchat is muted
+ to_chat(usr, "Deadchat is globally muted, un-mute deadchat before enabling this.")
+ return
+
+ if(A.GetComponent(/datum/component/deadchat_control))
+ to_chat(usr, "[A] is already under deadchat control!")
+ return
+
+ var/control_mode = input(usr, "Please select the control mode","Deadchat Control", null) as null|anything in list("democracy", "anarchy")
+
+ var/selected_mode
+ switch(control_mode)
+ if("democracy")
+ selected_mode = DEADCHAT_DEMOCRACY_MODE
+ if("anarchy")
+ selected_mode = DEADCHAT_ANARCHY_MODE
+ else
+ return
+
+ var/cooldown = input(usr, "Please enter a cooldown time in seconds. For democracy, it's the time between actions (must be greater than zero). For anarchy, it's the time between each user's actions, or -1 for no cooldown.", "Cooldown", null) as null|num
+ if(isnull(cooldown) || (cooldown == -1 && selected_mode == DEADCHAT_DEMOCRACY_MODE))
+ return
+ if(cooldown < 0 && selected_mode == DEADCHAT_DEMOCRACY_MODE)
+ to_chat(usr, "The cooldown for democracy mode must be greater than zero.")
+ return
+ if(cooldown == -1)
+ cooldown = 0
+ else
+ cooldown = cooldown SECONDS
+
+ A.deadchat_plays(selected_mode, cooldown)
+ message_admins("[key_name_admin(usr)] provided deadchat control to [A].")
+
+ if(href_list["removedeadchatcontrol"])
+ if(!check_rights(R_EVENT))
+ return
+
+ var/atom/movable/A = locateUID(href_list["removedeadchatcontrol"])
+ if(!istype(A))
+ return
+
+ if(!A.GetComponent(/datum/component/deadchat_control))
+ to_chat(usr, "[A] is not currently under deadchat control!")
+ return
+
+ A.stop_deadchat_plays()
+ message_admins("[key_name_admin(usr)] removed deadchat control from [A].")
+
/client/proc/view_var_Topic_list(href, href_list, hsrc)
if(href_list["VarsList"])
debug_variables(locate(href_list["VarsList"]))
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index 5e312ce96da..3281abf80ba 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -624,3 +624,27 @@
/// called when a mob gets shoved into an items turf. false means the mob will be shoved backwards normally, true means the mob will not be moved by the disarm proc.
/atom/movable/proc/shove_impact(mob/living/target, mob/living/attacker)
return FALSE
+
+/**
+ * Adds the deadchat_plays component to this atom with simple movement commands.
+ *
+ * Returns the component added.
+ * Arguments:
+ * * mode - Either DEADCHAT_ANARCHY_MODE or DEADCHAT_DEMOCRACY_MODE passed to the deadchat_control component. See [/datum/component/deadchat_control] for more info.
+ * * cooldown - The cooldown between command inputs passed to the deadchat_control component. See [/datum/component/deadchat_control] for more info.
+ */
+/atom/movable/proc/deadchat_plays(mode = DEADCHAT_ANARCHY_MODE, cooldown = 12 SECONDS)
+ return AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, list(), cooldown)
+
+/// Easy way to remove the component when the fun has been played out
+/atom/movable/proc/stop_deadchat_plays()
+ var/datum/component/deadchat_control/comp = GetComponent(/datum/component/deadchat_control)
+ qdel(comp)
+
+/atom/movable/vv_get_dropdown()
+ . = ..()
+ if(!GetComponent(/datum/component/deadchat_control))
+ .["Give deadchat control"] = "?_src_=vars;grantdeadchatcontrol=[UID()]"
+ else
+ .["Remove deadchat control"] = "?_src_=vars;removedeadchatcontrol=[UID()]"
+
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index e465d8ae24f..30f8b8569e5 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -216,6 +216,8 @@
if(GLOB.configuration.general.enable_auto_mute && !check_rights(R_ADMIN, 0) && last_message == message)
last_message_count++
+ if(SEND_SIGNAL(mob, COMSIG_MOB_AUTOMUTE_CHECK, src, last_message, mute_type) & WAIVE_AUTOMUTE_CHECK)
+ return FALSE
if(last_message_count >= SPAM_TRIGGER_AUTOMUTE)
to_chat(src, "You have exceeded the spam filter limit for identical messages. An auto-mute was applied.")
cmd_admin_mute(mob, mute_type, 1)
diff --git a/code/modules/events/immovable_rod.dm b/code/modules/events/immovable_rod.dm
index b30a9111c20..9c7169df52b 100644
--- a/code/modules/events/immovable_rod.dm
+++ b/code/modules/events/immovable_rod.dm
@@ -102,3 +102,16 @@ In my current plan for it, 'solid' will be defined as anything with density == 1
T.ex_act(EXPLODE_HEAVY)
if(loc == destination)
qdel(src)
+
+/obj/effect/immovablerod/deadchat_plays(mode = DEADCHAT_DEMOCRACY_MODE, cooldown = 6 SECONDS)
+ return AddComponent(/datum/component/deadchat_control/immovable_rod, mode, list(), cooldown)
+
+/**
+ * Rod will walk towards edge turf in the specified direction.
+ *
+ * Arguments:
+ * * direction - The direction to walk the rod towards: NORTH, SOUTH, EAST, WEST.
+ */
+/obj/effect/immovablerod/proc/walk_in_direction(direction)
+ destination = get_edge_target_turf(src, direction)
+ walk_towards(src, destination)
diff --git a/code/modules/mob/living/carbon/human/human_mob.dm b/code/modules/mob/living/carbon/human/human_mob.dm
index 24c799a48f6..84e145521e2 100644
--- a/code/modules/mob/living/carbon/human/human_mob.dm
+++ b/code/modules/mob/living/carbon/human/human_mob.dm
@@ -2065,3 +2065,83 @@ Eyes need to have significantly high darksight to shine unless the mob has the X
set category = "IC"
update_flavor_text()
+
+// Behavior for deadchat control
+
+/mob/living/carbon/human/proc/dchat_emote()
+ var/list/possible_emotes = list("scream", "clap", "snap", "crack", "dap", "burp")
+ emote(pick(possible_emotes))
+
+/mob/living/carbon/human/proc/dchat_attack()
+ var/turf/ahead = get_turf(get_step(src, dir))
+ var/mob/living/victim = locate(/mob/living) in ahead
+ var/in_hand = get_active_hand()
+ if(victim)
+ victim.attacked_by(in_hand, src, BODY_ZONE_CHEST)
+ return
+ var/obj/structure/other_victim = locate(/obj/structure) in ahead
+ if(other_victim)
+ do_attack_animation(other_victim, used_item = in_hand)
+ other_victim.attacked_by(in_hand, src)
+ return
+
+ visible_message("[src] swings [isnull(in_hand) ? "[p_their()] fists" : in_hand] wildly!")
+
+/mob/living/carbon/human/proc/dchat_pickup()
+ var/turf/ahead = get_step(src, dir)
+ var/obj/item/thing = locate(/obj/item) in ahead
+ if(!thing)
+ return
+
+ var/old_loc = thing.loc
+ var/obj/item/in_hand = get_active_hand()
+
+ if(in_hand)
+ visible_message("[src] drops [in_hand] and picks up [thing] instead!")
+ unEquip(in_hand)
+ in_hand.forceMove(old_loc)
+ else
+ visible_message("[src] picks up [thing]!")
+ put_in_active_hand(thing)
+
+/mob/living/carbon/human/proc/dchat_throw()
+ var/in_hand = get_active_hand()
+ if(!in_hand)
+ visible_message("[src] makes a throwing motion!")
+ return
+ var/atom/possible_target
+ var/cur_turf = get_turf(src)
+ for(var/i in 1 to 5)
+ cur_turf = get_step(cur_turf, dir)
+ possible_target = locate(/mob/living) in cur_turf
+ if(possible_target)
+ break
+
+ possible_target = locate(/obj/structure) in cur_turf
+ if(possible_target)
+ break
+
+ if(!possible_target)
+ throw_item(cur_turf)
+ else
+ throw_item(possible_target)
+
+/mob/living/carbon/human/proc/dchat_shove()
+ var/turf/ahead = get_turf(get_step(src, dir))
+ var/mob/living/carbon/human/H = locate(/mob/living/carbon/human) in ahead
+ if(!H)
+ visible_message("[src] tries to shove something away!")
+ return
+ dna?.species.disarm(src, H)
+
+
+/mob/living/carbon/human/deadchat_plays(mode = DEADCHAT_DEMOCRACY_MODE, cooldown = 7 SECONDS)
+ var/list/inputs = list(
+ "emote" = CALLBACK(src, PROC_REF(dchat_emote)),
+ "attack" = CALLBACK(src, PROC_REF(dchat_attack)),
+ "pickup" = CALLBACK(src, PROC_REF(dchat_pickup)),
+ "throw" = CALLBACK(src, PROC_REF(dchat_throw)),
+ "disarm" = CALLBACK(src, PROC_REF(dchat_shove)),
+ )
+
+ AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, inputs, cooldown)
diff --git a/code/modules/mob/living/simple_animal/friendly/dog.dm b/code/modules/mob/living/simple_animal/friendly/dog.dm
index 2b131b53e29..646b048218c 100644
--- a/code/modules/mob/living/simple_animal/friendly/dog.dm
+++ b/code/modules/mob/living/simple_animal/friendly/dog.dm
@@ -105,6 +105,48 @@
..(gibbed)
regenerate_icons()
+/mob/living/simple_animal/pet/dog/corgi/deadchat_plays(mode = DEADCHAT_ANARCHY_MODE, cooldown = 12 SECONDS)
+ . = AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, list(
+ "speak" = CALLBACK(src, PROC_REF(handle_automated_speech), TRUE),
+ "wear_hat" = CALLBACK(src, PROC_REF(find_new_hat)),
+ "drop_hat" = CALLBACK(src, PROC_REF(drop_hat)),
+ "spin" = CALLBACK(src, TYPE_PROC_REF(/mob, emote), "spin")), cooldown, CALLBACK(src, PROC_REF(end_dchat_plays)))
+
+ if(. == COMPONENT_INCOMPATIBLE)
+ return
+
+ stop_automated_movement = TRUE
+
+///Deadchat plays command that picks a new hat for Ian.
+/mob/living/simple_animal/pet/dog/corgi/proc/find_new_hat()
+ if(!isturf(loc))
+ return
+ var/list/possible_headwear = list()
+ for(var/obj/item/item in loc)
+ if(ispath(item.dog_fashion, /datum/dog_fashion/head))
+ possible_headwear += item
+ if(!length(possible_headwear))
+ for(var/obj/item/item in orange(1))
+ if(ispath(item.dog_fashion, /datum/dog_fashion/head) && Adjacent(item))
+ possible_headwear += item
+ if(!length(possible_headwear))
+ return
+ if(inventory_head)
+ inventory_head.forceMove(drop_location())
+ inventory_head = null
+ place_on_head(pick(possible_headwear))
+ visible_message("[src] puts [inventory_head] on [p_their()] own head, somehow.")
+
+///Deadchat plays command that drops the current hat off Ian.
+/mob/living/simple_animal/pet/dog/corgi/proc/drop_hat()
+ if(!inventory_head)
+ return
+ visible_message("[src] vigorously shakes [p_their()] head, dropping [inventory_head] to the ground.")
+ inventory_head.forceMove(drop_location())
+ inventory_head = null
+ update_corgi_fluff()
+ regenerate_icons()
+
/mob/living/simple_animal/pet/dog/corgi/show_inv(mob/user)
if(user.incapacitated() || !Adjacent(user))
return
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index e7638b81c79..0b859c30460 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -635,3 +635,14 @@
/mob/living/simple_animal/proc/npc_safe(mob/user)
return FALSE
+
+/mob/living/simple_animal/deadchat_plays(mode = DEADCHAT_ANARCHY_MODE, cooldown = 12 SECONDS)
+ . = AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, list(), cooldown, CALLBACK(src, PROC_REF(end_dchat_plays)))
+
+ if(. == COMPONENT_INCOMPATIBLE)
+ return
+
+ stop_automated_movement = TRUE
+
+/mob/living/simple_animal/proc/end_dchat_plays()
+ stop_automated_movement = FALSE
diff --git a/code/modules/mob/mob_say_base.dm b/code/modules/mob/mob_say_base.dm
index 54a4d195734..58135cceab3 100644
--- a/code/modules/mob/mob_say_base.dm
+++ b/code/modules/mob/mob_say_base.dm
@@ -62,6 +62,8 @@
if(client.handle_spam_prevention(message, MUTE_DEADCHAT))
return
+ if(SEND_SIGNAL(src, COMSIG_MOB_DEADSAY, message) & MOB_DEADSAY_SIGNAL_INTERCEPT)
+ return
if(message in USABLE_DEAD_EMOTES)
emote(copytext(message, 2), intentional = TRUE)
diff --git a/code/modules/power/singularity/singularity.dm b/code/modules/power/singularity/singularity.dm
index 5684ba4823d..53a645a3169 100644
--- a/code/modules/power/singularity/singularity.dm
+++ b/code/modules/power/singularity/singularity.dm
@@ -522,3 +522,20 @@
projectile_angle += angle_to_projectile / (distance ** 2)
P.damage += 10 / distance
P.set_angle(projectile_angle)
+
+/obj/singularity/proc/end_deadchat_plays()
+ move_self = TRUE
+
+
+/obj/singularity/deadchat_plays(mode = DEADCHAT_DEMOCRACY_MODE, cooldown = 12 SECONDS)
+ . = AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, list(), cooldown, CALLBACK(src, TYPE_PROC_REF(/atom/movable, stop_deadchat_plays)))
+
+ if(. == COMPONENT_INCOMPATIBLE)
+ return
+
+ move_self = FALSE
+
+
+/obj/singularity/deadchat_controlled/Initialize(mapload, starting_energy)
+ . = ..()
+ deadchat_plays(mode = DEADCHAT_DEMOCRACY_MODE)
diff --git a/paradise.dme b/paradise.dme
index c4344b48316..8e1b71ab716 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -353,6 +353,7 @@
#include "code\datums\cache\powermonitor.dm"
#include "code\datums\components\_component.dm"
#include "code\datums\components\caltrop.dm"
+#include "code\datums\components\deadchat_control.dm"
#include "code\datums\components\decal.dm"
#include "code\datums\components\defibrillator.dm"
#include "code\datums\components\ducttape.dm"