work on well of souls and yolo detection

This commit is contained in:
Boki 2026-02-20 16:40:50 -05:00
parent 3456e0d62a
commit 40d30115bf
41 changed files with 3031 additions and 148 deletions

View file

@ -155,6 +155,7 @@ public class InventoryManager : IInventoryManager
private async Task CtrlClickItems(List<PlacedItem> items, GridLayout layout, int clickDelayMs = Delays.ClickInterval)
{
await _game.KeyDown(Game.InputSender.VK.SHIFT);
await _game.HoldCtrl();
foreach (var item in items)
{
@ -163,6 +164,7 @@ public class InventoryManager : IInventoryManager
await Helpers.Sleep(clickDelayMs);
}
await _game.ReleaseCtrl();
await _game.KeyUp(Game.InputSender.VK.SHIFT);
await Helpers.Sleep(Delays.PostEscape);
}
@ -208,13 +210,31 @@ public class InventoryManager : IInventoryManager
for (var attempt = 1; attempt <= maxRetries; attempt++)
{
Log.Information("Searching for nameplate '{Name}' (attempt {Attempt}/{Max})", name, attempt, maxRetries);
var pos = await _screen.FindTextOnScreen(name, fuzzy: true);
// Move mouse to bottom-left so it doesn't cover nameplates
_game.MoveMouseInstant(0, 1440);
await Helpers.Sleep(100);
// Nameplates hidden by default — capture clean reference
using var reference = _screen.CaptureRawBitmap();
// Hold Alt to show nameplates, capture, then release
await _game.KeyDown(Game.InputSender.VK.MENU);
await Helpers.Sleep(50);
using var current = _screen.CaptureRawBitmap();
await _game.KeyUp(Game.InputSender.VK.MENU);
// Diff OCR — only processes the bright nameplate regions
var result = await _screen.NameplateDiffOcr(reference, current);
var pos = FindWordInOcrResult(result, name, fuzzy: true);
if (pos.HasValue)
{
Log.Information("Clicking nameplate '{Name}' at ({X},{Y})", name, pos.Value.X, pos.Value.Y);
await _game.LeftClickAt(pos.Value.X, pos.Value.Y);
return pos;
}
Log.Debug("Nameplate '{Name}' not found in diff OCR (attempt {Attempt}), text: {Text}", name, attempt, result.Text);
if (attempt < maxRetries)
await Helpers.Sleep(retryDelayMs);
}
@ -223,6 +243,73 @@ public class InventoryManager : IInventoryManager
return null;
}
private static (int X, int Y)? FindWordInOcrResult(OcrResponse result, string needle, bool fuzzy)
{
var lower = needle.ToLowerInvariant();
// Multi-word: match against full line text
if (lower.Contains(' '))
{
foreach (var line in result.Lines)
{
if (line.Words.Count == 0) continue;
if (line.Text.Contains(needle, StringComparison.OrdinalIgnoreCase))
{
var first = line.Words[0];
var last = line.Words[^1];
return ((first.X + last.X + last.Width) / 2, (first.Y + last.Y + last.Height) / 2);
}
if (fuzzy)
{
var sim = BigramSimilarity(Normalize(needle), Normalize(line.Text));
if (sim >= 0.55)
{
var first = line.Words[0];
var last = line.Words[^1];
return ((first.X + last.X + last.Width) / 2, (first.Y + last.Y + last.Height) / 2);
}
}
}
return null;
}
// Single word
foreach (var line in result.Lines)
foreach (var word in line.Words)
{
if (word.Text.Contains(needle, StringComparison.OrdinalIgnoreCase))
return (word.X + word.Width / 2, word.Y + word.Height / 2);
if (fuzzy && BigramSimilarity(Normalize(needle), Normalize(word.Text)) >= 0.55)
return (word.X + word.Width / 2, word.Y + word.Height / 2);
}
return null;
}
private static string Normalize(string s) =>
new(s.ToLowerInvariant().Where(char.IsLetterOrDigit).ToArray());
private static double BigramSimilarity(string a, string b)
{
if (a.Length < 2 || b.Length < 2) return a == b ? 1 : 0;
var bigramsA = new Dictionary<(char, char), int>();
for (var i = 0; i < a.Length - 1; i++)
{
var bg = (a[i], a[i + 1]);
bigramsA[bg] = bigramsA.GetValueOrDefault(bg) + 1;
}
var matches = 0;
for (var i = 0; i < b.Length - 1; i++)
{
var bg = (b[i], b[i + 1]);
if (bigramsA.TryGetValue(bg, out var count) && count > 0)
{
matches++;
bigramsA[bg] = count - 1;
}
}
return 2.0 * matches / (a.Length - 1 + b.Length - 1);
}
public async Task<bool> WaitForAreaTransition(int timeoutMs, Func<Task>? triggerAction = null)
{
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
@ -254,4 +341,64 @@ public class InventoryManager : IInventoryManager
{
return (Tracker.GetGrid(), Tracker.GetItems(), Tracker.FreeCells);
}
public async Task ClickStashTab(StashTabInfo tab, StashTabInfo? parentFolder = null)
{
if (parentFolder != null)
{
Log.Information("Clicking folder '{Folder}' at ({X},{Y})", parentFolder.Name, parentFolder.ClickX, parentFolder.ClickY);
await _game.LeftClickAt(parentFolder.ClickX, parentFolder.ClickY);
await Helpers.Sleep(200);
}
Log.Information("Clicking tab '{Tab}' at ({X},{Y})", tab.Name, tab.ClickX, tab.ClickY);
await _game.LeftClickAt(tab.ClickX, tab.ClickY);
await Helpers.Sleep(Delays.PostStashOpen);
}
public async Task GrabItemsFromStash(string layoutName, int maxItems, string? templatePath = null)
{
Log.Information("Grabbing up to {Max} items from stash layout '{Layout}' (template={Template})",
maxItems, layoutName, templatePath ?? "none");
var layout = GridLayouts.All[layoutName];
if (templatePath != null)
{
// Template matching mode: repeatedly find and click matching items
var grabbed = 0;
await _game.HoldCtrl();
while (grabbed < maxItems)
{
var match = await _screen.TemplateMatch(templatePath, layout.Region);
if (match == null) break;
Log.Information("Template match at ({X},{Y}), grabbing", match.X, match.Y);
await _game.LeftClickAt(match.X, match.Y);
await Helpers.Sleep(Delays.ClickInterval);
grabbed++;
}
await _game.ReleaseCtrl();
await Helpers.Sleep(Delays.PostEscape);
Log.Information("Grabbed {Count} matching items from stash", grabbed);
}
else
{
// Grid scan mode: grab all occupied cells
var result = await _screen.Grid.Scan(layoutName);
var grabbed = 0;
await _game.HoldCtrl();
foreach (var cell in result.Occupied)
{
if (grabbed >= maxItems) break;
var center = _screen.Grid.GetCellCenter(layout, cell.Row, cell.Col);
await _game.LeftClickAt(center.X, center.Y);
await Helpers.Sleep(Delays.ClickInterval);
grabbed++;
}
await _game.ReleaseCtrl();
await Helpers.Sleep(Delays.PostEscape);
Log.Information("Grabbed {Count} items from stash", grabbed);
}
}
}