Unfortunately that's not how MTA's damage system works.
The official Wiki explicitly states:
So onPlayerDamage server-side is not authoritative at all. If a cheater cancels onClientPlayerDamage on their client, the server never receives onPlayerDamage because the damage was prevented client-side before synchronization even happens. The player becomes invincible and your server sees absolutely nothing.
The phrase "never trust the client" is correct, but in this case MTA's architecture forces the client to be trusted for damage events. That's the core problem here.
A Real Approach: Server-Side Health Validation
Since you can't rely on damage events arriving, you need to validate expected damage against actual health changes using data you CAN trust server-side:
local playerHealthCache = {}
local HEALTH_DISCREPANCY_THRESHOLD = 50
addEventHandler("onPlayerSpawn", root, function()
playerHealthCache[source] = getElementHealth(source)
end)
addEventHandler("onPlayerWeaponFire", root, function(weapon, _, _, _, _, _, target)
if not target or not isElement(target) or getElementType(target) ~= "player" then return end
local expectedDamage = getWeaponDamage(weapon)
local currentHealth = playerHealthCache[target] or getElementHealth(target)
local expectedHealth = math.max(0, currentHealth - expectedDamage)
setTimer(function()
if not isElement(target) then return end
local actualHealth = getElementHealth(target)
local discrepancy = actualHealth - expectedHealth
if discrepancy > HEALTH_DISCREPANCY_THRESHOLD then
flagSuspiciousPlayer(target, "GODMODE_SUSPECTED", discrepancy)
end
playerHealthCache[target] = actualHealth
end, 150, 1)
end)
function getWeaponDamage(weaponID)
local damageTable = {
[22] = 8.25, [23] = 13.2, [24] = 46.2,
[25] = 49.5, [26] = 49.5, [27] = 39.6,
[28] = 6.6, [29] = 8.25, [30] = 9.9,
[31] = 9.9, [32] = 46.2, [33] = 75,
[34] = 75, [38] = 46.2
}
return damageTable[weaponID] or 25
end
function flagSuspiciousPlayer(player, reason, data)
local serial = getPlayerSerial(player)
outputDebugString(("[AC] %s flagged: %s (data: %s)"):format(getPlayerName(player), reason, tostring(data)))
end
addEventHandler("onPlayerQuit", root, function()
playerHealthCache[source] = nil
end)
The idea is simple: when someone fires a weapon at a player, you know the expected damage. Then you check if their health actually dropped by that amount. If they keep taking hits but health never goes down, something's wrong.
Also worth noting that wasEventCancelled() on client won't help for anti-cheat since the cheater controls that environment entirely. They can just patch out the detection or spoof the result.