[Tutorial] Anti-Pause Kill System
#1

Anti-Pause Kill System


Introduction
Hey everyone. This tutorial will explain how to implement a system that will allow you to "kill" paused players, which will be useful for DM servers where players will attempt unfair ways to avoid death. While my explanation is meant for novice scripters and is pretty simple in concept, I will also skim briefly on the topic of SA-MP player updates and mention some unrelated scripting tips that will lead to better coding habits. Hopefully by the end of this tutorial you will have a nice anti-pause kill system as well as a better understanding on those topics.
Why the quotes on "Kill"?
You can't really kill a player that is paused, at least not immediately. When a player's client sends updates to the server, they're sent to the callback OnPlayerUpdate. This means that the player's client-sided actions are seen by the player first before the rest of the server sees them. If a player happens to be paused, those updates don't get sent to OnPlayerUpdate and the rest of the server won't see those updates. Updates that don't get sent while a player is paused include Health/Armor loss and Death, which are needed in order for the rest of the server to see that a player has been killed. There are a few exceptions that will continue to update while a player is paused which will allow us to simulate a "kill", but we'll get to that later.
Paused Player
The first thing we want to do in order to be able to kill a paused player, is to obviously check whether the player is paused. This can be done with a global player array and a simple function.

Код:
// Top of your script after #include <a_samp>
#undef MAX_PLAYERS
#define MAX_PLAYERS // YOUR SERVER SLOT NUMBER HERE

 // Place this somewhere at the top of your script, below the redefine and outside all functions and callbacks
new gPlayerLastUpdate[MAX_PLAYERS]
MAX_PLAYERS by default is 500. Depending on how many slots your server has, redefine MAX_PLAYERS to that number. That way you won't have unused space (and wasted memory) in your array. We also want to place the gPlayerLastUpdate array under the callback OnPlayerUpdate, so we can know at all times when the player has last sent client information to the server. We'll set it to GetTickCount().

Код:
public OnPlayerUpdate(playerid)
{
    gPlayerLastUpdate[playerid] = GetTickCount();

    // Don't forget to return 1 or the player will not send updates to the server!
    return 1;
}
What this will do is set the player's variable to the server's uptime about every 30 times per second. Since GetTickCount() is updated every milisecond, OnPlayerUpdate will be the best choice for achieving the most accurate uptime information without having to create another timer. Note from the wiki page that GetTickCount() will cause problems for servers that have had uptimes of over 24 days. After that period, the variable we use will no longer be reliable so it would be a good idea to have the server restart itself before then, provided you plan to have your server up for that long. That task will be left to you.

The next thing we must do is create a stock function to check if a player is paused.

Код:
stock IsPlayerPaused(playerid, delay = 2000)
{
    return (GetTickCount() > (gPlayerLastUpdate[playerid] + delay)) ? true : false;
}
You may be wondering a few things. First, what is the "delay = 2000"? That is the default parameter value for "delay" so we don't have to set that parameter to a value every time. For example, we could use the function as IsPlayerPaused(playerid) without the delay parameter and by default the value will be 2000. Why have this parameter in the first place? There may be times when we want to change this delay, in case we want to check if a player has not been sending updates longer than 2000 milliseconds. (Player could be lagging for 2 seconds, for example)
Note that default parameter values cannot be set for public functions.

You may also be wondering what the "?" and ":" symbols are for. These are the symbols for the ternary operator, which can be used as an abbreviated form of if-else statements. There is no speed difference, only that the ternary operator uses one line whereas if-else statements use at least 2 lines.

The above statement
Код:
return (GetTickCount() > (gPlayerLastUpdate[playerid] + delay)) ? true : false;
is the equivalent of
Код:
if (GetTickCount() > (gPlayerLastUpdate[playerid] + delay)) {
    return true;
} else {
    return false;
}
Although I find the first example more convenient and less verbose, feel free to use the if-else statements if you don't want to use the ternary operator. For more info about this, see this thread.

Finally, the if (GetTickCount() > (gPlayerLastUpdate[playerid] + delay)) statement means that if the server uptime is greater than the player's last GetTickCount() update (which was previous set in OnPlayerUpdate) plus the delay time, then the player did not return the OnPlayerUpdate callback with a new gPlayerLastUpdate[playerid] value, and is "paused".
When to "Kill" a Paused Player
Let's think about how we'll be able to achieve this. Suppose we have Player A and Player B shooting each other. Player A decides to pause. B tries to lower A's health, but since A's client isn't sending updates to the server and isn't receiving updates either, A's health stays the same. What we could do here is work with the OnPlayerGiveDamage callback.

Difference Between OnPlayerGiveDamage and OnPlayerTakeDamage
By default, a player's health/armor loss is sent to the OnPlayerTakeDamage callback. OnPlayerGiveDamage is called when the playerid accurately on his screen shoots damagedid (as in no lag-shooting), but the amount of damage isnt actually updated to the damagedid's health/armor. Unlike OnPlayerTakeDamage, which depends on the playerid sending updates to the server (losing health/armor from issuerid, taking fall damage, etc), OnPlayerGiveDamage depends on the playerid giving damage to damagedid. This means that regardless of Player A pausing, Player B, as he's shooting Player A, will still send updates like damage amount and weapon used to the server.
Here is an example of what you could work with in OnPlayerGiveDamage

Код:
new gPauseKillWarnings[MAX_PLAYERS char];

public OnPlayerConnect(playerid)
{
    gPauseKillWarnings{playerid} = 0; // reset for new players
}

public OnPlayerGiveDamage(playerid, damagedid, Float:amount, weaponid)
{
    // We want to make sure that players on the same team cannot "pause kill" each other
    // If you have other damage-immune systems in place like spawn protection, check for those as well

    if (GetPlayerTeam(playerid) != GetPlayerTeam(damagedid)) {

        if (IsPlayerPaused(damagedid, 6000)) {

            // Player hasn't been sending updates for quite some time, issue the kill
            PauseKill(damagedid, playerid, weaponid);

        } else if (IsPlayerPaused(damagedid)) {

            // Player could be lagging, or could be avoiding the pause kill by pausing for less than the delay above
            // Let's increment some warnings just in case
                    gPauseKillWarnings{damagedid}++;
                    if (gPauseKillWarnings{damagedid} >= 5) {
                        PauseKill(damagedid, playerid, weaponid);
                    }
        }
    }
}
There are two possible outcomes here to deal with giving damage to a paused player. Either the paused player has been paused for a while (6000 millisecond delay) which in that case we kill the player, or the player could be lagging. For the latter case, we keep incrementing warnings until they reach 5 (or some arbitrary number you can set for yourself), and from there we kill the player. This is effective even if the player tries to avoid a pause detection by pausing very briefly and unpausing, because the warnings will most likely still stack up. Maybe for your server the default 2000 millisecond delay is too long. Feel free to edit this value and the max warning amount to fit your needs.

You'll notice the peculiar brackets used for the array gPauseKillWarnings. This is what's called a "char array" which can store values from 0 to 255 rather than the normal array's range of -2,147,483,648 to 2,147,483,647. It uses a lot less memory than a normal array, and since our warnings will stack up to 5 at most, it would make sense to use this type of array instead to save memory. Just remember when using char arrays that you initialize it with "char" after the size of the array and that brackets ({}) be used in place of ([]). For more info about char arrays, see this thread.
"Killing" a Paused Player
Obviously we need a PauseKill function to complete the fake kill, so let's start working on that.

The very first thing we want to do in this function is check if the player is alive. Suppose we have an "IsPlayerAlive" variable that's set to false in OnPlayerDeath and true in OnPlayerSpawn. What if the player, upon dying legitimately, pauses while he is dead and the variable is set to false, and what if some other player shoots the corpse of that paused player? Then the PauseKill function called in OnPlayerGiveDamage will be called repeatedly! The player giving damage will end up getting multiple unfair kills for each damage given to the dead player. We don't want that, so we'll have to make sure the player is alive before proceeding with the rest of the PauseKill function. PauseKill will later call OnPlayerDeath which will set IsPlayerAlive to false.

Код:
// Only values being used are 1 and 0, so why not use a char array?
new gIsPlayerAlive[MAX PLAYERS char];

OnPlayerConnect(playerid)
{
    gIsPlayerAlive{playerid} = 0; // false

    // ...

    return 1;
}

OnPlayerSpawn(playerid)
{
    gIsPlayerAlive{playerid} = 1; // true

    // ...

    return 1;
}

OnPlayerDeath(playerid)
{
    gIsPlayerAlive{playerid} = 0;

    // ...

    return 1;
}

stock PauseKill(playerid, killerid, weaponid)
{
    if (gIsPlayerAlive{playerid} == 0) return 0;
    
    // ...

    return 1;
}
An optional step you could take is finding out what kinds of updates can still be sent while Player A is pausing. A few exceptions I have found were the functions SetPlayerVirtualWorld and SetPlayerColor. Turns out, these will be extremely useful for faking a kill. If you have a player color set when he dies, for example gray, you'll know that the pause kill was put into effect when the player's color turns gray. When a player's virtual world is set to a world different than those of other players, the other players won't see that player, which can be made to look like the player has died. The player also won't be able to see the other players, but that won't matter since we can switch the player back to the previous VW upon spawning after his fake death.

The complete PauseKill function.

Код:
OnPlayerSpawn(playerid)
{
    gIsPlayerAlive{playerid} = 1;
    gPauseKillWarnings{playerid} = 0; // Reset the pause warnings
    if (GetPlayerVirtualWorld(playerid) != 0) SetPlayerVirtualWorld(playerid, 0); // Default virtual world

    // ...
}
stock PauseKill(playerid, killerid, weaponid)
{
    if (gIsPlayerAlive{playerid} == 0) return 0;

    OnPlayerDeath(playerid, killerid, weaponid);

    // Setting the virtual world will work immediately, even while the player is paused
    // Set this number to a virtual world no one else is in
    SetPlayerVirtualWorld(playerid, 1);
	
    SpawnPlayer(playerid); // Will call OnPlayerSpawn
    return 1;
}
We shouldn't be killing the player as soon as the player unpauses since we want the death to take place immediately. What this stock will do is call the callback OnPlayerDeath and all the code you have in that callback will be in effect, while the player is paused. SpawnPlayer(playerid) will respawn the player as if the player had died. What's great about SpawnPlayer(playerid) is that it won't apply until the paused player unpauses, meaning if a player has been killed and remained paused for a while, other players won't see the player until he unpauses and his virtual world is set back to 0.

This PauseKill function can also serve other purposes, like in an admin /killplayer command that checks whether a player is paused.
Some other approaches to an anti-pause kill system that I'll leave to you
  • Set the warnings to the player's heath and armor amount for a total of 200 on GivePlayerDamage and subtract with the damage amount. Then call PauseKill when gPauseKillWarnings <= 0.
  • With the 5-warning limit, have a timer reset the warnings to zero if the player doesn't pause after a very long time

By this point you should have a fully functional anti-pause kill system. Feel free to point out any errors or suggest improvements.

I hope you learned a lot from this tutorial. Thanks for reading.
Reply
#2

Very good explanation, a little annoyed by the larger letters but I learned a lot very good
Reply
#3

the approach is ok, the observations however is what's interesting
Reply
#4

I've been doing this for quite some time it's very useful good tutorial.
Reply
#5

Thank you I was thinking of this and now here it is !
Reply
#6

Thanks for the positive feedback!
Reply
#7

Quote:

We shouldn't be killing the player as soon as the player unpauses since we want the death to take place immediately. What this stock will do is call the callback OnPlayerDeath and all the code you have in that callback will be in effect,

Good point.
I should admit i'am amazed ,good job.
Reply


Forum Jump:


Users browsing this thread: 3 Guest(s)