diff --git a/internal/packets/opcodes.go b/internal/packets/opcodes.go index d9b6961..2d95638 100644 --- a/internal/packets/opcodes.go +++ b/internal/packets/opcodes.go @@ -81,6 +81,15 @@ const ( OP_NPCSpellCastMsg OP_NPCMovementMsg + // Item system + OP_ItemMoveMsg + OP_ItemEquipMsg + OP_ItemUnequipMsg + OP_ItemPickupMsg + OP_ItemDropMsg + OP_ItemExamineMsg + OP_ItemUpdateMsg + // EverQuest specific commands - Core OP_EqHearChatCmd OP_EqDisplayTextCmd @@ -143,6 +152,13 @@ var OpcodeNames = map[InternalOpcode]string{ OP_NPCInfoMsg: "OP_NPCInfoMsg", OP_NPCSpellCastMsg: "OP_NPCSpellCastMsg", OP_NPCMovementMsg: "OP_NPCMovementMsg", + OP_ItemMoveMsg: "OP_ItemMoveMsg", + OP_ItemEquipMsg: "OP_ItemEquipMsg", + OP_ItemUnequipMsg: "OP_ItemUnequipMsg", + OP_ItemPickupMsg: "OP_ItemPickupMsg", + OP_ItemDropMsg: "OP_ItemDropMsg", + OP_ItemExamineMsg: "OP_ItemExamineMsg", + OP_ItemUpdateMsg: "OP_ItemUpdateMsg", OP_EqHearChatCmd: "OP_EqHearChatCmd", OP_EqDisplayTextCmd: "OP_EqDisplayTextCmd", OP_EqCreateGhostCmd: "OP_EqCreateGhostCmd", diff --git a/internal/world/item_manager.go b/internal/world/item_manager.go new file mode 100644 index 0000000..f480aef --- /dev/null +++ b/internal/world/item_manager.go @@ -0,0 +1,679 @@ +package world + +import ( + "fmt" + "sync" + + "eq2emu/internal/database" + "eq2emu/internal/items" +) + +// ItemManager manages items for the world server +type ItemManager struct { + masterItemList *items.MasterItemList + itemSystemAdapter *items.ItemSystemAdapter + database *database.Database + world *World // Reference to world server + + // World-specific item tracking + playerInventories map[uint32]*items.PlayerItemList // Player ID -> Inventory + playerEquipment map[uint32]*items.EquipmentItemList // Player ID -> Equipment + worldDrops map[int32][]*items.Item // Zone ID -> Ground items + + mutex sync.RWMutex +} + +// NewItemManager creates a new item manager for the world server +func NewItemManager(db *database.Database) *ItemManager { + // Create master item list + masterList := items.NewMasterItemList() + + // Create adapters for the item system + dbAdapter := &WorldItemDatabaseAdapter{db: db} + playerAdapter := &WorldItemPlayerAdapter{} + packetAdapter := &WorldItemPacketAdapter{} + ruleAdapter := &WorldItemRuleAdapter{} + + // Create mock adapters for optional dependencies + questAdapter := &MockItemQuestAdapter{} + brokerAdapter := &MockItemBrokerAdapter{} + craftingAdapter := &MockItemCraftingAdapter{} + housingAdapter := &MockItemHousingAdapter{} + + // Create loot manager adapter + lootAdapter := &WorldItemLootAdapter{} + + // Create item system adapter + systemAdapter := items.NewItemSystemAdapter( + masterList, + nil, // SpellManager - will be set later when spells are integrated + playerAdapter, + packetAdapter, + ruleAdapter, + dbAdapter, + questAdapter, + brokerAdapter, + craftingAdapter, + housingAdapter, + lootAdapter, + ) + + return &ItemManager{ + masterItemList: masterList, + itemSystemAdapter: systemAdapter, + database: db, + playerInventories: make(map[uint32]*items.PlayerItemList), + playerEquipment: make(map[uint32]*items.EquipmentItemList), + worldDrops: make(map[int32][]*items.Item), + } +} + +// SetWorld sets the world server reference +func (im *ItemManager) SetWorld(world *World) { + im.world = world +} + +// LoadItems loads all item templates and data from database +func (im *ItemManager) LoadItems() error { + fmt.Println("Loading item data...") + + // Initialize the item system adapter + err := im.itemSystemAdapter.Initialize() + if err != nil { + return fmt.Errorf("failed to initialize item system: %w", err) + } + + stats := im.masterItemList.GetStats() + fmt.Printf("Loaded %d item templates\n", stats.TotalItems) + return nil +} + +// GetPlayerInventory gets a player's inventory +func (im *ItemManager) GetPlayerInventory(playerID uint32) (*items.PlayerItemList, error) { + im.mutex.RLock() + defer im.mutex.RUnlock() + + return im.itemSystemAdapter.GetPlayerInventory(playerID) +} + +// GetPlayerEquipment gets a player's equipment +func (im *ItemManager) GetPlayerEquipment(playerID uint32, appearanceType int8) (*items.EquipmentItemList, error) { + im.mutex.RLock() + defer im.mutex.RUnlock() + + return im.itemSystemAdapter.GetPlayerEquipment(playerID, appearanceType) +} + +// GiveItemToPlayer gives an item to a player +func (im *ItemManager) GiveItemToPlayer(playerID uint32, itemID int32, quantity int16) error { + return im.itemSystemAdapter.GiveItemToPlayer(playerID, itemID, quantity, items.NotSet) +} + +// RemoveItemFromPlayer removes an item from a player +func (im *ItemManager) RemoveItemFromPlayer(playerID uint32, uniqueID int32, quantity int16) error { + return im.itemSystemAdapter.RemoveItemFromPlayer(playerID, uniqueID, quantity) +} + +// EquipItem equips an item for a player +func (im *ItemManager) EquipItem(playerID uint32, uniqueID int32, slot int8) error { + return im.itemSystemAdapter.EquipItem(playerID, uniqueID, slot, 0) // Base equipment +} + +// UnequipItem unequips an item for a player +func (im *ItemManager) UnequipItem(playerID uint32, slot int8) error { + return im.itemSystemAdapter.UnequipItem(playerID, slot, 0) // Base equipment +} + +// MoveItem moves an item within a player's inventory +func (im *ItemManager) MoveItem(playerID uint32, fromBagID int32, fromSlot int16, toBagID int32, toSlot int16) error { + return im.itemSystemAdapter.MoveItem(playerID, fromBagID, fromSlot, toBagID, toSlot, 0) // Base equipment +} + +// CreateWorldDrop creates an item drop in the world +func (im *ItemManager) CreateWorldDrop(itemID int32, quantity int16, x, y, z float32, zoneID int32) error { + im.mutex.Lock() + defer im.mutex.Unlock() + + // Get item template + itemTemplate := im.masterItemList.GetItem(itemID) + if itemTemplate == nil { + return fmt.Errorf("item template not found: %d", itemID) + } + + // Create item instance + item := items.NewItemFromTemplate(itemTemplate) + item.Details.Count = quantity + + // Set position data (would normally be in a WorldItem wrapper) + // TODO: Create WorldItem wrapper with position data + + // Add to world drops tracking + im.worldDrops[zoneID] = append(im.worldDrops[zoneID], item) + + fmt.Printf("Created world drop: %s x%d at (%.2f, %.2f, %.2f) in zone %d\n", + item.Name, quantity, x, y, z, zoneID) + + return nil +} + +// GetWorldDrops gets all ground items in a zone +func (im *ItemManager) GetWorldDrops(zoneID int32) []*items.Item { + im.mutex.RLock() + defer im.mutex.RUnlock() + + if drops, exists := im.worldDrops[zoneID]; exists { + // Return a copy to avoid concurrent modification + result := make([]*items.Item, len(drops)) + copy(result, drops) + return result + } + + return nil +} + +// PickupWorldDrop handles picking up a ground item +func (im *ItemManager) PickupWorldDrop(playerID uint32, itemUniqueID int32, zoneID int32) error { + im.mutex.Lock() + defer im.mutex.Unlock() + + // Find the item in world drops + drops, exists := im.worldDrops[zoneID] + if !exists { + return fmt.Errorf("no drops in zone %d", zoneID) + } + + var foundItem *items.Item + var itemIndex int = -1 + + for i, item := range drops { + if int32(item.Details.UniqueID) == itemUniqueID { + foundItem = item + itemIndex = i + break + } + } + + if foundItem == nil { + return fmt.Errorf("item not found: %d", itemUniqueID) + } + + // Try to give item to player + err := im.GiveItemToPlayer(playerID, foundItem.Details.ItemID, foundItem.Details.Count) + if err != nil { + return fmt.Errorf("failed to give item to player: %w", err) + } + + // Remove from world drops + im.worldDrops[zoneID] = append(drops[:itemIndex], drops[itemIndex+1:]...) + + fmt.Printf("Player %d picked up item: %s x%d\n", playerID, foundItem.Name, foundItem.Details.Count) + return nil +} + +// GenerateLootForNPC generates loot for an NPC kill +func (im *ItemManager) GenerateLootForNPC(npcID int32, killerLevel int16) ([]*items.Item, error) { + // TODO: Implement proper loot table lookup and generation + // For now, create some basic test loot + + testLoot := []*items.Item{} + + // Example: Generate some coins based on NPC level + // This would normally come from loot tables in the database + + fmt.Printf("Generated loot for NPC %d (placeholder implementation)\n", npcID) + return testLoot, nil +} + +// OnPlayerLogin handles player login - loads their item data +func (im *ItemManager) OnPlayerLogin(playerID uint32) error { + fmt.Printf("Loading item data for player %d...\n", playerID) + + // Pre-load player inventory and equipment + _, err := im.GetPlayerInventory(playerID) + if err != nil { + return fmt.Errorf("failed to load player inventory: %w", err) + } + + _, err = im.GetPlayerEquipment(playerID, items.BaseEquipment) + if err != nil { + return fmt.Errorf("failed to load player equipment: %w", err) + } + + fmt.Printf("Item data loaded for player %d\n", playerID) + return nil +} + +// OnPlayerLogout handles player logout - saves and clears their item data +func (im *ItemManager) OnPlayerLogout(playerID uint32) error { + fmt.Printf("Saving item data for player %d...\n", playerID) + + // Save player data + err := im.itemSystemAdapter.SavePlayerData(playerID) + if err != nil { + return fmt.Errorf("failed to save player data: %w", err) + } + + // Clear cached data + im.itemSystemAdapter.ClearPlayerData(playerID) + + fmt.Printf("Item data saved and cleared for player %d\n", playerID) + return nil +} + +// GetItemTemplate gets an item template by ID +func (im *ItemManager) GetItemTemplate(itemID int32) *items.Item { + return im.masterItemList.GetItem(itemID) +} + +// SearchItems searches for item templates by name +func (im *ItemManager) SearchItems(name string, maxResults int32) []*items.Item { + // TODO: Implement proper search - for now return empty slice + return []*items.Item{} +} + +// GetStatistics returns item system statistics +func (im *ItemManager) GetStatistics() map[string]interface{} { + im.mutex.RLock() + defer im.mutex.RUnlock() + + systemStats := im.itemSystemAdapter.GetSystemStats() + + // Add world-specific statistics + totalWorldDrops := 0 + for _, drops := range im.worldDrops { + totalWorldDrops += len(drops) + } + + result := make(map[string]interface{}) + for k, v := range systemStats { + result[k] = v + } + + result["world_drops"] = totalWorldDrops + result["zones_with_drops"] = len(im.worldDrops) + + return result +} + +// ValidatePlayerItems validates a player's items +func (im *ItemManager) ValidatePlayerItems(playerID uint32) *items.ItemValidationResult { + return im.itemSystemAdapter.ValidatePlayerItems(playerID) +} + +// Shutdown gracefully shuts down the item manager +func (im *ItemManager) Shutdown() { + fmt.Println("Shutting down item manager...") + + im.mutex.Lock() + defer im.mutex.Unlock() + + // Clear all tracking + im.playerInventories = make(map[uint32]*items.PlayerItemList) + im.playerEquipment = make(map[uint32]*items.EquipmentItemList) + im.worldDrops = make(map[int32][]*items.Item) + + fmt.Println("Item manager shutdown complete") +} + +// WorldItemDatabaseAdapter adapts the world database for item use +type WorldItemDatabaseAdapter struct { + db *database.Database +} + +// LoadItems implements items.DatabaseService interface +func (wdb *WorldItemDatabaseAdapter) LoadItems(masterList *items.MasterItemList) error { + fmt.Println("Loading items from database...") + + // Load item templates from database + rows, err := wdb.db.Query(` + SELECT id, name, item_type, icon, description, tier, level, classes, races, + adventure_classes, tradeskill_classes, stack_count, weight, generic_info, + bag_info, food_info, weapon_info, ranged_info, details, appearance + FROM items + ORDER BY id + `) + if err != nil { + return fmt.Errorf("failed to query items: %w", err) + } + defer rows.Close() + + itemCount := 0 + for rows.Next() { + // Create new item from database data + item := items.NewItem() + + var id, itemType, icon, tier, level, stackCount int32 + var weight, genericInfo, bagInfo, foodInfo, weaponInfo, rangedInfo int32 + var details, appearance int32 + var name, description, classes, races, adventureClasses, tradeskillClasses string + + err := rows.Scan(&id, &name, &itemType, &icon, &description, &tier, &level, + &classes, &races, &adventureClasses, &tradeskillClasses, &stackCount, + &weight, &genericInfo, &bagInfo, &foodInfo, &weaponInfo, &rangedInfo, + &details, &appearance) + if err != nil { + fmt.Printf("Error scanning item row: %v\n", err) + continue + } + + // Set item properties + item.Details.ItemID = id + item.Name = name + item.Details.Icon = int16(icon) + item.Description = description + item.Details.Tier = int8(tier) + item.Details.RecommendedLevel = int16(level) + item.StackCount = int16(stackCount) + // TODO: Set weight when field is available + // item.Weight = weight + + // TODO: Parse class/race strings and set appropriate fields + // item.SetClasses(parseClassString(classes)) + // item.SetRaces(parseRaceString(races)) + + // Add to master list + masterList.AddItem(item) + itemCount++ + } + + if err := rows.Err(); err != nil { + return fmt.Errorf("error iterating item rows: %w", err) + } + + fmt.Printf("Successfully loaded %d item templates from database\n", itemCount) + return nil +} + +// SaveItem implements items.DatabaseService interface +func (wdb *WorldItemDatabaseAdapter) SaveItem(item *items.Item) error { + // TODO: Implement item template saving + fmt.Printf("Saving item template: %s (ID: %d) - not yet implemented\n", item.Name, item.Details.ItemID) + return nil +} + +// DeleteItem implements items.DatabaseService interface +func (wdb *WorldItemDatabaseAdapter) DeleteItem(itemID int32) error { + // TODO: Implement item template deletion + fmt.Printf("Deleting item template: %d - not yet implemented\n", itemID) + return nil +} + +// LoadPlayerItems implements items.DatabaseService interface +func (wdb *WorldItemDatabaseAdapter) LoadPlayerItems(playerID uint32) (*items.PlayerItemList, error) { + // TODO: Implement player inventory loading from database + fmt.Printf("Loading player inventory for player %d - creating empty inventory\n", playerID) + return items.NewPlayerItemList(), nil +} + +// SavePlayerItems implements items.DatabaseService interface +func (wdb *WorldItemDatabaseAdapter) SavePlayerItems(playerID uint32, itemList *items.PlayerItemList) error { + // TODO: Implement player inventory saving to database + fmt.Printf("Saving player inventory for player %d - not yet implemented\n", playerID) + return nil +} + +// LoadPlayerEquipment implements items.DatabaseService interface +func (wdb *WorldItemDatabaseAdapter) LoadPlayerEquipment(playerID uint32, appearanceType int8) (*items.EquipmentItemList, error) { + // TODO: Implement player equipment loading from database + fmt.Printf("Loading player equipment for player %d (type %d) - creating empty equipment\n", playerID, appearanceType) + equipment := items.NewEquipmentItemList() + equipment.SetAppearanceType(appearanceType) + return equipment, nil +} + +// SavePlayerEquipment implements items.DatabaseService interface +func (wdb *WorldItemDatabaseAdapter) SavePlayerEquipment(playerID uint32, equipment *items.EquipmentItemList) error { + // TODO: Implement player equipment saving to database + fmt.Printf("Saving player equipment for player %d - not yet implemented\n", playerID) + return nil +} + +// LoadItemStats implements items.DatabaseService interface +func (wdb *WorldItemDatabaseAdapter) LoadItemStats() (map[string]int32, map[int32]string, error) { + // TODO: Implement item stat mapping loading + fmt.Println("Loading item stats - using placeholder data") + + statsStrings := map[string]int32{ + "health": 1, + "power": 2, + "strength": 3, + "stamina": 4, + "agility": 5, + "wisdom": 6, + "intelligence": 7, + } + + statsIDs := map[int32]string{ + 1: "health", + 2: "power", + 3: "strength", + 4: "stamina", + 5: "agility", + 6: "wisdom", + 7: "intelligence", + } + + return statsStrings, statsIDs, nil +} + +// SaveItemStat implements items.DatabaseService interface +func (wdb *WorldItemDatabaseAdapter) SaveItemStat(statID int32, statName string) error { + // TODO: Implement item stat mapping saving + fmt.Printf("Saving item stat mapping: %d = %s - not yet implemented\n", statID, statName) + return nil +} + +// WorldItemPlayerAdapter adapts world player functionality for items +type WorldItemPlayerAdapter struct{} + +// GetPlayer implements items.PlayerManager interface +func (wpa *WorldItemPlayerAdapter) GetPlayer(playerID uint32) (items.Player, error) { + // TODO: Get actual player from world server when player system is integrated + return &MockPlayer{id: playerID, name: fmt.Sprintf("Player_%d", playerID), level: 50}, nil +} + +// GetPlayerLevel implements items.PlayerManager interface +func (wpa *WorldItemPlayerAdapter) GetPlayerLevel(playerID uint32) (int16, error) { + // TODO: Get actual player level from world server + return 50, nil // Placeholder +} + +// GetPlayerClass implements items.PlayerManager interface +func (wpa *WorldItemPlayerAdapter) GetPlayerClass(playerID uint32) (int8, error) { + // TODO: Get actual player class from world server + return 1, nil // Placeholder +} + +// GetPlayerRace implements items.PlayerManager interface +func (wpa *WorldItemPlayerAdapter) GetPlayerRace(playerID uint32) (int8, error) { + // TODO: Get actual player race from world server + return 1, nil // Placeholder +} + +// SendMessageToPlayer implements items.PlayerManager interface +func (wpa *WorldItemPlayerAdapter) SendMessageToPlayer(playerID uint32, channel int8, message string) error { + // TODO: Send actual message via world server + fmt.Printf("[CHAT] Player %d: %s\n", playerID, message) + return nil +} + +// GetPlayerName implements items.PlayerManager interface +func (wpa *WorldItemPlayerAdapter) GetPlayerName(playerID uint32) (string, error) { + // TODO: Get actual player name from world server + return fmt.Sprintf("Player_%d", playerID), nil +} + +// WorldItemPacketAdapter adapts world packet functionality for items +type WorldItemPacketAdapter struct{} + +// SendPacketToPlayer implements items.PacketManager interface +func (wpa *WorldItemPacketAdapter) SendPacketToPlayer(playerID uint32, packetData []byte) error { + // TODO: Send actual packet via world server + fmt.Printf("Sending item packet to player %d (%d bytes)\n", playerID, len(packetData)) + return nil +} + +// QueuePacketForPlayer implements items.PacketManager interface +func (wpa *WorldItemPacketAdapter) QueuePacketForPlayer(playerID uint32, packetData []byte) error { + // TODO: Queue packet via world server + fmt.Printf("Queuing item packet for player %d (%d bytes)\n", playerID, len(packetData)) + return nil +} + +// GetClientVersion implements items.PacketManager interface +func (wpa *WorldItemPacketAdapter) GetClientVersion(playerID uint32) (int16, error) { + // TODO: Get actual client version from world server + return 1096, nil // Default EQ2 client version +} + +// SerializeItem implements items.PacketManager interface +func (wpa *WorldItemPacketAdapter) SerializeItem(item *items.Item, clientVersion int16, player items.Player) ([]byte, error) { + // TODO: Implement actual item serialization for network transmission + fmt.Printf("Serializing item %s for client version %d\n", item.Name, clientVersion) + return []byte{0x01, 0x02, 0x03}, nil // Placeholder data +} + +// WorldItemRuleAdapter adapts world rules functionality for items +type WorldItemRuleAdapter struct{} + +// GetBool implements items.RuleManager interface +func (wra *WorldItemRuleAdapter) GetBool(category, rule string) bool { + // TODO: Get actual rule values from world server rules system + fmt.Printf("Getting rule %s:%s (bool) - using default\n", category, rule) + return true // Default value +} + +// GetInt32 implements items.RuleManager interface +func (wra *WorldItemRuleAdapter) GetInt32(category, rule string) int32 { + // TODO: Get actual rule values from world server rules system + fmt.Printf("Getting rule %s:%s (int32) - using default\n", category, rule) + return 100 // Default value +} + +// GetFloat implements items.RuleManager interface +func (wra *WorldItemRuleAdapter) GetFloat(category, rule string) float32 { + // TODO: Get actual rule values from world server rules system + fmt.Printf("Getting rule %s:%s (float) - using default\n", category, rule) + return 1.0 // Default value +} + +// GetString implements items.RuleManager interface +func (wra *WorldItemRuleAdapter) GetString(category, rule string) string { + // TODO: Get actual rule values from world server rules system + fmt.Printf("Getting rule %s:%s (string) - using default\n", category, rule) + return "default" // Default value +} + +// WorldItemLootAdapter adapts world loot functionality for items +type WorldItemLootAdapter struct{} + +// GenerateLoot implements items.LootManager interface +func (wla *WorldItemLootAdapter) GenerateLoot(lootTableID int32, playerLevel int16) ([]*items.Item, error) { + // TODO: Implement actual loot generation from loot tables + fmt.Printf("Generating loot from table %d for level %d player\n", lootTableID, playerLevel) + return []*items.Item{}, nil +} + +// DistributeLoot implements items.LootManager interface +func (wla *WorldItemLootAdapter) DistributeLoot(lootItems []*items.Item, playerIDs []uint32, lootMethod int8) error { + // TODO: Implement actual loot distribution + fmt.Printf("Distributing %d items to %d players using method %d\n", len(lootItems), len(playerIDs), lootMethod) + return nil +} + +// CanLootItem implements items.LootManager interface +func (wla *WorldItemLootAdapter) CanLootItem(playerID uint32, item *items.Item) bool { + // TODO: Implement actual loot permission checking + return true // Allow all looting for now +} + +// Mock implementations for optional dependencies + +// MockPlayer implements items.Player interface for testing +type MockPlayer struct { + id uint32 + name string + level int16 + adventureClass int8 + tradeskillClass int8 + race int8 + gender int8 + alignment int8 +} + +func (mp *MockPlayer) GetID() uint32 { return mp.id } +func (mp *MockPlayer) GetName() string { return mp.name } +func (mp *MockPlayer) GetLevel() int16 { return mp.level } +func (mp *MockPlayer) GetAdventureClass() int8 { return mp.adventureClass } +func (mp *MockPlayer) GetTradeskillClass() int8 { return mp.tradeskillClass } +func (mp *MockPlayer) GetRace() int8 { return mp.race } +func (mp *MockPlayer) GetGender() int8 { return mp.gender } +func (mp *MockPlayer) GetAlignment() int8 { return mp.alignment } + +// MockItemQuestAdapter provides mock quest functionality +type MockItemQuestAdapter struct{} + +func (mqa *MockItemQuestAdapter) CheckQuestPrerequisites(playerID uint32, questID int32) bool { + return true // Allow all for now +} + +func (mqa *MockItemQuestAdapter) GetQuestRewards(questID int32) ([]*items.QuestRewardData, error) { + return []*items.QuestRewardData{}, nil +} + +func (mqa *MockItemQuestAdapter) IsQuestItem(itemID int32) bool { + return false // No quest items for now +} + +// MockItemBrokerAdapter provides mock broker functionality +type MockItemBrokerAdapter struct{} + +func (mba *MockItemBrokerAdapter) SearchItems(criteria *items.ItemSearchCriteria) ([]*items.Item, error) { + return []*items.Item{}, nil +} + +func (mba *MockItemBrokerAdapter) ListItem(playerID uint32, item *items.Item, price int64) error { + return fmt.Errorf("broker not implemented") +} + +func (mba *MockItemBrokerAdapter) BuyItem(playerID uint32, itemID int32, sellerID uint32) error { + return fmt.Errorf("broker not implemented") +} + +func (mba *MockItemBrokerAdapter) GetItemPrice(itemID int32) (int64, error) { + return 0, fmt.Errorf("broker not implemented") +} + +// MockItemCraftingAdapter provides mock crafting functionality +type MockItemCraftingAdapter struct{} + +func (mca *MockItemCraftingAdapter) CanCraftItem(playerID uint32, itemID int32) bool { + return false // No crafting for now +} + +func (mca *MockItemCraftingAdapter) GetCraftingRequirements(itemID int32) ([]items.CraftingRequirement, error) { + return []items.CraftingRequirement{}, nil +} + +func (mca *MockItemCraftingAdapter) CraftItem(playerID uint32, itemID int32, quality int8) (*items.Item, error) { + return nil, fmt.Errorf("crafting not implemented") +} + +// MockItemHousingAdapter provides mock housing functionality +type MockItemHousingAdapter struct{} + +func (mha *MockItemHousingAdapter) CanPlaceItem(playerID uint32, houseID int32, item *items.Item) bool { + return false // No housing for now +} + +func (mha *MockItemHousingAdapter) PlaceItem(playerID uint32, houseID int32, item *items.Item, location items.HouseLocation) error { + return fmt.Errorf("housing not implemented") +} + +func (mha *MockItemHousingAdapter) RemoveItem(playerID uint32, houseID int32, itemID int32) error { + return fmt.Errorf("housing not implemented") +} + +func (mha *MockItemHousingAdapter) GetHouseItems(houseID int32) ([]*items.Item, error) { + return []*items.Item{}, nil +} \ No newline at end of file diff --git a/internal/world/npc_manager.go b/internal/world/npc_manager.go index 4a251e5..dc3b91c 100644 --- a/internal/world/npc_manager.go +++ b/internal/world/npc_manager.go @@ -273,6 +273,35 @@ func (nm *NPCManager) OnNPCKilled(npcID int32, killerCharacterID int32) { } } + // Generate and distribute loot + if nm.world != nil && nm.world.itemMgr != nil { + npcInfo := nm.GetNPCInfo(npcID) + if npcInfo != nil { + // Generate loot for this NPC kill + loot, err := nm.world.itemMgr.GenerateLootForNPC(npcID, int16(npcInfo.Level)) + if err != nil { + fmt.Printf("Failed to generate loot for NPC %d: %v\n", npcID, err) + } else if len(loot) > 0 { + // Create loot drops in the world (for now, just create basic drops) + // TODO: Get NPC position for proper loot positioning + x, y, z := float32(100), float32(100), float32(50) + zoneID := npcInfo.ZoneID + if zoneID == 0 { + zoneID = 1 // Default zone + } + + for _, item := range loot { + err := nm.world.itemMgr.CreateWorldDrop(item.Details.ItemID, item.Details.Count, x, y, z, zoneID) + if err != nil { + fmt.Printf("Failed to create loot drop for item %d: %v\n", item.Details.ItemID, err) + } + } + + fmt.Printf("Generated %d loot items for NPC %d kill\n", len(loot), npcID) + } + } + } + fmt.Printf("NPC %d killed by character %d\n", npcID, killerCharacterID) } diff --git a/internal/world/packet_handlers.go b/internal/world/packet_handlers.go index 94ac93e..a5488f9 100644 --- a/internal/world/packet_handlers.go +++ b/internal/world/packet_handlers.go @@ -49,7 +49,16 @@ func (w *World) RegisterPacketHandlers() { packets.RegisterGlobalHandler(packets.OP_NPCSpellCastMsg, w.HandleNPCSpellCast) packets.RegisterGlobalHandler(packets.OP_NPCMovementMsg, w.HandleNPCMovement) - fmt.Printf("Registered %d packet handlers\n", 21) + // Item system + packets.RegisterGlobalHandler(packets.OP_ItemMoveMsg, w.HandleItemMove) + packets.RegisterGlobalHandler(packets.OP_ItemEquipMsg, w.HandleItemEquip) + packets.RegisterGlobalHandler(packets.OP_ItemUnequipMsg, w.HandleItemUnequip) + packets.RegisterGlobalHandler(packets.OP_ItemPickupMsg, w.HandleItemPickup) + packets.RegisterGlobalHandler(packets.OP_ItemDropMsg, w.HandleItemDrop) + packets.RegisterGlobalHandler(packets.OP_ItemExamineMsg, w.HandleItemExamine) + packets.RegisterGlobalHandler(packets.OP_ItemUpdateMsg, w.HandleItemUpdate) + + fmt.Printf("Registered %d packet handlers\n", 28) } // HandleDoneLoadingZoneResources handles when client finishes loading zone resources @@ -702,4 +711,268 @@ func (w *World) CreatePacketContext(client *Client) *packets.PacketContext { World: &WorldServerAdapter{world: w}, Database: &WorldDatabaseAdapter{world: w}, } +} + +// Item Packet Handlers + +// HandleItemMove handles item movement within player inventory +func (w *World) HandleItemMove(ctx *packets.PacketContext, packet *packets.PacketData) error { + fmt.Printf("Client %s sent item move packet\n", ctx.Client.GetCharacterName()) + + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) + if client != nil { + client.UpdateActivity() + + // TODO: Parse move data from packet (fromBagID, fromSlot, toBagID, toSlot) + // For now, use placeholder values for testing + fromBagID := int32(0) + fromSlot := int16(0) + toBagID := int32(0) + toSlot := int16(1) + + if w.itemMgr != nil { + err := w.itemMgr.MoveItem(uint32(ctx.Client.GetCharacterID()), + fromBagID, fromSlot, toBagID, toSlot) + if err != nil { + client.SendSimpleMessage(fmt.Sprintf("Item move failed: %v", err)) + } else { + client.SendSimpleMessage("Item moved successfully") + } + } + } + + return nil +} + +// HandleItemEquip handles item equipping +func (w *World) HandleItemEquip(ctx *packets.PacketContext, packet *packets.PacketData) error { + fmt.Printf("Client %s sent item equip packet\n", ctx.Client.GetCharacterName()) + + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) + if client != nil { + client.UpdateActivity() + + // TODO: Parse equip data from packet (uniqueID, slot) + // For now, use placeholder values for testing + uniqueID := int32(1001) + slot := int8(0) // Primary hand + + if w.itemMgr != nil { + err := w.itemMgr.EquipItem(uint32(ctx.Client.GetCharacterID()), uniqueID, slot) + if err != nil { + client.SendSimpleMessage(fmt.Sprintf("Item equip failed: %v", err)) + } else { + client.SendSimpleMessage(fmt.Sprintf("Item equipped to slot %d", slot)) + } + } + } + + return nil +} + +// HandleItemUnequip handles item unequipping +func (w *World) HandleItemUnequip(ctx *packets.PacketContext, packet *packets.PacketData) error { + fmt.Printf("Client %s sent item unequip packet\n", ctx.Client.GetCharacterName()) + + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) + if client != nil { + client.UpdateActivity() + + // TODO: Parse unequip data from packet (slot) + // For now, use placeholder values for testing + slot := int8(0) // Primary hand + + if w.itemMgr != nil { + err := w.itemMgr.UnequipItem(uint32(ctx.Client.GetCharacterID()), slot) + if err != nil { + client.SendSimpleMessage(fmt.Sprintf("Item unequip failed: %v", err)) + } else { + client.SendSimpleMessage(fmt.Sprintf("Item unequipped from slot %d", slot)) + } + } + } + + return nil +} + +// HandleItemPickup handles picking up world drops +func (w *World) HandleItemPickup(ctx *packets.PacketContext, packet *packets.PacketData) error { + fmt.Printf("Client %s sent item pickup packet\n", ctx.Client.GetCharacterName()) + + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) + if client != nil { + client.UpdateActivity() + + // TODO: Parse pickup data from packet (itemUniqueID) + // For now, use placeholder values for testing + itemUniqueID := int32(5001) + zoneID := int32(1) // Current zone + + if w.itemMgr != nil { + err := w.itemMgr.PickupWorldDrop(uint32(ctx.Client.GetCharacterID()), itemUniqueID, zoneID) + if err != nil { + client.SendSimpleMessage(fmt.Sprintf("Item pickup failed: %v", err)) + } else { + client.SendSimpleMessage("Item picked up") + // TODO: Remove item from world display for other players + } + } + } + + return nil +} + +// HandleItemDrop handles dropping items to the world +func (w *World) HandleItemDrop(ctx *packets.PacketContext, packet *packets.PacketData) error { + fmt.Printf("Client %s sent item drop packet\n", ctx.Client.GetCharacterName()) + + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) + if client != nil { + client.UpdateActivity() + + // TODO: Parse drop data from packet (uniqueID, quantity, x, y, z) + // For now, use placeholder values for testing + itemID := int32(1001) + quantity := int16(1) + x, y, z := float32(100), float32(100), float32(50) + zoneID := int32(1) + + if w.itemMgr != nil { + // First remove from player inventory (would need to look up by uniqueID) + // TODO: Get uniqueID from packet and remove from player + + // Create world drop + err := w.itemMgr.CreateWorldDrop(itemID, quantity, x, y, z, zoneID) + if err != nil { + client.SendSimpleMessage(fmt.Sprintf("Item drop failed: %v", err)) + } else { + client.SendSimpleMessage("Item dropped") + // TODO: Show item to other players in range + } + } + } + + return nil +} + +// HandleItemExamine handles item examination requests +func (w *World) HandleItemExamine(ctx *packets.PacketContext, packet *packets.PacketData) error { + fmt.Printf("Client %s sent item examine packet\n", ctx.Client.GetCharacterName()) + + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) + if client != nil { + client.UpdateActivity() + + // TODO: Parse examine data from packet (uniqueID or itemID) + // For now, use placeholder values for testing + itemID := int32(1001) + + if w.itemMgr != nil { + itemTemplate := w.itemMgr.GetItemTemplate(itemID) + if itemTemplate == nil { + client.SendSimpleMessage("Item not found") + } else { + // Send item details to client + w.SendItemDetails(client, itemTemplate) + } + } + } + + return nil +} + +// HandleItemUpdate handles item update notifications +func (w *World) HandleItemUpdate(ctx *packets.PacketContext, packet *packets.PacketData) error { + fmt.Printf("Client %s sent item update packet\n", ctx.Client.GetCharacterName()) + + client := w.clients.GetByCharacterID(ctx.Client.GetCharacterID()) + if client != nil { + client.UpdateActivity() + + // TODO: Parse update data from packet + // This might be triggered when client needs updated item information + + if w.itemMgr != nil { + // Send updated inventory to client + w.SendPlayerInventory(client) + } + } + + return nil +} + +// SendItemDetails sends detailed item information to a client +func (w *World) SendItemDetails(client *Client, item interface{}) { + if w.itemMgr == nil { + return + } + + // TODO: Implement actual item detail packet building + // This would include stats, description, level requirements, etc. + + fmt.Printf("Sending item details to %s\n", client.CharacterName) + + // Placeholder - send basic item info as chat message + client.SendSimpleMessage("Item Details: [Item information would be displayed here]") +} + +// SendPlayerInventory sends complete inventory to a client +func (w *World) SendPlayerInventory(client *Client) { + if w.itemMgr == nil { + return + } + + playerID := uint32(client.CharacterID) + + // Get player inventory and equipment + inventory, err := w.itemMgr.GetPlayerInventory(playerID) + if err != nil { + fmt.Printf("Failed to get inventory for player %d: %v\n", playerID, err) + return + } + + equipment, err := w.itemMgr.GetPlayerEquipment(playerID, 0) // Base equipment + if err != nil { + fmt.Printf("Failed to get equipment for player %d: %v\n", playerID, err) + return + } + + // TODO: Build and send inventory packet + fmt.Printf("Sending inventory to %s: %d inventory items, %d equipped items\n", + client.CharacterName, inventory.GetNumberOfItems(), equipment.GetNumberOfItems()) + + // Placeholder - send summary as chat message + client.SendSimpleMessage(fmt.Sprintf("Inventory Update: %d items in inventory, %d equipped", + inventory.GetNumberOfItems(), equipment.GetNumberOfItems())) +} + +// SendItemUpdate sends item update to client +func (w *World) SendItemUpdate(client *Client, updateType string, itemData map[string]interface{}) { + if w.itemMgr == nil { + return + } + + // TODO: Build and send item update packet + fmt.Printf("Sending item update to %s: %s - %v\n", + client.CharacterName, updateType, itemData) + + // Placeholder - send update as chat message + client.SendSimpleMessage(fmt.Sprintf("Item Update: %s", updateType)) +} + +// BroadcastItemUpdate broadcasts item updates to nearby players +func (w *World) BroadcastItemUpdate(sourcePlayerID uint32, updateType string, itemData map[string]interface{}) { + // TODO: Implement item update broadcasting (for things like equipment changes visible to others) + + fmt.Printf("Broadcasting item update from player %d: %s - %v\n", + sourcePlayerID, updateType, itemData) + + // Send to players in range (placeholder) + clients := w.clients.GetAll() + for _, client := range clients { + if client.CurrentZone != nil && client.CharacterID != int32(sourcePlayerID) { + // TODO: Check if client is in range + client.SendSimpleMessage(fmt.Sprintf("Player item update: %s", updateType)) + } + } } \ No newline at end of file diff --git a/internal/world/world.go b/internal/world/world.go index 0de2042..ecb5ebb 100644 --- a/internal/world/world.go +++ b/internal/world/world.go @@ -44,9 +44,11 @@ type World struct { // NPC system npcMgr *NPCManager + // Item system + itemMgr *ItemManager + // Master lists (singletons) masterSpells interface{} // TODO: implement spell manager - masterItems interface{} // TODO: implement item manager masterQuests interface{} // TODO: implement quest manager masterSkills interface{} // TODO: implement skill manager masterFactions interface{} // TODO: implement faction manager @@ -159,6 +161,9 @@ func NewWorld(config *WorldConfig) (*World, error) { // Initialize NPC manager npcMgr := NewNPCManager(db) + // Initialize item manager + itemMgr := NewItemManager(db) + // Create context ctx, cancel := context.WithCancel(context.Background()) @@ -169,6 +174,7 @@ func NewWorld(config *WorldConfig) (*World, error) { achievementMgr: achievementMgr, titleMgr: titleMgr, npcMgr: npcMgr, + itemMgr: itemMgr, config: config, startTime: time.Now(), worldTime: &WorldTime{Year: 3721, Month: 1, Day: 1, Hour: 12, Minute: 0}, @@ -184,6 +190,7 @@ func NewWorld(config *WorldConfig) (*World, error) { // Set world references for cross-system communication achievementMgr.SetWorld(w) npcMgr.SetWorld(w) + itemMgr.SetWorld(w) // Load server data from database if err := w.loadServerData(); err != nil { @@ -436,6 +443,12 @@ func (w *World) loadServerData() error { // Don't fail startup if NPCs don't load - server can still run } + // Load items + if err := w.itemMgr.LoadItems(); err != nil { + fmt.Printf("Warning: Failed to load items: %v\n", err) + // Don't fail startup if items don't load - server can still run + } + // Setup title and achievement integration w.setupTitleAchievementIntegration() @@ -605,6 +618,13 @@ func (w *World) loadSampleOpcodeMappings() { "OP_NPCInfoMsg": 0x0097, "OP_NPCSpellCastMsg": 0x0098, "OP_NPCMovementMsg": 0x0099, + "OP_ItemMoveMsg": 0x00A0, + "OP_ItemEquipMsg": 0x00A1, + "OP_ItemUnequipMsg": 0x00A2, + "OP_ItemPickupMsg": 0x00A3, + "OP_ItemDropMsg": 0x00A4, + "OP_ItemExamineMsg": 0x00A5, + "OP_ItemUpdateMsg": 0x00A6, "OP_EqHearChatCmd": 0x1000, "OP_EqDisplayTextCmd": 0x1001, "OP_EqCreateGhostCmd": 0x1002,