#define FILTERSCRIPT #include <a_samp> new Float:previousHealthValue[MAX_PLAYERS], hits[MAX_PLAYERS], noDamageCount[MAX_PLAYERS], //count how many times the player didn't lose health after being hit bool:delayer[MAX_PLAYERS], // delayer is neccessary because damage is not updated quickly enough and gives false detections bool:afterSpawnCover[MAX_PLAYERS], //to avoid false detections straight after spawn which happened to me while testing it (it's set to 3 seconds right now) bool:isDead[MAX_PLAYERS]; // hp check to see if the player is dead didn't work(the value of hp stayed the same as before last shot) //////////////////////////////// //SETTINGS #define DELAY_TIME 500 // 500ms delay works well on my syrver with 2 players online, I'm not sure what's it's going to be like on a server with over 100 players online #define AFTER_SPAWN_COVER_TIME 3000 #define NO_DAMAGE_COUNT_LIMIT 0 // number of times the script detected someone taking no damage after shot needed to: send info to admins/kick/ban //SETTINGS //////////////////////////////// public OnFilterScriptInit() { print("\n--------------------------------------"); print("Health-hack detector by monday"); print("--------------------------------------\n"); return 1; } public OnPlayerConnect(playerid) { previousHealthValue[playerid] = 0.0; hits[playerid] = 0; noDamageCount[playerid] = 0; delayer[playerid] = false; return 1; } public OnPlayerDeath(playerid, killerid, reason) { isDead[playerid] = true; return 1; } public OnPlayerSpawn(playerid) { previousHealthValue[playerid] = 0.0; hits[playerid] = 0; delayer[playerid] = false; afterSpawnCover[playerid] = true; SetTimerEx("RemoveSpawnCover", AFTER_SPAWN_COVER_TIME, false, "i", playerid); isDead[playerid] = false; return 1; } forward SwitchDelay(playerid); public SwitchDelay(playerid) { delayer[playerid] = true; return 1; } forward RemoveSpawnCover(playerid); public RemoveSpawnCover(playerid) { afterSpawnCover[playerid] = false; return 1; } public OnPlayerWeaponShot(playerid, weaponid, hittype, hitid, Float:fX, Float:fY, Float:fZ) { if(hittype == BULLET_HIT_TYPE_PLAYER) { new Float:h, Float:a, Float:newHealthValue, message[128], cheaterName[MAX_PLAYER_NAME]; if(hits[hitid] == 0) { GetPlayerHealth(hitid, h); GetPlayerArmour(hitid, a); previousHealthValue[hitid] = h + a; if(isDead[hitid] == true || afterSpawnCover[hitid] == true) // + here must be a check for AFK to avoid false detections { hits[hitid] = 0; } else { SetTimerEx("SwitchDelay", DELAY_TIME, false, "i", hitid); } } else if(delayer[hitid] == true) { delayer[hitid] = false; GetPlayerHealth(hitid, h); GetPlayerArmour(hitid, a); newHealthValue = h + a; if(newHealthValue == previousHealthValue[hitid] && isDead[hitid] != true && afterSpawnCover[hitid] != true) { noDamageCount[hitid]++; if(noDamageCount[hitid] > NO_DAMAGE_COUNT_LIMIT) { //send message to admins, kick, ban GetPlayerName(hitid, cheaterName, sizeof(cheaterName)); format(message,sizeof(message),"%s didn't lose health %d times after being shot since his last login.",cheaterName, noDamageCount[hitid]); SendClientMessageToAll(0xED6755AA,message); } } else { previousHealthValue[hitid] = newHealthValue; //update health value if it decreased like it should } SetTimerEx("SwitchDelay", DELAY_TIME, false, "i", hitid); // new delay before next check } hits[hitid]++; } return 1; }
#define FILTERSCRIPT #include <a_samp> new hitsInaRow[MAX_PLAYERS]; new Float:lastMiddle_X[MAX_PLAYERS]; new Float:lastMiddle_Y[MAX_PLAYERS]; new Float:lastMiddle_Z[MAX_PLAYERS]; new Float:lastHit_X[MAX_PLAYERS]; new Float:lastHit_Y[MAX_PLAYERS]; new Float:lastHit_Z[MAX_PLAYERS]; new weaponUsed[MAX_PLAYERS]; new Float:averageSpread[MAX_PLAYERS]; public OnFilterScriptInit() { print("\n--------------------------------------"); print("No-spread detector by monday"); print("--------------------------------------\n"); return 1; } stock Float:GetPositiveNumber(Float:number) { if(0.0 > number) { number *= -1.0; } return number; } stock Float:GetDistanceBetweenPoints(Float:x1, Float:y1, Float:z1, Float:x2, Float:y2, Float:z2) { return VectorSize(x1-x2, y1-y2, z1-z2); } public OnPlayerConnect(playerid) { hitsInaRow[playerid]=0; lastMiddle_X[playerid]=0.0; lastMiddle_Y[playerid]=0.0; lastMiddle_Z[playerid]=0.0; lastHit_X[playerid]=0.0; lastHit_Y[playerid]=0.0; lastHit_Z[playerid]=0.0; averageSpread[playerid]=0.0; weaponUsed[playerid] = 0; return 1; } forward SpreadCheckReset(playerid); public SpreadCheckReset(playerid) { hitsInaRow[playerid]=0; lastMiddle_X[playerid]=0.0; lastMiddle_Y[playerid]=0.0; lastMiddle_Z[playerid]=0.0; lastHit_X[playerid]=0.0; lastHit_Y[playerid]=0.0; lastHit_Z[playerid]=0.0; averageSpread[playerid]=0.0; weaponUsed[playerid] = 0; return 1; } forward OnPlayerWeaponShot(playerid, weaponid, hittype, hitid, Float:fX, Float:fY, Float:fZ); public OnPlayerWeaponShot(playerid, weaponid, hittype, hitid, Float:fX, Float:fY, Float:fZ) { new Float:fOriginX, Float:fOriginY, Float:fOriginZ, Float:fVX, Float:fVY, Float:fVZ, Float:fHitPosX, Float:fHitPosY, Float:fHitPosZ, Float:middle_x, Float:middle_y, Float:middle_z, Float:compareMiddle_x, Float:compareMiddle_y, Float:compareMiddle_z, Float:compareHit_x, Float:compareHit_y, Float:compareHit_z, Float:compareBoth_x, Float:compareBoth_y, Float:compareBoth_z, Float:distance, Float:spreadValue, Float:spreadLimit, cheaterName[MAX_PLAYER_NAME], hitsNeeded, weaponTime, currentWeapon, finalMessage[128], gunName[32], spread[128]; currentWeapon = GetPlayerWeapon(playerid); GetPlayerLastShotVectors(playerid, fOriginX, fOriginY, fOriginZ, fHitPosX, fHitPosY, fHitPosZ); distance = GetDistanceBetweenPoints(fOriginX, fOriginY, fOriginZ, fHitPosX, fHitPosY, fHitPosZ); GetPlayerCameraFrontVector(playerid, fVX, fVY, fVZ); middle_x = fOriginX + floatmul(fVX, distance); middle_y = fOriginY + floatmul(fVY, distance); middle_z = fOriginZ + floatmul(fVZ, distance); // middle_x, middle_y, middle_z = middle of the screen // fHitPosX, fHitPosY, fHitPosZ = shooting point //compare with the last shot compareMiddle_x = GetPositiveNumber(middle_x - lastMiddle_X[playerid]); compareMiddle_y = GetPositiveNumber(middle_y - lastMiddle_Y[playerid]); compareMiddle_z = GetPositiveNumber(middle_z - lastMiddle_Z[playerid]); compareHit_x = GetPositiveNumber(fHitPosX - lastHit_X[playerid]); compareHit_y = GetPositiveNumber(fHitPosY - lastHit_Y[playerid]); compareHit_z = GetPositiveNumber(fHitPosZ - lastHit_Z[playerid]); compareBoth_x = GetPositiveNumber(compareMiddle_x - compareHit_x); compareBoth_y = GetPositiveNumber(compareMiddle_y - compareHit_y); compareBoth_z = GetPositiveNumber(compareMiddle_z - compareHit_z); spreadValue = GetPositiveNumber(compareBoth_x + compareBoth_y + compareBoth_z); //replace last shot data with new one lastMiddle_X[playerid] = middle_x; lastMiddle_Y[playerid] = middle_y; lastMiddle_Z[playerid] = middle_z; lastHit_X[playerid] = fHitPosX; lastHit_Y[playerid] = fHitPosY; lastHit_Z[playerid] = fHitPosZ; if (weaponUsed[playerid] != currentWeapon) { weaponUsed[playerid] = currentWeapon; SpreadCheckReset(playerid); return 1; } if (currentWeapon == 31 || currentWeapon == 30 ||currentWeapon == 28 || currentWeapon == 32 ||currentWeapon == 29) //m4, ak47, tec, uzi, mp5 - all of them are set to m4's limit which is the most accurate weapon. Mp5 has a bit faster shooting rate than other weapons included but its spread is also higher so it's not an issue, I tested it and it's not even close to get false detection { hitsNeeded = 3; // weaponTime = 500; //time after which the stored hits will be reset(to make sure that there is no breaks between single shots) weaponUsed[playerid] = currentWeapon; } else { if (currentWeapon == 24)//deagle { hitsNeeded = 2; weaponTime = 1600; weaponUsed[playerid] = currentWeapon; } else { return 1; } } if( GetPlayerSpecialAction(playerid) == SPECIAL_ACTION_DUCK ) { spreadLimit = 0.00075 * distance; //decreased limit if the player is crouching } else { spreadLimit = 0.00140 * distance; } format(spread, sizeof(spread), "Spread: %f Limit: %f",spreadValue, spreadLimit); //SendClientMessage(playerid, -1, spread); if(spreadValue < spreadLimit && 200.0 > distance > 3.0) { hitsInaRow[playerid]++; averageSpread[playerid] = averageSpread[playerid] + spreadValue; } if (hitsInaRow[playerid] == 1) { SetTimerEx("SpreadCheckReset", weaponTime, 0, "i", playerid); } if (hitsInaRow[playerid] == hitsNeeded || hitsInaRow[playerid] > hitsNeeded) { //send message to admins, kick, ban GetWeaponName(currentWeapon, gunName, sizeof(gunName)); GetPlayerName(playerid, cheaterName, MAX_PLAYER_NAME); averageSpread[playerid] /= float(hitsInaRow[playerid]); format(finalMessage, sizeof(finalMessage), "%s is shooting under spread limit using %s. Average spread=%f Limit=%f", cheaterName, gunName, averageSpread[playerid], spreadLimit); SendClientMessageToAll(0xED6755AA,finalMessage); SpreadCheckReset(playerid); } return 1; }
1) Lag / Desync is a big factor at SA-MP, you will face false positives. |
2) There are cheats that can set back the health to its old value showing as they got hit but as soon as they are hit, it is restored (as the "shot" package is sent to the server) |
4) This will clearly give false positives for idle players. If I'm right, onplayerweaponshot is called when a player is AFK aswell, however their health will decrease when they are back from idling. |
5) If not idle, the players who are not sending enough packets to the server (for example the same second they died) if the script checks a bit late, which is likely to happen because you have a delayer over there. The player is shot, the delayer fires off, the same second the player dies, then your delayer eventually checks what's the player's health after they died, usually when the player is dead the server can give false information about the player's health, that would send false positives aswell. |
3) OnPlayerWeaponShot callback can be spoofed and can be avoided to be called. 6) Most of these will be caused from desync, it will declare desynced players as health hacking. Don't forget the SA-MP server is not multi-threaded so is the client, it can not process all the data at once. 7) As above, it will really change the results you give on a full server, because there's the server's networking systems and packetloss, delays on the client's screen, delays from the server itself, server lag, code constantly running at the background, if the server hangs up even for 200ms the results would be noticeable on your detector and will cause for it to give false positives, not one or two, but many because the server players overall will feel this. 8 ) Sorry but some cute test you do on localhost will not prove anything, your ping on localhost is 1 and you never have packetloss, some bot you spawn in the game to shoot at will clearly be on the same network as you are so there would be absolutely no false positives ofcourse because you are already connected to the server pretty fine. |
floatabs
GetPositiveNumber
new bool:isDead[MAX_PLAYERS]; isDead[playerid] = false;
new bool:isDead[MAX_PLAYERS char ]; isDead{playerid} = false;