This is the first example that can send game input#
Independent action lab: this uses AcademyWorkshop, not YourFollowBot. Completing FollowBot is not a prerequisite.
AcademyWorkshop is a different bot from AcademyBot. Selecting and starting it does not automatically farm, move or attack. A checkbox permits input; each button queues one attempt.
Use a deliberate test session where you can observe the result and stop immediately. Movement or an attack can have real in-game consequences. The examples were compiled, not live-play validated on your character or build.
Do not use this exercise unattended. It contains no survival, loot, navigation recovery or encounter strategy. A complete class library is not the same as a production-ready autonomous bot.
Prepare the exact components#
- Build AcademyWorkshop, AcademyRoutine and AcademyMover. Install their matching output folders with the test client closed.
- Select AcademyWorkshop as the bot. Select AcademyRoutine and AcademyMover before pressing Start; the workshop retains the selected teaching instances for that run.
- For movement, bind the actual Move-only action on the game skill bar. Do not substitute an attack binding.
- For a skill test, open AcademyRoutine's panel and enter a usable DPB slot number. Leave -1 if you do not intend to attack.
- Enter a loaded area, Start the workshop, open its panel and read its warning.
- Enable Allow one-shot game input. Press exactly one action button and inspect the logs and game result before doing anything else.
Nothing happens until both permission and a button request are present. The buttons queue intent; they do not perform game reads on the WPF UI thread.
Keep intent, action and observation separate#
The bot consumes a queued request and validates live state. It uses the public managed input-session facade as a lifecycle operation; no lower-level implementation is needed in your extension.
An already active session is not silently taken over. The workshop rejects that attempt. When it opens its own session, the finally block releases its key state and closes only that session.
A movement request captures position, area hash and zone ID, then sends one step. It uses a stopwatch and yields on every bot tick until 750 ms have elapsed, abandoning the measurement on loading, death, a changed area or revoked permission. Actual elapsed time also depends on bot ticks. No movement retry is hidden in that wait. A skill request logs the routine result; it does not claim a hit.
The marker for ownership is set before enabling so cleanup is attempted even if session setup throws. Stop disposes the coroutine, clears pending intent and repeats ownership-aware cleanup as a fallback. Revoking permission releases owned input on the next bot tick. Start/Tick faults stop the workshop; cleanup still attempts both selected components if one Stop fails.
Where this goes: AcademyWorkshop.Run and its helpers. UI request fields, buttons and lifecycle methods are in the complete downloadable project.
private async Task Run()
{
while (true)
{
var request = Interlocked.Exchange(ref _request, 0);
if (request != 0 && _allowInput)
{
if (!LokiPoe.IsInGame || LokiPoe.Me == null || LokiPoe.Me.IsDead)
{
Log.Info("[AcademyWorkshop] Request discarded: a loaded living player is required. Click again only after it is ready.");
await Coroutine.Yield();
continue;
}
// Only this bot owns this short input session. Never switch off somebody else's session.
if (LokiPoe.ProcessHookManager.IsEnabled)
Log.Warn("[AcademyWorkshop] Another input session is active. No action sent.");
else
{
try
{
_ownsInput = true;
LokiPoe.ProcessHookManager.Enable();
if (LokiPoe.ProcessHookManager.IsEnabled)
{
if (request == 1) await MoveOnce();
else if (_routine != null)
{
var result = await _routine.Logic(new Logic("academy.attack-once", this));
Log.Info("[AcademyWorkshop] Routine handled request: " + result);
}
else Log.Info("[AcademyWorkshop] Select AcademyRoutine before Start.");
}
else Log.Warn("[AcademyWorkshop] Input session not ready. No retry.");
}
finally { ReleaseInput(); }
}
}
await Coroutine.Yield();
}
}
private async Task MoveOnce()
{
if (_mover == null) { Log.Info("[AcademyWorkshop] Select AcademyMover before Start."); return; }
var target = LokiPoe.ObjectManager.GetObjectsByType<AreaTransition>()
.Where(item => item.IsTargetable).OrderBy(item => item.Distance).FirstOrDefault();
if (target == null) { Log.Info("[AcademyWorkshop] No visible transition. No action."); return; }
var destination = target.Position; // Keep values, not a stale object across an await.
var before = LokiPoe.MyPosition;
var area = LokiPoe.LocalData.AreaHash; // Names are not instance identities.
var areaId = LokiPoe.CurrentWorldArea?.Id;
ExilePather.Reload();
if (!CanObserve(area, areaId)) return; // Loading or permission may change during planning.
if (!_mover.MoveTowards(destination)) { Log.Info("[AcademyWorkshop] No movement command accepted."); return; }
var observation = Stopwatch.StartNew();
while (observation.ElapsedMilliseconds < 750 && CanObserve(area, areaId))
await Coroutine.Yield(); // No extra movement; give control back on every tick.
if (CanObserve(area, areaId))
Log.Info("[AcademyWorkshop] Observed displacement: " + before.Distance(LokiPoe.MyPosition).ToString("F1") + ". Remaining distance: " + destination.Distance(LokiPoe.MyPosition).ToString("F1"));
}
private bool CanObserve(uint area, string areaId) => _running && _allowInput && area != 0 && areaId != null &&
LokiPoe.IsInGame && LokiPoe.Me != null && !LokiPoe.Me.IsDead &&
LokiPoe.LocalData.AreaHash == area && LokiPoe.CurrentWorldArea?.Id == areaId;
private void ReleaseInput()
{
if (!_ownsInput) return;
_ownsInput = false;
try { LokiPoe.ProcessHookManager.ClearAllKeyStates(); }
finally { LokiPoe.ProcessHookManager.Disable(); }
}The movement log separates accepted request from measured displacement. A missing component, target or binding produces a diagnostic and ends the attempt.
Manual acceptance checklist#
- Permission off: button clicks do not produce an action.
- Permission on but no click: the bot remains idle.
- No visible transition: movement reports no target, with no automatic retry.
- Invalid skill slot: no skill request succeeds.
- One valid movement request: inspect the API result, displacement and remaining distance separately.
- Stop during the observation wait: no new request is sent and owned input is released.
- Restart: no old queued click executes. If you left the checkbox enabled, it permits new clicks only; it does not create an action by itself.
Do not run this checklist against somebody else's character or an unattended account. These are manual runtime checks for the developer; they are not claimed as performed by the website build.
From one action to a reliable behavior#
Once the one-shot result is understood, move the decision into a task with a small state machine: idle, requested, waiting for evidence, completed or failed. Keep a timeout, a bounded attempt count and a cancellation path.
Prefer progress evidence appropriate to the action: distance decreased, a target disappeared, an inventory item changed or a UI state became ready. Do not use arbitrary long sleeps as proof of success.
Only then introduce repeated decisions. Keep the bot responsible for priorities, the routine for combat decisions and the mover for movement requests.
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.
AcademyWorkshop/AcademyWorkshop.cs
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using DreamPoeBot.Loki.Bot;
using DreamPoeBot.Loki.Bot.Pathfinding;
using DreamPoeBot.Loki.Common;
using DreamPoeBot.Loki.Coroutine;
using DreamPoeBot.Loki.Game;
using DreamPoeBot.Loki.Game.Objects;
using log4net;
using Message = DreamPoeBot.Loki.Bot.Message;
namespace DeveloperAcademy
{
// Optional advanced exercise. This bot CAN send input, one button press at a time.
public sealed class AcademyWorkshop : IBot
{
private static readonly ILog Log = Logger.GetLoggerInstanceForType();
private Coroutine _loop;
private UserControl _control;
private volatile bool _allowInput;
private volatile bool _running;
private int _request;
private bool _ownsInput;
private IRoutine _routine;
private IPlayerMover _mover;
public string Name => "AcademyWorkshop";
public string Description => "Opt-in lab: send one movement or skill command. Not an autonomous farming bot.";
public string Author => "Your name";
public string Version => "1.0.0";
public JsonSettings Settings => null;
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 = "This lab CAN move or attack. Select AcademyMover / AcademyRoutine before Start. Bind Move-only for movement; choose the skill slot in AcademyRoutine for a skill test. Each button queues one attempt, with no automatic retries.", TextWrapping = System.Windows.TextWrapping.Wrap });
var consent = new CheckBox { Content = "Allow one-shot game input", Margin = new System.Windows.Thickness(0, 12, 0, 8) };
consent.Checked += (sender, args) => _allowInput = true;
consent.Unchecked += (sender, args) => { _allowInput = false; Interlocked.Exchange(ref _request, 0); };
panel.Children.Add(consent);
AddButton(panel, "Move one step toward the nearest transition", 1);
AddButton(panel, "Use the chosen skill once on the nearest hostile", 2);
_control = new UserControl { Content = panel };
return _control;
}
}
private void AddButton(Panel panel, string label, int request)
{
var button = new Button { Content = label, Margin = new System.Windows.Thickness(0, 4, 0, 0) };
button.Click += (sender, args) =>
{
if (_allowInput && _running) Interlocked.CompareExchange(ref _request, request, 0);
};
panel.Children.Add(button);
}
public void Initialize() { }
public void Deinitialize() { Stop(); }
public void Start()
{
Stop();
var routine = RoutineManager.Current;
var mover = PlayerMoverManager.Current;
_routine = routine?.Name == "AcademyRoutine" ? routine : null;
_mover = mover?.Name == "AcademyMover" ? mover : null;
try
{
_routine?.Start();
_mover?.Start();
_loop = new Coroutine(Run);
_running = true;
}
catch { Stop(); throw; }
Log.Info("[AcademyWorkshop] Waiting for explicit input permission and a button request.");
}
public void Tick()
{
if (!_running) return;
try
{
// UI changes only record intent. Release owned input on the bot thread.
if (!_allowInput) ReleaseInput();
_routine?.Tick();
_mover?.Tick();
if (_loop != null && !_loop.IsFinished) _loop.Resume();
}
catch { Stop(); throw; } // No active session or queued action survives a fault.
}
public void Stop()
{
_running = false;
Interlocked.Exchange(ref _request, 0);
var loop = _loop;
_loop = null;
try { loop?.Dispose(); }
finally
{
var routine = _routine;
var mover = _mover;
_routine = null;
_mover = null;
try { ReleaseInput(); }
finally
{
try { routine?.Stop(); }
finally { mover?.Stop(); }
}
}
}
#region OneShotAction
private async Task Run()
{
while (true)
{
var request = Interlocked.Exchange(ref _request, 0);
if (request != 0 && _allowInput)
{
if (!LokiPoe.IsInGame || LokiPoe.Me == null || LokiPoe.Me.IsDead)
{
Log.Info("[AcademyWorkshop] Request discarded: a loaded living player is required. Click again only after it is ready.");
await Coroutine.Yield();
continue;
}
// Only this bot owns this short input session. Never switch off somebody else's session.
if (LokiPoe.ProcessHookManager.IsEnabled)
Log.Warn("[AcademyWorkshop] Another input session is active. No action sent.");
else
{
try
{
_ownsInput = true;
LokiPoe.ProcessHookManager.Enable();
if (LokiPoe.ProcessHookManager.IsEnabled)
{
if (request == 1) await MoveOnce();
else if (_routine != null)
{
var result = await _routine.Logic(new Logic("academy.attack-once", this));
Log.Info("[AcademyWorkshop] Routine handled request: " + result);
}
else Log.Info("[AcademyWorkshop] Select AcademyRoutine before Start.");
}
else Log.Warn("[AcademyWorkshop] Input session not ready. No retry.");
}
finally { ReleaseInput(); }
}
}
await Coroutine.Yield();
}
}
private async Task MoveOnce()
{
if (_mover == null) { Log.Info("[AcademyWorkshop] Select AcademyMover before Start."); return; }
var target = LokiPoe.ObjectManager.GetObjectsByType<AreaTransition>()
.Where(item => item.IsTargetable).OrderBy(item => item.Distance).FirstOrDefault();
if (target == null) { Log.Info("[AcademyWorkshop] No visible transition. No action."); return; }
var destination = target.Position; // Keep values, not a stale object across an await.
var before = LokiPoe.MyPosition;
var area = LokiPoe.LocalData.AreaHash; // Names are not instance identities.
var areaId = LokiPoe.CurrentWorldArea?.Id;
ExilePather.Reload();
if (!CanObserve(area, areaId)) return; // Loading or permission may change during planning.
if (!_mover.MoveTowards(destination)) { Log.Info("[AcademyWorkshop] No movement command accepted."); return; }
var observation = Stopwatch.StartNew();
while (observation.ElapsedMilliseconds < 750 && CanObserve(area, areaId))
await Coroutine.Yield(); // No extra movement; give control back on every tick.
if (CanObserve(area, areaId))
Log.Info("[AcademyWorkshop] Observed displacement: " + before.Distance(LokiPoe.MyPosition).ToString("F1") + ". Remaining distance: " + destination.Distance(LokiPoe.MyPosition).ToString("F1"));
}
private bool CanObserve(uint area, string areaId) => _running && _allowInput && area != 0 && areaId != null &&
LokiPoe.IsInGame && LokiPoe.Me != null && !LokiPoe.Me.IsDead &&
LokiPoe.LocalData.AreaHash == area && LokiPoe.CurrentWorldArea?.Id == areaId;
private void ReleaseInput()
{
if (!_ownsInput) return;
_ownsInput = false;
try { LokiPoe.ProcessHookManager.ClearAllKeyStates(); }
finally { LokiPoe.ProcessHookManager.Disable(); }
}
#endregion
public MessageResult Message(Message message) => MessageResult.Unprocessed;
public Task<LogicResult> Logic(Logic logic) => Task.FromResult(LogicResult.Unprovided);
}
}
AcademyRoutine/AcademyRoutine.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.Game;
using DreamPoeBot.Loki.Game.Objects;
using log4net;
using Message = DreamPoeBot.Loki.Bot.Message;
namespace DeveloperAcademy
{
public sealed class AcademyRoutine : IRoutine
{
private static readonly ILog Log = Logger.GetLoggerInstanceForType();
private readonly WaitTimer _interval = new WaitTimer(TimeSpan.FromSeconds(2));
private UserControl _control;
private volatile int _skillSlot = -1;
private volatile bool _listSkills;
public string Name => "AcademyRoutine";
public string Description => "Observe targets; optionally use one chosen skill when the workshop requests it.";
public string Author => "Your name";
public string Version => "1.0.0";
public JsonSettings Settings => null;
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 = "AcademyBot only observes. AcademyWorkshop can request one skill use. Enter the skill slot reported by DPB (1–13), not the keyboard key.", TextWrapping = System.Windows.TextWrapping.Wrap });
var slot = new TextBox { Text = "-1", MaxLength = 2, Margin = new System.Windows.Thickness(0, 8, 0, 0) };
slot.TextChanged += (sender, args) =>
{
int parsed;
_skillSlot = int.TryParse(slot.Text, out parsed) && parsed >= 1 && parsed <= 13 ? parsed : -1;
};
panel.Children.Add(slot);
var list = new Button { Content = "List bound skill slots in the log", Margin = new System.Windows.Thickness(0, 8, 0, 0) };
list.Click += (sender, args) => _listSkills = true;
panel.Children.Add(list);
_control = new UserControl { Content = panel };
return _control;
}
}
public void Initialize() { }
public void Deinitialize() { Stop(); }
public void Start() { _listSkills = false; _interval.Stop(); Log.Info("[AcademyRoutine] Started in observation mode."); }
public void Stop() { _listSkills = false; }
// A tick is not automatically a request to run combat logic.
#region InspectSkillSlots
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);
}
}
#endregion
public MessageResult Message(Message message) => MessageResult.Unprocessed;
#region ChooseTarget
public Task<LogicResult> Logic(Logic logic)
{
if (logic.Id == "academy.attack-once")
return Task.FromResult(AttackOnce());
// AcademyBot sends this sample-specific ID. Other bots may use different IDs.
if (logic.Id != "academy.inspect-target" || !_interval.IsFinished)
return Task.FromResult(LogicResult.Unprovided);
_interval.Reset();
if (!LokiPoe.IsInGame || LokiPoe.Me == null)
return Task.FromResult(LogicResult.Unprovided);
var target = LokiPoe.ObjectManager.GetObjectsByType<Monster>()
.Where(monster => monster.IsAliveHostile && monster.IsTargetable)
.OrderBy(monster => monster.Distance)
.FirstOrDefault();
if (target == null)
return Task.FromResult(LogicResult.Unprovided);
Log.Info("[AcademyRoutine] Candidate: " + target.Name + ". No attack performed.");
return Task.FromResult(LogicResult.Provided);
}
#endregion
#region UseOneSkill
private LogicResult AttackOnce()
{
// Take one settings snapshot: a UI edit must not change the slot mid-request.
var slot = _skillSlot;
if (!LokiPoe.IsInGame || LokiPoe.Me == null || LokiPoe.Me.IsDead ||
!LokiPoe.ProcessHookManager.IsEnabled)
{
Log.Info("[AcademyRoutine] Skill request skipped: loaded living player and caller-owned input session required.");
return LogicResult.Unprovided;
}
if (slot < 1 || slot > 13)
{
Log.Info("[AcademyRoutine] Choose a skill slot from 1 to 13 in the routine panel first.");
return LogicResult.Unprovided;
}
var target = LokiPoe.ObjectManager.GetObjectsByType<Monster>()
.Where(monster => monster.IsAliveHostile && monster.IsTargetable && monster.Distance <= 30)
.OrderBy(monster => monster.Distance).FirstOrDefault();
if (target == null) { Log.Info("[AcademyRoutine] No eligible target within 30 units."); return LogicResult.Unprovided; }
var skill = LokiPoe.InGameState.SkillBarHud.Slot(slot);
if (skill == null || !skill.CanUse())
{
Log.Info("[AcademyRoutine] The chosen slot has no usable skill. Check binding, resources and cooldown.");
return LogicResult.Unprovided;
}
var result = LokiPoe.InGameState.SkillBarHud.UseAt(slot, true, target.Position);
Log.Info("[AcademyRoutine] Skill request: " + result + ". This is not a hit or kill confirmation.");
return result == LokiPoe.InGameState.UseResult.None ? LogicResult.Provided : LogicResult.Unprovided;
}
#endregion
}
}
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;
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 PathfindingCommand 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 = new PathfindingCommand(start, position);
if (!ExilePather.FindPath(ref command) || command.Error != PathfindingError.None)
{
Log.Info("[AcademyMover] No successful path result: " + command.Error);
return false;
}
CurrentCommand = command;
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 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.