diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm
index 00429be14d8..3803d198d4f 100644
--- a/code/__defines/misc.dm
+++ b/code/__defines/misc.dm
@@ -477,3 +477,7 @@ GLOBAL_LIST_INIT(all_volume_channels, list(
#define SPECIES_SORT_WHITELISTED 2
#define SPECIES_SORT_RESTRICTED 3
#define SPECIES_SORT_CUSTOM 4
+
+// Vote Types
+#define VOTE_RESULT_TYPE_MAJORITY "Majority"
+#define VOTE_RESULT_TYPE_SKEWED "Seventy"
diff --git a/code/controllers/autotransfer.dm b/code/controllers/autotransfer.dm
index 6cb875e93c4..5ba87753d80 100644
--- a/code/controllers/autotransfer.dm
+++ b/code/controllers/autotransfer.dm
@@ -17,7 +17,7 @@ var/datum/controller/transfer_controller/transfer_controller
/datum/controller/transfer_controller/process()
currenttick = currenttick + 1
//VOREStation Edit START
- if (round_duration_in_ds >= shift_last_vote - 2 MINUTES)
+ if (round_duration_in_ds >= shift_last_vote - 2 MINUTES)
shift_last_vote = 99999999 //Setting to a stupidly high number since it'll be not used again.
to_world("Warning: You have one hour left in the shift. Wrap up your scenes in the next 60 minutes before the transfer is called.") //VOREStation Edit
if (round_duration_in_ds >= shift_hard_end - 1 MINUTE)
@@ -25,6 +25,6 @@ var/datum/controller/transfer_controller/transfer_controller
shift_hard_end = timerbuffer + config.vote_autotransfer_interval //If shuttle somehow gets recalled, let's force it to call again next time a vote would occur.
timerbuffer = timerbuffer + config.vote_autotransfer_interval //Just to make sure a vote doesn't occur immediately afterwords.
else if (round_duration_in_ds >= timerbuffer - 1 MINUTE)
- SSvote.autotransfer()
+ new /datum/vote/crew_transfer
//VOREStation Edit END
timerbuffer = timerbuffer + config.vote_autotransfer_interval
diff --git a/code/controllers/subsystems/SSvote.dm b/code/controllers/subsystems/SSvote.dm
new file mode 100644
index 00000000000..5ccaa7c5517
--- /dev/null
+++ b/code/controllers/subsystems/SSvote.dm
@@ -0,0 +1,16 @@
+SUBSYSTEM_DEF(vote)
+ name = "Vote"
+ wait = 10
+ priority = FIRE_PRIORITY_VOTE
+ runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT
+ flags = SS_KEEP_TIMING | SS_NO_INIT
+
+ var/datum/vote/active_vote
+
+/datum/controller/subsystem/vote/fire()
+ if(active_vote)
+ active_vote.tick()
+
+/datum/controller/subsystem/vote/proc/start_vote(datum/vote/V)
+ active_vote = V
+ active_vote.start()
diff --git a/code/controllers/subsystems/ticker.dm b/code/controllers/subsystems/ticker.dm
index b26bd701a61..8a4ef819c09 100644
--- a/code/controllers/subsystems/ticker.dm
+++ b/code/controllers/subsystems/ticker.dm
@@ -85,7 +85,7 @@ var/global/datum/controller/subsystem/ticker/ticker
if(start_immediately)
pregame_timeleft = 0
- else if(SSvote.time_remaining)
+ else if(SSvote.active_vote)
return // vote still going, wait for it.
// Time to start the game!
@@ -96,15 +96,15 @@ var/global/datum/controller/subsystem/ticker/ticker
fire() // Don't wait for next tick, do it now!
return
- if(pregame_timeleft <= config.vote_autogamemode_timeleft && !SSvote.gamemode_vote_called)
- SSvote.autogamemode() // Start the game mode vote (if we haven't had one already)
+ //if(pregame_timeleft <= config.vote_autogamemode_timeleft && !SSvote.gamemode_vote_called)
+ //.autogamemode() // Start the game mode vote (if we haven't had one already)
// Called during GAME_STATE_SETTING_UP (RUNLEVEL_SETUP)
/datum/controller/subsystem/ticker/proc/setup_tick(resumed = FALSE)
if(!setup_choose_gamemode())
// It failed, go back to lobby state and re-send the welcome message
pregame_timeleft = config.pregame_time
- SSvote.gamemode_vote_called = FALSE // Allow another autogamemode vote
+ // SSvote.gamemode_vote_called = FALSE // Allow another autogamemode vote
current_state = GAME_STATE_PREGAME
Master.SetRunLevel(RUNLEVEL_LOBBY)
pregame_welcome()
@@ -226,7 +226,7 @@ var/global/datum/controller/subsystem/ticker/ticker
mode.cleanup()
//call a transfer shuttle vote
to_world("The round has ended!")
- SSvote.autotransfer()
+ new /datum/vote/crew_transfer
// Called during GAME_STATE_FINISHED (RUNLEVEL_POSTGAME)
/datum/controller/subsystem/ticker/proc/post_game_tick()
diff --git a/code/controllers/subsystems/vote.dm b/code/controllers/subsystems/vote.dm
index c259c9ecd7e..712d62a625b 100644
--- a/code/controllers/subsystems/vote.dm
+++ b/code/controllers/subsystems/vote.dm
@@ -1,3 +1,4 @@
+/*
SUBSYSTEM_DEF(vote)
name = "Vote"
wait = 10
@@ -396,3 +397,4 @@ SUBSYSTEM_DEF(vote)
if(SSvote)
src << browse(SSvote.interface(src), "window=vote;size=500x[300 + SSvote.choices.len * 25]")
+*/
diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm
index 2221905e416..b2efe5b6cf0 100644
--- a/code/modules/mob/new_player/new_player.dm
+++ b/code/modules/mob/new_player/new_player.dm
@@ -117,8 +117,8 @@
if(statpanel("Lobby") && SSticker)
stat("Game Mode:", SSticker.hide_mode ? "Secret" : "[config.mode_names[master_mode]]")
- if(SSvote.mode)
- stat("Vote: [capitalize(SSvote.mode)]", "Time Left: [SSvote.time_remaining] s")
+ // if(SSvote.mode)
+ // stat("Vote: [capitalize(SSvote.mode)]", "Time Left: [SSvote.time_remaining] s")
if(SSticker.current_state == GAME_STATE_INIT)
stat("Time To Start:", "Server Initializing")
diff --git a/code/modules/vote/vote_datum.dm b/code/modules/vote/vote_datum.dm
new file mode 100644
index 00000000000..87bc80e99ce
--- /dev/null
+++ b/code/modules/vote/vote_datum.dm
@@ -0,0 +1,200 @@
+/datum/vote
+ // Person who started the vote
+ var/initiator = "the server"
+ // world.time the bote started at
+ var/started_time
+ // The question being asked
+ var/question
+ // Vote type text, for showing in UIs and stuff
+ var/vote_type_text = "unset"
+ // Do we want to show the vote count as it goes
+ var/show_counts = FALSE
+ // Vote result type. This determines how a winner is picked
+ var/vote_result_type = VOTE_RESULT_TYPE_MAJORITY
+ // Was this this vote custom started?
+ var/is_custom = FALSE
+ // Choices available in the vote
+ var/list/choices = list()
+ // Assoc list of [ckeys => choice] who have voted. We don't want to hold clients refs.___callbackvarset(list_or_datum, var_name, var_value)
+ var/list/voted = list()
+ // For how long will it be up
+ var/vote_time = 60 SECONDS
+
+/datum/vote/New(var/_initiator, var/_question, list/_choices, var/_is_custom = FALSE)
+ if(SSvote.active_vote)
+ CRASH("Attempted to start another vote with one already in progress!")
+
+ if(_initiator)
+ initiator = _initiator
+ if(_question)
+ question = _question
+ if(_choices)
+ choices = _choices
+
+ is_custom = _is_custom
+
+ // If we have no choices, dynamically generate them
+ if(!length(choices))
+ generate_choices()
+
+/datum/vote/proc/start()
+ var/text = "[capitalize(vote_type_text)] vote started by [initiator]."
+ if(is_custom)
+ vote_type_text = "custom"
+ text += "\n[question]"
+ if(usr)
+ log_admin("[capitalize(vote_type_text)] ([question]) vote started by [key_name(usr)].")
+
+ else if(usr)
+ log_admin("[capitalize(vote_type_text)] vote started by [key_name(usr)].")
+
+ log_vote(text)
+ started_time = world.time
+ announce(text)
+
+/datum/vote/proc/remaining()
+ return max(((started_time + vote_time - world.time)/10), 0)
+
+/datum/vote/proc/calculate_result()
+ if(!length(voted))
+ to_chat(world, span_interface("No votes were cast. Do you all hate democracy?!"))
+ return null
+
+ return calculate_vote_result(voted, choices, vote_result_type)
+
+
+/datum/vote/proc/calculate_vote_result(var/list/voted, var/list/choices, var/vote_result_type)
+ var/list/results = list()
+
+ for(var/ck in voted)
+ if(voted[ck] in results)
+ results[voted[ck]]++
+ else
+ results[voted[ck]] = 1
+
+ var/maxvotes = 0
+ for(var/res in results)
+ maxvotes = max(results[res], maxvotes)
+
+ var/list/winning_options = list()
+
+ for(var/res in results)
+ if(results[res] == maxvotes)
+ winning_options |= res
+
+ for(var/res in results)
+ to_chat(world, span_interface("[res] - [results[res]] vote\s"))
+
+ switch(vote_result_type)
+ if(VOTE_RESULT_TYPE_MAJORITY)
+ if(length(winning_options) == 1)
+ var/res = winning_options[1]
+ if(res in choices)
+ to_chat(world, span_interface("[res] won the vote!"))
+ return res
+ else
+ to_chat(world, span_interface("The winner of the vote ([sanitize(res)]) isn't a valid choice? What the heck?"))
+ stack_trace("Vote concluded with an invalid answer. Answer was [sanitize(res)], choices were [json_encode(choices)]")
+ return null
+
+ to_chat(world, span_interface("No clear winner. The vote did not pass."))
+ return null
+
+ if(VOTE_RESULT_TYPE_SKEWED)
+ var/required_votes = ceil(length(voted) * 0.7) // 70% of total votes
+ if(maxvotes >= required_votes && length(winning_options) == 1)
+ var/res = winning_options[1]
+ if(res in choices)
+ to_chat(world, span_interface("[res] won the vote with a 70% majority!"))
+ return res
+ else
+ to_chat(world, span_interface("The winner of the vote ([sanitize(res)]) isn't a valid choice? What the heck?"))
+ stack_trace("Vote concluded with an invalid answer. Answer was [sanitize(res)], choices were [json_encode(choices)]")
+ return null
+
+ to_chat(world, span_interface("No option received 70% of the votes. The vote did not pass."))
+ return null
+
+ return null
+
+/datum/vote/proc/announce(start_text, var/time = vote_time)
+ to_chat(world, span_lightpurple("Type vote or click here to place your vote. \
+ You have [time] seconds to vote."))
+ world << sound('sound/ambience/alarm4.ogg', repeat = 0, wait = 0, volume = 50, channel = 3)
+
+/datum/vote/Topic(href, list/href_list)
+ if(href_list["vote"] == "open")
+ if(src)
+ tgui_interact(usr)
+ else
+ to_chat(usr, "There is no active vote to participate in.")
+
+/datum/vote/proc/tick()
+ if(remaining() == 0)
+ var/result = calculate_result()
+ handle_result(result)
+ qdel(src)
+
+/datum/vote/Destroy(force)
+ if(SSvote.active_vote == src)
+ SSvote.active_vote = null
+ return ..()
+
+/datum/vote/proc/handle_result(result)
+ return
+
+/datum/vote/proc/generate_choices()
+ return
+
+/*
+ UI STUFFS
+*/
+
+/datum/vote/tgui_state(mob/user)
+ return GLOB.tgui_always_state
+
+/datum/vote/tgui_interact(mob/user, datum/tgui/ui = null)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "VotePanel", "Vote Panel")
+ ui.open()
+
+/datum/vote/tgui_data(mob/user)
+ var/list/data = list()
+ data["remaining"] = remaining()
+ data["user_vote"] = null
+ if(user.ckey in voted)
+ data["user_vote"] = voted[user.ckey]
+
+ data["question"] = question
+ data["choices"] = choices
+
+ if(show_counts || check_rights(R_ADMIN, FALSE, user))
+ data["show_counts"] = TRUE
+
+ var/list/counts = list()
+ for(var/ck in voted)
+ if(voted[ck] in counts)
+ counts[voted[ck]]++
+ else
+ counts[voted[ck]] = 1
+
+ data["counts"] = counts
+ else
+ data["show_counts"] = FALSE
+ data["counts"] = list()
+
+ return data
+
+/datum/vote/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
+ if(..())
+ return
+
+ . = TRUE
+
+ switch(action)
+ if("vote")
+ if(params["target"] in choices)
+ voted[usr.ckey] = params["target"]
+ else
+ message_admins(span_warning("User [key_name_admin(usr)] spoofed a vote in the vote panel!"))
diff --git a/code/modules/vote/vote_presets.dm b/code/modules/vote/vote_presets.dm
new file mode 100644
index 00000000000..a7a0829838b
--- /dev/null
+++ b/code/modules/vote/vote_presets.dm
@@ -0,0 +1,14 @@
+/datum/vote/crew_transfer
+ question = "End the shift"
+ choices = list("Initiate Crew Transfer", "Extend The Shift")
+ vote_type_text = "crew transfer"
+ vote_result_type = VOTE_RESULT_TYPE_SKEWED
+
+/datum/vote/crew_transfer/New()
+ if(SSticker.current_state < GAME_STATE_PLAYING)
+ CRASH("Attempted to call a shutle vote before the game starts!")
+ ..()
+
+/datum/vote/crew_transfer/handle_result(result)
+ if(result == "Initiate Crew Transfer")
+ init_shift_change(null, TRUE)
diff --git a/code/modules/vote/vote_verb.dm b/code/modules/vote/vote_verb.dm
new file mode 100644
index 00000000000..3198a8fabd8
--- /dev/null
+++ b/code/modules/vote/vote_verb.dm
@@ -0,0 +1,56 @@
+/client/verb/vote()
+ set category = "OOC"
+ set name = "Vote"
+
+ if(SSvote.active_vote)
+ SSvote.active_vote.tgui_interact(usr)
+ else
+ to_chat(usr, "There is no active vote")
+
+/client/proc/start_vote()
+ set category = "Admin"
+ set name = "Start Vote"
+ set desc = "Start a vote on the server"
+
+ if(!is_admin())
+ return
+
+ if(SSvote.active_vote)
+ to_chat(usr, "A vote is already in progress")
+ return
+
+ var/vote_types = subtypesof(/datum/vote)
+ vote_types |= "\[CUSTOM]"
+
+ var/list/votemap = list()
+ for(var/vtype in vote_types)
+ votemap["[vtype]"] = vtype
+
+ var/choice = tgui_input_list(usr, "Select a vote type", "Vote", vote_types)
+
+ if(choice == null)
+ return
+
+ if(choice != "\[CUSTOM]")
+ var/datum/votetype = votemap["[choice]"]
+ SSvote.start_vote(new votetype(usr.ckey))
+ return
+
+ var/question = tgui_input_text(usr, "What is the vote for?", "Create Vote", encode = FALSE)
+ if(isnull(question))
+ return
+
+ var/list/choices = list()
+ for(var/i in 1 to 10)
+ var/option = tgui_input_text(usr, "Please enter an option or hit cancel to finish", "Create Vote", encode = FALSE)
+ if(isnull(option) || !usr.client)
+ break
+ choices |= option
+
+ var/c2 = tgui_alert(usr, "Show counts while vote is happening?", "Counts", list("Yes", "No"))
+ var/c3 = input(usr, "Select a result calculation type", "Vote", VOTE_RESULT_TYPE_MAJORITY) as anything in list(VOTE_RESULT_TYPE_MAJORITY)
+
+ var/datum/vote/V = new /datum/vote(usr.ckey, question, choices, TRUE)
+ V.show_counts = (c2 == "Yes")
+ V.vote_result_type = c3
+ SSvote.start_vote(V)
diff --git a/tgui/packages/tgui/interfaces/VotePanel.tsx b/tgui/packages/tgui/interfaces/VotePanel.tsx
new file mode 100644
index 00000000000..f3ef4ff118c
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VotePanel.tsx
@@ -0,0 +1,44 @@
+import { useBackend } from '../backend';
+import { Box, Button, Section } from '../components';
+import { Window } from '../layouts';
+
+type Data = {
+ remaining: number;
+ question: string;
+ choices: string[];
+ user_vote: string | null;
+ counts: Record;
+ show_counts: boolean;
+};
+
+export const VotePanel = (props, context) => {
+ const { act, data } = useBackend();
+ const { remaining, question, choices, user_vote, counts, show_counts } = data;
+
+ return (
+
+
+
+
+ Time remaining: {remaining.toFixed(0)}s
+
+ {choices.map((choice, i) => (
+
+
+ ))}
+
+
+
+ );
+};
diff --git a/vorestation.dme b/vorestation.dme
index ff71e29b458..9a75dda37c1 100644
--- a/vorestation.dme
+++ b/vorestation.dme
@@ -344,6 +344,7 @@
#include "code\controllers\subsystems\skybox.dm"
#include "code\controllers\subsystems\sounds.dm"
#include "code\controllers\subsystems\sqlite.dm"
+#include "code\controllers\subsystems\SSvote.dm"
#include "code\controllers\subsystems\sun.dm"
#include "code\controllers\subsystems\supply.dm"
#include "code\controllers\subsystems\tgui.dm"
@@ -4218,6 +4219,9 @@
#include "code\modules\vore\resizing\sizegun_vr.dm"
#include "code\modules\vore\smoleworld\smoleworld_vr.dm"
#include "code\modules\vore\weight\fitness_machines_vr.dm"
+#include "code\modules\vote\vote_datum.dm"
+#include "code\modules\vote\vote_presets.dm"
+#include "code\modules\vote\vote_verb.dm"
#include "code\modules\webhooks\_webhook.dm"
#include "code\modules\webhooks\webhook_ahelp2discord.dm"
#include "code\modules\webhooks\webhook_custom_event.dm"