[SourcePawn] Урок 0 - Самые, самые основы

R1KO

fuck society
Сообщения
9,457
Реакции
7,786
  • Команда форума
  • #1
[SourcePawn] Урок 0 - Основы написания плагинов

<- К содержанию

Это скорее даже не урок, а монолог.
В нем я хочу рассказать о том, с какими проблемами чаще всего сталкиваются начинающие "кодеры" и дам несколько советов.

Писать постараюсь максимально просто.

Начну с самого основного - игроков.
Тут следует ввести 2 основных понятия:
  • Индекс игрока - номер слота, занимаемого игроком на сервере.
    Слот номер 0 это сервер. О нем позже.
    Максимальное кол-во слотов на сервере 64.
    Значит индексы игроков от 1 до 64-х.
    Еще 1 слот - Source/GO TV.
    Я не знаю занимает он один из слотов выделенных для игроков или для него добавляется отдельный (65-й).
    В SourcePawn есть константа которая обозначает это число:
    PHP:
    #define MAXPLAYERS                65
    Да-да, это та самая MAXPLAYERS, которую вы так часто видели.
    Это ничто иное как "псевдоним" числа 65.
    Так же есть еще 1 константа:
    PHP:
    int MaxClients;
    Она всегда равна количеству слотов на сервере.

    И так закрепим что такое индекс:
    Предположим что у нас пустой сервер, без Source/GO TV, и имеет 10 слотов.
    И того MaxClients равна 10, MAXPLAYERS как и всегда равна 65.
    На сервер заходит игрок Петя, и получает индекс 1.
    После него заходит Вася, и следовательно получает индекс 2.
    За ними заходит еще 2 игрока, в итоге получаем это:

    Индекс - Ник

    1 - Петя
    2 - Вася
    3 - Коля
    4 - Алеша

    Далее Вася решает что он наигрался и выходит. Слот 2 становится свободен.
    На сервер заходит игрок Инокентий, и получает минимальный свободный индекс - 2.
    Затем сервер покидает и Коля.
    Заходит Витя и занимает 3-й слот, а за ним Валера занимая 5-й слот.

    И того имеем:

    Индекс - Ник

    1 - Петя
    2 - Инокентий
    3 - Витя
    4 - Алеша
    5 - Валера

    Всё просто. Думаю, это уже ясно.

    Тут же следует сказать о мистической переменной client, она же чудом может называться iClient, i и много других названий.
    Так вот это просто переменная, которая хранит в себе индекс игрока.
    Плагины работают с игроками по их индексу, поэтому важно что хранится в переменной client, а не как она называется (это дело вкуса (в моих уроках это iClient)).
  • Юзерид, Userid игрока - уникальный номер игрока на сервере.
    Как и в случае со слотом 0 это сервер.
    Но с игроками немного иначе.
    Предположим что ситуация та же:
    У нас пустой сервер, без Source/GO TV, и имеет 10 слотов.
    И того MaxClients равна 10, MAXPLAYERS как и всегда равна 65.
    На сервер заходит игрок Петя, и получает индекс 1, юзер ид так же 1.
    После него заходит Вася, и следовательно получает индекс 2, юзер ид 2.
    За ними заходит еще 2 игрока, в итоге получаем это:

    Индекс - Юзерид - Ник

    1 - 1 - Петя
    2 - 2 - Вася
    3 - 3 - Коля
    4 - 4 - Алеша

    Но вот дальше ситуация меняется.

    Вася выходит, заходит Инокентий, и получает минимальный свободный индекс - 2, а вот юзер ид у него будет 5.
    Почему так?
    Юзерид это просто уникальный номер, который присваивается каждому заходящему на сервер игроку, каждый раз увеличиваясь на 1.

    Сейчас на сервере такая картина:

    Индекс - Юзерид - Ник

    1 - 1 - Петя
    2 - 5 - Инокентий
    3 - 3 - Коля
    4 - 4 - Алеша

    Сервер покидает Коля, заходит Витя и занимает 3-й слот, а Юзерид получает 6.
    Выходит Петя, заходит Оля и занимает 1-й слот, а Юзерид получает 7.
    Заходит Валера занимая 5-й слот и Юзерид 8.

    И того имеем:

    Индекс - Юзерид - Ник

    1 - 7 - Оля
    2 - 5 - Инокентий
    3 - 6 - Витя
    4 - 4 - Алеша
    5 - 8 - Валера

    И так с каждым заходящим игроком.
    Если Коля зайдет на сервер опять (просто реконнектнится, или позже решит продолжить нагиб) - его Юзерид будет не 6 как при выходе, а уже 9, т.к. сейчас найбольший Юзерид равен 8.
    И так до тех пор пока не дойдет до 2147483647 (максимальное положительное значение int). Затем он станет равен
    –2147483648 и будет уменьшаться до 0 (но 0 не станет, после -1 станет 1) и весь круг опять заново.
    Так же на 1 он сбрасывается при выключении/перезагрузке сервера.

    Над этим особо не заморачивайтесь. Это нужно просто для того чтобы как-то на небольшие промежутки времени идентифицировать игроков.
    Это часто используется в меню, таймерах, эвентах (событиях).
    Например, в промежутке между открытием меню админу со списком игроков, до момента когда админ выберет игрока, чтобы убедится что игрок выбранный админом всё еще на сервере, а не другой игрок занявший его слот, в описание пунктов меню передаются именно юзерид, а не индексы.

    Для работы с Юзерид есть 2 функции:
    • GetClientUserId - Получает Юзерид по индексу игрока.
    • GetClientOfUserId - Получает индекс игрока по его Юзерид (Возвращает 0 если игрока с таким Юзерид уже нет на сервере).
    Небольшой пример:
    Допустим мы хотим воскресить игрока через минуту после его смерти.

    PHP:
    #include <cstrike> // Чтобы иметь возможность воскрешать игроков
    
    public void OnPluginStart()
    {
        HookEvent("player_death", Event_PlayerDeath);    // Ловим событие игры с именем player_death (смерть игрока)
        // https://wiki.alliedmods.net/Counter-Strike:_Source_Events#player_death
        // Подробнее об эвентах: http://hlmod.ru/threads/sourcepawn-urok-3-sobytija-events.36891/
    }
    
    public void Event_PlayerDeath(Event hEvent, const char[] sEvName, bool bDontBroadcast)    // Функция будет вызвана когда на сервере произойдет событие player_death
    {
        // Из параметров события: https://wiki.alliedmods.net/Counter-Strike:_Source_Events#player_death
        // Мы видим что в событии по имени userid нам передается Юзерид игрока (жертвы).
        // Но плагины же работают с игроками по их индексу.
        int iUserID = hEvent.GetInt("userid");    // Получаем в переменную iUserID Юзерид игрока
        // Теперь нам нужно получить его индекс, имея iUserID
        // Используем для этого вышеприведенную ф-ю GetClientOfUserId
        int iClient = GetClientOfUserId(iUserID);        // Получили индекс игрока из Юзерид, в переменную iClient
    
        // Это всё дело можно было записать сразу так:
        // int iClient = GetClientOfUserId(hEvent.GetInt("userid"));
        // Но дело в том, что Юзерид нам еще понадобится и с целью оптимизации дабы не получать из эвента его снова, получаем его в переменную
    
        // Давайте убедимся что игрок еще на сервере
        if(iClient != 0)    // Можно сократить до if(iClient)
        {
            // Еще можно проверить что-то, например, давайте воскрешать только тех, у кого есть любой админ-флаг
            if(GetUserAdmin(iClient) != INVALID_ADMIN_ID)
            {
                // Как это работает ?
                // Функция GetUserAdmin возвращает уникальный ID админа или INVALID_ADMIN_ID если игрок не админ.
                // Значит если её результат не равен INVALID_ADMIN_ID то игрок админ
              
                // Теперь нам нужно создать задержку в минуту перед воскрешением.
                // Сделаем это таймером:
                CreateTimer(60.0, Timer_RespawnClient, iUserID);
                // Думаю ясно что 60.0 это время, через которое будет вызвана функция Timer_RespawnClient.
                // Я предпочитаю называть обратные вызовы (каллбэки) таймеров начиная с Timer_
                // В данные таймера (то что будет передано в каллбек) передаем Юзерид игрока, который как мы помним хранится в iUserID
            }
        }
    }
    
    public Action Timer_RespawnClient(Handle hTimer, any iUserID) // Это будет вызвано через 60 секунд, после смерти игрока
    {
        // теперь давайте получим индекс игрока, чтобы с ним можно было работать.
        int iClient = GetClientOfUserId(iUserID);        // Получили индекс игрока из Юзерид, в переменную iClient
        // Если к моменту вызова этого каллбека игрок покинул сервер то iClient будет равна 0.
        // Давайте проверим на сервере ли еще игрок:
        if(iClient)
        {
            // Если мы попали сюда то игрок еще на сервере.
            // Теперь убедимся что игрок еще мертв
            if(!IsPlayerAlive(iClient)) // Аналогично if(IsPlayerAlive(iClient) == false)
            {
                // Ну и воскрешаем его
                CS_RespawnPlayer(iClient);
                // Всё
            }
        }
      
        return Plugin_Stop; // Так нужно. Подробнее в уроке про таймеры.
    }

    Это не совсем рабочий код. Точнее он рабочий но с багами.
    Нужно учесть что раунд может закончится, а таймер будет продолжать идти.
    К примеру, если игрок умер, сразу же закончился раунд (через 10 сек после смерти) и игрок снова умер (через 20 сек после начала нового раунда) (менее через за 60 после прошлой смерти).
    Получается что прошло 10 сек + 20 сек = 30 сек. Игрок умирает, опять создается таймер на 60 сек. Но прошлый таймер еще жив и через 30 сек игрока возродит тот таймер, что был создан первым.
    Это уже не соответсвие заданию.
    Тут есть несколько вариантов решения этой проблемы. Наиболее простой:

    PHP:
    #include <cstrike> // Чтобы иметь возможность воскрешать игроков
    
    Handle g_hTimer[MAXPLAYERS+1]; // Да, та самая MAXPLAYERS, которая равна 65
    // Этой строкой мы создали переменную g_hTimer в 66-ти экземплярах, т.е. 66 штук. По одной для каждого игрока с учетом сервера и Source/GO TV
    
    public void OnPluginStart()
    {
        HookEvent("player_death", Event_PlayerDeath);    // Ловим смерть игрока
        HookEvent("player_spawn", Event_PlayerSpawn);     // Ловим спавн игрока
    }
    
    public void Event_PlayerSpawn(Event hEvent, const char[] sEvName, bool bDontBroadcast)
    {
        // Игрок спавнился
        int iClient = GetClientOfUserId(hEvent.GetInt("userid")); // Получаем его индекс
        if(g_hTimer[iClient]) // Если таймер игрока не равен null
        {
            // Через [индекс] мы обращаемся к определенному элементу массива, коим является g_hTimer
            KillTimer(g_hTimer[iClient]); // Убиваем таймер игрока
            g_hTimer[iClient] = null;    // Обнуляем таймер игрока
        }
    }
    
    public void OnClientDisconnect(int iClient)
    {
        // Игрок отключился от сервера
        if(g_hTimer[iClient]) // Если таймер игрока не равен null
        {
            KillTimer(g_hTimer[iClient]); // Убиваем таймер игрока
            g_hTimer[iClient] = null;    // Обнуляем таймер игрока
        }
    }
    
    public void Event_PlayerDeath(Event hEvent, const char[] sEvName, bool bDontBroadcast)
    {
        // Игрок умер
        int iUserID = hEvent.GetInt("userid");
        int iClient = GetClientOfUserId(iUserID);
    
        if(iClient && GetUserAdmin(iClient) != INVALID_ADMIN_ID) // Немного сократим код
        {
            if(g_hTimer[iClient]) // Если таймер игрока не равен null
            {
                KillTimer(g_hTimer[iClient]); // Убиваем таймер игрока
            }
            g_hTimer[iClient] = CreateTimer(60.0, Timer_RespawnClient, iClient);
            // теперь мы можем передать не Юзерид, а индекс, т.к. в случае выхода игрока - таймер будет уничтожен
        }
    }
    
    public Action Timer_RespawnClient(Handle hTimer, any iClient) // Это будет вызвано через 60 секунд, после смерти игрока
    {
        g_hTimer[iClient] = null;    // Сразу обнуляем его таймер
    
        if(!IsPlayerAlive(iClient))
        {
            CS_RespawnPlayer(iClient);
        }
    
        return Plugin_Stop;
    }

С этим кажется разобрались.

Дальше хотелось бы поговорить о цикле по игрокам.
Некоторые события (как начало раунда, конец раунда и т.д. и т.п.) не имеют в параметрах юзерид игрока. И это логично, ибо они вызываются не для конкретного игрока, а для всех одновременно.
Но что делать если нужно что-то сделать с каждым игроком (или каждым игроком определенной команды), к примеру в начале раунда? Выход - цикл по игрокам.

PHP:
#include <sdktools> // Чтобы иметь возможность убивать игроков

public void OnPluginStart()
{
    HookEvent("round_end", Event_RoundEnd);    // Ловим конец раунда
    // https://wiki.alliedmods.net/Generic_Source_Events#round_end
}

public void Event_RoundEnd(Event hEvent, const char[] sEvName, bool bDontBroadcast)
{
    // Раунд закончился
    // Давайте к примеру убьем всех выживших в проигравшей команде, за то, что они не выполнили задания (не спасли заложников, не заложили/обезвредили бомбу)
    // Получим победителя.
    // В параметрах события мы видим: winner    winner team/user id
    // Это значит что по ключу winner мы можем получить индекс победившей команды:
    // 2 - террористы
    // 3 - контр-террористы
    // 1 - ничья
    int iWinner = hEvent.GetInt("winner"); // Получаем индекс победившей команды в переменную iWinner
 
    if(iWinner < 2)    // Если ничья. Наверное можно проверить просто if(iWinner == 1), но лучше предохраниться
    {
        // В случае ничьей не будем делать ничего.
        // Для этого просто выйдем из функции
        return;
        // Всё что после return не будет выполнено
    }
 
    // Сюда мы дойдем в случае если раунд завершился не ничьей
 
    // и так, у нас есть индекс победившей команды.
    // Теперь пройдемся по всем игрокам
    for(int i = 1; i <= MaxClients; ++i)
    // тут мы создали переменную i и присвоили ей значение 1 т.к. с него начинаются индексы игроков.
    // Мы помним что индекс игрока это число от 1 до максимального кол-ва слотов, которым и является MaxClients
    // этот цикл пройдется по всем числам от 1 до MaxClients (включительно)
    {
        // Тут начинается тело цикла
        // Сейчас i равно 1, в следущей итерации будет равно 2, и так дальше пока не станет равно MaxClients
        // Теперь нам нужно убедиться что текущий слот занят игроком
        if(IsClientInGame(i)) // Это очень важная операция и всегда обязательно должна быть первой в условии
        {
            // Проверкой выше мы убедились что i (индекс игрока) занят игроком, и с ним можно работать.
            // т.е. сейчас i это тот же iClient что был в предыдущих примерах.
            // Теперь нам нужно узнать что игрок жив и он в проигравшей команде:
            // GetClientTeam получает индекс команды игрока
            if(GetClientTeam(i) != iWinner && IsPlayerAlive(i))
            // GetClientTeam(i) != iWinner это "Индекс команды игрока не равен индексу победившей команды"
            {
                // Игроку не повезло, он в команде лузеров
                // Валим его
                ForcePlayerSuicide(i); // Убиваем игрока
            }
        }
        // Тут заканчивается тело цикла происходит инкремент i (те самые ++i, прописанные внутри for)
        // i становится на 1 больше и всё тело цикла выполняется с начала с новым значением i
    }
}

Думаю тут тоже не должно возникнуть сложностей.

Просто всегда делайте так:
PHP:
for(int i = 1; i <= MaxClients; ++i)
{
    if(IsClientInGame(i))
    {
        // Вот здесь вы точно значете что игрок в игре и можете с ним работать
        // Ваш код...
    }
}

О сущностях (entity, edict)
Сущности - это как бы объекты на сервере. Кнопки, триггреры, оружия, зоны закупки, точки спавнов, сами игроки, свет и прочее - это всё сущности (далее просто entity). Даже сам сервер это entity.
Максимальное количество entity - 2048.
0 - Сервер
От 1 до MaxClients (включительно) - игроки
От MaxClients до 2048 - entity.
Работа с ними похожа на работу с игроками.
Так же можно проверять валиден ли индекс сущности: IsValidEntity / IsValidEdict
С ними можно проворачивать туже же фишку как с юзер ид игроков. Только у сущностей это называется ссылкой.
  • EntIndexToEntRef - получает ссылку на сущность из её индекса
  • EntRefToEntIndex - получает индекс сущности из её ссылки. В случае провала, вернет INVALID_ENT_REFERENCE (0xFFFFFFFF). В API написано что лучше проверять на не равенство именной это константе, а не -1.
    В Wiki сказано что все функции принимающие как аргумент индекс сущности, могут принимать вместо него ссылку.
    С сущностями нужно работать осторожно т.к. у каждой есть свои свойства и нюансы работы с ними.
    Большинство из них уничтожаются сервером в конце раунда. Подробную информация о сущности можно найти здесь Category:Source Base Entities - Valve Developer Community
    Каждая сущность имеет свой класс (например: дверь (func_door, func_door_rotating), триггер (trigger_multiple), оружие (weapon_*), проп (prop_dynamic, prop_physics) и другие).
Как придет вдохновение еще дополню здесь.

Немного о строках.

Строка - это массив символов. Это значит что помимо обращения к строке, вы можете обращаться к определенным символам в ней.
Конец строки - это нулевой символ.
Т.е. плагин видит строку только до тех пор, пока не встречает в ней нулевой символ. Не '0', а '\0'.
Например:
PHP:
char szMyString[64] = "моя первая строка!";
Размер строки szMyString равен 64. Это значит что в неё можно записать максимально 64 символа (вместе с нулевым)
Длина строки равна 18. Нулевой символ не считается.
Есть несколько функций для работы со строками:
  • PHP:
    sizeof(szMyString)
    - получает размер массива (в данном случае строки). Это оператор пре-процессора. Это значит что эта функция вызывается еще на этапе компиляции плагина.
    Т.е. компилятор заменит
    PHP:
    sizeof(szMyString)
    на 64
  • PHP:
    strlen(szMyString)
    - получает длину строки, т.е. кол-во символов до нулевого. В данном случае вернет 18
Очистка строки.
Я встречал много способов очистки строк:
  • PHP:
    strcopy(szMyString, sizeof(szMyString), "");
  • PHP:
    Format(szMyString, sizeof(szMyString), "");
  • PHP:
    szMyString = "";
  • PHP:
    szMyString[0] = '\0';
  • PHP:
    szMyString[0] = 0;
Хоть их результат одинаковый скорость разная.
Мне больше всего нравится последний вариант (он идентичен предпоследнему). Хоть выглядит сложно, но на самом деле всё очень просто если понимать что происходит.
Мы знаем что конец строки там, где стоит нулевой символ.
Следовательно если 1-й символ строки нулевой - она пустая.
Индексы массивов начинаются с 0, а значит что 1-й символ имеет индекс 0.
Т.е. обращение к первому символу строки выглядит так: szMyString[0]
Так вот если присвоть 1-му символу строки значение 0, то строка будет считаться пустой.
PHP:
szMyString[0] = 0; // То же что и '\0'
Исходя из утверждений выше, так же можно и проверять пуста ли строка:
PHP:
if(szMyString[0] != 0) // Оно же if(szMyString[0] != '\0')
{
    // Строка не пустая
}
Или более сокращенный вариант:
PHP:
if(szMyString[0])
{
    // Строка не пустая
}
Это наиболее быстрые и простые способы очистки и провери строк (не вызывают внешних функций самого SM)

Если вам нужно образать строку после определенного символа можно просто установить его в 0.

PHP:
char szMyString[] = "Часть1|Часть2"; // Размер строки компилятор определит сам, т.к. скобки пустые. Это работает только если задано начальное значение
Что можно сделать с этой строкой?
Получить только "Часть1":
PHP:
int index = FindCharInString(szMyString, '|');
if(index != -1)
{
    szMyString[index] = 0;
}
Теперь с пояснениями:
PHP:
// Ф-я FindCharInString ищет символ в строке и возвращает его индекс. Если символ не найден вернет -1
int index = FindCharInString(szMyString, '|'); // Ищем в строке szMyString символ | и заносим результат в index
// Обратите внимание, что ф-я возвращает индекс первого вхождения символа в строку.
// Это значит что если в строке есть несколько таких символов то она вернет индекс первого
// Если нужно искать символ с конца строки то нужно использовать: FindCharInString(szMyString, '|', true)
// При этом индекс символа будет всё ровно с начала строки
if(index != -1) // Убеждаемся что символ найден
{
    // В даном случае index равен 6
    szMyString[index] = 0; // Устанаваливаем элемент с индексом записанным в index значение 0
    // Мы ж помним что в index записан индекс символа | т.е. 6
    // После всех этих действий szMyString будет равна "Часть1"
}
Получим только "Часть2":
PHP:
int index = FindCharInString(szMyString, '|');
if(index != -1)
{
    // До этого момента всё тоже самое.
    // А дальше происходит магия
    strcopy(szMyString, sizeof(szMyString), szMyString[index+1]);
    // Мы копируем в szMyString, её же но пропуская index символов.
}
Как же так?
Дело в том что компилятор понимает какой тип данных требует ф-я.
Если ей нужна строка то szMyString[index] будет передана с пропуском первых index-х символов. В нашем случае 6 (index)+1 (сам символ |) = 7. И того остается только "Часть2"
Если же ей нужен только 1 символ то будет передан только 7-й символ (т.к. с 0-я индексация) т.е. szMyString[7] = 'Ч'.

Это немного сложно по началу, но если разобраться то всё просто.
Вот пример:
Очень часто бывает нужно узнать что у игрока некоторое оружие в руках
PHP:
char szWeapon[32];
GetClientWeapon(iClient, szWeapon, sizeof(szWeapon)); // Получаем текущее оружие у игрока
// В szWeapon получили к примеру "weapon_deagle"
// Вы хотите узнать не "weapon_ak47" ли это
// Мы видим что начало "weapon_" у всех оружий совпадает, давайте пропустим его
if(StrEqual(szWeapon[7], "ak47")) // Мы пропускаем "weapon_" и сравниваем только "deagle" и "ak47". Таким образом сэкономив 7 итераций цикла сравнения
{
    // Это ak47
}

UPD #1
Немного о типах их преобразованиях.
Часто нужно конвертировать строку в число и наоборот, а так же числа в другие числовые типы (целые в дробные/логические и наоборот).
Как же это сделать?
Вот список ф-й, который помогут вам в этом:


  • StringToInt - Преобразует строку в целое число.
    PHP:
    char szValue[] = "457";
    
    int iValue = StringToInt(szValue);
    // iValue равно 457
    Если в ф-ю передать не валидную строку (например просто текст или дробное число) - вернет 0.
  • StringToIntEx - Аналогично StringToInt но вместо возврата результата, записывает его по указанному адресу.
    PHP:
    char szValue[] = "864";
    int iValue;
    
    StringToIntEx(szValue, iValue);
    // iValue равно 864
  • IntToString - Целое число в строку.
    PHP:
    int iValue = 984864;
    
    char szValue[64];
    IntToString(iValue, szValue, sizeof(szValue);
    // szValue равно "984864"
  • StringToFloat, StringToFloatEx, FloatToString - всё тоже самое но для дробных чисел.
Что касается строк - этого хватит для решения большинства задач.
Для того чтобы говорить о типах следует ввести 2 понятия:
  • Преобразование в тип - наглядный пример со строками выше.
    Это правильное преобразование значения одного типа к соответствующему значению другого типа.
    Например:
    PHP:
    int iValue = 353;
    
    float fValue = float(iValue);
    // fValue равно 353.0
    Здесь ф-я float преобразует целое число в дробное.
    Обратное преобразование:
    • RoundFloat - Окгругляет дробное к ближайшему целому.
      PHP:
      float fValue = 65.33;
      
      int iValue = RoundFloat(fValue);
      
      // iValue равно 65 т.к. 65.33 ближе к 65 чем 66
    • RoundToCeil - Окгругляет дробное к ближайшему целому в большую сторону.
      PHP:
      float fValue = 65.33;
      
      int iValue = RoundToCeil(fValue);
      
      // iValue равно 66
      [php]
      [*][URL='https://sm.alliedmods.net/new-api/float/RoundToFloor']RoundToFloor[/URL] - Окгругляет дробное к ближайшему целому в меньшую сторону.
      [php]
      
      float fValue = 65.76;
      
      int iValue = RoundToFloor(fValue);
      
      // iValue равно 65
    • RoundToNearest - Окгругляет дробное к целому по стандарту IEEE.
    • RoundToZero - Окгругляет дробное до ближайшего целого числа до нуля (дословный перевод).
    Думаю тут всё ясно.
  • Приведение к типу - тут уже всё по другому.
    Единтственное с чем я смог придумать аналогию - с геометрическими фигурами (а точнее их проекциями).
    cfc7bb0dc719b11ee588b6fa0ad1c203.jpg

    Тут мы видим что по факту у нас есть цилиндр.
    А вот если смотреть на него с разных сторон то будем видеть разные фигуры.
    Если посмотреть сверху - увидим круг
    Если сбоку - видим прямоугольник.
    Это как "у каждого своя правда но истина всегда одна".
    Так вот допустим у нас есть переменная:
    PHP:
    int iValue = 654;
    Она имеет тип int, поэтому мы работаем с ней как с целым числом.
    Давайте переведем её в 16-ю систему исчисления и в 2-ю.
    16: 28E
    2: 00000000 00000000 00000010 10001110 (Именно в таким виде она хранится в памяти)
    Приведем это число к дробному представлению:

    PHP:
    float fValue = view_as<float>(654);
    И получаем результат: 9.16449
    Т.е. приведение к типу просто заставляет виртуальную машину работать с тем же значением, как с другим типом.
    Поэтому приведение можно использовать далеко не всегда и не везде.
 
Последнее редактирование:

Kruzya

Участник
Сообщения
12,970
Реакции
10,914
  • Команда форума
  • #2
И так до тех пор пока не дойдет до 2147483647 (максимальное положительное значение int).
После этого вроде бы он сбросится на 1.
Не сбросится.
В отличие от SourceMod, который возвращает всегда подписанное число (т.е. которое может быть и отрицательным), сервер хранит индекс в неподписанном виде. То есть после достижения максимального инта в SP, число пойдет в отрицательное значение.
 

MAGNAT2645

Участник
Сообщения
63
Реакции
7
Я не знаю занимает он один из слотов выделенных для игроков или для него добавляется отдельный (65-й).
По-моему, добавляется отдельный. У меня стоит 32 слота и однажды я включил SourceTV для тестирований. В итоге слотов у меня стало 33. Даже в консоли где-то пишет, что добавляется 1 слот под бота SourceTV.
 

bigboi300

Не удаляю копирайты
Сообщения
153
Реакции
28
  • Мне нравится
Реакции: R1KO

R1KO

fuck society
Сообщения
9,457
Реакции
7,786
  • Команда форума
  • #5
Дополнил инфу
 

Planes

Потерявшиеся точка с запятой
Сообщения
356
Реакции
97
Сколько изучал этот язык, всегда задавался вопросом, откуда это ваше iClient берется :D
Спасибо за урок, нужно было с него начинать)
 

FORTES

Участник
Сообщения
45
Реакции
5
Да бросьте ребят, если сервер на 32 слота и включить tv_enable 1 сервер заберёт 1 слот для GOTV и будет 31 слот отображаться, GOTV будет как игрок его можно будет найти в админ меню например в муте бане или кике.
 

R1KO

fuck society
Сообщения
9,457
Реакции
7,786
  • Команда форума
  • #8
и такс.. "нагорело так сказать"
до меня дошло что для совсем начинающих это проясняет некоторые вещи но не объясняет суть происходящего.
далее будет список правил и советов начинающим:

Вопрос: С чего начать?
Ответ: Берем почти любую книгу по C++ аля "C++ для начинающих/за 24 часа/за 21 день/всей жизни не хватит" (наверное можно и по C но сколько их не читал - они почему-то они как правило хуже объясняют основы) и читаем такие разделы (желательно в таком же порядке):
  • Переменные (и их виды: локальные/глобальные)
  • Типы данных
  • Массивы
  • Циклы
  • Функции

Примеры книг
http://s-engineer.ru/HTKM/cpp-za-21-den.pdf
http://ict.edu.ru/ft/005576/2001-0092-0-01.pdf
http://phbme.kpi.ua/~prat/edu/books/programming/c/1_K_R.pdf
и т.д.
не помню какие из них читал, поэтому конкретное что-то посоветовать не могу, но бегло пробежался - вроде норм

при чтении главное осознавать написанное.


теперь свод советов и правил:
- НИКОГДА (вообще) тупо не копировать код! сначала нужно в нем разобраться, изучить что и как он делает.
- не боимся пробовать. прежде чем что-то спросить нужно 5 раз погуглить (и желательно 3 из них на английском) и 10 раз подумать, попробовать сделать самому, а потом уже, если не получилось - спрашивать
- вся логика должна находиться внутри функций. Вне ф-й может быть только объявление глобальных переменных.

Правильно
PHP:
int a;

public void OnPluginStart()
{
	// какой-то код
}

public void MyFunc()
{
	// какой-то код
	
	if (какое-то условие)
	{
	// какой-то код
	}
}

Не правильно:
PHP:
int a;

// какой-то код

// да, некоторые и так делают.
{
	// какой-то код
}

public void OnPluginStart()
{
	// какой-то код

	//и даже так делают
	{
		// какой-то код
		// по синтаксису так в принципе можно - но не нужно
	}
}

// какой-то код

- Если не компилируется - сначала читаем ошибку. Потом копируем её и даем переводчику. Читаем что он перевел и пытаемся это понять, затем смотрим код и исправляем.
Если сделали всё это раз 10 и по прежнему не получилось исправить - спрашиваем кого-то и пока ждем ответ продолжаем пытаться исправить.

- Побольше смотрим чужие плагины. Если не знаем как что-то реализовать - думаем где это реализовано и можно подсмотреть.
- Если видим что-то непонятно - раскладываем на мелкие составляющие и пытаемся понять.
Например:
есть говнокод
PHP:
 stock void CGOPrintToChat(int iClient, const char[] message, any ...)
{
	int a = strlen(message)+190, l;
	char[] b = new char[a];
	l = ReplaceString(b, VFormat(b, a, message, 3), "\n", "", false);
	for(int i; i < ZCOLOR; i++) ReplaceString(b, a, color_t[i], color_c[i]);
	if(l)
	{
		l++;
		char[][] s = new char[l][192];
		for (int m = ExplodeString(b, "", s, l, 192), p; p < m; p++) PrintToChat(iClient, " %s", s[p]);
	}
	else  PrintToChat(iClient, " %s", b);
}

на первый взгляд страшно и не ясно.
для начала выносим все переданные в параметры ф-и, немножко переформатиурем и получаем:

PHP:
 stock void CGOPrintToChat(int iClient, const char[] message, any ...)
{
	int a = strlen(message)+190;
	char[] b = new char[a];
	
	int len = VFormat(b, a, message, 3); // Вынесли из ReplaceString
	int l = ReplaceString(b, len, "\n", "", false);
	for(int i; i < ZCOLOR; i++)
	{
		ReplaceString(b, a, color_t[i], color_c[i]);
	}
	if(l)
	{
		l++;
		char[][] s = new char[l][192];
		int m = ExplodeString(b, "", s, l, 192); // Вынесли из конструкции цикла
		for (int p = 0; p < m; p++)
		{
			PrintToChat(iClient, " %s", s[p]);
		}
	}
	else  PrintToChat(iClient, " %s", b);
}
с этим уже можно работать

PHP:
 stock void CGOPrintToChat(int iClient, const char[] message, any ...)
{
	int a = strlen(message)+190; // Получаем длину входящей строки с форматом сообщения и добавляем к ней 190 для подстановки значений форматирования
	char[] b = new char[a]; // создаем новую строку с полученным выше размером
	
	int len = VFormat(b, a, message, 3); // форматируем строку (подставляются значения вместо %s/%d и т.д.)
	int l = ReplaceString(b, len, "\n", "", false);// заменяем все переносы строк на эти 2 символа т.к. вероятность их встретить в тексте очень низкая. 
	// это нужно для того чтобы узнать сколько у нас переносов в строках чтобы создать необходимо кол-во буферов. 
	// Хз зачем если можно было просто посчитать циклом, было бы куда быстрее
	for(int i; i < ZCOLOR; i++) // тут мы заменяем текстовые алиасы цветов на их коды
	{
		ReplaceString(b, a, color_t[i], color_c[i]);
	}
	if(l) // если у нас есть переносы строк
	{
		l++; // добавляем 1 т.к. строк будет на 1 больше чем переносов
		// строка1/перенос/строка2/перенос/строка3
		// Переносов - 2, строк -3
		char[][] s = new char[l][192]; // Создаем массив строк в кол-ве полученном выше
		int m = ExplodeString(b, "", s, l, 192); // разделяем наш текст по тем 2-м символам (фактически по переносам строк) и помещаем результат в созданный массив
		// вернет кол-во полученных кусочком
		for (int p = 0; p < m; p++)
		{
			PrintToChat(iClient, " %s", s[p]); // выводим каждый кусок
		}
	}
	else // если у нас нет переносов то выводим сразу наш текст
	{
		 PrintToChat(iClient, " %s", b);
	}
}

мей би когда-то дополню
 

Kruzya

Участник
Сообщения
12,970
Реакции
10,914
  • Команда форума
  • #9
Так же есть еще 1 константа:
C++:
int MaxClients;
Она всегда равна количеству слотов на сервере.
Кстати, об этом.
Это немного не так работает для CS:GO. Оффтоп
В процессе разработки GameX (для тех, кто не в курсе: замена SB и некоторых других систем с модульностью), мы решили, что сервер будет сам слать всю информацию о себе веб-панели периодически, а та сохранять в базе.
По итогу, был написан код, который при смене карты, отправляет информацию с именем карты и максимальным кол-вом игроков (сменить максимальное кол-во слотов на лету - нельзя, потому пошли на такую оптимизацию) Оффтоп:
C++:
void Information_SendStart()
{
    DBGLOG("Information_SendStart()")
    char szCurrentMap[256];
    GetCurrentMap(szCurrentMap, sizeof(szCurrentMap));

    JSONObject hRequest = new JSONObject();
    hRequest.SetString("map", szCurrentMap);
    hRequest.SetInt("max_players", Information_MaxPlayers());

    DBGLOG("Information_SendStart(): Map '%s', MaxPlayers '%d'", szCurrentMap, Information_MaxPlayers())
    GameX_DoRequest("server/info", hRequest, Information_OnStartDelivered);
    CloseHandle(hRequest);
}

/**
* @section Helper functions
*/
static int Information_MaxPlayers()
{
    DBGLOG("Information_MaxPlayers()")
    int iSlots = MaxClients;

    // Setting for respecting max visible players.
    char szValue[4];
    GameX_GetConfigValue("RespectMaxVisiblePlayers", szValue, sizeof(szValue), "0");
    if (szValue[0] != '0')
    {
        DBGLOG("Information_MaxPlayers(): We respect Max visible players. Going to subfunction...")
        int iMaxVisiblePlayers = Information_MaxVisiblePlayers();

        iSlots = (iMaxVisiblePlayers != -1 ? iMaxVisiblePlayers : iSlots);
    }

    return iSlots;
}

static int Information_MaxVisiblePlayers()
{
    static Handle hVisibleMaxPlayers = null;
    static bool bIsSupported = true;

    DBGLOG("Information_MaxVisiblePlayers(): sv_visiblemaxplayers = %x", hVisibleMaxPlayers)
    DBGLOG("Information_MaxVisiblePlayers(): Is feature supported on game? %d", bIsSupported)
    if (!bIsSupported)
    {
        return -1;
    }

    // Initialize convar handle.
    if (hVisibleMaxPlayers == null)
    {
        hVisibleMaxPlayers = FindConVar("sv_visiblemaxplayers");

        // Some games doesn't support `sv_visiblemaxplayers` console variable.
        if (hVisibleMaxPlayers == null)
        {
            DBGLOG("Information_MaxVisiblePlayers(): Game doesn't supported. Disabling feature...")
            bIsSupported = false;
            return -1;
        }
    }

    return GetConVarInt(hVisibleMaxPlayers);
}
Вроде, всё красиво, чётко. Берём значение MaxClients, помещаем в iSlots. Если у нас RespectMaxVisiblePlayers активен (у сервера есть переменная sv_visiblemaxplayers, позволяющая изменять внешнее кол-во слотов), то берём значение наоборот из неё, если она не равна -1 (использовать реальное кол-во слотов).
В теории, этот код не даст сбоя, ведь:
Так же есть еще 1 константа:
C++:
int MaxClients;
Она всегда равна количеству слотов на сервере.
Но, увы и ах. Моему удивлению не было предела, когда вместо реальных 24 слотов, прилетело 64:
43831

43833

43832
Т.е., по итогу, в CS:GO там хранится лимит движка (почему? непонятно), а не реальное кол-во слотов. Чтобы прийти к этому, было потрачено ещё немного времени на добавление дополнительных отладочных логов (кто мог подумать, что фэйл будет на таком банальном месте?) с последующим просмотром.
C-подобный:
L 06/13/2019 - 22:37:20: [GameX.smx] Information_SendStart()
L 06/13/2019 - 22:37:20: [GameX.smx] Information_MaxPlayers()
L 06/13/2019 - 22:37:20: [GameX.smx] GetConfigValue(): RespectMaxisiblePlayers
L 06/13/2019 - 22:37:20: [GameX.smx] Information_SendStart(): Map 'de_dust2', MaxPlayers '64'
Ответ на вопрос "какого чёрта происходит?" нашёлся быстро на официальной вики AM:
Max Players, Clients
CS:GO has four different values that all affect the maximum number of players that can join a game. The lowest one of the four determines the value used.
...
The engine's Maxclients
This is the number known as gpGlobals->maxClients in SM extensions or MM:S plugins and MaxClients in SourcePawn.
It is able to be changed in other games, up to the maximum, by setting the -maxplayers command line parameter. As this doesn't exist in CS:GO, it acts like other games when not set and uses a hardcoded default, 64 for CS:GO.
Ниже по этой же ссылке нам советуют другой способ определения кол-ва слотов:
The GameTypes max can be retrieved in SourcePawn with the new GetMaxHumanPlayers native or in C++ with IServerGameClients::GetMaxHumanPlayers.
Честно, после такой подлянки писать плагины вообще не хочется, ибо "видение мира" подорвано: есть говно-игры, на которых MaxClients работает не так, как мы привыкли к этому в CS:S или TF2, и приходится прибегать к костылям.

P.S.: Стал ли я фиксить проблему со слотами для CS:GO? Нет. И не буду. Буду ссылаться на использование sv_visiblemaxplayers с последующим включением "уважения" этого параметра в конфиге. Ради одной игры париться так смысла не вижу.
 

Temlik

Участник
Сообщения
668
Реакции
174
Очепятки: "выдим", "ка с другим" (поиском по странице)
Сообщения автоматически склеены:

Будет разница в производительности, если заменить MaxClients (64) на GetMaxHumanPlayers (игроков 10-20) к примеру у 100-150 плагинов? То есть мы получем выигрыш в итерациях, но большой ли?
 
Последнее редактирование:

Svyatoy

Участник
Сообщения
335
Реакции
137
Очепятки: выдим, ка с другим (поиском по странице)
Сообщения автоматически склеены:

Будет разница в производительности, если заменить MaxClients (64) на GetMaxHumanPlayers (игроков 10-20) к примеру у 100-150 плагинов? То есть мы получем выигрыш в итерациях, но большой ли?
В CS:GO он возвращает всегда 64, насколько я знаю. Смысл в подобном есть, можно ввести свой define MAX_CLIENTS 20, например, и перекомпилировать заменив все MaxClients => MAX_CLIENTS и MAXPLAYERS => MAX_CLIENTS+1, но только, если плагин не публичный т.к. в публичном каждому придётся править исходних и компилировать, что логично...
1) Получишь экономию в памяти
2) Получишь сокращение времени прохода циклов
 

Svyatoy

Участник
Сообщения
335
Реакции
137
его нет смысла ибо он равен кол-ву слотов
Пусть будет для надёжности, а то по невнимательности могут создать массив меньше количества слотов и получится куча ошибок :)
А так да, спасибо за правку!
 

Nekro

Терра инкогнита
Сообщения
4,025
Реакции
2,260
Пусть будет для надёжности, а то по невнимательности могут создать массив меньше количества слотов и получится куча ошибок :)
А так да, спасибо за правку!
Крайне узконаправленно, скорее будет больше вреда, чем пользы. Переделывать каждый раз под очередной сервер такое себе дело, а когда 120 плагинов переделывать, то возможно ошибиться. Выгода крайне мизирная, компьютеру без особой разницы 20 раз посчитать или 120. Мартышкин труд одним словом, бесполезное занятие
 

Svyatoy

Участник
Сообщения
335
Реакции
137
Крайне узконаправленно, скорее будет больше вреда, чем пользы. Переделывать каждый раз под очередной сервер такое себе дело, а когда 120 плагинов переделывать, то возможно ошибиться. Выгода крайне мизирная, компьютеру без особой разницы 20 раз посчитать или 120. Мартышкин труд одним словом, бесполезное занятие
Тогда в чём смысл увеличивать КПД ДВС? Какая разница будет он расходовать 7л/100км или 21л/100км
Речь не идёт о всех плагинах, поэтому и говорю - что только для непубличного использования, и понимая все возможные плюсы и минусы.
 

Nekro

Терра инкогнита
Сообщения
4,025
Реакции
2,260
Разве у нас речь не о 300%? При 20 игроках на сервере мы выделяем памяти под 65?
Память под 65? Т.е. вместо 0.005 мбайт памяти мы выделим 0.007 мбайт памяти и в общем у нас будет забито 0.425 гигабайта из 2.0 гигабайт
Сообщения автоматически склеены:

Или если угодно
 

Вложения

  • Screenshot_17.png
    Screenshot_17.png
    11.2 КБ · Просмотры: 39
  • Screenshot_18.png
    Screenshot_18.png
    7.9 КБ · Просмотры: 35

Temlik

Участник
Сообщения
668
Реакции
174
Да с памятью и так всё норм. Я имею ввиду, будет ли легче камню, особенно если он бюджетный?
 

Svyatoy

Участник
Сообщения
335
Реакции
137
Память под 65? Т.е. вместо 0.005 мбайт памяти мы выделим 0.007 мбайт памяти и в общем у нас будет забито 0.425 гигабайта из 2.0 гигабайт
Сообщения автоматически склеены:

Или если угодно
Я это прекрасно понимаю, у каждого свой подход к этому. Да, экономия не о чём, но если я в своём непубличном моде могу просто создать define и сэкономить память - я это сделаю. В публичном, это конечно бессмысленно, так как минусы с лихвой перекроют все плюсы.
Сообщения автоматически склеены:

Да с памятью и так всё норм. Я имею ввиду, будет ли легче камню, особенно если он бюджетный?
Нет, как уже говорили ранее MaxClients равно количеству слотов.
 
Сверху Снизу