Skip to content

Aim statistics: a clue for your staff, never a sanction

Bastrel keeps, for every player, the last 50 weapon impacts the clients declare (weaponDamageEvent), per weapon category, and measures a few things about them: how often the head is hit, how far the targets are, how fast the player changes target, and how many fatal hits he declares per minute. When several of these look inhuman together, it writes one line, BAS-AIM-001, for your staff to look at.

This is a rule of Bastrel, not a setting. On its own, the aim module produces a score and a staff alert, plus at most one screenshot sent with the alert (setting aim.screenshot), nothing else: it never issues a kick or a ban, whatever your configuration says (matrix, override, score threshold). A human reads the line and decides. One thing to know: its score adds to the player’s total like any other, so after about a dozen findings it can push the total past a kick threshold; that kick then stays pending and is applied by the next enforce detection of another module (spec section 12.3), never by aim. An aim statistic is a clue, not a proof: a very good player, a lucky streak or a sniper rifle can look like this.

Once BAS-AIM-001 is lifted, a finding asks for a screenshot of the player together with the staff alert, so that a human has an image to read next to the numbers. This is the aim.screenshot key (true by default; false keeps the alert alone). It never adds a kick or a ban, and a pending kick threshold stays pending. The image is only taken if the screenshots module is enabled and within its own limits (delay per player, pending requests); a screenshot is personal data and you are responsible for it. It is not available as a per-rule override: the aim rules refuse overrides. While the rule is unverified, nothing is asked.

BAS-AIM-001 ships as unverified: the distance reads the position of the victim, which nobody has yet read on a player who moves, and the “fatal hit” flag is a declaration by the client. So today:

  • the line appears in the log and in the detections table, with would_have empty;
  • no alert, no Discord message, no score, no kick, no ban;
  • it is lifted later, in a separate release, after a capture on a real server.

Nothing is judged out of the box: aim.categories and aim.thresholds ship empty, because no value is a measure. Fill them from what you see in your own observe logs.

aim = {
categories = { WEAPON_PISTOL = 'pistol', WEAPON_PUMPSHOTGUN = 'shotgun' }, -- weapon name or hash -> category
thresholds = {
pistol = { headRatio = 0.60, avgDistance = 60.0, minInterTargetMs = 250, killsPerMinute = 8.0 },
shotgun = { headRatio = 0.50, avgDistance = 40.0 }, -- a key you leave out switches its signal off
},
}

A weapon that is not in categories (the melee WEAPON_UNARMED is refused, by name or by hash) and a category without an entry in thresholds are never judged. A category needs at least aim.minSignals (2) keys in its row, or it can never produce a finding: Bastrel warns at startup when it does not (a row with a single key is a dead row).

On a full window (aim.minShots, 20 impacts or more), per category:

Signal True when
head the share of impacts at a head component (gameevents.headComponents) is at least headRatio
distance the mean distance shooter-victim is at least avgDistance metres
switch at least aim.minFastSwitches (3) changes of target happen less than minInterTargetMs apart, but not less than aim.batchMs (50 ms) apart
kills the impacts declared fatal (willKill) reach killsPerMinute (the minute is measured over at least aim.minSpanMs, 10 s)

BAS-AIM-001 is written when at least aim.minSignals (2) signals are true together. One signal alone is too common. The line carries the category, the measures and the signal names. After a finding the window is emptied and the player gets no other line for aim.alertCooldownMs (5 min, all categories together).

An impact counts only when the shooter is the attacker the server recognises, the event was not cancelled, the weapon is in categories, the damage declared is above 0, and the shot has exactly one victim other than the shooter’s own character, and that victim is a ped (a player or an NPC): a hit on a vehicle or an object is not counted. Nothing is counted while the module is inert.

  • A shot at a ped the shooter owns produces no event at all (a ped created by his own client): Bastrel does not see it. It sees shots at players and at peds owned by another client.
  • Fast target switches are timed by the moment the events reach the server, not by the moment of the shot. Two impacts that arrive less than aim.batchMs apart (a network batch after a lag, two pellets of a shotgun) are never a switch. The price: a real switch faster than batchMs is not seen.
  • The head, the target and the “fatal” flag come from the client’s declaration. A cheater who lies there lowers his own statistics; the module is a clue, and a human decides.
  • The distance is read after the impact, from the current positions of the two characters, not from where they were at the instant of the shot. A victim who moves fast adds some error.
  • The module does not use the graces of graces (loading, routing bucket): a shot is not a state of the player.

All in config/modules.lua, block aim. An invalid value takes its default with a warning at startup; Bastrel never stops for it.

Key Default Meaning
windowSize 50 impacts kept per player and category (10 to 200)
minShots 20 no judgement before this many impacts (5 to windowSize)
alertCooldownMs 300000 one finding per player in this window
minSignals 2 signals that must hold together (1 to 4)
minFastSwitches 3 fast changes of target needed for switch
minSpanMs 10000 shortest span used to count kills per minute
batchMs 50 impacts that arrive closer than this are never a target switch (0 to 1000)
screenshot true true: a finding also asks for a screenshot of the player, in addition to the staff alert (never a kick or a ban); false: alert only. See above
categories, thresholds empty see above