Recipe System
Complete tradeskill recipe management system for EverQuest II, converted from C++ EQ2EMu codebase. Handles recipe definitions, player recipe knowledge, crafting components, and database persistence with modern Go concurrency patterns.
Overview
The recipe system manages all aspects of tradeskill crafting in EQ2:
- Recipe Management: Master recipe lists with complex component relationships
- Player Recipe Knowledge: Individual player recipe collections and progress tracking
- Recipe Books: Tradeskill recipe book items and learning mechanics
- Database Integration: Complete SQLite persistence with efficient loading
- Component System: Multi-slot component requirements (primary, fuel, build slots)
- Crafting Stages: 5-stage crafting progression with products and byproducts
- Tradeskill Classes: Class-based recipe access control and validation
- Integration Interfaces: Seamless integration with player, item, client, and event systems
Core Components
Recipe Data Structure
type Recipe struct {
// Core recipe identification
ID int32 // Unique recipe ID
SoeID int32 // SOE recipe CRC ID
Name string // Recipe display name
Description string // Recipe description
// Requirements and properties
Level int8 // Required level
Tier int8 // Recipe tier (1-10)
Skill int32 // Required tradeskill ID
Technique int32 // Technique requirement
Knowledge int32 // Knowledge requirement
Classes int32 // Tradeskill class bitmask
// Product information
ProductItemID int32 // Resulting item ID
ProductName string // Product display name
ProductQty int8 // Quantity produced
// Component titles and quantities (6 slots)
PrimaryBuildCompTitle string // Primary component name
Build1CompTitle string // Build slot 1 name
Build2CompTitle string // Build slot 2 name
Build3CompTitle string // Build slot 3 name
Build4CompTitle string // Build slot 4 name
FuelCompTitle string // Fuel component name
// Components map: slot -> list of valid item IDs
// Slots: 0=primary, 1-4=build slots, 5=fuel
Components map[int8][]int32
// Products map: stage -> products/byproducts (5 stages)
Products map[int8]*RecipeProducts
// Player progression
HighestStage int8 // Highest completed stage for player recipes
}
Component Slots
The recipe system uses 6 component slots:
- Slot 0 (Primary): Primary crafting material
- Slot 1-4 (Build): Secondary build components
- Slot 5 (Fuel): Fuel component for crafting device
Each slot can contain multiple valid item IDs, providing flexibility in component selection.
Crafting Stages
Recipes support 5 crafting stages (0-4), each with potential products and byproducts:
type RecipeProducts struct {
ProductID int32 // Main product item ID
ProductQty int8 // Product quantity
ByproductID int32 // Byproduct item ID
ByproductQty int8 // Byproduct quantity
}
Master Recipe Lists
type MasterRecipeList struct {
recipes map[int32]*Recipe // Recipe ID -> Recipe
mutex sync.RWMutex // Thread-safe access
}
type MasterRecipeBookList struct {
recipeBooks map[int32]*Recipe // Book ID -> Recipe
mutex sync.RWMutex // Thread-safe access
}
Player Recipe Collections
type PlayerRecipeList struct {
recipes map[int32]*Recipe // Player's known recipes
mutex sync.RWMutex // Thread-safe access
}
type PlayerRecipeBookList struct {
recipeBooks map[int32]*Recipe // Player's recipe books
mutex sync.RWMutex // Thread-safe access
}
Database Integration
RecipeManager
High-level recipe system management with complete database integration:
type RecipeManager struct {
db *database.DB
masterRecipeList *MasterRecipeList
masterRecipeBookList *MasterRecipeBookList
loadedRecipes map[int32]*Recipe
loadedRecipeBooks map[int32]*Recipe
statisticsEnabled bool
stats RecipeManagerStats
}
Key Operations
Loading Recipes
// Load all recipes from database with complex component relationships
manager.LoadRecipes()
// Load all recipe books
manager.LoadRecipeBooks()
// Load player-specific recipes
manager.LoadPlayerRecipes(playerRecipeList, characterID)
// Load player recipe books
manager.LoadPlayerRecipeBooks(playerRecipeBookList, characterID)
Saving Recipe Progress
// Save new recipe for player
manager.SavePlayerRecipe(characterID, recipeID)
// Save recipe book for player
manager.SavePlayerRecipeBook(characterID, recipebookID)
// Update recipe progress
manager.UpdatePlayerRecipe(characterID, recipeID, highestStage)
Database Schema
The system integrates with these key database tables:
recipe
: Master recipe definitionsrecipe_comp_list
: Component list definitionsrecipe_comp_list_item
: Individual component itemsrecipe_secondary_comp
: Secondary component mappingscharacter_recipes
: Player recipe knowledgecharacter_recipe_books
: Player recipe book ownershipitems
: Item definitions and recipe books
Integration Interfaces
RecipeSystemAdapter
Primary integration interface for external systems:
type RecipeSystemAdapter interface {
// Player Integration
GetPlayerRecipeList(characterID int32) *PlayerRecipeList
LoadPlayerRecipes(characterID int32) error
// Recipe Management
GetRecipe(recipeID int32) *Recipe
ValidateRecipe(recipe *Recipe) bool
// Progress Tracking
UpdateRecipeProgress(characterID int32, recipeID int32, stage int8) error
GetRecipeProgress(characterID int32, recipeID int32) int8
}
Database Adapter
Abstracts database operations for testing and multiple database support:
type DatabaseRecipeAdapter interface {
LoadAllRecipes() ([]*Recipe, error)
LoadPlayerRecipes(characterID int32) ([]*Recipe, error)
SavePlayerRecipe(characterID int32, recipeID int32) error
UpdatePlayerRecipe(characterID int32, recipeID int32, highestStage int8) error
}
Player Integration
type PlayerRecipeAdapter interface {
CanPlayerLearnRecipe(characterID int32, recipeID int32) bool
GetPlayerTradeskillLevel(characterID int32, skillID int32) int32
AwardTradeskillExperience(characterID int32, skillID int32, experience int32) error
}
Item Integration
type ItemRecipeAdapter interface {
ValidateRecipeComponents(recipeID int32) bool
PlayerHasComponents(characterID int32, recipeID int32) bool
ConsumeRecipeComponents(characterID int32, recipeID int32) error
AwardRecipeProduct(characterID int32, itemID int32, quantity int8) error
}
Client Integration
type ClientRecipeAdapter interface {
SendRecipeList(characterID int32) error
SendRecipeLearned(characterID int32, recipeID int32) error
SendTradeskillWindow(characterID int32, deviceID int32) error
SendCraftingResults(characterID int32, success bool, itemID int32, quantity int8) error
}
Event Integration
type EventRecipeAdapter interface {
OnRecipeLearned(characterID int32, recipeID int32) error
OnRecipeCrafted(characterID int32, recipeID int32, success bool) error
CheckCraftingAchievements(characterID int32, recipeID int32) error
}
Usage Examples
Basic Recipe Management
// Create recipe manager
db, _ := database.Open("world.db")
manager := NewRecipeManager(db)
// Load all recipes and recipe books
manager.LoadRecipes()
manager.LoadRecipeBooks()
// Get a specific recipe
recipe := manager.GetRecipe(12345)
if recipe != nil {
fmt.Printf("Recipe: %s (Level %d, Tier %d)\n",
recipe.Name, recipe.Level, recipe.Tier)
}
// Check recipe validation
if recipe.IsValid() {
fmt.Println("Recipe is valid")
}
Player Recipe Operations
// Load player recipes
playerRecipes := NewPlayerRecipeList()
manager.LoadPlayerRecipes(playerRecipes, characterID)
// Check if player knows a recipe
if playerRecipes.HasRecipe(recipeID) {
fmt.Println("Player knows this recipe")
}
// Learn a new recipe
recipe := manager.GetRecipe(recipeID)
if recipe != nil {
playerCopy := NewRecipeFromRecipe(recipe)
playerRecipes.AddRecipe(playerCopy)
manager.SavePlayerRecipe(characterID, recipeID)
}
Recipe Component Analysis
// Get components for each slot
for slot := int8(0); slot < 6; slot++ {
components := recipe.GetComponentsBySlot(slot)
title := recipe.GetComponentTitleForSlot(slot)
quantity := recipe.GetComponentQuantityForSlot(slot)
fmt.Printf("Slot %d (%s): %d needed\n", slot, title, quantity)
for _, itemID := range components {
fmt.Printf(" - Item ID: %d\n", itemID)
}
}
Crafting Stage Processing
// Process each crafting stage
for stage := int8(0); stage < 5; stage++ {
products := recipe.GetProductsForStage(stage)
if products != nil {
fmt.Printf("Stage %d produces:\n", stage)
fmt.Printf(" Product: %d (qty: %d)\n",
products.ProductID, products.ProductQty)
if products.ByproductID > 0 {
fmt.Printf(" Byproduct: %d (qty: %d)\n",
products.ByproductID, products.ByproductQty)
}
}
}
Integration with Full System
// Create system dependencies
deps := &RecipeSystemDependencies{
Database: &DatabaseRecipeAdapterImpl{},
Player: &PlayerRecipeAdapterImpl{},
Item: &ItemRecipeAdapterImpl{},
Client: &ClientRecipeAdapterImpl{},
Event: &EventRecipeAdapterImpl{},
Crafting: &CraftingRecipeAdapterImpl{},
}
// Create integrated recipe system
adapter := NewRecipeManagerAdapter(db, deps)
adapter.Initialize()
// Handle recipe learning workflow
err := adapter.PlayerLearnRecipe(characterID, recipeID)
if err != nil {
fmt.Printf("Failed to learn recipe: %v\n", err)
}
// Handle recipe book acquisition workflow
err = adapter.PlayerObtainRecipeBook(characterID, bookID)
if err != nil {
fmt.Printf("Failed to obtain recipe book: %v\n", err)
}
Tradeskill Classes
The system supports all EQ2 tradeskill classes with bitmask-based access control:
Base Classes
- Provisioner (food and drink)
- Woodworker (wooden items, bows, arrows)
- Carpenter (furniture and housing items)
- Outfitter (light and medium armor)
- Armorer (heavy armor and shields)
- Weaponsmith (metal weapons)
- Tailor (cloth armor and bags)
- Scholar (spells and combat arts)
- Jeweler (jewelry)
- Sage (advanced spells)
- Alchemist (potions and poisons)
- Craftsman (general crafting)
- Tinkerer (tinkered items)
Class Validation
// Check if a tradeskill class can use a recipe
canUse := recipe.CanUseRecipeByClass(classID)
// Special handling for "any class" recipes (adornments + artisan)
if recipe.Classes < 4 {
// Any class can use this recipe
}
Crafting Devices
Recipes require specific crafting devices:
- Stove & Keg: Provisioning recipes
- Forge: Weaponsmithing and armoring
- Sewing Table & Mannequin: Tailoring and outfitting
- Woodworking Table: Woodworking and carpentry
- Chemistry Table: Alchemy
- Jeweler's Table: Jewelcrafting
- Loom & Spinning Wheel: Advanced tailoring
- Engraved Desk: Scribing and sage recipes
- Work Bench: Tinkering
Statistics and Monitoring
RecipeManagerStats
type RecipeManagerStats struct {
TotalRecipesLoaded int32
TotalRecipeBooksLoaded int32
PlayersWithRecipes int32
LoadOperations int32
SaveOperations int32
}
// Get current statistics
stats := manager.GetStatistics()
fmt.Printf("Loaded %d recipes, %d recipe books\n",
stats.TotalRecipesLoaded, stats.TotalRecipeBooksLoaded)
Validation
// Comprehensive system validation
issues := manager.Validate()
for _, issue := range issues {
fmt.Printf("Validation issue: %s\n", issue)
}
// Recipe-specific validation
if !recipe.IsValid() {
fmt.Println("Recipe has invalid data")
}
Thread Safety
All recipe system components are fully thread-safe:
- RWMutex Protection: All maps and collections use read-write mutexes
- Atomic Operations: Statistics counters use atomic operations where appropriate
- Safe Copying: Recipe copying creates deep copies of all nested data
- Concurrent Access: Multiple goroutines can safely read recipe data simultaneously
- Write Synchronization: All write operations are properly synchronized
Performance Considerations
Efficient Loading
- Batch Loading: All recipes loaded in single database query with complex joins
- Component Resolution: Components loaded separately and linked to recipes
- Memory Management: Recipes cached in memory for fast access
- Statistics Tracking: Optional statistics collection for performance monitoring
Memory Usage
- Component Maps: Pre-allocated component arrays for each recipe
- Product Arrays: Fixed-size arrays for 5 crafting stages
- Player Copies: Player recipe instances are copies, not references
- Cleanup: Proper cleanup of database connections and prepared statements
Error Handling
Comprehensive Error Types
var (
ErrRecipeNotFound = errors.New("recipe not found")
ErrRecipeBookNotFound = errors.New("recipe book not found")
ErrInvalidRecipeID = errors.New("invalid recipe ID")
ErrDuplicateRecipe = errors.New("duplicate recipe ID")
ErrMissingComponents = errors.New("missing required components")
ErrInsufficientSkill = errors.New("insufficient skill level")
ErrWrongTradeskillClass = errors.New("wrong tradeskill class")
ErrCannotLearnRecipe = errors.New("cannot learn recipe")
ErrCannotUseRecipeBook = errors.New("cannot use recipe book")
)
Error Propagation
All database operations return detailed errors that can be handled appropriately by calling systems.
Database Schema Compatibility
C++ EQ2EMu Compatibility
The Go implementation maintains full compatibility with the existing C++ EQ2EMu database schema:
- Recipe Table: Direct mapping of all C++ Recipe fields
- Component System: Preserves complex component relationships
- Player Data: Compatible with existing character recipe storage
- Query Optimization: Uses same optimized queries as C++ version
Migration Support
The system can seamlessly work with existing EQ2EMu databases without requiring schema changes or data migration.
Future Enhancements
Areas marked for future implementation:
- Lua Integration: Recipe validation and custom logic via Lua scripts
- Advanced Crafting: Rare material handling and advanced crafting mechanics
- Recipe Discovery: Dynamic recipe discovery and experimentation
- Guild Recipes: Guild-specific recipe sharing and management
- Seasonal Recipes: Time-based recipe availability
- Recipe Sets: Collection-based achievements and bonuses
Testing
Unit Tests
func TestRecipeValidation(t *testing.T) {
recipe := NewRecipe()
recipe.ID = 12345
recipe.Name = "Test Recipe"
recipe.Level = 50
recipe.Tier = 5
assert.True(t, recipe.IsValid())
}
func TestPlayerRecipeOperations(t *testing.T) {
playerRecipes := NewPlayerRecipeList()
recipe := NewRecipe()
recipe.ID = 12345
assert.True(t, playerRecipes.AddRecipe(recipe))
assert.True(t, playerRecipes.HasRecipe(12345))
assert.Equal(t, 1, playerRecipes.Size())
}
Integration Tests
func TestDatabaseIntegration(t *testing.T) {
db := setupTestDatabase()
manager := NewRecipeManager(db)
err := manager.LoadRecipes()
assert.NoError(t, err)
recipes, books := manager.Size()
assert.Greater(t, recipes, int32(0))
}
This recipe system provides a complete, thread-safe, and efficient implementation of EverQuest II tradeskill recipes with modern Go patterns while maintaining compatibility with the existing C++ EQ2EMu architecture.