464 lines
14 KiB
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: (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)
|
|
} |