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 try
{ {
children = this.ragfairServerHelper.reparentPresets(defaultPreset._items[0], defaultPreset._items); children = this.itemHelper.reparentItemAndChildren(defaultPreset._items[0], defaultPreset._items);
} }
catch (error) catch (error)
{ {
@ -1014,7 +1014,7 @@ export class LocationGenerator
{ {
if (children?.length > 0) if (children?.length > 0)
{ {
items = this.ragfairServerHelper.reparentPresets(rootItem, children); items = this.itemHelper.reparentItemAndChildren(rootItem, children);
} }
} }
catch (error) catch (error)

View File

@ -203,11 +203,9 @@ export class RagfairOfferGenerator
{ {
return currencyCount; return currencyCount;
} }
else
{
return this.handbookHelper.inRUB(currencyCount, currencyType); return this.handbookHelper.inRUB(currencyCount, currencyType);
} }
}
/** /**
* Check userId, if its a player, return their pmc _id, otherwise return userId parameter * 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 // Get item + sub-items if preset, otherwise just get item
const items: Item[] = isPreset const itemWithChildren: Item[] = isPreset
? this.ragfairServerHelper.getPresetItems(assortItem) ? this.ragfairServerHelper.getPresetItems(assortItem)
: [ : [
...[assortItem], ...[assortItem],
...this.itemHelper.findAndReturnChildrenByAssort( ...this.itemHelper.findAndReturnChildrenByAssort(assortItem._id, this.ragfairAssortGenerator.getAssortItems(),
assortItem._id,
this.ragfairAssortGenerator.getAssortItems(),
), ),
]; ];
@ -372,7 +368,18 @@ export class RagfairOfferGenerator
const assortSingleOfferProcesses = []; const assortSingleOfferProcesses = [];
for (let index = 0; index < offerCount; index++) 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); await Promise.all(assortSingleOfferProcesses);

View File

@ -1112,7 +1112,7 @@ export class RepeatableQuestGenerator
{ {
const rootItem = preset.find(x => x._tpl === tpl); const rootItem = preset.find(x => x._tpl === tpl);
rewardItem.target = rootItem._id; // Target property and root items id must match 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 else
{ {

View File

@ -75,7 +75,7 @@ export class InventoryHelper
* @param output Client response object * @param output Client response object
* @returns IItemEventRouterResponse * @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); const itemWithModsToAddClone = this.jsonUtil.clone(request.itemWithModsToAdd);
@ -83,7 +83,7 @@ export class InventoryHelper
const stashFS2D = this.getStashSlotMap(pmcData, sessionId); const stashFS2D = this.getStashSlotMap(pmcData, sessionId);
const sortingTableFS2D = this.getSortingTableSlotMap(pmcData); 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( const errorOutput = this.placeItemInInventory(
stashFS2D, stashFS2D,
sortingTableFS2D, sortingTableFS2D,
@ -99,25 +99,7 @@ export class InventoryHelper
} }
// Apply/remove FiR to item + mods // Apply/remove FiR to item + mods
for (const item of itemWithModsToAddClone) this.setFindInRaidStatusForItem(itemWithModsToAddClone, request.foundInRaid);
{
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 // Remove trader properties from root item
this.removeTraderRagfairRelatedUpdProperties(itemWithModsToAddClone[0].upd); 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) * 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 pmcData Profile to add items to
@ -495,7 +507,11 @@ export class InventoryHelper
useSortingTable: boolean, useSortingTable: boolean,
output: IItemEventRouterResponse): IItemEventRouterResponse 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]); const findSlotResult = this.containerHelper.findSlotForItem(stashFS2D, itemSize[0], itemSize[1]);
if (findSlotResult.success) if (findSlotResult.success)
@ -518,7 +534,9 @@ export class InventoryHelper
} }
catch (err) 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)); this.logger.error(this.localisationService.getText("inventory-fill_container_failed", errorText));
return this.httpResponse.appendErrorToOutput( return this.httpResponse.appendErrorToOutput(
@ -527,8 +545,9 @@ export class InventoryHelper
); );
} }
// Store details for object, incuding container item will be placed in // Store details for object, incuding container item will be placed in
itemWithChildren[0].parentId = playerInventory.stash; rootItem.parentId = playerInventory.stash;
itemWithChildren[0].location = { rootItem.slotId = "hideout";
rootItem.location = {
x: findSlotResult.x, x: findSlotResult.x,
y: findSlotResult.y, y: findSlotResult.y,
r: findSlotResult.rotation ? 1 : 0, r: findSlotResult.rotation ? 1 : 0,
@ -774,7 +793,8 @@ export class InventoryHelper
protected splitStackIntoSmallerStacks(assortItems: Item[], requestItem: AddItem, result: IAddItemTempObject[]): void protected splitStackIntoSmallerStacks(assortItems: Item[], requestItem: AddItem, result: IAddItemTempObject[]): void
{ {
for (const item of assortItems) for (const item of assortItems)
{// Iterated item matches root item {
// Iterated item matches root item
if (item._id === requestItem.item_id) if (item._id === requestItem.item_id)
{ {
// Get item details from db // 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"]; 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 namespace ItemHelper

View File

@ -323,7 +323,7 @@ export class QuestHelper
items.push(this.jsonUtil.clone(mod)); items.push(this.jsonUtil.clone(mod));
} }
rewardItems = rewardItems.concat(this.ragfairServerHelper.reparentPresets(target, items)); rewardItems = rewardItems.concat(this.itemHelper.reparentItemAndChildren(target, items));
} }
return rewardItems; return rewardItems;

View File

@ -272,7 +272,7 @@ export class RagfairServerHelper
public getPresetItems(item: Item): Item[] public getPresetItems(item: Item): Item[]
{ {
const preset = this.jsonUtil.clone(this.databaseServer.getTables().globals.ItemPresets[item._id]._items); 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( const presetItems = this.jsonUtil.clone(
this.databaseServer.getTables().globals.ItemPresets[itemId]._items, this.databaseServer.getTables().globals.ItemPresets[itemId]._items,
); );
presets.push(this.reparentPresets(item, presetItems)); presets.push(this.itemHelper.reparentItemAndChildren(item, presetItems));
} }
} }
return presets; 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 { IProcessSellTradeRequestData } from "@spt-aki/models/eft/trade/IProcessSellTradeRequestData";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { Traders } from "@spt-aki/models/enums/Traders"; 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 { ITraderConfig } from "@spt-aki/models/spt/config/ITraderConfig";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder"; import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder";
@ -25,6 +26,7 @@ import { JsonUtil } from "@spt-aki/utils/JsonUtil";
export class TradeHelper export class TradeHelper
{ {
protected traderConfig: ITraderConfig; protected traderConfig: ITraderConfig;
protected inventoryConfig: IInventoryConfig;
constructor( constructor(
@inject("WinstonLogger") protected logger: ILogger, @inject("WinstonLogger") protected logger: ILogger,
@ -41,6 +43,7 @@ export class TradeHelper
) )
{ {
this.traderConfig = this.configServer.getConfig(ConfigTypes.TRADER); 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") if (buyRequestData.tid.toLocaleLowerCase() === "ragfair")
{ {
// Get raw offer from ragfair, clone to prevent altering offer itself
const allOffers = this.ragfairServer.getOffers(); 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 = { const request: IAddItemDirectRequest = {
itemWithModsToAdd: offerWithItem.items, itemWithModsToAdd: this.itemHelper.reparentItemAndChildren(offerItems[0], offerItems),
foundInRaid: true, foundInRaid: this.inventoryConfig.newItemsMarkedFound,
callback: callback, callback: callback,
useSortingTable: true 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 // TODO - handle traders