diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..ddd2fefb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# WFInfo — Agent Guide + +WPF (.NET Framework 4.8) desktop app for Warframe. OCR + market prices. + +## Build & Run +- `dotnet build -c Release` (output: `bin/Release/net48/WFInfo.exe`) +- Launch with no args for normal UI mode +- Startup object is `WFInfo.CustomEntrypoint.Main()` (not `App.Main()`) + +## Test Framework (headless OCR regression) +- Triggered automatically when any `.json` arg is passed: `WFInfo.exe map.json [output.json]` +- `cd tests && run_tests.bat` — locates built `WFInfo.exe` automatically +- Test data: `tests/data/.json` + `.png` pairs, listed in `tests/map.json` +- Exit codes: 0=all pass, 1=partial fail, 2=fatal error +- Real OCR pipeline (no mocks) — first run downloads market data from warframestat.us API + +## Architecture +- **Entry** → `CustomEntrypoint.Main()` → Tesseract DLL bootstrap → `App.Main()` (WPF) +- **Dependency Injection** via `Microsoft.Extensions.DependencyInjection` in `Main.cs` +- **OCR** → `Ocr.cs`: screenshot → `ExtractPartBoxAutomatically` → Tesseract → Levenshtein `GetPartName()` +- **Data** → `Data.cs`: JSON from `api.warframestat.us/wfinfo/prices`, JWT auth, WebSocket for warframe.market +- **Auto-mode** → `LogCapture.cs`: reads Warframe `EE.log` via memory-mapped file, triggers on `"Got rewards"` +- **Screenshots** → dual backend: GDI (fallback) + Windows.Graphics.Capture (Win10+ 2004+) +- **Languages** → `LanguageProcessing/`: 11 processors (CJK, Cyrillic, Latin, Thai, Turkish, Polish) + +## Key Tech Stack +- .NET Framework 4.8, WPF, WinForms interop +- Tesseract 5.2.0 (native DLLs via Costura.Fody bundling) +- Newtonsoft.Json, SharpDX.Direct3D11, AutoUpdater.NET + +## Quirks & Gotchas +- `AllowUnsafeBlocks=true` — Tesseract interop +- Costura merges `Tesseract50.dll`, `leptonica-1.82.0.dll` into the exe +- Tesseract DLLs downloaded from GitHub `WFCD/WFinfo/libs` branch on first run to `%APPDATA%\WFInfo\tesseract5\` +- Release build auto-generates `update.xml` + `WFInfo.zip` via MSBuild targets +- Debug logs at `%APPDATA%\WFInfo\debug.log` (async queue, flushed every 250ms) +- DPI awareness: PerMonitorV2 (app.manifest) +- No CI workflows present +- Dependabot: NuGet weekly diff --git a/WFInfo/Data.cs b/WFInfo/Data.cs index 9af1b9ed..b62a22b6 100644 --- a/WFInfo/Data.cs +++ b/WFInfo/Data.cs @@ -1073,11 +1073,15 @@ public string GetPartName(string name, out int low, bool suppressLogging, out bo // Use English name (split[0]) for length comparison regardless of locale cache int englishNameLength = split[0].Length; - int lengthDiff = Math.Abs((useLocalizedNames && split.Length >= 3 ? split[2].Length : split[0].Length) - name.Length); - if (lengthDiff > Math.Max(englishNameLength, name.Length) / 2) continue; // Use localized name only if cache locale matches and available, otherwise fall back to English string comparisonName = useLocalizedNames && split.Length >= 3 ? split[2] : split[0]; + + // Normalize stored name for length comparison (Korean and others strip whitespace) + string normalizedStoredName = processor.NormalizeForPatternMatching(comparisonName); + int lengthDiff = Math.Abs(normalizedStoredName.Length - normalizedName.Length); + if (lengthDiff > Math.Max(englishNameLength, normalizedName.Length) / 2) continue; + marketItemsSnapshot.Add(Tuple.Create(split[0], comparisonName)); } } @@ -1087,16 +1091,45 @@ public string GetPartName(string name, out int low, bool suppressLogging, out bo } } + // Add ignored items to the snapshot so they compete equally with market items + var ignoredItems = processor.IgnoredItemNames; + if (ignoredItems != null) + { + foreach (var kvp in ignoredItems) + { + // Normalize ignored item names to match normalized OCR input format + // e.g., "Чертёж: Форма" -> "форма (чертеж)" for proper matching + string normalizedIgnoredName = processor.NormalizeForPatternMatching(kvp.Value); + marketItemsSnapshot.Add(Tuple.Create(kvp.Key, normalizedIgnoredName)); + } + } + // Do heavy Levenshtein work outside lock foreach (var item in marketItemsSnapshot) { string englishName = item.Item1; string storedName = item.Item2; - int val = processor.CalculateLevenshteinDistance(name, storedName); + // Normalize stored name to match OCR input format for fair comparison + string normalizedStoredName = processor.NormalizeForPatternMatching(storedName); + + // For Korean: use full CalculateLevenshteinDistance with Jamo-aware similarity + // to handle severely mangled OCR (e.g., "뉴모 오티스 섬" vs "뉴로옵틱스") + int val; + if (processor.Locale == "ko") + { + // CalculateLevenshteinDistance normalizes internally, pass raw strings + val = processor.CalculateLevenshteinDistance(name, storedName); + } + else + { + // Use SimpleLevenshteinDistance to avoid aggressive preprocessing + // that removes blueprint terms (prevents "чертёж" removal causing false matches) + val = processor.SimpleLevenshteinDistance(normalizedName, normalizedStoredName); + } - // Distance filter: Only accept matches with distance < 50% of string length (like GetLocalizedNameData) - if (val >= storedName.Length * 0.5) continue; + // Distance filter: Use language-specific threshold (default 50%, Korean 60% for garbled OCR) + if (val >= normalizedStoredName.Length * processor.DistanceThresholdRatio) continue; if (val < low) { @@ -1156,6 +1189,117 @@ public string GetPartName(string name, out int low, bool suppressLogging, out bo return lowest; } + /// + /// Gets the localized name for an English part name from market items. + /// Returns the English name if no localized version is available. + /// + public string GetLocalizedNameForClipboard(string englishName) + { + if (_settings.Locale == "en" || string.IsNullOrEmpty(englishName)) + return englishName; + + lock (marketItemsLock) + { + if (marketItems == null) + return englishName; + + // Check if cached locale matches current locale + string cachedLocale = marketItems.TryGetValue("locale", out var localeToken) ? localeToken?.ToString() : null; + if (cachedLocale != _settings.Locale) + return englishName; // Fall back to English if locale doesn't match + + foreach (var marketItem in marketItems) + { + if (marketItem.Key == "locale" || marketItem.Key == "version") + continue; + + string[] split = marketItem.Value.ToString().Split('|'); + if (split.Length < 3) + continue; + + // Compare against English name (split[0]) + if (split[0].Equals(englishName, StringComparison.OrdinalIgnoreCase)) + { + // Return localized name (split[2]) if available and not empty + string localizedName = split[2]; + if (!string.IsNullOrEmpty(localizedName)) + return localizedName; + break; + } + } + } + + return englishName; // Fallback to English if not found + } + + /// + /// Removes blueprint terms from a localized part name for clipboard display. + /// Uses the current language processor's BlueprintRemovals list. + /// + public string RemoveBlueprintTerms(string localizedName) + { + if (string.IsNullOrEmpty(localizedName)) + return localizedName; + + var processor = LanguageProcessorFactory.GetCurrentProcessor(); + string result = localizedName; + + // Get blueprint removal terms for current language + var blueprintTerms = processor.BlueprintRemovals; + if (blueprintTerms != null) + { + foreach (var term in blueprintTerms) + { + if (string.IsNullOrEmpty(term)) + continue; + + string escapedTerm = Regex.Escape(term); + + // Remove " Term" or " Term " patterns (case insensitive) + result = Regex.Replace(result, $"\\s+{escapedTerm}\\s*$", "", RegexOptions.IgnoreCase); + result = Regex.Replace(result, $"\\s+{escapedTerm}\\s+", " ", RegexOptions.IgnoreCase); + + // Remove term preceded by common punctuation: " - Term", " – Term", " — Term", ": Term" + result = Regex.Replace(result, $"[:\\-–—]\\s*{escapedTerm}\\s*$", "", RegexOptions.IgnoreCase); + result = Regex.Replace(result, $"[:\\-–—]\\s*{escapedTerm}\\s+", " ", RegexOptions.IgnoreCase); + + // Remove term followed by common punctuation: "Term - ", "Term:" + result = Regex.Replace(result, $"\\s*{escapedTerm}\\s*[:\\-–—]", "", RegexOptions.IgnoreCase); + + // Remove term at boundaries (standalone) + result = Regex.Replace(result, $"\\b{escapedTerm}\\b", "", RegexOptions.IgnoreCase); + } + } + + // Always strip English "Blueprint" regardless of locale + result = Regex.Replace(result, "\\s*Blueprint\\s*$", "", RegexOptions.IgnoreCase); + result = Regex.Replace(result, "\\s*Blueprint\\s+", " ", RegexOptions.IgnoreCase); + result = Regex.Replace(result, "^Blueprint\\s*[:\\-–—]?\\s*", "", RegexOptions.IgnoreCase); + + // Special handling for Russian "Чертёж:" prefix format + if (_settings.Locale == "ru") + { + result = Regex.Replace(result, "^Черт[её]ж:\\s*", "", RegexOptions.IgnoreCase); + result = Regex.Replace(result, "^черт[её]ж:\\s*", "", RegexOptions.IgnoreCase); + } + + return result.Trim(); + } + + /// + /// Checks if a part name is an ignored item (0 plat, 0 ducats). + /// Uses the current language processor's cached HashSet for O(1) lookup. + /// Works with both English and localized names. + /// + public bool IsIgnoredItem(string partName) + { + if (string.IsNullOrEmpty(partName)) + return false; + + var processor = LanguageProcessorFactory.GetCurrentProcessor(); + return processor.IsIgnoredItem(partName); + } + public string GetPartNameHuman(string name, out int low) { // Checks the Levenshtein Distance of a string and returns the index in Names() of the closest part optimized for human searching string lowest = null; diff --git a/WFInfo/FodyWeavers.xsd b/WFInfo/FodyWeavers.xsd index f2dbece7..dbeb1020 100644 --- a/WFInfo/FodyWeavers.xsd +++ b/WFInfo/FodyWeavers.xsd @@ -27,6 +27,16 @@ A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + A list of runtimes to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of runtimes names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + Obsolete, use UnmanagedWinX86Assemblies instead diff --git a/WFInfo/LanguageProcessing/ChineseLanguageProcessor.cs b/WFInfo/LanguageProcessing/ChineseLanguageProcessor.cs index 51eb169c..d4683dde 100644 --- a/WFInfo/LanguageProcessing/ChineseLanguageProcessor.cs +++ b/WFInfo/LanguageProcessing/ChineseLanguageProcessor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using WFInfo.Settings; @@ -118,6 +119,23 @@ public SimplifiedChineseLanguageProcessor(IReadOnlyApplicationSettings settings) public override string[] BlueprintRemovals => new[] { "蓝图", "设计图" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "Forma 蓝图", + ["Exilus Weapon Adapter Blueprint"] = "Exilus 武器适配器 蓝图", + ["Kuva"] = "赤毒", + ["Riven Sliver"] = "裂罅碎块", + ["Ayatan Amber Star"] = "阿耶檀识 琥珀星", + ["Galariak Prime Blueprint"] = "加拉瑞克 Prime 蓝图", + ["Galariak Prime Blade"] = "加拉瑞克 Prime 刀刃 蓝图", + ["Galariak Prime Handle"] = "加拉瑞克 Prime 握柄 蓝图", + ["Sagek Prime Blueprint"] = "萨杰克 Prime 蓝图", + ["Sagek Prime Barrel"] = "萨杰克 Prime 枪管 蓝图", + ["Sagek Prime Receiver"] = "萨杰克 Prime 枪机 蓝图" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override int CalculateLevenshteinDistance(string s, string t) { return LevenshteinDistanceWithPreprocessing(s, t, BlueprintRemovals, NormalizeChineseCharacters, callBaseDefault: true); @@ -138,6 +156,23 @@ public TraditionalChineseLanguageProcessor(IReadOnlyApplicationSettings settings public override string[] BlueprintRemovals => new[] { "藍圖", "設計圖" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "Forma 藍圖", + ["Exilus Weapon Adapter Blueprint"] = "Exilus 武器適配器 藍圖", + ["Kuva"] = "赤毒", + ["Riven Sliver"] = "裂罅碎塊", + ["Ayatan Amber Star"] = "阿耶檀識 琥珀星", + ["Galariak Prime Blueprint"] = "加拉瑞克 Prime 藍圖", + ["Galariak Prime Blade"] = "加拉瑞克 Prime 刀刃 藍圖", + ["Galariak Prime Handle"] = "加拉瑞克 Prime 握柄 藍圖", + ["Sagek Prime Blueprint"] = "薩傑克 Prime 藍圖", + ["Sagek Prime Barrel"] = "薩傑克 Prime 槍管 藍圖", + ["Sagek Prime Receiver"] = "薩傑克 Prime 槍機 藍圖" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override int CalculateLevenshteinDistance(string s, string t) { return LevenshteinDistanceWithPreprocessing(s, t, BlueprintRemovals, NormalizeChineseCharacters, callBaseDefault: true); diff --git a/WFInfo/LanguageProcessing/CyrillicLanguageProcessor.cs b/WFInfo/LanguageProcessing/CyrillicLanguageProcessor.cs index 72725be4..3b707869 100644 --- a/WFInfo/LanguageProcessing/CyrillicLanguageProcessor.cs +++ b/WFInfo/LanguageProcessing/CyrillicLanguageProcessor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using WFInfo.Settings; @@ -16,14 +17,33 @@ public RussianLanguageProcessor(IReadOnlyApplicationSettings settings) : base(se public override string Locale => "ru"; - public override string[] BlueprintRemovals => new string[0]; // No blueprint removals - handled in NormalizeForPatternMatching + public override string[] BlueprintRemovals => new[] { "чертёж", "чертеж", "(чертёж)", "(чертеж)" }; + + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "Чертёж: Форма", + ["Exilus Weapon Adapter Blueprint"] = "Чертёж: Эксилус адаптер оружия", + ["Kuva"] = "Кува", + ["Riven Sliver"] = "Осколок Ривена", + ["Ayatan Amber Star"] = "Янтарная звезда Аятана", + ["Galariak Prime Blueprint"] = "Чертёж: Галариак Прайм", + ["Galariak Prime Blade"] = "Чертёж: Галариак Прайм клинок", + ["Galariak Prime Handle"] = "Чертёж: Галариак Прайм рукоять", + ["Sagek Prime Blueprint"] = "Чертёж: Сагек Прайм", + ["Sagek Prime Barrel"] = "Чертёж: Сагек Прайм ствол", + ["Sagek Prime Receiver"] = "Чертёж: Сагек Прайм приёмник" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; public override string CharacterWhitelist => GenerateCharacterRange(0x0400, 0x04FF) + GenerateCharacterRange(0x0500, 0x052F) + ": "; // Cyrillic + Cyrillic Supplement public override int CalculateLevenshteinDistance(string s, string t) { - // For Russian, don't normalize Cyrillic to Latin - we want to match Russian to Russian - return LevenshteinDistanceWithPreprocessing(s, t, BlueprintRemovals, null); + // For Russian, normalize both strings before comparison to ensure consistent matching + string normalizedS = NormalizeForPatternMatching(s); + string normalizedT = NormalizeForPatternMatching(t); + return SimpleLevenshteinDistance(normalizedS, normalizedT); } public override string NormalizeForPatternMatching(string input) @@ -73,12 +93,31 @@ public UkrainianLanguageProcessor(IReadOnlyApplicationSettings settings) : base( public override string[] BlueprintRemovals => new[] { "Кресленник" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "Кресленник: Форма", + ["Exilus Weapon Adapter Blueprint"] = "Кресленник: Екзилус адаптер зброї", + ["Kuva"] = "Кува", + ["Riven Sliver"] = "Уламок Рівена", + ["Ayatan Amber Star"] = "Янтарна зірка Аятана", + ["Galariak Prime Blueprint"] = "Кресленник: Ґаларіак-Прайм", + ["Galariak Prime Blade"] = "Кресленник: Ґаларіак-Прайм лезо", + ["Galariak Prime Handle"] = "Кресленник: Ґаларіак-Прайм рукоять", + ["Sagek Prime Blueprint"] = "Кресленник: Сагек Прайм", + ["Sagek Prime Barrel"] = "Кресленник: Сагек Прайм ствол", + ["Sagek Prime Receiver"] = "Кресленник: Сагек Прайм приймач" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override string CharacterWhitelist => GenerateCharacterRange(0x0400, 0x04FF) + GenerateCharacterRange(0x0500, 0x052F) + ": -()"; // Cyrillic + Cyrillic Supplement public override int CalculateLevenshteinDistance(string s, string t) { - // For Ukrainian, don't normalize Cyrillic to Latin - we want to match Ukrainian to Ukrainian - return LevenshteinDistanceWithPreprocessing(s, t, BlueprintRemovals, null); + // For Ukrainian, normalize both strings before comparison to ensure consistent matching + string normalizedS = NormalizeForPatternMatching(s); + string normalizedT = NormalizeForPatternMatching(t); + return SimpleLevenshteinDistance(normalizedS, normalizedT); } public override string NormalizeForPatternMatching(string input) diff --git a/WFInfo/LanguageProcessing/EnglishLanguageProcessor.cs b/WFInfo/LanguageProcessing/EnglishLanguageProcessor.cs index abd3f07a..462ad84c 100644 --- a/WFInfo/LanguageProcessing/EnglishLanguageProcessor.cs +++ b/WFInfo/LanguageProcessing/EnglishLanguageProcessor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using WFInfo.Settings; @@ -18,6 +19,24 @@ public EnglishLanguageProcessor(IReadOnlyApplicationSettings settings) : base(se public override string[] BlueprintRemovals => new[] { "Blueprint" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "Forma Blueprint", + ["Exilus Weapon Adapter Blueprint"] = "Exilus Weapon Adapter Blueprint", + ["Kuva"] = "Kuva", + ["Riven Sliver"] = "Riven Sliver", + ["Ayatan Amber Star"] = "Ayatan Amber Star", + ["Ayatan Cyan Star"] = "Ayatan Cyan Star", + ["Galariak Prime Blueprint"] = "Galariak Prime Blueprint", + ["Galariak Prime Blade"] = "Galariak Prime Blade", + ["Galariak Prime Handle"] = "Galariak Prime Handle", + ["Sagek Prime Blueprint"] = "Sagek Prime Blueprint", + ["Sagek Prime Barrel"] = "Sagek Prime Barrel", + ["Sagek Prime Receiver"] = "Sagek Prime Receiver" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override string CharacterWhitelist => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; public override int CalculateLevenshteinDistance(string s, string t) diff --git a/WFInfo/LanguageProcessing/EuropeanLanguageProcessor.cs b/WFInfo/LanguageProcessing/EuropeanLanguageProcessor.cs index 7c36d3bb..9a440755 100644 --- a/WFInfo/LanguageProcessing/EuropeanLanguageProcessor.cs +++ b/WFInfo/LanguageProcessing/EuropeanLanguageProcessor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using WFInfo.Settings; namespace WFInfo.LanguageProcessing @@ -82,6 +83,23 @@ public GermanLanguageProcessor(IReadOnlyApplicationSettings settings) : base(set public override string[] BlueprintRemovals => new[] { "Blaupause", "Plan" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "Forma Blaupause", + ["Exilus Weapon Adapter Blueprint"] = "Exilus-Waffenadapter Blaupause", + ["Kuva"] = "Kuva", + ["Riven Sliver"] = "Riven-Splitter", + ["Ayatan Amber Star"] = "Ayatan Amber Stern", + ["Galariak Prime Blueprint"] = "Galariak Prime Blaupause", + ["Galariak Prime Blade"] = "Galariak Prime: Klinge Blaupause", + ["Galariak Prime Handle"] = "Galariak Prime: Griff Blaupause", + ["Sagek Prime Blueprint"] = "Sagek Prime Blaupause", + ["Sagek Prime Barrel"] = "Sagek Prime: Lauf Blaupause", + ["Sagek Prime Receiver"] = "Sagek Prime: Empfänger Blaupause" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override string CharacterWhitelist => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz " + GenerateCharacterRange(0x00C4, 0x00C4) + GenerateCharacterRange(0x00D6, 0x00D6) + GenerateCharacterRange(0x00DC, 0x00DC) + GenerateCharacterRange(0x00DF, 0x00DF) + GenerateCharacterRange(0x00E4, 0x00E4) + GenerateCharacterRange(0x00F6, 0x00F6) + GenerateCharacterRange(0x00FC, 0x00FC); // German with umlauts } @@ -99,6 +117,23 @@ public SpanishLanguageProcessor(IReadOnlyApplicationSettings settings) : base(se public override string[] BlueprintRemovals => new[] { "Plano", "Diseño" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "Plano Forma", + ["Exilus Weapon Adapter Blueprint"] = "Plano Adaptador Exilus de Arma", + ["Kuva"] = "Kuva", + ["Riven Sliver"] = "Fragmento Riven", + ["Ayatan Amber Star"] = "Estrella Ámbar Ayatan", + ["Galariak Prime Blueprint"] = "Plano Galariak Prime", + ["Galariak Prime Blade"] = "Plano Hoja Galariak Prime", + ["Galariak Prime Handle"] = "Plano Mango Galariak Prime", + ["Sagek Prime Blueprint"] = "Plano Sagek Prime", + ["Sagek Prime Barrel"] = "Plano Cañón Sagek Prime", + ["Sagek Prime Receiver"] = "Plano Receptor Sagek Prime" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override string CharacterWhitelist => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz " + GenerateCharacterRange(0x00C1, 0x00C1) + // Á GenerateCharacterRange(0x00C9, 0x00C9) + // É @@ -130,6 +165,23 @@ public PortugueseLanguageProcessor(IReadOnlyApplicationSettings settings) : base public override string[] BlueprintRemovals => new[] { "Planta", "Projeto" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "Forma (Planta)", + ["Exilus Weapon Adapter Blueprint"] = "Adaptador Exilus de Arma (Planta)", + ["Kuva"] = "Kuva", + ["Riven Sliver"] = "Fragmento Riven", + ["Ayatan Amber Star"] = "Estrela Âmbar Ayatan", + ["Galariak Prime Blueprint"] = "Galariak Prime (Planta)", + ["Galariak Prime Blade"] = "Galariak Prime: Lâmina (Planta)", + ["Galariak Prime Handle"] = "Galariak Prime: Cabo (Planta)", + ["Sagek Prime Blueprint"] = "Sagek Prime (Planta)", + ["Sagek Prime Barrel"] = "Sagek Prime: Cano (Planta)", + ["Sagek Prime Receiver"] = "Sagek Prime: Receptor (Planta)" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override string CharacterWhitelist => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz " + GenerateCharacterRange(0x00C0, 0x00C0) + // À GenerateCharacterRange(0x00C1, 0x00C1) + // Á @@ -173,6 +225,23 @@ public FrenchLanguageProcessor(IReadOnlyApplicationSettings settings) : base(set public override string[] BlueprintRemovals => new[] { "Schéma", "Plan" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "Forma (Schéma)", + ["Exilus Weapon Adapter Blueprint"] = "Adaptateur Exilus d'Arme (Schéma)", + ["Kuva"] = "Kuva", + ["Riven Sliver"] = "Éclat Riven", + ["Ayatan Amber Star"] = "Étoile Ambre Ayatan", + ["Galariak Prime Blueprint"] = "Galariak Prime (Schéma)", + ["Galariak Prime Blade"] = "Galariak Prime - Lame (Schéma)", + ["Galariak Prime Handle"] = "Galariak Prime - Manche (Schéma)", + ["Sagek Prime Blueprint"] = "Sagek Prime (Schéma)", + ["Sagek Prime Barrel"] = "Sagek Prime - Canon (Schéma)", + ["Sagek Prime Receiver"] = "Sagek Prime - Récepteur (Schéma)" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override string CharacterWhitelist => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz " + GenerateCharacterRange(0x00C0, 0x00C0) + // À GenerateCharacterRange(0x00C2, 0x00C2) + //  @@ -220,6 +289,23 @@ public ItalianLanguageProcessor(IReadOnlyApplicationSettings settings) : base(se public override string[] BlueprintRemovals => new[] { "Progetto", "Piano" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "Forma (Progetto)", + ["Exilus Weapon Adapter Blueprint"] = "Adattatore Exilus per Arma (Progetto)", + ["Kuva"] = "Kuva", + ["Riven Sliver"] = "Scheggia Riven", + ["Ayatan Amber Star"] = "Stella Ambra Ayatan", + ["Galariak Prime Blueprint"] = "Galariak Prime (Progetto)", + ["Galariak Prime Blade"] = "Galariak Prime - Lama (Progetto)", + ["Galariak Prime Handle"] = "Galariak Prime - Impugnatura (Progetto)", + ["Sagek Prime Blueprint"] = "Sagek Prime (Progetto)", + ["Sagek Prime Barrel"] = "Sagek Prime - Canna (Progetto)", + ["Sagek Prime Receiver"] = "Sagek Prime - Ricevitore (Progetto)" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override string CharacterWhitelist => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-() " + GenerateCharacterRange(0x00C0, 0x00C0) + // À GenerateCharacterRange(0x00C8, 0x00C8) + // È diff --git a/WFInfo/LanguageProcessing/JapaneseLanguageProcessor.cs b/WFInfo/LanguageProcessing/JapaneseLanguageProcessor.cs index 091c3f0b..b52ace59 100644 --- a/WFInfo/LanguageProcessing/JapaneseLanguageProcessor.cs +++ b/WFInfo/LanguageProcessing/JapaneseLanguageProcessor.cs @@ -50,6 +50,23 @@ public JapaneseLanguageProcessor(IReadOnlyApplicationSettings settings) : base(s public override string[] BlueprintRemovals => new[] { "設計図", "青図" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "フォルマ 設計図", + ["Exilus Weapon Adapter Blueprint"] = "エクシラス ウェポン アダプター 設計図", + ["Kuva"] = "クバ", + ["Riven Sliver"] = "リヴン スリバー", + ["Ayatan Amber Star"] = "アヤタン アンバー スター", + ["Galariak Prime Blueprint"] = "ガラリアク プライム 設計図", + ["Galariak Prime Blade"] = "ガラリアク プライム ブレード 設計図", + ["Galariak Prime Handle"] = "ガラリアク プライム ハンドル 設計図", + ["Sagek Prime Blueprint"] = "サゲク プライム 設計図", + ["Sagek Prime Barrel"] = "サゲク プライム バレル 設計図", + ["Sagek Prime Receiver"] = "サゲク プライム レシーバー 設計図" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override string CharacterWhitelist => GenerateCharacterRange(0x3040, 0x309F) + GenerateCharacterRange(0x30A0, 0x30FF) + diff --git a/WFInfo/LanguageProcessing/KoreanLanguageProcessor.cs b/WFInfo/LanguageProcessing/KoreanLanguageProcessor.cs index 66d12a8d..f590a760 100644 --- a/WFInfo/LanguageProcessing/KoreanLanguageProcessor.cs +++ b/WFInfo/LanguageProcessing/KoreanLanguageProcessor.cs @@ -181,8 +181,31 @@ public KoreanLanguageProcessor(IReadOnlyApplicationSettings settings) : base(set public override string Locale => "ko"; + /// + /// Korean uses 75% threshold due to severe OCR garbling of complex Hangul characters + /// and trailing garbage from adjacent UI elements. + /// + public override double DistanceThresholdRatio => 0.75; + public override string[] BlueprintRemovals => new[] { "설계도" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "포르마 설계도", + ["Exilus Weapon Adapter Blueprint"] = "엑실루스 무기 어댑터 설계도", + ["Kuva"] = "쿠바", + ["Riven Sliver"] = "리븐 파편", + ["Ayatan Amber Star"] = "아야탄 앰버 스타", + ["Galariak Prime Blueprint"] = "갈라리아크 프라임 설계도", + ["Galariak Prime Blade"] = "갈라리아크 프라임 블레이드 설계도", + ["Galariak Prime Handle"] = "갈라리아크 프라임 핸들 설계도", + ["Sagek Prime Blueprint"] = "사게크 프라임 설계도", + ["Sagek Prime Barrel"] = "사게크 프라임 배럴 설계도", + ["Sagek Prime Receiver"] = "사게크 프라임 리시버 설계도" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override string CharacterWhitelist => string.Concat(GenerateCharacterRangeIterator(0xAC00, 0xC6FF)) + GenerateCharacterRange(0xC700, 0xD5FF) + diff --git a/WFInfo/LanguageProcessing/LanguageProcessor.cs b/WFInfo/LanguageProcessing/LanguageProcessor.cs index 0e897252..5bd129ca 100644 --- a/WFInfo/LanguageProcessing/LanguageProcessor.cs +++ b/WFInfo/LanguageProcessing/LanguageProcessor.cs @@ -18,7 +18,9 @@ public abstract class LanguageProcessor { // Per-type normalized blueprint removals to avoid recomputing on every call private static readonly ConcurrentDictionary _normalizedBlueprintRemovalsCache = new ConcurrentDictionary(); - + // Per-type ignored item names HashSet for O(1) lookup performance + private static readonly ConcurrentDictionary> _ignoredItemNamesCache = new ConcurrentDictionary>(); + protected readonly IReadOnlyApplicationSettings _settings; protected readonly CultureInfo _culture; @@ -77,11 +79,80 @@ private static CultureInfo GetCultureInfo(string locale) /// public abstract string[] BlueprintRemovals { get; } + /// + /// Gets the ignored item name translations for this language. + /// Maps English ignored item names to their localized equivalents. + /// + public abstract IReadOnlyDictionary IgnoredItemNames { get; } + + /// + /// Gets a HashSet of all ignored item names (both English and localized) for O(1) lookup. + /// This is cached per processor type to avoid rebuilding on every call. + /// + public HashSet GetIgnoredItemNamesHashSet() + { + return _ignoredItemNamesCache.GetOrAdd(GetType(), type => + { + var hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); + var ignoredItems = IgnoredItemNames; + + if (ignoredItems != null) + { + foreach (var kvp in ignoredItems) + { + // Add both English key and localized value + if (!string.IsNullOrEmpty(kvp.Key)) + hashSet.Add(kvp.Key); + if (!string.IsNullOrEmpty(kvp.Value)) + hashSet.Add(kvp.Value); + } + } + + return hashSet; + }); + } + + /// + /// Checks if a part name is an ignored item using efficient HashSet lookup. + /// Also performs substring matching to handle OCR with leading/trailing garbage. + /// + /// Part name to check (can be English or localized) + /// True if the item should be ignored (0 plat/ducats) + public virtual bool IsIgnoredItem(string partName) + { + if (string.IsNullOrEmpty(partName)) + return false; + + var ignoredSet = GetIgnoredItemNamesHashSet(); + if (ignoredSet.Contains(partName)) + return true; + + // Substring matching: check if any ignored item name is contained within the OCR text + // This handles cases like "제 잃빼미 포르마 설계도" containing "포르마 설계도" + foreach (var ignoredName in ignoredSet) + { + if (!string.IsNullOrEmpty(ignoredName) && ignoredName.Length >= 4) + { + if (partName.Contains(ignoredName)) + return true; + } + } + + return false; + } + /// /// Gets the Tesseract character whitelist for this language /// public abstract string CharacterWhitelist { get; } + /// + /// Gets the maximum Levenshtein distance ratio for matching (0.0-1.0). + /// Matches with distance >= threshold * stringLength are rejected. + /// Default is 0.5 (50%), languages with severe OCR issues may override to 0.6 (60%). + /// + public virtual double DistanceThresholdRatio => 0.5; + /// /// Calculates Levenshtein distance between two strings using language-specific logic /// diff --git a/WFInfo/LanguageProcessing/PolishLanguageProcessor.cs b/WFInfo/LanguageProcessing/PolishLanguageProcessor.cs index 78241aa8..f8488dab 100644 --- a/WFInfo/LanguageProcessing/PolishLanguageProcessor.cs +++ b/WFInfo/LanguageProcessing/PolishLanguageProcessor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using WFInfo.Settings; @@ -18,6 +19,23 @@ public PolishLanguageProcessor(IReadOnlyApplicationSettings settings) : base(set public override string[] BlueprintRemovals => new[] { "Plan", "Schemat" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "Forma - Plan", + ["Exilus Weapon Adapter Blueprint"] = "Adapter Exilus dla Broni - Plan", + ["Kuva"] = "Kuva", + ["Riven Sliver"] = "Odłamek Rivena", + ["Ayatan Amber Star"] = "Bursztynowa Gwiazda Ayatan", + ["Galariak Prime Blueprint"] = "Galariak Prime - Plan", + ["Galariak Prime Blade"] = "Galariak Prime: Ostrze - Plan", + ["Galariak Prime Handle"] = "Galariak Prime: Rękojeść - Plan", + ["Sagek Prime Blueprint"] = "Sagek Prime - Plan", + ["Sagek Prime Barrel"] = "Sagek Prime: Lufa - Plan", + ["Sagek Prime Receiver"] = "Sagek Prime: Korpus - Plan" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override string CharacterWhitelist => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz " + GenerateCharacterRange(0x0104, 0x0107) + GenerateCharacterRange(0x0118, 0x0119) + GenerateCharacterRange(0x0141, 0x0144) + GenerateCharacterRange(0x015A, 0x015A) + "\u00d3\u00f3\u015a\u015b\u0179\u017a\u017b\u017c"; // Polish with ranges + missing letters public override int CalculateLevenshteinDistance(string s, string t) diff --git a/WFInfo/LanguageProcessing/ThaiLanguageProcessor.cs b/WFInfo/LanguageProcessing/ThaiLanguageProcessor.cs index 0d72aa23..187d557f 100644 --- a/WFInfo/LanguageProcessing/ThaiLanguageProcessor.cs +++ b/WFInfo/LanguageProcessing/ThaiLanguageProcessor.cs @@ -39,6 +39,23 @@ public ThaiLanguageProcessor(IReadOnlyApplicationSettings settings) : base(setti public override string[] BlueprintRemovals => new[] { "แบบแปลน", "ภาพวาด" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "ฟอร์มา แบบแปลน", + ["Exilus Weapon Adapter Blueprint"] = "เอกซิลัส อแดปเตอร์อาวุธ แบบแปลน", + ["Kuva"] = "คูวา", + ["Riven Sliver"] = "เศษริเวน", + ["Ayatan Amber Star"] = "ดาวอำพันอายาตัน", + ["Galariak Prime Blueprint"] = "กาลาริแอค ไพรม์ แบบแปลน", + ["Galariak Prime Blade"] = "กาลาริแอค ไพรม์ ใบมีด แบบแปลน", + ["Galariak Prime Handle"] = "กาลาริแอค ไพรม์ ด้ามจับ แบบแปลน", + ["Sagek Prime Blueprint"] = "ซาเจ็ค ไพรม์ แบบแปลน", + ["Sagek Prime Barrel"] = "ซาเจ็ค ไพรม์ ลำกล้อง แบบแปลน", + ["Sagek Prime Receiver"] = "ซาเจ็ค ไพรม์ ตัวรับ แบบแปลน" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override string CharacterWhitelist => GenerateCharacterRange(0x0E00, 0x0E7F) + " "; // Thai characters public override int CalculateLevenshteinDistance(string s, string t) diff --git a/WFInfo/LanguageProcessing/TurkishLanguageProcessor.cs b/WFInfo/LanguageProcessing/TurkishLanguageProcessor.cs index 3f578559..e5c530a6 100644 --- a/WFInfo/LanguageProcessing/TurkishLanguageProcessor.cs +++ b/WFInfo/LanguageProcessing/TurkishLanguageProcessor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using WFInfo.Settings; @@ -18,6 +19,23 @@ public TurkishLanguageProcessor(IReadOnlyApplicationSettings settings) : base(se public override string[] BlueprintRemovals => new[] { "Plan", "Şema" }; + private static readonly IReadOnlyDictionary _ignoredItemNames = new Dictionary + { + ["Forma Blueprint"] = "Forma Plan", + ["Exilus Weapon Adapter Blueprint"] = "Silah Exilus Adaptörü Plan", + ["Kuva"] = "Kuva", + ["Riven Sliver"] = "Riven Parçası", + ["Ayatan Amber Star"] = "Ayatan Amber Yıldızı", + ["Galariak Prime Blueprint"] = "Galariak Prime Plan", + ["Galariak Prime Blade"] = "Galariak Prime Bıçak", + ["Galariak Prime Handle"] = "Galariak Prime Kabza", + ["Sagek Prime Blueprint"] = "Sagek Prime Plan", + ["Sagek Prime Barrel"] = "Sagek Prime Namlu", + ["Sagek Prime Receiver"] = "Sagek Prime Alıcı" + }; + + public override IReadOnlyDictionary IgnoredItemNames => _ignoredItemNames; + public override string CharacterWhitelist => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz " + "ÇçĞğİıÖöŞşÜü"; // Turkish-specific characters public override int CalculateLevenshteinDistance(string s, string t) @@ -49,7 +67,33 @@ public override bool IsPartNameValid(string partName) return !string.IsNullOrEmpty(partName) && partName.Replace(" ", "").Length >= 6; } - + /// + /// Checks if a part name is an ignored item with Turkish diacritics normalization. + /// Normalizes input to handle OCR that loses Turkish diacritics. + /// + public override bool IsIgnoredItem(string partName) + { + if (string.IsNullOrEmpty(partName)) + return false; + + // Normalize input to handle OCR without diacritics + string normalizedInput = NormalizeTurkishCharacters(partName); + var ignoredSet = GetIgnoredItemNamesHashSet(); + + // Check raw input first + if (ignoredSet.Contains(partName)) + return true; + + // Check normalized input against normalized set values + foreach (var item in ignoredSet) + { + if (NormalizeTurkishCharacters(item).Equals(normalizedInput, StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + /// /// Normalizes Turkish characters to standard equivalents for comparison /// diff --git a/WFInfo/Ocr.cs b/WFInfo/Ocr.cs index 0190539a..5f1c9d94 100644 --- a/WFInfo/Ocr.cs +++ b/WFInfo/Ocr.cs @@ -43,6 +43,10 @@ public enum WFtheme : int EQUINOX, DARK_LOTUS, ZEPHYR, + CONQUERA, + DEADLOCK, + LUNAR_RENEWAL, + POM_2, UNKNOWN = -1, AUTO = -2, CUSTOM = -3 @@ -71,7 +75,11 @@ class OCR Color.FromArgb(255, 255, 255), //LEGACY Color.FromArgb(158, 159, 167), //EQUINOX Color.FromArgb(140, 119, 147), //DARK_LOTUS - Color.FromArgb(253, 132, 2), }; //ZEPHER + Color.FromArgb(253, 132, 2), //ZEPHER + Color.FromArgb(200, 100, 200), //CONQUERA - medium-light purple + Color.FromArgb(25, 35, 60), //DEADLOCK - dark navy + Color.FromArgb(160, 40, 40), //LUNAR_RENEWAL - deep red + Color.FromArgb(12, 45, 25), }; //POM_2 - actual dark forest green //highlight colors from selected items public static Color[] ThemeSecondary = new Color[] { Color.FromArgb(245, 227, 173), //VITRUVIAN @@ -88,7 +96,11 @@ class OCR Color.FromArgb(232, 213, 93), //LEGACY Color.FromArgb(232, 227, 227), //EQUINOX Color.FromArgb(200, 169, 237), //DARK_LOTUS - Color.FromArgb(255, 53, 0) }; //ZEPHER + Color.FromArgb(255, 53, 0), //ZEPHER + Color.FromArgb(255, 215, 0), //CONQUERA + Color.FromArgb(255, 255, 255), //DEADLOCK + Color.FromArgb(255, 200, 100), //LUNAR_RENEWAL + Color.FromArgb(100, 255, 100) }; //POM_2 private static int numberOfRewardsDisplayed; @@ -335,7 +347,14 @@ internal static void ProcessRewardScreen(Bitmap file = null) { hideRewardInfo = true; } - //else if (correctName != "Kuva" || correctName != "Exilus Weapon Adapter Blueprint" || correctName != "Riven Sliver" || correctName != "Ayatan Amber Star") + + // Check if this is an ignored item using both English name AND raw OCR text (localized) + // This catches items even if GetPartName() fails to convert properly + if (Main.dataBase.IsIgnoredItem(correctName) || Main.dataBase.IsIgnoredItem(part)) + { + hideRewardInfo = true; + } + primeRewards.Add(correctName); string plat = job["plat"].ToObject(); string primeSetPlat = null; @@ -379,7 +398,12 @@ internal static void ProcessRewardScreen(Bitmap file = null) { if (!string.IsNullOrEmpty(clipboard)) { clipboard += "- "; } - clipboard += "[" + correctName.Replace(" Blueprint", "") + "]: " + plat + ":platinum: "; + // Get localized name for clipboard (uses current WFInfo locale) + string localizedName = Main.dataBase.GetLocalizedNameForClipboard(correctName); + // Remove blueprint terms for the current language + localizedName = Main.dataBase.RemoveBlueprintTerms(localizedName); + + clipboard += "[" + localizedName + "]: " + plat + ":platinum: "; if (primeSetPlat != null) { @@ -480,8 +504,8 @@ internal static int GetSelectedReward(Point lastClick) Debug.WriteLine(lastClick.ToString()); var primeRewardIndex = 0; lastClick.Offset(-_window.Window.X, -_window.Window.Y); - var width = _window.Window.Width * (int)_window.DpiScaling; - var height = _window.Window.Height * (int)_window.DpiScaling; + var width = _window.Window.Width; + var height = _window.Window.Height; var mostWidth = (int)(pixleRewardWidth * _window.ScreenScaling * uiScaling); var mostLeft = (width / 2) - (mostWidth / 2); var bottom = height / 2 - (int)((pixleRewardYDisplay - pixleRewardHeight) * _window.ScreenScaling * 0.5 * uiScaling); @@ -640,7 +664,7 @@ public static WFtheme GetThemeWeighted(out double closestThresh, Bitmap image = - double[] weights = new double[15] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; + double[] weights = new double[Enum.GetValues(typeof(WFtheme)).Cast().Max() + 1]; int minWidth = mostWidth / 4; if (image == null || image.Height == 0) @@ -1194,7 +1218,7 @@ private static List FindAllParts(Bitmap filteredImage, Bitmap unf // causes padded bounds to overlap with adjacent items double hMargin = IsCJKLocale() ? Math.Min(_settings.SnapItHorizontalNameMargin, 0.3) // Cap at 0.3 for CJK - : _settings.SnapItHorizontalNameMargin; + : Math.Max(_settings.SnapItHorizontalNameMargin, 0.2); // Min 0.2 for Latin to bridge horizontal text splits int HorizontalPad = (int)(bounds.Height * hMargin); @@ -1239,7 +1263,7 @@ private static List FindAllParts(Bitmap filteredImage, Bitmap unf // Max combined width to prevent merging text from different items in the grid // Each item tile is roughly 130-140px wide at 1080p; cap at 160px to allow // multi-line wrapping within one item but prevent cross-item cascading merges - int maxGroupWidth = (int)(160 * _window.ScreenScaling); + int maxGroupWidth = (int)(180 * _window.ScreenScaling); for (; i >= 0; i--) { @@ -1277,7 +1301,64 @@ private static List FindAllParts(Bitmap filteredImage, Bitmap unf } } - + // Post-pass: merge single-fragment groups that are vertically close + // and horizontally overlapping. Tesseract SparseText can split multi-word + // item names across separate lines; if fragments were returned out of order + // relative to other items, the inline loop can't pair them. This pass + // handles that by checking ALL single-fragment groups together. + { + int maxGroupWidth = (int)(180 * _window.ScreenScaling); + bool merged = true; + while (merged) + { + merged = false; + for (int a = 0; a < foundItems.Count; a++) + { + if (foundItems[a].Item1.Count > 2) continue; + for (int b = a + 1; b < foundItems.Count; b++) + { + if (foundItems[b].Item1.Count > 2) continue; + // Limit total fragments to 3 to prevent cascade merges + if (foundItems[a].Item1.Count + foundItems[b].Item1.Count > 3) continue; + // Skip merging with very short text (likely OCR noise like "QG", "GT", "CO") + if (foundItems[a].Item1[0].Name.Length < 5 || foundItems[b].Item1[0].Name.Length < 5) continue; + + var boundsB = foundItems[b].Item2; + int vertGap = Math.Max(0, Math.Max(foundItems[a].Item2.Top - boundsB.Bottom, boundsB.Top - foundItems[a].Item2.Bottom)); + int avgHeight = (foundItems[a].Item2.Height + boundsB.Height) / 2; + if (vertGap <= avgHeight) + { + int overlapLeft = Math.Max(foundItems[a].Item2.Left, boundsB.Left); + int overlapRight = Math.Min(foundItems[a].Item2.Right, boundsB.Right); + if (overlapRight > overlapLeft) + { + int combinedLeft = Math.Min(foundItems[a].Item2.Left, boundsB.Left); + int combinedRight = Math.Max(foundItems[a].Item2.Right, boundsB.Right); + if (combinedRight - combinedLeft <= maxGroupWidth) + { + int left = Math.Min(foundItems[a].Item2.Left, boundsB.Left); + int top = Math.Min(foundItems[a].Item2.Top, boundsB.Top); + int right = Math.Max(foundItems[a].Item2.Right, boundsB.Right); + int bot = Math.Max(foundItems[a].Item2.Bottom, boundsB.Bottom); + var combinedBounds = new Rectangle(left, top, right - left, bot - top); + var mergedList = new List(foundItems[a].Item1); + mergedList.AddRange(foundItems[b].Item1); + if (_settings.Debug) + Main.AddLog($"SnapIt: Post-pass merged \"{foundItems[a].Item1[0].Name}\" + \"{foundItems[b].Item1[0].Name}\" (gap={vertGap}, avgH={avgHeight})"); + foundItems.RemoveAt(b); + foundItems.RemoveAt(a); + foundItems.Add(Tuple.Create(mergedList, combinedBounds)); + merged = true; + break; + } + } + } + } + if (merged) break; + } + } + } + // Process item groups foreach( Tuple, Rectangle> itemGroup in foundItems) { @@ -1727,7 +1808,7 @@ private static void GetItemCounts(Bitmap filteredImage, Bitmap filteredImageClea g.DrawRectangle(cyan, cloneRect); //do OCR - using (var page = _tesseractService.FirstEngine.Process(cloneBitmap, PageSegMode.SingleLine)) + using (var page = _tesseractService.NumbersOnlyEngine.Process(cloneBitmap, PageSegMode.SingleLine)) { using (var iterator = page.GetIterator()) { @@ -2186,6 +2267,15 @@ public static bool ThemeThresholdFilter(Color test, WFtheme theme) case WFtheme.ZEPHYR: return ((Math.Abs(test.GetHue() - primary.GetHue()) < 4 && test.GetSaturation() >= 0.55) || (Math.Abs(test.GetHue() - secondary.GetHue()) < 4 && test.GetSaturation() >= 0.66)) && test.GetBrightness() >= 0.25; + case WFtheme.CONQUERA: + return (Math.Abs(test.GetHue() - primary.GetHue()) < 25 && test.GetSaturation() >= 0.20 && test.GetBrightness() >= 0.15 && test.GetBrightness() <= 0.65) + || (test.GetSaturation() <= 0.25 && test.GetBrightness() >= 0.55); + case WFtheme.DEADLOCK: + return test.GetSaturation() <= 0.08 && test.GetBrightness() >= 0.80; + case WFtheme.LUNAR_RENEWAL: + return test.GetSaturation() <= 0.15 && test.GetBrightness() >= 0.85; + case WFtheme.POM_2: + return Math.Abs(test.GetHue() - secondary.GetHue()) < 30 && test.GetSaturation() >= 0.25 && test.GetBrightness() >= 0.55; default: // This shouldn't be ran // Only for initial testing diff --git a/WFInfo/Properties/AssemblyInfo.cs b/WFInfo/Properties/AssemblyInfo.cs index 8d6c8cd5..2e6e5422 100644 --- a/WFInfo/Properties/AssemblyInfo.cs +++ b/WFInfo/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("9.8.0.0")] -[assembly: AssemblyFileVersion("9.8.0.0")] +[assembly: AssemblyVersion("9.8.1.0")] +[assembly: AssemblyFileVersion("9.8.1.0")] diff --git a/WFInfo/Services/Screenshot/GdiScreenshotService.cs b/WFInfo/Services/Screenshot/GdiScreenshotService.cs index 53c56325..082f1866 100644 --- a/WFInfo/Services/Screenshot/GdiScreenshotService.cs +++ b/WFInfo/Services/Screenshot/GdiScreenshotService.cs @@ -41,8 +41,8 @@ public Task> CaptureScreenshot() height = window.Height; center = new Point(window.X + window.Width / 2, window.Y + window.Height / 2); - width *= (int)_window.DpiScaling; - height *= (int)_window.DpiScaling; + // Screen.Bounds is already in physical pixels for DPI-aware apps + // No DpiScaling multiplication needed } Bitmap image = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); diff --git a/WFInfo/Services/WindowInfo/Win32WindowInfoService.cs b/WFInfo/Services/WindowInfo/Win32WindowInfoService.cs index bd98dd93..7c3e0cfc 100644 --- a/WFInfo/Services/WindowInfo/Win32WindowInfoService.cs +++ b/WFInfo/Services/WindowInfo/Win32WindowInfoService.cs @@ -14,10 +14,7 @@ public double ScreenScaling { get { - if (Window.Width * 9 > Window.Height * 16) // image is less than 16:9 aspect - return Window.Height / 1080.0; - else - return Window.Width / 1920.0; //image is higher than 16:9 aspect + return Math.Max(Window.Width / 1920.0, Window.Height / 1080.0); } } diff --git a/WFInfo/Settings/ApplicationSettings.cs b/WFInfo/Settings/ApplicationSettings.cs index c322cd33..ca3309ee 100644 --- a/WFInfo/Settings/ApplicationSettings.cs +++ b/WFInfo/Settings/ApplicationSettings.cs @@ -88,7 +88,7 @@ public MouseButton? ActivationMouseButton public double MinimumEfficiencyValue { get; set; } = 4.5; public bool DoSnapItCount { get; set; } = false; public int SnapItDelay { get; set; } = 20000; - public double SnapItHorizontalNameMargin { get; set; } = 0; + public double SnapItHorizontalNameMargin { get; set; } = 0.25; public bool DoCustomNumberBoxWidth { get; set; } = false; public double SnapItNumberBoxWidth { get; set; } = 0.4; public bool SnapMultiThreaded { get; set; } = true; diff --git a/WFInfo/WFInfo.csproj b/WFInfo/WFInfo.csproj index b4844b2e..d682df07 100644 --- a/WFInfo/WFInfo.csproj +++ b/WFInfo/WFInfo.csproj @@ -79,7 +79,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -93,7 +93,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - +