Panorama Voting System

How to create custom Yes/No votes using the Panorama UI system.

Overview

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.

Callback Types

The voting system uses two callback types:

YesNoVoteHandler

Called for vote events (start, individual votes, end):

void Handler(VoteAction action, int param1, int param2)
  • VoteAction.Start: Vote has started (param1 and param2 unused)
  • VoteAction.Vote: Player voted (param1 = client slot, param2 = choice)
    • VOTE_OPTION1 (0) = Yes
    • VOTE_OPTION2 (1) = No
  • VoteAction.End: Vote has ended (param1 = -1, param2 = VoteEndReason)

YesNoVoteResult

Called when vote ends to determine if it passed:

bool Result(int numVotes, int yesVotes, int noVotes, int numClients,
            int[] clientInfoSlot, int[] clientInfoItem)
  • Returns: true to pass the vote, false to fail
  • numVotes: Total votes cast
  • yesVotes: Number of yes votes
  • noVotes: Number of no votes
  • numClients: Total number of clients in vote pool
  • clientInfoSlot: Array of player slots who voted
  • clientInfoItem: Array of vote choices (corresponding to clientInfoSlot)

Basic Vote Creation

There are two main functions to start a vote:

  1. PanoramaSendYesNoVoteToAll - Creates a vote for all players
  2. PanoramaSendYesNoVote - Creates a vote for specific players (using recipients bitmask)

Simple Vote to All Players

c#
c++
python
go
js
lua
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;
        }
    }
}

Enumerations

CastVote

Vote choice values used in callbacks:

ValueDescription
VOTE_OPTION1 (0)Yes vote
VOTE_OPTION2 (1)No vote
VOTE_NOTINCLUDED (-1)Player not included in vote
VOTE_UNCAST (5)Player didn't vote

VoteAction

Events passed to the handler callback:

ValueDescriptionparam1param2
StartVote startedunusedunused
VotePlayer votedclient slotvote choice (CastVote)
EndVote ended-1VoteEndReason

VoteEndReason

Reasons why a vote ended:

ValueDescription
AllVotesAll possible votes were cast
TimeUpVote duration expired
CancelledVote was manually cancelled

Recipients Bitmask (Selective Voting)

To create a vote for specific players, use PanoramaSendYesNoVote with a recipients bitmask. Each bit in the uint64 represents a player slot:

  • Bit 0 (value 1) = Player slot 0
  • Bit 1 (value 2) = Player slot 1
  • Bit 2 (value 4) = Player slot 2
  • Bit N (value 2^N) = Player slot N

Building Recipients Bitmask

c#
python
// Include specific players by slot
public ulong BuildRecipients(int[] playerSlots)
{
    ulong recipients = 0;
    foreach (int slot in playerSlots)
    {
        if (slot >= 0 && slot < 64)
        {
            recipients |= (1UL << slot);
        }
    }
    return recipients;
}

// Examples:
// Only player slot 0
ulong player0Only = 1UL << 0;  // = 1

// Only player slot 5
ulong player5Only = 1UL << 5;  // = 32

// Players 0, 1, and 2
ulong players012 = (1UL << 0) | (1UL << 1) | (1UL << 2);  // = 7

// Use in vote
int[] targetPlayers = { 0, 2, 5 };
ulong recipients = BuildRecipients(targetPlayers);

PanoramaSendYesNoVote(
    duration: 30.0,
    caller: VOTE_CALLER_SERVER,
    voteTitle: "#SFUI_vote_passed",
    detailStr: "Kick player?",
    votePassTitle: "#SFUI_vote_passed",
    detailPassStr: "Player kicked",
    failReason: 0,
    recipients: recipients,
    result: OnVoteResult,
    handler: OnVoteHandler
);

Custom Vote Pass Logic

The result callback allows you to implement custom logic for determining if a vote passes:

// Require 60% yes votes
private bool OnVoteResult(int numVotes, int yesVotes, int noVotes,
                          int numClients, int[] clientInfoSlot, int[] clientInfoItem)
{
    if (numVotes == 0) return false;

    double yesPercent = (double)yesVotes / numVotes * 100.0;
    bool passed = yesPercent >= 60.0;

    PrintToServer($"Vote: {yesPercent:F1}% yes ({yesVotes}/{numVotes})\n");
    return passed;
}

// Require majority of ALL clients (not just voters)
private bool OnVoteResultStrict(int numVotes, int yesVotes, int noVotes,
                                int numClients, int[] clientInfoSlot, int[] clientInfoItem)
{
    bool passed = yesVotes > (numClients / 2);
    PrintToServer($"Vote: {yesVotes}/{numClients} clients voted yes\n");
    return passed;
}

// Analyze individual votes
private bool OnVoteResultDetailed(int numVotes, int yesVotes, int noVotes,
                                  int numClients, int[] clientInfoSlot, int[] clientInfoItem)
{
    PrintToServer("Vote details:\n");
    for (int i = 0; i < clientInfoSlot.Length; i++)
    {
        int slot = clientInfoSlot[i];
        int choice = clientInfoItem[i];
        string vote = choice == (int)CastVote.VOTE_OPTION1 ? "Yes" : "No";
        PrintToServer($"  Slot {slot}: {vote}\n");
    }

    return yesVotes > noVotes;
}

Vote Helper Functions

The voting system provides several utility functions:

Check If Vote Is Active

bool isVoting = PanoramaIsVoteInProgress();
if (isVoting)
{
    PrintToServer("A vote is currently active!\n");
}

Remove Player From Vote

Removes a player from the current vote pool (they can no longer vote):

PanoramaRemovePlayerFromVote(3);  // Remove player slot 3

Check If Player Is In Vote Pool

int playerSlot = 5;
if (PanoramaIsPlayerInVotePool(playerSlot))
{
    PrintToServer($"Player {playerSlot} can vote\n");
}

Redraw Vote UI for Player

Forces the vote UI to refresh for a specific player:

bool success = PanoramaRedrawVoteToClient(playerSlot);

End Vote Manually

PanoramaEndVote(VoteEndReason.Cancelled);

Practical Examples

Example 1: Map Vote with Team Restriction

Only allow Counter-Terrorists to vote on changing the map:

public void StartMapVote()
{
    // Build recipients bitmask for CT team only
    ulong recipients = 0;
    for (int slot = 0; slot < 64; slot++)
    {
        int handle = PlayerSlotToEntHandle(slot);
        if (handle == -1) continue;

        int team = GetEntSchemaInt32(handle, "CBaseEntity", "m_iTeamNum", 0);
        if (team == 3)  // 3 = Counter-Terrorist
        {
            recipients |= (1UL << slot);
        }
    }

    if (recipients == 0)
    {
        PrintToServer("No CT players to vote!\n");
        return;
    }

    PanoramaSendYesNoVote(
        duration: 30.0,
        caller: VOTE_CALLER_SERVER,
        voteTitle: "#SFUI_vote_passed",
        detailStr: "Change map to de_dust2?",
        votePassTitle: "#SFUI_vote_passed",
        detailPassStr: "Changing map to de_dust2",
        failReason: 0,
        recipients: recipients,
        result: (numVotes, yesVotes, noVotes, numClients, slots, items) =>
        {
            return yesVotes > noVotes;
        },
        handler: (action, param1, param2) =>
        {
            if (action == VoteAction.End)
            {
                // Check if vote passed (determined by result callback)
                // Execute map change if needed
                ServerCommand("changelevel de_dust2");
            }
        }
    );
}

Example 2: Tracking Vote Progress

private int totalYesVotes = 0;
private int totalNoVotes = 0;

public void StartTrackedVote()
{
    totalYesVotes = 0;
    totalNoVotes = 0;

    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: (numVotes, yesVotes, noVotes, numClients, slots, items) =>
        {
            PrintToServer($"Final: {yesVotes} Yes, {noVotes} No\n");
            return yesVotes > (numClients / 2);  // Majority of all clients
        },
        handler: (action, param1, param2) =>
        {
            switch (action)
            {
                case VoteAction.Start:
                    PrintToChatAll("Vote started: Restart match?");
                    break;

                case VoteAction.Vote:
                    int choice = param2;
                    if (choice == (int)CastVote.VOTE_OPTION1)
                        totalYesVotes++;
                    else if (choice == (int)CastVote.VOTE_OPTION2)
                        totalNoVotes++;

                    PrintToChatAll($"Current: {totalYesVotes} Yes, {totalNoVotes} No");
                    break;

                case VoteAction.End:
                    VoteEndReason reason = (VoteEndReason)param2;
                    PrintToChatAll($"Vote ended: {reason}");
                    break;
            }
        }
    );
}

Example 3: Player-Initiated Kick Vote

public void StartKickVote(int callerSlot, int targetSlot)
{
    if (PanoramaIsVoteInProgress())
    {
        PrintToChat(callerSlot, "A vote is already in progress!");
        return;
    }

    int targetHandle = PlayerSlotToEntHandle(targetSlot);
    string targetName = GetPlayerName(targetHandle);

    PanoramaSendYesNoVoteToAll(
        duration: 30.0,
        caller: callerSlot,
        voteTitle: "#SFUI_vote_kick_player",
        detailStr: targetName,
        votePassTitle: "#SFUI_vote_passed",
        detailPassStr: $"Kicking {targetName}",
        failReason: 0,
        result: (numVotes, yesVotes, noVotes, numClients, slots, items) =>
        {
            // Require 60% yes votes
            return numVotes > 0 && ((double)yesVotes / numVotes) >= 0.6;
        },
        handler: (action, param1, param2) =>
        {
            if (action == VoteAction.Start)
            {
                // Remove target from vote pool
                PanoramaRemovePlayerFromVote(targetSlot);
                PrintToChatAll($"Vote: Kick {targetName}?");
            }
            else if (action == VoteAction.End)
            {
                VoteEndReason reason = (VoteEndReason)param2;
                if (reason != VoteEndReason.Cancelled)
                {
                    // Kick player if vote passed (determined by result callback)
                    ServerCommand($"kickid {targetSlot}");
                }
            }
        }
    );
}

Translation Strings

The voting system uses CS2's built-in translation strings. Common ones include:

Translation StringDescription
#SFUI_vote_passedVote passed message
#SFUI_vote_failedVote failed message
#SFUI_vote_kick_playerKick player vote
#Panorama_voteGeneric vote message

Method Reference

Starting Votes

MethodDescription
PanoramaSendYesNoVoteToAll(duration, caller, voteTitle, detailStr, votePassTitle, detailPassStr, failReason, result, handler)Start a vote for all players
PanoramaSendYesNoVote(duration, caller, voteTitle, detailStr, votePassTitle, detailPassStr, failReason, recipients, result, handler)Start a vote for specific players (using bitmask)

Parameters

  • duration (double) - Maximum time in seconds for the vote
  • caller (int) - Player slot who initiated the vote, or VOTE_CALLER_SERVER for server votes
  • voteTitle (string) - Translation string for the vote message (e.g., #SFUI_vote_passed)
  • detailStr (string) - Additional detail text for the vote
  • votePassTitle (string) - Translation string shown when vote passes
  • detailPassStr (string) - Additional detail text when vote passes
  • failReason (int) - Reason code for vote failure
  • recipients (uint64) - Bitmask of player slots who can vote (only for PanoramaSendYesNoVote)
  • result (YesNoVoteResult callback) - Function that determines if vote passes:
    bool Result(int numVotes, int yesVotes, int noVotes, int numClients,
                int[] clientInfoSlot, int[] clientInfoItem)
    
    • Returns true to pass the vote, false to fail
    • numVotes: Total votes cast
    • yesVotes: Number of yes votes
    • noVotes: Number of no votes
    • numClients: Total clients in vote pool
    • clientInfoSlot: Array of player slots who voted
    • clientInfoItem: Array of vote choices
  • handler (YesNoVoteHandler callback) - Function called for vote events:
    void Handler(VoteAction action, int param1, int param2)
    
    • action: Event type (Start, Vote, End)
    • param1: Depends on action (client slot for Vote, -1 for End)
    • param2: Depends on action (vote choice for Vote, VoteEndReason for End)

Vote Management

MethodDescription
PanoramaIsVoteInProgress()Returns true if a vote is currently active
PanoramaEndVote(VoteEndReason reason)Manually end the current vote with a reason

Player Management

MethodDescription
PanoramaRemovePlayerFromVote(int playerSlot)Remove a player from the current vote pool
PanoramaIsPlayerInVotePool(int playerSlot)Check if a player can vote in the current vote
PanoramaRedrawVoteToClient(int playerSlot)Force refresh the vote UI for a player

Tips and Best Practices

  1. Check for active votes - Always use PanoramaIsVoteInProgress() before starting a new vote
  2. Implement custom pass logic - Use the result callback to define what "passing" means for your vote
  3. Track vote progress - Use the handler callback's VoteAction.Vote event to monitor votes in real-time
  4. Handle disconnections - Remove disconnected players using PanoramaRemovePlayerFromVote()
  5. Use appropriate durations - 20-30 seconds is typical for most votes
  6. Validate recipients - Ensure at least one player is in the recipients bitmask
  7. Use translation strings - Stick to #SFUI_vote or #Panorama_vote strings for proper localization
  8. Remove vote targets - Don't allow players being voted on to participate
  9. Separate logic - Use result for pass/fail logic, handler for events and actions
  10. 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!