2023-03-03 16:23:46 +01:00
import { inject , injectable } from "tsyringe" ;
2024-05-21 19:59:04 +02:00
import { InventoryHelper } from "@spt/helpers/InventoryHelper" ;
import { ItemHelper } from "@spt/helpers/ItemHelper" ;
import { TraderAssortHelper } from "@spt/helpers/TraderAssortHelper" ;
import { TraderHelper } from "@spt/helpers/TraderHelper" ;
import { IPmcData } from "@spt/models/eft/common/IPmcData" ;
import { Item } from "@spt/models/eft/common/tables/IItem" ;
import { IAddItemsDirectRequest } from "@spt/models/eft/inventory/IAddItemsDirectRequest" ;
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse" ;
import { IProcessBuyTradeRequestData } from "@spt/models/eft/trade/IProcessBuyTradeRequestData" ;
import { IProcessSellTradeRequestData } from "@spt/models/eft/trade/IProcessSellTradeRequestData" ;
import { BackendErrorCodes } from "@spt/models/enums/BackendErrorCodes" ;
import { ConfigTypes } from "@spt/models/enums/ConfigTypes" ;
import { Traders } from "@spt/models/enums/Traders" ;
import { IInventoryConfig } from "@spt/models/spt/config/IInventoryConfig" ;
import { ITraderConfig } from "@spt/models/spt/config/ITraderConfig" ;
import { ILogger } from "@spt/models/spt/utils/ILogger" ;
import { EventOutputHolder } from "@spt/routers/EventOutputHolder" ;
import { ConfigServer } from "@spt/servers/ConfigServer" ;
import { RagfairServer } from "@spt/servers/RagfairServer" ;
import { FenceService } from "@spt/services/FenceService" ;
import { LocalisationService } from "@spt/services/LocalisationService" ;
import { PaymentService } from "@spt/services/PaymentService" ;
import { TraderPurchasePersisterService } from "@spt/services/TraderPurchasePersisterService" ;
import { ICloner } from "@spt/utils/cloners/ICloner" ;
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil" ;
2023-03-03 16:23:46 +01:00
@injectable ( )
export class TradeHelper
{
protected traderConfig : ITraderConfig ;
2024-01-14 22:12:56 +01:00
protected inventoryConfig : IInventoryConfig ;
2023-03-03 16:23:46 +01:00
constructor (
2024-05-28 16:04:20 +02:00
@inject ( "PrimaryLogger" ) protected logger : ILogger ,
2023-03-03 16:23:46 +01:00
@inject ( "EventOutputHolder" ) protected eventOutputHolder : EventOutputHolder ,
@inject ( "TraderHelper" ) protected traderHelper : TraderHelper ,
@inject ( "ItemHelper" ) protected itemHelper : ItemHelper ,
@inject ( "PaymentService" ) protected paymentService : PaymentService ,
@inject ( "FenceService" ) protected fenceService : FenceService ,
2024-01-14 23:30:05 +01:00
@inject ( "LocalisationService" ) protected localisationService : LocalisationService ,
2023-05-24 16:51:05 +02:00
@inject ( "HttpResponseUtil" ) protected httpResponse : HttpResponseUtil ,
2023-03-03 16:23:46 +01:00
@inject ( "InventoryHelper" ) protected inventoryHelper : InventoryHelper ,
@inject ( "RagfairServer" ) protected ragfairServer : RagfairServer ,
2024-01-15 15:25:17 +01:00
@inject ( "TraderAssortHelper" ) protected traderAssortHelper : TraderAssortHelper ,
2024-05-17 21:32:41 +02:00
@inject ( "TraderPurchasePersisterService" )
protected traderPurchasePersisterService : TraderPurchasePersisterService ,
2023-11-16 22:42:06 +01:00
@inject ( "ConfigServer" ) protected configServer : ConfigServer ,
2024-05-28 16:04:20 +02:00
@inject ( "PrimaryCloner" ) protected cloner : ICloner ,
2023-03-03 16:23:46 +01:00
)
{
this . traderConfig = this . configServer . getConfig ( ConfigTypes . TRADER ) ;
2024-01-14 22:12:56 +01:00
this . inventoryConfig = this . configServer . getConfig ( ConfigTypes . INVENTORY ) ;
2023-03-03 16:23:46 +01:00
}
/ * *
* Buy item from flea or trader
* @param pmcData Player profile
* @param buyRequestData data from client
* @param sessionID Session id
* @param foundInRaid Should item be found in raid
2024-01-16 12:47:40 +01:00
* @param output IItemEventRouterResponse
* @returns IItemEventRouterResponse
2023-03-03 16:23:46 +01:00
* /
2023-11-16 22:42:06 +01:00
public buyItem (
pmcData : IPmcData ,
buyRequestData : IProcessBuyTradeRequestData ,
sessionID : string ,
foundInRaid : boolean ,
2024-01-16 12:47:40 +01:00
output : IItemEventRouterResponse ,
2024-01-16 13:21:42 +01:00
) : void
2023-03-03 16:23:46 +01:00
{
2024-01-14 23:30:05 +01:00
let offerItems : Item [ ] = [ ] ;
2024-04-23 05:43:35 +02:00
let buyCallback : ( buyCount : number ) = > void ;
2024-01-14 11:09:43 +01:00
if ( buyRequestData . tid . toLocaleLowerCase ( ) === "ragfair" )
{
2024-01-15 15:25:17 +01:00
buyCallback = ( buyCount : number ) = >
2024-01-14 23:30:05 +01:00
{
const allOffers = this . ragfairServer . getOffers ( ) ;
// We store ragfair offerid in buyRequestData.item_id
2024-05-17 21:32:41 +02:00
const offerWithItem = allOffers . find ( ( x ) = > x . _id === buyRequestData . item_id ) ;
2024-01-14 23:30:05 +01:00
const itemPurchased = offerWithItem . items [ 0 ] ;
2024-02-02 19:54:07 +01:00
2024-01-14 23:30:05 +01:00
// Ensure purchase does not exceed trader item limit
2024-01-15 15:25:17 +01:00
const assortHasBuyRestrictions = this . itemHelper . hasBuyRestrictions ( itemPurchased ) ;
if ( assortHasBuyRestrictions )
2024-01-14 23:30:05 +01:00
{
2024-02-02 19:54:07 +01:00
this . checkPurchaseIsWithinTraderItemLimit (
sessionID ,
buyRequestData . tid ,
itemPurchased ,
buyRequestData . item_id ,
buyCount ,
) ;
2024-03-30 14:15:28 +01:00
// Decrement trader item count
2024-03-30 13:56:19 +01:00
const itemPurchaseDetails = {
2024-02-02 19:54:07 +01:00
items : [ { itemId : buyRequestData.item_id , count : buyCount } ] ,
traderId : buyRequestData.tid ,
2024-01-15 15:25:17 +01:00
} ;
2024-03-30 13:56:19 +01:00
this . traderHelper . addTraderPurchasesToPlayerProfile ( sessionID , itemPurchaseDetails , itemPurchased ) ;
2024-01-14 23:30:05 +01:00
}
} ;
2024-01-14 22:12:56 +01:00
// Get raw offer from ragfair, clone to prevent altering offer itself
2024-01-14 11:09:43 +01:00
const allOffers = this . ragfairServer . getOffers ( ) ;
2024-05-17 21:32:41 +02:00
const offerWithItemCloned = this . cloner . clone ( allOffers . find ( ( x ) = > x . _id === buyRequestData . item_id ) ) ;
2024-01-14 23:30:05 +01:00
offerItems = offerWithItemCloned . items ;
}
else if ( buyRequestData . tid === Traders . FENCE )
{
2024-01-15 15:25:17 +01:00
buyCallback = ( buyCount : number ) = >
2024-01-14 23:30:05 +01:00
{
// Update assort/flea item values
const traderAssorts = this . traderHelper . getTraderAssortsByTraderId ( buyRequestData . tid ) . items ;
2024-05-17 21:32:41 +02:00
const itemPurchased = traderAssorts . find ( ( assort ) = > assort . _id === buyRequestData . item_id ) ;
2024-02-02 19:54:07 +01:00
2024-01-14 23:30:05 +01:00
// Decrement trader item count
2024-01-16 13:08:30 +01:00
itemPurchased . upd . StackObjectsCount -= buyCount ;
2024-02-02 19:54:07 +01:00
2024-02-02 16:56:37 +01:00
this . fenceService . amendOrRemoveFenceOffer ( buyRequestData . item_id , buyCount ) ;
2024-01-14 23:30:05 +01:00
} ;
2024-01-14 22:12:56 +01:00
2024-01-14 23:30:05 +01:00
const fenceItems = this . fenceService . getRawFenceAssorts ( ) . items ;
2024-05-17 21:32:41 +02:00
const rootItemIndex = fenceItems . findIndex ( ( item ) = > item . _id === buyRequestData . item_id ) ;
2024-01-14 23:30:05 +01:00
if ( rootItemIndex === - 1 )
2024-01-14 22:12:56 +01:00
{
2024-01-14 23:30:05 +01:00
this . logger . debug ( ` Tried to buy item ${ buyRequestData . item_id } from fence that no longer exists ` ) ;
const message = this . localisationService . getText ( "ragfair-offer_no_longer_exists" ) ;
2024-01-16 13:21:42 +01:00
this . httpResponse . appendErrorToOutput ( output , message ) ;
return ;
2024-01-14 23:30:05 +01:00
}
2024-01-14 22:12:56 +01:00
2024-01-14 23:30:05 +01:00
offerItems = this . itemHelper . findAndReturnChildrenAsItems ( fenceItems , buyRequestData . item_id ) ;
}
2024-01-15 15:25:17 +01:00
else
{
// Non-fence trader
buyCallback = ( buyCount : number ) = >
{
// Update assort/flea item values
const traderAssorts = this . traderHelper . getTraderAssortsByTraderId ( buyRequestData . tid ) . items ;
2024-05-17 21:32:41 +02:00
const itemPurchased = traderAssorts . find ( ( x ) = > x . _id === buyRequestData . item_id ) ;
2024-01-15 15:25:17 +01:00
// Ensure purchase does not exceed trader item limit
const assortHasBuyRestrictions = this . itemHelper . hasBuyRestrictions ( itemPurchased ) ;
if ( assortHasBuyRestrictions )
{
2024-02-02 19:54:07 +01:00
this . checkPurchaseIsWithinTraderItemLimit (
sessionID ,
buyRequestData . tid ,
itemPurchased ,
buyRequestData . item_id ,
buyCount ,
) ;
2024-01-15 15:25:17 +01:00
}
2024-04-06 23:22:41 +02:00
// Check if trader has enough stock
if ( itemPurchased . upd . StackObjectsCount < buyCount )
{
throw new Error (
` Unable to purchase ${ buyCount } items, this would exceed the remaining stock left ${ itemPurchased . upd . StackObjectsCount } from the traders assort: ${ buyRequestData . tid } this refresh ` ,
) ;
}
2024-01-15 15:25:17 +01:00
// Decrement trader item count
itemPurchased . upd . StackObjectsCount -= buyCount ;
2024-03-30 13:59:56 +01:00
if ( assortHasBuyRestrictions )
2024-01-15 15:25:17 +01:00
{
const itemPurchaseDat = {
2024-02-02 19:54:07 +01:00
items : [ { itemId : buyRequestData.item_id , count : buyCount } ] ,
traderId : buyRequestData.tid ,
2024-01-15 15:25:17 +01:00
} ;
2024-03-30 13:55:18 +01:00
this . traderHelper . addTraderPurchasesToPlayerProfile ( sessionID , itemPurchaseDat , itemPurchased ) ;
2024-01-15 15:25:17 +01:00
}
} ;
// Get all trader assort items
const traderItems = this . traderAssortHelper . getAssort ( sessionID , buyRequestData . tid ) . items ;
2024-02-02 19:54:07 +01:00
2024-01-15 15:25:17 +01:00
// Get item + children for purchase
const relevantItems = this . itemHelper . findAndReturnChildrenAsItems ( traderItems , buyRequestData . item_id ) ;
offerItems . push ( . . . relevantItems ) ;
}
2024-01-14 22:12:56 +01:00
2024-01-14 23:30:05 +01:00
// Get item details from db
const itemDbDetails = this . itemHelper . getItem ( offerItems [ 0 ] . _tpl ) [ 1 ] ;
const itemMaxStackSize = itemDbDetails . _props . StackMaxSize ;
const itemsToSendTotalCount = buyRequestData . count ;
let itemsToSendRemaining = itemsToSendTotalCount ;
2024-01-16 19:25:03 +01:00
2024-02-05 15:07:21 +01:00
// Construct array of items to send to player
const itemsToSendToPlayer : Item [ ] [ ] = [ ] ;
2024-01-14 23:30:05 +01:00
while ( itemsToSendRemaining > 0 )
{
2024-05-13 19:58:17 +02:00
const offerClone = this . cloner . clone ( offerItems ) ;
2024-02-05 15:07:21 +01:00
// Handle stackable items that have a max stack size limit
2024-01-14 23:30:05 +01:00
const itemCountToSend = Math . min ( itemMaxStackSize , itemsToSendRemaining ) ;
2024-02-05 15:07:21 +01:00
offerClone [ 0 ] . upd . StackObjectsCount = itemCountToSend ;
2024-01-14 22:12:56 +01:00
2024-01-14 23:30:05 +01:00
// Prevent any collisions
2024-02-05 15:07:21 +01:00
this . itemHelper . remapRootItemId ( offerClone ) ;
if ( offerClone . length > 1 )
2024-01-15 15:25:17 +01:00
{
2024-02-05 15:07:21 +01:00
this . itemHelper . reparentItemAndChildren ( offerClone [ 0 ] , offerClone ) ;
2024-01-15 15:25:17 +01:00
}
2024-01-16 19:25:03 +01:00
2024-02-05 15:07:21 +01:00
itemsToSendToPlayer . push ( offerClone ) ;
2024-01-14 23:30:05 +01:00
// Remove amount of items added to player stash
itemsToSendRemaining -= itemCountToSend ;
2024-01-16 19:25:03 +01:00
}
2024-02-05 15:07:21 +01:00
// Construct request
const request : IAddItemsDirectRequest = {
itemsWithModsToAdd : itemsToSendToPlayer ,
foundInRaid : foundInRaid ,
callback : buyCallback ,
useSortingTable : false ,
} ;
// Add items + their children to stash
this . inventoryHelper . addItemsToStash ( sessionID , request , pmcData , output ) ;
if ( output . warnings . length > 0 )
{
return ;
}
2024-01-16 19:25:03 +01:00
/// Pay for purchase
this . paymentService . payMoney ( pmcData , buyRequestData , sessionID , output ) ;
if ( output . warnings . length > 0 )
{
const errorMessage = ` Transaction failed: ${ output . warnings [ 0 ] . errmsg } ` ;
this . httpResponse . appendErrorToOutput ( output , errorMessage , BackendErrorCodes . UNKNOWN_TRADING_ERROR ) ;
}
2023-03-03 16:23:46 +01:00
}
/ * *
* Sell item to trader
2023-10-10 13:03:20 +02:00
* @param profileWithItemsToSell Profile to remove items from
* @param profileToReceiveMoney Profile to accept the money for selling item
2023-05-24 16:51:05 +02:00
* @param sellRequest Request data
2023-03-03 16:23:46 +01:00
* @param sessionID Session id
2024-01-16 13:21:42 +01:00
* @param output IItemEventRouterResponse
2023-03-03 16:23:46 +01:00
* /
2023-11-16 22:42:06 +01:00
public sellItem (
profileWithItemsToSell : IPmcData ,
profileToReceiveMoney : IPmcData ,
sellRequest : IProcessSellTradeRequestData ,
sessionID : string ,
2024-01-16 13:21:42 +01:00
output : IItemEventRouterResponse ,
) : void
2023-03-03 16:23:46 +01:00
{
2023-05-24 16:51:05 +02:00
// Find item in inventory and remove it
for ( const itemToBeRemoved of sellRequest . items )
2023-03-03 16:23:46 +01:00
{
2023-05-24 16:51:05 +02:00
const itemIdToFind = itemToBeRemoved . id . replace ( /\s+/g , "" ) ; // Strip out whitespace
2023-03-03 16:23:46 +01:00
2023-05-24 16:51:05 +02:00
// Find item in player inventory, or show error to player if not found
2024-05-17 21:32:41 +02:00
const matchingItemInInventory = profileWithItemsToSell . Inventory . items . find ( ( x ) = > x . _id === itemIdToFind ) ;
2023-05-24 16:51:05 +02:00
if ( ! matchingItemInInventory )
{
const errorMessage = ` Unable to sell item ${ itemToBeRemoved . id } , cannot be found in player inventory ` ;
this . logger . error ( errorMessage ) ;
2023-03-03 16:23:46 +01:00
2024-01-16 13:21:42 +01:00
this . httpResponse . appendErrorToOutput ( output , errorMessage ) ;
return ;
2023-03-03 16:23:46 +01:00
}
2023-05-24 16:51:05 +02:00
this . logger . debug ( ` Selling: id: ${ matchingItemInInventory . _id } tpl: ${ matchingItemInInventory . _tpl } ` ) ;
2023-10-10 13:03:20 +02:00
2024-05-01 22:17:09 +02:00
if ( sellRequest . tid === Traders . FENCE )
{
this . fenceService . addItemsToFenceAssort (
profileWithItemsToSell . Inventory . items ,
matchingItemInInventory ,
) ;
}
2023-10-10 13:03:20 +02:00
// Also removes children
2024-01-16 13:21:42 +01:00
this . inventoryHelper . removeItem ( profileWithItemsToSell , itemToBeRemoved . id , sessionID , output ) ;
2023-03-03 16:23:46 +01:00
}
2023-05-24 16:51:05 +02:00
// Give player money for sold item(s)
2024-01-16 13:21:42 +01:00
this . paymentService . giveProfileMoney ( profileToReceiveMoney , sellRequest . price , sellRequest , output , sessionID ) ;
2023-03-03 16:23:46 +01:00
}
2023-12-14 16:47:01 +01:00
/ * *
* Traders allow a limited number of purchases per refresh cycle ( default 60 mins )
2024-01-20 01:19:13 +01:00
* @param sessionId Session id
* @param traderId Trader assort is purchased from
2023-12-14 16:47:01 +01:00
* @param assortBeingPurchased the item from trader being bought
* @param assortId Id of assort being purchased
2024-01-20 01:19:13 +01:00
* @param count How many of the item are being bought
2023-12-14 16:47:01 +01:00
* /
2024-02-02 19:54:07 +01:00
protected checkPurchaseIsWithinTraderItemLimit (
sessionId : string ,
traderId : string ,
assortBeingPurchased : Item ,
assortId : string ,
count : number ,
) : void
2023-03-03 16:23:46 +01:00
{
2024-02-02 19:54:07 +01:00
const traderPurchaseData = this . traderPurchasePersisterService . getProfileTraderPurchase (
sessionId ,
traderId ,
assortBeingPurchased . _id ,
) ;
2024-01-20 01:19:13 +01:00
if ( ( traderPurchaseData ? . count ? ? 0 + count ) > assortBeingPurchased . upd ? . BuyRestrictionMax )
2023-03-03 16:23:46 +01:00
{
2023-11-16 22:42:06 +01:00
throw new Error (
2023-12-14 16:47:01 +01:00
` Unable to purchase ${ count } items, this would exceed your purchase limit of ${ assortBeingPurchased . upd . BuyRestrictionMax } from the traders assort: ${ assortId } this refresh ` ,
2023-11-16 22:42:06 +01:00
) ;
2023-03-03 16:23:46 +01:00
}
}
2023-11-16 22:42:06 +01:00
}