Skip to content

Commit

Permalink
[MIRROR] Vote clean up and admin additions (#2277)
Browse files Browse the repository at this point in the history
* Vote clean up and admin additions (#82981)

## About The Pull Request

- Fixes `vote_delay` not being a thing. I broke this two years ago but
there's no bug report associated.

- Admins can now reset the vote delay (to let people vote again
instantly)

- Admins can now end the current vote immediately (rather than
cancelling)

- Custom multi and custom single combined into one vote

## Why It's Good For The Game

Makes voting a bit easier to use, both for admins and for coders adding
new votes.


![image](https://github.com/tgstation/tgstation/assets/51863163/40b8857c-76b7-4a58-82bc-1b82640d550a)

## Changelog

:cl: Melbert
admin: Custom Single and Custom Multi votes are now combined into one
vote
admin: Admins can now end votes instantly, rather than cancelling them
admin: Admins can now reset the vote cooldown
fix: Vote cooldown actually applies now
/:cl:

* Vote clean up and admin additions

---------

Co-authored-by: MrMelbert <[email protected]>
  • Loading branch information
2 people authored and StealsThePRs committed May 2, 2024
1 parent 27293ba commit fd8fef6
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 199 deletions.
3 changes: 3 additions & 0 deletions code/__DEFINES/subsystems.dm
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,6 @@
#define VOTE_WINNER_METHOD_WEIGHTED_RANDOM "Weighted Random"
/// There is no winner for this vote.
#define VOTE_WINNER_METHOD_NONE "None"

/// Returned by [/datum/vote/proc/can_be_initiated] to denote the vote is valid and can be initiated.
#define VOTE_AVAILABLE "Vote Available"
4 changes: 2 additions & 2 deletions code/controllers/configuration/entries/general.dm
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,13 @@

/// minimum time between voting sessions (deciseconds, 10 minute default)
/datum/config_entry/number/vote_delay
default = 6000
default = 10 MINUTES
integer = FALSE
min_val = 0

/// length of voting period (deciseconds, default 1 minute)
/datum/config_entry/number/vote_period
default = 600
default = 1 MINUTES
integer = FALSE
min_val = 0

Expand Down
119 changes: 94 additions & 25 deletions code/controllers/subsystem/vote.dm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ SUBSYSTEM_DEF(vote)
var/list/voted = list()
/// A list of all ckeys currently voting for the current vote.
var/list/voting = list()
/// World.time we started our last vote
var/last_vote_time = -INFINITY

/datum/controller/subsystem/vote/Initialize()
for(var/vote_type in subtypesof(/datum/vote))
Expand All @@ -30,16 +32,20 @@ SUBSYSTEM_DEF(vote)

return SS_INIT_SUCCESS


// Called by master_controller
/datum/controller/subsystem/vote/fire()
if(!current_vote)
return
current_vote.time_remaining = round((current_vote.started_time + CONFIG_GET(number/vote_period) - world.time) / 10)
if(current_vote.time_remaining < 0)
process_vote_result()
SStgui.close_uis(src)
reset()
end_vote()

/// Ends the current vote.
/datum/controller/subsystem/vote/proc/end_vote()
ASSERT(current_vote)
process_vote_result()
SStgui.close_uis(src)
reset()

/// Resets all of our vars after votes conclude / are cancelled.
/datum/controller/subsystem/vote/proc/reset()
Expand Down Expand Up @@ -168,24 +174,10 @@ SUBSYSTEM_DEF(vote)
* * vote_type - The type of vote to initiate. Can be a [/datum/vote] typepath, a [/datum/vote] instance, or the name of a vote datum.
* * vote_initiator_name - The ckey (if player initiated) or name that initiated a vote. Ex: "UristMcAdmin", "the server"
* * vote_initiator - If a person / mob initiated the vote, this is the mob that did it
* * forced - Whether we're forcing the vote to go through regardless of existing votes or other circumstances. Note: If the vote is admin created, forced becomes true regardless.
* * forced - Whether we're forcing the vote to go through regardless of existing votes or other circumstances.
*/
/datum/controller/subsystem/vote/proc/initiate_vote(vote_type, vote_initiator_name, mob/vote_initiator, forced = FALSE)

// Even if it's forced we can't vote before we're set up
if(!MC_RUNNING(init_stage))
if(vote_initiator)
to_chat(vote_initiator, span_warning("You cannot start vote now, the server is not done initializing."))
return FALSE

// Check if we have unlimited voting power.
// Admin started (or forced) voted will go through even if there's an ongoing vote,
// if voting is on cooldown, or regardless if a vote is config disabled (in some cases)
var/unlimited_vote_power = forced || !!GLOB.admin_datums[vote_initiator?.ckey]

if(current_vote && !unlimited_vote_power)
if(vote_initiator)
to_chat(vote_initiator, span_warning("There is already a vote in progress! Please wait for it to finish."))
if(!can_vote_start(vote_initiator, forced))
return FALSE

// Get our actual datum
Expand All @@ -212,7 +204,7 @@ SUBSYSTEM_DEF(vote)
return FALSE

// Vote can't be initiated in our circumstances? No vote
if(!to_vote.can_be_initiated(vote_initiator, unlimited_vote_power))
if(to_vote.can_be_initiated(forced) != VOTE_AVAILABLE)
return FALSE

// Okay, we're ready to actually create a vote -
Expand All @@ -223,8 +215,12 @@ SUBSYSTEM_DEF(vote)
if(!to_vote.create_vote(vote_initiator))
return FALSE

if(!vote_initiator_name && vote_initiator)
vote_initiator_name = vote_initiator.key

// Okay, the vote's happening now, for real. Set it up.
current_vote = to_vote
last_vote_time = world.time

var/duration = CONFIG_GET(number/vote_period)
var/to_display = current_vote.initiate_vote(vote_initiator_name, duration)
Expand All @@ -248,6 +244,36 @@ SUBSYSTEM_DEF(vote)

return TRUE

/**
* Checks if we can start a vote.
*
* * vote_initiator - The mob that initiated the vote.
* * forced - Whether we're forcing the vote to go through regardless of existing votes or other circumstances.
*
* Returns TRUE if we can start a vote, FALSE if we can't.
*/
/datum/controller/subsystem/vote/proc/can_vote_start(mob/vote_initiator, forced)
// Even if it's forced we can't vote before we're set up
if(!MC_RUNNING(init_stage))
if(vote_initiator)
to_chat(vote_initiator, span_warning("You cannot start a vote now, the server is not done initializing."))
return FALSE

if(forced)
return TRUE

var/next_allowed_time = last_vote_time + CONFIG_GET(number/vote_delay)
if(next_allowed_time > world.time)
if(vote_initiator)
to_chat(vote_initiator, span_warning("A vote was initiated recently. You must wait [DisplayTimeText(next_allowed_time - world.time)] before a new vote can be started!"))
return FALSE

if(current_vote)
if(vote_initiator)
to_chat(vote_initiator, span_warning("There is already a vote in progress! Please wait for it to finish."))
return FALSE

return TRUE
/datum/controller/subsystem/vote/ui_state()
return GLOB.always_state

Expand Down Expand Up @@ -282,11 +308,12 @@ SUBSYSTEM_DEF(vote)
if(!istype(vote))
continue

var/can_vote = vote.can_be_initiated(is_lower_admin)
var/list/vote_data = list(
"name" = vote_name,
"canBeInitiated" = vote.can_be_initiated(forced = is_lower_admin),
"canBeInitiated" = can_vote == VOTE_AVAILABLE,
"config" = vote.is_config_enabled(),
"message" = vote.message,
"message" = can_vote == VOTE_AVAILABLE ? vote.default_message : can_vote,
)

if(vote == current_vote)
Expand All @@ -310,7 +337,13 @@ SUBSYSTEM_DEF(vote)
all_vote_data += list(vote_data)

data["possibleVotes"] = all_vote_data
data["LastVoteTime"] = last_vote_time - world.time

return data

/datum/controller/subsystem/vote/ui_static_data(mob/user)
var/list/data = list()
data["VoteCD"] = CONFIG_GET(number/vote_delay)
return data

/datum/controller/subsystem/vote/ui_act(action, params)
Expand All @@ -323,19 +356,37 @@ SUBSYSTEM_DEF(vote)
switch(action)
if("cancel")
if(!voter.client?.holder)
message_admins("[key_name(voter)] tried to cancel the current vote while having no admin holder, \
this is potentially a malicious exploit and worth noting.")
return

voter.log_message("cancelled a vote.", LOG_ADMIN)
message_admins("[key_name_admin(voter)] has cancelled the current vote.")
SStgui.close_uis(src)
reset()
return TRUE

if("endNow")
if(!voter.client?.holder)
message_admins("[key_name(voter)] tried to end the current vote while having no admin holder, \
this is potentially a malicious exploit and worth noting.")
return

voter.log_message("ended the current vote early", LOG_ADMIN)
message_admins("[key_name_admin(voter)] has ended the current vote.")
end_vote()
return TRUE

if("toggleVote")
var/datum/vote/selected = possible_votes[params["voteName"]]
if(!istype(selected))
return
if(!check_rights_for(voter.client, R_ADMIN))
message_admins("[key_name(voter)] tried to toggle vote availability while having improper rights, \
this is potentially a malicious exploit and worth noting.")
return

return selected.toggle_votable(voter)
return selected.toggle_votable()

if("callVote")
var/datum/vote/selected = possible_votes[params["voteName"]]
Expand All @@ -344,14 +395,28 @@ SUBSYSTEM_DEF(vote)

// Whether the user actually can initiate this vote is checked in initiate_vote,
// meaning you can't spoof initiate a vote you're not supposed to be able to
return initiate_vote(selected, voter.key, voter)
return initiate_vote(
vote_type = selected,
vote_initiator_name = voter.key,
vote_initiator = voter,
forced = !!GLOB.admin_datums[voter.ckey],
)

if("voteSingle")
return submit_single_vote(voter, params["voteOption"])

if("voteMulti")
return submit_multi_vote(voter, params["voteOption"])

if("resetCooldown")
if(!voter.client.holder)
message_admins("[key_name(voter)] tried to reset the vote cooldown while having no admin holder, \
this is potentially a malicious exploit and worth noting.")
return

last_vote_time = -INFINITY
return TRUE

/datum/controller/subsystem/vote/ui_close(mob/user)
voting -= user.client?.ckey

Expand All @@ -360,6 +425,10 @@ SUBSYSTEM_DEF(vote)
set category = "OOC"
set name = "Vote"

if(!SSvote.initialized)
to_chat(usr, span_notice("<i>Voting is not set up yet!</i>"))
return

SSvote.ui_interact(usr)

/// Datum action given to mobs that allows players to vote on the current vote.
Expand Down
49 changes: 23 additions & 26 deletions code/datums/votes/_vote_datum.dm
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,25 @@
var/list/default_choices
/// Does the name of this vote contain the word "vote"?
var/contains_vote_in_name = FALSE
/// What message do we want to pass to the player-side vote panel as a tooltip?
var/message = "Click to initiate a vote."
/// What message do we show as the tooltip of this vote if the vote can be initiated?
var/default_message = "Click to initiate a vote."
/// The counting method we use for votes.
var/count_method = VOTE_COUNT_METHOD_SINGLE
/// The method for selecting a winner.
var/winner_method = VOTE_WINNER_METHOD_SIMPLE
/// Should we show details about the number of votes submitted for each option?
var/display_statistics = TRUE

// Internal values used when tracking ongoing votes.
// Don't mess with these, change the above values / override procs for subtypes.
/// An assoc list of [all choices] to [number of votes in the current running vote].
var/list/choices = list()
VAR_FINAL/list/choices = list()
/// A assoc list of [ckey] to [what they voted for in the current running vote].
var/list/choices_by_ckey = list()
VAR_FINAL/list/choices_by_ckey = list()
/// The world time this vote was started.
var/started_time
VAR_FINAL/started_time = -1
/// The time remaining in this vote's run.
var/time_remaining
/// The counting method we use for votes.
var/count_method = VOTE_COUNT_METHOD_SINGLE
/// The method for selecting a winner.
var/winner_method = VOTE_WINNER_METHOD_SIMPLE
/// Should we show details about the number of votes submitted for each option?
var/display_statistics = TRUE
VAR_FINAL/time_remaining = -1

/**
* Used to determine if this vote is a possible
Expand All @@ -55,14 +55,13 @@
choices.Cut()
choices_by_ckey.Cut()
started_time = null
time_remaining = null
time_remaining = -1

/**
* If this vote has a config associated, toggles it between enabled and disabled.
* Returns TRUE on a successful toggle, FALSE otherwise
*/
/datum/vote/proc/toggle_votable(mob/toggler)
return FALSE
/datum/vote/proc/toggle_votable()
return

/**
* If this vote has a config associated, returns its value (True or False, usually).
Expand All @@ -74,20 +73,18 @@
/**
* Checks if the passed mob can initiate this vote.
*
* Return TRUE if the mob can begin the vote, allowing anyone to actually vote on it.
* Return FALSE if the mob cannot initiate the vote.
* * forced - if being invoked by someone who is an admin
*
* Return VOTE_AVAILABLE if the mob can initiate the vote.
* Return a string with the reason why the mob can't initiate the vote.
*/
/datum/vote/proc/can_be_initiated(mob/by_who, forced = FALSE)
/datum/vote/proc/can_be_initiated(forced = FALSE)
SHOULD_CALL_PARENT(TRUE)

if(started_time)
var/next_allowed_time = (started_time + CONFIG_GET(number/vote_delay))
if(next_allowed_time > world.time && !forced)
message = "A vote was initiated recently. You must wait [DisplayTimeText(next_allowed_time - world.time)] before a new vote can be started!"
return FALSE
if(!forced && !is_config_enabled())
return "This vote is currently disabled by the server configuration."

message = initial(message)
return TRUE
return VOTE_AVAILABLE

/**
* Called prior to the vote being initiated.
Expand Down
Loading

0 comments on commit fd8fef6

Please sign in to comment.