diff --git a/.github/workflows/byond.yml b/.github/workflows/byond.yml
index ea641188e33..b39f676ab72 100644
--- a/.github/workflows/byond.yml
+++ b/.github/workflows/byond.yml
@@ -15,7 +15,7 @@ on:
env:
MACRO_COUNT: 0
GENDER_COUNT: 6
- TO_WORLD_COUNT: 181
+ TO_WORLD_COUNT: 179
#These variables are filled from dependencies.sh inside the steps, DO NOT SET THEM HERE
BYOND_MAJOR: ""
diff --git a/aurorastation.dme b/aurorastation.dme
index aebcb16fcba..1e9a4f6f566 100644
--- a/aurorastation.dme
+++ b/aurorastation.dme
@@ -338,7 +338,6 @@
#include "code\controllers\subsystems\evacuation\evacuation_pods.dm"
#include "code\controllers\subsystems\evacuation\evacuation_predicate.dm"
#include "code\controllers\subsystems\evacuation\evacuation_shuttle.dm"
-#include "code\controllers\subsystems\evacuation\~evac.dm"
#include "code\controllers\subsystems\initialization\atlas.dm"
#include "code\controllers\subsystems\initialization\atoms.dm"
#include "code\controllers\subsystems\initialization\codex.dm"
@@ -501,6 +500,11 @@
#include "code\datums\uplink\telecrystals.dm"
#include "code\datums\uplink\uplink_categories.dm"
#include "code\datums\uplink\uplink_items.dm"
+#include "code\datums\votes\_vote_datum.dm"
+#include "code\datums\votes\crew_transfer.dm"
+#include "code\datums\votes\custom_vote.dm"
+#include "code\datums\votes\restart.dm"
+#include "code\datums\votes\round_type.dm"
#include "code\datums\wires\airlock.dm"
#include "code\datums\wires\alarm.dm"
#include "code\datums\wires\apc.dm"
diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm
index 81c151856e8..e446d9216b6 100644
--- a/code/__DEFINES/logging.dm
+++ b/code/__DEFINES/logging.dm
@@ -13,6 +13,7 @@
//wrapper macros for easier grepping
#define DIRECT_OUTPUT(A, B) A << B
+#define SEND_SOUND(target, sound) DIRECT_OUTPUT(target, sound)
#define SEND_TEXT(target, text) DIRECT_OUTPUT(target, text)
#define WRITE_FILE(file, text) DIRECT_OUTPUT(file, text)
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 584273f21f7..210b5d826b7 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -271,3 +271,16 @@
#define SS_NO_DISPLAY 128
#define SS_IS_RUNNING(subsystem) (subsystem.can_fire && (GAME_STATE & subsystem.runlevels))
+
+// Vote subsystem counting methods
+/// First past the post. One selection per person, and the selection with the most votes wins.
+#define VOTE_COUNT_METHOD_SINGLE 1
+/// Approval voting. Any number of selections per person, and the selection with the most votes wins.
+#define VOTE_COUNT_METHOD_MULTI 2
+
+/// The choice with the most votes wins. Ties are broken by the first choice to reach that number of votes.
+#define VOTE_WINNER_METHOD_SIMPLE "Simple"
+/// The winning choice is selected randomly based on the number of votes each choice has.
+#define VOTE_WINNER_METHOD_WEIGHTED_RANDOM "Weighted Random"
+/// There is no winner for this vote.
+#define VOTE_WINNER_METHOD_NONE "None"
diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm
index 4190b8b9cea..92b3c7c8035 100644
--- a/code/__HELPERS/_lists.dm
+++ b/code/__HELPERS/_lists.dm
@@ -228,6 +228,32 @@
};\
} while(FALSE)
+/**
+ * Picks a random element from a list based on a weighting system.
+ * For example, given the following list:
+ * A = 6, B = 3, C = 1, D = 0
+ * A would have a 60% chance of being picked,
+ * B would have a 30% chance of being picked,
+ * C would have a 10% chance of being picked,
+ * and D would have a 0% chance of being picked.
+ * You should only pass integers in.
+ */
+/proc/pick_weight(list/list_to_pick)
+ var/total = 0
+ var/item
+ for(item in list_to_pick)
+ if(!list_to_pick[item])
+ list_to_pick[item] = 0
+ total += list_to_pick[item]
+
+ total = rand(1, total)
+ for(item in list_to_pick)
+ total -= list_to_pick[item]
+ if(total <= 0 && list_to_pick[item])
+ return item
+
+ return null
+
///replaces reverseList ~Carnie
/proc/reverse_range(list/inserted_list, start = 1, end = 0)
if(inserted_list.len)
diff --git a/code/_onclick/hud/action.dm b/code/_onclick/hud/action.dm
index 2c7a2b09f53..9e760cefd0b 100644
--- a/code/_onclick/hud/action.dm
+++ b/code/_onclick/hud/action.dm
@@ -44,11 +44,17 @@
return
Remove(owner)
owner = T
- owner.actions.Add(src)
- owner.update_action_buttons()
+ //This shit is because our actions are different than TG ones, remove it when we update the action datum
+ if(istype(T))
+ owner.actions.Add(src)
+ owner.update_action_buttons()
return
/datum/action/proc/Remove(mob/living/T)
+ //This shit is because our actions are different than TG ones, remove it when we update the action datum
+ if(!istype(T))
+ return
+
if(button)
if(T.client)
T.client.screen -= button
diff --git a/code/controllers/subsystems/evacuation/~evac.dm b/code/controllers/subsystems/evacuation/~evac.dm
deleted file mode 100644
index e49dec2b309..00000000000
--- a/code/controllers/subsystems/evacuation/~evac.dm
+++ /dev/null
@@ -1,5 +0,0 @@
-#undef EVAC_IDLE
-#undef EVAC_PREPPING
-#undef EVAC_IN_TRANSIT
-#undef EVAC_COOLDOWN
-#undef EVAC_COMPLETE
diff --git a/code/controllers/subsystems/statpanel.dm b/code/controllers/subsystems/statpanel.dm
index 49d9827488c..71a69917ad8 100644
--- a/code/controllers/subsystems/statpanel.dm
+++ b/code/controllers/subsystems/statpanel.dm
@@ -35,7 +35,7 @@ SUBSYSTEM_DEF(statpanels)
"Round Time: [get_round_duration_formatted()]",
"Ship Time: [worldtime2text()]",
"Current Space Sector: [SSatlas.current_sector.name]",
- "Last Transfer Vote: [SSvote.last_transfer_vote ? time2text(SSvote.last_transfer_vote, "hh:mm") : "Never"]"
+ "Last Transfer Vote: [GLOB.last_transfer_vote ? time2text(GLOB.last_transfer_vote, "hh:mm") : "Never"]"
)
if(eta_status)
global_data += eta_status
diff --git a/code/controllers/subsystems/ticker.dm b/code/controllers/subsystems/ticker.dm
index 74966e91c06..fc2be4f3543 100644
--- a/code/controllers/subsystems/ticker.dm
+++ b/code/controllers/subsystems/ticker.dm
@@ -4,6 +4,9 @@
#define SETUP_REVOTE 1
#define SETUP_REATTEMPT 2
+///The time at which the next automatic transfer vote will be called
+GLOBAL_VAR_INIT(next_transfer_time, null)
+
var/datum/controller/subsystem/ticker/SSticker
/datum/controller/subsystem/ticker
@@ -73,6 +76,9 @@ var/datum/controller/subsystem/ticker/SSticker
pregame()
restart_timeout = GLOB.config.restart_timeout
+ //Initialize the auto-transfer time
+ GLOB.next_transfer_time = GLOB.config.vote_autotransfer_initial
+
return SS_INIT_SUCCESS
/datum/controller/subsystem/ticker/stat_entry(msg)
@@ -137,10 +143,9 @@ var/datum/controller/subsystem/ticker/SSticker
total_players = length(GLOB.player_list)
if (current_state == GAME_STATE_PREGAME && pregame_timeleft == GLOB.config.vote_autogamemode_timeleft)
- if (!SSvote.time_remaining)
- SSvote.autogamemode()
- pregame_timeleft--
- return
+ SSvote.autogamemode()
+ pregame_timeleft--
+ return
if (pregame_timeleft <= 20 && !testmerges_printed)
print_testmerges()
@@ -226,6 +231,13 @@ var/datum/controller/subsystem/ticker/SSticker
to_world("An admin has delayed the round end")
else if(!delay_notified)
to_world("An admin has delayed the round end")
+
+ //If we have not finished the game already, and assuming it's time, call the transfer vote as per config
+ if(!game_finished && !mode_finished && !post_game)
+ if(get_round_duration() >= GLOB.next_transfer_time - 600)
+ SSvote.autotransfer()
+ GLOB.next_transfer_time += GLOB.config.vote_autotransfer_interval
+
return 1
/datum/controller/subsystem/ticker/proc/declare_completion()
@@ -528,12 +540,6 @@ var/datum/controller/subsystem/ticker/SSticker
var/can_start = src.mode.can_start()
if(can_start & GAME_FAILURE_NO_PLAYERS)
- var/list/voted_not_ready = list()
- for(var/mob/abstract/new_player/player in SSvote.round_voters)
- if((player.client)&&(!player.ready))
- voted_not_ready += player.ckey
- message_admins("The following players voted for [mode.name], but did not ready up: [jointext(voted_not_ready, ", ")]")
- log_game("Ticker: Players voted for [mode.name], but did not ready up: [jointext(voted_not_ready, ", ")]")
fail_reasons += "Not enough players, [mode.required_players] player(s) needed"
if(can_start & GAME_FAILURE_NO_ANTAGS)
diff --git a/code/controllers/subsystems/vote.dm b/code/controllers/subsystems/vote.dm
index 321b0828285..008da629c37 100644
--- a/code/controllers/subsystems/vote.dm
+++ b/code/controllers/subsystems/vote.dm
@@ -1,450 +1,405 @@
+/// Define to mimic a span macro but for the purple font that vote specifically uses.
+#define vote_font(text) ("" + text + "")
+
SUBSYSTEM_DEF(vote)
- name = "Voting"
- wait = 1 SECOND
+ name = "Vote"
+ wait = 1 SECONDS
flags = SS_KEEP_TIMING
init_order = INIT_ORDER_VOTE
runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT
- var/next_transfer_time
-
- var/initiator = null
- var/started_time = null
- var/time_remaining = 0
- var/mode = null
- var/question = null
- var/list/choices = list()
+ /// A list of all generated action buttons
+ var/list/datum/action/generated_actions = list()
+ /// All votes that we can possible vote for.
+ var/list/datum/vote/possible_votes = list()
+ /// The vote we're currently voting on.
+ var/datum/vote/current_vote
+ /// A list of all ckeys who have voted for the current vote.
var/list/voted = list()
- var/list/current_votes = list()
- var/auto_muted = 0
- var/last_transfer_vote = null
+ /// A list of all ckeys currently voting for the current vote.
+ var/list/voting = list()
- var/list/round_voters = list()
+/datum/controller/subsystem/vote/Initialize()
+ for(var/vote_type in subtypesof(/datum/vote))
+ var/datum/vote/vote = new vote_type()
+ if(!vote.is_accessible_vote())
+ qdel(vote)
+ continue
-/datum/controller/subsystem/vote/Initialize(timeofday)
- next_transfer_time = GLOB.config.vote_autotransfer_initial
- return SS_INIT_SUCCESS
-
-/datum/controller/subsystem/vote/fire(resumed = FALSE)
- if (mode)
- // No more change mode votes after the game has started.
- if(mode == "gamemode" && ROUND_IS_STARTED)
- to_world("Voting aborted due to game start.")
- reset()
- return
-
- if(((started_time + GLOB.config.vote_period - world.time)) < 0)
- result()
- SStgui.close_uis(src)
- reset()
-
- if (get_round_duration() >= next_transfer_time - 600)
- autotransfer()
- next_transfer_time += GLOB.config.vote_autotransfer_interval
+ possible_votes[vote.name] = vote
return SS_INIT_SUCCESS
-/datum/controller/subsystem/vote/proc/autotransfer()
- initiate_vote("crew_transfer","the server", 1)
- LOG_DEBUG("The server has called a crew transfer vote")
-/datum/controller/subsystem/vote/proc/autogamemode()
- for(var/thing in GLOB.clients)
- var/client/C = thing
- window_flash(C)
- initiate_vote("gamemode","the server", 1)
- LOG_DEBUG("The server has called a gamemode vote")
+// Called by master_controller
+/datum/controller/subsystem/vote/fire()
+ if(!current_vote)
+ return
+ current_vote.time_remaining = round((current_vote.started_time + 600 - world.time) / 10) //Used to be "CONFIG_GET(number/vote_period)" instead of 600
+ if(current_vote.time_remaining < 0)
+ process_vote_result()
+ SStgui.close_uis(src)
+ reset()
+/// Resets all of our vars after votes conclude / are cancelled.
/datum/controller/subsystem/vote/proc/reset()
- initiator = null
- time_remaining = 0
- mode = null
- question = null
- choices.Cut()
voted.Cut()
- current_votes.Cut()
+ voting.Cut()
+
+ current_vote?.reset()
+ current_vote = null
+
+ QDEL_LIST(generated_actions)
+
SStgui.update_uis(src)
-/datum/controller/subsystem/vote/proc/get_result()
- //get the highest number of votes
- var/greatest_votes = 0
+/**
+ * Process the results of the vote.
+ * Collects all the winners, breaks any ties that occur,
+ * prints the results of the vote to the world,
+ * and finally follows through with the effects of the vote.
+ */
+/datum/controller/subsystem/vote/proc/process_vote_result()
+
+ // First collect all the non-voters we have.
+ var/list/non_voters = GLOB.directory.Copy() - voted
+ // Remove AFK or clientless non-voters.
+ for(var/non_voter_ckey in non_voters)
+ var/client/non_voter_client = non_voters[non_voter_ckey]
+ if(!istype(non_voter_client) || non_voter_client.is_afk())
+ non_voters -= non_voter_ckey
+
+ // Now get the result of the vote.
+ // This is a list, as we could have a tie (multiple winners).
+ var/list/winners = current_vote.get_vote_result(non_voters)
+
+ // Now we should determine who actually won the vote.
+ var/final_winner
+ // 1 winner? That's the winning option
+ if(length(winners) == 1)
+ final_winner = winners[1]
+
+ // More than 1 winner? Tiebreaker between all the winners
+ else if(length(winners) > 1)
+ final_winner = current_vote.tiebreaker(winners)
+
+ // Announce the results of the vote to the world.
+ var/to_display = current_vote.get_result_text(winners, final_winner, non_voters)
+
var/total_votes = 0
- for(var/option in choices)
- var/votes = choices[option]["votes"]
- total_votes += votes
- if(votes > greatest_votes)
- greatest_votes = votes
- //default-vote for everyone who didn't vote
- if(!GLOB.config.vote_no_default && choices.len)
- var/non_voters = (GLOB.clients.len - total_votes)
- if(non_voters > 0)
- if(mode == "restart")
- choices["Continue Playing"]["votes"] += non_voters
- if(choices["Continue Playing"]["votes"] >= greatest_votes)
- greatest_votes = choices["Continue Playing"]["votes"]
- else if(mode == "gamemode")
- if(GLOB.master_mode in choices)
- choices[GLOB.master_mode]["votes"] += non_voters
- if(choices[GLOB.master_mode]["votes"] >= greatest_votes)
- greatest_votes = choices[GLOB.master_mode]["votes"]
- else if(mode == "crew_transfer")
- var/factor = 0.5
- switch(get_round_duration() / (10 * 60)) // minutes
- if(0 to 60)
- factor = 0.5
- if(61 to 120)
- factor = 0.8
- if(121 to 240)
- factor = 1
- if(241 to 300)
- factor = 1.2
- else
- factor = 1.4
- choices["Initiate Crew Transfer"]["votes"] = round(choices["Initiate Crew Transfer"]["votes"] * factor)
- to_world("Crew Transfer Factor: [factor]")
- greatest_votes = max(choices["Initiate Crew Transfer"]["votes"], choices["Continue The Round"]["votes"])
+ var/list/vote_choice_data = list()
+ for(var/choice in current_vote.choices)
+ var/choice_votes = current_vote.choices[choice]
+ total_votes += choice_votes
+ vote_choice_data["[choice]"] = choice_votes
- if(mode == "crew_transfer")
- if(round(get_round_duration() / 36000)+12 <= 14)
- // Credit to Scopes @ oldcode.
- to_world("Majority voting rule in effect. 2/3rds majority needed to initiate transfer.")
- choices["Initiate Crew Transfer"]["votes"] = round(choices["Initiate Crew Transfer"]["votes"] - round(total_votes / 3))
- greatest_votes = max(choices["Initiate Crew Transfer"]["votes"], choices["Continue The Round"]["votes"])
+ // stringify the winners to prevent potential unimplemented serialization errors.
+ // Perhaps this can be removed in the future and we assert that vote choices must implement serialization.
+ var/final_winner_string = final_winner && "[final_winner]"
+ var/list/winners_string = list()
+ for(var/winner in winners)
+ winners_string += "[winner]"
- //get all options with that many votes and return them in a list
- . = list()
- if(greatest_votes)
- for(var/option in choices)
- if(choices[option]["votes"] == greatest_votes)
- . += option
+ var/list/vote_log_data = list(
+ "choices" = vote_choice_data,
+ "total" = total_votes,
+ "winners" = winners_string,
+ "final_winner" = final_winner_string,
+ )
+ var/log_string = replacetext(to_display, "\n", "\\n") // 'keep' the newlines, but dont actually print them as newlines
+ log_vote(log_string, vote_log_data)
+ to_chat(world, SPAN_INFO(vote_font("\n[to_display]")))
-/datum/controller/subsystem/vote/proc/announce_result()
- var/list/winners = get_result()
- var/text
- if(winners.len > 0)
- if(winners.len > 1)
- if(mode != "gamemode" || SSticker.hide_mode == 0) // Here we are making sure we don't announce potential game modes
- text = "Vote Tied Between:\n"
- for(var/option in winners)
- text += "\t[option]\n"
- . = pick(winners)
+ // Finally, doing any effects on vote completion
+ if (final_winner) // if no one voted, or the vote cannot be won, final_winner will be null
+ current_vote.finalize_vote(final_winner)
- for(var/key in current_votes)
- if(current_votes[key] == .)
- round_voters += key // Keep track of who voted for the winning round.
+/**
+ * One selection per person, and the selection with the most votes wins.
+ */
+/datum/controller/subsystem/vote/proc/submit_single_vote(mob/voter, their_vote)
+ if(!current_vote)
+ return
+ if(!voter?.ckey)
+ return
+ if(FALSE && voter.stat == DEAD && !voter.client?.holder) //Used to be "CONFIG_GET(flag/no_dead_vote)"
+ return
- if((mode == "gamemode" && . == "Extended") || SSticker.hide_mode == 0) // Announce Extended gamemode, but not other gamemodes
- text += "Vote Result: [.]"
- else
- if(mode != "gamemode")
- text += "Vote Result: [.]"
- else
- text += "The vote has ended." // What will be shown if it is a gamemode vote that isn't extended
+ // If user has already voted, remove their specific vote
+ if(voter.ckey in current_vote.choices_by_ckey)
+ var/their_old_vote = current_vote.choices_by_ckey[voter.ckey]
+ current_vote.choices[their_old_vote]--
else
- text += "Vote Result: Inconclusive - No Votes!"
- if(mode == "add_antagonist")
- antag_add_failed = 1
- log_vote(text)
- to_world("[text]")
+ voted += voter.ckey
-/datum/controller/subsystem/vote/proc/result()
- . = announce_result()
- var/restart = 0
- if(.)
- switch(mode)
- if("restart")
- if(. == "Restart Round")
- restart = 1
- if("gamemode")
- if(GLOB.master_mode != .)
- SSpersistent_configuration.last_gamemode = .
- if(SSticker.mode)
- restart = 1
- else
- GLOB.master_mode = .
- if("crew_transfer")
- if(. == "Initiate Crew Transfer")
- init_shift_change(null, 1)
- last_transfer_vote = get_round_duration()
- if("add_antagonist")
- if(isnull(.) || . == "None")
- antag_add_failed = 1
- else
- GLOB.additional_antag_types |= GLOB.antag_names_to_ids[.]
+ current_vote.choices_by_ckey[voter.ckey] = their_vote
+ current_vote.choices[their_vote]++
- if(mode == "gamemode") //fire this even if the vote fails.
- if(!GLOB.round_progressing)
- GLOB.round_progressing = 1
- to_world("The round will start soon.")
+ return TRUE
- if(restart)
- to_world("World restarting due to vote...")
- feedback_set_details("end_error","restart vote")
- sleep(50)
- log_game("Rebooting due to restart vote")
- world.Reboot()
+/**
+ * Any number of selections per person, and the selection with the most votes wins.
+ */
+/datum/controller/subsystem/vote/proc/submit_multi_vote(mob/voter, their_vote)
+ if(!current_vote)
+ return
+ if(!voter?.ckey)
+ return
+ if(FALSE && voter.stat == DEAD && !voter.client?.holder) //Used to be "CONFIG_GET(flag/no_dead_vote)"
+ return
-/datum/controller/subsystem/vote/proc/submit_vote(ckey, vote)
- if(mode)
- if (mode == "crew_transfer")
- if(GLOB.config.vote_no_dead && usr && !usr.client.holder)
- if (isnewplayer(usr))
- to_chat(usr, "You must be playing or have been playing to start a vote.")
- return 0
- else if (isobserver(usr))
- var/mob/abstract/observer/O = usr
- if (O.started_as_observer)
- to_chat(usr, "You must be playing or have been playing to start a vote.")
- return 0
- if(vote in choices)
- if(current_votes[ckey])
- choices[current_votes[ckey]]["votes"]--
- var/vote_descriptor = ""
- if(isnewplayer(usr))
- vote_descriptor = ", from the lobby"
- else if(isobserver(usr))
- vote_descriptor = ", as an observer"
- log_vote("[ckey] submitted their vote for: [vote][vote_descriptor]")
- voted += usr.ckey
- choices[vote]["votes"]++ //check this
- current_votes[ckey] = vote
- return 1
- return 0
+ else
+ voted += voter.ckey
+ if(current_vote.choices_by_ckey[voter.ckey + their_vote] == 1)
+ current_vote.choices_by_ckey[voter.ckey + their_vote] = 0
+ current_vote.choices[their_vote]--
-/datum/controller/subsystem/vote/proc/initiate_vote(vote_type, initiator_key, automatic = FALSE)
- if(!mode)
- if(started_time != null && !(check_rights(R_ADMIN|R_MOD) || automatic))
- // Transfer votes are their own little special snowflake
- var/next_allowed_time = 0
- if (vote_type == "crew_transfer")
- if (GLOB.config.vote_no_dead && !usr.client.holder)
- if (isnewplayer(usr))
- to_chat(usr, "You must be playing or have been playing to start a vote.")
- return 0
- else if (isobserver(usr))
- var/mob/abstract/observer/O = usr
- if (O.started_as_observer)
- to_chat(usr, "You must be playing or have been playing to start a vote.")
- return 0
+ else
+ current_vote.choices_by_ckey[voter.ckey + their_vote] = 1
+ current_vote.choices[their_vote]++
- if (last_transfer_vote)
- next_allowed_time = (last_transfer_vote + GLOB.config.vote_delay)
- else
- next_allowed_time = GLOB.config.transfer_timeout
- else
- next_allowed_time = (started_time + GLOB.config.vote_delay)
+ return TRUE
- if(next_allowed_time > get_round_duration())
- return 0
+/**
+ * Initiates a vote, allowing all players to vote on something.
+ *
+ * * 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.
+ */
+/datum/controller/subsystem/vote/proc/initiate_vote(vote_type, vote_initiator_name, mob/vote_initiator, forced = FALSE)
- //reset()
- switch(vote_type)
- if("restart")
- AddChoice("Restart Round")
- AddChoice("Continue Playing")
- if("gamemode")
- round_voters.Cut() //Delete the old list, since we are having a new gamemode vote
- if(SSticker.current_state >= 2)
- return 0
- for (var/F in GLOB.config.votable_modes)
- var/datum/game_mode/M = GLOB.gamemode_cache[F]
- if(!M)
- continue
- AddChoice(F, capitalize(M.name), "", M.required_players)
- AddChoice(ROUNDTYPE_STR_SECRET, "Secret")
- if(ROUNDTYPE_STR_MIXED_SECRET in choices)
- AddChoice(ROUNDTYPE_STR_MIXED_SECRET, "Mixed Secret")
- if("crew_transfer")
- if(check_rights(R_ADMIN|R_MOD, 0))
- question = "End the shift?"
- AddChoice("Initiate Crew Transfer")
- AddChoice("Continue The Round")
- else
- if (get_security_level() == "red" || get_security_level() == "delta")
- to_chat(initiator_key, "The current alert status is too high to call for a crew transfer!")
- return 0
- if(SSticker.current_state <= 2)
- to_chat(initiator_key, "The crew transfer button has been disabled!")
- return 0
- question = "End the shift?"
- AddChoice("Initiate Crew Transfer")
- AddChoice("Continue The Round")
- if("add_antagonist")
- if(!GLOB.config.allow_extra_antags || SSticker.current_state >= 2)
- return 0
- for(var/antag_type in GLOB.all_antag_types)
- var/datum/antagonist/antag = GLOB.all_antag_types[antag_type]
- if(!(antag.id in GLOB.additional_antag_types) && antag.is_votable())
- AddChoice(antag.role_text)
- AddChoice("None")
- if("custom")
- question = tgui_input_text(usr, "What is the vote for?")
- if(!question)
- return 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
- for(var/i=1,i<=10,i++)
- var/option = capitalize(sanitize(input(usr,"Please enter an option or hit cancel to finish") as text|null))
- if(!option || mode || !usr.client) break
- AddChoice(option)
- else
- return 0
- mode = vote_type
- initiator = initiator_key
- started_time = world.time
- var/text = "[capitalize(mode)] vote started by [initiator]."
- if(mode == "custom")
- text += "\n[sanitizeSafe(question)]"
+ // 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 || !!(vote_initiator.client?.holder?.rights & (R_ADMIN)) //Used GLOB.admin_datum
- log_vote(text)
- for(var/cc in GLOB.clients)
- var/client/C = cc
- to_chat(C, "[text]\nType vote or click here to place your votes.\nYou have [GLOB.config.vote_period/10] seconds to vote.")
- if(C.prefs.sfx_toggles & ASFX_VOTE) //Personal mute
- switch(vote_type)
- if("crew_transfer")
- sound_to(C, sound('sound/effects/vote.ogg', repeat = 0, wait = 0, volume = 50, channel = 3))
- if("gamemode")
- sound_to(C, sound('sound/ambience/vote_alarm.ogg', repeat = 0, wait = 0, volume = 50, channel = 3))
- if("custom")
- sound_to(C, sound('sound/ambience/vote_alarm.ogg', repeat = 0, wait = 0, volume = 50, channel = 3))
- if(mode == "gamemode" && GLOB.round_progressing)
- GLOB.round_progressing = 0
- to_world("Round start has been delayed.")
- SStgui.update_uis(src)
- return 1
- return 0
+ 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
-/datum/controller/subsystem/vote/proc/AddChoice(name, display_name, extra_text = "", required_players = 0)
- if(!display_name)
- display_name = name
- choices[name] = list(
- "name" = display_name,
- "extra" = extra_text,
- "required_players" = required_players,
- "votes" = 0
- )
+ // Get our actual datum
+ var/datum/vote/to_vote
+ // If we were passed a path: find the path in possible_votes
+ if(ispath(vote_type, /datum/vote))
+ var/datum/vote/vote_path = vote_type
+ to_vote = possible_votes[initial(vote_path.name)]
-/datum/controller/subsystem/vote/ui_state(mob/user)
+ // If we were passed an instance: use the instance
+ else if(istype(vote_type, /datum/vote))
+ to_vote = vote_type
+
+ // If we got neither a path or an instance, it could be a vote name, but is likely just an error / null
+ else
+ to_vote = possible_votes[vote_type]
+ if(!to_vote)
+ stack_trace("Voting initiate_vote was passed an invalid vote type. (Got: [vote_type || "null"])")
+
+ // No valid vote found? No vote
+ if(!istype(to_vote))
+ if(vote_initiator)
+ to_chat(vote_initiator, SPAN_WARNING("Invalid voting choice."))
+ return FALSE
+
+ // Vote can't be initiated in our circumstances? No vote
+ if(!to_vote.can_be_initiated(vote_initiator, unlimited_vote_power))
+ return FALSE
+
+ // Okay, we're ready to actually create a vote -
+ // Do a reset, just to make sure
+ reset()
+
+ // Try to create the vote. If the creation fails, no vote
+ if(!to_vote.create_vote(vote_initiator))
+ return FALSE
+
+ // Okay, the vote's happening now, for real. Set it up.
+ current_vote = to_vote
+
+ var/duration = 600 //As above, used to be "CONFIG_GET(number/vote_period)"
+ var/to_display = current_vote.initiate_vote(vote_initiator_name, duration)
+
+ log_vote(to_display)
+ to_chat(world, SPAN_INFO(vote_font("\n[SPAN_BOLD(to_display)]\n\
+ Type vote or click here to place your votes.\n\
+ You have [DisplayTimeText(duration)] to vote.")))
+
+ // And now that it's going, give everyone a voter action
+ for(var/client/new_voter as anything in GLOB.clients)
+ var/datum/action/vote/voting_action = new()
+ voting_action.name = "Vote: [current_vote.override_question || current_vote.name]"
+ voting_action.Grant(new_voter.mob)
+
+ //new_voter.player_details.player_actions += voting_action
+ generated_actions += voting_action
+
+ if(current_vote.vote_sound && (new_voter.prefs.sfx_toggles & ASFX_VOTE)) //Used "new_voter.prefs.read_preference(/datum/preference/toggle/sound_announcements)" but we don't have it
+ SEND_SOUND(new_voter, sound(current_vote.vote_sound))
+
+ return TRUE
+
+/datum/controller/subsystem/vote/ui_state()
return GLOB.always_state
-/datum/controller/subsystem/vote/ui_status(mob/user, datum/ui_state/state)
- return UI_INTERACTIVE
-
/datum/controller/subsystem/vote/ui_interact(mob/user, datum/tgui/ui)
+ // Tracks who is currently voting
+ voting |= user.client?.ckey
ui = SStgui.try_update_ui(user, src, ui)
- if (!ui)
- ui = new(user, src, "Voting", "Voting", 400, 500)
+ if(!ui)
+ ui = new(user, src, "VotePanel")
ui.open()
-/datum/controller/subsystem/vote/Topic(href, href_list)
- . = ..()
- if(href_list["open"])
- SSvote.ui_interact(usr)
- return TRUE
+/datum/controller/subsystem/vote/ui_data(mob/user)
+ var/list/data = list()
-/datum/controller/subsystem/vote/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ var/is_lower_admin = (user.client?.holder?.rights & (R_ADMIN | R_MOD))
+ var/is_upper_admin = (user.client?.holder?.rights & (R_ADMIN)) // Used "check_rights_for(user.client, R_ADMIN)" but we don't have that
+
+ data["user"] = list(
+ "ckey" = user.client?.ckey,
+ "isLowerAdmin" = is_lower_admin,
+ "isUpperAdmin" = is_upper_admin,
+ // What the current user has selected in any ongoing votes.
+ "singleSelection" = current_vote?.choices_by_ckey[user.client?.ckey],
+ "multiSelection" = current_vote?.choices_by_ckey,
+ )
+
+ data["voting"]= is_lower_admin ? voting : list()
+
+ var/list/all_vote_data = list()
+ for(var/vote_name in possible_votes)
+ var/datum/vote/vote = possible_votes[vote_name]
+ if(!istype(vote))
+ continue
+
+ var/list/vote_data = list(
+ "name" = vote_name,
+ "canBeInitiated" = vote.can_be_initiated(forced = is_lower_admin),
+ "config" = vote.is_config_enabled(),
+ "message" = vote.message,
+ )
+
+ if(vote == current_vote)
+ var/list/choices = list()
+ for(var/key in current_vote.choices)
+ choices += list(list(
+ "name" = key,
+ "votes" = current_vote.choices[key],
+ ))
+
+ data["currentVote"] = list(
+ "name" = current_vote.name,
+ "question" = current_vote.override_question,
+ "timeRemaining" = current_vote.time_remaining,
+ "countMethod" = current_vote.count_method,
+ "choices" = choices,
+ "vote" = vote_data,
+ )
+
+ all_vote_data += list(vote_data)
+
+ data["possibleVotes"] = all_vote_data
+
+ return data
+
+/datum/controller/subsystem/vote/ui_act(action, params)
. = ..()
if(.)
return
- . = TRUE
- var/isstaff = ui.user.client.holder && (ui.user.client.holder.rights & (R_ADMIN|R_MOD))
+ var/mob/voter = usr
switch(action)
if("cancel")
- if(isstaff)
- reset()
- return TRUE
- if("toggle_restart")
- if(isstaff)
- GLOB.config.allow_vote_restart = !GLOB.config.allow_vote_restart
- SStgui.update_uis(src)
- return TRUE
- if("toggle_gamemode")
- if(isstaff)
- GLOB.config.allow_vote_mode = !GLOB.config.allow_vote_mode
- SStgui.update_uis(src)
- return TRUE
- if("restart")
- if(isstaff)
- initiate_vote("restart", ui.user.key)
- return TRUE
- else if (GLOB.config.allow_vote_restart)
- var/admin_number_present = 0
- var/admin_number_afk = 0
+ if(!(voter.client?.holder?.rights & (R_ADMIN | R_MOD)))
+ return
- for (var/s in GLOB.staff)
- var/client/X = s
- if (X.holder.rights & R_ADMIN)
- admin_number_present++
- if (X.is_afk())
- admin_number_afk++
- if (X.prefs.toggles & SOUND_ADMINHELP)
- sound_to(X, 'sound/effects/adminhelp.ogg')
-
- if ((admin_number_present - admin_number_afk) <= 0)
- initiate_vote("restart", ui.user.key)
- return TRUE
- else
- log_and_message_admins("tried to start a restart vote.", usr, null)
- to_chat(ui.user, "There are active admins around! You cannot start a restart vote due to this.")
- if("gamemode")
- if(GLOB.config.allow_vote_mode || isstaff)
- initiate_vote("gamemode", ui.user.key)
- return TRUE
- if("crew_transfer")
- if(GLOB.config.allow_vote_restart || isstaff)
- initiate_vote("crew_transfer", ui.user.key)
- return TRUE
- if("add_antagonist")
- if(!antag_add_failed && GLOB.config.allow_extra_antags)
- initiate_vote("add_antagonist", ui.user.key)
- return TRUE
- if("custom")
- if(isstaff)
- initiate_vote("custom", ui.user.key)
- return TRUE
- if("vote")
- var/list/T = params["vote"]
- var/our_vote = T["choice"]
- if(our_vote)
- if(submit_vote(ui.user.ckey, our_vote))
- SStgui.update_uis(src)
+ message_admins("[key_name_admin(voter)] has cancelled the current vote.")
+ reset()
return TRUE
-/datum/controller/subsystem/vote/ui_data(mob/user)
- var/list/data = list()
- if(choices.len != LAZYLEN(data["choices"]))
- data["choices"] = list()
- for(var/choice in choices)
- data["choices"] += list(list(
- "choice" = choice,
- "votes" = choices[choice]["votes"],
- "extra" = choices[choice]["extra"],
- "required_players" = choices[choice]["required_players"],
- ))
+ if("toggleVote")
+ var/datum/vote/selected = possible_votes[params["voteName"]]
+ if(!istype(selected))
+ return
- data["mode"] = mode
- data["voted"] = current_votes[user.ckey]
- data["endtime"] = started_time + GLOB.config.vote_period
- data["allow_vote_restart"] = GLOB.config.allow_vote_restart
- data["allow_vote_mode"] = GLOB.config.allow_vote_mode
- data["allow_extra_antags"] = (!antag_add_failed && GLOB.config.allow_extra_antags)
+ return selected.toggle_votable(voter)
- if(!question)
- data["question"] = capitalize(mode)
- else
- data["question"] = question
- data["is_staff"] = user.client.holder && (user.client.holder.rights & (R_ADMIN|R_MOD))
- var/slevel = get_security_level()
- data["is_code_red"] = (slevel == "red" || slevel == "delta")
- data["total_players"] = SSticker.total_players
- data["total_players_ready"] = SSticker.total_players_ready
- return data
+ if("callVote")
+ var/datum/vote/selected = possible_votes[params["voteName"]]
+ if(!istype(selected))
+ return
+ // 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)
+
+ if("voteSingle")
+ return submit_single_vote(voter, params["voteOption"])
+
+ if("voteMulti")
+ return submit_multi_vote(voter, params["voteOption"])
+
+/datum/controller/subsystem/vote/ui_close(mob/user)
+ voting -= user.client?.ckey
+
+/*#########################
+ Aurora snowflake area
+###########################*/
+
+/datum/controller/subsystem/vote/proc/autogamemode()
+ initiate_vote(/datum/vote/gamemode, "Server", null, TRUE)
+
+/datum/controller/subsystem/vote/proc/autotransfer()
+ initiate_vote(/datum/vote/crewtransfer, "Server", null, TRUE)
+
+/*##############################
+ Aurora snowflake area end
+################################*/
+
+/// Mob level verb that allows players to vote on the current vote.
/mob/verb/vote()
set category = "OOC"
set name = "Vote"
- SSvote.ui_interact(src)
+ SSvote.ui_interact(usr)
+
+/// Datum action given to mobs that allows players to vote on the current vote.
+/datum/action/vote
+ name = "Vote!"
+ button_icon_state = "vote"
+ //show_to_observers = FALSE
+
+/datum/action/vote/IsAvailable(feedback = FALSE)
+ return TRUE // Democracy is always available to the free people
+
+/datum/action/vote/Trigger(trigger_flags)
+ . = ..()
+ if(!.)
+ return
+
+ owner.vote()
+ Remove(owner)
+
+// We also need to remove our action from the player actions when we're cleaning up.
+/datum/action/vote/Remove(mob/removed_from)
+ // if(removed_from.client)
+ // removed_from.client?.player_details.player_actions -= src
+
+ // else if(removed_from.ckey)
+ // var/datum/player_details/associated_details = GLOB.player_details[removed_from.ckey]
+ // associated_details?.player_actions -= src
+
+ return ..()
+
+#undef vote_font
diff --git a/code/datums/votes/_vote_datum.dm b/code/datums/votes/_vote_datum.dm
new file mode 100644
index 00000000000..dc301a552c6
--- /dev/null
+++ b/code/datums/votes/_vote_datum.dm
@@ -0,0 +1,250 @@
+
+/**
+ * # Vote Singleton
+ *
+ * A singleton datum that represents a type of vote for the voting subsystem.
+ */
+/datum/vote
+ /// The name of the vote.
+ var/name
+ /// If supplied, an override question will be displayed instead of the name of the vote.
+ var/override_question
+ /// The sound effect played to everyone when this vote is initiated.
+ var/vote_sound = 'sound/effects/vote.ogg'
+ /// A list of default choices we have for this vote.
+ 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."
+
+ // 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.
+ var/count_method = VOTE_COUNT_METHOD_SINGLE
+ /// The method for selecting a winner.
+ var/winner_method = VOTE_WINNER_METHOD_SIMPLE
+
+/**
+ * Used to determine if this vote is a possible
+ * vote type for the vote subsystem.
+ *
+ * If FALSE is returned, this vote singleton
+ * will not be created when the vote subsystem initializes,
+ * meaning no one will be able to hold this vote.
+ */
+/datum/vote/proc/is_accessible_vote()
+ return !!length(default_choices)
+
+/**
+ * Resets our vote to its default state.
+ */
+/datum/vote/proc/reset()
+ SHOULD_CALL_PARENT(TRUE)
+
+ choices.Cut()
+ choices_by_ckey.Cut()
+ started_time = null
+ time_remaining = null
+
+/**
+ * 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
+
+/**
+ * If this vote has a config associated, returns its value (True or False, usually).
+ * If it has no config, returns -1.
+ */
+/datum/vote/proc/is_config_enabled()
+ return -1
+
+/**
+ * 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.
+ */
+/datum/vote/proc/can_be_initiated(mob/by_who, forced = FALSE)
+ SHOULD_CALL_PARENT(TRUE)
+
+ if(started_time)
+ var/next_allowed_time = (started_time + GLOB.config.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
+
+ message = initial(message)
+ return TRUE
+
+/**
+ * Called prior to the vote being initiated.
+ *
+ * Return FALSE to prevent the vote from being initiated.
+ */
+/datum/vote/proc/create_vote(mob/vote_creator)
+ SHOULD_CALL_PARENT(TRUE)
+
+ for(var/key in default_choices)
+ choices[key] = 0
+
+ return TRUE
+
+/**
+ * Called when this vote is actually initiated.
+ *
+ * Return a string - the text displayed to the world when the vote is initiated.
+ */
+/datum/vote/proc/initiate_vote(initiator, duration)
+ SHOULD_CALL_PARENT(TRUE)
+
+ started_time = world.time
+ time_remaining = round(duration / 10)
+
+ return "[contains_vote_in_name ? "[capitalize(name)]" : "[capitalize(name)] vote"] started by [initiator || "Central Command"]."
+
+/**
+ * Gets the result of the vote.
+ *
+ * non_voters - a list of all ckeys who didn't vote in the vote.
+ *
+ * Returns a list of all options that won.
+ * If there were no votes at all, the list will be length = 0, non-null.
+ * If only one option one, the list will be length = 1.
+ * If there was a tie, the list will be length > 1.
+ */
+/datum/vote/proc/get_vote_result(list/non_voters)
+ RETURN_TYPE(/list)
+ SHOULD_CALL_PARENT(TRUE)
+
+ switch(winner_method)
+ if(VOTE_WINNER_METHOD_NONE)
+ return list()
+ if(VOTE_WINNER_METHOD_SIMPLE)
+ return get_simple_winner()
+ if(VOTE_WINNER_METHOD_WEIGHTED_RANDOM)
+ return get_random_winner()
+
+ stack_trace("invalid select winner method: [winner_method]. Defaulting to simple.")
+ return get_simple_winner()
+
+/// Gets the winner of the vote, selecting the choice with the most votes.
+/datum/vote/proc/get_simple_winner()
+ var/highest_vote = 0
+ var/list/current_winners = list()
+
+ for(var/option in choices)
+ var/vote_count = choices[option]
+ if(vote_count < highest_vote)
+ continue
+
+ if(vote_count > highest_vote)
+ highest_vote = vote_count
+ current_winners = list(option)
+ continue
+ current_winners += option
+
+ return length(current_winners) ? current_winners : list()
+
+/// Gets the winner of the vote, selecting a random choice from all choices based on their vote count.
+/datum/vote/proc/get_random_winner()
+ var/winner = pick_weight(choices)
+ return winner ? list(winner) : list()
+
+/**
+ * Gets the resulting text displayed when the vote is completed.
+ *
+ * all_winners - list of all options that won. Can be multiple, in the event of ties.
+ * real_winner - the option that actually won.
+ * non_voters - a list of all ckeys who didn't vote in the vote.
+ *
+ * Return a formatted string of text to be displayed to everyone.
+ */
+/datum/vote/proc/get_result_text(list/all_winners, real_winner, list/non_voters)
+ var/returned_text = ""
+ if(override_question)
+ returned_text += SPAN_BOLD(override_question)
+ else
+ returned_text += SPAN_BOLD("[capitalize(name)] Vote")
+
+ returned_text += "\nWinner Selection: "
+ switch(winner_method)
+ if(VOTE_WINNER_METHOD_NONE)
+ returned_text += "None"
+ if(VOTE_WINNER_METHOD_WEIGHTED_RANDOM)
+ returned_text += "Weighted Random"
+ else
+ returned_text += "Simple"
+
+ var/total_votes = 0 // for determining percentage of votes
+ for(var/option in choices)
+ total_votes += choices[option]
+
+ if(total_votes <= 0)
+ return SPAN_BOLD("Vote Result: Inconclusive - No Votes!")
+
+ returned_text += "\nResults:"
+ for(var/option in choices)
+ returned_text += "\n"
+ var/votes = choices[option]
+ var/percentage_text = ""
+ if(votes > 0)
+ var/actual_percentage = round((votes / total_votes) * 100, 0.1)
+ var/text = "[actual_percentage]"
+ var/spaces_needed = 5 - length(text)
+ for(var/_ in 1 to spaces_needed)
+ returned_text += " "
+ percentage_text += "[text]%"
+ else
+ percentage_text = " 0%"
+ returned_text += "[percentage_text] | [SPAN_BOLD(option)]: [choices[option]]"
+
+ if(!real_winner) // vote has no winner or cannot be won, but still had votes
+ return returned_text
+
+ returned_text += "\n"
+ returned_text += get_winner_text(all_winners, real_winner, non_voters)
+
+ return returned_text
+
+/**
+ * Gets the text that displays the winning options within the result text.
+ *
+ * all_winners - list of all options that won. Can be multiple, in the event of ties.
+ * real_winner - the option that actually won.
+ * non_voters - a list of all ckeys who didn't vote in the vote.
+ *
+ * Return a formatted string of text to be displayed to everyone.
+ */
+/datum/vote/proc/get_winner_text(list/all_winners, real_winner, list/non_voters)
+ var/returned_text = ""
+ if(length(all_winners) > 1)
+ returned_text += "\n[SPAN_BOLD("Vote Tied Between:")]"
+ for(var/a_winner in all_winners)
+ returned_text += "\n\t[a_winner]"
+
+ returned_text += SPAN_BOLD("\nVote Result: [real_winner]")
+ return returned_text
+
+/**
+ * How this vote handles a tiebreaker between multiple winners.
+ */
+/datum/vote/proc/tiebreaker(list/winners)
+ return pick(winners)
+
+/**
+ * Called when a vote is actually all said and done.
+ * Apply actual vote effects here.
+ */
+/datum/vote/proc/finalize_vote(winning_option)
+ return
diff --git a/code/datums/votes/crew_transfer.dm b/code/datums/votes/crew_transfer.dm
new file mode 100644
index 00000000000..bacb8eae55f
--- /dev/null
+++ b/code/datums/votes/crew_transfer.dm
@@ -0,0 +1,73 @@
+///Snowflake var to know when it's ok to vote the transfer, and show in the statpanel the last time it happened
+GLOBAL_VAR(last_transfer_vote)
+
+/datum/vote/crewtransfer
+ name = "Crew Transfer"
+ message = "Initiate crew transfer"
+ winner_method = VOTE_WINNER_METHOD_NONE //We handle this ourself
+ default_choices = list("Initiate Crew Transfer", "Continue The Round")
+
+
+/datum/vote/crewtransfer/can_be_initiated(mob/by_who, forced)
+ . = ..()
+ //Transfer already in progress, noone can call another vote
+ if(SSatlas.current_map.shuttle_call_restart_timer || (evacuation_controller.state != EVAC_IDLE))
+ to_chat(by_who, SPAN_NOTICE("Transfer or evacuation are already in progress."))
+ return FALSE
+
+ //If in lobby, roundend, setup or whatever else
+ if(SSticker.current_state != GAME_STATE_PLAYING)
+ to_chat(by_who, SPAN_NOTICE("The game is not running yet or has already finished."))
+ return FALSE
+
+ //Admins, system and whatnot can always start this vote otherwise
+ if(forced)
+ return TRUE
+
+ if(GLOB.security_level >= SEC_LEVEL_RED)
+ to_chat(by_who, "The current alert status is too high to call for a crew transfer!")
+ return FALSE
+
+ //If enought time from the start passed, allows the vote
+ var/next_allowed_time = 0
+ if(GLOB.last_transfer_vote)
+ next_allowed_time = (GLOB.last_transfer_vote + GLOB.config.vote_delay)
+ else
+ next_allowed_time = GLOB.config.transfer_timeout
+
+ if(next_allowed_time > get_round_duration()) //Sorry, not the time yet
+ to_chat(by_who, SPAN_NOTICE("Time left until the crew transfer can be voted: [next_allowed_time - get_round_duration()]"))
+ return FALSE
+ else
+ return TRUE //We did it bro, we can vote the transfer now!
+
+
+/datum/vote/crewtransfer/get_vote_result(list/non_voters)
+ SHOULD_CALL_PARENT(FALSE)
+
+ //Calculate the factor based on the duration in minutes
+ var/factor = 0.5
+ switch(get_round_duration() / (10 * 60)) // minutes
+ if(0 to 180) //Up to 3 hours
+ factor = 0.67 //2/3rd, rounded up from 0.6 periodic
+ else
+ factor = 1.0 //Equal weight
+
+ var/list/result = null
+ if((choices["Initiate Crew Transfer"]*factor) >= (choices["Continue The Round"]))
+ result = list("Initiate Crew Transfer")
+ else
+ result = list("Continue The Round")
+
+ if(round(get_round_duration() / 36000)+12 <= 14)
+ to_world(SPAN_VOTE("Majority voting rule in effect. 2/3rds majority needed to initiate transfer."))
+
+ return result
+
+
+/datum/vote/crewtransfer/finalize_vote(winning_option)
+ to_world(SPAN_VOTE("Vote result: [winning_option]"))
+ GLOB.last_transfer_vote = get_round_duration()
+
+ if(winning_option == "Initiate Crew Transfer")
+ init_shift_change(null, TRUE)
diff --git a/code/datums/votes/custom_vote.dm b/code/datums/votes/custom_vote.dm
new file mode 100644
index 00000000000..7b1604480f5
--- /dev/null
+++ b/code/datums/votes/custom_vote.dm
@@ -0,0 +1,73 @@
+/// The max amount of options someone can have in a custom vote.
+#define MAX_CUSTOM_VOTE_OPTIONS 10
+
+/datum/vote/custom_vote/single
+ name = "Custom Standard"
+ message = "Click here to start a custom vote (one selection per voter)"
+
+/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.
+/datum/vote/custom_vote/is_accessible_vote()
+ return TRUE
+
+/datum/vote/custom_vote/reset()
+ default_choices = null
+ override_question = null
+ return ..()
+
+/datum/vote/custom_vote/can_be_initiated(mob/by_who, forced = FALSE)
+ . = ..()
+ if(!.)
+ return FALSE
+
+ // Custom votes can only be created if they're forced to be made.
+ // (Either an admin makes it, or otherwise.)
+ return forced
+
+/datum/vote/custom_vote/create_vote(mob/vote_creator)
+ var/custom_win_method = tgui_input_list(
+ vote_creator,
+ "How should the vote winner be determined?",
+ "Winner Method",
+ list("Simple", "Weighted Random", "No Winner"),
+ default = "Simple",
+ )
+ switch(custom_win_method)
+ if("Simple")
+ winner_method = VOTE_WINNER_METHOD_SIMPLE
+ if("Weighted Random")
+ winner_method = VOTE_WINNER_METHOD_WEIGHTED_RANDOM
+ if("No Winner")
+ winner_method = VOTE_WINNER_METHOD_NONE
+ else
+ to_chat(vote_creator, SPAN_WARNING("Unknown winner method. Contact a coder."))
+ return FALSE
+
+ override_question = tgui_input_text(vote_creator, "What is the vote for?", "Custom Vote")
+ if(!override_question)
+ return FALSE
+
+ default_choices = list()
+ for(var/i in 1 to MAX_CUSTOM_VOTE_OPTIONS)
+ var/option = tgui_input_text(vote_creator, "Please enter an option, or hit cancel to finish. [MAX_CUSTOM_VOTE_OPTIONS] max.", "Options", max_length = MAX_NAME_LEN)
+ if(!vote_creator?.client)
+ return FALSE
+ if(!option)
+ break
+
+ default_choices += capitalize(option)
+
+ if(!length(default_choices))
+ return FALSE
+
+ return ..()
+
+/datum/vote/custom_vote/initiate_vote(initiator, duration)
+ . = ..()
+ . += "\n[override_question]"
+
+#undef MAX_CUSTOM_VOTE_OPTIONS
diff --git a/code/datums/votes/restart.dm b/code/datums/votes/restart.dm
new file mode 100644
index 00000000000..be7f28db9e3
--- /dev/null
+++ b/code/datums/votes/restart.dm
@@ -0,0 +1,42 @@
+/datum/vote/restart
+ name = "Restart"
+ message = "Restart the server?"
+ default_choices = list("Restart Round", "Continue Playing")
+
+/datum/vote/restart/can_be_initiated(mob/by_who, forced)
+ . = ..()
+
+ //If it's disabled and you're not staff, no voting
+ if(!GLOB.config.allow_vote_restart && !forced)
+ return FALSE
+
+ //Prompt the mob to confirm the special use case if present, otherwise say it's possible so the button isn't greyed out
+ if(ismob(by_who))
+ var/acknowledge = tgui_alert(by_who, "CAUTION! This is a vote that must be used only in case of server issues that require a restart, and in no other case! Do you want to continue?",
+ "Caution!", list("No", "Yes"), 1 MINUTE, TRUE)
+
+ if(acknowledge == "Yes")
+ return TRUE
+ else
+ return FALSE
+
+ else
+ return TRUE
+
+
+/datum/vote/restart/finalize_vote(winning_option)
+ if(winning_option != "Restart Round")
+ return
+
+ to_world("World restarting due to vote...")
+ feedback_set_details("end_error","restart vote")
+ sleep(50)
+ log_game("Rebooting due to restart vote")
+ world.Reboot()
+
+/datum/vote/restart/toggle_votable(mob/toggler)
+ if(toggler?.client?.holder && (toggler.client.holder.rights & (R_ADMIN|R_MOD)))
+ GLOB.config.allow_vote_restart = !GLOB.config.allow_vote_restart
+
+/datum/vote/restart/is_config_enabled()
+ return GLOB.config.allow_vote_restart
diff --git a/code/datums/votes/round_type.dm b/code/datums/votes/round_type.dm
new file mode 100644
index 00000000000..0bbad675f0d
--- /dev/null
+++ b/code/datums/votes/round_type.dm
@@ -0,0 +1,73 @@
+/datum/vote/gamemode
+ name = "Gamemode"
+ vote_sound = 'sound/ambience/vote_alarm.ogg'
+ message = "Vote for the gamemode of this round"
+
+/datum/vote/gamemode/create_vote(mob/vote_creator)
+ if(SSticker.current_state != GAME_STATE_PREGAME)
+ return FALSE
+
+ //List of modes, will be inserted sorted by criteria
+ var/list/gamemodes_list = list()
+
+ default_choices = list()
+
+ //Put secret as the first option
+ default_choices.Add("Secret")
+
+ //Sort the gamemodes
+ for(var/votable_mode_name in GLOB.config.votable_modes)
+ var/datum/game_mode/M = GLOB.gamemode_cache[votable_mode_name]
+ if(!M)
+ continue
+
+ BINARY_INSERT(M, gamemodes_list, /datum/game_mode, M, required_players, COMPARE_KEY)
+
+ //Actually populate the voting choices, which are now sorted
+ for(var/datum/game_mode/votable_mode_sorted in gamemodes_list)
+ default_choices += capitalize(votable_mode_sorted.name)
+
+ //Stop the countdown while we vote
+ GLOB.round_progressing = FALSE
+
+ . = ..()
+
+//Otherwise we get thanos snapped from the possible votes list
+/datum/vote/gamemode/is_accessible_vote()
+ //Only accessible in pregame setup, further restricted from mobs in "can_be_initiated()"
+ if(SSticker.current_state == GAME_STATE_PREGAME)
+ return TRUE
+ return FALSE
+
+/datum/vote/gamemode/can_be_initiated(mob/by_who, forced)
+ . = ..()
+ //This can only be called by the server, noone else, not even admins
+ if(forced && !ismob(by_who))
+ return TRUE
+ return FALSE
+
+
+/datum/vote/gamemode/get_simple_winner()
+ var/list/winner = ..()
+
+ //If noone voted, start extended
+ //yes this is kind of stupid, if noone won the list should be empty not have them all
+ //no it's not worth to change it, unless you want to do it yourself, if so, be my guest I guess
+ if(length(winner) == length(default_choices))
+ to_world(SPAN_NOTICE("No votes for gamemode registered, defaulting to extended!"))
+ return list("extended")
+
+ else
+ return winner
+
+/datum/vote/gamemode/finalize_vote(winning_option)
+ to_world(SPAN_WARNING("The round will start soon."))
+
+ if(GLOB.master_mode != lowertext(winning_option))
+ SSpersistent_configuration.last_gamemode = lowertext(winning_option)
+ GLOB.master_mode = lowertext(winning_option)
+
+/datum/vote/gamemode/reset()
+ . = ..()
+ //Resume the countdown at vote end
+ GLOB.round_progressing = TRUE
diff --git a/code/game/machinery/computer/camera.dm b/code/game/machinery/computer/camera.dm
index d454dcc9e42..14305a375e3 100644
--- a/code/game/machinery/computer/camera.dm
+++ b/code/game/machinery/computer/camera.dm
@@ -76,7 +76,7 @@
if(!network_access)
return TRUE
- return (check_camera_access(user, ACCESS_SECURITY) && security_level >= SEC_LEVEL_BLUE) || check_camera_access(user, network_access)
+ return (check_camera_access(user, ACCESS_SECURITY) && GLOB.security_level >= SEC_LEVEL_BLUE) || check_camera_access(user, network_access)
/obj/machinery/computer/security/Topic(href, href_list)
if(..())
diff --git a/code/game/machinery/firealarm.dm b/code/game/machinery/firealarm.dm
index 36ce7323ae2..4134922e181 100644
--- a/code/game/machinery/firealarm.dm
+++ b/code/game/machinery/firealarm.dm
@@ -225,7 +225,7 @@
update_icon()
if(isContactLevel(z))
- INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(set_security_level), (security_level ? get_security_level() : "green"))
+ INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(set_security_level), (GLOB.security_level ? get_security_level() : "green"))
soundloop = new(src, FALSE)
diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm
index f0acd9baeba..c231e4b1327 100644
--- a/code/modules/mob/living/silicon/robot/robot.dm
+++ b/code/modules/mob/living/silicon/robot/robot.dm
@@ -338,7 +338,7 @@
return
var/list/modules = list()
modules.Add(GLOB.robot_module_types)
- if((crisis_override && security_level == SEC_LEVEL_RED) || security_level == SEC_LEVEL_DELTA || crisis == TRUE)
+ if((crisis_override && GLOB.security_level == SEC_LEVEL_RED) || GLOB.security_level == SEC_LEVEL_DELTA || crisis == TRUE)
to_chat(src, SPAN_WARNING("Crisis mode active. Combat module available."))
modules += "Combat"
mod_type = tgui_input_list(src, "Please, select a module!", "Robot", modules)
diff --git a/code/modules/modular_computers/file_system/programs/command/command_and_communications.dm b/code/modules/modular_computers/file_system/programs/command/command_and_communications.dm
index 1ac78d58d87..36b61d77e38 100644
--- a/code/modules/modular_computers/file_system/programs/command/command_and_communications.dm
+++ b/code/modules/modular_computers/file_system/programs/command/command_and_communications.dm
@@ -49,8 +49,8 @@
data["isAI"] = issilicon(usr)
data["authenticated"] = is_authenticated(user)
data["boss_short"] = SSatlas.current_map.boss_short
- data["current_security_level"] = security_level
- data["current_security_level_title"] = num2seclevel(security_level)
+ data["current_security_level"] = GLOB.security_level
+ data["current_security_level_title"] = num2seclevel(GLOB.security_level)
data["current_maint_all_access"] = maint_all_access
data["def_SEC_LEVEL_DELTA"] = SEC_LEVEL_DELTA
@@ -190,7 +190,7 @@
var/current_level = text2num(params["target"])
var/confirm = alert("Are you sure you want to change alert level to [num2seclevel(current_level)]?", filedesc, "No", "Yes")
if(confirm == "Yes" && !computer.use_check_and_message(usr, (isAI(usr) ? USE_ALLOW_NON_ADJACENT : FALSE)))
- var/old_level = security_level
+ var/old_level = GLOB.security_level
if(!current_level)
current_level = SEC_LEVEL_GREEN
if(current_level < SEC_LEVEL_GREEN)
@@ -198,10 +198,10 @@
if(current_level > SEC_LEVEL_BLUE)
current_level = SEC_LEVEL_BLUE
set_security_level(current_level)
- if(security_level != old_level)
+ if(GLOB.security_level != old_level)
log_game("[key_name(usr)] has changed the security level to [get_security_level()].", ckey = key_name(usr))
message_admins("[key_name_admin(usr)] has changed the security level to [get_security_level()].")
- switch(security_level)
+ switch(GLOB.security_level)
if(SEC_LEVEL_GREEN)
feedback_inc("alert_comms_green",1)
if(SEC_LEVEL_BLUE)
diff --git a/code/modules/modular_computers/file_system/programs/security/camera.dm b/code/modules/modular_computers/file_system/programs/security/camera.dm
index 038378b4c79..b9214406fe7 100644
--- a/code/modules/modular_computers/file_system/programs/security/camera.dm
+++ b/code/modules/modular_computers/file_system/programs/security/camera.dm
@@ -92,7 +92,7 @@
if(!network_access)
return TRUE
- return (check_network_access(user, ACCESS_SECURITY) && security_level >= SEC_LEVEL_BLUE) || check_network_access(user, network_access)
+ return (check_network_access(user, ACCESS_SECURITY) && GLOB.security_level >= SEC_LEVEL_BLUE) || check_network_access(user, network_access)
/datum/computer_file/program/camera_monitor/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
diff --git a/code/modules/projectiles/pins.dm b/code/modules/projectiles/pins.dm
index 4615082896f..b83fde04309 100644
--- a/code/modules/projectiles/pins.dm
+++ b/code/modules/projectiles/pins.dm
@@ -305,13 +305,13 @@ var/list/wireless_firing_pins = list() //A list of all initialized wireless firi
var/obj/item/gun/energy/EG = gun
if(EG.required_firemode_auth[EG.sel_mode] == WIRELESS_PIN_STUN)
return TRUE
- else if (security_level == SEC_LEVEL_YELLOW || security_level == SEC_LEVEL_RED)
+ else if (GLOB.security_level == SEC_LEVEL_YELLOW || GLOB.security_level == SEC_LEVEL_RED)
return TRUE
else
fail_message = SPAN_WARNING("Unable to fire: insufficient security level.")
return FALSE
else
- if (security_level == SEC_LEVEL_YELLOW || security_level == SEC_LEVEL_RED)
+ if (GLOB.security_level == SEC_LEVEL_YELLOW || GLOB.security_level == SEC_LEVEL_RED)
return TRUE
else
fail_message = SPAN_WARNING("Unable to fire: insufficient security level.")
diff --git a/code/modules/security levels/keycard authentication.dm b/code/modules/security levels/keycard authentication.dm
index cd732771da4..565a8cf9e31 100644
--- a/code/modules/security levels/keycard authentication.dm
+++ b/code/modules/security levels/keycard authentication.dm
@@ -217,7 +217,7 @@ var/global/maint_all_access = 0
if(!I)
return ..(M)
var/list/A = I.GetAccess()
- var/maint_sec_access = ((security_level > SEC_LEVEL_GREEN) && has_access(ACCESS_SECURITY, accesses = A))
+ var/maint_sec_access = ((GLOB.security_level > SEC_LEVEL_GREEN) && has_access(ACCESS_SECURITY, accesses = A))
var/exceptional_circumstances = maint_all_access || maint_sec_access
if(exceptional_circumstances && src.check_access_list(list(ACCESS_MAINT_TUNNELS)))
return 1
diff --git a/code/modules/security levels/security levels.dm b/code/modules/security levels/security levels.dm
index a3b837e3408..8f1dbe1f787 100644
--- a/code/modules/security levels/security levels.dm
+++ b/code/modules/security levels/security levels.dm
@@ -1,4 +1,4 @@
-/var/security_level = 0
+GLOBAL_VAR_INIT(security_level, SEC_LEVEL_GREEN)
//0 = code green
//1 = code yellow
//2 = code blue
@@ -23,34 +23,34 @@
level = SEC_LEVEL_DELTA
//Will not be announced if you try to set to the same level as it already is
- if(level >= SEC_LEVEL_GREEN && level <= SEC_LEVEL_DELTA && level != security_level)
+ if(level >= SEC_LEVEL_GREEN && level <= SEC_LEVEL_DELTA && level != GLOB.security_level)
switch(level)
if(SEC_LEVEL_GREEN)
security_announcement.Announce("[GLOB.config.alert_desc_green]", "Attention! Security level lowered to green.")
- security_level = SEC_LEVEL_GREEN
+ GLOB.security_level = SEC_LEVEL_GREEN
SSnightlight.end_temp_disable()
if(SEC_LEVEL_BLUE)
- if(security_level < SEC_LEVEL_BLUE)
+ if(GLOB.security_level < SEC_LEVEL_BLUE)
security_announcement_sound.Announce("[GLOB.config.alert_desc_blue_upto]", "Attention! Security level elevated to blue.")
else
security_announcement.Announce("[GLOB.config.alert_desc_blue_downto]", "Attention! Security level lowered to blue.")
- security_level = SEC_LEVEL_BLUE
+ GLOB.security_level = SEC_LEVEL_BLUE
SSnightlight.end_temp_disable()
if(SEC_LEVEL_YELLOW)
security_announcement_sound.Announce("[GLOB.config.alert_desc_yellow_to]", "Attention! Biohazard alert declared!")
- security_level = SEC_LEVEL_YELLOW
+ GLOB.security_level = SEC_LEVEL_YELLOW
SSnightlight.end_temp_disable()
if(SEC_LEVEL_RED)
- if(security_level < SEC_LEVEL_RED)
+ if(GLOB.security_level < SEC_LEVEL_RED)
security_announcement_sound.Announce("[GLOB.config.alert_desc_red_upto]", "Attention! Security level elevated to red!", new_sound = 'sound/effects/high_alert.ogg')
SSnightlight.temp_disable()
else
security_announcement.Announce("[GLOB.config.alert_desc_red_downto]", "Attention! Code red!")
- security_level = SEC_LEVEL_RED
+ GLOB.security_level = SEC_LEVEL_RED
post_display_status("alert", "redalert")
if(SEC_LEVEL_DELTA)
security_announcement_sound.Announce("[GLOB.config.alert_desc_delta]", "Attention! Delta security level reached!", new_sound = 'sound/effects/siren.ogg')
- security_level = SEC_LEVEL_DELTA
+ GLOB.security_level = SEC_LEVEL_DELTA
SSnightlight.temp_disable()
var/newlevel = get_security_level()
@@ -59,7 +59,7 @@
powercontrol.manage_emergency(newlevel)
/proc/get_security_level()
- switch(security_level)
+ switch(GLOB.security_level)
if(SEC_LEVEL_GREEN)
return "green"
if(SEC_LEVEL_BLUE)
@@ -96,15 +96,3 @@
return SEC_LEVEL_RED
if("delta")
return SEC_LEVEL_DELTA
-
-
-/*DEBUG
-/mob/verb/set_thing0()
- set_security_level(0)
-/mob/verb/set_thing1()
- set_security_level(1)
-/mob/verb/set_thing2()
- set_security_level(2)
-/mob/verb/set_thing3()
- set_security_level(3)
-*/
diff --git a/html/changelogs/fluffyghost-updatessvote.yml b/html/changelogs/fluffyghost-updatessvote.yml
new file mode 100644
index 00000000000..66bc7a07a7f
--- /dev/null
+++ b/html/changelogs/fluffyghost-updatessvote.yml
@@ -0,0 +1,43 @@
+################################
+# Example Changelog File
+#
+# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb.
+#
+# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.)
+# When it is, any changes listed below will disappear.
+#
+# Valid Prefixes:
+# bugfix
+# wip (For works in progress)
+# tweak
+# soundadd
+# sounddel
+# rscadd (general adding of nice things)
+# rscdel (general deleting of nice things)
+# imageadd
+# imagedel
+# maptweak
+# spellcheck (typo fixes)
+# experiment
+# balance
+# admin
+# backend
+# security
+# refactor
+#################################
+
+# Your name.
+author: FluffyGhost
+
+# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again.
+delete-after: True
+
+# Any changes you've made. See valid prefix list above.
+# INDENT WITH TWO SPACES. NOT TABS. SPACES.
+# SCREW THIS UP AND IT WON'T WORK.
+# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries.
+# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog.
+changes:
+ - backend: "Updated vote subsystem."
+ - backend: "Turned some variables into proper globs."
+ - backend: "Adapted the ticker and actions to behave properly with the updated subsystem."
diff --git a/tgui/packages/tgui/interfaces/VotePanel.tsx b/tgui/packages/tgui/interfaces/VotePanel.tsx
new file mode 100644
index 00000000000..594993fd947
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VotePanel.tsx
@@ -0,0 +1,272 @@
+import { BooleanLike } from 'common/react';
+
+import { useBackend } from '../backend';
+import { Box, Button, Collapsible, Icon, LabeledList, NoticeBox, Section, Stack } from '../components';
+import { Window } from '../layouts';
+
+enum VoteConfig {
+ None = -1,
+ Disabled = 0,
+ Enabled = 1,
+}
+
+type Vote = {
+ name: string;
+ canBeInitiated: BooleanLike;
+ config: VoteConfig;
+ message: string;
+};
+
+type Option = {
+ name: string;
+ votes: number;
+};
+
+type ActiveVote = {
+ vote: Vote;
+ question: string | null;
+ timeRemaining: number;
+ choices: Option[];
+ countMethod: number;
+};
+
+type UserData = {
+ ckey: string;
+ isLowerAdmin: BooleanLike;
+ isUpperAdmin: BooleanLike;
+ singleSelection: string | null;
+ multiSelection: string[] | null;
+ countMethod: VoteSystem;
+};
+
+enum VoteSystem {
+ VOTE_SINGLE = 1,
+ VOTE_MULTI = 2,
+}
+
+type Data = {
+ currentVote: ActiveVote;
+ possibleVotes: Vote[];
+ user: UserData;
+ voting: string[];
+};
+
+export const VotePanel = (props, context) => {
+ const { data } = useBackend(context);
+ const { currentVote, user } = data;
+
+ /**
+ * Adds the voting type to title if there is an ongoing vote.
+ */
+ let windowTitle = 'Vote';
+ if (currentVote) {
+ windowTitle +=
+ ': ' +
+ (currentVote.question || currentVote.vote.name).replace(/^\w/, (c) =>
+ c.toUpperCase()
+ );
+ }
+
+ return (
+
+
+
+
+
+ {!!user.isLowerAdmin && currentVote && }
+
+
+
+
+
+
+ );
+};
+
+/**
+ * The create vote options menu. Only upper admins can disable voting.
+ * @returns A section visible to everyone with vote options.
+ */
+const VoteOptions = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { possibleVotes, user } = data;
+
+ return (
+
+
+
+ {possibleVotes.map((option) => (
+
+ {!!user.isLowerAdmin && option.config !== VoteConfig.None && (
+
+ act('toggleVote', {
+ voteName: option.name,
+ })
+ }
+ />
+ )}
+
+ ))}
+
+
+
+ );
+};
+
+/**
+ * View Voters by ckey. Admin only.
+ * @returns A collapsible list of voters
+ */
+const VotersList = (props, context) => {
+ const { data } = useBackend(context);
+
+ return (
+
+
+
+ {data.voting.map((voter) => {
+ return {voter};
+ })}
+
+
+
+ );
+};
+
+/**
+ * The choices panel which displays all options in the list.
+ * @returns A section visible to all users.
+ */
+const ChoicesPanel = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { currentVote, user } = data;
+
+ return (
+
+
+ {currentVote && currentVote.countMethod === VoteSystem.VOTE_SINGLE ? (
+ Select one option
+ ) : null}
+ {currentVote &&
+ currentVote.choices.length !== 0 &&
+ currentVote.countMethod === VoteSystem.VOTE_SINGLE ? (
+
+ {currentVote.choices.map((choice) => (
+
+ c.toUpperCase())}
+ textAlign="right"
+ buttons={
+
+ }>
+ {user.singleSelection &&
+ choice.name === user.singleSelection && (
+
+ )}
+ {choice.votes} Votes
+
+
+
+ ))}
+
+ ) : null}
+ {currentVote && currentVote.countMethod === VoteSystem.VOTE_MULTI ? (
+ Select any number of options
+ ) : null}
+ {currentVote &&
+ currentVote.choices.length !== 0 &&
+ currentVote.countMethod === VoteSystem.VOTE_MULTI ? (
+
+ {currentVote.choices.map((choice) => (
+
+ c.toUpperCase())}
+ textAlign="right"
+ buttons={
+
+ }>
+ {user.multiSelection &&
+ user.multiSelection[user.ckey.concat(choice.name)] === 1 ? (
+
+ ) : null}
+ {choice.votes} Votes
+
+
+
+ ))}
+
+ ) : null}
+ {currentVote ? null : No vote active!}
+
+
+ );
+};
+
+/**
+ * Countdown timer at the bottom. Includes a cancel vote option for admins.
+ * @returns A section visible to everyone.
+ */
+const TimePanel = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { currentVote, user } = data;
+
+ return (
+
+
+
+
+ Time Remaining:
+ {currentVote?.timeRemaining || 0}s
+
+ {!!user.isLowerAdmin && (
+
+ )}
+
+
+
+ );
+};