diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 6b44745c2e9..9784890bcd4 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -42,6 +42,9 @@
#define COMSIG_PARENT_QDELETING "parent_qdeleting"
/// generic topic handler (usr, href_list)
#define COMSIG_TOPIC "handle_topic"
+/// handler for vv_do_topic (usr, href_list)
+#define COMSIG_VV_TOPIC "vv_topic"
+ #define COMPONENT_VV_HANDLED (1<<0)
/// fires on the target datum when an element is attached to it (/datum/element)
#define COMSIG_ELEMENT_ATTACH "element_attach"
diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm
index c624b3ff6b7..2c54136718d 100644
--- a/code/__DEFINES/vv.dm
+++ b/code/__DEFINES/vv.dm
@@ -87,6 +87,9 @@
#define VV_HK_EDIT_FILTERS "edit_filters"
#define VV_HK_ADD_AI "add_ai"
+// /atom/movable
+#define VV_HK_DEADCHAT_PLAYS "deadchat_plays"
+
// /obj
#define VV_HK_OSAY "osay"
#define VV_HK_MASS_DEL_TYPE "mass_delete_type"
diff --git a/code/datums/components/deadchat_control.dm b/code/datums/components/deadchat_control.dm
index db6c7a2546b..6940caeb609 100644
--- a/code/datums/components/deadchat_control.dm
+++ b/code/datums/components/deadchat_control.dm
@@ -1,30 +1,48 @@
#define DEMOCRACY_MODE "democracy"
#define ANARCHY_MODE "anarchy"
+/**
+ * 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
- var/timerid
+ /// The id for the DEMOCRACY_MODE looping vote timer.
+ var/timerid
+ /// Assoc list of key-chat command string, value-callback pairs. list("right" = CALLBACK(GLOBAL_PROC, .proc/_step, src, EAST))
var/list/datum/callback/inputs = list()
+ /// Assoc list of ckey:value pairings. In DEMOCRACY_MODE, value is the player's vote. In 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()
+ /// Either DEMOCRACY_MODE which will execute a single command after the cooldown based on player votes, or ANARCHY_MODE which allows each player to do a single command every cooldown.
var/deadchat_mode
+ /// In DEMOCRACY_MODE, this is how long players have to vote on an input. In ANARCHY_MODE, this is how long between inputs for each unique player.
var/input_cooldown
-/datum/component/deadchat_control/Initialize(_deadchat_mode, _inputs, _input_cooldown = 12 SECONDS)
+ /// 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/orbit_begin)
RegisterSignal(parent, COMSIG_ATOM_ORBIT_STOP, .proc/orbit_stop)
+ RegisterSignal(parent, COMSIG_VV_TOPIC, .proc/handle_vv_topic)
+ RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/on_examine)
deadchat_mode = _deadchat_mode
inputs = _inputs
input_cooldown = _input_cooldown
+ on_removal = _on_removal
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)
+ on_removal?.Invoke()
inputs = null
orbiters = null
ckey_to_cooldown = null
@@ -34,22 +52,25 @@
SIGNAL_HANDLER
message = lowertext(message)
+
if(!inputs[message])
- return
+ return
+
if(deadchat_mode == ANARCHY_MODE)
- var/cooldown = ckey_to_cooldown[source.ckey]
- if(cooldown)
+ 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 [cooldown * 0.1] seconds.")
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
+ ckey_to_cooldown[source.ckey] = world.time + input_cooldown
+ to_chat(source, "\"[message]\" input accepted. You are now on cooldown for [input_cooldown * 0.1] seconds.")
+ return MOB_DEADSAY_SIGNAL_INTERCEPT
+
+ if(deadchat_mode == 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/remove_cooldown(ckey)
- ckey_to_cooldown.Remove(ckey)
-
/datum/component/deadchat_control/proc/democracy_loop()
if(QDELETED(parent) || deadchat_mode != DEMOCRACY_MODE)
deltimer(timerid)
@@ -57,14 +78,14 @@
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."
+ var/message = "[parent] has done action [result]!
New vote started. It will end in [input_cooldown * 0.1] 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
@@ -74,7 +95,7 @@
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
@@ -82,7 +103,7 @@
if(votes[vote] > prev_value)
prev_value = votes[vote]
result = vote
-
+
if(result in inputs)
return result
@@ -110,3 +131,61 @@
if(orbiter in orbiters)
UnregisterSignal(orbiter, COMSIG_MOB_DEADSAY)
orbiters -= orbiter
+
+/// Allows for this component to be removed via a dedicated VV dropdown entry.
+/datum/component/deadchat_control/proc/handle_vv_topic(datum/source, mob/user, list/href_list)
+ SIGNAL_HANDLER
+ if(!href_list[VV_HK_DEADCHAT_PLAYS] || !check_rights(R_FUN))
+ return
+ . = COMPONENT_VV_HANDLED
+ INVOKE_ASYNC(src, .proc/async_handle_vv_topic, user, href_list)
+
+/// Async proc handling the alert input and associated logic for an admin removing this component via the VV dropdown.
+/datum/component/deadchat_control/proc/async_handle_vv_topic(mob/user, list/href_list)
+ if(alert(user, "Remove deadchat control from [parent]?", "Deadchat Plays [parent]", "Remove", "Cancel") == "Remove")
+ // Quick sanity check as this is an async call.
+ if(QDELETED(src))
+ return
+
+ to_chat(user, "Deadchat can no longer control [parent].")
+ log_admin("[key_name(user)] has removed deadchat control from [parent]")
+ message_admins("[key_name(user)] has removed deadchat control from [parent]")
+
+ qdel(src)
+
+/// 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
+
+ examine_list += "[A.p_theyre(TRUE)] currently under deadchat control using the [deadchat_mode] ruleset!"
+
+ if(deadchat_mode == DEMOCRACY_MODE)
+ examine_list += "Type a command into chat to vote on an action. This happens once every [input_cooldown * 0.1] seconds."
+ else if(deadchat_mode == ANARCHY_MODE)
+ examine_list += "Type a command into chat to perform. You may do this once every [input_cooldown * 0.1] seconds."
+
+ var/extended_examine = "Command list:"
+
+ for(var/possible_input in inputs)
+ extended_examine += " [possible_input]"
+
+ extended_examine += "."
+
+ examine_list += extended_examine
+
+/**
+ * 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(GLOBAL_PROC, .proc/_step, parent, NORTH)
+ inputs["down"] = CALLBACK(GLOBAL_PROC, .proc/_step, parent, SOUTH)
+ inputs["left"] = CALLBACK(GLOBAL_PROC, .proc/_step, parent, WEST)
+ inputs["right"] = CALLBACK(GLOBAL_PROC, .proc/_step, parent, EAST)
diff --git a/code/datums/datumvars.dm b/code/datums/datumvars.dm
index dd28dd55f43..14177a63102 100644
--- a/code/datums/datumvars.dm
+++ b/code/datums/datumvars.dm
@@ -39,6 +39,8 @@
/datum/proc/vv_do_topic(list/href_list)
if(!usr || !usr.client || !usr.client.holder || !check_rights(NONE))
return FALSE //This is VV, not to be called by anything else.
+ if(SEND_SIGNAL(src, COMSIG_VV_TOPIC, usr, href_list) & COMPONENT_VV_HANDLED)
+ return FALSE
if(href_list[VV_HK_MODIFY_TRAITS])
usr.client.holder.modify_traits(src)
return TRUE
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index 073e6202c93..7cfb22dbb1c 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -1042,7 +1042,43 @@
if(. <= GRAB_AGGRESSIVE)
ADD_TRAIT(pulling, TRAIT_FLOORED, CHOKEHOLD_TRAIT)
+/**
+ * Adds the deadchat_plays component to this atom with simple movement commands.
+ *
+ * Returns the component added.
+ * Arguments:
+ * * mode - Either ANARCHY_MODE or 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 = ANARCHY_MODE, cooldown = 12 SECONDS)
+ return AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, list(), cooldown)
+/atom/movable/vv_get_dropdown()
+ . = ..()
+ VV_DROPDOWN_OPTION(VV_HK_DEADCHAT_PLAYS, "Start/Stop Deadchat Plays")
+
+/atom/movable/vv_do_topic(list/href_list)
+ . = ..()
+
+ if(!.)
+ return
+
+ if(href_list[VV_HK_DEADCHAT_PLAYS] && check_rights(R_FUN))
+ if(alert(usr, "Allow deadchat to control [src] via chat commands?", "Deadchat Plays [src]", "Allow", "Cancel") == "Cancel")
+ return
+
+ // Alert is async, so quick sanity check to make sure we should still be doing this.
+ if(QDELETED(src))
+ return
+
+ // This should never happen, but if it does it should not be silent.
+ if(deadchat_plays() == COMPONENT_INCOMPATIBLE)
+ to_chat(usr, "Deadchat control not compatible with [src].")
+ CRASH("deadchat_control component incompatible with object of type: [type]")
+
+ to_chat(usr, "Deadchat now control [src].")
+ log_admin("[key_name(usr)] has added deadchat control to [src]")
+ message_admins("[key_name(usr)] has added deadchat control to [src]")
/obj/item/proc/do_pickup_animation(atom/target)
set waitfor = FALSE
diff --git a/code/modules/mob/living/simple_animal/hostile/goose.dm b/code/modules/mob/living/simple_animal/hostile/goose.dm
index 418d260e6f6..28284705cff 100644
--- a/code/modules/mob/living/simple_animal/hostile/goose.dm
+++ b/code/modules/mob/living/simple_animal/hostile/goose.dm
@@ -103,7 +103,7 @@
// 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()
+ deadchat_plays()
/mob/living/simple_animal/hostile/retaliate/goose/vomit/Destroy()
UnregisterSignal(src, COMSIG_MOVABLE_MOVED)
@@ -233,14 +233,16 @@
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()
+/mob/living/simple_animal/hostile/retaliate/goose/vomit/deadchat_plays(mode = ANARCHY_MODE, cooldown = 12 SECONDS)
+ . = AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, list(
+ "vomit" = CALLBACK(src, .proc/vomit_prestart, 25),
+ "honk" = CALLBACK(src, /atom/movable.proc/say, "HONK!!!"),
+ "spin" = CALLBACK(src, /mob.proc/emote, "spin")), cooldown, CALLBACK(src, .proc/stop_deadchat_plays))
+
+ if(. == COMPONENT_INCOMPATIBLE)
+ return
+
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"
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index 9a6ac09b6ca..cb8a01a78fd 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -702,3 +702,14 @@
if(user.incapacitated())
return
return relaydrive(user, direction)
+
+/mob/living/simple_animal/deadchat_plays(mode = ANARCHY_MODE, cooldown = 12 SECONDS)
+ . = AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, list(), cooldown, CALLBACK(src, .proc/stop_deadchat_plays))
+
+ if(. == COMPONENT_INCOMPATIBLE)
+ return
+
+ stop_automated_movement = TRUE
+
+/mob/living/simple_animal/proc/stop_deadchat_plays()
+ stop_automated_movement = FALSE
diff --git a/code/modules/power/singularity/singularity.dm b/code/modules/power/singularity/singularity.dm
index 68c0638e3f5..05ec3d3627e 100644
--- a/code/modules/power/singularity/singularity.dm
+++ b/code/modules/power/singularity/singularity.dm
@@ -413,13 +413,18 @@
qdel(src)
return gain
-/obj/singularity/deadchat_controlled
+/obj/singularity/deadchat_plays(mode = DEMOCRACY_MODE, cooldown = 12 SECONDS)
+ . = AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, list(), cooldown, CALLBACK(src, .proc/stop_deadchat_plays))
+
+ if(. == COMPONENT_INCOMPATIBLE)
+ return
+
move_self = FALSE
+/obj/singularity/proc/stop_deadchat_plays()
+ move_self = TRUE
+
/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)))
+ deadchat_plays(mode = DEMOCRACY_MODE)
+