package loot import ( "fmt" "log" "eq2emu/internal/items" ) // PacketBuilder interface for building loot-related packets type PacketBuilder interface { BuildUpdateLootPacket(chest *TreasureChest, playerID uint32, clientVersion int32) ([]byte, error) BuildLootItemPacket(item *items.Item, playerID uint32, clientVersion int32) ([]byte, error) BuildStoppedLootingPacket(chestID int32, playerID uint32, clientVersion int32) ([]byte, error) BuildLootResponsePacket(result *ChestInteractionResult, clientVersion int32) ([]byte, error) } // LootPacketBuilder builds loot-related packets for client communication type LootPacketBuilder struct { itemPacketBuilder ItemPacketBuilder } // ItemPacketBuilder interface for building item-related packet data type ItemPacketBuilder interface { BuildItemData(item *items.Item, clientVersion int32) ([]byte, error) GetItemAppearanceData(item *items.Item) (int32, int16, int16, int16, int16, int16, int16) } // NewLootPacketBuilder creates a new loot packet builder func NewLootPacketBuilder(itemPacketBuilder ItemPacketBuilder) *LootPacketBuilder { return &LootPacketBuilder{ itemPacketBuilder: itemPacketBuilder, } } // BuildUpdateLootPacket builds an UpdateLoot packet to show chest contents to a player func (lpb *LootPacketBuilder) BuildUpdateLootPacket(chest *TreasureChest, playerID uint32, clientVersion int32) ([]byte, error) { log.Printf("%s Building UpdateLoot packet for chest %d, player %d, version %d", LogPrefixLoot, chest.ID, playerID, clientVersion) // Start with base packet structure packet := &LootPacketData{ PacketType: "UpdateLoot", ChestID: chest.ID, SpawnID: chest.SpawnID, PlayerID: playerID, ClientVersion: clientVersion, } // Add loot items lootItems := chest.LootResult.GetItems() packet.ItemCount = int16(len(lootItems)) packet.Items = make([]*LootItemData, len(lootItems)) for i, item := range lootItems { itemData, err := lpb.buildLootItemData(item, clientVersion) if err != nil { log.Printf("%s Failed to build item data for item %d: %v", LogPrefixLoot, item.Details.ItemID, err) continue } packet.Items[i] = itemData } // Add coin information packet.Coins = chest.LootResult.GetCoins() // Build packet based on client version return lpb.buildVersionSpecificLootPacket(packet) } // buildLootItemData builds loot item data for a specific item func (lpb *LootPacketBuilder) buildLootItemData(item *items.Item, clientVersion int32) (*LootItemData, error) { // Get item appearance data appearanceID, red, green, blue, highlightRed, highlightGreen, highlightBlue := lpb.itemPacketBuilder.GetItemAppearanceData(item) return &LootItemData{ ItemID: item.Details.ItemID, UniqueID: item.Details.UniqueID, Name: item.Name, Count: item.Details.Count, Tier: item.Details.Tier, Icon: item.Details.Icon, AppearanceID: appearanceID, Red: red, Green: green, Blue: blue, HighlightRed: highlightRed, HighlightGreen: highlightGreen, HighlightBlue: highlightBlue, ItemType: item.GenericInfo.ItemType, NoTrade: (int32(item.GenericInfo.ItemFlags) & int32(LootFlagNoTrade)) != 0, Heirloom: (int32(item.GenericInfo.ItemFlags) & int32(LootFlagHeirloom)) != 0, Lore: (int32(item.GenericInfo.ItemFlags) & int32(LootFlagLore)) != 0, }, nil } // buildVersionSpecificLootPacket builds the actual packet bytes based on client version func (lpb *LootPacketBuilder) buildVersionSpecificLootPacket(packet *LootPacketData) ([]byte, error) { switch { case packet.ClientVersion >= 60114: return lpb.buildLootPacketV60114(packet) case packet.ClientVersion >= 1193: return lpb.buildLootPacketV1193(packet) case packet.ClientVersion >= 546: return lpb.buildLootPacketV546(packet) case packet.ClientVersion >= 373: return lpb.buildLootPacketV373(packet) default: return lpb.buildLootPacketV1(packet) } } // buildLootPacketV60114 builds loot packet for client version 60114+ func (lpb *LootPacketBuilder) buildLootPacketV60114(packet *LootPacketData) ([]byte, error) { // This is the most recent packet format with all features buffer := NewPacketBuffer() // Packet header buffer.WriteInt32(packet.ChestID) buffer.WriteInt32(packet.SpawnID) buffer.WriteInt16(packet.ItemCount) buffer.WriteInt32(packet.Coins) // Loot options buffer.WriteInt8(1) // loot_all_enabled buffer.WriteInt8(1) // auto_loot_enabled buffer.WriteInt8(0) // loot_timeout (0 = no timeout) // Item array for _, item := range packet.Items { if item == nil { continue } buffer.WriteInt32(item.ItemID) buffer.WriteInt64(item.UniqueID) buffer.WriteString(item.Name) buffer.WriteInt16(item.Count) buffer.WriteInt8(item.Tier) buffer.WriteInt16(item.Icon) buffer.WriteInt32(item.AppearanceID) buffer.WriteInt16(item.Red) buffer.WriteInt16(item.Green) buffer.WriteInt16(item.Blue) buffer.WriteInt16(item.HighlightRed) buffer.WriteInt16(item.HighlightGreen) buffer.WriteInt16(item.HighlightBlue) buffer.WriteInt8(item.ItemType) buffer.WriteBool(item.NoTrade) buffer.WriteBool(item.Heirloom) buffer.WriteBool(item.Lore) // Extended item data for newer clients buffer.WriteInt32(0) // adornment_slot0 buffer.WriteInt32(0) // adornment_slot1 buffer.WriteInt32(0) // adornment_slot2 } return buffer.GetBytes(), nil } // buildLootPacketV1193 builds loot packet for client version 1193+ func (lpb *LootPacketBuilder) buildLootPacketV1193(packet *LootPacketData) ([]byte, error) { buffer := NewPacketBuffer() buffer.WriteInt32(packet.ChestID) buffer.WriteInt32(packet.SpawnID) buffer.WriteInt16(packet.ItemCount) buffer.WriteInt32(packet.Coins) buffer.WriteInt8(1) // loot_all_enabled for _, item := range packet.Items { if item == nil { continue } buffer.WriteInt32(item.ItemID) buffer.WriteInt64(item.UniqueID) buffer.WriteString(item.Name) buffer.WriteInt16(item.Count) buffer.WriteInt8(item.Tier) buffer.WriteInt16(item.Icon) buffer.WriteInt32(item.AppearanceID) buffer.WriteInt16(item.Red) buffer.WriteInt16(item.Green) buffer.WriteInt16(item.Blue) buffer.WriteInt8(item.ItemType) buffer.WriteBool(item.NoTrade) buffer.WriteBool(item.Heirloom) } return buffer.GetBytes(), nil } // buildLootPacketV546 builds loot packet for client version 546+ func (lpb *LootPacketBuilder) buildLootPacketV546(packet *LootPacketData) ([]byte, error) { buffer := NewPacketBuffer() buffer.WriteInt32(packet.ChestID) buffer.WriteInt32(packet.SpawnID) buffer.WriteInt16(packet.ItemCount) buffer.WriteInt32(packet.Coins) for _, item := range packet.Items { if item == nil { continue } buffer.WriteInt32(item.ItemID) buffer.WriteInt64(item.UniqueID) buffer.WriteString(item.Name) buffer.WriteInt16(item.Count) buffer.WriteInt8(item.Tier) buffer.WriteInt16(item.Icon) buffer.WriteInt8(item.ItemType) buffer.WriteBool(item.NoTrade) } return buffer.GetBytes(), nil } // buildLootPacketV373 builds loot packet for client version 373+ func (lpb *LootPacketBuilder) buildLootPacketV373(packet *LootPacketData) ([]byte, error) { buffer := NewPacketBuffer() buffer.WriteInt32(packet.ChestID) buffer.WriteInt16(packet.ItemCount) buffer.WriteInt32(packet.Coins) for _, item := range packet.Items { if item == nil { continue } buffer.WriteInt32(item.ItemID) buffer.WriteString(item.Name) buffer.WriteInt16(item.Count) buffer.WriteInt16(item.Icon) buffer.WriteInt8(item.ItemType) } return buffer.GetBytes(), nil } // buildLootPacketV1 builds loot packet for client version 1 (oldest) func (lpb *LootPacketBuilder) buildLootPacketV1(packet *LootPacketData) ([]byte, error) { buffer := NewPacketBuffer() buffer.WriteInt32(packet.ChestID) buffer.WriteInt16(packet.ItemCount) for _, item := range packet.Items { if item == nil { continue } buffer.WriteInt32(item.ItemID) buffer.WriteString(item.Name) buffer.WriteInt16(item.Count) } return buffer.GetBytes(), nil } // BuildLootItemPacket builds a packet for when a player loots a specific item func (lpb *LootPacketBuilder) BuildLootItemPacket(item *items.Item, playerID uint32, clientVersion int32) ([]byte, error) { log.Printf("%s Building LootItem packet for item %d, player %d", LogPrefixLoot, item.Details.ItemID, playerID) buffer := NewPacketBuffer() // Basic loot item response buffer.WriteInt32(item.Details.ItemID) buffer.WriteInt64(item.Details.UniqueID) buffer.WriteString(item.Name) buffer.WriteInt16(item.Details.Count) buffer.WriteInt8(1) // success flag return buffer.GetBytes(), nil } // BuildStoppedLootingPacket builds a packet when player stops looting func (lpb *LootPacketBuilder) BuildStoppedLootingPacket(chestID int32, playerID uint32, clientVersion int32) ([]byte, error) { log.Printf("%s Building StoppedLooting packet for chest %d, player %d", LogPrefixLoot, chestID, playerID) buffer := NewPacketBuffer() buffer.WriteInt32(chestID) return buffer.GetBytes(), nil } // BuildLootResponsePacket builds a response packet for chest interactions func (lpb *LootPacketBuilder) BuildLootResponsePacket(result *ChestInteractionResult, clientVersion int32) ([]byte, error) { buffer := NewPacketBuffer() // Result code and message buffer.WriteInt8(result.Result) buffer.WriteBool(result.Success) buffer.WriteString(result.Message) // Items received buffer.WriteInt16(int16(len(result.Items))) for _, item := range result.Items { buffer.WriteInt32(item.Details.ItemID) buffer.WriteString(item.Name) buffer.WriteInt16(item.Details.Count) } // Coins received buffer.WriteInt32(result.Coins) // Experience gained buffer.WriteInt32(result.Experience) // Status flags buffer.WriteBool(result.ChestEmpty) buffer.WriteBool(result.ChestClosed) return buffer.GetBytes(), nil } // LootPacketData represents the data structure for loot packets type LootPacketData struct { PacketType string ChestID int32 SpawnID int32 PlayerID uint32 ClientVersion int32 ItemCount int16 Items []*LootItemData Coins int32 } // LootItemData represents an item in a loot packet type LootItemData struct { ItemID int32 UniqueID int64 Name string Count int16 Tier int8 Icon int16 AppearanceID int32 Red int16 Green int16 Blue int16 HighlightRed int16 HighlightGreen int16 HighlightBlue int16 ItemType int8 NoTrade bool Heirloom bool Lore bool } // PacketBuffer is a simple buffer for building packet data type PacketBuffer struct { data []byte } // NewPacketBuffer creates a new packet buffer func NewPacketBuffer() *PacketBuffer { return &PacketBuffer{ data: make([]byte, 0, 1024), } } // WriteInt8 writes an 8-bit integer func (pb *PacketBuffer) WriteInt8(value int8) { pb.data = append(pb.data, byte(value)) } // WriteInt16 writes a 16-bit integer func (pb *PacketBuffer) WriteInt16(value int16) { pb.data = append(pb.data, byte(value), byte(value>>8)) } // WriteInt32 writes a 32-bit integer func (pb *PacketBuffer) WriteInt32(value int32) { pb.data = append(pb.data, byte(value), byte(value>>8), byte(value>>16), byte(value>>24)) } // WriteInt64 writes a 64-bit integer func (pb *PacketBuffer) WriteInt64(value int64) { pb.data = append(pb.data, byte(value), byte(value>>8), byte(value>>16), byte(value>>24), byte(value>>32), byte(value>>40), byte(value>>48), byte(value>>56)) } // WriteBool writes a boolean as a single byte func (pb *PacketBuffer) WriteBool(value bool) { if value { pb.data = append(pb.data, 1) } else { pb.data = append(pb.data, 0) } } // WriteString writes a null-terminated string func (pb *PacketBuffer) WriteString(value string) { pb.data = append(pb.data, []byte(value)...) pb.data = append(pb.data, 0) // null terminator } // GetBytes returns the current buffer data func (pb *PacketBuffer) GetBytes() []byte { return pb.data } // LootPacketService provides high-level packet building services type LootPacketService struct { packetBuilder *LootPacketBuilder clientService ClientService } // ClientService interface for client-related operations type ClientService interface { GetClientVersion(playerID uint32) int32 SendPacketToPlayer(playerID uint32, packetType string, data []byte) error } // NewLootPacketService creates a new loot packet service func NewLootPacketService(packetBuilder *LootPacketBuilder, clientService ClientService) *LootPacketService { return &LootPacketService{ packetBuilder: packetBuilder, clientService: clientService, } } // SendLootUpdate sends a loot update packet to a player func (lps *LootPacketService) SendLootUpdate(chest *TreasureChest, playerID uint32) error { clientVersion := lps.clientService.GetClientVersion(playerID) packet, err := lps.packetBuilder.BuildUpdateLootPacket(chest, playerID, clientVersion) if err != nil { return fmt.Errorf("failed to build loot update packet: %v", err) } return lps.clientService.SendPacketToPlayer(playerID, "UpdateLoot", packet) } // SendLootResponse sends a loot interaction response to a player func (lps *LootPacketService) SendLootResponse(result *ChestInteractionResult, playerID uint32) error { clientVersion := lps.clientService.GetClientVersion(playerID) packet, err := lps.packetBuilder.BuildLootResponsePacket(result, clientVersion) if err != nil { return fmt.Errorf("failed to build loot response packet: %v", err) } return lps.clientService.SendPacketToPlayer(playerID, "LootResponse", packet) } // SendStoppedLooting sends a stopped looting packet to a player func (lps *LootPacketService) SendStoppedLooting(chestID int32, playerID uint32) error { clientVersion := lps.clientService.GetClientVersion(playerID) packet, err := lps.packetBuilder.BuildStoppedLootingPacket(chestID, playerID, clientVersion) if err != nil { return fmt.Errorf("failed to build stopped looting packet: %v", err) } return lps.clientService.SendPacketToPlayer(playerID, "StoppedLooting", packet) }