diff --git a/project/src/generators/LocationGenerator.ts b/project/src/generators/LocationGenerator.ts index 1045b9b9..ebb623e7 100644 --- a/project/src/generators/LocationGenerator.ts +++ b/project/src/generators/LocationGenerator.ts @@ -975,7 +975,7 @@ export class LocationGenerator { try { - children = this.ragfairServerHelper.reparentPresets(defaultPreset._items[0], defaultPreset._items); + children = this.itemHelper.reparentItemAndChildren(defaultPreset._items[0], defaultPreset._items); } catch (error) { @@ -1014,7 +1014,7 @@ export class LocationGenerator { if (children?.length > 0) { - items = this.ragfairServerHelper.reparentPresets(rootItem, children); + items = this.itemHelper.reparentItemAndChildren(rootItem, children); } } catch (error) diff --git a/project/src/generators/RagfairOfferGenerator.ts b/project/src/generators/RagfairOfferGenerator.ts index b0932eea..5bcf832c 100644 --- a/project/src/generators/RagfairOfferGenerator.ts +++ b/project/src/generators/RagfairOfferGenerator.ts @@ -203,10 +203,8 @@ export class RagfairOfferGenerator { return currencyCount; } - else - { - return this.handbookHelper.inRUB(currencyCount, currencyType); - } + + return this.handbookHelper.inRUB(currencyCount, currencyType); } /** @@ -352,13 +350,11 @@ export class RagfairOfferGenerator } // Get item + sub-items if preset, otherwise just get item - const items: Item[] = isPreset + const itemWithChildren: Item[] = isPreset ? this.ragfairServerHelper.getPresetItems(assortItem) : [ ...[assortItem], - ...this.itemHelper.findAndReturnChildrenByAssort( - assortItem._id, - this.ragfairAssortGenerator.getAssortItems(), + ...this.itemHelper.findAndReturnChildrenByAssort(assortItem._id, this.ragfairAssortGenerator.getAssortItems(), ), ]; @@ -372,7 +368,18 @@ export class RagfairOfferGenerator const assortSingleOfferProcesses = []; for (let index = 0; index < offerCount; index++) { - assortSingleOfferProcesses.push(this.createSingleOfferForItem(items, isPreset, itemDetails)); + delete itemWithChildren[0].parentId; + delete itemWithChildren[0].slotId; + + if (!isPreset) + { + // presets get unique id generated during getPresetItems() earlier + itemWithChildren[0]._id = this.hashUtil.generate(); + } + + delete itemWithChildren[0].parentId; + + assortSingleOfferProcesses.push(this.createSingleOfferForItem(itemWithChildren, isPreset, itemDetails)); } await Promise.all(assortSingleOfferProcesses); diff --git a/project/src/generators/RepeatableQuestGenerator.ts b/project/src/generators/RepeatableQuestGenerator.ts index 0e6a6333..ef299657 100644 --- a/project/src/generators/RepeatableQuestGenerator.ts +++ b/project/src/generators/RepeatableQuestGenerator.ts @@ -1112,7 +1112,7 @@ export class RepeatableQuestGenerator { const rootItem = preset.find(x => x._tpl === tpl); rewardItem.target = rootItem._id; // Target property and root items id must match - rewardItem.items = this.ragfairServerHelper.reparentPresets(rootItem, preset); + rewardItem.items = this.itemHelper.reparentItemAndChildren(rootItem, preset); } else { diff --git a/project/src/helpers/InventoryHelper.ts b/project/src/helpers/InventoryHelper.ts index 4e9c71ab..4ce2a53f 100644 --- a/project/src/helpers/InventoryHelper.ts +++ b/project/src/helpers/InventoryHelper.ts @@ -75,7 +75,7 @@ export class InventoryHelper * @param output Client response object * @returns IItemEventRouterResponse */ - public addItemToInventory(sessionId: string, request: IAddItemDirectRequest, pmcData: IPmcData, output: IItemEventRouterResponse): IItemEventRouterResponse + public addItemToStash(sessionId: string, request: IAddItemDirectRequest, pmcData: IPmcData, output: IItemEventRouterResponse): IItemEventRouterResponse { const itemWithModsToAddClone = this.jsonUtil.clone(request.itemWithModsToAdd); @@ -83,7 +83,7 @@ export class InventoryHelper 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 + // Find empty slot in stash for item being added - adds 'location' + parentid + slotId properties to root item const errorOutput = this.placeItemInInventory( stashFS2D, sortingTableFS2D, @@ -99,25 +99,7 @@ export class InventoryHelper } // 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; - } - } - } + this.setFindInRaidStatusForItem(itemWithModsToAddClone, request.foundInRaid); // Remove trader properties from root item this.removeTraderRagfairRelatedUpdProperties(itemWithModsToAddClone[0].upd); @@ -150,7 +132,37 @@ export class InventoryHelper } /** - * @deprecated - use addItemDirect() + * Set FiR status for an item + its children + * @param itemWithChildren An item + * @param foundInRaid Item was found in raid + */ + private setFindInRaidStatusForItem(itemWithChildren: Item[], foundInRaid: boolean) + { + for (const item of itemWithChildren) + { + // Ensure item has upd object + if (!item.upd) + { + item.upd = {}; + } + + if (foundInRaid) + { + item.upd.SpawnedInSession = foundInRaid; + } + + else + { + if (delete item.upd.SpawnedInSession) + { + delete item.upd.SpawnedInSession; + } + } + } + } + + /** + * @deprecated - use addItemToStash() * * 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 @@ -495,7 +507,11 @@ export class InventoryHelper useSortingTable: boolean, output: IItemEventRouterResponse): IItemEventRouterResponse { - const itemSize = this.getItemSize(itemWithChildren[0]._tpl, itemWithChildren[0]._id, itemWithChildren); + // Get x/y size of item + const rootItem = itemWithChildren[0]; + const itemSize = this.getItemSize(rootItem._tpl, rootItem._id, itemWithChildren); + + // Look for a place to slot item into const findSlotResult = this.containerHelper.findSlotForItem(stashFS2D, itemSize[0], itemSize[1]); if (findSlotResult.success) @@ -518,7 +534,9 @@ export class InventoryHelper } catch (err) { - const errorText = typeof err === "string" ? ` -> ${err}` : ""; + const errorText = (typeof err === "string") + ? ` -> ${err}` + : ""; this.logger.error(this.localisationService.getText("inventory-fill_container_failed", errorText)); return this.httpResponse.appendErrorToOutput( @@ -527,8 +545,9 @@ export class InventoryHelper ); } // Store details for object, incuding container item will be placed in - itemWithChildren[0].parentId = playerInventory.stash; - itemWithChildren[0].location = { + rootItem.parentId = playerInventory.stash; + rootItem.slotId = "hideout"; + rootItem.location = { x: findSlotResult.x, y: findSlotResult.y, r: findSlotResult.rotation ? 1 : 0, @@ -774,7 +793,8 @@ export class InventoryHelper protected splitStackIntoSmallerStacks(assortItems: Item[], requestItem: AddItem, result: IAddItemTempObject[]): void { for (const item of assortItems) - {// Iterated item matches root item + { + // Iterated item matches root item if (item._id === requestItem.item_id) { // Get item details from db diff --git a/project/src/helpers/ItemHelper.ts b/project/src/helpers/ItemHelper.ts index bf06224e..3eb4de3b 100644 --- a/project/src/helpers/ItemHelper.ts +++ b/project/src/helpers/ItemHelper.ts @@ -1331,6 +1331,79 @@ export class ItemHelper { return ["front_plate", "back_plate", "side_plate", "left_side_plate", "right_side_plate"]; } + + /** + * Generate new unique ids for child items while preserving hierarchy + * @param rootItem Base/primary item + * @param itemWithChildren Primary item + children of primary item + * @returns Item array with updated IDs + */ + public reparentItemAndChildren(rootItem: Item, itemWithChildren: Item[]): Item[] + { + const oldRootId = itemWithChildren[0]._id; + const idMappings = {}; + + idMappings[oldRootId] = rootItem._id; + + for (const mod of itemWithChildren) + { + if (idMappings[mod._id] === undefined) + { + idMappings[mod._id] = this.hashUtil.generate(); + } + + // Has parentId + no remapping exists for its parent + if (mod.parentId !== undefined && idMappings[mod.parentId] === undefined) + { + // Make remapping for items parentId + idMappings[mod.parentId] = this.hashUtil.generate(); + } + + mod._id = idMappings[mod._id]; + + if (mod.parentId !== undefined) + { + mod.parentId = idMappings[mod.parentId]; + } + } + + // Force item's details into first location of presetItems + if (itemWithChildren[0]._tpl !== rootItem._tpl) + { + this.logger.warning(`Reassigning root item from ${itemWithChildren[0]._tpl} to ${rootItem._tpl}`); + } + + itemWithChildren[0] = rootItem; + + return itemWithChildren; + } + + /** + * Update a root items _id property value to be unique + * @param itemWithChildren Item to update root items _id property + * @param newId Optional: new id to use + */ + public remapRootItemId(itemWithChildren: Item[], newId = this.hashUtil.generate()): void + { + const rootItemExistingId = itemWithChildren[0]._id; + + for (const item of itemWithChildren) + { + // Root, update id + if (item._id === rootItemExistingId) + { + item._id = newId; + + continue; + } + + // Child with parent of root, update + if (item.parentId === rootItemExistingId) + { + item.parentId = newId; + } + } + } } namespace ItemHelper diff --git a/project/src/helpers/QuestHelper.ts b/project/src/helpers/QuestHelper.ts index ff68aad1..a19fcc7e 100644 --- a/project/src/helpers/QuestHelper.ts +++ b/project/src/helpers/QuestHelper.ts @@ -323,7 +323,7 @@ export class QuestHelper items.push(this.jsonUtil.clone(mod)); } - rewardItems = rewardItems.concat(this.ragfairServerHelper.reparentPresets(target, items)); + rewardItems = rewardItems.concat(this.itemHelper.reparentItemAndChildren(target, items)); } return rewardItems; diff --git a/project/src/helpers/RagfairServerHelper.ts b/project/src/helpers/RagfairServerHelper.ts index cf3a7175..94f8993b 100644 --- a/project/src/helpers/RagfairServerHelper.ts +++ b/project/src/helpers/RagfairServerHelper.ts @@ -272,7 +272,7 @@ export class RagfairServerHelper public getPresetItems(item: Item): Item[] { const preset = this.jsonUtil.clone(this.databaseServer.getTables().globals.ItemPresets[item._id]._items); - return this.reparentPresets(item, preset); + return this.itemHelper.reparentItemAndChildren(item, preset); } /** @@ -290,56 +290,10 @@ export class RagfairServerHelper const presetItems = this.jsonUtil.clone( this.databaseServer.getTables().globals.ItemPresets[itemId]._items, ); - presets.push(this.reparentPresets(item, presetItems)); + presets.push(this.itemHelper.reparentItemAndChildren(item, presetItems)); } } return presets; } - - /** - * Generate new unique ids for child items while preserving hierarchy - * @param rootItem Base/primary item of preset - * @param preset Primary item + children of primary item - * @returns Item array with new IDs - */ - public reparentPresets(rootItem: Item, preset: Item[]): Item[] - { - const oldRootId = preset[0]._id; - const idMappings = {}; - - idMappings[oldRootId] = rootItem._id; - - for (const mod of preset) - { - if (idMappings[mod._id] === undefined) - { - idMappings[mod._id] = this.hashUtil.generate(); - } - - // Has parentId + no remapping exists for its parent - if (mod.parentId !== undefined && idMappings[mod.parentId] === undefined) - { - // Make remapping for items parentId - idMappings[mod.parentId] = this.hashUtil.generate(); - } - - mod._id = idMappings[mod._id]; - - if (mod.parentId !== undefined) - { - mod.parentId = idMappings[mod.parentId]; - } - } - - // Force item's details into first location of presetItems - if (preset[0]._tpl !== rootItem._tpl) - { - this.logger.warning(`Reassigning root item from ${preset[0]._tpl} to ${rootItem._tpl}`); - } - - preset[0] = rootItem; - - return preset; - } } diff --git a/project/src/helpers/TradeHelper.ts b/project/src/helpers/TradeHelper.ts index 30e151b6..61600345 100644 --- a/project/src/helpers/TradeHelper.ts +++ b/project/src/helpers/TradeHelper.ts @@ -11,6 +11,7 @@ import { IProcessBuyTradeRequestData } from "@spt-aki/models/eft/trade/IProcessB import { IProcessSellTradeRequestData } from "@spt-aki/models/eft/trade/IProcessSellTradeRequestData"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { Traders } from "@spt-aki/models/enums/Traders"; +import { IInventoryConfig } from "@spt-aki/models/spt/config/IInventoryConfig"; 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"; @@ -25,6 +26,7 @@ import { JsonUtil } from "@spt-aki/utils/JsonUtil"; export class TradeHelper { protected traderConfig: ITraderConfig; + protected inventoryConfig: IInventoryConfig; constructor( @inject("WinstonLogger") protected logger: ILogger, @@ -41,6 +43,7 @@ export class TradeHelper ) { this.traderConfig = this.configServer.getConfig(ConfigTypes.TRADER); + this.inventoryConfig = this.configServer.getConfig(ConfigTypes.INVENTORY); } /** @@ -128,17 +131,41 @@ export class TradeHelper if (buyRequestData.tid.toLocaleLowerCase() === "ragfair") { + // Get raw offer from ragfair, clone to prevent altering offer itself const allOffers = this.ragfairServer.getOffers(); - const offerWithItem = allOffers.find((x) => x._id === buyRequestData.item_id); + const offerWithItemCloned = this.jsonUtil.clone(allOffers.find((x) => x._id === buyRequestData.item_id)); + const offerItems = offerWithItemCloned.items; - const request: IAddItemDirectRequest = { - itemWithModsToAdd: offerWithItem.items, - foundInRaid: true, - callback: callback, - useSortingTable: true + + // 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; + 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; + + // Prevent any collisions + this.itemHelper.remapRootItemId(offerItems); + + // Construct request + const request: IAddItemDirectRequest = { + itemWithModsToAdd: this.itemHelper.reparentItemAndChildren(offerItems[0], offerItems), + foundInRaid: this.inventoryConfig.newItemsMarkedFound, + callback: callback, + useSortingTable: true + }; + + this.inventoryHelper.addItemToStash(sessionID, request, pmcData, output); + + // Remove amount of items added to player stash + itemsToSendRemaining -= itemCountToSend; } - return this.inventoryHelper.addItemToInventory(sessionID, request, pmcData, output); + return output; } // TODO - handle traders