跳转到指南

文档 9.6.24

事件与触发器

Tosun AntiCheat 发出、供你的脚本监听的事件,你可以触发以完成集成的事件,以及把你自己的事件映射为豁免、密钥检查、频率限制和陷阱的配置列表。反作弊内部事件受保护,不得调用。

概览#

FiveM 有本地事件和网络事件。本地事件只停留在游戏的一侧:用 TriggerEvent 触发,用 AddEventHandler 接收。网络事件跨越网络:服务器用 TriggerClientEvent 发送,客户端用 TriggerServerEvent 发送,接收方必须用 RegisterNetEvent 注册。Tosun AntiCheat 在服务器和客户端上都使用这两种事件。

本页列出反作弊发出、供你监听的事件,你可以触发以集成脚本的事件,以及把你自己的事件名变成豁免、密钥检查、频率限制或陷阱的配置列表(ts.DetectionExempt、ts.ProtectedEvents、ts.EventLimiter、ts.triggerList)。

反作弊还为自身的加入握手、密钥交换、Shield、管理菜单和网页面板操作注册了许多内部事件。它们受 nonce、密钥、签名、频率限制或管理人员检查保护,其中一些是陷阱。它们不属于集成接口:不要触发它们,不要在它们上注册处理器,也不要复用它们的名称。请改用 exports。

类型触发方式接收方式谁可以触发
服务器本地事件TriggerEventAddEventHandler任何服务器资源。
客户端本地事件TriggerEventAddEventHandler该玩家游戏中的任何客户端脚本,包括注入的作弊代码。
网络事件,客户端到服务器TriggerServerEventRegisterNetEvent + AddEventHandler任何已连接的玩家,可携带任意参数。
网络事件,服务器到客户端TriggerClientEventRegisterNetEvent + AddEventHandler任何服务器资源。客户端脚本也可以用 TriggerEvent 在本地触发同名事件。

常见集成任务#

选择与你的目标相符的事件或设置。每一项在本页下文都有详细说明。

目标使用
在检测以封禁、踢出或日志行结束时做出响应ts_anticheat:playerBanned
只在真正执行了封禁时做出响应ts_anticheat:banCommitted
响应解封ts_anticheat:playerUnbanned
镜像实时日志流ts_anticheat:newLog
在反作弊重新应用设置时刷新你的缓存tosun-ac:settingsReloaded / ts_anticheat:configUpdated
在自定义框架上应用面板中的角色改名tosun-ac:renameCharacter
自定义出生后开启客户端检查playerSpawned
自定义角色选择后刷新面板tosun-ac:panel:characterReady
让客户端重新检查管理人员状态ts_anticheat:adminStateChanged
从你的服务器脚本在客户端豁免一名玩家tosun-ac:bridge:clientExempt
在你的菜单或操作事件触发时自动豁免玩家ts.DetectionExempt
为你自己的客户端到服务器事件要求按玩家分配的密钥ts.ProtectedEvents + <name>:safe

反作弊发出的服务器事件#

这些是服务器本地事件。在任何服务器脚本中用 AddEventHandler 监听它们。不要自己触发它们;请参阅下文的安全章节。

事件触发时机数据
ts_anticheat:playerBanned每一种检测结果:BAN、KICK 和 LOG。它在踢出、写入封禁和记录日志之前触发。作为管理人员或白名单被跳过的玩家不会触发它。data 表:playerId(字符串)、playerName、reason、banID、side('server')、punishment('BAN'、'KICK' 或 'LOG')
ts_anticheat:banCommitted仅 BAN 结果(检测以及 ban 和 punish exports),在封禁记录的写入进入队列之后、面板 webhook 和断开连接之前立即触发。playerId(数字),row 表:banID、playerName、reason、license、steam、discord、ip、HWID、HWID2 到 HWID5、config(检测配置键)
ts_anticheat:playerUnbannedunban export、/ts unban 命令或游戏内菜单移除封禁之后。网页面板中的解封不会触发它。banID(传给 unban export 的值,原样不变)
ts_anticheat:newLog每一条实时日志:检测、客户端日志行、管理菜单操作以及部分反作弊内部日志行。payload 表:src(类别标签,不是玩家 id)、event(消息文本)、status(默认 'Info')、playerName、time(os.time())
tosun-ac:settingsReloaded设置同步重新应用设置之后。这会在启动时以及几乎每个同步周期发生,而不只是在有变化时。无
ts_anticheat:shield:screenshot_saved请求的 Shield 截图已到达并通过检查。src(数字)、b64(base64 图片,64 到 2,500,000 个字符)
ts_anticheat:shield:ban_video_saved被踢出或被封禁玩家的游戏视频在 120 秒内上传完成。src(数字)、videoUrl、reason、config(reason 和 config 来自客户端)
<name>:safe对 ts.ProtectedEvents 中所列事件的一次调用通过了密钥和数据检查。src(数字,在线玩家 id),然后是密钥之后的原始参数
tosun-ac:renameCharacter在 qb、qbcore、qbox、qbx 或 esx 以外的框架上,网页面板执行了角色改名。citizenid(字符串)、firstName(第一个词)、lastName(名字其余部分,可能为 '')

反作弊发出的客户端事件#

这些事件会到达客户端脚本。ts_anticheat:configUpdated 是客户端本地事件;用 AddEventHandler 监听。ts_anticheat:receiveEventKey 是来自服务器的网络事件;请在你的客户端脚本中用 RegisterNetEvent 注册它。

事件类型触发时机数据
ts_anticheat:configUpdated客户端本地事件客户端应用了来自服务器的设置并重新扫描其 DetectionExempt 钩子之后。大约每个同步间隔触发一次。无
ts_anticheat:receiveEventKey网络事件,服务器到客户端作为对 ts_anticheat:requestEventKey(你的请求或反作弊自己的请求)的回复,以及服务器每次给玩家签发新密钥时。它在加入握手之后到达。key(字符串,玩家的 SafeEvents 密钥)

监听器示例#

一个对踢出和封禁做出响应的服务器脚本,以及一个在反作弊重新应用设置时丢弃缓存的客户端脚本。每个服务器监听器都会先检查事件是否由反作弊触发。

-- server.lua(你的资源)
local AC = 'tosun-ac' -- 反作弊资源(文件夹)名称

AddEventHandler('ts_anticheat:playerBanned', function(d)
  if GetInvokingResource() ~= AC then return end -- 忽略伪造的调用
  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(你的资源)
local myCache
AddEventHandler('ts_anticheat:configUpdated', function()
  myCache = nil
end)

playerBanned 详解#

这个名称是历史遗留的。ts_anticheat:playerBanned 会在每一种检测结果时触发,而不只是封禁。请按 data.punishment 进行过滤。

data.playerId 是字符串。用作服务器 id 之前请先用 tonumber 转换。

该事件在执行动作之前触发。聊天广播、日志、DropPlayer 和封禁写入都在你的处理器之后运行。

以下情况不会触发:目标离线;玩家作为管理人员被跳过(除非 ts.AdminBypassDetections 为 false 或开启了 ts.Debug,否则管理人员会被跳过);玩家在临时白名单或面板白名单中。

每种结果都会生成 banID,但只有 BAN 才会保存。对管理人员的 KICK 会在事件已经触发之后被取消。

当封禁界面(editable/server/sv_banscreen.lua)正在向某玩家显示时,该玩家的大多数后续检测都会被吞掉,也不会为其触发事件。

banCommitted 详解#

ts_anticheat:banCommitted 是“确实执行了封禁”的最佳钩子。踢出和日志行请使用 playerBanned。

反作弊惩罚流程中的每个 BAN 结果都会触发它:检测以及 ban 和 punish exports。离线封禁(offlineBan 和 offlineBanByLicense exports)以及在网页面板中执行的封禁不会触发它。

封禁记录的写入通过 MySQL.Async.execute 排队且不等待完成,因此你的处理器运行时该记录可能还不在数据库中。

row 中的标识符已去掉前缀(license:、license2:、steam:、discord:、ip:)。缺失的标识符或令牌为英文文本 'Not found';缺失的 ip 为 'Hidden'。HWID 到 HWID5 是原始的玩家令牌。

newLog 和 settingsReloaded 详解#

这两个事件都触发得很频繁。请让它们的处理器保持轻量。

ts_anticheat:newLog:字段 src 是类别标签,不是玩家 id。检测使用 'DETECTION',status 为 BAN、KICK 或 LOG。客户端日志行使用固定的标签和状态:'AntiCheat' 配 'Tespit'(硬编码的土耳其语,意为“检测”),'AC-Grace' 配 'Info','AC-FakeTrigger' 配 'Warning'。管理菜单条目通常使用管理员的名字,status 为 BAN、KICK、INFO 或 SUCCESS。少数内部日志行使用其他类别。

tosun-ac:settingsReloaded:反作弊不会比较新旧值,因此该事件会在启动同步之后以及大多数同步周期触发,间隔为 ts.ServerPerf.configSyncIntervalSec 秒(自带配置中为 120,缺少该键时为 60,最小 20)。请把它理解为“设置已重新应用”,而不是“设置已更改”。

每次这样的同步之后,服务器还会把设置发送给所有客户端,随后每个客户端都会触发 ts_anticheat:configUpdated。

如果其他服务器资源触发了 tosun-ac:settingsReloaded,反作弊会为每名在线玩家执行一次完整的管理人员权限刷新。

Shield 截图和视频事件#

这两个事件都需要 Shield(ts.shield.enabled 不为 false;自带配置中已开启)。

ts_anticheat:shield:screenshot_saved:用服务器 export RequestShieldScreenshot(src) 请求截图。图片必须在请求后 60 秒内到达,反作弊对每名玩家每 20 秒最多接受一张。网页面板也可以请求截图,这些截图同样会触发该事件。

ts_anticheat:shield:ban_video_saved:需要 ts.shield.banGameplayVideo 不为 false(自带配置中已开启)。上传窗口在 KICK 或 BAN 之后开放 120 秒,视频 URL 的主机必须与面板一致。

ban_video_saved 中的 reason 和 config 来自客户端,没有经过类型检查或截断。请把它们当作不可信文本。

local AC = 'tosun-ac'

-- 服务器:向该玩家的游戏请求一张截图
exports[AC]:RequestShieldScreenshot(src)

AddEventHandler('ts_anticheat:shield:screenshot_saved', function(src, b64)
  if GetInvokingResource() ~= AC then return end
  -- b64 是 base64 图片字符串
end)

自定义框架上的角色改名#

网页面板可以给角色改名。对于 qb、qbcore、qbox 和 qbx,反作弊会更新 players.charinfo;对于 esx,会更新 users.firstname 和 users.lastname。对于其他任何 ts.Framework.framework 值,它会触发 tosun-ac:renameCharacter,由你自己应用改名。

无论是否存在监听器,反作弊都会向面板报告 'event_emitted_unknown_fw'。

新名字会去掉控制字符,最长 64 个字符,且至少需要 3 个字符。firstName 是第一个词;lastName 是其余部分,可能为空。

示例使用 oxmysql 的 MySQL.update 调用和一个虚构的 characters 表。请根据你的数据库结构调整查询。

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)

你可以触发的事件#

这些是供你的脚本触发的反作弊事件。框架的死亡和复活事件以及你的 ts.DetectionExempt 事件在各自的章节中介绍。

事件从哪里触发效果限制
tosun-ac:panel:characterReady客户端,使用 TriggerServerEvent。在面板的在线玩家列表中刷新该玩家的角色数据。每名玩家每 20 秒一次。
ts_anticheat:locale:setSelf客户端,使用 TriggerServerEvent 并附带语言代码。只为该玩家设置反作弊服务器消息的语言。会在服务器控制台输出一行。没有频率限制。代码必须是 2 或 3 个小写字母,并且存在对应的 locales/<lang>.json 文件。
ts_anticheat:requestEventKey客户端,使用 TriggerServerEvent。通过 ts_anticheat:receiveEventKey 发回该玩家的 SafeEvents 密钥。每名玩家每 3 秒一次。密钥只发给发出请求的玩家。
playerSpawned客户端,使用 TriggerEvent(本地)。开启大多数客户端检查,并启动出生宽限窗口。该客户端上的其他所有资源也会收到它。
ts_anticheat:adminStateChanged服务器,使用 TriggerClientEvent 发给一名玩家。客户端会再次向服务器查询其真实的管理人员状态。服务器对每名玩家每秒最多回答一次。
tosun-ac:bridge:clientExempt服务器,使用 TriggerClientEvent;需要在你的资源中加入桥接文件。为该玩家提供客户端豁免。500 到 300000 毫秒。

自定义出生:playerSpawned#

在反作弊看到本地 playerSpawned 事件之前,大多数客户端检查都保持关闭:回血、无敌、传送、隐身、自由视角、武器、OCR、native、坐标和载具无敌。通常由 spawnmanager 触发它。

如果你的出生或多角色系统不使用 spawnmanager,请在玩家进入世界后在本地触发 playerSpawned,或调用客户端 export SetSpawned(true)。

playerSpawned 还会启动 ts.spawnGraceSecondsMs 的出生宽限窗口。自带值为 500,代码会将其提高到最小值 5000 毫秒;没有该键时代码使用 25000。窗口内的检测不会被取消,只会额外记录一条 'AC-Grace' 日志行。

playerSpawned 还会清除反作弊的死亡状态,并标记一次客户端传送。

自带的 ts.DetectionExempt.clientEvents 列表包含 playerSpawned,因此每次 playerSpawned 还会启动一次时长为 defaultDurationMs(60 秒)的客户端豁免。

SetSpawned(false) 每 5 分钟最多生效一次,检查会在约 30 秒的可见移动之后重新开启。

-- 客户端,在你自己的出生逻辑之后
TriggerEvent('playerSpawned')

-- 或者,不对其他资源产生副作用
exports['tosun-ac']:SetSpawned(true)

自定义多角色:characterReady#

在服务器上,反作弊监听 QBCore:Server:OnPlayerLoaded、QBCore:Server:PlayerLoaded、esx:playerLoaded 和 qbx_core:server:playerLoaded。每个事件都会立即把已加载的角色推送到面板的在线玩家列表。如果自定义多角色脚本不触发其中任何一个,就应在角色选择完成后从客户端触发 tosun-ac:panel:characterReady。

每次调用都会把该玩家的记录写入数据库,并在设置了面板 URL 和密钥时向面板发送一次 HTTPS 请求。因此服务器对每名玩家每 20 秒只接受一次调用,其余的会被忽略。

-- 客户端,在角色选择完成之后
TriggerServerEvent('tosun-ac:panel:characterReady')

玩家语言:locale:setSelf#

客户端可以为自己选择反作弊服务器消息的语言。成功时服务器会在服务器控制台输出一行。

它只改变发给该玩家的服务器消息,不改变客户端的语言设置。

反作弊自己的客户端不会发送此事件。服务器端的替代方法是 setPlayerLocale export 和 ts_setlang 命令。

-- 客户端
TriggerServerEvent('ts_anticheat:locale:setSelf', 'en')

管理人员变更:adminStateChanged#

你的管理脚本授予或移除管理人员权限之后,通知该玩家的客户端重新检查其管理人员状态。该事件本身不能改变状态:客户端会向服务器发送一个新的 nonce,只有服务器的回复才起决定作用。

反作弊中没有任何地方发送此事件。客户端也会每 20 秒自行重新检查一次,因此这个调用只是消除了这段延迟。

作弊程序在本地触发它不会有任何收获;服务器对每名玩家每秒最多回答一次检查。

-- 服务器,在更改 src 的管理人员权限之后
TriggerClientEvent('ts_anticheat:adminStateChanged', src)

桥接事件:tosun-ac:bridge:clientExempt#

客户端桥接文件会在你自己的资源内注册这个网络事件。你的服务器脚本把它发送给一名玩家,桥接文件就会为该玩家调用反作弊的客户端 SetExempt export。

durationMs 是以毫秒为单位的数字,或不区分大小写的预设名称:SHORT、MEDIUM、LONG 或 XL(30、60、120 或 180 秒)。值会被限制在 500 到 300000 毫秒之间。不填值时使用 convar 默认值(60000)。

豁免键为 'bridge:' 加上 tag;tag 为 nil 时则加上你的资源名。

这只是客户端豁免。服务器检查会继续运行。

反作弊从不发送此事件。如果你的多个资源都包含桥接文件,一次 TriggerClientEvent 会在每个资源中各运行一次。

  1. 在你资源的 fxmanifest.lua 中添加 client_script '@tosun-ac/bridge/tosun_ac_client.lua'。
  2. 可选,在 server.cfg 中:如果你重命名了反作弊文件夹,添加 setr tosun_ac_resource 并填写新名称(默认 tosun-ac)。要更改默认时长,添加 setr tosun_ac_bridge_client_ms 并填写以毫秒为单位的值(默认 60000)。
  3. 在你的服务器脚本中发送 TriggerClientEvent('tosun-ac:bridge:clientExempt', src, durationMs, tag)。
-- 服务器:在理发菜单打开期间豁免该玩家 2 分钟
TriggerClientEvent('tosun-ac:bridge:clientExempt', src, 'LONG', 'barber')

框架的死亡和复活事件#

客户端监听这些事件,以便知道玩家何时死亡或倒地。该状态有效期间,回血突增、无敌、传送、隐身、观战以及其他几项客户端检查都会被跳过。原生死亡也会通过 gameEventTriggered 伤害事件被捕获。

自 9.5.9 起,玩家重新明显活跃后,反作弊会自动结束此状态。

自定义急救脚本可以在本地触发这些名称之一,使反作弊的状态保持一致。这些是框架共用的名称,因此该客户端上监听它们的其他所有资源也会运行(例如 ESX 死亡界面)。

事件类型效果
hospital:client:isDead网络倒地;启动 25 秒死亡后窗口。
hospital:client:SetDeathState网络,布尔参数true:倒地,并启动 25 秒死亡后窗口。false:复活,并启动 10 秒复活后窗口。
hospital:client:SetLaststand网络,布尔参数true:倒地,并启动 25 秒死亡后窗口(false 会被忽略)。另外有 45 秒自动豁免。
qb-medical:client:OnDeath网络倒地;启动 25 秒死亡后窗口。
esx:onPlayerDeath网络倒地;启动 25 秒死亡后窗口。另外有 45 秒自动豁免。
baseevents:onPlayerDied本地(autoExempt 开启时为网络)启动 25 秒死亡后窗口。另外有 45 秒自动豁免。
baseevents:onPlayerKilled仅本地启动 25 秒死亡后窗口。
hospital:client:Revive网络复活;启动 10 秒复活后窗口。
qb-ambulancejob:client:RevivePlayer网络复活;启动 10 秒复活后窗口。
qb-medical:client:OnRevive网络复活;启动 10 秒复活后窗口。
esx_ambulancejob:revive网络复活;启动 10 秒复活后窗口。
esx_basicneeds:onRevive网络复活;启动 10 秒复活后窗口。
playerSpawned本地清除倒地状态,死亡后窗口还剩约 17 秒。
-- 自定义急救脚本(客户端),在你自己的复活逻辑之后
TriggerEvent('esx_ambulancejob:revive')

自动豁免事件(autoExempt)#

启用 ts.autoExempt.enabled(自带配置中为 true)时,客户端会监听常见的框架事件,并通过 SetExempt export 授予短时客户端豁免。豁免期间的客户端检测不会受到惩罚,而是作为标有 '[exempt]' 的 LOG 行上报。设置 ts.autoExempt.enabled = false 可关闭此功能。

client/anticheat_auto_exempt.lua 的头部注释说这些事件无法被远程触发。这是错误的:它们是用 RegisterNetEvent 注册的,因此服务器也可以触发它们。不过效果仍然只是客户端豁免。

场景时长事件
外观菜单180 秒illenium-appearance:client:openMenu, appearance:client:openMenu, fivem-appearance:client:openMenu, ox_appearance:openMenu
服装菜单180 秒qb-clothing:client:openMenu, qb-clothes:client:openMenu, rcore_clothing:openClothingShop, clothing:client:openMenu
ESX 皮肤和理发菜单180 秒esx_skin:openSaveableMenu, esx_skin:openRestrictedMenu, skinchanger:model:loaded, tgg_barber:open, barbershop:client:open
角色选择30 秒qb-multicharacter:client:chooseChar, qb-multicharacter:client:closeNUIdefault, esx_multicharacter:SetupCharacters, esx:restoreLoadout, ox:playerLoaded, qbx_core:client:playerLoggedOut
死亡和濒死(QB、ESX)45 秒esx_ambulancejob:setDeathStatus, esx:onPlayerDeath, hospital:client:SetLaststand, hospital:client:OnPlayerLaststand, qb-ambulancejob:client:playerDead
死亡(其他脚本)45 秒wasabi_ambulance:clientDeath, ars_ambulancejob:client:dead, baseevents:onPlayerDied, baseevents:onPlayerWasted
角色已加载15 秒QBCore:Client:OnPlayerLoaded, qbx_core:client:playerLoaded, esx:playerLoaded, ox:playerLoaded
反作弊在客户端启动60 秒onClientResourceStart

加入关卡:框架已加载事件#

启用 ts.NetworkJoin.waitForFrameworkLoaded(自带配置中为 true)且框架为 qb、qbox 或 esx 时,客户端会等待一个框架已加载事件,然后才完成加入反作弊的流程。这会推迟该玩家所有客户端保护的启动。其他框架值会跳过等待。

在 minReadyDelayMs(自带配置中为 15 秒)过去且加载画面结束之后,客户端会等待其中一个已加载事件,或等待 LocalPlayer.state.isLoggedIn(qb、qbox)或 ESX.PlayerLoaded(esx),总计最多 180 秒。然后它会等待 postFrameworkSettleMs(自带配置中为 5 秒)并等待碰撞加载完成,再报告就绪。

框架已加载事件重置事件
qbQBCore:Client:OnPlayerLoadedQBCore:Client:OnPlayerUnload
qboxQBCore:Client:OnPlayerLoaded, qbx_core:client:playerLoggedIn, qbx_multicharacter:client:chooseCharQBCore:Client:OnPlayerUnload
esxesx:playerLoadedesx:onPlayerLogout

受保护事件(SafeEvents)#

ts.ProtectedEvents 让反作弊在你自己的客户端到服务器事件上检查按玩家分配的密钥。通过检查的调用会在服务器上以本地事件 '<name>:safe' 的形式再次触发,第一个参数是玩家 id。自带列表只包含 'test:event'。

TriggerSafeServerEvent 只存在于反作弊自己的客户端中,并没有导出,因此其他资源必须使用上面的 receiveEventKey 模式。反作弊的客户端每 5 秒请求一次密钥,直到收到为止(最多尝试 12 次)。

服务器对每名玩家每 3 秒最多响应一次密钥请求,反作弊自己的请求也计算在内。如果没有收到密钥,请几秒后再次请求。保持你的处理器处于注册状态,并始终发送你收到的最新密钥。

  1. 在 configs/anticheat_config.lua 中把你的事件名加入 ts.ProtectedEvents。该列表只在启动时读取一次,因此修改后请重启反作弊。
  2. 在你的客户端脚本中注册 ts_anticheat:receiveEventKey 以保存密钥,然后发送 ts_anticheat:requestEventKey。
  3. 发送你的事件时把密钥作为第一个参数:TriggerServerEvent(name, key, ...)。
  4. 在你的服务器脚本中用 AddEventHandler 处理 '<name>:safe'。第一个参数是玩家的服务器 id,后面是你的参数。
-- configs/anticheat_config.lua
ts.ProtectedEvents = { ['my-shop:buy'] = true }

-- 客户端(你的资源)
local key
RegisterNetEvent('ts_anticheat:receiveEventKey', function(k) key = k end)
TriggerServerEvent('ts_anticheat:requestEventKey')
-- 稍后,在 key 已设置之后:
TriggerServerEvent('my-shop:buy', key, 'bread')

-- 服务器(你的资源)
AddEventHandler('my-shop:buy:safe', function(src, item)
  if GetInvokingResource() ~= 'tosun-ac' then return end
  -- 在这里检查金钱、库存和权限
end)

SafeEvents 检查什么、不检查什么#

SafeEvents 只验证传输。它从不惩罚任何人,也不能替代你自己处理器中的检查。

  • 每次客户端调用都会先计入 ts.SafeEventRateLimit(每 ts.SafeEventRateWindow 秒;默认每 10 秒 30 次),即使没有密钥也一样。
  • 如果玩家还没有密钥,调用会被静默丢弃。
  • 密钥错误、触发频率限制、参数超过 ts.SafeEventMaxArgs 个(默认 64)或数据不合法时,调用会被丢弃,并且每名玩家、每个事件、每个窗口写入一条数据库日志。
  • 来自服务器的调用(source 0)会被忽略,不会触发 ':safe' 事件。
  • 原始网络事件仍会到达你自己的资源用 RegisterNetEvent 注册的任何处理器。只有 ':safe' 镜像事件受到把关。
  • 绝不要对 ':safe' 名称使用 RegisterNetEvent;那样客户端就能直接调用它。
  • SafeEvents 只是一层额外保护,你自己的处理器仍必须检查权限、余额和所有权。
  • 不要把框架事件(esx、qb)加入列表。它们的调用方不会发送密钥,因此 SafeEvents 会拒绝这些调用、写入日志行,并且永远不会触发 ':safe'。框架自己的处理器仍会运行。
  • configs/anticheat_config.lua 中的土耳其语注释说没有密钥的调用会导致误封。在当前版本中它们只会被丢弃。

ts.DetectionExempt:把你的事件映射为豁免#

ts.DetectionExempt 把你自己的事件名变成临时检测豁免。用于会合法改变玩家外观、位置或状态的菜单和服务器操作:服装、理发、房屋、车库、角色选择和复活。这些列表从 configs/anticheat_config.lua 读取;面板设置不会改变它们。ts.MenuDetectionExempt 是为旧配置保留的别名。

键类型自带值效果
enabled布尔值truefalse 会关闭所有 DetectionExempt 钩子。
defaultDurationMs数字(毫秒)60000没有自己 durationMs 的条目所使用的豁免时长。服务器条目被限制在 1000 到 120000 毫秒,客户端条目被限制在 1000 到 300000 毫秒。
budgetMsPer10Min数字(毫秒)240000一名玩家在 10 分钟内可从 serverEvents 累计获得的软豁免时间上限。
clientEvents字符串列表153 个条目(142 个不重复名称)客户端网络事件。其中一个触发时,该玩家的客户端检查会被豁免。
localEvents字符串列表11 个条目客户端本地事件(仅客户端上的 TriggerEvent)。效果与 clientEvents 相同。
serverEvents字符串或表的列表34 个条目客户端到服务器的网络事件。玩家触发其中一个时,该玩家获得软豁免。

DetectionExempt.serverEvents#

反作弊会用 RegisterNetEvent 注册列表中的每个名称。玩家触发它时,该玩家获得软豁免:服务器移动检查以及服务器端的无敌、隐身、noclip 和镜头检查会放宽,该玩家的客户端检查通过 state bag 被豁免。这绝不是免于惩罚。

条目可以是字符串或表:{ event = 'name', durationMs = 60000, resource = 'owner-resource', always = true }。durationMs 被限制在 1000 到 120000 毫秒。

只有在所属资源已启动或正在启动时才会安装钩子。所属资源是事件名第一个冒号之前的前缀;qb-clothes 映射到 qb-clothing,hospital 映射到 qb-ambulancejob。前缀与资源名不同时请设置 resource。always = true 会在所属资源未运行时也安装钩子。

钩子在反作弊启动 2.5 秒后安装,并在其他任何资源启动 1 秒后再次安装。

玩家可以自己触发这些事件,因此每名玩家都有一个额度(budgetMsPer10Min)。玩家用完额度时,反作弊会写入一条控制台警告和一条数据库日志,并在这 10 分钟的剩余时间内忽略该玩家的后续请求。

在服务器控制台运行 tosunac_integration,可以查看挂钩了多少个服务器豁免事件。它只能在服务器控制台运行,输出也在那里。

DetectionExempt.clientEvents 和 localEvents#

clientEvents 中的名称在客户端上用 RegisterNetEvent 注册。其中一个触发时,无论来自服务器还是来自本地 TriggerEvent,客户端都会被豁免 defaultDurationMs。

clientEvents 只接受字符串。表形式的条目,包括带有 durationMs 的条目,在客户端上会被静默忽略。

豁免期间,客户端检测不会受到惩罚,而是作为标有 '[exempt]' 的 LOG 行上报,每项检查每分钟最多一次,因此滥用行为在面板中仍然可见。服务器检查不受影响。

localEvents 的工作方式相同,但只使用 AddEventHandler,因此只响应客户端上的 TriggerEvent。

客户端在加载 3.5 秒后扫描这两个列表,并在每次设置同步时再次扫描。

你没有运行的脚本的事件永远不会触发,因此列表中多余的名称不会造成影响。自带的 clientEvents 列表重复了 11 个名称;重复项无害。

DetectionExempt 示例#

把你的名称加入 configs/anticheat_config.lua 中已有的列表,然后重启反作弊。替换整个表会删除自带条目。示例只显示新增的条目。

如果代码由你控制,请优先使用 exports:客户端 export SetExempt(true, ms, reason),从受信任资源调用服务器 export SetExempt(src, ms, reason),以及在传送玩家之前立即调用服务器 export MarkTeleport(src, ms)。

ts.DetectionExempt = {
  enabled           = true,
  defaultDurationMs = 60000,
  budgetMsPer10Min  = 240000,

  -- 客户端网络事件:只接受字符串
  clientEvents = {
    -- ...自带条目...
    'my-clothing:client:openMenu',
  },

  -- 客户端本地事件(客户端上的 TriggerEvent)
  localEvents = {
    -- ...自带条目...
    'my-menu:opened',
  },

  -- 客户端到服务器事件:为发送者提供软豁免
  serverEvents = {
    -- ...自带条目...
    '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:网络事件的频率限制#

ts.EventLimiter 把事件名映射为每名玩家在 5 秒内的最大调用次数。反作弊在加载时用 RegisterServerEvent 注册每个名称,并按玩家统计调用次数;所有计数器每 5 秒重置一次。你自己对该事件的处理器仍会运行。

当计数达到上限时,反作弊会以配置 ts.EventLimiter(server_event_spam,默认 KICK)尝试一次检测。

自带列表有 25 个条目:test:event、esx:getSharedObject、银行和 QBCore 金钱事件、复活、载具生成、背包、监狱和手铐事件,以及 HCheat:TempDisableDetection。

名称和上限在加载时读取。数据库表 ac_event_limits 会在同步时替换 ts.EventLimiter,但不会改变已注册的处理器,因此编辑任何一个之后都请重启反作弊。

ts.EventLimiter = {
  -- ...自带条目...
  ['my-shop:buy'] = 10,
}

陷阱事件:绝不要使用这些名称#

有些事件只是为了抓作弊而存在。遵守以下规则,你自己的脚本就绝不会触发它们。

  • 反作弊会注册只有作弊程序才会触发的陷阱事件。绝不要触发它们,也绝不要重复使用它们的名称。
  • 要查看你自己服务器上的陷阱名称,请运行 /ts evtlist(列表输出到服务器控制台)。要让某个事件不再被视为陷阱,请使用 /ts evtwhitelist <eventName>。参见“命令”主题。
  • 用你的资源名作为你自己事件的前缀,例如 myresource:doThing,这样它们就绝不会与陷阱冲突。

声音事件频率防护#

反作弊在自己的资源中把这些声音事件注册为网络事件,并按玩家计数。超过 ts.premiumGuard.soundPerSec(默认每秒 6 次)时会执行 soundAction(默认 'log')。只有 soundCancel = true(默认 false)时才会取消该事件。如果你的声音资源自己注册了该事件,它的处理器仍会运行。

只有 ts.premiumGuard.enabled 和 ts.premiumGuard.soundGuard 都为 true(自带配置中两者都为 true)时,该防护才会运行。在确认你的声音脚本不会超过上限之前,请保持 soundCancel 为 false。

事件资源
InteractSound_SV:PlayOnAllInteractSound
InteractSound_SV:PlayWithinDistanceInteractSound
InteractSound_SV:PlayOnOneInteractSound
xsound:stateSoundxsound
xsound:server:playxsound
xsound:server:playUrlxsound
xsound:server:playUrlPosxsound

反作弊发送给其他资源的事件#

在管理人员、面板或检测操作之后,反作弊会触发属于其他资源的事件。如果你运行了其中某个资源,请预期会收到这些调用。

天气和时间只使用第一个已启动的同步资源,顺序为:qb-weather、Renewed-Weathersync(仅 export)、cd_easytime、wd_weather、weathersync、vSync、qb-weathersync。如果都没有启动,反作弊会直接在每个客户端上设置天气和时间。

hospital:client:Revive 和 esx_ambulancejob:revive 也是反作弊的监听事件(参见“框架的死亡和复活事件”),因此这些复活也会开启 10 秒复活后窗口。

事件端发送时机
qb-weather:server:RequestStateChange服务器本地qb-weather 没有 setWeather 或 setTime export 时更改天气或时间。对于时间,值是一个 { hour, minute } 表。
cd_easytime:setWeather, cd_easytime:setTime服务器本地使用 cd_easytime 更改天气或时间。
<res>:setWeather, <res>:setTime服务器本地当 wd_weather、weathersync、vSync 或 qb-weathersync 没有对应的 export 时,用它们更改天气或时间。
wasabi_ambulance:revive客户端本地wasabi_ambulance 已启动时,管理人员或面板执行复活。
hospital:client:Revive客户端本地使用 qb-ambulancejob 或 Starter-Hospital 时,管理人员或面板执行复活。
esx_ambulancejob:revive客户端本地使用 esx_ambulancejob 时,管理人员或面板执行复活。
<res>:revive, <res>:client:Revive, <res>:client:revive客户端本地其他情况下管理人员或面板执行复活:ambulance、qb-ambulancejob、cd_ambulance 和 ars_ambulancejob 中第一个已启动的资源会收到全部三个名称。如果玩家仍处于死亡状态,随后会执行原生复活。
QBCore:Notify客户端本地;服务器到客户端反作弊执行的每一次治疗,即使没有 qb-core 也会发送(它的 qb-core 检查始终为 true)。还有面板的金钱、背包和通知操作。
esx:showNotification, QBCore:Player:SetPlayerData, inventory:client:ItemBox服务器到客户端面板的金钱、背包和通知操作,以及以物品形式发放武器。
chat:addMessage服务器到客户端;客户端本地聊天消息和面板广播。ts.chatMessages = true(默认 false)时,每一种检测结果(包括 LOG)都会以硬编码的土耳其语发送给所有玩家(editable/server/sv_editme.lua)。
chat:addSuggestion客户端本地管理菜单文件加载时添加 /unspectate 命令提示。
qb-phone:client:CustomNotification, qs-smartphone:client:sendNotification, lb-phone:notification, yseries:notification, gks-phone:client:notification, roadphone:notify服务器到客户端踢出和封禁时的管理人员提醒,以及 phoneNotify 和 phoneNotifyAdmins exports。只有第一个已启动的手机资源会收到(npwd 使用 export;都没有时使用聊天)。需要 ts.AdminMenu.enable = true。

安全:谁可以触发哪个事件#

只有反作弊可以触发它发出的事件,其他资源不得触发它们。下表列出了每个事件可以由谁触发。

事件谁可以触发
ts_anticheat:playerBanned仅限反作弊。
ts_anticheat:banCommitted仅限反作弊。
ts_anticheat:newLog仅限反作弊。
ts_anticheat:shield:ban_video_saved仅限反作弊。
tosun-ac:settingsReloaded仅限反作弊。
ts_anticheat:configUpdated仅限反作弊。
ts_anticheat:adminStateChanged任何服务器资源。
tosun-ac:panel:characterReady任何客户端。每名玩家每 20 秒限一次。
ts_anticheat:locale:setSelf任何客户端。只影响调用者本人。
ts_anticheat:requestEventKey任何客户端。密钥只发给发出请求的玩家。
tosun-ac:bridge:clientExempt你的服务器脚本。

安全:检查 GetInvokingResource#

任何服务器资源都可以触发服务器本地事件。在你的监听器中,先确认事件是由反作弊触发的,再执行操作。反作弊自己的封禁界面监听器也使用类似的检查。

对 playerBanned、banCommitted、playerUnbanned、newLog、settingsReloaded、Shield 事件、renameCharacter 以及你的 '<name>:safe' 处理器都应用这项检查。

绝不要从你自己的资源触发反作弊的事件,即使是为了测试。有几个内置监听器不检查调用方。

AddEventHandler('ts_anticheat:banCommitted', function(playerId, row)
  if GetInvokingResource() ~= 'tosun-ac' then return end -- 如果重命名过,填你的文件夹名
  -- 在这里可以安全地处理 row
end)

安全:服务器权威设计#

客户端能触发的任何东西都只被视为提示,而不是权威依据。在你自己的处理器中也请遵循同样的规则。

  • 客户端豁免(clientEvents、localEvents、autoExempt、桥接事件和客户端 SetExempt export)永远不会改变服务器端的执行。它们生效期间,客户端检测会被记录为 '[exempt]',而不是受到惩罚。
  • serverEvents 豁免是软豁免:在服务器上它们只放宽移动和可见性检查(武器、金钱、伤害、事件滥用、注入和崩溃类惩罚仍然适用),并且每名玩家受 budgetMsPer10Min 限制。
  • 管理人员状态由服务器受 nonce 保护的回复决定。向客户端推送管理人员标志不能授予管理员权限。
  • SafeEvents 密钥只验证传输。你的 ':safe' 处理器必须在服务器上检查权限、余额和所有权。
  • 作弊程序可以在本地触发框架死亡事件。倒地标志会在约 20 秒的明显活跃游戏之后被清除。
  • 要从服务器端豁免玩家,请使用 exports:从受信任资源调用 SetExempt(src, ms, reason),或在传送之前调用 MarkTeleport(src, ms)。

内部事件#

除本页列出的事件外,反作弊还为其加入握手、密钥交换、Shield、管理菜单、网页面板操作和一个配套资源注册了内部事件。它们受 nonce、密钥、签名、频率限制或管理人员检查保护,其中有几个是会惩罚调用者的陷阱。本文档有意不对它们做说明。

  • 不要从服务器或客户端触发内部事件。
  • 不要在它们上注册处理器,也不要复用它们的名称。
  • 不要触发任何名称以 ts_anticheat: 或 tosun-ac: 开头的事件,除非本页将其列为你可以触发的事件。
  • 执行操作请使用 exports:例如 punish、MarkTeleport、freezePlayer、setPlayerHealth、RequestShieldScreenshot 和 quarantine。

9.6.15 中的已知问题#

这些行为存在于发布的代码中。请提前做好应对。

  • tosun-ac:settingsReloaded 和 ts_anticheat:configUpdated 几乎在每个同步周期都会触发,而不只是在设置更改时。
  • ts_anticheat:playerBanned 除 BAN 外,也会在 KICK 和 LOG 时触发。
  • 实时日志的管理人员手机提醒会在 ts_anticheat:newLog 中监听 status 'TESPİT' 或 'Detection',但没有任何地方发出这两个值,因此该提醒永远不会触发。
  • 反作弊执行的每一次治疗都会在客户端触发 QBCore:Notify,即使服务器没有 qb-core。
  • ts.quarantine.autoQuarantine 没有任何效果:它唯一的输入是一个没有任何地方发出的内部事件。请改用 quarantine export。
  • configs/anticheat_config.lua 中有几条注释是土耳其语,其中一些已经过时:ts.ProtectedEvents 中没有密钥的调用会被丢弃,而不是被封禁;ts.EventLimiter 也不会踢出玩家。

避免重启时重复进行昂贵分析#

每次启动都会核对资源内容的 SHA256 指纹。文件和扫描策略未变的资源会复用服务器 KVP 中经过认证的结果,不再重复昂贵的签名和事件分析。文件、例外或规则变化会使相关分析失效并重新执行。指纹检查仍需读取文件;不保证固定的 resmon 数值。

  1. 正常更新时保留 KVP 缓存;损坏或无法验证的记录会重新分析。
  2. 启动目录收集 Lua/JS 中明确声明的事件名。动态名称、不可读或 escrow 文件及 DLL 不会被认定为已完整扫描。
  3. 手动自动配置会等待安全分析完成。不完整、可疑、已变化或超时的资源不会自动获准。
  4. 在测试服务器检查玩家加入、正常游戏和管理员操作;结合玩家数量与框架负载评估 resmon。