Skip to content

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, vInconclusive

The 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, sideScav

tools/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): Script

A 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): string

tools/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): bool

Turn 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): bool

Turn 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): bool

Put 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): bool

Attach 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): bool

The 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): bool

Mint 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): bool

The 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): bool

Drive 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): bool

Wait 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): bool

Send 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): string

pact'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): bool

Issue 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): bool

EFT.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): bool

walk, 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): bool

EFT.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): bool

Hold 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): bool

Aim 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): bool

EFT.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): bool

EFT.Player::Jump(). ISSUE-ONLY.

tools/autoscript.nim:1350

prone

nim
proc prone*(s: var Script): bool

EFT.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): bool

EFT.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): bool

EFT.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): bool

Send 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): bool

command(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): bool

Hold 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): bool

THE 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): bool

Did 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): int

Print 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): bool

Everything 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): int

Every 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): string

The 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): string

tools/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

aoughwl — self-hosted platform for things n stuff. Contact / Support on Discord for access to the private backends.