Skip to content

State checks: what a player's character is doing

Bastrel reads, every 1.5 seconds per player, the state of the character as the server sees it, and compares it with what your server knows. A menu that makes a player invincible, invisible, strongly armoured or much stronger shows up here. This page covers seven codes.

All seven codes ship as unverified. FiveM does not document whether the value a server native returns follows what a cheating client sets, and Bastrel never took that on faith. Until a test in game shows that the server sees the value, these codes are held in observe whatever your configuration says:

  • they appear in the log and in the detections table, with would_have empty;
  • no alert, no Discord message, no score, no kick, no ban;
  • they never cancel anything.

They are lifted one at a time, in a later release, after a capture on a real server. You can already read the journal to learn what your own scripts trigger, and declare them (below) so that the day a code goes live it is quiet on your server.

Code What it sees Severity How certain
BAS-ST-001 A weapon in hand that is not the one your inventory (ox_inventory) says is equipped critical deterministic
BAS-ST-010 A weapon damage, defense or melee modifier that differs from the expected value (state.expectedModifiers) critical deterministic
BAS-ST-020 Invincible critical deterministic
BAS-ST-021 More health or armour than your configured caps high deterministic
BAS-ST-022 Several impacts from different players, no loss of health or armour low clue only
BAS-ST-030 Invisible character medium clue only
BAS-ST-040 Camera focus far from the character medium clue only
  • A player is read once per interval (sampler.intervalMs, 1.5 s). A finding needs two consecutive samples that agree (state.confirmSamples), so a state seen once is never reported. A sample that does not agree, a native that answers nothing, or a grace, starts the count again.
  • The evidence of a finding carries the values read. BAS-ST-030 carries visible (false, the value the server read) besides samples; BAS-ST-010 carries the three modifiers read, the expected ones, epsilon and relEpsilon.
  • A finding is reported once per player and code every 10 seconds (state.reportMs), however long the state lasts.
  • Nothing is judged while the player loads (graces.joinMs, 15 s) nor just after a change of routing bucket (graces.bucketChangeMs, 3 s). With QBX, the 15 s start when the character is loaded: a player on the character screen is never judged, however long it lasts, and a logout or a character change starts the wait again. Without that signal (another framework, qbx_core stopped, a load event missed because Bastrel restarted while the player was choosing), the 15 s start when the player joins; a load event Bastrel misses leaves that player unjudged until the next load (never a false report). There is no automatic grace for a respawn: the reanimation scripts of your server call allowTeleport.
  • BAS-ST-020 does not judge a character the framework says is dead or in last stand (with QBX: isdead or inlaststand), because qbx_medical makes the ped invincible then. The framework’s death state can be written by the client (qbx_medical copies a state bag), so the exemption holds only while the server agrees: the character stays within state.deadMoveMeters (5 m) of where it was first seen dead, unless it is carried (attached) or a passenger in a vehicle that another ped drives (a character driving away, or in a vehicle with no readable driver, is not excused), or a teleport your scripts declared with allowTeleport lands it within movement.teleportGraceRadius (25 m) of the declared destination (the reference point then follows it), and it sends no weapon damage and throws no grenade, rocket or Molotov. Explosions and vehicle weapons are not counted yet. At most state.reviveGraceMax (3) such episodes are excused per state.reviveGraceWindowMs (10 minutes); past that, nothing is excused. After a revive, a character still invincible is excused for state.reviveGraceMs (10 s), then BAS-ST-020 comes back. A finding within the window after a revive carries afterRevive: true for the review. If the framework answer is unknown, the check judges as before. For the owner: qbx_medical can leave the invincibility on after a revive (seen in game, 2026-10-24: GetPlayerInvincible stays 1 at full health until SetPlayerInvincible(false)); the grace does not cover that case, so Bastrel reports it once the grace ends. Report it to qbx_medical or clear the invincibility in your revive flow.
  • The limits of BAS-ST-021 are yours (state.healthCap, state.armourCap), never the maximum the game client reports: a client that raises its own maximum would raise the limit with it. Known gap: in one test (one client; maximum health raised first with SetPedMaxHealth, then health set to 300) the server read 200, so BAS-ST-021 reported excess armour but not excess health. That test does not establish that the server never sees health above 200. Armour is seen (150 against a cap of 100).
  • BAS-ST-040 sees the focus when the player renders a scripted camera and keeps setting the streaming focus at the same point (tested: a focus 300 m from the character is reported). A streaming focus set on its own does not move the position the server reads; a rendered camera on its own was not tested.
  • BAS-ST-022 is a clue declared by other players’ clients, so it asks for at least state.noEffect.impacts (5) impacts of damage above zero in state.noEffect.windowMs (10 s), from at least state.noEffect.minAttackers (2, never less) different players, none cancelled (by Bastrel, or by another resource whose handler ran before Bastrel’s: a handler that runs after is not seen), with no loss of health or armour between the sample before the first impact and the samples after. The check waits state.noEffect.settleMs (3 s) after the last impact and never judges at the first sample that follows the impacts, because the health the server reads can lag behind a shot. A heal or regeneration that takes health plus armour above the previous sample cancels the judgement of that window (a known miss, never an accusation). A heal that exactly cancels the damage within one sample interval (for example a return to the cap) looks the same as godmode and stays a known false positive of this clue (low, clue only, unverified). A shot counts only when its list of victims holds exactly one player other than the shooter, so one forged event cannot spread impacts over several players. A line shooter.not_loaded is logged once per session of a shooter that the framework reports as not loaded while declaring an impact (the impact still counts). Bastrel warns at startup when state.noEffect.windowMs minus settleMs is below sampler.intervalMs, because the check can then hardly judge. A victim that the framework reports as dead or downed is excused only while the server corroborates it, as for BAS-ST-020 (the body stays where it was first seen dead, or is carried, or followed a teleport your server declared, and it fires nothing): the state of the framework can be forged by a client, so a “dead” victim who walks away or shoots is judged. A player under an invincible grace and a victim shot only by one player are never reported. A shot at a ped that the shooter owns does not reach the server at all, so the check only sees shots at players.

BAS-ST-001: a weapon outside the inventory

Section titled “BAS-ST-001: a weapon outside the inventory”

Each sample, Bastrel compares the weapon the server reads on the character (GetSelectedPedWeapon) with the weapon ox_inventory says is equipped. It reports when a weapon is in hand and the inventory shows no weapon, or another one, for two consecutive samples (about 3 seconds). This is what a menu that gives weapons produces.

It never judges, and starts the count again, in these cases:

  • the inventory status is unknown (ox_inventory stopped, inventory not loaded yet, an export that fails): “unknown” is never read as “no weapon”;
  • the weapon in hand is a fist, or in state.weaponIgnore; the inventory is then not even read;
  • the character is in a vehicle, or the server cannot tell;
  • a weapon grace covers the weapon in hand (a grace for the equipped weapon alone does not). Bastrel grants it by itself (state.weaponGraceMs, 2 s) when ox_inventory moves, buys or uses a weapon, and when the equipped weapon appears or disappears between two samples (a server-side RemoveItem has no hook, so this covers it). Your scripts can add one with exports.bastrel:allowState(src, 'weapon', 10000, { hash = <weapon hash> }), only if grants.allowedResources lists them.

Limits. The inventory status comes in part from the player’s own client (ox_inventory records what the client answers), so it is never a proof on its own. A grace can be missing when a player holds many weapons (grants.maxWeaponHashes): the absence of a grace is never a proof of cheating either. The name of the parachute is not listed in the FiveM weapon reference, so it is not excluded by default: a player holding one shows as a finding (journal only) until you add its name or hash to state.weaponIgnore. A player with fists, no readable weapon or in a vehicle costs no export; an armed player costs one GetCurrentWeapon per sample, plus GetInventory when nothing is equipped. The cost is measured before the code is lifted.

ox_inventory already disarms. With its default setting (inventory:weaponmismatch true) the ox_inventory client takes away, every 200 ms, a weapon that is in hand while no weapon is equipped in the inventory. A weapon given by a menu is therefore usually removed before the second sample, and BAS-ST-001 does not report it (seen on 2026-10-20). The code stays useful against a client that defeats that disarming. To test it, run setr inventory:weaponmismatch false then restart ox_inventory, and put the setting back to true (and restart) afterwards.

Telling Bastrel your scripts are legitimate

Section titled “Telling Bastrel your scripts are legitimate”

Use the grace exports (see grants.md) from your server scripts:

Your script Call
Cutscene, character creation, invincible by design exports.bastrel:allowState(src, 'invincible', 10000)
Admin invisibility, character creation exports.bastrel:allowState(src, 'invisible', 0), then revokeState
Your script sets a weapon modifier exports.bastrel:allowState(src, 'weaponModifier', 10000)
Your script gives more than 200 health exports.bastrel:allowState(src, 'health', 10000), or raise state.healthCap
Your script gives a weapon outside ox_inventory exports.bastrel:allowState(src, 'weapon', 10000, { hash = GetHashKey('WEAPON_PISTOL') })
Staff spectate, scripted camera exports.bastrel:allowState(src, 'camera', 10000)

weapon, weaponModifier, health and camera are refused until grants.allowedResources lists the resources allowed to grant them.

Setting Default Meaning
state.confirmSamples 2 Consecutive samples before a finding (2 to 10).
state.modifierEpsilon 0.01 Gap from the expected value above which a finding is raised (strictly greater) (above 0, at most 1). The tolerance of one getter is the larger of this value and state.modifierRelEpsilon times the expected value. The game hands the server a coarsely rounded modifier: 0.99607849 for 1.0 at rest and 4.9951124 for a set of 5.0 (seen in game, 19 October 2026: gaps of 0.0039 and 0.0049, and the gap grows with the value). Bastrel warns at startup when the tolerance of a getter is below 0.0049, because every player would then be flagged. A gap under the tolerance is not seen: 1.005 and 1.009 pass for an expected 1.0.
state.modifierRelEpsilon 0.01 Relative part of the tolerance (above 0, at most 1): 1 % of the expected value, so a server that expects 10.0 tolerates 0.1.
state.expectedModifiers { damage = 1.0, defense = 1.0, melee = 1.0 } The modifier your server expects for each weapon getter (numbers above 0). Change one only if a script of yours sets it for everybody.
state.healthCap 200 Health above this is a finding (1 to 1000).
state.armourCap 100 Armour above this is a finding (1 to 1000).
state.focusMaxDistance 150.0 Metres between the camera focus and the character.
state.reportMs 10000 One report per player and code in this window (1000 to 3600000).
state.noEffect.impacts 5 Impacts for BAS-ST-022 (2 to 100).
state.noEffect.windowMs 10000 Window of those impacts (1000 to 600000).
state.noEffect.minAttackers 2 Different players needed (2 to 16).
state.noEffect.settleMs 3000 Wait after the last impact before judging (0 to 60000, below windowMs).
state.weaponGraceMs 2000 Grace after an inventory change, used by the weapon check (500 to 10000).
state.reviveGraceMs 10000 Grace after a revive for BAS-ST-020: a character still invincible is excused this long (1000 to 60000).
state.deadMoveMeters 5.0 Distance from where a character was first seen dead beyond which BAS-ST-020 no longer excuses it (above 0, at most 100; not applied when carried or a passenger in a vehicle that another ped drives).
state.reviveGraceMax 3 Dead episodes excused per window for BAS-ST-020 (1 to 20).
state.reviveGraceWindowMs 600000 Sliding window of state.reviveGraceMax (60000 to 3600000).
state.weaponIgnore { 'WEAPON_UNARMED' } Weapons never compared with the inventory by BAS-ST-001 and BAS-GE-060: names or integer hashes, at most 64. Fists are always excluded, whatever this list says. If Bastrel cannot compute the hash of one of the names (or of the fists), BAS-ST-001 and BAS-GE-060 both judge nothing (a warning is printed once). Add the parachute once you know its name on your build.
graces.joinMs 15000 No check while a player loads, counted from the load of the character with QBX (0 to 600000).
graces.bucketChangeMs 3000 No check after a routing bucket change (0 to 600000).
scoring.weights['BAS-ST-…'] 40, 40, 40, 30, 5, 10, 10 Weight of each code in the score once it is lifted.

A missing setting takes its default silently. An invalid value takes its default and a warning is printed at startup; Bastrel never stops for it.

The checks run inside the scheduler (sampler), spread over the interval, with 12 native reads per sample, plus one or two ox_inventory exports for an armed player on foot, and a few more reads (position, vehicle, driver seat, attachment of the character) while the framework reports a player as dead and invincible, which is the episode of BAS-ST-020. The cost is measured on a development server before any code is lifted.