Which area am I in?#
Use AcademyObserver.Tick from the plugin lesson. It guards IsInGame and Me, then compares LocalData.AreaHash and CurrentWorldArea.Id with fields retained from the last observation. Name is only the log label. A changed hash catches a new instance with the same name; a changed ID catches a local zone that shares a hash.
On a change, clear area-specific targets and decisions; reacquire live objects instead of retaining wrappers across travel. Ignore hash zero. The observer resets after an observed loading state and at Start. Test first entry, repeated ticks, same-name/different-hash and same-hash/different-ID cases. Special transfers can share both values: a travel workflow also needs bounded loading/arrival checks, not a claim that hash comparison proves arrival.
A meaningful area diagnostic without a repeated log on every tick.
What objects can I use right now?#
The routine lesson uses ObjectManager.GetObjectsByType<Monster>, then filters eligibility before selecting a target. The mover preview uses the same pattern with AreaTransition.
Choose the type that describes your goal, then inspect the documented properties. Keep null handling, and do not infer that an empty list means the world contains nothing: the client may be loading or the object may not currently be known.
A query is an observation. Reading a position or finding an object does not interact with it. An action requires its own readiness checks and result handling.
How many item entries am I carrying?#
AcademyTasksBot's InventoryReport asks InstanceInfo.GetPlayerInventoryItemsBySlot(InventorySlot.Main) and reports Count. This is a read of the carried inventory, not a stash scan.
Pick the relevant inventory slot explicitly. Do not substitute a guessed numeric enum value. Follow InventorySlot in the reference to see the available named values for your product.
For an item operation, find its public inventory-control API and read the overload's prerequisites and return type. Moving, selling or consuming an item is not part of this read-only recipe.
- Build and install AcademyTasksBot from the source kit using the setup guide. Select it as the bot; this isolated lab does not drive plugins, a routine or a mover.
- Start in a loaded area. AreaReport handles the first changed-area turn; InventoryReport runs on a later pass and reports at most once every five seconds.
- Keep the WaitTimer field across calls. Run returns false while idle and true only on the pass that produced the report. Count is item entries/stacks, not item quantities or free grid squares.
Where this goes: Complete InventoryReport task in AcademyTasksBot.cs. Imports: System, System.Threading.Tasks, DreamPoeBot.Loki.Bot, DreamPoeBot.Loki.Common, DreamPoeBot.Loki.Game, DreamPoeBot.Loki.Game.GameData and log4net.
internal sealed class InventoryReport : ITask
{
private static readonly ILog Log = Logger.GetLoggerInstanceForType();
private readonly WaitTimer _interval = new WaitTimer(TimeSpan.FromSeconds(5));
public string Name => "InventoryReport";
public string Description => "Lower priority: count carried items at most once every five seconds.";
public string Author => "Your name";
public string Version => "1.0.0";
public void Start() { _interval.Stop(); }
public void Stop() { }
public void Tick() { }
public Task<bool> Run()
{
if (!_interval.IsFinished || !LokiPoe.IsInGame || LokiPoe.Me == null)
return Task.FromResult(false);
_interval.Reset();
var items = LokiPoe.InstanceInfo.GetPlayerInventoryItemsBySlot(InventorySlot.Main);
Log.Info("[InventoryReport] Carried item entries: " + items.Count);
// Count is item entries/stacks, not stack quantities or occupied grid squares.
// Do not retain Item wrappers across loading screens or long waits.
return Task.FromResult(true);
}
public MessageResult Message(Message message) => MessageResult.Unprocessed;
public Task<LogicResult> Logic(Logic logic) => Task.FromResult(LogicResult.Unprovided);
}An [InventoryReport] Carried item entries: N diagnostic. Stop ends the reports; Start makes the next report eligible again.
Which skill-slot number should I choose?#
Select AcademyRoutine, run AcademyBot in a loaded area and click the list button in the routine's panel. Read the reported slot number beside the skill name.
The loop inspects slots 1–13 and only reports skills that are on the skill bar. It never uses a skill. Use this diagnostic instead of guessing that a keyboard letter equals a particular slot.
After choosing a slot, the action method still checks whether its current skill can be used. Bindings can change; the earlier diagnostic is not permanent readiness proof.
Where this goes: AcademyRoutine.Tick. The routine's List bound skill slots in the log button queues the request; AcademyBot must be running to consume it.
public void Tick()
{
if (!_listSkills) return;
_listSkills = false;
if (!LokiPoe.IsInGame || LokiPoe.Me == null) return;
for (var slot = 1; slot <= 13; slot++)
{
var skill = LokiPoe.InGameState.SkillBarHud.Slot(slot);
if (skill != null && skill.IsOnSkillBar)
Log.Info("[AcademyRoutine] Slot " + slot + ": " + skill.Name);
}
}Log lines such as Slot 5: <skill name>. No movement or attack is sent.
How do components share intent?#
Use a settings model for user configuration, Message for a small synchronous agreement and Logic for an awaited behavior request. Give custom IDs a clear namespace such as academy.*.
Document the ID, input types, execution context and result semantics on both sides. An ID string alone is not a universal interface. A plugin's Message handler should return Unprocessed for requests it does not own.
For UI-triggered game work, copy only a small request value into a thread-safe field or queue. Consume it from the bot, validate fresh state and clear it on Stop.
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.
AcademyTasksBot/AcademyTasksBot.cs
using System;
using System.Threading.Tasks;
using System.Windows.Controls;
using DreamPoeBot.Loki.Bot;
using DreamPoeBot.Loki.Common;
using DreamPoeBot.Loki.Coroutine;
using DreamPoeBot.Loki.Game;
using DreamPoeBot.Loki.Game.GameData;
using log4net;
using Message = DreamPoeBot.Loki.Bot.Message;
namespace DeveloperAcademy
{
public sealed class AcademyTasksBot : IBot
{
private TaskManager _tasks;
private Coroutine _loop;
private UserControl _control;
public string Name => "AcademyTasksBot";
public string Description => "Read-only task priorities: report the area first, then inspect the carried inventory.";
public string Author => "Your name";
public string Version => "1.0.0";
public JsonSettings Settings => null;
public UserControl Control => _control ?? (_control = new UserControl { Content = new TextBlock { Text = "Start this bot and watch AreaReport followed by InventoryReport. No plugins, routine or mover are driven by this isolated task example.", TextWrapping = System.Windows.TextWrapping.Wrap, Margin = new System.Windows.Thickness(12) } });
public void Initialize() { }
public void Deinitialize() { Stop(); }
#region TaskHost
public void Start()
{
Stop();
_tasks = new TaskManager();
if (!_tasks.Add(new AreaReport()) || !_tasks.Add(new InventoryReport()))
throw new InvalidOperationException("Could not register the teaching tasks.");
try { _tasks.Start(); _loop = new Coroutine(Run); }
catch { Stop(); throw; }
}
public void Tick()
{
try
{
_tasks?.Tick();
if (_loop != null && !_loop.IsFinished) _loop.Resume();
}
catch { Stop(); throw; }
}
private async Task Run()
{
while (true)
{
if (LokiPoe.IsInGame && LokiPoe.Me != null)
await _tasks.Run(TaskGroup.Enabled, RunBehavior.UntilHandled);
await Coroutine.Yield();
}
}
public void Stop()
{
var loop = _loop;
_loop = null;
try { loop?.Dispose(); }
finally { var tasks = _tasks; _tasks = null; tasks?.Stop(); }
}
#endregion
public MessageResult Message(Message message) => _tasks?.SendMessage(TaskGroup.Enabled, message) ?? MessageResult.Unprocessed;
public Task<LogicResult> Logic(Logic logic) => Task.FromResult(LogicResult.Unprovided);
}
internal sealed class AreaReport : ITask
{
private static readonly ILog Log = Logger.GetLoggerInstanceForType();
private uint? _lastAreaHash;
private string _lastAreaId;
public string Name => "AreaReport";
public string Description => "Higher priority: report a changed area instance or local zone once.";
public string Author => "Your name";
public string Version => "1.0.0";
public void Start() { _lastAreaHash = null; _lastAreaId = null; }
public void Stop() { _lastAreaHash = null; _lastAreaId = null; }
public void Tick() { }
public Task<bool> Run()
{
if (!LokiPoe.IsInGame || LokiPoe.Me == null) return Task.FromResult(false);
var area = LokiPoe.CurrentWorldArea;
var hash = LokiPoe.LocalData.AreaHash;
if (area == null || hash == 0 || (hash == _lastAreaHash && area.Id == _lastAreaId)) return Task.FromResult(false);
_lastAreaHash = hash;
_lastAreaId = area.Id;
Log.Info("[AreaReport] " + area.Name + " (" + area.Id + ", instance " + hash + ")");
return Task.FromResult(true); // This iteration is handled; lower priority tasks wait.
}
public MessageResult Message(Message message) => MessageResult.Unprocessed;
public Task<LogicResult> Logic(Logic logic) => Task.FromResult(LogicResult.Unprovided);
}
#region InventoryTask
internal sealed class InventoryReport : ITask
{
private static readonly ILog Log = Logger.GetLoggerInstanceForType();
private readonly WaitTimer _interval = new WaitTimer(TimeSpan.FromSeconds(5));
public string Name => "InventoryReport";
public string Description => "Lower priority: count carried items at most once every five seconds.";
public string Author => "Your name";
public string Version => "1.0.0";
public void Start() { _interval.Stop(); }
public void Stop() { }
public void Tick() { }
public Task<bool> Run()
{
if (!_interval.IsFinished || !LokiPoe.IsInGame || LokiPoe.Me == null)
return Task.FromResult(false);
_interval.Reset();
var items = LokiPoe.InstanceInfo.GetPlayerInventoryItemsBySlot(InventorySlot.Main);
Log.Info("[InventoryReport] Carried item entries: " + items.Count);
// Count is item entries/stacks, not stack quantities or occupied grid squares.
// Do not retain Item wrappers across loading screens or long waits.
return Task.FromResult(true);
}
public MessageResult Message(Message message) => MessageResult.Unprocessed;
public Task<LogicResult> Logic(Logic logic) => Task.FromResult(LogicResult.Unprovided);
}
#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.