From 7683eeb2bdbc00383787eb71112c89ed8be9bf8e Mon Sep 17 00:00:00 2001 From: Karl Nickels Date: Tue, 7 Apr 2026 13:38:16 -0400 Subject: [PATCH 1/7] Fix update failure when encountering unreleased game content The database updater was crashing when trying to process unreleased content (6.5.0 data) because it directly accessed the Mappings dictionary without checking if keys existed first. Changes: - UpdateCharacters: Add defensive checks for character name, skill, and constellation hashes before accessing Mappings dictionary - UpdateArtifacts: Add defensive check for artifact piece hashes - UpdateWeapons and UpdateMaterials already had proper checks This allows the updater to: - Skip unreleased content with warnings instead of crashing - Save all successfully processed released content (6.4) - Complete updates successfully even when remote data contains unreleased items Fixes issue where update would fail completely and roll back all changes when encountering missing mapping keys, leaving users stuck on outdated game data versions. --- InventoryKamera/data/DatabaseManager.cs | 42 +++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/InventoryKamera/data/DatabaseManager.cs b/InventoryKamera/data/DatabaseManager.cs index 392b750..99d7327 100644 --- a/InventoryKamera/data/DatabaseManager.cs +++ b/InventoryKamera/data/DatabaseManager.cs @@ -346,7 +346,15 @@ private UpdateStatus UpdateCharacters(bool force) Logger.Debug("Found {0} playable characters available", characters.Count); characters.AsParallel().ForAll(character => { - string name = Mappings[character["nameTextMapHash"].ToString()].ToString(); + // Check if character name mapping exists (skip unreleased characters) + string nameHash = character["nameTextMapHash"].ToString(); + if (!Mappings.ContainsKey(nameHash)) + { + Logger.Warn("Character hash {0} not found in Mappings. It's likely unreleased.", nameHash); + return; + } + + string name = Mappings[nameHash].ToString(); try { @@ -395,9 +403,20 @@ private UpdateStatus UpdateCharacters(bool force) if (elementSkill == null) continue; - skill = Mappings[elementSkill["nameTextMapHash"].ToString()].ToString(); + string skillHash = elementSkill["nameTextMapHash"].ToString(); + if (!Mappings.ContainsKey(skillHash)) + { + Logger.Warn("Character {0} traveler skill hash {1} not found in Mappings. It's likely unreleased.", nameGOOD, skillHash); + continue; + } + skill = Mappings[skillHash].ToString(); const3Description = talents.Where(entry => entry["openConfig"].ToString().Contains($"Player_{element.Key}")).ElementAt(2)["descTextMapHash"].ToString(); + if (!Mappings.ContainsKey(const3Description)) + { + Logger.Warn("Character {0} traveler constellation hash {1} not found in Mappings. It's likely unreleased.", nameGOOD, const3Description); + continue; + } const3Description = Mappings[const3Description].ToString(); if (const3Description.Contains(skill)) @@ -414,6 +433,11 @@ private UpdateStatus UpdateCharacters(bool force) else // Any other character that isn't traveler { skill = skills.First(entry => entry["skillIcon"].ToString().Contains($"Skill_S_{name}"))["nameTextMapHash"].ToString(); + if (!Mappings.ContainsKey(skill)) + { + Logger.Warn("Character {0} skill hash {1} not found in Mappings. It's likely unreleased.", nameGOOD, skill); + return; + } skill = Mappings[skill].ToString(); @@ -426,6 +450,11 @@ private UpdateStatus UpdateCharacters(bool force) // The skill/burst name is always mentioned in the constellation's description so we'll check for it const3Description = talents.Where(entry => entry["icon"].ToString().Contains(name)).ElementAt(2)["descTextMapHash"].ToString(); + if (!Mappings.ContainsKey(const3Description)) + { + Logger.Warn("Character {0} constellation hash {1} not found in Mappings. It's likely unreleased.", nameGOOD, const3Description); + return; + } const3Description = Mappings[const3Description].ToString(); if (const3Description.Contains(skill)) @@ -547,7 +576,14 @@ private UpdateStatus UpdateArtifacts(bool force) foreach (var artifact in sets.Where(s => artifactIDs.Values.Contains((int)s["id"]))) { var slot = artifactIDs.First(x => x.Value != null && (int)x.Value == (int)artifact["id"]).Key; - var artifactName = Mappings[artifact["nameTextMapHash"].ToString()]; // Goblet of the Sojourner + string artifactHash = artifact["nameTextMapHash"].ToString(); + // Check if artifact piece mapping exists (skip unreleased artifacts) + if (!Mappings.ContainsKey(artifactHash)) + { + Logger.Warn("Artifact piece hash {0} not found in Mappings. It's likely unreleased.", artifactHash); + continue; + } + var artifactName = Mappings[artifactHash]; // Goblet of the Sojourner string artifactNamePascalCase = CultureInfo.GetCultureInfo("en").TextInfo.ToTitleCase(artifactName); // Goblet Of The Sojourner string artifactNameGOOD = Regex.Replace(artifactNamePascalCase, @"[\W]", string.Empty); // GobletOfTheSojourner string artifactNormalized = artifactNameGOOD.ToLower(); // gobletofthesojourner From 9ea05263bcdea4a28f51b093eedc729911eb9b99 Mon Sep 17 00:00:00 2001 From: Karl Nickels Date: Tue, 7 Apr 2026 13:38:25 -0400 Subject: [PATCH 2/7] Add documentation files --- CLAUDE.md | 137 +++++++++++++++++ MODERNIZATION_PLAN.md | 337 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 474 insertions(+) create mode 100644 CLAUDE.md create mode 100644 MODERNIZATION_PLAN.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a99b462 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,137 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Inventory Kamera is a Windows Forms (.NET Framework 4.7.2) desktop application that scans Genshin Impact inventory data using OCR (Optical Character Recognition). It captures screenshots of the game window and uses Tesseract OCR with custom trained data to extract information about characters, weapons, artifacts, and materials. The scanned data is exported in GOOD (Genshin Open Object Description) format, a JSON-based standard compatible with popular Genshin Impact optimizer tools. + +## Build Commands + +### Building the Project +```bash +# Build in Debug configuration +msbuild InventoryKamera.sln /p:Configuration=Debug + +# Build in Release configuration +msbuild InventoryKamera.sln /p:Configuration=Release + +# Build for specific platform (x64, x86, or AnyCPU) +msbuild InventoryKamera.sln /p:Configuration=Release /p:Platform=x64 +``` + +### Running the Application +```bash +# Run from Debug build +./InventoryKamera/bin/Debug/InventoryKamera.exe + +# Run from Release build +./InventoryKamera/bin/Release/InventoryKamera.exe +``` + +### Restoring NuGet Packages +```bash +nuget restore InventoryKamera.sln +``` + +## Architecture Overview + +### Core Components + +**Data Flow Pipeline:** +1. `Navigation` - Captures game window and simulates keyboard/mouse input +2. `Scraper` classes - Extract regions from screenshots and queue them for OCR +3. `ImageProcessorWorker` threads - Process OCR queue using Tesseract engines +4. `InventoryKamera` class - Aggregates results and manages worker threads +5. `GOOD` class - Exports data in GOOD JSON format + +### Key Directories + +- **`InventoryKamera/data/`** - Core data structures and export logic + - `InventoryKamera.cs` - Main orchestration class, manages multi-threaded OCR processing + - `DatabaseManager.cs` - Loads/updates reference data from local JSON files and GitHub + - `GOOD.cs` - Exports scanned data to GOOD JSON format + - `Inventory.cs` - Container for weapons, artifacts, and materials + - `OCRImageCollection.cs` - Wrapper for queuing images to worker threads + +- **`InventoryKamera/game/`** - Game data models and navigation + - `Navigation.cs` - Window capture, input simulation, aspect ratio handling + - `Character.cs`, `Weapon.cs`, `Artifact.cs` - Game object models with GOOD serialization + +- **`InventoryKamera/scraping/`** - OCR scanning implementations + - `Scraper.cs` - Base OCR functionality, manages Tesseract engine pool + - `WeaponScraper.cs`, `ArtifactScraper.cs`, `CharacterScraper.cs`, `MaterialScraper.cs` - Specialized scanners for each inventory type + +- **`InventoryKamera/ui/`** - Windows Forms UI + - `MainForm.cs` - Main application window, scan controls, settings + - `UserInterface.cs` - Static helper for thread-safe UI updates from worker threads + +### Multi-Threading Architecture + +The application uses a producer-consumer pattern: +- **Main thread (UI thread):** Navigation and screenshot capture +- **Scraper threads:** Capture screenshots, crop regions, enqueue to `workerQueue` +- **Image processor workers (2-3 threads):** Dequeue items, run OCR, validate and store results + +Worker threads are spawned in `InventoryKamera.GatherData()` and process items from the shared `Queue`. The queue is thread-safe and workers use `TryDequeue()` to retrieve work items. A special "END" message signals workers to terminate after weapon/artifact scanning completes. + +### OCR Engine Pool + +`Scraper.cs` maintains a `ConcurrentBag` with 8 pre-initialized engines to avoid initialization overhead. Workers borrow an engine, perform OCR, and return it to the pool. Custom trained data files in `tessdata/` improve accuracy for Genshin Impact's UI font. + +### Reference Data + +The `inventorylists/` directory contains JSON files mapping item names to GOOD format identifiers: +- `characters.json` - Character data with constellation order and weapon type +- `weapons.json`, `artifacts.json` - Item name mappings +- `materials.json`, `devmaterials.json`, `allmaterials.json` - Material name mappings + +These files are loaded at startup and can be auto-updated from Dimbreath's GenshinData GitHub repository via `DatabaseManager`. + +### Game Integration + +The scanner requires: +- Genshin Impact running in 16:9 or 16:10 resolution (windowed or fullscreen) +- Game language set to English +- Paimon menu open before starting scan + +`Navigation.Initialize()` finds the game process (GenshinImpact.exe or YuanShen.exe), captures window dimensions, and verifies aspect ratio. The `InputSimulator` library sends keyboard inputs to navigate menus while `Graphics.CopyFromScreen()` captures specific regions. + +### Validation and Error Logging + +Each scanned item is validated (e.g., `Weapon.IsValid()` checks name, rarity, level, refinement). Invalid items trigger detailed logging to `./logging/weapons/`, `./logging/artifacts/`, etc., with cropped images and metadata for debugging. Users can enable "Log All Screenshots" to capture every scan attempt. + +## Important Development Notes + +### Resolution and Aspect Ratio Handling +All region coordinates in scrapers are calculated as ratios of the game window dimensions (see `Navigation.CaptureWindow()` and region calculations in scraper classes). When adding new regions, always use proportional coordinates based on a reference resolution (typically 1280x720 or 1920x1080). + +### Traveler Character Handling +The Traveler character's name is user-specific. `CharacterScraper.ScanMainCharacterName()` OCRs the name from the character screen, then `Scraper.AddTravelerToCharacterList()` creates a character entry by cloning the generic "traveler" template from `characters.json`. + +### Thread Safety +UI updates from worker threads must use `UserInterface` static methods, which internally use `Control.BeginInvoke()`. Never call WinForms controls directly from worker threads. + +### Tesseract Language Files +The custom trained data files (`genshin_best_eng.traineddata`, `genshin_fast_09_04_21.traineddata`) are critical for accuracy. These files are copied to the output directory during build (see `.csproj` `` elements). If OCR accuracy degrades, check that these files are present in the `tessdata/` directory. + +## Common Workflows + +### Adding Support for a New Character +1. Update `inventorylists/characters.json` with character data (name, constellation order, weapon type) +2. The scanner should automatically detect the character during the next scan +3. Test by scanning the character screen with the new character + +### Debugging OCR Issues +1. Enable "Log All Screenshots" in the UI +2. Run a scan to populate `./logging/` with cropped images +3. Examine the saved images to identify problematic regions +4. Adjust region coordinates in the relevant scraper class +5. Consider updating Tesseract trained data if text recognition is consistently failing + +### Updating Reference Data +The "Update Lookup Tables" option in the UI runs `DatabaseManager.UpdateAll()`, which: +1. Fetches latest data from Dimbreath's GenshinData repository +2. Parses JSON to extract item names and metadata +3. Converts to GOOD format and saves to `inventorylists/` +4. Should be run after each major Genshin Impact version update \ No newline at end of file diff --git a/MODERNIZATION_PLAN.md b/MODERNIZATION_PLAN.md new file mode 100644 index 0000000..267692b --- /dev/null +++ b/MODERNIZATION_PLAN.md @@ -0,0 +1,337 @@ +# Inventory Kamera Modernization Plan + +This document outlines the planned improvements to Inventory Kamera to improve maintainability, resilience, and user experience. + +## Background + +The current application has proven fragile when external dependencies change: +- Hard-coded GitHub repository URLs that broke when the data source moved from `Dimbreath/GenshinData` to `DimbreathBot/AnimeGameData` +- No graceful handling of missing or unreleased game data +- Fixed-size WinForms UI that's Windows-only and cramped + +This plan addresses these issues in two phases. + +--- + +## Phase 1: Configurable Data Sources + +**Goal:** Make the application resilient to future data source changes without requiring recompilation. + +### Current State +All data source URLs are hard-coded constants in `DatabaseManager.cs`: +```csharp +private const string CharactersURL = "https://raw.githubusercontent.com/DimbreathBot/AnimeGameData/master/ExcelBinOutput/AvatarExcelConfigData.json"; +private const string MappingsURL = "https://raw.githubusercontent.com/DimbreathBot/AnimeGameData/master/TextMap/TextMapEN.json"; +// ... etc +``` + +### Proposed Changes + +#### 1.1 Create Configuration File +- Add `datasources.json` configuration file with default URLs +- Structure: + ```json + { + "repository": { + "baseUrl": "https://raw.githubusercontent.com/DimbreathBot/AnimeGameData/master", + "endpoints": { + "characters": "/ExcelBinOutput/AvatarExcelConfigData.json", + "constellations": "/ExcelBinOutput/AvatarTalentExcelConfigData.json", + "skills": "/ExcelBinOutput/AvatarSkillExcelConfigData.json", + "artifacts": "/ExcelBinOutput/DisplayItemExcelConfigData.json", + "artifactsCodex": "/ExcelBinOutput/ReliquaryCodexExcelConfigData.json", + "setArtifacts": "/ExcelBinOutput/ReliquaryExcelConfigData.json", + "weapons": "/ExcelBinOutput/WeaponExcelConfigData.json", + "materials": "/ExcelBinOutput/MaterialExcelConfigData.json", + "mappings": "/TextMap/TextMapEN.json" + } + }, + "fallbackRepositories": [ + { + "name": "Sycamore0 Fork", + "baseUrl": "https://raw.githubusercontent.com/Sycamore0/GenshinData/master" + } + ] + } + ``` + +#### 1.2 Configuration Manager +- Create `DataSourceConfigManager` class to: + - Load configuration from `datasources.json` + - Provide URL builder methods + - Validate configuration on load + - Support runtime configuration reload + +#### 1.3 Settings UI Integration +- Add "Data Sources" tab in Options menu +- Allow users to: + - View current repository URL + - Switch to fallback repository + - Manually enter custom repository URL + - Test connection to repository + - Reset to defaults + +#### 1.4 Fallback Logic +- Implement automatic fallback when primary repository fails: + 1. Try primary repository + 2. On 404/timeout, try fallback repositories in order + 3. Log which repository succeeded + 4. Remember last successful repository for next run + +### Benefits +- ✅ Users can fix broken updates themselves by changing configuration +- ✅ Community can maintain forks with updated URLs +- ✅ No recompilation needed when data sources change +- ✅ Graceful degradation when primary source is unavailable + +### Implementation Estimate +- Small scope, can be done within existing .NET Framework 4.7.2 WinForms app +- No UI framework changes required +- Backwards compatible with existing installations + +--- + +## Phase 2: UI Modernization & Cross-Platform Support + +**Goal:** Create a modern, cross-platform desktop application with better UX. + +### Current State +- Windows Forms (.NET Framework 4.7.2) +- Fixed window size (non-resizable) +- Cramped layout with absolute positioning +- Windows-only +- Dated visual design + +### Target State +- Cross-platform desktop app (Windows, macOS, Linux) +- Resizable, responsive layouts +- Modern UI design +- Better user experience and accessibility + +### Technology Options + +#### Option A: Avalonia UI (Recommended) +**Pros:** +- Cross-platform (Windows, macOS, Linux) +- XAML-based (similar to WPF, easier migration from WinForms concepts) +- Good performance +- Active development and community +- Supports .NET 6+ + +**Cons:** +- Steeper learning curve than MAUI for desktop +- Smaller ecosystem than WPF + +#### Option B: .NET MAUI +**Pros:** +- Official Microsoft framework +- Cross-platform (Windows, macOS, iOS, Android) +- XAML-based +- Good tooling in Visual Studio + +**Cons:** +- Desktop support still maturing +- Heavier runtime +- More mobile-focused + +**Recommendation:** Avalonia UI for better desktop focus and Linux support + +### Proposed Changes + +#### 2.1 Project Migration +- **New Project Structure:** + ``` + InventoryKamera.sln + ├── InventoryKamera.Core/ # Shared business logic (target: .NET 8) + │ ├── Data/ # DatabaseManager, GOOD export + │ ├── Scanning/ # OCR, scrapers + │ └── Models/ # Character, Weapon, Artifact + ├── InventoryKamera.Avalonia/ # New Avalonia UI (target: .NET 8) + │ ├── Views/ # XAML views + │ ├── ViewModels/ # MVVM view models + │ └── Services/ # Platform-specific services + └── InventoryKamera.Legacy/ # Current WinForms app (maintenance only) + ``` + +- Migrate to **.NET 8** (current LTS) +- Extract business logic to `.Core` library +- Implement new UI in `.Avalonia` project +- Keep legacy WinForms app for reference/fallback + +#### 2.2 Core Library Extraction +**Extract from WinForms dependencies:** +- `DatabaseManager` - No UI dependencies +- `Scraper`, `ArtifactScraper`, `CharacterScraper`, etc. - Pure logic +- `GOOD`, `Inventory` - Data models +- `Navigation` - Will need platform abstraction for screen capture + +**Requires platform abstraction:** +- Screen capture (Windows: `Graphics.CopyFromScreen`, cross-platform: SkiaSharp or Avalonia) +- Input simulation (Windows: `WindowsInput`, cross-platform: platform-specific implementations) +- Window detection (process enumeration, focus management) + +#### 2.3 New UI Design + +**Main Window Layout:** +``` +┌─────────────────────────────────────────────────────┐ +│ Inventory Kamera [─][□][×] │ +├─────────────────────────────────────────────────────┤ +│ ┌─────────┐ │ +│ │ Scan │ Scan Settings: │ +│ │ Settings│ ☑ Characters ☑ Weapons │ +│ │ │ ☑ Artifacts ☑ Materials │ +│ │ Game │ Min Rarity: [3★ ▼] │ +│ │ Status │ │ +│ │ │ Scan Speed: [Normal ──●────── Fast] │ +│ │ Output │ │ +│ │ │ Output: [C:\...\GenshinData [...] ] │ +│ │ Options │ │ +│ └─────────┘ [Scan Inventory] │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ Scan Progress │ │ +│ │ Characters: 45/87 ████████░░░░░░░ 52% │ │ +│ │ Weapons: 123/245 ████████░░░░░░░ 50% │ │ +│ │ │ │ +│ │ [Preview of current item being scanned] │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ +│ Console Output: │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ [2026-04-07 12:45] Starting scan... │ │ +│ │ [2026-04-07 12:45] Scanning characters... │ │ +│ │ [2026-04-07 12:45] Found: Hu Tao (C1, 90) │ │ +│ └──────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +**Improvements:** +- Resizable window with responsive layout +- Proper spacing and padding +- Real-time scan progress with visual feedback +- Modern progress bars +- Console/log output area +- Tabbed interface for advanced settings +- Light/dark mode support + +#### 2.4 MVVM Architecture +- Implement proper separation of concerns +- ViewModels handle business logic +- Views are purely declarative (XAML) +- Commands for user actions +- Observable properties for data binding + +#### 2.5 Platform-Specific Implementations + +**Windows:** +- Use existing `WindowsInput` for input simulation +- `Graphics.CopyFromScreen` for screen capture +- Process enumeration for finding Genshin Impact + +**macOS:** +- CGWindowListCreateImage for screen capture +- Core Graphics for input simulation +- Process enumeration via NSRunningApplication + +**Linux:** +- X11/Wayland screen capture +- xdotool or similar for input simulation +- Process enumeration via /proc + +Implement these as platform services with common interfaces. + +#### 2.6 Enhanced Features (Nice-to-have) + +- **Scan history:** Track past scans, diff between scans +- **Data visualization:** Charts showing character/weapon distribution +- **Export formats:** Support additional formats beyond GOOD (CSV, Excel, etc.) +- **Scan profiles:** Save different scan configurations +- **Automation:** Schedule periodic scans +- **Cloud sync:** Optional backup to cloud storage + +### Migration Strategy + +1. **Phase 2.1:** Extract core logic to `.Core` library +2. **Phase 2.2:** Create basic Avalonia UI with main workflow +3. **Phase 2.3:** Implement platform abstractions for Windows +4. **Phase 2.4:** Feature parity with WinForms version +5. **Phase 2.5:** Add Linux/macOS support +6. **Phase 2.6:** Enhanced features and polish +7. **Phase 2.7:** Beta testing with community +8. **Phase 2.8:** Release v2.0 + +### Benefits +- ✅ Linux users (Steam Deck!) can use the tool +- ✅ macOS users can use the tool +- ✅ Modern, responsive UI +- ✅ Better UX and accessibility +- ✅ Easier to maintain (MVVM, proper separation) +- ✅ Future-proof (.NET 8+) + +### Implementation Estimate +- Major undertaking, essentially a rewrite +- 3-6 months for feature parity with careful planning +- Suitable for a new major version (v2.0) + +--- + +## Branch Strategy + +``` +master (main development) +├── fix/defensive-mapping-checks (current - bug fixes for v1.x) +├── feat/configurable-data-sources (Phase 1) +└── modernize/avalonia-ui (Phase 2) + ├── feat/core-library-extraction + ├── feat/avalonia-ui-foundation + ├── feat/mvvm-implementation + ├── feat/platform-abstractions + └── feat/cross-platform-support +``` + +--- + +## Success Criteria + +### Phase 1 Success +- [ ] Users can change data source URLs without recompiling +- [ ] Application automatically falls back to alternative sources +- [ ] Settings UI allows testing connection to repositories +- [ ] No breaking changes to existing functionality + +### Phase 2 Success +- [ ] Application runs on Windows, macOS, and Linux +- [ ] All existing features work in new UI +- [ ] Window is resizable with proper responsive layout +- [ ] Scan workflow is smoother and more intuitive +- [ ] Performance is equal to or better than WinForms version +- [ ] Community feedback is positive + +--- + +## Community Impact + +### For Users +- **Phase 1:** More reliable updates, self-service fixes +- **Phase 2:** Cross-platform support, better UX, modern design + +### For Steam Deck/Linux Users +- Currently cannot use the tool (Wine might work but is unreliable) +- Phase 2 enables native Linux support +- Genshin Impact runs on Steam Deck via Proton +- This would be the first native inventory scanner for Linux + +### For Contributors +- Phase 1: Minimal barrier to entry, same tech stack +- Phase 2: Modern architecture, cleaner codebase, easier to contribute + +--- + +## Next Steps + +1. ✅ Complete current bug fixes (defensive checks, URL updates, enable update menu) +2. ✅ Submit PR to upstream project +3. **Start Phase 1:** Create issue for configurable data sources +4. Gather community feedback on Phase 2 plans +5. Begin Phase 1 implementation in separate branch From ee8a52d0a81ed9b0b5d31bbb0f964de1a5d66b9d Mon Sep 17 00:00:00 2001 From: Karl Nickels Date: Tue, 7 Apr 2026 14:18:21 -0400 Subject: [PATCH 3/7] Add detailed debug logging to artifact scanner to diagnose crash --- InventoryKamera/scraping/ArtifactScraper.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/InventoryKamera/scraping/ArtifactScraper.cs b/InventoryKamera/scraping/ArtifactScraper.cs index f66671d..a2e79f9 100644 --- a/InventoryKamera/scraping/ArtifactScraper.cs +++ b/InventoryKamera/scraping/ArtifactScraper.cs @@ -166,16 +166,27 @@ private void ClearFilters() public async void QueueScan(int id) { + Logger.Debug("QueueScan starting for artifact #{0}", id); + Logger.Debug(" Capturing item card..."); var card = GetItemCard(); + Logger.Debug(" Item card captured successfully"); Bitmap name, gearSlot, mainStat, subStats, level, equipped, locked; + Logger.Debug(" Extracting name bitmap..."); name = GetItemNameBitmap(card); + Logger.Debug(" Extracting locked bitmap..."); locked = GetLockedBitmap(card); + Logger.Debug(" Extracting equipped bitmap..."); equipped = GetEquippedBitmap(card); + Logger.Debug(" Extracting gear slot bitmap..."); gearSlot = GetGearSlotBitmap(card); + Logger.Debug(" Extracting main stat bitmap..."); mainStat = GetMainStatBitmap(card); + Logger.Debug(" Extracting level bitmap..."); level = GetLevelBitmap(card); + Logger.Debug(" Extracting substats bitmap..."); subStats = GetSubstatsBitmap(card); + Logger.Debug(" All bitmaps extracted successfully"); //Navigation.DisplayBitmap(name); @@ -198,17 +209,21 @@ public async void QueueScan(int id) card }; + Logger.Debug(" Checking rarity and level filters..."); bool belowRarity = GetRarity(name) < Properties.Settings.Default.MinimumArtifactRarity; bool belowLevel = ScanArtifactLevel(level) < Properties.Settings.Default.MinimumArtifactLevel; StopScanning = (SortByLevel && belowLevel) || (!SortByLevel && belowRarity); if (StopScanning || belowRarity || belowLevel) { + Logger.Debug(" Artifact #{0} filtered out (belowRarity={1}, belowLevel={2})", id, belowRarity, belowLevel); artifactImages.ForEach(i => i.Dispose()); return; } // Send images to Worker Queue + Logger.Debug(" Enqueueing artifact #{0} to worker queue...", id); InventoryKamera.workerQueue.Enqueue(new OCRImageCollection(artifactImages, "artifact", id)); + Logger.Debug(" Artifact #{0} enqueued successfully", id); } private Bitmap GetSubstatsBitmap(Bitmap card) @@ -249,6 +264,7 @@ private Bitmap GetGearSlotBitmap(Bitmap card) public static async Task CatalogueFromBitmapsAsync(List bm, int id) { + Logger.Debug("CatalogueFromBitmapsAsync starting for artifact #{0}", id); // Init Variables string gearSlot = null; string mainStat = null; @@ -261,6 +277,7 @@ public static async Task CatalogueFromBitmapsAsync(List bm, in if (bm.Count >= 6) { + Logger.Debug(" Processing artifact #{0} with {1} bitmaps", id, bm.Count); int a_name = 0; int a_gearSlot = 1; int a_mainStat = 2; int a_level = 3; int a_subStats = 4; int a_equippedCharacter = 5; int a_lock = 6; // Get Rarity rarity = GetRarity(bm[a_name]); @@ -276,6 +293,7 @@ public static async Task CatalogueFromBitmapsAsync(List bm, in _lock = GenshinProcesor.CompareColors(lockedColor, lockStatus); // Improved Scanning using multi threading + Logger.Debug(" Starting OCR tasks for artifact #{0}...", id); List tasks = new List(); var taskGear = Task.Run(() => gearSlot = ScanArtifactGearSlot(bm[a_gearSlot])); @@ -295,8 +313,11 @@ public static async Task CatalogueFromBitmapsAsync(List bm, in tasks.Add(taskEquip); } + Logger.Debug(" Waiting for {0} OCR tasks to complete for artifact #{1}...", tasks.Count, id); await Task.WhenAll(tasks.ToArray()); + Logger.Debug(" All OCR tasks completed for artifact #{0}", id); } + Logger.Debug(" Creating Artifact object for #{0}: set={1}, rarity={2}, level={3}, slot={4}", id, setName, rarity, level, gearSlot); return new Artifact(setName, rarity, level, gearSlot, mainStat, subStats, equippedCharacter, id, _lock); } From 6fcb4e93745608154209c10ee8caaf32b980d71a Mon Sep 17 00:00:00 2001 From: Karl Nickels Date: Tue, 7 Apr 2026 14:33:37 -0400 Subject: [PATCH 4/7] Add sanctifying elixir artifact support for 16:9 resolution Sanctified artifacts have a purple indicator bar that shifts the visual layout. Without this fix, the scanner hangs when processing rerolled artifacts because bitmap coordinates are misaligned. Changes: - Add IsSanctified() to detect purple sanctifying indicator - Add GetSanctifyBitmap() to extract indicator region - Update GetSubstatsBitmap() with 0.0520 Y-shift for sanctified artifacts - Update GetLevelBitmap() with 0.0520 Y-shift for sanctified artifacts - Add GetArtifactLockedBitmap() with 0.0520 Y-shift for sanctified artifacts Based on fix from Sirielia/Inventory_Kamera fork, adapted for 16:9. Fixes hang/crash when scanning artifacts created with sanctifying elixir. --- InventoryKamera/scraping/ArtifactScraper.cs | 47 +++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/InventoryKamera/scraping/ArtifactScraper.cs b/InventoryKamera/scraping/ArtifactScraper.cs index a2e79f9..755b3ad 100644 --- a/InventoryKamera/scraping/ArtifactScraper.cs +++ b/InventoryKamera/scraping/ArtifactScraper.cs @@ -175,7 +175,7 @@ public async void QueueScan(int id) Logger.Debug(" Extracting name bitmap..."); name = GetItemNameBitmap(card); Logger.Debug(" Extracting locked bitmap..."); - locked = GetLockedBitmap(card); + locked = GetArtifactLockedBitmap(card); Logger.Debug(" Extracting equipped bitmap..."); equipped = GetEquippedBitmap(card); Logger.Debug(" Extracting gear slot bitmap..."); @@ -228,13 +228,50 @@ public async void QueueScan(int id) private Bitmap GetSubstatsBitmap(Bitmap card) { + bool isSanctified = IsSanctified(card); + double sanctifiedShift = Navigation.IsNormal ? 0.0520 : 0.0471; + double yShift = isSanctified ? sanctifiedShift : 0.0; + return GenshinProcesor.CopyBitmap(card,new Rectangle( x:(int)(card.Width * 0.0911), - y:(int)(card.Height * (Navigation.IsNormal ? 0.4216 : 0.3682)), + y:(int)(card.Height * ((Navigation.IsNormal ? 0.4216 : 0.3682) + yShift)), width:(int)(card.Width * 0.8097), height:(int)(card.Height * (Navigation.IsNormal ? 0.1841 : 0.1573)))); } + private bool IsSanctified(Bitmap card) + { + // Check for purple sanctifying indicator bar + var sanctifyBitmap = GetSanctifyBitmap(card); + Color purple = Color.FromArgb(255, 138, 107, 197); // Purple sanctifying indicator color + Color pixelColor = sanctifyBitmap.GetPixel(sanctifyBitmap.Width / 2, sanctifyBitmap.Height / 2); + sanctifyBitmap.Dispose(); + return GenshinProcesor.CompareColors(purple, pixelColor, 30); // 30 tolerance for color matching + } + + private Bitmap GetSanctifyBitmap(Bitmap card) + { + return GenshinProcesor.CopyBitmap(card, new Rectangle( + x: (int)(card.Width * 0.40), + y: (int)(card.Height * (Navigation.IsNormal ? 0.3333 : 0.2941)), + width: (int)(card.Width * 0.20), + height: (int)(card.Height * (Navigation.IsNormal ? 0.0526 : 0.0470)))); + } + + private Bitmap GetArtifactLockedBitmap(Bitmap card) + { + bool isSanctified = IsSanctified(card); + double sanctifiedShift = Navigation.IsNormal ? 0.0520 : 0.0471; + double yShift = isSanctified ? sanctifiedShift : 0.0; + + return GenshinProcesor.CopyBitmap(card, + new Rectangle( + x: (int)(card.Width * 0.75), + y: (int)(card.Height * ((Navigation.IsNormal ? 0.353 : 0.309) + yShift)), + width: (int)(card.Width * 0.0955), + height: (int)(card.Height * (Navigation.IsNormal ? 0.055 : 0.0495)))); + } + private Bitmap GetMainStatBitmap(Bitmap card) { return GenshinProcesor.CopyBitmap(card, new Rectangle( @@ -246,9 +283,13 @@ private Bitmap GetMainStatBitmap(Bitmap card) private Bitmap GetLevelBitmap(Bitmap card) { + bool isSanctified = IsSanctified(card); + double sanctifiedShift = Navigation.IsNormal ? 0.0520 : 0.0465; + double yShift = isSanctified ? sanctifiedShift : 0.0; + return GenshinProcesor.CopyBitmap(card, new Rectangle( x: (int)(card.Width * 0.0506), - y: (int)(card.Height * (Navigation.IsNormal ? 0.3634 : 0.3197)), + y: (int)(card.Height * ((Navigation.IsNormal ? 0.3634 : 0.3197) + yShift)), width: (int)(card.Width * 0.1417), height: (int)(card.Height * (Navigation.IsNormal ? 0.0416 : 0.0347)))); } From 468aebf202535bf4b976b11756d1ce826d98927d Mon Sep 17 00:00:00 2001 From: Karl Nickels Date: Tue, 7 Apr 2026 14:38:14 -0400 Subject: [PATCH 5/7] Add 30-second timeout to OCR worker threads and improve logging OCR worker threads were hanging indefinitely when processing problematic artifacts, causing the entire scan to freeze. This adds timeouts and additional debug logging to diagnose and recover from hangs. Changes: - Add 30-second timeout to artifact OCR processing - Add 30-second timeout to weapon OCR processing - Skip hung artifacts with error message instead of freezing - Add debug logging to IsSanctified() to show color detection - Add debug logging to bitmap extraction steps When timeout occurs, the artifact/weapon is skipped and scanning continues with remaining items. This prevents one problematic item from blocking the entire scan. Addresses issue where scanner hangs on specific artifacts during OCR processing, particularly during substat parsing. --- InventoryKamera/data/InventoryKamera.cs | 24 +++++++++++++++++---- InventoryKamera/scraping/ArtifactScraper.cs | 7 +++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/InventoryKamera/data/InventoryKamera.cs b/InventoryKamera/data/InventoryKamera.cs index e82acc0..b17b189 100644 --- a/InventoryKamera/data/InventoryKamera.cs +++ b/InventoryKamera/data/InventoryKamera.cs @@ -270,8 +270,16 @@ public void ImageProcessorWorker() UserInterface.SetGearPictureBox(imageCollection.Bitmaps.Last()); - // Scan as weapon - Weapon weapon = weaponScraper.CatalogueFromBitmapsAsync(imageCollection.Bitmaps, imageCollection.Id).Result; + // Scan as weapon with 30 second timeout + var weaponTask = weaponScraper.CatalogueFromBitmapsAsync(imageCollection.Bitmaps, imageCollection.Id); + if (!weaponTask.Wait(TimeSpan.FromSeconds(30))) + { + Logger.Error("Weapon #{0} OCR timed out after 30 seconds - skipping", imageCollection.Id); + UserInterface.AddError($"Weapon #{imageCollection.Id} scan timed out - possibly problematic weapon data"); + imageCollection.Bitmaps.ForEach(b => b.Dispose()); + break; + } + Weapon weapon = weaponTask.Result; UserInterface.SetGear(imageCollection.Bitmaps.Last(), weapon); string weaponPath = $"./logging/weapons/weapon{weapon.Id}/"; @@ -334,8 +342,16 @@ public void ImageProcessorWorker() } UserInterface.SetGearPictureBox(imageCollection.Bitmaps.Last()); - // Scan as artifact - Artifact artifact = ArtifactScraper.CatalogueFromBitmapsAsync(imageCollection.Bitmaps, imageCollection.Id).Result; + // Scan as artifact with 30 second timeout + var artifactTask = ArtifactScraper.CatalogueFromBitmapsAsync(imageCollection.Bitmaps, imageCollection.Id); + if (!artifactTask.Wait(TimeSpan.FromSeconds(30))) + { + Logger.Error("Artifact #{0} OCR timed out after 30 seconds - skipping", imageCollection.Id); + UserInterface.AddError($"Artifact #{imageCollection.Id} scan timed out - possibly problematic artifact data"); + imageCollection.Bitmaps.ForEach(b => b.Dispose()); + break; + } + Artifact artifact = artifactTask.Result; UserInterface.SetGear(imageCollection.Bitmaps.Last(), artifact); string artifactPath = $"./logging/artifacts/artifact{artifact.Id}/"; diff --git a/InventoryKamera/scraping/ArtifactScraper.cs b/InventoryKamera/scraping/ArtifactScraper.cs index 755b3ad..d3536d8 100644 --- a/InventoryKamera/scraping/ArtifactScraper.cs +++ b/InventoryKamera/scraping/ArtifactScraper.cs @@ -184,8 +184,10 @@ public async void QueueScan(int id) mainStat = GetMainStatBitmap(card); Logger.Debug(" Extracting level bitmap..."); level = GetLevelBitmap(card); + Logger.Debug(" Level bitmap extracted"); Logger.Debug(" Extracting substats bitmap..."); subStats = GetSubstatsBitmap(card); + Logger.Debug(" Substats bitmap extracted"); Logger.Debug(" All bitmaps extracted successfully"); @@ -245,8 +247,11 @@ private bool IsSanctified(Bitmap card) var sanctifyBitmap = GetSanctifyBitmap(card); Color purple = Color.FromArgb(255, 138, 107, 197); // Purple sanctifying indicator color Color pixelColor = sanctifyBitmap.GetPixel(sanctifyBitmap.Width / 2, sanctifyBitmap.Height / 2); + bool isSanctified = GenshinProcesor.CompareColors(purple, pixelColor, 30); // 30 tolerance for color matching + Logger.Debug(" IsSanctified check: expected RGB({0},{1},{2}), got RGB({3},{4},{5}), result={6}", + purple.R, purple.G, purple.B, pixelColor.R, pixelColor.G, pixelColor.B, isSanctified); sanctifyBitmap.Dispose(); - return GenshinProcesor.CompareColors(purple, pixelColor, 30); // 30 tolerance for color matching + return isSanctified; } private Bitmap GetSanctifyBitmap(Bitmap card) From ccce88c399f9383e562d665806de8bf5940ea210 Mon Sep 17 00:00:00 2001 From: Karl Nickels Date: Tue, 7 Apr 2026 14:44:22 -0400 Subject: [PATCH 6/7] Fix CompareColors call - remove invalid tolerance parameter --- InventoryKamera/scraping/ArtifactScraper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InventoryKamera/scraping/ArtifactScraper.cs b/InventoryKamera/scraping/ArtifactScraper.cs index d3536d8..25cee9e 100644 --- a/InventoryKamera/scraping/ArtifactScraper.cs +++ b/InventoryKamera/scraping/ArtifactScraper.cs @@ -247,7 +247,7 @@ private bool IsSanctified(Bitmap card) var sanctifyBitmap = GetSanctifyBitmap(card); Color purple = Color.FromArgb(255, 138, 107, 197); // Purple sanctifying indicator color Color pixelColor = sanctifyBitmap.GetPixel(sanctifyBitmap.Width / 2, sanctifyBitmap.Height / 2); - bool isSanctified = GenshinProcesor.CompareColors(purple, pixelColor, 30); // 30 tolerance for color matching + bool isSanctified = GenshinProcesor.CompareColors(purple, pixelColor); // CompareColors uses tolerance of 10 for each channel Logger.Debug(" IsSanctified check: expected RGB({0},{1},{2}), got RGB({3},{4},{5}), result={6}", purple.R, purple.G, purple.B, pixelColor.R, pixelColor.G, pixelColor.B, isSanctified); sanctifyBitmap.Dispose(); From 67a261c3c2fc95da04dc696c630341622bb4aed1 Mon Sep 17 00:00:00 2001 From: Karl Nickels Date: Tue, 7 Apr 2026 14:52:10 -0400 Subject: [PATCH 7/7] Fix sanctified detection to handle color variations and improve timeout The sanctified purple indicator varies in brightness from dark purple RGB(138,107,197) to light purple RGB(220,192,255). The exact color matching with tolerance=10 was failing on light purple variants, causing wrong coordinates and Tesseract hangs. Changes: - Replace exact color matching with purple range detection (B>R>G pattern) - Check Blue channel > 150 to ensure it's purple-ish - Switch from Task.Wait() to Task.WhenAny() for more reliable timeout - Add debug logging for OCR start/completion This should detect all purple variants and properly apply the coordinate shift for sanctified artifacts, preventing OCR hangs on malformed regions. --- InventoryKamera/data/InventoryKamera.cs | 5 ++++- InventoryKamera/scraping/ArtifactScraper.cs | 15 ++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/InventoryKamera/data/InventoryKamera.cs b/InventoryKamera/data/InventoryKamera.cs index b17b189..3351a6d 100644 --- a/InventoryKamera/data/InventoryKamera.cs +++ b/InventoryKamera/data/InventoryKamera.cs @@ -343,14 +343,17 @@ public void ImageProcessorWorker() UserInterface.SetGearPictureBox(imageCollection.Bitmaps.Last()); // Scan as artifact with 30 second timeout + Logger.Debug("Starting OCR for artifact #{0}", imageCollection.Id); var artifactTask = ArtifactScraper.CatalogueFromBitmapsAsync(imageCollection.Bitmaps, imageCollection.Id); - if (!artifactTask.Wait(TimeSpan.FromSeconds(30))) + var completedTask = Task.WhenAny(artifactTask, Task.Delay(30000)).Result; + if (completedTask != artifactTask) { Logger.Error("Artifact #{0} OCR timed out after 30 seconds - skipping", imageCollection.Id); UserInterface.AddError($"Artifact #{imageCollection.Id} scan timed out - possibly problematic artifact data"); imageCollection.Bitmaps.ForEach(b => b.Dispose()); break; } + Logger.Debug("OCR completed for artifact #{0}", imageCollection.Id); Artifact artifact = artifactTask.Result; UserInterface.SetGear(imageCollection.Bitmaps.Last(), artifact); diff --git a/InventoryKamera/scraping/ArtifactScraper.cs b/InventoryKamera/scraping/ArtifactScraper.cs index 25cee9e..035485d 100644 --- a/InventoryKamera/scraping/ArtifactScraper.cs +++ b/InventoryKamera/scraping/ArtifactScraper.cs @@ -244,14 +244,19 @@ private Bitmap GetSubstatsBitmap(Bitmap card) private bool IsSanctified(Bitmap card) { // Check for purple sanctifying indicator bar + // The purple color varies in brightness, so check if it's in the purple range var sanctifyBitmap = GetSanctifyBitmap(card); - Color purple = Color.FromArgb(255, 138, 107, 197); // Purple sanctifying indicator color Color pixelColor = sanctifyBitmap.GetPixel(sanctifyBitmap.Width / 2, sanctifyBitmap.Height / 2); - bool isSanctified = GenshinProcesor.CompareColors(purple, pixelColor); // CompareColors uses tolerance of 10 for each channel - Logger.Debug(" IsSanctified check: expected RGB({0},{1},{2}), got RGB({3},{4},{5}), result={6}", - purple.R, purple.G, purple.B, pixelColor.R, pixelColor.G, pixelColor.B, isSanctified); + + // Purple range: Blue should be highest, Red should be moderate, Green lowest + // Check if this is a purple-ish color (Blue > Red > Green pattern) + bool isPurple = pixelColor.B > pixelColor.R && pixelColor.R >= pixelColor.G && + pixelColor.B > 150; // Blue channel must be reasonably high + + Logger.Debug(" IsSanctified check: got RGB({0},{1},{2}), isPurple={3}", + pixelColor.R, pixelColor.G, pixelColor.B, isPurple); sanctifyBitmap.Dispose(); - return isSanctified; + return isPurple; } private Bitmap GetSanctifyBitmap(Bitmap card)