diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index bc79e571..5fae8e95 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -1,12 +1,6 @@ name: Build Browser Only on: - push: - branches: [main, develop] - paths: ["src/BalatroSeedOracle.Browser/**", "src/BalatroSeedOracle/**"] - pull_request: - branches: [main] - paths: ["src/BalatroSeedOracle.Browser/**", "src/BalatroSeedOracle/**"] workflow_dispatch: jobs: @@ -19,6 +13,8 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v4 + with: + submodules: recursive - name: Setup .NET SDK uses: actions/setup-dotnet@v4 diff --git a/.github/workflows/deploy-browser.yml b/.github/workflows/deploy-browser.yml index 9327bb87..fdaeeb8c 100644 --- a/.github/workflows/deploy-browser.yml +++ b/.github/workflows/deploy-browser.yml @@ -1,18 +1,7 @@ name: Deploy Browser Version on: - push: - branches: [main] workflow_dispatch: - inputs: - environment: - description: "Deployment environment" - required: true - default: "staging" - type: choice - options: - - staging - - production env: DOTNET_VERSION: "10.0.x" diff --git a/.github/workflows/publish-motely-wasm.yml b/.github/workflows/publish-motely-wasm.yml index b4282a78..1d77c797 100644 --- a/.github/workflows/publish-motely-wasm.yml +++ b/.github/workflows/publish-motely-wasm.yml @@ -14,15 +14,7 @@ name: Publish Motely WASM to npm on: - push: - tags: - - 'motely-wasm-v*' workflow_dispatch: - inputs: - version: - description: 'Override npm package version (e.g., 1.0.1). Empty = use package.json version.' - required: false - type: string env: DOTNET_VERSION: '10.0.x' diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index 2bd16c9e..3b6c0955 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -17,6 +17,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@v4 with: + submodules: recursive fetch-depth: 0 - name: Setup .NET SDK diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a2e23ee9..dcb117ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,6 @@ name: Complete Release Build on: - push: - tags: - - 'v*' workflow_dispatch: jobs: diff --git a/.gitignore b/.gitignore index 06181f8a..9cbeacef 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,10 @@ avalonia.license.local build.log docs/BLUEPRINT_GITHUB_PAGES_FIX.md *.md + +# Build/publish logs and scratch txt — these have leaked into the repo +# multiple times (publish_log.txt, publish_retry.txt, temp_duckdb_ops.txt, +# build_output.txt). Stop the bleed at the gitignore level. +publish*.txt +build_output.txt +temp_*.txt diff --git a/BROWSER_COOP_COEP_FIX.md b/BROWSER_COOP_COEP_FIX.md deleted file mode 100644 index d900a792..00000000 --- a/BROWSER_COOP_COEP_FIX.md +++ /dev/null @@ -1,79 +0,0 @@ -# Browser COOP/COEP Headers Fix - -## The Problem - -Browsers require `Cross-Origin-Opener-Policy` (COOP) and `Cross-Origin-Embedder-Policy` (COEP) headers to enable `SharedArrayBuffer`, which is required for .NET WASM threading. - -**However**, browsers only trust these headers from "secure contexts": -- ✅ `localhost` (trusted) -- ✅ `127.0.0.1` (usually trusted) -- ✅ HTTPS URLs -- ❌ **IP addresses over HTTP are NOT trusted** - -## Why You Keep Seeing This Error - -When you access `http://192.168.0.171:3141/BSO/`, the browser sees an "untrustworthy origin" and ignores the COOP/COEP headers, even though the server is sending them correctly. - -## Solutions - -### ✅ Solution 1: Use `localhost` (Recommended for Local Development) - -**Change your URL from:** -``` -http://192.168.0.171:3141/BSO/ -``` - -**To:** -``` -http://localhost:3141/BSO/ -``` - -This works immediately - no configuration changes needed. - -### ✅ Solution 2: Use `127.0.0.1` (Alternative) - -``` -http://127.0.0.1:3141/BSO/ -``` - -### ⚠️ Solution 3: Set Up HTTPS (Required for Network Access) - -If you need to access from another device on your network, you MUST use HTTPS: - -1. Generate a self-signed certificate: -```powershell -# Run as Administrator -New-SelfSignedCertificate -DnsName "localhost", "192.168.0.171" -CertStoreLocation "cert:\LocalMachine\My" -FriendlyName "BSO Dev Cert" -``` - -2. Configure Kestrel in `MotelyApiHost.cs` to use HTTPS - -3. Access via: `https://192.168.0.171:3141/BSO/` - -### ❌ Solution 4: Browser Flags (NOT Recommended) - -You can start Chrome with flags to allow insecure origins: -```powershell -chrome.exe --unsafely-treat-insecure-origin-as-secure=http://192.168.0.171:3141 -``` - -**Warning:** This is insecure and should only be used for development. - -## ✅ Permanent Fix Applied - -**Update:** `ApiServerWindow.cs` now automatically listens on **both** `localhost` and the configured IP address. This means: - -- ✅ Browser can use `http://localhost:3141/BSO/` (COOP/COEP headers work) -- ✅ Network access still works via `http://192.168.0.171:3141/BSO/` (for other devices) - -**After restarting Motely.TUI**, the API will be available on both URLs. Just use `localhost` in your browser! - -## Quick Fix Right Now - -**Just use `localhost` instead of the IP address:** - -``` -http://localhost:3141/BSO/ -``` - -The headers are already configured correctly - the browser just needs a trusted origin to accept them. diff --git a/BROWSER_SEARCH_FIX_PLAN.md b/BROWSER_SEARCH_FIX_PLAN.md deleted file mode 100644 index 731177f4..00000000 --- a/BROWSER_SEARCH_FIX_PLAN.md +++ /dev/null @@ -1,104 +0,0 @@ -# Browser Search Fix Plan - -## Documentation References - -**MUST READ before implementing:** - -- **Platform Abstraction Pattern**: https://docs.avaloniaui.net/docs/guides/building-cross-platform-applications/dealing-with-platforms -- **Dependency Injection**: https://docs.avaloniaui.net/docs/guides/implementation-guides/how-to-implement-dependency-injection -- **Project Architecture**: `x:\BalatroSeedOracle\docs\ARCHITECTURE.md` -- **Avalonia Best Practices**: `x:\BalatroSeedOracle\docs\AVALONIA_BEST_PRACTICES.md` - -## Key Rules from Docs - -1. **NO `#if BROWSER` or `#if !BROWSER`** in shared code - use interfaces + DI -2. **Interfaces** in Core/shared project (e.g., `IDuckDBService`, `IPlatformServices`) -3. **Platform implementations** in head projects (Desktop, Browser, iOS, Android) -4. **DI registration** via `PlatformServices.RegisterServices` at startup -5. **Runtime detection** via `IPlatformServices.SupportsFileSystem` (not compile-time) - -## Problem -`MotelySearchOrchestrator.Launch()` uses `MotelySearchDatabase` (native DuckDB) when `OutputDbPath` is provided. -This fails in Browser because DuckDB.NET doesn't support WASM - Browser needs JS interop to DuckDB-WASM. - -## Solution Architecture - -``` -Desktop: - SearchManager.StartSearchAsync() - → MotelySearchOrchestrator.Launch(config, params WITH OutputDbPath) - → Motely writes to MotelySearchDatabase (native DuckDB) - → ActiveSearchContext queries via IDuckDBService (DesktopDuckDBService) - -Browser: - SearchManager.StartSearchAsync() - → MotelySearchOrchestrator.Launch(config, params WITHOUT OutputDbPath, WITH resultCallback) - → Results come via callback - → SearchManager stores results via IDuckDBService (BrowserDuckDBService → DuckDB-WASM) - → ActiveSearchContext queries via IDuckDBService (BrowserDuckDBService) -``` - -## Files to Modify - -### 1. `src/BalatroSeedOracle/Services/SearchManager.cs` - -```csharp -// In StartSearchAsync(): - -var platformServices = ServiceHelper.GetService(); -var isBrowser = platformServices != null && !platformServices.SupportsFileSystem; - -if (isBrowser) -{ - // Browser: No OutputDbPath, use callback to store results - var duckDb = ServiceHelper.GetService(); - // Create in-memory DB via BrowserDuckDBService - // Use resultCallback to insert rows - - var searchParams = new JsonSearchParams - { - Threads = criteria.ThreadCount, - BatchSize = criteria.BatchSize, - // NO OutputDbPath! - }; - - var motelySearch = MotelySearchOrchestrator.Launch(config, searchParams, result => - { - // Store result via IDuckDBService - // This routes to BrowserDuckDBService → DuckDB-WASM - }); -} -else -{ - // Desktop: Use OutputDbPath (existing code) - var searchParams = new JsonSearchParams - { - OutputDbPath = dbPath, - // ... - }; - var motelySearch = MotelySearchOrchestrator.Launch(config, searchParams); -} -``` - -### 2. `src/BalatroSeedOracle/Services/ActiveSearchContext.cs` - -Already fixed - uses `IDuckDBService` for cross-platform DB queries. - -### 3. Browser DuckDB Table Creation - -`BrowserDuckDBService` needs to create the results table schema before search starts. -Use `MotelyRunConfig.Factory(config).Columns` to get column definitions. - -## Key Points - -1. **No `#if BROWSER`** - Use `IPlatformServices.SupportsFileSystem` at runtime -2. **One SearchManager** - Platform detection at runtime, not compile time -3. **IDuckDBService abstraction** - Already wired up correctly -4. **MotelySearchOrchestrator** - Works on all platforms, just don't pass OutputDbPath for browser - -## Testing - -1. Desktop: `dotnet build src/BalatroSeedOracle.Desktop` -2. Browser: `dotnet build src/BalatroSeedOracle.Browser` -3. Run Desktop and verify search works -4. Run Browser and verify search works (results stored in DuckDB-WASM) diff --git a/Directory.Build.props b/Directory.Build.props index a7541098..fab1aa2e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -23,6 +23,15 @@ + + + true + + enable diff --git a/HOTPATH_AUDIT_FINDINGS.md b/HOTPATH_AUDIT_FINDINGS.md deleted file mode 100644 index 5d78b5fe..00000000 --- a/HOTPATH_AUDIT_FINDINGS.md +++ /dev/null @@ -1,54 +0,0 @@ -# MotelyJson Filter SIMD Hotpath Audit - COMPLETED - -## ✅ All Fixes Applied - -### 1. MotelyJsonVoucherFilterDesc ✅ -- Removed debug logging from CreateFilter -- Removed bounds checking from hotpath (trust config) - -### 2. MotelyJsonScoring.CheckSoulJokerForSeed ✅ -- Removed recalculation of `minAnte`, `maxAnte` - now parameters -- Removed recalculation of `maxPackSlots` - use precomputed `maxPackSlotsPerAnte` -- Removed unnecessary ante-needed check (loop starts at precomputed `minAnte`) -- All call sites updated to pass precomputed values - -### 3. MotelyJsonPlayingCardFilterDesc ✅ -- Removed null check on `EffectiveAntes` in CreateFilter -- Removed null coalescing (`?? Array.Empty()`) in hotpath - -### 4. MotelyJsonTagFilterDesc ✅ -- Optimized ante checking logic (removed redundant array search) -- Removed null check on `EffectiveAntes` in early exit loop - -### 5. All Other Filters ✅ -- Joker, Tarot, Spectral, Planet, Event, Boss: All clean - no unnecessary hotpath waste found -- Any remaining null checks are on optional config fields (Sources, Tags, etc.) which is correct - ---- - -## Philosophy Locked In - -**Trust config at parse time. Hotpaths are pure logic.** - -- ✅ Zero defensive checks on precomputed fields -- ✅ Zero recalculations of precomputed values -- ✅ Zero null coalescing in SIMD paths -- ✅ Parameters pass all necessary precomputed values - ---- - -## Build Status - -✅ **All files compile successfully** - Ready for deployment - ---- - -## Files Changed - -1. `MotelyJsonVoucherFilterDesc.cs` - Removed asserts and debug logging -2. `MotelyJsonScoring.cs` - Removed hotpath recalculations, updated signature -3. `MotelyJsonSoulJokerFilterDesc.cs` - Updated call site -4. `MotelyJsonSeedScoreDesc.cs` - Updated call sites (2 locations) -5. `MotelyJsonPlayingCardFilterDesc.cs` - Removed null checks -6. `MotelyJsonTagFilterDesc.cs` - Removed null checks in hotpath - diff --git a/ITEM_CONFIG_UX_PRD.md b/ITEM_CONFIG_UX_PRD.md deleted file mode 100644 index 7573146a..00000000 --- a/ITEM_CONFIG_UX_PRD.md +++ /dev/null @@ -1,59 +0,0 @@ -# Item Configuration UI Redesign - PRD - -## Problem Statement -The current ItemConfigPopup (388 lines) that appears when right-clicking dropped items is: -- ❌ Way too big - can't see all elements -- ❌ Not UX friendly -- ❌ Not user-friendly -- ❌ Takes up too much screen space - -## Current Implementation -**File**: `src/Controls/ItemConfigPopup.axaml` (388 lines) -**Triggered**: Right-click on item in MUST/SHOULD drop zones -**Purpose**: Configure item properties (edition, stickers, seals, antes, score, label, min count) - -## Requirements - -### Must Configure: -1. **Edition** (Jokers/Standard Cards): None, Foil, Holographic, Polychrome, Negative -2. **Stickers** (Jokers): Eternal, Perishable, Rental -3. **Seals** (Standard Cards): None, Purple, Gold, Red, Blue -4. **Antes**: Checkboxes for antes 1-8 -5. **Score**: Numeric input -6. **Label**: Text input (optional friendly name) -7. **Min Count**: For "at least N" requirements - -### Design Goals: -- ✅ **Compact**: Fit everything in a small, scannable area -- ✅ **Clear**: User can see all options without scrolling -- ✅ **Fast**: Quick to configure and close -- ✅ **Contextual**: Only show relevant options (e.g., seals only for playing cards) -- ✅ **Balatro themed**: Match game visual style - -### Proposed Improvements: -1. **Two-column layout** instead of vertical stack -2. **Smaller controls** (compact checkboxes, smaller text) -3. **Grouped sections** with clear visual separation -4. **Smart visibility** (hide irrelevant options based on item type) -5. **Quick actions** row at bottom (Apply, Cancel, Delete) - -### Alternative: Use ItemConfigPanel? -There's a shorter version at `src/Components/ItemConfigPanel.axaml` (226 lines). -- Check if it's better designed -- If yes, switch to using Panel instead of Popup -- If no, redesign Popup to be more compact - -## Success Criteria -- [ ] All config options visible without scrolling -- [ ] Popup size reduced by at least 30% -- [ ] User can configure item in < 5 seconds -- [ ] Matches Balatro visual theme -- [ ] Works for all item types (Jokers, Playing Cards, Vouchers, etc.) - -## Files to Modify -- `src/Controls/ItemConfigPopup.axaml` (redesign) -- `src/ViewModels/ItemConfigPopupViewModel.cs` (verify logic) -- OR switch to `src/Components/ItemConfigPanel.axaml` if it's better - -## Context -This is the "hard part" of finishing the MVP - item configuration is critical for creating filters but the current UI is frustrating to use. User wants a complete redesign that makes sense. diff --git a/JAML_EDITOR_ENHANCEMENTS.md b/JAML_EDITOR_ENHANCEMENTS.md deleted file mode 100644 index aad7323a..00000000 --- a/JAML_EDITOR_ENHANCEMENTS.md +++ /dev/null @@ -1,577 +0,0 @@ -# JAML Editor Enhancement Plan -## Making It REAL...SOLID...GREAT...COZY 🎯 - -Based on research into modern code editor best practices and YAML editor features, here's a comprehensive enhancement plan to make the JAML editor world-class. - ---- - -## 🎨 Visual Enhancements - -### 1. **Error Squiggles & Underlines** ⚠️ -**Priority: HIGH** - -- Red squiggly underlines for syntax errors -- Yellow warnings for schema violations -- Blue info for suggestions -- Click error → jump to problem - -**Implementation:** -```csharp -// Use AvaloniaEdit's TextMarkerService -var markerService = new TextMarkerService(_jamlEditor.Document); -_jamlEditor.TextArea.TextView.BackgroundRenderers.Add(markerService); - -// Mark errors -var marker = markerService.Create(startOffset, length); -marker.MarkerColor = Colors.Red; -marker.MarkerTypes = TextMarkerTypes.SquigglyUnderline; -``` - -**Benefits:** -- Immediate visual feedback -- No need to check status bar -- Professional editor feel - ---- - -### 2. **Hover Tooltips** 💡 -**Priority: HIGH** - -- Hover over property → show description from schema -- Hover over joker → show joker info (rarity, description) -- Hover over anchor → show anchor definition -- Hover over error → show error details - -**Implementation:** -```csharp -_jamlEditor.TextArea.TextView.MouseHover += (s, e) => { - var position = _jamlEditor.GetPositionFromPoint(e.GetPosition(_jamlEditor)); - if (position.HasValue) { - var word = GetWordAtPosition(position.Value); - var tooltip = GetTooltipForWord(word); - ShowTooltip(tooltip, e.GetPosition(_jamlEditor)); - } -}; -``` - -**Tooltip Content:** -- **Property**: Description from `jaml.schema.json` -- **Joker**: Name, rarity, description from `BalatroData` -- **Anchor**: Full definition preview -- **Error**: Error message + fix suggestion - ---- - -### 3. **Current Line Highlighting** ✨ -**Priority: MEDIUM** - -- Subtle background highlight on current line -- Makes it easy to see where you are - -**Implementation:** -```csharp -_jamlEditor.Options.HighlightCurrentLine = true; -_jamlEditor.TextArea.IndentationSize = 2; -``` - ---- - -### 4. **Bracket Matching** 🔗 -**Priority: MEDIUM** - -- Highlight matching brackets `[]`, `{}` -- Highlight when cursor is on bracket -- Visual indicator for nested structures - -**Implementation:** -```csharp -_jamlEditor.TextArea.TextView.BracketMatchingBrush = Brushes.Yellow; -_jamlEditor.Options.EnableBracketMatching = true; -``` - ---- - -### 5. **Minimap** 🗺️ -**Priority: LOW** - -- Small overview of entire file on right side -- Click to jump to section -- Shows structure at a glance - -**Note**: AvaloniaEdit may need custom implementation or third-party control. - ---- - -## 🧭 Navigation Features - -### 6. **Go to Definition** 🎯 -**Priority: HIGH** - -- Ctrl+Click on anchor reference (`*anchor_name`) → jump to definition -- Ctrl+Click on property → jump to schema definition -- Right-click → "Go to Definition" - -**Implementation:** -```csharp -_jamlEditor.TextArea.TextView.MouseDown += (s, e) => { - if (e.KeyModifiers.HasFlag(KeyModifiers.Control)) { - var position = GetPositionFromPoint(e.GetPosition(_jamlEditor)); - var word = GetWordAtPosition(position); - if (IsAnchorReference(word)) { - var definition = FindAnchorDefinition(word); - JumpToPosition(definition); - } - } -}; -``` - ---- - -### 7. **Find All References** 🔍 -**Priority: MEDIUM** - -- Right-click anchor definition → "Find All References" -- Shows all places where anchor is used -- Click result → jump to reference - -**Implementation:** -- Parse document for all `*anchor_name` references -- Show in popup/panel -- Highlight in editor - ---- - -### 8. **Anchor Navigation Panel** 📋 -**Priority: MEDIUM** - -- Sidebar showing all anchors defined in file -- Click anchor → jump to definition -- Shows anchor name + preview -- Color-coded: defined vs referenced - ---- - -## 🛠️ Editing Features - -### 9. **Format on Save** 🎨 -**Priority: MEDIUM** - -- Auto-format YAML on save (Ctrl+S) -- Consistent indentation (2 spaces) -- Sort properties (optional) -- Clean up whitespace - -**Implementation:** -```csharp -private void FormatJaml() -{ - var deserializer = new DeserializerBuilder().Build(); - var serializer = new SerializerBuilder() - .WithIndentedSequences() - .WithIndentation(2, 2) - .Build(); - - try { - var obj = deserializer.Deserialize(JamlContent); - JamlContent = serializer.Serialize(obj); - } catch { /* Invalid YAML */ } -} -``` - ---- - -### 10. **Smart Indentation** 📏 -**Priority: MEDIUM** - -- Auto-indent on Enter -- Maintain indentation level -- Smart dedent for `-` list items -- Tab/Shift+Tab for indent/dedent - -**Implementation:** -```csharp -_jamlEditor.Options.IndentationSize = 2; -_jamlEditor.Options.ConvertTabsToSpaces = true; -_jamlEditor.TextArea.IndentationStrategy = new DefaultIndentationStrategy(); -``` - ---- - -### 11. **Code Snippets** 📝 -**Priority: HIGH** - -- Type `joker` → Tab → expands to full joker clause -- Type `anchor` → Tab → expands to anchor definition -- Type `and` → Tab → expands to And clause template - -**Snippets:** -- `joker` → `joker: Blueprint\nantes: [1, 2]\nscore: 10` -- `anchor` → `name: &name value` -- `and` → `And:\n Antes: [1, 2]\n Mode: Max\n clauses:` -- `or` → `Or:\n - joker: Blueprint` -- `cluster` → Full cluster pattern with anchors - -**Implementation:** -```csharp -_jamlEditor.TextArea.TextEntered += (s, e) => { - if (e.Text == "\t" || e.Text == " ") { - var word = GetCurrentWord(); - var snippet = GetSnippetForWord(word); - if (snippet != null) { - InsertSnippet(snippet); - } - } -}; -``` - ---- - -### 12. **Multi-Cursor Editing** 👆 -**Priority: LOW** - -- Alt+Click → add cursor -- Ctrl+D → select next occurrence -- Edit multiple places at once - -**Note**: May require custom implementation or AvaloniaEdit extension. - ---- - -### 13. **Word Wrap Toggle** 📄 -**Priority: LOW** - -- Toggle word wrap on/off -- Useful for long lines -- Preserves indentation - -**Implementation:** -```csharp -_jamlEditor.WordWrap = true; // Toggle button -``` - ---- - -## ✅ Validation & Errors - -### 14. **Error Panel** 📊 -**Priority: HIGH** - -- Panel at bottom showing all errors -- Click error → jump to line -- Group by severity (Error/Warning/Info) -- Count badges - -**Implementation:** -```csharp -public class ErrorPanel : UserControl -{ - public ObservableCollection Errors { get; } - - // Update on validation - private void OnValidationComplete(List errors) - { - Errors.Clear(); - foreach (var error in errors) - { - Errors.Add(new ErrorItem { - Line = error.Line, - Message = error.Message, - Severity = error.Severity - }); - } - } -} -``` - ---- - -### 15. **Real-Time Schema Validation** ✅ -**Priority: HIGH** - -- Validate against `jaml.schema.json` in real-time -- Check property names, types, enums -- Validate antes ranges (1-8) -- Validate slot ranges (0-5) - -**Implementation:** -```csharp -private void ValidateAgainstSchema(string jaml) -{ - var schema = LoadSchema("jaml.schema.json"); - var validator = new JsonSchemaValidator(schema); - var errors = validator.Validate(jaml); - - foreach (var error in errors) - { - MarkError(error.Line, error.Column, error.Message); - } -} -``` - ---- - -### 16. **Quick Fixes** 🔧 -**Priority: MEDIUM** - -- Lightbulb icon on errors -- Click → show suggested fixes -- "Add missing property" -- "Fix indentation" -- "Quote string" - -**Implementation:** -```csharp -private void ShowQuickFixes(int line, int column) -{ - var fixes = GetQuickFixesForError(line, column); - var menu = new ContextMenu(); - foreach (var fix in fixes) - { - menu.Items.Add(new MenuItem { - Header = fix.Description, - Command = new RelayCommand(() => ApplyFix(fix)) - }); - } - menu.Open(_jamlEditor); -} -``` - ---- - -## 🎯 Anchor-Specific Features - -### 17. **Anchor Visual Indicators** 🎨 -**Priority: MEDIUM** - -- Highlight anchor definitions with subtle background -- Highlight anchor references with different color -- Show connection line (optional, advanced) - -**Implementation:** -```csharp -private void HighlightAnchors() -{ - var anchors = FindAllAnchors(_jamlEditor.Text); - foreach (var anchor in anchors) - { - if (anchor.IsDefinition) - { - MarkWithColor(anchor.Range, Colors.LightBlue); - } - else - { - MarkWithColor(anchor.Range, Colors.LightGreen); - } - } -} -``` - ---- - -### 18. **Anchor Rename** ✏️ -**Priority: MEDIUM** - -- Rename anchor definition → updates all references -- F2 to rename -- Preview changes before applying - -**Implementation:** -```csharp -private void RenameAnchor(string oldName, string newName) -{ - // Find all references - var references = FindAllReferences(oldName); - - // Replace in document - foreach (var ref in references) - { - _jamlEditor.Document.Replace(ref.Offset, ref.Length, newName); - } -} -``` - ---- - -### 19. **Anchor Validation** ✅ -**Priority: MEDIUM** - -- Warn if anchor is defined but never used -- Error if anchor is referenced but not defined -- Suggest similar anchor names (typo detection) - ---- - -## 🔍 Search & Navigation - -### 20. **Advanced Search** 🔎 -**Priority: MEDIUM** - -- Ctrl+F → Find -- Ctrl+H → Replace -- Regex support -- Match case -- Whole word -- Search in selection - -**Note**: AvaloniaEdit has built-in search, but can be enhanced. - ---- - -### 21. **Go to Line** 📍 -**Priority: LOW** - -- Ctrl+G → Go to line number -- Quick navigation in large files - -**Implementation:** -```csharp -private void GoToLine(int lineNumber) -{ - var line = _jamlEditor.Document.GetLineByNumber(lineNumber); - _jamlEditor.CaretOffset = line.Offset; - _jamlEditor.TextArea.Caret.BringCaretToView(); -} -``` - ---- - -## 📚 Documentation & Help - -### 22. **Inline Documentation** 📖 -**Priority: MEDIUM** - -- Show property descriptions inline (optional) -- Toggle documentation panel -- Link to full schema docs - ---- - -### 23. **Context-Sensitive Help** ❓ -**Priority: LOW** - -- F1 on property → open help -- Link to YAML best practices -- Link to JAML examples - ---- - -## 🎨 Polish & UX - -### 24. **Undo/Redo Stack** ↩️ -**Priority: MEDIUM** - -- Better undo/redo (AvaloniaEdit has this, but can be enhanced) -- Show undo stack in menu -- Clear undo on save (optional) - ---- - -### 25. **Copy with Syntax** 📋 -**Priority: LOW** - -- Copy as formatted YAML -- Copy as JSON (converted) -- Copy as code block (for markdown) - ---- - -### 26. **Line Numbers with Errors** 🔢 -**Priority: MEDIUM** - -- Highlight line numbers with errors -- Click line number → select line -- Show error count per line - ---- - -### 27. **Status Bar Enhancements** 📊 -**Priority: LOW** - -- Show cursor position (line:column) -- Show selection length -- Show file encoding -- Show indentation mode (spaces/tabs) - ---- - -## 🚀 Performance - -### 28. **Lazy Validation** ⚡ -**Priority: MEDIUM** - -- Only validate visible portion (for large files) -- Debounce validation (already done!) -- Cache validation results - ---- - -### 29. **Incremental Parsing** 📈 -**Priority: LOW** - -- Only re-parse changed sections -- Faster for large files - ---- - -## 🎯 Priority Summary - -### **Phase 1: Core Polish** (HIGH Priority) -1. ✅ Error Squiggles & Underlines -2. ✅ Hover Tooltips -3. ✅ Go to Definition -4. ✅ Code Snippets -5. ✅ Error Panel -6. ✅ Real-Time Schema Validation - -### **Phase 2: Navigation** (MEDIUM Priority) -7. Find All References -8. Anchor Navigation Panel -9. Format on Save -10. Smart Indentation -11. Quick Fixes -12. Anchor Visual Indicators - -### **Phase 3: Advanced Features** (LOW Priority) -13. Minimap -14. Multi-Cursor Editing -15. Word Wrap Toggle -16. Advanced Search -17. Anchor Rename -18. Inline Documentation - ---- - -## 📝 Implementation Notes - -### AvaloniaEdit Capabilities -- ✅ Syntax highlighting (already done) -- ✅ Code folding (already done) -- ✅ Autocomplete (just added!) -- ✅ Line numbers (already done) -- ✅ Bracket matching (needs enabling) -- ✅ Current line highlight (needs enabling) -- ⚠️ Error markers (needs TextMarkerService) -- ⚠️ Hover tooltips (needs custom implementation) -- ⚠️ Go to definition (needs custom implementation) - -### Dependencies Needed -- **JsonSchema.Net** - For schema validation -- **YamlDotNet** - Already have it! -- **Custom Controls** - For error panel, anchor panel - ---- - -## 🎉 Expected Impact - -After implementing Phase 1: -- **Professional feel** - Error squiggles, tooltips, go-to-definition -- **Faster editing** - Code snippets, smart indentation -- **Fewer errors** - Real-time validation, error panel -- **Better navigation** - Go to definition, find references - -**Result**: A **cozy**, **productive**, **professional** JAML editor that rivals VS Code! 🚀 - ---- - -**Last Updated**: 2025-01-XX -**Status**: Research Complete - Ready for Implementation diff --git a/MONO_WASM_FIX.md b/MONO_WASM_FIX.md deleted file mode 100644 index 0478f386..00000000 --- a/MONO_WASM_FIX.md +++ /dev/null @@ -1,101 +0,0 @@ -# Mono WASM Assertion Failure Fix - -## Problem - -**Symptom**: Palindrome search (and potentially other searches) in the browser cause a Mono WASM runtime assertion failure: - -``` -[MONO] * Assertion at /__w/1/s/src/runtime/src/mono/mono/mini/mini-runtime.c:2713, condition '' not met -``` - -**Root Cause**: Mono WASM has known issues with `Barrier` synchronization primitives used in multi-threaded mode. The assertion at line 2713 in the Mono runtime's mini-runtime indicates that a disabled or unsupported threading feature was attempted. - -**Affected Code Path**: -1. User initiates palindrome search in browser -2. `MotelyWasm.SearchSeedsWithOptions()` creates a `SearchOptionsDto` with `Palindrome = true` -3. `MotelyPalindromeSeedProvider` is created to lazily generate palindrome seeds -4. `MotelySearch` spawns multiple threads (default: `Environment.ProcessorCount`) -5. Threads use `Barrier` for synchronization in the pause/unpause flow -6. **Mono WASM assertion fails** during barrier synchronization - -## Solution - -**Approach**: Force single-threaded search mode for WASM/browser builds by capping `threadCount` to 1. - -**Rationale**: -- Mono WASM threading support is incomplete, especially for advanced synchronization primitives -- Single-threaded mode eliminates all `Barrier` usage, avoiding the runtime assertion -- Performance impact is minimal in browser context (limited CPU anyway) -- All search functionality still works (just sequential instead of parallel) - -**Changes Made**: - -### File: `x:\BalatroSeedOracle\external\Motely\Motely.WASM\MotelyWasm.cs` (lines 276-279) - -**Before**: -```csharp -int threadCount = options.ThreadCount.GetValueOrDefault(); -threadCount = threadCount <= 0 ? Environment.ProcessorCount : Math.Clamp(threadCount, 1, Environment.ProcessorCount); -_searchThreadCount = threadCount; -``` - -**After**: -```csharp -int threadCount = options.ThreadCount.GetValueOrDefault(); -// WASM has issues with Barrier synchronization in multi-threaded mode (Mono WASM assertion failure) -// Force single-threaded search for WASM builds to avoid runtime assertion at mini-runtime.c:2713 -threadCount = 1; -_searchThreadCount = threadCount; -``` - -## Test Cases - -### Palindrome Search (Previously Failing) -- **Before Fix**: Crashes with Mono WASM assertion at mini-runtime.c:2713 -- **After Fix**: Completes successfully with single-threaded execution - -### Random Search -- Should work (was likely working before, now guaranteed) - -### Keyword Search -- Should work with single-threaded fallback - -### Sequential/JAML Filter Search -- Should work with single-threaded fallback - -## Trade-offs - -| Aspect | Impact | -|--------|--------| -| Correctness | ✅ Fixed - no more assertion errors | -| Performance | ⚠️ Slower (single-threaded) but still functional | -| Compatibility | ✅ Maintained - only affects browser builds | -| User Experience | ✅ Better - searches complete without crashing | - -## Alternative Approaches Considered - -1. **Disable Barriers entirely**: Requires deeper changes to `MotelySearch` architecture -2. **Use semaphores instead of Barriers**: Still likely to fail in Mono WASM -3. **WebWorker-based threading**: Would require complete architectural rewrite -4. **Upgrade Mono/.NET version**: Future option when WASM threading is more mature - -## Future Improvements - -When Mono WASM threading is more mature (or we migrate to newer .NET versions with better WASM support), we can: - -1. Re-enable multi-threaded search -2. Profile to find optimal thread count for browser -3. Use WebWorkers for true parallel execution (different from managed threads) -4. Add user-facing thread count control in UI - -## Version Impact - -- **motely-wasm v1.0.4**: Multi-threaded (broken for palindrome search) -- **motely-wasm v1.0.5**: Single-threaded (fixed, palindrome search works) -- **BalatroSeedOracle.Browser**: Should update to use motely-wasm v1.0.5+ - -## References - -- Mono WASM Threading Limitations: https://github.com/dotnet/runtime/issues -- Balatro Seed Oracle Issue: Search doesn't find seeds (Mono WASM assertion) -- Test Environment: Vercel v0 preview (browser-based WASM execution) diff --git a/TECH_DEBT_TODO.md b/TECH_DEBT_TODO.md deleted file mode 100644 index 5d13c1ee..00000000 --- a/TECH_DEBT_TODO.md +++ /dev/null @@ -1,309 +0,0 @@ -# Tech Debt TODO - Small Fixes for AI Agents - -**Total Items**: 135+ -**Status**: Ready for AI agents to grab and complete -**Priority**: Small, quick fixes that improve code quality - -## ✅ Recently Completed (January 2026) - -### AOT Compilation Refactoring -- [x] **AOT_001**: Removed all reflection from `JamlTypeAsKeyConverter` - replaced with static property mappings -- [x] **AOT_002**: Implemented `System.Text.Json` source generation via `MotelyJsonSerializerContext` -- [x] **AOT_003**: Implemented YamlDotNet static context via `MotelyYamlStaticContext` -- [x] **AOT_004**: Enabled AOT for Desktop builds (`PublishAot=true`) -- [x] **AOT_005**: Enabled AOT for Browser builds (`RunAOTCompilation=true`, `PublishTrimmed=true`) -- [x] **AOT_006**: Created `IExcelExporter` interface for platform abstraction -- [x] **AOT_007**: Implemented `ClosedXmlExcelExporter` for Desktop -- [x] **AOT_008**: Implemented `BrowserExcelExporter` stub for Browser -- [x] **AOT_009**: Removed DuckDB references from shared `BalatroSeedOracle` library -- [x] **AOT_010**: Updated all `Enum.GetValues()` calls to use generic `Enum.GetValues()` - ---- - -## 🔴 Critical: Logging Violations - -- [ ] **TECH_DEBT_001**: Replace `Console.WriteLine(response)` in `MCP/McpServerHost.cs:54` with `DebugLogger.Log()` -- [ ] **TECH_DEBT_002**: Replace `Console.WriteLine(JsonSerializer.Serialize(errorResponse))` in `MCP/McpServerHost.cs:72` with `DebugLogger.LogError()` -- [ ] **TECH_DEBT_003**: Remove `Console.WriteLine` in `App.axaml.cs:55` - replace with `DebugLogger.Log()` -- [ ] **TECH_DEBT_004**: Remove `Debug.WriteLine` calls in `Services/Storage/BrowserLocalStorageAppDataStore.cs` (lines 19, 29, 47, 61) - use `DebugLogger` instead -- [ ] **TECH_DEBT_005**: Check `Helpers/DebugLogger.cs` for any `Console.WriteLine` that should be conditional on debug mode - ---- - -## 🟠 Code Style: Null Checks - -- [ ] **TECH_DEBT_006**: Replace `== null` with `is null` in `ViewModels/FiltersModalViewModel.cs:315` (`_originalCriteriaHash == null`) -- [ ] **TECH_DEBT_007**: Replace `== null` with `is null` in `ViewModels/FiltersModalViewModel.cs:399` (`searchManager != null`) -- [ ] **TECH_DEBT_008**: Replace `== null` with `is null` in `ViewModels/FiltersModalViewModel.cs:493` (`dbFiles == null`) -- [ ] **TECH_DEBT_009**: Replace `== null` with `is null` in `ViewModels/FiltersModalViewModel.cs:679` (`LoadedConfig != null`) -- [ ] **TECH_DEBT_010**: Replace `== null` with `is null` in `ViewModels/FiltersModalViewModel.cs:718` (`LoadedConfig != null`) -- [ ] **TECH_DEBT_011**: Replace `== null` with `is null` in `ViewModels/FiltersModalViewModel.cs:739` (`config.Must != null`) -- [ ] **TECH_DEBT_012**: Replace `== null` with `is null` in `ViewModels/FiltersModalViewModel.cs:753` (`config.Should != null`) -- [ ] **TECH_DEBT_013**: Replace `== null` with `is null` in `ViewModels/FiltersModalViewModel.cs:767` (`config.MustNot != null`) -- [ ] **TECH_DEBT_014**: Replace `== null` with `is null` in `ViewModels/FiltersModalViewModel.cs:805` (`config != null`) -- [ ] **TECH_DEBT_015**: Replace `== null` with `is null` in `ViewModels/FiltersModalViewModel.cs:1013` (`config == null`) -- [ ] **TECH_DEBT_016**: Replace all `== null` / `!= null` checks in `ViewModels/FiltersModalViewModel.cs` lines 1123-1801 (scan entire file) -- [ ] **TECH_DEBT_017**: Scan all ViewModels for `== null` / `!= null` and replace with `is null` / `is not null` - ---- - -## 🟡 Async/Await Issues - -- [ ] **TECH_DEBT_018**: Check `ViewModels/FiltersModalViewModel.cs:289` `SaveCurrentFilter()` - verify it properly awaits all async calls -- [ ] **TECH_DEBT_019**: Check `ViewModels/FiltersModalViewModel.cs:381` `CleanupFilterDatabases()` - verify async pattern -- [ ] **TECH_DEBT_020**: Check `ViewModels/FiltersModalViewModel.cs:486` `DumpDatabasesToFertilizerAsync()` - verify async pattern -- [ ] **TECH_DEBT_021**: Check `ViewModels/FiltersModalViewModel.cs:573` `LoadFilter()` - verify async pattern -- [ ] **TECH_DEBT_022**: Check `ViewModels/FiltersModalViewModel.cs:687` `DeleteFilter()` - verify async pattern -- [ ] **TECH_DEBT_023**: Check `ViewModels/FiltersModalViewModel.cs:783` `ReloadVisualFromSavedFile()` - verify async pattern -- [ ] **TECH_DEBT_024**: Check `ViewModels/FiltersModalViewModel.cs:892` `RefreshSaveTabData()` - verify async pattern -- [ ] **TECH_DEBT_025**: Check `ViewModels/FiltersModalViewModel.cs:2108` `UpdateVisualBuilderFromItemConfigs()` - verify async pattern -- [ ] **TECH_DEBT_026**: Check `ViewModels/AnalyzerViewModel.cs:244` `AnalyzeCurrentSeedAsync()` - verify async pattern -- [ ] **TECH_DEBT_027**: Check all async methods in `ViewModels/FilterTabs/ValidateFilterTabViewModel.cs` - verify they properly await -- [ ] **TECH_DEBT_028**: Check all async methods in `ViewModels/FilterTabs/SaveFilterTabViewModel.cs` - verify they properly await - ---- - -## 🟢 MVVM Violations: Code-Behind Logic - -- [ ] **TECH_DEBT_029**: Move business logic from `Views/MainWindow.axaml.cs:57-64` `OnWindowClosing()` to `MainWindowViewModel` -- [ ] **TECH_DEBT_030**: Move business logic from `Views/MainWindow.axaml.cs:54` `OnWindowSizeChanged()` to `MainWindowViewModel` -- [ ] **TECH_DEBT_031**: Review `Views/BalatroMainMenu.axaml.cs` for any business logic that should be in ViewModel -- [ ] **TECH_DEBT_032**: Review all `.axaml.cs` files in `Views/` for business logic that belongs in ViewModels -- [ ] **TECH_DEBT_033**: Review all `.axaml.cs` files in `Components/` for business logic that belongs in ViewModels -- [ ] **TECH_DEBT_034**: Check `Components/FilterTabs/JamlEditorTab.axaml.cs` - ensure all logic is in `JamlEditorTabViewModel` -- [ ] **TECH_DEBT_035**: Check `Controls/SortableResultsGrid.axaml.cs` - ensure all logic is in `SortableResultsGridViewModel` - ---- - -## 🔵 Bad Comments / AI Slop - -- [ ] **TECH_DEBT_036**: Remove or improve generic XML comments like `/// ` with no actual description -- [ ] **TECH_DEBT_037**: Remove redundant comments that just repeat the code (e.g., `// Set the value` above `value = 5`) -- [ ] **TECH_DEBT_038**: Remove "AI-generated" style comments that are obvious (e.g., `// Initialize the variable`) -- [ ] **TECH_DEBT_039**: Review `ViewModels/FiltersModalViewModel.cs` for bad XML comments - improve or remove -- [ ] **TECH_DEBT_040**: Review `ViewModels/FilterTabs/SaveFilterTabViewModel.cs` for bad XML comments - improve or remove -- [ ] **TECH_DEBT_041**: Remove comments like `// BUG FIX:` - if it's fixed, the comment is unnecessary -- [ ] **TECH_DEBT_042**: Remove comments like `// TODO:` that are outdated or already implemented -- [ ] **TECH_DEBT_043**: Clean up comments in `Services/Storage/BrowserLocalStorageAppDataStore.cs` - remove obvious ones - ---- - -## 🟣 Error Handling - -- [ ] **TECH_DEBT_044**: Add proper error handling to `ViewModels/FiltersModalViewModel.cs:289` `SaveCurrentFilter()` if missing -- [ ] **TECH_DEBT_045**: Add proper error handling to `ViewModels/FiltersModalViewModel.cs:381` `CleanupFilterDatabases()` if missing -- [ ] **TECH_DEBT_046**: Check all async methods for missing try-catch blocks -- [ ] **TECH_DEBT_047**: Ensure all file I/O operations have proper error handling -- [ ] **TECH_DEBT_048**: Ensure all service calls have proper error handling -- [ ] **TECH_DEBT_049**: Check for empty catch blocks that should log errors - ---- - -## 🟤 Naming Conventions - -- [ ] **TECH_DEBT_050**: Check for methods that don't follow naming conventions (e.g., `DoSomething()` should be `DoSomethingAsync()` if async) -- [ ] **TECH_DEBT_051**: Check for private fields that don't use `_camelCase` convention -- [ ] **TECH_DEBT_052**: Check for properties that don't use `PascalCase` convention -- [ ] **TECH_DEBT_053**: Check for local variables that use inconsistent naming - ---- - -## ⚪ Code Duplication - -- [ ] **TECH_DEBT_054**: Find duplicate error handling patterns and extract to helper method -- [ ] **TECH_DEBT_055**: Find duplicate null check patterns and extract to helper method -- [ ] **TECH_DEBT_056**: Find duplicate logging patterns and standardize -- [ ] **TECH_DEBT_057**: Check `ViewModels/FiltersModalViewModel.cs` for duplicate code blocks -- [ ] **TECH_DEBT_058**: Check `ViewModels/FilterTabs/` for duplicate code blocks - ---- - -## 🔴 Unused Code - -- [ ] **TECH_DEBT_059**: Remove unused `using` statements across all files -- [ ] **TECH_DEBT_060**: Remove unused private fields -- [ ] **TECH_DEBT_061**: Remove unused private methods -- [ ] **TECH_DEBT_062**: Remove commented-out code blocks -- [ ] **TECH_DEBT_063**: Remove dead code paths - ---- - -## 🟠 Magic Numbers / Strings - -- [ ] **TECH_DEBT_064**: Replace magic numbers with named constants (e.g., `500` → `VALIDATION_DELAY_MS`) -- [ ] **TECH_DEBT_065**: Replace magic strings with constants (e.g., `"Filters"` → `MODAL_TYPE_FILTERS`) -- [ ] **TECH_DEBT_066**: Check `ViewModels/FiltersModalViewModel.cs` for magic numbers/strings -- [ ] **TECH_DEBT_067**: Check `ViewModels/FilterTabs/` for magic numbers/strings - ---- - -## 🟡 Inconsistent Patterns - -- [ ] **TECH_DEBT_068**: Standardize error logging format across all ViewModels -- [ ] **TECH_DEBT_069**: Standardize success logging format across all ViewModels -- [ ] **TECH_DEBT_070**: Standardize async method patterns (all should use `ConfigureAwait(false)` for library code) -- [ ] **TECH_DEBT_071**: Standardize command patterns (all should use `[RelayCommand]` consistently) -- [ ] **TECH_DEBT_072**: Standardize property change notification patterns - ---- - -## 🟢 Missing Documentation - -- [ ] **TECH_DEBT_073**: Add XML comments to public methods in `Services/FilterService.cs` -- [ ] **TECH_DEBT_074**: Add XML comments to public methods in `Services/SearchInstance.cs` -- [ ] **TECH_DEBT_075**: Add XML comments to public properties in ViewModels -- [ ] **TECH_DEBT_076**: Add XML comments to complex business logic methods - ---- - -## 🔵 Performance Issues - -- [ ] **TECH_DEBT_077**: Check for LINQ queries that could be optimized (e.g., `.ToList()` when not needed) -- [ ] **TECH_DEBT_078**: Check for string concatenation in loops (use `StringBuilder`) -- [ ] **TECH_DEBT_079**: Check for unnecessary object allocations in hot paths -- [ ] **TECH_DEBT_080**: Check for missing `ConfigureAwait(false)` in library code - ---- - -## 🟣 Type Safety - -- [ ] **TECH_DEBT_081**: Replace `var` with explicit types where it improves readability -- [ ] **TECH_DEBT_082**: Check for unnecessary type casts -- [ ] **TECH_DEBT_083**: Check for `as` casts that should use pattern matching -- [ ] **TECH_DEBT_084**: Check for nullable reference type warnings - ---- - -## 🟤 Platform-Specific Code - -- [ ] **TECH_DEBT_085**: Ensure all `#if BROWSER` blocks have corresponding `#else` blocks -- [ ] **TECH_DEBT_086**: Check for platform-specific code that's not properly guarded -- [ ] **TECH_DEBT_087**: Verify browser-specific code doesn't break desktop builds - ---- - -## ⚪ Resource Management - -- [ ] **TECH_DEBT_088**: Check for missing `IDisposable` implementations -- [ ] **TECH_DEBT_089**: Check for missing `using` statements for `IDisposable` objects -- [ ] **TECH_DEBT_090**: Check for event handlers that aren't unsubscribed -- [ ] **TECH_DEBT_091**: Check for memory leaks in long-running operations - ---- - -## 🔴 Validation - -- [ ] **TECH_DEBT_092**: Add input validation to all public methods -- [ ] **TECH_DEBT_093**: Add null checks for all constructor parameters -- [ ] **TECH_DEBT_094**: Add validation for file paths before I/O operations -- [ ] **TECH_DEBT_095**: Add validation for user input in ViewModels - ---- - -## 🟠 Testing - -- [ ] **TECH_DEBT_096**: Add unit tests for `Helpers/DebugLogger.cs` -- [ ] **TECH_DEBT_097**: Add unit tests for `Services/FilterService.cs` -- [ ] **TECH_DEBT_098**: Add unit tests for ViewModel commands -- [ ] **TECH_DEBT_099**: Add integration tests for filter loading/saving - ---- - -## 🟡 Miscellaneous - -- [ ] **TECH_DEBT_100**: Remove all `// TODO:` comments that are outdated -- [ ] **TECH_DEBT_101**: Remove all `// FIXME:` comments that are fixed -- [ ] **TECH_DEBT_102**: Remove all `// HACK:` comments - either fix or document why it's needed -- [ ] **TECH_DEBT_103**: Standardize file headers (copyright, license, etc.) -- [ ] **TECH_DEBT_104**: Ensure all files have consistent line endings (LF) -- [ ] **TECH_DEBT_105**: Remove trailing whitespace from all files -- [ ] **TECH_DEBT_106**: Ensure consistent indentation (spaces, not tabs) -- [ ] **TECH_DEBT_107**: Remove unused project references -- [ ] **TECH_DEBT_108**: Update outdated package versions -- [ ] **TECH_DEBT_109**: Remove duplicate resource definitions -- [ ] **TECH_DEBT_110**: Ensure all XAML files use consistent naming conventions - ---- - -## 🟢 Code Quality - -- [ ] **TECH_DEBT_111**: Reduce cyclomatic complexity in methods over 20 lines -- [ ] **TECH_DEBT_112**: Break down methods over 50 lines into smaller methods -- [ ] **TECH_DEBT_113**: Extract magic numbers to constants -- [ ] **TECH_DEBT_114**: Extract repeated patterns to helper methods -- [ ] **TECH_DEBT_115**: Improve variable names for clarity - ---- - -## 🔵 Accessibility - -- [ ] **TECH_DEBT_116**: Add proper ARIA labels to interactive elements -- [ ] **TECH_DEBT_117**: Ensure keyboard navigation works for all controls -- [ ] **TECH_DEBT_118**: Add tooltips to icon-only buttons -- [ ] **TECH_DEBT_119**: Ensure color contrast meets WCAG standards - ---- - -## 🟣 Localization - -- [ ] **TECH_DEBT_120**: Extract hardcoded strings to resource files -- [ ] **TECH_DEBT_121**: Add localization support for error messages -- [ ] **TECH_DEBT_122**: Add localization support for UI text - ---- - -## 🟤 Documentation - -- [ ] **TECH_DEBT_123**: Update README.md with latest features -- [ ] **TECH_DEBT_124**: Add code examples to complex methods -- [ ] **TECH_DEBT_125**: Document complex algorithms -- [ ] **TECH_DEBT_126**: Add architecture diagrams - ---- - -## ⚪ Security - -- [ ] **TECH_DEBT_127**: Review file path handling for path traversal vulnerabilities -- [ ] **TECH_DEBT_128**: Review user input validation for injection attacks -- [ ] **TECH_DEBT_129**: Ensure sensitive data isn't logged -- [ ] **TECH_DEBT_130**: Review authentication/authorization if applicable - ---- - -## 🔴 Final Polish - -- [ ] **TECH_DEBT_131**: Run code formatter on entire solution -- [ ] **TECH_DEBT_132**: Fix all compiler warnings -- [ ] **TECH_DEBT_133**: Fix all linter errors -- [ ] **TECH_DEBT_134**: Run static analysis tools -- [ ] **TECH_DEBT_135**: Review code coverage report - ---- - -## 📝 How to Use This List - -1. **Pick an item** - Any AI agent can grab any item -2. **Complete it** - Make the fix -3. **Mark it done** - Check the box `[x]` -4. **Commit** - Small, focused commits are best -5. **Move on** - Grab the next item - -### Priority Order (Suggested) -1. Critical logging violations (001-005) -2. Code style null checks (006-017) -3. Async/await issues (018-028) -4. MVVM violations (029-035) -5. Bad comments (036-043) -6. Then work through the rest - -### Notes -- Each item is **small and actionable** -- Most items take **5-15 minutes** to complete -- Items are **independent** - can be done in any order -- **Test after each fix** - don't break existing functionality - ---- - -**Last Updated**: 2025-01-XX -**Total Items**: 135+ -**Status**: Ready for AI agents 🚀 diff --git a/YAML_Anchors_Implementation_Plan.md b/YAML_Anchors_Implementation_Plan.md deleted file mode 100644 index c74776c9..00000000 --- a/YAML_Anchors_Implementation_Plan.md +++ /dev/null @@ -1,578 +0,0 @@ -# YAML Anchors & Aliases - Detailed Implementation Plan for Avalonia UI Visual Builder - -## Overview - -This document provides a detailed, step-by-step implementation plan for adding YAML anchors and aliases support to the Avalonia UI Visual Builder. The parser already supports anchors, so this is purely a front-end UI implementation. - -## Architecture Overview - -``` -┌─────────────────────────────────────────────────────────┐ -│ Avalonia UI Visual Builder │ -├─────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ -│ │ JAML │───▶│ Anchor │───▶│ Template │ │ -│ │ Parser │ │ Detector │ │ Panel │ │ -│ └──────────────┘ └──────────────┘ └──────────┘ │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ -│ │ Config │ │ Anchor │ │ Clause │ │ -│ │ Object │ │ Metadata │ │ Tree │ │ -│ └──────────────┘ └──────────────┘ └──────────┘ │ -│ │ -└─────────────────────────────────────────────────────────┘ -``` - -## Phase 1: Display Support (Read-Only) - -### Goal -Allow users to **view** anchors and aliases in the Visual Builder, but not edit them yet. - -### Tasks - -#### 1.1 Create Anchor Detection Service - -**File**: `src/BalatroSeedOracle/Services/YamlAnchorService.cs` - -```csharp -using System.Collections.Generic; -using YamlDotNet.RepresentationModel; - -namespace BalatroSeedOracle.Services -{ - /// - /// Service for detecting and managing YAML anchors and aliases - /// - public class YamlAnchorService - { - /// - /// Represents an anchor definition in YAML - /// - public class AnchorDefinition - { - public string Name { get; set; } = ""; - public YamlNode Node { get; set; } = null!; - public string Preview { get; set; } = ""; - public int UsageCount { get; set; } = 0; - } - - /// - /// Represents an alias reference - /// - public class AliasReference - { - public string AnchorName { get; set; } = ""; - public YamlNode Node { get; set; } = null!; - public string Path { get; set; } = ""; // YAML path like "Should[0].clauses[1]" - } - - /// - /// Parse YAML and extract all anchor definitions - /// - public static Dictionary ExtractAnchors(string jamlContent) - { - var anchors = new Dictionary(); - - try - { - var yamlStream = new YamlStream(); - using (var reader = new StringReader(jamlContent)) - { - yamlStream.Load(reader); - } - - TraverseForAnchors(yamlStream.Documents[0].RootNode, anchors, ""); - } - catch (Exception ex) - { - DebugLogger.LogError("YamlAnchorService", $"Error extracting anchors: {ex.Message}"); - } - - return anchors; - } - - /// - /// Find all alias references to a specific anchor - /// - public static List FindAliasReferences(string jamlContent, string anchorName) - { - var references = new List(); - - try - { - var yamlStream = new YamlStream(); - using (var reader = new StringReader(jamlContent)) - { - yamlStream.Load(reader); - } - - TraverseForAliases(yamlStream.Documents[0].RootNode, references, anchorName, ""); - } - catch (Exception ex) - { - DebugLogger.LogError("YamlAnchorService", $"Error finding alias references: {ex.Message}"); - } - - return references; - } - - private static void TraverseForAnchors( - YamlNode node, - Dictionary anchors, - string path) - { - if (node is YamlScalarNode scalar && scalar.Anchor != null) - { - anchors[scalar.Anchor] = new AnchorDefinition - { - Name = scalar.Anchor, - Node = scalar, - Preview = scalar.Value ?? "", - UsageCount = 0 // Will be calculated separately - }; - } - else if (node is YamlMappingNode mapping) - { - foreach (var pair in mapping.Children) - { - var childPath = string.IsNullOrEmpty(path) - ? pair.Key.ToString() - : $"{path}.{pair.Key}"; - TraverseForAnchors(pair.Value, anchors, childPath); - } - } - else if (node is YamlSequenceNode sequence) - { - for (int i = 0; i < sequence.Children.Count; i++) - { - TraverseForAnchors(sequence.Children[i], anchors, $"{path}[{i}]"); - } - } - } - - private static void TraverseForAliases( - YamlNode node, - List references, - string targetAnchorName, - string path) - { - if (node is YamlAliasNode alias && alias.Value.Anchor == targetAnchorName) - { - references.Add(new AliasReference - { - AnchorName = targetAnchorName, - Node = alias, - Path = path - }); - } - else if (node is YamlMappingNode mapping) - { - foreach (var pair in mapping.Children) - { - var childPath = string.IsNullOrEmpty(path) - ? pair.Key.ToString() - : $"{path}.{pair.Key}"; - TraverseForAliases(pair.Value, references, targetAnchorName, childPath); - } - } - else if (node is YamlSequenceNode sequence) - { - for (int i = 0; i < sequence.Children.Count; i++) - { - TraverseForAliases(sequence.Children[i], references, targetAnchorName, $"{path}[{i}]"); - } - } - } - } -} -``` - -#### 1.2 Add Template Panel to Visual Builder - -**File**: `src/BalatroSeedOracle/Components/FilterTabs/TemplatesPanel.axaml` - -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -**File**: `src/BalatroSeedOracle/ViewModels/FilterTabs/TemplatesPanelViewModel.cs` - -```csharp -using System.Collections.ObjectModel; -using BalatroSeedOracle.Services; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace BalatroSeedOracle.ViewModels.FilterTabs -{ - public partial class TemplatesPanelViewModel : ObservableObject - { - [ObservableProperty] - private ObservableCollection _templates = new(); - - [ObservableProperty] - private TemplateViewModel? _selectedTemplate; - - public void LoadTemplates(string jamlContent) - { - Templates.Clear(); - - var anchors = YamlAnchorService.ExtractAnchors(jamlContent); - - foreach (var anchor in anchors.Values) - { - // Count usage - var references = YamlAnchorService.FindAliasReferences(jamlContent, anchor.Name); - anchor.UsageCount = references.Count; - - Templates.Add(new TemplateViewModel - { - Name = anchor.Name, - Preview = anchor.Preview, - UsageCount = anchor.UsageCount - }); - } - } - } - - public partial class TemplateViewModel : ObservableObject - { - [ObservableProperty] - private string _name = ""; - - [ObservableProperty] - private string _preview = ""; - - [ObservableProperty] - private int _usageCount = 0; - } -} -``` - -#### 1.3 Integrate Template Panel into Visual Builder - -**File**: `src/BalatroSeedOracle/Components/FilterTabs/VisualBuilderTab.axaml` - -Add to the Grid layout (left sidebar): - -```xml - - - - - - - - - -``` - -**File**: `src/BalatroSeedOracle/ViewModels/FilterTabs/VisualBuilderTabViewModel.cs` - -Add property: - -```csharp -[ObservableProperty] -private TemplatesPanelViewModel _templatesPanel = new(); - -// In constructor or LoadFromParentCollections: -private void LoadAnchors() -{ - if (_parentViewModel?.JamlEditorTab is JamlEditorTabViewModel jamlVm) - { - TemplatesPanel.LoadTemplates(jamlVm.JamlContent); - } -} -``` - -#### 1.4 Show Anchor/Alias Indicators in Clause Tree - -**File**: `src/BalatroSeedOracle/ViewModels/FilterTabs/ClauseRowViewModel.cs` - -Add properties: - -```csharp -[ObservableProperty] -private bool _isAnchorDefinition = false; - -[ObservableProperty] -private string _anchorName = ""; - -[ObservableProperty] -private bool _isAliasReference = false; - -[ObservableProperty] -private string _referencedAnchorName = ""; -``` - -Update clause tree rendering to show icons for anchors/aliases. - -### Testing Checklist for Phase 1 - -- [ ] Load JAML with anchors - templates panel shows all anchors -- [ ] Load JAML with aliases - clause tree shows alias indicators -- [ ] Click anchor in template panel - highlights references in clause tree -- [ ] Click alias in clause tree - jumps to anchor definition -- [ ] Usage count shows correct number of references -- [ ] Preview shows truncated template content - -## Phase 2: Basic Editing Support - -### Goal -Allow users to **create** anchors from selections and **edit** anchor templates. - -### Tasks - -#### 2.1 Create Anchor from Selection - -**File**: `src/BalatroSeedOracle/ViewModels/FilterTabs/VisualBuilderTabViewModel.cs` - -```csharp -[RelayCommand] -private async Task CreateAnchorFromSelection() -{ - // Get selected clause(s) from visual builder - var selectedItems = GetSelectedClauses(); - if (selectedItems.Count == 0) - { - // Show error: "Please select clauses to create a template" - return; - } - - // Show dialog to name the anchor - var anchorName = await ShowAnchorNameDialog(); - if (string.IsNullOrWhiteSpace(anchorName)) - return; - - // Serialize selected clauses to YAML - var yaml = SerializeClausesToYaml(selectedItems); - - // Add anchor marker - var anchoredYaml = AddAnchorToYaml(yaml, anchorName); - - // Replace original with alias reference - var aliasYaml = $"*{anchorName}"; - - // Update JAML content - await UpdateJamlWithAnchor(anchoredYaml, aliasYaml, selectedItems); - - // Refresh template panel - LoadAnchors(); -} -``` - -#### 2.2 Edit Anchor Template - -**File**: `src/BalatroSeedOracle/ViewModels/FilterTabs/TemplatesPanelViewModel.cs` - -```csharp -[RelayCommand] -private async Task EditTemplate(TemplateViewModel template) -{ - // Load anchor definition from JAML - var anchorYaml = ExtractAnchorYaml(template.Name); - - // Open editor (reuse clause editor) - var editedYaml = await ShowTemplateEditor(anchorYaml); - - // Update all references - await UpdateAnchorDefinition(template.Name, editedYaml); - - // Refresh template panel and clause tree - LoadTemplates(_jamlContent); -} -``` - -#### 2.3 Expand Alias to Inline - -**File**: `src/BalatroSeedOracle/ViewModels/FilterTabs/VisualBuilderTabViewModel.cs` - -```csharp -[RelayCommand] -private async Task ExpandAlias(ClauseRowViewModel aliasRow) -{ - // Get anchor definition - var anchorYaml = GetAnchorDefinition(aliasRow.ReferencedAnchorName); - - // Replace alias with full clause structure - await ReplaceAliasWithInline(aliasRow, anchorYaml); - - // Refresh display - LoadAnchors(); -} -``` - -### Testing Checklist for Phase 2 - -- [ ] Create anchor from selection - works correctly -- [ ] Edit anchor - all references update -- [ ] Expand alias - replaces with inline structure -- [ ] Delete anchor - shows confirmation if in use -- [ ] Invalid anchor name - shows error -- [ ] Round-trip: Save and reload preserves anchors - -## Phase 3: Advanced Features - -### Goal -Support parameterized templates with merge keys and template library. - -### Tasks - -#### 3.1 Parameterized Templates (Merge Keys) - -**File**: `src/BalatroSeedOracle/Services/YamlAnchorService.cs` - -Add method to detect merge keys: - -```csharp -public static bool IsMergeKey(YamlNode node) -{ - if (node is YamlMappingNode mapping) - { - return mapping.Children.Any(pair => - pair.Key is YamlScalarNode key && - key.Value == "<<" && - pair.Value is YamlAliasNode); - } - return false; -} -``` - -#### 3.2 Template Library - -Create template library system: -- Pre-defined templates users can insert -- Common joker combinations -- Standard ante patterns - -#### 3.3 Template Validation - -- Validate anchor structure -- Check for circular references -- Warn about invalid templates - -### Testing Checklist for Phase 3 - -- [ ] Parameter override works with merge keys -- [ ] Template library shows available templates -- [ ] Insert template from library works -- [ ] Circular reference detected -- [ ] Template structure validation works - -## Integration Points - -### Files to Modify - -1. **VisualBuilderTabViewModel.cs** - - Add `TemplatesPanel` property - - Add `LoadAnchors()` method - - Add anchor creation/editing commands - -2. **VisualBuilderTab.axaml** - - Add Templates Panel to layout - - Add anchor/alias indicators to clause tree - -3. **JamlEditorTabViewModel.cs** - - Add anchor detection when loading JAML - - Preserve anchors when saving - -4. **ClauseRowViewModel.cs** - - Add anchor/alias properties - - Add visual indicators - -### New Files to Create - -1. `Services/YamlAnchorService.cs` - Anchor detection service -2. `ViewModels/FilterTabs/TemplatesPanelViewModel.cs` - Template panel ViewModel -3. `Components/FilterTabs/TemplatesPanel.axaml` - Template panel UI - -## Timeline Estimate - -- **Phase 1**: 3-5 days (read-only display) -- **Phase 2**: 5-7 days (basic editing) -- **Phase 3**: 7-10 days (advanced features) - -**Total**: 15-22 days for full implementation - -## Dependencies - -- ✅ YamlDotNet (already in use) -- ✅ Parser support (already working) -- ⚠️ JamlFormatter anchor preservation (may need parser team help) - -## Success Criteria - -1. Users can view all anchors in template panel -2. Users can see alias references in clause tree -3. Users can create anchors from selections -4. Users can edit anchor templates -5. Users can expand aliases to inline -6. Round-trip preservation works (save/reload) - ---- - -**Document Version**: 1.0 -**Last Updated**: 2025-01-XX -**For**: Avalonia UI Visual Builder Implementation diff --git a/publish_log.txt b/publish_log.txt deleted file mode 100644 index 3067188b..00000000 --- a/publish_log.txt +++ /dev/null @@ -1,11 +0,0 @@ - Determining projects to restore... - Restored X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj (in 320 ms). - 3 of 4 projects are up-to-date for restore. - Motely -> X:\BalatroSeedOracle\external\Motely\Motely\bin\Release\net10.0\Motely.dll - Motely.Orchestration -> X:\BalatroSeedOracle\external\Motely\Motely.Orchestration\bin\Release\net10.0\Motely.Orchestration.dll -X:\BalatroSeedOracle\src\BalatroSeedOracle\Controls/SortableResultsGrid.axaml(115,10,115,10): Avalonia error AVLN2000: Unable to resolve suitable regular or attached property CanUserReorderColumns on type Avalonia.Controls.TreeDataGrid:Avalonia.Controls.TreeDataGrid Line 115, position 10. [X:\BalatroSeedOracle\src\BalatroSeedOracle\BalatroSeedOracle.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle\Controls/SortableResultsGrid.axaml(115,10,115,10): Avalonia error AVLN2000: Unable to resolve suitable regular or attached property GridLinesVisibility on type Avalonia.Controls.TreeDataGrid:Avalonia.Controls.TreeDataGrid Line 115, position 10. [X:\BalatroSeedOracle\src\BalatroSeedOracle\BalatroSeedOracle.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle\Controls/SortableResultsGrid.axaml(115,10,115,10): Avalonia error AVLN2000: Unable to resolve suitable regular or attached property HeadersVisibility on type Avalonia.Controls.TreeDataGrid:Avalonia.Controls.TreeDataGrid Line 115, position 10. [X:\BalatroSeedOracle\src\BalatroSeedOracle\BalatroSeedOracle.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle\Controls/SortableResultsGrid.axaml(115,10,115,10): Avalonia error AVLN2000: Unable to resolve suitable regular or attached property SelectionMode on type Avalonia.Controls.TreeDataGrid:Avalonia.Controls.TreeDataGrid Line 115, position 10. [X:\BalatroSeedOracle\src\BalatroSeedOracle\BalatroSeedOracle.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle\Controls/SortableResultsGrid.axaml(115,10,115,10): Avalonia error AVLN2000: Unable to resolve suitable regular or attached property RowBackground on type Avalonia.Controls.TreeDataGrid:Avalonia.Controls.TreeDataGrid Line 115, position 10. [X:\BalatroSeedOracle\src\BalatroSeedOracle\BalatroSeedOracle.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle\Controls/SortableResultsGrid.axaml(117,36,117,36): Avalonia error AVLN2000: Unable to resolve property or method of name 'TreeDataGridSource' on type 'BalatroSeedOracle.ViewModels.Controls.SortableResultsGridViewModel'. Line 117, position 36. [X:\BalatroSeedOracle\src\BalatroSeedOracle\BalatroSeedOracle.csproj] diff --git a/publish_retry.txt b/publish_retry.txt deleted file mode 100644 index 4c545f80..00000000 --- a/publish_retry.txt +++ /dev/null @@ -1,36 +0,0 @@ - Determining projects to restore... - All projects are up-to-date for restore. - Motely -> X:\BalatroSeedOracle\external\Motely\Motely\bin\Release\net10.0\Motely.dll - Motely.Orchestration -> X:\BalatroSeedOracle\external\Motely\Motely.Orchestration\bin\Release\net10.0\Motely.Orchestration.dll - BalatroSeedOracle -> X:\BalatroSeedOracle\src\BalatroSeedOracle\bin\Release\net10.0\BalatroSeedOracle.dll -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\MotelyJsonConfig.cs(1854,83): warning IL3050: Using member 'System.Enum.GetValues(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. It might not be possible to create an array of the enum type at runtime. Use the GetValues overload or the GetValuesAsUnderlyingType method instead. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\MotelyJsonConfig.cs(1848,79): warning IL3050: Using member 'System.Enum.GetValues(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. It might not be possible to create an array of the enum type at runtime. Use the GetValues overload or the GetValuesAsUnderlyingType method instead. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\JamlTypeAsKeyConverter.cs(44,20): warning IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties', 'DynamicallyAccessedMemberTypes.NonPublicProperties' in call to 'System.Type.GetProperty(String, BindingFlags)'. The parameter 'type' of method 'Motely.Filters.MotelyJson.JamlTypeAsKeyNodeDeserializer.FindPropertyWithAlias(Type, String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\JamlFormatter.cs(100,26): warning IL3050: Using member 'YamlDotNet.Serialization.SerializerBuilder.SerializerBuilder()' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. This builder configures the serializer to use reflection which is not compatible with ahead-of-time compilation or assembly trimming. You need to use the code generator/analyzer to generate static code and use the 'StaticSerializerBuilder' object instead of this one. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\MotelyRunSeedScoreDesc.cs(26,26): warning IL2026: Using member 'System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\MotelyRunSeedScoreDesc.cs(26,26): warning IL3050: Using member 'System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\MotelyRunConfig.Serialization.cs(20,16): warning IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\MotelyRunConfig.Serialization.cs(20,16): warning IL3050: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\JamlFormatter.cs(137,28): warning IL3050: Using member 'YamlDotNet.Serialization.DeserializerBuilder.DeserializerBuilder()' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. This builder configures the deserializer to use reflection which is not compatible with ahead-of-time compilation or assembly trimming. You need to use the code generator/analyzer to generate static code and use the 'StaticDeserializerBuilder' object instead of this one. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\MotelyJsonConfig.cs(1084,38): warning IL2026: Using member 'System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\MotelyJsonConfig.cs(1084,38): warning IL3050: Using member 'System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\MotelyJsonConfig.cs(1658,16): warning IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\MotelyJsonConfig.cs(1658,16): warning IL3050: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely.DB\SeedSourceProvider.cs(74,32): warning CA1416: This call site is reachable on: 'browser' 1.0 and later. 'Console.ReadLine()' is unsupported on: 'browser' all versions. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416) [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\JamlTypeAsKeyConverter.cs(206,27): warning IL2067: 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicParameterlessConstructor' in call to 'System.Activator.CreateInstance(Type)'. The parameter 'expectedType' of method 'Motely.Filters.MotelyJson.JamlTypeAsKeyNodeDeserializer.Deserialize(IParser, Type, Func, out Object, ObjectDeserializer)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely\filters\MotelyJson\JamlTypeAsKeyConverter.cs(239,39): warning IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties', 'DynamicallyAccessedMemberTypes.NonPublicProperties' in call to 'System.Type.GetProperty(String, BindingFlags)'. The return value of method 'System.Object.GetType()' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. [X:\BalatroSeedOracle\external\Motely\Motely\Motely.csproj::TargetFramework=net10.0-browser] - Motely -> X:\BalatroSeedOracle\external\Motely\Motely\bin\Release\net10.0-browser\Motely.dll -X:\BalatroSeedOracle\external\Motely\Motely.Orchestration\Executors\JsonSearchExecutor.cs(174,44): warning CS0219: The variable 'cancelHandler' is assigned but its value is never used [X:\BalatroSeedOracle\external\Motely\Motely.Orchestration\Motely.Orchestration.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely.Orchestration\Executors\JsonSearchExecutor.cs(560,36): warning CA1416: This call site is reachable on: 'browser' 1.0 and later. 'Console.ReadLine()' is unsupported on: 'browser' all versions. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416) [X:\BalatroSeedOracle\external\Motely\Motely.Orchestration\Motely.Orchestration.csproj::TargetFramework=net10.0-browser] -X:\BalatroSeedOracle\external\Motely\Motely.Orchestration\Executors\JsonSearchExecutor.cs(444,36): warning CA1416: This call site is reachable on: 'browser' 1.0 and later. 'Console.ReadLine()' is unsupported on: 'browser' all versions. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416) [X:\BalatroSeedOracle\external\Motely\Motely.Orchestration\Motely.Orchestration.csproj::TargetFramework=net10.0-browser] - Motely.Orchestration -> X:\BalatroSeedOracle\external\Motely\Motely.Orchestration\bin\Release\net10.0-browser\Motely.Orchestration.dll -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserLocalStorageAppDataStore.cs(55,20): error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task' to 'System.Threading.Tasks.ValueTask' [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserLocalStorageAppDataStore.cs(60,20): error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task' to 'System.Threading.Tasks.ValueTask' [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserLocalStorageAppDataStore.cs(98,20): error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task' to 'System.Threading.Tasks.ValueTask' [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserLocalStorageAppDataStore.cs(130,20): error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task>' to 'System.Threading.Tasks.ValueTask>' [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserLocalStorageAppDataStore.cs(135,20): error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task>' to 'System.Threading.Tasks.ValueTask>' [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Program.cs(34,56): error CS0234: The type or namespace name 'SoundFlowAudioManager' does not exist in the namespace 'BalatroSeedOracle.Services' (are you missing an assembly reference?) [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserApiHostService.cs(17,32): warning CS0067: The event 'BrowserApiHostService.StatusChanged' is never used [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserDuckDBAppender.cs(69,20): warning IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserDuckDBConnection.cs(52,20): warning IL2026: Using member 'System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserDuckDBConnection.cs(34,20): warning IL2026: Using member 'System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] diff --git a/publish_retry_2.txt b/publish_retry_2.txt deleted file mode 100644 index 4e1b5589..00000000 --- a/publish_retry_2.txt +++ /dev/null @@ -1,23 +0,0 @@ - Determining projects to restore... - All projects are up-to-date for restore. - Motely -> X:\BalatroSeedOracle\external\Motely\Motely\bin\Release\net10.0\Motely.dll - Motely.Orchestration -> X:\BalatroSeedOracle\external\Motely\Motely.Orchestration\bin\Release\net10.0\Motely.Orchestration.dll - BalatroSeedOracle -> X:\BalatroSeedOracle\src\BalatroSeedOracle\bin\Release\net10.0\BalatroSeedOracle.dll - Motely -> X:\BalatroSeedOracle\external\Motely\Motely\bin\Release\net10.0-browser\Motely.dll - Motely.Orchestration -> X:\BalatroSeedOracle\external\Motely\Motely.Orchestration\bin\Release\net10.0-browser\Motely.Orchestration.dll -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserLocalStorageAppDataStore.cs(55,20): error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task' to 'System.Threading.Tasks.ValueTask' [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserLocalStorageAppDataStore.cs(60,20): error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task' to 'System.Threading.Tasks.ValueTask' [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserLocalStorageAppDataStore.cs(98,20): error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task' to 'System.Threading.Tasks.ValueTask' [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserLocalStorageAppDataStore.cs(130,20): error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task>' to 'System.Threading.Tasks.ValueTask>' [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserLocalStorageAppDataStore.cs(135,20): error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task>' to 'System.Threading.Tasks.ValueTask>' [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserApiHostService.cs(17,32): warning CS0067: The event 'BrowserApiHostService.StatusChanged' is never used [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserDuckDBAppender.cs(69,20): warning IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserDuckDBConnection.cs(34,20): warning IL2026: Using member 'System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Services\BrowserDuckDBConnection.cs(52,20): warning IL2026: Using member 'System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\Program.cs(38,16): warning CA1416: This call site is reachable on all platforms. 'BrowserAppBuilder.StartBrowserAppAsync(AppBuilder, string, BrowserPlatformOptions?)' is only supported on: 'browser'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416) [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle\Services\SoundFlowAudioManager.cs(728,35): warning CA1416: This call site is reachable on all platforms. 'JSObject.GetPropertyAsDouble(string)' is only supported on: 'browser'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416) [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle\Services\SoundFlowAudioManager.cs(726,34): warning CA1416: This call site is reachable on all platforms. 'JSObject.GetPropertyAsDouble(string)' is only supported on: 'browser'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416) [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle\Services\SoundFlowAudioManager.cs(723,34): warning CA1416: This call site is reachable on all platforms. 'JSObject.GetPropertyAsDouble(string)' is only supported on: 'browser'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416) [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle\Services\SoundFlowAudioManager.cs(724,35): warning CA1416: This call site is reachable on all platforms. 'JSObject.GetPropertyAsDouble(string)' is only supported on: 'browser'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416) [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle\Services\SoundFlowAudioManager.cs(725,33): warning CA1416: This call site is reachable on all platforms. 'JSObject.GetPropertyAsDouble(string)' is only supported on: 'browser'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416) [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] -X:\BalatroSeedOracle\src\BalatroSeedOracle\Services\SoundFlowAudioManager.cs(727,34): warning CA1416: This call site is reachable on all platforms. 'JSObject.GetPropertyAsDouble(string)' is only supported on: 'browser'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416) [X:\BalatroSeedOracle\src\BalatroSeedOracle.Browser\BalatroSeedOracle.Browser.csproj] diff --git a/src/BalatroSeedOracle/# Browser Search Fix Plan.md b/src/BalatroSeedOracle/# Browser Search Fix Plan.md deleted file mode 100644 index 731177f4..00000000 --- a/src/BalatroSeedOracle/# Browser Search Fix Plan.md +++ /dev/null @@ -1,104 +0,0 @@ -# Browser Search Fix Plan - -## Documentation References - -**MUST READ before implementing:** - -- **Platform Abstraction Pattern**: https://docs.avaloniaui.net/docs/guides/building-cross-platform-applications/dealing-with-platforms -- **Dependency Injection**: https://docs.avaloniaui.net/docs/guides/implementation-guides/how-to-implement-dependency-injection -- **Project Architecture**: `x:\BalatroSeedOracle\docs\ARCHITECTURE.md` -- **Avalonia Best Practices**: `x:\BalatroSeedOracle\docs\AVALONIA_BEST_PRACTICES.md` - -## Key Rules from Docs - -1. **NO `#if BROWSER` or `#if !BROWSER`** in shared code - use interfaces + DI -2. **Interfaces** in Core/shared project (e.g., `IDuckDBService`, `IPlatformServices`) -3. **Platform implementations** in head projects (Desktop, Browser, iOS, Android) -4. **DI registration** via `PlatformServices.RegisterServices` at startup -5. **Runtime detection** via `IPlatformServices.SupportsFileSystem` (not compile-time) - -## Problem -`MotelySearchOrchestrator.Launch()` uses `MotelySearchDatabase` (native DuckDB) when `OutputDbPath` is provided. -This fails in Browser because DuckDB.NET doesn't support WASM - Browser needs JS interop to DuckDB-WASM. - -## Solution Architecture - -``` -Desktop: - SearchManager.StartSearchAsync() - → MotelySearchOrchestrator.Launch(config, params WITH OutputDbPath) - → Motely writes to MotelySearchDatabase (native DuckDB) - → ActiveSearchContext queries via IDuckDBService (DesktopDuckDBService) - -Browser: - SearchManager.StartSearchAsync() - → MotelySearchOrchestrator.Launch(config, params WITHOUT OutputDbPath, WITH resultCallback) - → Results come via callback - → SearchManager stores results via IDuckDBService (BrowserDuckDBService → DuckDB-WASM) - → ActiveSearchContext queries via IDuckDBService (BrowserDuckDBService) -``` - -## Files to Modify - -### 1. `src/BalatroSeedOracle/Services/SearchManager.cs` - -```csharp -// In StartSearchAsync(): - -var platformServices = ServiceHelper.GetService(); -var isBrowser = platformServices != null && !platformServices.SupportsFileSystem; - -if (isBrowser) -{ - // Browser: No OutputDbPath, use callback to store results - var duckDb = ServiceHelper.GetService(); - // Create in-memory DB via BrowserDuckDBService - // Use resultCallback to insert rows - - var searchParams = new JsonSearchParams - { - Threads = criteria.ThreadCount, - BatchSize = criteria.BatchSize, - // NO OutputDbPath! - }; - - var motelySearch = MotelySearchOrchestrator.Launch(config, searchParams, result => - { - // Store result via IDuckDBService - // This routes to BrowserDuckDBService → DuckDB-WASM - }); -} -else -{ - // Desktop: Use OutputDbPath (existing code) - var searchParams = new JsonSearchParams - { - OutputDbPath = dbPath, - // ... - }; - var motelySearch = MotelySearchOrchestrator.Launch(config, searchParams); -} -``` - -### 2. `src/BalatroSeedOracle/Services/ActiveSearchContext.cs` - -Already fixed - uses `IDuckDBService` for cross-platform DB queries. - -### 3. Browser DuckDB Table Creation - -`BrowserDuckDBService` needs to create the results table schema before search starts. -Use `MotelyRunConfig.Factory(config).Columns` to get column definitions. - -## Key Points - -1. **No `#if BROWSER`** - Use `IPlatformServices.SupportsFileSystem` at runtime -2. **One SearchManager** - Platform detection at runtime, not compile time -3. **IDuckDBService abstraction** - Already wired up correctly -4. **MotelySearchOrchestrator** - Works on all platforms, just don't pass OutputDbPath for browser - -## Testing - -1. Desktop: `dotnet build src/BalatroSeedOracle.Desktop` -2. Browser: `dotnet build src/BalatroSeedOracle.Browser` -3. Run Desktop and verify search works -4. Run Browser and verify search works (results stored in DuckDB-WASM) diff --git a/src/BalatroSeedOracle/BalatroSeedOracle.csproj b/src/BalatroSeedOracle/BalatroSeedOracle.csproj index 50395412..39e45948 100644 --- a/src/BalatroSeedOracle/BalatroSeedOracle.csproj +++ b/src/BalatroSeedOracle/BalatroSeedOracle.csproj @@ -2,7 +2,6 @@ net10.0 - true BalatroSeedOracle diff --git a/src/BalatroSeedOracle/Extensions/ServiceCollectionExtensions.cs b/src/BalatroSeedOracle/Extensions/ServiceCollectionExtensions.cs index cf0e8d2b..49a453a1 100644 --- a/src/BalatroSeedOracle/Extensions/ServiceCollectionExtensions.cs +++ b/src/BalatroSeedOracle/Extensions/ServiceCollectionExtensions.cs @@ -99,7 +99,15 @@ this IServiceCollection services sp.GetRequiredService(), sp.GetService() )); - services.AddSingleton(); + services.AddSingleton(sp => new SearchModalViewModel( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetService(), + sp.GetService() + )); services.AddTransient(sp => new Views.Modals.SearchModal( sp.GetRequiredService() )); diff --git a/src/BalatroSeedOracle/ViewModels/BalatroMainMenuViewModel.cs b/src/BalatroSeedOracle/ViewModels/BalatroMainMenuViewModel.cs index ef622d86..fed190d7 100644 --- a/src/BalatroSeedOracle/ViewModels/BalatroMainMenuViewModel.cs +++ b/src/BalatroSeedOracle/ViewModels/BalatroMainMenuViewModel.cs @@ -407,15 +407,8 @@ private void SeedSearch() } catch (Exception ex) { - IsModalVisible = false; ActiveModal = null; - DebugLogger.LogError( - "BalatroMainMenuViewModel", - $"Failed to open search modal: {ex}" - ); - ShowErrorModal( - $"Failed to open Search Modal:\n\n{ex.Message}\n\nPlease check the logs for details." - ); + HandleModalOpenError("search", "Search", ex); } } @@ -464,14 +457,7 @@ private void Editor() } catch (Exception ex) { - IsModalVisible = false; - DebugLogger.LogError( - "BalatroMainMenuViewModel", - $"Failed to open filters modal: {ex}" - ); - ShowErrorModal( - $"Failed to open Designer Modal:\n\n{ex.Message}\n\nPlease check the logs for details." - ); + HandleModalOpenError("filters", "Designer", ex); } } @@ -487,14 +473,7 @@ private void Analyze() } catch (Exception ex) { - IsModalVisible = false; - DebugLogger.LogError( - "BalatroMainMenuViewModel", - $"Failed to open analyze modal: {ex}" - ); - ShowErrorModal( - $"Failed to open Analyzer Modal:\n\n{ex.Message}\n\nPlease check the logs for details." - ); + HandleModalOpenError("analyze", "Analyzer", ex); } } @@ -510,14 +489,7 @@ private void Tool() } catch (Exception ex) { - IsModalVisible = false; - DebugLogger.LogError( - "BalatroMainMenuViewModel", - $"Failed to open settings modal: {ex}" - ); - ShowErrorModal( - $"Failed to open Settings Modal:\n\n{ex.Message}\n\nPlease check the logs for details." - ); + HandleModalOpenError("settings", "Settings", ex); } } @@ -528,58 +500,40 @@ private void Settings() // Settings now opens SettingsModal via ModalRequested event } + // Widget toggle commands all share the pattern: + // "if disabled, no-op; otherwise play click sound and flip visibility". + // Each [RelayCommand] needs to be its own method so the source generator can + // produce a corresponding ICommand property — but the body is a one-liner now. [RelayCommand] - private void ToggleMusicMixerWidget() - { - if (!IsMusicMixerWidgetEnabled) - return; // Can't toggle if not enabled - PlayButtonClickSound(); - IsMusicMixerWidgetVisible = !IsMusicMixerWidgetVisible; - } + private void ToggleMusicMixerWidget() => + ToggleWidget(IsMusicMixerWidgetEnabled, () => IsMusicMixerWidgetVisible = !IsMusicMixerWidgetVisible); [RelayCommand] - private void ToggleVisualizerWidget() - { - if (!IsVisualizerWidgetEnabled) - return; - PlayButtonClickSound(); - IsVisualizerWidgetVisible = !IsVisualizerWidgetVisible; - } + private void ToggleVisualizerWidget() => + ToggleWidget(IsVisualizerWidgetEnabled, () => IsVisualizerWidgetVisible = !IsVisualizerWidgetVisible); [RelayCommand] - private void ToggleTransitionDesignerWidget() - { - if (!IsTransitionDesignerWidgetEnabled) - return; - PlayButtonClickSound(); - IsTransitionDesignerWidgetVisible = !IsTransitionDesignerWidgetVisible; - } + private void ToggleTransitionDesignerWidget() => + ToggleWidget(IsTransitionDesignerWidgetEnabled, () => IsTransitionDesignerWidgetVisible = !IsTransitionDesignerWidgetVisible); [RelayCommand] - private void ToggleFertilizerWidget() - { - if (!IsFertilizerWidgetEnabled) - return; - PlayButtonClickSound(); - IsFertilizerWidgetVisible = !IsFertilizerWidgetVisible; - } + private void ToggleFertilizerWidget() => + ToggleWidget(IsFertilizerWidgetEnabled, () => IsFertilizerWidgetVisible = !IsFertilizerWidgetVisible); [RelayCommand] - private void ToggleHostApiWidget() - { - if (!IsHostApiWidgetEnabled) - return; - PlayButtonClickSound(); - IsHostApiWidgetVisible = !IsHostApiWidgetVisible; - } + private void ToggleHostApiWidget() => + ToggleWidget(IsHostApiWidgetEnabled, () => IsHostApiWidgetVisible = !IsHostApiWidgetVisible); [RelayCommand] - private void ToggleEventFXWidget() + private void ToggleEventFXWidget() => + ToggleWidget(IsEventFXWidgetEnabled, () => IsEventFXWidgetVisible = !IsEventFXWidgetVisible); + + private void ToggleWidget(bool enabled, Action toggle) { - if (!IsEventFXWidgetEnabled) + if (!enabled) return; PlayButtonClickSound(); - IsEventFXWidgetVisible = !IsEventFXWidgetVisible; + toggle(); } [RelayCommand] @@ -789,6 +743,18 @@ private void ShowErrorModal(string errorMessage) ShowModal("ERROR", errorModal); } + // Common error path for the modal-launching commands. logName is the + // lowercase name used in DebugLogger; displayName is the user-facing + // name shown in the error modal title text. + private void HandleModalOpenError(string logName, string displayName, Exception ex) + { + IsModalVisible = false; + DebugLogger.LogError("BalatroMainMenuViewModel", $"Failed to open {logName} modal: {ex}"); + ShowErrorModal( + $"Failed to open {displayName} Modal:\n\n{ex.Message}\n\nPlease check the logs for details." + ); + } + #endregion #region Settings Management @@ -1049,8 +1015,7 @@ private void PlayButtonClickSound() { try { - var audioManager = ServiceHelper.GetService(); - audioManager?.PlaySfx("button", 1.0f); + _audioManager?.PlaySfx("button", 1.0f); } catch (Exception ex) { diff --git a/src/BalatroSeedOracle/ViewModels/FiltersModalViewModel.cs b/src/BalatroSeedOracle/ViewModels/FiltersModalViewModel.cs index e0f9db4a..5b26bd37 100644 --- a/src/BalatroSeedOracle/ViewModels/FiltersModalViewModel.cs +++ b/src/BalatroSeedOracle/ViewModels/FiltersModalViewModel.cs @@ -184,6 +184,20 @@ public FiltersModalViewModel( public string[] DeckDisplayValues { get; } = Enum.GetNames(typeof(MotelyDeck)); public string[] StakeDisplayValues { get; } = Enum.GetNames(typeof(MotelyStake)); + // The stake-by-UI-index mapping is order-specific and uses lowercase JSON values + // (MotelyStake enum has gaps so we can't derive this from Enum.GetNames). + private static readonly string[] StakeNamesByIndex = + { + "white", + "red", + "green", + "black", + "blue", + "purple", + "orange", + "gold", + }; + // Deck/Stake index helpers public int SelectedDeckIndex { @@ -901,7 +915,6 @@ private List GeneratePlayingCardsList() /// public Motely.Filters.MotelyJsonConfig BuildConfigFromCurrentState() { - // Get author from UserProfileService var userProfileService = ServiceHelper.GetService(); var author = userProfileService?.GetAuthorName() ?? "Unknown"; @@ -912,7 +925,6 @@ public Motely.Filters.MotelyJsonConfig BuildConfigFromCurrentState() // Preserve original DateCreated and Author when re-saving an existing filter DateCreated = _originalDateCreated ?? DateTime.Now, Author = _originalAuthor ?? author, - // Use enum ToString() for JSON serialization Deck = SelectedDeck.ToString(), Stake = SelectedStake.ToString().ToLower(), Must = new List(), @@ -920,114 +932,101 @@ public Motely.Filters.MotelyJsonConfig BuildConfigFromCurrentState() MustNot = new List(), }; - // CRITICAL FIX: Read from VisualBuilderTab's collections if available - // The VisualBuilderTab has its own SelectedMust/Should/MustNot collections (FilterItem objects) + var visualBuilder = VisualBuilderTab as FilterTabs.VisualBuilderTabViewModel; + BsoLogger.LogImportant( "FiltersModalViewModel", - $"🔍 BuildConfig: VisualBuilderTab={VisualBuilderTab?.GetType().Name ?? "NULL"}" + $"🔍 BuildConfig: VisualBuilderTab={visualBuilder?.GetType().Name ?? "NULL"}" ); - if (VisualBuilderTab is FilterTabs.VisualBuilderTabViewModel visualVm) + if (visualBuilder is not null) { BsoLogger.LogImportant( "FiltersModalViewModel", - $"✅ USING VisualBuilder PATH: {visualVm.SelectedMust.Count} must, {visualVm.SelectedShould.Count} should" + $"✅ USING VisualBuilder PATH: {visualBuilder.SelectedMust.Count} must, {visualBuilder.SelectedShould.Count} should" ); - // Build Must clauses directly from FilterItem objects (including FilterOperatorItems) - foreach (var filterItem in visualVm.SelectedMust) - { - BsoLogger.LogImportant( - "FiltersModalViewModel", - $"Processing MUST item: Name={filterItem.Name}, Type={filterItem.Type}, ActualType={filterItem.GetType().Name}" - ); - - var clause = ConvertFilterItemToClause(filterItem); - if (clause is not null) - { - config.Must.Add(clause); - BsoLogger.Log( - "FiltersModalViewModel", - $"Added clause: Type={clause.Type}, HasClauses={clause.Clauses is not null}, ClausesCount={clause.Clauses?.Count ?? 0}" - ); - } - else - { - BsoLogger.LogError( - "FiltersModalViewModel", - $"Failed to convert {filterItem.Name} to clause!" - ); - } - } + AppendVisualClauses(visualBuilder.SelectedMust, config.Must); + AppendVisualClauses(visualBuilder.SelectedShould, config.Should); + AppendBannedItemsAsMustNot(visualBuilder.SelectedMust, config.MustNot); } else { - // Fallback to parent's key-based collections - foreach (var itemKey in SelectedMust) - { - if (ItemConfigs.TryGetValue(itemKey, out var itemConfig)) - { - var clause = ConvertItemConfigToClause(itemConfig); - if (clause is not null) - config.Must.Add(clause); - } - } + AppendKeyedClauses(SelectedMust, config.Must); + AppendKeyedClauses(SelectedShould, config.Should); + AppendKeyedClauses(SelectedMustNot, config.MustNot); } - // Build Should clauses - if (VisualBuilderTab is FilterTabs.VisualBuilderTabViewModel visualVm2) - { - foreach (var filterItem in visualVm2.SelectedShould) - { - var clause = ConvertFilterItemToClause(filterItem); - if (clause is not null) - config.Should.Add(clause); - } - } - else + return config; + } + + // ConvertFilterItemToClause returns null for BannedItems operators by design, + // so they're skipped here and surfaced via AppendBannedItemsAsMustNot. + private void AppendVisualClauses( + IEnumerable source, + List target + ) + { + foreach (var filterItem in source) { - foreach (var itemKey in SelectedShould) + BsoLogger.LogImportant( + "FiltersModalViewModel", + $"Processing item: Name={filterItem.Name}, Type={filterItem.Type}, ActualType={filterItem.GetType().Name}" + ); + + var clause = ConvertFilterItemToClause(filterItem); + if (clause is null) { - if (ItemConfigs.TryGetValue(itemKey, out var itemConfig)) - { - var clause = ConvertItemConfigToClause(itemConfig); - if (clause is not null) - config.Should.Add(clause); - } + BsoLogger.LogError( + "FiltersModalViewModel", + $"Failed to convert {filterItem.Name} to clause!" + ); + continue; } + + target.Add(clause); + BsoLogger.Log( + "FiltersModalViewModel", + $"Added clause: Type={clause.Type}, HasClauses={clause.Clauses is not null}, ClausesCount={clause.Clauses?.Count ?? 0}" + ); } + } - // Build MustNot clauses - handle both BannedItems operator and direct MustNot - if (VisualBuilderTab is FilterTabs.VisualBuilderTabViewModel visualVm3) + // BannedItems live inside the Must collection as a FilterOperatorItem wrapper; + // unwrap their children into MustNot to match the JSON config schema. + private void AppendBannedItemsAsMustNot( + IEnumerable source, + List mustNot + ) + { + foreach (var item in source) { - // Check for BannedItems operator in Must collection - foreach (var item in visualVm3.SelectedMust) + if (item is not Models.FilterOperatorItem op || op.OperatorType != "BannedItems") + continue; + + foreach (var child in op.Children) { - if (item is Models.FilterOperatorItem op && op.OperatorType == "BannedItems") - { - foreach (var child in op.Children) - { - var clause = ConvertFilterItemToClause(child); - if (clause is not null) - config.MustNot.Add(clause); - } - } + var clause = ConvertFilterItemToClause(child); + if (clause is not null) + mustNot.Add(clause); } } - else + } + + private void AppendKeyedClauses( + IEnumerable itemKeys, + List target + ) + { + foreach (var itemKey in itemKeys) { - foreach (var itemKey in SelectedMustNot) - { - if (ItemConfigs.TryGetValue(itemKey, out var itemConfig)) - { - var clause = ConvertItemConfigToClause(itemConfig); - if (clause is not null) - config.MustNot.Add(clause); - } - } - } + if (!ItemConfigs.TryGetValue(itemKey, out var itemConfig)) + continue; - return config; + var clause = ConvertItemConfigToClause(itemConfig); + if (clause is not null) + target.Add(clause); + } } private MotelyJsonConfig.MotelyJsonFilterClause? ConvertItemConfigToClause( @@ -1289,41 +1288,9 @@ public void LoadConfigIntoState(Motely.Filters.MotelyJsonConfig config) SelectedStake = stake; } - // Load Must clauses - if (config.Must is not null) - { - foreach (var clause in config.Must) - { - var itemKey = GenerateNextItemKey(); - var itemConfig = ConvertClauseToItemConfig(clause, itemKey); - ItemConfigs[itemKey] = itemConfig; - SelectedMust.Add(itemKey); - } - } - - // Load Should clauses - if (config.Should is not null) - { - foreach (var clause in config.Should) - { - var itemKey = GenerateNextItemKey(); - var itemConfig = ConvertClauseToItemConfig(clause, itemKey); - ItemConfigs[itemKey] = itemConfig; - SelectedShould.Add(itemKey); - } - } - - // Load MustNot clauses - if (config.MustNot is not null) - { - foreach (var clause in config.MustNot) - { - var itemKey = GenerateNextItemKey(); - var itemConfig = ConvertClauseToItemConfig(clause, itemKey); - ItemConfigs[itemKey] = itemConfig; - SelectedMustNot.Add(itemKey); - } - } + LoadClausesIntoSelection(config.Must, SelectedMust); + LoadClausesIntoSelection(config.Should, SelectedShould); + LoadClausesIntoSelection(config.MustNot, SelectedMustNot); LoadedConfig = config; @@ -1339,6 +1306,24 @@ public void LoadConfigIntoState(Motely.Filters.MotelyJsonConfig config) } } + // Each clause is registered in ItemConfigs under a freshly generated key, + // and the key is appended to the appropriate selection collection. + private void LoadClausesIntoSelection( + IEnumerable? clauses, + ObservableCollection selection + ) + { + if (clauses is null) + return; + + foreach (var clause in clauses) + { + var itemKey = GenerateNextItemKey(); + ItemConfigs[itemKey] = ConvertClauseToItemConfig(clause, itemKey); + selection.Add(itemKey); + } + } + private ItemConfig ConvertClauseToItemConfig( MotelyJsonConfig.MotelyJsonFilterClause clause, string itemKey @@ -1562,53 +1547,8 @@ private object CreateLoadTabContent() await UpdateVisualBuilderFromItemConfigs(); ExpandDropZonesWithItems(); - // Update deck and stake selection indices - if (!string.IsNullOrEmpty(config.Deck)) - { - var deckIndex = Array.IndexOf( - new[] - { - "Red", - "Blue", - "Yellow", - "Green", - "Black", - "Magic", - "Nebula", - "Ghost", - "Abandoned", - "Checkered", - "Zodiac", - "Painted", - "Anaglyph", - "Plasma", - "Erratic", - }, - config.Deck - ); - if (deckIndex >= 0) - SelectedDeckIndex = deckIndex; - } - - if (!string.IsNullOrEmpty(config.Stake)) - { - var stakeIndex = Array.IndexOf( - new[] - { - "white", - "red", - "green", - "black", - "blue", - "purple", - "orange", - "gold", - }, - config.Stake.ToLower() - ); - if (stakeIndex >= 0) - SelectedStakeIndex = stakeIndex; - } + ApplyDeckFromConfig(config.Deck); + ApplyStakeFromConfig(config.Stake); // Switch to Visual Builder tab SelectedTabIndex = 1; @@ -1708,52 +1648,8 @@ private object CreateLoadTabContent() ExpandDropZonesWithItems(); // Preserve deck/stake selections from the copy - if (!string.IsNullOrEmpty(newConfig.Deck)) - { - var deckIndex = Array.IndexOf( - new[] - { - "Red", - "Blue", - "Yellow", - "Green", - "Black", - "Magic", - "Nebula", - "Ghost", - "Abandoned", - "Checkered", - "Zodiac", - "Painted", - "Anaglyph", - "Plasma", - "Erratic", - }, - newConfig.Deck - ); - if (deckIndex >= 0) - SelectedDeckIndex = deckIndex; - } - - if (!string.IsNullOrEmpty(newConfig.Stake)) - { - var stakeIndex = Array.IndexOf( - new[] - { - "white", - "red", - "green", - "black", - "blue", - "purple", - "orange", - "gold", - }, - newConfig.Stake.ToLower() - ); - if (stakeIndex >= 0) - SelectedStakeIndex = stakeIndex; - } + ApplyDeckFromConfig(newConfig.Deck); + ApplyStakeFromConfig(newConfig.Stake); SelectedTabIndex = 1; BsoLogger.Log( @@ -1828,30 +1724,24 @@ private object CreateLoadTabContent() return filterSelector; } - // Convert index to deck name via enum - private string GetDeckName(int index) + // Lookup deck/stake name from a config blob and apply to the SelectedXIndex setter. + // Both no-op if the name is missing or unrecognised, leaving the previous selection intact. + private void ApplyDeckFromConfig(string? deckName) { - if (index >= 0 && index <= 14) - return ((Motely.MotelyDeck)index).ToString(); - return "Red"; + if (string.IsNullOrEmpty(deckName)) + return; + var index = Array.IndexOf(DeckDisplayValues, deckName); + if (index >= 0) + SelectedDeckIndex = index; } - // Convert index to stake name via enum (handles gaps in enum values) - private string GetStakeName(int index) + private void ApplyStakeFromConfig(string? stakeName) { - var stake = index switch - { - 0 => MotelyStake.White, - 1 => MotelyStake.Red, - 2 => MotelyStake.Green, - 3 => MotelyStake.Black, - 4 => MotelyStake.Blue, - 5 => MotelyStake.Purple, - 6 => MotelyStake.Orange, - 7 => MotelyStake.Gold, - _ => MotelyStake.White, - }; - return stake.ToString().ToLower(); + if (string.IsNullOrEmpty(stakeName)) + return; + var index = Array.IndexOf(StakeNamesByIndex, stakeName.ToLower()); + if (index >= 0) + SelectedStakeIndex = index; } /// diff --git a/src/BalatroSeedOracle/ViewModels/SearchModalViewModel.cs b/src/BalatroSeedOracle/ViewModels/SearchModalViewModel.cs index 19299af3..9a098357 100644 --- a/src/BalatroSeedOracle/ViewModels/SearchModalViewModel.cs +++ b/src/BalatroSeedOracle/ViewModels/SearchModalViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; @@ -13,6 +14,7 @@ using BalatroSeedOracle.Helpers; using BalatroSeedOracle.Models; using BalatroSeedOracle.Services; +using BalatroSeedOracle.Services.Export; using BalatroSeedOracle.Views.Modals; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -42,6 +44,8 @@ public partial class SearchModalViewModel private readonly BalatroSeedOracle.Services.Storage.IAppDataStore _appDataStore; private readonly IPlatformServices _platformServices; private readonly Func _analyzeModalFactory; + private readonly IResultsDatabaseExporter? _resultsDatabaseExporter; + private readonly IParquetExporter? _parquetExporter; private ActiveSearchContext? _searchContext; private string _currentSearchId = string.Empty; @@ -290,7 +294,9 @@ public SearchModalViewModel( UserProfileService userProfileService, BalatroSeedOracle.Services.Storage.IAppDataStore appDataStore, IPlatformServices platformServices, - Func analyzeModalFactory + Func analyzeModalFactory, + IResultsDatabaseExporter? resultsDatabaseExporter = null, + IParquetExporter? parquetExporter = null ) { _searchManager = searchManager; @@ -299,6 +305,8 @@ Func analyzeModalFactory _platformServices = platformServices; _analyzeModalFactory = analyzeModalFactory ?? throw new ArgumentNullException(nameof(analyzeModalFactory)); + _resultsDatabaseExporter = resultsDatabaseExporter; + _parquetExporter = parquetExporter; _consoleBuffer = new CircularConsoleBuffer(1000); SearchResults = new ObservableCollection(); @@ -326,6 +334,244 @@ Func analyzeModalFactory /// Creates an AnalyzeModalViewModel via DI factory (no ServiceHelper). Used by ResultsTab to show analyze modal. public AnalyzeModalViewModel CreateAnalyzeModalViewModel() => _analyzeModalFactory(); + /// + /// Add a seed to the favorites store. Called from ResultsTab grid event. + /// + public void AddSeedToFavorites(string? seed) + { + if (!string.IsNullOrWhiteSpace(seed)) + { + FavoritesService.Instance.AddFavoriteItem(seed); + } + } + + /// + /// Open the analyze modal pre-loaded with the given seed. Called from ResultsTab grid event. + /// + public void OpenAnalyzeModalForSeed(string? seed) + { + if (string.IsNullOrWhiteSpace(seed) || MainMenu == null) + { + return; + } + + var analyzeVm = CreateAnalyzeModalViewModel(); + var analyzeModal = new AnalyzeModal(analyzeVm); + analyzeModal.SetSeedAndAnalyze(seed); + MainMenu.ShowModal("SEED ANALYZER", analyzeModal); + } + + /// + /// Request the View to open the pop-out DataGridResultsWindow for the active search. + /// Window construction stays in the View; the VM only resolves the data + raises the event. + /// + public void RequestPopOutResults() + { + try + { + if (string.IsNullOrEmpty(_currentSearchId)) + { + BsoLogger.LogError( + "SearchModalViewModel", + "Cannot pop out - no active search ID" + ); + return; + } + + var searchInstance = _searchManager.GetSearch(_currentSearchId); + if (searchInstance == null) + { + BsoLogger.LogError( + "SearchModalViewModel", + $"Cannot pop out - search instance not found: {_currentSearchId}" + ); + return; + } + + ShowDataGridResultsRequested?.Invoke(this, (searchInstance, LoadedConfig?.Name)); + BsoLogger.Log( + "SearchModalViewModel", + $"Requested pop-out window for search: {_currentSearchId}" + ); + } + catch (Exception ex) + { + BsoLogger.LogError( + "SearchModalViewModel", + $"Failed to pop out results: {ex.Message}" + ); + } + } + + /// + /// Export the supplied search results to Parquet/CSV/DuckDB depending on the file extension + /// the user selects in the storage save dialog. Pure business logic moved out of code-behind. + /// + public async Task ExportSearchResultsAsync( + TopLevel? topLevel, + IEnumerable? results + ) + { + try + { + if (results == null || !results.Any()) + { + BsoLogger.Log("SearchModalViewModel", "No results to export"); + return; + } + + if (topLevel == null) + { + BsoLogger.LogError( + "SearchModalViewModel", + "Could not get TopLevel for file picker" + ); + return; + } + + var fileTypeChoices = + new List + { + new Avalonia.Platform.Storage.FilePickerFileType("Parquet Files") + { + Patterns = new[] { "*.parquet" }, + }, + new Avalonia.Platform.Storage.FilePickerFileType("CSV Files") + { + Patterns = new[] { "*.csv" }, + }, + }; + + if (_resultsDatabaseExporter != null && _resultsDatabaseExporter.IsAvailable) + { + fileTypeChoices.Add( + new Avalonia.Platform.Storage.FilePickerFileType("Search Results (.db)") + { + Patterns = new[] { "*.db" }, + } + ); + fileTypeChoices.Add( + new Avalonia.Platform.Storage.FilePickerFileType( + "Search Results Lake (.ducklake)" + ) + { + Patterns = new[] { "*.ducklake" }, + } + ); + } + + var file = await topLevel.StorageProvider.SaveFilePickerAsync( + new Avalonia.Platform.Storage.FilePickerSaveOptions + { + Title = "Export Search Results", + DefaultExtension = "parquet", + SuggestedFileName = + $"search_results_{DateTime.Now:yyyyMMdd_HHmmss}.parquet", + FileTypeChoices = fileTypeChoices, + } + ); + + if (file == null) + { + BsoLogger.Log("SearchModalViewModel", "Export cancelled by user"); + return; + } + + var first = results.First(); + var labels = first?.Labels ?? Array.Empty(); + var filePath = file.Path.LocalPath; + + if (filePath.EndsWith(".parquet", StringComparison.OrdinalIgnoreCase)) + { + if (_parquetExporter == null || !_parquetExporter.IsAvailable) + { + BsoLogger.Log( + "SearchModalViewModel", + "Parquet export not available on this platform" + ); + return; + } + + var headers = new List { "SEED", "TOTALSCORE" }; + headers.AddRange(labels.Select(l => l.ToUpperInvariant())); + + var rows = new List>(); + foreach (var result in results) + { + var row = new List { result.Seed, result.TotalScore }; + if (result.Scores != null) + { + row.AddRange(result.Scores.Cast()); + } + rows.Add(row); + } + + await _parquetExporter.ExportAsync(filePath, headers, rows); + BsoLogger.Log( + "SearchModalViewModel", + $"Exported {results.Count()} results to Parquet: {filePath}" + ); + } + else if ( + filePath.EndsWith(".db", StringComparison.OrdinalIgnoreCase) + || filePath.EndsWith(".ducklake", StringComparison.OrdinalIgnoreCase) + ) + { + if (_resultsDatabaseExporter == null || !_resultsDatabaseExporter.IsAvailable) + { + BsoLogger.Log( + "SearchModalViewModel", + "Database export not available on this platform" + ); + return; + } + + await _resultsDatabaseExporter.ExportToAsync( + filePath, + results.ToList(), + labels.ToList() + ); + var ext = System.IO.Path.GetExtension(filePath); + BsoLogger.Log( + "SearchModalViewModel", + $"Exported {results.Count()} results to {ext}: {filePath}" + ); + } + else + { + var header = "SEED,TOTALSCORE"; + if (labels.Length > 0) + { + header += + "," + string.Join(",", labels.Select(l => l.ToUpperInvariant())); + } + + var csv = new System.Text.StringBuilder(); + csv.AppendLine(header); + + foreach (var result in results) + { + var csvRow = $"{result.Seed},{result.TotalScore}"; + if (result.Scores != null && result.Scores.Length > 0) + { + csvRow += "," + string.Join(",", result.Scores); + } + csv.AppendLine(csvRow); + } + + await System.IO.File.WriteAllTextAsync(filePath, csv.ToString()); + BsoLogger.Log( + "SearchModalViewModel", + $"Exported {results.Count()} results to CSV: {filePath}" + ); + } + } + catch (Exception ex) + { + BsoLogger.LogError("SearchModalViewModel", $"Export failed: {ex.Message}"); + } + } + partial void OnSelectedTabIndexChanged(int value) { OnPropertyChanged(nameof(CurrentTabContent)); @@ -343,10 +589,11 @@ partial void OnContinueFromLastChanged(bool value) // Update button text when Continue checkbox changes OnPropertyChanged(nameof(CookButtonText)); - // If user just enabled Continue while search is NOT running, load saved progress + // If user just enabled Continue while search is NOT running, reset progress. + // (Saved-progress restoration was removed when Motely took over DB ownership.) if (value && !IsSearching) { - LoadSavedProgressAsync().ConfigureAwait(false); + ProgressPercent = 0.0; } } @@ -395,6 +642,12 @@ string filterName )>? MinimizeToDesktopRequested; public event EventHandler? CopyToClipboardRequested; + /// + /// Raised by ResultsTab pop-out: the View constructs and shows the DataGridResultsWindow + /// (window construction is a View concern; VM only supplies the data context). + /// + public event EventHandler<(ActiveSearchContext Search, string? FilterName)>? ShowDataGridResultsRequested; + #endregion #region Command Implementations @@ -1074,19 +1327,6 @@ private string GetSearchId() return $"{normalizedFilterName}_{deck}_{stake}"; } - /// - /// Gets the database path for the current filter/deck/stake combination. - /// - private string GetDatabasePath() - { - var searchId = GetSearchId(); - if (string.IsNullOrEmpty(searchId)) - return string.Empty; - - var searchResultsDir = AppPaths.SearchResultsDir; - return Path.Combine(searchResultsDir, $"{searchId}.db"); - } - private async Task BuildSearchCriteriaAsync() { BsoLogger.LogImportant( @@ -2142,16 +2382,6 @@ private static string FormatSeedsCount(long count) } } - /// - /// Load saved search progress - feature has been removed - /// - private Task LoadSavedProgressAsync() - { - // Search state persistence has been removed - Motely now owns all database operations - ProgressPercent = 0.0; - return Task.CompletedTask; - } - #endregion #region Shader Transition Helpers diff --git a/src/BalatroSeedOracle/Views/Modals/StandardModal.axaml b/src/BalatroSeedOracle/Views/Modals/StandardModal.axaml index ba45d3cc..9b030f77 100644 --- a/src/BalatroSeedOracle/Views/Modals/StandardModal.axaml +++ b/src/BalatroSeedOracle/Views/Modals/StandardModal.axaml @@ -1,7 +1,43 @@ - + x:Class="BalatroSeedOracle.Views.Modals.StandardModal" + Classes.squeeze="{Binding Squeeze, RelativeSource={RelativeSource Mode=Self}}"> + + + + + + + + + + + @@ -17,21 +53,22 @@ - - + + + VerticalAlignment="Center"> - + - - - - - + + + + + - - + + - -