diff --git a/project/src/controllers/TradeController.ts b/project/src/controllers/TradeController.ts index 19fcf741..1a62a682 100644 --- a/project/src/controllers/TradeController.ts +++ b/project/src/controllers/TradeController.ts @@ -115,7 +115,7 @@ export class TradeController type: "buy_from_trader", tid: (sellerIsTrader) ? fleaOffer.user.id : "ragfair", // eslint-disable-next-line @typescript-eslint/naming-convention - item_id: fleaOffer.root, + item_id: fleaOffer._id, // Store ragfair offerId in buyRequestData.item_id count: offer.count, // eslint-disable-next-line @typescript-eslint/naming-convention scheme_id: 0, diff --git a/project/src/helpers/InventoryHelper.ts b/project/src/helpers/InventoryHelper.ts index b09a6dfe..4e9c71ab 100644 --- a/project/src/helpers/InventoryHelper.ts +++ b/project/src/helpers/InventoryHelper.ts @@ -10,6 +10,7 @@ import { TraderAssortHelper } from "@spt-aki/helpers/TraderAssortHelper"; import { IPmcData } from "@spt-aki/models/eft/common/IPmcData"; import { Inventory } from "@spt-aki/models/eft/common/tables/IBotBase"; import { Item, Location, Upd } from "@spt-aki/models/eft/common/tables/IItem"; +import { IAddItemDirectRequest } from "@spt-aki/models/eft/inventory/IAddItemDirectRequest"; import { AddItem, IAddItemRequestData } from "@spt-aki/models/eft/inventory/IAddItemRequestData"; import { IAddItemTempObject } from "@spt-aki/models/eft/inventory/IAddItemTempObject"; import { IInventoryMergeRequestData } from "@spt-aki/models/eft/inventory/IInventoryMergeRequestData"; @@ -67,6 +68,90 @@ export class InventoryHelper } /** + * Add whatever is passed in `request.itemWithModsToAdd` into player inventory (if it fits) + * @param sessionId Session id + * @param request addItemDirect request + * @param pmcData Player profile + * @param output Client response object + * @returns IItemEventRouterResponse + */ + public addItemToInventory(sessionId: string, request: IAddItemDirectRequest, pmcData: IPmcData, output: IItemEventRouterResponse): IItemEventRouterResponse + { + const itemWithModsToAddClone = this.jsonUtil.clone(request.itemWithModsToAdd); + + // get stash layouts ready for use + const stashFS2D = this.getStashSlotMap(pmcData, sessionId); + const sortingTableFS2D = this.getSortingTableSlotMap(pmcData); + + // Find an empty slot in stash for item being added - adds 'location' property to root item + const errorOutput = this.placeItemInInventory( + stashFS2D, + sortingTableFS2D, + itemWithModsToAddClone, + pmcData.Inventory, + request.useSortingTable, + output, + ); + if (errorOutput) + { + // Failed to place, error out + return errorOutput; + } + + // Apply/remove FiR to item + mods + for (const item of itemWithModsToAddClone) + { + if (!item.upd) + { + item.upd = {}; + } + + if (request.foundInRaid) + { + item.upd.SpawnedInSession = request.foundInRaid; + } + else + { + if (delete item.upd.SpawnedInSession) + { + delete item.upd.SpawnedInSession; + } + } + } + + // Remove trader properties from root item + this.removeTraderRagfairRelatedUpdProperties(itemWithModsToAddClone[0].upd); + + // Run callback + try + { + if (typeof request.callback === "function") + { + request.callback(); + } + } + catch (err) + { + // Callback failed + const message = typeof err === "string" + ? err + : this.localisationService.getText("http-unknown_error"); + + return this.httpResponse.appendErrorToOutput(output, message); + } + + // Add item + mods to output + profile inventory + output.profileChanges[sessionId].items.new.push(...itemWithModsToAddClone); + pmcData.Inventory.items.push(...itemWithModsToAddClone); + + this.logger.debug(`Added item ${itemWithModsToAddClone[0]._tpl} with ${itemWithModsToAddClone.length - 1} mods to inventory`); + + return output; + } + + /** + * @deprecated - use addItemDirect() + * * BUG: Passing the same item multiple times with a count of 1 will cause multiples of that item to be added (e.g. x3 separate objects of tar cola with count of 1 = 9 tarcolas being added to inventory) * @param pmcData Profile to add items to * @param request request data to add items @@ -175,7 +260,7 @@ export class InventoryHelper { // Update Items `location` properties const itemWithChildren = this.itemHelper.findAndReturnChildrenAsItems(itemsToAddPool, itemToAdd.itemRef._id); - const errorOutput = this.placeItemInInventory( + const errorOutput = this.placeItemInInventoryLegacy( itemToAdd, stashFS2D, sortingTableFS2D, @@ -252,7 +337,7 @@ export class InventoryHelper } // Remove invalid properties prior to adding to inventory - this.removeTraderRelatedUpdProperties(rootItemUpd); + this.removeTraderRagfairRelatedUpdProperties(rootItemUpd); // Add root item to client return object output.profileChanges[sessionID].items.new.push({ @@ -381,10 +466,10 @@ export class InventoryHelper } /** - * Remove properties from a Upd object used by a trader + * Remove properties from a Upd object used by a trader/ragfair * @param upd Object to update */ - protected removeTraderRelatedUpdProperties(upd: Upd): void + protected removeTraderRagfairRelatedUpdProperties(upd: Upd): void { if (upd.UnlimitedCount !== undefined) { @@ -402,6 +487,108 @@ export class InventoryHelper } } + protected placeItemInInventory( + stashFS2D: number[][], + sortingTableFS2D: number[][], + itemWithChildren: Item[], + playerInventory: Inventory, + useSortingTable: boolean, + output: IItemEventRouterResponse): IItemEventRouterResponse + { + const itemSize = this.getItemSize(itemWithChildren[0]._tpl, itemWithChildren[0]._id, itemWithChildren); + const findSlotResult = this.containerHelper.findSlotForItem(stashFS2D, itemSize[0], itemSize[1]); + + if (findSlotResult.success) + { + /* Fill in the StashFS_2D with an imaginary item, to simulate it already being added + * so the next item to search for a free slot won't find the same one */ + const itemSizeX = findSlotResult.rotation ? itemSize[1] : itemSize[0]; + const itemSizeY = findSlotResult.rotation ? itemSize[0] : itemSize[1]; + + try + { + stashFS2D = this.containerHelper.fillContainerMapWithItem( + stashFS2D, + findSlotResult.x, + findSlotResult.y, + itemSizeX, + itemSizeY, + false, + ); // TODO: rotation not passed in, bad? + } + catch (err) + { + const errorText = typeof err === "string" ? ` -> ${err}` : ""; + this.logger.error(this.localisationService.getText("inventory-fill_container_failed", errorText)); + + return this.httpResponse.appendErrorToOutput( + output, + this.localisationService.getText("inventory-no_stash_space"), + ); + } + // Store details for object, incuding container item will be placed in + itemWithChildren[0].parentId = playerInventory.stash; + itemWithChildren[0].location = { + x: findSlotResult.x, + y: findSlotResult.y, + r: findSlotResult.rotation ? 1 : 0, + rotation: findSlotResult.rotation, + }; + + // Success! exit + return; + } + + // Space not found in main stash, use sorting table + if (useSortingTable) + { + const findSortingSlotResult = this.containerHelper.findSlotForItem( + sortingTableFS2D, + itemSize[0], + itemSize[1], + ); + const itemSizeX = findSortingSlotResult.rotation ? itemSize[1] : itemSize[0]; + const itemSizeY = findSortingSlotResult.rotation ? itemSize[0] : itemSize[1]; + try + { + sortingTableFS2D = this.containerHelper.fillContainerMapWithItem( + sortingTableFS2D, + findSortingSlotResult.x, + findSortingSlotResult.y, + itemSizeX, + itemSizeY, + false, + ); // TODO: rotation not passed in, bad? + } + catch (err) + { + const errorText = typeof err === "string" ? ` -> ${err}` : ""; + this.logger.error(this.localisationService.getText("inventory-fill_container_failed", errorText)); + + return this.httpResponse.appendErrorToOutput( + output, + this.localisationService.getText("inventory-no_stash_space"), + ); + } + + // Store details for object, incuding container item will be placed in + itemWithChildren[0].parentId = playerInventory.sortingTable; + itemWithChildren[0].location = { + x: findSortingSlotResult.x, + y: findSortingSlotResult.y, + r: findSortingSlotResult.rotation ? 1 : 0, + rotation: findSortingSlotResult.rotation, + }; + } + else + { + return this.httpResponse.appendErrorToOutput( + output, + this.localisationService.getText("inventory-no_stash_space"), + ); + } + } + /** * Take the given item, find a free slot in passed in inventory and place it there * If no space in inventory, place in sorting table @@ -414,7 +601,7 @@ export class InventoryHelper * @param output Client output object * @returns Client error output if placing item failed */ - protected placeItemInInventory( + protected placeItemInInventoryLegacy( itemToAdd: IAddItemTempObject, stashFS2D: number[][], sortingTableFS2D: number[][], @@ -796,10 +983,10 @@ export class InventoryHelper * inputs Item template ID, Item Id, InventoryItem (item from inventory having _id and _tpl) * outputs [width, height] */ - public getItemSize(itemTpl: string, itemID: string, inventoryItem: Item[]): number[] + public getItemSize(itemTpl: string, itemID: string, inventoryItems: Item[]): number[] { // -> Prepares item Width and height returns [sizeX, sizeY] - return this.getSizeByInventoryItemHash(itemTpl, itemID, this.getInventoryItemHash(inventoryItem)); + return this.getSizeByInventoryItemHash(itemTpl, itemID, this.getInventoryItemHash(inventoryItems)); } // note from 2027: there IS a thing i didn't explore and that is Merges With Children diff --git a/project/src/helpers/TradeHelper.ts b/project/src/helpers/TradeHelper.ts index b5270d85..601e4ec1 100644 --- a/project/src/helpers/TradeHelper.ts +++ b/project/src/helpers/TradeHelper.ts @@ -5,6 +5,7 @@ import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; 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"; +import { IAddItemDirectRequest } from "@spt-aki/models/eft/inventory/IAddItemDirectRequest"; 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"; @@ -18,6 +19,7 @@ import { RagfairServer } from "@spt-aki/servers/RagfairServer"; import { FenceService } from "@spt-aki/services/FenceService"; import { PaymentService } from "@spt-aki/services/PaymentService"; import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil"; +import { JsonUtil } from "@spt-aki/utils/JsonUtil"; @injectable() export class TradeHelper @@ -26,6 +28,7 @@ export class TradeHelper constructor( @inject("WinstonLogger") protected logger: ILogger, + @inject("JsonUtil") protected jsonUtil: JsonUtil, @inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder, @inject("TraderHelper") protected traderHelper: TraderHelper, @inject("ItemHelper") protected itemHelper: ItemHelper, @@ -70,12 +73,14 @@ export class TradeHelper const callback = () => { + // Update assort/flea item values let itemPurchased: Item; const isRagfair = buyRequestData.tid.toLocaleLowerCase() === "ragfair"; if (isRagfair) { const allOffers = this.ragfairServer.getOffers(); - const offersWithItem = allOffers.find((x) => x.items[0]._id === buyRequestData.item_id); + // We store ragfair offerid in buyRequestData.item_id + const offersWithItem = allOffers.find((x) => x._id === buyRequestData.item_id); itemPurchased = offersWithItem.items[0]; } else @@ -119,10 +124,26 @@ export class TradeHelper // Increment non-fence trader item buy count this.incrementAssortBuyCount(itemPurchased, buyRequestData.count); } - - this.logger.debug(`Bought item: ${buyRequestData.item_id} from: ${Traders[buyRequestData.tid]}`); }; + if (buyRequestData.tid.toLocaleLowerCase() === "ragfair") + { + const allOffers = this.ragfairServer.getOffers(); + const offersWithItem = allOffers.find((x) => x._id === buyRequestData.item_id); + + const request: IAddItemDirectRequest = { + itemWithModsToAdd: offersWithItem.items, + foundInRaid: true, + callback: callback, + useSortingTable: true + } + + return this.inventoryHelper.addItemToInventory(sessionID, request, pmcData, output); + } + + // TODO - handle traders + // TODO - handle fence + return this.inventoryHelper.addItem(pmcData, newReq, output, sessionID, callback, foundInRaid, upd); } diff --git a/project/src/models/eft/inventory/IAddItemDirectRequest.ts b/project/src/models/eft/inventory/IAddItemDirectRequest.ts new file mode 100644 index 00000000..c02ea1e3 --- /dev/null +++ b/project/src/models/eft/inventory/IAddItemDirectRequest.ts @@ -0,0 +1,10 @@ +import { Item } from "../common/tables/IItem" + +export interface IAddItemDirectRequest +{ + /** Item and child mods to add to player inventory */ + itemWithModsToAdd: Item[]; + foundInRaid: boolean; + callback: () => void; + useSortingTable: boolean; +} \ No newline at end of file diff --git a/project/src/services/FenceService.ts b/project/src/services/FenceService.ts index b96e0932..4dd11b54 100644 --- a/project/src/services/FenceService.ts +++ b/project/src/services/FenceService.ts @@ -565,8 +565,7 @@ export class FenceService const plateTpl = plateSlot._props.filters[0].Plate if (!plateTpl) { - this.logger.warning(`Fence generation: item: ${itemDbDetails._id} ${itemDbDetails._name} lacks a default plate for slot: ${plateSlot._name}, skipping`); - + // Bsg data lacks a default plate, skip adding mod continue; } const modItemDbDetails = this.itemHelper.getItem(plateTpl)[1]; diff --git a/project/src/utils/RagfairOfferHolder.ts b/project/src/utils/RagfairOfferHolder.ts index fc772c8e..cd40c717 100644 --- a/project/src/utils/RagfairOfferHolder.ts +++ b/project/src/utils/RagfairOfferHolder.ts @@ -61,10 +61,10 @@ export class RagfairOfferHolder { const trader = offer.user.id; const offerId = offer._id; - const template = offer.items[0]._tpl; + const itemTpl = offer.items[0]._tpl; this.offersById.set(offerId, offer); this.addOfferByTrader(trader, offer); - this.addOfferByTemplates(template, offer); + this.addOfferByTemplates(itemTpl, offer); } public removeOffer(offer: IRagfairOffer): void