Build FollowBot · Movement milestone · validate one step at a time

Build FollowBot · 3. Find and follow the leader

Turn the task-driven host into a follower: identify the leader, keep a distance and verify actual movement.

After this lesson

You can implement a same-area FollowLeaderTask with a product-correct leader lookup, bounded movement, context reset and observable failure paths.

Build it yourself · selected, product-specific fragments · no finished bot download. Each checkpoint explains what to implement and how to check it.

Define a small, useful follower#

The goal is simple: when following is enabled and the party leader is loaded nearby, approach them; once close enough, stop requesting movement. If the leader is absent, the area changes or movement makes no progress, stop that intent rather than guessing a route.

Use an isolated test client with a passive routine and no unrelated action plugins. Following is real game input: keep the first tests supervised. The fragments are compiled and exercised offline; they are not a live-tested finished bot.

  • Default: FollowEnabled = false, read-only status only.
  • Resume following beyond 25 game units; stop at 12. Between those distances, preserve the previous following decision.
  • At most one movement attempt every 200 ms. Three rejected requests or three seconds without at least three units of follower displacement latch a fault.
  • Same-area only. No automatic party join, portal click, zone travel, resurrection, combat or loot.

Identify WHO first; then obtain a fresh position#

PartyMembers identifies the current party leader. ObjectManager supplies the currently loaded Player object. These are different questions: being listed in the party does not make a destination visible or reachable.

For DPB1, the name comes from PartyMember.PlayerEntry, with its online check. For DPB2, the public PartyMember exposes CharacterName directly. The product switch above changes the snippet; no external extension-method library is needed.

The helper rejects yourself as leader, an unavailable/dead local character and a missing/dead loaded leader. DPB2's roster shape does not provide the DPB1 IsOnline check here; requiring the current loaded Player is the conservative local gate, not a cross-zone presence claim.

Where this goes: A helper used by LeaderStatusTask and FollowLeaderTask. Imports: System, System.Linq, DreamPoeBot.Loki.Game, DreamPoeBot.Loki.Game.GameData and DreamPoeBot.Loki.Game.Objects.

C# · selected fragment
public static Player FindVisibleLeader()
{
    if (!LokiPoe.IsInGame || LokiPoe.Me == null || LokiPoe.Me.IsDead ||
        LokiPoe.LocalData.AreaHash == 0) return null;

    var member = LokiPoe.InstanceInfo.PartyMembers
        .FirstOrDefault(p => p.MemberStatus == PartyStatus.PartyLeader);
    var entry = member?.PlayerEntry;
    var name = entry != null && entry.IsOnline ? entry.Name : null;
    if (string.IsNullOrWhiteSpace(name) || name == LokiPoe.Me.Name) return null;

    // Party membership identifies WHO; loaded objects provide WHERE.
    // A roster entry alone is not evidence that the leader is reachable.
    return LokiPoe.ObjectManager.GetObjectsByType<Player>()
        .FirstOrDefault(p => p.Name == name && !p.IsDead);
}
Check your result

In read-only mode, log a leader name and current distance, or a clear reason to remain idle. No party / you are leader / leader not loaded must never select an arbitrary nearby player.

Make it yours

Change the party leader while staying in the same area. Your next lookup must select the new leader without restarting the client.

Do not carry a Player through a journey#

  1. Resolve the leader on each decision. Capture only the needed values: leader name/ID, destination position, local area hash and zone ID.
  2. Before sending a command, verify that the context still matches and reacquire the leader if any await occurred. A stored position from another area is not a fallback destination.
  3. On loading, death, pause, missing leader, leader change or a changed hash/zone: release the movement intent and call ForgetTarget. On explicit Stop/Start: call ResetSession.
  4. Perform those invalidation checks every Tick, outside the 200 ms movement throttle. A loading screen shorter than the throttle interval must still cancel old intent.
Keep in mind

Hash plus zone ID and observed loading cover ordinary transitions, not every special transfer. Cross-zone travel needs its own arrival verification; do not infer it from the area's display name.

Keep a gap instead of oscillating#

With a single threshold a follower can alternate move/stop near the boundary. Two thresholds create hysteresis: idle at 20, follow after reaching 26, keep following at 20, stop at 12.

The helper below stores only values and uses a monotonic session clock. It also limits command rate and detects no displacement. Leader movement and accepted commands cannot keep resetting the progress watchdog.

Where this goes: A helper file FollowMemory.cs in your project. Imports: System and DreamPoeBot.Common. Create one instance per FollowLeaderTask, not one per Run call.

C# · selected fragment
// State for ONE follower session. No game objects are retained here.
internal sealed class FollowMemory
{
    private bool _following;
    private bool _hasOrigin;
    private Vector2i _origin;
    private double _progressAt;
    private double _nextAttempt;
    private int _rejections;
    public bool Blocked { get; private set; }

    public void ResetSession()
    {
        Blocked = false;
        ForgetTarget();
    }

    public void ForgetTarget()
    {
        _following = false;
        _hasOrigin = false;
        _nextAttempt = 0;
        _rejections = 0;
        // A fault stays latched until an explicit Stop/Start or rearm.
    }

    public bool WantsStep(double distance, Vector2i position, double seconds)
    {
        if (Blocked) return false;
        if (double.IsNaN(distance) || double.IsInfinity(distance) || distance < 0)
        {
            ForgetTarget();
            return false;
        }
        if (distance <= 12) { ForgetTarget(); return false; }
        if (!_following && distance <= 25) return false;
        _following = true;
        if (!_hasOrigin || _origin.Distance(position) >= 3)
        {
            _hasOrigin = true;
            _origin = position;
            _progressAt = seconds;
        }
        // Leader motion and accepted commands do NOT reset this watchdog.
        if (seconds - _progressAt >= 3) { Blocked = true; return false; }
        if (seconds < _nextAttempt) return false;
        _nextAttempt = seconds + 0.2;
        return true;
    }

    public void RecordRequest(bool accepted)
    {
        if (accepted) _rejections = 0;
        else if (++_rejections >= 3) Blocked = true;
    }
}
Check your result

Distances 20 → 26 → 20 → 12 produce idle → follow → follow → idle (subject to the 200 ms command throttle). Staying stuck for three seconds blocks further requests.

Make it yours

Predict the result if the leader keeps moving but your character stays still. The watchdog must still block.

Keep in mind

Displacement is a minimal stuck detector, not proof of approaching the leader: circling can produce displacement. A production extension should also track progress along a fixed path segment and bound replanning. Do not describe this sample as complete stuck recovery.

Assemble FollowLeaderTask.Run yourself#

Implement this in the ITask.Run method created in part 2. Keep a Stopwatch field started at session Start and pass Elapsed.TotalSeconds to WantsStep. Never use DateTime.Now for the watchdog.

A false mover result is not a reason for a while loop inside Run. Count it with RecordRequest(false), finish the turn, and let the host yield. On the third rejection, cancel and latch the fault.

With FollowEnabled false, keep the read-only LeaderStatusTask reporting identity and distance at a bounded rate. FollowLeaderTask cancels owned movement, calls ForgetTarget and returns before WantsStep. Observation time must not accumulate a false stuck alarm. Do not clear Blocked except through the explicit rearm path.

The task returns true when it actually attempts work, not simply because following is enabled. Waiting for the next timed decision may return false in this follow-only milestone; adding other action tasks later requires an explicit shared input-ownership policy.

implementation checklist
1. Require ready/alive/unpaused state and a valid area.
2. Resolve the leader again; cancel on missing/changed context.
3. If FollowEnabled is false: cancel owned movement, ForgetTarget, return false.
4. Snapshot destination, area identity and follower position.
5. Ask the persistent FollowMemory.WantsStep(distance, position, clockSeconds).
6. If blocked: cancel input, log once and stop/rearm explicitly.
7. If no step is due: return false; release movement if now close/invalid.
8. Prepare navigation; recheck context and current leader.
9. Request ONE step, record its result; cancel immediately if now blocked.
10. Return true for an attempted step; yield in the host, then observe again.
Check your result

You can explain every true/false return. Missing leader, waiting interval and disabled following do not become hidden endless loops.

Delegate movement; do not duplicate the mover#

Have the host prepare navigation with ExilePather.Reload and check IsReady. Pass the retained selected IPlayerMover and the fresh snapshot to this fragment. It does not enable input or invent a path.

The bot owns the input session; the task owns follow intent; the mover plans/submits movement. Open your session only when movement is explicitly enabled, never take over an already active foreign session, and release only your own session on disable, failure or Stop.

Before the first request, verify a suitable Move-only binding and the selected mover's requirements. Use the mover guide to implement your own small mover, or deliberately select a compatible installed one. AcademyMover is a limited one-step teaching mover, not production navigation.

RecordRequest receives the mover's actual boolean result. True is not arrival: on the next ticks compare fresh player position and distance. Do not replace that observation with a sleep or a success log.

Where this goes: RequestStep helper used by FollowLeaderTask after WantsStep returns true. Imports: DreamPoeBot.Common, DreamPoeBot.Loki.Bot, DreamPoeBot.Loki.Bot.Pathfinding, DreamPoeBot.Loki.Game.

C# · selected fragment
public static bool RequestStep(IPlayerMover mover, Vector2i destination,
    uint areaHash, string areaId)
{
    if (mover == null || !LokiPoe.IsInGame || LokiPoe.Me == null ||
        LokiPoe.Me.IsDead || LokiPoe.InstanceInfo.IsGamePaused ||
        !ExilePather.IsReady || !LokiPoe.ProcessHookManager.IsEnabled ||
        areaHash == 0 || string.IsNullOrEmpty(areaId) ||
        LokiPoe.LocalData.AreaHash != areaHash || LokiPoe.CurrentWorldArea?.Id != areaId)
        return false;
    return mover.MoveTowards(destination);
}

Cancellation must stop intent AND owned input#

  1. When within the stop distance, disabled, dead, paused or losing the leader, cancel any movement still held by this workflow. Merely returning false is not guaranteed to release a held movement request.
  2. For the initial follower, the bot is the only action owner. Centralize release/cleanup in the host, using the same ownership-aware input cleanup pattern as the action workshop.
  3. If you later add combat or action plugins, decide who owns input on each turn. A following task must not clear keys owned by a routine. Cancellation, task precedence and Stop must use that shared rule.
  4. Keep a fault latched until explicit rearm or Stop/Start. A refreshed leader position, successful API return or loading screen must not silently turn the fault into unlimited retries.
Check your result

Turning FollowEnabled off stops requests and releases owned movement on the next bot tick, even if the movement throttle is not due.

Checkpoint 3: test the follower in small steps#

  1. Read-only: no party, own character as leader, missing player, dead leader, leader outside the loaded area. Expected: idle with no command.
  2. Read-only: leader change and hash/zone change, including a new instance with the same display name. Expected: no old destination reused.
  3. Decision-only: evaluate 20 → 26 → 20 → 12. Check distance hysteresis separately from the timer.
  4. Controlled movement: explicitly enable following with a compatible mover. Expected: approach a loaded leader, stop requesting when close, resume only beyond the outer distance.
  5. Rejection/no progress: use an offline fake mover first. Repeated false results or accepted commands without follower displacement must latch a fault; movement of the leader alone must not prevent this.
  6. Stop/disable during movement, then restart. Expected: no old command replay, no second loop, only your owned input released.
  7. Plugin integration: insert a throttled status task before FollowLeaderTask. Expected: useful reports without starving following. Keep combat disabled until the next extension is deliberately tested.
Keep in mind

Offline checks cover the published helpers and real task-manager dispatch. Successful helper tests do not certify the learner's assembled bot, the selected mover, live navigation or a game-version update.

When the leader leaves the area#

The first implementation stops following and reports leader unavailable. That is an intentional result, not a reason to walk toward the last coordinates or click the nearest portal.

To extend it, create a separate TravelToLeaderTask above FollowLeaderTask. Give it states such as locate destination, approach a verified transition, interact once, wait for loading, verify arrival, then reacquire the leader. Each state needs a timeout and an attempt budget.

Use party destination information only where the selected product actually exposes it. Two instances can share the same area name or template; a different hash alone is not proof you reached the leader. Verify the leader again before handing control back.

Do not copy helpers such as WalkablePosition or PlayerAction from private projects into an example and present them as built-in DPB. If you introduce a helper, teach its contract and implementation separately.

Grow the same bot without replacing its architecture#

  • Add persisted distance settings and validate 0 < stop distance < resume distance.
  • Add a status panel showing idle/following/blocked, leader identity and the last reason for cancellation.
  • Add a compatible CombatTask at a deliberate priority; document hook_combat and any routine-specific messages.
  • Add a portal/travel task only after bounded waiting and arrival detection are tested.
  • Keep the API reference as your lookup tool. This walkthrough explains composition; it does not replace the contracts.

Keep building

Look up a specific DPB1 API →