487 lines
12 KiB
Go
487 lines
12 KiB
Go
package trade
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Trade represents an active trade between two entities
|
|
// Converted from C++ Trade class
|
|
type Trade struct {
|
|
// Core trade participants
|
|
trader1 *TradeParticipant // First trader (initiator)
|
|
trader2 *TradeParticipant // Second trader (recipient)
|
|
|
|
// Trade state
|
|
state TradeState // Current state of the trade
|
|
startTime time.Time // When the trade was initiated
|
|
|
|
// Thread safety
|
|
mutex sync.RWMutex
|
|
|
|
// TODO: Add references to packet system and entity manager when available
|
|
// packetManager *PacketManager
|
|
// entityManager *EntityManager
|
|
}
|
|
|
|
// NewTrade creates a new trade between two entities
|
|
// Converted from C++ Trade::Trade constructor
|
|
func NewTrade(entity1, entity2 Entity) *Trade {
|
|
if entity1 == nil || entity2 == nil {
|
|
return nil
|
|
}
|
|
|
|
trade := &Trade{
|
|
trader1: NewTradeParticipant(entity1.GetID(), entity1.IsBot(), entity1.GetClientVersion()),
|
|
trader2: NewTradeParticipant(entity2.GetID(), entity2.IsBot(), entity2.GetClientVersion()),
|
|
state: TradeStateActive,
|
|
startTime: time.Now(),
|
|
}
|
|
|
|
// TODO: Open trade window when packet system is available
|
|
// trade.openTradeWindow()
|
|
|
|
return trade
|
|
}
|
|
|
|
// GetTrader1ID returns the ID of the first trader
|
|
func (t *Trade) GetTrader1ID() int32 {
|
|
t.mutex.RLock()
|
|
defer t.mutex.RUnlock()
|
|
return t.trader1.EntityID
|
|
}
|
|
|
|
// GetTrader2ID returns the ID of the second trader
|
|
func (t *Trade) GetTrader2ID() int32 {
|
|
t.mutex.RLock()
|
|
defer t.mutex.RUnlock()
|
|
return t.trader2.EntityID
|
|
}
|
|
|
|
// GetTradee returns the other participant in the trade
|
|
// Converted from C++ Trade::GetTradee
|
|
func (t *Trade) GetTradee(entityID int32) int32 {
|
|
t.mutex.RLock()
|
|
defer t.mutex.RUnlock()
|
|
|
|
if t.trader1.EntityID == entityID {
|
|
return t.trader2.EntityID
|
|
} else if t.trader2.EntityID == entityID {
|
|
return t.trader1.EntityID
|
|
}
|
|
|
|
return 0 // Invalid entity ID
|
|
}
|
|
|
|
// GetParticipant returns the trade participant for an entity
|
|
func (t *Trade) GetParticipant(entityID int32) *TradeParticipant {
|
|
t.mutex.RLock()
|
|
defer t.mutex.RUnlock()
|
|
|
|
if t.trader1.EntityID == entityID {
|
|
return t.trader1
|
|
} else if t.trader2.EntityID == entityID {
|
|
return t.trader2
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetState returns the current trade state
|
|
func (t *Trade) GetState() TradeState {
|
|
t.mutex.RLock()
|
|
defer t.mutex.RUnlock()
|
|
return t.state
|
|
}
|
|
|
|
// AddItemToTrade adds an item to a participant's trade offer
|
|
// Converted from C++ Trade::AddItemToTrade
|
|
func (t *Trade) AddItemToTrade(entityID int32, item Item, quantity int32, slot int8) error {
|
|
if t.state != TradeStateActive {
|
|
return &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Trade is not active",
|
|
}
|
|
}
|
|
|
|
t.mutex.Lock()
|
|
defer t.mutex.Unlock()
|
|
|
|
participant := t.getParticipantUnsafe(entityID)
|
|
if participant == nil {
|
|
return &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Entity is not part of this trade",
|
|
}
|
|
}
|
|
|
|
// Auto-find slot if needed
|
|
if slot == TradeSlotAutoFind {
|
|
slot = participant.GetNextFreeSlot()
|
|
}
|
|
|
|
// Validate slot
|
|
if slot < 0 || slot >= participant.MaxSlots {
|
|
return &TradeValidationError{
|
|
Code: TradeResultSlotOutOfRange,
|
|
Message: fmt.Sprintf("Invalid trade slot: %d", slot),
|
|
}
|
|
}
|
|
|
|
// Check if slot is already occupied
|
|
if _, exists := participant.Items[slot]; exists {
|
|
return &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Trade slot is already occupied",
|
|
}
|
|
}
|
|
|
|
// Validate quantity
|
|
if quantity <= 0 {
|
|
quantity = 1
|
|
}
|
|
|
|
if quantity > item.GetQuantity() {
|
|
return &TradeValidationError{
|
|
Code: TradeResultInsufficientQty,
|
|
Message: "Not enough quantity available",
|
|
}
|
|
}
|
|
|
|
// Check if item is already in trade
|
|
if participant.HasItem(item.GetID()) {
|
|
return &TradeValidationError{
|
|
Code: TradeResultAlreadyInTrade,
|
|
Message: "Item is already in trade",
|
|
}
|
|
}
|
|
|
|
// Validate item tradability
|
|
otherID := t.getTradeeUnsafe(entityID)
|
|
if err := t.validateItemTradability(item, entityID, otherID); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add item to trade
|
|
participant.Items[slot] = TradeItemInfo{
|
|
Item: item,
|
|
Quantity: quantity,
|
|
}
|
|
|
|
// Reset acceptance flags
|
|
t.trader1.HasAccepted = false
|
|
t.trader2.HasAccepted = false
|
|
|
|
// TODO: Send trade packet when packet system is available
|
|
// t.sendTradePacket()
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveItemFromTrade removes an item from a participant's trade offer
|
|
// Converted from C++ Trade::RemoveItemFromTrade
|
|
func (t *Trade) RemoveItemFromTrade(entityID int32, slot int8) error {
|
|
if t.state != TradeStateActive {
|
|
return &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Trade is not active",
|
|
}
|
|
}
|
|
|
|
t.mutex.Lock()
|
|
defer t.mutex.Unlock()
|
|
|
|
participant := t.getParticipantUnsafe(entityID)
|
|
if participant == nil {
|
|
return &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Entity is not part of this trade",
|
|
}
|
|
}
|
|
|
|
// Check if slot has an item
|
|
if _, exists := participant.Items[slot]; !exists {
|
|
return &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Trade slot is empty",
|
|
}
|
|
}
|
|
|
|
// Remove item
|
|
delete(participant.Items, slot)
|
|
|
|
// Reset acceptance flags
|
|
t.trader1.HasAccepted = false
|
|
t.trader2.HasAccepted = false
|
|
|
|
// TODO: Send trade packet when packet system is available
|
|
// t.sendTradePacket()
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddCoinsToTrade adds coins to a participant's trade offer
|
|
// Converted from C++ Trade::AddCoinToTrade
|
|
func (t *Trade) AddCoinsToTrade(entityID int32, amount int64) error {
|
|
if t.state != TradeStateActive {
|
|
return &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Trade is not active",
|
|
}
|
|
}
|
|
|
|
if amount <= 0 {
|
|
return &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Invalid coin amount",
|
|
}
|
|
}
|
|
|
|
t.mutex.Lock()
|
|
defer t.mutex.Unlock()
|
|
|
|
participant := t.getParticipantUnsafe(entityID)
|
|
if participant == nil {
|
|
return &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Entity is not part of this trade",
|
|
}
|
|
}
|
|
|
|
newTotal := participant.Coins + amount
|
|
|
|
// TODO: Validate entity has sufficient coins when entity system is available
|
|
// For now, assume validation is done elsewhere
|
|
|
|
participant.Coins = newTotal
|
|
|
|
// Reset acceptance flags
|
|
t.trader1.HasAccepted = false
|
|
t.trader2.HasAccepted = false
|
|
|
|
// TODO: Send trade packet when packet system is available
|
|
// t.sendTradePacket()
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveCoinsFromTrade removes coins from a participant's trade offer
|
|
// Converted from C++ Trade::RemoveCoinFromTrade
|
|
func (t *Trade) RemoveCoinsFromTrade(entityID int32, amount int64) error {
|
|
if t.state != TradeStateActive {
|
|
return &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Trade is not active",
|
|
}
|
|
}
|
|
|
|
t.mutex.Lock()
|
|
defer t.mutex.Unlock()
|
|
|
|
participant := t.getParticipantUnsafe(entityID)
|
|
if participant == nil {
|
|
return &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Entity is not part of this trade",
|
|
}
|
|
}
|
|
|
|
if amount >= participant.Coins {
|
|
participant.Coins = 0
|
|
} else {
|
|
participant.Coins -= amount
|
|
}
|
|
|
|
// Reset acceptance flags
|
|
t.trader1.HasAccepted = false
|
|
t.trader2.HasAccepted = false
|
|
|
|
// TODO: Send trade packet when packet system is available
|
|
// t.sendTradePacket()
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetTradeAccepted marks a participant as having accepted the trade
|
|
// Converted from C++ Trade::SetTradeAccepted
|
|
func (t *Trade) SetTradeAccepted(entityID int32) (bool, error) {
|
|
if t.state != TradeStateActive {
|
|
return false, &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Trade is not active",
|
|
}
|
|
}
|
|
|
|
t.mutex.Lock()
|
|
defer t.mutex.Unlock()
|
|
|
|
participant := t.getParticipantUnsafe(entityID)
|
|
if participant == nil {
|
|
return false, &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Entity is not part of this trade",
|
|
}
|
|
}
|
|
|
|
participant.HasAccepted = true
|
|
|
|
// Check if both parties have accepted
|
|
if t.trader1.HasAccepted && t.trader2.HasAccepted {
|
|
// Complete the trade
|
|
t.completeTrade()
|
|
return true, nil
|
|
}
|
|
|
|
// TODO: Send acceptance packet to other trader when packet system is available
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// HasAcceptedTrade checks if a participant has accepted the trade
|
|
// Converted from C++ Trade::HasAcceptedTrade
|
|
func (t *Trade) HasAcceptedTrade(entityID int32) bool {
|
|
t.mutex.RLock()
|
|
defer t.mutex.RUnlock()
|
|
|
|
participant := t.getParticipantUnsafe(entityID)
|
|
if participant == nil {
|
|
return false
|
|
}
|
|
|
|
return participant.HasAccepted
|
|
}
|
|
|
|
// CancelTrade cancels the trade
|
|
// Converted from C++ Trade::CancelTrade
|
|
func (t *Trade) CancelTrade(entityID int32) error {
|
|
t.mutex.Lock()
|
|
defer t.mutex.Unlock()
|
|
|
|
if t.state != TradeStateActive {
|
|
return &TradeValidationError{
|
|
Code: TradeResultInvalidSlot,
|
|
Message: "Trade is not active",
|
|
}
|
|
}
|
|
|
|
t.state = TradeStateCanceled
|
|
|
|
// TODO: Send cancel packets to both traders when packet system is available
|
|
// TODO: Clear trade references on entities when entity system is available
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetTraderSlot returns the item in a specific slot for a trader
|
|
// Converted from C++ Trade::GetTraderSlot
|
|
func (t *Trade) GetTraderSlot(entityID int32, slot int8) Item {
|
|
t.mutex.RLock()
|
|
defer t.mutex.RUnlock()
|
|
|
|
participant := t.getParticipantUnsafe(entityID)
|
|
if participant == nil {
|
|
return nil
|
|
}
|
|
|
|
if itemInfo, exists := participant.Items[slot]; exists {
|
|
return itemInfo.Item
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetTradeInfo returns comprehensive information about the trade
|
|
func (t *Trade) GetTradeInfo() map[string]interface{} {
|
|
t.mutex.RLock()
|
|
defer t.mutex.RUnlock()
|
|
|
|
info := make(map[string]interface{})
|
|
info["state"] = t.state
|
|
info["start_time"] = t.startTime
|
|
info["trader1_id"] = t.trader1.EntityID
|
|
info["trader2_id"] = t.trader2.EntityID
|
|
info["trader1_accepted"] = t.trader1.HasAccepted
|
|
info["trader2_accepted"] = t.trader2.HasAccepted
|
|
info["trader1_items"] = len(t.trader1.Items)
|
|
info["trader2_items"] = len(t.trader2.Items)
|
|
info["trader1_coins"] = t.trader1.Coins
|
|
info["trader2_coins"] = t.trader2.Coins
|
|
|
|
return info
|
|
}
|
|
|
|
// Private helper methods
|
|
|
|
// getParticipantUnsafe returns participant without locking (must be called with lock held)
|
|
func (t *Trade) getParticipantUnsafe(entityID int32) *TradeParticipant {
|
|
if t.trader1.EntityID == entityID {
|
|
return t.trader1
|
|
} else if t.trader2.EntityID == entityID {
|
|
return t.trader2
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getTradeeUnsafe returns the other trader's ID without locking
|
|
func (t *Trade) getTradeeUnsafe(entityID int32) int32 {
|
|
if t.trader1.EntityID == entityID {
|
|
return t.trader2.EntityID
|
|
} else if t.trader2.EntityID == entityID {
|
|
return t.trader1.EntityID
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// validateItemTradability checks if an item can be traded between entities
|
|
// Converted from C++ Trade::CheckItem
|
|
func (t *Trade) validateItemTradability(item Item, traderID, otherID int32) error {
|
|
// Check no-trade flag
|
|
if item.IsNoTrade() {
|
|
// TODO: Only allow no-trade items with bots when bot system is available
|
|
return &TradeValidationError{
|
|
Code: TradeResultNoTrade,
|
|
Message: "Item cannot be traded",
|
|
}
|
|
}
|
|
|
|
// Check heirloom restrictions
|
|
if item.IsHeirloom() {
|
|
// TODO: Implement heirloom group checking when group system is available
|
|
// For now, allow heirloom trades with basic time/attunement checks
|
|
|
|
if item.IsAttuned() {
|
|
return &TradeValidationError{
|
|
Code: TradeResultHeirloom,
|
|
Message: "Attuned heirloom items cannot be traded",
|
|
}
|
|
}
|
|
|
|
// Check time-based restrictions (48 hours default)
|
|
creationTime := item.GetCreationTime()
|
|
if time.Since(creationTime) > 48*time.Hour {
|
|
return &TradeValidationError{
|
|
Code: TradeResultHeirloom,
|
|
Message: "Heirloom item sharing period has expired",
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// completeTrade finalizes the trade and transfers items/coins
|
|
// Converted from C++ Trade::CompleteTrade
|
|
func (t *Trade) completeTrade() {
|
|
t.state = TradeStateCompleted
|
|
|
|
// TODO: Implement actual item/coin transfer when entity and inventory systems are available
|
|
// This would involve:
|
|
// 1. Remove items/coins from each trader's inventory
|
|
// 2. Add the other trader's items/coins to their inventory
|
|
// 3. Send completion packets
|
|
// 4. Log the trade
|
|
// 5. Clear trade references on entities
|
|
|
|
// For now, just mark as completed
|
|
} |