fix and clean up recipes

This commit is contained in:
Sky Johnson 2025-08-05 21:02:00 -05:00
parent d1b19072f7
commit 37574a7db2
8 changed files with 981 additions and 459 deletions

View File

@ -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,
}
}

View File

@ -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))
}
}

View 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
}

View File

@ -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()
}

View File

@ -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++
}
}

View File

@ -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()
}

View 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))
}
}

View File

@ -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),