Skip to content

Merge dev into main - #303

Merged
pjerra merged 102 commits into
mainfrom
dev
Oct 23, 2026
Merged

pjerra merged 102 commits into
mainfrom
dev

Conversation

@pjerra

@pjerra pjerra commented Oct 23, 2026

Copy link
Copy Markdown
Owner

Brings main up to dev: PRs #287-#302 (Uldaman pull order and CC, trace spell checks, start z check via .gps, observe leader deaths/rest/instance id, trace-score spell checks, preflight tank and faction warnings, player-order pauses, flaky session tests, GM selection echo, packs-left status on the dashboard). Every change merged to dev through a reviewed PR with green CI; main has nothing dev lacks.

pjerra added 30 commits October 18, 2026 21:24
Both are Turtle additions that the world export already carries as map
189 spawn rows, but neither was in a plan's boss list: the Scarlet
Monastery drafts only pulled them as members of a pack, so a bot run
neither names them as a boss nor keeps its step count in line with the
wing it clears.

scarlet_library: bosses 3974, 61983, 6487. Brother Wystan (EventAI, spell
list 180317 with Fireball 8402, movement_type 2) stands on the way to
Arcanist Doan, so the new leg sits between Loksey's and Doan's.
scarlet_graveyard: bosses 3983, 61972, 14693, 4543. Duke Dreadmoore lies
in the crypt level (z -3.7, script npc_duke_dreadmoore sets him to 20 %
health on reset and yells on aggro and death); his crypt pack stays
needs_walk until that part is mapped.

Generated with tools/dungeongen from the spec (no hand edits to the
drafts). plan-check at origin/dev e50dc28:
  scarlet_library   88 points, 2 warnings, exit 0 -> 89 points, 1 warning
                    (a detour on leg brother_wystan), exit 0
  scarlet_graveyard 1 of 141 points fail, exit 11 -> 1 of 143, exit 11
                    (the same needs_walk crypt group, now leg
                    duke_dreadmoore); the plan stays on planCheckKnownFailing
Legs: library 18/24/2, graveyard 14/6/22/11.
…ery plan attribute the rows cannot carry

An exact via (the launch point of the direct step after it, Wailing
Caverns' Serpentis foot) becomes a move row with flags 64, TortoiseBots'
new exact waypoint: its run leader counts it done only once the lead bot
stands within 1.5 yd of it, still for 2 s, and only then sends the
move_direct. Validate refuses the flag on any other kind and an exact
move that is not followed by its move_direct (nothing inserted between).

Every plan attribute the rows cannot carry is now a warning, one per
attribute, naming where it stands and what the run leader does instead
(skip, leftover_entries, leg rest, needs_walk, patrol, boss tactics and
companions_loot, a dropped companions_loot false, fleers, no_cc,
summoned_kill_first, hire_composition, level_band, raid, watchdog, the
entrance and teleports, avoid without a navmesh, a loot or ritual stand,
an object's spawn or range, a loot batch, an area trigger's expect
check). check_skip (plan check only), equiv and notes are not warned.
… a rolled-back module fails closed

The direct step right after an exact via is written as move_direct_exact.
A TortoiseBots with exact waypoints reads it as a move_direct launched
from the exact move before it; an older one does not know the kind and
its loader skips the row (UnknownKind), so a binary rollback that walks
the foot as a plain move never sends the hop over the hole from short of
it. Validate pairs them both ways (flag 64 move right before a
move_direct_exact, a move_direct_exact right after a flag 64 move), the
avoid-box router and the long-hop cutter treat it as a direct step.
…har(16)) and refuse longer kinds

move_direct_exact is 17 characters; ai_dungeon_step.kind is a
varchar(16), so the INSERT would fail in strict mode or cut the kind.
The kind is now move_hop_exact (14). Validate refuses any kind longer
than the column (MaxKind), and a test holds every kind the exporter
writes to it.
…ts exact move

The cutter measured a move_hop_exact like any move and, for a hop longer
than MoveGapWarnYd, put its navmesh points between the exact move and the
hop, which Validate then refused. The hop is now indivisible there (no
Cut call, nothing inserted before it; it stays the next hop's anchor), as
the avoid-box router already had it. dungeon.Parse holds a direct via to
25 yd, so the test sets a 65 yd hop on the parsed plan.
SM drafts: Brother Wystan and Duke Dreadmoore get boss steps
…ak has them

/api/minimap/<id> now answers World map (and each world-map floor),
Minimap and Navmesh in that order, every slot with kind, floor, label
and available, and a one-line reason instead of picture fields when the
folder has none. A map the folder has nothing for answers 200 with three
missing slots; only an unset --minimap-dir answers 404.

A slot's reason comes from an optional <id>.status.json (64 KiB, 160
characters a reason), with a built-in line per kind otherwise. The main
<id>.json stands in only for a source without a file of its own, so a
picture is never listed twice.

The page's picker shows all three slots, disables a missing one as
"<label> (none)" with its reason, and falls back to the first available
picture when a saved choice went missing; the map picture layer is
greyed with the minimap's reason when nothing is available.
plan-export: exact waypoints (move flag 64 + move_hop_exact) and warnings for every attribute the rows cannot carry
Review round 1 of the three-slot change:
- /minimap/ serves only images again: the allow-list used by
  serveMinimapImage loses status, while the folder reader keeps it, so
  <id>.status.png can never be handed out as an image.
- an answer with no available picture at all (an empty folder, or one
  just re-extracted) is kept for its reasons but asked again after 30 s,
  as before, via the pure PictureSlots.keep/hasPicture helpers.
- the greyed picture layer's reason now names every missing slot instead
  of only the minimap's when nothing is available; with no reasons at
  all it stays empty so the page's own text stands.
- a status reason's control characters (line break, tab) become spaces.
- the floor test uses 2f, 10f, 3f so a text sort can no longer pass it.
- README: missing slots carry name and source too, and name is the label.
run_meta.server_trace sends .bot trace start <run_id> driver=... and
.bot trace stop (and trace status, which the stop's retries read), but
CheckCommand rejected the whole trace group: the start never went out and
the run reported the server trace state unknown. Allow exactly trace
start|stop|status, with trace start's driver= argument and its grammar;
every other non-commander .bot line stays refused.
The preflight's .bot maintenance walks every hire to a vendor right after
its autogear: the party scatters over maps 0/1 (one to map 43) and the run
leader pauses with "the lead bot is not on the plan's map". In observe
mode maintenance is now off unless observe.maintenance is true; the
inventory and talent reads still run. A driven (commander) run is
unchanged.
Dashboard: always list the three map pictures (World map, Minimap, Navmesh) with a reason when missing
…#248)

checkObserve refused from_leg / from_pull_group, so an observed run always
started at step 1; the 2026-10-20 worker had to start the server run by hand
(.bot run start wailing_caverns from 207) and adopt it.

from_leg maps to the leg's number in the exported rows (an optional leg
takes none) and goes out as '.bot run start <plan> from <n>'; from_step
(observe only) passes the run leader's own step number. from_pull_group
needs that step number: the plan's rows are rebuilt with the exporter's
option sets (plain, cut hops, approach paths, avoid-box routes) and the set
whose step count matches the one the server lists in '.bot run list' is
used; when none matches, the probe refuses before a bot is hired and names
from_step. The start words go out in startRunLeader's line, resolved once
at the probe; invalid combinations stay refused.
A dungeon the client draws from WMO art gets its real minimap: the
terrain tiles, the WMO groups placed by the WDT/ADT MODF and MOGI
with the settled transform (2 px/yd, 128-yd tile step, bottom-left
anchored tiles, yaw plus the 180-degree term, rising PZ+z0), or the
WMO tiles over the terrain. The JSON gains from=terrain|wmo|
terrain+wmo, --list shows the makeup, a skipped tile or group is
logged, and a source with no picture writes its reason into
<id>.status.json.
Review round 1 on #248 (A, D, E, G):
- A busy run of the same plan is adopted only when its status is at the
  requested step (or in the requested leg, when the start was sent by
  name); another step, another leg, or a leg sent as a number refuses
  with a note instead of watching a run from the wrong place.
- A leg whose name the command grammar takes and is unique in the plan is
  sent as 'from leg <name>'; else its row number. The run leader's start
  line ('The run starts at step X of Y (leg NAME)') is compared with the
  plan file: a drifted deployment stops the run and refuses the watch. A
  start the module moved back into a scripted event is noted.
- Before 'run start' the lead bot must stand within 80 yd of the start
  step's point (the mapped step's coordinates, or the leg's first point);
  observe.allow_far_start turns the refusal into a note. PartialStart
  carries the point and the resolved step from the resolver.
Review round 1 (B): the hire flow's top-up of a reused hire or one short
of drinks or ammo called .bot maintenance in observe mode too, right
after autogear, and scattered the party like the preflight did. prepEnv
carries the same observe.maintenance decision (preflightMaintains), the
hire check warns and reports the supplies as they are when it is off,
and a driven run is unchanged.
Review round 1 (C): GroupStep for from_pull_group took the first row whose
comment names the group, skipping the 'approach path' / 'path' / 'path
(keeps out of an avoid box)' rows cutLongHops and routeAroundAvoid put
before it: the run leader would start at the pull row and walk in from its
own reach instead of the exported approach (live Wailing Caverns leg 9
group 4: step 202, not 199). It now walks back over those rows to the first
row after the previous anchor. StepPoint exposes a step's coordinates for
the observer's place check.
Review round 1 (F): the trace argument regexp allowed 64 characters after
driver=, while RunMeta.Check allows the 96 the server stores, and it did
not anchor the word to the driver= prefix. A separate anchored
^driver=[A-Za-z0-9_.+\-:]{1,96}$ is required on the fourth word only;
trace start's run id keeps the commander's argument grammar, so a
driver= word cannot stand in its slot.
Round 3 (Codex): both safety gates could fail open.
- verifyStartReply accepted a start whose message was absent (ACK only,
  changed text, a dropped line) with just a note. Every non-empty partial
  start must now be proven: the parsed start line first, else `run status`
  right after the start (the step, and the leg, must be the requested
  ones); if neither proves it, the newly started run is stopped and the
  watch refused (no polls, normal unbind).
- checkStartPlace skipped the party-position check when the start step's
  point or the lead's position was not known. Both now refuse the start
  with a clear message unless observe.allow_far_start is true.
Tests: ACK-only at another step stops and rejects; ACK-only with a status
proof is accepted; missing point refused; allow_far_start accepted; in
range accepted; out of range refused. README and LIVE-CHECKS state both
gates.
Round 4 (Codex): a parsed start reply was accepted without comparing its
step, and from_step (Leg -1) skipped the leg check too: a request for
step 3 accepted "The run starts at step 1 ..." and watched it. The reply
no longer overwrites the requested step. For a step start the reported
step must equal the requested one, and the leg must equal the expected
one (the requested leg, or the requested step's leg resolved from the
plan rows: PartialStart.ExpectLeg). The module's documented "moved back
to the step that starts the event" is the only accepted step difference
and is noted. A mismatch stops the run, returns an error without polling
or watching. planexport.PlanLeg maps a row leg to the plan leg for
ExpectLeg; observeStepInfo resolves it with the step's point.

Tests: a wrong step for from_step and for from_pull_group stops exactly
once with no status polls; a wrong leg with the right step stops; the
requested step and the documented move-back are accepted (the latter
noted). README and LIVE-CHECKS say the step and leg are compared.
Keep every terrain minimap except the five Q-e maps byte for byte as the
old extractor wrote it; draw WMO art over the terrain only on 33, 36,
289, 329 and 800. Fail the all-dungeon alignment test on any measured
failure except the known client-data exceptions (189, 819, 822, 533),
log the raw shift, drop placements outside the navmesh box and the two
Frostmane mini-hives, cap drawables and sampled pixels with a status
reason, draw per tile rectangle, and write the known limits as the
JSON note. Fixture tests pin draw order, alpha 128, uid dedup, terrain
underlay, empty groups and both caps, each mutation-proved.
…tions

A rejected MOGI entry used to compact the group slice, so a tile's
original group id could land on another group's box. Keep one slot per
MOGI entry (nil when rejected), drop tiles whose group was rejected,
and test an invalid group 0 before a tiled group 1.

The all-dungeon test now records the expected artOnNav, plan points and
shift of the four exception maps and fails on any value outside the
band (an improvement included); the client-gated skips print NOT RUN.
Observe mode: start from a leg or pull group (#248), server trace lines allowed (#249), no maintenance in the observe start (#253)
minimap-extract: real indoor minimaps for the 21 dungeons that had none (WMO tiles), indoor art over the terrain minimap on 5 maps
The audit's Stage 1 needs a fixture per dungeon plan that says how far the
plan's stored route and what the server's navmesh plan are apart, leg by
leg. The command builds the objective chain (entrance, then per leg its
boss, else its to/last step point) and the stored polyline (the plan's own
points in the order planexport writes them), plans every leg twice (plain,
and on a scratch planner with SetAvoid of the plan's avoid boxes, whose
path the metrics use), and reports status, end gap, live and stored length,
ratio, stored points' distance to the live path, Z jumps over 8 yd, the
breaks touched (exact/direct/gm_cross/check_skip vias within 6 yd of the
path, or within 10 yd of a partial path's end), pull groups' corridor
membership (radius + 18 yd, dz 8) and redundant approach/via points
(within 5 yd), with the SHA-256 of every mmap file used.

internal/routeoracle holds the report; its tests plan over a synthetic
navfixture mesh, so no real mmaps are needed.
The zf-full-19z16 run stopped at the four-mob pack on the west road: the
lead stood at 1629 1099, the foot of the pool stairs, which is a navmesh
island, and three followers plus the healer went up to z 31-46.8 and took
six minutes to gather back. Give the last two packs approach points on
the lower floor west of the stair mouth so the run leader's party stays
there.

Pin the real Gong of Zul'Farrak (141832) in a test, not the Turtle
decoration copy 20901. Record where the Mallet comes from: item 9240 is
made by using 9241, and 9241 drops only from Qiaga the Keeper (7996) on
map 0, so no bot run can acquire it and the leg stays optional. Note the
missing pyramid phase gate (instance data 1, value 7): the plan format
has no instance-data wait, so the stairs leg keeps its Nekrum-seen proxy.
The review's three highs:

- The live route and the stored route were two different walks: boss legs
  planned previous objective to boss position directly, while the stored
  length counted the via points, the to, every pull group's approach
  points and centre, and the boss approaches. The oracle now plans the
  driver's own ordered sequence (via, to, pull approaches and pull
  centres or holds, step stands, boss approaches, boss, after-boss
  stands), hop by hop, and scores each stored hop against its own
  navmesh hop: per-segment rows in the JSON, per-leg sums in the table.
- A partial path poisoned the numbers behind it: a hop that does not
  path complete now fails its leg; the leg's remaining hops are planned
  on from the point the failed path reached and marked non_comparable,
  the legs after a failed leg are indeterminate, and every aggregate
  (ratio, packs, breaks, redundancy) is over the complete legs only.
- The report is written under the plan's name, which the command read
  from the plan JSON: a name like ../escape would write outside --out.
  The plan's own name is validated with the same strict basename regex
  as --plan, it must equal the requested plan name, and the resolved
  destination must stay under --out.

Tests: a boss leg with to and pull groups (the walk and its ratio), a
partial leg followed by another leg (indeterminate), a traversal name in
the plan JSON (refused, nothing written outside --out), a name mismatch,
and the end-to-end fixture run; each fails if its check is removed.
… points, no break hops

The walk mode (still the default) proves the stored hops are walkable; it
cannot say whether a route could be planned from a boss list. The bosses
mode plans each leg once, avoid-aware, from the previous objective to the
leg's boss (else its to or last point), skipping nothing.

- A leg the mesh does not reach is FAILED, adds nothing to any aggregate,
  and names the stored break it hit: a break hop whose foot the mesh reaches
  from the start and whose landing reaches the objective, or one within 15 yd
  of the live end or of the line to the objective.
- The leg after a failed one is planned from the last point reached and is
  labelled non-comparable and excluded; it is also planned from the previous
  objective (bridged), reported beside the primary numbers, never in them.
- Complete legs report packs whose centre the path passes, hand points
  within 10 yd of it, floor jumps and live length. No stored/live ratio.
- Files are <plan>-bosses.txt and .json; an unknown --mode is a usage error.

Tests: a fixture mesh with a gap fails the leg and names the break; a
failed leg and the leg after it stay out of the totals; a break far from
the end is named because it bridges; the mode defaults to walk.
pjerra added 29 commits October 22, 2026 01:46
…mutations, vet, full test run and docs still to do
An observed run's report read 0 leader deaths and resurrections: only the
group-mode driver counted them. The observe watch now follows the party's
deaths in the feed, counts the lead bot's deaths and revives (report, legs,
leader_death trace events, seconds dead), and counts a wipe when every
member died within 90 s even though the lead bot revived in between, each
death newer than the last counted wipe (the RFC run: all five dead within
63 s, never in one frame).
Driven-run start: check the commander's z against .gps FloorZ (#292)
…indow

Observe: count the lead bot's deaths and revives, and a wipe spread over 90 s (#264)
A paused TBM:RUN line still carries the phase it paused in, so a run
paused in the rest phase kept its rest open and the whole pause
(PausedWait, 10 min) went into rest_seconds and rest_share. The rest is
now open only while the run is running in phase rest; any other state
closes it, and a resume in the same phase opens a new one.

The new test fails at 290 s rest without the state check and passes at
10 s with it.
cc_hold joins only long crowd control (the short stuns end unflagged and
read as broken holds) and a mob killed while flagged is not broken. A
taunt with a target answers only that mob's episode.
Observe: count the run leader's rest phases and expose the instance id (#271 A+B)
trace-score: taunt latency, long-CC hold and pet threat in the spells check (#289 T-c)
A paused status whose note says the module paused for a player order (the
observer's own cmd summon, a move) is now in report.remote.observe.player_orders
with the step and the order; controls also records the player order and the
resume after it. The module's own note addenda count once.
…rom failing walk, flight and replay tests (#296)

The WSL2 host steps its wall clock back about 2 s every 30 s. The session slews its movement
time towards the wall clock, so a step during a walk or flight slows the client time for
seconds (walk tests timed out at 2 s, FeatherFallMidFlight saw a step 0.03 yd over its
bound), and the capture stamps records with the wall clock, so a step put records before
earlier ones and the replay missed the knockback landing (17 decoded, 18 live).

- harness: sessions run on a steady clock (monotonic-driven clockFn); packet, ready and
  condition waits are 15 s instead of 2 s (they end on arrival)
- replay tests: the live scenario runs again when the wall clock stepped during it
- no product code changed
…#296)

selectConfirmed accepted any echo of the player's target field that showed the
unit, so an echo from an earlier select of the same unit, with another unit's
select still pending behind it, confirmed the new select; the GM line then ran
on that other unit. The session now keeps a log of the selections it sent and
moves a lower bound on the server's progress with each echo (to the oldest send
the echo can answer). A selection is confirmed only when every send from there
on is that unit. Reselecting the unit already confirmed needs no echo (the core
sends none for an unchanged field). When a new echo could not be told from an
old one, a neutral selection (none, else the agent itself) is made and confirmed
first. Respawn's wait for a cleared selection uses the same rule.
Preflight: tank HP floor and other-faction warning (#266 first slice)
Observe: record player-order pauses and their resume (#270 agent side)
Tests: make the session tests independent of WSL wall-clock steps (#296)
…on (#296 review)

UNIT_FIELD_TARGET is also written by Unit::Attack, Unit::AttackStop and a stun,
without touching the selection GM commands read. An echo of a value the session
never sent now leaves the selection in force unknown, and a repeated select is
confirmed with no echo only when the base came from an echo of the session's
own send; otherwise a neutral selection is made first (one the log, the dropped
sends and the field do not hold). The log is capped at 32 sends; an echo of a
dropped unit confirms nothing. The confirmed select lives in the session
(SelectConfirmed); GMOn (.aura world buffs) goes through it and GMOnConfirmed,
and .respawn is sent with GMOnConfirmed on a confirmed clear.
… stunned (#296 review 2)

The core rewrites the player's target field only through Attack, AttackStop
and stun apply/end; a rewrite to exactly the unit of a pending select read as
its echo. While the player is hot (its own SMSG_ATTACKSTART until the
ATTACKSTOP, UNIT_FLAG_IN_COMBAT or UNIT_FLAG_STUNNED) and for
SelectionCombatSettle after, no echo confirms and no GM line on a selection
goes out. Cooling down fences off every send so far; SelectConfirmed waits
inside its timer, then selects again (through a neutral selection when the
unit was fenced off) and needs the echo of that new send. A neutral selection
no longer counts the unknown base's placeholder as a unit.
When GPS started before the session had read the create block, the target
field was unknown and SelectConfirmed made a neutral selection of nothing
first, which the test refused (CI run 39915580906). The test now echoes every
selection, allows only one neutral selection of nothing before the commander,
and still checks that .gps runs with the commander selected.
GM lines run only on a confirmed selection (#296)
* Parse run leader left-packs field; a run with packs left is no clear

* Reset packs-left on every run_leader event

* Prefer clean clear in ghost tiebreak

* Ghost tie-break needs a confirmed clean clear, not just Left 0
@pjerra
pjerra merged commit 254c6ed into main Oct 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant