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/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 diff --git a/InventoryKamera/data/InventoryKamera.cs b/InventoryKamera/data/InventoryKamera.cs index e82acc0..3351a6d 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,19 @@ 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 + Logger.Debug("Starting OCR for artifact #{0}", imageCollection.Id); + var artifactTask = ArtifactScraper.CatalogueFromBitmapsAsync(imageCollection.Bitmaps, imageCollection.Id); + 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); string artifactPath = $"./logging/artifacts/artifact{artifact.Id}/"; diff --git a/InventoryKamera/scraping/ArtifactScraper.cs b/InventoryKamera/scraping/ArtifactScraper.cs index f66671d..035485d 100644 --- a/InventoryKamera/scraping/ArtifactScraper.cs +++ b/InventoryKamera/scraping/ArtifactScraper.cs @@ -166,16 +166,29 @@ 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); - locked = GetLockedBitmap(card); + Logger.Debug(" Extracting locked bitmap..."); + locked = GetArtifactLockedBitmap(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(" Level bitmap extracted"); + Logger.Debug(" Extracting substats bitmap..."); subStats = GetSubstatsBitmap(card); + Logger.Debug(" Substats bitmap extracted"); + Logger.Debug(" All bitmaps extracted successfully"); //Navigation.DisplayBitmap(name); @@ -198,28 +211,77 @@ 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) { + 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 + // The purple color varies in brightness, so check if it's in the purple range + var sanctifyBitmap = GetSanctifyBitmap(card); + Color pixelColor = sanctifyBitmap.GetPixel(sanctifyBitmap.Width / 2, sanctifyBitmap.Height / 2); + + // 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 isPurple; + } + + 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( @@ -231,9 +293,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)))); } @@ -249,6 +315,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 +328,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 +344,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 +364,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); } 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