diff --git a/IMAGE_PROCESSING_SUMMARY.md b/IMAGE_PROCESSING_SUMMARY.md deleted file mode 100644 index 9313033..0000000 --- a/IMAGE_PROCESSING_SUMMARY.md +++ /dev/null @@ -1,317 +0,0 @@ -# Item Images in Database - Implementation Summary - -## āœ… Completed Successfully - -Successfully added WebP image processing and storage for item icons in the database. - ---- - -## What Was Added - -### 1. Database Migration: `add_item_images` - -**New Columns in `items` Table:** -- `icon_large` (BLOB) - 256x256 WebP image -- `icon_medium` (BLOB) - 64x64 WebP image -- `icon_small` (BLOB) - 16x16 WebP image - -All columns are nullable (items without icons will have NULL values). - -### 2. Image Processing in `ItemDatabase` - -**New Method: `save_to_db_with_images()`** -```rust -pub fn save_to_db_with_images>( - &self, - conn: &mut SqliteConnection, - icon_path: P, -) -> Result<(usize, usize), diesel::result::Error> -``` - -**Features:** -- Processes PNG icons from the ItemIcons directory -- Converts to WebP format (85% quality) -- Generates 3 sizes: 256px, 64px, 16px -- Returns tuple: (items_saved, images_processed) -- Handles missing icons gracefully (stores NULL) -- Uses transactions for data consistency - -**Helper Method: `process_item_icon()`** -- Loads PNG file by item ID (e.g., "33.png" for Copper Ore) -- Processes image at 3 resolutions using `ImageProcessor` -- Returns tuple of WebP blobs -- Logs warnings for failed conversions - -### 3. Updated xml-parser - -The xml-parser now: -- Locates ItemIcons directory from CB_ASSETS_PATH -- Calls `save_to_db_with_images()` instead of `save_to_db()` -- Reports both item count and processed image count - ---- - -## Results - -### Processing Statistics - -``` -āœ… Total items: 1360 -āœ… Items with icons: 1228 -āœ… Items without icons: 132 (no source PNG available) -šŸ’¾ Total image storage: 9.03 MB -``` - -### Sample Item Icons - -``` -Copper Ore (ID: 33) - Large (256px): 9.6 KB | Medium (64px): 2.4 KB | Small (16px): 0.4 KB - Total per item: 12.4 KB - -Iron Ore (ID: 34) - Large (256px): 3.3 KB | Medium (64px): 1.8 KB | Small (16px): 0.4 KB - Total per item: 5.5 KB -``` - -### Storage Efficiency - -**WebP Compression Benefits:** -- Original PNG (256x256, RGBA): ~20-40 KB per icon -- WebP Large (256px): ~3-10 KB (70-80% reduction) -- WebP Medium (64px): ~1-2 KB -- WebP Small (16px): ~0.3-0.5 KB - -**Total storage for 1228 items with 3 sizes each:** -- Only 9.03 MB for all processed images -- Average ~7.4 KB per item (all 3 sizes combined) -- Excellent compression with minimal quality loss - ---- - -## Code Changes - -### Files Modified - -1. **`migrations/2026-01-11-135041-0000_add_item_images/up.sql`** - - Added 3 BLOB columns to items table - -2. **`src/schema.rs`** - - Auto-regenerated with new columns - -3. **`src/databases/item_database.rs`** - - Added `save_to_db_with_images()` method - - Added `process_item_icon()` helper - - Updated `load_from_db()` ItemRecord struct - -4. **`src/bin/xml-parser.rs`** - - Updated to use `save_to_db_with_images()` - - Added icon path construction - -### Files Created - -1. **`src/bin/verify-images.rs`** - - Verification tool to check image storage - - Shows statistics and sample image sizes - ---- - -## Usage - -### Saving Items with Images - -```rust -use cursebreaker_parser::ItemDatabase; -use diesel::sqlite::SqliteConnection; - -let item_db = ItemDatabase::load_from_xml("items.xml")?; -let mut conn = SqliteConnection::establish("database.db")?; - -// Process and save with images -let icon_path = "/path/to/CBAssets/Data/Textures/ItemIcons"; -let (items_count, images_count) = item_db.save_to_db_with_images(&mut conn, icon_path)?; - -println!("Saved {} items with {} icons", items_count, images_count); -``` - -### Retrieving Images from Database - -```sql --- Get item with images -SELECT id, name, icon_large, icon_medium, icon_small -FROM items -WHERE id = 33; -- Copper Ore - --- Find items with icons -SELECT id, name -FROM items -WHERE icon_large IS NOT NULL; - --- Get just the small icon for quick previews -SELECT id, name, icon_small -FROM items; -``` - -### Serving Images in Web API - -For your interactive map and wiki: - -1. **Small icons (16px)** - List views, tooltips, inventory grids -2. **Medium icons (64px)** - Item cards, search results, crafting UI -3. **Large icons (256px)** - Detail pages, zoomed views, high-res displays - -**Example API endpoint:** -```rust -// Get item icon by size -async fn get_item_icon(item_id: i32, size: String) -> Result> { - let item = query_item(item_id)?; - - let icon_data = match size.as_str() { - "small" => item.icon_small, - "medium" => item.icon_medium, - "large" | _ => item.icon_large, - }; - - match icon_data { - Some(data) => Ok(data), - None => Err("Icon not found"), - } -} - -// Serve as image/webp -// Response headers: Content-Type: image/webp -``` - ---- - -## Benefits - -āœ… **Efficient Storage** - WebP compression saves ~70-80% space -āœ… **Multiple Resolutions** - Responsive design ready -āœ… **Self-Contained** - No external file dependencies -āœ… **Fast Delivery** - No filesystem lookups, direct from DB -āœ… **Transactional** - Images saved atomically with item data -āœ… **Graceful Degradation** - Missing icons handled automatically -āœ… **Web-Optimized** - WebP format supported by all modern browsers - ---- - -## Performance Considerations - -### Processing Time -- ~1228 images processed during save -- Each image generates 3 sizes (256px, 64px, 16px) -- Total: ~3,684 WebP encodings -- Processing time: ~5-10 seconds on modern hardware - -### Database Size Impact -- Before images: ~130 MB (JSON data + columns) -- After images: ~139 MB (+9 MB for all icons) -- Only 7% increase in database size -- All icons for all sizes included - -### Query Performance -- Small overhead when selecting items (if loading all columns) -- Minimal impact if only selecting needed columns -- Consider separate queries for image data vs metadata - -**Optimization Tips:** -```sql --- Fast: Get metadata only -SELECT id, name, item_type, level, price -FROM items; - --- Slower: Include large images unnecessarily -SELECT * -FROM items; - --- Optimal: Get specific image size when needed -SELECT id, name, icon_small -FROM items; -``` - ---- - -## Future Enhancements - -### Optional Additions: - -1. **Lazy Loading** - Separate images table with FK to items - ```sql - CREATE TABLE item_images ( - item_id INTEGER PRIMARY KEY, - icon_large BLOB, - icon_medium BLOB, - icon_small BLOB, - FOREIGN KEY (item_id) REFERENCES items(id) - ); - ``` - -2. **Image Metadata** - Track source info - - Original file path - - Processing timestamp - - Source dimensions - - Compression ratio - -3. **Additional Sizes** - More resolution options - - 512px for retina displays - - 32px for medium-sized UI elements - - 8px for tiny thumbnails - -4. **Image Variants** - Different visual styles - - Grayscale for disabled/locked items - - Outlined versions for specific UI needs - - Colored overlays for rarity/quality - ---- - -## Migration Management - -**To rollback (columns stay but can be cleared):** -```bash -diesel migration revert -``` - -**To reprocess images after icon updates:** -```bash -# Just rerun the xml-parser - it uses REPLACE INTO -cargo run --bin xml-parser -``` - ---- - -## Testing - -### Verify Images - -```bash -# Check image statistics -cargo run --bin verify-images - -# Extract and view an image -sqlite3 cursebreaker.db "SELECT hex(icon_large) FROM items WHERE id=33;" | xxd -r -p > copper_ore.webp -# View with: xdg-open copper_ore.webp -``` - -### Performance Test - -```bash -# Time the full processing -time cargo run --bin xml-parser - -# Should complete in 5-15 seconds for 1228 images -``` - ---- - -## Ready for Production - -Your item images are now: -- āœ… Stored in the database -- āœ… Optimally compressed -- āœ… Available in 3 sizes -- āœ… Ready for your interactive map and wiki -- āœ… Fast to query and serve -- āœ… Properly handled when missing - -The database now contains all the data needed for a fully functional item system with visual assets! diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md deleted file mode 100644 index 4e08f6d..0000000 --- a/MIGRATION_PLAN.md +++ /dev/null @@ -1,243 +0,0 @@ -# Items Table Expansion - Migration Plan - -## Goal -Expand the items table to support efficient queries for an interactive map and media wiki server. - -## Database Design Strategy - -We'll use a **hybrid approach**: -1. **Commonly queried fields** → Direct columns in `items` table -2. **One-to-many/many-to-many relationships** → Separate normalized tables -3. **Complex nested data** → Keep in JSON `data` column - ---- - -## Phase 1: Add Core Columns to Items Table - -### New Columns for `items` table: -```sql -ALTER TABLE items ADD COLUMN item_type TEXT NOT NULL DEFAULT 'resource'; -ALTER TABLE items ADD COLUMN level INTEGER NOT NULL DEFAULT 1; -ALTER TABLE items ADD COLUMN price INTEGER NOT NULL DEFAULT 0; -ALTER TABLE items ADD COLUMN max_stack INTEGER NOT NULL DEFAULT 1; -ALTER TABLE items ADD COLUMN skill TEXT NOT NULL DEFAULT 'none'; -ALTER TABLE items ADD COLUMN tool TEXT NOT NULL DEFAULT 'none'; -ALTER TABLE items ADD COLUMN description TEXT NOT NULL DEFAULT ''; - --- Boolean flags (stored as INTEGER: 0=false, 1=true) -ALTER TABLE items ADD COLUMN two_handed INTEGER NOT NULL DEFAULT 0; -ALTER TABLE items ADD COLUMN undroppable INTEGER NOT NULL DEFAULT 0; -ALTER TABLE items ADD COLUMN undroppable_on_death INTEGER NOT NULL DEFAULT 0; -ALTER TABLE items ADD COLUMN unequip_destroy INTEGER NOT NULL DEFAULT 0; -ALTER TABLE items ADD COLUMN generate_icon INTEGER NOT NULL DEFAULT 0; -ALTER TABLE items ADD COLUMN hide_milestone INTEGER NOT NULL DEFAULT 0; -ALTER TABLE items ADD COLUMN cannot_craft_exceptional INTEGER NOT NULL DEFAULT 0; -ALTER TABLE items ADD COLUMN storage_all_items INTEGER NOT NULL DEFAULT 0; - --- IDs for relationships -ALTER TABLE items ADD COLUMN ability_id INTEGER NOT NULL DEFAULT 0; -ALTER TABLE items ADD COLUMN special_ability INTEGER NOT NULL DEFAULT 0; -ALTER TABLE items ADD COLUMN learn_ability_id INTEGER NOT NULL DEFAULT 0; -ALTER TABLE items ADD COLUMN book_id INTEGER NOT NULL DEFAULT 0; -ALTER TABLE items ADD COLUMN swap_item INTEGER NOT NULL DEFAULT 0; -ALTER TABLE items ADD COLUMN storage_size INTEGER NOT NULL DEFAULT 0; -``` - -**Use Case:** Direct filtering and sorting -- "Show all items with level > 50" -- "Show all weapons" -- "Show stackable items" - ---- - -## Phase 2: Create Related Tables - -### 2.1 Item Categories (many-to-many) -```sql -CREATE TABLE item_categories ( - item_id INTEGER NOT NULL, - category TEXT NOT NULL, - PRIMARY KEY (item_id, category), - FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE -); - -CREATE INDEX idx_item_categories_category ON item_categories(category); -``` - -**Use Case:** -- "Show all bows" -- "Show all heavy armor" - -### 2.2 Item Stats (one-to-many) -```sql -CREATE TABLE item_stats ( - item_id INTEGER NOT NULL, - stat_type TEXT NOT NULL, - value REAL NOT NULL, - PRIMARY KEY (item_id, stat_type), - FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE -); - -CREATE INDEX idx_item_stats_stat_type ON item_stats(stat_type); -CREATE INDEX idx_item_stats_value ON item_stats(value); -``` - -**Use Case:** -- "Show all items with Health > 100" -- "Show all items with DamagePhysical" - -### 2.3 Crafting Recipes (normalized) -```sql -CREATE TABLE crafting_recipes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - product_item_id INTEGER NOT NULL, - skill TEXT NOT NULL, - level INTEGER NOT NULL, - workbench_id INTEGER NOT NULL, - xp INTEGER NOT NULL DEFAULT 0, - unlocked_by_default INTEGER NOT NULL DEFAULT 1, - checks TEXT, -- nullable, for conditional recipes - FOREIGN KEY (product_item_id) REFERENCES items(id) ON DELETE CASCADE -); - -CREATE INDEX idx_crafting_recipes_product ON crafting_recipes(product_item_id); -CREATE INDEX idx_crafting_recipes_skill ON crafting_recipes(skill); -CREATE INDEX idx_crafting_recipes_level ON crafting_recipes(level); - -CREATE TABLE crafting_recipe_items ( - recipe_id INTEGER NOT NULL, - item_id INTEGER NOT NULL, - amount INTEGER NOT NULL, - PRIMARY KEY (recipe_id, item_id), - FOREIGN KEY (recipe_id) REFERENCES crafting_recipes(id) ON DELETE CASCADE, - FOREIGN KEY (item_id) REFERENCES items(id) -); - -CREATE INDEX idx_crafting_recipe_items_item ON crafting_recipe_items(item_id); -``` - -**Use Case:** -- "Show all recipes that use Copper Ore" -- "Show all Blacksmithy recipes" -- "What can I craft with these items?" - -### 2.4 Item Storage (for storage_items vec) -```sql -CREATE TABLE item_storage_allowed ( - storage_item_id INTEGER NOT NULL, - allowed_item_id INTEGER NOT NULL, - PRIMARY KEY (storage_item_id, allowed_item_id), - FOREIGN KEY (storage_item_id) REFERENCES items(id) ON DELETE CASCADE, - FOREIGN KEY (allowed_item_id) REFERENCES items(id) -); -``` - -**Use Case:** -- "What items can be stored in this container?" - -### 2.5 Item XP Boosts -```sql -CREATE TABLE item_xp_boosts ( - item_id INTEGER NOT NULL, - skill_type TEXT NOT NULL, - multiplier REAL NOT NULL, - PRIMARY KEY (item_id, skill_type), - FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE -); -``` - -### 2.6 Permanent Stat Boosts -```sql -CREATE TABLE item_permanent_stat_boosts ( - item_id INTEGER NOT NULL, - stat_type TEXT NOT NULL, - amount INTEGER NOT NULL, - PRIMARY KEY (item_id, stat_type), - FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE -); -``` - ---- - -## Phase 3: Keep JSON Data Column - -The `data` column stays as-is for: -- Complete item retrieval without joins -- Fields rarely queried (animations, models, audio, etc.) -- Custom names/descriptions with checks -- Future flexibility - ---- - -## Implementation Steps - -### Step 1: Create Migration File -- Create `migrations/2026-01-11-expand-items/up.sql` -- Create `migrations/2026-01-11-expand-items/down.sql` - -### Step 2: Update Rust Schema -- Run `diesel migration generate expand_items` -- Update `src/schema.rs` (diesel will auto-generate) - -### Step 3: Update `save_to_db()` in `item_database.rs` -- Insert item columns -- Insert related records (categories, stats, recipes, etc.) -- Use transactions for consistency - -### Step 4: Update `load_from_db()` in `item_database.rs` -- Load items with all related data -- Reconstruct full Item struct from columns + related tables - -### Step 5: Add Query Helper Methods -```rust -// Examples: -pub fn search_by_name(&self, query: &str) -> Vec<&Item> -pub fn filter_by_level_range(&self, min: i32, max: i32) -> Vec<&Item> -pub fn filter_by_stat(&self, stat_type: StatType, min_value: f32) -> Vec<&Item> -pub fn get_items_using_ingredient(&self, ingredient_id: i32) -> Vec<&Item> -``` - ---- - -## Benefits of This Approach - -āœ… **Efficient Queries** - No JSON parsing for common filters -āœ… **Flexible** - JSON fallback for complex data -āœ… **Maintainable** - Clear relationships between entities -āœ… **Scalable** - Can add indexes as needed -āœ… **Wiki-Friendly** - Easy joins for "Used In" sections - ---- - -## Query Examples for Wiki/Map - -```sql --- Find all level 50+ weapons -SELECT * FROM items WHERE item_type = 'weapon' AND level >= 50; - --- Find items that give health bonuses -SELECT i.* FROM items i -JOIN item_stats s ON i.id = s.item_id -WHERE s.stat_type = 'health' AND s.value > 0; - --- Find all recipes using Copper Ore (id=33) -SELECT i.name, r.level, r.skill FROM crafting_recipes r -JOIN crafting_recipe_items ri ON r.id = ri.recipe_id -JOIN items i ON r.product_item_id = i.id -WHERE ri.item_id = 33; - --- Find all bows -SELECT i.* FROM items i -JOIN item_categories c ON i.id = c.item_id -WHERE c.category = 'bow'; -``` - ---- - -## Next Steps - -Would you like me to: -1. āœ… Generate the migration files? -2. āœ… Update the `save_to_db()` and `load_from_db()` methods? -3. āœ… Add query helper methods? -4. āš ļø Keep it simple and just add Phase 1 columns first? diff --git a/REFACTORING_SUMMARY.md b/REFACTORING_SUMMARY.md deleted file mode 100644 index ce92932..0000000 --- a/REFACTORING_SUMMARY.md +++ /dev/null @@ -1,298 +0,0 @@ -# Database Schema Refactoring Summary - -## Overview - -Successfully refactored the minimap tiles system to use a single unified table with a `zoom` column instead of separate tables and multiple WebP columns. - -## Changes Made - -### 1. Database Schema - -#### Before: -```sql --- Two separate tables -CREATE TABLE minimap_tiles ( - webp_512 BLOB, - webp_256 BLOB, - webp_128 BLOB, - webp_64 BLOB, - webp_512_size INTEGER, - webp_256_size INTEGER, - webp_128_size INTEGER, - webp_64_size INTEGER, - ... -); - -CREATE TABLE merged_tiles ( - zoom_level INTEGER, - webp_data BLOB, - ... -); -``` - -#### After: -```sql --- Single unified table -CREATE TABLE minimap_tiles ( - x INTEGER NOT NULL, - y INTEGER NOT NULL, - zoom INTEGER NOT NULL, -- 0, 1, or 2 - width INTEGER NOT NULL, -- Always 512 - height INTEGER NOT NULL, -- Always 512 - image BLOB NOT NULL, -- Single WebP column - image_size INTEGER NOT NULL, - original_file_size INTEGER, -- Only for zoom=2 - source_path TEXT NOT NULL, - processed_at TIMESTAMP NOT NULL, - UNIQUE(x, y, zoom) -); -``` - -### 2. Data Model (`minimap_models.rs`) - -**Before:** -```rust -pub struct MinimapTileRecord { - pub webp_512: Vec, - pub webp_256: Vec, - pub webp_128: Vec, - pub webp_64: Vec, - pub webp_512_size: i32, - // ... more fields -} -``` - -**After:** -```rust -pub struct MinimapTileRecord { - pub x: i32, - pub y: i32, - pub zoom: i32, // NEW: Zoom level (0, 1, 2) - pub width: i32, - pub height: i32, - pub image: Vec, // UNIFIED: Single image column - pub image_size: i32, - pub original_file_size: Option, - pub source_path: String, - pub processed_at: String, -} -``` - -### 3. ImageProcessor (`image_processor.rs`) - -**Added new methods:** - -```rust -impl ImageProcessor { - /// Encode image to lossless WebP - pub fn encode_webp_lossless(img: &RgbaImage) - -> Result, ImageProcessingError> - - /// Create a black tile of specified size - pub fn create_black_tile(size: u32) -> RgbaImage - - /// Merge multiple tiles into a single image - pub fn merge_tiles( - tiles: &HashMap<(i32, i32), Vec>, - grid_x: i32, - grid_y: i32, - tile_size: u32, - output_size: u32, - ) -> Result -} -``` - -### 4. MinimapDatabase (`minimap_database.rs`) - -**Completely rewritten to:** -- Process all PNG files at zoom level 2 (original, lossless WebP) -- Automatically generate zoom level 1 tiles (2Ɨ2 merged) -- Automatically generate zoom level 0 tiles (4Ɨ4 merged) -- Store all zoom levels in a single `minimap_tiles` table - -**Key method:** -```rust -pub fn load_from_directory() -> Result { - // Step 1: Load all PNGs → zoom level 2 - // Step 2: Generate zoom level 1 (2Ɨ2 merged) - // Step 3: Generate zoom level 0 (4Ɨ4 merged) -} -``` - -### 5. Image Parser (`image-parser.rs`) - -**Simplified significantly:** -- No longer needs to be run separately from merge process -- Single command now generates **all** zoom levels -- Updated output to show statistics per zoom level - -**Usage:** -```bash -cd cursebreaker-parser -cargo run --bin image-parser --release -``` - -**Output includes:** -- Tile counts per zoom level (0, 1, 2) -- Storage size per zoom level -- Total compression ratio -- Map bounds - -### 6. Map Server (`cursebreaker-map/src/main.rs`) - -**Updated to use new schema:** - -**Before:** -```rust -// Queried merged_tiles table -use cursebreaker_parser::schema::merged_tiles::dsl::*; -merged_tiles - .filter(zoom_level.eq(z)) - .select(webp_data) -``` - -**After:** -```rust -// Queries unified minimap_tiles table -use cursebreaker_parser::schema::minimap_tiles::dsl::*; -minimap_tiles - .filter(zoom.eq(z)) - .select(image) -``` - -### 7. Removed Files - -- āŒ **`merge-tiles.rs`** - No longer needed (merged into image-parser) -- āŒ **`merged_tiles` table** - Replaced by unified minimap_tiles - -## Benefits - -1. **Simpler Schema**: One table instead of two -2. **Cleaner Code**: Single column for images instead of multiple -3. **Single Command**: One tool (`image-parser`) generates all zoom levels -4. **Maintainability**: Easier to understand and modify -5. **Consistency**: All tiles stored in the same way - -## Migration Path - -### For Existing Databases: - -1. **Run migration** (automatically done): - ```bash - diesel migration run - ``` - This will: - - Drop old `minimap_tiles` and `merged_tiles` tables - - Create new unified `minimap_tiles` table - -2. **Regenerate tiles**: - ```bash - cd cursebreaker-parser - cargo run --bin image-parser --release - ``` - -3. **Start map server**: - ```bash - cd ../cursebreaker-map - cargo run --release - ``` - -## Technical Details - -### Zoom Level Mapping - -| Zoom | Description | Merge Factor | Typical Count | -|------|-------------|--------------|---------------| -| 0 | Most zoomed out | 4Ɨ4 | ~31 tiles | -| 1 | Medium zoom | 2Ɨ2 | ~105 tiles | -| 2 | Full detail (original) | 1Ɨ1 | ~345 tiles | - -### Coordinate System - -- **Zoom 2**: Uses original tile coordinates (e.g., x=5, y=10) -- **Zoom 1**: Uses divided coordinates (e.g., x=2, y=5 for 2Ɨ2 grid starting at 4,10) -- **Zoom 0**: Uses divided coordinates (e.g., x=1, y=2 for 4Ɨ4 grid starting at 4,8) - -### WebP Encoding - -All tiles use **lossless WebP** compression: -- No quality loss -- Smaller than PNG -- Faster to decode than PNG -- Browser-native format - -## Testing - -After running the refactored system: - -1. **Check tile counts**: - ```sql - SELECT zoom, COUNT(*) as count - FROM minimap_tiles - GROUP BY zoom; - ``` - Expected: ~31 for zoom 0, ~105 for zoom 1, ~345 for zoom 2 - -2. **Verify storage**: - ```sql - SELECT - zoom, - COUNT(*) as tiles, - SUM(image_size) / 1048576 as mb - FROM minimap_tiles - GROUP BY zoom; - ``` - -3. **Test map viewer**: - - Open `http://127.0.0.1:3000` - - Zoom in/out to verify all levels load correctly - - Check browser DevTools network tab for tile requests - -## Performance - -Tile generation time: -- **Old approach**: Run image-parser (~30s) + run merge-tiles (~90s) = ~2 minutes total -- **New approach**: Run image-parser once (~90s) = ~1.5 minutes total -- **Improvement**: Simpler workflow, one less step - -Database storage: -- Similar total size (~111 MB) -- Cleaner schema with single image column -- Indexed by (zoom, x, y) for fast queries - -## Files Modified - -``` -cursebreaker-parser/ -ā”œā”€ā”€ migrations/ -│ └── 2026-01-10-122732-0000_restructure_minimap_tiles/ -│ ā”œā”€ā”€ up.sql # NEW -│ └── down.sql # NEW -ā”œā”€ā”€ src/ -│ ā”œā”€ā”€ bin/ -│ │ ā”œā”€ā”€ image-parser.rs # MODIFIED -│ │ └── merge-tiles.rs # DELETED -│ ā”œā”€ā”€ databases/ -│ │ └── minimap_database.rs # REWRITTEN -│ ā”œā”€ā”€ image_processor.rs # MODIFIED (added merge methods) -│ ā”œā”€ā”€ types/ -│ │ └── minimap_models.rs # MODIFIED (new schema) -│ └── schema.rs # REGENERATED -└── Cargo.toml # MODIFIED (removed merge-tiles bin) - -cursebreaker-map/ -└── src/ - └── main.rs # MODIFIED (use new schema) -``` - -## Backward Compatibility - -āš ļø **Breaking Changes:** -- Old database will be wiped by migration -- Must re-run `image-parser` to regenerate tiles -- `merge-tiles` command no longer exists - -āœ… **No Breaking Changes:** -- Map viewer API unchanged (`/api/tiles/:z/:x/:y`) -- Frontend code unchanged -- Tile coordinates same at each zoom level diff --git a/SCHEMA_EXPANSION_SUMMARY.md b/SCHEMA_EXPANSION_SUMMARY.md deleted file mode 100644 index de1ce57..0000000 --- a/SCHEMA_EXPANSION_SUMMARY.md +++ /dev/null @@ -1,297 +0,0 @@ -# Items Table Expansion - Implementation Summary - -## āœ… Completed: Balanced Approach - -Successfully expanded the items database schema with commonly queried columns and crafting recipe normalization. - ---- - -## What Was Added - -### 1. New Columns in `items` Table - -**Item Classification:** -- `item_type` (TEXT) - weapon, armor, resource, consumable, etc. -- `level` (INTEGER) - item level requirement -- `price` (INTEGER) - base item price -- `max_stack` (INTEGER) - maximum stack size -- `storage_size` (INTEGER) - if item is a container, how many slots -- `skill` (TEXT) - related skill (swordsmanship, mining, etc.) -- `tool` (TEXT) - tool type (pickaxe, hatchet, etc.) -- `description` (TEXT) - item description for search - -**Boolean Flags:** -- `two_handed` (INTEGER 0/1) -- `undroppable` (INTEGER 0/1) -- `undroppable_on_death` (INTEGER 0/1) -- `unequip_destroy` (INTEGER 0/1) -- `generate_icon` (INTEGER 0/1) -- `hide_milestone` (INTEGER 0/1) -- `cannot_craft_exceptional` (INTEGER 0/1) -- `storage_all_items` (INTEGER 0/1) - -**Ability/Item IDs:** -- `ability_id` (INTEGER) -- `special_ability` (INTEGER) -- `learn_ability_id` (INTEGER) -- `book_id` (INTEGER) -- `swap_item` (INTEGER) - -**Indexes Created:** -- `idx_items_type` - for filtering by item type -- `idx_items_level` - for level range queries -- `idx_items_price` - for price range queries -- `idx_items_skill` - for skill-related queries - -### 2. New Tables for Crafting Recipes - -**`crafting_recipes` Table:** -- `id` (INTEGER PRIMARY KEY AUTOINCREMENT) -- `product_item_id` (INTEGER) - FK to items(id) -- `skill` (TEXT) - required skill -- `level` (INTEGER) - required level -- `workbench_id` (INTEGER) - required workbench -- `xp` (INTEGER) - XP gained from crafting -- `unlocked_by_default` (INTEGER 0/1) -- `checks` (TEXT, nullable) - conditional requirements - -**Indexes:** -- `idx_crafting_recipes_product` - find recipes for an item -- `idx_crafting_recipes_skill` - find recipes by skill -- `idx_crafting_recipes_level` - find recipes by level -- `idx_crafting_recipes_workbench` - find recipes by workbench - -**`crafting_recipe_items` Table:** -- `recipe_id` (INTEGER) - FK to crafting_recipes(id) -- `item_id` (INTEGER) - FK to items(id) for ingredient -- `amount` (INTEGER) - quantity required - -**Indexes:** -- `idx_crafting_recipe_items_item` - find recipes using this ingredient - -### 3. Preserved JSON Data Column - -The full `data` column remains for: -- Complete item retrieval without joins -- Complex nested data (stats, animations, custom names) -- Future flexibility -- Backwards compatibility - ---- - -## Example Queries for Your Wiki/Map - -### Basic Filtering - -```sql --- Find all weapons above level 50 -SELECT id, name, item_type, level, price -FROM items -WHERE item_type = 'weapon' AND level > 50; - --- Find stackable items -SELECT id, name, max_stack -FROM items -WHERE max_stack > 1; - --- Find two-handed weapons -SELECT id, name, item_type, level -FROM items -WHERE two_handed = 1 AND item_type = 'weapon'; - --- Find mining tools -SELECT id, name, tool, level -FROM items -WHERE tool = 'pickaxe'; -``` - -### Crafting Queries - -```sql --- Find all recipes that use Copper Ore (id=33) -SELECT - i.id, - i.name AS product, - r.skill, - r.level, - ri.amount AS copper_needed -FROM crafting_recipes r -JOIN crafting_recipe_items ri ON r.id = ri.recipe_id -JOIN items i ON r.product_item_id = i.id -WHERE ri.item_id = 33; - --- Find all Blacksmithy recipes -SELECT - i.name AS product, - r.level, - r.xp -FROM crafting_recipes r -JOIN items i ON r.product_item_id = i.id -WHERE r.skill = 'blacksmithy' -ORDER BY r.level; - --- Get recipe details with all ingredients -SELECT - prod.name AS product, - ing.name AS ingredient, - ri.amount -FROM crafting_recipes r -JOIN items prod ON r.product_item_id = prod.id -JOIN crafting_recipe_items ri ON r.id = ri.recipe_id -JOIN items ing ON ri.item_id = ing.id -WHERE prod.id = 100; -- Replace with actual product ID -``` - -### Combined Queries - -```sql --- Find expensive high-level consumables -SELECT id, name, level, price, description -FROM items -WHERE item_type = 'consumable' - AND level >= 30 - AND price > 1000 -ORDER BY price DESC; - --- Find storage containers by size -SELECT id, name, storage_size, storage_all_items -FROM items -WHERE storage_size > 0 -ORDER BY storage_size DESC; -``` - ---- - -## Code Changes - -### Files Modified - -1. **`migrations/2026-01-11-133543-0000_expand_items/up.sql`** - - Added ALTER TABLE statements for new columns - - Created crafting_recipes and crafting_recipe_items tables - - Added indexes - -2. **`src/schema.rs`** - - Auto-regenerated by diesel with new schema - -3. **`src/databases/item_database.rs`** - - Updated `save_to_db()` - now populates all new columns and crafting tables - - Updated `load_from_db()` - updated struct to match new schema - - Uses transactions for data consistency - -### How It Works - -1. **Saving Items:** - - Each item is saved with all scalar fields as direct columns - - Enums (ItemType, SkillType, Tool) are converted to strings - - Booleans are stored as integers (0/1) - - Crafting recipes are inserted into separate tables - - Uses a transaction to ensure all-or-nothing saves - - Uses `replace_into` for items to handle updates - -2. **Loading Items:** - - Items are loaded from the JSON `data` column (complete info) - - Could be extended to join crafting tables if needed - - Fast for simple ID lookups - ---- - -## Testing Results - -``` -āœ… Sample items with expanded columns: - - 0 - Null (Type: resource, Level: 1, Price: 0, MaxStack: 1, Skill: none) - 33 - Copper Ore (Type: resource, Level: 1, Price: 0, MaxStack: 1, Skill: none) - 34 - Iron Ore (Type: resource, Level: 10, Price: 0, MaxStack: 1, Skill: none) - 61 - Spruce Log (Type: resource, Level: 1, Price: 10, MaxStack: 1, Skill: none) - 62 - Oak Log (Type: resource, Level: 10, Price: 0, MaxStack: 1, Skill: none) - -āœ… Successfully saved 1360 items to database -``` - -All items are properly stored with expanded columns! - ---- - -## Next Steps / Future Enhancements - -### Optional Phase 2 Additions (when needed): - -1. **Item Categories Table** (if you need to filter by categories often): - ```sql - CREATE TABLE item_categories ( - item_id INTEGER NOT NULL, - category TEXT NOT NULL, - PRIMARY KEY (item_id, category), - FOREIGN KEY (item_id) REFERENCES items(id) - ); - ``` - -2. **Item Stats Table** (if you need to query by specific stats): - ```sql - CREATE TABLE item_stats ( - item_id INTEGER NOT NULL, - stat_type TEXT NOT NULL, - value REAL NOT NULL, - PRIMARY KEY (item_id, stat_type), - FOREIGN KEY (item_id) REFERENCES items(id) - ); - ``` - -3. **Query Helper Methods** in `ItemDatabase`: - ```rust - pub fn filter_by_type(&self, item_type: ItemType) -> Vec<&Item> - pub fn filter_by_level_range(&self, min: i32, max: i32) -> Vec<&Item> - pub fn find_recipes_using(&self, ingredient_id: i32) -> Vec - ``` - ---- - -## Benefits Achieved - -āœ… **Efficient Filtering** - Can filter/search without parsing JSON -āœ… **Wiki-Ready** - Easy to generate "Used In" sections for recipes -āœ… **Map Integration** - Fast queries for map markers/filters -āœ… **Flexible** - JSON fallback for complex data -āœ… **Indexed** - Fast queries on common fields -āœ… **Maintainable** - Clear schema with relationships - ---- - -## Migration Management - -**To rollback the migration:** -```bash -diesel migration revert --database-url=cursebreaker.db --migration-dir=cursebreaker-parser/migrations -``` - -**To rerun after changes:** -```bash -diesel migration redo --database-url=cursebreaker.db --migration-dir=cursebreaker-parser/migrations -``` - -**To regenerate schema.rs:** -```bash -cd cursebreaker-parser && diesel print-schema --database-url=../cursebreaker.db > src/schema.rs -``` - ---- - -## Database Size - -- Before: ~130MB (with just JSON data) -- After: ~130MB (minimal increase, new columns use default values where not set) -- Crafting tables will add ~1-5MB when populated with recipe data - ---- - -## Ready for Production - -The implementation is complete and tested! Your interactive map and media wiki can now: - -- Filter items by type, level, price, skill, etc. -- Show crafting recipes with ingredients -- Find which recipes use specific items -- Query items efficiently without parsing JSON -- Scale to handle many concurrent queries diff --git a/STATS_IMPLEMENTATION_SUMMARY.md b/STATS_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 18fb6bb..0000000 --- a/STATS_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,412 +0,0 @@ -# Item Stats in Database - Implementation Summary - -## āœ… Completed Successfully - -Successfully added normalized item stats storage to the database for efficient querying and filtering. - ---- - -## What Was Added - -### 1. Database Migration: `add_item_stats` - -**New Table: `item_stats`** -```sql -CREATE TABLE item_stats ( - item_id INTEGER NOT NULL, - stat_type TEXT NOT NULL, - value REAL NOT NULL, -- Float/REAL for precise stat values - PRIMARY KEY (item_id, stat_type), - FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE -); -``` - -**Indexes Created:** -- `idx_item_stats_stat_type` - Find all items with a specific stat -- `idx_item_stats_value` - Range queries on stat values -- `idx_item_stats_type_value` - Combined queries (type + value range) - -**Composite Primary Key:** -- `(item_id, stat_type)` - Ensures each item can only have one value per stat type -- Prevents duplicate stats for the same item - -### 2. Supported Stat Types - -All 20 stat types from the game: - -**Damage Stats:** -- `damage_physical` - Physical Damage -- `damage_magical` - Magical Damage -- `damage_ranged` - Ranged Damage - -**Accuracy Stats:** -- `accuracy_physical` - Physical Accuracy -- `accuracy_magical` - Magical Accuracy -- `accuracy_ranged` - Ranged Accuracy - -**Resistance Stats:** -- `resistance_physical` - Physical Resistance -- `resistance_magical` - Magical Resistance -- `resistance_ranged` - Ranged Resistance - -**Health & Mana:** -- `health` - Health -- `mana` - Mana -- `health_regen` - Health Regeneration -- `mana_regen` - Mana Regeneration - -**Special Stats:** -- `critical` - Critical -- `healing` - Healing -- `movement_speed` - Movement Speed - -**Enemy Type Damage:** -- `critter_slaying` - Damage against Critters -- `damage_vs_beasts` - Damage against Beasts -- `damage_vs_undead` - Damage against Undead - -### 3. Code Changes - -**`src/databases/item_database.rs`** -- Updated `save_to_db_with_images()` to also save stats -- Iterates through `item.stats` vec and inserts each stat -- Maps `StatType` enum to string representation -- Uses same transaction as item and recipe saving - ---- - -## Results from Test Data - -### Statistics - -``` -āœ… Total stat entries: 84 -šŸ“Š Most common stats: - - health: 20 items - - resistance_magical: 9 items - - resistance_physical: 9 items - - accuracy_magical: 7 items - - mana: 7 items - - mana_regen: 7 items -``` - -### Example Items with Stats - -**The Bad Ring (ID: 73)** - 12 stats (testing item) -- All stats maxed at 1000 (accuracy, damage, health, mana, resistances) - -**Ring of the High Mage (ID: 394)** - 5 stats -- Health: 50 -- Mana: 50 -- Mana Regen: 2 -- Magical Resistance: 35 -- Physical Resistance: 15 - -**Crown of the Tyrant** - High health bonus -- Health: 100 - ---- - -## Usage Examples - -### SQL Queries - -```sql --- Find all items with health bonuses -SELECT i.id, i.name, s.value as health -FROM items i -JOIN item_stats s ON i.id = s.item_id -WHERE s.stat_type = 'health' -ORDER BY s.value DESC; - --- Find weapons with high physical damage -SELECT i.id, i.name, s.value as damage -FROM items i -JOIN item_stats s ON i.id = s.item_id -WHERE i.item_type = 'weapon' - AND s.stat_type = 'damage_physical' - AND s.value > 50 -ORDER BY s.value DESC; - --- Find items with multiple resistance types -SELECT i.id, i.name, COUNT(*) as resistance_count -FROM items i -JOIN item_stats s ON i.id = s.item_id -WHERE s.stat_type LIKE 'resistance_%' -GROUP BY i.id, i.name -HAVING COUNT(*) >= 2; - --- Find balanced items (have both offense and defense) -SELECT i.id, i.name -FROM items i -WHERE EXISTS ( - SELECT 1 FROM item_stats - WHERE item_id = i.id AND stat_type LIKE 'damage_%' -) -AND EXISTS ( - SELECT 1 FROM item_stats - WHERE item_id = i.id AND stat_type LIKE 'resistance_%' -); - --- Get all stats for a specific item -SELECT stat_type, value -FROM item_stats -WHERE item_id = 394 -ORDER BY stat_type; - --- Find items within a stat range -SELECT i.name, s.value -FROM items i -JOIN item_stats s ON i.id = s.item_id -WHERE s.stat_type = 'health' - AND s.value BETWEEN 50 AND 100; -``` - -### Rust Queries - -```rust -use diesel::prelude::*; -use cursebreaker_parser::schema::{items, item_stats}; - -// Find items with high health -let high_health_items = item_stats::table - .inner_join(items::table) - .filter(item_stats::stat_type.eq("health")) - .filter(item_stats::value.gt(100.0)) - .select((items::name, item_stats::value)) - .load::<(String, f32)>(&mut conn)?; - -// Get all stats for an item -let item_id = 394; -let stats = item_stats::table - .filter(item_stats::item_id.eq(item_id)) - .select((item_stats::stat_type, item_stats::value)) - .load::<(String, f32)>(&mut conn)?; -``` - ---- - -## Web API Examples - -For your interactive map and wiki: - -### 1. Filter Items by Stat - -```rust -// GET /api/items?stat=health&min=50&max=100 -pub async fn filter_items_by_stat( - stat: String, - min: f32, - max: f32, -) -> Result> { - let items = item_stats::table - .inner_join(items::table) - .filter(item_stats::stat_type.eq(stat)) - .filter(item_stats::value.between(min, max)) - .select(items::all_columns) - .load(&mut conn)?; - - Ok(items) -} -``` - -### 2. Item Comparison Tool - -```rust -// GET /api/items/compare?ids=394,340,179 -pub async fn compare_items(item_ids: Vec) -> Result { - let stats = item_stats::table - .filter(item_stats::item_id.eq_any(item_ids)) - .load::<(i32, String, f32)>(&mut conn)?; - - // Group by item and return comparison table - Ok(build_comparison(stats)) -} -``` - -### 3. Best Items for Stat - -```rust -// GET /api/items/best?stat=damage_physical&limit=10 -pub async fn best_items_for_stat( - stat: String, - limit: i64, -) -> Result> { - let items = item_stats::table - .inner_join(items::table) - .filter(item_stats::stat_type.eq(stat)) - .order_by(item_stats::value.desc()) - .limit(limit) - .select((items::all_columns, item_stats::value)) - .load(&mut conn)?; - - Ok(items) -} -``` - -### 4. Item Build Suggestions - -```rust -// GET /api/builds?focus=mage -pub async fn suggest_build(focus: String) -> Result { - // For mage build: high mana, mana_regen, magical damage/accuracy - let stats_to_prioritize = match focus.as_str() { - "mage" => vec!["mana", "mana_regen", "damage_magical", "accuracy_magical"], - "warrior" => vec!["health", "damage_physical", "resistance_physical"], - "archer" => vec!["damage_ranged", "accuracy_ranged", "critical"], - _ => vec![] - }; - - // Find best items for each slot with those stats - // ... -} -``` - ---- - -## Benefits - -āœ… **Efficient Filtering** - Query items by stat without parsing JSON -āœ… **Range Queries** - Find items within stat value ranges -āœ… **Comparative Analysis** - Easy item comparisons -āœ… **Build Optimization** - Find best items for specific builds -āœ… **Indexed** - Fast queries on stat types and values -āœ… **Normalized** - No data duplication, consistent values -āœ… **Type-Safe** - Composite PK prevents duplicate stats - ---- - -## Performance Considerations - -### Query Optimization - -**Fast Queries** (uses indexes): -```sql --- Single stat lookup -WHERE stat_type = 'health' -- Uses idx_item_stats_stat_type - --- Range query -WHERE value > 50 -- Uses idx_item_stats_value - --- Combined -WHERE stat_type = 'health' AND value > 50 -- Uses idx_item_stats_type_value -``` - -**Slower Queries** (may need optimization): -```sql --- Multiple stat requirements (requires multiple joins) --- Consider caching or denormalizing for frequently accessed combinations -``` - -### Storage Impact - -- 84 stat entries currently -- Average ~3-4 stats per item with stats -- Minimal storage: ~8 bytes per entry (int + text + float) -- Total: < 1 KB for current data - -As more items with stats are added, storage remains efficient due to normalization. - ---- - -## Wiki Feature Ideas - -### 1. Item Comparison Tool -Show side-by-side comparison of multiple items with all their stats. - -### 2. Best-in-Slot Lists -- Best weapons for physical damage -- Best armor for magical resistance -- Best accessories for health/mana - -### 3. Build Guides -- "Mage Build" - show items with high mana/magical stats -- "Tank Build" - show items with high health/resistances -- "DPS Build" - show items with high damage/critical - -### 4. Stat Distribution Charts -- Visualize stat value distributions -- Show which stats are rare vs common -- Identify stat "sweet spots" - -### 5. Upgrade Paths -- Show progression of items by stat values -- "From 50 health → 100 health → 150 health" - ---- - -## Future Enhancements - -### Optional Additions: - -1. **Stat Ranges Table** - For items with variable stats - ```sql - CREATE TABLE item_stat_ranges ( - item_id INTEGER, - stat_type TEXT, - min_value REAL, - max_value REAL, - PRIMARY KEY (item_id, stat_type) - ); - ``` - -2. **Derived Stats** - Calculate total stats for equipment sets - ```sql - -- Sum up stats across multiple equipped items - SELECT stat_type, SUM(value) as total - FROM item_stats - WHERE item_id IN (/* equipped item IDs */) - GROUP BY stat_type; - ``` - -3. **Stat Scaling** - Track how stats scale with item level - ```sql - SELECT i.level, AVG(s.value) as avg_damage - FROM items i - JOIN item_stats s ON i.id = s.item_id - WHERE s.stat_type = 'damage_physical' - GROUP BY i.level; - ``` - -4. **Stat Categories** - Group related stats - ```sql - CREATE TABLE stat_categories ( - stat_type TEXT PRIMARY KEY, - category TEXT -- 'offense', 'defense', 'utility', etc. - ); - ``` - ---- - -## Verification - -Run the verification script anytime: -```bash -cargo run --bin verify-stats -``` - -Shows: -- Total stat entries -- Breakdown by stat type -- Items with most stats -- Example queries (high damage, health bonuses) - ---- - -## Ready for Production - -Your item stats are now: -- āœ… Stored in normalized table -- āœ… Efficiently queryable -- āœ… Properly indexed -- āœ… Ready for filtering and comparison -- āœ… Available for build optimization -- āœ… Perfect for wiki features - -The database now contains complete item data with: -- āœ… Metadata (type, level, price, etc.) -- āœ… Images (3 sizes, WebP) -- āœ… Crafting recipes (normalized) -- āœ… **Stats (normalized, queryable)** - -Your interactive map and wiki have everything needed for a rich item browsing experience! šŸŽ® diff --git a/cursebreaker-map/OPTIMIZATION_SUMMARY.md b/cursebreaker-map/OPTIMIZATION_SUMMARY.md deleted file mode 100644 index c522119..0000000 --- a/cursebreaker-map/OPTIMIZATION_SUMMARY.md +++ /dev/null @@ -1,228 +0,0 @@ -# Tile Merging Optimization Summary - -## Problem - -The initial implementation loaded 345-510 individual tile images at every zoom level, resulting in: -- Slow initial page load -- Many HTTP requests overwhelming the browser -- Poor user experience when zooming - -## Solution - -Implemented a tile merging system that combines adjacent tiles at lower zoom levels: - -### Zoom Level 0 (Most Zoomed Out) -- **Merge Factor**: 4Ɨ4 tiles merged into single 512px images -- **Total Tiles**: 31 images -- **Reduction**: 91% fewer HTTP requests vs zoom level 2 -- **Use Case**: Fast initial load and overview - -### Zoom Level 1 (Medium Zoom) -- **Merge Factor**: 2Ɨ2 tiles merged into single 512px images -- **Total Tiles**: 105 images -- **Reduction**: 70% fewer HTTP requests vs zoom level 2 -- **Use Case**: Balanced detail and performance - -### Zoom Level 2 (Most Zoomed In) -- **Merge Factor**: 1Ɨ1 (no merging) -- **Total Tiles**: 345 images -- **Use Case**: Full detail viewing - -## Implementation Details - -### 1. Database Schema (`merged_tiles` table) - -```sql -CREATE TABLE merged_tiles ( - id INTEGER PRIMARY KEY, - x INTEGER NOT NULL, -- Tile coordinate at this zoom - y INTEGER NOT NULL, -- Tile coordinate at this zoom - zoom_level INTEGER NOT NULL, -- 0, 1, or 2 - merge_factor INTEGER NOT NULL,-- 1, 4, or 16 - width INTEGER NOT NULL, -- Always 512 - height INTEGER NOT NULL, -- Always 512 - webp_data BLOB NOT NULL, -- Lossless WebP - webp_size INTEGER NOT NULL, - processed_at TIMESTAMP NOT NULL, - source_tiles TEXT NOT NULL, -- Tracking info - UNIQUE(zoom_level, x, y) -); -``` - -### 2. Tile Merger Tool (`merge-tiles`) - -Located at: `cursebreaker-parser/src/bin/merge-tiles.rs` - -**What it does:** -- Reads all tiles from `minimap_tiles` table -- For zoom level 2: Re-encodes each tile as lossless WebP -- For zoom level 1: Merges 2Ɨ2 tile grids, resizes each to 256px, combines into 512px image -- For zoom level 0: Merges 4Ɨ4 tile grids, resizes each to 128px, combines into 512px image -- Missing tiles are filled with black pixels -- Stores all merged tiles in `merged_tiles` table - -**Performance:** -- Processes 345 original tiles → 481 merged tiles (all zoom levels) -- Takes ~1.5 minutes to run -- Total storage: ~111 MB (lossless WebP) - -**Usage:** -```bash -cd cursebreaker-parser -cargo run --bin merge-tiles --release -``` - -### 3. Backend Changes - -**File**: `cursebreaker-map/src/main.rs` - -**Changes:** -- Updated `get_tile()` to query `merged_tiles` table instead of `minimap_tiles` -- Changed tile coordinate parameter from `u32` to `i32` to match database zoom levels -- Simplified logic - no need to select different blob columns based on zoom - -### 4. Frontend Changes - -**File**: `cursebreaker-map/static/map.js` - -**Key Changes:** - -```javascript -// Map Leaflet zoom levels to database zoom levels -if (currentZoom === 0) { - dbZoom = 0; - mergeFactor = 4; // 4Ɨ4 tiles per merged tile -} else if (currentZoom === 1) { - dbZoom = 1; - mergeFactor = 2; // 2Ɨ2 tiles per merged tile -} else { - dbZoom = 2; - mergeFactor = 1; // 1Ɨ1 (no merging) -} - -// Calculate merged tile coordinates -const minMergedX = Math.floor(bounds.min_x / mergeFactor); -const maxMergedX = Math.floor(bounds.max_x / mergeFactor); - -// Calculate pixel bounds for merged tiles -const pixelMinX = mergedX * mergeFactor * tileSize; -const pixelMinY = mergedY * mergeFactor * tileSize; -``` - -## Results - -### Performance Improvements - -| Metric | Before | After | Improvement | -|--------|--------|-------|-------------| -| Initial Load (Zoom 0) | 345 requests | 31 requests | **91% faster** | -| Medium Zoom (Zoom 1) | 345 requests | 105 requests | **70% faster** | -| Full Detail (Zoom 2) | 345 requests | 345 requests | Baseline | - -### Storage - -- **Total Size**: 111.30 MB (all zoom levels) -- **Compression**: Lossless WebP -- **Quality**: No quality loss from merging (lossless throughout) - -### User Experience - -āœ… **Fast initial load** - Only 31 tiles at lowest zoom -āœ… **Smooth zooming** - Progressive detail with 3 zoom levels -āœ… **No gaps** - Missing tiles filled with black -āœ… **High quality** - Lossless compression preserves all detail -āœ… **Responsive** - Dramatic reduction in HTTP requests - -## Technical Considerations - -### Why Lossless WebP? - -- User requested no lossy compression for quality preservation -- WebP lossless is more efficient than PNG -- No generation of compression artifacts -- Maintains perfect quality for merged tiles - -### Missing Tiles - -- Handled by filling with black (Rgba `[0, 0, 0, 255]`) -- Alternative options: transparent, gray, or skip entirely -- Current approach ensures consistent tile grid - -### Merge Factor Choice - -- 4Ɨ4 for zoom 0: Aggressive merging for fast overview -- 2Ɨ2 for zoom 1: Balance between detail and performance -- 1Ɨ1 for zoom 2: Full original quality - -These factors were chosen based on: -- Map dimensions (30Ɨ17 tiles) -- Typical web map conventions -- Performance testing - -## Future Optimizations - -Potential improvements: - -1. **On-Demand Generation**: Generate merged tiles on first request instead of pre-generating -2. **Cache Headers**: Add HTTP caching headers for browser caching -3. **Progressive Loading**: Load center tiles first, then edges -4. **Tile Prioritization**: Load visible tiles before off-screen tiles -5. **WebP Quality Tuning**: Test near-lossless modes for even smaller files - -## Files Changed - -``` -cursebreaker-parser/ -ā”œā”€ā”€ migrations/ -│ └── 2026-01-10-120919-0000_create_merged_tiles/ -│ ā”œā”€ā”€ up.sql # NEW: Create merged_tiles table -│ └── down.sql # NEW: Drop merged_tiles table -ā”œā”€ā”€ src/ -│ ā”œā”€ā”€ bin/ -│ │ └── merge-tiles.rs # NEW: Tile merging tool -│ └── schema.rs # MODIFIED: Regenerated with merged_tiles -└── Cargo.toml # MODIFIED: Added chrono, merge-tiles bin - -cursebreaker-map/ -ā”œā”€ā”€ src/ -│ └── main.rs # MODIFIED: Serve merged tiles -ā”œā”€ā”€ static/ -│ └── map.js # MODIFIED: Request merged tiles -└── README.md # MODIFIED: Updated documentation - -cursebreaker.db # MODIFIED: Added merged_tiles table + data -``` - -## Running the Optimized Map - -### First Time Setup -```bash -# Generate merged tiles (one time, ~1.5 minutes) -cd cursebreaker-parser -cargo run --bin merge-tiles --release - -# Start server -cd ../cursebreaker-map -cargo run --release - -# Open http://127.0.0.1:3000 -``` - -### Subsequent Runs -```bash -# Just start server (merged tiles persisted in DB) -cd cursebreaker-map -cargo run --release -``` - -## Verification - -To verify the optimization is working: - -1. Open browser DevTools → Network tab -2. Load the map -3. Check number of tile requests: - - Zoom 0: Should see ~31 requests to `/api/tiles/0/...` - - Zoom 1: Should see ~105 requests to `/api/tiles/1/...` - - Zoom 2: Should see ~345 requests to `/api/tiles/2/...` -4. Console should show: `Loading X merged tiles` diff --git a/cursebreaker-parser/README.md b/cursebreaker-parser/README.md index a4d0e1f..7a5ebfc 100644 --- a/cursebreaker-parser/README.md +++ b/cursebreaker-parser/README.md @@ -45,7 +45,12 @@ The project provides multiple binaries to handle different parsing tasks. This a 2. **scene-parser** - Parses Unity scenes and extracts world resource locations - Slow execution (Unity project initialization) - Extracts InteractableResource components and their positions - - Saves to `world_resources` table (item_id and 2D coordinates) + - Saves to `world_resources` table (harvestable_id and 2D coordinates) + - Processes item icons for harvestables: + - Looks up the first item drop for each harvestable from `harvestable_drops` table + - Loads the icon from `Data/Textures/ItemIcons/{item_id}.png` + - Applies white outline (1px) and resizes to 64x64 + - Converts to WebP and stores in `resource_icons` table - Run this when scene files change ```bash cargo run --bin scene-parser @@ -87,6 +92,11 @@ The project provides multiple binaries to handle different parsing tasks. This a cargo run --bin verify-stats ``` +9. **verify-resource-icons** - Verifies resource icons for harvestables + ```bash + cargo run --bin verify-resource-icons + ``` + ### Building for Production Build specific binaries for release: @@ -185,6 +195,39 @@ for resource in copper_ore { See `examples/query_world_resources.rs` for a complete example. +### Querying Resource Icons + +```rust +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; + +// Connect to database +let mut conn = SqliteConnection::establish("../cursebreaker.db")?; + +// Define the structure +#[derive(Queryable, Debug)] +struct ResourceIcon { + item_id: i32, // Harvestable ID + name: String, // Harvestable name + icon_64: Vec, // WebP image data (64x64 with white border) +} + +// Query icon for a specific harvestable +use cursebreaker_parser::schema::resource_icons::dsl::*; + +let copper_icon = resource_icons + .filter(item_id.eq(2)) // Harvestable ID for Copper Ore + .first::(&mut conn)?; + +println!("Found icon for: {}", copper_icon.name); +println!("Icon size: {} bytes (WebP format)", copper_icon.icon_64.len()); + +// Save to file if needed +std::fs::write("copper_ore.webp", &copper_icon.icon_64)?; +``` + +See `examples/resource_icons_example.rs` for a complete example. + ### Additional Databases Similar APIs are available for other game data types: @@ -227,6 +270,7 @@ The project includes several example programs demonstrating different aspects of - **game_data_demo.rs** - Comprehensive demo loading and querying all game data types (Items, NPCs, Quests, Harvestables, Loot) - **item_database_demo.rs** - Focused on item database operations - **query_world_resources.rs** - Querying world resource locations from the database +- **resource_icons_example.rs** - Querying processed harvestable icons with white borders - **fast_travel_example.rs** - Working with fast travel locations - **maps_example.rs** - Map data handling - **player_houses_example.rs** - Player house management @@ -252,7 +296,8 @@ cursebreaker-parser/ │ │ ā”œā”€ā”€ verify-db.rs # Database verification │ │ ā”œā”€ā”€ verify-expanded-db.rs # Expanded database verification │ │ ā”œā”€ā”€ verify-images.rs # Image verification -│ │ └── verify-stats.rs # Stats verification +│ │ ā”œā”€ā”€ verify-stats.rs # Stats verification +│ │ └── verify-resource-icons.rs # Resource icons verification │ ā”œā”€ā”€ xml_parser.rs # XML parsing utilities │ ā”œā”€ā”€ image_processor.rs # Image processing utilities │ ā”œā”€ā”€ item_loader.rs # Item loading logic @@ -297,6 +342,7 @@ The parser uses Diesel for database operations with SQLite. Database migrations - Quest definitions and phases - Harvestable resources and drop tables - World resource locations from Unity scenes +- Resource icons for harvestables (64x64 WebP with white borders) - Minimap tiles and metadata - Shop inventories and pricing - Player houses and locations diff --git a/cursebreaker-parser/XML_PARSING.md b/cursebreaker-parser/XML_PARSING.md deleted file mode 100644 index 8eba99a..0000000 --- a/cursebreaker-parser/XML_PARSING.md +++ /dev/null @@ -1,442 +0,0 @@ -# XML Parsing in Cursebreaker Parser - -This document describes the XML parsing functionality added to the cursebreaker-parser project. - -## Overview - -The parser now supports loading game data from Cursebreaker's XML files and storing them in efficient data structures for runtime access and SQL database serialization. - -## Features - -- āœ… Parse Items, NPCs, Quests, and Harvestables XML files with full attribute and nested element support -- āœ… In-memory databases with fast lookups by ID, name, and various filters -- āœ… JSON serialization for SQL database storage -- āœ… Type-safe data structures with serde support -- āœ… Easy-to-use API with query methods -- āœ… Cross-referencing support between different data types - -## Quick Start - -### Loading Items - -```rust -use cursebreaker_parser::ItemDatabase; - -let item_db = ItemDatabase::load_from_xml("Data/XMLs/Items/Items.xml")?; -println!("Loaded {} items", item_db.len()); -``` - -### Querying Items - -```rust -// Get by ID -if let Some(item) = item_db.get_by_id(150) { - println!("Found: {}", item.name); -} - -// Get by category -let bows = item_db.get_by_category("bow"); - -// Get by slot -let weapons = item_db.get_by_slot("weapon"); - -// Get by skill requirement -let magic_items = item_db.get_by_skill("magic"); - -// Get all items -for item in item_db.all_items() { - println!("{}: {}", item.id, item.name); -} -``` - -### SQL Serialization - -```rust -// Prepare items for SQL insertion -let sql_data = item_db.prepare_for_sql(); - -for (id, name, json_data) in sql_data { - // INSERT INTO items (id, name, data) VALUES (?, ?, ?) - // Use your preferred SQL library to insert -} -``` - -## Data Structures - -### Item - -The main `Item` struct contains all item attributes from the XML: - -```rust -pub struct Item { - // Required - pub id: i32, - pub name: String, - - // Optional attributes - pub level: Option, - pub description: Option, - pub price: Option, - pub slot: Option, - pub category: Option, - pub skill: Option, - - // ... many more fields - - // Nested elements - pub stats: Vec, - pub crafting_recipes: Vec, - pub animations: Option, - pub generate_rules: Vec, -} -``` - -### ItemStat - -Represents item statistics: - -```rust -pub struct ItemStat { - // Damage - pub damagephysical: Option, - pub damagemagical: Option, - pub damageranged: Option, - - // Accuracy - pub accuracyphysical: Option, - pub accuracymagical: Option, - pub accuracyranged: Option, - - // Resistance - pub resistancephysical: Option, - pub resistancemagical: Option, - pub resistanceranged: Option, - - // Core stats - pub health: Option, - pub mana: Option, - pub manaregen: Option, - pub healing: Option, - - // Harvesting - pub harvestingspeedwoodcutting: Option, -} -``` - -## Example Programs - -Run the demos to see all features in action: - -```bash -# Items only -cargo run --example item_database_demo - -# All game data (Items, NPCs, Quests, Harvestables) -cargo run --example game_data_demo -``` - -## Loading NPCs - -```rust -use cursebreaker_parser::NpcDatabase; - -let npc_db = NpcDatabase::load_from_xml("Data/XMLs/Npcs/NPCInfo.xml")?; -println!("Loaded {} NPCs", npc_db.len()); -``` - -### Querying NPCs - -```rust -// Get by ID -if let Some(npc) = npc_db.get_by_id(1) { - println!("Found: {}", npc.name); -} - -// Get hostile NPCs -let hostile = npc_db.get_hostile(); - -// Get interactable NPCs -let interactable = npc_db.get_interactable(); - -// Get NPCs by tag -let undead = npc_db.get_by_tag("Undead"); - -// Get shopkeepers -let shops = npc_db.get_shopkeepers(); -``` - -## Loading Quests - -```rust -use cursebreaker_parser::QuestDatabase; - -let quest_db = QuestDatabase::load_from_xml("Data/XMLs/Quests/Quests.xml")?; -println!("Loaded {} quests", quest_db.len()); -``` - -### Querying Quests - -```rust -// Get by ID -if let Some(quest) = quest_db.get_by_id(1) { - println!("Quest: {}", quest.name); - println!("Phases: {}", quest.phase_count()); -} - -// Get main quests -let main_quests = quest_db.get_main_quests(); - -// Get side quests -let side_quests = quest_db.get_side_quests(); - -// Get hidden quests -let hidden = quest_db.get_hidden_quests(); -``` - -## Loading Harvestables - -```rust -use cursebreaker_parser::HarvestableDatabase; - -let harvestable_db = HarvestableDatabase::load_from_xml("Data/XMLs/Harvestables/HarvestableInfo.xml")?; -println!("Loaded {} harvestables", harvestable_db.len()); -``` - -### Querying Harvestables - -```rust -// Get by type ID -if let Some(harvestable) = harvestable_db.get_by_typeid(1) { - println!("Found: {}", harvestable.name); -} - -// Get by skill -let woodcutting = harvestable_db.get_by_skill("Woodcutting"); -let mining = harvestable_db.get_by_skill("mining"); -let fishing = harvestable_db.get_by_skill("Fishing"); - -// Get trees (harvestables with tree=1) -let trees = harvestable_db.get_trees(); - -// Get by tool requirement -let hatchet_nodes = harvestable_db.get_by_tool("hatchet"); -let pickaxe_nodes = harvestable_db.get_by_tool("pickaxe"); - -// Get by level range -let beginner = harvestable_db.get_by_level_range(1, 10); -let advanced = harvestable_db.get_by_level_range(50, 100); -``` - -## Loading Loot Tables - -```rust -use cursebreaker_parser::LootDatabase; - -let loot_db = LootDatabase::load_from_xml("Data/XMLs/Loot/Loot.xml")?; -println!("Loaded {} loot tables", loot_db.len()); -``` - -### Querying Loot Tables - -```rust -// Get all loot tables for a specific NPC -let npc_id = 45; -let tables = loot_db.get_tables_for_npc(npc_id); - -// Get all drops for a specific NPC -let drops = loot_db.get_drops_for_npc(npc_id); -for drop in drops { - println!("Item ID: {}, Rate: {:?}", drop.item, drop.rate); -} - -// Find which NPCs drop a specific item -let item_id = 180; -let npcs = loot_db.get_npcs_dropping_item(item_id); -println!("Item {} drops from {} NPCs", item_id, npcs.len()); - -// Get all tables with conditional drops (checks field) -let conditional = loot_db.get_conditional_tables(); - -// Get all tables with guaranteed drops (rate = 1) -let guaranteed = loot_db.get_tables_with_guaranteed_drops(); - -// Get all unique item IDs that can drop -let droppable_items = loot_db.get_all_droppable_items(); - -// Get all NPCs that have loot tables -let npcs_with_loot = loot_db.get_all_npcs_with_loot(); -``` - -## Cross-referencing Data - -```rust -// Find items rewarded by quests -for quest in quest_db.all_quests() { - for reward in &quest.rewards { - if let Some(item_id) = reward.item { - if let Some(item) = item_db.get_by_id(item_id) { - println!("Quest '{}' rewards: {}", quest.name, item.name); - } - } - } -} - -// Find NPCs that give quests -for npc in npc_db.all_npcs() { - if !npc.questmarkers.is_empty() { - println!("NPC '{}' has {} quest markers", npc.name, npc.questmarkers.len()); - } -} - -// Find items that drop from harvestables -for harvestable in harvestable_db.all_harvestables() { - for drop in &harvestable.drops { - if let Some(item) = item_db.get_by_id(drop.id) { - println!("'{}' drops: {} (rate: {})", - harvestable.name, item.name, drop.droprate.unwrap_or(0)); - } - } -} - -// Find what items an NPC drops -let npc_id = 45; -if let Some(npc) = npc_db.get_by_id(npc_id) { - let drops = loot_db.get_drops_for_npc(npc_id); - println!("NPC '{}' drops {} items:", npc.name, drops.len()); - for drop in drops { - if let Some(item) = item_db.get_by_id(drop.item) { - println!(" - {}", item.name); - } - } -} - -// Find which NPCs drop a specific item -let item_id = 180; -if let Some(item) = item_db.get_by_id(item_id) { - let npcs = loot_db.get_npcs_dropping_item(item_id); - println!("Item '{}' drops from:", item.name); - for npc_id in npcs { - if let Some(npc) = npc_db.get_by_id(npc_id) { - println!(" - {}", npc.name); - } - } -} -``` - -## Statistics from XML Files - -When loaded from `/home/connor/repos/CBAssets/Data/XMLs/`: - -### Items.xml -- **Total Items**: 1,360 -- **Weapons**: 166 -- **Armor**: 148 -- **Consumables**: 294 -- **Trinkets**: 59 -- **Bows**: 18 -- **Magic Items**: 76 - -### NPCs/NPCInfo.xml -- **Total NPCs**: 1,242 -- **Hostile NPCs**: 328 -- **Interactable NPCs**: 512 -- **Undead**: 71 -- **Predators**: 13 -- **Quest Givers**: 108 - -### Quests/Quests.xml -- **Total Quests**: 108 -- **Main Quests**: 19 -- **Side Quests**: 89 -- **Hidden Quests**: 2 -- **Unique Quest Reward Items**: 70 - -### Harvestables/HarvestableInfo.xml -- **Total Harvestables**: 96 -- **Trees**: 9 -- **Woodcutting**: 10 -- **Mining**: 11 -- **Fishing**: 11 -- **Alchemy**: 50 -- **Level 1-10**: 31 -- **Level 11-50**: 37 -- **Level 51-100**: 28 -- **Unique Items from Harvestables**: 98 - -### Loot/Loot.xml -- **Total Loot Tables**: 175 -- **NPCs with Loot**: 267 -- **Droppable Items**: 405 -- **Tables with Conditional Drops**: 33 -- **Tables with Guaranteed Drops**: Multiple tables include guaranteed (rate=1) drops - -## File Structure - -``` -cursebreaker-parser/ -ā”œā”€ā”€ src/ -│ ā”œā”€ā”€ lib.rs # Library exports -│ ā”œā”€ā”€ main.rs # Main binary (Unity + XML parsing) -│ ā”œā”€ā”€ types/ -│ │ ā”œā”€ā”€ mod.rs -│ │ ā”œā”€ā”€ item.rs # Item data structures -│ │ ā”œā”€ā”€ npc.rs # NPC data structures -│ │ ā”œā”€ā”€ quest.rs # Quest data structures -│ │ ā”œā”€ā”€ harvestable.rs # Harvestable data structures -│ │ ā”œā”€ā”€ loot.rs # Loot table data structures -│ │ └── interactable_resource.rs -│ ā”œā”€ā”€ xml_parser.rs # XML parsing logic (all types) -│ ā”œā”€ā”€ item_database.rs # ItemDatabase for runtime access -│ ā”œā”€ā”€ npc_database.rs # NpcDatabase for runtime access -│ ā”œā”€ā”€ quest_database.rs # QuestDatabase for runtime access -│ ā”œā”€ā”€ harvestable_database.rs # HarvestableDatabase for runtime access -│ └── loot_database.rs # LootDatabase for runtime access -└── examples/ - ā”œā”€ā”€ item_database_demo.rs # Items usage example - └── game_data_demo.rs # Full game data example -``` - -## Dependencies Added - -```toml -quick-xml = "0.37" # XML parsing -serde = { version = "1.0", features = ["derive"] } # Serialization -serde_json = "1.0" # JSON serialization -diesel = { version = "2.2", features = ["sqlite"], optional = true } # SQL (optional) -thiserror = "1.0" # Error handling -``` - -## Completed Features - -- āœ… Items (`/XMLs/Items/Items.xml`) -- āœ… NPCs (`/XMLs/Npcs/NPCInfo.xml`) -- āœ… Quests (`/XMLs/Quests/Quests.xml`) -- āœ… Harvestables (`/XMLs/Harvestables/HarvestableInfo.xml`) -- āœ… Loot tables (`/XMLs/Loot/Loot.xml`) - -## Future Enhancements - -The same pattern can be extended to parse other XML files: - -- [ ] Maps (`/XMLs/Maps/*.xml`) -- [ ] Dialogue (`/XMLs/Dialogue/*.xml`) -- [ ] Events (`/XMLs/Events/*.xml`) -- [ ] Achievements (`/XMLs/Achievements/*.xml`) -- [ ] Traits (`/XMLs/Traits/*.xml`) -- [ ] Shops (`/XMLs/Shops/*.xml`) - -Each follows the same pattern: -1. Define data structures in `src/types/` -2. Create parser in `src/xml_parser.rs` -3. Create database wrapper for runtime access -4. Add to `lib.rs` exports - -## Integration with Unity Parser - -The main binary (`src/main.rs`) demonstrates integration of both systems: - -1. Load game data from XML files (Items, etc.) -2. Parse Unity scenes for game objects -3. Cross-reference data (e.g., item IDs in loot spawners) - -This creates a complete game data pipeline from source files to runtime. diff --git a/cursebreaker-parser/examples/resource_icons_example.rs b/cursebreaker-parser/examples/resource_icons_example.rs new file mode 100644 index 0000000..bda2cb2 --- /dev/null +++ b/cursebreaker-parser/examples/resource_icons_example.rs @@ -0,0 +1,53 @@ +//! Example: Query resource icons from the database +//! +//! This example shows how to retrieve processed resource icons for harvestables. +//! Icons are 64x64 WebP images with white borders. + +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; +use std::env; + +fn main() -> Result<(), Box> { + // Connect to database + let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string()); + let mut conn = SqliteConnection::establish(&database_url)?; + + // Define the structure + #[derive(Queryable, Debug)] + struct ResourceIcon { + item_id: i32, + name: String, + icon_64: Vec, + } + + // Import schema + use cursebreaker_parser::schema::resource_icons::dsl::*; + + // Query all resource icons + let icons = resource_icons.load::(&mut conn)?; + + println!("šŸ“¦ Resource Icons Database"); + println!("========================\n"); + println!("Total icons: {}\n", icons.len()); + + for icon in icons { + println!("Harvestable ID: {}", icon.item_id); + println!(" Name: {}", icon.name); + println!(" Icon size: {} bytes (WebP format, 64x64 with white border)", icon.icon_64.len()); + println!(); + } + + // Example: Get icon for a specific harvestable + println!("\nšŸ” Looking up Copper Ore (harvestable_id = 2):"); + let copper_icon = resource_icons + .filter(item_id.eq(2)) + .first::(&mut conn)?; + + println!(" Name: {}", copper_icon.name); + println!(" Icon size: {} bytes", copper_icon.icon_64.len()); + + // You can save the icon to a file for testing: + // std::fs::write("copper_ore.webp", &copper_icon.icon_64)?; + + Ok(()) +} diff --git a/cursebreaker-parser/migrations/2026-01-12-063636-0000_expand_harvestables/down.sql b/cursebreaker-parser/migrations/2026-01-12-063636-0000_expand_harvestables/down.sql new file mode 100644 index 0000000..dc67c85 --- /dev/null +++ b/cursebreaker-parser/migrations/2026-01-12-063636-0000_expand_harvestables/down.sql @@ -0,0 +1,10 @@ +-- Revert to the simple harvestables table +DROP TABLE IF EXISTS harvestable_drops; +DROP TABLE IF EXISTS harvestables; + +CREATE TABLE harvestables ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + data TEXT NOT NULL +); + diff --git a/cursebreaker-parser/migrations/2026-01-12-063636-0000_expand_harvestables/up.sql b/cursebreaker-parser/migrations/2026-01-12-063636-0000_expand_harvestables/up.sql new file mode 100644 index 0000000..7b8d5c6 --- /dev/null +++ b/cursebreaker-parser/migrations/2026-01-12-063636-0000_expand_harvestables/up.sql @@ -0,0 +1,39 @@ +-- Restructure harvestables table to store expanded data +DROP TABLE IF EXISTS harvestables; + +CREATE TABLE harvestables ( + id INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL, + comment TEXT NOT NULL, + level INTEGER NOT NULL, + skill TEXT NOT NULL, + tool TEXT NOT NULL, + min_health INTEGER NOT NULL, + max_health INTEGER NOT NULL, + harvesttime INTEGER NOT NULL, + hittime INTEGER NOT NULL, + respawntime INTEGER NOT NULL +); + +-- Create harvestable_drops table +CREATE TABLE harvestable_drops ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + harvestable_id INTEGER NOT NULL, + item_id INTEGER NOT NULL, + minamount INTEGER NOT NULL, + maxamount INTEGER NOT NULL, + droprate INTEGER NOT NULL, + droprateboost INTEGER NOT NULL, + amountboost INTEGER NOT NULL, + comment TEXT NOT NULL, + FOREIGN KEY (harvestable_id) REFERENCES harvestables(id), + FOREIGN KEY (item_id) REFERENCES items(id) +); + +CREATE INDEX idx_harvestable_drops_harvestable_id ON harvestable_drops(harvestable_id); +CREATE INDEX idx_harvestable_drops_item_id ON harvestable_drops(item_id); +CREATE INDEX idx_harvestables_skill ON harvestables(skill); +CREATE INDEX idx_harvestables_tool ON harvestables(tool); +CREATE INDEX idx_harvestables_level ON harvestables(level); + diff --git a/cursebreaker-parser/src/bin/scene-parser.rs b/cursebreaker-parser/src/bin/scene-parser.rs index 18622ce..de6741c 100644 --- a/cursebreaker-parser/src/bin/scene-parser.rs +++ b/cursebreaker-parser/src/bin/scene-parser.rs @@ -122,19 +122,19 @@ fn process_item_icons( conn: &mut SqliteConnection, scene: &unity_parser::UnityScene, ) -> Result<(), Box> { - use cursebreaker_parser::schema::{resource_icons, items}; + use cursebreaker_parser::schema::{resource_icons, items, harvestables, harvestable_drops}; - // Collect unique item IDs from resources - let mut unique_items: HashMap = HashMap::new(); + // Collect unique harvestable IDs from resources + let mut unique_harvestables: HashMap = HashMap::new(); scene.world .query_all::<(&InteractableResource, &unity_parser::GameObject)>() .for_each(|(resource, object)| { - unique_items.entry(resource.type_id as i32) + unique_harvestables.entry(resource.type_id as i32) .or_insert_with(|| object.name.to_string()); }); - info!(" Found {} unique resource types", unique_items.len()); + info!(" Found {} unique harvestable types", unique_harvestables.len()); // Clear existing resource icons (regenerated each run) diesel::delete(resource_icons::table).execute(conn)?; @@ -146,22 +146,46 @@ fn process_item_icons( let mut processed_count = 0; let mut failed_count = 0; - // Process each unique item - for (item_id, default_name) in unique_items.iter() { - // Try to get the actual item name from the items table - let item_name: String = items::table - .filter(items::id.eq(item_id)) - .select(items::name) + // Process each unique harvestable + for (harvestable_id, default_name) in unique_harvestables.iter() { + // Get the harvestable name + let harvestable_name: String = harvestables::table + .filter(harvestables::id.eq(harvestable_id)) + .select(harvestables::name) .first(conn) .unwrap_or_else(|_| default_name.clone()); - // Construct icon path + // Get the first item drop for this harvestable + let item_id_result: Result = harvestable_drops::table + .filter(harvestable_drops::harvestable_id.eq(harvestable_id)) + .select(harvestable_drops::item_id) + .order(harvestable_drops::id.asc()) + .first(conn); + + let item_id = match item_id_result { + Ok(id) => id, + Err(_) => { + warn!(" āš ļø No drops found for harvestable {} ({})", harvestable_id, harvestable_name); + failed_count += 1; + continue; + } + }; + + // Get the item name + let item_name: String = items::table + .filter(items::id.eq(&item_id)) + .select(items::name) + .first(conn) + .unwrap_or_else(|_| format!("Item {}", item_id)); + + // Construct icon path using the item_id from the drop let icon_path = PathBuf::from(cb_assets_path) .join("Data/Textures/ItemIcons") .join(format!("{}.png", item_id)); if !icon_path.exists() { - warn!(" āš ļø Icon not found for item {} ({}): {}", item_id, item_name, icon_path.display()); + warn!(" āš ļø Icon not found for harvestable {} ({}) -> item {} ({}): {}", + harvestable_id, harvestable_name, item_id, item_name, icon_path.display()); failed_count += 1; continue; } @@ -170,38 +194,38 @@ fn process_item_icons( match processor.process_image(&icon_path, &[64], None, Some(&outline_config)) { Ok(processed) => { if let Some(icon_data) = processed.get(64) { - // Insert into database + // Insert into database using harvestable_id as the key match diesel::insert_into(resource_icons::table) .values(( - resource_icons::item_id.eq(item_id), - resource_icons::name.eq(&item_name), + resource_icons::item_id.eq(harvestable_id), + resource_icons::name.eq(&harvestable_name), resource_icons::icon_64.eq(icon_data.as_slice()), )) .execute(conn) { Ok(_) => { - info!(" āœ“ Processed icon for item {} ({}): {} bytes", - item_id, item_name, icon_data.len()); + info!(" āœ“ Harvestable {} ({}) -> Item {} ({}): {} bytes", + harvestable_id, harvestable_name, item_id, item_name, icon_data.len()); processed_count += 1; } Err(e) => { - warn!(" āš ļø Failed to insert icon for item {} ({}): {}", - item_id, item_name, e); + warn!(" āš ļø Failed to insert icon for harvestable {} ({}): {}", + harvestable_id, harvestable_name, e); failed_count += 1; } } } } Err(e) => { - warn!(" āš ļø Failed to process icon for item {} ({}): {}", - item_id, item_name, e); + warn!(" āš ļø Failed to process icon for harvestable {} ({}) -> item {} ({}): {}", + harvestable_id, harvestable_name, item_id, item_name, e); failed_count += 1; } } } - info!("āœ… Processed {} item icons ({} succeeded, {} failed)", - unique_items.len(), processed_count, failed_count); + info!("āœ… Processed {} harvestable icons ({} succeeded, {} failed)", + unique_harvestables.len(), processed_count, failed_count); Ok(()) } diff --git a/cursebreaker-parser/src/bin/xml-parser.rs b/cursebreaker-parser/src/bin/xml-parser.rs index 77ee15c..b93c2c5 100644 --- a/cursebreaker-parser/src/bin/xml-parser.rs +++ b/cursebreaker-parser/src/bin/xml-parser.rs @@ -5,7 +5,7 @@ //! - Populating the SQLite database with the parsed data //! - Generating statistics about the loaded data -use cursebreaker_parser::ItemDatabase; +use cursebreaker_parser::{ItemDatabase, HarvestableDatabase}; use log::{info, warn, LevelFilter}; use unity_parser::log::DedupLogger; use diesel::prelude::*; diff --git a/cursebreaker-parser/src/databases/harvestable_database.rs b/cursebreaker-parser/src/databases/harvestable_database.rs index 798f17d..204b574 100644 --- a/cursebreaker-parser/src/databases/harvestable_database.rs +++ b/cursebreaker-parser/src/databases/harvestable_database.rs @@ -62,27 +62,21 @@ impl HarvestableDatabase { /// Get harvestables by skill pub fn get_by_skill(&self, skill: &str) -> Vec<&Harvestable> { + use crate::types::SkillType; + let skill_type = skill.parse::().unwrap_or(SkillType::None); self.harvestables .iter() - .filter(|h| { - h.skill - .as_ref() - .map(|s| s.eq_ignore_ascii_case(skill)) - .unwrap_or(false) - }) + .filter(|h| h.skill == skill_type) .collect() } /// Get harvestables that require a specific tool pub fn get_by_tool(&self, tool: &str) -> Vec<&Harvestable> { + use crate::types::Tool; + let tool_type = tool.parse::().unwrap_or(Tool::None); self.harvestables .iter() - .filter(|h| { - h.tool - .as_ref() - .map(|t| t.eq_ignore_ascii_case(tool)) - .unwrap_or(false) - }) + .filter(|h| h.tool == tool_type) .collect() } @@ -106,11 +100,7 @@ impl HarvestableDatabase { pub fn get_by_level_range(&self, min_level: i32, max_level: i32) -> Vec<&Harvestable> { self.harvestables .iter() - .filter(|h| { - h.level - .map(|l| l >= min_level && l <= max_level) - .unwrap_or(false) - }) + .filter(|h| h.level >= min_level && h.level <= max_level) .collect() } @@ -126,38 +116,136 @@ impl HarvestableDatabase { /// Prepare harvestables for SQL insertion (deprecated - use save_to_db instead) #[deprecated(note = "Use save_to_db() to save directly to SQLite database")] - pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> { + #[allow(deprecated)] + pub fn prepare_for_sql(&self) -> Vec<(i32, String, String, String, i32, String, String, i32, i32, i32, i32, i32)> { + use crate::types::{SkillType, Tool}; + self.harvestables .iter() .map(|harvestable| { - let json = serde_json::to_string(harvestable).unwrap_or_else(|_| "{}".to_string()); - (harvestable.typeid, harvestable.name.clone(), json) + let skill_str = match harvestable.skill { + SkillType::None => "none", + SkillType::Swordsmanship => "swordsmanship", + SkillType::Archery => "archery", + SkillType::Magic => "magic", + SkillType::Defence => "defence", + SkillType::Mining => "mining", + SkillType::Woodcutting => "woodcutting", + SkillType::Fishing => "fishing", + SkillType::Cooking => "cooking", + SkillType::Carpentry => "carpentry", + SkillType::Blacksmithy => "blacksmithy", + SkillType::Tailoring => "tailoring", + SkillType::Alchemy => "alchemy", + }.to_string(); + + let tool_str = match harvestable.tool { + Tool::None => "none", + Tool::Pickaxe => "pickaxe", + Tool::Hatchet => "hatchet", + Tool::Scythe => "scythe", + Tool::Hammer => "hammer", + Tool::Shears => "shears", + Tool::FishingRod => "fishingrod", + }.to_string(); + + ( + harvestable.typeid, + harvestable.name.clone(), + harvestable.desc.clone(), + harvestable.comment.clone(), + harvestable.level, + skill_str, + tool_str, + harvestable.min_health, + harvestable.max_health, + harvestable.harvesttime, + harvestable.hittime, + harvestable.respawntime, + ) }) .collect() } /// Save all harvestables to SQLite database pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { - use crate::schema::harvestables; + use crate::schema::{harvestables, harvestable_drops}; + use crate::types::{SkillType, Tool}; - let records: Vec<_> = self - .harvestables - .iter() - .map(|harvestable| { - let json = serde_json::to_string(harvestable).unwrap_or_else(|_| "{}".to_string()); - ( - harvestables::id.eq(harvestable.typeid), - harvestables::name.eq(&harvestable.name), - harvestables::data.eq(json), - ) - }) - .collect(); + // Clear existing data + diesel::delete(harvestable_drops::table).execute(conn)?; + diesel::delete(harvestables::table).execute(conn)?; let mut count = 0; - for record in records { + for harvestable in &self.harvestables { + // Convert enums to strings for database storage + let skill_str = match harvestable.skill { + SkillType::None => "none", + SkillType::Swordsmanship => "swordsmanship", + SkillType::Archery => "archery", + SkillType::Magic => "magic", + SkillType::Defence => "defence", + SkillType::Mining => "mining", + SkillType::Woodcutting => "woodcutting", + SkillType::Fishing => "fishing", + SkillType::Cooking => "cooking", + SkillType::Carpentry => "carpentry", + SkillType::Blacksmithy => "blacksmithy", + SkillType::Tailoring => "tailoring", + SkillType::Alchemy => "alchemy", + }; + + let tool_str = match harvestable.tool { + Tool::None => "none", + Tool::Pickaxe => "pickaxe", + Tool::Hatchet => "hatchet", + Tool::Scythe => "scythe", + Tool::Hammer => "hammer", + Tool::Shears => "shears", + Tool::FishingRod => "fishingrod", + }; + + // Insert harvestable diesel::insert_into(harvestables::table) - .values(&record) + .values(( + harvestables::id.eq(harvestable.typeid), + harvestables::name.eq(&harvestable.name), + harvestables::description.eq(&harvestable.desc), + harvestables::comment.eq(&harvestable.comment), + harvestables::level.eq(harvestable.level), + harvestables::skill.eq(skill_str), + harvestables::tool.eq(tool_str), + harvestables::min_health.eq(harvestable.min_health), + harvestables::max_health.eq(harvestable.max_health), + harvestables::harvesttime.eq(harvestable.harvesttime), + harvestables::hittime.eq(harvestable.hittime), + harvestables::respawntime.eq(harvestable.respawntime), + )) .execute(conn)?; + + // Insert drops + for drop in &harvestable.drops { + // Try to insert, but skip if foreign key constraint fails (item doesn't exist) + let insert_result = diesel::insert_into(harvestable_drops::table) + .values(( + harvestable_drops::harvestable_id.eq(harvestable.typeid), + harvestable_drops::item_id.eq(drop.id), + harvestable_drops::minamount.eq(drop.minamount), + harvestable_drops::maxamount.eq(drop.maxamount), + harvestable_drops::droprate.eq(drop.droprate), + harvestable_drops::droprateboost.eq(drop.droprateboost), + harvestable_drops::amountboost.eq(drop.amountboost), + harvestable_drops::comment.eq(&drop.comment), + )) + .execute(conn); + + // Log warning if insert failed but continue + if let Err(e) = insert_result { + eprintln!("Warning: Failed to insert drop for harvestable {} (item {}): {}", + harvestable.typeid, drop.id, e); + } + } + count += 1; } @@ -166,22 +254,91 @@ impl HarvestableDatabase { /// Load all harvestables from SQLite database pub fn load_from_db(conn: &mut SqliteConnection) -> Result { - use crate::schema::harvestables::dsl::*; + use crate::schema::{harvestables, harvestable_drops}; + use crate::types::{Harvestable, HarvestableDrop, SkillType, Tool}; + use diesel::prelude::*; #[derive(Queryable)] struct HarvestableRecord { - id: Option, + id: i32, name: String, - data: String, + description: String, + comment: String, + level: i32, + skill: String, + tool: String, + min_health: i32, + max_health: i32, + harvesttime: i32, + hittime: i32, + respawntime: i32, } - let records = harvestables.load::(conn)?; + #[derive(Queryable)] + struct HarvestableDropRecord { + id: Option, + harvestable_id: i32, + item_id: i32, + minamount: i32, + maxamount: i32, + droprate: i32, + droprateboost: i32, + amountboost: i32, + comment: String, + } + + let harv_records = harvestables::table.load::(conn)?; + let drop_records = harvestable_drops::table.load::(conn)?; let mut loaded_harvestables = Vec::new(); - for record in records { - if let Ok(harvestable) = serde_json::from_str::(&record.data) { - loaded_harvestables.push(harvestable); + for record in harv_records { + let mut harvestable = Harvestable { + typeid: record.id, + name: record.name, + actionname: String::new(), + desc: record.description, + comment: record.comment, + level: record.level, + skill: record.skill.parse().unwrap_or(SkillType::None), + tool: record.tool.parse().unwrap_or(Tool::None), + min_health: record.min_health, + max_health: record.max_health, + harvesttime: record.harvesttime, + hittime: record.hittime, + respawntime: record.respawntime, + harvestsfx: String::new(), + endsfx: String::new(), + receiveitemsfx: String::new(), + animation: String::new(), + takehitanimation: String::new(), + endgfx: String::new(), + tree: false, + hidemilestone: false, + nohighlight: false, + hideminimap: false, + noleftclickinteract: false, + interactdistance: String::new(), + drops: Vec::new(), + }; + + // Add drops for this harvestable + for drop_rec in &drop_records { + if drop_rec.harvestable_id == record.id { + harvestable.drops.push(HarvestableDrop { + id: drop_rec.item_id, + minamount: drop_rec.minamount, + maxamount: drop_rec.maxamount, + droprate: drop_rec.droprate, + droprateboost: drop_rec.droprateboost, + amountboost: drop_rec.amountboost, + checks: String::new(), + comment: drop_rec.comment.clone(), + dontconsumehealth: false, + }); + } } + + loaded_harvestables.push(harvestable); } let mut db = Self::new(); diff --git a/cursebreaker-parser/src/schema.rs b/cursebreaker-parser/src/schema.rs index b540eb4..033c8f6 100644 --- a/cursebreaker-parser/src/schema.rs +++ b/cursebreaker-parser/src/schema.rs @@ -31,10 +31,33 @@ diesel::table! { } diesel::table! { - harvestables (id) { + harvestable_drops (id) { id -> Nullable, + harvestable_id -> Integer, + item_id -> Integer, + minamount -> Integer, + maxamount -> Integer, + droprate -> Integer, + droprateboost -> Integer, + amountboost -> Integer, + comment -> Text, + } +} + +diesel::table! { + harvestables (id) { + id -> Integer, name -> Text, - data -> Text, + description -> Text, + comment -> Text, + level -> Integer, + skill -> Text, + tool -> Text, + min_health -> Integer, + max_health -> Integer, + harvesttime -> Integer, + hittime -> Integer, + respawntime -> Integer, } } @@ -174,12 +197,15 @@ diesel::table! { diesel::joinable!(crafting_recipe_items -> crafting_recipes (recipe_id)); diesel::joinable!(crafting_recipe_items -> items (item_id)); diesel::joinable!(crafting_recipes -> items (product_item_id)); +diesel::joinable!(harvestable_drops -> harvestables (harvestable_id)); +diesel::joinable!(harvestable_drops -> items (item_id)); diesel::joinable!(item_stats -> items (item_id)); diesel::allow_tables_to_appear_in_same_query!( crafting_recipe_items, crafting_recipes, fast_travel_locations, + harvestable_drops, harvestables, item_stats, items, diff --git a/cursebreaker-parser/src/types/cursebreaker/harvestable.rs b/cursebreaker-parser/src/types/cursebreaker/harvestable.rs index 7ddea88..df2b433 100644 --- a/cursebreaker-parser/src/types/cursebreaker/harvestable.rs +++ b/cursebreaker-parser/src/types/cursebreaker/harvestable.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use super::item::{SkillType, Tool}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Harvestable { @@ -7,40 +8,41 @@ pub struct Harvestable { pub name: String, // Basic attributes - pub actionname: Option, - pub desc: Option, - pub comment: Option, - pub level: Option, - pub skill: Option, - pub tool: Option, + pub actionname: String, + pub desc: String, + pub comment: String, + pub level: i32, + pub skill: SkillType, + pub tool: Tool, - // Health (can be range like "3-5" or single value) - pub health: Option, + // Health + pub min_health: i32, + pub max_health: i32, // Timing - pub harvesttime: Option, - pub hittime: Option, - pub respawntime: Option, + pub harvesttime: i32, + pub hittime: i32, + pub respawntime: i32, // Audio - pub harvestsfx: Option, - pub endsfx: Option, - pub receiveitemsfx: Option, + pub harvestsfx: String, + pub endsfx: String, + pub receiveitemsfx: String, // Visuals - pub animation: Option, - pub takehitanimation: Option, - pub endgfx: Option, + pub animation: String, + pub takehitanimation: String, + pub endgfx: String, // Behavior flags - pub tree: Option, - pub hidemilestone: Option, - pub nohighlight: Option, - pub hideminimap: Option, - pub noleftclickinteract: Option, + pub tree: bool, + pub hidemilestone: bool, + pub nohighlight: bool, + pub hideminimap: bool, + pub noleftclickinteract: bool, // Interaction - pub interactdistance: Option, + pub interactdistance: String, // Drops pub drops: Vec, @@ -49,14 +51,14 @@ pub struct Harvestable { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HarvestableDrop { pub id: i32, - pub minamount: Option, - pub maxamount: Option, - pub droprate: Option, - pub droprateboost: Option, - pub amountboost: Option, - pub checks: Option, - pub comment: Option, - pub dontconsumehealth: Option, + pub minamount: i32, + pub maxamount: i32, + pub droprate: i32, + pub droprateboost: i32, + pub amountboost: i32, + pub checks: String, + pub comment: String, + pub dontconsumehealth: bool, } impl Harvestable { @@ -64,45 +66,46 @@ impl Harvestable { Self { typeid, name, - actionname: None, - desc: None, - comment: None, - level: None, - skill: None, - tool: None, - health: None, - harvesttime: None, - hittime: None, - respawntime: None, - harvestsfx: None, - endsfx: None, - receiveitemsfx: None, - animation: None, - takehitanimation: None, - endgfx: None, - tree: None, - hidemilestone: None, - nohighlight: None, - hideminimap: None, - noleftclickinteract: None, - interactdistance: None, + actionname: String::new(), + desc: String::new(), + comment: String::new(), + level: 0, + skill: SkillType::None, + tool: Tool::None, + min_health: 0, + max_health: 0, + harvesttime: 0, + hittime: 0, + respawntime: 0, + harvestsfx: String::new(), + endsfx: String::new(), + receiveitemsfx: String::new(), + animation: String::new(), + takehitanimation: String::new(), + endgfx: String::new(), + tree: false, + hidemilestone: false, + nohighlight: false, + hideminimap: false, + noleftclickinteract: false, + interactdistance: String::new(), drops: Vec::new(), } } /// Check if this is a tree pub fn is_tree(&self) -> bool { - self.tree == Some(1) + self.tree } /// Check if this requires a tool pub fn requires_tool(&self) -> bool { - self.tool.is_some() + !matches!(self.tool, Tool::None) } /// Get the skill associated with this harvestable - pub fn get_skill(&self) -> Option<&str> { - self.skill.as_deref() + pub fn get_skill(&self) -> SkillType { + self.skill } /// Get all item IDs that can drop from this harvestable diff --git a/cursebreaker-parser/src/xml_parser.rs b/cursebreaker-parser/src/xml_parser.rs index 8b1a061..875afa1 100644 --- a/cursebreaker-parser/src/xml_parser.rs +++ b/cursebreaker-parser/src/xml_parser.rs @@ -9,6 +9,7 @@ use crate::types::{ PlayerHouse, Trait, TraitTrainer, Shop, ShopItem, + SkillType, Tool, }; use quick_xml::events::Event; use quick_xml::reader::Reader; @@ -180,6 +181,20 @@ fn parse_stat(attrs: &HashMap) -> ItemStat { } } +/// Parse health range string like "3-5" or "3" into (min, max) +fn parse_health_range(health_str: &str) -> (i32, i32) { + if let Some(dash_pos) = health_str.find('-') { + let min_str = &health_str[..dash_pos]; + let max_str = &health_str[dash_pos + 1..]; + let min = min_str.trim().parse().unwrap_or(0); + let max = max_str.trim().parse().unwrap_or(0); + (min, max) + } else { + let val = health_str.trim().parse().unwrap_or(0); + (val, val) + } +} + // ============================================================================ // NPC Parser // ============================================================================ @@ -549,45 +564,49 @@ pub fn parse_harvestables_xml>(path: P) -> Result>(path: P) -> Result() { let drop = HarvestableDrop { id, - minamount: attrs.get("minamount").and_then(|v| v.parse().ok()), - maxamount: attrs.get("maxamount").and_then(|v| v.parse().ok()), - droprate: attrs.get("droprate").and_then(|v| v.parse().ok()), - droprateboost: attrs.get("droprateboost").and_then(|v| v.parse().ok()), - amountboost: attrs.get("amountboost").and_then(|v| v.parse().ok()), - checks: attrs.get("checks").cloned(), - comment: attrs.get("comment").cloned(), - dontconsumehealth: attrs.get("dontconsumehealth").and_then(|v| v.parse().ok()), + minamount: attrs.get("minamount").and_then(|v| v.parse().ok()).unwrap_or(0), + maxamount: attrs.get("maxamount").and_then(|v| v.parse().ok()).unwrap_or(0), + droprate: attrs.get("droprate").and_then(|v| v.parse().ok()).unwrap_or(0), + droprateboost: attrs.get("droprateboost").and_then(|v| v.parse().ok()).unwrap_or(0), + amountboost: attrs.get("amountboost").and_then(|v| v.parse().ok()).unwrap_or(0), + checks: attrs.get("checks").cloned().unwrap_or_default(), + comment: attrs.get("comment").cloned().unwrap_or_default(), + dontconsumehealth: attrs.get("dontconsumehealth").and_then(|v| v.parse().ok()).unwrap_or(0) == 1, }; harvestable.drops.push(drop); }