Veteran Member
Join Date: Nov 2017
Location: Ukraine on fire
|
03-27-2020
, 12:42
Re: [TUT] SourcePawn Scripting - Tips, Basics to Advanced
|
#32
|
Minimal sample "How to stop (delete) global timer"
I see a really lot of questions about :
Quote:
- Native "KillTimer" reported: Invalid timer handle XXX (error 3)
- Handle XXX is invalid (error 1)
- TIMER_FLAG_NO_MAPCHANGE doesn't stop timer when round ends/lost etc...
|
when somebody wants to start single or repeatable timer and stop it on round_end, manually or using TIMER_FLAG_NO_MAPCHANGE.
To do it without errors, you have to create timer correcly and nullify global variable in correct events.
Here are minimal correct samples with comments:
1. Global single timer (timer_single.sp)
Spoiler
PHP Code:
#include <sourcemod>
Handle g_iTimer; // our global timer's variable
public void OnPluginStart()
{
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy);
}
// let's create timer when round starts:
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
delete g_iTimer; // delete previous timer in case "round_start" fired twice
g_iTimer = CreateTimer(30.0, Timer_Sample);
// do not put here TIMER_FLAG_NO_MAPCHANGE !!!
// because handle's variable will not be zeroed automatically in case map change happens before timer is fired
}
public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
{
delete g_iTimer; // stop timer if it is not fired yet, so this timer will not fire on the next round or between level load
// "delete" keyword stops the timer and automatically assign null with variable.
// Notice: delete 0 will not raise an exception.
}
public void OnMapEnd() // required, because forcible change level doesn't fire "round_end" event
{
delete g_iTimer;
}
public Action Timer_Sample(Handle timer)
{
//do something
PrintToChatAll("tick");
// do not forget to zero timer's handle here, because after callback finishes, timer's handle become invalid
g_iTimer = null;
}
2. Global repeatable timer (timer_repeat.sp)
Spoiler
PHP Code:
#include <sourcemod>
Handle g_iTimer;
public void OnPluginStart()
{
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy);
}
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
// let's start repeatable timer on each round start
delete g_iTimer; // prevent double-run (just in case)
// do not put here TIMER_FLAG_NO_MAPCHANGE !!!
// because handle variable will not be zeroed
g_iTimer = CreateTimer(1.0, Timer_Sample, _, TIMER_REPEAT);
}
public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
{
delete g_iTimer;
// "delete" keyword automatically assign null with variable.
// Notice: delete 0 will not raise an exception.
}
public void OnMapEnd() // required, because ForceChangeLevel doesn't cause "round_end" event
{
delete g_iTimer;
}
public Action Timer_Sample(Handle timer)
{
// do something
PrintToChatAll("tick");
return Plugin_Continue;
/*
Notice 1: if you want forcibly stop timer here, you have to zero handle's variable firstly
g_iTimer = null;
return Plugin_Stop;
Notice 2: Never try to delete or KillTimer() within this timer callback itself.
*/
}
3. Global single per-client timer (timer_single_per-client.sp)
Spoiler
PHP Code:
#include <sourcemod>
Handle g_iTimer[MAXPLAYERS+1];
public void OnPluginStart()
{
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy);
RegConsoleCmd("sm_timer", CmdTestTimer, "Re-starts timer for testing")
}
Action CmdTestTimer(int client, int args)
{
delete g_iTimer[client];
g_iTimer[client] = CreateTimer(15.0, Timer_Sample, GetClientUserId(client));
}
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
for ( int i = 1; i <= MaxClients; i++ )
{
if ( IsClientInGame(i) )
{
// start single per-client timer
delete g_iTimer[i];
g_iTimer[i] = CreateTimer(3600.0, Timer_Sample, GetClientUserId(i));
}
}
}
public void OnClientDisconnect(int client)
{
delete g_iTimer[client];
}
void Reset()
{
for ( int i = 1; i <= MaxClients; i++ )
{
delete g_iTimer[i];
}
}
public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
{
Reset();
}
public void OnMapEnd()
{
Reset();
}
public Action Timer_Sample(Handle timer, int UserId)
{
int client = GetClientOfUserId(UserId);
if ( client && IsClientInGame(client) )
{
g_iTimer[client] = null;
// do something
// ...
PrintToChat(client, "tick");
}
}
4. Global repeatable per-client timer (timer_repeat_per-client.sp)
Spoiler
PHP Code:
#include <sourcemod>
Handle g_iTimer[MAXPLAYERS+1];
public void OnPluginStart()
{
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy);
RegConsoleCmd("sm_timer", CmdTestTimer, "Starts timer for testing")
}
Action CmdTestTimer(int client, int args)
{
// stop timer if it was already running
// this will also set the variable to zero
delete g_iTimer[client];
PrintToChatAll("timer = %i", g_iTimer[client]);
// starts repeatable timer
// pass reference number of client, so we can ensure it is not replaced when timer will actually fire
g_iTimer[client] = CreateTimer(15.0, Timer_Sample, GetClientUserId(client), TIMER_REPEAT);
}
public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast)
{
for ( int i = 1; i <= MaxClients; i++ )
{
if ( IsClientInGame(i) )
{
// start single per-client timer
delete g_iTimer[i];
g_iTimer[i] = CreateTimer(15.0, Timer_Sample, GetClientUserId(i), TIMER_REPEAT);
}
}
}
public void OnClientDisconnect(int client)
{
// ensure, you kill timer when player disconnects
delete g_iTimer[client];
}
void Reset()
{
for ( int i = 0; i <= MaxClients; i++ )
{
delete g_iTimer[i]; // stops timer for each player
}
}
public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast)
{
Reset();
}
public void OnMapEnd() // required, because ForceChangeLevel doesn't cause "round_end" event
{
Reset();
}
public Action Timer_Sample(Handle timer, int UserId)
{
// check is it the same client
int client = GetClientOfUserId(UserId);
// ensure it is valid
if ( client && IsClientInGame(client) )
{
// do something
// ...
PrintToChat(client, "tick");
return Plugin_Continue;
/* or, if you want to Stop timer,
don't forget to 'null' timer firstly:
g_iTimer[client] = null;
return Plugin_Stop;
*/
}
// here you can't null timer, because you don't know correct client index for accessing handle's variable,
// so OnClientDisconnect() forward is used instead
return Plugin_Stop;
}
5. (No global var.) Pause repeatable timer
- without the need to manage handle in global variable (timer_repeat_pause.sp)
Spoiler
PHP Code:
/*
This sample demonstrates 'Slapping player' after 30.0 sec delay on round start and repeat each 2 seconds until round ends.
Thanks to Marttt for idea and code.
*/
#pragma semicolon 1
#pragma newdecls required
#include <sourcemod>
#include <sdktools>
#define WAIT_TIME 30.0
float g_fRoundStartTime;
public void OnPluginStart()
{
HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy);
HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy);
// starts the timer that will never be destroed
CreateTimer(2.0, Timer_CheckPlayerZone, _, TIMER_REPEAT);
}
public Action Event_RoundStart(Handle event, const char[] name, bool dontBroadcast)
{
g_fRoundStartTime = GetEngineTime();
}
public Action Event_RoundEnd(Handle event, const char[] name, bool dontBroadcast)
{
g_fRoundStartTime = 0.0;
}
public void OnMapEnd()
{
g_fRoundStartTime = 0.0;
}
public Action Timer_CheckPlayerZone(Handle timer)
{
if ( g_fRoundStartTime == 0.0 ) // pause timer between "round_end" - "round_start" events
return Plugin_Continue;
if ( GetEngineTime() - g_fRoundStartTime < WAIT_TIME ) // pause timer within 30.0 sec interval after "round_start"
return Plugin_Continue;
for ( int i = 1; i <= MaxClients; i++ )
{
if ( IsClientInGame(i) && IsPlayerAlive(i) )
{
SlapPlayer(i, 20, true);
PrintToChat(i, "You are being slapped!");
}
}
return Plugin_Continue;
}
-------------------
General tips:- Do not use TIMER_FLAG_NO_MAPCHANGE flag if you assign timer's handle to a global variable to control the timer via this variable later.
- TIMER_FLAG_NO_MAPCHANGE flag can be safely used if you create timer without assigning its handle to a variable.
- When you kill timer outside the timer's callback, use 'delete' keyword.
- When you kill repeatable timer within its own callback, use "return Plugin_Stop" only!
- Do not use "delete" or "KillTimer()" there, otherwise you might receive error:
Quote:
Plugin "XXX.smx" encountered error 23: Native detected error
[SM] Invalid timer handle XXX (error 3) during timer end, displayed function is timer callback, not the stack trace
|
- You should always assign zero to timer's global variable in the very last line of timer's callback (before return) if timer's handle is about to destruct, that happen:
- for repeatable timer, when you pass "return Plugin_Stop"
- for single time, automatically when callback finishes.
When your global variable is array-based and you don't know how to find it within timer's callback:
- with per-client timers, use OnClientDisconnect() forward to do it.
- with incremental based global timers:
Spoiler
such as:
PHP Code:
const int MAX = 100;
Handle g_iTimer[MAX];
int g_iTimerCount;
public void OnPluginStart()
{
g_iTimer[g_iTimerCount++] = CreateTimer( 1.0, Timer_Sample );
g_iTimer[g_iTimerCount++] = CreateTimer( 2.0, Timer_Sample );
}
to nullify such global variables in timer's callback you can use a helper function:
PHP Code:
void NullifyHandle(Handle timer)
{
for( int i = 0; i <= g_iTimerCount; i++ )
{
if( timer == g_iTimer[i])
{
g_iTimer[i] = null;
break;
}
}
}
Usage:
PHP Code:
public Action Timer_Sample(Handle timer)
{
// some code
// ...
NullifyHandle(timer); // finds corresponding value in global array of timer handles and zeroing it.
return Plugin_Stop;
}
- Delete timer before creating it to prevent it from firing twice, since some events like "round_start" could be fire multiple times.
- To stop timer on round end, use both "round_end" event and "OnMapEnd()" forward, because "round_end" doesn't fire when map is forcibly changed.
Good luck!
P.S. If you found some grammar mistakes, please PM me.
__________________
Last edited by Dragokas; 01-09-2021 at 09:53.
|
|