Система голосования Panorama

Как создавать пользовательские голосования Да/Нет с использованием системы UI Panorama.

Обзор

Система голосования Panorama позволяет создавать пользовательские голосования Да/Нет, которые отображаются в интерфейсе CS2. Игроки могут голосовать с помощью встроенного игрового интерфейса, что делает процесс естественным и знакомым.

Типы обратных вызовов

Система голосования использует два типа обратных вызовов:

YesNoVoteHandler

Вызывается для событий голосования (начало, индивидуальные голоса, конец):

void Handler(VoteAction action, int param1, int param2)
  • VoteAction.Start: Голосование началось (param1 и param2 не используются)
  • VoteAction.Vote: Игрок проголосовал (param1 = слот клиента, param2 = выбор)
    • VOTE_OPTION1 (0) = Да
    • VOTE_OPTION2 (1) = Нет
  • VoteAction.End: Голосование завершено (param1 = -1, param2 = VoteEndReason)

YesNoVoteResult

Вызывается в конце голосования для определения, прошло ли оно:

bool Result(int numVotes, int yesVotes, int noVotes, int numClients,
            int[] clientInfoSlot, int[] clientInfoItem)
  • Возвращает: true для прохождения голосования, false для провала
  • numVotes: Всего отдано голосов
  • yesVotes: Количество голосов "Да"
  • noVotes: Количество голосов "Нет"
  • numClients: Общее количество клиентов в пуле голосования
  • clientInfoSlot: Массив слотов игроков, которые проголосовали
  • clientInfoItem: Массив выборов голосования (соответствует clientInfoSlot)

Базовое создание голосования

Есть две основные функции для начала голосования:

  1. PanoramaSendYesNoVoteToAll - Создает голосование для всех игроков
  2. PanoramaSendYesNoVote - Создает голосование для определенных игроков (используя битовую маску получателей)

Простое голосование для всех игроков

c#
c++
python
go
js
lua
using Plugify;
using static s2sdk.s2sdk;

public unsafe class VoteExample : Plugin
{
    public void OnPluginStart()
    {
        // Создаем простое голосование для всех игроков
        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
        );
    }

    // Вызывается для определения, прошло ли голосование
    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");

        // Простое большинство: более 50% голосов "Да"
        return yesVotes > noVotes;
    }

    // Вызывается для событий голосования
    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");

                // Выполнить действие на основе результата
                // Примечание: Вызывается ПОСЛЕ того, как OnVoteResult определит успех/провал
                break;
        }
    }
}

Перечисления

CastVote

Значения выбора голосования, используемые в обратных вызовах:

ЗначениеОписание
VOTE_OPTION1 (0)Голос "Да"
VOTE_OPTION2 (1)Голос "Нет"
VOTE_NOTINCLUDED (-1)Игрок не включен в голосование
VOTE_UNCAST (5)Игрок не проголосовал

VoteAction

События, передаваемые в обратный вызов обработчика:

ЗначениеОписаниеparam1param2
StartГолосование началосьне используетсяне используется
VoteИгрок проголосовалслот клиентавыбор голоса (CastVote)
EndГолосование завершено-1VoteEndReason

VoteEndReason

Причины завершения голосования:

ЗначениеОписание
AllVotesВсе возможные голоса были отданы
TimeUpВремя голосования истекло
CancelledГолосование было отменено вручную

Битовая маска получателей (Выборочное голосование)

Чтобы создать голосование для определенных игроков, используйте PanoramaSendYesNoVote с битовой маской получателей. Каждый бит в uint64 представляет слот игрока:

  • Бит 0 (значение 1) = Слот игрока 0
  • Бит 1 (значение 2) = Слот игрока 1
  • Бит 2 (значение 4) = Слот игрока 2
  • Бит N (значение 2^N) = Слот игрока N

Создание битовой маски получателей

c#
python
// Включить определенных игроков по слотам
public ulong BuildRecipients(int[] playerSlots)
{
    ulong recipients = 0;
    foreach (int slot in playerSlots)
    {
        if (slot >= 0 && slot < 64)
        {
            recipients |= (1UL << slot);
        }
    }
    return recipients;
}

// Примеры:
// Только игрок в слоте 0
ulong player0Only = 1UL << 0;  // = 1

// Только игрок в слоте 5
ulong player5Only = 1UL << 5;  // = 32

// Игроки 0, 1 и 2
ulong players012 = (1UL << 0) | (1UL << 1) | (1UL << 2);  // = 7

// Использование в голосовании
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
);

Пользовательская логика прохождения голосования

Обратный вызов result позволяет реализовать пользовательскую логику для определения прохождения голосования:

// Требуется 60% голосов "Да"
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;
}

// Требуется большинство ВСЕХ клиентов (не только проголосовавших)
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;
}

// Анализ индивидуальных голосов
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;
}

Вспомогательные функции голосования

Система голосования предоставляет несколько утилитарных функций:

Проверка активного голосования

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

Удаление игрока из голосования

Удаляет игрока из текущего пула голосования (он больше не может голосовать):

PanoramaRemovePlayerFromVote(3);  // Удалить игрока в слоте 3

Проверка, находится ли игрок в пуле голосования

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

Перерисовка UI голосования для игрока

Принудительно обновляет UI голосования для конкретного игрока:

bool success = PanoramaRedrawVoteToClient(playerSlot);

Ручное завершение голосования

PanoramaEndVote(VoteEndReason.Cancelled);

Практические примеры

Пример 1: Голосование за карту с ограничением по команде

Разрешить только контр-террористам голосовать за смену карты:

public void StartMapVote()
{
    // Создать битовую маску получателей только для команды CT
    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)
            {
                // Проверить, прошло ли голосование (определяется обратным вызовом result)
                // Выполнить смену карты при необходимости
                ServerCommand("changelevel de_dust2");
            }
        }
    );
}

Пример 2: Отслеживание прогресса голосования

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);  // Большинство всех клиентов
        },
        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;
            }
        }
    );
}

Пример 3: Голосование за кик, инициированное игроком

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) =>
        {
            // Требуется 60% голосов "Да"
            return numVotes > 0 && ((double)yesVotes / numVotes) >= 0.6;
        },
        handler: (action, param1, param2) =>
        {
            if (action == VoteAction.Start)
            {
                // Удалить цель из пула голосования
                PanoramaRemovePlayerFromVote(targetSlot);
                PrintToChatAll($"Vote: Kick {targetName}?");
            }
            else if (action == VoteAction.End)
            {
                VoteEndReason reason = (VoteEndReason)param2;
                if (reason != VoteEndReason.Cancelled)
                {
                    // Кикнуть игрока, если голосование прошло (определяется обратным вызовом result)
                    ServerCommand($"kickid {targetSlot}");
                }
            }
        }
    );
}

Строки перевода

Система голосования использует встроенные строки перевода CS2. Распространенные включают:

Строка переводаОписание
#SFUI_vote_passedСообщение о прохождении голосования
#SFUI_vote_failedСообщение о провале голосования
#SFUI_vote_kick_playerГолосование за кик игрока
#Panorama_voteОбщее сообщение голосования

Справочник методов

Запуск голосований

МетодОписание
PanoramaSendYesNoVoteToAll(duration, caller, voteTitle, detailStr, votePassTitle, detailPassStr, failReason, result, handler)Начать голосование для всех игроков
PanoramaSendYesNoVote(duration, caller, voteTitle, detailStr, votePassTitle, detailPassStr, failReason, recipients, result, handler)Начать голосование для определенных игроков (используя битовую маску)

Параметры

  • duration (double) - Максимальное время в секундах для голосования
  • caller (int) - Слот игрока, который инициировал голосование, или VOTE_CALLER_SERVER для голосований сервера
  • voteTitle (string) - Строка перевода для сообщения голосования (например, #SFUI_vote_passed)
  • detailStr (string) - Дополнительный текст деталей для голосования
  • votePassTitle (string) - Строка перевода, показываемая при прохождении голосования
  • detailPassStr (string) - Дополнительный текст деталей при прохождении голосования
  • failReason (int) - Код причины провала голосования
  • recipients (uint64) - Битовая маска слотов игроков, которые могут голосовать (только для PanoramaSendYesNoVote)
  • result (обратный вызов YesNoVoteResult) - Функция, которая определяет, прошло ли голосование:
    bool Result(int numVotes, int yesVotes, int noVotes, int numClients,
                int[] clientInfoSlot, int[] clientInfoItem)
    
    • Возвращает true для прохождения голосования, false для провала
    • numVotes: Всего отдано голосов
    • yesVotes: Количество голосов "Да"
    • noVotes: Количество голосов "Нет"
    • numClients: Всего клиентов в пуле голосования
    • clientInfoSlot: Массив слотов игроков, которые проголосовали
    • clientInfoItem: Массив выборов голосования
  • handler (обратный вызов YesNoVoteHandler) - Функция, вызываемая для событий голосования:
    void Handler(VoteAction action, int param1, int param2)
    
    • action: Тип события (Start, Vote, End)
    • param1: Зависит от действия (слот клиента для Vote, -1 для End)
    • param2: Зависит от действия (выбор голоса для Vote, VoteEndReason для End)

Управление голосованием

МетодОписание
PanoramaIsVoteInProgress()Возвращает true, если голосование в данный момент активно
PanoramaEndVote(VoteEndReason reason)Вручную завершить текущее голосование с указанием причины

Управление игроками

МетодОписание
PanoramaRemovePlayerFromVote(int playerSlot)Удалить игрока из текущего пула голосования
PanoramaIsPlayerInVotePool(int playerSlot)Проверить, может ли игрок голосовать в текущем голосовании
PanoramaRedrawVoteToClient(int playerSlot)Принудительно обновить UI голосования для игрока

Советы и лучшие практики

  1. Проверяйте активные голосования - Всегда используйте PanoramaIsVoteInProgress() перед началом нового голосования
  2. Реализуйте пользовательскую логику прохождения - Используйте обратный вызов result для определения, что означает "прохождение" для вашего голосования
  3. Отслеживайте прогресс голосования - Используйте событие VoteAction.Vote обратного вызова handler для мониторинга голосов в реальном времени
  4. Обрабатывайте отключения - Удаляйте отключившихся игроков с помощью PanoramaRemovePlayerFromVote()
  5. Используйте подходящую длительность - 20-30 секунд типично для большинства голосований
  6. Проверяйте получателей - Убедитесь, что в битовой маске получателей есть хотя бы один игрок
  7. Используйте строки перевода - Придерживайтесь строк #SFUI_vote или #Panorama_vote для правильной локализации
  8. Удаляйте цели голосования - Не позволяйте игрокам, против которых голосуют, участвовать
  9. Разделяйте логику - Используйте result для логики прохождения/провала, handler для событий и действий
  10. Логируйте данные голосования - Используйте массивы clientInfoSlot и clientInfoItem для детального отслеживания голосов

С системой голосования Panorama вы можете создавать увлекательный демократичный игровой опыт, используя нативный UI CS2 для бесшовного взаимодействия с игроками!