2024-05-21 17:59:04 +00: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" ;
2024-09-24 12:47:29 +01:00
import { IItem } from "@spt/models/eft/common/tables/IItem" ;
2024-05-21 17:59:04 +00:00
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" ;
2024-09-22 11:33:49 +01:00
import { QuestStatus } from "@spt/models/enums/QuestStatus" ;
2024-05-21 17:59:04 +00:00
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" ;
2024-09-22 11:33:49 +01:00
import { DatabaseService } from "@spt/services/DatabaseService" ;
2024-05-21 17:59:04 +00:00
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 { HttpResponseUtil } from "@spt/utils/HttpResponseUtil" ;
2024-07-23 11:12:53 -04:00
import { ICloner } from "@spt/utils/cloners/ICloner" ;
import { inject , injectable } from "tsyringe" ;
2023-03-03 15:23:46 +00:00
@injectable ( )
2024-07-23 11:12:53 -04:00
export class TradeHelper {
2023-03-03 15:23:46 +00:00
protected traderConfig : ITraderConfig ;
2024-01-14 21:12:56 +00:00
protected inventoryConfig : IInventoryConfig ;
2023-03-03 15:23:46 +00:00
constructor (
2024-05-28 14:04:20 +00:00
@inject ( "PrimaryLogger" ) protected logger : ILogger ,
2024-09-22 11:33:49 +01:00
@inject ( "DatabaseService" ) protected databaseService : DatabaseService ,
2023-03-03 15:23:46 +00: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 22:30:05 +00:00
@inject ( "LocalisationService" ) protected localisationService : LocalisationService ,
2023-05-24 15:51:05 +01:00
@inject ( "HttpResponseUtil" ) protected httpResponse : HttpResponseUtil ,
2023-03-03 15:23:46 +00:00
@inject ( "InventoryHelper" ) protected inventoryHelper : InventoryHelper ,
@inject ( "RagfairServer" ) protected ragfairServer : RagfairServer ,
2024-01-15 14:25:17 +00:00
@inject ( "TraderAssortHelper" ) protected traderAssortHelper : TraderAssortHelper ,
2024-05-17 15:32:41 -04:00
@inject ( "TraderPurchasePersisterService" )
protected traderPurchasePersisterService : TraderPurchasePersisterService ,
2023-11-16 21:42:06 +00:00
@inject ( "ConfigServer" ) protected configServer : ConfigServer ,
2024-05-28 14:04:20 +00:00
@inject ( "PrimaryCloner" ) protected cloner : ICloner ,
2024-07-23 11:12:53 -04:00
) {
2023-03-03 15:23:46 +00:00
this . traderConfig = this . configServer . getConfig ( ConfigTypes . TRADER ) ;
2024-01-14 21:12:56 +00:00
this . inventoryConfig = this . configServer . getConfig ( ConfigTypes . INVENTORY ) ;
2023-03-03 15:23:46 +00: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 11:47:40 +00:00
* @param output IItemEventRouterResponse
* @returns IItemEventRouterResponse
2023-03-03 15:23:46 +00:00
* /
2023-11-16 21:42:06 +00:00
public buyItem (
pmcData : IPmcData ,
buyRequestData : IProcessBuyTradeRequestData ,
sessionID : string ,
foundInRaid : boolean ,
2024-01-16 11:47:40 +00:00
output : IItemEventRouterResponse ,
2024-07-23 11:12:53 -04:00
) : void {
2024-09-24 12:47:29 +01:00
let offerItems : IItem [ ] = [ ] ;
2024-04-22 23:43:35 -04:00
let buyCallback : ( buyCount : number ) = > void ;
2024-07-23 11:12:53 -04:00
if ( buyRequestData . tid . toLocaleLowerCase ( ) === "ragfair" ) {
buyCallback = ( buyCount : number ) = > {
2024-01-14 22:30:05 +00:00
const allOffers = this . ragfairServer . getOffers ( ) ;
// We store ragfair offerid in buyRequestData.item_id
2024-05-17 15:32:41 -04:00
const offerWithItem = allOffers . find ( ( x ) = > x . _id === buyRequestData . item_id ) ;
2024-01-14 22:30:05 +00:00
const itemPurchased = offerWithItem . items [ 0 ] ;
2024-02-02 13:54:07 -05:00
2024-01-14 22:30:05 +00:00
// Ensure purchase does not exceed trader item limit
2024-01-15 14:25:17 +00:00
const assortHasBuyRestrictions = this . itemHelper . hasBuyRestrictions ( itemPurchased ) ;
2024-07-23 11:12:53 -04:00
if ( assortHasBuyRestrictions ) {
2024-02-02 13:54:07 -05:00
this . checkPurchaseIsWithinTraderItemLimit (
sessionID ,
2024-06-08 19:38:16 +01:00
pmcData ,
2024-02-02 13:54:07 -05:00
buyRequestData . tid ,
itemPurchased ,
buyRequestData . item_id ,
buyCount ,
) ;
2024-03-30 13:15:28 +00:00
// Decrement trader item count
2024-03-30 12:56:19 +00:00
const itemPurchaseDetails = {
2024-02-02 13:54:07 -05:00
items : [ { itemId : buyRequestData.item_id , count : buyCount } ] ,
traderId : buyRequestData.tid ,
2024-01-15 14:25:17 +00:00
} ;
2024-03-30 12:56:19 +00:00
this . traderHelper . addTraderPurchasesToPlayerProfile ( sessionID , itemPurchaseDetails , itemPurchased ) ;
2024-01-14 22:30:05 +00:00
}
} ;
2024-01-14 21:12:56 +00:00
// Get raw offer from ragfair, clone to prevent altering offer itself
2024-01-14 10:09:43 +00:00
const allOffers = this . ragfairServer . getOffers ( ) ;
2024-05-17 15:32:41 -04:00
const offerWithItemCloned = this . cloner . clone ( allOffers . find ( ( x ) = > x . _id === buyRequestData . item_id ) ) ;
2024-01-14 22:30:05 +00:00
offerItems = offerWithItemCloned . items ;
2024-07-23 11:12:53 -04:00
} else if ( buyRequestData . tid === Traders . FENCE ) {
buyCallback = ( buyCount : number ) = > {
2024-01-14 22:30:05 +00:00
// Update assort/flea item values
const traderAssorts = this . traderHelper . getTraderAssortsByTraderId ( buyRequestData . tid ) . items ;
2024-05-17 15:32:41 -04:00
const itemPurchased = traderAssorts . find ( ( assort ) = > assort . _id === buyRequestData . item_id ) ;
2024-02-02 13:54:07 -05:00
2024-01-14 22:30:05 +00:00
// Decrement trader item count
2024-01-16 12:08:30 +00:00
itemPurchased . upd . StackObjectsCount -= buyCount ;
2024-02-02 13:54:07 -05:00
2024-02-02 15:56:37 +00:00
this . fenceService . amendOrRemoveFenceOffer ( buyRequestData . item_id , buyCount ) ;
2024-01-14 22:30:05 +00:00
} ;
2024-01-14 21:12:56 +00:00
2024-01-14 22:30:05 +00:00
const fenceItems = this . fenceService . getRawFenceAssorts ( ) . items ;
2024-05-17 15:32:41 -04:00
const rootItemIndex = fenceItems . findIndex ( ( item ) = > item . _id === buyRequestData . item_id ) ;
2024-07-23 11:12:53 -04:00
if ( rootItemIndex === - 1 ) {
2024-01-14 22:30:05 +00: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 12:21:42 +00:00
this . httpResponse . appendErrorToOutput ( output , message ) ;
return ;
2024-01-14 22:30:05 +00:00
}
2024-01-14 21:12:56 +00:00
2024-01-14 22:30:05 +00:00
offerItems = this . itemHelper . findAndReturnChildrenAsItems ( fenceItems , buyRequestData . item_id ) ;
2024-07-23 11:12:53 -04:00
} else {
2024-01-15 14:25:17 +00:00
// Non-fence trader
2024-07-23 11:12:53 -04:00
buyCallback = ( buyCount : number ) = > {
2024-01-15 14:25:17 +00:00
// Update assort/flea item values
const traderAssorts = this . traderHelper . getTraderAssortsByTraderId ( buyRequestData . tid ) . items ;
2024-06-08 12:56:24 +01:00
const itemPurchased = traderAssorts . find ( ( item ) = > item . _id === buyRequestData . item_id ) ;
2024-01-15 14:25:17 +00:00
// Ensure purchase does not exceed trader item limit
const assortHasBuyRestrictions = this . itemHelper . hasBuyRestrictions ( itemPurchased ) ;
2024-07-23 11:12:53 -04:00
if ( assortHasBuyRestrictions ) {
2024-06-08 12:56:24 +01:00
// Will throw error if check fails
2024-02-02 13:54:07 -05:00
this . checkPurchaseIsWithinTraderItemLimit (
sessionID ,
2024-06-08 19:38:16 +01:00
pmcData ,
2024-02-02 13:54:07 -05:00
buyRequestData . tid ,
itemPurchased ,
buyRequestData . item_id ,
buyCount ,
) ;
2024-01-15 14:25:17 +00:00
}
2024-04-06 21:22:41 +00:00
// Check if trader has enough stock
2024-07-23 11:12:53 -04:00
if ( itemPurchased . upd . StackObjectsCount < buyCount ) {
2024-04-06 21:22:41 +00:00
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 14:25:17 +00:00
// Decrement trader item count
itemPurchased . upd . StackObjectsCount -= buyCount ;
2024-07-23 11:12:53 -04:00
if ( assortHasBuyRestrictions ) {
2024-01-15 14:25:17 +00:00
const itemPurchaseDat = {
2024-02-02 13:54:07 -05:00
items : [ { itemId : buyRequestData.item_id , count : buyCount } ] ,
traderId : buyRequestData.tid ,
2024-01-15 14:25:17 +00:00
} ;
2024-03-30 12:55:18 +00:00
this . traderHelper . addTraderPurchasesToPlayerProfile ( sessionID , itemPurchaseDat , itemPurchased ) ;
2024-01-15 14:25:17 +00:00
}
} ;
// Get all trader assort items
const traderItems = this . traderAssortHelper . getAssort ( sessionID , buyRequestData . tid ) . items ;
2024-02-02 13:54:07 -05:00
2024-01-15 14:25:17 +00:00
// Get item + children for purchase
const relevantItems = this . itemHelper . findAndReturnChildrenAsItems ( traderItems , buyRequestData . item_id ) ;
2024-07-23 11:12:53 -04:00
if ( relevantItems . length === 0 ) {
this . logger . error (
` Purchased trader: ${ buyRequestData . tid } offer: ${ buyRequestData . item_id } has no items ` ,
) ;
2024-06-12 23:07:38 +01:00
}
2024-01-15 14:25:17 +00:00
offerItems . push ( . . . relevantItems ) ;
}
2024-01-14 21:12:56 +00:00
2024-01-14 22:30:05 +00: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 18:25:03 +00:00
2024-02-05 14:07:21 +00:00
// Construct array of items to send to player
2024-09-24 12:47:29 +01:00
const itemsToSendToPlayer : IItem [ ] [ ] = [ ] ;
2024-07-23 11:12:53 -04:00
while ( itemsToSendRemaining > 0 ) {
2024-05-13 17:58:17 +00:00
const offerClone = this . cloner . clone ( offerItems ) ;
2024-02-05 14:07:21 +00:00
// Handle stackable items that have a max stack size limit
2024-01-14 22:30:05 +00:00
const itemCountToSend = Math . min ( itemMaxStackSize , itemsToSendRemaining ) ;
2024-02-05 14:07:21 +00:00
offerClone [ 0 ] . upd . StackObjectsCount = itemCountToSend ;
2024-01-14 21:12:56 +00:00
2024-01-14 22:30:05 +00:00
// Prevent any collisions
2024-02-05 14:07:21 +00:00
this . itemHelper . remapRootItemId ( offerClone ) ;
2024-07-23 11:12:53 -04:00
if ( offerClone . length > 1 ) {
2024-02-05 14:07:21 +00:00
this . itemHelper . reparentItemAndChildren ( offerClone [ 0 ] , offerClone ) ;
2024-01-15 14:25:17 +00:00
}
2024-01-16 18:25:03 +00:00
2024-02-05 14:07:21 +00:00
itemsToSendToPlayer . push ( offerClone ) ;
2024-01-14 22:30:05 +00:00
// Remove amount of items added to player stash
itemsToSendRemaining -= itemCountToSend ;
2024-01-16 18:25:03 +00:00
}
2024-02-05 14:07:21 +00: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 ) ;
2024-07-23 11:12:53 -04:00
if ( output . warnings . length > 0 ) {
2024-02-05 14:07:21 +00:00
return ;
}
2024-01-16 18:25:03 +00:00
/// Pay for purchase
this . paymentService . payMoney ( pmcData , buyRequestData , sessionID , output ) ;
2024-07-23 11:12:53 -04:00
if ( output . warnings . length > 0 ) {
2024-01-16 18:25:03 +00:00
const errorMessage = ` Transaction failed: ${ output . warnings [ 0 ] . errmsg } ` ;
this . httpResponse . appendErrorToOutput ( output , errorMessage , BackendErrorCodes . UNKNOWN_TRADING_ERROR ) ;
}
2023-03-03 15:23:46 +00:00
}
/ * *
* Sell item to trader
2023-10-10 11:03:20 +00:00
* @param profileWithItemsToSell Profile to remove items from
* @param profileToReceiveMoney Profile to accept the money for selling item
2023-05-24 15:51:05 +01:00
* @param sellRequest Request data
2023-03-03 15:23:46 +00:00
* @param sessionID Session id
2024-01-16 12:21:42 +00:00
* @param output IItemEventRouterResponse
2023-03-03 15:23:46 +00:00
* /
2023-11-16 21:42:06 +00:00
public sellItem (
profileWithItemsToSell : IPmcData ,
profileToReceiveMoney : IPmcData ,
sellRequest : IProcessSellTradeRequestData ,
sessionID : string ,
2024-01-16 12:21:42 +00:00
output : IItemEventRouterResponse ,
2024-07-23 11:12:53 -04:00
) : void {
2024-09-22 11:33:49 +01:00
// TODO - make more generic to support all quests that have this condition type
// Try to reduce perf hit as this is expensive to do every sale
// MUST OCCUR PRIOR TO ITEMS BEING REMOVED FROM INVENTORY
if ( sellRequest . tid === Traders . RAGMAN ) {
// Edge case, `Circulate` quest needs to track when certain items are sold to him
this . incrementCirculateSoldToTraderCounter ( profileWithItemsToSell , profileToReceiveMoney , sellRequest ) ;
}
2023-05-24 15:51:05 +01:00
// Find item in inventory and remove it
2024-07-23 11:12:53 -04:00
for ( const itemToBeRemoved of sellRequest . items ) {
2023-05-24 15:51:05 +01:00
const itemIdToFind = itemToBeRemoved . id . replace ( /\s+/g , "" ) ; // Strip out whitespace
2023-03-03 15:23:46 +00:00
2023-05-24 15:51:05 +01:00
// Find item in player inventory, or show error to player if not found
2024-05-17 15:32:41 -04:00
const matchingItemInInventory = profileWithItemsToSell . Inventory . items . find ( ( x ) = > x . _id === itemIdToFind ) ;
2024-07-23 11:12:53 -04:00
if ( ! matchingItemInInventory ) {
2023-05-24 15:51:05 +01:00
const errorMessage = ` Unable to sell item ${ itemToBeRemoved . id } , cannot be found in player inventory ` ;
this . logger . error ( errorMessage ) ;
2023-03-03 15:23:46 +00:00
2024-01-16 12:21:42 +00:00
this . httpResponse . appendErrorToOutput ( output , errorMessage ) ;
return ;
2023-03-03 15:23:46 +00:00
}
2023-05-24 15:51:05 +01:00
this . logger . debug ( ` Selling: id: ${ matchingItemInInventory . _id } tpl: ${ matchingItemInInventory . _tpl } ` ) ;
2023-10-10 11:03:20 +00:00
2024-07-23 11:12:53 -04:00
if ( sellRequest . tid === Traders . FENCE ) {
2024-05-01 20:17:09 +00:00
this . fenceService . addItemsToFenceAssort (
profileWithItemsToSell . Inventory . items ,
matchingItemInInventory ,
) ;
}
2024-09-22 11:33:49 +01:00
// Remove item from inventory + any child items it has
2024-01-16 12:21:42 +00:00
this . inventoryHelper . removeItem ( profileWithItemsToSell , itemToBeRemoved . id , sessionID , output ) ;
2023-03-03 15:23:46 +00:00
}
2023-05-24 15:51:05 +01:00
// Give player money for sold item(s)
2024-01-16 12:21:42 +00:00
this . paymentService . giveProfileMoney ( profileToReceiveMoney , sellRequest . price , sellRequest , output , sessionID ) ;
2023-03-03 15:23:46 +00:00
}
2024-09-22 11:33:49 +01:00
protected incrementCirculateSoldToTraderCounter (
profileWithItemsToSell : IPmcData ,
profileToReceiveMoney : IPmcData ,
sellRequest : IProcessSellTradeRequestData ,
) {
const circulateQuestId = "6663149f1d3ec95634095e75" ;
const activeCirculateQuest = profileToReceiveMoney . Quests . find (
( quest ) = > quest . qid === circulateQuestId && quest . status === QuestStatus . Started ,
) ;
// Player not on Circulate quest ,exit
if ( ! activeCirculateQuest ) {
return ;
}
// Find related task condition
const taskCondition = Object . values ( profileToReceiveMoney . TaskConditionCounters ) . find (
( condition ) = > condition . sourceId === circulateQuestId && condition . type === "SellItemToTrader" ,
) ;
// No relevant condtion in profile, nothing to increment
if ( ! taskCondition ) {
this . logger . error ( "Unable to find `sellToTrader` task counter for Circulate quest in profile, skipping" ) ;
return ;
}
// Condition exists in profile
const circulateQuestDb = this . databaseService . getQuests ( ) [ circulateQuestId ] ;
if ( ! circulateQuestDb ) {
this . logger . error ( ` Unable to find quest: ${ circulateQuestId } in db, skipping ` ) ;
return ;
}
// Get sellToTrader condition from quest
const sellItemToTraderCondition = circulateQuestDb . conditions . AvailableForFinish . find (
( condition ) = > condition . conditionType === "SellItemToTrader" ,
) ;
// Quest doesnt have a sellItemToTrader condition, nothing to do
if ( ! sellItemToTraderCondition ) {
this . logger . error ( "Unable to find `sellToTrader` counter for Circulate quest in db, skipping" ) ;
return ;
}
// Iterate over items sold to trader
const itemsTplsThatIncrement = sellItemToTraderCondition . target ;
for ( const itemSoldToTrader of sellRequest . items ) {
// Get sold items' details from profile
const itemDetails = profileWithItemsToSell . Inventory . items . find (
( inventoryItem ) = > inventoryItem . _id === itemSoldToTrader . id ,
) ;
if ( ! itemDetails ) {
this . logger . error (
` Unable to find item in inventory to sell to trader with id: ${ itemSoldToTrader . id } , cannot increment counter, skipping ` ,
) ;
continue ;
}
// Is sold item on the increment list
if ( itemsTplsThatIncrement . includes ( itemDetails . _tpl ) ) {
taskCondition . value += itemSoldToTrader . count ;
}
}
}
2023-12-14 15:47:01 +00:00
/ * *
* Traders allow a limited number of purchases per refresh cycle ( default 60 mins )
2024-01-20 00:19:13 +00:00
* @param sessionId Session id
2024-06-08 19:38:16 +01:00
* @param pmcData Profile making the purchase
2024-01-20 00:19:13 +00:00
* @param traderId Trader assort is purchased from
2023-12-14 15:47:01 +00:00
* @param assortBeingPurchased the item from trader being bought
* @param assortId Id of assort being purchased
2024-01-20 00:19:13 +00:00
* @param count How many of the item are being bought
2023-12-14 15:47:01 +00:00
* /
2024-02-02 13:54:07 -05:00
protected checkPurchaseIsWithinTraderItemLimit (
sessionId : string ,
2024-06-08 19:38:16 +01:00
pmcData : IPmcData ,
2024-02-02 13:54:07 -05:00
traderId : string ,
2024-09-24 12:47:29 +01:00
assortBeingPurchased : IItem ,
2024-02-02 13:54:07 -05:00
assortId : string ,
count : number ,
2024-07-23 11:12:53 -04:00
) : void {
const traderPurchaseData = this . traderPurchasePersisterService . getProfileTraderPurchase (
sessionId ,
traderId ,
assortBeingPurchased . _id ,
) ;
const traderItemPurchaseLimit = this . traderHelper . getAccountTypeAdjustedTraderPurchaseLimit (
assortBeingPurchased . upd ? . BuyRestrictionMax ,
pmcData . Info . GameVersion ,
) ;
if ( ( traderPurchaseData ? . count ? ? 0 + count ) > traderItemPurchaseLimit ) {
2023-11-16 21:42:06 +00:00
throw new Error (
2024-06-08 19:38:16 +01:00
` Unable to purchase: ${ count } items, this would exceed your purchase limit of ${ traderItemPurchaseLimit } from the trader: ${ traderId } assort: ${ assortId } this refresh ` ,
2023-11-16 21:42:06 +00:00
) ;
2023-03-03 15:23:46 +00:00
}
}
2023-11-16 21:42:06 +00:00
}