Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
d78df16
Give Brother Wystan and Duke Dreadmoore boss steps in the SM drafts
pjerra Oct 18, 2026
5523aaa
plan-export: write exact via points as move flag 64 and warn about ev…
pjerra Oct 18, 2026
94da3f1
plan-export: the hop after an exact via is kind move_direct_exact, so…
pjerra Oct 20, 2026
15a9e88
plan-export: name the exact hop's kind move_hop_exact (fits kind varc…
pjerra Oct 20, 2026
f558c91
plan-export: the long-hop cutter never splits a move_hop_exact from i…
pjerra Oct 20, 2026
f95a4c6
Merge pull request #237 from pjerra/feat/sm-side-bosses
pjerra Oct 20, 2026
f181872
dashboard: list the three map picture slots with reasons, as Zul'Farr…
pjerra Oct 20, 2026
9006c70
Merge pull request #245 from pjerra/feat/planexport-exact
pjerra Oct 20, 2026
4941cda
dashboard: keep status files out of the image route, re-ask empty maps
pjerra Oct 20, 2026
70d3896
CheckCommand: allow the server trace lines a run sends (#249)
pjerra Oct 20, 2026
ceabf52
Observe mode: the kit check sends no .bot maintenance by default (#253)
pjerra Oct 20, 2026
a73f75f
Merge pull request #256 from pjerra/feat/map-layers-three
pjerra Oct 20, 2026
f96a12c
Observe mode: start from a leg or pull group of the run leader's plan…
pjerra Oct 20, 2026
6cb6c0e
minimap-extract: compose the client's WMO indoor minimap tiles
pjerra Oct 20, 2026
c0738c1
Observe mode: a partway start is checked for place, leg and a busy run
pjerra Oct 20, 2026
0ac6864
Hire check: .bot maintenance follows the observe switch (#253)
pjerra Oct 20, 2026
d187850
planexport: a pull group starts at the walk before it
pjerra Oct 20, 2026
884335e
CheckCommand: the trace driver word is 1-96 characters
pjerra Oct 20, 2026
8a63969
Observe mode: a partway start fails closed without proof or place
pjerra Oct 20, 2026
b96d5c9
Observe mode: the start reply's step and leg must be the requested ones
pjerra Oct 20, 2026
2876a8f
minimap-extract: make the WMO pass rules real and bound the compositor
pjerra Oct 20, 2026
1e7d89b
minimap-extract: keep MOGI group ids positional; band the known excep…
pjerra Oct 20, 2026
699bdb7
Merge dev into fix/observe-start-trace-maintenance
pjerra Oct 20, 2026
091f774
Merge dev into feat/wmo-minimap
pjerra Oct 20, 2026
c216ce1
Merge pull request #260 from pjerra/fix/observe-start-trace-maintenance
pjerra Oct 20, 2026
6dd50f1
Merge pull request #262 from pjerra/feat/wmo-minimap
pjerra Oct 20, 2026
b762c29
route-oracle: measure a plan's stored route against the navmesh
pjerra Oct 20, 2026
b860ad1
zulfarrak: approach the pool stairs from the lower floor
pjerra Oct 20, 2026
f4e3cec
route-oracle: score one route, and fail legs that do not path
pjerra Oct 20, 2026
25db331
route-oracle: --mode bosses plans entrance to each boss live, no hand…
pjerra Oct 20, 2026
44c0ca5
route-oracle: write the reports without following a symlink
pjerra Oct 20, 2026
d9f4f1d
route-oracle: --objectives bosses measures the boss list alone; one p…
pjerra Oct 20, 2026
a2a3632
route-oracle: NearYd is the minimum over a group's positions; folded …
pjerra Oct 20, 2026
e1cdd16
Merge pull request #274 from pjerra/feat/route-oracle
pjerra Oct 20, 2026
f11df6d
plan-export: a wait's hold and a hold's script value carry the ZF sta…
pjerra Oct 21, 2026
1c32473
Licence: AGPL-3.0-or-later from now on (earlier versions stay MIT; th…
pjerra Oct 21, 2026
848693b
zulfarrak: velratha pull stands 20 yd clear of their packs; a hold va…
pjerra Oct 21, 2026
f230f0a
Merge pull request #277 from pjerra/chore/relicense-agpl
pjerra Oct 21, 2026
6e9b55f
README: the Velratha approach points are the moved ones
pjerra Oct 21, 2026
3bfbad0
Merge pull request #278 from pjerra/feat/zf-full-module-plan
pjerra Oct 21, 2026
5da82fe
remote gossip: the cmd gossip order counts the menu from 1, so a plan…
pjerra Oct 21, 2026
78c5a7e
plan-export: a gossip step with an option number is a gossip row for …
pjerra Oct 21, 2026
ae56a97
Merge origin/dev into feat/planexport-gossip
pjerra Oct 21, 2026
db9f044
Merge pull request #279 from pjerra/feat/planexport-gossip
pjerra Oct 21, 2026
653729f
observe: count a wipe once, on the rising edge of all-dead
pjerra Oct 21, 2026
fdd16a2
enter: send cmd enter at a door above or below the walkable ground
pjerra Oct 21, 2026
1faf0c1
docs: the observe wipe count and the cmd-enter door rule
pjerra Oct 21, 2026
2205289
Merge pull request #280 from pjerra/fix/observe-wipes-enter-dz
pjerra Oct 21, 2026
9969123
zulfarrak: pull the Antu'sul pair before the camp, each from a stand;…
pjerra Oct 21, 2026
052bfb5
observe: count one wipe while a member may revive; emit the wipe trac…
pjerra Oct 21, 2026
23383b5
Merge pull request #281 from pjerra/fix/zf-antusul-order
pjerra Oct 21, 2026
fdf6d35
Merge pull request #282 from pjerra/fix/observe-wipe-hysteresis
pjerra Oct 21, 2026
a15d55b
tracecheck: count off-tank threat only when the mob is on the holder
pjerra Oct 21, 2026
32c344e
Merge pull request #283 from pjerra/fix/threat-oracle-refine
pjerra Oct 21, 2026
6838c25
plan-export: --carrier-chests exports a companions_loot-false leg's c…
pjerra Oct 21, 2026
32e8ff1
plan-export: --carrier-chests refuses a standless chest; the warning …
pjerra Oct 21, 2026
44fc702
Merge pull request #284 from pjerra/feat/export-carrier-chests
pjerra Oct 21, 2026
1ae1e18
Add observe.keep_loot_setting
pjerra Oct 21, 2026
12daf03
Merge pull request #285 from pjerra/feat/observe-keep-loot-setting
pjerra Oct 21, 2026
e32d014
Zul'Farrak: leg 'leave' list, island clearance rule, velratha approac…
pjerra Oct 21, 2026
e3a2b92
Leave walk check takes the shortest distance over every segment, adds…
pjerra Oct 21, 2026
bc3f48b
Merge pull request #286 from pjerra/feat/zf-leave-island
pjerra Oct 21, 2026
6fc0636
Uldaman: pull the Keeper's neighbours first and Grimlok's corridor on…
pjerra Oct 21, 2026
9d94f85
Merge remote-tracking branch 'origin/dev' into feat/uldaman-pull-order
pjerra Oct 21, 2026
330a858
Merge pull request #287 from pjerra/feat/uldaman-pull-order
pjerra Oct 21, 2026
4f31de5
Uldaman plan: cc_priority, cc_always, no_cc and kill_first for the ca…
pjerra Oct 21, 2026
2c08cf7
Trace: keep the action verdict in feed frames and add a report-only s…
pjerra Oct 21, 2026
deeb4f5
Live checks doc: eleven trace checks
pjerra Oct 21, 2026
e0b9e9a
README: restore the spells agent-command row and keep the spells chec…
pjerra Oct 21, 2026
312d066
Uldaman CC: no boss.first add is cc_always, and a boss fight never cr…
pjerra Oct 21, 2026
96186b9
Spells report: say what the frames can show
pjerra Oct 21, 2026
a9490b2
Merge pull request #290 from pjerra/feat/uldaman-cc
pjerra Oct 21, 2026
abd37a3
Merge pull request #291 from pjerra/feat/trace-spell-usage
pjerra Oct 21, 2026
74862a8
WIP: start z check via .gps (StartHands.GPS, ParseGPS, checkStartZ); …
pjerra Oct 21, 2026
75623ae
Driven-run start: check the commander's z against .gps FloorZ (#292)
pjerra Oct 22, 2026
4d081c6
Start z check: select the commander before .gps and ignore replies ab…
pjerra Oct 22, 2026
1012b65
trace-score: add taunt latency, cc hold and pet threat to the spells …
pjerra Oct 22, 2026
cea8f5e
observe: count the lead bot's deaths and a wipe spread over 90 s
pjerra Oct 22, 2026
ae90de0
observe: count the run leader's rest phases and expose the instance id
pjerra Oct 22, 2026
28ee70c
observe: a lone death after a counted wipe is no second wipe
pjerra Oct 22, 2026
b067735
Merge pull request #293 from pjerra/fix/start-commander-z-check
pjerra Oct 22, 2026
79c52a6
Merge pull request #294 from pjerra/fix/ta264-observe-leader-deaths-w…
pjerra Oct 22, 2026
d3c8081
observe: paused time is not rest
pjerra Oct 22, 2026
8d361ef
Merge origin/dev into fix/ta271-observe-rest-instance
pjerra Oct 22, 2026
966c4e1
trace-score: fix cc_hold and taunt latency false warnings
pjerra Oct 22, 2026
ab1aa4a
Merge pull request #295 from pjerra/fix/ta271-observe-rest-instance
pjerra Oct 22, 2026
7ac921d
trace-score: count Entangling Roots in cc_hold
pjerra Oct 22, 2026
e48f459
Merge pull request #297 from pjerra/fix/ta289-spell-checks
pjerra Oct 22, 2026
a9e0824
observe: record the pause for a player's order and the resume after it
pjerra Oct 22, 2026
f9bfc75
preflight: warn on a weak tank and on a hire of the other faction (#266)
pjerra Oct 22, 2026
cf905b9
session tests: keep the host's wall-clock steps and short deadlines f…
pjerra Oct 23, 2026
fd6a37a
observe: a player-order pause that ends in a stop writes no resume (#…
pjerra Oct 23, 2026
dbec4ae
GM lines wait for a selection echo that cannot answer an older select…
pjerra Oct 23, 2026
c6e2e27
preflight: check the party's faction before maintenance moves the hir…
pjerra Oct 23, 2026
f7a5998
Merge pull request #298 from pjerra/fix/ta266
pjerra Oct 23, 2026
21bfbe2
Merge pull request #299 from pjerra/fix-ta270-player-orders
pjerra Oct 23, 2026
070124e
Merge pull request #300 from pjerra/fix/ta296-flaky-tests
pjerra Oct 23, 2026
701ef2d
Selection echo: a combat rewrite of the target field is not a selecti…
pjerra Oct 23, 2026
4c72af5
Selection echo: no confirmation while the agent attacks, fights or is…
pjerra Oct 23, 2026
fa88f39
GPS test: allow the neutral selection of nothing before the commander
pjerra Oct 23, 2026
e9b14ff
Merge pull request #301 from pjerra/fix/gm-selection-echo
pjerra Oct 23, 2026
567b392
Dashboard: a run that left packs alive is no full clear (#302)
pjerra Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
682 changes: 661 additions & 21 deletions LICENSE

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions LICENSES/MIT-turtle-agent-before-agpl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 pjerra

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
5 changes: 5 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
turtle-agent
Copyright (c) 2026 pjerra

turtle-agent is licensed under the GNU Affero General Public License v3.0 or later (see LICENSE).
Versions before the licence change were MIT-licensed (LICENSES/MIT-turtle-agent-before-agpl.txt).
The third-party code below stays under its own licence and is compatible with the AGPL.
Contributions: one commit (af8a2fd, session create) by Sagiroth, relicensed with his consent (see the licence-change PR).

turtle-agent contains code ported from agent-wow
(https://github.com/agent-wow/agent-wow), commit 5ef4916, used under the MIT License:

Expand Down
208 changes: 175 additions & 33 deletions README.md

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions cmd/turtle-agent/commander_gps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package main

import (
"context"
"strings"
"time"

"github.com/pjerra/turtle-agent/internal/events"
)

// The `.gps` read of the start's z check (dungeon.StartHands.GPS). `.gps` is a plain GM line (no ".bot " in front) and the core answers it
// with several system messages; the one with GroundZ and FloorZ ends the read (a TileX line may follow it).
const (
gpsTotalWait = 3 * time.Second // the whole read
gpsQuietWait = 400 * time.Millisecond // the read ends this long after the last line once a line has come
)

// GPS selects the agent's own character, waits until the server shows it selected, and sends ".gps": the core reports the unit the GM has
// selected, so a mob or a bot left selected would be read in the commander's place. It returns the system lines that followed, joined by
// newlines. An error is a selection or a line that could not be sent, or no reply within gpsTotalWait.
func (g remoteGame) GPS(ctx context.Context) (string, error) {
log := g.c.cur().Log()
since := log.Last()
if err := g.selectThenGM(ctx, g.AgentGUID(), ".gps"); err != nil {
return "", err
}
return readGPSReply(ctx, log, since, gpsTotalWait, gpsQuietWait)
}

// readGPSReply collects the system lines the log gets after since: it returns at the line that has "FloorZ" (the one that ends the reply),
// else quiet after the last line once one has come, else at total with an error when there was none.
func readGPSReply(ctx context.Context, log *events.Log, since uint64, total, quiet time.Duration) (string, error) {
tctx, cancel := context.WithTimeout(ctx, total)
defer cancel()
var lines []string
cursor := since
for {
p := log.Read(events.Query{Since: cursor, Kinds: []string{events.KindSystem}, Limit: events.MaxLimit})
for _, e := range p.Events {
if e.Text == "" {
continue
}
lines = append(lines, e.Text)
if strings.Contains(e.Text, "FloorZ") {
return strings.Join(lines, "\n"), nil
}
}
cursor = p.Next
if p.More {
continue
}
wctx, cancel := tctx, context.CancelFunc(func() {})
if len(lines) > 0 {
wctx, cancel = context.WithTimeout(tctx, quiet)
}
err := log.Wait(wctx, cursor)
cancel()
if err != nil {
if err := ctx.Err(); err != nil {
return "", err
}
if len(lines) == 0 {
return "", context.DeadlineExceeded
}
return strings.Join(lines, "\n"), nil
}
}
}
109 changes: 109 additions & 0 deletions cmd/turtle-agent/commander_gps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package main

import (
"context"
"strings"
"testing"
"time"

"github.com/pjerra/turtle-agent/internal/events"
"github.com/pjerra/turtle-agent/internal/opcode"
"github.com/pjerra/turtle-agent/internal/packet"
"github.com/pjerra/turtle-agent/internal/session"
)

func gpsSys(l *events.Log, text string) { l.Append(events.Event{Kind: events.KindSystem, Text: text}) }

// The reply is the system lines after the send, up to the one with FloorZ; earlier lines and other kinds are not part of it.
func TestReadGPSReplyCollectsTheLinesToFloorZ(t *testing.T) {
log := events.New(100)
gpsSys(log, "old line")
since := log.Last()
go func() {
time.Sleep(20 * time.Millisecond)
gpsSys(log, "Map: 70 (Uldaman)")
log.Append(events.Event{Kind: events.KindChat, Text: "somebody says hi"})
gpsSys(log, "X: 1 Y: 2 Z: 3")
gpsSys(log, "GroundZ: 3.000000 FloorZ: 3.000000 Have height data (Map: 1 VMap: 1)")
}()
got, err := readGPSReply(context.Background(), log, since, 2*time.Second, 400*time.Millisecond)
if err != nil || got != "Map: 70 (Uldaman)\nX: 1 Y: 2 Z: 3\nGroundZ: 3.000000 FloorZ: 3.000000 Have height data (Map: 1 VMap: 1)" {
t.Fatalf("%q %v", got, err)
}
}

// A reply without a FloorZ line ends on a quiet gap after its last line; no reply at all is an error after the total wait.
func TestReadGPSReplyQuietAndTimeout(t *testing.T) {
log := events.New(100)
since := log.Last()
gpsSys(log, "There is no such command")
start := time.Now()
got, err := readGPSReply(context.Background(), log, since, 2*time.Second, 50*time.Millisecond)
if err != nil || !strings.Contains(got, "no such command") || time.Since(start) > time.Second {
t.Fatalf("%q %v after %s", got, err, time.Since(start))
}
if _, err := readGPSReply(context.Background(), log, log.Last(), 80*time.Millisecond, 50*time.Millisecond); err == nil {
t.Fatal("no reply must be an error")
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := readGPSReply(ctx, log, log.Last(), time.Second, 50*time.Millisecond); err == nil {
t.Fatal("a cancelled context must be an error")
}
}

// `.gps` reports the unit the commander has selected, so the read selects the agent's own character (guid 0x2A) first and sends the line
// only once the server shows that selection. A stranger left selected (a mob, a bot) must not be read in its place.
func TestGPSSelectsTheCommanderBeforeTheLine(t *testing.T) {
const stranger = packet.GUID(0x4D2)
r := newRig(t)
r.srv.Send(opcode.SMSGUpdateObject, selfBlock(stranger)) // the server shows a stranger selected
g := remoteGame{c: &commanderService{cur: func() *session.Session { return r.s }}}
type reply struct {
text string
err error
}
done := make(chan reply, 1)
go func() {
text, err := g.GPS(context.Background())
done <- reply{text, err}
}()
// The server applies each selection and echoes it. A neutral selection of nothing may come first (when GPS starts before the session
// read the create block, the field is unknown: session.SelectConfirmed); anything else, or a line on anything but the commander, fails.
var ops []uint16
inForce := stranger
for len(ops) == 0 || ops[len(ops)-1] != opcode.CMSGMessagechat {
p, err := r.srv.Next(2 * time.Second)
if err != nil {
t.Fatalf("after %v: %v", ops, err)
}
switch p.Op {
case opcode.CMSGSetSelection:
guid := packet.NewReader(p.Body).GUID()
if guid != 0x2A && (guid != 0 || len(ops) != 0) {
t.Fatalf("selected %#x, not the commander (0x2A) or a first neutral selection of nothing", uint64(guid))
}
inForce = guid
ops = append(ops, p.Op)
r.srv.Send(opcode.SMSGUpdateObject, selfValues(guid)) // the server's echo of the selection
case opcode.CMSGMessagechat:
if inForce != 0x2A {
t.Fatalf("the .gps line was sent with %#x selected, not the commander", uint64(inForce))
}
if !strings.Contains(string(p.Body), ".gps") {
t.Fatalf("chat line %q", p.Body)
}
ops = append(ops, p.Op)
r.s.Log().Append(events.Event{Kind: events.KindSystem, Text: "X: 1.000000 Y: 2.000000 Z: 3.000000 Orientation: 0.000000\n" +
"GroundZ: 3.000000 FloorZ: 3.000000 Have height data (Map: 1 VMap: 1)"})
}
}
select {
case got := <-done:
if got.err != nil || !strings.Contains(got.text, "FloorZ: 3.000000") {
t.Fatalf("%q %v", got.text, got.err)
}
case <-time.After(5 * time.Second):
t.Fatal("GPS did not return")
}
}
68 changes: 21 additions & 47 deletions cmd/turtle-agent/commander_methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ func (g remoteGame) ViewStats() tbm.ViewStats { return g.c.feed.
// Team is the agent's own side (dungeon.TeamSource): the feed carries no race, and the owned bots share the account's faction.
func (g remoteGame) Team() string { return g.c.cur().Team() }

// BotTeam implements dungeon.BotTeamer: a unit's side from its race in the agent's object store ("" when the unit is not in view).
func (g remoteGame) BotTeam(guid packet.GUID) string { return session.Team(g.c.cur().RaceOf(guid)) }

// BotLines implements dungeon.BotLiner: a plain `.bot maintenance|inv|logout <bot>` and the lines it was answered with.
func (g remoteGame) BotLines(ctx context.Context, command string, wait time.Duration, done func(line string) bool) ([]string, bool, error) {
return g.c.cmdr.Lines(ctx, command, wait, done)
Expand Down Expand Up @@ -376,33 +379,15 @@ func (g remoteGame) GMVisible(ctx context.Context, on bool) error {
// gmSelectConfirmWait is how long the server is given to show a selection before a line that acts on it is given up on.
var gmSelectConfirmWait = 3 * time.Second

// selectConfirmed selects the unit and waits until the SERVER shows it as the agent's selection (the target field of the agent's own object,
// which the core sends back after it handled CMSG_SET_SELECTION). The core queues that packet (map thread) apart from a GM's chat line (world
// thread), so a line sent right after can run on the selection from before: none (the sender itself) or a stale one, which can be another
// player. Run 6b: `.instance groupunbind all` answered "Glacienne is not in a group" for a selected Lcwarra. An error, with nothing sent, when
// the server does not show the selection in time.
// selectConfirmed selects the unit (0: none) and waits until the SERVER is known to have it in force as the agent's selection
// (session.SelectConfirmed: an echo of the target field of the agent's own object that answers this select and not an older one, nor a
// combat or stun rewrite of the field). The core queues that packet (map thread) apart from a GM's chat line (world thread), so a line sent
// right after can run on the selection from before: none (the sender itself) or a stale one, which can be another player. Run 6b:
// `.instance groupunbind all` answered "Glacienne is not in a group" for a selected Lcwarra; issue #296: a stale echo of the lead, with a
// stranger's select pending behind it, let `.additem` run on the stranger. An error, with nothing sent, when the server does not show the
// selection in time.
func (g remoteGame) selectConfirmed(ctx context.Context, guid packet.GUID) error {
s := g.c.cur()
if _, err := s.Target(ctx, guid, ""); err != nil {
return err
}
timer := time.NewTimer(gmSelectConfirmWait)
defer timer.Stop()
tick := time.NewTicker(25 * time.Millisecond)
defer tick.Stop()
for {
sel, ok := s.ServerSelection()
if ok && sel == guid {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return fmt.Errorf("the server did not show %v as the agent's selection within %s (it shows %v): not sending a line that acts on the selection", guid, gmSelectConfirmWait, sel)
case <-tick.C:
}
}
return g.c.cur().SelectConfirmed(ctx, guid, gmSelectConfirmWait)
}

// gmSelectAttempts is how many times selectThenGM selects and confirms again when the selection changed after the confirmation.
Expand Down Expand Up @@ -567,28 +552,17 @@ func (g remoteGame) GoXYZ(ctx context.Context, mapID uint32, at dungeon.Point) e
// around the commander. The selection is cleared first and the server is waited for to show it cleared, else the line would act on that
// one unit alone (or fail on a player).
func (g remoteGame) Respawn(ctx context.Context) error {
s := g.c.cur()
if err := s.ClearTarget(ctx); err != nil {
return err
}
timer := time.NewTimer(gmSelectConfirmWait)
defer timer.Stop()
tick := time.NewTicker(25 * time.Millisecond)
defer tick.Stop()
for {
if sel, ok := s.ServerSelection(); ok && sel == 0 {
break
}
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
sel, _ := s.ServerSelection()
return fmt.Errorf("the server did not show the selection cleared within %s (it shows %v): not sending .respawn", gmSelectConfirmWait, sel)
case <-tick.C:
}
return respawnUnselected(ctx, g.c.cur())
}

// respawnUnselected clears the selection, waits until the server is known to have it cleared (a stale echo of an earlier clear, or the
// field cleared by the end of a fight, does not count), and sends ".respawn" only while that stands (selectThenGM with no unit).
func respawnUnselected(ctx context.Context, s *session.Session) error {
g := remoteGame{c: &commanderService{cur: func() *session.Session { return s }}}
if err := g.selectThenGM(ctx, 0, ".respawn"); err != nil {
return fmt.Errorf("respawn with nothing selected: %w", err)
}
return s.GM(ctx, ".respawn")
return nil
}

// WalkSelf is walk_path for the agent's character.
Expand Down
25 changes: 1 addition & 24 deletions cmd/turtle-agent/dungeon_leader_pulltest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package main

import (
"context"
"fmt"
"time"

"github.com/pjerra/turtle-agent/internal/brain"
"github.com/pjerra/turtle-agent/internal/dungeon"
Expand All @@ -16,28 +14,7 @@ import (
// Respawn sends the character's own ".respawn" with nothing selected (dungeon.Respawner), as the commander's does: the selection is cleared
// first and the server is waited for to show it cleared, else the line would act on that one unit alone.
func (g *sessionGame) Respawn(ctx context.Context) error {
s := g.cur()
if err := s.ClearTarget(ctx); err != nil {
return err
}
timer := time.NewTimer(gmSelectConfirmWait)
defer timer.Stop()
tick := time.NewTicker(25 * time.Millisecond)
defer tick.Stop()
for {
if sel, ok := s.ServerSelection(); ok && sel == 0 {
break
}
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
sel, _ := s.ServerSelection()
return fmt.Errorf("the server did not show the selection cleared within %s (it shows %v): not sending .respawn", gmSelectConfirmWait, sel)
case <-tick.C:
}
}
return s.GM(ctx, ".respawn")
return respawnUnselected(ctx, g.cur())
}

// CombatMark is the position of the event log now (dungeon.CombatMeter).
Expand Down
14 changes: 14 additions & 0 deletions cmd/turtle-agent/dungeon_methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ type dungeonRunParams struct {
// (AUTO-IMPROVE step 0), so its waypoints and pull order are unchecked. Without it such a plan is refused.
AllowDraft bool `json:"allow_draft,omitempty"`
FromLeg *legParam `json:"from_leg,omitempty"`
// FromStep is an observed run's exact start step in the run leader's own plan: the step number `.bot run status` shows, from 1
// (README "Observe mode"). It is the fallback for a from_leg / from_pull_group whose step cannot be mapped offline (the deployed rows
// were exported with other options); observe only, and it excludes from_leg and from_pull_group.
FromStep *int `json:"from_step,omitempty"`
// FromPullGroup starts the run inside from_leg, at that pull group (remote mode; README "Start anywhere"): the commander's own GM move
// brings the party to the group's gather spot. StartHere keeps the old from_leg (no move: the run starts where the lead bot stands).
// Pulltest runs one pull group as a scored scenario instead of the plan (README "Pull test").
Expand Down Expand Up @@ -278,6 +282,16 @@ type observeParam struct {
MaxRunS float64 `json:"max_run_s,omitempty"`
StallS float64 `json:"stall_s,omitempty"`
PausedWaitS float64 `json:"paused_wait_s,omitempty"`
// Maintenance sends `.bot maintenance <bot>` to every bot at the preflight (and at the hire check); off by default. Maintenance
// walks a bot to a vendor for repairs and supplies: right after the autogear of a hire that scatters the party across the maps, and
// the run leader pauses with "the lead bot is not on the plan's map". The kit check's inventory and talent reads run either way.
Maintenance bool `json:"maintenance,omitempty"`
// AllowFarStart lets a partway start (from_leg, from_pull_group, from_step) go on although the lead bot stands more than 80 yd
// from the start step's point; the note says so. Off by default: a party in the wrong place is refused before `run start`.
AllowFarStart bool `json:"allow_far_start,omitempty"`
// KeepLootSetting leaves the bots' corpse looting as the operator set it: the start sends no `.bot behavior <bot> loot on` (the undo
// of a boss `loot off` a crashed run left in place). Off by default. Observe only; a driven run always sends the undo.
KeepLootSetting bool `json:"keep_loot_setting,omitempty"`
}

type dungeonRunResult struct {
Expand Down
Loading
Loading