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)); } } }