Appearance
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()
