The Panorama Voting System allows you to create custom Yes/No votes that appear in the CS2 UI. Players can vote using the native game interface, making it seamless and familiar.
Key Features:
Native CS2 UI integration
Customizable vote messages and pass/fail text
Selective player targeting using bitmask
Vote event callbacks (start, vote, end)
Custom pass/fail logic with detailed vote statistics
using Plugify;
using static s2sdk.s2sdk;
public unsafe class VoteExample : Plugin
{
public void OnPluginStart()
{
// Create a simple vote for all players
PanoramaSendYesNoVoteToAll(
duration: 30.0,
caller: VOTE_CALLER_SERVER,
voteTitle: "#SFUI_vote_passed",
detailStr: "Restart the match?",
votePassTitle: "#SFUI_vote_passed",
detailPassStr: "Match restarting",
failReason: 0,
result: OnVoteResult,
handler: OnVoteHandler
);
}
// Called to determine if vote passes
private bool OnVoteResult(int numVotes, int yesVotes, int noVotes,
int numClients, int[] clientInfoSlot, int[] clientInfoItem)
{
PrintToServer($"Vote finished: {yesVotes} Yes, {noVotes} No, " +
$"{numVotes}/{numClients} voted\n");
// Simple majority: more than 50% yes votes
return yesVotes > noVotes;
}
// Called for vote events
private void OnVoteHandler(VoteAction action, int param1, int param2)
{
switch (action)
{
case VoteAction.Start:
PrintToServer("Vote started!\n");
break;
case VoteAction.Vote:
int clientSlot = param1;
int choice = param2;
string vote = choice == (int)CastVote.VOTE_OPTION1 ? "Yes" : "No";
PrintToServer($"Player {clientSlot} voted: {vote}\n");
break;
case VoteAction.End:
VoteEndReason reason = (VoteEndReason)param2;
PrintToServer($"Vote ended: {reason}\n");
// Execute action based on result
// Note: This is called AFTER OnVoteResult determines pass/fail
break;
}
}
}
#include <plugify/cpp_plugin.hpp>
#include "s2sdk.hpp"
using namespace s2sdk;
class VoteExample : public plg::IPluginEntry {
public:
void OnPluginStart() override {
PanoramaSendYesNoVoteToAll(
30.0,
VOTE_CALLER_SERVER,
"#SFUI_vote_passed",
"Restart the match?",
"#SFUI_vote_passed",
"Match restarting",
0,
// Result callback - determines pass/fail
[](int numVotes, int yesVotes, int noVotes, int numClients,
const plg::vector<int>& clientInfoSlot,
const plg::vector<int>& clientInfoItem) -> bool {
PrintToServer(std::format("Vote: {} Yes, {} No, {}/{} voted\n",
yesVotes, noVotes, numVotes, numClients).c_str());
return yesVotes > noVotes; // Simple majority
},
// Handler callback - vote events
[](VoteAction action, int param1, int param2) {
switch (action) {
case VoteAction::Start:
PrintToServer("Vote started!\n");
break;
case VoteAction::Vote: {
int clientSlot = param1;
int choice = param2;
const char* vote = choice == static_cast<int>(CastVote::VOTE_OPTION1)
? "Yes" : "No";
PrintToServer(std::format("Player {} voted: {}\n",
clientSlot, vote).c_str());
break;
}
case VoteAction::End: {
VoteEndReason reason = static_cast<VoteEndReason>(param2);
PrintToServer("Vote ended\n");
break;
}
}
}
);
}
};
local plugify = require 'plugify'
local Plugin = plugify.Plugin
local s2 = require 's2sdk'
local VoteExample = {}
setmetatable(VoteExample, { __index = Plugin })
function VoteExample:plugin_start()
local function on_vote_result(num_votes, yes_votes, no_votes, num_clients,
client_info_slot, client_info_item)
s2:PrintToServer(string.format("Vote: %d Yes, %d No, %d/%d voted\n",
yes_votes, no_votes, num_votes, num_clients))
return yes_votes > no_votes
end
local function on_vote_handler(action, param1, param2)
if action == s2.VoteAction.Start then
s2:PrintToServer("Vote started!\n")
elseif action == s2.VoteAction.Vote then
local client_slot = param1
local choice = param2
local vote = choice == s2.CastVote.VOTE_OPTION1 and "Yes" or "No"
s2:PrintToServer(string.format("Player %d voted: %s\n",
client_slot, vote))
elseif action == s2.VoteAction.End then
local reason = param2
s2:PrintToServer(string.format("Vote ended: %d\n", reason))
end
end
s2:PanoramaSendYesNoVoteToAll(
30.0,
s2.VOTE_CALLER_SERVER,
"#SFUI_vote_passed",
"Restart the match?",
"#SFUI_vote_passed",
"Match restarting",
0,
on_vote_result,
on_vote_handler
)
end
local M = {}
M.VoteExample = VoteExample
return M
The voting system uses CS2's built-in translation strings. Common ones include:
Translation String
Description
#SFUI_vote_passed
Vote passed message
#SFUI_vote_failed
Vote failed message
#SFUI_vote_kick_player
Kick player vote
#Panorama_vote
Generic vote message
Translation String Requirement: Only use translation strings that start with #SFUI_vote or #Panorama_vote. Other strings may not display correctly in the voting UI.
Check for active votes - Always use PanoramaIsVoteInProgress() before starting a new vote
Implement custom pass logic - Use the result callback to define what "passing" means for your vote
Track vote progress - Use the handler callback's VoteAction.Vote event to monitor votes in real-time
Handle disconnections - Remove disconnected players using PanoramaRemovePlayerFromVote()
Use appropriate durations - 20-30 seconds is typical for most votes
Validate recipients - Ensure at least one player is in the recipients bitmask
Use translation strings - Stick to #SFUI_vote or #Panorama_vote strings for proper localization
Remove vote targets - Don't allow players being voted on to participate
Separate logic - Use result for pass/fail logic, handler for events and actions
Log vote data - Use clientInfoSlot and clientInfoItem arrays for detailed vote tracking
With the Panorama Voting System, you can create engaging, democratic gameplay experiences that leverage CS2's native UI for a seamless player experience!