Gate virtual-bullet spawn on rack admission: +68% tick rate, selected gun unchanged
The default rack is now Pattern-only (31c7c01), but membership filters SELECTION,
not SPAWNING - so all 13 unselected guns still ran `predict` + `spawnBullets`
every tick to feed fitness tables nobody reads. Measured waste: Tsetlin alone
0.98 ms/tick, KNN 0.30, plus 10 more. This generalises the gate TMPATTERN already
had to every gun, behind `TR_VBULLET_ADMIT_ONLY` (default 1 = gate, 0 = old).
OFFLINE COST (release build, 400 ticks, min of 2 reps, 13-gun rack):
OFF 2.14 ms/tick (implied 467 ticks/s)
ON 0.04 ms/tick (implied 27667 ticks/s)
-> reclaimed 2.10 ms/tick, ~98% of the virtual-bullet cost. Tsetlin's 0.98
disappears, KNN's 0.30 disappears, only Pattern (0.017) survives.
Note the absolute scale is lower than an earlier unoptimised measurement (~6.1
ms/t) because this is a -d:release build; the ON-vs-OFF DELTA is the robust result.
LIVE TICK RATE (ONE frozen binary, 4 runs/arm x 4 rounds, all 8 concurrent, gate
varied by env only):
ON 146.2 ticks/s (142.3, 145.2, 147.9, 149.4)
OFF 86.9 ticks/s ( 95.0, 41.4, 99.1, 112.1)
NO OVERLAP: ON min 142.3 > OFF max 112.1. Excluding a game-outcome outlier in
the OFF arm, OFF max is still 112.1. Outside the noise.
**+68% tick rate**, which also makes every future A/B faster. Both arms still pay
the fixed 8192-slot ring scan in tickBullets.
SAFETY, verified not assumed: Pattern is admitted under the shipped rack, so its
own fitness keeps accumulating and the SELECTED gun is unchanged - Pattern 100% in
all 4 runs both arms, and Pattern was the ONLY gun with vShots>0 in the ON arm
while all 13 had vShots>0 in the OFF arm. So admission is the correct predicate.
Mid-round transitions are safe by construction: only predict/spawn are gated, while
tickBullets still resolves every active bullet and the feedback case still calls
the owning gun's onResult.
A TOOLING BUG THIS CAUGHT, and a correction to the task's assumption:
`acceptance_offline_vs_online.nim` IS affected (I had assumed it was not). It
compares the live per-gun vShots against an offline replay that always spawns all
guns, so under the default gate the online non-Pattern vShots are 0 while offline
is ~400 - a guaranteed mismatch. Fixed by pinning `TR_VBULLET_ADMIT_ONLY=0` inside
that test (same putEnv/defer pattern as TR_RECORD_WORLDSTATE), keeping 12/12. The
test is about offline/online METRIC parity, so it needs every gun spawning.
Unaffected (verified from source): audit_virtual_guns.nim,
measure_cornering_guns.nim, sweep_tm_pattern.nim - all offline, none read
GUN_STATS_PATH.
Guards: test_vbullet_admit_gate 12 (new, pure), test_gun_harness 39,
test_vbullet_metric 11, test_power_selection 3, test_adaptive_radar 41,
test_tfil_ring_weights 24, test_power_policy 26, test_ram_decision 28,
test_rack_membership 48, test_selector_tiebreak 19, test_tm_pattern_registration 20,
test_tm_pattern_learning 3, acceptance_offline_vs_online 12/12. ModularBot compiles.
This commit is contained in:
@@ -230,6 +230,24 @@ proc admittedGuns*(fitLen: int, mode: RackMode,
|
||||
if result.len == 0:
|
||||
for gunId in 0..<fitLen: result.add gunId
|
||||
|
||||
proc vBulletAdmitted*(gunId: int, mode: RackMode,
|
||||
membership: openArray[RackMembership],
|
||||
admitOnly: bool = true): bool =
|
||||
## `TR_VBULLET_ADMIT_ONLY` gate for the LIVE predict+spawn block.
|
||||
##
|
||||
## Rack membership historically filtered only SELECTION, so every unselected
|
||||
## gun still ran `predict`/`spawnBullets` each tick to feed a fitness table the
|
||||
## selector would never read. This predicate lets the live loop skip BOTH for a
|
||||
## gun the current rack does not admit.
|
||||
##
|
||||
## admitOnly = true (SHIPPED default) — a gun spawns only while admitted.
|
||||
## admitOnly = false — every gun spawns (the pre-change behaviour).
|
||||
##
|
||||
## `onResult` feedback is deliberately NOT covered here: a bullet spawned
|
||||
## while admitted must still resolve and be attributed to its gun even if the
|
||||
## rack changes mid-round.
|
||||
(not admitOnly) or rackAdmitted(gunId, mode, membership)
|
||||
|
||||
# ── runtime ranking knobs (A/B without rebuilding) ────────────────────────────
|
||||
#
|
||||
# Every knob below defaults to the SHIPPED constant, so an unset environment
|
||||
|
||||
@@ -69,6 +69,15 @@ proc main() =
|
||||
putEnv("TR_RECORD_WORLDSTATE", "1")
|
||||
defer: delEnv("TR_RECORD_WORLDSTATE")
|
||||
|
||||
# This test validates PER-GUN offline==online parity, so it needs the FULL
|
||||
# virtual-bullet rack online. `TR_VBULLET_ADMIT_ONLY=1` (shipped) skips
|
||||
# predict+spawn for every non-admitted gun, so under the shipped onlyPattern
|
||||
# rack their online `vShots` would be 0 while the offline range still spawns
|
||||
# all of them — a guaranteed MISMATCH. Pin the gate OFF for this battle so the
|
||||
# live loop spawns every gun and the per-gun comparison stays well-defined.
|
||||
putEnv("TR_VBULLET_ADMIT_ONLY", "0")
|
||||
defer: delEnv("TR_VBULLET_ADMIT_ONLY")
|
||||
|
||||
echo "=== live battle: ModularBot vs OscillatorBot, 1 round, max speed ==="
|
||||
let battle = runBattle(@[modularBotDir, adversaryDir], rounds = 1,
|
||||
timeout = 240000, maxSpeed = true)
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
## MEASUREMENT ONLY — per-tick cost of the virtual-bullet SPAWN gate
|
||||
## (`TR_VBULLET_ADMIT_ONLY`), on the REAL 14-gun rack over recorded DrussGT
|
||||
## movement.
|
||||
##
|
||||
## Gating SKIPS both `predict()` and `spawnBullets()` for a gun the current rack
|
||||
## does not admit (shipped default rack = onlyPattern). This tool drives the same
|
||||
## real guns + real `VirtualTracker` as `measure_parallel_vbullet_cost.nim`, but
|
||||
## reports the ONE comparison the live bot cares about:
|
||||
##
|
||||
## OFF = pre-change: every non-TM gun predicts+spawns (ids 0..12)
|
||||
## ON = shipped: only the rack-admitted guns predict+spawn (Pattern, id 5)
|
||||
##
|
||||
## Both arms run the SAME 13-gun rack (ids 0..12; TMSelect is disabled in the
|
||||
## live bot via `EnableTmSelector=false`, TMPATTERN id 14 is separately gated).
|
||||
## The gate predicate is the SHIPPED `vb.vBulletAdmitted` against the SHIPPED
|
||||
## `DefaultRackMembership`, so this measures the real rule.
|
||||
##
|
||||
## Measured directly (not derived): predict+spawn wall time and the full
|
||||
## `tickBullets` (resolve + onResult) wall time, per tick, for each arm. A
|
||||
## per-gun breakdown names which costs disappear.
|
||||
##
|
||||
## Usage:
|
||||
## nim c -r common_libs/tests/measure_vbullet_admit_gate.nim
|
||||
|
||||
import std/[os, strformat, times, tables, math, monotimes, algorithm, random]
|
||||
import gun_harness/offline_range
|
||||
import gun_harness/gun_interface
|
||||
import gun_harness/virtual_bullets as vb
|
||||
import gun_harness/selector
|
||||
import range_guns
|
||||
|
||||
const
|
||||
NGun = 14
|
||||
NBins = len(vb.PowerBins)
|
||||
TmGun = 13 ## TMSelect — disabled in the live bot
|
||||
RepoRoot = currentSourcePath().parentDir.parentDir.parentDir
|
||||
FixturesDir = RepoRoot / "tools" / "fixtures"
|
||||
FixtureNames = [
|
||||
"tr_drussgt_vs_corners.jsonl",
|
||||
"tr_drussgt_vs_crazy.jsonl",
|
||||
"tr_drussgt_vs_modularbot.jsonl",
|
||||
"tr_drussgt_vs_modularbot_shield.jsonl",
|
||||
"tr_drussgt_vs_spinbot.jsonl",
|
||||
"drussgt_vs_ramfire.jsonl",
|
||||
"drussgt_vs_drussgt.jsonl",
|
||||
]
|
||||
MaxTicks = 2000
|
||||
Warmup = 150
|
||||
Measure = 400
|
||||
|
||||
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor",
|
||||
"Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead",
|
||||
"DecayGF", "KNN", "TMSelect"]
|
||||
|
||||
proc loadFixtures(): seq[Fixture] =
|
||||
for n in FixtureNames:
|
||||
let p = FixturesDir / n
|
||||
if fileExists(p): result.add loadFixture(p)
|
||||
|
||||
proc buildStates(fx: seq[Fixture], maxTicks: int): seq[seq[WorldState]] =
|
||||
var ticks = maxTicks
|
||||
for f in fx: ticks = min(ticks, f.states.len)
|
||||
result = newSeq[seq[WorldState]](fx.len)
|
||||
for k in 0..<fx.len:
|
||||
result[k] = newSeq[WorldState](ticks)
|
||||
for si in 0..<ticks:
|
||||
var ws = fx[k].states[si]
|
||||
let s0 = fx[0].states[si]
|
||||
ws.selfX = s0.selfX; ws.selfY = s0.selfY
|
||||
ws.selfHeading = s0.selfHeading; ws.selfSpeed = s0.selfSpeed
|
||||
ws.selfRadarHeading = s0.selfRadarHeading; ws.selfEnergy = s0.selfEnergy
|
||||
ws.tick = si
|
||||
result[k][si] = ws
|
||||
|
||||
proc enemyTable(states: seq[seq[WorldState]], si: int):
|
||||
Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]] =
|
||||
for k in 0..<states.len:
|
||||
let e = states[k][si]
|
||||
result[k + 1] = (x: e.enemyX, y: e.enemyY, lastSeenTick: si, alive: true)
|
||||
|
||||
type
|
||||
ArmResult = object
|
||||
ticks: int
|
||||
psNs: int64 ## predict+spawn wall time
|
||||
resNs: int64 ## full tickBullets (resolve + onResult) wall time
|
||||
spawns: int
|
||||
|
||||
proc runArm(states: seq[seq[WorldState]], gateOn: bool): ArmResult =
|
||||
## Drive the real 13-gun live rack (ids 0..12) with the shipped gate.
|
||||
let rack = buildAllGunDrivers(seed = 1, enableTmSelector = false)
|
||||
var tracker = vb.initTracker(NGun, bmPath)
|
||||
let ticks = states[0].len
|
||||
for si in 0..<ticks:
|
||||
let ws = states[0][si]
|
||||
let measuring = si >= Warmup and si < Warmup + Measure
|
||||
let t0 = getMonoTime()
|
||||
for gi in 0..<NGun:
|
||||
if gi == TmGun: continue # disabled in the live bot
|
||||
# The SHIPPED gate predicate against the SHIPPED rack.
|
||||
if not vb.vBulletAdmitted(gi, rm1v1, DefaultRackMembership, gateOn):
|
||||
continue
|
||||
var preds: array[NBins, GunPrediction]
|
||||
for b in 0..<NBins:
|
||||
preds[b] = rack[gi].predictCb(ws, bulletSpeed(vb.PowerBins[b]))
|
||||
tracker.spawnBullets(gi, preds, ws, 1)
|
||||
if measuring: inc result.spawns, NBins
|
||||
let t1 = getMonoTime()
|
||||
let et = enemyTable(states, si)
|
||||
tracker.tickBullets(ws, et,
|
||||
proc(gunId: GunId, binIdx: int, e: FeedbackEvent) =
|
||||
rack[gunId].resultCb(e))
|
||||
let t2 = getMonoTime()
|
||||
if measuring:
|
||||
inc result.ticks
|
||||
result.psNs += (t1 - t0).inNanoseconds
|
||||
result.resNs += (t2 - t1).inNanoseconds
|
||||
|
||||
proc perGunCosts(states: seq[seq[WorldState]]):
|
||||
tuple[pred: array[NGun, float], onRes: array[NGun, float]] =
|
||||
## Per-gun predict+spawn ns/tick and onResult ns/tick (always FULL rack).
|
||||
let rack = buildAllGunDrivers(seed = 1, enableTmSelector = false)
|
||||
var tracker = vb.initTracker(NGun, bmPath)
|
||||
var counts: array[NGun, int]
|
||||
var onCalls: array[NGun, int]
|
||||
var acc: tuple[pred: array[NGun, float], onRes: array[NGun, float]]
|
||||
let ticks = states[0].len
|
||||
for si in 0..<ticks:
|
||||
let ws = states[0][si]
|
||||
let measuring = si >= Warmup and si < Warmup + Measure
|
||||
for gi in 0..<NGun:
|
||||
if gi == TmGun: continue
|
||||
let t0 = getMonoTime()
|
||||
var preds: array[NBins, GunPrediction]
|
||||
for b in 0..<NBins:
|
||||
preds[b] = rack[gi].predictCb(ws, bulletSpeed(vb.PowerBins[b]))
|
||||
tracker.spawnBullets(gi, preds, ws, 1)
|
||||
let dt = (getMonoTime() - t0).inNanoseconds
|
||||
if measuring:
|
||||
acc.pred[gi] += dt.float
|
||||
inc counts[gi]
|
||||
let et = enemyTable(states, si)
|
||||
tracker.tickBullets(ws, et,
|
||||
proc(gunId: GunId, binIdx: int, e: FeedbackEvent) =
|
||||
let t0 = getMonoTime()
|
||||
rack[gunId].resultCb(e)
|
||||
let dt = (getMonoTime() - t0).inNanoseconds
|
||||
if measuring:
|
||||
acc.onRes[gunId] += dt.float
|
||||
inc onCalls[gunId])
|
||||
for gi in 0..<NGun:
|
||||
if counts[gi] > 0: acc.pred[gi] /= counts[gi].float
|
||||
if onCalls[gi] > 0: acc.onRes[gi] /= onCalls[gi].float
|
||||
result = acc
|
||||
|
||||
proc main() =
|
||||
randomize(1)
|
||||
let fx = loadFixtures()
|
||||
let states = buildStates(fx, MaxTicks)
|
||||
echo fmt"fixtures={fx.len} ticks={states[0].len} warmup={Warmup} measured={Measure}"
|
||||
echo "shipped rack: onlyPattern (DefaultRackMembership); predicate vb.vBulletAdmitted"
|
||||
|
||||
# Two reps, take the min (least noise).
|
||||
var offPs, offRes, onPs, onRes: int64
|
||||
var offSpawns, onSpawns, ticksRun: int
|
||||
for rep in 0..<2:
|
||||
let off = runArm(states, gateOn = false)
|
||||
let on = runArm(states, gateOn = true)
|
||||
if rep == 0:
|
||||
offPs = off.psNs; offRes = off.resNs; offSpawns = off.spawns
|
||||
onPs = on.psNs; onRes = on.resNs; onSpawns = on.spawns
|
||||
ticksRun = off.ticks
|
||||
else:
|
||||
offPs = min(offPs, off.psNs); offRes = min(offRes, off.resNs)
|
||||
onPs = min(onPs, on.psNs); onRes = min(onRes, on.resNs)
|
||||
offSpawns = min(offSpawns, off.spawns); onSpawns = min(onSpawns, on.spawns)
|
||||
|
||||
let n = max(1, ticksRun).float
|
||||
let offPSms = offPs.float / 1.0e6 / n # ms/tick
|
||||
let offRSms = offRes.float / 1.0e6 / n
|
||||
let onPSms = onPs.float / 1.0e6 / n
|
||||
let onRSms = onRes.float / 1.0e6 / n
|
||||
let offTot = offPSms + offRSms
|
||||
let onTot = onPSms + onRSms
|
||||
|
||||
echo ""
|
||||
echo "=================== PER-TICK COST: gate ON vs OFF (13-gun live rack) ==================="
|
||||
echo "arm predict+spawn ms/t resolve+onResult ms/t total ms/t ticks/s spawns/t"
|
||||
echo fmt"OFF {offPSms:>20.2f} {offRSms:>22.2f} {offTot:>12.2f} {1000.0/offTot:>7.0f} {offSpawns.float/n:>10.1f}"
|
||||
echo fmt"ON {onPSms:>20.2f} {onRSms:>22.2f} {onTot:>12.2f} {1000.0/onTot:>7.0f} {onSpawns.float/n:>10.1f}"
|
||||
echo fmt"reclaimed = {offTot - onTot:.2f} ms/tick ({100.0*(offTot-onTot)/offTot:.0f}% of the gate-OFF total)"
|
||||
echo fmt"implied ticks/s: OFF {1000.0/offTot:.0f} -> ON {1000.0/onTot:.0f} (x{offTot/onTot:.2f})"
|
||||
echo "(note: tickBullets also scans the fixed 8192-slot ring each tick, present in BOTH arms)"
|
||||
|
||||
let pg = perGunCosts(states)
|
||||
echo ""
|
||||
echo "=================== PER-GUN COST (full rack, names what disappears) ==================="
|
||||
echo "gun predict+spawn ms/t onResult ms/t gateON"
|
||||
var sumPred, sumOn: float
|
||||
for gi in 0..<NGun:
|
||||
if gi == TmGun: continue
|
||||
sumPred += pg.pred[gi]; sumOn += pg.onRes[gi]
|
||||
let admitted = vb.vBulletAdmitted(gi, rm1v1, DefaultRackMembership, true)
|
||||
echo fmt"{GunNames[gi]:<12}{pg.pred[gi]/1.0e6:>24.3f}{pg.onRes[gi]/1.0e6:>18.3f}{($admitted):>10}"
|
||||
echo fmt"TOTAL {sumPred/1.0e6:>24.3f}{sumOn/1.0e6:>18.3f}"
|
||||
echo ""
|
||||
|
||||
when isMainModule:
|
||||
main()
|
||||
@@ -0,0 +1,99 @@
|
||||
## Pure unit guard for the `TR_VBULLET_ADMIT_ONLY` spawn-gate predicate.
|
||||
##
|
||||
## No Java, no battle, no virtual bullets: `vBulletAdmitted` is a pure function of
|
||||
## (gunId, rack mode, membership table, admitOnly) over the SHIPPED
|
||||
## `DefaultRackMembership`, so the live gate can be pinned without running the
|
||||
## bot. Covers:
|
||||
## * the shipped default rack admits Pattern (5) and nothing else;
|
||||
## * `admitOnly=false` (TR_VBULLET_ADMIT_ONLY=0) restores "spawn for every gun";
|
||||
## * mode-restricted membership (`rmOnly1v1` / `rmOnlyMelee`) flips with mode;
|
||||
## * an empty membership table filters nothing (pre-change callers);
|
||||
## * out-of-range ids are admitted, negative ids are not;
|
||||
## * the TMPATTERN special case: forcing `admitOnly=true` keeps gun 14 gated
|
||||
## even when the knob is off — the exact composition the live loop uses.
|
||||
##
|
||||
## Run with plain:
|
||||
## nim c -r common_libs/tests/test_vbullet_admit_gate.nim
|
||||
|
||||
import gun_harness/virtual_bullets
|
||||
import gun_harness/selector
|
||||
|
||||
var failures = 0
|
||||
proc check(name: string, ok: bool) =
|
||||
if ok: echo "PASS: ", name
|
||||
else: echo "FAIL: ", name; inc failures
|
||||
|
||||
proc testShippedDefaultRack() =
|
||||
## The shipped onlyPattern table: Pattern (5) admitted in both modes, every
|
||||
## other registered gun (0..14) off.
|
||||
check "default: Pattern admitted in 1v1",
|
||||
vBulletAdmitted(5, rm1v1, DefaultRackMembership, true)
|
||||
check "default: Pattern admitted in melee",
|
||||
vBulletAdmitted(5, rmMelee, DefaultRackMembership, true)
|
||||
var allOthersOff = true
|
||||
for gi in 0..<len(DefaultRackMembership):
|
||||
if gi == 5: continue
|
||||
if vBulletAdmitted(gi, rm1v1, DefaultRackMembership, true): allOthersOff = false
|
||||
if vBulletAdmitted(gi, rmMelee, DefaultRackMembership, true): allOthersOff = false
|
||||
check "default: every other gun (incl. TMPATTERN 14) is NOT admitted",
|
||||
allOthersOff
|
||||
|
||||
proc testKnobOffRestoresFullRack() =
|
||||
## TR_VBULLET_ADMIT_ONLY=0: the gate is a no-op for the general guns, i.e.
|
||||
## every id spawns regardless of rack membership.
|
||||
var allAdmitted = true
|
||||
for gi in 0..<len(DefaultRackMembership):
|
||||
if not vBulletAdmitted(gi, rm1v1, DefaultRackMembership, false): allAdmitted = false
|
||||
if not vBulletAdmitted(gi, rmMelee, DefaultRackMembership, false): allAdmitted = false
|
||||
check "knob=0: vBulletAdmitted is true for EVERY gun in both modes", allAdmitted
|
||||
|
||||
proc testModeRestrictedMembership() =
|
||||
# 0: both, 1: only1v1, 2: onlyMelee, 3: off
|
||||
let mem = [rmBoth, rmOnly1v1, rmOnlyMelee, rmOff]
|
||||
check "1v1: both + only1v1 admitted, onlyMelee/off not",
|
||||
vBulletAdmitted(0, rm1v1, mem, true) and
|
||||
vBulletAdmitted(1, rm1v1, mem, true) and
|
||||
(not vBulletAdmitted(2, rm1v1, mem, true)) and
|
||||
(not vBulletAdmitted(3, rm1v1, mem, true))
|
||||
check "melee: both + onlyMelee admitted, only1v1/off not",
|
||||
vBulletAdmitted(0, rmMelee, mem, true) and
|
||||
vBulletAdmitted(2, rmMelee, mem, true) and
|
||||
(not vBulletAdmitted(1, rmMelee, mem, true)) and
|
||||
(not vBulletAdmitted(3, rmMelee, mem, true))
|
||||
check "knob=0 overrides even rmOff / mode restrictions",
|
||||
vBulletAdmitted(3, rm1v1, mem, false) and
|
||||
vBulletAdmitted(1, rmMelee, mem, false)
|
||||
|
||||
proc testEmptyAndOutOfRange() =
|
||||
let empty: seq[RackMembership] = @[]
|
||||
check "empty membership filters nothing (pre-change callers)",
|
||||
vBulletAdmitted(0, rm1v1, empty, true) and
|
||||
vBulletAdmitted(13, rmMelee, empty, true)
|
||||
let mem = [rmOff, rmOff]
|
||||
check "id past the membership table is admitted",
|
||||
vBulletAdmitted(7, rm1v1, mem, true)
|
||||
check "negative id is not admitted when the gate is on",
|
||||
(not vBulletAdmitted(-1, rm1v1, mem, true))
|
||||
|
||||
proc testTmPatternStaysGatedWhenKnobOff() =
|
||||
## The live loop composes the TMPATTERN gate as
|
||||
## vBulletAdmitted(14, mode, mem, VBulletAdmitOnly or id == TmPatternId)
|
||||
## so gun 14 stays gated even when TR_VBULLET_ADMIT_ONLY=0.
|
||||
let knobOff = false
|
||||
for mode in [rm1v1, rmMelee]:
|
||||
let composed = vBulletAdmitted(14, mode, DefaultRackMembership,
|
||||
knobOff or 14 == 14)
|
||||
let alwaysGated = vBulletAdmitted(14, mode, DefaultRackMembership, true)
|
||||
check "knob=0 keeps TMPATTERN (14) gated exactly as when gated",
|
||||
composed == alwaysGated and composed == false
|
||||
|
||||
testShippedDefaultRack()
|
||||
testKnobOffRestoresFullRack()
|
||||
testModeRestrictedMembership()
|
||||
testEmptyAndOutOfRange()
|
||||
testTmPatternStaysGatedWhenKnobOff()
|
||||
|
||||
if failures > 0:
|
||||
echo "\n", failures, " check(s) FAILED"
|
||||
quit(1)
|
||||
echo "\nAll vbullet-admit-gate checks passed."
|
||||
Reference in New Issue
Block a user