Appearance
Automation — the verb index
Every exported symbol of tools/autoscript.nim, extracted from source. This is the signature index; the narrative reference with failure modes, the wire format and the refusal table is the automation API.
If a signature here disagrees with one over there, this page is theone to believe — it was read out of the file, not typed.
autoscript -- the automation library. A TEST IS A NIMONY SCRIPT.
=========================================================================== WHAT THIS IS FOR
Testing a feature that only shows itself in a raid means: own the gear that triggers it, get into the right map on the right side, and be sure you really deployed. Doing that by hand -- buy an M4, three magazines, 180 rounds, walk the menu, remember to tick practice mode -- is why features get tested once.
So a test is a script. The script SAYS what it wants, and running it launches the whole stack, mints the gear, drives entry, and prints a verdict:
import autoscript
var s = newScript("woods-pmc-m4") onMap(s, "Woods") asSide(s, "pmc") equipWeapon(s, "FirstPrimaryWeapon", "5447a9cd4bdc2dbd208b4567", # M4A1 "55d4887d4bdc2d962f8b4570", # a STANAG in its mod_magazine "54527ac44bdc2d36668b4567") # loaded with M855A1 equip(s, "Headwear", "5aa7e276e5b5b000171d0647") # Altyn equip(s, "TacticalVest", "5ab8dced86f774646209ec87") # ANA M1 carryLoaded(s, "TacticalVest", "55d4887d4bdc2d962f8b4570", 3, "54527ac44bdc2d36668b4567") # 3 mags, loaded with M855A1 carry(s, "TacticalVest", "590c678286f77426c9660122", 2) # 2 IFAKs stock(s, "54527ac44bdc2d36668b4567", 180) # spare ammo runAndQuit(s)
runAndQuit(s) is the whole runner. It exits 0 PASS, 1 FAIL, 2 INCONCLUSIVE.
=========================================================================== THE VERDICT, AND WHY THERE ARE THREE OF THEM
PASS every declared item was READ BACK off the profile, and the host's own raid-phase latch said DEPLOYED. FAIL something measurable did not happen: gear was refused, or fewer items came back than were asked for, or the latch reported a phase other than DEPLOYED. INCONCLUSIVE the question was never asked. No install, no backend, no profile, the client never got far enough, or the raid-phase latch produced no observation at all.
A script that TRIES to enter a raid and never deploys is INCONCLUSIVE, never PASS. A run that could not look is not a pass (CLAUDE.md 9b). finish enforces that once, centrally, rather than trusting every step to. A script that never attempts a raid at all -- a gear-only test -- is not making a claim about deployment and is not failed for one; raidAttempted is what tells those two apart, and collapsing them would be a check that cannot pass.
=========================================================================== HOW "AM I DEPLOYED" IS DECIDED -- and how it is NOT
The ONLY trustworthy signal is the host's raid-phase latch (raidphase.nim). It arms on GameWorld cached + MainPlayer Unity-alive + MainPlayer in AllAlivePlayersList + Camera.main non-null, survives the MainPlayer null flicker (~3 frames in 4) and clears on SessionEndUIScene.
A raid-ENTRY log line is NOT proof of raid STATE. That mistake invalidated four experiments in one week, and this file therefore never treats enterraid.py exiting 0, a RegisterPlayer line, or a scene-load marker as deployment. It waits for the latch to SAY so.
The latch lives in the game process and this runner does not. The latch's one out-of-process observable is the host log line
natesp: raid phase = DEPLOYED -- LATCHED at tick ... [S1 ... S5 ...]
printed every 5s by natesp.nim. That line is gated: gNeDiag = gNeOn and readBoolKey("natEspDiag"), so BOTH natEsp and natEspDiag must be on, and debugEsp is what populates the GameWorld cache the latch reads. So armPhaseReporting writes those three keys into aowlspt-host.json BEFORE the client is launched (host flags are read at boot), and says it did.
If no raid phase = line ever appears, the verdict is INCONCLUSIVE naming the three flags -- NOT "you did not deploy". Those are different facts and collapsing them is how a broken instrument reads as a broken feature.
=========================================================================== WHY THE GEAR IS MINTED BY THE BACKEND AND NOT BY THIS PROCESS
mods/tarkov IS the SPT emulator and it owns the profile. emu/loadout.nim does the minting there, reusing the machinery that already exists: emu/spawn's name search (with its ambiguity refusal), emu/trading.giveItem for the stash, emu/grid for real cells in a worn container, and above all emu/bots.validateSlots -- the ancestry-aware slot validator -- and emu/bots.auditStacks.
A client-side mod could not do this even if it wanted to: serve() inside the game process registers into nothing, so a mod that reports over HTTP reports nowhere. The backend's plain-HTTP control port is the only channel a separate process has, and emu/loadout answers on it at /aowlspt/tarkov/autoscript/loadout.
=========================================================================== HOW THE GEAR CHECK CAN FAIL -- the falsifiable part
applyGear does not believe the mint. The server saves the profile, then RE-READS it off the store and counts, per request, how many items of that template are parented where the request asked. Five numbers come back and they are five different numbers: requested, baseline (how many were ALREADY there, read before anything was minted), minted, placed (after MINUS baseline) and rejected.
placed < requested is a FAIL, and it is reachable. Point equip at Headwear with a rifle template: validateSlots refuses it, minted goes to 1 and placed to 0, and the runner prints
GEAR FAIL asked for 1, minted 1, read back 0; 1 refusal(s) ! validateSlots: slot refused: 5447a9cd... -> Headwear on 55d7217a...
That is the input that makes this check fail. A check whose failing input cannot be named is not a check.
=========================================================================== THE STEPS, IF A SCRIPT WANTS THEM SEPARATELY
launchStack(s) start backend+client if nothing is running; wait for the backend's control port to answer pickProfile(s) choose the profile to mint into and play applyGear(s) POST the loadout; read the report back verifyGear(s) the read-back alone, mints nothing enterRaid(s) drive menu -> offline raid on the declared map waitDeployed(s, secs) wait for the raid-phase latch to say DEPLOYED finish(s) print the report, return the exit code run(s) all of the above, in order, stopping at the first step that does not PASS
Every step returns bool and every step that returns false has already written its reason into the script's log. None of them throw.
=========================================================================== ACTUATION -- AND WHICH VERBS ACTUALLY PROVE ANYTHING
The verbs are NOT equally strong, and this library refuses to pretend they are. Exactly two of them assert a FINISHED STATE:
walkAndSee pact re-reads EFT.Player::get_Position -- the game's own number -- and reports metres actually travelled. magCycle the completion line carries the game's own Magazine.Count.
Every other verb (look, fire, aim, sprint, jump, prone, pose, lean, command, reload) is ISSUE-ONLY: a PASS means THE HOST ACCEPTED THE CALL. pact has no readback for rotation, trigger, aim, stance or the ECommand channel today. An issue-only PASS that read like a verified one would be precisely the check that cannot fail, so the distinction is in the transcript, in the doc comments, and in actuated's INCONCLUSIVE.
The full reference -- every verb, its arguments, its refusals, the loadout wire format and the report's field meanings -- is docs/AUTOMATION-API.md.
Types
Verdict
nim
Verdict* = enum
vPass, vFail, vInconclusiveThe order matters: worst below takes the highest, and INCONCLUSIVE is deliberately WORSE than FAIL. A run that could not look must never be reported as a run that looked and found a fault -- that is a different and much more actionable thing.
tools/autoscript.nim:192
Side
nim
Side* = enum
sidePmc, sideScavtools/autoscript.nim:199
Script
nim
Script* = object
name*: string
root*: string
map*: string
side*: Side
clear*: bool ## strip the character before minting
launch*: bool ## start the stack, or attach to a running one
entryDriver*: string ## tools\enterraid.py, or "" to look beside the CWD
gear: seq[Gear]
# ---- state gathered as the run proceeds ----
port*: int
profileId*: string
profileName*: string
verdict*: Verdict
reason*: string
lines: seq[string]
gearRequested*: int
gearMinted*: int
gearPlaced*: int
phaseSeen*: string ## the last raid phase the host log reported
entered*: bool ## the latch said DEPLOYED at least once
steps*: int
## How many steps have reported. Zero means NOTHING RAN, which `finish`
## turns into INCONCLUSIVE. Without this counter a script that used the
## steps individually could never report PASS: `worse` is monotone, so a
## seed of INCONCLUSIVE is absorbing. Seeding PASS instead needs something
## to distinguish "everything passed" from "nothing was attempted", and
## this is it.
raidAttempted*: bool
actuation*: bool
## Whether this script intends to ACTUATE the player in raid. Arming is
## opt-in because it turns on three host flags (`playerActuation`,
## `liveInspector`, `liveInspectorWrite`) that are default-OFF by rule, and
## a script that only checks gear has no business turning them on.
ammoMode*: string
## The rung to put `mods/ammoloading` on before the launch, or "" to leave
## that mod exactly as the install has it.
inspSerial*: int
## Bumped into every inspector batch. The channel triggers on a CONTENT
## CHANGE, so two identical batches would run once; this is what makes the
## second one run.
actuations*: int ## pact requests this script sent
actuationsOk*: int ## of those, ones whose outcome line was observed
## Whether `enterRaid` ran. The never-deployed rule in `finish` applies
## only when it did: a script that deliberately tests gear alone and never
## goes near a raid is not making a claim about deployment, and failing it
## for one would be a check that cannot pass. A script that DOES try and
## never deploys is still INCONCLUSIVE, which is the rule that matters.tools/autoscript.nim:212
Constants
DefaultRoot
nim
DefaultRoot* = "D:\\Aowlspt"The live install. Overridable per script with atRoot.
tools/autoscript.nim:181
ScriptSession
nim
ScriptSession* = "000000000000000000000002"This runner's own session cookie. Deliberately NOT ProbeSession, so a backend log can tell a script's traffic from the launcher's probe.
tools/autoscript.nim:184
Verbs
newScript
nim
proc newScript*(name: string): ScriptA script with nothing declared. Defaults: the live install, Woods, PMC, strip the character first, launch the stack.
clear defaults TRUE on purpose. A test that runs against whatever the character happened to be wearing is a test whose result depends on the last run, and "the slot is already occupied" is the most common way a loadout silently does not arrive.
tools/autoscript.nim:265
atRoot
nim
proc atRoot*(s: var Script; root: string)The install to drive. Default D:\Aowlspt.
tools/autoscript.nim:291
onMap
nim
proc onMap*(s: var Script; map: string)The map, by the name the SELECT LOCATION screen displays -- "Woods", "Customs", "Factory", "Interchange", "Reserve", "Shoreline", "Lighthouse", "Streets of Tarkov", "Ground Zero", "The Lab".
tools/autoscript.nim:295
asSide
nim
proc asSide*(s: var Script; side: string)"pmc" or "scav". Anything else is refused at run time with a reason rather than folded into pmc -- a scav test that quietly ran as a PMC is a result that means nothing.
tools/autoscript.nim:301
keepGear
nim
proc keepGear*(s: var Script)Do NOT strip the character before minting. Use when a script is testing what a raid did to an existing loadout.
tools/autoscript.nim:307
withEntryDriver
nim
proc withEntryDriver*(s: var Script; path: string)Where enterraid.py is. Default: tools\enterraid.py relative to the working directory, i.e. the repo root.
tools/autoscript.nim:312
attachOnly
nim
proc attachOnly*(s: var Script)Do not launch anything; drive whatever is already running. Use when a human already has the client up.
tools/autoscript.nim:317
equip
nim
proc equip*(s: var Script; slot, tpl: string)Wear one item in one of the 14 equipment slots. A slot holds ONE thing; asking for more is refused by name rather than silently placing the first.
tools/autoscript.nim:325
equipLoaded
nim
proc equipLoaded*(s: var Script; slot, tpl, ammo: string)Wear it, and fill its cartridges with ammo to the capacity the TEMPLATE declares. Refused, with a reason, when the template's cartridge filter does not accept that round -- a magazine "filled" with ammo the filter omits is a gun that spawns empty.
FOR MAGAZINES AND OTHER CARTRIDGE-BEARING CONTAINERS, NOT WEAPONS. A weapon declares _props.Chambers, not _props.Cartridges (measured), holds no rounds directly, and needs a magazine mounted in mod_magazine. Pointing this verb at one is REFUSED by the backend with a reason that says the template is a chambered item and that no magazine was named -- which is the right refusal, but equipWeapon is the verb you wanted.
tools/autoscript.nim:331
equipWeapon
nim
proc equipWeapon*(s: var Script; slot, weapon, magazine, ammo: string)Wear a WEAPON with a magazine mounted in it and a round in the chamber.
This is what equipLoaded could never do, and the reason is a measured fact about the database, not a preference:
5447a9cd4bdc2dbd208b4567 (M4A1) declares _props.Chambers[0]._name = "patron_in_weapon" and NO _props.Cartridges. 55d4887d4bdc2d962f8b4570 (STANAG) declares _props.Cartridges and no slots of its own.
So a weapon holds no rounds directly. equipLoaded(s, slot, m4, ammo) asked the backend for a cartridge capacity the weapon does not have, was REFUSED with "the database declares no _props.Cartridges[0]._max_count", and -- even had it succeeded -- would still have mounted nothing in mod_magazine. That is why GetCurrentMagazine() read null in raid and why pactMagCycle had nothing to cycle. equipWeapon is the fix.
The magazine is NAMED, never guessed. The M4A1's mod_magazine filter admits 20 templates and "the first one" is an arbitrary document order -- the same guess this library refuses in byName. A magazine the filter excludes is REFUSED BEFORE MINTING, because a magazine the client drops on load saves to the profile, reads back, and still leaves the raid unarmed.
The acceptance is a walk of the finished state, not a re-read of the write: the server re-reads the saved profile, finds the item worn in slot, and counts its mod_magazine children and THEIR cartridges. magPlaced == 0 and roundsPlaced == 0 are separate FAILs, because "no magazine" and "an empty magazine" break a raid differently.
tools/autoscript.nim:346
carry
nim
proc carry*(s: var Script; container, tpl: string; count: int)count of an item in real cells of the container worn in that equipment slot -- "TacticalVest", "Backpack", "Pockets", "SecuredContainer".
Refused, never placed at 0,0, when there is no room: an overlapping item is drawn on top of what is already there and cannot be picked up, which looks exactly like never having been given it.
Equip the container EARLIER IN THE SCRIPT than the things that go in it. Requests are applied in declaration order, and "nothing is worn in TacticalVest" is the refusal you get otherwise -- it is named, not silent.
tools/autoscript.nim:378
carryLoaded
nim
proc carryLoaded*(s: var Script; container, tpl: string; count: int; ammo: string)count magazines in the container, each loaded with ammo. This is the "magazines = 3, ammo = m855a1" case.
tools/autoscript.nim:392
stock
nim
proc stock*(s: var Script; tpl: string; count: int)count of an item loose in the stash, stack-limit aware and merged into stacks already there. For spares the raid does not start with.
tools/autoscript.nim:399
byName
nim
proc byName*(s: var Script; query: string; count: int)Stash count of whatever the English item name query names. Convenient and deliberately strict: an AMBIGUOUS name is REFUSED and the candidates are listed, because "the first match" is an arbitrary document order and a test that equipped the wrong rifle proves nothing.
tools/autoscript.nim:405
wear
nim
proc wear*(s: var Script; slot, tpl: string; condition: int)equip with a durability/resource percentage, applied only to the properties the template actually declares.
tools/autoscript.nim:413
withActuation
nim
proc withActuation*(s: var Script)Declare that this script drives the PLAYER once the raid is DEPLOYED.
Arms three host flags before the launch, because all three are read at boot: playerActuation (pact itself), liveInspector and liveInspectorWrite (the file channel the request travels on, and its write gate). Without this, every actuation verb below refuses BY NAME rather than sitting silent.
tools/autoscript.nim:423
withAmmoLoading
nim
proc withAmmoLoading*(s: var Script; mode: string)Put mods/ammoloading on a rung of its own ladder before the launch: "off" / "probe" / "read" / "spawn" / "animate". probe is the rung that proves the trigger fires with zero dereferences and no bundle; the two rungs above it additionally need a bundle riding a vanilla key via mods/textures, which this library does not set up and does not pretend to.
tools/autoscript.nim:433
verdictName
nim
proc verdictName*(v: Verdict): stringtools/autoscript.nim:446
note
nim
proc note*(s: var Script; line: string)A script's own commentary, into the same transcript the runner prints.
tools/autoscript.nim:456
armPhaseReporting
nim
proc armPhaseReporting*(s: var Script): boolTurn on the three host flags that make the raid-phase latch OBSERVABLE from outside the game process, before the client boots.
debugEsp populates the GameWorld cache the latch reads natEsp gNeDiag = gNeOn and readBoolKey("natEspDiag") -- both natEspDiag halves are required, which is measured, not assumed
Host flags are read at boot, so this must happen BEFORE the launch. A failure here is reported and does not stop the run: the run then simply cannot conclude anything about deployment, and waitDeployed says exactly that instead of guessing.
tools/autoscript.nim:557
armActuation
nim
proc armActuation*(s: var Script): boolTurn on the three flags that make in-raid actuation REACHABLE from outside the game process, before the client boots.
playerActuation pact itself; without it every request is refused liveInspector the file channel the request travels on liveInspectorWrite the gate allow write needs; pact CALLS INTO GAME CODE, so it is deliberately on the write side
Returns false and SAYS WHY. A false here does not stop the run: the actuation steps then refuse by name, which is a better outcome than a run that silently never actuated and still passed.
tools/autoscript.nim:626
armAmmoLoading
nim
proc armAmmoLoading*(s: var Script): boolPut mods/ammoloading on the declared rung before the launch. Mod config is read at mod load, so this must happen first, exactly like a host flag.
tools/autoscript.nim:663
launchStack
nim
proc launchStack*(s: var Script): boolAttach to a running backend, or start aowlspt-launch.exe and wait for the backend's plain-HTTP control port to ANSWER.
Readiness is aowlsession.answers, which asks the tarkov mod's own selfcheck route. That is the right readiness: the next thing this runner does is ask that mod for profiles, and the backend only begins listening after every mod has loaded. A fixed sleep would be a guess, and the false crash (the client needs 60s+ to reach profile-select and then WAITS for a human) is exactly what a guess gets wrong.
tools/autoscript.nim:694
pickProfile
nim
proc pickProfile*(s: var Script): boolThe profile to mint into and play: the one played last, else the most recent. aowlsession.autoProfile decides, so a script and the launcher agree on which character "the profile" means.
tools/autoscript.nim:766
applyGear
nim
proc applyGear*(s: var Script): boolMint the declared loadout, then take the server's READ-BACK verdict.
This never asserts its own write. The server saves, re-reads the profile off the store, and counts what is really parented where the script asked. See the module header for the input that makes this FAIL.
tools/autoscript.nim:910
verifyGear
nim
proc verifyGear*(s: var Script): boolThe read-back alone: mints nothing, saves nothing, same counting. Use it after a raid to assert what survived.
tools/autoscript.nim:936
enterRaid
nim
proc enterRaid*(s: var Script): boolDrive the menu into an offline raid on the declared map.
tools/enterraid.py does the driving and is validated: it navigates by GameObject NAME plus a visible filter (menu captions live in DefaultUIButton._text, which findtext cannot see), it ticks practice mode -- without which the client runs ONLINE matchmaking and aborts with "a task was cancelled" -- and it goes inspector-silent during the load, because an inspector command stalls the Unity main thread and times matchmaking out. All of that is wrapped rather than reimplemented.
Its exit code is NOT the verdict. Entry is not state. This step passing means "the entry driver ran"; waitDeployed is what decides whether a raid actually happened.
tools/autoscript.nim:955
waitDeployed
nim
proc waitDeployed*(s: var Script; seconds: int): boolWait for the raid-phase LATCH to say DEPLOYED.
Three outcomes, and the third one is real:
PASS the host log carried raid phase = DEPLOYED FAIL it carried raid phase = <something else> and never DEPLOYED within the budget -- the latch looked and said no INCONCLUSIVE it carried NO raid phase = line at all, so the latch never reported. That is an instrument that did not run, not a raid that did not happen, and the two must not share a verdict.
tools/autoscript.nim:1003
pactDo
nim
proc pactDo*(s: var Script; request: string; seconds: int = 20): boolSend one pact request and wait for the host to say it ARRIVED.
PASS the host log carried pact-rpc: QUEUED "<request>" FAIL it carried pact-rpc: REFUSED -- the host looked at the request and declined it, and its own line says why INCONCLUSIVE neither line appeared. The request never reached pact: the inspector is off, the write gate is off, or the client is not draining. That is an instrument that did not run.
request is the pact sub-command verbatim -- "magcycle", "wait 60", "move 0 1 90", "look 5 0", "fire on", "reload", "status".
tools/autoscript.nim:1125
pactStatusLine
nim
proc pactStatusLine*(s: var Script; seconds: int = 15): stringpact's counter line, or "" when it did not answer. Diagnostic AND the basis of every effect assertion below.
tools/autoscript.nim:1214
actuated
nim
proc actuated*(s: var Script; request: string; settleSeconds: int = 6): boolIssue one pact request AND ASSERT IT TOOK EFFECT.
The assertion is on pact's own readback counters, which it computes from EFT.Player::get_Position -- the game's number, maintained by the game, that this library never writes. took rising is metres actually travelled past PactMinMoveM; noeffect rising is the call having been made and the world not having moved.
PASS took rose across the request FAIL noeffect or never rose -- the call was made (or dropped with a stated reason) and the finished state did not change INCONCLUSIVE the request was never accepted, pact never answered a status request, or neither counter moved. The last case is the honest one for a verb pact does not readback-check at all (look, fire, aim, lean): nothing was measured.
NOTE, and it is the point of this proc existing: only MOVEMENT has a readback in pact today. actuated(s, "look 5 0") will report INCONCLUSIVE "no counter moved", which is correct and is not a pass. Do not read a silent look as a look that happened.
tools/autoscript.nim:1229
walk
nim
proc walk*(s: var Script; strafe, forward: float; frames: int): boolEFT.Player::Move(Vector2), held for frames frames.
strafe is +right / -left, forward is +forward / -back; the game clamps the magnitude, so 1.0 is a full-speed input and 0.5 is a walk. frames is FRAMES, not seconds -- pact re-applies the held vector once per drain tick, because Move is an input sample and one call is one frame of walking.
ISSUE-ONLY. Use walkAndSee for the version that asserts metres travelled.
tools/autoscript.nim:1300
walkAndSee
nim
proc walkAndSee*(s: var Script; strafe, forward: float; frames: int; settleSeconds: int = 6): boolwalk, then assert the player really moved, via pact's get_Position readback. THIS is the movement verb a test should use: walk alone can report PASS with the player wedged against a wall.
tools/autoscript.nim:1311
look
nim
proc look*(s: var Script; dx, dy: float): boolEFT.Player::Rotate(Vector2, bool) -- a DELTA in degrees, not an absolute heading. +dx turns right, +dy looks down (the game's own sign convention at its call sites).
ISSUE-ONLY: pact has no rotation readback, so a PASS here means the host accepted the call. It does NOT mean the camera turned, and this library will not pretend otherwise.
tools/autoscript.nim:1318
fire
nim
proc fire*(s: var Script; on: bool): boolHold or release the trigger -- FirearmController::SetTriggerPressed.
REFUSED by pact, by name, unless Player::HasFirearmInHands() is true: on a knife or empty hands the FirearmController pointer is the abstract base and calling a FirearmController method on it is type confusion that no nil check catches. ISSUE-ONLY; the readback for "did it shoot" is the ammo count, not a pact counter.
tools/autoscript.nim:1328
aim
nim
proc aim*(s: var Script; on: bool): boolAim down sights -- FirearmController::SetAiming. Same firearm-in-hands refusal as fire. ISSUE-ONLY.
tools/autoscript.nim:1338
sprint
nim
proc sprint*(s: var Script; on: bool): boolEFT.Player::EnableSprint(bool). ISSUE-ONLY here, but note that the target table DOES carry get_IsSprintEnabled as its readback -- if a future pact uses it, this verb should become an actuated-style assertion rather than keeping a PASS that only means "accepted".
tools/autoscript.nim:1343
jump
nim
proc jump*(s: var Script): boolEFT.Player::Jump(). ISSUE-ONLY.
tools/autoscript.nim:1350
prone
nim
proc prone*(s: var Script): boolEFT.Player::ToggleProne(). A TOGGLE, not a set: calling it twice returns the player to where they started, which is a real way to write a test that proves nothing. ISSUE-ONLY.
tools/autoscript.nim:1354
pose
nim
proc pose*(s: var Script; delta: float): boolEFT.Player::ChangePose(float). Negative crouches, positive stands; the magnitude is a pose-level delta and NOT metres. ISSUE-ONLY.
tools/autoscript.nim:1360
lean
nim
proc lean*(s: var Script; dir: float): boolEFT.Player::ToggleLean(float) -- -1 left, 0 centre, +1 right, by the game's own convention at its call sites. ISSUE-ONLY.
tools/autoscript.nim:1365
command
nim
proc command*(s: var Script; code: int): boolSend a raw ECommand through the captured GamePlayerOwner. 16 is reload.
This channel is the one that can be UNAVAILABLE on its own: it needs the GamePlayerOwner::LateUpdate capture detour to have attached, and pact warns after 600 drain frames if it never did. When that happens, movement, look, fire, aim and the magazine cycle all still work -- so a FAIL here is specifically about the ECommand channel and must not be read as pact being broken.
tools/autoscript.nim:1370
reload
nim
proc reload*(s: var Script): boolcommand(16) by name. ISSUE-ONLY: it presses the reload key, and whether a magazine actually changed hands is magCycle's question, not this one.
tools/autoscript.nim:1381
pactWait
nim
proc pactWait*(s: var Script; frames: int): boolHold pact's queue for frames frames before the next request executes. Frames, not seconds: it is a queue entry, so it composes with the held movement vector rather than sleeping this process.
tools/autoscript.nim:1386
magCycle
nim
proc magCycle*(s: var Script; seconds: int = 60): boolTHE USER'S OWN EXAMPLE, with nobody at the keyboard: unload the magazine in the weapon and load the same ammunition back.
Four outcomes, each naming its own stage:
INCONCLUSIVE the request never reached pact (see pactDo) FAIL pact REFUSED it -- no firearm in hands, not deployed, a hop that did not validate. Nothing was issued. FAIL the cycle was ISSUED and did not complete -- the game's own Magazine.Count did not come back. A PARTIAL load counts here, on purpose. PASS pact magcycle: COMPLETE, which is that same count readback -- a number the game maintains and we never write.
tools/autoscript.nim:1392
assertAmmoLoadingFired
nim
proc assertAmmoLoadingFired*(s: var Script; seconds: int = 40): boolDid mods/ammoloading OBSERVE the cycle?
This is the negative-shaped end of the test and the only part that speaks for the mod. It asserts on the MOD'S OWN announcement, not on ours:
PASS ammoloading FIRST FIRING (or a heartbeat reading ARMED and FIRING) after the cycle FAIL the mod says ARMED but NEVER FIRED -- it was installed, it was watching, and the detour did not fire. THIS IS THE VERDICT THE WHOLE SCRIPT EXISTS TO BE ABLE TO PRODUCE. INCONCLUSIVE NOT ARMED, or nothing at all -- no hook is installed, so a zero counter says nothing about whether loading happened
tools/autoscript.nim:1429
pactStatusNote
nim
proc pactStatusNote*(s: var Script)Ask pact for its counter line and put it in the transcript. Diagnostic only: it changes no verdict, because a counter line is what you READ when a verdict has already been reached, not the verdict itself.
tools/autoscript.nim:1472
finish
nim
proc finish*(s: var Script): intPrint the transcript's conclusion and return the process exit code: 0 PASS, 1 FAIL, 2 INCONCLUSIVE.
The "never entered a raid" rule is enforced HERE, once, rather than trusted to every step: a script whose latch never said DEPLOYED cannot report PASS, whatever else went right.
tools/autoscript.nim:1494
runToRaid
nim
proc runToRaid*(s: var Script; deploySeconds: int = 420): boolEverything run does EXCEPT the verdict: stack, profile, gear, entry, and the raid-phase latch. Returns true only when the latch actually said DEPLOYED, so a script can put in-raid work after it without having to re-derive what "we are in a raid" means.
A script that uses this owns its own ending: call finish(s) and quitWith it, or the run reports nothing.
tools/autoscript.nim:1537
run
nim
proc run*(s: var Script): intEvery step in order, stopping at the first that does not PASS. Returns the exit code -- quit run(s) is the whole of a script's main.
tools/autoscript.nim:1551
verdictOf
nim
proc verdictOf*(s: Script): stringThe worst verdict any step has reported so far, as a word. For a script that inverts or branches on the outcome -- scripts/gear_must_fail.nim is the one that must.
tools/autoscript.nim:1561
reasonOf
nim
proc reasonOf*(s: Script): stringtools/autoscript.nim:1567
quitWith
nim
proc quitWith*(code: int)Exit with an explicit code, so a script need not import std/syncio for quit alone. 0 PASS, 1 FAIL, 2 INCONCLUSIVE -- and nothing else, because a fourth code would be a fourth outcome nothing knows how to read.
tools/autoscript.nim:1569
runAndQuit
nim
proc runAndQuit*(s: var Script)run and exit with its code. This exists so a script does not have to import std/syncio for quit alone -- the one-line main of every script is runAndQuit(s).
tools/autoscript.nim:1575

