Работа таймера, особенности

Nekro

Терра инкогнита
Сообщения
4,169
Реакции
2,500
Здравствуйте, товарищи!
Есть следующий пример кода таймера
example_timer:
Handle hTimerPlayer[MAXPLAYERS+1];

public void OnPluginStart()
{
    HookEvent("player_death", Event_PlayerDeath, EventHookMode_Post);
}

public void OnClientDisconnect(int client)
{
    if(hTimerPlayer[client])
        delete hTimerPlayer[client];
}

void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast)
{
    int client = GetClientOfUserId(event.GetInt("userid"));

    if(hTimerPlayer[client])
        delete hTimerPlayer[client];
    hTimerPlayer[client] = CreateTimer(1.5, Timer_Callback, GetClientUserId(client));
}

public Action Timer_Callback(Handle timer, int UserId)
{
    int client = GetClientOfUserId(UserId);

    if (IsValidClient(client))
    {
        hTimerPlayer[client] = null;
    }

    return Plugin_Stop;
}

bool IsValidClient(int client)
{
    return 0 < client <= MaxClients && IsClientInGame(client);
}
В данном варианте редко, но может возникать ошибка удаления уже невалидного handle, если в массиве осталась устаревшая ссылка на таймер.
C-подобный:
[SM] Exception reported: Handle 44a00880 is invalid (error 3)
[SM] Blaming: example_timer.smx
[SM] Call stack trace:
[SM] [0] CloseHandle
Предположение такое, что иногда события выхода игрока, а так же форвард OnClientDisconnect - могут не успевать его поймать и игрок выходит раньше, чем форвард успевает сработать. В результате по userid уже нельзя получить валидный индекс игрока.
А callback таймера в таком случае получает уже невалидный index == 0

Следовательно обнулить переменную таймера мы не можем
hTimerPlayer[client] = null;
Так как отсутствует index вышедшего игрока.

int client = GetClientOfUserId(UserId);

Есть несколько вариантов решения:

1) А если пойти во все тяжкие и передавать не UserId игрока, а сразу его index?
Тогда мы даже при его выходе совершенно точно будем знать index и сможем обнулить переменную
example_timer_2:
Handle hTimerPlayer[MAXPLAYERS+1];

public void OnPluginStart()
{
    HookEvent("player_death", Event_PlayerDeath, EventHookMode_Post);
}

public void OnClientDisconnect(int client)
{
    if(hTimerPlayer[client])
        delete hTimerPlayer[client];
}

void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast)
{
    int client = GetClientOfUserId(event.GetInt("userid"));

    if(hTimerPlayer[client])
        delete hTimerPlayer[client];
    hTimerPlayer[client] = CreateTimer(1.5, Timer_Callback, client);
}

public Action Timer_Callback(Handle timer, int client)
{
    if (IsValidIndex(client))
    {
        hTimerPlayer[client] = null;

        if(IsValidClient(client))
        {
            //    Шаманство над валидным игроком
        }
    }

    return Plugin_Stop;
}

bool IsValidClient(int client)
{
    return 0 < client <= MaxClients && IsClientInGame(client);
}

bool IsValidIndex(int client)
{
    return 0 < client <= MaxClients;
}

Так же есть функция IsValidHandle · handles · SourceMod Scripting API Reference вопрос на сколько она рабочая..

В нашем чате (кстати заходим SourcePawn ) товарищи с сообщества Nebraska и Vit_ amin поделились двумя следующими идеями подходов к таймерам

2) Вот вариант на фреймах

example_timer_3:
/*
**    Альтернативный вариант таймера #1
*/

enum struct PlayerTimer
{
    bool active;
    float endTime;
}

PlayerTimer g_TimerPlayer[MAXPLAYERS + 1];

//    Старт таймера
void StartPlayerTimer(int client, float seconds)
{
    g_TimerPlayer[client].active = true;
    g_TimerPlayer[client].endTime = GetGameTime() + seconds;
}

//    Сброс таймера
void ResetPlayerTimer(int client)
{
    g_TimerPlayer[client].active = false;
    g_TimerPlayer[client].endTime = 0.0;
}

public void OnPluginStart()
{
    HookEvent("player_death", Event_PlayerDeath, EventHookMode_Post);
}

public void OnClientDisconnect(int client)
{
    ResetPlayerTimer(client);
}

void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast)
{
    int client = GetClientOfUserId(event.GetInt("userid"));

    if (!IsValidClient(client))
        return;

    StartPlayerTimer(client, 1.5);
}

public void OnGameFrame()
{
    float gameTime = GetGameTime();

    for (int client = 1; client <= MaxClients; client++)
    {
        if (!g_TimerPlayer[client].active)
            continue;

        if (gameTime < g_TimerPlayer[client].endTime)
            continue;

        ResetPlayerTimer(client);
        Timer_PlayerCallback(client);
    }
}

void Timer_PlayerCallback(int client)
{
    if (!IsValidClient(client))
        return;

    // Тут логика после истечения таймера
}

bool IsValidClient(int client)
{
    return 0 < client <= MaxClients && IsClientInGame(client);
}

3) Вот вариант с ДатаПаком

example_timer_4:
/*
**    Альтернативный вариант таймера #2
*/

int g_PlayerGen[MAXPLAYERS + 1];
int g_TimerGen[MAXPLAYERS + 1];

public void OnClientConnected(int client)
{
    g_PlayerGen[client]++;
    g_TimerGen[client] = 0;
}

public void OnClientDisconnect(int client)
{
    g_PlayerGen[client]++;
    g_TimerGen[client] = 0;
}

void StartPlayerTimer(int client, float seconds)
{
    DataPack dp = new DataPack();

    g_TimerGen[client]++;

    dp.WriteCell(client);
    dp.WriteCell(g_PlayerGen[client]);
    dp.WriteCell(g_TimerGen[client]);

    CreateTimer(seconds, Timer_PlayerCallback, dp);
}

public Action Timer_PlayerCallback(Handle timer, DataPack dp)
{
    dp.Reset();

    int client = dp.ReadCell();
    int playerGen = dp.ReadCell();
    int timerGen = dp.ReadCell();

    delete dp;

    if (!IsValidIndex(client))
        return Plugin_Stop;

    if (g_PlayerGen[client] != playerGen)
        return Plugin_Stop;

    if (g_TimerGen[client] != timerGen)
        return Plugin_Stop;

    if (!IsValidClient(client))
        return Plugin_Stop;

    // Актуальный таймер
    PrintToServer("Таймер отработал для %N", client);

    return Plugin_Stop;
}

bool IsValidClient(int client)
{
    return 0 < client <= MaxClients && IsClientInGame(client);
}

bool IsValidIndex(int client)
{
    return 0 < client <= MaxClients;
}

4) Так же можно модифицировать первый вариант и не выбирать index или UserId, а передавать через датапак сразу и то и то

Подведём итоги:

1) Стандартный пример
Классический вариант с CreateTimer и хранением Handle в массиве.
Рабочий вариант, но в редких случаях может приводить к ошибкам удаления невалидного handle,
если в массиве осталась устаревшая ссылка на таймер.

2) Передача client index вместо userid
Позволяет всегда знать индекс слота и корректно обнулить переменную таймера
даже если игрок уже вышел.
Однако не защищает от ситуации, когда слот игрока будет переиспользован
новым подключением.

3) Таймер через OnGameFrame
Реализуется без Handle и CreateTimer.
Таймер хранит только время окончания и проверяется каждый кадр.
Подходит для простых локальных таймеров на игроков и полностью исключает
проблемы с удалением handle.

4) Вариант через DataPack и поколения (generation)
Позволяет безопасно использовать CreateTimer, защищая:
- от смены игрока в том же слоте
- от устаревших таймеров того же игрока

Старые таймеры при этом не удаляются, а просто игнорируются в callback.

5) Комбинированный вариант
Можно передавать через DataPack сразу:
- client index
- userid

и дополнительно использовать проверку валидности handle через
IsValidHandle().

Такой подход даёт дополнительную защиту и более гибкий контроль над таймерами.
 

Grey83

не пишу плагины с весны 2022
Сообщения
8,805
Реакции
5,254
в редких случаях может приводить к ошибкам удаления невалидного handle, если в массиве осталась устаревшая ссылка на таймер.
А можно подробностей больше в каких случаях такое может случиться? Очень уж любопытно.

Ну и в первом с третьим примерах кода все IsValidClient() можно (даже скорее нужно) поменять на IsClientInGame() и при этом работа кода не то что не ухудшится, а даже немного лучше станет. =)


Можно ещё ставить таймерам флаг, чтобы они при смене карты вырубались, но тогда ещё обязательно нужно добавлять OnMapEnd(), в котором будет цикл по всем хэндлам таймеров с приравниванию их значений к null. Хотя должно хватать простого их обнуления в OnClientDisconnect().
 

Nekro

Терра инкогнита
Сообщения
4,169
Реакции
2,500
А можно подробностей больше в каких случаях такое может случиться? Очень уж любопытно.
В том и проблема, что кода 3 строчки, а ошибка выходит. То при попытке удаления при Event_PlayerDeath, то при OnClientDisconnect, одним словом при удалении. А визуально уязвимостей не видно, в связи с чем предположил, что может быть проблема с обнулением хендела в момент выхода игрока. Что даёт проблему при удалении уже у нового игрока в Event_PlayerDeath
Ну и в первом с третьим примерах кода все IsValidClient() можно (даже скорее нужно) поменять на IsClientInGame() и при этом работа кода не то что не ухудшится, а даже немного лучше станет. =)
По идеи да, но как ловилась ошибка в событии смерти или воскрешение, теперь перестраховываюсь. Проверка маленькая - а сон крепкий)
Можно ещё ставить таймерам флаг, чтобы они при смене карты вырубались, но тогда ещё обязательно нужно добавлять OnMapEnd(), в котором будет цикл по всем хэндлам таймеров с приравниванию их значений к null. Хотя должно хватать простого их обнуления в OnClientDisconnect().
На практике такой код использую для таймеров в 0.1 - 1.5 секунды. Если что то дольше, то конечно обязательно нужно.

На сколько рентабельно считаете передавать index и UserId через datapack в таймере?
Так же ни разу не видел, что бы кто либо использовал IsValidHandle()
 

Dragokas

Добрая душа
Сообщения
237
Реакции
223
Nekro, у вас схожая проблема с этой темой.
На мой взгляд оптимальным вариантом будет оставить всё как есть, а таймер обнулять найдя нужный перебором:
C-подобный:
public Action Timer_Callback(Handle timer, int UserId)
{
    int client = GetClientOfUserId(UserId);
    // ...
   
    NullifyHandle(timer);
   
    return Plugin_Stop;
}

void NullifyHandle(Handle timer)
{
    for( int i = 0; i < sizeof(hTimerPlayer); i++ )
    {
        if( timer == hTimerPlayer[i])
        {
            hTimerPlayer[i] = null;
            break;
        }
    }
}

Второй вариант: если это допустимо тз, убрать вообще везде весь код завершения таймера (если игрок не планирует умирать дважды за 1,5 сек.).

а оно вроде не работало совсем
Не знаю как раньше, на sm 1.13 вполне себе работает:
C-подобный:
public void OnPluginStart()
{
    Handle hTimer = CreateTimer(1.0, OnTrigger);
    PrintToServer("Handle 0x%X valid? %b", hTimer, IsValidHandle(hTimer));
    CloseHandle(hTimer);
    PrintToServer("Handle 0x%X valid? %b", hTimer, IsValidHandle(hTimer));
}

Action OnTrigger(Handle timer) { return Plugin_Continue; }
Handle 0x1B5000E2 valid? 1
Handle 0x1B5000E2 valid? 0
Кроме того, что в этой теме ему не место. Ну и из билда 7304 он удален из API.
У меня есть пару вариантов более-менее оправданного (без-альтернативного) использования, но не буду ими здесь оффтопить.
 

Nekro

Терра инкогнита
Сообщения
4,169
Реакции
2,500
если игрок не планирует умирать дважды за 1,5 сек
Как раз использую его для воскрешения, так что вполне может и планировать.
Сообщения автоматически склеены:

таймер обнулять найдя нужный перебором
Отличный вариант
 

Reg1oxeN

Участник
Сообщения
417
Реакции
333
То при попытке удаления при Event_PlayerDeath, то при OnClientDisconnect, одним словом при удалении.
а может это, как его, занулить хотя бы элемент в массиве после удаления?
ну, чтобы он, хотя бы, стал не валидным для любого следующего вызова if.
 

Nekro

Терра инкогнита
Сообщения
4,169
Реакции
2,500
а может это, как его, занулить хотя бы элемент в массиве после удаления?
ну, чтобы он, хотя бы, стал не валидным для любого следующего вызова if.
Так если удаление успешно, то можно и занулить
 

WeSTMan

А вот тут текст!
Сообщения
854
Реакции
534
Так если удаление успешно, то можно и занулить
Кажется, что у тебя таймер закрываться может по истечению карты,так как флаги стоят по дефолту и вроде бы там нет проверки на валидный.
Он закрывает таймер, но на нал не сбрасывает,а потом ты снова закрываешь

1. Таймер закрываем см и не сбрасывает хкндл (твою переменную)
2. Ты делаешь проверку,думая,что контролируешь таймер полностью,но это не так)
3. Т к хкндл не сброшен см,то ты снова закрываешь таймер и получаешь ошибку
Сообщения автоматически склеены:

В конце карты сбрасывай свою переменную в нал без удаления таймера и должно быть ок
 
Последнее редактирование:
Сверху Снизу