2023-03-03 16:23:46 +01:00
import { inject , injectable } from "tsyringe" ;
2023-10-19 19:21:17 +02:00
import { InventoryHelper } from "@spt-aki/helpers/InventoryHelper" ;
import { ItemHelper } from "@spt-aki/helpers/ItemHelper" ;
2024-01-15 15:25:17 +01:00
import { TraderAssortHelper } from "@spt-aki/helpers/TraderAssortHelper" ;
2023-10-19 19:21:17 +02:00
import { TraderHelper } from "@spt-aki/helpers/TraderHelper" ;
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData" ;
import { Item , Upd } from "@spt-aki/models/eft/common/tables/IItem" ;
2024-01-14 11:09:43 +01:00
import { IAddItemDirectRequest } from "@spt-aki/models/eft/inventory/IAddItemDirectRequest" ;
2023-10-19 19:21:17 +02:00
import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse" ;
import { IProcessBuyTradeRequestData } from "@spt-aki/models/eft/trade/IProcessBuyTradeRequestData" ;
import { IProcessSellTradeRequestData } from "@spt-aki/models/eft/trade/IProcessSellTradeRequestData" ;
2024-01-16 13:21:42 +01:00
import { BackendErrorCodes } from "@spt-aki/models/enums/BackendErrorCodes" ;
2023-10-19 19:21:17 +02:00
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes" ;
import { Traders } from "@spt-aki/models/enums/Traders" ;
2024-01-14 22:12:56 +01:00
import { IInventoryConfig } from "@spt-aki/models/spt/config/IInventoryConfig" ;
2023-10-19 19:21:17 +02:00
import { ITraderConfig } from "@spt-aki/models/spt/config/ITraderConfig" ;
import { ILogger } from "@spt-aki/models/spt/utils/ILogger" ;
import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder" ;
import { ConfigServer } from "@spt-aki/servers/ConfigServer" ;
import { RagfairServer } from "@spt-aki/servers/RagfairServer" ;
import { FenceService } from "@spt-aki/services/FenceService" ;
2024-01-14 23:30:05 +01:00
import { LocalisationService } from "@spt-aki/services/LocalisationService" ;
2023-10-19 19:21:17 +02:00
import { PaymentService } from "@spt-aki/services/PaymentService" ;
2024-01-20 01:19:13 +01:00
import { TraderPurchasePersisterService } from "@spt-aki/services/TraderPurchasePersisterService" ;
2023-10-19 19:21:17 +02:00
import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil" ;
2024-01-14 11:09:43 +01:00
import { JsonUtil } from "@spt-aki/utils/JsonUtil" ;
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 (
@inject ( "WinstonLogger" ) protected logger : ILogger ,
2024-01-14 11:09:43 +01:00
@inject ( "JsonUtil" ) protected jsonUtil : JsonUtil ,
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-01-20 01:19:13 +01:00
@inject ( "TraderPurchasePersisterService" ) protected traderPurchasePersisterService : TraderPurchasePersisterService ,
2023-11-16 22:42:06 +01:00
@inject ( "ConfigServer" ) protected configServer : ConfigServer ,
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-01-15 15:25:17 +01:00
let buyCallback : { ( buyCount : number ) } ;
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
const offerWithItem = allOffers . find ( ( x ) = > x . _id === buyRequestData . item_id ) ;
const itemPurchased = offerWithItem . items [ 0 ] ;
// 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-01-20 01:19:13 +01:00
this . checkPurchaseIsWithinTraderItemLimit ( sessionID , buyRequestData . tid , itemPurchased , buyRequestData . item_id , buyCount ) ;
2024-01-14 23:30:05 +01:00
}
// Decrement trader item count
2024-01-15 15:25:17 +01:00
if ( this . traderConfig . persistPurchaseDataInProfile && assortHasBuyRestrictions )
2024-01-14 23:30:05 +01:00
{
2024-01-15 15:25:17 +01:00
const itemPurchaseDat = {
items : [ {
itemId : buyRequestData.item_id ,
count : buyCount
} ] ,
traderId : buyRequestData.tid
} ;
this . traderHelper . addTraderPurchasesToPlayerProfile ( sessionID , itemPurchaseDat ) ;
2024-01-14 23:30:05 +01:00
}
2024-01-15 15:25:17 +01:00
if ( assortHasBuyRestrictions )
2024-01-14 23:30:05 +01:00
{
// Increment non-fence trader item buy count
2024-01-15 15:25:17 +01:00
this . incrementAssortBuyCount ( itemPurchased , buyCount ) ;
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-01-14 22:12:56 +01:00
const offerWithItemCloned = this . jsonUtil . 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 ;
const itemPurchased = traderAssorts . find ( ( x ) = > x . _id === buyRequestData . item_id ) ;
// Decrement trader item count
2024-01-16 13:08:30 +01:00
itemPurchased . upd . StackObjectsCount -= buyCount ;
2024-01-14 23:30:05 +01:00
this . fenceService . removeFenceOffer ( buyRequestData . item_id ) ;
} ;
2024-01-14 22:12:56 +01:00
2024-01-14 23:30:05 +01:00
const fenceItems = this . fenceService . getRawFenceAssorts ( ) . items ;
const rootItemIndex = fenceItems . findIndex ( ( item ) = > item . _id === buyRequestData . item_id ) ;
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 ;
const itemPurchased = traderAssorts . find ( ( x ) = > x . _id === buyRequestData . item_id ) ;
// Ensure purchase does not exceed trader item limit
const assortHasBuyRestrictions = this . itemHelper . hasBuyRestrictions ( itemPurchased ) ;
if ( assortHasBuyRestrictions )
{
2024-01-20 01:19:13 +01:00
this . checkPurchaseIsWithinTraderItemLimit ( sessionID , buyRequestData . tid , itemPurchased , buyRequestData . item_id , buyCount ) ;
2024-01-15 15:25:17 +01:00
}
// Decrement trader item count
itemPurchased . upd . StackObjectsCount -= buyCount ;
if ( this . traderConfig . persistPurchaseDataInProfile && assortHasBuyRestrictions )
{
const itemPurchaseDat = {
items : [ {
itemId : buyRequestData.item_id ,
count : buyCount
} ] ,
traderId : buyRequestData.tid
} ;
this . traderHelper . addTraderPurchasesToPlayerProfile ( sessionID , itemPurchaseDat ) ;
}
if ( assortHasBuyRestrictions )
{
// Increment non-fence trader item buy count
this . incrementAssortBuyCount ( itemPurchased , buyCount ) ;
}
} ;
// Get all trader assort items
const traderItems = this . traderAssortHelper . getAssort ( sessionID , buyRequestData . tid ) . items ;
// 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
// Loop until all items have been sent to player stash
2024-01-14 23:30:05 +01:00
while ( itemsToSendRemaining > 0 )
{
// Handle edge case when remaining items to send < max stack size
const itemCountToSend = Math . min ( itemMaxStackSize , itemsToSendRemaining ) ;
offerItems [ 0 ] . upd . StackObjectsCount = itemCountToSend ;
2024-01-14 22:12:56 +01:00
2024-01-14 23:30:05 +01:00
// Prevent any collisions
this . itemHelper . remapRootItemId ( offerItems ) ;
2024-01-14 22:12:56 +01:00
2024-01-14 23:30:05 +01:00
// Construct request
const request : IAddItemDirectRequest = {
itemWithModsToAdd : this.itemHelper.reparentItemAndChildren ( offerItems [ 0 ] , offerItems ) ,
2024-01-16 12:47:40 +01:00
foundInRaid : foundInRaid ,
2024-01-14 23:30:05 +01:00
callback : buyCallback ,
2024-01-19 10:08:11 +01:00
useSortingTable : false
2024-01-14 23:30:05 +01:00
} ;
2024-01-14 11:09:43 +01:00
2024-01-14 23:30:05 +01:00
// Add item + children to stash
this . inventoryHelper . addItemToStash ( sessionID , request , pmcData , output ) ;
2024-01-15 15:25:17 +01:00
if ( output . warnings . length > 0 )
{
2024-01-16 13:21:42 +01:00
return ;
2024-01-15 15:25:17 +01:00
}
2024-01-16 19:25:03 +01:00
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
}
/// 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
2023-11-16 22:42:06 +01: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
// 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
}
/ * *
* Increment the assorts buy count by number of items purchased
* Show error on screen if player attempts to buy more than what the buy max allows
* @param assortBeingPurchased assort being bought
* @param itemsPurchasedCount number of items being bought
* /
protected incrementAssortBuyCount ( assortBeingPurchased : Item , itemsPurchasedCount : number ) : void
{
assortBeingPurchased . upd . BuyRestrictionCurrent += itemsPurchasedCount ;
if ( assortBeingPurchased . upd . BuyRestrictionCurrent > assortBeingPurchased . upd . BuyRestrictionMax )
{
2023-10-10 13:03:20 +02:00
throw new Error ( "Unable to purchase item, Purchase limit reached" ) ;
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-01-20 01:19:13 +01:00
protected checkPurchaseIsWithinTraderItemLimit ( sessionId : string , traderId : string , assortBeingPurchased : Item , assortId : string , count : number ) : void
2023-03-03 16:23:46 +01:00
{
2024-01-20 01:19:13 +01:00
const traderPurchaseData = this . traderPurchasePersisterService . getProfileTraderPurchase ( sessionId , traderId , assortBeingPurchased . _id ) ;
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
}