reduced duplicate insert errors of portals, still present because of two players disovering the same portal at the same time, other changes to inventory
This commit is contained in:
parent
e7ca39318f
commit
6c646719dd
6 changed files with 1093 additions and 232 deletions
|
|
@ -11,7 +11,7 @@ from pathlib import Path
|
|||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Depends, Query
|
||||
from fastapi import FastAPI, HTTPException, Depends, Query, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
|
@ -24,15 +24,65 @@ from database import (
|
|||
ItemRatings, ItemSpells, ItemRawData, DATABASE_URL, create_indexes
|
||||
)
|
||||
|
||||
# Import helpers to share enum mappings
|
||||
import helpers
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# FastAPI app
|
||||
# Import suitbuilder router - after logger is defined
|
||||
try:
|
||||
from suitbuilder import router as suitbuilder_router, set_database_connection
|
||||
SUITBUILDER_AVAILABLE = True
|
||||
logger.info("Suitbuilder module imported successfully")
|
||||
except ImportError as e:
|
||||
logger.warning(f"Suitbuilder module not available: {e}")
|
||||
SUITBUILDER_AVAILABLE = False
|
||||
set_database_connection = None
|
||||
|
||||
# FastAPI app with comprehensive OpenAPI documentation
|
||||
app = FastAPI(
|
||||
title="Inventory Service",
|
||||
description="Microservice for Asheron's Call item data processing and queries",
|
||||
version="1.0.0"
|
||||
title="Inventory Service API",
|
||||
description="""
|
||||
## Comprehensive Inventory Management API for Asheron's Call
|
||||
|
||||
This service provides powerful search and data processing capabilities for game item inventories.
|
||||
|
||||
### Key Features
|
||||
- **Item Search**: Advanced filtering across 40+ parameters including spells, stats, sets, and more
|
||||
- **Character Management**: Multi-character inventory tracking and processing
|
||||
- **Equipment Optimization**: Integration with suitbuilder for optimal equipment combinations
|
||||
- **Real-time Processing**: Live inventory data ingestion and normalization
|
||||
- **Comprehensive Filtering**: Support for armor, jewelry, weapons, clothing, and spell-based searches
|
||||
|
||||
### Main Endpoints
|
||||
- `/search/items` - Advanced item search with extensive filtering options
|
||||
- `/process-inventory` - Process and normalize raw inventory data
|
||||
- `/inventory/{character}` - Get character-specific inventory data
|
||||
- `/suitbuilder/search` - Equipment optimization and suit building
|
||||
|
||||
### Search Parameters
|
||||
The search endpoint supports comprehensive filtering including:
|
||||
- **Text Search**: Item names, descriptions, properties
|
||||
- **Character Filtering**: Specific characters or cross-character search
|
||||
- **Equipment Status**: Equipped, unequipped, or all items
|
||||
- **Item Categories**: Armor, jewelry, weapons, clothing filters
|
||||
- **Combat Properties**: Damage, armor, attack bonuses, ratings
|
||||
- **Spell Filtering**: Specific spells, cantrips, legendary effects
|
||||
- **Requirements**: Level, skill requirements
|
||||
- **Enhancements**: Materials, workmanship, item sets, tinkering
|
||||
- **Item State**: Bonded, attuned, condition, rarity
|
||||
- **Sorting & Pagination**: Flexible result ordering and pagination
|
||||
""",
|
||||
version="1.0.0",
|
||||
contact={
|
||||
"name": "Dereth Tracker",
|
||||
"url": "https://github.com/your-repo/dereth-tracker",
|
||||
},
|
||||
license_info={
|
||||
"name": "MIT",
|
||||
},
|
||||
)
|
||||
|
||||
# Configure CORS
|
||||
|
|
@ -44,6 +94,11 @@ app.add_middleware(
|
|||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include suitbuilder router
|
||||
if SUITBUILDER_AVAILABLE:
|
||||
app.include_router(suitbuilder_router, prefix="/suitbuilder", tags=["suitbuilder"])
|
||||
logger.info("Suitbuilder endpoints included at /suitbuilder")
|
||||
|
||||
# Database connection
|
||||
database = databases.Database(DATABASE_URL)
|
||||
engine = sa.create_engine(DATABASE_URL)
|
||||
|
|
@ -188,6 +243,9 @@ def load_comprehensive_enums():
|
|||
|
||||
ENUM_MAPPINGS = load_comprehensive_enums()
|
||||
|
||||
# Share enum mappings with helpers module for suitbuilder
|
||||
helpers.set_enum_mappings(ENUM_MAPPINGS)
|
||||
|
||||
# Pydantic models
|
||||
class InventoryItem(BaseModel):
|
||||
"""Raw inventory item from plugin."""
|
||||
|
|
@ -204,12 +262,100 @@ class ProcessedItem(BaseModel):
|
|||
burden: int
|
||||
# Add other fields as needed
|
||||
|
||||
# Response Models for API Documentation
|
||||
class ItemSearchResponse(BaseModel):
|
||||
"""Response model for item search endpoint."""
|
||||
items: List[Dict[str, Any]]
|
||||
total_count: int
|
||||
page: int
|
||||
limit: int
|
||||
has_next: bool
|
||||
has_previous: bool
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"items": [
|
||||
{
|
||||
"name": "Gold Celdon Girth",
|
||||
"character_name": "Megamula XXXIII",
|
||||
"armor_level": 320,
|
||||
"crit_damage_rating": 2,
|
||||
"set_name": "Soldier's",
|
||||
"material_name": "Gold",
|
||||
"equipped": False,
|
||||
"spells": ["Legendary Strength", "Legendary Endurance"]
|
||||
}
|
||||
],
|
||||
"total_count": 1247,
|
||||
"page": 1,
|
||||
"limit": 200,
|
||||
"has_next": True,
|
||||
"has_previous": False
|
||||
}
|
||||
}
|
||||
|
||||
class ProcessingStats(BaseModel):
|
||||
"""Response model for inventory processing results."""
|
||||
processed_count: int
|
||||
error_count: int
|
||||
character_name: str
|
||||
timestamp: datetime
|
||||
errors: Optional[List[str]] = None
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"processed_count": 186,
|
||||
"error_count": 22,
|
||||
"character_name": "Megamula XXXIII",
|
||||
"timestamp": "2024-01-15T10:30:00Z",
|
||||
"errors": ["Item ID 12345: SQL type error", "Item ID 67890: Missing required field"]
|
||||
}
|
||||
}
|
||||
|
||||
class SetListResponse(BaseModel):
|
||||
"""Response model for equipment sets list."""
|
||||
sets: List[Dict[str, Any]]
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"sets": [
|
||||
{"set_name": "Soldier's", "item_count": 847, "set_id": 13},
|
||||
{"set_name": "Adept's", "item_count": 623, "set_id": 14},
|
||||
{"set_name": "Hearty", "item_count": 1205, "set_id": 42}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Response model for health check."""
|
||||
status: str
|
||||
timestamp: datetime
|
||||
database_connected: bool
|
||||
version: str
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"status": "healthy",
|
||||
"timestamp": "2024-01-15T10:30:00Z",
|
||||
"database_connected": True,
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
# Startup/shutdown events
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
"""Initialize database connection and create tables."""
|
||||
await database.connect()
|
||||
|
||||
# Share database connection with suitbuilder
|
||||
if SUITBUILDER_AVAILABLE and set_database_connection:
|
||||
set_database_connection(database)
|
||||
|
||||
# Create tables if they don't exist
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
|
|
@ -1155,12 +1301,42 @@ def extract_item_properties(item_data: Dict[str, Any]) -> Dict[str, Any]:
|
|||
return properties
|
||||
|
||||
# API endpoints
|
||||
@app.post("/process-inventory")
|
||||
@app.post("/process-inventory",
|
||||
response_model=ProcessingStats,
|
||||
summary="Process Raw Inventory Data",
|
||||
description="""
|
||||
**Process and normalize raw inventory data from game plugins.**
|
||||
|
||||
This endpoint receives raw inventory JSON data from game plugins and processes it
|
||||
into normalized database tables with comprehensive enum translation and validation.
|
||||
|
||||
### Processing Steps:
|
||||
1. **Data Validation**: Validate incoming JSON structure and required fields
|
||||
2. **Enum Translation**: Convert game IDs to human-readable names using comprehensive enum database
|
||||
3. **Normalization**: Split item data into related tables (combat stats, spells, enhancements, etc.)
|
||||
4. **Error Handling**: Track and report any processing errors with detailed error messages
|
||||
5. **Statistics**: Return comprehensive processing statistics
|
||||
|
||||
### Database Schema:
|
||||
- **items**: Core item properties (name, icon, value, etc.)
|
||||
- **item_combat_stats**: Armor level, damage bonuses, attack ratings
|
||||
- **item_enhancements**: Material, workmanship, item sets, tinkering
|
||||
- **item_spells**: Spell names and categories
|
||||
- **item_requirements**: Level and skill requirements
|
||||
- **item_ratings**: All rating values (crit damage, damage resist, etc.)
|
||||
- **item_raw_data**: Original JSON for complex queries
|
||||
|
||||
### Error Handling:
|
||||
Returns detailed error information for items that fail to process,
|
||||
including SQL type errors, missing fields, and validation failures.
|
||||
""",
|
||||
tags=["Data Processing"])
|
||||
async def process_inventory(inventory: InventoryItem):
|
||||
"""Process raw inventory data and store in normalized format."""
|
||||
"""Process raw inventory data and store in normalized database format."""
|
||||
|
||||
processed_count = 0
|
||||
error_count = 0
|
||||
processing_errors = []
|
||||
|
||||
async with database.transaction():
|
||||
# First, delete all existing items for this character from all related tables
|
||||
|
|
@ -1345,20 +1521,25 @@ async def process_inventory(inventory: InventoryItem):
|
|||
processed_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing item {item_data.get('Id', 'unknown')}: {e}")
|
||||
error_msg = f"Error processing item {item_data.get('Id', 'unknown')}: {e}"
|
||||
logger.error(error_msg)
|
||||
processing_errors.append(error_msg)
|
||||
error_count += 1
|
||||
|
||||
logger.info(f"Inventory processing complete for {inventory.character_name}: {processed_count} processed, {error_count} errors, {len(inventory.items)} total items received")
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"processed": processed_count,
|
||||
"errors": error_count,
|
||||
"total_received": len(inventory.items),
|
||||
"character": inventory.character_name
|
||||
}
|
||||
return ProcessingStats(
|
||||
processed_count=processed_count,
|
||||
error_count=error_count,
|
||||
character_name=inventory.character_name,
|
||||
timestamp=inventory.timestamp,
|
||||
errors=processing_errors if processing_errors else None
|
||||
)
|
||||
|
||||
@app.get("/inventory/{character_name}")
|
||||
@app.get("/inventory/{character_name}",
|
||||
summary="Get Character Inventory",
|
||||
description="Retrieve processed inventory data for a specific character with normalized item properties.",
|
||||
tags=["Character Data"])
|
||||
async def get_character_inventory(
|
||||
character_name: str,
|
||||
limit: int = Query(1000, le=5000),
|
||||
|
|
@ -1613,14 +1794,48 @@ async def get_character_inventory(
|
|||
"items": processed_items
|
||||
}
|
||||
|
||||
@app.get("/health")
|
||||
@app.get("/health",
|
||||
response_model=HealthResponse,
|
||||
summary="Service Health Check",
|
||||
description="Returns service health status, database connectivity, and version information.",
|
||||
tags=["System"])
|
||||
async def health_check():
|
||||
"""Health check endpoint."""
|
||||
return {"status": "healthy", "service": "inventory-service"}
|
||||
"""Health check endpoint with comprehensive status information."""
|
||||
try:
|
||||
# Test database connectivity
|
||||
await database.fetch_one("SELECT 1")
|
||||
db_connected = True
|
||||
except:
|
||||
db_connected = False
|
||||
|
||||
return HealthResponse(
|
||||
status="healthy" if db_connected else "degraded",
|
||||
timestamp=datetime.now(),
|
||||
database_connected=db_connected,
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
@app.get("/sets/list")
|
||||
@app.get("/sets/list",
|
||||
response_model=SetListResponse,
|
||||
summary="List Equipment Sets",
|
||||
description="""
|
||||
**Get all unique equipment set names with item counts.**
|
||||
|
||||
Returns a list of all equipment sets found in the database along with
|
||||
the number of items available for each set. Useful for understanding
|
||||
available equipment options and set coverage.
|
||||
|
||||
### Common Set IDs:
|
||||
- **13**: Soldier's (combat-focused armor)
|
||||
- **14**: Adept's (magical armor)
|
||||
- **42**: Hearty (health/vitality focused)
|
||||
- **21**: Wise (mana-focused)
|
||||
- **40**: Heroic Protector
|
||||
- **41**: Heroic Destroyer
|
||||
""",
|
||||
tags=["Equipment Sets"])
|
||||
async def list_equipment_sets():
|
||||
"""Get all unique equipment set names from the database."""
|
||||
"""Get all unique equipment set names with item counts from the database."""
|
||||
try:
|
||||
# Get equipment set IDs (the numeric collection sets)
|
||||
query = """
|
||||
|
|
@ -1650,17 +1865,19 @@ async def list_equipment_sets():
|
|||
"item_count": item_count
|
||||
})
|
||||
|
||||
return {
|
||||
"equipment_sets": equipment_sets,
|
||||
"total_sets": len(equipment_sets)
|
||||
}
|
||||
return SetListResponse(
|
||||
sets=equipment_sets
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list equipment sets: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to list equipment sets")
|
||||
|
||||
|
||||
@app.get("/enum-info")
|
||||
@app.get("/enum-info",
|
||||
summary="Get Enum Information",
|
||||
description="Get comprehensive information about available enum translations and database statistics.",
|
||||
tags=["System"])
|
||||
async def get_enum_info():
|
||||
"""Get information about available enum translations."""
|
||||
if ENUM_MAPPINGS is None:
|
||||
|
|
@ -1679,7 +1896,10 @@ async def get_enum_info():
|
|||
"database_version": ENUM_MAPPINGS.get('full_database', {}).get('metadata', {}).get('version', 'unknown')
|
||||
}
|
||||
|
||||
@app.get("/translate/{enum_type}/{value}")
|
||||
@app.get("/translate/{enum_type}/{value}",
|
||||
summary="Translate Enum Value",
|
||||
description="Translate a specific enum value to human-readable name using the comprehensive enum database.",
|
||||
tags=["System"])
|
||||
async def translate_enum_value(enum_type: str, value: int):
|
||||
"""Translate a specific enum value to human-readable name."""
|
||||
|
||||
|
|
@ -1753,28 +1973,63 @@ async def get_character_inventory_raw(character_name: str):
|
|||
# INVENTORY SEARCH API ENDPOINTS
|
||||
# ===================================================================
|
||||
|
||||
@app.get("/search/items")
|
||||
@app.get("/search/items",
|
||||
response_model=ItemSearchResponse,
|
||||
summary="Advanced Item Search",
|
||||
description="""
|
||||
**Comprehensive item search with extensive filtering capabilities.**
|
||||
|
||||
This endpoint provides powerful search functionality across all character inventories
|
||||
with support for 40+ filter parameters including text search, equipment categories,
|
||||
combat properties, spells, requirements, and more.
|
||||
|
||||
### Example Searches:
|
||||
- **Basic Text Search**: `?text=Celdon` - Find all items with "Celdon" in name
|
||||
- **Character Specific**: `?character=Megamula%20XXXIII` - Items from one character
|
||||
- **Multi-Character**: `?characters=Char1,Char2,Char3` - Items from specific characters
|
||||
- **Set-Based**: `?item_set=13&min_crit_damage_rating=2` - Soldier's set with CD2+
|
||||
- **Spell Search**: `?legendary_cantrips=Legendary%20Strength,Legendary%20Endurance`
|
||||
- **Category Filter**: `?armor_only=true&min_armor=300` - High-level armor only
|
||||
- **Equipment Status**: `?equipment_status=unequipped` - Only inventory items
|
||||
|
||||
### Advanced Filtering:
|
||||
- **Combat Properties**: Filter by damage, armor, attack bonuses, ratings
|
||||
- **Spell Effects**: Search for specific cantrips, wards, or legendary effects
|
||||
- **Item Categories**: Armor, jewelry, weapons, clothing with sub-categories
|
||||
- **Enhancement Data**: Material types, workmanship, item sets, tinkering
|
||||
- **Requirements**: Level, skill requirements for wielding
|
||||
- **Item State**: Bonded, attuned, condition, rarity filters
|
||||
|
||||
### Response Format:
|
||||
Returns paginated results with item details including translated properties,
|
||||
combat stats, spell information, and character ownership data.
|
||||
""",
|
||||
tags=["Search"])
|
||||
async def search_items(
|
||||
# Text search
|
||||
text: str = Query(None, description="Search item names, descriptions, or properties"),
|
||||
character: str = Query(None, description="Limit search to specific character"),
|
||||
characters: str = Query(None, description="Comma-separated list of character names"),
|
||||
text: str = Query(None, description="Search item names, descriptions, or properties", example="Celdon"),
|
||||
character: str = Query(None, description="Limit search to specific character", example="Megamula XXXIII"),
|
||||
characters: str = Query(None, description="Comma-separated list of character names", example="Char1,Char2,Char3"),
|
||||
include_all_characters: bool = Query(False, description="Search across all characters"),
|
||||
|
||||
# Equipment filtering
|
||||
equipment_status: str = Query(None, description="equipped, unequipped, or all"),
|
||||
equipment_slot: int = Query(None, description="Equipment slot mask (e.g., 1=head, 512=chest)"),
|
||||
slot_names: str = Query(None, description="Comma-separated list of slot names (e.g., Head,Chest,Ring)"),
|
||||
equipment_status: str = Query(None, description="Filter by equipment status: 'equipped', 'unequipped', or omit for all", example="unequipped"),
|
||||
equipment_slot: int = Query(None, description="Equipment slot mask (1=head, 2=neck, 4=chest, 8=abdomen, 16=upper_arms, 32=lower_arms, 64=hands, 128=upper_legs, 256=lower_legs, 512=feet, 1024=chest2, 2048=bracelet, 4096=ring)", example=4),
|
||||
slot_names: str = Query(None, description="Comma-separated list of slot names", example="Head,Chest,Ring"),
|
||||
|
||||
# Item category filtering
|
||||
armor_only: bool = Query(False, description="Show only armor items"),
|
||||
jewelry_only: bool = Query(False, description="Show only jewelry items"),
|
||||
weapon_only: bool = Query(False, description="Show only weapon items"),
|
||||
clothing_only: bool = Query(False, description="Show only clothing items (shirts/pants)"),
|
||||
shirt_only: bool = Query(False, description="Show only shirt underclothes"),
|
||||
pants_only: bool = Query(False, description="Show only pants underclothes"),
|
||||
underwear_only: bool = Query(False, description="Show all underclothes (shirts and pants)"),
|
||||
|
||||
# Spell filtering
|
||||
has_spell: str = Query(None, description="Must have this specific spell (by name)"),
|
||||
spell_contains: str = Query(None, description="Spell name contains this text"),
|
||||
legendary_cantrips: str = Query(None, description="Comma-separated list of legendary cantrip names"),
|
||||
has_spell: str = Query(None, description="Must have this specific spell (by name)", example="Legendary Strength"),
|
||||
spell_contains: str = Query(None, description="Spell name contains this text", example="Legendary"),
|
||||
legendary_cantrips: str = Query(None, description="Comma-separated list of legendary cantrip names", example="Legendary Strength,Legendary Endurance"),
|
||||
|
||||
# Combat properties
|
||||
min_damage: int = Query(None, description="Minimum damage"),
|
||||
|
|
@ -1782,8 +2037,8 @@ async def search_items(
|
|||
min_armor: int = Query(None, description="Minimum armor level"),
|
||||
max_armor: int = Query(None, description="Maximum armor level"),
|
||||
min_attack_bonus: float = Query(None, description="Minimum attack bonus"),
|
||||
min_crit_damage_rating: int = Query(None, description="Minimum critical damage rating"),
|
||||
min_damage_rating: int = Query(None, description="Minimum damage rating"),
|
||||
min_crit_damage_rating: int = Query(None, description="Minimum critical damage rating (0-2)", example=2),
|
||||
min_damage_rating: int = Query(None, description="Minimum damage rating (0-3)", example=3),
|
||||
min_heal_boost_rating: int = Query(None, description="Minimum heal boost rating"),
|
||||
min_vitality_rating: int = Query(None, description="Minimum vitality rating"),
|
||||
min_damage_resist_rating: int = Query(None, description="Minimum damage resist rating"),
|
||||
|
|
@ -1807,12 +2062,12 @@ async def search_items(
|
|||
min_level: int = Query(None, description="Minimum wield level requirement"),
|
||||
|
||||
# Enhancements
|
||||
material: str = Query(None, description="Material type (partial match)"),
|
||||
min_workmanship: float = Query(None, description="Minimum workmanship"),
|
||||
material: str = Query(None, description="Material type (partial match)", example="Gold"),
|
||||
min_workmanship: float = Query(None, description="Minimum workmanship", example=9.5),
|
||||
has_imbue: bool = Query(None, description="Has imbue effects"),
|
||||
item_set: str = Query(None, description="Item set ID (single set)"),
|
||||
item_sets: str = Query(None, description="Comma-separated list of item set IDs"),
|
||||
min_tinks: int = Query(None, description="Minimum tinker count"),
|
||||
item_set: str = Query(None, description="Item set ID (single set)", example="13"),
|
||||
item_sets: str = Query(None, description="Comma-separated list of item set IDs", example="13,14,42"),
|
||||
min_tinks: int = Query(None, description="Minimum tinker count", example=3),
|
||||
|
||||
# Item state
|
||||
bonded: bool = Query(None, description="Bonded status"),
|
||||
|
|
@ -1836,6 +2091,15 @@ async def search_items(
|
|||
Search items across characters with comprehensive filtering options.
|
||||
"""
|
||||
try:
|
||||
# Initialize underwear filter type
|
||||
underwear_filter_type = None
|
||||
if shirt_only:
|
||||
underwear_filter_type = "shirts"
|
||||
elif pants_only:
|
||||
underwear_filter_type = "pants"
|
||||
elif underwear_only:
|
||||
underwear_filter_type = "all_underwear"
|
||||
|
||||
# Build base query with CTE for computed slot names
|
||||
query_parts = ["""
|
||||
WITH items_with_slots AS (
|
||||
|
|
@ -1873,9 +2137,15 @@ async def search_items(
|
|||
COALESCE((rd.int_values->>'376')::int, -1)
|
||||
) as heal_boost_rating,
|
||||
COALESCE((rd.int_values->>'379')::int, -1) as vitality_rating,
|
||||
COALESCE((rd.int_values->>'308')::int, -1) as damage_resist_rating,
|
||||
GREATEST(
|
||||
COALESCE((rd.int_values->>'308')::int, -1),
|
||||
COALESCE((rd.int_values->>'371')::int, -1)
|
||||
) as damage_resist_rating,
|
||||
COALESCE((rd.int_values->>'315')::int, -1) as crit_resist_rating,
|
||||
COALESCE((rd.int_values->>'316')::int, -1) as crit_damage_resist_rating,
|
||||
GREATEST(
|
||||
COALESCE((rd.int_values->>'316')::int, -1),
|
||||
COALESCE((rd.int_values->>'375')::int, -1)
|
||||
) as crit_damage_resist_rating,
|
||||
COALESCE((rd.int_values->>'317')::int, -1) as healing_resist_rating,
|
||||
COALESCE((rd.int_values->>'331')::int, -1) as nether_resist_rating,
|
||||
COALESCE((rd.int_values->>'342')::int, -1) as healing_rating,
|
||||
|
|
@ -1895,6 +2165,7 @@ async def search_items(
|
|||
COALESCE(enh.tinks, -1) as tinks,
|
||||
COALESCE(enh.item_set, '') as item_set,
|
||||
rd.original_json,
|
||||
COALESCE((rd.int_values->>'218103821')::int, 0) as coverage_mask,
|
||||
|
||||
-- Compute slot_name in SQL
|
||||
CASE
|
||||
|
|
@ -1979,6 +2250,47 @@ async def search_items(
|
|||
SELECT * FROM items_with_slots
|
||||
"""]
|
||||
|
||||
# Apply underwear filtering by modifying the CTE query
|
||||
if underwear_filter_type:
|
||||
# Insert WHERE clause into the CTE before the closing parenthesis
|
||||
cte_where_clause = None
|
||||
if underwear_filter_type == "shirts":
|
||||
# Shirts: ObjectClass 3 with UnderwearChest (8) but NOT pants patterns
|
||||
# Exclude items that have both UnderwearUpperLegs (2) and UnderwearLowerLegs (4) which indicate pants
|
||||
cte_where_clause = """WHERE i.object_class = 3
|
||||
AND ((rd.int_values->>'218103821')::int & 8) > 0
|
||||
AND NOT ((rd.int_values->>'218103821')::int & 6) = 6
|
||||
AND i.name NOT ILIKE '%robe%'
|
||||
AND i.name NOT ILIKE '%cloak%'
|
||||
AND i.name NOT ILIKE '%pallium%'
|
||||
AND i.name NOT ILIKE '%armet%'
|
||||
AND i.name NOT ILIKE '%pants%'
|
||||
AND i.name NOT ILIKE '%breeches%'"""
|
||||
elif underwear_filter_type == "pants":
|
||||
# Pants: ObjectClass 3 with UnderwearUpperLegs (2) - covers both pants and breeches
|
||||
# Include both full pants (2&4) and breeches (2 only)
|
||||
cte_where_clause = """WHERE i.object_class = 3
|
||||
AND ((rd.int_values->>'218103821')::int & 2) = 2
|
||||
AND i.name NOT ILIKE '%robe%'
|
||||
AND i.name NOT ILIKE '%cloak%'
|
||||
AND i.name NOT ILIKE '%pallium%'
|
||||
AND i.name NOT ILIKE '%armet%'"""
|
||||
elif underwear_filter_type == "all_underwear":
|
||||
# All underwear: ObjectClass 3 with any underwear bits (2,4,8,16)
|
||||
cte_where_clause = """WHERE i.object_class = 3
|
||||
AND ((rd.int_values->>'218103821')::int & 30) > 0
|
||||
AND i.name NOT ILIKE '%robe%'
|
||||
AND i.name NOT ILIKE '%cloak%'
|
||||
AND i.name NOT ILIKE '%pallium%'
|
||||
AND i.name NOT ILIKE '%armet%'"""
|
||||
|
||||
if cte_where_clause:
|
||||
# Insert the WHERE clause before the closing parenthesis of the CTE
|
||||
query_parts[0] = query_parts[0].replace(
|
||||
"LEFT JOIN item_raw_data rd ON i.id = rd.item_id\n )",
|
||||
f"LEFT JOIN item_raw_data rd ON i.id = rd.item_id\n {cte_where_clause}\n )"
|
||||
)
|
||||
|
||||
conditions = []
|
||||
params = {}
|
||||
|
||||
|
|
@ -2024,14 +2336,24 @@ async def search_items(
|
|||
|
||||
# Item category filtering
|
||||
if armor_only:
|
||||
# Armor: ObjectClass 2 (Clothing) or 3 (Armor) with armor_level > 0
|
||||
conditions.append("(object_class IN (2, 3) AND COALESCE(armor_level, 0) > 0)")
|
||||
# Armor: ObjectClass 2 (Armor) with armor_level > 0
|
||||
conditions.append("(object_class = 2 AND COALESCE(armor_level, 0) > 0)")
|
||||
elif jewelry_only:
|
||||
# Jewelry: ObjectClass 4 (Jewelry) - rings, bracelets, necklaces, amulets
|
||||
conditions.append("object_class = 4")
|
||||
elif weapon_only:
|
||||
# Weapons: ObjectClass 6 (MeleeWeapon), 7 (MissileWeapon), 8 (Caster) with max_damage > 0
|
||||
conditions.append("(object_class IN (6, 7, 8) AND COALESCE(max_damage, 0) > 0)")
|
||||
elif clothing_only:
|
||||
# Clothing: ObjectClass 3 (Clothing) - shirts and pants only, exclude cloaks and robes
|
||||
# Focus on underclothes: shirts, pants, breeches, etc.
|
||||
conditions.append("""(object_class = 3 AND
|
||||
name NOT ILIKE '%cloak%' AND
|
||||
name NOT ILIKE '%robe%' AND
|
||||
name NOT ILIKE '%pallium%' AND
|
||||
name NOT ILIKE '%armet%' AND
|
||||
(name ILIKE '%shirt%' OR name ILIKE '%pants%' OR name ILIKE '%breeches%' OR name ILIKE '%baggy%' OR name ILIKE '%tunic%'))""")
|
||||
# Underwear filtering is handled in the CTE modification above
|
||||
|
||||
# Spell filtering - need to join with item_spells and use spell database
|
||||
spell_join_added = False
|
||||
|
|
@ -2350,7 +2672,8 @@ async def search_items(
|
|||
"crit_damage_rating": "crit_damage_rating",
|
||||
"heal_boost_rating": "heal_boost_rating",
|
||||
"vitality_rating": "vitality_rating",
|
||||
"damage_resist_rating": "damage_resist_rating"
|
||||
"damage_resist_rating": "damage_resist_rating",
|
||||
"crit_damage_resist_rating": "crit_damage_resist_rating"
|
||||
}
|
||||
sort_field = sort_mapping.get(sort_by, "name")
|
||||
sort_direction = "DESC" if sort_dir.lower() == "desc" else "ASC"
|
||||
|
|
@ -2396,9 +2719,15 @@ async def search_items(
|
|||
COALESCE((rd.int_values->>'376')::int, -1)
|
||||
) as heal_boost_rating,
|
||||
COALESCE((rd.int_values->>'379')::int, -1) as vitality_rating,
|
||||
COALESCE((rd.int_values->>'308')::int, -1) as damage_resist_rating,
|
||||
GREATEST(
|
||||
COALESCE((rd.int_values->>'308')::int, -1),
|
||||
COALESCE((rd.int_values->>'371')::int, -1)
|
||||
) as damage_resist_rating,
|
||||
COALESCE((rd.int_values->>'315')::int, -1) as crit_resist_rating,
|
||||
COALESCE((rd.int_values->>'316')::int, -1) as crit_damage_resist_rating,
|
||||
GREATEST(
|
||||
COALESCE((rd.int_values->>'316')::int, -1),
|
||||
COALESCE((rd.int_values->>'375')::int, -1)
|
||||
) as crit_damage_resist_rating,
|
||||
COALESCE((rd.int_values->>'317')::int, -1) as healing_resist_rating,
|
||||
COALESCE((rd.int_values->>'331')::int, -1) as nether_resist_rating,
|
||||
COALESCE((rd.int_values->>'350')::int, -1) as dot_resist_rating,
|
||||
|
|
@ -2561,8 +2890,11 @@ async def search_items(
|
|||
item['coverage'] = ', '.join(coverage_parts)
|
||||
else:
|
||||
item['coverage'] = f"Coverage_{coverage_value}"
|
||||
# Add raw coverage mask for armor reduction system
|
||||
item['coverage_mask'] = coverage_value
|
||||
else:
|
||||
item['coverage'] = None
|
||||
item['coverage_mask'] = 0
|
||||
|
||||
# Add sophisticated equipment slot translation using Mag-SuitBuilder logic
|
||||
# Use both EquipableSlots_Decal and Coverage for armor reduction
|
||||
|
|
@ -2657,20 +2989,14 @@ async def search_items(
|
|||
|
||||
items.append(item)
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"total_count": total_count,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total_pages": (total_count + limit - 1) // limit,
|
||||
"search_criteria": {
|
||||
"text": text,
|
||||
"character": character,
|
||||
"include_all_characters": include_all_characters,
|
||||
"equipment_status": equipment_status,
|
||||
"filters_applied": len(conditions)
|
||||
}
|
||||
}
|
||||
return ItemSearchResponse(
|
||||
items=items,
|
||||
total_count=total_count,
|
||||
page=page,
|
||||
limit=limit,
|
||||
has_next=page * limit < total_count,
|
||||
has_previous=page > 1
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Search error: {e}", exc_info=True)
|
||||
|
|
@ -2832,7 +3158,10 @@ async def find_equipment_upgrades(
|
|||
raise HTTPException(status_code=500, detail=f"Equipment upgrades search failed: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/characters/list")
|
||||
@app.get("/characters/list",
|
||||
summary="List Characters",
|
||||
description="Get a list of all characters that have inventory data in the database.",
|
||||
tags=["Character Data"])
|
||||
async def list_inventory_characters():
|
||||
"""List all characters that have inventory data."""
|
||||
try:
|
||||
|
|
@ -3495,6 +3824,7 @@ async def test_simple_search(characters: str = Query(..., description="Comma-sep
|
|||
|
||||
@app.get("/optimize/suits/stream")
|
||||
async def stream_optimize_suits(
|
||||
request: Request, # Add request to detect client disconnection
|
||||
characters: str = Query(..., description="Comma-separated character names"),
|
||||
primary_set: Optional[int] = Query(None, description="Primary set ID requirement"),
|
||||
secondary_set: Optional[int] = Query(None, description="Secondary set ID requirement"),
|
||||
|
|
@ -3590,6 +3920,15 @@ async def stream_optimize_suits(
|
|||
logger.info(f"Generated {len(armor_combinations)} armor combinations")
|
||||
|
||||
for i, armor_combo in enumerate(armor_combinations):
|
||||
# Check for client disconnection
|
||||
if await request.is_disconnected():
|
||||
logger.info("Client disconnected, stopping search")
|
||||
yield {
|
||||
"event": "cancelled",
|
||||
"data": json.dumps({"message": "Search cancelled by client"})
|
||||
}
|
||||
return
|
||||
|
||||
# Check time limit
|
||||
if time.time() - start_time > limit_config["time_limit"]:
|
||||
yield {
|
||||
|
|
@ -3630,6 +3969,15 @@ async def stream_optimize_suits(
|
|||
logger.info(f"Combination {i}: no scored suits returned")
|
||||
else:
|
||||
logger.info(f"Combination {i}: no complete suit generated")
|
||||
|
||||
# Also check for disconnection every 10 iterations
|
||||
if i % 10 == 0 and await request.is_disconnected():
|
||||
logger.info("Client disconnected during search")
|
||||
yield {
|
||||
"event": "cancelled",
|
||||
"data": json.dumps({"message": "Search cancelled by client"})
|
||||
}
|
||||
return
|
||||
|
||||
# Final status
|
||||
yield {
|
||||
|
|
@ -3804,6 +4152,15 @@ def determine_item_slots(item):
|
|||
coverage = item.get("coverage_mask", item.get("coverage", 0))
|
||||
item_name = item["name"].lower()
|
||||
|
||||
# Name-based detection for clothing items first (for ObjectClass 3)
|
||||
if item["object_class"] == 3:
|
||||
if any(word in item_name for word in ["pants", "breeches", "baggy"]):
|
||||
slots.append("Pants")
|
||||
return slots
|
||||
elif any(word in item_name for word in ["shirt", "tunic"]):
|
||||
slots.append("Shirt")
|
||||
return slots
|
||||
|
||||
# Use coverage mask if available
|
||||
if coverage and coverage > 0:
|
||||
slots.extend(decode_coverage_to_slots(coverage, item["object_class"], item["name"]))
|
||||
|
|
@ -3844,6 +4201,16 @@ def decode_coverage_to_slots(coverage_mask, object_class=None, item_name=''):
|
|||
|
||||
# Only check for clothing patterns if this is actually a clothing item (ObjectClass 3)
|
||||
if object_class == 3:
|
||||
# Check coverage mask patterns for underclothes first
|
||||
# Shirts: UnderwearChest (8) OR UnderwearAbdomen (16) = mask & 24 > 0
|
||||
if coverage_mask & 24: # 8 + 16 = 24
|
||||
slots.append("Shirt")
|
||||
return slots
|
||||
|
||||
# Pants: UnderwearUpperLegs (2) AND UnderwearLowerLegs (4) = mask & 6 = 6
|
||||
if (coverage_mask & 6) == 6: # Both bits 2 and 4 must be set
|
||||
slots.append("Pants")
|
||||
return slots
|
||||
# Check for clothing patterns based on actual inventory data
|
||||
# Specific coverage patterns for ObjectClass 3 clothing items:
|
||||
|
||||
|
|
@ -3865,8 +4232,12 @@ def decode_coverage_to_slots(coverage_mask, object_class=None, item_name=''):
|
|||
slots.append("Shirt")
|
||||
return slots
|
||||
|
||||
# Pants = UnderwearUpperLegs (2) + UnderwearLowerLegs (4) = 6
|
||||
if coverage_mask & 2 and coverage_mask & 4: # UnderwearUpperLegs + UnderwearLowerLegs
|
||||
# Pants = UnderwearUpperLegs (2) + UnderwearLowerLegs (4) + UnderwearAbdomen (16) = 22
|
||||
if coverage_mask & 2 and coverage_mask & 4 and coverage_mask & 16: # Full pants pattern
|
||||
slots.append("Pants")
|
||||
return slots
|
||||
# Also check for simple pants pattern without abdomen
|
||||
elif coverage_mask & 2 and coverage_mask & 4: # UnderwearUpperLegs + UnderwearLowerLegs
|
||||
slots.append("Pants")
|
||||
return slots
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue