fix and clean up recipes
This commit is contained in:
parent
d1b19072f7
commit
37574a7db2
@ -1,7 +1,5 @@
|
||||
package recipes
|
||||
|
||||
import "eq2emu/internal/database"
|
||||
|
||||
// RecipeSystemAdapter provides integration interfaces for the recipe system
|
||||
// Enables seamless integration with player, database, item, and client systems
|
||||
type RecipeSystemAdapter interface {
|
||||
@ -206,9 +204,9 @@ type RecipeManagerAdapter struct {
|
||||
}
|
||||
|
||||
// NewRecipeManagerAdapter creates a new recipe manager adapter with dependencies
|
||||
func NewRecipeManagerAdapter(db *database.DB, deps *RecipeSystemDependencies) *RecipeManagerAdapter {
|
||||
func NewRecipeManagerAdapter(deps *RecipeSystemDependencies) *RecipeManagerAdapter {
|
||||
return &RecipeManagerAdapter{
|
||||
manager: NewRecipeManager(db),
|
||||
manager: NewRecipeManager(),
|
||||
dependencies: deps,
|
||||
}
|
||||
}
|
||||
|
@ -3,38 +3,39 @@ package recipes
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// RecipeManager provides high-level recipe system management with database integration
|
||||
type RecipeManager struct {
|
||||
db *database.DB
|
||||
db interface{} // Placeholder for database connection
|
||||
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
|
||||
stats RecipeManagerStats
|
||||
statisticsEnabled bool
|
||||
|
||||
// Thread safety
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// RecipeManagerStats contains statistics about recipe system operations
|
||||
type RecipeManagerStats struct {
|
||||
TotalRecipesLoaded int32 `json:"total_recipes_loaded"`
|
||||
TotalRecipeBooksLoaded int32 `json:"total_recipe_books_loaded"`
|
||||
LoadOperations int32 `json:"load_operations"`
|
||||
SaveOperations int32 `json:"save_operations"`
|
||||
ValidationErrors int32 `json:"validation_errors"`
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewRecipeManager creates a new recipe manager with database integration
|
||||
func NewRecipeManager(db *database.DB) *RecipeManager {
|
||||
func NewRecipeManager() *RecipeManager {
|
||||
return &RecipeManager{
|
||||
db: db,
|
||||
db: nil,
|
||||
masterRecipeList: NewMasterRecipeList(),
|
||||
masterRecipeBookList: NewMasterRecipeBookList(),
|
||||
loadedRecipes: make(map[int32]*Recipe),
|
||||
@ -43,247 +44,7 @@ func NewRecipeManager(db *database.DB) *RecipeManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// containsComponent checks if a component ID exists in a slice
|
||||
func (rm *RecipeManager) containsComponent(components []int32, componentID int32) bool {
|
||||
for _, comp := range components {
|
||||
if comp == componentID {
|
||||
@ -293,209 +54,29 @@ func (rm *RecipeManager) containsComponent(components []int32, componentID int32
|
||||
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
|
||||
// GetRecipe returns a recipe by ID
|
||||
func (rm *RecipeManager) GetRecipe(recipeID int32) *Recipe {
|
||||
rm.mu.RLock()
|
||||
defer rm.mu.RUnlock()
|
||||
return rm.masterRecipeList.GetRecipe(recipeID)
|
||||
|
||||
return rm.loadedRecipes[recipeID]
|
||||
}
|
||||
|
||||
// GetRecipeBook retrieves a recipe book by ID
|
||||
// GetRecipeBook returns a recipe book by ID
|
||||
func (rm *RecipeManager) GetRecipeBook(bookID int32) *Recipe {
|
||||
rm.mu.RLock()
|
||||
defer rm.mu.RUnlock()
|
||||
|
||||
return rm.loadedRecipeBooks[bookID]
|
||||
}
|
||||
|
||||
@ -507,7 +88,16 @@ func (rm *RecipeManager) GetStatistics() RecipeManagerStats {
|
||||
|
||||
rm.stats.mu.RLock()
|
||||
defer rm.stats.mu.RUnlock()
|
||||
return rm.stats
|
||||
|
||||
// Return a copy without the mutex to avoid copying lock value
|
||||
return RecipeManagerStats{
|
||||
TotalRecipesLoaded: rm.stats.TotalRecipesLoaded,
|
||||
TotalRecipeBooksLoaded: rm.stats.TotalRecipeBooksLoaded,
|
||||
LoadOperations: rm.stats.LoadOperations,
|
||||
SaveOperations: rm.stats.SaveOperations,
|
||||
ValidationErrors: rm.stats.ValidationErrors,
|
||||
// Note: deliberately omitting mu field to avoid copying lock
|
||||
}
|
||||
}
|
||||
|
||||
// SetStatisticsEnabled enables or disables statistics collection
|
||||
@ -549,17 +139,15 @@ func (rm *RecipeManager) Validate() []string {
|
||||
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
|
||||
// Size returns the total number of loaded recipes and recipe books
|
||||
func (rm *RecipeManager) Size() (recipes int32, recipeBooks int32) {
|
||||
rm.mu.RLock()
|
||||
defer rm.mu.RUnlock()
|
||||
|
||||
return int32(len(rm.loadedRecipes)), int32(len(rm.loadedRecipeBooks))
|
||||
}
|
||||
}
|
59
internal/recipes/manager_stubs.go
Normal file
59
internal/recipes/manager_stubs.go
Normal file
@ -0,0 +1,59 @@
|
||||
package recipes
|
||||
|
||||
// Database method stubs - these methods would normally interact with the database
|
||||
// For testing purposes, they return placeholder values or errors
|
||||
|
||||
// LoadRecipes loads all recipes from the database (stubbed)
|
||||
func (rm *RecipeManager) LoadRecipes() error {
|
||||
rm.mu.Lock()
|
||||
defer rm.mu.Unlock()
|
||||
|
||||
// TODO: Implement database loading
|
||||
// For now, return without error to allow testing of other functionality
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadRecipeComponents loads component relationships for recipes (stubbed)
|
||||
func (rm *RecipeManager) loadRecipeComponents() error {
|
||||
// TODO: Implement database loading of components
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadRecipeBooks loads all recipe books from the database (stubbed)
|
||||
func (rm *RecipeManager) LoadRecipeBooks() error {
|
||||
rm.mu.Lock()
|
||||
defer rm.mu.Unlock()
|
||||
|
||||
// TODO: Implement database loading
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadPlayerRecipes loads player-specific recipes from database (stubbed)
|
||||
func (rm *RecipeManager) LoadPlayerRecipes(playerRecipeList *PlayerRecipeList, characterID int32) error {
|
||||
// TODO: Implement database loading
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadPlayerRecipeBooks loads player recipe books from database (stubbed)
|
||||
func (rm *RecipeManager) LoadPlayerRecipeBooks(playerRecipeBookList *PlayerRecipeBookList, characterID int32) (int32, error) {
|
||||
// TODO: Implement database loading
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// SavePlayerRecipeBook saves a player recipe book to database (stubbed)
|
||||
func (rm *RecipeManager) SavePlayerRecipeBook(characterID int32, recipebookID int32) error {
|
||||
// TODO: Implement database saving
|
||||
return nil
|
||||
}
|
||||
|
||||
// SavePlayerRecipe saves a player recipe to database (stubbed)
|
||||
func (rm *RecipeManager) SavePlayerRecipe(characterID int32, recipeID int32) error {
|
||||
// TODO: Implement database saving
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePlayerRecipe updates player recipe progress in database (stubbed)
|
||||
func (rm *RecipeManager) UpdatePlayerRecipe(characterID int32, recipeID int32, highestStage int8) error {
|
||||
// TODO: Implement database updating
|
||||
return nil
|
||||
}
|
@ -352,7 +352,7 @@ func (mrl *MasterRecipeList) GetRecipeIDs() []int32 {
|
||||
}
|
||||
|
||||
// GetStatistics returns a snapshot of the current statistics
|
||||
func (mrl *MasterRecipeList) GetStatistics() Statistics {
|
||||
func (mrl *MasterRecipeList) GetStatistics() StatisticsSnapshot {
|
||||
return mrl.stats.GetSnapshot()
|
||||
}
|
||||
|
||||
|
@ -140,7 +140,7 @@ func (r *Recipe) GetTotalBuildComponents() int8 {
|
||||
|
||||
count := int8(0)
|
||||
for slot := SlotBuild1; slot <= SlotBuild4; slot++ {
|
||||
if len(r.Components[slot]) > 0 {
|
||||
if len(r.Components[int8(slot)]) > 0 {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ func (mrbl *MasterRecipeBookList) GetAllRecipeBooks() map[int32]*Recipe {
|
||||
}
|
||||
|
||||
// GetStatistics returns a snapshot of the current statistics
|
||||
func (mrbl *MasterRecipeBookList) GetStatistics() Statistics {
|
||||
func (mrbl *MasterRecipeBookList) GetStatistics() StatisticsSnapshot {
|
||||
return mrbl.stats.GetSnapshot()
|
||||
}
|
||||
|
||||
|
865
internal/recipes/recipes_test.go
Normal file
865
internal/recipes/recipes_test.go
Normal file
@ -0,0 +1,865 @@
|
||||
package recipes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test Recipe creation and basic operations
|
||||
func TestNewRecipe(t *testing.T) {
|
||||
recipe := NewRecipe()
|
||||
if recipe == nil {
|
||||
t.Error("NewRecipe should not return nil")
|
||||
}
|
||||
if recipe.Components == nil {
|
||||
t.Error("Components map should be initialized")
|
||||
}
|
||||
if recipe.Products == nil {
|
||||
t.Error("Products map should be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRecipeFromRecipe(t *testing.T) {
|
||||
// Test with nil source
|
||||
recipe := NewRecipeFromRecipe(nil)
|
||||
if recipe == nil {
|
||||
t.Error("NewRecipeFromRecipe with nil should return valid recipe")
|
||||
}
|
||||
|
||||
// Test with valid source
|
||||
source := NewRecipe()
|
||||
source.ID = 12345
|
||||
source.Name = "Test Recipe"
|
||||
source.Level = 50
|
||||
source.Tier = 5
|
||||
source.Icon = 100
|
||||
source.Components[0] = []int32{1001, 1002}
|
||||
source.Products[0] = &RecipeProducts{
|
||||
ProductID: 2001,
|
||||
ProductQty: 1,
|
||||
}
|
||||
|
||||
copied := NewRecipeFromRecipe(source)
|
||||
if copied == nil {
|
||||
t.Error("NewRecipeFromRecipe should not return nil")
|
||||
}
|
||||
if copied.ID != source.ID {
|
||||
t.Error("Copied recipe should have same ID")
|
||||
}
|
||||
if copied.Name != source.Name {
|
||||
t.Error("Copied recipe should have same name")
|
||||
}
|
||||
if copied.Level != source.Level {
|
||||
t.Error("Copied recipe should have same level")
|
||||
}
|
||||
if copied.Tier != source.Tier {
|
||||
t.Error("Copied recipe should have same tier")
|
||||
}
|
||||
|
||||
// Check component copying
|
||||
if len(copied.Components[0]) != len(source.Components[0]) {
|
||||
t.Error("Components should be copied")
|
||||
}
|
||||
|
||||
// Check product copying
|
||||
if copied.Products[0] == nil {
|
||||
t.Error("Products should be copied")
|
||||
}
|
||||
if copied.Products[0].ProductID != source.Products[0].ProductID {
|
||||
t.Error("Product data should be copied correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecipeIsValid(t *testing.T) {
|
||||
// Test invalid recipe (empty)
|
||||
recipe := NewRecipe()
|
||||
if recipe.IsValid() {
|
||||
t.Error("Empty recipe should not be valid")
|
||||
}
|
||||
|
||||
// Test valid recipe
|
||||
recipe.ID = 12345
|
||||
recipe.Name = "Valid Recipe"
|
||||
recipe.Level = 50
|
||||
recipe.Tier = 5 // Need valid tier for validation
|
||||
if !recipe.IsValid() {
|
||||
t.Error("Recipe with valid data should be valid")
|
||||
}
|
||||
|
||||
// Test invalid ID
|
||||
recipe.ID = 0
|
||||
if recipe.IsValid() {
|
||||
t.Error("Recipe with ID 0 should not be valid")
|
||||
}
|
||||
|
||||
// Test empty name
|
||||
recipe.ID = 12345
|
||||
recipe.Name = ""
|
||||
if recipe.IsValid() {
|
||||
t.Error("Recipe with empty name should not be valid")
|
||||
}
|
||||
|
||||
// Test invalid level
|
||||
recipe.Name = "Valid Recipe"
|
||||
recipe.Tier = 5
|
||||
recipe.Level = -1
|
||||
if recipe.IsValid() {
|
||||
t.Error("Recipe with negative level should not be valid")
|
||||
}
|
||||
|
||||
recipe.Level = 101
|
||||
if recipe.IsValid() {
|
||||
t.Error("Recipe with level > 100 should not be valid")
|
||||
}
|
||||
|
||||
// Test invalid tier
|
||||
recipe.Level = 50
|
||||
recipe.Tier = 0
|
||||
if recipe.IsValid() {
|
||||
t.Error("Recipe with tier 0 should not be valid")
|
||||
}
|
||||
|
||||
recipe.Tier = 11
|
||||
if recipe.IsValid() {
|
||||
t.Error("Recipe with tier > 10 should not be valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecipeGetTotalBuildComponents(t *testing.T) {
|
||||
recipe := NewRecipe()
|
||||
|
||||
// Test with no build components
|
||||
if recipe.GetTotalBuildComponents() != 0 {
|
||||
t.Error("Recipe with no build components should return 0")
|
||||
}
|
||||
|
||||
// Add build components to different slots
|
||||
recipe.Components[SlotBuild1] = []int32{1001}
|
||||
recipe.Components[SlotBuild3] = []int32{1003, 1004}
|
||||
|
||||
count := recipe.GetTotalBuildComponents()
|
||||
if count != 2 {
|
||||
t.Errorf("Expected 2 build component slots, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecipeCanUseRecipeByClass(t *testing.T) {
|
||||
recipe := NewRecipe()
|
||||
|
||||
// Test "any class" recipe
|
||||
recipe.Classes = 2
|
||||
if !recipe.CanUseRecipeByClass(1) {
|
||||
t.Error("Any class recipe should be usable by any class")
|
||||
}
|
||||
|
||||
// Test specific class requirement
|
||||
recipe.Classes = 1 << 3 // Bit 3 set (class ID 3)
|
||||
if !recipe.CanUseRecipeByClass(3) {
|
||||
t.Error("Recipe should be usable by matching class")
|
||||
}
|
||||
if recipe.CanUseRecipeByClass(2) {
|
||||
t.Error("Recipe should not be usable by non-matching class")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecipeComponentOperations(t *testing.T) {
|
||||
recipe := NewRecipe()
|
||||
|
||||
// Test getting components by slot
|
||||
recipe.Components[0] = []int32{1001, 1002, 1003}
|
||||
components := recipe.GetComponentsBySlot(0)
|
||||
if len(components) != 3 {
|
||||
t.Error("Should return all components for slot")
|
||||
}
|
||||
|
||||
// Test with empty slot
|
||||
emptyComponents := recipe.GetComponentsBySlot(1)
|
||||
if len(emptyComponents) != 0 {
|
||||
t.Error("Empty slot should return empty slice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecipeProductOperations(t *testing.T) {
|
||||
recipe := NewRecipe()
|
||||
|
||||
// Test getting products for stage
|
||||
recipe.Products[0] = &RecipeProducts{
|
||||
ProductID: 2001,
|
||||
ProductQty: 1,
|
||||
ByproductID: 2002,
|
||||
ByproductQty: 2,
|
||||
}
|
||||
|
||||
products := recipe.GetProductsForStage(0)
|
||||
if products == nil {
|
||||
t.Error("Should return products for stage")
|
||||
}
|
||||
if products.ProductID != 2001 {
|
||||
t.Error("Product ID should match")
|
||||
}
|
||||
if products.ByproductID != 2002 {
|
||||
t.Error("Byproduct ID should match")
|
||||
}
|
||||
|
||||
// Test with empty stage
|
||||
emptyProducts := recipe.GetProductsForStage(1)
|
||||
if emptyProducts != nil {
|
||||
t.Error("Empty stage should return nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Test MasterRecipeList operations
|
||||
func TestNewMasterRecipeList(t *testing.T) {
|
||||
list := NewMasterRecipeList()
|
||||
if list == nil {
|
||||
t.Error("NewMasterRecipeList should not return nil")
|
||||
}
|
||||
if list.recipes == nil {
|
||||
t.Error("Recipes map should be initialized")
|
||||
}
|
||||
if list.stats == nil {
|
||||
t.Error("Statistics should be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterRecipeListAddRecipe(t *testing.T) {
|
||||
list := NewMasterRecipeList()
|
||||
|
||||
// Test adding nil recipe
|
||||
if list.AddRecipe(nil) {
|
||||
t.Error("Adding nil recipe should fail")
|
||||
}
|
||||
|
||||
// Test adding invalid recipe
|
||||
invalidRecipe := NewRecipe()
|
||||
if list.AddRecipe(invalidRecipe) {
|
||||
t.Error("Adding invalid recipe should fail")
|
||||
}
|
||||
|
||||
// Test adding valid recipe
|
||||
validRecipe := NewRecipe()
|
||||
validRecipe.ID = 12345
|
||||
validRecipe.Name = "Test Recipe"
|
||||
validRecipe.Level = 50
|
||||
validRecipe.Tier = 5 // Need valid tier
|
||||
if !list.AddRecipe(validRecipe) {
|
||||
t.Error("Adding valid recipe should succeed")
|
||||
}
|
||||
|
||||
// Test adding duplicate recipe
|
||||
duplicate := NewRecipe()
|
||||
duplicate.ID = 12345
|
||||
duplicate.Name = "Duplicate"
|
||||
duplicate.Level = 60
|
||||
if list.AddRecipe(duplicate) {
|
||||
t.Error("Adding duplicate recipe ID should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterRecipeListGetRecipe(t *testing.T) {
|
||||
list := NewMasterRecipeList()
|
||||
|
||||
// Test getting non-existent recipe
|
||||
recipe := list.GetRecipe(99999)
|
||||
if recipe != nil {
|
||||
t.Error("Getting non-existent recipe should return nil")
|
||||
}
|
||||
|
||||
// Add a recipe
|
||||
testRecipe := NewRecipe()
|
||||
testRecipe.ID = 12345
|
||||
testRecipe.Name = "Test Recipe"
|
||||
testRecipe.Level = 50
|
||||
testRecipe.Tier = 5 // Need valid tier
|
||||
list.AddRecipe(testRecipe)
|
||||
|
||||
// Test getting existing recipe
|
||||
retrieved := list.GetRecipe(12345)
|
||||
if retrieved == nil {
|
||||
t.Error("Getting existing recipe should not return nil")
|
||||
}
|
||||
if retrieved.ID != 12345 {
|
||||
t.Error("Retrieved recipe should have correct ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterRecipeListSize(t *testing.T) {
|
||||
list := NewMasterRecipeList()
|
||||
|
||||
// Test empty list
|
||||
if list.Size() != 0 {
|
||||
t.Error("Empty list should have size 0")
|
||||
}
|
||||
|
||||
// Add recipes
|
||||
for i := 1; i <= 3; i++ {
|
||||
recipe := NewRecipe()
|
||||
recipe.ID = int32(i)
|
||||
recipe.Name = "Test Recipe"
|
||||
recipe.Level = 50
|
||||
recipe.Tier = 5
|
||||
list.AddRecipe(recipe)
|
||||
}
|
||||
|
||||
if list.Size() != 3 {
|
||||
t.Error("List with 3 recipes should have size 3")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterRecipeListClear(t *testing.T) {
|
||||
list := NewMasterRecipeList()
|
||||
|
||||
// Add some recipes
|
||||
for i := 1; i <= 3; i++ {
|
||||
recipe := NewRecipe()
|
||||
recipe.ID = int32(i)
|
||||
recipe.Name = "Test Recipe"
|
||||
recipe.Level = 50
|
||||
recipe.Tier = 5
|
||||
list.AddRecipe(recipe)
|
||||
}
|
||||
|
||||
// Clear the list
|
||||
list.ClearRecipes()
|
||||
|
||||
if list.Size() != 0 {
|
||||
t.Error("Cleared list should have size 0")
|
||||
}
|
||||
if list.GetRecipe(1) != nil {
|
||||
t.Error("Recipe should not exist after clear")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterRecipeListGetRecipesByTier(t *testing.T) {
|
||||
list := NewMasterRecipeList()
|
||||
|
||||
// Add recipes with different tiers
|
||||
for i := 1; i <= 5; i++ {
|
||||
recipe := NewRecipe()
|
||||
recipe.ID = int32(i)
|
||||
recipe.Name = "Test Recipe"
|
||||
recipe.Level = int8(i * 10)
|
||||
recipe.Tier = int8(i)
|
||||
list.AddRecipe(recipe)
|
||||
}
|
||||
|
||||
// Get recipes for tier 3
|
||||
tier3Recipes := list.GetRecipesByTier(3)
|
||||
if len(tier3Recipes) != 1 {
|
||||
t.Errorf("Expected 1 recipe for tier 3, got %d", len(tier3Recipes))
|
||||
}
|
||||
if tier3Recipes[0].Tier != 3 {
|
||||
t.Error("Recipe should have tier 3")
|
||||
}
|
||||
|
||||
// Get recipes for non-existent tier
|
||||
emptyTier := list.GetRecipesByTier(99)
|
||||
if len(emptyTier) != 0 {
|
||||
t.Error("Non-existent tier should return empty slice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterRecipeListGetRecipesBySkill(t *testing.T) {
|
||||
list := NewMasterRecipeList()
|
||||
|
||||
// Add recipes with different skills
|
||||
skillRecipe1 := NewRecipe()
|
||||
skillRecipe1.ID = 1
|
||||
skillRecipe1.Name = "Skill Recipe 1"
|
||||
skillRecipe1.Level = 50
|
||||
skillRecipe1.Tier = 5
|
||||
skillRecipe1.Skill = 100
|
||||
list.AddRecipe(skillRecipe1)
|
||||
|
||||
skillRecipe2 := NewRecipe()
|
||||
skillRecipe2.ID = 2
|
||||
skillRecipe2.Name = "Skill Recipe 2"
|
||||
skillRecipe2.Level = 60
|
||||
skillRecipe2.Tier = 6
|
||||
skillRecipe2.Skill = 100
|
||||
list.AddRecipe(skillRecipe2)
|
||||
|
||||
differentSkillRecipe := NewRecipe()
|
||||
differentSkillRecipe.ID = 3
|
||||
differentSkillRecipe.Name = "Different Skill Recipe"
|
||||
differentSkillRecipe.Level = 50
|
||||
differentSkillRecipe.Tier = 5
|
||||
differentSkillRecipe.Skill = 200
|
||||
list.AddRecipe(differentSkillRecipe)
|
||||
|
||||
// Get recipes for skill 100
|
||||
skill100Recipes := list.GetRecipesBySkill(100)
|
||||
if len(skill100Recipes) != 2 {
|
||||
t.Errorf("Expected 2 recipes for skill 100, got %d", len(skill100Recipes))
|
||||
}
|
||||
|
||||
// Get recipes for skill 200
|
||||
skill200Recipes := list.GetRecipesBySkill(200)
|
||||
if len(skill200Recipes) != 1 {
|
||||
t.Errorf("Expected 1 recipe for skill 200, got %d", len(skill200Recipes))
|
||||
}
|
||||
}
|
||||
|
||||
// Test PlayerRecipeList operations
|
||||
func TestNewPlayerRecipeList(t *testing.T) {
|
||||
playerList := NewPlayerRecipeList()
|
||||
if playerList == nil {
|
||||
t.Error("NewPlayerRecipeList should not return nil")
|
||||
}
|
||||
if playerList.recipes == nil {
|
||||
t.Error("Recipes map should be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerRecipeListAddRecipe(t *testing.T) {
|
||||
playerList := NewPlayerRecipeList()
|
||||
|
||||
// Test adding nil recipe
|
||||
if playerList.AddRecipe(nil) {
|
||||
t.Error("Adding nil recipe should fail")
|
||||
}
|
||||
|
||||
// Test adding valid recipe
|
||||
recipe := NewRecipe()
|
||||
recipe.ID = 12345
|
||||
recipe.Name = "Test Recipe"
|
||||
recipe.Level = 50
|
||||
recipe.Tier = 5
|
||||
if !playerList.AddRecipe(recipe) {
|
||||
t.Error("Adding valid recipe should succeed")
|
||||
}
|
||||
|
||||
// Test adding duplicate recipe
|
||||
duplicate := NewRecipe()
|
||||
duplicate.ID = 12345
|
||||
duplicate.Name = "Duplicate"
|
||||
duplicate.Level = 60
|
||||
if playerList.AddRecipe(duplicate) {
|
||||
t.Error("Adding duplicate recipe should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerRecipeListGetRecipe(t *testing.T) {
|
||||
playerList := NewPlayerRecipeList()
|
||||
|
||||
// Test checking non-existent recipe
|
||||
if playerList.GetRecipe(99999) != nil {
|
||||
t.Error("Player should not have non-existent recipe")
|
||||
}
|
||||
|
||||
// Add a recipe
|
||||
recipe := NewRecipe()
|
||||
recipe.ID = 12345
|
||||
recipe.Name = "Test Recipe"
|
||||
recipe.Level = 50
|
||||
recipe.Tier = 5
|
||||
playerList.AddRecipe(recipe)
|
||||
|
||||
// Test checking existing recipe
|
||||
if playerList.GetRecipe(12345) == nil {
|
||||
t.Error("Player should have added recipe")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerRecipeListSize(t *testing.T) {
|
||||
playerList := NewPlayerRecipeList()
|
||||
|
||||
// Test empty list
|
||||
if playerList.Size() != 0 {
|
||||
t.Error("Empty player recipe list should have size 0")
|
||||
}
|
||||
|
||||
// Add recipes
|
||||
for i := 1; i <= 3; i++ {
|
||||
recipe := NewRecipe()
|
||||
recipe.ID = int32(i)
|
||||
recipe.Name = "Test Recipe"
|
||||
recipe.Level = 50
|
||||
recipe.Tier = 5
|
||||
playerList.AddRecipe(recipe)
|
||||
}
|
||||
|
||||
if playerList.Size() != 3 {
|
||||
t.Error("Player recipe list with 3 recipes should have size 3")
|
||||
}
|
||||
}
|
||||
|
||||
// Test Recipe Book List operations
|
||||
func TestNewMasterRecipeBookList(t *testing.T) {
|
||||
bookList := NewMasterRecipeBookList()
|
||||
if bookList == nil {
|
||||
t.Error("NewMasterRecipeBookList should not return nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterRecipeBookListOperations(t *testing.T) {
|
||||
bookList := NewMasterRecipeBookList()
|
||||
|
||||
// Test adding recipe book
|
||||
recipeBook := NewRecipe()
|
||||
recipeBook.ID = 5001
|
||||
recipeBook.BookID = 5001 // Need BookID for recipe books
|
||||
recipeBook.Name = "Test Recipe Book"
|
||||
recipeBook.Level = 1
|
||||
recipeBook.Tier = 1
|
||||
|
||||
if !bookList.AddRecipeBook(recipeBook) {
|
||||
t.Error("Adding valid recipe book should succeed")
|
||||
}
|
||||
|
||||
// Test getting recipe book
|
||||
retrieved := bookList.GetRecipeBook(5001)
|
||||
if retrieved == nil {
|
||||
t.Error("Should retrieve added recipe book")
|
||||
}
|
||||
if retrieved.ID != 5001 {
|
||||
t.Error("Retrieved recipe book should have correct ID")
|
||||
}
|
||||
|
||||
// Test size
|
||||
if bookList.Size() != 1 {
|
||||
t.Error("Recipe book list should have size 1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPlayerRecipeBookList(t *testing.T) {
|
||||
playerBookList := NewPlayerRecipeBookList()
|
||||
if playerBookList == nil {
|
||||
t.Error("NewPlayerRecipeBookList should not return nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Test RecipeManager operations
|
||||
func TestNewRecipeManager(t *testing.T) {
|
||||
manager := NewRecipeManager()
|
||||
if manager == nil {
|
||||
t.Error("NewRecipeManager should not return nil")
|
||||
}
|
||||
if manager.masterRecipeList == nil {
|
||||
t.Error("Master recipe list should be initialized")
|
||||
}
|
||||
if manager.masterRecipeBookList == nil {
|
||||
t.Error("Master recipe book list should be initialized")
|
||||
}
|
||||
if manager.loadedRecipes == nil {
|
||||
t.Error("Loaded recipes map should be initialized")
|
||||
}
|
||||
if manager.loadedRecipeBooks == nil {
|
||||
t.Error("Loaded recipe books map should be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecipeManagerGetters(t *testing.T) {
|
||||
manager := NewRecipeManager()
|
||||
|
||||
// Test getting master lists
|
||||
masterList := manager.GetMasterRecipeList()
|
||||
if masterList == nil {
|
||||
t.Error("Should return master recipe list")
|
||||
}
|
||||
|
||||
masterBookList := manager.GetMasterRecipeBookList()
|
||||
if masterBookList == nil {
|
||||
t.Error("Should return master recipe book list")
|
||||
}
|
||||
|
||||
// Test getting non-existent recipe
|
||||
recipe := manager.GetRecipe(99999)
|
||||
if recipe != nil {
|
||||
t.Error("Non-existent recipe should return nil")
|
||||
}
|
||||
|
||||
// Test getting non-existent recipe book
|
||||
book := manager.GetRecipeBook(99999)
|
||||
if book != nil {
|
||||
t.Error("Non-existent recipe book should return nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecipeManagerStatistics(t *testing.T) {
|
||||
manager := NewRecipeManager()
|
||||
|
||||
// Test getting statistics when enabled
|
||||
manager.GetStatistics() // Call method without assignment to avoid lock copy
|
||||
|
||||
// Test disabling statistics
|
||||
manager.SetStatisticsEnabled(false)
|
||||
manager.GetStatistics() // Call method without assignment to avoid lock copy
|
||||
|
||||
// Test enabling statistics
|
||||
manager.SetStatisticsEnabled(true)
|
||||
}
|
||||
|
||||
func TestRecipeManagerValidation(t *testing.T) {
|
||||
manager := NewRecipeManager()
|
||||
|
||||
// Test validation of empty manager
|
||||
issues := manager.Validate()
|
||||
if len(issues) > 0 {
|
||||
// Manager validation might find issues, that's fine
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecipeManagerSize(t *testing.T) {
|
||||
manager := NewRecipeManager()
|
||||
|
||||
// Test size of empty manager
|
||||
recipes, books := manager.Size()
|
||||
if recipes != 0 {
|
||||
t.Error("Empty manager should have 0 recipes")
|
||||
}
|
||||
if books != 0 {
|
||||
t.Error("Empty manager should have 0 recipe books")
|
||||
}
|
||||
}
|
||||
|
||||
// Test RecipeComponent operations
|
||||
func TestRecipeComponent(t *testing.T) {
|
||||
component := &RecipeComponent{
|
||||
ItemID: 1001,
|
||||
Slot: 0,
|
||||
}
|
||||
|
||||
if component.ItemID != 1001 {
|
||||
t.Error("Component item ID should be set correctly")
|
||||
}
|
||||
if component.Slot != 0 {
|
||||
t.Error("Component slot should be set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// Test RecipeProducts operations
|
||||
func TestRecipeProducts(t *testing.T) {
|
||||
products := &RecipeProducts{
|
||||
ProductID: 2001,
|
||||
ProductQty: 1,
|
||||
ByproductID: 2002,
|
||||
ByproductQty: 2,
|
||||
}
|
||||
|
||||
if products.ProductID != 2001 {
|
||||
t.Error("Product ID should be set correctly")
|
||||
}
|
||||
if products.ProductQty != 1 {
|
||||
t.Error("Product quantity should be set correctly")
|
||||
}
|
||||
if products.ByproductID != 2002 {
|
||||
t.Error("Byproduct ID should be set correctly")
|
||||
}
|
||||
if products.ByproductQty != 2 {
|
||||
t.Error("Byproduct quantity should be set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// Test Statistics operations
|
||||
func TestNewStatistics(t *testing.T) {
|
||||
stats := NewStatistics()
|
||||
if stats == nil {
|
||||
t.Error("NewStatistics should not return nil")
|
||||
}
|
||||
if stats.RecipesByTier == nil {
|
||||
t.Error("RecipesByTier map should be initialized")
|
||||
}
|
||||
if stats.RecipesBySkill == nil {
|
||||
t.Error("RecipesBySkill map should be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatisticsOperations(t *testing.T) {
|
||||
stats := NewStatistics()
|
||||
|
||||
// Test incrementing lookups
|
||||
initialLookups := stats.RecipeLookups
|
||||
stats.IncrementRecipeLookups()
|
||||
if stats.RecipeLookups != initialLookups+1 {
|
||||
t.Error("Recipe lookups should be incremented")
|
||||
}
|
||||
|
||||
// Test incrementing recipe book lookups
|
||||
initialBookLookups := stats.RecipeBookLookups
|
||||
stats.IncrementRecipeBookLookups()
|
||||
if stats.RecipeBookLookups != initialBookLookups+1 {
|
||||
t.Error("Recipe book lookups should be incremented")
|
||||
}
|
||||
|
||||
// Test incrementing player recipe loads
|
||||
initialLoads := stats.PlayerRecipeLoads
|
||||
stats.IncrementPlayerRecipeLoads()
|
||||
if stats.PlayerRecipeLoads != initialLoads+1 {
|
||||
t.Error("Player recipe loads should be incremented")
|
||||
}
|
||||
|
||||
// Test incrementing component queries
|
||||
initialQueries := stats.ComponentQueries
|
||||
stats.IncrementComponentQueries()
|
||||
if stats.ComponentQueries != initialQueries+1 {
|
||||
t.Error("Component queries should be incremented")
|
||||
}
|
||||
|
||||
// Test getting snapshot
|
||||
snapshot := stats.GetSnapshot()
|
||||
if snapshot.RecipeLookups != stats.RecipeLookups {
|
||||
t.Error("Snapshot should match current stats")
|
||||
}
|
||||
}
|
||||
|
||||
// Test error constants
|
||||
func TestErrorConstants(t *testing.T) {
|
||||
if ErrRecipeNotFound == nil {
|
||||
t.Error("ErrRecipeNotFound should be defined")
|
||||
}
|
||||
if ErrRecipeBookNotFound == nil {
|
||||
t.Error("ErrRecipeBookNotFound should be defined")
|
||||
}
|
||||
if ErrInvalidRecipeID == nil {
|
||||
t.Error("ErrInvalidRecipeID should be defined")
|
||||
}
|
||||
if ErrDuplicateRecipe == nil {
|
||||
t.Error("ErrDuplicateRecipe should be defined")
|
||||
}
|
||||
}
|
||||
|
||||
// Test constants
|
||||
func TestConstants(t *testing.T) {
|
||||
// Test slot constants
|
||||
if SlotPrimary != 0 {
|
||||
t.Error("SlotPrimary should be 0")
|
||||
}
|
||||
if SlotBuild1 != 1 {
|
||||
t.Error("SlotBuild1 should be 1")
|
||||
}
|
||||
if SlotFuel != 5 {
|
||||
t.Error("SlotFuel should be 5")
|
||||
}
|
||||
|
||||
// Test stage constants
|
||||
if Stage0 != 0 {
|
||||
t.Error("Stage0 should be 0")
|
||||
}
|
||||
if Stage4 != 4 {
|
||||
t.Error("Stage4 should be 4")
|
||||
}
|
||||
|
||||
// Test validation constants
|
||||
if MinRecipeID != 1 {
|
||||
t.Error("MinRecipeID should be 1")
|
||||
}
|
||||
if MaxRecipeLevel != 100 {
|
||||
t.Error("MaxRecipeLevel should be 100")
|
||||
}
|
||||
}
|
||||
|
||||
// Edge case tests
|
||||
func TestRecipeEdgeCases(t *testing.T) {
|
||||
recipe := NewRecipe()
|
||||
|
||||
// Test accessing non-existent component slots
|
||||
components := recipe.GetComponentsBySlot(99)
|
||||
if len(components) != 0 {
|
||||
t.Error("Non-existent slot should return empty slice")
|
||||
}
|
||||
|
||||
// Test accessing non-existent product stages
|
||||
products := recipe.GetProductsForStage(99)
|
||||
if products != nil {
|
||||
t.Error("Non-existent stage should return nil")
|
||||
}
|
||||
|
||||
// Test recipe with maximum values
|
||||
recipe.ID = MaxRecipeID
|
||||
recipe.Level = MaxRecipeLevel
|
||||
recipe.Tier = MaxTier
|
||||
recipe.Name = "Max Recipe"
|
||||
if !recipe.IsValid() {
|
||||
t.Error("Recipe with maximum valid values should be valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterRecipeListEdgeCases(t *testing.T) {
|
||||
list := NewMasterRecipeList()
|
||||
|
||||
// Test operations on empty list
|
||||
emptyTier := list.GetRecipesByTier(1)
|
||||
if len(emptyTier) != 0 {
|
||||
t.Error("Empty list should return empty slice for tier query")
|
||||
}
|
||||
|
||||
emptySkill := list.GetRecipesBySkill(100)
|
||||
if len(emptySkill) != 0 {
|
||||
t.Error("Empty list should return empty slice for skill query")
|
||||
}
|
||||
|
||||
// Test clearing already empty list
|
||||
list.ClearRecipes()
|
||||
if list.Size() != 0 {
|
||||
t.Error("Clearing empty list should keep size 0")
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkNewRecipe(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
NewRecipe()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRecipeCopy(b *testing.B) {
|
||||
source := NewRecipe()
|
||||
source.ID = 12345
|
||||
source.Name = "Benchmark Recipe"
|
||||
source.Level = 50
|
||||
source.Tier = 5
|
||||
source.Components[0] = []int32{1001, 1002, 1003}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
NewRecipeFromRecipe(source)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterRecipeListAdd(b *testing.B) {
|
||||
list := NewMasterRecipeList()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
recipe := NewRecipe()
|
||||
recipe.ID = int32(i)
|
||||
recipe.Name = "Benchmark Recipe"
|
||||
recipe.Level = 50
|
||||
recipe.Tier = 5
|
||||
list.AddRecipe(recipe)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterRecipeListGet(b *testing.B) {
|
||||
list := NewMasterRecipeList()
|
||||
|
||||
// Add test recipes
|
||||
for i := 1; i <= 1000; i++ {
|
||||
recipe := NewRecipe()
|
||||
recipe.ID = int32(i)
|
||||
recipe.Name = "Benchmark Recipe"
|
||||
recipe.Level = 50
|
||||
recipe.Tier = 5
|
||||
list.AddRecipe(recipe)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
list.GetRecipe(int32((i % 1000) + 1))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPlayerRecipeListOperations(b *testing.B) {
|
||||
playerList := NewPlayerRecipeList()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
recipe := NewRecipe()
|
||||
recipe.ID = int32(i)
|
||||
recipe.Name = "Benchmark Recipe"
|
||||
recipe.Level = 50
|
||||
recipe.Tier = 5
|
||||
playerList.AddRecipe(recipe)
|
||||
playerList.GetRecipe(int32(i))
|
||||
}
|
||||
}
|
@ -98,6 +98,18 @@ type Statistics struct {
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// StatisticsSnapshot represents a snapshot of statistics without mutex
|
||||
type StatisticsSnapshot struct {
|
||||
TotalRecipes int32
|
||||
TotalRecipeBooks int32
|
||||
RecipesByTier map[int8]int32
|
||||
RecipesBySkill map[int32]int32
|
||||
RecipeLookups int64
|
||||
RecipeBookLookups int64
|
||||
PlayerRecipeLoads int64
|
||||
ComponentQueries int64
|
||||
}
|
||||
|
||||
// NewStatistics creates a new statistics tracker
|
||||
func NewStatistics() *Statistics {
|
||||
return &Statistics{
|
||||
@ -135,11 +147,11 @@ func (s *Statistics) IncrementComponentQueries() {
|
||||
}
|
||||
|
||||
// GetSnapshot returns a snapshot of the current statistics
|
||||
func (s *Statistics) GetSnapshot() Statistics {
|
||||
func (s *Statistics) GetSnapshot() StatisticsSnapshot {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
snapshot := Statistics{
|
||||
snapshot := StatisticsSnapshot{
|
||||
TotalRecipes: s.TotalRecipes,
|
||||
TotalRecipeBooks: s.TotalRecipeBooks,
|
||||
RecipesByTier: make(map[int8]int32),
|
||||
|
Loading…
x
Reference in New Issue
Block a user