Skip to content

Automation — the reference scripts

The scripts in scripts/ of the aowlspt repo, reproduced in full and verbatim. They are the working examples the library is exercised against, so they are the ones that are known to compile.

scripts/actuation_tour.nim

nim
## actuation_tour -- every player-actuation verb, in one run, each with its own
## three-valued verdict.
##
##     .\installer\build\aowl.exe script scripts\actuation_tour.nim
##
## This is the reference script for `docs/PLAYER_ACTUATION.md`: it exercises the
## whole surface -- gear, entry, movement, look, stance, aim, fire, reload, and
## the magazine unload/load cycle -- with nobody at the keyboard.
##
## WHAT IT PROVES, AND WHAT IT DOES NOT
## ------------------------------------
## Read this before believing a green run. The verbs are NOT equally strong, and
## the library refuses to pretend they are.
##
##   walkAndSee   PROVES movement. pact re-reads `EFT.Player::get_Position` --
##                the game's own number -- and reports metres actually travelled.
##                `took` rising is the finished state having changed.
##   magCycle     PROVES a magazine cycle. The completion line carries the game's
##                own `Magazine.Count` readback, which this library never writes.
##   everything   ISSUE-ONLY. pact has no readback for rotation, trigger, aim,
##   else         stance or the ECommand channel today, so a PASS on `look`,
##                `fire`, `aim`, `pose`, `lean`, `prone`, `jump`, `sprint`,
##                `command` or `reload` means THE HOST ACCEPTED THE CALL and
##                nothing more. It is not evidence the camera turned or the gun
##                fired.
##
## That asymmetry is deliberately visible in the transcript rather than smoothed
## over: an issue-only PASS that read like a verified one would be precisely the
## check that cannot fail.
##
## `actuated(s, "look 15 0")` will therefore report INCONCLUSIVE, "no readback
## counter moved". That is the correct answer -- nothing was measured -- and it
## is the honest thing to see in a report until pact grows a rotation readback.
##
## TEMPLATE IDS -- pasted, not searched; see woods_pmc_m4.nim for why.
##   5447a9cd4bdc2dbd208b4567  Colt M4A1 5.56x45 assault rifle
##   55d4887d4bdc2d962f8b4570  Colt M4A1 STANAG 30-round magazine
##   54527ac44bdc2d36668b4567  5.56x45mm M855A1
##   5ab8dced86f774646209ec87  ANA Tactical M1 armored rig

import autoscript

proc main() =
  var s = newScript("actuation-tour")

  onMap(s, "Factory")     # smallest map, shortest load
  asSide(s, "pmc")
  withActuation(s)        # arms playerActuation + liveInspector + write, at BOOT

  # Declaration order matters: the rig must exist before anything goes in it.
  equip(s, "TacticalVest", "5ab8dced86f774646209ec87")
  equipWeapon(s, "FirstPrimaryWeapon", "5447a9cd4bdc2dbd208b4567",
              "55d4887d4bdc2d962f8b4570", "54527ac44bdc2d36668b4567")
  carryLoaded(s, "TacticalVest", "55d4887d4bdc2d962f8b4570", 2,
              "54527ac44bdc2d36668b4567")
  stock(s, "54527ac44bdc2d36668b4567", 120)

  if runToRaid(s):
    # Let the deploy animation finish. A verb issued while the weapon is still
    # being drawn refuses at HasFirearmInHands -- a TIMING answer wearing a
    # capability answer's clothes.
    discard pactWait(s, 300)
    pactStatusNote(s)

    # ---- movement: the one thing that is genuinely VERIFIED ---------------
    # Forward at full input for 90 frames, then assert metres travelled.
    discard walkAndSee(s, 0.0, 1.0, 90)
    # Strafe back, so the tour does not walk the character off the map.
    discard walkAndSee(s, 0.0, -1.0, 90)

    # ---- look / stance / weapon handling: ISSUE-ONLY ----------------------
    # Each of these reports PASS on ACCEPTANCE. Do not read them as effects.
    discard look(s, 30.0, 0.0)
    discard look(s, -30.0, 0.0)
    discard pose(s, -1.0)          # crouch
    discard pose(s, 1.0)           # stand back up
    discard lean(s, 1.0)
    discard lean(s, 0.0)
    discard sprint(s, true)
    discard sprint(s, false)
    discard aim(s, true)
    discard aim(s, false)

    # ---- the trigger -----------------------------------------------------
    # Held, then RELEASED. A `fire on` with no matching `fire off` leaves the
    # trigger down for the rest of the raid, which corrupts every later verb.
    discard fire(s, true)
    discard pactWait(s, 20)
    discard fire(s, false)

    # ---- THE USER'S OWN EXAMPLE ------------------------------------------
    # Unload the magazine and load the ammunition back, with nobody at the
    # keyboard. Verified: the completion line carries the game's own
    # Magazine.Count.
    discard magCycle(s, 60)

    # The reload KEY is a different channel (ECommand through the captured
    # GamePlayerOwner) and can be unavailable on its own. Issue-only.
    discard reload(s)

    pactStatusNote(s)

  quitWith(finish(s))

main()

scripts/ammoloading_e2e.nim

nim
## ammoloading_e2e -- THE WHOLE POINT, in one file.
##
##     .\installer\build\aowl.exe script scripts\ammoloading_e2e.nim
##
## "I shouldn't have to unload the magazine and reload the ammo for something to
## trigger for you to observe." This script does that, with nobody at the
## keyboard: it mints a weapon and a loaded magazine, enters an offline raid,
## waits for the raid-phase LATCH to say DEPLOYED, unloads and reloads the
## magazine through `pact`, and then asks `mods/ammoloading` whether it SAW it.
##
## WHAT EACH VERDICT MEANS, AND WHY THEY MUST BE DIFFERENT
## -------------------------------------------------------
## Every stage names itself, because "it didn't work" is four different problems:
##
##   STACK  INCONCLUSIVE   the backend never came up; nothing was tested
##   GEAR   FAIL           the weapon or magazine did not read back after
##                         minting -- there was nothing to cycle. Two distinct
##                         sub-cases, and they are NOT collapsed:
##                           magPlaced=0    no magazine is in the weapon's
##                                          mod_magazine slot on the saved
##                                          profile; GetCurrentMagazine() will
##                                          read null in raid
##                           roundsPlaced=0 the magazine IS mounted and holds
##                                          nothing; a reload test against an
##                                          empty magazine proves nothing
##   DEPLOY INCONCLUSIVE   the latch never reported (needs debugEsp + natEsp +
##                         natEspDiag, read at boot)
##   DEPLOY FAIL           the latch reported another phase and never DEPLOYED
##   PACT   INCONCLUSIVE   the request never reached the host -- the inspector
##                         channel is off. THE SCRIPT DID NOT RUN.
##   PACT   FAIL           pact REFUSED it and its own line says why (no firearm
##                         in hands, not deployed, a hop that did not validate)
##   MAGCYC FAIL           the cycle was ISSUED and Magazine.Count did not come
##                         back. A partial load lands here on purpose.
##   AMMOLD FAIL           the cycle completed and mods/ammoloading is ARMED but
##                         NEVER FIRED. **THIS IS THE VERDICT THIS SCRIPT EXISTS
##                         TO BE ABLE TO PRODUCE.** The mod did not see it.
##   AMMOLD INCONCLUSIVE   the mod says NOT ARMED, or says nothing -- no hook is
##                         installed, so its zero counter means nothing
##
## "The script did not run" (INCONCLUSIVE, exit 2) and "the mod never fired"
## (FAIL, exit 1) are deliberately different exit codes.
##
## THE RUNG
## --------
## `probe` -- install the verified prefix on `LoadMagazineProcess::Start` and
## COUNT firings, with zero dereferences. It is the rung that proves the TRIGGER
## fires, which is the thing that has never been demonstrated. `spawn` and
## `animate` additionally need a bundle riding a vanilla key via mods/textures,
## which is not set up, and asking for them here would make this test fail for a
## reason that has nothing to do with the trigger.
##
## THE TEMPLATE IDS -- pasted, not searched; see woods_pmc_m4.nim for why.
##   5447a9cd4bdc2dbd208b4567  Colt M4A1 5.56x45 assault rifle
##   55d4887d4bdc2d962f8b4570  Colt M4A1 STANAG 30-round magazine
##   54527ac44bdc2d36668b4567  5.56x45mm M855A1
##   5ab8dced86f774646209ec87  ANA Tactical M1 armored rig

import autoscript

proc main() =
  var s = newScript("ammoloading-e2e")

  onMap(s, "Factory")   # the smallest map, so the load is the shortest
  asSide(s, "pmc")

  # Arm BEFORE the launch. Both of these are read at boot: the host flags by the
  # host, the rung by the mod when it loads. Declaring them after the client is
  # up would write a file that says "on" over a process that is off.
  withActuation(s)
  withAmmoLoading(s, "probe")

  # The rig first, then what goes in it -- requests apply in declaration order.
  equip(s, "TacticalVest", "5ab8dced86f774646209ec87")

  # THE WEAPON MUST HAVE A MAGAZINE IN IT. `pact`'s chain is
  # HasFirearmInHands -> get_HandsController -> get_Item -> GetCurrentMagazine,
  # and an empty weapon refuses at the fourth hop with "no current magazine" --
  # a correct refusal that would nevertheless make this test fail for a gear
  # reason while looking like an actuation one.
  #
  # It is no longer unverified, and `equipLoaded` was the wrong verb. MEASURED
  # against this install's db.json:
  #
  #   5447a9cd4bdc2dbd208b4567 (M4A1)   _props.Chambers[0]._name =
  #                                     "patron_in_weapon"; NO _props.Cartridges
  #   55d4887d4bdc2d962f8b4570 (STANAG) _props.Cartridges[0]._max_count = 30
  #
  # So `equipLoaded` on the WEAPON asked the backend for a cartridge capacity a
  # weapon does not have. It was REFUSED -- "the database declares no
  # _props.Cartridges[0]._max_count" -- and even if it had not been, nothing was
  # ever mounted in the weapon's `mod_magazine` slot. That is exactly why
  # `GetCurrentMagazine()` came back null and this test could never reach the
  # thing it exists to measure.
  #
  # `equipWeapon` mounts a NAMED magazine (the M4A1's mod_magazine filter admits
  # 20 templates; picking one by document order is the guess this library
  # refuses), loads it, and chambers a round. The acceptance is a walk of the
  # saved profile: `magPlaced` and `roundsPlaced` are separate FAILs, so an empty
  # magazine cannot pass as a loaded one.
  equipWeapon(s, "FirstPrimaryWeapon", "5447a9cd4bdc2dbd208b4567",
              "55d4887d4bdc2d962f8b4570", "54527ac44bdc2d36668b4567")

  # Spares in the rig, so the unload has somewhere to put 30 rounds. The unload
  # moves ammunition into the inventory; a character with nowhere to put it is a
  # cycle that fails at UnloadMagazine for a container reason.
  carryLoaded(s, "TacticalVest", "55d4887d4bdc2d962f8b4570", 2,
              "54527ac44bdc2d36668b4567")
  stock(s, "54527ac44bdc2d36668b4567", 120)

  if runToRaid(s):
    # In a live raid. Let the weapon finish being drawn before touching it: a
    # magazine cycle issued during the deploy animation refuses at
    # HasFirearmInHands, which is a TIMING answer wearing a capability answer's
    # clothes.
    discard pactDo(s, "wait 300", 20)
    pactStatusNote(s)

    if magCycle(s, 60):
      discard assertAmmoLoadingFired(s, 40)
    pactStatusNote(s)

  quitWith(finish(s))

main()

scripts/gear_must_fail.nim

nim
## gear_must_fail -- the acceptance test FOR THE LIBRARY ITSELF.
##
##     .\installer\build\aowl.exe script scripts\gear_must_fail.nim
##
## CLAUDE.md 9b: a verification that cannot fail IS the bug. The gear check in
## `autoscript` claims it can tell "the loadout arrived" from "the loadout did
## not". This script is the input that makes it say NO.
##
## It asks for a **rifle in the Headwear slot**. `emu/bots.validateSlots` -- the
## ancestry-aware validator, the same one the bot generator runs -- refuses that
## placement, the item is dropped before the profile is saved, and the read-back
## finds nothing there. The expected transcript is:
##
##     GEAR    requested=1 minted=1 placed=0 slotDropped=1 flatStacks=0
##       ! validateSlots: slot refused: 5447a9cd4bdc2dbd208b4567 -> Headwear
##                        on 55d7217a4bdc2d86028b456d
##     GEAR    FAIL  asked for 1, minted 1, read back 0; 1 refusal(s)
##
##     === gear-must-fail: FAIL ===
##
## and the process exits **1**.
##
## So this script INVERTS the verdict: it exits 0 when the gear check said FAIL,
## and 1 when it said anything else. A PASS here would mean the gear check
## reported success for gear that is provably not on the profile -- which is the
## exact defect this file exists to catch, and it is louder as a failing test
## than as a paragraph in a doc.
##
## `attachOnly` -- it drives a stack that is already up and never launches the
## client, because it has nothing to do with a raid. It also never reaches the
## raid steps at all: `run` stops at the first step that is not PASS.
##
## Note the three-outcome discipline survives the inversion. If the gear step
## came back INCONCLUSIVE -- no backend, no profile, an install whose tarkov.dll
## predates this feature -- that is NOT a pass either. The question was not
## asked, and this script says so and exits 2.

import autoscript

proc main() =
  var s = newScript("gear-must-fail")
  attachOnly(s)

  # A Colt M4A1 in the helmet slot. Not a typo: the point.
  equip(s, "Headwear", "5447a9cd4bdc2dbd208b4567")

  if not launchStack(s):
    note(s, "no backend to ask -- INCONCLUSIVE, not a pass")
    quitWith(2)
  if not pickProfile(s):
    note(s, "no profile to mint into -- INCONCLUSIVE, not a pass")
    quitWith(2)

  let gearSaidOk = applyGear(s)
  if gearSaidOk:
    note(s, "DEFECT: the gear check reported PASS for a rifle in the " &
            "Headwear slot. It cannot distinguish gear that arrived from " &
            "gear that did not, which makes every other PASS it has ever " &
            "printed meaningless.")
    quitWith(1)
  if verdictOf(s) == "INCONCLUSIVE":
    note(s, "the gear check could not look (" & reasonOf(s) &
            ") -- INCONCLUSIVE, not a pass")
    quitWith(2)
  note(s, "the gear check correctly refused a rifle in the Headwear slot " &
          "and reported FAIL. The check can fail, so its passes mean " &
          "something.")
  quitWith(0)

main()

scripts/woods_pmc_m4.nim

nim
## woods_pmc_m4 -- the worked example.
##
## "Go into Woods as a PMC with an M4A1, three loaded magazines, an Altyn, a rig,
## two IFAKs and 180 spare rounds, and tell me whether that actually happened."
##
##     .\installer\build\aowl.exe script scripts\woods_pmc_m4.nim
##
## `aowl script` builds this file and RUNS it. Running it starts the backend and
## the client if they are not already up, waits for the backend's control port to
## answer, picks the profile, mints the gear, drives the menu into an offline
## raid on Woods, waits for the host's raid-phase latch to say DEPLOYED, and
## exits 0 PASS / 1 FAIL / 2 INCONCLUSIVE.
##
## THE TEMPLATE IDS
## ----------------
## Pasted rather than searched, because an id is exact and free while a name
## search is a 2.7 MB scan whose AMBIGUOUS answers this library refuses on
## purpose. `byName(s, "bandage", 2)` exists for when a name is unambiguous, and
## the refusal names the candidates when it is not.
##
##   5447a9cd4bdc2dbd208b4567  Colt M4A1 5.56x45 assault rifle
##   55d4887d4bdc2d962f8b4570  Colt M4A1 5.56x45 STANAG 30-round magazine
##   54527ac44bdc2d36668b4567  5.56x45mm M855A1
##   5aa7e276e5b5b000171d0647  Altyn bulletproof helmet
##   5ab8dced86f774646209ec87  ANA Tactical M1 armored rig
##   590c678286f77426c9660122  IFAK personal tactical first aid kit
##
## ORDER MATTERS, AND IT IS NOT A STYLE RULE
## -----------------------------------------
## The rig is equipped BEFORE the magazines that go inside it. Requests are
## applied in declaration order, and a `carry` into an empty slot is refused with
## "nothing is worn in TacticalVest" -- named, not silent, but still a FAIL.

import autoscript

proc main() =
  var s = newScript("woods-pmc-m4")

  onMap(s, "Woods")
  asSide(s, "pmc")

  # Worn. `clear` defaults true, so the character is stripped first and these
  # slots are guaranteed empty -- "the slot is already occupied" is the most
  # common way a loadout silently does not arrive.
  equip(s, "Headwear", "5aa7e276e5b5b000171d0647")
  equip(s, "TacticalVest", "5ab8dced86f774646209ec87")
  equip(s, "FirstPrimaryWeapon", "5447a9cd4bdc2dbd208b4567")

  # In the rig. Each magazine is filled to the capacity its own template
  # declares, with a round its own cartridge filter accepts -- a mag "filled"
  # with ammo the filter omits is a gun that spawns empty, and that is refused
  # with a reason rather than written.
  carryLoaded(s, "TacticalVest", "55d4887d4bdc2d962f8b4570", 3,
              "54527ac44bdc2d36668b4567")

  # Also in the rig, not the pockets. MEASURED, 2026-08-31, against the live
  # db.json: `clear` deliberately exempts Pockets -- a character with no Pockets
  # item does not spawn at all -- so the four pocket cells are still holding
  # whatever the character had, and IFAKs into them come back
  # "no grid of 627a4e6b255f7527fb05a0f6 (4) has a free cell". That is a correct,
  # named refusal, and the fix is to put them somewhere with room.
  carry(s, "TacticalVest", "590c678286f77426c9660122", 2)

  # Loose in the stash, so a reload after the raid has somewhere to come from.
  stock(s, "54527ac44bdc2d36668b4567", 180)

  # Launch, mint, enter, wait for the raid-phase latch, print the verdict, and
  # exit 0 PASS / 1 FAIL / 2 INCONCLUSIVE.
  runAndQuit(s)

main()

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