diff --git a/Poe2Trade.sln b/Automata.sln similarity index 69% rename from Poe2Trade.sln rename to Automata.sln index 557f2ba..5e213a9 100644 --- a/Poe2Trade.sln +++ b/Automata.sln @@ -5,25 +5,27 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}" 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 -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 -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 -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 -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 -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 -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 -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 -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 -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 Global 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}.Release|Any CPU.ActiveCfg = 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 GlobalSection(NestedProjects) = preSolution {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} {859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3} = {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 EndGlobal diff --git a/assets/currency-tab.png.png b/assets/currency-tab.png.png new file mode 100644 index 0000000..f4ff631 Binary files /dev/null and b/assets/currency-tab.png.png differ diff --git a/assets/currency/currency/alch.png b/assets/currency/currency/alch.png new file mode 100644 index 0000000..fb6401e Binary files /dev/null and b/assets/currency/currency/alch.png differ diff --git a/assets/currency/currency/annul.png b/assets/currency/currency/annul.png new file mode 100644 index 0000000..9582f9e Binary files /dev/null and b/assets/currency/currency/annul.png differ diff --git a/assets/currency/currency/artificers-shard.png b/assets/currency/currency/artificers-shard.png new file mode 100644 index 0000000..ea4a36b Binary files /dev/null and b/assets/currency/currency/artificers-shard.png differ diff --git a/assets/currency/currency/artificers.png b/assets/currency/currency/artificers.png new file mode 100644 index 0000000..c6fa577 Binary files /dev/null and b/assets/currency/currency/artificers.png differ diff --git a/assets/currency/currency/aug.png b/assets/currency/currency/aug.png new file mode 100644 index 0000000..fb32205 Binary files /dev/null and b/assets/currency/currency/aug.png differ diff --git a/assets/currency/currency/bauble.png b/assets/currency/currency/bauble.png new file mode 100644 index 0000000..928ca0a Binary files /dev/null and b/assets/currency/currency/bauble.png differ diff --git a/assets/currency/currency/chance-shard.png b/assets/currency/currency/chance-shard.png new file mode 100644 index 0000000..ea21c72 Binary files /dev/null and b/assets/currency/currency/chance-shard.png differ diff --git a/assets/currency/currency/chance.png b/assets/currency/currency/chance.png new file mode 100644 index 0000000..cb2240f Binary files /dev/null and b/assets/currency/currency/chance.png differ diff --git a/assets/currency/currency/chaos.png b/assets/currency/currency/chaos.png new file mode 100644 index 0000000..4a6dc3b Binary files /dev/null and b/assets/currency/currency/chaos.png differ diff --git a/assets/currency/currency/divine.png b/assets/currency/currency/divine.png new file mode 100644 index 0000000..6bd9ea3 Binary files /dev/null and b/assets/currency/currency/divine.png differ diff --git a/assets/currency/currency/etcher.png b/assets/currency/currency/etcher.png new file mode 100644 index 0000000..1931db4 Binary files /dev/null and b/assets/currency/currency/etcher.png differ diff --git a/assets/currency/currency/exalted.png b/assets/currency/currency/exalted.png new file mode 100644 index 0000000..34f2686 Binary files /dev/null and b/assets/currency/currency/exalted.png differ diff --git a/assets/currency/currency/fracturing-orb.png b/assets/currency/currency/fracturing-orb.png new file mode 100644 index 0000000..cc47437 Binary files /dev/null and b/assets/currency/currency/fracturing-orb.png differ diff --git a/assets/currency/currency/gcp.png b/assets/currency/currency/gcp.png new file mode 100644 index 0000000..9238533 Binary files /dev/null and b/assets/currency/currency/gcp.png differ diff --git a/assets/currency/currency/greater-chaos-orb.png b/assets/currency/currency/greater-chaos-orb.png new file mode 100644 index 0000000..4a6dc3b Binary files /dev/null and b/assets/currency/currency/greater-chaos-orb.png differ diff --git a/assets/currency/currency/greater-exalted-orb.png b/assets/currency/currency/greater-exalted-orb.png new file mode 100644 index 0000000..34f2686 Binary files /dev/null and b/assets/currency/currency/greater-exalted-orb.png differ diff --git a/assets/currency/currency/greater-jewellers-orb.png b/assets/currency/currency/greater-jewellers-orb.png new file mode 100644 index 0000000..82f3fc3 Binary files /dev/null and b/assets/currency/currency/greater-jewellers-orb.png differ diff --git a/assets/currency/currency/greater-orb-of-augmentation.png b/assets/currency/currency/greater-orb-of-augmentation.png new file mode 100644 index 0000000..fb32205 Binary files /dev/null and b/assets/currency/currency/greater-orb-of-augmentation.png differ diff --git a/assets/currency/currency/greater-orb-of-transmutation.png b/assets/currency/currency/greater-orb-of-transmutation.png new file mode 100644 index 0000000..9cb1b9d Binary files /dev/null and b/assets/currency/currency/greater-orb-of-transmutation.png differ diff --git a/assets/currency/currency/greater-regal-orb.png b/assets/currency/currency/greater-regal-orb.png new file mode 100644 index 0000000..391652c Binary files /dev/null and b/assets/currency/currency/greater-regal-orb.png differ diff --git a/assets/currency/currency/hinekoras-lock.png b/assets/currency/currency/hinekoras-lock.png new file mode 100644 index 0000000..42ea010 Binary files /dev/null and b/assets/currency/currency/hinekoras-lock.png differ diff --git a/assets/currency/currency/lesser-jewellers-orb.png b/assets/currency/currency/lesser-jewellers-orb.png new file mode 100644 index 0000000..2b127ec Binary files /dev/null and b/assets/currency/currency/lesser-jewellers-orb.png differ diff --git a/assets/currency/currency/mirror.png b/assets/currency/currency/mirror.png new file mode 100644 index 0000000..2ec1b0b Binary files /dev/null and b/assets/currency/currency/mirror.png differ diff --git a/assets/currency/currency/perfect-chaos-orb.png b/assets/currency/currency/perfect-chaos-orb.png new file mode 100644 index 0000000..4a6dc3b Binary files /dev/null and b/assets/currency/currency/perfect-chaos-orb.png differ diff --git a/assets/currency/currency/perfect-exalted-orb.png b/assets/currency/currency/perfect-exalted-orb.png new file mode 100644 index 0000000..34f2686 Binary files /dev/null and b/assets/currency/currency/perfect-exalted-orb.png differ diff --git a/assets/currency/currency/perfect-jewellers-orb.png b/assets/currency/currency/perfect-jewellers-orb.png new file mode 100644 index 0000000..495b81d Binary files /dev/null and b/assets/currency/currency/perfect-jewellers-orb.png differ diff --git a/assets/currency/currency/perfect-orb-of-augmentation.png b/assets/currency/currency/perfect-orb-of-augmentation.png new file mode 100644 index 0000000..fb32205 Binary files /dev/null and b/assets/currency/currency/perfect-orb-of-augmentation.png differ diff --git a/assets/currency/currency/perfect-orb-of-transmutation.png b/assets/currency/currency/perfect-orb-of-transmutation.png new file mode 100644 index 0000000..9cb1b9d Binary files /dev/null and b/assets/currency/currency/perfect-orb-of-transmutation.png differ diff --git a/assets/currency/currency/perfect-regal-orb.png b/assets/currency/currency/perfect-regal-orb.png new file mode 100644 index 0000000..391652c Binary files /dev/null and b/assets/currency/currency/perfect-regal-orb.png differ diff --git a/assets/currency/currency/regal-shard.png b/assets/currency/currency/regal-shard.png new file mode 100644 index 0000000..6ae24ae Binary files /dev/null and b/assets/currency/currency/regal-shard.png differ diff --git a/assets/currency/currency/regal.png b/assets/currency/currency/regal.png new file mode 100644 index 0000000..391652c Binary files /dev/null and b/assets/currency/currency/regal.png differ diff --git a/assets/currency/currency/scrap.png b/assets/currency/currency/scrap.png new file mode 100644 index 0000000..c0a5ab5 Binary files /dev/null and b/assets/currency/currency/scrap.png differ diff --git a/assets/currency/currency/transmutation-shard.png b/assets/currency/currency/transmutation-shard.png new file mode 100644 index 0000000..6b8587e Binary files /dev/null and b/assets/currency/currency/transmutation-shard.png differ diff --git a/assets/currency/currency/transmute.png b/assets/currency/currency/transmute.png new file mode 100644 index 0000000..9cb1b9d Binary files /dev/null and b/assets/currency/currency/transmute.png differ diff --git a/assets/currency/currency/vaal.png b/assets/currency/currency/vaal.png new file mode 100644 index 0000000..84c1bbf Binary files /dev/null and b/assets/currency/currency/vaal.png differ diff --git a/assets/currency/currency/whetstone.png b/assets/currency/currency/whetstone.png new file mode 100644 index 0000000..c09d7c7 Binary files /dev/null and b/assets/currency/currency/whetstone.png differ diff --git a/assets/currency/currency/wisdom.png b/assets/currency/currency/wisdom.png new file mode 100644 index 0000000..4ad7d3e Binary files /dev/null and b/assets/currency/currency/wisdom.png differ diff --git a/memory-offsets.json b/memory-offsets.json new file mode 100644 index 0000000..19547bc --- /dev/null +++ b/memory-offsets.json @@ -0,0 +1,12 @@ +{ + "ProcessName": "PathOfExileSteam", + "GameStatePattern": "", + "InGameStateOffset": 0, + "IngameDataOffset": 0, + "TerrainDataOffset": 0, + "NumColsOffset": 0, + "NumRowsOffset": 0, + "LayerMeleeOffset": 0, + "BytesPerRowOffset": 0, + "SubTilesPerCell": 23 +} diff --git a/src/Automata.Bot/AtlasExecutor.cs b/src/Automata.Bot/AtlasExecutor.cs index b31aece..2f4e307 100644 --- a/src/Automata.Bot/AtlasExecutor.cs +++ b/src/Automata.Bot/AtlasExecutor.cs @@ -1,11 +1,11 @@ -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.Inventory; -using Poe2Trade.Navigation; -using Poe2Trade.Screen; +using Automata.Core; +using Automata.Game; +using Automata.Inventory; +using Automata.Navigation; +using Automata.Screen; using Serilog; -namespace Poe2Trade.Bot; +namespace Automata.Bot; /// /// Captures the full endgame atlas as a panorama image. diff --git a/src/Automata.Bot/Automata.Bot.csproj b/src/Automata.Bot/Automata.Bot.csproj new file mode 100644 index 0000000..6c73cb3 --- /dev/null +++ b/src/Automata.Bot/Automata.Bot.csproj @@ -0,0 +1,16 @@ + + + net8.0-windows10.0.19041.0 + enable + enable + + + + + + + + + + + diff --git a/src/Automata.Bot/BotOrchestrator.cs b/src/Automata.Bot/BotOrchestrator.cs index 605c6b4..708f005 100644 --- a/src/Automata.Bot/BotOrchestrator.cs +++ b/src/Automata.Bot/BotOrchestrator.cs @@ -1,13 +1,13 @@ -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.Inventory; -using Poe2Trade.GameLog; -using Poe2Trade.Navigation; -using Poe2Trade.Screen; -using Poe2Trade.Trade; +using Automata.Core; +using Automata.Game; +using Automata.Inventory; +using Automata.GameLog; +using Automata.Navigation; +using Automata.Screen; +using Automata.Trade; using Serilog; -namespace Poe2Trade.Bot; +namespace Automata.Bot; public class BotStatus { @@ -52,6 +52,7 @@ public class BotOrchestrator : IAsyncDisposable public volatile bool ShowFightPositionOverlay = true; private readonly Dictionary _scrapExecutors = new(); private readonly Dictionary _diamondExecutors = new(); + private CraftingExecutor? _craftingExecutor; // Events public event Action? StatusUpdated; @@ -164,6 +165,10 @@ public class BotOrchestrator : IAsyncDisposable TradeQueue.Clear(); + // Stop crafting + _craftingExecutor?.Stop(); + _craftingExecutor = null; + // Stop navigation and mapping await Navigation.Stop(); KulemakExecutor.Stop(); @@ -270,6 +275,12 @@ public class BotOrchestrator : IAsyncDisposable return; } } + if (_craftingExecutor != null && _craftingExecutor.State != CraftingState.Idle + && _craftingExecutor.State != CraftingState.Done) + { + State = _craftingExecutor.State.ToString(); + return; + } if (KulemakExecutor.State != MappingState.Idle) { State = KulemakExecutor.State.ToString(); diff --git a/src/Automata.Bot/CombatManager.cs b/src/Automata.Bot/CombatManager.cs index d56896f..d494cde 100644 --- a/src/Automata.Bot/CombatManager.cs +++ b/src/Automata.Bot/CombatManager.cs @@ -1,10 +1,10 @@ using System.Diagnostics; -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.Screen; +using Automata.Core; +using Automata.Game; +using Automata.Screen; using Serilog; -namespace Poe2Trade.Bot; +namespace Automata.Bot; /// /// Manages the attack state machine (click → hold) with mana monitoring and flask usage. diff --git a/src/Automata.Bot/CraftingExecutor.cs b/src/Automata.Bot/CraftingExecutor.cs new file mode 100644 index 0000000..6b0a738 --- /dev/null +++ b/src/Automata.Bot/CraftingExecutor.cs @@ -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? 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 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)); + } + } +} diff --git a/src/Automata.Bot/DiamondExecutor.cs b/src/Automata.Bot/DiamondExecutor.cs index f3e9833..590a58f 100644 --- a/src/Automata.Bot/DiamondExecutor.cs +++ b/src/Automata.Bot/DiamondExecutor.cs @@ -1,12 +1,12 @@ using System.Collections.Concurrent; -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.Inventory; -using Poe2Trade.Screen; -using Poe2Trade.Trade; +using Automata.Core; +using Automata.Game; +using Automata.Inventory; +using Automata.Screen; +using Automata.Trade; using Serilog; -namespace Poe2Trade.Bot; +namespace Automata.Bot; public class DiamondExecutor { diff --git a/src/Automata.Bot/FlaskManager.cs b/src/Automata.Bot/FlaskManager.cs index 70b0649..476b4b7 100644 --- a/src/Automata.Bot/FlaskManager.cs +++ b/src/Automata.Bot/FlaskManager.cs @@ -1,9 +1,9 @@ using System.Diagnostics; -using Poe2Trade.Game; -using Poe2Trade.Screen; +using Automata.Game; +using Automata.Screen; using Serilog; -namespace Poe2Trade.Bot; +namespace Automata.Bot; /// /// Monitors life/mana and presses flask keys when they drop below thresholds. diff --git a/src/Automata.Bot/GameExecutor.cs b/src/Automata.Bot/GameExecutor.cs index 0d8b143..1f7386e 100644 --- a/src/Automata.Bot/GameExecutor.cs +++ b/src/Automata.Bot/GameExecutor.cs @@ -1,11 +1,11 @@ using System.Diagnostics; -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.Inventory; -using Poe2Trade.Screen; +using Automata.Core; +using Automata.Game; +using Automata.Inventory; +using Automata.Screen; using Serilog; -namespace Poe2Trade.Bot; +namespace Automata.Bot; /// /// Base class for game executors that interact with the game world. diff --git a/src/Automata.Bot/KulemakExecutor.cs b/src/Automata.Bot/KulemakExecutor.cs index 8990b62..1abd402 100644 --- a/src/Automata.Bot/KulemakExecutor.cs +++ b/src/Automata.Bot/KulemakExecutor.cs @@ -1,12 +1,12 @@ -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.GameLog; -using Poe2Trade.Inventory; -using Poe2Trade.Navigation; -using Poe2Trade.Screen; +using Automata.Core; +using Automata.Game; +using Automata.GameLog; +using Automata.Inventory; +using Automata.Navigation; +using Automata.Screen; using Serilog; -namespace Poe2Trade.Bot; +namespace Automata.Bot; /// /// Kulemak-specific boss run executor: scripted 4-phase + ring fight, @@ -218,7 +218,7 @@ public class KulemakExecutor : MappingExecutor await _game.MoveMouseTo(x, y); await Sleep(200); await _game.CtrlLeftClickAt(x, y); - await Sleep(500); + await Sleep(1000); var matches = await _screen.TemplateMatchAll(NewInstanceTemplate); if (matches.Count == 0) diff --git a/src/Automata.Bot/MappingExecutor.cs b/src/Automata.Bot/MappingExecutor.cs index 980e04a..a8979ea 100644 --- a/src/Automata.Bot/MappingExecutor.cs +++ b/src/Automata.Bot/MappingExecutor.cs @@ -1,13 +1,13 @@ using System.Diagnostics; -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.GameLog; -using Poe2Trade.Inventory; -using Poe2Trade.Navigation; -using Poe2Trade.Screen; +using Automata.Core; +using Automata.Game; +using Automata.GameLog; +using Automata.Inventory; +using Automata.Navigation; +using Automata.Screen; using Serilog; -namespace Poe2Trade.Bot; +namespace Automata.Bot; /// /// Shared infrastructure for any map/boss activity: combat loop, WASD navigation, diff --git a/src/Automata.Bot/Poe2Trade.Bot.csproj b/src/Automata.Bot/Poe2Trade.Bot.csproj deleted file mode 100644 index a6e1157..0000000 --- a/src/Automata.Bot/Poe2Trade.Bot.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - net8.0-windows10.0.19041.0 - enable - enable - - - - - - - - - - - diff --git a/src/Automata.Bot/ScrapExecutor.cs b/src/Automata.Bot/ScrapExecutor.cs index 9f2cea1..3631265 100644 --- a/src/Automata.Bot/ScrapExecutor.cs +++ b/src/Automata.Bot/ScrapExecutor.cs @@ -1,11 +1,11 @@ -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.Inventory; -using Poe2Trade.Screen; -using Poe2Trade.Trade; +using Automata.Core; +using Automata.Game; +using Automata.Inventory; +using Automata.Screen; +using Automata.Trade; using Serilog; -namespace Poe2Trade.Bot; +namespace Automata.Bot; public class ScrapExecutor { diff --git a/src/Automata.Bot/TradeExecutor.cs b/src/Automata.Bot/TradeExecutor.cs index 66a7384..dfb1a34 100644 --- a/src/Automata.Bot/TradeExecutor.cs +++ b/src/Automata.Bot/TradeExecutor.cs @@ -1,11 +1,11 @@ -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.Inventory; -using Poe2Trade.Screen; -using Poe2Trade.Trade; +using Automata.Core; +using Automata.Game; +using Automata.Inventory; +using Automata.Screen; +using Automata.Trade; using Serilog; -namespace Poe2Trade.Bot; +namespace Automata.Bot; public class TradeExecutor { diff --git a/src/Automata.Bot/TradeQueue.cs b/src/Automata.Bot/TradeQueue.cs index b3eb16a..5521247 100644 --- a/src/Automata.Bot/TradeQueue.cs +++ b/src/Automata.Bot/TradeQueue.cs @@ -1,7 +1,7 @@ -using Poe2Trade.Core; +using Automata.Core; using Serilog; -namespace Poe2Trade.Bot; +namespace Automata.Bot; public class TradeQueue { diff --git a/src/Automata.Core/Poe2Trade.Core.csproj b/src/Automata.Core/Automata.Core.csproj similarity index 100% rename from src/Automata.Core/Poe2Trade.Core.csproj rename to src/Automata.Core/Automata.Core.csproj diff --git a/src/Automata.Core/ConfigStore.cs b/src/Automata.Core/ConfigStore.cs index 14107e4..6c93261 100644 --- a/src/Automata.Core/ConfigStore.cs +++ b/src/Automata.Core/ConfigStore.cs @@ -2,7 +2,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Serilog; -namespace Poe2Trade.Core; +namespace Automata.Core; public class SavedLink { @@ -18,8 +18,8 @@ public class SavedSettings { public bool Paused { get; set; } public List Links { get; set; } = []; - public string Poe2LogPath { 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 GameLogPath { get; set; } = @"C:\Program Files (x86)\Steam\steamapps\common\Path of Exile 2\logs\Client.txt"; + public string GameWindowTitle { get; set; } = "Path of Exile 2"; public string BrowserUserDataDir { get; set; } = "./browser-data"; public int TravelTimeoutMs { get; set; } = 15000; public int StashScanTimeoutMs { get; set; } = 10000; @@ -38,6 +38,9 @@ public class SavedSettings public string OcrEngine { get; set; } = "WinOCR"; public KulemakSettings Kulemak { get; set; } = new(); public DiamondSettings Diamond { get; set; } = new(); + public List Crafts { get; set; } = []; + public List CurrencyPositions { get; set; } = []; + public string SelectedLeague { get; set; } = ""; } public class DiamondPriceConfig diff --git a/src/Automata.Core/Delays.cs b/src/Automata.Core/Delays.cs index a0b7bee..01b9f16 100644 --- a/src/Automata.Core/Delays.cs +++ b/src/Automata.Core/Delays.cs @@ -1,9 +1,9 @@ -namespace Poe2Trade.Core; +namespace Automata.Core; public static class Delays { public const int PostFocus = 300; - public const int PostTravel = 3000; + public const int PostTravel = 5000; public const int PostStashOpen = 1000; public const int ClickInterval = 150; public const int PostEscape = 500; diff --git a/src/Automata.Core/Helpers.cs b/src/Automata.Core/Helpers.cs index 81519f6..19f5205 100644 --- a/src/Automata.Core/Helpers.cs +++ b/src/Automata.Core/Helpers.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Core; +namespace Automata.Core; public static class Helpers { diff --git a/src/Automata.Core/LinkManager.cs b/src/Automata.Core/LinkManager.cs index 87b10cd..7915a9a 100644 --- a/src/Automata.Core/LinkManager.cs +++ b/src/Automata.Core/LinkManager.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Poe2Trade.Core; +namespace Automata.Core; public class TradeLink { diff --git a/src/Automata.Core/Logging.cs b/src/Automata.Core/Logging.cs index 8640d5f..bd4f173 100644 --- a/src/Automata.Core/Logging.cs +++ b/src/Automata.Core/Logging.cs @@ -1,7 +1,7 @@ using Serilog; using Serilog.Events; -namespace Poe2Trade.Core; +namespace Automata.Core; public static class Logging { diff --git a/src/Automata.Core/Poe2ScoutClient.cs b/src/Automata.Core/Poe2ScoutClient.cs new file mode 100644 index 0000000..51c07ff --- /dev/null +++ b/src/Automata.Core/Poe2ScoutClient.cs @@ -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> GetLeaguesAsync() + { + try + { + var json = await Http.GetStringAsync("leagues"); + return JsonSerializer.Deserialize>(json, JsonOpts) ?? []; + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to fetch leagues from poe2scout"); + return []; + } + } + + public static async Task> GetAllCurrencyAsync(string league) + { + var all = new List(); + 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(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 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; + } + } +} diff --git a/src/Automata.Core/StashCalibration.cs b/src/Automata.Core/StashCalibration.cs index c7052e4..587b8b6 100644 --- a/src/Automata.Core/StashCalibration.cs +++ b/src/Automata.Core/StashCalibration.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Core; +namespace Automata.Core; public class StashTabInfo { diff --git a/src/Automata.Core/Types.cs b/src/Automata.Core/Types.cs index 3287afd..9f27990 100644 --- a/src/Automata.Core/Types.cs +++ b/src/Automata.Core/Types.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Core; +namespace Automata.Core; public record Region(int X, int Y, int Width, int Height); @@ -94,7 +94,8 @@ public enum PostAction public enum BotMode { Trading, - Mapping + Mapping, + Crafting } public enum MappingState @@ -138,3 +139,46 @@ public interface IGameStateProvider GameUiState CurrentState { get; } event Action? 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 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 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 Items); + +public record Poe2ScoutCurrency(int Id, string ApiId, string Text, string IconUrl, double CurrentPrice); + diff --git a/src/Automata.Game/Poe2Trade.Game.csproj b/src/Automata.Game/Automata.Game.csproj similarity index 80% rename from src/Automata.Game/Poe2Trade.Game.csproj rename to src/Automata.Game/Automata.Game.csproj index d7dab72..fa49371 100644 --- a/src/Automata.Game/Poe2Trade.Game.csproj +++ b/src/Automata.Game/Automata.Game.csproj @@ -6,6 +6,6 @@ true - + diff --git a/src/Automata.Game/ClipboardHelper.cs b/src/Automata.Game/ClipboardHelper.cs index ed65833..c895916 100644 --- a/src/Automata.Game/ClipboardHelper.cs +++ b/src/Automata.Game/ClipboardHelper.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using System.Text; -namespace Poe2Trade.Game; +namespace Automata.Game; /// /// Win32 clipboard access without WinForms dependency. diff --git a/src/Automata.Game/GameController.cs b/src/Automata.Game/GameController.cs index 81490cc..5b82b3c 100644 --- a/src/Automata.Game/GameController.cs +++ b/src/Automata.Game/GameController.cs @@ -1,7 +1,7 @@ -using Poe2Trade.Core; +using Automata.Core; using Serilog; -namespace Poe2Trade.Game; +namespace Automata.Game; public class GameController : IGameController { @@ -10,7 +10,7 @@ public class GameController : IGameController public GameController(SavedSettings config) { - _windowManager = new WindowManager(config.Poe2WindowTitle); + _windowManager = new WindowManager(config.GameWindowTitle); _input = new InputSender(); } diff --git a/src/Automata.Game/IGameController.cs b/src/Automata.Game/IGameController.cs index d5e9dfe..9359f16 100644 --- a/src/Automata.Game/IGameController.cs +++ b/src/Automata.Game/IGameController.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Game; +namespace Automata.Game; public interface IGameController { diff --git a/src/Automata.Game/InputSender.cs b/src/Automata.Game/InputSender.cs index b984d24..4942aeb 100644 --- a/src/Automata.Game/InputSender.cs +++ b/src/Automata.Game/InputSender.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using Poe2Trade.Core; +using Automata.Core; -namespace Poe2Trade.Game; +namespace Automata.Game; public class InputSender { diff --git a/src/Automata.Game/WindowManager.cs b/src/Automata.Game/WindowManager.cs index 04838db..f593cfb 100644 --- a/src/Automata.Game/WindowManager.cs +++ b/src/Automata.Game/WindowManager.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using Serilog; -namespace Poe2Trade.Game; +namespace Automata.Game; public class WindowManager { diff --git a/src/Automata.Inventory/Automata.Inventory.csproj b/src/Automata.Inventory/Automata.Inventory.csproj new file mode 100644 index 0000000..bfb3ac5 --- /dev/null +++ b/src/Automata.Inventory/Automata.Inventory.csproj @@ -0,0 +1,13 @@ + + + net8.0-windows10.0.19041.0 + enable + enable + + + + + + + + diff --git a/src/Automata.Inventory/IInventoryManager.cs b/src/Automata.Inventory/IInventoryManager.cs index 0cf6d9c..ee1a93d 100644 --- a/src/Automata.Inventory/IInventoryManager.cs +++ b/src/Automata.Inventory/IInventoryManager.cs @@ -1,7 +1,7 @@ -using Poe2Trade.Core; -using Poe2Trade.Screen; +using Automata.Core; +using Automata.Screen; -namespace Poe2Trade.Inventory; +namespace Automata.Inventory; public interface IInventoryManager { diff --git a/src/Automata.Inventory/InventoryManager.cs b/src/Automata.Inventory/InventoryManager.cs index 2d3e1d7..1e31c55 100644 --- a/src/Automata.Inventory/InventoryManager.cs +++ b/src/Automata.Inventory/InventoryManager.cs @@ -1,10 +1,10 @@ -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.GameLog; -using Poe2Trade.Screen; +using Automata.Core; +using Automata.Game; +using Automata.GameLog; +using Automata.Screen; using Serilog; -namespace Poe2Trade.Inventory; +namespace Automata.Inventory; public class InventoryManager : IInventoryManager { diff --git a/src/Automata.Inventory/InventoryTracker.cs b/src/Automata.Inventory/InventoryTracker.cs index 255e92b..ef33f4f 100644 --- a/src/Automata.Inventory/InventoryTracker.cs +++ b/src/Automata.Inventory/InventoryTracker.cs @@ -1,8 +1,8 @@ -using Poe2Trade.Core; -using Poe2Trade.Screen; +using Automata.Core; +using Automata.Screen; using Serilog; -namespace Poe2Trade.Inventory; +namespace Automata.Inventory; public class PlacedItem { diff --git a/src/Automata.Inventory/Poe2Trade.Inventory.csproj b/src/Automata.Inventory/Poe2Trade.Inventory.csproj deleted file mode 100644 index d3c2a6f..0000000 --- a/src/Automata.Inventory/Poe2Trade.Inventory.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net8.0-windows10.0.19041.0 - enable - enable - - - - - - - - diff --git a/src/Automata.Inventory/StashCalibrator.cs b/src/Automata.Inventory/StashCalibrator.cs index 0b01fdf..657f9d6 100644 --- a/src/Automata.Inventory/StashCalibrator.cs +++ b/src/Automata.Inventory/StashCalibrator.cs @@ -1,9 +1,9 @@ -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.Screen; +using Automata.Core; +using Automata.Game; +using Automata.Screen; using Serilog; -namespace Poe2Trade.Inventory; +namespace Automata.Inventory; public class StashCalibrator { diff --git a/src/Automata.Items/Poe2Trade.Items.csproj b/src/Automata.Items/Automata.Items.csproj similarity index 62% rename from src/Automata.Items/Poe2Trade.Items.csproj rename to src/Automata.Items/Automata.Items.csproj index 5418aa8..5addc92 100644 --- a/src/Automata.Items/Poe2Trade.Items.csproj +++ b/src/Automata.Items/Automata.Items.csproj @@ -5,7 +5,7 @@ enable - - + + diff --git a/src/Automata.Items/ItemReader.cs b/src/Automata.Items/ItemReader.cs index e8d4654..c460243 100644 --- a/src/Automata.Items/ItemReader.cs +++ b/src/Automata.Items/ItemReader.cs @@ -1,8 +1,8 @@ -using Poe2Trade.Core; -using Poe2Trade.Game; +using Automata.Core; +using Automata.Game; using Serilog; -namespace Poe2Trade.Items; +namespace Automata.Items; /// /// Reads item data by hovering and pressing Ctrl+C to copy item text to clipboard. diff --git a/src/Automata.Memory/Automata.Memory.csproj b/src/Automata.Memory/Automata.Memory.csproj new file mode 100644 index 0000000..fa49371 --- /dev/null +++ b/src/Automata.Memory/Automata.Memory.csproj @@ -0,0 +1,11 @@ + + + net8.0-windows10.0.19041.0 + enable + enable + true + + + + + diff --git a/src/Automata.Memory/Native.cs b/src/Automata.Memory/Native.cs new file mode 100644 index 0000000..3283c56 --- /dev/null +++ b/src/Automata.Memory/Native.cs @@ -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); +} diff --git a/src/Automata.Memory/PatternScanner.cs b/src/Automata.Memory/PatternScanner.cs new file mode 100644 index 0000000..ba9d2fa --- /dev/null +++ b/src/Automata.Memory/PatternScanner.cs @@ -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; + } + + /// + /// 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. + /// + 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; + } + + /// + /// FindPattern + RIP-relative resolution: reads int32 displacement at matched address and resolves to absolute address. + /// Result = matchAddr + 4 + displacement + /// + 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; + } + + /// + /// Parses a pattern string into bytes, mask, and result offset. + /// Tokens: hex byte (e.g. "4C") = must-match, "??" = wildcard, "^" = result offset marker. + /// + internal static (byte[] Bytes, bool[] Mask, int ResultOffset) Parse(string pattern) + { + var tokens = pattern.Split(' ', StringSplitOptions.RemoveEmptyEntries); + var bytes = new List(); + var mask = new List(); + 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); + } +} diff --git a/src/Automata.Memory/ProcessMemory.cs b/src/Automata.Memory/ProcessMemory.cs new file mode 100644 index 0000000..7330f0c --- /dev/null +++ b/src/Automata.Memory/ProcessMemory.cs @@ -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 buffer) + { + unsafe + { + fixed (byte* ptr = buffer) + { + return Native.ReadProcessMemory(_handle, address, (nint)ptr, buffer.Length, out _); + } + } + } + + public T Read(nint address) where T : unmanaged + { + Span buf = stackalloc byte[Unsafe.SizeOf()]; + if (!ReadBytes(address, buf)) + return default; + return Unsafe.ReadUnaligned(ref buf[0]); + } + + public nint ReadPointer(nint address) => Read(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())) + { + Log.Error("GetModuleInformation failed"); + return null; + } + + return (info.lpBaseOfDll, info.SizeOfImage); + } + + /// + /// 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. + /// + public nint FollowChain(nint baseAddr, ReadOnlySpan 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); + } + } +} diff --git a/src/Automata.Memory/TerrainOffsets.cs b/src/Automata.Memory/TerrainOffsets.cs new file mode 100644 index 0000000..950205d --- /dev/null +++ b/src/Automata.Memory/TerrainOffsets.cs @@ -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"; + + /// Pattern to find GameState base pointer. Empty = unknown for POE2. + 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; } + + /// Number of sub-tiles per terrain cell (typically 23 for POE). + 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(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); + } + } +} diff --git a/src/Automata.Memory/TerrainReader.cs b/src/Automata.Memory/TerrainReader.cs new file mode 100644 index 0000000..86c456d --- /dev/null +++ b/src/Automata.Memory/TerrainReader.cs @@ -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(terrainBase + _offsets.NumColsOffset); + var numRows = _memory.Read(terrainBase + _offsets.NumRowsOffset); + var bytesPerRow = _memory.Read(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(); + } +} diff --git a/src/Automata.Navigation/AtlasPanorama.cs b/src/Automata.Navigation/AtlasPanorama.cs index 78192e3..354baa6 100644 --- a/src/Automata.Navigation/AtlasPanorama.cs +++ b/src/Automata.Navigation/AtlasPanorama.cs @@ -1,10 +1,10 @@ using System.Diagnostics; using OpenCvSharp; -using Poe2Trade.Core; -using Poe2Trade.Screen; +using Automata.Core; +using Automata.Screen; using Serilog; -namespace Poe2Trade.Navigation; +namespace Automata.Navigation; public record AtlasProgress(int TilesCaptured, int Row, string Phase); diff --git a/src/Automata.Navigation/Poe2Trade.Navigation.csproj b/src/Automata.Navigation/Automata.Navigation.csproj similarity index 73% rename from src/Automata.Navigation/Poe2Trade.Navigation.csproj rename to src/Automata.Navigation/Automata.Navigation.csproj index a14735f..29698e6 100644 --- a/src/Automata.Navigation/Poe2Trade.Navigation.csproj +++ b/src/Automata.Navigation/Automata.Navigation.csproj @@ -12,8 +12,8 @@ - - - + + + diff --git a/src/Automata.Navigation/IconDetector.cs b/src/Automata.Navigation/IconDetector.cs index 8e915c0..9312879 100644 --- a/src/Automata.Navigation/IconDetector.cs +++ b/src/Automata.Navigation/IconDetector.cs @@ -1,7 +1,7 @@ using OpenCvSharp; using Serilog; -namespace Poe2Trade.Navigation; +namespace Automata.Navigation; /// /// Detects minimap icons (doors, checkpoints) via template matching. diff --git a/src/Automata.Navigation/MinimapCapture.cs b/src/Automata.Navigation/MinimapCapture.cs index 576422a..e7e3093 100644 --- a/src/Automata.Navigation/MinimapCapture.cs +++ b/src/Automata.Navigation/MinimapCapture.cs @@ -1,10 +1,10 @@ using OpenCvSharp; -using Poe2Trade.Screen; +using Automata.Screen; using Serilog; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; using Size = OpenCvSharp.Size; -namespace Poe2Trade.Navigation; +namespace Automata.Navigation; public class MinimapCapture : IFrameConsumer, IDisposable { diff --git a/src/Automata.Navigation/NavigationExecutor.cs b/src/Automata.Navigation/NavigationExecutor.cs index bf5a804..a179164 100644 --- a/src/Automata.Navigation/NavigationExecutor.cs +++ b/src/Automata.Navigation/NavigationExecutor.cs @@ -1,11 +1,11 @@ using System.Diagnostics; using OpenCvSharp; -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.Screen; +using Automata.Core; +using Automata.Game; +using Automata.Screen; using Serilog; -namespace Poe2Trade.Navigation; +namespace Automata.Navigation; public class NavigationExecutor : IDisposable { diff --git a/src/Automata.Navigation/NavigationTypes.cs b/src/Automata.Navigation/NavigationTypes.cs index 408e7ab..8b2c77a 100644 --- a/src/Automata.Navigation/NavigationTypes.cs +++ b/src/Automata.Navigation/NavigationTypes.cs @@ -1,7 +1,7 @@ -using Poe2Trade.Core; +using Automata.Core; using OpenCvSharp; -namespace Poe2Trade.Navigation; +namespace Automata.Navigation; public enum MinimapMode { diff --git a/src/Automata.Navigation/PathFinder.cs b/src/Automata.Navigation/PathFinder.cs index b36faf1..9445e81 100644 --- a/src/Automata.Navigation/PathFinder.cs +++ b/src/Automata.Navigation/PathFinder.cs @@ -1,7 +1,7 @@ using OpenCvSharp; using Serilog; -namespace Poe2Trade.Navigation; +namespace Automata.Navigation; /// /// Last BFS result for visualization. diff --git a/src/Automata.Navigation/PerspectiveCalibrator.cs b/src/Automata.Navigation/PerspectiveCalibrator.cs index fe1e9ac..1f3ab8f 100644 --- a/src/Automata.Navigation/PerspectiveCalibrator.cs +++ b/src/Automata.Navigation/PerspectiveCalibrator.cs @@ -1,9 +1,9 @@ using OpenCvSharp; -using Poe2Trade.Core; -using Poe2Trade.Screen; +using Automata.Core; +using Automata.Screen; using Serilog; -namespace Poe2Trade.Navigation; +namespace Automata.Navigation; public record CalibrationResult(float BestFactor, double BestConfidence, Dictionary AllResults); diff --git a/src/Automata.Navigation/StuckDetector.cs b/src/Automata.Navigation/StuckDetector.cs index f50fb16..5dd02c5 100644 --- a/src/Automata.Navigation/StuckDetector.cs +++ b/src/Automata.Navigation/StuckDetector.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Navigation; +namespace Automata.Navigation; /// /// Detects when the player hasn't moved significantly over a window of frames. diff --git a/src/Automata.Navigation/WallColorTracker.cs b/src/Automata.Navigation/WallColorTracker.cs index 6d889db..a28d810 100644 --- a/src/Automata.Navigation/WallColorTracker.cs +++ b/src/Automata.Navigation/WallColorTracker.cs @@ -1,7 +1,7 @@ using OpenCvSharp; using Serilog; -namespace Poe2Trade.Navigation; +namespace Automata.Navigation; /// /// Tracks HSV distribution of confirmed wall pixels and computes an adaptive diff --git a/src/Automata.Navigation/WorldMap.cs b/src/Automata.Navigation/WorldMap.cs index fa9ca41..0206e4c 100644 --- a/src/Automata.Navigation/WorldMap.cs +++ b/src/Automata.Navigation/WorldMap.cs @@ -2,7 +2,7 @@ using System.Diagnostics; using OpenCvSharp; using Serilog; -namespace Poe2Trade.Navigation; +namespace Automata.Navigation; public class WorldMap : IDisposable { diff --git a/src/Automata.Screen/Poe2Trade.Screen.csproj b/src/Automata.Screen/Automata.Screen.csproj similarity index 91% rename from src/Automata.Screen/Poe2Trade.Screen.csproj rename to src/Automata.Screen/Automata.Screen.csproj index bcfbcf4..780d038 100644 --- a/src/Automata.Screen/Poe2Trade.Screen.csproj +++ b/src/Automata.Screen/Automata.Screen.csproj @@ -15,6 +15,6 @@ - + diff --git a/src/Automata.Screen/BossDetector.cs b/src/Automata.Screen/BossDetector.cs index 94db95b..2fb6f31 100644 --- a/src/Automata.Screen/BossDetector.cs +++ b/src/Automata.Screen/BossDetector.cs @@ -1,8 +1,8 @@ using OpenCvSharp; using Serilog; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; -namespace Poe2Trade.Screen; +namespace Automata.Screen; /// /// Detects bosses using YOLO running on a background thread. diff --git a/src/Automata.Screen/DaemonTypes.cs b/src/Automata.Screen/DaemonTypes.cs index 8aee843..f149805 100644 --- a/src/Automata.Screen/DaemonTypes.cs +++ b/src/Automata.Screen/DaemonTypes.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Poe2Trade.Screen; +namespace Automata.Screen; public class OcrWord { @@ -49,7 +49,7 @@ public class DiffOcrResponse { public string Text { get; set; } = ""; public List Lines { get; set; } = []; - public Poe2Trade.Core.Region? Region { get; set; } + public Automata.Core.Region? Region { get; set; } } public class TemplateMatchResult diff --git a/src/Automata.Screen/DesktopDuplication.cs b/src/Automata.Screen/DesktopDuplication.cs index 8f2a30b..4d81bcf 100644 --- a/src/Automata.Screen/DesktopDuplication.cs +++ b/src/Automata.Screen/DesktopDuplication.cs @@ -5,9 +5,9 @@ using SharpGen.Runtime; using Vortice.Direct3D; using Vortice.Direct3D11; using Vortice.DXGI; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; -namespace Poe2Trade.Screen; +namespace Automata.Screen; public sealed class DesktopDuplication : IScreenCapture { diff --git a/src/Automata.Screen/DetectGridHandler.cs b/src/Automata.Screen/DetectGridHandler.cs index 7fca666..ab4b4f0 100644 --- a/src/Automata.Screen/DetectGridHandler.cs +++ b/src/Automata.Screen/DetectGridHandler.cs @@ -1,10 +1,10 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using Serilog; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; class DetectGridHandler { diff --git a/src/Automata.Screen/DetectionTypes.cs b/src/Automata.Screen/DetectionTypes.cs index 3d05050..2ada4bf 100644 --- a/src/Automata.Screen/DetectionTypes.cs +++ b/src/Automata.Screen/DetectionTypes.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; public record DetectedEnemy( float Confidence, diff --git a/src/Automata.Screen/DiffCropHandler.cs b/src/Automata.Screen/DiffCropHandler.cs index 03215ee..acd300f 100644 --- a/src/Automata.Screen/DiffCropHandler.cs +++ b/src/Automata.Screen/DiffCropHandler.cs @@ -1,10 +1,10 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using Serilog; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; class DiffCropHandler { diff --git a/src/Automata.Screen/EdgeCropHandler.cs b/src/Automata.Screen/EdgeCropHandler.cs index ebe68df..6fbe46b 100644 --- a/src/Automata.Screen/EdgeCropHandler.cs +++ b/src/Automata.Screen/EdgeCropHandler.cs @@ -1,10 +1,10 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using Serilog; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; class EdgeCropHandler { diff --git a/src/Automata.Screen/EnemyDetector.cs b/src/Automata.Screen/EnemyDetector.cs index d2588d9..37727bf 100644 --- a/src/Automata.Screen/EnemyDetector.cs +++ b/src/Automata.Screen/EnemyDetector.cs @@ -1,9 +1,9 @@ using OpenCvSharp; -using Poe2Trade.Core; +using Automata.Core; using Serilog; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; -namespace Poe2Trade.Screen; +namespace Automata.Screen; /// /// Detects enemies on screen using two-stage approach: diff --git a/src/Automata.Screen/FramePipeline.cs b/src/Automata.Screen/FramePipeline.cs index 3c0451d..e8734e5 100644 --- a/src/Automata.Screen/FramePipeline.cs +++ b/src/Automata.Screen/FramePipeline.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; public class FramePipeline : IDisposable { diff --git a/src/Automata.Screen/FramePipelineService.cs b/src/Automata.Screen/FramePipelineService.cs index cc48d7c..b213661 100644 --- a/src/Automata.Screen/FramePipelineService.cs +++ b/src/Automata.Screen/FramePipelineService.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Poe2Trade.Screen; +namespace Automata.Screen; public class FramePipelineService : IDisposable { diff --git a/src/Automata.Screen/FrameSaver.cs b/src/Automata.Screen/FrameSaver.cs index 9bdfb17..2eb2c88 100644 --- a/src/Automata.Screen/FrameSaver.cs +++ b/src/Automata.Screen/FrameSaver.cs @@ -1,8 +1,8 @@ using OpenCvSharp; using Serilog; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; -namespace Poe2Trade.Screen; +namespace Automata.Screen; /// /// Saves full-screen frames as JPEGs for YOLO training data collection. diff --git a/src/Automata.Screen/GameStateDetector.cs b/src/Automata.Screen/GameStateDetector.cs index f26ffb1..89be1bd 100644 --- a/src/Automata.Screen/GameStateDetector.cs +++ b/src/Automata.Screen/GameStateDetector.cs @@ -1,7 +1,7 @@ -using Poe2Trade.Core; +using Automata.Core; using Serilog; -namespace Poe2Trade.Screen; +namespace Automata.Screen; /// /// Classifies the current game UI state by probing known pixel positions on each frame. diff --git a/src/Automata.Screen/GdiCapture.cs b/src/Automata.Screen/GdiCapture.cs index d4329de..ff72148 100644 --- a/src/Automata.Screen/GdiCapture.cs +++ b/src/Automata.Screen/GdiCapture.cs @@ -3,9 +3,9 @@ using System.Drawing.Imaging; using System.Runtime.InteropServices; using OpenCvSharp; using OpenCvSharp.Extensions; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; -namespace Poe2Trade.Screen; +namespace Automata.Screen; public sealed class GdiCapture : IScreenCapture { diff --git a/src/Automata.Screen/GridHandler.cs b/src/Automata.Screen/GridHandler.cs index 838e1a8..6943bcd 100644 --- a/src/Automata.Screen/GridHandler.cs +++ b/src/Automata.Screen/GridHandler.cs @@ -1,8 +1,8 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; using System.Drawing; using Serilog; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; public class GridHandler { diff --git a/src/Automata.Screen/GridReader.cs b/src/Automata.Screen/GridReader.cs index 48518d4..0a9285d 100644 --- a/src/Automata.Screen/GridReader.cs +++ b/src/Automata.Screen/GridReader.cs @@ -1,7 +1,7 @@ -using Poe2Trade.Core; +using Automata.Core; using Serilog; -namespace Poe2Trade.Screen; +namespace Automata.Screen; public class GridLayout { diff --git a/src/Automata.Screen/HudReader.cs b/src/Automata.Screen/HudReader.cs index fb867a8..3e60e93 100644 --- a/src/Automata.Screen/HudReader.cs +++ b/src/Automata.Screen/HudReader.cs @@ -1,9 +1,9 @@ using OpenCvSharp; -using Poe2Trade.Core; +using Automata.Core; using Serilog; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; -namespace Poe2Trade.Screen; +namespace Automata.Screen; public record HudSnapshot { diff --git a/src/Automata.Screen/IFrameConsumer.cs b/src/Automata.Screen/IFrameConsumer.cs index 4dbcc99..b7089c6 100644 --- a/src/Automata.Screen/IFrameConsumer.cs +++ b/src/Automata.Screen/IFrameConsumer.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; public interface IFrameConsumer { diff --git a/src/Automata.Screen/IOcrEngine.cs b/src/Automata.Screen/IOcrEngine.cs index 87c8397..6822e5c 100644 --- a/src/Automata.Screen/IOcrEngine.cs +++ b/src/Automata.Screen/IOcrEngine.cs @@ -1,6 +1,6 @@ using System.Drawing; -namespace Poe2Trade.Screen; +namespace Automata.Screen; public interface IOcrEngine : IDisposable { diff --git a/src/Automata.Screen/IScreenCapture.cs b/src/Automata.Screen/IScreenCapture.cs index e7c901e..4e96887 100644 --- a/src/Automata.Screen/IScreenCapture.cs +++ b/src/Automata.Screen/IScreenCapture.cs @@ -1,7 +1,7 @@ using OpenCvSharp; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; -namespace Poe2Trade.Screen; +namespace Automata.Screen; public interface IScreenCapture : IDisposable { diff --git a/src/Automata.Screen/IScreenReader.cs b/src/Automata.Screen/IScreenReader.cs index d044f57..3d5e808 100644 --- a/src/Automata.Screen/IScreenReader.cs +++ b/src/Automata.Screen/IScreenReader.cs @@ -1,6 +1,6 @@ -using Poe2Trade.Core; +using Automata.Core; -namespace Poe2Trade.Screen; +namespace Automata.Screen; public interface IScreenReader : IDisposable { diff --git a/src/Automata.Screen/ImagePreprocessor.cs b/src/Automata.Screen/ImagePreprocessor.cs index bb033f2..4e374e6 100644 --- a/src/Automata.Screen/ImagePreprocessor.cs +++ b/src/Automata.Screen/ImagePreprocessor.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; using System.Drawing; using OpenCvSharp; diff --git a/src/Automata.Screen/ImageUtils.cs b/src/Automata.Screen/ImageUtils.cs index 2004c4f..8cb346b 100644 --- a/src/Automata.Screen/ImageUtils.cs +++ b/src/Automata.Screen/ImageUtils.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; using System.Drawing; using System.Drawing.Imaging; diff --git a/src/Automata.Screen/LootDebugDetector.cs b/src/Automata.Screen/LootDebugDetector.cs index fbf7a28..47c2f4c 100644 --- a/src/Automata.Screen/LootDebugDetector.cs +++ b/src/Automata.Screen/LootDebugDetector.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Poe2Trade.Screen; +namespace Automata.Screen; /// /// Debug-only: periodically captures the screen, runs loot label detection, diff --git a/src/Automata.Screen/LootLabel.cs b/src/Automata.Screen/LootLabel.cs index 0ae2d0b..a3fd8d3 100644 --- a/src/Automata.Screen/LootLabel.cs +++ b/src/Automata.Screen/LootLabel.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; /// /// A detected loot label on screen with its position and classified tier. diff --git a/src/Automata.Screen/Ocr/EasyOcrEngine.cs b/src/Automata.Screen/Ocr/EasyOcrEngine.cs index 4ea9900..47d6655 100644 --- a/src/Automata.Screen/Ocr/EasyOcrEngine.cs +++ b/src/Automata.Screen/Ocr/EasyOcrEngine.cs @@ -1,6 +1,6 @@ using System.Drawing; -namespace Poe2Trade.Screen.Ocr; +namespace Automata.Screen.Ocr; /// /// OCR engine wrapping the Python EasyOCR daemon. diff --git a/src/Automata.Screen/Ocr/OcrEngineFactory.cs b/src/Automata.Screen/Ocr/OcrEngineFactory.cs index 774a419..8ee2636 100644 --- a/src/Automata.Screen/Ocr/OcrEngineFactory.cs +++ b/src/Automata.Screen/Ocr/OcrEngineFactory.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Poe2Trade.Screen.Ocr; +namespace Automata.Screen.Ocr; public static class OcrEngineFactory { diff --git a/src/Automata.Screen/Ocr/OneOcrEngine.cs b/src/Automata.Screen/Ocr/OneOcrEngine.cs index c54e952..a982024 100644 --- a/src/Automata.Screen/Ocr/OneOcrEngine.cs +++ b/src/Automata.Screen/Ocr/OneOcrEngine.cs @@ -3,7 +3,7 @@ using System.Drawing.Imaging; using System.Runtime.InteropServices; using Serilog; -namespace Poe2Trade.Screen.Ocr; +namespace Automata.Screen.Ocr; /// /// OCR engine using OneOCR (Windows 11 Snipping Tool's built-in engine). diff --git a/src/Automata.Screen/Ocr/WinOcrEngine.cs b/src/Automata.Screen/Ocr/WinOcrEngine.cs index 269e433..f9688f1 100644 --- a/src/Automata.Screen/Ocr/WinOcrEngine.cs +++ b/src/Automata.Screen/Ocr/WinOcrEngine.cs @@ -7,7 +7,7 @@ using Windows.Storage.Streams; using BitmapDecoder = Windows.Graphics.Imaging.BitmapDecoder; using SdImageFormat = System.Drawing.Imaging.ImageFormat; -namespace Poe2Trade.Screen.Ocr; +namespace Automata.Screen.Ocr; public sealed class WinOcrEngine : IOcrEngine { diff --git a/src/Automata.Screen/OnnxYoloDetector.cs b/src/Automata.Screen/OnnxYoloDetector.cs index 006ce6f..0ac27bd 100644 --- a/src/Automata.Screen/OnnxYoloDetector.cs +++ b/src/Automata.Screen/OnnxYoloDetector.cs @@ -5,7 +5,7 @@ using OpenCvSharp; using OpenCvSharp.Dnn; using Serilog; -namespace Poe2Trade.Screen; +namespace Automata.Screen; /// /// YOLO11 object detection via ONNX Runtime with CUDA GPU acceleration. diff --git a/src/Automata.Screen/PythonDetectBridge.cs b/src/Automata.Screen/PythonDetectBridge.cs index c97c016..6a083a3 100644 --- a/src/Automata.Screen/PythonDetectBridge.cs +++ b/src/Automata.Screen/PythonDetectBridge.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; using System.Diagnostics; using System.Text.Json; diff --git a/src/Automata.Screen/PythonOcrBridge.cs b/src/Automata.Screen/PythonOcrBridge.cs index de55714..ef206d9 100644 --- a/src/Automata.Screen/PythonOcrBridge.cs +++ b/src/Automata.Screen/PythonOcrBridge.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; using System.Diagnostics; using System.Drawing; diff --git a/src/Automata.Screen/ScreenCapture.cs b/src/Automata.Screen/ScreenCapture.cs index 586b749..8d64f61 100644 --- a/src/Automata.Screen/ScreenCapture.cs +++ b/src/Automata.Screen/ScreenCapture.cs @@ -1,9 +1,9 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; static class ScreenCapture { diff --git a/src/Automata.Screen/ScreenFrame.cs b/src/Automata.Screen/ScreenFrame.cs index 29110a4..7acceeb 100644 --- a/src/Automata.Screen/ScreenFrame.cs +++ b/src/Automata.Screen/ScreenFrame.cs @@ -1,7 +1,7 @@ using OpenCvSharp; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; -namespace Poe2Trade.Screen; +namespace Automata.Screen; public class ScreenFrame : IDisposable { diff --git a/src/Automata.Screen/ScreenReader.cs b/src/Automata.Screen/ScreenReader.cs index b4c098c..c2457a7 100644 --- a/src/Automata.Screen/ScreenReader.cs +++ b/src/Automata.Screen/ScreenReader.cs @@ -3,12 +3,12 @@ using System.Drawing.Imaging; using System.Runtime.InteropServices; using OpenCvSharp; using OpenCvSharp.Extensions; -using Poe2Trade.Core; +using Automata.Core; using Serilog; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; using Size = OpenCvSharp.Size; -namespace Poe2Trade.Screen; +namespace Automata.Screen; public class ScreenReader : IScreenReader { diff --git a/src/Automata.Screen/SignalProcessing.cs b/src/Automata.Screen/SignalProcessing.cs index be9fce9..3f9fee9 100644 --- a/src/Automata.Screen/SignalProcessing.cs +++ b/src/Automata.Screen/SignalProcessing.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; static class SignalProcessing { diff --git a/src/Automata.Screen/TemplateMatchHandler.cs b/src/Automata.Screen/TemplateMatchHandler.cs index dc92086..d779cf1 100644 --- a/src/Automata.Screen/TemplateMatchHandler.cs +++ b/src/Automata.Screen/TemplateMatchHandler.cs @@ -1,9 +1,9 @@ -namespace Poe2Trade.Screen; +namespace Automata.Screen; using System.Drawing; using OpenCvSharp; using OpenCvSharp.Extensions; -using Region = Poe2Trade.Core.Region; +using Region = Automata.Core.Region; class TemplateMatchHandler { diff --git a/src/Automata.Trade/Poe2Trade.Trade.csproj b/src/Automata.Trade/Automata.Trade.csproj similarity index 77% rename from src/Automata.Trade/Poe2Trade.Trade.csproj rename to src/Automata.Trade/Automata.Trade.csproj index 41c563c..0d0f761 100644 --- a/src/Automata.Trade/Poe2Trade.Trade.csproj +++ b/src/Automata.Trade/Automata.Trade.csproj @@ -5,6 +5,6 @@ enable - + diff --git a/src/Automata.Trade/ITradeMonitor.cs b/src/Automata.Trade/ITradeMonitor.cs index 24a0917..ef45711 100644 --- a/src/Automata.Trade/ITradeMonitor.cs +++ b/src/Automata.Trade/ITradeMonitor.cs @@ -1,6 +1,6 @@ -using Poe2Trade.Core; +using Automata.Core; -namespace Poe2Trade.Trade; +namespace Automata.Trade; public interface ITradeMonitor : IAsyncDisposable { diff --git a/src/Automata.Trade/Selectors.cs b/src/Automata.Trade/Selectors.cs index 74b0568..eb0345a 100644 --- a/src/Automata.Trade/Selectors.cs +++ b/src/Automata.Trade/Selectors.cs @@ -1,4 +1,4 @@ -namespace Poe2Trade.Trade; +namespace Automata.Trade; public static class Selectors { diff --git a/src/Automata.Trade/TradeDaemonBridge.cs b/src/Automata.Trade/TradeDaemonBridge.cs index 459ff89..df7c768 100644 --- a/src/Automata.Trade/TradeDaemonBridge.cs +++ b/src/Automata.Trade/TradeDaemonBridge.cs @@ -2,10 +2,10 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; -using Poe2Trade.Core; +using Automata.Core; using Serilog; -namespace Poe2Trade.Trade; +namespace Automata.Trade; public class TradeDaemonBridge : ITradeMonitor { diff --git a/src/Automata.Ui/App.axaml b/src/Automata.Ui/App.axaml index 91e962e..62860d1 100644 --- a/src/Automata.Ui/App.axaml +++ b/src/Automata.Ui/App.axaml @@ -1,7 +1,7 @@ diff --git a/src/Automata.Ui/App.axaml.cs b/src/Automata.Ui/App.axaml.cs index 590fa93..fa094e6 100644 --- a/src/Automata.Ui/App.axaml.cs +++ b/src/Automata.Ui/App.axaml.cs @@ -2,19 +2,19 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Microsoft.Extensions.DependencyInjection; -using Poe2Trade.Bot; -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.GameLog; -using Poe2Trade.Inventory; -using Poe2Trade.Screen; -using Poe2Trade.Screen.Ocr; -using Poe2Trade.Trade; -using Poe2Trade.Ui.Overlay; -using Poe2Trade.Ui.ViewModels; -using Poe2Trade.Ui.Views; +using Automata.Bot; +using Automata.Core; +using Automata.Game; +using Automata.GameLog; +using Automata.Inventory; +using Automata.Screen; +using Automata.Screen.Ocr; +using Automata.Trade; +using Automata.Ui.Overlay; +using Automata.Ui.ViewModels; +using Automata.Ui.Views; -namespace Poe2Trade.Ui; +namespace Automata.Ui; public partial class App : Application { @@ -42,7 +42,7 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => - new ClientLogWatcher(sp.GetRequiredService().Poe2LogPath)); + new ClientLogWatcher(sp.GetRequiredService().GameLogPath)); services.AddSingleton(); services.AddSingleton(); @@ -59,6 +59,7 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); var provider = services.BuildServiceProvider(); @@ -70,6 +71,7 @@ public partial class App : Application mainVm.SettingsVm = provider.GetRequiredService(); mainVm.MappingVm = provider.GetRequiredService(); mainVm.AtlasVm = provider.GetRequiredService(); + mainVm.CraftingVm = provider.GetRequiredService(); var window = new MainWindow { DataContext = mainVm }; window.SetConfigStore(store); diff --git a/src/Automata.Ui/Poe2Trade.Ui.csproj b/src/Automata.Ui/Automata.Ui.csproj similarity index 63% rename from src/Automata.Ui/Poe2Trade.Ui.csproj rename to src/Automata.Ui/Automata.Ui.csproj index 87bca2a..7829aba 100644 --- a/src/Automata.Ui/Poe2Trade.Ui.csproj +++ b/src/Automata.Ui/Automata.Ui.csproj @@ -16,12 +16,12 @@ - - - - - - - + + + + + + + diff --git a/src/Automata.Ui/Converters/ValueConverters.cs b/src/Automata.Ui/Converters/ValueConverters.cs index 6546878..0b705dc 100644 --- a/src/Automata.Ui/Converters/ValueConverters.cs +++ b/src/Automata.Ui/Converters/ValueConverters.cs @@ -2,10 +2,10 @@ using System.Globalization; using Avalonia; using Avalonia.Data.Converters; using Avalonia.Media; -using Poe2Trade.Core; -using Poe2Trade.Ui.ViewModels; +using Automata.Core; +using Automata.Ui.ViewModels; -namespace Poe2Trade.Ui.Converters; +namespace Automata.Ui.Converters; public class LogLevelToBrushConverter : IValueConverter { diff --git a/src/Automata.Ui/Overlay/D2dNativeMethods.cs b/src/Automata.Ui/Overlay/D2dNativeMethods.cs index abd26c2..8befc8f 100644 --- a/src/Automata.Ui/Overlay/D2dNativeMethods.cs +++ b/src/Automata.Ui/Overlay/D2dNativeMethods.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Poe2Trade.Ui.Overlay; +namespace Automata.Ui.Overlay; /// Win32 P/Invoke for the D2D overlay window, DWM transparency, and frame timing. internal static partial class D2dNativeMethods diff --git a/src/Automata.Ui/Overlay/D2dOverlay.cs b/src/Automata.Ui/Overlay/D2dOverlay.cs index 2311a5f..80b1732 100644 --- a/src/Automata.Ui/Overlay/D2dOverlay.cs +++ b/src/Automata.Ui/Overlay/D2dOverlay.cs @@ -1,12 +1,12 @@ using System.Diagnostics; using System.Runtime; using System.Runtime.InteropServices; -using Poe2Trade.Bot; -using Poe2Trade.Ui.Overlay.Layers; +using Automata.Bot; +using Automata.Ui.Overlay.Layers; using Vortice.Mathematics; -using static Poe2Trade.Ui.Overlay.D2dNativeMethods; +using static Automata.Ui.Overlay.D2dNativeMethods; -namespace Poe2Trade.Ui.Overlay; +namespace Automata.Ui.Overlay; /// /// Fullscreen transparent overlay rendered with Direct2D on a dedicated thread. @@ -18,7 +18,7 @@ public sealed class D2dOverlay private const int Height = 1440; private const double TargetFrameMs = 16.0; // ~60 fps private const int FocusCheckInterval = 60; // frames between focus checks (~1 Hz) - private const string ClassName = "Poe2D2dOverlay"; + private const string ClassName = "D2dOverlayWnd"; private readonly BotOrchestrator _bot; private readonly List _layers = []; @@ -261,7 +261,7 @@ public sealed class D2dOverlay | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE; return CreateWindowExW( - exStyle, ClassName, "Poe2Overlay", + exStyle, ClassName, "Overlay", WS_POPUP | WS_VISIBLE, 0, 0, Width, Height, 0, 0, hInstance, 0); diff --git a/src/Automata.Ui/Overlay/D2dRenderContext.cs b/src/Automata.Ui/Overlay/D2dRenderContext.cs index 178201e..884047c 100644 --- a/src/Automata.Ui/Overlay/D2dRenderContext.cs +++ b/src/Automata.Ui/Overlay/D2dRenderContext.cs @@ -6,7 +6,7 @@ using DWriteFactory = Vortice.DirectWrite.IDWriteFactory; using D2dFactoryType = Vortice.Direct2D1.FactoryType; using DwFactoryType = Vortice.DirectWrite.FactoryType; -namespace Poe2Trade.Ui.Overlay; +namespace Automata.Ui.Overlay; public sealed class D2dRenderContext : IDisposable { diff --git a/src/Automata.Ui/Overlay/IOverlayLayer.cs b/src/Automata.Ui/Overlay/IOverlayLayer.cs index 05c2a08..1bd78fb 100644 --- a/src/Automata.Ui/Overlay/IOverlayLayer.cs +++ b/src/Automata.Ui/Overlay/IOverlayLayer.cs @@ -1,7 +1,7 @@ -using Poe2Trade.Navigation; -using Poe2Trade.Screen; +using Automata.Navigation; +using Automata.Screen; -namespace Poe2Trade.Ui.Overlay; +namespace Automata.Ui.Overlay; public record OverlayState( IReadOnlyList Enemies, diff --git a/src/Automata.Ui/Overlay/Layers/D2dDebugTextLayer.cs b/src/Automata.Ui/Overlay/Layers/D2dDebugTextLayer.cs index 6f8d94b..2fbd137 100644 --- a/src/Automata.Ui/Overlay/Layers/D2dDebugTextLayer.cs +++ b/src/Automata.Ui/Overlay/Layers/D2dDebugTextLayer.cs @@ -3,7 +3,7 @@ using Vortice.Direct2D1; using Vortice.DirectWrite; using Vortice.Mathematics; -namespace Poe2Trade.Ui.Overlay.Layers; +namespace Automata.Ui.Overlay.Layers; internal sealed class D2dDebugTextLayer : ID2dOverlayLayer, IDisposable { diff --git a/src/Automata.Ui/Overlay/Layers/D2dEnemyBoxLayer.cs b/src/Automata.Ui/Overlay/Layers/D2dEnemyBoxLayer.cs index a0c823c..13c0eb1 100644 --- a/src/Automata.Ui/Overlay/Layers/D2dEnemyBoxLayer.cs +++ b/src/Automata.Ui/Overlay/Layers/D2dEnemyBoxLayer.cs @@ -3,7 +3,7 @@ using Vortice.Direct2D1; using Vortice.DirectWrite; using Vortice.Mathematics; -namespace Poe2Trade.Ui.Overlay.Layers; +namespace Automata.Ui.Overlay.Layers; internal sealed class D2dEnemyBoxLayer : ID2dOverlayLayer, IDisposable { diff --git a/src/Automata.Ui/Overlay/Layers/D2dHudInfoLayer.cs b/src/Automata.Ui/Overlay/Layers/D2dHudInfoLayer.cs index 8ce549c..6381856 100644 --- a/src/Automata.Ui/Overlay/Layers/D2dHudInfoLayer.cs +++ b/src/Automata.Ui/Overlay/Layers/D2dHudInfoLayer.cs @@ -3,7 +3,7 @@ using Vortice.Direct2D1; using Vortice.DirectWrite; using Vortice.Mathematics; -namespace Poe2Trade.Ui.Overlay.Layers; +namespace Automata.Ui.Overlay.Layers; internal sealed class D2dHudInfoLayer : ID2dOverlayLayer, IDisposable { diff --git a/src/Automata.Ui/Overlay/Layers/D2dLootLabelLayer.cs b/src/Automata.Ui/Overlay/Layers/D2dLootLabelLayer.cs index f6119fa..3ebfdcd 100644 --- a/src/Automata.Ui/Overlay/Layers/D2dLootLabelLayer.cs +++ b/src/Automata.Ui/Overlay/Layers/D2dLootLabelLayer.cs @@ -3,7 +3,7 @@ using Vortice.Direct2D1; using Vortice.DirectWrite; using Vortice.Mathematics; -namespace Poe2Trade.Ui.Overlay.Layers; +namespace Automata.Ui.Overlay.Layers; internal sealed class D2dLootLabelLayer : ID2dOverlayLayer, IDisposable { diff --git a/src/Automata.Ui/Program.cs b/src/Automata.Ui/Program.cs index 77c09f6..171915b 100644 --- a/src/Automata.Ui/Program.cs +++ b/src/Automata.Ui/Program.cs @@ -1,8 +1,8 @@ using System.Runtime.InteropServices; using Avalonia; -using Poe2Trade.Core; +using Automata.Core; -namespace Poe2Trade.Ui; +namespace Automata.Ui; class Program { diff --git a/src/Automata.Ui/ViewModels/AtlasViewModel.cs b/src/Automata.Ui/ViewModels/AtlasViewModel.cs index abd00e4..f2acc8b 100644 --- a/src/Automata.Ui/ViewModels/AtlasViewModel.cs +++ b/src/Automata.Ui/ViewModels/AtlasViewModel.cs @@ -3,11 +3,11 @@ using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Poe2Trade.Bot; -using Poe2Trade.Navigation; +using Automata.Bot; +using Automata.Navigation; using Serilog; -namespace Poe2Trade.Ui.ViewModels; +namespace Automata.Ui.ViewModels; public partial class AtlasViewModel : ObservableObject, IDisposable { diff --git a/src/Automata.Ui/ViewModels/CraftingViewModel.cs b/src/Automata.Ui/ViewModels/CraftingViewModel.cs new file mode 100644 index 0000000..e2dd557 --- /dev/null +++ b/src/Automata.Ui/ViewModels/CraftingViewModel.cs @@ -0,0 +1,247 @@ +using System.Collections.ObjectModel; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Automata.Bot; +using Automata.Core; +using Serilog; + +namespace Automata.Ui.ViewModels; + +public partial class CraftStepViewModel : ObservableObject +{ + private readonly CraftStep _model; + private readonly Action _onChanged; + + [ObservableProperty] private string _label; + [ObservableProperty] private string _selectedCurrency; + [ObservableProperty] private string _requiredModsText; + [ObservableProperty] private bool _matchAll; + [ObservableProperty] private decimal? _maxAttempts; + [ObservableProperty] private decimal? _onFailGoTo; + + public CraftStepViewModel(CraftStep model, Action onChanged) + { + _model = model; + _onChanged = onChanged; + _label = model.Label; + _selectedCurrency = model.CurrencyName; + _requiredModsText = string.Join(", ", model.RequiredMods); + _matchAll = model.MatchAll; + _maxAttempts = model.MaxAttempts; + _onFailGoTo = model.OnFailGoTo; + } + + partial void OnLabelChanged(string value) { _model.Label = value; _onChanged(); } + partial void OnSelectedCurrencyChanged(string value) { _model.CurrencyName = value ?? ""; _onChanged(); } + partial void OnMatchAllChanged(bool value) { _model.MatchAll = value; _onChanged(); } + partial void OnMaxAttemptsChanged(decimal? value) { _model.MaxAttempts = (int)(value ?? 500); _onChanged(); } + partial void OnOnFailGoToChanged(decimal? value) { _model.OnFailGoTo = value.HasValue ? (int)value.Value : null; _onChanged(); } + + partial void OnRequiredModsTextChanged(string value) + { + _model.RequiredMods = string.IsNullOrWhiteSpace(value) + ? [] + : value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList(); + _onChanged(); + } +} + +public partial class CraftRecipeViewModel : ObservableObject +{ + private readonly CraftRecipe _model; + private readonly Action _onChanged; + + [ObservableProperty] private string _name; + + public string Id => _model.Id; + public CraftRecipe Model => _model; + + public CraftRecipeViewModel(CraftRecipe model, Action onChanged) + { + _model = model; + _onChanged = onChanged; + _name = model.Name; + } + + partial void OnNameChanged(string value) + { + _model.Name = value; + _onChanged(); + } +} + +public partial class CraftingViewModel : ObservableObject +{ + private readonly BotOrchestrator _bot; + private CraftingExecutor? _executor; + + public ObservableCollection Crafts { get; } = []; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasSelectedCraft))] + private CraftRecipeViewModel? _selectedCraft; + + public ObservableCollection Steps { get; } = []; + public ObservableCollection CurrencyNames { get; } = []; + + [ObservableProperty] private string _craftState = "Idle"; + [ObservableProperty] private int _currentStepIndex; + [ObservableProperty] private int _currentAttempt; + [ObservableProperty] private int _totalAttempts; + [ObservableProperty] private string? _lastItemText; + [ObservableProperty] private string _itemPositionText = ""; + + public bool HasSelectedCraft => SelectedCraft != null; + + public CraftingViewModel(BotOrchestrator bot) + { + _bot = bot; + RefreshCurrencyNames(); + + // Load from config + foreach (var recipe in bot.Config.Crafts) + Crafts.Add(new CraftRecipeViewModel(recipe, SaveConfig)); + + if (Crafts.Count > 0) + SelectedCraft = Crafts[0]; + } + + public void RefreshCurrencyNames() + { + CurrencyNames.Clear(); + foreach (var cp in _bot.Config.CurrencyPositions) + { + if (cp.Name == "Craft Item") continue; + CurrencyNames.Add(cp.Name); + } + UpdateItemPositionText(); + } + + private void UpdateItemPositionText() + { + var craft = _bot.Config.CurrencyPositions + .FirstOrDefault(c => c.Name == "Craft Item"); + ItemPositionText = craft != null && (craft.X != 0 || craft.Y != 0) + ? $"({craft.X}, {craft.Y})" + : "(not set)"; + } + + partial void OnSelectedCraftChanged(CraftRecipeViewModel? value) + { + Steps.Clear(); + if (value == null) return; + + foreach (var step in value.Model.Steps) + Steps.Add(new CraftStepViewModel(step, SaveConfig)); + } + + [RelayCommand] + private void AddCraft() + { + var recipe = new CraftRecipe(); + _bot.Config.Crafts.Add(recipe); + var vm = new CraftRecipeViewModel(recipe, SaveConfig); + Crafts.Add(vm); + SelectedCraft = vm; + SaveConfig(); + } + + [RelayCommand] + private void RemoveCraft() + { + if (SelectedCraft == null) return; + _bot.Config.Crafts.Remove(SelectedCraft.Model); + Crafts.Remove(SelectedCraft); + SelectedCraft = Crafts.Count > 0 ? Crafts[0] : null; + SaveConfig(); + } + + [RelayCommand] + private void AddStep() + { + if (SelectedCraft == null) return; + var step = new CraftStep { Label = $"Step {SelectedCraft.Model.Steps.Count + 1}" }; + SelectedCraft.Model.Steps.Add(step); + Steps.Add(new CraftStepViewModel(step, SaveConfig)); + SaveConfig(); + } + + [RelayCommand] + private void RemoveStep(CraftStepViewModel? stepVm) + { + if (SelectedCraft == null || stepVm == null) return; + var idx = Steps.IndexOf(stepVm); + if (idx < 0) return; + SelectedCraft.Model.Steps.RemoveAt(idx); + Steps.RemoveAt(idx); + SaveConfig(); + } + + [RelayCommand] + private void MoveStepUp(CraftStepViewModel? stepVm) + { + if (SelectedCraft == null || stepVm == null) return; + var idx = Steps.IndexOf(stepVm); + if (idx <= 0) return; + SelectedCraft.Model.Steps.RemoveAt(idx); + SelectedCraft.Model.Steps.Insert(idx - 1, SelectedCraft.Model.Steps[idx - 1]); + // Rebuild properly: swap in model list + (SelectedCraft.Model.Steps[idx - 1], SelectedCraft.Model.Steps[idx]) = + (SelectedCraft.Model.Steps[idx], SelectedCraft.Model.Steps[idx - 1]); + Steps.Move(idx, idx - 1); + SaveConfig(); + } + + [RelayCommand] + private void MoveStepDown(CraftStepViewModel? stepVm) + { + if (SelectedCraft == null || stepVm == null) return; + var idx = Steps.IndexOf(stepVm); + if (idx < 0 || idx >= Steps.Count - 1) return; + (SelectedCraft.Model.Steps[idx], SelectedCraft.Model.Steps[idx + 1]) = + (SelectedCraft.Model.Steps[idx + 1], SelectedCraft.Model.Steps[idx]); + Steps.Move(idx, idx + 1); + SaveConfig(); + } + + [RelayCommand] + private async Task StartCraft() + { + if (SelectedCraft == null) return; + if (_executor != null && _executor.State == CraftingState.Running) return; + + _executor = new CraftingExecutor(_bot.Game, _bot.Config); + _executor.StateChanged += OnExecutorStateChanged; + + Log.Information("Starting craft: {Name}", SelectedCraft.Name); + await _executor.RunCraft(SelectedCraft.Model); + } + + [RelayCommand] + private void StopCraft() + { + _executor?.Stop(); + } + + private void OnExecutorStateChanged(CraftingState state) + { + Dispatcher.UIThread.Post(() => + { + CraftState = state.ToString(); + if (_executor != null) + { + CurrentStepIndex = _executor.CurrentStepIndex; + CurrentAttempt = _executor.CurrentAttempt; + TotalAttempts = _executor.TotalAttempts; + LastItemText = _executor.LastItemText; + } + _bot.UpdateExecutorState(); + }); + } + + private void SaveConfig() + { + _bot.Store.Save(); + } +} diff --git a/src/Automata.Ui/ViewModels/DebugViewModel.cs b/src/Automata.Ui/ViewModels/DebugViewModel.cs index f269e52..43dfce5 100644 --- a/src/Automata.Ui/ViewModels/DebugViewModel.cs +++ b/src/Automata.Ui/ViewModels/DebugViewModel.cs @@ -1,12 +1,12 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Poe2Trade.Bot; -using Poe2Trade.Core; -using Poe2Trade.Game; -using Poe2Trade.Screen; +using Automata.Bot; +using Automata.Core; +using Automata.Game; +using Automata.Screen; using Serilog; -namespace Poe2Trade.Ui.ViewModels; +namespace Automata.Ui.ViewModels; public partial class DebugViewModel : ObservableObject { diff --git a/src/Automata.Ui/ViewModels/MainWindowViewModel.cs b/src/Automata.Ui/ViewModels/MainWindowViewModel.cs index ece47bd..8de3079 100644 --- a/src/Automata.Ui/ViewModels/MainWindowViewModel.cs +++ b/src/Automata.Ui/ViewModels/MainWindowViewModel.cs @@ -4,12 +4,12 @@ using System.Runtime.InteropServices; using Avalonia.Media.Imaging; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Poe2Trade.Bot; -using Poe2Trade.Core; -using Poe2Trade.Navigation; +using Automata.Bot; +using Automata.Core; +using Automata.Navigation; using Serilog; -namespace Poe2Trade.Ui.ViewModels; +namespace Automata.Ui.ViewModels; public class LogEntry { @@ -122,7 +122,7 @@ public partial class MainWindowViewModel : ObservableObject [ObservableProperty] private BotMode _botMode; public static LinkMode[] LinkModes { get; } = [LinkMode.Live, LinkMode.Scrap, LinkMode.Diamond]; - public static BotMode[] BotModes { get; } = [BotMode.Trading, BotMode.Mapping]; + public static BotMode[] BotModes { get; } = [BotMode.Trading, BotMode.Mapping, BotMode.Crafting]; public MainWindowViewModel(BotOrchestrator bot) { @@ -181,6 +181,7 @@ public partial class MainWindowViewModel : ObservableObject public SettingsViewModel? SettingsVm { get; set; } public MappingViewModel? MappingVm { get; set; } public AtlasViewModel? AtlasVm { get; set; } + public CraftingViewModel? CraftingVm { get; set; } partial void OnBotModeChanged(BotMode value) { diff --git a/src/Automata.Ui/ViewModels/MappingViewModel.cs b/src/Automata.Ui/ViewModels/MappingViewModel.cs index 292e840..0514963 100644 --- a/src/Automata.Ui/ViewModels/MappingViewModel.cs +++ b/src/Automata.Ui/ViewModels/MappingViewModel.cs @@ -2,11 +2,11 @@ using System.Collections.ObjectModel; using Timer = System.Timers.Timer; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; -using Poe2Trade.Bot; -using Poe2Trade.Core; -using Poe2Trade.Screen; +using Automata.Bot; +using Automata.Core; +using Automata.Screen; -namespace Poe2Trade.Ui.ViewModels; +namespace Automata.Ui.ViewModels; public partial class MappingViewModel : ObservableObject, IDisposable { diff --git a/src/Automata.Ui/ViewModels/SettingsViewModel.cs b/src/Automata.Ui/ViewModels/SettingsViewModel.cs index 283f580..e7d15f5 100644 --- a/src/Automata.Ui/ViewModels/SettingsViewModel.cs +++ b/src/Automata.Ui/ViewModels/SettingsViewModel.cs @@ -1,18 +1,21 @@ using System.Collections.ObjectModel; +using System.IO; +using Avalonia.Media.Imaging; +using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Poe2Trade.Bot; -using Poe2Trade.Core; -using Poe2Trade.Inventory; +using Automata.Bot; +using Automata.Core; +using Automata.Inventory; using Serilog; -namespace Poe2Trade.Ui.ViewModels; +namespace Automata.Ui.ViewModels; public partial class SettingsViewModel : ObservableObject { private readonly BotOrchestrator _bot; - [ObservableProperty] private string _poe2LogPath = ""; + [ObservableProperty] private string _gameLogPath = ""; [ObservableProperty] private string _windowTitle = ""; [ObservableProperty] private decimal? _travelTimeoutMs = 15000; [ObservableProperty] private decimal? _stashScanTimeoutMs = 10000; @@ -31,18 +34,41 @@ public partial class SettingsViewModel : ObservableObject public ObservableCollection ShopTabs { get; } = []; public ObservableCollection DiamondPrices { get; } = []; + // Currency positioning + public ObservableCollection Currencies { get; } = []; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasSelectedCurrency))] + private CurrencyPositionViewModel? _selectedCurrency; + + [ObservableProperty] private Bitmap? _currencyTabImage; + [ObservableProperty] private int _currencyImageWidth; + [ObservableProperty] private int _currencyImageHeight; + + public bool HasSelectedCurrency => SelectedCurrency != null; + + // League / poe2scout + public ObservableCollection Leagues { get; } = []; + [ObservableProperty] private string _selectedLeague = ""; + [ObservableProperty] private bool _isLoadingCurrencies; + [ObservableProperty] private string _currencyStatus = ""; + private double _divinePrice; // divine price in exalted from league data + public SettingsViewModel(BotOrchestrator bot) { _bot = bot; LoadFromConfig(); LoadTabs(); + LoadCurrencies(); + LoadCurrencyImage(); + _ = LoadLeaguesAsync(); } private void LoadFromConfig() { var s = _bot.Store.Settings; - Poe2LogPath = s.Poe2LogPath; - WindowTitle = s.Poe2WindowTitle; + GameLogPath = s.GameLogPath; + WindowTitle = s.GameWindowTitle; TravelTimeoutMs = s.TravelTimeoutMs; StashScanTimeoutMs = s.StashScanTimeoutMs; WaitForMoreItemsMs = s.WaitForMoreItemsMs; @@ -96,8 +122,8 @@ public partial class SettingsViewModel : ObservableObject { _bot.Store.UpdateSettings(s => { - s.Poe2LogPath = Poe2LogPath; - s.Poe2WindowTitle = WindowTitle; + s.GameLogPath = GameLogPath; + s.GameWindowTitle = WindowTitle; s.TravelTimeoutMs = (int)(TravelTimeoutMs ?? 15000); s.StashScanTimeoutMs = (int)(StashScanTimeoutMs ?? 10000); s.WaitForMoreItemsMs = (int)(WaitForMoreItemsMs ?? 20000); @@ -225,7 +251,250 @@ public partial class SettingsViewModel : ObservableObject IsSaved = false; } - partial void OnPoe2LogPathChanged(string value) => IsSaved = false; + private const string CraftItemName = "Craft Item"; + + private void LoadCurrencies() + { + Currencies.Clear(); + var iconDir = Path.GetFullPath("assets/currency/currency"); + var saved = _bot.Store.Settings.CurrencyPositions; + + // Ensure "Craft Item" is always first + var craftPos = saved.FirstOrDefault(c => c.Name == CraftItemName); + if (craftPos == null) + craftPos = new CurrencyPosition { Name = CraftItemName }; + Currencies.Add(new CurrencyPositionViewModel(craftPos, SaveCurrencies) { IsPinned = true }); + + foreach (var pos in saved.Where(c => c.Name != CraftItemName)) + { + var vm = new CurrencyPositionViewModel(pos, SaveCurrencies); + // Load cached icon from disk if available + if (!string.IsNullOrEmpty(pos.ApiId)) + { + var cached = Path.Combine(iconDir, pos.ApiId + ".png"); + if (File.Exists(cached)) + { + try { vm.IconBitmap = new Bitmap(cached); } + catch { /* ignore corrupt files */ } + } + } + Currencies.Add(vm); + } + } + + private List _leagueData = []; + + private async Task LoadLeaguesAsync() + { + try + { + CurrencyStatus = "Loading leagues..."; + var leagues = await Poe2ScoutClient.GetLeaguesAsync(); + if (leagues.Count == 0) + { + CurrencyStatus = "Failed to load leagues"; + return; + } + + _leagueData = leagues; + + await Dispatcher.UIThread.InvokeAsync(() => + { + Leagues.Clear(); + foreach (var l in leagues) + Leagues.Add(l.Value); + }); + + var saved = _bot.Store.Settings.SelectedLeague; + if (!string.IsNullOrEmpty(saved) && Leagues.Contains(saved)) + SelectedLeague = saved; + else + SelectedLeague = Leagues[0]; + + CurrencyStatus = $"{leagues.Count} leagues loaded"; + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to load leagues"); + CurrencyStatus = "Failed to load leagues"; + } + } + + partial void OnSelectedLeagueChanged(string value) + { + if (string.IsNullOrEmpty(value)) return; + _bot.Store.UpdateSettings(s => s.SelectedLeague = value); + _divinePrice = _leagueData.FirstOrDefault(l => l.Value == value)?.DivinePrice ?? 0; + _ = LoadCurrencyPricesAsync(value); + } + + private async Task LoadCurrencyPricesAsync(string league) + { + if (IsLoadingCurrencies) return; + IsLoadingCurrencies = true; + CurrencyStatus = "Loading currency prices..."; + + try + { + var apiCurrencies = await Poe2ScoutClient.GetAllCurrencyAsync(league); + if (apiCurrencies.Count == 0) + { + CurrencyStatus = "No currencies returned"; + return; + } + + await Dispatcher.UIThread.InvokeAsync(() => MergeCurrencies(apiCurrencies)); + CurrencyStatus = $"{apiCurrencies.Count} currencies loaded"; + + // Download icons in background + _ = DownloadIconsAsync(); + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to load currency prices"); + CurrencyStatus = "Failed to load prices"; + } + finally + { + IsLoadingCurrencies = false; + } + } + + private void MergeCurrencies(List apiCurrencies) + { + // Build lookups from saved positions — ApiId first, name as fallback + var byApiId = new Dictionary(StringComparer.OrdinalIgnoreCase); + var byName = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var c in Currencies) + { + if (!string.IsNullOrEmpty(c.ApiId)) + byApiId.TryAdd(c.ApiId, c); + byName.TryAdd(c.Name, c); + } + + var merged = new List(); + var consumed = new HashSet(); + + foreach (var api in apiCurrencies) + { + var divPrice = _divinePrice > 0 ? api.CurrentPrice / _divinePrice : 0; + + // Match by ApiId first, then by name — preserves X/Y positions + CurrencyPositionViewModel? existing = null; + if (byApiId.TryGetValue(api.ApiId, out var byId)) existing = byId; + else if (byName.TryGetValue(api.Text, out var byN)) existing = byN; + + if (existing != null) + { + existing.ApiId = api.ApiId; + existing.Price = divPrice; + existing.IconUrl = api.IconUrl; + merged.Add(existing); + consumed.Add(existing); + } + else + { + var model = new CurrencyPosition + { + Name = api.Text, ApiId = api.ApiId, Price = divPrice + }; + merged.Add(new CurrencyPositionViewModel(model, SaveCurrencies) + { + IconUrl = api.IconUrl + }); + } + } + + // Append custom entries not matched by API (pinned items handled separately) + foreach (var vm in Currencies) + { + if (!consumed.Contains(vm) && !vm.IsPinned) + merged.Add(vm); + } + + // Rebuild: pinned items first, then merged + var pinned = Currencies.Where(c => c.IsPinned && !consumed.Contains(c)).ToList(); + Currencies.Clear(); + foreach (var vm in pinned) + Currencies.Add(vm); + foreach (var vm in merged) + Currencies.Add(vm); + + SaveCurrencies(); + } + + private async Task DownloadIconsAsync() + { + var iconDir = Path.GetFullPath("assets/currency/currency"); + var items = Currencies.Where(c => !string.IsNullOrEmpty(c.IconUrl) && !string.IsNullOrEmpty(c.ApiId)).ToList(); + + foreach (var item in items) + { + var ext = Path.GetExtension(new Uri(item.IconUrl!).AbsolutePath); + if (string.IsNullOrEmpty(ext)) ext = ".png"; + var savePath = Path.Combine(iconDir, item.ApiId + ext); + + var path = await Poe2ScoutClient.DownloadIconAsync(item.IconUrl!, savePath); + if (path != null) + { + try + { + var bitmap = new Bitmap(path); + await Dispatcher.UIThread.InvokeAsync(() => item.IconBitmap = bitmap); + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to load icon bitmap from {Path}", path); + } + } + } + } + + private void LoadCurrencyImage() + { + var path = Path.GetFullPath("assets/currency-tab.png.png"); + if (!File.Exists(path)) return; + CurrencyTabImage = new Bitmap(path); + CurrencyImageWidth = CurrencyTabImage.PixelSize.Width; + CurrencyImageHeight = CurrencyTabImage.PixelSize.Height; + } + + [RelayCommand] + private void AddCurrency() + { + var pos = new CurrencyPosition { Name = "New Currency" }; + Currencies.Add(new CurrencyPositionViewModel(pos, SaveCurrencies)); + SelectedCurrency = Currencies[^1]; + SaveCurrencies(); + } + + [RelayCommand] + private void RemoveCurrency(CurrencyPositionViewModel? item) + { + if (item == null || item.IsPinned) return; + Currencies.Remove(item); + if (SelectedCurrency == item) + SelectedCurrency = Currencies.Count > 0 ? Currencies[0] : null; + SaveCurrencies(); + } + + public void OnCurrencyImageClick(int imagePixelX, int imagePixelY) + { + if (SelectedCurrency == null) return; + SelectedCurrency.X = imagePixelX; + SelectedCurrency.Y = imagePixelY; + SaveCurrencies(); + } + + private void SaveCurrencies() + { + _bot.Store.UpdateSettings(s => + { + s.CurrencyPositions = Currencies.Select(c => c.ToModel()).ToList(); + }); + } + + partial void OnGameLogPathChanged(string value) => IsSaved = false; partial void OnWindowTitleChanged(string value) => IsSaved = false; partial void OnTravelTimeoutMsChanged(decimal? value) => IsSaved = false; partial void OnStashScanTimeoutMsChanged(decimal? value) => IsSaved = false; @@ -262,3 +531,59 @@ public partial class DiamondPriceViewModel : ObservableObject Enabled = Enabled, }; } + +public partial class CurrencyPositionViewModel : ObservableObject +{ + private readonly CurrencyPosition _model; + private readonly Action _onChanged; + + [ObservableProperty] private string _name; + [ObservableProperty] private int _x; + [ObservableProperty] private int _y; + [ObservableProperty] private string _apiId = ""; + [ObservableProperty] private double _price; + [ObservableProperty] private Bitmap? _iconBitmap; + + public string? IconUrl { get; set; } + public bool IsPinned { get; init; } + + public bool IsPositioned => X != 0 || Y != 0; + public string PositionText => IsPositioned ? $"({X}, {Y})" : "(not set)"; + public string PriceText => Price > 0 ? $"{Price:F2} div" : ""; + + public CurrencyPositionViewModel(CurrencyPosition model, Action onChanged) + { + _model = model; + _onChanged = onChanged; + _name = model.Name; + _x = model.X; + _y = model.Y; + _apiId = model.ApiId; + _price = model.Price; + } + + partial void OnNameChanged(string value) { _model.Name = value; _onChanged(); } + + partial void OnXChanged(int value) + { + _model.X = value; + OnPropertyChanged(nameof(IsPositioned)); + OnPropertyChanged(nameof(PositionText)); + _onChanged(); + } + + partial void OnYChanged(int value) + { + _model.Y = value; + OnPropertyChanged(nameof(IsPositioned)); + OnPropertyChanged(nameof(PositionText)); + _onChanged(); + } + + partial void OnPriceChanged(double value) => OnPropertyChanged(nameof(PriceText)); + + public CurrencyPosition ToModel() => new() + { + Name = Name, X = X, Y = Y, ApiId = ApiId, Price = Price + }; +} diff --git a/src/Automata.Ui/ViewModels/StashTabViewModel.cs b/src/Automata.Ui/ViewModels/StashTabViewModel.cs index 883f889..0685056 100644 --- a/src/Automata.Ui/ViewModels/StashTabViewModel.cs +++ b/src/Automata.Ui/ViewModels/StashTabViewModel.cs @@ -1,8 +1,8 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; -using Poe2Trade.Core; +using Automata.Core; -namespace Poe2Trade.Ui.ViewModels; +namespace Automata.Ui.ViewModels; public partial class StashTabViewModel : ObservableObject { diff --git a/src/Automata.Ui/Views/MainWindow.axaml b/src/Automata.Ui/Views/MainWindow.axaml index a8bbbad..a03eef3 100644 --- a/src/Automata.Ui/Views/MainWindow.axaml +++ b/src/Automata.Ui/Views/MainWindow.axaml @@ -1,7 +1,7 @@ + + + + + + + + + +