This commit is contained in:
Boki 2026-02-26 20:45:24 -05:00
parent 53641fc8e7
commit 657d307485
28 changed files with 2045 additions and 161 deletions

View file

@ -47,9 +47,11 @@ public class BotOrchestrator : IAsyncDisposable
public FrameSaver FrameSaver { get; }
public LootDebugDetector LootDebugDetector { get; }
public KulemakExecutor KulemakExecutor { get; }
public AtlasExecutor AtlasExecutor { get; }
public volatile bool ShowYoloOverlay = true;
public volatile bool ShowFightPositionOverlay = true;
private readonly Dictionary<string, ScrapExecutor> _scrapExecutors = new();
private readonly Dictionary<string, DiamondExecutor> _diamondExecutors = new();
// Events
public event Action? StatusUpdated;
@ -94,6 +96,7 @@ public class BotOrchestrator : IAsyncDisposable
enemyDetector: EnemyDetector);
KulemakExecutor = new KulemakExecutor(game, screen, inventory, logWatcher, store.Settings, BossDetector, HudReader, Navigation);
AtlasExecutor = new AtlasExecutor(game, screen, inventory, store.Settings, pipelineService.Pipeline);
logWatcher.AreaEntered += area =>
{
@ -144,6 +147,31 @@ public class BotOrchestrator : IAsyncDisposable
}
}
public async Task EmergencyStop()
{
Log.Warning("EMERGENCY STOP triggered");
_paused = true;
Store.SetPaused(true);
// Stop all trade executors
foreach (var exec in _scrapExecutors.Values)
await exec.Stop();
_scrapExecutors.Clear();
foreach (var exec in _diamondExecutors.Values)
await exec.Stop();
_diamondExecutors.Clear();
TradeQueue.Clear();
// Stop navigation and mapping
await Navigation.Stop();
KulemakExecutor.Stop();
State = "Stopped (END)";
StatusUpdated?.Invoke();
}
public void Pause()
{
_paused = true;
@ -185,6 +213,29 @@ public class BotOrchestrator : IAsyncDisposable
_ = DeactivateLink(id);
}
public void ChangeLinkMode(string id, LinkMode newMode)
{
var link = Links.UpdateMode(id, newMode);
if (link == null) return;
StatusUpdated?.Invoke();
if (!_started || !link.Active) return;
_ = Task.Run(async () =>
{
try
{
await DeactivateLink(id);
await ActivateLink(link);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to change link mode: {Id} → {Mode}", id, newMode);
Emit("error", $"Failed to switch mode: {link.Name}");
}
});
}
public BotStatus GetStatus() => new()
{
Paused = _paused,
@ -211,6 +262,14 @@ public class BotOrchestrator : IAsyncDisposable
return;
}
}
foreach (var diamondExec in _diamondExecutors.Values)
{
if (diamondExec.State != DiamondState.Idle && diamondExec.State != DiamondState.WaitingForListings)
{
State = diamondExec.State.ToString();
return;
}
}
if (KulemakExecutor.State != MappingState.Idle)
{
State = KulemakExecutor.State.ToString();
@ -273,6 +332,10 @@ public class BotOrchestrator : IAsyncDisposable
await Inventory.ClearToStash();
Emit("info", "Inventory cleared");
// Wire trade monitor events before activating links to avoid race
TradeMonitor.NewListings += OnNewListings;
TradeMonitor.DiamondListings += OnDiamondListings;
// Load links
var allUrls = new HashSet<string>(cliUrls);
foreach (var l in Store.Settings.Links)
@ -287,9 +350,6 @@ public class BotOrchestrator : IAsyncDisposable
Emit("info", $"Loaded (inactive): {link.Name}");
}
// Wire trade monitor events
TradeMonitor.NewListings += OnNewListings;
Emit("info", $"Loaded {allUrls.Count} trade link(s)");
Log.Information("Bot started");
}
@ -360,6 +420,8 @@ public class BotOrchestrator : IAsyncDisposable
Log.Information("Shutting down bot...");
foreach (var exec in _scrapExecutors.Values)
await exec.Stop();
foreach (var exec in _diamondExecutors.Values)
await exec.Stop();
EnemyDetector.Dispose();
Screen.Dispose();
await TradeMonitor.DisposeAsync();
@ -367,25 +429,46 @@ public class BotOrchestrator : IAsyncDisposable
PipelineService.Dispose();
}
private void OnNewListings(string searchId, List<string> itemIds)
private void OnNewListings(string searchId, List<TradeItem> items)
{
if (_paused)
{
Emit("warn", $"New listings ({itemIds.Count}) skipped - bot paused");
Emit("warn", $"New listings ({items.Count}) skipped - bot paused");
return;
}
if (!Links.IsActive(searchId))
{
Emit("warn", $"New listings ({items.Count}) skipped - link {searchId} inactive");
return;
}
Log.Information("New listings: {SearchId} ({Count} items)", searchId, items.Count);
Emit("info", $"New listings: {items.Count} items from {searchId}");
TradeQueue.Enqueue(new TradeInfo(
SearchId: searchId,
Items: items,
Timestamp: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
));
}
private void OnDiamondListings(string searchId, List<PricedTradeItem> items)
{
if (_paused)
{
Emit("warn", $"Diamond listings ({items.Count}) skipped - bot paused");
return;
}
if (!Links.IsActive(searchId)) return;
Log.Information("New listings: {SearchId} ({Count} items)", searchId, itemIds.Count);
Emit("info", $"New listings: {itemIds.Count} items from {searchId}");
foreach (var item in items)
{
var display = DiamondSettings.KnownDiamonds.GetValueOrDefault(item.Name, item.Name);
Emit("info", $"Diamond: {display} @ {item.PriceAmount} {item.PriceCurrency}");
}
TradeQueue.Enqueue(new TradeInfo(
SearchId: searchId,
ItemIds: itemIds,
WhisperText: "",
Timestamp: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
TradeUrl: ""
));
if (_diamondExecutors.TryGetValue(searchId, out var exec))
exec.EnqueueItems(items);
}
private async Task ActivateLink(TradeLink link)
@ -412,6 +495,29 @@ public class BotOrchestrator : IAsyncDisposable
}
});
}
else if (link.Mode == LinkMode.Diamond)
{
var searchId = TradeMonitor.ExtractSearchId(link.Url);
var diamondExec = new DiamondExecutor(searchId, Game, Screen, TradeMonitor, Inventory, Config);
diamondExec.StateChanged += _ => UpdateExecutorState();
diamondExec.ItemBought += () => { Interlocked.Increment(ref _tradesCompleted); StatusUpdated?.Invoke(); };
diamondExec.ItemFailed += () => { Interlocked.Increment(ref _tradesFailed); StatusUpdated?.Invoke(); };
_diamondExecutors[searchId] = diamondExec;
await TradeMonitor.AddDiamondSearch(link.Url);
Emit("info", $"Diamond search started: {link.Name}");
StatusUpdated?.Invoke();
_ = diamondExec.RunLoop().ContinueWith(t =>
{
if (t.IsFaulted)
{
Log.Error(t.Exception!, "Diamond loop error: {LinkId}", link.Id);
Emit("error", $"Diamond loop failed: {link.Name}");
_diamondExecutors.Remove(searchId);
}
});
}
else
{
await TradeMonitor.AddSearch(link.Url);
@ -433,6 +539,14 @@ public class BotOrchestrator : IAsyncDisposable
await scrapExec.Stop();
_scrapExecutors.Remove(id);
}
// Diamond executors are keyed by searchId, not link id — but they're the same
if (_diamondExecutors.TryGetValue(id, out var diamondExec))
{
await diamondExec.Stop();
_diamondExecutors.Remove(id);
}
await TradeMonitor.PauseSearch(id);
}