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.
dotnet --list-sdks2. Download and extract the matching source kit#
- Use the download button at the top of this lesson. Choose the same product as your client.
- Extract the entire ZIP into a working folder, for example C:\DpbAcademy. Do not edit files inside the ZIP preview.
- Open that folder in your editor. Keep Directory.Build.props beside the project folders: it provides their shared target and references.
- Open PowerShell in that extracted folder. Replace C:\DPB below with your actual client folder; quotes are required if it contains spaces.
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.
.\Build.ps1 -DpbDirectory 'C:\DPB' -Project AcademyObserver
.\Build.ps1 -DpbDirectory 'C:\DPB' -Project AcademyBotBoth 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.
dotnet build .\AcademyObserver\AcademyObserver.csproj -c Release '-p:DpbDirectory=C:\DPB'
dotnet build .\AcademyBot\AcademyBot.csproj -c Release '-p:DpbDirectory=C:\DPB'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#
- Stop and close your test DPB instance before replacing DLLs.
- Copy artifacts/Plugins/AcademyObserver into the client's Plugins folder.
- Copy artifacts/Plugins/AcademyBot into the same Plugins folder.
- Check the exact structure below. The folder and entry DLL names must match; avoid an extra nested AcademyObserver folder.
- 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.
DPB/
Plugins/
AcademyObserver/
AcademyObserver.dll
AcademyBot/
AcademyBot.dllDo 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#
- Use your normal DPB connection workflow and enter a loaded game area.
- 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.
- Press Start with AcademyBot selected. Enabling the plugin is not the same as starting the bot.
- Look for the Enabled line, the AcademyBot Started line and then the Area line in DPB's log.
- 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.
- Press Stop. The observer should no longer receive ticks from AcademyBot.
[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>)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 &.
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.
<DpbDirectory Condition="'$(DpbDirectory)' == ''">C:\DPB</DpbDirectory>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#
- Change the Area: text in AcademyObserver.cs to My first plugin — area:.
- Rebuild AcademyObserver using the same command.
- Close the test client, replace its AcademyObserver.dll, restart, re-enable the plugin if needed and Start AcademyBot.
- Verify the changed text. This proves you loaded the new artifact, not just that the source compiled.
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
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
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.