added items

This commit is contained in:
Boki 2026-02-28 15:13:31 -05:00
parent c3de5fdb63
commit 5f90bc137b
158 changed files with 2316 additions and 512 deletions

View file

@ -5,25 +5,27 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poe2Trade.Core", "src\Poe2Trade.Core\Poe2Trade.Core.csproj", "{6432F6A5-11A0-4960-AFFC-E810D4325C35}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Core", "src\Automata.Core\Automata.Core.csproj", "{6432F6A5-11A0-4960-AFFC-E810D4325C35}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poe2Trade.Game", "src\Poe2Trade.Game\Poe2Trade.Game.csproj", "{97B8362D-777C-4ED1-B964-D6598B333E4C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Game", "src\Automata.Game\Automata.Game.csproj", "{97B8362D-777C-4ED1-B964-D6598B333E4C}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poe2Trade.Screen", "src\Poe2Trade.Screen\Poe2Trade.Screen.csproj", "{F92C5EA2-8999-41BC-9B28-D52AD5F3542C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Screen", "src\Automata.Screen\Automata.Screen.csproj", "{F92C5EA2-8999-41BC-9B28-D52AD5F3542C}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poe2Trade.Items", "src\Poe2Trade.Items\Poe2Trade.Items.csproj", "{9CAB0D49-1E24-4F76-ABF8-9A5ED6819F00}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Items", "src\Automata.Items\Automata.Items.csproj", "{9CAB0D49-1E24-4F76-ABF8-9A5ED6819F00}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poe2Trade.Trade", "src\Poe2Trade.Trade\Poe2Trade.Trade.csproj", "{8F73A696-EB54-4C6F-9603-5A6BAC5D334A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Trade", "src\Automata.Trade\Automata.Trade.csproj", "{8F73A696-EB54-4C6F-9603-5A6BAC5D334A}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poe2Trade.Log", "src\Poe2Trade.Log\Poe2Trade.Log.csproj", "{B68D787D-7A83-4D8F-9F10-0B72C2E99B49}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Log", "src\Automata.Log\Automata.Log.csproj", "{B68D787D-7A83-4D8F-9F10-0B72C2E99B49}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poe2Trade.Bot", "src\Poe2Trade.Bot\Poe2Trade.Bot.csproj", "{188C4F87-153F-4182-B816-9FB56F08CF3A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Bot", "src\Automata.Bot\Automata.Bot.csproj", "{188C4F87-153F-4182-B816-9FB56F08CF3A}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poe2Trade.Inventory", "src\Poe2Trade.Inventory\Poe2Trade.Inventory.csproj", "{F186DDC8-6843-43E9-8BD3-9F914C5E784E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Inventory", "src\Automata.Inventory\Automata.Inventory.csproj", "{F186DDC8-6843-43E9-8BD3-9F914C5E784E}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poe2Trade.Ui", "src\Poe2Trade.Ui\Poe2Trade.Ui.csproj", "{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Ui", "src\Automata.Ui\Automata.Ui.csproj", "{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poe2Trade.Navigation", "src\Poe2Trade.Navigation\Poe2Trade.Navigation.csproj", "{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Navigation", "src\Automata.Navigation\Automata.Navigation.csproj", "{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Memory", "src\Automata.Memory\Automata.Memory.csproj", "{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -74,6 +76,10 @@ Global
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Debug|Any CPU.Build.0 = Debug|Any CPU {D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Release|Any CPU.ActiveCfg = Release|Any CPU {D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Release|Any CPU.Build.0 = Release|Any CPU {D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Release|Any CPU.Build.0 = Release|Any CPU
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{6432F6A5-11A0-4960-AFFC-E810D4325C35} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} {6432F6A5-11A0-4960-AFFC-E810D4325C35} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
@ -86,5 +92,6 @@ Global
{F186DDC8-6843-43E9-8BD3-9F914C5E784E} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} {F186DDC8-6843-43E9-8BD3-9F914C5E784E} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} {859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} {D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

BIN
assets/currency-tab.png.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

12
memory-offsets.json Normal file
View file

@ -0,0 +1,12 @@
{
"ProcessName": "PathOfExileSteam",
"GameStatePattern": "",
"InGameStateOffset": 0,
"IngameDataOffset": 0,
"TerrainDataOffset": 0,
"NumColsOffset": 0,
"NumRowsOffset": 0,
"LayerMeleeOffset": 0,
"BytesPerRowOffset": 0,
"SubTilesPerCell": 23
}

View file

@ -1,11 +1,11 @@
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Game; using Automata.Game;
using Poe2Trade.Inventory; using Automata.Inventory;
using Poe2Trade.Navigation; using Automata.Navigation;
using Poe2Trade.Screen; using Automata.Screen;
using Serilog; using Serilog;
namespace Poe2Trade.Bot; namespace Automata.Bot;
/// <summary> /// <summary>
/// Captures the full endgame atlas as a panorama image. /// Captures the full endgame atlas as a panorama image.

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
<ProjectReference Include="..\Automata.Game\Automata.Game.csproj" />
<ProjectReference Include="..\Automata.Screen\Automata.Screen.csproj" />
<ProjectReference Include="..\Automata.Trade\Automata.Trade.csproj" />
<ProjectReference Include="..\Automata.Log\Automata.Log.csproj" />
<ProjectReference Include="..\Automata.Inventory\Automata.Inventory.csproj" />
<ProjectReference Include="..\Automata.Navigation\Automata.Navigation.csproj" />
</ItemGroup>
</Project>

View file

@ -1,13 +1,13 @@
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Game; using Automata.Game;
using Poe2Trade.Inventory; using Automata.Inventory;
using Poe2Trade.GameLog; using Automata.GameLog;
using Poe2Trade.Navigation; using Automata.Navigation;
using Poe2Trade.Screen; using Automata.Screen;
using Poe2Trade.Trade; using Automata.Trade;
using Serilog; using Serilog;
namespace Poe2Trade.Bot; namespace Automata.Bot;
public class BotStatus public class BotStatus
{ {
@ -52,6 +52,7 @@ public class BotOrchestrator : IAsyncDisposable
public volatile bool ShowFightPositionOverlay = true; public volatile bool ShowFightPositionOverlay = true;
private readonly Dictionary<string, ScrapExecutor> _scrapExecutors = new(); private readonly Dictionary<string, ScrapExecutor> _scrapExecutors = new();
private readonly Dictionary<string, DiamondExecutor> _diamondExecutors = new(); private readonly Dictionary<string, DiamondExecutor> _diamondExecutors = new();
private CraftingExecutor? _craftingExecutor;
// Events // Events
public event Action? StatusUpdated; public event Action? StatusUpdated;
@ -164,6 +165,10 @@ public class BotOrchestrator : IAsyncDisposable
TradeQueue.Clear(); TradeQueue.Clear();
// Stop crafting
_craftingExecutor?.Stop();
_craftingExecutor = null;
// Stop navigation and mapping // Stop navigation and mapping
await Navigation.Stop(); await Navigation.Stop();
KulemakExecutor.Stop(); KulemakExecutor.Stop();
@ -270,6 +275,12 @@ public class BotOrchestrator : IAsyncDisposable
return; return;
} }
} }
if (_craftingExecutor != null && _craftingExecutor.State != CraftingState.Idle
&& _craftingExecutor.State != CraftingState.Done)
{
State = _craftingExecutor.State.ToString();
return;
}
if (KulemakExecutor.State != MappingState.Idle) if (KulemakExecutor.State != MappingState.Idle)
{ {
State = KulemakExecutor.State.ToString(); State = KulemakExecutor.State.ToString();

View file

@ -1,10 +1,10 @@
using System.Diagnostics; using System.Diagnostics;
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Game; using Automata.Game;
using Poe2Trade.Screen; using Automata.Screen;
using Serilog; using Serilog;
namespace Poe2Trade.Bot; namespace Automata.Bot;
/// <summary> /// <summary>
/// Manages the attack state machine (click → hold) with mana monitoring and flask usage. /// Manages the attack state machine (click → hold) with mana monitoring and flask usage.

View file

@ -0,0 +1,207 @@
using Automata.Core;
using Automata.Game;
using Serilog;
namespace Automata.Bot;
public class CraftingExecutor
{
private readonly IGameController _game;
private readonly SavedSettings _config;
private CancellationTokenSource? _cts;
private CraftingState _state = CraftingState.Idle;
public event Action<CraftingState>? StateChanged;
public CraftingState State => _state;
public int CurrentStepIndex { get; private set; }
public int CurrentAttempt { get; private set; }
public int TotalAttempts { get; private set; }
public string? LastItemText { get; private set; }
public CraftingExecutor(IGameController game, SavedSettings config)
{
_game = game;
_config = config;
}
private void SetState(CraftingState s)
{
_state = s;
StateChanged?.Invoke(s);
}
public void Stop()
{
_cts?.Cancel();
SetState(CraftingState.Idle);
Log.Information("Crafting executor stopped");
}
private (int x, int y)? ResolvePosition(string name)
{
var pos = _config.CurrencyPositions.FirstOrDefault(
c => c.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (pos != null && (pos.X != 0 || pos.Y != 0))
return (pos.X, pos.Y);
return null;
}
public async Task RunCraft(CraftRecipe recipe)
{
_cts = new CancellationTokenSource();
var ct = _cts.Token;
CurrentStepIndex = 0;
TotalAttempts = 0;
LastItemText = null;
try
{
SetState(CraftingState.Running);
Log.Information("Starting craft: {Name} ({StepCount} steps)", recipe.Name, recipe.Steps.Count);
// Resolve item position: "Craft Item" entry, fallback to recipe ItemX/ItemY
var itemPos = ResolvePosition("Craft Item");
var itemX = itemPos?.x ?? recipe.ItemX;
var itemY = itemPos?.y ?? recipe.ItemY;
await _game.FocusGame();
await Helpers.RandomDelay(200, 400);
while (CurrentStepIndex < recipe.Steps.Count)
{
ct.ThrowIfCancellationRequested();
var step = recipe.Steps[CurrentStepIndex];
CurrentAttempt = 0;
// Resolve currency position: by name, fallback to step CurrencyX/CurrencyY
int curX = step.CurrencyX, curY = step.CurrencyY;
if (!string.IsNullOrEmpty(step.CurrencyName))
{
var resolved = ResolvePosition(step.CurrencyName);
if (resolved.HasValue)
{
curX = resolved.Value.x;
curY = resolved.Value.y;
}
else
{
Log.Warning("Step {Index}: currency '{Name}' not found, using fallback {X},{Y}",
CurrentStepIndex, step.CurrencyName, step.CurrencyX, step.CurrencyY);
}
}
Log.Information("Step {Index}: {Label} (max {Max}) currency=({CurX},{CurY}) item=({ItemX},{ItemY})",
CurrentStepIndex, step.Label, step.MaxAttempts, curX, curY, itemX, itemY);
var stepDone = false;
while (!stepDone)
{
ct.ThrowIfCancellationRequested();
// 1. Right-click currency
SetState(CraftingState.Running);
await _game.RightClickAt(curX, curY);
await Helpers.RandomDelay(80, 150);
ct.ThrowIfCancellationRequested();
// 2. Left-click item
await _game.LeftClickAt(itemX, itemY);
await Helpers.RandomDelay(150, 250);
ct.ThrowIfCancellationRequested();
// 3. Read item via Ctrl+C
SetState(CraftingState.ReadingItem);
await _game.MoveMouseTo(itemX, itemY);
await Helpers.RandomDelay(80, 120);
await _game.HoldCtrl();
await Helpers.Sleep(30);
await _game.PressKey(InputSender.VK.C);
await _game.ReleaseCtrl();
await Helpers.RandomDelay(40, 80);
var itemText = ClipboardHelper.Read();
LastItemText = itemText;
StateChanged?.Invoke(_state); // notify UI of LastItemText update
CurrentAttempt++;
TotalAttempts++;
// 4. Check mods
SetState(CraftingState.CheckingMods);
if (step.RequiredMods.Count == 0)
{
// No condition — single application, advance
Log.Information("Step {Index}: no condition, advancing", CurrentStepIndex);
stepDone = true;
}
else
{
var matched = CheckMods(itemText, step.RequiredMods, step.MatchAll);
if (matched)
{
Log.Information("Step {Index}: condition met after {Attempts} attempts",
CurrentStepIndex, CurrentAttempt);
stepDone = true;
}
else if (step.MaxAttempts > 0 && CurrentAttempt >= step.MaxAttempts)
{
Log.Warning("Step {Index}: max attempts ({Max}) reached",
CurrentStepIndex, step.MaxAttempts);
if (step.OnFailGoTo.HasValue)
{
Log.Information("Jumping to step {Target}", step.OnFailGoTo.Value);
CurrentStepIndex = step.OnFailGoTo.Value;
stepDone = true; // break inner loop, outer loop continues at new index
continue;
}
else
{
SetState(CraftingState.Failed);
Log.Error("Craft failed: max attempts on step {Index}", CurrentStepIndex);
return;
}
}
}
if (!stepDone)
await Helpers.RandomDelay(30, 80);
}
CurrentStepIndex++;
}
SetState(CraftingState.Done);
Log.Information("Craft complete: {Name} ({Total} total attempts)", recipe.Name, TotalAttempts);
}
catch (OperationCanceledException)
{
SetState(CraftingState.Idle);
Log.Information("Craft cancelled");
}
catch (Exception ex)
{
SetState(CraftingState.Failed);
Log.Error(ex, "Craft failed unexpectedly");
}
}
private static bool CheckMods(string itemText, List<string> requiredMods, bool matchAll)
{
if (string.IsNullOrEmpty(itemText)) return false;
if (matchAll)
{
return requiredMods.All(mod =>
itemText.Contains(mod, StringComparison.OrdinalIgnoreCase));
}
else
{
return requiredMods.Any(mod =>
itemText.Contains(mod, StringComparison.OrdinalIgnoreCase));
}
}
}

View file

@ -1,12 +1,12 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Game; using Automata.Game;
using Poe2Trade.Inventory; using Automata.Inventory;
using Poe2Trade.Screen; using Automata.Screen;
using Poe2Trade.Trade; using Automata.Trade;
using Serilog; using Serilog;
namespace Poe2Trade.Bot; namespace Automata.Bot;
public class DiamondExecutor public class DiamondExecutor
{ {

View file

@ -1,9 +1,9 @@
using System.Diagnostics; using System.Diagnostics;
using Poe2Trade.Game; using Automata.Game;
using Poe2Trade.Screen; using Automata.Screen;
using Serilog; using Serilog;
namespace Poe2Trade.Bot; namespace Automata.Bot;
/// <summary> /// <summary>
/// Monitors life/mana and presses flask keys when they drop below thresholds. /// Monitors life/mana and presses flask keys when they drop below thresholds.

View file

@ -1,11 +1,11 @@
using System.Diagnostics; using System.Diagnostics;
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Game; using Automata.Game;
using Poe2Trade.Inventory; using Automata.Inventory;
using Poe2Trade.Screen; using Automata.Screen;
using Serilog; using Serilog;
namespace Poe2Trade.Bot; namespace Automata.Bot;
/// <summary> /// <summary>
/// Base class for game executors that interact with the game world. /// Base class for game executors that interact with the game world.

View file

@ -1,12 +1,12 @@
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Game; using Automata.Game;
using Poe2Trade.GameLog; using Automata.GameLog;
using Poe2Trade.Inventory; using Automata.Inventory;
using Poe2Trade.Navigation; using Automata.Navigation;
using Poe2Trade.Screen; using Automata.Screen;
using Serilog; using Serilog;
namespace Poe2Trade.Bot; namespace Automata.Bot;
/// <summary> /// <summary>
/// Kulemak-specific boss run executor: scripted 4-phase + ring fight, /// Kulemak-specific boss run executor: scripted 4-phase + ring fight,
@ -218,7 +218,7 @@ public class KulemakExecutor : MappingExecutor
await _game.MoveMouseTo(x, y); await _game.MoveMouseTo(x, y);
await Sleep(200); await Sleep(200);
await _game.CtrlLeftClickAt(x, y); await _game.CtrlLeftClickAt(x, y);
await Sleep(500); await Sleep(1000);
var matches = await _screen.TemplateMatchAll(NewInstanceTemplate); var matches = await _screen.TemplateMatchAll(NewInstanceTemplate);
if (matches.Count == 0) if (matches.Count == 0)

View file

@ -1,13 +1,13 @@
using System.Diagnostics; using System.Diagnostics;
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Game; using Automata.Game;
using Poe2Trade.GameLog; using Automata.GameLog;
using Poe2Trade.Inventory; using Automata.Inventory;
using Poe2Trade.Navigation; using Automata.Navigation;
using Poe2Trade.Screen; using Automata.Screen;
using Serilog; using Serilog;
namespace Poe2Trade.Bot; namespace Automata.Bot;
/// <summary> /// <summary>
/// Shared infrastructure for any map/boss activity: combat loop, WASD navigation, /// Shared infrastructure for any map/boss activity: combat loop, WASD navigation,

View file

@ -1,16 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Poe2Trade.Core\Poe2Trade.Core.csproj" />
<ProjectReference Include="..\Poe2Trade.Game\Poe2Trade.Game.csproj" />
<ProjectReference Include="..\Poe2Trade.Screen\Poe2Trade.Screen.csproj" />
<ProjectReference Include="..\Poe2Trade.Trade\Poe2Trade.Trade.csproj" />
<ProjectReference Include="..\Poe2Trade.Log\Poe2Trade.Log.csproj" />
<ProjectReference Include="..\Poe2Trade.Inventory\Poe2Trade.Inventory.csproj" />
<ProjectReference Include="..\Poe2Trade.Navigation\Poe2Trade.Navigation.csproj" />
</ItemGroup>
</Project>

View file

@ -1,11 +1,11 @@
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Game; using Automata.Game;
using Poe2Trade.Inventory; using Automata.Inventory;
using Poe2Trade.Screen; using Automata.Screen;
using Poe2Trade.Trade; using Automata.Trade;
using Serilog; using Serilog;
namespace Poe2Trade.Bot; namespace Automata.Bot;
public class ScrapExecutor public class ScrapExecutor
{ {

View file

@ -1,11 +1,11 @@
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Game; using Automata.Game;
using Poe2Trade.Inventory; using Automata.Inventory;
using Poe2Trade.Screen; using Automata.Screen;
using Poe2Trade.Trade; using Automata.Trade;
using Serilog; using Serilog;
namespace Poe2Trade.Bot; namespace Automata.Bot;
public class TradeExecutor public class TradeExecutor
{ {

View file

@ -1,7 +1,7 @@
using Poe2Trade.Core; using Automata.Core;
using Serilog; using Serilog;
namespace Poe2Trade.Bot; namespace Automata.Bot;
public class TradeQueue public class TradeQueue
{ {

View file

@ -2,7 +2,7 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Serilog; using Serilog;
namespace Poe2Trade.Core; namespace Automata.Core;
public class SavedLink public class SavedLink
{ {
@ -18,8 +18,8 @@ public class SavedSettings
{ {
public bool Paused { get; set; } public bool Paused { get; set; }
public List<SavedLink> Links { get; set; } = []; public List<SavedLink> Links { get; set; } = [];
public string Poe2LogPath { get; set; } = @"C:\Program Files (x86)\Steam\steamapps\common\Path of Exile 2\logs\Client.txt"; public string GameLogPath { get; set; } = @"C:\Program Files (x86)\Steam\steamapps\common\Path of Exile 2\logs\Client.txt";
public string Poe2WindowTitle { get; set; } = "Path of Exile 2"; public string GameWindowTitle { get; set; } = "Path of Exile 2";
public string BrowserUserDataDir { get; set; } = "./browser-data"; public string BrowserUserDataDir { get; set; } = "./browser-data";
public int TravelTimeoutMs { get; set; } = 15000; public int TravelTimeoutMs { get; set; } = 15000;
public int StashScanTimeoutMs { get; set; } = 10000; public int StashScanTimeoutMs { get; set; } = 10000;
@ -38,6 +38,9 @@ public class SavedSettings
public string OcrEngine { get; set; } = "WinOCR"; public string OcrEngine { get; set; } = "WinOCR";
public KulemakSettings Kulemak { get; set; } = new(); public KulemakSettings Kulemak { get; set; } = new();
public DiamondSettings Diamond { get; set; } = new(); public DiamondSettings Diamond { get; set; } = new();
public List<CraftRecipe> Crafts { get; set; } = [];
public List<CurrencyPosition> CurrencyPositions { get; set; } = [];
public string SelectedLeague { get; set; } = "";
} }
public class DiamondPriceConfig public class DiamondPriceConfig

View file

@ -1,9 +1,9 @@
namespace Poe2Trade.Core; namespace Automata.Core;
public static class Delays public static class Delays
{ {
public const int PostFocus = 300; public const int PostFocus = 300;
public const int PostTravel = 3000; public const int PostTravel = 5000;
public const int PostStashOpen = 1000; public const int PostStashOpen = 1000;
public const int ClickInterval = 150; public const int ClickInterval = 150;
public const int PostEscape = 500; public const int PostEscape = 500;

View file

@ -1,4 +1,4 @@
namespace Poe2Trade.Core; namespace Automata.Core;
public static class Helpers public static class Helpers
{ {

View file

@ -1,6 +1,6 @@
using Serilog; using Serilog;
namespace Poe2Trade.Core; namespace Automata.Core;
public class TradeLink public class TradeLink
{ {

View file

@ -1,7 +1,7 @@
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
namespace Poe2Trade.Core; namespace Automata.Core;
public static class Logging public static class Logging
{ {

View file

@ -0,0 +1,80 @@
using System.Text.Json;
using Serilog;
namespace Automata.Core;
public static class Poe2ScoutClient
{
private static readonly HttpClient Http = new()
{
BaseAddress = new Uri("https://poe2scout.com/api/"),
Timeout = TimeSpan.FromSeconds(10),
};
private static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNameCaseInsensitive = true,
};
public static async Task<List<Poe2ScoutLeague>> GetLeaguesAsync()
{
try
{
var json = await Http.GetStringAsync("leagues");
return JsonSerializer.Deserialize<List<Poe2ScoutLeague>>(json, JsonOpts) ?? [];
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to fetch leagues from poe2scout");
return [];
}
}
public static async Task<List<Poe2ScoutCurrency>> GetAllCurrencyAsync(string league)
{
var all = new List<Poe2ScoutCurrency>();
var page = 1;
try
{
while (true)
{
var url = $"items/currency/currency?page={page}&perPage=25&league={Uri.EscapeDataString(league)}&referenceCurrency=exalted";
var json = await Http.GetStringAsync(url);
var result = JsonSerializer.Deserialize<Poe2ScoutCurrencyPage>(json, JsonOpts);
if (result == null) break;
all.AddRange(result.Items);
if (page >= result.Pages) break;
page++;
}
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to fetch currency page {Page} from poe2scout", page);
}
return all;
}
public static async Task<string?> DownloadIconAsync(string iconUrl, string savePath)
{
try
{
if (File.Exists(savePath)) return savePath;
var dir = Path.GetDirectoryName(savePath);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
var bytes = await Http.GetByteArrayAsync(iconUrl);
await File.WriteAllBytesAsync(savePath, bytes);
return savePath;
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to download icon from {Url}", iconUrl);
return null;
}
}
}

View file

@ -1,4 +1,4 @@
namespace Poe2Trade.Core; namespace Automata.Core;
public class StashTabInfo public class StashTabInfo
{ {

View file

@ -1,4 +1,4 @@
namespace Poe2Trade.Core; namespace Automata.Core;
public record Region(int X, int Y, int Width, int Height); public record Region(int X, int Y, int Width, int Height);
@ -94,7 +94,8 @@ public enum PostAction
public enum BotMode public enum BotMode
{ {
Trading, Trading,
Mapping Mapping,
Crafting
} }
public enum MappingState public enum MappingState
@ -138,3 +139,46 @@ public interface IGameStateProvider
GameUiState CurrentState { get; } GameUiState CurrentState { get; }
event Action<GameUiState, GameUiState>? StateChanged; event Action<GameUiState, GameUiState>? StateChanged;
} }
public enum CraftingState
{
Idle, Running, ReadingItem, CheckingMods, Paused, Done, Failed
}
public class CraftStep
{
public string Label { get; set; } = "";
public string CurrencyName { get; set; } = "";
public int CurrencyX { get; set; }
public int CurrencyY { get; set; }
public List<string> RequiredMods { get; set; } = [];
public bool MatchAll { get; set; } = true;
public int MaxAttempts { get; set; } = 500;
public int? OnFailGoTo { get; set; }
}
public class CraftRecipe
{
public string Id { get; set; } = Guid.NewGuid().ToString("N")[..8];
public string Name { get; set; } = "New Craft";
public int ItemX { get; set; }
public int ItemY { get; set; }
public List<CraftStep> Steps { get; set; } = [];
}
public class CurrencyPosition
{
public string Name { get; set; } = "";
public int X { get; set; }
public int Y { get; set; }
public string ApiId { get; set; } = "";
public double Price { get; set; }
}
// poe2scout.com API DTOs
public record Poe2ScoutLeague(string Value, double DivinePrice, double ChaosDivinePrice);
public record Poe2ScoutCurrencyPage(int CurrentPage, int Pages, int Total, List<Poe2ScoutCurrency> Items);
public record Poe2ScoutCurrency(int Id, string ApiId, string Text, string IconUrl, double CurrentPrice);

View file

@ -6,6 +6,6 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Poe2Trade.Core\Poe2Trade.Core.csproj" /> <ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,7 +1,7 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
namespace Poe2Trade.Game; namespace Automata.Game;
/// <summary> /// <summary>
/// Win32 clipboard access without WinForms dependency. /// Win32 clipboard access without WinForms dependency.

View file

@ -1,7 +1,7 @@
using Poe2Trade.Core; using Automata.Core;
using Serilog; using Serilog;
namespace Poe2Trade.Game; namespace Automata.Game;
public class GameController : IGameController public class GameController : IGameController
{ {
@ -10,7 +10,7 @@ public class GameController : IGameController
public GameController(SavedSettings config) public GameController(SavedSettings config)
{ {
_windowManager = new WindowManager(config.Poe2WindowTitle); _windowManager = new WindowManager(config.GameWindowTitle);
_input = new InputSender(); _input = new InputSender();
} }

View file

@ -1,4 +1,4 @@
namespace Poe2Trade.Game; namespace Automata.Game;
public interface IGameController public interface IGameController
{ {

View file

@ -1,7 +1,7 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Poe2Trade.Core; using Automata.Core;
namespace Poe2Trade.Game; namespace Automata.Game;
public class InputSender public class InputSender
{ {

View file

@ -1,7 +1,7 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Serilog; using Serilog;
namespace Poe2Trade.Game; namespace Automata.Game;
public class WindowManager public class WindowManager
{ {

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
<ProjectReference Include="..\Automata.Game\Automata.Game.csproj" />
<ProjectReference Include="..\Automata.Screen\Automata.Screen.csproj" />
<ProjectReference Include="..\Automata.Log\Automata.Log.csproj" />
</ItemGroup>
</Project>

View file

@ -1,7 +1,7 @@
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Screen; using Automata.Screen;
namespace Poe2Trade.Inventory; namespace Automata.Inventory;
public interface IInventoryManager public interface IInventoryManager
{ {

View file

@ -1,10 +1,10 @@
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Game; using Automata.Game;
using Poe2Trade.GameLog; using Automata.GameLog;
using Poe2Trade.Screen; using Automata.Screen;
using Serilog; using Serilog;
namespace Poe2Trade.Inventory; namespace Automata.Inventory;
public class InventoryManager : IInventoryManager public class InventoryManager : IInventoryManager
{ {

View file

@ -1,8 +1,8 @@
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Screen; using Automata.Screen;
using Serilog; using Serilog;
namespace Poe2Trade.Inventory; namespace Automata.Inventory;
public class PlacedItem public class PlacedItem
{ {

View file

@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Poe2Trade.Core\Poe2Trade.Core.csproj" />
<ProjectReference Include="..\Poe2Trade.Game\Poe2Trade.Game.csproj" />
<ProjectReference Include="..\Poe2Trade.Screen\Poe2Trade.Screen.csproj" />
<ProjectReference Include="..\Poe2Trade.Log\Poe2Trade.Log.csproj" />
</ItemGroup>
</Project>

View file

@ -1,9 +1,9 @@
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Game; using Automata.Game;
using Poe2Trade.Screen; using Automata.Screen;
using Serilog; using Serilog;
namespace Poe2Trade.Inventory; namespace Automata.Inventory;
public class StashCalibrator public class StashCalibrator
{ {

View file

@ -5,7 +5,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Poe2Trade.Core\Poe2Trade.Core.csproj" /> <ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
<ProjectReference Include="..\Poe2Trade.Game\Poe2Trade.Game.csproj" /> <ProjectReference Include="..\Automata.Game\Automata.Game.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,8 +1,8 @@
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Game; using Automata.Game;
using Serilog; using Serilog;
namespace Poe2Trade.Items; namespace Automata.Items;
/// <summary> /// <summary>
/// Reads item data by hovering and pressing Ctrl+C to copy item text to clipboard. /// Reads item data by hovering and pressing Ctrl+C to copy item text to clipboard.

View file

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,39 @@
using System.Runtime.InteropServices;
namespace Automata.Memory;
internal static partial class Native
{
public const uint PROCESS_VM_READ = 0x0010;
public const uint PROCESS_QUERY_INFORMATION = 0x0400;
public const uint LIST_MODULES_ALL = 0x03;
[StructLayout(LayoutKind.Sequential)]
public struct MODULEINFO
{
public nint lpBaseOfDll;
public int SizeOfImage;
public nint EntryPoint;
}
// kernel32.dll
[LibraryImport("kernel32.dll", SetLastError = true)]
public static partial nint OpenProcess(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int dwProcessId);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool CloseHandle(nint hObject);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool ReadProcessMemory(nint hProcess, nint lpBaseAddress, nint lpBuffer, nint nSize, out nint lpNumberOfBytesRead);
// psapi.dll
[LibraryImport("psapi.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool EnumProcessModulesEx(nint hProcess, nint[] lphModule, int cb, out int lpcbNeeded, uint dwFilterFlag);
[LibraryImport("psapi.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool GetModuleInformation(nint hProcess, nint hModule, out MODULEINFO lpmodinfo, int cb);
}

View file

@ -0,0 +1,144 @@
using Serilog;
namespace Automata.Memory;
public sealed class PatternScanner
{
private readonly ProcessMemory _memory;
private byte[]? _imageCache;
private nint _moduleBase;
private int _moduleSize;
public PatternScanner(ProcessMemory memory)
{
_memory = memory;
}
/// <summary>
/// Finds a pattern in the main module and returns the absolute address at the ^ marker position.
/// Pattern format: "48 8B ?? ?? ?? ?? ?? 4C ^ 8B 05" where ?? = wildcard, ^ = result offset.
/// </summary>
public nint FindPattern(string pattern)
{
EnsureImageCached();
if (_imageCache is null)
return 0;
var (bytes, mask, resultOffset) = Parse(pattern);
var matchIndex = Scan(_imageCache, bytes, mask);
if (matchIndex < 0)
{
Log.Warning("Pattern not found: {Pattern}", pattern);
return 0;
}
var absolute = _moduleBase + matchIndex + resultOffset;
Log.Debug("Pattern matched at 0x{Address:X} (module+0x{Offset:X})", absolute, matchIndex + resultOffset);
return absolute;
}
/// <summary>
/// FindPattern + RIP-relative resolution: reads int32 displacement at matched address and resolves to absolute address.
/// Result = matchAddr + 4 + displacement
/// </summary>
public nint FindPatternRip(string pattern)
{
var addr = FindPattern(pattern);
if (addr == 0) return 0;
EnsureImageCached();
if (_imageCache is null) return 0;
var bufferOffset = (int)(addr - _moduleBase);
if (bufferOffset + 4 > _imageCache.Length)
{
Log.Warning("RIP resolution out of bounds at 0x{Address:X}", addr);
return 0;
}
var displacement = BitConverter.ToInt32(_imageCache, bufferOffset);
var resolved = addr + 4 + displacement;
Log.Debug("RIP resolved: 0x{Address:X} + 4 + {Disp} = 0x{Result:X}", addr, displacement, resolved);
return resolved;
}
private void EnsureImageCached()
{
if (_imageCache is not null)
return;
var module = _memory.GetMainModule();
if (module is null)
{
Log.Error("Failed to get main module for pattern scanning");
return;
}
(_moduleBase, _moduleSize) = module.Value;
_imageCache = _memory.ReadBytes(_moduleBase, _moduleSize);
if (_imageCache is null)
Log.Error("Failed to read main module image ({Size} bytes)", _moduleSize);
else
Log.Information("Cached module image: base=0x{Base:X}, size={Size}", _moduleBase, _moduleSize);
}
private static int Scan(byte[] image, byte[] pattern, bool[] mask)
{
var end = image.Length - pattern.Length;
for (var i = 0; i <= end; i++)
{
var match = true;
for (var j = 0; j < pattern.Length; j++)
{
if (mask[j] && image[i + j] != pattern[j])
{
match = false;
break;
}
}
if (match) return i;
}
return -1;
}
/// <summary>
/// Parses a pattern string into bytes, mask, and result offset.
/// Tokens: hex byte (e.g. "4C") = must-match, "??" = wildcard, "^" = result offset marker.
/// </summary>
internal static (byte[] Bytes, bool[] Mask, int ResultOffset) Parse(string pattern)
{
var tokens = pattern.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var bytes = new List<byte>();
var mask = new List<bool>();
var resultOffset = 0;
var markerFound = false;
foreach (var token in tokens)
{
if (token == "^")
{
markerFound = true;
resultOffset = bytes.Count;
continue;
}
if (token == "??")
{
bytes.Add(0);
mask.Add(false);
}
else
{
bytes.Add(Convert.ToByte(token, 16));
mask.Add(true);
}
}
if (!markerFound)
resultOffset = 0;
return (bytes.ToArray(), mask.ToArray(), resultOffset);
}
}

View file

@ -0,0 +1,129 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Serilog;
namespace Automata.Memory;
public sealed class ProcessMemory : IDisposable
{
private nint _handle;
private bool _disposed;
public string ProcessName { get; }
public int ProcessId { get; private set; }
private ProcessMemory(string processName, nint handle, int processId)
{
ProcessName = processName;
_handle = handle;
ProcessId = processId;
}
public static ProcessMemory? Attach(string processName)
{
var procs = Process.GetProcessesByName(processName);
if (procs.Length == 0)
{
Log.Warning("Process '{Name}' not found", processName);
return null;
}
var proc = procs[0];
var handle = Native.OpenProcess(
Native.PROCESS_VM_READ | Native.PROCESS_QUERY_INFORMATION,
false,
proc.Id);
if (handle == 0)
{
Log.Error("Failed to open process '{Name}' (PID {Pid})", processName, proc.Id);
return null;
}
Log.Information("Attached to '{Name}' (PID {Pid})", processName, proc.Id);
return new ProcessMemory(processName, handle, proc.Id);
}
public bool ReadBytes(nint address, Span<byte> buffer)
{
unsafe
{
fixed (byte* ptr = buffer)
{
return Native.ReadProcessMemory(_handle, address, (nint)ptr, buffer.Length, out _);
}
}
}
public T Read<T>(nint address) where T : unmanaged
{
Span<byte> buf = stackalloc byte[Unsafe.SizeOf<T>()];
if (!ReadBytes(address, buf))
return default;
return Unsafe.ReadUnaligned<T>(ref buf[0]);
}
public nint ReadPointer(nint address) => Read<nint>(address);
public byte[]? ReadBytes(nint address, int length)
{
var buffer = new byte[length];
if (!ReadBytes(address, buffer.AsSpan()))
return null;
return buffer;
}
public (nint Base, int Size)? GetMainModule()
{
var modules = new nint[1];
if (!Native.EnumProcessModulesEx(_handle, modules, nint.Size, out _, Native.LIST_MODULES_ALL))
{
Log.Error("EnumProcessModulesEx failed");
return null;
}
if (!Native.GetModuleInformation(_handle, modules[0], out var info, Unsafe.SizeOf<Native.MODULEINFO>()))
{
Log.Error("GetModuleInformation failed");
return null;
}
return (info.lpBaseOfDll, info.SizeOfImage);
}
/// <summary>
/// Follows a pointer chain. Dereferences all offsets except the last one (which is added).
/// Example: FollowChain(base, [136, 536, 2768]) reads ptr at base+136, reads ptr at result+536, returns result+2768.
/// </summary>
public nint FollowChain(nint baseAddr, ReadOnlySpan<int> offsets)
{
if (offsets.Length == 0)
return baseAddr;
var current = baseAddr;
for (var i = 0; i < offsets.Length - 1; i++)
{
current = ReadPointer(current + offsets[i]);
if (current == 0)
{
Log.Debug("Pointer chain broken at offset index {Index} (offset 0x{Offset:X})", i, offsets[i]);
return 0;
}
}
return current + offsets[^1];
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (_handle != 0)
{
Native.CloseHandle(_handle);
_handle = 0;
Log.Debug("Detached from '{Name}'", ProcessName);
}
}
}

View file

@ -0,0 +1,76 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Serilog;
namespace Automata.Memory;
public sealed class TerrainOffsets
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never
};
public string ProcessName { get; set; } = "PathOfExileSteam";
/// <summary>Pattern to find GameState base pointer. Empty = unknown for POE2.</summary>
public string GameStatePattern { get; set; } = "";
// Pointer chain: GameState → InGameState → IngameData → TerrainData
public int InGameStateOffset { get; set; }
public int IngameDataOffset { get; set; }
public int TerrainDataOffset { get; set; }
// Within TerrainData struct
public int NumColsOffset { get; set; }
public int NumRowsOffset { get; set; }
public int LayerMeleeOffset { get; set; }
public int BytesPerRowOffset { get; set; }
/// <summary>Number of sub-tiles per terrain cell (typically 23 for POE).</summary>
public int SubTilesPerCell { get; set; } = 23;
public static TerrainOffsets Load(string path)
{
if (!File.Exists(path))
{
Log.Information("Offsets file not found at '{Path}', using defaults", path);
var defaults = new TerrainOffsets();
defaults.Save(path);
return defaults;
}
try
{
var json = File.ReadAllText(path);
var offsets = JsonSerializer.Deserialize<TerrainOffsets>(json, JsonOptions);
if (offsets is null)
{
Log.Warning("Failed to deserialize '{Path}', using defaults", path);
return new TerrainOffsets();
}
Log.Information("Loaded offsets from '{Path}'", path);
return offsets;
}
catch (Exception ex)
{
Log.Error(ex, "Error loading offsets from '{Path}'", path);
return new TerrainOffsets();
}
}
public void Save(string path)
{
try
{
var json = JsonSerializer.Serialize(this, JsonOptions);
File.WriteAllText(path, json);
Log.Debug("Saved offsets to '{Path}'", path);
}
catch (Exception ex)
{
Log.Error(ex, "Error saving offsets to '{Path}'", path);
}
}
}

View file

@ -0,0 +1,141 @@
using Serilog;
namespace Automata.Memory;
public sealed class WalkabilityGrid
{
public int Width { get; }
public int Height { get; }
public byte[] Data { get; }
public WalkabilityGrid(int width, int height, byte[] data)
{
Width = width;
Height = height;
Data = data;
}
public bool IsWalkable(int x, int y)
{
if (x < 0 || x >= Width || y < 0 || y >= Height)
return false;
return Data[y * Width + x] == 0;
}
}
public sealed class TerrainReader : IDisposable
{
private readonly TerrainOffsets _offsets;
private ProcessMemory? _memory;
private PatternScanner? _scanner;
private nint _gameStateBase;
private bool _disposed;
public bool IsReady => _gameStateBase != 0;
public TerrainReader(TerrainOffsets offsets)
{
_offsets = offsets;
}
public bool Initialize()
{
_memory?.Dispose();
_memory = ProcessMemory.Attach(_offsets.ProcessName);
if (_memory is null)
return false;
if (string.IsNullOrWhiteSpace(_offsets.GameStatePattern))
{
Log.Warning("GameStatePattern is empty — offsets not yet configured for POE2");
return false;
}
_scanner = new PatternScanner(_memory);
_gameStateBase = _scanner.FindPatternRip(_offsets.GameStatePattern);
if (_gameStateBase == 0)
{
Log.Error("Failed to resolve GameState base pointer");
return false;
}
Log.Information("GameState base: 0x{Address:X}", _gameStateBase);
return true;
}
public WalkabilityGrid? ReadTerrain()
{
if (_memory is null || _gameStateBase == 0)
return null;
// Follow pointer chain: GameState → InGameState → IngameData → TerrainData
var terrainBase = _memory.FollowChain(_gameStateBase, [
_offsets.InGameStateOffset,
_offsets.IngameDataOffset,
_offsets.TerrainDataOffset
]);
if (terrainBase == 0)
{
Log.Debug("Terrain pointer chain returned null");
return null;
}
var numCols = _memory.Read<int>(terrainBase + _offsets.NumColsOffset);
var numRows = _memory.Read<int>(terrainBase + _offsets.NumRowsOffset);
var bytesPerRow = _memory.Read<int>(terrainBase + _offsets.BytesPerRowOffset);
if (numCols <= 0 || numRows <= 0 || bytesPerRow <= 0)
{
Log.Warning("Invalid terrain dimensions: {Cols}x{Rows}, bytesPerRow={Bpr}", numCols, numRows, bytesPerRow);
return null;
}
var gridWidth = numCols * _offsets.SubTilesPerCell;
var gridHeight = numRows * _offsets.SubTilesPerCell;
// Read melee layer pointer
var layerPtr = _memory.ReadPointer(terrainBase + _offsets.LayerMeleeOffset);
if (layerPtr == 0)
{
Log.Warning("Melee layer pointer is null");
return null;
}
// Read raw terrain data
var rawSize = bytesPerRow * gridHeight;
var rawData = _memory.ReadBytes(layerPtr, rawSize);
if (rawData is null)
{
Log.Warning("Failed to read terrain data ({Size} bytes)", rawSize);
return null;
}
// Unpack 4-bit nibbles: each byte → 2 cells (low nibble = even col, high nibble = odd col)
var data = new byte[gridWidth * gridHeight];
for (var row = 0; row < gridHeight; row++)
{
var rowStart = row * bytesPerRow;
for (var col = 0; col < gridWidth; col++)
{
var byteIndex = rowStart + col / 2;
if (byteIndex >= rawData.Length) break;
data[row * gridWidth + col] = (col % 2 == 0)
? (byte)(rawData[byteIndex] & 0x0F)
: (byte)((rawData[byteIndex] >> 4) & 0x0F);
}
}
Log.Information("Terrain read: {Width}x{Height} ({Cols}x{Rows} cells)", gridWidth, gridHeight, numCols, numRows);
return new WalkabilityGrid(gridWidth, gridHeight, data);
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_memory?.Dispose();
}
}

View file

@ -1,10 +1,10 @@
using System.Diagnostics; using System.Diagnostics;
using OpenCvSharp; using OpenCvSharp;
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Screen; using Automata.Screen;
using Serilog; using Serilog;
namespace Poe2Trade.Navigation; namespace Automata.Navigation;
public record AtlasProgress(int TilesCaptured, int Row, string Phase); public record AtlasProgress(int TilesCaptured, int Row, string Phase);

View file

@ -12,8 +12,8 @@
<PackageReference Include="System.Drawing.Common" Version="8.0.12" /> <PackageReference Include="System.Drawing.Common" Version="8.0.12" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Poe2Trade.Core\Poe2Trade.Core.csproj" /> <ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
<ProjectReference Include="..\Poe2Trade.Game\Poe2Trade.Game.csproj" /> <ProjectReference Include="..\Automata.Game\Automata.Game.csproj" />
<ProjectReference Include="..\Poe2Trade.Screen\Poe2Trade.Screen.csproj" /> <ProjectReference Include="..\Automata.Screen\Automata.Screen.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,7 +1,7 @@
using OpenCvSharp; using OpenCvSharp;
using Serilog; using Serilog;
namespace Poe2Trade.Navigation; namespace Automata.Navigation;
/// <summary> /// <summary>
/// Detects minimap icons (doors, checkpoints) via template matching. /// Detects minimap icons (doors, checkpoints) via template matching.

View file

@ -1,10 +1,10 @@
using OpenCvSharp; using OpenCvSharp;
using Poe2Trade.Screen; using Automata.Screen;
using Serilog; using Serilog;
using Region = Poe2Trade.Core.Region; using Region = Automata.Core.Region;
using Size = OpenCvSharp.Size; using Size = OpenCvSharp.Size;
namespace Poe2Trade.Navigation; namespace Automata.Navigation;
public class MinimapCapture : IFrameConsumer, IDisposable public class MinimapCapture : IFrameConsumer, IDisposable
{ {

View file

@ -1,11 +1,11 @@
using System.Diagnostics; using System.Diagnostics;
using OpenCvSharp; using OpenCvSharp;
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Game; using Automata.Game;
using Poe2Trade.Screen; using Automata.Screen;
using Serilog; using Serilog;
namespace Poe2Trade.Navigation; namespace Automata.Navigation;
public class NavigationExecutor : IDisposable public class NavigationExecutor : IDisposable
{ {

View file

@ -1,7 +1,7 @@
using Poe2Trade.Core; using Automata.Core;
using OpenCvSharp; using OpenCvSharp;
namespace Poe2Trade.Navigation; namespace Automata.Navigation;
public enum MinimapMode public enum MinimapMode
{ {

View file

@ -1,7 +1,7 @@
using OpenCvSharp; using OpenCvSharp;
using Serilog; using Serilog;
namespace Poe2Trade.Navigation; namespace Automata.Navigation;
/// <summary> /// <summary>
/// Last BFS result for visualization. /// Last BFS result for visualization.

View file

@ -1,9 +1,9 @@
using OpenCvSharp; using OpenCvSharp;
using Poe2Trade.Core; using Automata.Core;
using Poe2Trade.Screen; using Automata.Screen;
using Serilog; using Serilog;
namespace Poe2Trade.Navigation; namespace Automata.Navigation;
public record CalibrationResult(float BestFactor, double BestConfidence, Dictionary<float, double> AllResults); public record CalibrationResult(float BestFactor, double BestConfidence, Dictionary<float, double> AllResults);

View file

@ -1,4 +1,4 @@
namespace Poe2Trade.Navigation; namespace Automata.Navigation;
/// <summary> /// <summary>
/// Detects when the player hasn't moved significantly over a window of frames. /// Detects when the player hasn't moved significantly over a window of frames.

View file

@ -1,7 +1,7 @@
using OpenCvSharp; using OpenCvSharp;
using Serilog; using Serilog;
namespace Poe2Trade.Navigation; namespace Automata.Navigation;
/// <summary> /// <summary>
/// Tracks HSV distribution of confirmed wall pixels and computes an adaptive /// Tracks HSV distribution of confirmed wall pixels and computes an adaptive

View file

@ -2,7 +2,7 @@ using System.Diagnostics;
using OpenCvSharp; using OpenCvSharp;
using Serilog; using Serilog;
namespace Poe2Trade.Navigation; namespace Automata.Navigation;
public class WorldMap : IDisposable public class WorldMap : IDisposable
{ {

View file

@ -15,6 +15,6 @@
<PackageReference Include="Vortice.DXGI" Version="3.8.2" /> <PackageReference Include="Vortice.DXGI" Version="3.8.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Poe2Trade.Core\Poe2Trade.Core.csproj" /> <ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,8 +1,8 @@
using OpenCvSharp; using OpenCvSharp;
using Serilog; using Serilog;
using Region = Poe2Trade.Core.Region; using Region = Automata.Core.Region;
namespace Poe2Trade.Screen; namespace Automata.Screen;
/// <summary> /// <summary>
/// Detects bosses using YOLO running on a background thread. /// Detects bosses using YOLO running on a background thread.

View file

@ -1,6 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace Poe2Trade.Screen; namespace Automata.Screen;
public class OcrWord public class OcrWord
{ {
@ -49,7 +49,7 @@ public class DiffOcrResponse
{ {
public string Text { get; set; } = ""; public string Text { get; set; } = "";
public List<OcrLine> Lines { get; set; } = []; public List<OcrLine> Lines { get; set; } = [];
public Poe2Trade.Core.Region? Region { get; set; } public Automata.Core.Region? Region { get; set; }
} }
public class TemplateMatchResult public class TemplateMatchResult

View file

@ -5,9 +5,9 @@ using SharpGen.Runtime;
using Vortice.Direct3D; using Vortice.Direct3D;
using Vortice.Direct3D11; using Vortice.Direct3D11;
using Vortice.DXGI; using Vortice.DXGI;
using Region = Poe2Trade.Core.Region; using Region = Automata.Core.Region;
namespace Poe2Trade.Screen; namespace Automata.Screen;
public sealed class DesktopDuplication : IScreenCapture public sealed class DesktopDuplication : IScreenCapture
{ {

View file

@ -1,10 +1,10 @@
namespace Poe2Trade.Screen; namespace Automata.Screen;
using System.Drawing; using System.Drawing;
using System.Drawing.Imaging; using System.Drawing.Imaging;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Serilog; using Serilog;
using Region = Poe2Trade.Core.Region; using Region = Automata.Core.Region;
class DetectGridHandler class DetectGridHandler
{ {

View file

@ -1,4 +1,4 @@
namespace Poe2Trade.Screen; namespace Automata.Screen;
public record DetectedEnemy( public record DetectedEnemy(
float Confidence, float Confidence,

Some files were not shown because too many files have changed in this diff Show more