package recipes import ( "fmt" "sync" "eq2emu/internal/database" ) // RecipeManager provides high-level recipe system management with database integration type RecipeManager struct { db *database.DB masterRecipeList *MasterRecipeList masterRecipeBookList *MasterRecipeBookList loadedRecipes map[int32]*Recipe loadedRecipeBooks map[int32]*Recipe mu sync.RWMutex statisticsEnabled bool // Statistics stats RecipeManagerStats } // RecipeManagerStats tracks recipe system usage and performance metrics type RecipeManagerStats struct { TotalRecipesLoaded int32 TotalRecipeBooksLoaded int32 PlayersWithRecipes int32 LoadOperations int32 SaveOperations int32 mu sync.RWMutex } // NewRecipeManager creates a new recipe manager with database integration func NewRecipeManager(db *database.DB) *RecipeManager { return &RecipeManager{ db: db, masterRecipeList: NewMasterRecipeList(), masterRecipeBookList: NewMasterRecipeBookList(), loadedRecipes: make(map[int32]*Recipe), loadedRecipeBooks: make(map[int32]*Recipe), statisticsEnabled: true, } } // LoadRecipes loads all recipes from the database with complex component relationships func (rm *RecipeManager) LoadRecipes() error { rm.mu.Lock() defer rm.mu.Unlock() query := `SELECT r.id, r.soe_id, r.level, r.icon, r.skill_level, r.technique, r.knowledge, r.name, r.description, i.name as book, r.bench, ipc.adventure_classes, r.stage4_id, r.name, r.stage4_qty, pcl.name as primary_comp_title, r.primary_comp_qty, fcl.name as fuel_comp_title, r.fuel_comp_qty, bc.name AS build_comp_title, bc.qty AS build_comp_qty, bc2.name AS build2_comp_title, bc2.qty AS build2_comp_qty, bc3.name AS build3_comp_title, bc3.qty AS build3_comp_qty, bc4.name AS build4_comp_title, bc4.qty AS build4_comp_qty, r.stage0_id, r.stage1_id, r.stage2_id, r.stage3_id, r.stage4_id, r.stage0_qty, r.stage1_qty, r.stage2_qty, r.stage3_qty, r.stage4_qty, r.stage0_byp_id, r.stage1_byp_id, r.stage2_byp_id, r.stage3_byp_id, r.stage4_byp_id, r.stage0_byp_qty, r.stage1_byp_qty, r.stage2_byp_qty, r.stage3_byp_qty, r.stage4_byp_qty FROM recipe r LEFT JOIN ((SELECT recipe_id, soe_recipe_crc FROM item_details_recipe_items GROUP BY soe_recipe_crc) as idri) ON idri.soe_recipe_crc = r.soe_id LEFT JOIN items i ON idri.recipe_id = i.id INNER JOIN items ipc ON r.stage4_id = ipc.id INNER JOIN recipe_comp_list pcl ON r.primary_comp_list = pcl.id INNER JOIN recipe_comp_list fcl ON r.fuel_comp_list = fcl.id LEFT JOIN (SELECT rsc.recipe_id, rsc.comp_list, rsc.index, rcl.name, rsc.qty FROM recipe_secondary_comp rsc INNER JOIN recipe_comp_list rcl ON rcl.id = rsc.comp_list WHERE index = 0) AS bc ON bc.recipe_id = r.id LEFT JOIN (SELECT rsc.recipe_id, rsc.comp_list, rsc.index, rcl.name, rsc.qty FROM recipe_secondary_comp rsc INNER JOIN recipe_comp_list rcl ON rcl.id = rsc.comp_list WHERE index = 1) AS bc2 ON bc2.recipe_id = r.id LEFT JOIN (SELECT rsc.recipe_id, rsc.comp_list, rsc.index, rcl.name, rsc.qty FROM recipe_secondary_comp rsc INNER JOIN recipe_comp_list rcl ON rcl.id = rsc.comp_list WHERE index = 2) AS bc3 ON bc3.recipe_id = r.id LEFT JOIN (SELECT rsc.recipe_id, rsc.comp_list, rsc.index, rcl.name, rsc.qty FROM recipe_secondary_comp rsc INNER JOIN recipe_comp_list rcl ON rcl.id = rsc.comp_list WHERE index = 3) AS bc4 ON bc4.recipe_id = r.id WHERE r.bHaveAllProducts` loadedCount := int32(0) err := rm.db.Query(query, func(row *database.Row) error { recipe := NewRecipe() // Column index for scanning col := 0 recipe.ID = int32(row.Int(col)) col++ recipe.SoeID = int32(row.Int(col)) col++ recipe.Level = int8(row.Int(col)) col++ recipe.Icon = int16(row.Int(col)) col++ recipe.Skill = int32(row.Int(col)) col++ recipe.Technique = int32(row.Int(col)) col++ recipe.Knowledge = int32(row.Int(col)) col++ recipe.Name = row.Text(col) col++ recipe.Description = row.Text(col) col++ // Book name (nullable) if !row.IsNull(col) { recipe.Book = row.Text(col) } col++ // Device (nullable) if !row.IsNull(col) { recipe.Device = row.Text(col) } col++ recipe.Classes = int32(row.Int(col)) col++ // Product information recipe.ProductItemID = int32(row.Int(col)) col++ recipe.ProductName = row.Text(col) col++ recipe.ProductQty = int8(row.Int(col)) col++ // Component titles and quantities if !row.IsNull(col) { recipe.PrimaryBuildCompTitle = row.Text(col) } col++ recipe.PrimaryCompQty = int16(row.Int(col)) col++ if !row.IsNull(col) { recipe.FuelCompTitle = row.Text(col) } col++ recipe.FuelCompQty = int16(row.Int(col)) col++ // Build component titles and quantities if !row.IsNull(col) { recipe.Build1CompTitle = row.Text(col) } col++ if !row.IsNull(col + 1) { recipe.Build1CompQty = int16(row.Int(col + 1)) } col += 2 if !row.IsNull(col) { recipe.Build2CompTitle = row.Text(col) } col++ if !row.IsNull(col) { recipe.Build2CompQty = int16(row.Int(col)) } col++ if !row.IsNull(col) { recipe.Build3CompTitle = row.Text(col) } col++ if !row.IsNull(col) { recipe.Build3CompQty = int16(row.Int(col)) } col++ if !row.IsNull(col) { recipe.Build4CompTitle = row.Text(col) } col++ if !row.IsNull(col) { recipe.Build4CompQty = int16(row.Int(col)) } col++ // Set tier based on level (C++ logic: level / 10 + 1) recipe.Tier = recipe.Level/10 + 1 // Initialize products for all stages for stage := int8(0); stage < 5; stage++ { stageID := int32(row.Int(col)) stageQty := int8(row.Int(col + 5)) bypassID := int32(row.Int(col + 10)) bypassQty := int8(row.Int(col + 15)) recipe.Products[stage] = &RecipeProducts{ ProductID: stageID, ProductQty: stageQty, ByproductID: bypassID, ByproductQty: bypassQty, } col++ } if rm.masterRecipeList.AddRecipe(recipe) { rm.loadedRecipes[recipe.ID] = recipe loadedCount++ } return nil }) if err != nil { return fmt.Errorf("failed to load recipes: %w", err) } // Load recipe components after loading recipes if err := rm.loadRecipeComponents(); err != nil { return fmt.Errorf("failed to load recipe components: %w", err) } // Update statistics if rm.statisticsEnabled { rm.stats.mu.Lock() rm.stats.TotalRecipesLoaded = loadedCount rm.stats.LoadOperations++ rm.stats.mu.Unlock() } return nil } // loadRecipeComponents loads component relationships for recipes func (rm *RecipeManager) loadRecipeComponents() error { query := `SELECT r.id, pc.item_id AS primary_comp, fc.item_id AS fuel_comp, sc.item_id as secondary_comp, rsc.index + 1 AS slot FROM recipe r INNER JOIN (select comp_list, item_id FROM recipe_comp_list_item) as pc ON r.primary_comp_list = pc.comp_list INNER JOIN (select comp_list, item_id FROM recipe_comp_list_item) as fc ON r.fuel_comp_list = fc.comp_list LEFT JOIN recipe_secondary_comp rsc ON rsc.recipe_id = r.id LEFT JOIN (select comp_list, item_id FROM recipe_comp_list_item) as sc ON rsc.comp_list = sc.comp_list WHERE r.bHaveAllProducts ORDER BY r.id, rsc.index ASC` var currentRecipeID int32 var currentRecipe *Recipe err := rm.db.Query(query, func(row *database.Row) error { recipeID := int32(row.Int(0)) primaryComp := int32(row.Int(1)) fuelComp := int32(row.Int(2)) var secondaryComp int32 var slot int8 if !row.IsNull(3) { secondaryComp = int32(row.Int(3)) } if !row.IsNull(4) { slot = int8(row.Int(4)) } // Get the recipe if it's different from the current one if currentRecipeID != recipeID { currentRecipeID = recipeID currentRecipe = rm.masterRecipeList.GetRecipe(recipeID) if currentRecipe == nil { return nil } } if currentRecipe != nil && !row.IsNull(3) && !row.IsNull(4) { // Add primary component (slot 0) if !rm.containsComponent(currentRecipe.Components[0], primaryComp) { currentRecipe.AddBuildComponent(primaryComp, 0, false) } // Add fuel component (slot 5) if !rm.containsComponent(currentRecipe.Components[5], fuelComp) { currentRecipe.AddBuildComponent(fuelComp, 5, false) } // Add secondary component to appropriate slot if slot >= 1 && slot <= 4 { if !rm.containsComponent(currentRecipe.Components[slot], secondaryComp) { currentRecipe.AddBuildComponent(secondaryComp, slot, false) } } } return nil }) return err } // containsComponent checks if a component ID exists in the component slice func (rm *RecipeManager) containsComponent(components []int32, componentID int32) bool { for _, comp := range components { if comp == componentID { return true } } return false } // LoadRecipeBooks loads all recipe books from the database func (rm *RecipeManager) LoadRecipeBooks() error { rm.mu.Lock() defer rm.mu.Unlock() query := `SELECT id, name, tradeskill_default_level FROM items WHERE item_type='Recipe'` loadedCount := int32(0) err := rm.db.Query(query, func(row *database.Row) error { recipe := NewRecipe() recipe.BookID = int32(row.Int(0)) recipe.BookName = row.Text(1) recipe.Level = int8(row.Int(2)) if rm.masterRecipeBookList.AddRecipeBook(recipe) { rm.loadedRecipeBooks[recipe.BookID] = recipe loadedCount++ } return nil }) if err != nil { return fmt.Errorf("failed to load recipe books: %w", err) } // Update statistics if rm.statisticsEnabled { rm.stats.mu.Lock() rm.stats.TotalRecipeBooksLoaded = loadedCount rm.stats.LoadOperations++ rm.stats.mu.Unlock() } return nil } // LoadPlayerRecipes loads recipes for a specific player from the database func (rm *RecipeManager) LoadPlayerRecipes(playerRecipeList *PlayerRecipeList, characterID int32) error { rm.mu.RLock() defer rm.mu.RUnlock() query := `SELECT recipe_id, highest_stage FROM character_recipes WHERE char_id = ?` loadedCount := 0 err := rm.db.Query(query, func(row *database.Row) error { recipeID := int32(row.Int(0)) highestStage := int8(row.Int(1)) // Get master recipe masterRecipe := rm.masterRecipeList.GetRecipe(recipeID) if masterRecipe == nil { return nil } // Create player copy of recipe playerRecipe := NewRecipeFromRecipe(masterRecipe) playerRecipe.HighestStage = highestStage if playerRecipeList.AddRecipe(playerRecipe) { loadedCount++ } return nil }, characterID) if err != nil { return fmt.Errorf("failed to load player recipes: %w", err) } // Update statistics if rm.statisticsEnabled { rm.stats.mu.Lock() rm.stats.PlayersWithRecipes++ rm.stats.LoadOperations++ rm.stats.mu.Unlock() } return nil } // LoadPlayerRecipeBooks loads recipe books for a specific player from the database func (rm *RecipeManager) LoadPlayerRecipeBooks(playerRecipeBookList *PlayerRecipeBookList, characterID int32) (int32, error) { rm.mu.RLock() defer rm.mu.RUnlock() query := `SELECT recipebook_id FROM character_recipe_books WHERE char_id = ? ORDER BY recipebook_id` count := int32(0) var lastID int32 err := rm.db.Query(query, func(row *database.Row) error { recipebookID := int32(row.Int(0)) // Skip duplicates if recipebookID == lastID { return nil } // Create recipe book entry recipe := NewRecipe() recipe.BookID = recipebookID recipe.BookName = fmt.Sprintf("Recipe Book %d", recipebookID) // TODO: Get actual name from items table if playerRecipeBookList.AddRecipeBook(recipe) { count++ } lastID = recipebookID return nil }, characterID) if err != nil { return 0, fmt.Errorf("failed to load player recipe books: %w", err) } return count, nil } // SavePlayerRecipeBook saves a player's recipe book to the database func (rm *RecipeManager) SavePlayerRecipeBook(characterID int32, recipebookID int32) error { query := `INSERT INTO character_recipe_books (char_id, recipebook_id) VALUES (?, ?)` err := rm.db.Exec(query, characterID, recipebookID) if err != nil { return fmt.Errorf("failed to save player recipe book: %w", err) } // Update statistics if rm.statisticsEnabled { rm.stats.mu.Lock() rm.stats.SaveOperations++ rm.stats.mu.Unlock() } return nil } // SavePlayerRecipe saves a player's recipe to the database func (rm *RecipeManager) SavePlayerRecipe(characterID int32, recipeID int32) error { query := `INSERT INTO character_recipes (char_id, recipe_id) VALUES (?, ?)` err := rm.db.Exec(query, characterID, recipeID) if err != nil { return fmt.Errorf("failed to save player recipe: %w", err) } // Update statistics if rm.statisticsEnabled { rm.stats.mu.Lock() rm.stats.SaveOperations++ rm.stats.mu.Unlock() } return nil } // UpdatePlayerRecipe updates a player's recipe progress in the database func (rm *RecipeManager) UpdatePlayerRecipe(characterID int32, recipeID int32, highestStage int8) error { query := `UPDATE character_recipes SET highest_stage = ? WHERE char_id = ? AND recipe_id = ?` err := rm.db.Exec(query, highestStage, characterID, recipeID) if err != nil { return fmt.Errorf("failed to update player recipe: %w", err) } // Update statistics if rm.statisticsEnabled { rm.stats.mu.Lock() rm.stats.SaveOperations++ rm.stats.mu.Unlock() } return nil } // GetMasterRecipeList returns the master recipe list func (rm *RecipeManager) GetMasterRecipeList() *MasterRecipeList { rm.mu.RLock() defer rm.mu.RUnlock() return rm.masterRecipeList } // GetMasterRecipeBookList returns the master recipe book list func (rm *RecipeManager) GetMasterRecipeBookList() *MasterRecipeBookList { rm.mu.RLock() defer rm.mu.RUnlock() return rm.masterRecipeBookList } // GetRecipe retrieves a recipe by ID from the master list func (rm *RecipeManager) GetRecipe(recipeID int32) *Recipe { rm.mu.RLock() defer rm.mu.RUnlock() return rm.masterRecipeList.GetRecipe(recipeID) } // GetRecipeBook retrieves a recipe book by ID func (rm *RecipeManager) GetRecipeBook(bookID int32) *Recipe { rm.mu.RLock() defer rm.mu.RUnlock() return rm.loadedRecipeBooks[bookID] } // GetStatistics returns current recipe system statistics func (rm *RecipeManager) GetStatistics() RecipeManagerStats { if !rm.statisticsEnabled { return RecipeManagerStats{} } rm.stats.mu.RLock() defer rm.stats.mu.RUnlock() return rm.stats } // SetStatisticsEnabled enables or disables statistics collection func (rm *RecipeManager) SetStatisticsEnabled(enabled bool) { rm.mu.Lock() defer rm.mu.Unlock() rm.statisticsEnabled = enabled } // Validate performs comprehensive recipe system validation func (rm *RecipeManager) Validate() []string { rm.mu.RLock() defer rm.mu.RUnlock() var issues []string // Validate master recipe list if rm.masterRecipeList == nil { issues = append(issues, "master recipe list is nil") return issues } // Validate master recipe book list if rm.masterRecipeBookList == nil { issues = append(issues, "master recipe book list is nil") } // Check for recipes with invalid data for _, recipe := range rm.loadedRecipes { if recipe.ID == 0 { issues = append(issues, fmt.Sprintf("recipe has invalid ID: %s", recipe.Name)) } if recipe.Name == "" { issues = append(issues, fmt.Sprintf("recipe %d has empty name", recipe.ID)) } if recipe.Level < 0 || recipe.Level > 100 { issues = append(issues, fmt.Sprintf("recipe %d has invalid level: %d", recipe.ID, recipe.Level)) } if len(recipe.Products) != 5 { issues = append(issues, fmt.Sprintf("recipe %d has invalid products array length: %d", recipe.ID, len(recipe.Products))) } if len(recipe.Components) != 6 { issues = append(issues, fmt.Sprintf("recipe %d has invalid components array length: %d", recipe.ID, len(recipe.Components))) } } return issues } // Size returns the total count of loaded recipes func (rm *RecipeManager) Size() (recipes int32, recipeBooks int32) { rm.mu.RLock() defer rm.mu.RUnlock() return int32(len(rm.loadedRecipes)), int32(len(rm.loadedRecipeBooks)) }