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