557 lines
16 KiB
Markdown
557 lines
16 KiB
Markdown
# 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
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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 definitions
|
|
- `recipe_comp_list`: Component list definitions
|
|
- `recipe_comp_list_item`: Individual component items
|
|
- `recipe_secondary_comp`: Secondary component mappings
|
|
- `character_recipes`: Player recipe knowledge
|
|
- `character_recipe_books`: Player recipe book ownership
|
|
- `items`: Item definitions and recipe books
|
|
|
|
## Integration Interfaces
|
|
|
|
### RecipeSystemAdapter
|
|
|
|
Primary integration interface for external systems:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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. |