added items
|
|
@ -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
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/currency/currency/alch.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/currency/currency/annul.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
assets/currency/currency/artificers-shard.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
assets/currency/currency/artificers.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
assets/currency/currency/aug.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/bauble.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/currency/currency/chance-shard.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/currency/currency/chance.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/chaos.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/divine.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
assets/currency/currency/etcher.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
assets/currency/currency/exalted.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/fracturing-orb.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
assets/currency/currency/gcp.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
assets/currency/currency/greater-chaos-orb.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/greater-exalted-orb.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/greater-jewellers-orb.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
assets/currency/currency/greater-orb-of-augmentation.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/greater-orb-of-transmutation.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/greater-regal-orb.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/hinekoras-lock.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/lesser-jewellers-orb.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
assets/currency/currency/mirror.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
assets/currency/currency/perfect-chaos-orb.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/perfect-exalted-orb.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/perfect-jewellers-orb.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/currency/currency/perfect-orb-of-augmentation.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/perfect-orb-of-transmutation.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/perfect-regal-orb.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/regal-shard.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
assets/currency/currency/regal.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/scrap.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
assets/currency/currency/transmutation-shard.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/currency/currency/transmute.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/vaal.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/currency/currency/whetstone.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
assets/currency/currency/wisdom.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
12
memory-offsets.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"ProcessName": "PathOfExileSteam",
|
||||||
|
"GameStatePattern": "",
|
||||||
|
"InGameStateOffset": 0,
|
||||||
|
"IngameDataOffset": 0,
|
||||||
|
"TerrainDataOffset": 0,
|
||||||
|
"NumColsOffset": 0,
|
||||||
|
"NumRowsOffset": 0,
|
||||||
|
"LayerMeleeOffset": 0,
|
||||||
|
"BytesPerRowOffset": 0,
|
||||||
|
"SubTilesPerCell": 23
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
16
src/Automata.Bot/Automata.Bot.csproj
Normal 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>
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
207
src/Automata.Bot/CraftingExecutor.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace Poe2Trade.Core;
|
namespace Automata.Core;
|
||||||
|
|
||||||
public static class Helpers
|
public static class Helpers
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace Poe2Trade.Core;
|
namespace Automata.Core;
|
||||||
|
|
||||||
public class TradeLink
|
public class TradeLink
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
80
src/Automata.Core/Poe2ScoutClient.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace Poe2Trade.Core;
|
namespace Automata.Core;
|
||||||
|
|
||||||
public class StashTabInfo
|
public class StashTabInfo
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace Poe2Trade.Game;
|
namespace Automata.Game;
|
||||||
|
|
||||||
public interface IGameController
|
public interface IGameController
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
13
src/Automata.Inventory/Automata.Inventory.csproj
Normal 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>
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
11
src/Automata.Memory/Automata.Memory.csproj
Normal 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>
|
||||||
39
src/Automata.Memory/Native.cs
Normal 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);
|
||||||
|
}
|
||||||
144
src/Automata.Memory/PatternScanner.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/Automata.Memory/ProcessMemory.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/Automata.Memory/TerrainOffsets.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
141
src/Automata.Memory/TerrainReader.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace Poe2Trade.Screen;
|
namespace Automata.Screen;
|
||||||
|
|
||||||
public record DetectedEnemy(
|
public record DetectedEnemy(
|
||||||
float Confidence,
|
float Confidence,
|
||||||
|
|
|
||||||