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

🆑 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
/🆑
This commit is contained in:
MrMelbert
2024-05-01 17:55:01 -05:00
committed by GitHub
parent 906f4cdf49
commit d1cadb24f9
9 changed files with 308 additions and 200 deletions

View File

@@ -346,3 +346,6 @@
#define VOTE_WINNER_METHOD_WEIGHTED_RANDOM "Weighted Random" #define VOTE_WINNER_METHOD_WEIGHTED_RANDOM "Weighted Random"
/// There is no winner for this vote. /// There is no winner for this vote.
#define VOTE_WINNER_METHOD_NONE "None" #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"

View File

@@ -184,13 +184,13 @@
/// minimum time between voting sessions (deciseconds, 10 minute default) /// minimum time between voting sessions (deciseconds, 10 minute default)
/datum/config_entry/number/vote_delay /datum/config_entry/number/vote_delay
default = 6000 default = 10 MINUTES
integer = FALSE integer = FALSE
min_val = 0 min_val = 0
/// length of voting period (deciseconds, default 1 minute) /// length of voting period (deciseconds, default 1 minute)
/datum/config_entry/number/vote_period /datum/config_entry/number/vote_period
default = 600 default = 1 MINUTES
integer = FALSE integer = FALSE
min_val = 0 min_val = 0

View File

@@ -18,6 +18,8 @@ SUBSYSTEM_DEF(vote)
var/list/voted = list() var/list/voted = list()
/// A list of all ckeys currently voting for the current vote. /// A list of all ckeys currently voting for the current vote.
var/list/voting = list() var/list/voting = list()
/// World.time we started our last vote
var/last_vote_time = -INFINITY
/datum/controller/subsystem/vote/Initialize() /datum/controller/subsystem/vote/Initialize()
for(var/vote_type in subtypesof(/datum/vote)) for(var/vote_type in subtypesof(/datum/vote))
@@ -30,16 +32,20 @@ SUBSYSTEM_DEF(vote)
return SS_INIT_SUCCESS return SS_INIT_SUCCESS
// Called by master_controller // Called by master_controller
/datum/controller/subsystem/vote/fire() /datum/controller/subsystem/vote/fire()
if(!current_vote) if(!current_vote)
return return
current_vote.time_remaining = round((current_vote.started_time + CONFIG_GET(number/vote_period) - world.time) / 10) current_vote.time_remaining = round((current_vote.started_time + CONFIG_GET(number/vote_period) - world.time) / 10)
if(current_vote.time_remaining < 0) if(current_vote.time_remaining < 0)
process_vote_result() end_vote()
SStgui.close_uis(src)
reset() /// 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. /// Resets all of our vars after votes conclude / are cancelled.
/datum/controller/subsystem/vote/proc/reset() /datum/controller/subsystem/vote/proc/reset()
@@ -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_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_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 * * 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) /datum/controller/subsystem/vote/proc/initiate_vote(vote_type, vote_initiator_name, mob/vote_initiator, forced = FALSE)
if(!can_vote_start(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 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."))
return FALSE return FALSE
// Get our actual datum // Get our actual datum
@@ -212,7 +204,7 @@ SUBSYSTEM_DEF(vote)
return FALSE return FALSE
// Vote can't be initiated in our circumstances? No vote // 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 return FALSE
// Okay, we're ready to actually create a vote - // Okay, we're ready to actually create a vote -
@@ -223,8 +215,12 @@ SUBSYSTEM_DEF(vote)
if(!to_vote.create_vote(vote_initiator)) if(!to_vote.create_vote(vote_initiator))
return FALSE 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. // Okay, the vote's happening now, for real. Set it up.
current_vote = to_vote current_vote = to_vote
last_vote_time = world.time
var/duration = CONFIG_GET(number/vote_period) var/duration = CONFIG_GET(number/vote_period)
var/to_display = current_vote.initiate_vote(vote_initiator_name, duration) var/to_display = current_vote.initiate_vote(vote_initiator_name, duration)
@@ -248,6 +244,36 @@ SUBSYSTEM_DEF(vote)
return TRUE 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() /datum/controller/subsystem/vote/ui_state()
return GLOB.always_state return GLOB.always_state
@@ -282,11 +308,12 @@ SUBSYSTEM_DEF(vote)
if(!istype(vote)) if(!istype(vote))
continue continue
var/can_vote = vote.can_be_initiated(is_lower_admin)
var/list/vote_data = list( var/list/vote_data = list(
"name" = vote_name, "name" = vote_name,
"canBeInitiated" = vote.can_be_initiated(forced = is_lower_admin), "canBeInitiated" = can_vote == VOTE_AVAILABLE,
"config" = vote.is_config_enabled(), "config" = vote.is_config_enabled(),
"message" = vote.message, "message" = can_vote == VOTE_AVAILABLE ? vote.default_message : can_vote,
) )
if(vote == current_vote) if(vote == current_vote)
@@ -310,9 +337,15 @@ SUBSYSTEM_DEF(vote)
all_vote_data += list(vote_data) all_vote_data += list(vote_data)
data["possibleVotes"] = all_vote_data data["possibleVotes"] = all_vote_data
data["LastVoteTime"] = last_vote_time - world.time
return data 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) /datum/controller/subsystem/vote/ui_act(action, params)
. = ..() . = ..()
if(.) if(.)
@@ -323,19 +356,37 @@ SUBSYSTEM_DEF(vote)
switch(action) switch(action)
if("cancel") if("cancel")
if(!voter.client?.holder) 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 return
voter.log_message("cancelled a vote.", LOG_ADMIN) voter.log_message("cancelled a vote.", LOG_ADMIN)
message_admins("[key_name_admin(voter)] has cancelled the current vote.") message_admins("[key_name_admin(voter)] has cancelled the current vote.")
SStgui.close_uis(src)
reset() reset()
return TRUE 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") if("toggleVote")
var/datum/vote/selected = possible_votes[params["voteName"]] var/datum/vote/selected = possible_votes[params["voteName"]]
if(!istype(selected)) if(!istype(selected))
return 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") if("callVote")
var/datum/vote/selected = possible_votes[params["voteName"]] var/datum/vote/selected = possible_votes[params["voteName"]]
@@ -344,7 +395,12 @@ SUBSYSTEM_DEF(vote)
// Whether the user actually can initiate this vote is checked in initiate_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 // 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") if("voteSingle")
return submit_single_vote(voter, params["voteOption"]) return submit_single_vote(voter, params["voteOption"])
@@ -352,6 +408,15 @@ SUBSYSTEM_DEF(vote)
if("voteMulti") if("voteMulti")
return submit_multi_vote(voter, params["voteOption"]) 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) /datum/controller/subsystem/vote/ui_close(mob/user)
voting -= user.client?.ckey voting -= user.client?.ckey
@@ -360,6 +425,10 @@ SUBSYSTEM_DEF(vote)
set category = "OOC" set category = "OOC"
set name = "Vote" set name = "Vote"
if(!SSvote.initialized)
to_chat(usr, span_notice("<i>Voting is not set up yet!</i>"))
return
SSvote.ui_interact(usr) SSvote.ui_interact(usr)
/// Datum action given to mobs that allows players to vote on the current vote. /// Datum action given to mobs that allows players to vote on the current vote.

View File

@@ -15,19 +15,8 @@
var/list/default_choices var/list/default_choices
/// Does the name of this vote contain the word "vote"? /// Does the name of this vote contain the word "vote"?
var/contains_vote_in_name = FALSE var/contains_vote_in_name = FALSE
/// What message do we want to pass to the player-side vote panel as a tooltip? /// What message do we show as the tooltip of this vote if the vote can be initiated?
var/message = "Click to initiate a vote." var/default_message = "Click to initiate a vote."
// 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()
/// A assoc list of [ckey] to [what they voted for in the current running vote].
var/list/choices_by_ckey = list()
/// The world time this vote was started.
var/started_time
/// The time remaining in this vote's run.
var/time_remaining
/// The counting method we use for votes. /// The counting method we use for votes.
var/count_method = VOTE_COUNT_METHOD_SINGLE var/count_method = VOTE_COUNT_METHOD_SINGLE
/// The method for selecting a winner. /// The method for selecting a winner.
@@ -35,6 +24,17 @@
/// Should we show details about the number of votes submitted for each option? /// Should we show details about the number of votes submitted for each option?
var/display_statistics = TRUE 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_FINAL/list/choices = list()
/// A assoc list of [ckey] to [what they voted for in the current running vote].
VAR_FINAL/list/choices_by_ckey = list()
/// The world time this vote was started.
VAR_FINAL/started_time = -1
/// The time remaining in this vote's run.
VAR_FINAL/time_remaining = -1
/** /**
* Used to determine if this vote is a possible * Used to determine if this vote is a possible
* vote type for the vote subsystem. * vote type for the vote subsystem.
@@ -55,14 +55,13 @@
choices.Cut() choices.Cut()
choices_by_ckey.Cut() choices_by_ckey.Cut()
started_time = null started_time = null
time_remaining = null time_remaining = -1
/** /**
* If this vote has a config associated, toggles it between enabled and disabled. * 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) /datum/vote/proc/toggle_votable()
return FALSE return
/** /**
* If this vote has a config associated, returns its value (True or False, usually). * If this vote has a config associated, returns its value (True or False, usually).
@@ -74,20 +73,18 @@
/** /**
* Checks if the passed mob can initiate this vote. * 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. * * forced - if being invoked by someone who is an admin
* Return FALSE if the mob cannot initiate the vote. *
* 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) SHOULD_CALL_PARENT(TRUE)
if(started_time) if(!forced && !is_config_enabled())
var/next_allowed_time = (started_time + CONFIG_GET(number/vote_delay)) return "This vote is currently disabled by the server configuration."
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
message = initial(message) return VOTE_AVAILABLE
return TRUE
/** /**
* Called prior to the vote being initiated. * Called prior to the vote being initiated.

View File

@@ -1,14 +1,9 @@
/// The max amount of options someone can have in a custom vote. /// The max amount of options someone can have in a custom vote.
#define MAX_CUSTOM_VOTE_OPTIONS 10 #define MAX_CUSTOM_VOTE_OPTIONS 10
/datum/vote/custom_vote/single /datum/vote/custom_vote
name = "Custom Standard" name = "Custom"
message = "Click here to start a custom vote (one selection per voter)" default_message = "Click here to start a custom vote."
/datum/vote/custom_vote/multi
name = "Custom Multi"
message = "Click here to start a custom multi vote (multiple selections per voter)"
count_method = VOTE_COUNT_METHOD_MULTI
// Custom votes ares always accessible. // Custom votes ares always accessible.
/datum/vote/custom_vote/is_accessible_vote() /datum/vote/custom_vote/is_accessible_vote()
@@ -17,23 +12,45 @@
/datum/vote/custom_vote/reset() /datum/vote/custom_vote/reset()
default_choices = null default_choices = null
override_question = null override_question = null
count_method = VOTE_COUNT_METHOD_SINGLE
return ..() return ..()
/datum/vote/custom_vote/can_be_initiated(mob/by_who, forced = FALSE) /datum/vote/custom_vote/can_be_initiated(forced)
. = ..() . = ..()
if(!.) if(. != VOTE_AVAILABLE)
return FALSE return .
if(forced)
return .
// Custom votes can only be created if they're forced to be made. // Custom votes can only be created if they're forced to be made.
// (Either an admin makes it, or otherwise.) // (Either an admin makes it, or otherwise.)
return forced return "Only admins can create custom votes."
/datum/vote/custom_vote/create_vote(mob/vote_creator) /datum/vote/custom_vote/create_vote(mob/vote_creator)
var/custom_count_method = tgui_input_list(
user = vote_creator,
message = "Single or multiple choice?",
title = "Choice Method",
items = list("Single", "Multiple"),
default = "Single",
)
switch(custom_count_method)
if("Single")
count_method = VOTE_COUNT_METHOD_SINGLE
if("Multiple")
count_method = VOTE_COUNT_METHOD_MULTI
if(null)
return FALSE
else
stack_trace("Got '[custom_count_method]' in create_vote() for custom voting.")
to_chat(vote_creator, span_boldwarning("Unknown choice method. Contact a coder."))
return FALSE
var/custom_win_method = tgui_input_list( var/custom_win_method = tgui_input_list(
vote_creator, user = vote_creator,
"How should the vote winner be determined?", message = "How should the vote winner be determined?",
"Winner Method", title = "Winner Method",
list("Simple", "Weighted Random", "No Winner"), items = list("Simple", "Weighted Random", "No Winner"),
default = "Simple", default = "Simple",
) )
switch(custom_win_method) switch(custom_win_method)
@@ -43,7 +60,10 @@
winner_method = VOTE_WINNER_METHOD_WEIGHTED_RANDOM winner_method = VOTE_WINNER_METHOD_WEIGHTED_RANDOM
if("No Winner") if("No Winner")
winner_method = VOTE_WINNER_METHOD_NONE winner_method = VOTE_WINNER_METHOD_NONE
if(null)
return FALSE
else else
stack_trace("Got '[custom_win_method]' in create_vote() for custom voting.")
to_chat(vote_creator, span_boldwarning("Unknown winner method. Contact a coder.")) to_chat(vote_creator, span_boldwarning("Unknown winner method. Contact a coder."))
return FALSE return FALSE
@@ -54,7 +74,7 @@
list("Yes", "No"), list("Yes", "No"),
) )
if(display_stats == null) if(isnull(display_stats))
return FALSE return FALSE
display_statistics = display_stats == "Yes" display_statistics = display_stats == "Yes"
@@ -74,6 +94,9 @@
if(!length(default_choices)) if(!length(default_choices))
return FALSE return FALSE
// Sanity for all the tgui input stalling we are doing
if(isnull(vote_creator.client?.holder))
return FALSE
return ..() return ..()

View File

@@ -1,6 +1,6 @@
/datum/vote/map_vote /datum/vote/map_vote
name = "Map" name = "Map"
message = "Vote for next round's map!" default_message = "Vote for next round's map!"
count_method = VOTE_COUNT_METHOD_SINGLE count_method = VOTE_COUNT_METHOD_SINGLE
winner_method = VOTE_WINNER_METHOD_WEIGHTED_RANDOM winner_method = VOTE_WINNER_METHOD_WEIGHTED_RANDOM
display_statistics = FALSE display_statistics = FALSE
@@ -30,44 +30,26 @@
SSmapping.map_voted = TRUE // voted by not voting, very sad. SSmapping.map_voted = TRUE // voted by not voting, very sad.
return FALSE return FALSE
/datum/vote/map_vote/toggle_votable(mob/toggler) /datum/vote/map_vote/toggle_votable()
if(!toggler)
CRASH("[type] wasn't passed a \"toggler\" mob to toggle_votable.")
if(!check_rights_for(toggler.client, R_ADMIN))
return FALSE
CONFIG_SET(flag/allow_vote_map, !CONFIG_GET(flag/allow_vote_map)) CONFIG_SET(flag/allow_vote_map, !CONFIG_GET(flag/allow_vote_map))
return TRUE
/datum/vote/map_vote/is_config_enabled() /datum/vote/map_vote/is_config_enabled()
return CONFIG_GET(flag/allow_vote_map) return CONFIG_GET(flag/allow_vote_map)
/datum/vote/map_vote/can_be_initiated(mob/by_who, forced = FALSE) /datum/vote/map_vote/can_be_initiated(forced)
. = ..() . = ..()
if(!.) if(. != VOTE_AVAILABLE)
return FALSE return .
if(forced) if(forced)
return TRUE return VOTE_AVAILABLE
var/number_of_choices = length(check_population()) var/number_of_choices = length(check_population())
if(number_of_choices < 2) if(number_of_choices < 2)
message = "There [number_of_choices == 1 ? "is only one map" : "are no maps"] to choose from." return "There [number_of_choices == 1 ? "is only one map" : "are no maps"] to choose from."
return FALSE
if(SSmapping.map_vote_rocked) if(SSmapping.map_vote_rocked)
return TRUE return VOTE_AVAILABLE
if(!CONFIG_GET(flag/allow_vote_map))
message = "Map voting is disabled by server configuration settings."
return FALSE
if(SSmapping.map_voted) if(SSmapping.map_voted)
message = "The next map has already been selected." return "The next map has already been selected."
return FALSE return VOTE_AVAILABLE
message = initial(message)
return TRUE
/// Before we create a vote, remove all maps from our choices that are outside of our population range. /// Before we create a vote, remove all maps from our choices that are outside of our population range.
/// Note that this can result in zero remaining choices for our vote, which is not ideal (but ultimately okay). /// Note that this can result in zero remaining choices for our vote, which is not ideal (but ultimately okay).

View File

@@ -7,7 +7,8 @@
CHOICE_RESTART, CHOICE_RESTART,
CHOICE_CONTINUE, CHOICE_CONTINUE,
) )
message = "Vote to restart the ongoing round." default_message = "Vote to restart the ongoing round. \
Only works if there are no non-AFK admins online."
/// This proc checks to see if any admins are online for the purposes of this vote to see if it can pass. Returns TRUE if there are valid admins online (Has +SERVER and is not AFK), FALSE otherwise. /// This proc checks to see if any admins are online for the purposes of this vote to see if it can pass. Returns TRUE if there are valid admins online (Has +SERVER and is not AFK), FALSE otherwise.
/datum/vote/restart_vote/proc/admins_present() /datum/vote/restart_vote/proc/admins_present()
@@ -19,36 +20,24 @@
return FALSE return FALSE
/datum/vote/restart_vote/toggle_votable(mob/toggler) /datum/vote/restart_vote/toggle_votable()
if(!toggler)
CRASH("[type] wasn't passed a \"toggler\" mob to toggle_votable.")
if(!check_rights_for(toggler.client, R_ADMIN))
return FALSE
CONFIG_SET(flag/allow_vote_restart, !CONFIG_GET(flag/allow_vote_restart)) CONFIG_SET(flag/allow_vote_restart, !CONFIG_GET(flag/allow_vote_restart))
return TRUE
/datum/vote/restart_vote/is_config_enabled() /datum/vote/restart_vote/is_config_enabled()
return CONFIG_GET(flag/allow_vote_restart) return CONFIG_GET(flag/allow_vote_restart)
/datum/vote/restart_vote/can_be_initiated(mob/by_who, forced) /datum/vote/restart_vote/create_vote(mob/vote_creator)
. = ..() . = ..()
if(!.) if(!.)
return FALSE return
if(!admins_present())
return
async_alert_about_admins(vote_creator)
if(!forced && !CONFIG_GET(flag/allow_vote_restart)) /datum/vote/restart_vote/proc/async_alert_about_admins(mob/vote_creator)
message = "Restart voting is disabled by server configuration settings." set waitfor = FALSE
return FALSE tgui_alert(vote_creator, "Note: Regardless of the results of this vote, \
the round will not automatically restart because an active admin is online.")
// We still want players to be able to vote to restart even if valid admins are online. Let's update the message just so that the player is aware of this fact.
// We don't want to lock-out the vote though, so we'll return TRUE.
if(admins_present())
message = "Regardless of the results of this vote, the round will not automatically restart because an admin is online."
return TRUE
message = initial(message)
return TRUE
/datum/vote/restart_vote/get_vote_result(list/non_voters) /datum/vote/restart_vote/get_vote_result(list/non_voters)
if(!CONFIG_GET(flag/default_no_vote)) if(!CONFIG_GET(flag/default_no_vote))

View File

@@ -10,58 +10,40 @@
CHOICE_TO_ROCK, CHOICE_TO_ROCK,
CHOICE_NOT_TO_ROCK, CHOICE_NOT_TO_ROCK,
) )
message = "Override the current map vote." default_message = "Override the current map vote."
/// The number of times we have rocked the vote thus far. /// The number of times we have rocked the vote thus far.
var/rocking_votes = 0 var/rocking_votes = 0
/datum/vote/rock_the_vote/toggle_votable(mob/toggler) /datum/vote/rock_the_vote/toggle_votable()
if(!toggler)
CRASH("[type] wasn't passed a \"toggler\" mob to toggle_votable.")
if(!check_rights_for(toggler.client, R_ADMIN))
return FALSE
CONFIG_SET(flag/allow_rock_the_vote, !CONFIG_GET(flag/allow_rock_the_vote)) CONFIG_SET(flag/allow_rock_the_vote, !CONFIG_GET(flag/allow_rock_the_vote))
return TRUE
/datum/vote/rock_the_vote/is_config_enabled() /datum/vote/rock_the_vote/is_config_enabled()
return CONFIG_GET(flag/allow_rock_the_vote) return CONFIG_GET(flag/allow_rock_the_vote)
/datum/vote/rock_the_vote/can_be_initiated(mob/by_who, forced) /datum/vote/rock_the_vote/can_be_initiated(forced)
. = ..() . = ..()
if(. != VOTE_AVAILABLE)
if(!.) return .
return FALSE
if(!forced && !CONFIG_GET(flag/allow_rock_the_vote))
message = "Rocking the vote is disabled by this server's configuration settings."
return FALSE
if(SSticker.current_state == GAME_STATE_FINISHED) if(SSticker.current_state == GAME_STATE_FINISHED)
message = "The game is finished, no map votes can be initiated." return "The game is finished, no map votes can be initiated."
return FALSE
if(rocking_votes >= CONFIG_GET(number/max_rocking_votes)) if(rocking_votes >= CONFIG_GET(number/max_rocking_votes))
message = "The maximum number of times to rock the vote has been reached." return "The maximum number of times to rock the vote has been reached."
return FALSE
if(SSmapping.map_vote_rocked) if(SSmapping.map_vote_rocked)
message = "The vote has already been rocked! Initiate a map vote!" return "The vote has already been rocked! Initiate a map vote!"
return FALSE
if(!SSmapping.map_voted) if(!SSmapping.map_voted)
message = "Rocking the vote is disabled because no map has been voted on yet!" return "Rocking the vote is disabled because no map has been voted on yet!"
return FALSE
if(SSmapping.map_force_chosen) if(SSmapping.map_force_chosen)
message = "Rocking the vote is disabled because an admin has forcibly set the map!" return "Rocking the vote is disabled because an admin has forcibly set the map!"
return FALSE
if(EMERGENCY_ESCAPED_OR_ENDGAMED && SSmapping.map_voted) if(EMERGENCY_ESCAPED_OR_ENDGAMED && SSmapping.map_voted)
message = "The emergency shuttle has already left the station and the next map has already been chosen!" return "The emergency shuttle has already left the station and the next map has already been chosen!"
return FALSE
message = initial(message) return VOTE_AVAILABLE
return TRUE
/datum/vote/rock_the_vote/finalize_vote(winning_option) /datum/vote/rock_the_vote/finalize_vote(winning_option)
rocking_votes++ rocking_votes++

View File

@@ -5,6 +5,7 @@ import {
Box, Box,
Button, Button,
Collapsible, Collapsible,
Dimmer,
Icon, Icon,
LabeledList, LabeledList,
NoticeBox, NoticeBox,
@@ -59,11 +60,13 @@ type Data = {
possibleVotes: Vote[]; possibleVotes: Vote[];
user: UserData; user: UserData;
voting: string[]; voting: string[];
LastVoteTime: number;
VoteCD: number;
}; };
export const VotePanel = (props) => { export const VotePanel = (props) => {
const { data } = useBackend<Data>(); const { act, data } = useBackend<Data>();
const { currentVote, user } = data; const { currentVote, user, LastVoteTime, VoteCD } = data;
/** /**
* Adds the voting type to title if there is an ongoing vote. * Adds the voting type to title if there is an ongoing vote.
@@ -81,7 +84,19 @@ export const VotePanel = (props) => {
<Window title={windowTitle} width={400} height={500}> <Window title={windowTitle} width={400} height={500}>
<Window.Content> <Window.Content>
<Stack fill vertical> <Stack fill vertical>
<Section title="Create Vote"> <Section
title="Create Vote"
buttons={
!!user.isLowerAdmin && (
<Button
icon="refresh"
content="Reset Cooldown"
disabled={LastVoteTime + VoteCD <= 0}
onClick={() => act('resetCooldown')}
/>
)
}
>
<VoteOptions /> <VoteOptions />
{!!user.isLowerAdmin && currentVote && <VotersList />} {!!user.isLowerAdmin && currentVote && <VotersList />}
</Section> </Section>
@@ -93,51 +108,86 @@ export const VotePanel = (props) => {
); );
}; };
const VoteOptionDimmer = (props) => {
const { data } = useBackend<Data>();
const { LastVoteTime, VoteCD } = data;
return (
<Dimmer>
<Box textAlign="center">
<Box fontSize={2} bold>
Vote Cooldown
</Box>
<Box fontSize={1.5}>{Math.floor((VoteCD + LastVoteTime) / 10)}s</Box>
</Box>
</Dimmer>
);
};
/** /**
* The create vote options menu. Only upper admins can disable voting. * The create vote options menu. Only upper admins can disable voting.
* @returns A section visible to everyone with vote options. * @returns A section visible to everyone with vote options.
*/ */
const VoteOptions = (props) => { const VoteOptions = (props) => {
const { act, data } = useBackend<Data>(); const { act, data } = useBackend<Data>();
const { possibleVotes, user } = data; const { possibleVotes, user, LastVoteTime, VoteCD } = data;
return ( return (
<Stack.Item> <Stack.Item>
<Collapsible title="Start a Vote"> <Collapsible title="Start a Vote">
<Stack vertical justify="space-between"> <Section>
{possibleVotes.map((option) => ( {LastVoteTime + VoteCD > 0 && <VoteOptionDimmer />}
<Stack.Item key={option.name}> <Stack vertical justify="space-between">
{!!user.isLowerAdmin && option.config !== VoteConfig.None && ( {possibleVotes.map((option) => (
<Button.Checkbox <Stack.Item key={option.name}>
mr={option.config === VoteConfig.Disabled ? 1 : 1.6} <Stack>
color="red" {!!user.isLowerAdmin && (
checked={option.config === VoteConfig.Enabled} <Stack.Item>
disabled={!user.isUpperAdmin} <Button.Checkbox
content={ width={7}
option.config === VoteConfig.Enabled color="red"
? 'Enabled' checked={option.config === VoteConfig.Enabled}
: 'Disabled' disabled={
} !user.isUpperAdmin ||
onClick={() => option.config === VoteConfig.None
act('toggleVote', { }
voteName: option.name, tooltip={
}) option.config === VoteConfig.None
} ? 'This vote cannot be disabled.'
/> : null
)} }
<Button content={
disabled={!option.canBeInitiated} option.config === VoteConfig.Enabled
tooltip={option.message} ? 'Enabled'
content={option.name} : 'Disabled'
onClick={() => }
act('callVote', { onClick={() =>
voteName: option.name, act('toggleVote', {
}) voteName: option.name,
} })
/> }
</Stack.Item> />
))} </Stack.Item>
</Stack> )}
<Stack.Item>
<Button
width={12}
textAlign={'center'}
disabled={!option.canBeInitiated}
tooltip={option.message}
content={option.name}
onClick={() =>
act('callVote', {
voteName: option.name,
})
}
/>
</Stack.Item>
</Stack>
</Stack.Item>
))}
</Stack>
</Section>
</Collapsible> </Collapsible>
</Stack.Item> </Stack.Item>
); );
@@ -153,11 +203,11 @@ const VotersList = (props) => {
return ( return (
<Stack.Item> <Stack.Item>
<Collapsible <Collapsible
title={`View Voters${ title={`View Active Voters${
data.voting.length ? `: ${data.voting.length}` : '' data.voting.length ? ` (${data.voting.length})` : ''
}`} }`}
> >
<Section height={8} fill scrollable> <Section height={4} fill scrollable>
{data.voting.map((voter) => { {data.voting.map((voter) => {
return <Box key={voter}>{voter}</Box>; return <Box key={voter}>{voter}</Box>;
})} })}
@@ -275,13 +325,26 @@ const TimePanel = (props) => {
{currentVote?.timeRemaining || 0}s {currentVote?.timeRemaining || 0}s
</Box> </Box>
{!!user.isLowerAdmin && ( {!!user.isLowerAdmin && (
<Button <Stack>
color="red" <Stack.Item>
disabled={!user.isLowerAdmin || !currentVote} <Button
onClick={() => act('cancel')} color="green"
> disabled={!user.isLowerAdmin || !currentVote}
Cancel Vote onClick={() => act('endNow')}
</Button> >
End Now
</Button>
</Stack.Item>
<Stack.Item>
<Button
color="red"
disabled={!user.isLowerAdmin || !currentVote}
onClick={() => act('cancel')}
>
Cancel Vote
</Button>
</Stack.Item>
</Stack>
)} )}
</Stack> </Stack>
</Section> </Section>