Independent labs · 15–25 min exercise

Setup & loading lab: the area-observer plugin

An independent build-and-load exercise with AcademyObserver and a supplied read-only host. Skip repeated setup if you already completed the nearby-alert project.

After this lesson

AcademyObserver appears in DPB and reports the current area while AcademyBot runs. Then you change the log text and verify your rebuilt DLL.

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

1. Install the build tools once#

New to DPB and looking for one guided first project? Use the nearby-enemy alert instead. This lab is an alternative and a reusable setup reference, not a mandatory second beginning.

Install a Windows .NET SDK with the dotnet command available. The .NET 8 SDK is suitable for these projects. A runtime alone cannot compile code.

For DPB1, also install the .NET Framework 4.8 Developer Pack / targeting pack. Installing only its runtime is not enough. DPB2 uses the Windows desktop targeting support for .NET 8 instead.

The selected kit targets .NET Framework 4.8 (net48) and x64. Keep these settings for the first exercise. Use DreamPoeBot.exe, log4net.dll and Newtonsoft.Json.dll from one complete DPB1 installation.

Where this goes: Run in PowerShell. At least one compatible SDK should be listed.

powershell
dotnet --list-sdks

2. Download and extract the matching source kit#

  1. Use the download button at the top of this lesson. Choose the same product as your client.
  2. Extract the entire ZIP into a working folder, for example C:\DpbAcademy. Do not edit files inside the ZIP preview.
  3. Open that folder in your editor. Keep Directory.Build.props beside the project folders: it provides their shared target and references.
  4. Open PowerShell in that extracted folder. Replace C:\DPB below with your actual client folder; quotes are required if it contains spaces.
Keep in mind

The ZIP contains original C# source, project files, a build helper and instructions. It does not contain DPB binaries, client configuration, credentials or a prebuilt plugin.

3. Build the two first-lesson projects#

Open AcademyObserver/AcademyObserver.cs to see the complete plugin class. Open AcademyBot/AcademyBot.cs to see the small host that calls it. You do not need to paste a fragment into an unknown class.

If local PowerShell policy prevents running a script, do not disable policy globally. You can use the direct dotnet build command in the next section.

Where this goes: PowerShell, in the extracted kit root. Build.ps1 compiles only; it never installs or starts anything.

powershell
.\Build.ps1 -DpbDirectory 'C:\DPB' -Project AcademyObserver
.\Build.ps1 -DpbDirectory 'C:\DPB' -Project AcademyBot
Check your result

Both commands finish with Build succeeded. The output folders contain AcademyObserver.dll and AcademyBot.dll under artifacts/Plugins/<project name>. If the first build fails, use the first error in the output, not the last summary line.

The equivalent command without the helper#

Where this goes: Same extracted folder. The project settings still come from Directory.Build.props.

powershell
dotnet build .\AcademyObserver\AcademyObserver.csproj -c Release '-p:DpbDirectory=C:\DPB'
dotnet build .\AcademyBot\AcademyBot.csproj -c Release '-p:DpbDirectory=C:\DPB'
Keep in mind

If your DPB1 executable was renamed, pass -DpbAssembly 'C:\DPB\YourClient.exe' to Build.ps1, or -p:DpbAssembly=... to dotnet build. DPB2 must reference the managed DreamPoeBot.dll, not its launcher .exe.

4. Put each DLL where the loader expects it#

  1. Stop and close your test DPB instance before replacing DLLs.
  2. Copy artifacts/Plugins/AcademyObserver into the client's Plugins folder.
  3. Copy artifacts/Plugins/AcademyBot into the same Plugins folder.
  4. Check the exact structure below. The folder and entry DLL names must match; avoid an extra nested AcademyObserver folder.
  5. Start your test DPB instance normally. Enable AcademyObserver in the plugins list. Select AcademyBot in the bot selector.

Where this goes: Final client-side layout. The PDB is optional for debugging.

text
DPB/
  Plugins/
    AcademyObserver/
      AcademyObserver.dll
    AcademyBot/
      AcademyBot.dll
Keep in mind

Do not copy DreamPoeBot, log4net or Newtonsoft.Json alongside these examples. DPB supplies those assemblies. All extension types use Plugins folders, including a bot, routine or mover.

5. Verify the first result#

  1. Use your normal DPB connection workflow and enter a loaded game area.
  2. Use a log filter that includes Info, such as Full or Debug-Info-Warn on the reviewed clients. A Debug-only filter can hide these example messages.
  3. Press Start with AcademyBot selected. Enabling the plugin is not the same as starting the bot.
  4. Look for the Enabled line, the AcademyBot Started line and then the Area line in DPB's log.
  5. Enter a new instance through your normal test workflow. The observer should report its name, zone ID and instance hash, including when the displayed name is unchanged.
  6. Press Stop. The observer should no longer receive ticks from AcademyBot.
text
[AcademyObserver] Enabled. Waiting for a bot to call Tick.
[AcademyBot] Started. Observation only; no movement or combat.
[AcademyObserver] Area: <current area name> (<zone ID>, instance <hash>)
Check your result

A changed hash or zone ID produces one log, at most every two seconds. The same area stays quiet. An observed loading state also resets the observer; this is not a complete travel-confirmation system.

Optional: make the editor recognize DPB types#

The DpbDirectory argument applies to a command-line build. Your editor does not automatically inherit it. For completion and reference resolution, set a local default in the shared project settings.

Add the line below inside the existing PropertyGroup in Directory.Build.props, before DpbAssembly. Replace the example with your client folder. The condition keeps command-line overrides working. In XML, a path containing & must use &amp;.

Open AcademyObserver/AcademyObserver.csproj in Visual Studio, or the extracted root in your editor. Restore/build after changing references. Keep Directory.Build.props beside the project folders.

Where this goes: Your local copy of Directory.Build.props, inside its existing PropertyGroup. Do not share your personal installation path in a public source package.

xml
<DpbDirectory Condition="'$(DpbDirectory)' == ''">C:\DPB</DpbDirectory>
Keep in mind

This is a class library, not a standalone application. Build the DLL and load it in DPB; pressing Run in an editor is not a replacement for the installation and host steps above.

6. Make it unmistakably yours#

  1. Change the Area: text in AcademyObserver.cs to My first plugin — area:.
  2. Rebuild AcademyObserver using the same command.
  3. Close the test client, replace its AcademyObserver.dll, restart, re-enable the plugin if needed and Start AcademyBot.
  4. Verify the changed text. This proves you loaded the new artifact, not just that the source compiled.
Check your result

Your edited message appears in DPB. You now have the complete edit → build → install → select → run feedback loop.

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);
    }
}
AcademyBot/AcademyBot.cs
Complete file · DPB1
using System;
using System.Linq;
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 log4net;
using Message = DreamPoeBot.Loki.Bot.Message;

namespace DeveloperAcademy
{
    public sealed class AcademyBot : IBot
    {
        private static readonly ILog Log = Logger.GetLoggerInstanceForType();
        private Coroutine _loop;
        private UserControl _control;
        private IPlugin[] _plugins = Array.Empty<IPlugin>();
        private IRoutine _routine;
        private IPlayerMover _mover;
        public string Name => "AcademyBot";
        public string Description => "A teaching host: run the Academy components without game input.";
        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 = "Enable AcademyNearbyAlert or AcademyObserver. Optionally select AcademyRoutine and AcademyMover. This bot only drives named teaching components.",
                TextWrapping = System.Windows.TextWrapping.Wrap, Margin = new System.Windows.Thickness(12) }
        });
        public void Initialize() { }
        public void Deinitialize() { Stop(); }

        #region BotLifecycle
        public void Start()
        {
            Stop();
            // Keep the instances we started, so Stop never targets a later selection.
            _plugins = Observers();
            var selectedRoutine = RoutineManager.Current;
            _routine = selectedRoutine?.Name == "AcademyRoutine" ? selectedRoutine : null;
            var selectedMover = PlayerMoverManager.Current;
            _mover = selectedMover?.Name == "AcademyMover" ? selectedMover : null;
            try
            {
                foreach (var plugin in _plugins)
                    (plugin as IStartStopEvents)?.Start();
                _routine?.Start();
                _mover?.Start();
                _loop = new Coroutine(Run);
            }
            catch { Stop(); throw; }
            Log.Info("[AcademyBot] Started. Observation only; no movement or combat.");
        }

        public void Tick()
        {
            if (_loop == null) return;
            try
            {
                foreach (var plugin in _plugins)
                    (plugin as ITickEvents)?.Tick();
                _routine?.Tick();
                _mover?.Tick();
                if (_loop != null && !_loop.IsFinished)
                    _loop.Resume();
            }
            catch { Stop(); throw; }
        }

        public void Stop()
        {
            var loop = _loop;
            _loop = null;
            try { loop?.Dispose(); }
            finally
            {
                var plugins = _plugins;
                var routine = _routine;
                var mover = _mover;
                _plugins = Array.Empty<IPlugin>();
                _routine = null;
                _mover = null;
                foreach (var plugin in plugins) StopComponent(plugin as IStartStopEvents);
                StopComponent(routine);
                StopComponent(mover);
            }
        }
        private static void StopComponent(IStartStopEvents component)
        {
            try { component?.Stop(); }
            catch (Exception error) { Log.Warn("[AcademyBot] A component failed to stop; continuing cleanup.", error); }
        }
        #endregion

        #region BotLoop
        private async Task Run()
        {
            while (true)
            {
                if (LokiPoe.IsInGame && LokiPoe.Me != null)
                {
                    if (_routine != null)
                        await _routine.Logic(new Logic("academy.inspect-target", this));
                }
                // Return control to DPB between iterations. Never use Thread.Sleep here.
                await Coroutine.Yield();
            }
        }
        #endregion

        // Intentional teaching scope: do not drive unrelated installed extensions.
        // This name check is not an authentication or trust boundary.
        private static IPlugin[] Observers() => PluginManager.EnabledPlugins
            .Where(plugin => plugin.Name == "AcademyObserver" || plugin.Name == "AcademySettings" || plugin.Name == "AcademyNearbyAlert").ToArray();
        public MessageResult Message(Message message) => MessageResult.Unprocessed;
        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.