Independent labs · 15 min exercise

Build a plugin that does useful work

Extend the independent area-observer lab with lifecycle callbacks and messages. For a first guided project, use the nearby-enemy alert.

After this lesson

You can extend a working plugin, choose optional interfaces and verify that it is actually being ticked.

Download DPB1 source kit ↓ 8 complete projects · source only · .NET Framework 4.8 (net48)

Start from the complete class, not an empty interface#

AcademyObserver implements IPlugin plus ITickEvents and IStartStopEvents. The shared properties describe the extension and expose its settings/control. It has no persisted settings yet, so Settings returns null.

The public, parameterless class is the loader entry point. Keep its containing project and output folder name stable until you have completed the first build/run loop. Your class name, displayed Name and assembly filename are related conventions, not interchangeable settings.

Do a small amount of work on each tick#

The timer is created once and reused. A new timer on every Tick would never measure the intended interval. Checking the timer before enumerating game objects also avoids unnecessary repeated work.

The early returns are not failures: disabled, not due yet, not in game, area not available and unchanged area are all valid reasons to do nothing.

Where this goes: Already inside AcademyObserver/AcademyObserver.cs. The timer, enabled flag and previous area are fields on the same class.

C# · from the source kit
public void Tick()
{
    if (!_enabled) return;
    // Loading screens and character selection are normal, not errors.
    if (!LokiPoe.IsInGame || LokiPoe.Me == null)
    {
        ResetArea(); // Reacquire state when the player becomes available again.
        return;
    }
    if (!_interval.IsFinished) return;
    _interval.Reset();
    var area = LokiPoe.CurrentWorldArea;
    var hash = LokiPoe.LocalData.AreaHash;
    if (area == null || hash == 0 || (hash == _lastAreaHash && area.Id == _lastAreaId))
        return;

    // Name is a label. Hash identifies the instance; Id also distinguishes local zones.
    _lastAreaHash = hash;
    _lastAreaId = area.Id;
    Log.Info("[AcademyObserver] Area: " + area.Name + " (" + area.Id + ", instance " + hash + ")");
}
Check your result

One log on the first valid area and on a changed hash or zone ID, including a new same-name instance. Hash zero is treated as not ready. Name is a human-readable label, not identity.

Handle only the message you own#

This handler clears the previous area identity so a later tick can report it again. Sending a Message does not itself make a tick happen.

A caller can pass new Message("academy.reset-observer", this) to the plugin's Message method. Call from your normal bot execution context. If you trigger it from WPF, queue the request for Tick instead of reading or changing game state in a button event.

Where this goes: AcademyObserver.Message. The ID academy.reset-observer is defined by this sample, not reserved by the DPB platform.

C# · from the source kit
public MessageResult Message(Message message)
{
    // This ID belongs to this sample, not to the DPB platform.
    if (message.Id != "academy.reset-observer")
        return MessageResult.Unprocessed;
    ResetArea();
    return MessageResult.Processed;
}
Check your result

Known message returns Processed; an unknown ID returns Unprocessed. Other extensions can therefore distinguish unsupported requests from handled ones.

Build an alert instead of guessing the next edit#

The nearby-alert course walks through counting monsters directly in a plugin, adding a threshold, remembering whether it already warned and saving settings. It uses a separate AcademyNearbyAlert project so you can keep this area observer unchanged.

Download its focused kit and follow the exact Tick replacements. You do not need to adapt a routine's Logic method or understand asynchronous target selection just to count nearby objects.

Keep in mind

Keep the timer and game-state guards. Movement and combat belong in an explicit workflow with result handling and cancellation, not an unbounded first Tick exercise.

Complete source files

These are the exact C# files in the DPB1 download. Use the complete ZIP for project settings, references, build commands and installation instructions.

AcademyObserver/AcademyObserver.cs
Complete file · DPB1
using System;
using System.Threading.Tasks;
using System.Windows.Controls;
using DreamPoeBot.Loki.Bot;
using DreamPoeBot.Loki.Common;
using DreamPoeBot.Loki.Game;
using log4net;
using Message = DreamPoeBot.Loki.Bot.Message;

namespace DeveloperAcademy
{
    // IPlugin alone does not include Tick, Start or Stop. Opt in explicitly.
    public sealed class AcademyObserver : IPlugin, ITickEvents, IStartStopEvents
    {
        private static readonly ILog Log = Logger.GetLoggerInstanceForType();
        private readonly WaitTimer _interval = new WaitTimer(TimeSpan.FromSeconds(2));
        private UserControl _control;
        private bool _enabled;
        private uint? _lastAreaHash;
        private string _lastAreaId;

        public string Name => "AcademyObserver";
        public string Description => "A read-only first plugin: report the current area.";
        public string Author => "Your name";
        public string Version => "1.0.0";
        public JsonSettings Settings => null; // No persisted settings in this first lesson.
        public UserControl Control => _control ?? (_control = new UserControl
        {
            Content = new TextBlock
            {
                Text = "Enable this plugin, select AcademyBot, then press Start. Watch the log.",
                TextWrapping = System.Windows.TextWrapping.Wrap,
                Margin = new System.Windows.Thickness(12)
            }
        });

        public void Initialize() { }
        public void Deinitialize() { _enabled = false; }
        public void Enable()
        {
            _enabled = true;
            ResetArea();
            Log.Info("[AcademyObserver] Enabled. Waiting for a bot to call Tick.");
        }
        public void Disable() { _enabled = false; Log.Info("[AcademyObserver] Disabled."); }
        public void Start() { ResetArea(); _interval.Stop(); }
        public void Stop() { ResetArea(); }
        private void ResetArea() { _lastAreaHash = null; _lastAreaId = null; }

        #region ObserveArea
        public void Tick()
        {
            if (!_enabled) return;
            // Loading screens and character selection are normal, not errors.
            if (!LokiPoe.IsInGame || LokiPoe.Me == null)
            {
                ResetArea(); // Reacquire state when the player becomes available again.
                return;
            }
            if (!_interval.IsFinished) return;
            _interval.Reset();
            var area = LokiPoe.CurrentWorldArea;
            var hash = LokiPoe.LocalData.AreaHash;
            if (area == null || hash == 0 || (hash == _lastAreaHash && area.Id == _lastAreaId))
                return;

            // Name is a label. Hash identifies the instance; Id also distinguishes local zones.
            _lastAreaHash = hash;
            _lastAreaId = area.Id;
            Log.Info("[AcademyObserver] Area: " + area.Name + " (" + area.Id + ", instance " + hash + ")");
        }
        #endregion

        #region HandleMessage
        public MessageResult Message(Message message)
        {
            // This ID belongs to this sample, not to the DPB platform.
            if (message.Id != "academy.reset-observer")
                return MessageResult.Unprocessed;
            ResetArea();
            return MessageResult.Processed;
        }
        #endregion

        public Task<LogicResult> Logic(Logic logic) => Task.FromResult(LogicResult.Unprovided);
    }
}

Keep building

Look up a specific DPB1 API →
Download integrity and validation scope

Source kit: 26 files, 30,747 bytes. No client binaries or credentials.

SHA-256: f3d229e4e090feadc8713639c7e4c62fe3c6fb5aa54460af87829e0369ba5e95

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.