diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm
index ec6030c87d9..1cc7ea12d42 100644
--- a/code/__DEFINES/components.dm
+++ b/code/__DEFINES/components.dm
@@ -114,6 +114,8 @@
#define COMPONENT_BLOCK_TOOL_ATTACK 1
#define COMSIG_ATOM_INTERCEPT_TELEPORT "intercept_teleport" //called when teleporting into a protected turf: (channel, turf/origin)
#define COMPONENT_BLOCK_TELEPORT 1
+#define COMSIG_ATOM_ORBIT_BEGIN "atom_orbit_begin" //called when an atom starts orbiting another atom: (atom)
+#define COMSIG_ATOM_ORBIT_STOP "atom_orbit_stop" //called when an atom stops orbiting another atom: (atom)
/////////////////
#define COMSIG_ATOM_ATTACK_GHOST "atom_attack_ghost" //from base of atom/attack_ghost(): (mob/dead/observer/ghost)
#define COMSIG_ATOM_ATTACK_HAND "atom_attack_hand" //from base of atom/attack_hand(): (mob/user)
@@ -204,7 +206,8 @@
#define SPEECH_LANGUAGE 5
#define SPEECH_IGNORE_SPAM 6
#define SPEECH_FORCED 7 */
-
+#define COMSIG_MOB_DEADSAY "mob_deadsay" // from /mob/say_dead(): (mob/speaker, message)
+ #define MOB_DEADSAY_SIGNAL_INTERCEPT 1
// /mob/living signals
#define COMSIG_LIVING_RESIST "living_resist" //from base of mob/living/resist() (/mob/living)
#define COMSIG_LIVING_IGNITED "living_ignite" //from base of mob/living/IgniteMob() (/mob/living)
diff --git a/code/datums/components/deadchat_control.dm b/code/datums/components/deadchat_control.dm
new file mode 100644
index 00000000000..7c6398e51ab
--- /dev/null
+++ b/code/datums/components/deadchat_control.dm
@@ -0,0 +1,106 @@
+#define DEMOCRACY_MODE "democracy"
+#define ANARCHY_MODE "anarchy"
+
+/datum/component/deadchat_control
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+ var/timerid
+
+ var/list/datum/callback/inputs = list()
+ var/list/ckey_to_cooldown = list()
+ var/orbiters = list()
+ var/deadchat_mode
+ var/input_cooldown
+
+/datum/component/deadchat_control/Initialize(_deadchat_mode, _inputs, _input_cooldown = 12 SECONDS)
+ if(!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+ RegisterSignal(parent, COMSIG_ATOM_ORBIT_BEGIN, .proc/orbit_begin)
+ RegisterSignal(parent, COMSIG_ATOM_ORBIT_STOP, .proc/orbit_stop)
+ deadchat_mode = _deadchat_mode
+ inputs = _inputs
+ input_cooldown = _input_cooldown
+ if(deadchat_mode == DEMOCRACY_MODE)
+ timerid = addtimer(CALLBACK(src, .proc/democracy_loop), input_cooldown, TIMER_STOPPABLE | TIMER_LOOP)
+ notify_ghosts("[parent] is now deadchat controllable!", source = parent, action = NOTIFY_ORBIT, header="Something Interesting!")
+
+
+/datum/component/deadchat_control/Destroy(force, silent)
+ inputs = null
+ orbiters = null
+ ckey_to_cooldown = null
+ return ..()
+
+/datum/component/deadchat_control/proc/deadchat_react(mob/source, message)
+ message = lowertext(message)
+ if(!inputs[message])
+ return
+ if(deadchat_mode == ANARCHY_MODE)
+ var/cooldown = ckey_to_cooldown[source.ckey]
+ if(cooldown)
+ return MOB_DEADSAY_SIGNAL_INTERCEPT
+ inputs[message].Invoke()
+ ckey_to_cooldown[source.ckey] = TRUE
+ addtimer(CALLBACK(src, .proc/remove_cooldown, source.ckey), input_cooldown)
+ else if(deadchat_mode == DEMOCRACY_MODE)
+ ckey_to_cooldown[source.ckey] = message
+ return MOB_DEADSAY_SIGNAL_INTERCEPT
+
+/datum/component/deadchat_control/proc/remove_cooldown(ckey)
+ ckey_to_cooldown.Remove(ckey)
+
+/datum/component/deadchat_control/proc/democracy_loop()
+ if(QDELETED(parent) || deadchat_mode != DEMOCRACY_MODE)
+ deltimer(timerid)
+ return
+ var/result = count_democracy_votes()
+ if(!isnull(result))
+ inputs[result].Invoke()
+ var/message = "[parent] has done action [result]!
New vote started. It will end in [input_cooldown/10] seconds."
+ for(var/M in orbiters)
+ to_chat(M, message)
+ else
+ var/message = "No votes were cast this cycle."
+ for(var/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 == DEMOCRACY_MODE)
+ timerid = addtimer(CALLBACK(src, .proc/democracy_loop), input_cooldown, TIMER_STOPPABLE | TIMER_LOOP)
+ else
+ deltimer(timerid)
+
+/datum/component/deadchat_control/proc/orbit_begin(atom/source, atom/orbiter)
+ RegisterSignal(orbiter, COMSIG_MOB_DEADSAY, .proc/deadchat_react)
+ orbiters |= orbiter
+
+/datum/component/deadchat_control/proc/orbit_stop(atom/source, atom/orbiter)
+ if(orbiter in orbiters)
+ UnregisterSignal(orbiter, COMSIG_MOB_DEADSAY)
+ orbiters -= orbiter
diff --git a/code/datums/components/orbiter.dm b/code/datums/components/orbiter.dm
index efa0fd14d55..2e4d34e716c 100644
--- a/code/datums/components/orbiter.dm
+++ b/code/datums/components/orbiter.dm
@@ -60,6 +60,7 @@
orbiters[orbiter] = TRUE
orbiter.orbiting = src
RegisterSignal(orbiter, COMSIG_MOVABLE_MOVED, .proc/orbiter_move_react)
+ SEND_SIGNAL(parent, COMSIG_ATOM_ORBIT_BEGIN, orbiter)
var/matrix/initial_transform = matrix(orbiter.transform)
// Head first!
@@ -86,6 +87,7 @@
if(!orbiters[orbiter])
return
UnregisterSignal(orbiter, COMSIG_MOVABLE_MOVED)
+ SEND_SIGNAL(parent, COMSIG_ATOM_ORBIT_STOP, orbiter)
orbiter.SpinAnimation(0, 0)
orbiters -= orbiter
orbiter.stop_orbit(src)
diff --git a/code/modules/mob/living/simple_animal/hostile/goose.dm b/code/modules/mob/living/simple_animal/hostile/goose.dm
index 69163bb3acb..d9347c9d344 100644
--- a/code/modules/mob/living/simple_animal/hostile/goose.dm
+++ b/code/modules/mob/living/simple_animal/hostile/goose.dm
@@ -64,6 +64,10 @@
goosevomit = new
goosevomit.Grant(src)
RegisterSignal(src, COMSIG_MOVABLE_MOVED, .proc/goosement)
+ // 5% chance every round to have anarchy mode deadchat control on birdboat.
+ if(prob(5))
+ desc = "[initial(desc)] It's waddling more than usual. It seems to be possessed."
+ deadchat_plays_goose()
/mob/living/simple_animal/hostile/retaliate/goose/vomit/Destroy()
UnregisterSignal(src, COMSIG_MOVABLE_MOVED)
@@ -155,6 +159,16 @@
vomitCoefficient = 1
vomitTimeBonus = 0
+/// A proc to make it easier for admins to make the goose playable by deadchat.
+/mob/living/simple_animal/hostile/retaliate/goose/vomit/proc/deadchat_plays_goose()
+ stop_automated_movement = TRUE
+ AddComponent(/datum/component/deadchat_control, ANARCHY_MODE, list(
+ "up" = CALLBACK(GLOBAL_PROC, .proc/_step, src, NORTH),
+ "down" = CALLBACK(GLOBAL_PROC, .proc/_step, src, SOUTH),
+ "left" = CALLBACK(GLOBAL_PROC, .proc/_step, src, WEST),
+ "right" = CALLBACK(GLOBAL_PROC, .proc/_step, src, EAST),
+ "vomit" = CALLBACK(src, .proc/vomit_prestart, 25)), 12 SECONDS, 4 SECONDS)
+
/datum/action/cooldown/vomit
name = "Vomit"
check_flags = AB_CHECK_CONSCIOUS
diff --git a/code/modules/mob/say.dm b/code/modules/mob/say.dm
index cc12a56eb70..250aefefa82 100644
--- a/code/modules/mob/say.dm
+++ b/code/modules/mob/say.dm
@@ -78,6 +78,8 @@
var/source = "DEAD: [name][alt_name]"
var/rendered = " [emoji_parse(spanned)]"
log_talk(message, LOG_SAY, tag="DEAD")
+ if(SEND_SIGNAL(src, COMSIG_MOB_DEADSAY, message) & MOB_DEADSAY_SIGNAL_INTERCEPT)
+ return
deadchat_broadcast(rendered, source, follow_target = src, speaker_key = key)
///Check if this message is an emote
diff --git a/code/modules/power/singularity/singularity.dm b/code/modules/power/singularity/singularity.dm
index 27ab3ed300b..d3865389d89 100644
--- a/code/modules/power/singularity/singularity.dm
+++ b/code/modules/power/singularity/singularity.dm
@@ -446,3 +446,14 @@
explosion(src.loc,(dist),(dist*2),(dist*4))
qdel(src)
return(gain)
+
+/obj/singularity/deadchat_controlled
+ move_self = FALSE
+
+/obj/singularity/deadchat_controlled/Initialize(mapload, starting_energy)
+ . = ..()
+ AddComponent(/datum/component/deadchat_control, DEMOCRACY_MODE, list(
+ "up" = CALLBACK(GLOBAL_PROC, .proc/_step, src, NORTH),
+ "down" = CALLBACK(GLOBAL_PROC, .proc/_step, src, SOUTH),
+ "left" = CALLBACK(GLOBAL_PROC, .proc/_step, src, WEST),
+ "right" = CALLBACK(GLOBAL_PROC, .proc/_step, src, EAST)))
diff --git a/tgstation.dme b/tgstation.dme
index 909db17444c..3ca8ae3b07c 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -363,6 +363,7 @@
#include "code\datums\components\chasm.dm"
#include "code\datums\components\construction.dm"
#include "code\datums\components\creamed.dm"
+#include "code\datums\components\deadchat_control.dm"
#include "code\datums\components\decal.dm"
#include "code\datums\components\dejavu.dm"
#include "code\datums\components\earprotection.dm"