eq2go/internal/items/loot/packets.go

464 lines
14 KiB
Go

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: (item.GenericInfo.ItemFlags & uint32(LootFlagNoTrade)) != 0,
Heirloom: (item.GenericInfo.ItemFlags & uint32(LootFlagHeirloom)) != 0,
Lore: (item.GenericInfo.ItemFlags & uint32(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)
}