Documentation 9.6.24
Events and triggers
The events Tosun AntiCheat emits for your scripts to listen to, the events you can trigger to integrate, and the config lists that map your own events to exemptions, key checks, rate limits and traps. Internal anticheat events are protected and must not be called.
Overview#
FiveM has local events and net events. A local event stays on one side of the game: TriggerEvent fires it and AddEventHandler receives it. A net event crosses the network: the server sends it with TriggerClientEvent, a client sends it with TriggerServerEvent, and the receiving side must register it with RegisterNetEvent. Tosun AntiCheat uses both kinds, on the server and on the client.
This page lists the events the anticheat emits for you to listen to, the events you may trigger to integrate your scripts, and the config lists (ts.DetectionExempt, ts.ProtectedEvents, ts.EventLimiter, ts.triggerList) that turn your own event names into exemptions, key checks, rate limits or traps.
The anticheat also registers many internal events for its own join handshake, key exchange, Shield, admin menu and web panel actions. They are protected by nonces, keys, signatures, rate limits or staff checks, and some of them are traps. They are not part of the integration surface: do not trigger them, do not register handlers on them and do not reuse their names. Use the exports instead.
| Kind | Fire with | Receive with | Who can fire it |
|---|---|---|---|
| Server local event | TriggerEvent | AddEventHandler | Any server resource. |
| Client local event | TriggerEvent | AddEventHandler | Any client script on that player's game, injected cheat code included. |
| Net event, client to server | TriggerServerEvent | RegisterNetEvent + AddEventHandler | Any connected player, with any arguments. |
| Net event, server to client | TriggerClientEvent | RegisterNetEvent + AddEventHandler | Any server resource. A client script can also fire the same name locally with TriggerEvent. |
Common integration tasks#
Pick the event or setting that matches what you want to do. Each one is described in detail further down this page.
| Goal | Use |
|---|---|
| React when a detection ends in a ban, kick or log line | ts_anticheat:playerBanned |
| React only when a ban was actually issued | ts_anticheat:banCommitted |
| React to an unban | ts_anticheat:playerUnbanned |
| Mirror the live log feed | ts_anticheat:newLog |
| Refresh your cache when the anticheat re-applies settings | tosun-ac:settingsReloaded / ts_anticheat:configUpdated |
| Apply a panel character rename on a custom framework | tosun-ac:renameCharacter |
| Turn on client checks after a custom spawn | playerSpawned |
| Refresh the panel after custom character selection | tosun-ac:panel:characterReady |
| Make the client re-check staff status | ts_anticheat:adminStateChanged |
| Exempt a player on the client from your server script | tosun-ac:bridge:clientExempt |
| Exempt players automatically when your menu or action event fires | ts.DetectionExempt |
| Require a per-player key on your own client-to-server event | ts.ProtectedEvents + <name>:safe |
Server events the anticheat emits#
These are server local events. Listen to them with AddEventHandler in any server script. Do not fire them yourself; see the security sections below.
| Event | When it fires | Payload |
|---|---|---|
ts_anticheat:playerBanned | Every detection outcome: BAN, KICK and LOG. It fires before the kick, the ban insert and the logs. It does not fire for players skipped as staff or whitelisted. | data table: playerId (string), playerName, reason, banID, side ('server'), punishment ('BAN', 'KICK' or 'LOG') |
ts_anticheat:banCommitted | BAN outcomes only (detections and the ban and punish exports), right after the ban row insert is queued and before the panel webhook and the drop. | playerId (number), row table: banID, playerName, reason, license, steam, discord, ip, HWID, HWID2 to HWID5, config (detection config key) |
ts_anticheat:playerUnbanned | After the unban export, the /ts unban command or the in-game menu removed a ban. Web panel unbans do not fire it. | banID (the value passed to the unban export, unchanged) |
ts_anticheat:newLog | Every live log line: detections, client log lines, admin menu actions and some internal anticheat lines. | payload table: src (category label, not a player id), event (message text), status (default 'Info'), playerName, time (os.time()) |
tosun-ac:settingsReloaded | After a settings sync re-applied settings. This happens on startup and on almost every sync cycle, not only when something changed. | none |
ts_anticheat:shield:screenshot_saved | A requested Shield screenshot arrived and passed the checks. | src (number), b64 (base64 image, 64 to 2,500,000 characters) |
ts_anticheat:shield:ban_video_saved | The gameplay video of a kicked or banned player was uploaded within 120 s. | src (number), videoUrl, reason, config (reason and config come from the client) |
<name>:safe | A call of an event listed in ts.ProtectedEvents passed the key and payload checks. | src (number, online player id), then the original arguments after the key |
tosun-ac:renameCharacter | A web panel character rename on a framework other than qb, qbcore, qbox, qbx or esx. | citizenid (string), firstName (first word), lastName (rest of the name, may be '') |
Client events the anticheat emits#
These events reach client scripts. ts_anticheat:configUpdated is a client local event; listen with AddEventHandler. ts_anticheat:receiveEventKey is a net event from the server; register it with RegisterNetEvent in your client script.
| Event | Type | When it fires | Payload |
|---|---|---|---|
ts_anticheat:configUpdated | Client local event | After the client applied settings from the server and re-scanned its DetectionExempt hooks. Expect it about once per sync interval. | none |
ts_anticheat:receiveEventKey | Net event, server to client | In reply to ts_anticheat:requestEventKey (yours or the anticheat's own request), and whenever the server issues the player a new key. It arrives after the join handshake. | key (string, the player's SafeEvents key) |
Listener example#
A server script that reacts to kicks and bans, and a client script that drops a cache when the anticheat re-applies settings. Each server listener first checks that the anticheat fired the event.
-- server.lua (your resource)
local AC = 'tosun-ac' -- the anticheat resource (folder) name
AddEventHandler('ts_anticheat:playerBanned', function(d)
if GetInvokingResource() ~= AC then return end -- ignore forged calls
if d.punishment == 'LOG' then return end
print(('[AC] %s %s: %s'):format(d.punishment, d.playerName, d.reason))
end)
AddEventHandler('ts_anticheat:banCommitted', function(playerId, row)
if GetInvokingResource() ~= AC then return end
print('AC ban', row.banID, row.license, row.reason)
end)
-- client.lua (your resource)
local myCache
AddEventHandler('ts_anticheat:configUpdated', function()
myCache = nil
end)playerBanned in detail#
The name is historical. ts_anticheat:playerBanned fires for every detection outcome, not only bans. Filter on data.punishment.
data.playerId is a string. Convert it with tonumber before you use it as a server id.
The event fires before the action. The chat broadcast, the logs, DropPlayer and the ban insert run after your handler.
It does not fire when the target is offline, when the player is skipped as staff (staff are skipped unless ts.AdminBypassDetections is false or ts.Debug is on), or when the player is on the temporary whitelist or the panel whitelist.
A banID is generated for every outcome but stored only for BAN. A KICK of a staff member is cancelled after the event has already fired.
While the ban screen (editable/server/sv_banscreen.lua) is shown to a player, most further detections for that player are swallowed and no event fires for them.
banCommitted in detail#
ts_anticheat:banCommitted is the best hook for 'a ban was actually issued'. Use playerBanned for kicks and log lines.
It fires for every BAN outcome of the anticheat's punish pipeline: detections and the ban and punish exports. Offline bans (the offlineBan and offlineBanByLicense exports) and bans made in the web panel do not fire it.
The ban row insert is queued with MySQL.Async.execute and not awaited, so the row may not be in the database yet when your handler runs.
Identifiers in the row have their prefix removed (license:, license2:, steam:, discord:, ip:). A missing identifier or token is the English text 'Not found'; a missing ip is 'Hidden'. HWID to HWID5 are raw player tokens.
newLog and settingsReloaded in detail#
Both events fire often. Keep their handlers cheap.
ts_anticheat:newLog: the field src is a category label, not a player id. Detections use 'DETECTION' with status BAN, KICK or LOG. Client log lines use a fixed tag and status: 'AntiCheat' with 'Tespit' (hard-coded Turkish for 'Detection'), 'AC-Grace' with 'Info', and 'AC-FakeTrigger' with 'Warning'. Admin menu entries normally use the admin's name with status BAN, KICK, INFO or SUCCESS. A few internal lines use other categories.
tosun-ac:settingsReloaded: the anticheat does not compare old and new values, so the event fires after the startup sync and on most sync cycles, every ts.ServerPerf.configSyncIntervalSec seconds (120 in the shipped config, 60 if the key is missing, minimum 20). Read it as 'settings were re-applied', not 'settings changed'.
After each such sync the server also sends the settings to all clients, and each client then fires ts_anticheat:configUpdated.
If another server resource fires tosun-ac:settingsReloaded, the anticheat runs a full staff permission refresh for every online player.
Shield screenshot and video events#
Both events need Shield (ts.shield.enabled not false; it is on in the shipped config).
ts_anticheat:shield:screenshot_saved: request a screenshot with the server export RequestShieldScreenshot(src). The image must arrive within 60 s of the request, and the anticheat accepts at most one per 20 s per player. The web panel can request screenshots too, and those also fire the event.
ts_anticheat:shield:ban_video_saved: needs ts.shield.banGameplayVideo not false (on in the shipped config). The upload window opens for 120 s after a KICK or BAN, and the video URL host must match the panel.
reason and config in ban_video_saved come from the client and are not type-checked or truncated. Treat them as untrusted text.
local AC = 'tosun-ac'
-- server: ask the player's game for a screenshot
exports[AC]:RequestShieldScreenshot(src)
AddEventHandler('ts_anticheat:shield:screenshot_saved', function(src, b64)
if GetInvokingResource() ~= AC then return end
-- b64 is a base64 image string
end)Character rename on a custom framework#
The web panel can rename a character. For qb, qbcore, qbox and qbx the anticheat updates players.charinfo; for esx it updates users.firstname and users.lastname. For any other ts.Framework.framework value it fires tosun-ac:renameCharacter so you can apply the rename yourself.
The anticheat reports 'event_emitted_unknown_fw' to the panel whether or not a listener exists.
The new name has control characters removed, is capped at 64 characters and must be at least 3 characters long. firstName is the first word; lastName is the rest and may be empty.
The example uses the oxmysql MySQL.update call and a made-up characters table. Adapt the query to your schema.
AddEventHandler('tosun-ac:renameCharacter', function(cid, first, last)
if GetInvokingResource() ~= 'tosun-ac' then return end
MySQL.update('UPDATE characters SET first = ?, last = ? WHERE cid = ?', { first, last, cid })
end)Events you can trigger#
These are the anticheat events meant to be fired by your scripts. The framework death and revive events and your ts.DetectionExempt events are covered in their own sections.
| Event | Fire from | Effect | Limits |
|---|---|---|---|
tosun-ac:panel:characterReady | Client, with TriggerServerEvent. | Refreshes the player's character data in the panel's online player list. | One call per 20 s per player. |
ts_anticheat:locale:setSelf | Client, with TriggerServerEvent and a language code. | Sets the language of anticheat server messages for that player only. Prints a line to the server console. | No rate limit. The code must be 2 or 3 lowercase letters with an existing locales/<lang>.json file. |
ts_anticheat:requestEventKey | Client, with TriggerServerEvent. | Sends the player's SafeEvents key back through ts_anticheat:receiveEventKey. | One call per 3 s per player. The key goes only to the requesting player. |
playerSpawned | Client, with TriggerEvent (local). | Turns on most client checks and starts the spawn grace window. | Every other resource on that client also receives it. |
ts_anticheat:adminStateChanged | Server, with TriggerClientEvent to one player. | The client asks the server again for its real staff status. | The server answers at most once per second per player. |
tosun-ac:bridge:clientExempt | Server, with TriggerClientEvent; needs the bridge file in your resource. | Client-side exemption for that player. | 500 to 300000 ms. |
Custom spawn: playerSpawned#
Most client checks stay off until the anticheat sees the local playerSpawned event: regen, godmode, teleport, invisibility, freecam, weapons, OCR, native, coords and vehicle godmode. spawnmanager normally fires it.
If your spawn or multicharacter system does not use spawnmanager, fire playerSpawned locally once the player is in the world, or call the client export SetSpawned(true).
playerSpawned also starts the spawn grace window of ts.spawnGraceSecondsMs. The shipped value is 500, which the code raises to its 5000 ms minimum; without the key the code uses 25000. A detection inside the window is not cancelled. It only adds an 'AC-Grace' log line.
playerSpawned also clears the anticheat's death state and marks a client teleport.
The shipped ts.DetectionExempt.clientEvents list contains playerSpawned, so each playerSpawned also starts a client exemption of defaultDurationMs (60 s).
SetSpawned(false) takes effect at most once per 5 minutes, and the checks turn back on after about 30 s of visible movement.
-- client, after your own spawn logic
TriggerEvent('playerSpawned')
-- or, without the side effects on other resources
exports['tosun-ac']:SetSpawned(true)Custom multicharacter: characterReady#
On the server the anticheat listens to QBCore:Server:OnPlayerLoaded, QBCore:Server:PlayerLoaded, esx:playerLoaded and qbx_core:server:playerLoaded. Each one pushes the loaded character to the panel's online player list right away. A custom multicharacter script that fires none of them should fire tosun-ac:panel:characterReady from the client after character selection.
Each call writes the player's row to the database and, when the panel URL and key are set, sends an HTTPS request to the panel. The server therefore accepts one call per 20 s per player and ignores the rest.
-- client, after character selection finished
TriggerServerEvent('tosun-ac:panel:characterReady')Player language: locale:setSelf#
A client can choose the language of the anticheat's server messages for itself. On success the server prints a line to the server console.
It changes only the server messages sent to that player. It does not change the client-side locale.
The anticheat's own client does not send this event. Server-side alternatives are the setPlayerLocale export and the ts_setlang command.
-- client
TriggerServerEvent('ts_anticheat:locale:setSelf', 'en')Staff changes: adminStateChanged#
After your admin script grants or removes staff rights, tell the player's client to re-check its staff status. The event cannot change the status by itself: the client sends a fresh nonce to the server, and only the server's reply decides.
Nothing in the anticheat sends this event. The client also re-checks on its own every 20 s, so the call only removes that delay.
A cheat that fires it locally gains nothing; the server answers the check at most once per second per player.
-- server, after changing staff rights for src
TriggerClientEvent('ts_anticheat:adminStateChanged', src)Bridge event: tosun-ac:bridge:clientExempt#
The client bridge file registers this net event inside your own resource. Your server script sends it to a player, and the bridge calls the anticheat's client SetExempt export for that player.
durationMs is a number in ms or a preset name in any case: SHORT, MEDIUM, LONG or XL (30, 60, 120 or 180 s). Values are clamped to 500 to 300000 ms. Without a value the convar default applies (60000).
The exemption key is 'bridge:' followed by tag, or by your resource name when tag is nil.
This is a client-side exemption only. Server checks keep running.
The anticheat never sends this event. If several of your resources include the bridge file, one TriggerClientEvent runs once in each of them.
- Add client_script '@tosun-ac/bridge/tosun_ac_client.lua' to your resource's fxmanifest.lua.
- Optional, in server.cfg: if you renamed the anticheat folder, add setr tosun_ac_resource with the new name (default tosun-ac). To change the default duration, add setr tosun_ac_bridge_client_ms with a value in ms (default 60000).
- From your server script, send TriggerClientEvent('tosun-ac:bridge:clientExempt', src, durationMs, tag).
-- server: exempt the player for 2 minutes while a barber menu is open
TriggerClientEvent('tosun-ac:bridge:clientExempt', src, 'LONG', 'barber')Framework death and revive events#
The client listens to these events to know when a player is dead or downed. While that state is active, the regen-spike, godmode, teleport, invisibility, spectate and several other client checks are skipped. Native deaths are also caught through the gameEventTriggered damage event.
Since 9.5.9 the anticheat ends this state on its own once the player is clearly active again.
A custom ambulance script can fire one of these names locally to align the anticheat's state. These are shared framework names, so every other resource on that client that listens to them runs too (for example ESX death screens).
| Event | Type | Effect |
|---|---|---|
hospital:client:isDead | Net | Downed; starts the 25 s post-death window. |
hospital:client:SetDeathState | Net, boolean argument | true: downed and 25 s post-death window. false: revived and 10 s post-revive window. |
hospital:client:SetLaststand | Net, boolean argument | true: downed and 25 s post-death window (false is ignored). Also a 45 s auto-exemption. |
qb-medical:client:OnDeath | Net | Downed; starts the 25 s post-death window. |
esx:onPlayerDeath | Net | Downed; starts the 25 s post-death window. Also a 45 s auto-exemption. |
baseevents:onPlayerDied | Local (net while autoExempt is on) | Starts the 25 s post-death window. Also a 45 s auto-exemption. |
baseevents:onPlayerKilled | Local only | Starts the 25 s post-death window. |
hospital:client:Revive | Net | Revived; starts the 10 s post-revive window. |
qb-ambulancejob:client:RevivePlayer | Net | Revived; starts the 10 s post-revive window. |
qb-medical:client:OnRevive | Net | Revived; starts the 10 s post-revive window. |
esx_ambulancejob:revive | Net | Revived; starts the 10 s post-revive window. |
esx_basicneeds:onRevive | Net | Revived; starts the 10 s post-revive window. |
playerSpawned | Local | Clears the downed state and leaves about 17 s of the post-death window. |
-- custom ambulance (client), after your own revive logic
TriggerEvent('esx_ambulancejob:revive')Automatic exemption events (autoExempt)#
With ts.autoExempt.enabled (true in the shipped config) the client listens to common framework events and grants a short client exemption through the SetExempt export. Client detections during the exemption are not punished; they are reported as LOG lines marked '[exempt]'. Set ts.autoExempt.enabled = false to turn this off.
The header comment in client/anticheat_auto_exempt.lua says these events cannot be triggered remotely. That is wrong: they are registered with RegisterNetEvent, so the server can fire them too. The effect is still a client exemption only.
| Situation | Duration | Events |
|---|---|---|
| Appearance menus | 180 s | illenium-appearance:client:openMenu, appearance:client:openMenu, fivem-appearance:client:openMenu, ox_appearance:openMenu |
| Clothing menus | 180 s | qb-clothing:client:openMenu, qb-clothes:client:openMenu, rcore_clothing:openClothingShop, clothing:client:openMenu |
| ESX skin and barber menus | 180 s | esx_skin:openSaveableMenu, esx_skin:openRestrictedMenu, skinchanger:model:loaded, tgg_barber:open, barbershop:client:open |
| Character selection | 30 s | qb-multicharacter:client:chooseChar, qb-multicharacter:client:closeNUIdefault, esx_multicharacter:SetupCharacters, esx:restoreLoadout, ox:playerLoaded, qbx_core:client:playerLoggedOut |
| Death and laststand (QB, ESX) | 45 s | esx_ambulancejob:setDeathStatus, esx:onPlayerDeath, hospital:client:SetLaststand, hospital:client:OnPlayerLaststand, qb-ambulancejob:client:playerDead |
| Death (other scripts) | 45 s | wasabi_ambulance:clientDeath, ars_ambulancejob:client:dead, baseevents:onPlayerDied, baseevents:onPlayerWasted |
| Character loaded | 15 s | QBCore:Client:OnPlayerLoaded, qbx_core:client:playerLoaded, esx:playerLoaded, ox:playerLoaded |
| Anticheat start on the client | 60 s | onClientResourceStart |
Join gate: framework loaded events#
With ts.NetworkJoin.waitForFrameworkLoaded (true in the shipped config) and the qb, qbox or esx framework, the client waits for a framework loaded event before it finishes joining the anticheat. This delays the start of all client protection for that player. Other framework values skip the wait.
After minReadyDelayMs (15 s in the shipped config) and the end of the loading screen, the client waits for one of the loaded events, or for LocalPlayer.state.isLoggedIn (qb, qbox) or ESX.PlayerLoaded (esx), up to 180 s in total. It then waits postFrameworkSettleMs (5 s in the shipped config) and for collision before it reports ready.
| Framework | Loaded events | Reset events |
|---|---|---|
qb | QBCore:Client:OnPlayerLoaded | QBCore:Client:OnPlayerUnload |
qbox | QBCore:Client:OnPlayerLoaded, qbx_core:client:playerLoggedIn, qbx_multicharacter:client:chooseChar | QBCore:Client:OnPlayerUnload |
esx | esx:playerLoaded | esx:onPlayerLogout |
Protected events (SafeEvents)#
ts.ProtectedEvents makes the anticheat check a per-player key on your own client-to-server events. A call that passes is fired again on the server as the local event '<name>:safe' with the player id first. The shipped list contains only 'test:event'.
TriggerSafeServerEvent exists only inside the anticheat's own client and is not exported, so other resources must use the receiveEventKey pattern above. The anticheat's client requests the key every 5 s until it arrives (at most 12 tries).
The server answers at most one key request per 3 s per player, and the anticheat's own requests count too. If no key arrives, request it again after a few seconds. Keep your handler registered and always send the latest key you received.
- Add your event name to ts.ProtectedEvents in configs/anticheat_config.lua. The list is read once at start, so restart the anticheat after changes.
- In your client script, register ts_anticheat:receiveEventKey to store the key, then send ts_anticheat:requestEventKey.
- Send your event with the key as the first argument: TriggerServerEvent(name, key, ...).
- In your server script, handle '<name>:safe' with AddEventHandler. The first argument is the player's server id, followed by your arguments.
-- configs/anticheat_config.lua
ts.ProtectedEvents = { ['my-shop:buy'] = true }
-- client (your resource)
local key
RegisterNetEvent('ts_anticheat:receiveEventKey', function(k) key = k end)
TriggerServerEvent('ts_anticheat:requestEventKey')
-- later, once key is set:
TriggerServerEvent('my-shop:buy', key, 'bread')
-- server (your resource)
AddEventHandler('my-shop:buy:safe', function(src, item)
if GetInvokingResource() ~= 'tosun-ac' then return end
-- check money, stock and permissions here
end)What SafeEvents checks and what it does not#
SafeEvents validates transport only. It never punishes anyone, and it does not replace the checks in your own handler.
- Every client call first counts against ts.SafeEventRateLimit per ts.SafeEventRateWindow seconds (30 calls per 10 s by default), even without a key.
- If the player has no key yet, the call is dropped silently.
- A wrong key, the rate limit, more than ts.SafeEventMaxArgs arguments (64 by default) or a bad payload drops the call and writes one database log line per player, event and window.
- Calls from the server (source 0) are ignored and fire no ':safe' event.
- The raw net event still reaches any handler your own resource registered with RegisterNetEvent. Only the ':safe' mirror is gated.
- Never RegisterNetEvent the ':safe' name; that would let clients call it directly.
- SafeEvents is an extra layer. Your own handler must still check permissions, balances and ownership.
- Do not add framework events (esx, qb) to the list. Their callers send no key, so SafeEvents rejects those calls, writes log lines and never fires ':safe'. The framework's own handlers still run.
- The Turkish comment in configs/anticheat_config.lua says keyless calls cause a false ban. In this version they are only dropped.
ts.DetectionExempt: map your events to exemptions#
ts.DetectionExempt turns your own event names into temporary detection exemptions. Use it for menus and server actions that legitimately change a player's appearance, position or state: clothing, barbers, housing, garages, character selection and revives. The lists are read from configs/anticheat_config.lua; panel settings do not change them. ts.MenuDetectionExempt is an alias kept for old configs.
| Key | Type | Shipped value | Effect |
|---|---|---|---|
enabled | boolean | true | false turns off all DetectionExempt hooks. |
defaultDurationMs | number (ms) | 60000 | Exemption length for entries without their own durationMs. Server entries are clamped to 1000 to 120000 ms, client entries to 1000 to 300000 ms. |
budgetMsPer10Min | number (ms) | 240000 | Maximum soft exemption time one player can collect from serverEvents in 10 minutes. |
clientEvents | list of strings | 153 entries (142 unique names) | Client net events. When one fires, the player's client checks are exempt. |
localEvents | list of strings | 11 entries | Client local events (TriggerEvent on the client only). Same effect as clientEvents. |
serverEvents | list of strings or tables | 34 entries | Client-to-server net events. When a player fires one, that player gets a soft exemption. |
DetectionExempt.serverEvents#
The anticheat registers each listed name with RegisterNetEvent. When a player triggers it, that player gets a soft exemption: server movement checks and the server-side godmode, invisibility, noclip and camera checks are relaxed, and the player's client checks are exempt through a state bag. It is never immunity from punishment.
An entry is a string or a table: { event = 'name', durationMs = 60000, resource = 'owner-resource', always = true }. durationMs is clamped to 1000 to 120000 ms.
The hook is installed only while the owner resource is started or starting. The owner is the event prefix before the first colon; qb-clothes maps to qb-clothing and hospital maps to qb-ambulancejob. Set resource when the prefix differs from the resource name. always = true installs the hook even when the owner is not running.
Hooks are installed 2.5 s after the anticheat starts and again 1 s after any other resource starts.
Players can fire these events themselves, which is why each player has a budget (budgetMsPer10Min). When a player uses it up, the anticheat writes one console warning and one database log line, and ignores further requests from that player for the rest of the 10 minutes.
Run tosunac_integration in the server console to see how many server exemption events are hooked. It works from the server console only, and the output goes there.
DetectionExempt.clientEvents and localEvents#
clientEvents names are registered with RegisterNetEvent on the client. When one fires, from the server or from a local TriggerEvent, the client is exempt for defaultDurationMs.
clientEvents accepts strings only. Table entries, including ones with durationMs, are silently ignored on the client.
During the exemption, client detections are not punished. They are reported as LOG lines marked '[exempt]', at most once a minute per check, so abuse stays visible in the panel. Server checks are not affected.
localEvents works the same way but uses AddEventHandler only, so it reacts only to TriggerEvent on the client.
The client scans both lists 3.5 s after it loads and again on every settings sync.
An event of a script you do not run never fires, so extra names in the list do no harm. The shipped clientEvents list repeats 11 names; duplicates are harmless.
DetectionExempt example#
Add your names to the existing lists in configs/anticheat_config.lua, then restart the anticheat. Replacing the whole table removes the shipped entries. The example shows only new entries.
Prefer the exports when you control the code: the client export SetExempt(true, ms, reason), the server export SetExempt(src, ms, reason) from a trusted resource, and the server export MarkTeleport(src, ms) right before you teleport a player.
ts.DetectionExempt = {
enabled = true,
defaultDurationMs = 60000,
budgetMsPer10Min = 240000,
-- client net events: strings only
clientEvents = {
-- ...shipped entries...
'my-clothing:client:openMenu',
},
-- client local events (TriggerEvent on the client)
localEvents = {
-- ...shipped entries...
'my-menu:opened',
},
-- client-to-server events: soft exemption for the sender
serverEvents = {
-- ...shipped entries...
'my-clothing:server:saveOutfit',
{ event = 'my-housing:server:enter', durationMs = 60000 },
{ event = 'myhouse:server:enter', resource = 'my-housing' },
{ event = 'my-garage:server:takeOut', durationMs = 45000, always = true },
},
}ts.EventLimiter: rate limits for net events#
ts.EventLimiter maps event names to a maximum number of calls per player in 5 s. The anticheat registers each name with RegisterServerEvent at load and counts calls per player; all counters reset every 5 s. Your own handler for the event still runs.
When a count reaches its limit, the anticheat attempts a detection with config ts.EventLimiter (server_event_spam, KICK by default).
The shipped list has 25 entries: test:event, esx:getSharedObject, bank and QBCore money events, revive, vehicle spawn, inventory, jail and handcuff events, and HCheat:TempDisableDetection.
Names and limits are read at load. The database table ac_event_limits replaces ts.EventLimiter on sync but does not change the registered handlers, so restart the anticheat after editing either one.
ts.EventLimiter = {
-- ...shipped entries...
['my-shop:buy'] = 10,
}Trap events: never use these names#
Some events exist only to catch cheats. Follow these rules so your own scripts never trigger one.
- The anticheat registers trap events that only cheats fire. Never fire them and never reuse their names.
- To see the trap names on your own server, run /ts evtlist (the list prints to the server console). To stop treating an event as a trap, use /ts evtwhitelist <eventName>. See the Commands topic.
- Give your own events your resource name as a prefix, for example myresource:doThing, so they never collide with a trap.
Sound event rate guard#
The anticheat registers these sound events as net events in its own resource and counts them per player. Above ts.premiumGuard.soundPerSec (6 per second by default) it applies soundAction ('log' by default). It cancels the event only when soundCancel = true (false by default). Your sound resource's handlers still run when that resource registered the event itself.
The guard runs only while ts.premiumGuard.enabled and ts.premiumGuard.soundGuard are true (both true in the shipped config). Keep soundCancel false until you have checked that your sound scripts stay under the limit.
| Event | Resource |
|---|---|
InteractSound_SV:PlayOnAll | InteractSound |
InteractSound_SV:PlayWithinDistance | InteractSound |
InteractSound_SV:PlayOnOne | InteractSound |
xsound:stateSound | xsound |
xsound:server:play | xsound |
xsound:server:playUrl | xsound |
xsound:server:playUrlPos | xsound |
Events the anticheat sends to other resources#
After staff, panel or detection actions the anticheat fires events that belong to other resources. If you run one of these resources, expect these calls.
Weather and time use only the first started sync resource, in this order: qb-weather, Renewed-Weathersync (export only), cd_easytime, wd_weather, weathersync, vSync, qb-weathersync. If none is started, the anticheat sets weather and time directly on every client.
hospital:client:Revive and esx_ambulancejob:revive are also anticheat listeners (see Framework death and revive events), so these revives also open the 10 s post-revive window.
| Event | Side | Sent when |
|---|---|---|
qb-weather:server:RequestStateChange | Server local | Weather or time change when qb-weather has no setWeather or setTime export. For time the value is a { hour, minute } table. |
cd_easytime:setWeather, cd_easytime:setTime | Server local | Weather or time change with cd_easytime. |
<res>:setWeather, <res>:setTime | Server local | Weather or time change with wd_weather, weathersync, vSync or qb-weathersync when that resource has no matching export. |
wasabi_ambulance:revive | Client local | Staff or panel revive when wasabi_ambulance is started. |
hospital:client:Revive | Client local | Staff or panel revive with qb-ambulancejob or Starter-Hospital. |
esx_ambulancejob:revive | Client local | Staff or panel revive with esx_ambulancejob. |
<res>:revive, <res>:client:Revive, <res>:client:revive | Client local | Staff or panel revive otherwise: the first started of ambulance, qb-ambulancejob, cd_ambulance and ars_ambulancejob gets all three names. A native resurrect follows if the player is still dead. |
QBCore:Notify | Client local; server to client | Every heal the anticheat performs, even without qb-core (its qb-core check is always true). Also panel money, inventory and notify actions. |
esx:showNotification, QBCore:Player:SetPlayerData, inventory:client:ItemBox | Server to client | Panel money, inventory and notify actions, and weapon-as-item grants. |
chat:addMessage | Server to client; client local | Chat messages and panel broadcasts. With ts.chatMessages = true (default false), every detection outcome, LOG included, goes to all players in hard-coded Turkish (editable/server/sv_editme.lua). |
chat:addSuggestion | Client local | Adds the /unspectate suggestion when the admin menu file loads. |
qb-phone:client:CustomNotification, qs-smartphone:client:sendNotification, lb-phone:notification, yseries:notification, gks-phone:client:notification, roadphone:notify | Server to client | Staff alerts on kicks and bans, and the phoneNotify and phoneNotifyAdmins exports. Only the first started phone gets them (npwd uses an export; chat if none). Needs ts.AdminMenu.enable = true. |
Security: who may fire which event#
Only the anticheat may fire the events it emits. Other resources must not trigger them. The table lists who may fire each event.
| Event | Who may fire it |
|---|---|
ts_anticheat:playerBanned | Only the anticheat. |
ts_anticheat:banCommitted | Only the anticheat. |
ts_anticheat:newLog | Only the anticheat. |
ts_anticheat:shield:ban_video_saved | Only the anticheat. |
tosun-ac:settingsReloaded | Only the anticheat. |
ts_anticheat:configUpdated | Only the anticheat. |
ts_anticheat:adminStateChanged | Any server resource. |
tosun-ac:panel:characterReady | Any client. Limited to one call per 20 s per player. |
ts_anticheat:locale:setSelf | Any client. Affects only the caller. |
ts_anticheat:requestEventKey | Any client. The key goes only to the requesting player. |
tosun-ac:bridge:clientExempt | Your server scripts. |
Security: check GetInvokingResource#
Any server resource can fire a server local event. In your listeners, check that the anticheat fired it before you act. The anticheat's own ban screen listener uses a similar check.
Apply the check to playerBanned, banCommitted, playerUnbanned, newLog, settingsReloaded, the Shield events, renameCharacter and your '<name>:safe' handlers.
Never fire the anticheat's events from your own resources, even for testing. Several built-in listeners do not check the caller.
AddEventHandler('ts_anticheat:banCommitted', function(playerId, row)
if GetInvokingResource() ~= 'tosun-ac' then return end -- your folder name if renamed
-- safe to act on row here
end)Security: server-authoritative design#
Anything a client can trigger is treated as a hint, not as authority. Keep the same rule in your own handlers.
- Client exemptions (clientEvents, localEvents, autoExempt, the bridge event and the client SetExempt export) never change server enforcement. While they are active, client detections are logged as '[exempt]' instead of punished.
- serverEvents exemptions are soft: on the server they relax only movement and visibility checks (weapon, money, damage, event-abuse, injection and crash punishments still apply), and each player is capped by budgetMsPer10Min.
- Staff status is decided by the server's nonce-protected reply. Pushing a staff flag to a client cannot grant admin.
- The SafeEvents key only validates transport. Your ':safe' handler must check permissions, balances and ownership on the server.
- A cheat can fire framework death events locally. The downed flag clears after about 20 s of clearly active play.
- To exempt a player from the server side, use the exports: SetExempt(src, ms, reason) from a trusted resource, or MarkTeleport(src, ms) before a teleport.
Internal events#
Besides the events on this page, the anticheat registers internal events for its join handshake, key exchange, Shield, admin menu, web panel actions and a companion resource. They are protected by nonces, keys, signatures, rate limits or staff checks, and several of them are traps that punish the caller. They are not documented here on purpose.
- Do not trigger internal events, from the server or from a client.
- Do not register handlers on them and do not reuse their names.
- Do not fire any event whose name starts with ts_anticheat: or tosun-ac: unless this page lists it as one you can trigger.
- Use the exports for actions: for example punish, MarkTeleport, freezePlayer, setPlayerHealth, RequestShieldScreenshot and quarantine.
Known issues in 9.6.15#
These behaviors are in the shipped code. Plan around them.
- tosun-ac:settingsReloaded and ts_anticheat:configUpdated fire on almost every sync cycle, not only when settings change.
- ts_anticheat:playerBanned fires for KICK and LOG as well as BAN.
- The staff phone alert for live logs listens for the status 'TESPİT' or 'Detection' in ts_anticheat:newLog, but nothing emits either value, so that alert never fires.
- Every heal the anticheat performs fires QBCore:Notify on the client, even on servers without qb-core.
- ts.quarantine.autoQuarantine has no effect: its only input is an internal event that nothing emits. Use the quarantine export instead.
- Several comments in configs/anticheat_config.lua are in Turkish and some are outdated: ts.ProtectedEvents keyless calls are dropped, not banned, and ts.EventLimiter does not kick.
Avoid unnecessary analysis after a restart#
Each startup checks resource content SHA256 fingerprints. Resources whose files and scan policy are unchanged reuse authenticated server KVP results instead of repeating expensive signature and event analysis. Changed files, exceptions or scan rules invalidate the affected analysis. Fingerprint checks still read files; no fixed resmon figure is guaranteed.
- Keep the KVP cache during a normal update; corrupted or unverifiable entries are analyzed again.
- The startup catalog collects literal event names declared in Lua/JS files. Dynamic names, unreadable or escrow files and DLLs are not certified as completely scanned.
- Manual automatic setup waits for security analysis. Incomplete, suspicious, changed or timed-out resources are never automatically approved.
- Test player joins, normal gameplay and admin actions on a staging server, and interpret resmon alongside player count and framework load.