Client signals: what a client says about itself
Bastrel’s client script is small and decides nothing. Every few minutes the server asks each loaded player for a short report: the list of the commands his client knows and the list of the resources it runs. The server compares them with the resources it knows and writes what does not add up in the journal. This page covers BAS-CL-010, BAS-CL-011 and the list whitelists.clientResources you fill in.
A claim, never a proof
Section titled “A claim, never a proof”What a client reports is declared by the client. A cheater controls what his client says: an executor that lies, or that does not answer, goes through. These signals catch low-end executors, and give your staff something to read. They are never enough on their own to sanction a player:
- both codes are heuristics (not deterministic) of severity medium: they carry no score weight (0 in
scoring.weights): they alert and go to human review, they never raise a score, and they never kick or ban by themselves (with the decay, even a few points per window would add up to a kick on the word of a client); - both ship as
unverified: whatever your configuration says, they are held inobserve(journal and detections table,would_haveempty, no alert, no Discord message, no score, nothing cancelled) until a test in game lifts them. What the fieldresourceof the native commands of the game contains, and what list of resources a real client reports, were never captured; - both depend on the list
whitelists.clientResourcesand stay inobserveuntil you setcalibrated = true, even after the first point is lifted; - a report that is missing, late or cut is unknown, never a finding. A client that stays silent is the business of the heartbeat, not of this module.
| Code | What it sees | Severity | How certain |
|---|---|---|---|
BAS-CL-010 |
Commands whose owner resource is empty or unknown to the server (typical of an injected menu) | medium | heuristic, unverified |
BAS-CL-011 |
Resources the client runs that the server does not know | medium | heuristic, unverified |
How it works
Section titled “How it works”clientsignals.firstReportMs(60 s) after a player joins, or after his character is loaded, then everyclientsignals.reportMs(5 min), the server sends a request with a report number, the size of a chunk (chunkEntries, 100), the number of chunks (6) and the gap between two chunks (gapMs, 1 s). The client has no period or limit of its own: everything comes from the request, and it never sends a report nobody asked for. The request is a net event the client script listens to (RegisterNetEvent); the chunks it sends back are registered by the server as an internal event of Bastrel.- The client lists its commands (
GetRegisteredCommands) and its started resources (GetNumResources,GetResourceByFindIndex,GetResourceState), cuts the names to 128 bytes, and sends them back in chunks, one chunk per gap, commands first. Every chunk also carries the number of entries the client read (a count, no threshold). A report is at most 2 x 6 chunks; a new request replaces the one in progress. - The server puts the chunks back together by player and report number. A chunk from another report, another player, a kind already complete or a part already received is only counted. Each list is kept up to
clientsignals.maxEntries(512) entries; the rest is dropped and marked, which is not a detection. A list is also marked as cut when the client says it read more entries than it sent (it cuts its own lists to the size the request allows), and the evidence of a finding says so (entriesTruncated). - A list is judged once all its chunks are in. A report not complete after
clientsignals.replyMs(30 s) is dropped and written asclientsignals.reportwithresult: incomplete: nothing is concluded from it. - A finding is one per report and code, never one per command, with the names (sorted, at most
clientsignals.evidenceMax, 20) in the evidence and their total. A finding is passed on once per player, code andclientsignals.violationReportMs(10 min). - Every message a client sends is limited by the same checks as any other event: an honest client produces no
BAS-NE-*.
The list you fill in: whitelists.clientResources
Section titled “The list you fill in: whitelists.clientResources”clientResources = { known = {}, systemResources = {}, systemCommands = {}, calibrated = false },The three lists ship empty (no name is invented for you):
systemResources: names that exist on a client without being a resource of your server (resources of the game itself) and the owner names carried by the native commands of the game, when they have one;systemCommands: the names of commands whose owner resource is empty (commands of the game itself). A command with an empty owner cannot be matched by a resource list, only by its name; a command whose owner is not empty and unknown is still reported, even if its name is listed;known: client-only resources of yours.
How to fill them: leave calibrated = false, wait for a few players to play, and read the evidence (names and owners) of the BAS-CL-010 and BAS-CL-011 findings in your journal: they are written even before you calibrate (journal lines only: while a code is unverified, would_have stays empty and nobody is sanctioned). The clientsignals.report lines only count entries and name none. The development test resource (not shipped) captures bastest cl-commands and bastest cl-resources list the same names on one client. List what you recognise, then set calibrated = true. Each list holds at most 512 names of 1 to 128 bytes. A list with an invalid entry is refused as a whole: Bastrel prints a [bastrel] warning at startup, writes an error clientsignals.list_invalid, and asks nothing and judges nothing until you fix it.
Lifting BAS-CL-010 out of unverified depends on a capture of what the native commands of a real client carry in resource: if it is empty, systemCommands is what silences them. Observed on 2026-10-24: an honest client lists 501 of 1453 commands with no owner resource (native input commands such as +aim or +attack); they are declared in systemCommands.
Configuration
Section titled “Configuration”config/modules.lua, block clientsignals (an invalid value takes its default, with a warning at startup):
| Key | Default | Meaning |
|---|---|---|
enabled |
true |
Turns the module off |
reportMs |
300000 | Period of the report per player (60000 to 3600000) |
firstReportMs |
60000 | Wait before the first report after the connection or the character load (10000 to 600000, and at least partRatePerMs + replyMs: a smaller value is raised to that sum with a config error, so that a character reload cannot send two full reports inside one window of the chunk limit) |
replyMs |
30000 | Time the server waits for a whole report (at least 2 x chunks x gapMs + replyMarginMs; a gapMs above 2083 raises it, and then firstReportMs, with a config error). A late report is only a clientsignals.report line with result: incomplete, never a code |
replyMarginMs |
5000 | Slack added to 2 x chunks x gapMs for that minimum (1000 to 60000); raise it if a real client needs more |
gapMs |
1000 | Gap between two chunks, set by the server |
chunkEntries |
100 | Entries per chunk (10 to 500) |
maxEntries |
512 | Entries kept per list (chunkEntries or more, at most 64 chunks) |
partRatePerMs |
30000 | Window of the rate limit of the chunks, at most 2 x chunks per window and player (1000 to 60000) |
violationReportMs |
600000 | A finding is passed on once per player, code and window |
evidenceMax |
20 | Names in the evidence |
logWindowMs |
60000 | Shortest time between two clientsignals.report lines of one player |
Score weights are in scoring.weights (BAS-CL-010 and BAS-CL-011: 0, a claim of a client never feeds the score).
Limits you should know
Section titled “Limits you should know”systemCommandsmatches on the name only. A command with an empty owner that reuses a listed name (for examplequit) is not seen.- The list is declared by the client. An executor that lies or does not answer passes. This is a signal and evidence, not a protection.
- A menu injected into a legitimate resource registers its commands under the name of that resource and is not seen (a known limit).
- A resource name is compared as written, after the cut to 128 bytes on both sides (the server cuts the names of its own resources the same way the client does).
- No report is asked from a player whose character the framework says is not loaded, none while one is in flight, and none while your
clientResourceslist is refused. A character load restarts the wait for the first report. - The resources of the server are read once and kept until a resource starts or stops. The list is also read again when the
refreshcommand completes (the documented server eventonResourceListRefresh; that it fires in game is on the test list). - The names are written in the evidence with control characters replaced by
?.