Preview before you move#
This is an independent lab using the supplied AcademyBot from the source kit. You do not need to implement FollowBot or complete its walkthrough first. Use the setup/loading reference if this is your first DLL build.
- Build and install AcademyMover and AcademyBot.
- Select AcademyMover and AcademyBot, enter a loaded area and Start.
- Open AcademyMover's configuration panel and click Preview path once.
- The next bot tick looks for a targetable visible area transition and requests a path toward it. No movement is sent from this preview button.
A Planned N path points diagnostic when a suitable path exists. If there is no transition, the sample reports that and stops the attempt. A button press is not a promise that a route exists.
Use the path API for your product#
ExilePather.Reload prepares navigation data; it is not a boolean success result. Check IsReady before pathfinding.
DPB1 uses a PathfindingCommand passed by ref to FindPath. DPB2 FindPath returns a path result with Success and Path. The snippet and downloaded source switch with the selected product.
CurrentCommand exposes the latest command/result for consumers. Its type differs between DPB1 and DPB2; do not paste one product's declaration into the other project.
The sample skips points very close to the player and refuses a first candidate more than 35 units away. These are teaching limits, not a complete movement algorithm. No candidate means no action.
Where this goes: AcademyMover.TryPlan in the selected DPB2 source project. The preview caller requests ExilePather.Reload before planning.
private bool TryPlan(Vector2i position, out Vector2i next)
{
next = default(Vector2i);
CurrentCommand = null;
if (!LokiPoe.IsInGame || LokiPoe.Me == null || LokiPoe.Me.IsDead || !ExilePather.IsReady)
{
Log.Info("[AcademyMover] Planning skipped: player or navigation data is not ready.");
return false;
}
var start = LokiPoe.MyPosition;
if (start.Distance(position) < 5)
{
Log.Info("[AcademyMover] Already close to the destination. No step requested.");
return false; // Already close: no movement command was sent.
}
var command = ExilePather.FindPath(start, position);
CurrentCommand = command;
if (!command.Success)
{
Log.Info("[AcademyMover] No successful path result. No step requested.");
return false;
}
var path = command.Path;
if (path == null || path.Count == 0)
{
Log.Info("[AcademyMover] The returned path is empty.");
return false;
}
Log.Info("[AcademyMover] Planned " + path.Count + " path points. Planning does not move the player.");
// Follow the returned path, starting near the player. Refuse a distant first hop.
var candidates = path.Where(point => start.Distance(point) >= 6).Take(1).ToArray();
if (candidates.Length == 0 || start.Distance(candidates[0]) > 35)
{
Log.Info("[AcademyMover] No next point within the sample's 6-to-35-unit step limits.");
return false;
}
next = candidates[0];
return true;
}MoveTowards submits one command#
The caller owns the input session. The mover does not enable a session behind the caller's back. It also needs a Move-only action bound on the skill bar.
After a valid path, the mover submits one movement request using the Move-only slot. It checks the returned UseResult and returns true only for None.
Do not replace Move-only with an arbitrary attack binding. Do not use a path result as proof that input was sent, or an accepted input as proof that the character moved.
Where this goes: AcademyMover.MoveTowards. AcademyBot never calls it; AcademyWorkshop calls it only after explicit opt-in.
public bool MoveTowards(Vector2i position, params dynamic[] user)
{
// The caller owns the input session. A mover must not enable it behind its back.
if (!LokiPoe.IsInGame || LokiPoe.Me == null || LokiPoe.Me.IsDead ||
!LokiPoe.ProcessHookManager.IsEnabled) return false;
var area = LokiPoe.LocalData.AreaHash;
var areaId = LokiPoe.CurrentWorldArea?.Id;
if (area == 0 || areaId == null) return false;
Vector2i next;
if (!TryPlan(position, out next)) return false;
if (!LokiPoe.IsInGame || LokiPoe.Me == null || LokiPoe.Me.IsDead ||
LokiPoe.LocalData.AreaHash != area || LokiPoe.CurrentWorldArea?.Id != areaId ||
!LokiPoe.ProcessHookManager.IsEnabled) return false;
var move = LokiPoe.InGameState.SkillBarHud.LastBoundMoveSkill;
if (move == null || !move.CanUse())
{
Log.Info("[AcademyMover] Bind the Move-only action to the skill bar first.");
return false;
}
var result = LokiPoe.InGameState.SkillBarHud.UseAt(move.Slot, false, next);
Log.Info("[AcademyMover] Movement request: " + result);
return result == LokiPoe.InGameState.UseResult.None;
// True means the request was accepted, NOT that the destination was reached.
}A successful API request produces Movement request: None. The workshop then measures the player again and reports displacement and remaining distance. Zero displacement is still possible.
A useful mover needs an honest caller#
- Already close: check remaining distance before asking for another step. The sample's close threshold is five units.
- Not ready or no route: return without input; let the owner decide whether and when a new attempt is appropriate.
- Request accepted: yield, then compare fresh position or distance. Do not increment a success counter solely because the API returned None.
- No progress: cap retries and log the reason. Repeating the same command indefinitely is not recovery.
- Area changed, death or disconnect: abandon stale destinations and reacquire state.
- Stop: clear pending requests and release only input owned by your workflow.
AcademyMover intentionally does not implement skill-based travel, obstacle learning, automatic area transitions or a full stuck-recovery strategy. You get a working one-step foundation and the contract to extend, not a claim of production navigation.
Complete source files
These are the exact C# files in the DPB2 download. Use the complete ZIP for project settings, references, build commands and installation instructions.
AcademyMover/AcademyMover.cs
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Controls;
using DreamPoeBot.Common;
using DreamPoeBot.Loki.Bot;
using DreamPoeBot.Loki.Bot.Pathfinding;
using DreamPoeBot.Loki.Common;
using DreamPoeBot.Loki.Game;
using DreamPoeBot.Loki.Game.Objects;
using log4net;
using Message = DreamPoeBot.Loki.Bot.Message;
using DreamPoeBot.Loki.Bot.Pathfinding.RDWrapper.Types;
namespace DeveloperAcademy
{
public sealed class AcademyMover : IPlayerMover
{
private static readonly ILog Log = Logger.GetLoggerInstanceForType();
private UserControl _control;
private volatile bool _previewRequested;
public string Name => "AcademyMover";
public string Description => "Preview paths, or send one walking command when called by the opt-in workshop.";
public string Author => "Your name";
public string Version => "1.0.0";
public JsonSettings Settings => null;
public PathResult? CurrentCommand { get; private set; }
public UserControl Control
{
get
{
if (_control != null) return _control;
var panel = new StackPanel { Margin = new System.Windows.Thickness(12) };
panel.Children.Add(new TextBlock { Text = "While AcademyBot runs, preview a path to a visible transition. No movement is performed.",
TextWrapping = System.Windows.TextWrapping.Wrap });
var button = new Button { Content = "Preview path once", Margin = new System.Windows.Thickness(0, 10, 0, 0) };
// The UI thread only queues a request. Read game state on the next bot tick.
button.Click += (sender, args) => _previewRequested = true;
panel.Children.Add(button);
_control = new UserControl { Content = panel };
return _control;
}
}
public void Initialize() { }
public void Deinitialize() { Stop(); }
public void Start() { Stop(); Log.Info("[AcademyMover] Started in preview mode."); }
public void Stop() { _previewRequested = false; CurrentCommand = null; }
public MessageResult Message(Message message) => MessageResult.Unprocessed;
public Task<LogicResult> Logic(Logic logic) => Task.FromResult(LogicResult.Unprovided);
public void Tick()
{
if (!_previewRequested) return;
_previewRequested = false;
if (!LokiPoe.IsInGame || LokiPoe.Me == null || LokiPoe.Me.IsDead)
{
Log.Info("[AcademyMover] Preview skipped: enter a loaded area with a living player.");
return;
}
var target = LokiPoe.ObjectManager.GetObjectsByType<AreaTransition>()
.Where(item => item.IsTargetable).OrderBy(item => item.Distance).FirstOrDefault();
if (target == null)
{
Log.Info("[AcademyMover] No targetable transition is currently available.");
return;
}
ExilePather.Reload();
Vector2i next;
TryPlan(target.Position, out next);
}
#region PlanPath
private bool TryPlan(Vector2i position, out Vector2i next)
{
next = default(Vector2i);
CurrentCommand = null;
if (!LokiPoe.IsInGame || LokiPoe.Me == null || LokiPoe.Me.IsDead || !ExilePather.IsReady)
{
Log.Info("[AcademyMover] Planning skipped: player or navigation data is not ready.");
return false;
}
var start = LokiPoe.MyPosition;
if (start.Distance(position) < 5)
{
Log.Info("[AcademyMover] Already close to the destination. No step requested.");
return false; // Already close: no movement command was sent.
}
var command = ExilePather.FindPath(start, position);
CurrentCommand = command;
if (!command.Success)
{
Log.Info("[AcademyMover] No successful path result. No step requested.");
return false;
}
var path = command.Path;
if (path == null || path.Count == 0)
{
Log.Info("[AcademyMover] The returned path is empty.");
return false;
}
Log.Info("[AcademyMover] Planned " + path.Count + " path points. Planning does not move the player.");
// Follow the returned path, starting near the player. Refuse a distant first hop.
var candidates = path.Where(point => start.Distance(point) >= 6).Take(1).ToArray();
if (candidates.Length == 0 || start.Distance(candidates[0]) > 35)
{
Log.Info("[AcademyMover] No next point within the sample's 6-to-35-unit step limits.");
return false;
}
next = candidates[0];
return true;
}
#endregion
#region MoveOneStep
public bool MoveTowards(Vector2i position, params dynamic[] user)
{
// The caller owns the input session. A mover must not enable it behind its back.
if (!LokiPoe.IsInGame || LokiPoe.Me == null || LokiPoe.Me.IsDead ||
!LokiPoe.ProcessHookManager.IsEnabled) return false;
var area = LokiPoe.LocalData.AreaHash;
var areaId = LokiPoe.CurrentWorldArea?.Id;
if (area == 0 || areaId == null) return false;
Vector2i next;
if (!TryPlan(position, out next)) return false;
if (!LokiPoe.IsInGame || LokiPoe.Me == null || LokiPoe.Me.IsDead ||
LokiPoe.LocalData.AreaHash != area || LokiPoe.CurrentWorldArea?.Id != areaId ||
!LokiPoe.ProcessHookManager.IsEnabled) return false;
var move = LokiPoe.InGameState.SkillBarHud.LastBoundMoveSkill;
if (move == null || !move.CanUse())
{
Log.Info("[AcademyMover] Bind the Move-only action to the skill bar first.");
return false;
}
var result = LokiPoe.InGameState.SkillBarHud.UseAt(move.Slot, false, next);
Log.Info("[AcademyMover] Movement request: " + result);
return result == LokiPoe.InGameState.UseResult.None;
// True means the request was accepted, NOT that the destination was reached.
}
#endregion
}
}
Keep building
Look up a specific DPB2 API →Download integrity and validation scope
Source kit: 26 files, 30,744 bytes. No client binaries or credentials.
SHA-256: b8877783cae166a0499a0bb400cfe835a9f2db2cbe20e328c36760e3ce32ad59
Compile baseline: DPB1 0.3.29.47 / DPB2 0.4.5.88. Compilation is not a live game test. Follow the lesson's manual checks in your own test setup.