Improve buyItem() handling of multiple and stackable item purchases

rename `reparentPresets` to `reparentItemAndChildren` and move to `itemHelper`
This commit is contained in:
Dev 2024-01-14 21:12:56 +00:00
parent c2f390d4ac
commit 5005a5160a
8 changed files with 176 additions and 95 deletions

View File

@ -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)

View File

@ -203,11 +203,9 @@ export class RagfairOfferGenerator
{
return currencyCount;
}
else
{
return this.handbookHelper.inRUB(currencyCount, currencyType);
}
}
/**
* Check userId, if its a player, return their pmc _id, otherwise return userId parameter
@ -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);

View File

@ -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
{

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
// 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: offerWithItem.items,
foundInRaid: true,
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