0
0
mirror of https://github.com/sp-tarkov/server.git synced 2025-02-13 08:30:44 -05:00

Fix flea selling issues (!374)

Co-authored-by: Dev <dev@dev.sp-tarkov.com>
Reviewed-on: SPT/Server#374
This commit is contained in:
chomp 2024-07-15 18:24:23 +00:00
parent 77da49bb9e
commit 4fd113d00d
5 changed files with 349 additions and 93 deletions

View File

@ -414,8 +414,6 @@ export class RagfairController
{ {
const output = this.eventOutputHolder.getOutput(sessionID); const output = this.eventOutputHolder.getOutput(sessionID);
const fullProfile = this.saveServer.getProfile(sessionID); const fullProfile = this.saveServer.getProfile(sessionID);
const sellAsPack = offerRequest.sellInOnePiece; // a group of items that much be all purchased at once
const itemsToListCount = offerRequest.items.length; // Count of root items being sold (no children)
const validationMessage = ""; const validationMessage = "";
if (!this.isValidPlayerOfferRequest(offerRequest, validationMessage)) if (!this.isValidPlayerOfferRequest(offerRequest, validationMessage))
@ -429,60 +427,70 @@ export class RagfairController
return this.httpResponse.appendErrorToOutput(output, "Unknown offer type, cannot list item on flea"); return this.httpResponse.appendErrorToOutput(output, "Unknown offer type, cannot list item on flea");
} }
switch (typeOfOffer)
{
case FleaOfferType.SINGLE:
return this.createSingleOffer(sessionID, offerRequest, fullProfile, output);
case FleaOfferType.MULTI:
return this.createMultiOffer(sessionID, offerRequest, fullProfile, output);
case FleaOfferType.PACK:
return this.createPackOffer(sessionID, offerRequest, fullProfile, output);
}
}
protected createSingleOffer(
sessionID: string,
offerRequest: IAddOfferRequestData,
fullProfile: ISptProfile,
output: IItemEventRouterResponse): IItemEventRouterResponse
{
const pmcData = fullProfile.characters.pmc;
const itemsToListCount = offerRequest.items.length; // Does not count stack size, only items
// Find items to be listed on flea from player inventory // Find items to be listed on flea from player inventory
const { items: itemsInInventoryToList, errorMessage: itemsInInventoryError } const { items: itemsAndChildrenInInventoryToList, errorMessage: itemsInInventoryError }
= this.getItemsToListOnFleaFromInventory(pmcData, offerRequest.items); = this.getItemsToListOnFleaFromInventory(pmcData, offerRequest.items);
if (!itemsInInventoryToList || itemsInInventoryError) if (!itemsAndChildrenInInventoryToList || itemsInInventoryError)
{ {
this.httpResponse.appendErrorToOutput(output, itemsInInventoryError); this.httpResponse.appendErrorToOutput(output, itemsInInventoryError);
} }
// Total count of items summed using their stack counts
const stackCountTotal = this.ragfairOfferHelper.getTotalStackCountSize(itemsAndChildrenInInventoryToList);
// Checks are done, create the offer // Checks are done, create the offer
const playerListedPriceInRub = this.calculateRequirementsPriceInRub(offerRequest.requirements); const playerListedPriceInRub = this.calculateRequirementsPriceInRub(offerRequest.requirements);
const offer = this.createPlayerOffer( const offer = this.createPlayerOffer(
sessionID, sessionID,
offerRequest.requirements, offerRequest.requirements,
this.ragfairHelper.mergeStackable(itemsInInventoryToList), itemsAndChildrenInInventoryToList[0],
sellAsPack, false,
); );
const rootItem = offer.items[0]; const rootItem = offer.items[0];
// Get average of items quality+children // Get average of items quality+children
const qualityMultiplier = this.itemHelper.getItemQualityModifierForItems(offer.items, true); const qualityMultiplier = this.itemHelper.getItemQualityModifierForItems(offer.items, true);
let averageOfferPrice = this.ragfairPriceService.getFleaPriceForOfferItems(offer.items);
// Average offer price for single item (or whole weapon)
let averageOfferPriceSingleItem = this.ragfairPriceService.getFleaPriceForOfferItems(offer.items);
// Check for and apply item price modifer if it exists in config // Check for and apply item price modifer if it exists in config
const itemPriceModifer = this.ragfairConfig.dynamic.itemPriceMultiplier[rootItem._tpl]; const itemPriceModifer = this.ragfairConfig.dynamic.itemPriceMultiplier[rootItem._tpl];
if (itemPriceModifer) if (itemPriceModifer)
{ {
averageOfferPrice *= itemPriceModifer; averageOfferPriceSingleItem *= itemPriceModifer;
} }
// Multiply single item price by quality // Multiply single item price by quality
averageOfferPrice *= qualityMultiplier; averageOfferPriceSingleItem *= qualityMultiplier;
// Define packs as a single count item
const itemStackCount = sellAsPack
? 1
: itemsToListCount;
// Average out price of offer
const averageSingleItemPrice = sellAsPack
? averageOfferPrice / itemsToListCount // Packs contains multiple items sold as one
: averageOfferPrice / itemStackCount; // Normal offer, single items can be purchased from listing
// Get averaged price of player listing to use when calculating sell chance
const averagePlayerListedPriceInRub = sellAsPack
? playerListedPriceInRub / itemsToListCount
: playerListedPriceInRub;
// Packs are reduced to the average price of a single item in the pack vs the averaged single price of an item // Packs are reduced to the average price of a single item in the pack vs the averaged single price of an item
const sellChancePercent = this.ragfairSellHelper.calculateSellChance( const sellChancePercent = this.ragfairSellHelper.calculateSellChance(
averageSingleItemPrice, averageOfferPriceSingleItem,
averagePlayerListedPriceInRub, playerListedPriceInRub,
qualityMultiplier, qualityMultiplier,
); );
offer.sellResult = this.ragfairSellHelper.rollForSale(sellChancePercent, itemStackCount); offer.sellResult = this.ragfairSellHelper.rollForSale(sellChancePercent, itemsToListCount);
// Subtract flea market fee from stash // Subtract flea market fee from stash
if (this.ragfairConfig.sell.fees) if (this.ragfairConfig.sell.fees)
@ -492,7 +500,217 @@ export class RagfairController
rootItem, rootItem,
pmcData, pmcData,
playerListedPriceInRub, playerListedPriceInRub,
itemStackCount, stackCountTotal,
offerRequest,
output,
);
if (taxFeeChargeFailed)
{
return output;
}
}
// Add offer to players profile + add to client response
fullProfile.characters.pmc.RagfairInfo.offers.push(offer);
output.profileChanges[sessionID].ragFairOffers.push(offer);
// Remove items from inventory after creating offer
for (const itemToRemove of offerRequest.items)
{
this.inventoryHelper.removeItem(pmcData, itemToRemove, sessionID, output);
}
return output;
}
protected createMultiOffer(
sessionID: string,
offerRequest: IAddOfferRequestData,
fullProfile: ISptProfile,
output: IItemEventRouterResponse): IItemEventRouterResponse
{
const pmcData = fullProfile.characters.pmc;
const itemsToListCount = offerRequest.items.length; // Does not count stack size, only items
// multi-offers are all the same item,
// Get first item and its children and use as template
const firstListingAndChidren = this.itemHelper.findAndReturnChildrenAsItems(
pmcData.Inventory.items,
offerRequest.items[0]);
// Find items to be listed on flea (+ children) from player inventory
const { items: itemsAndChildrenInInventoryToList, errorMessage: itemsInInventoryError }
= this.getItemsToListOnFleaFromInventory(pmcData, offerRequest.items);
if (!itemsAndChildrenInInventoryToList || itemsInInventoryError)
{
this.httpResponse.appendErrorToOutput(output, itemsInInventoryError);
}
// Total count of items summed using their stack counts
const stackCountTotal = this.ragfairOfferHelper.getTotalStackCountSize(itemsAndChildrenInInventoryToList);
// When listing identical items on flea, condense separate items into one stack with a merged stack count
// e.g. 2 ammo items, stackObjectCount = 3 for each, will result in 1 stack of 6
if (!firstListingAndChidren[0].upd)
{
firstListingAndChidren[0].upd = {};
}
firstListingAndChidren[0].upd.StackObjectsCount = stackCountTotal;
// Create flea object
const offer = this.createPlayerOffer(
sessionID,
offerRequest.requirements,
firstListingAndChidren,
false,
);
// This is the item that will be listed on flea, has merged stackObjectCount
const newRootOfferItem = offer.items[0];
// Average offer price for single item (or whole weapon)
let averageOfferPrice = this.ragfairPriceService.getFleaPriceForOfferItems(offer.items);
// Check for and apply item price modifer if it exists in config
const itemPriceModifer = this.ragfairConfig.dynamic.itemPriceMultiplier[newRootOfferItem._tpl];
if (itemPriceModifer)
{
averageOfferPrice *= itemPriceModifer;
}
// Get average of item+children quality
const qualityMultiplier = this.itemHelper.getItemQualityModifierForItems(offer.items, true);
// Multiply single item price by quality
averageOfferPrice *= qualityMultiplier;
// Get price player listed items for in roubles
const playerListedPriceInRub = this.calculateRequirementsPriceInRub(offerRequest.requirements);
// Roll sale chance
const sellChancePercent = this.ragfairSellHelper.calculateSellChance(
averageOfferPrice,
playerListedPriceInRub,
qualityMultiplier,
);
// Create array of sell times for items listed
offer.sellResult = this.ragfairSellHelper.rollForSale(sellChancePercent, itemsToListCount);
// Subtract flea market fee from stash
if (this.ragfairConfig.sell.fees)
{
const taxFeeChargeFailed = this.chargePlayerTaxFee(
sessionID,
newRootOfferItem,
pmcData,
playerListedPriceInRub,
stackCountTotal,
offerRequest,
output,
);
if (taxFeeChargeFailed)
{
return output;
}
}
// Add offer to players profile + add to client response
fullProfile.characters.pmc.RagfairInfo.offers.push(offer);
output.profileChanges[sessionID].ragFairOffers.push(offer);
// Remove items from inventory after creating offer
for (const itemToRemove of offerRequest.items)
{
this.inventoryHelper.removeItem(pmcData, itemToRemove, sessionID, output);
}
return output;
}
protected createPackOffer(
sessionID: string,
offerRequest: IAddOfferRequestData,
fullProfile: ISptProfile,
output: IItemEventRouterResponse): IItemEventRouterResponse
{
const pmcData = fullProfile.characters.pmc;
const itemsToListCount = offerRequest.items.length; // Does not count stack size, only items
// multi-offers are all the same item,
// Get first item and its children and use as template
const firstListingAndChidren = this.itemHelper.findAndReturnChildrenAsItems(
pmcData.Inventory.items,
offerRequest.items[0]);
// Find items to be listed on flea (+ children) from player inventory
const { items: itemsAndChildrenInInventoryToList, errorMessage: itemsInInventoryError }
= this.getItemsToListOnFleaFromInventory(pmcData, offerRequest.items);
if (!itemsAndChildrenInInventoryToList || itemsInInventoryError)
{
this.httpResponse.appendErrorToOutput(output, itemsInInventoryError);
}
// Total count of items summed using their stack counts
const stackCountTotal = this.ragfairOfferHelper.getTotalStackCountSize(itemsAndChildrenInInventoryToList);
// When listing identical items on flea, condense separate items into one stack with a merged stack count
// e.g. 2 ammo items, stackObjectCount = 3 for each, will result in 1 stack of 6
if (!firstListingAndChidren[0].upd)
{
firstListingAndChidren[0].upd = {};
}
firstListingAndChidren[0].upd.StackObjectsCount = stackCountTotal;
// Create flea object
const offer = this.createPlayerOffer(
sessionID,
offerRequest.requirements,
firstListingAndChidren,
true,
);
// This is the item that will be listed on flea, has merged stackObjectCount
const newRootOfferItem = offer.items[0];
// Single price for an item
let singleItemPrice = this.ragfairPriceService.getFleaPriceForItem(firstListingAndChidren[0]._tpl);
// Check for and apply item price modifer if it exists in config
const itemPriceModifer = this.ragfairConfig.dynamic.itemPriceMultiplier[newRootOfferItem._tpl];
if (itemPriceModifer)
{
singleItemPrice *= itemPriceModifer;
}
// Get average of item+children quality
const qualityMultiplier = this.itemHelper.getItemQualityModifierForItems(offer.items, true);
// Multiply single item price by quality
singleItemPrice *= qualityMultiplier;
// Get price player listed items for in roubles
const playerListedPriceInRub = this.calculateRequirementsPriceInRub(offerRequest.requirements);
// Roll sale chance
const sellChancePercent = this.ragfairSellHelper.calculateSellChance(
singleItemPrice * stackCountTotal,
playerListedPriceInRub,
qualityMultiplier,
);
// Create array of sell times for items listed + sell all at once as its a pack
offer.sellResult = this.ragfairSellHelper.rollForSale(sellChancePercent, itemsToListCount, true);
// Subtract flea market fee from stash
if (this.ragfairConfig.sell.fees)
{
const taxFeeChargeFailed = this.chargePlayerTaxFee(
sessionID,
newRootOfferItem,
pmcData,
playerListedPriceInRub,
stackCountTotal,
offerRequest, offerRequest,
output, output,
); );
@ -539,7 +757,7 @@ export class RagfairController
* @param rootItem Base item being listed (used when client tax cost not found and must be done on server) * @param rootItem Base item being listed (used when client tax cost not found and must be done on server)
* @param pmcData Player profile * @param pmcData Player profile
* @param requirementsPriceInRub Rouble cost player chose for listing (used when client tax cost not found and must be done on server) * @param requirementsPriceInRub Rouble cost player chose for listing (used when client tax cost not found and must be done on server)
* @param itemStackCount How many items were listed in player (used when client tax cost not found and must be done on server) * @param itemStackCount How many items were listed by player (used when client tax cost not found and must be done on server)
* @param offerRequest Add offer request object from client * @param offerRequest Add offer request object from client
* @param output IItemEventRouterResponse * @param output IItemEventRouterResponse
* @returns True if charging tax to player failed * @returns True if charging tax to player failed
@ -645,9 +863,9 @@ export class RagfairController
protected getItemsToListOnFleaFromInventory( protected getItemsToListOnFleaFromInventory(
pmcData: IPmcData, pmcData: IPmcData,
itemIdsFromFleaOfferRequest: string[], itemIdsFromFleaOfferRequest: string[],
): { items: Item[] | undefined, errorMessage: string | undefined } ): { items: Item[][] | undefined, errorMessage: string | undefined }
{ {
const itemsToReturn = []; const itemsToReturn: Item[][] = [];
let errorMessage: string | undefined = undefined; let errorMessage: string | undefined = undefined;
// Count how many items are being sold and multiply the requested amount accordingly // Count how many items are being sold and multiply the requested amount accordingly
@ -665,7 +883,7 @@ export class RagfairController
} }
item = this.itemHelper.fixItemStackCount(item); item = this.itemHelper.fixItemStackCount(item);
itemsToReturn.push(...this.itemHelper.findAndReturnChildrenAsItems(pmcData.Inventory.items, itemId)); itemsToReturn.push(this.itemHelper.findAndReturnChildrenAsItems(pmcData.Inventory.items, itemId));
} }
if (!itemsToReturn?.length) if (!itemsToReturn?.length)
@ -689,12 +907,7 @@ export class RagfairController
const loyalLevel = 1; const loyalLevel = 1;
const formattedItems: Item[] = items.map((item) => const formattedItems: Item[] = items.map((item) =>
{ {
const isChild = items.some((it) => it._id === item.parentId); const isChild = items.some((subItem) => subItem._id === item.parentId);
if (!isChild && !sellInOnePiece)
{
// Ensure offer with multiple of an item has its stack count reset
item.upd.StackObjectsCount = 1;
}
return { return {
_id: item._id, _id: item._id,

View File

@ -147,7 +147,7 @@ export class RagfairHelper
} }
/** /**
* Merges Root Items * Iterate over array of identical items and merge stack count
* Ragfair allows abnormally large stacks. * Ragfair allows abnormally large stacks.
*/ */
public mergeStackable(items: Item[]): Item[] public mergeStackable(items: Item[]): Item[]

View File

@ -333,7 +333,8 @@ export class RagfairOfferHelper
for (const offer of profileOffers.values()) for (const offer of profileOffers.values())
{ {
if (offer.sellResult && offer.sellResult.length > 0 && timestamp >= offer.sellResult[0].sellTime) if (offer.sellResult?.length > 0
&& timestamp >= offer.sellResult[0].sellTime)
{ {
// Item sold // Item sold
let totalItemsCount = 1; let totalItemsCount = 1;
@ -341,7 +342,8 @@ export class RagfairOfferHelper
if (!offer.sellInOnePiece) if (!offer.sellInOnePiece)
{ {
totalItemsCount = offer.items.reduce((sum: number, item) => sum + item.upd.StackObjectsCount, 0); // offer.items.reduce((sum, item) => sum + item.upd?.StackObjectsCount ?? 0, 0);
totalItemsCount = this.getTotalStackCountSize([offer.items]);
boughtAmount = offer.sellResult[0].amount; boughtAmount = offer.sellResult[0].amount;
} }
@ -358,6 +360,28 @@ export class RagfairOfferHelper
return true; return true;
} }
/**
* Count up all rootitem StackObjectsCount properties of an array of items
* @param itemsInInventoryToList items to sum up
* @returns Total count
*/
public getTotalStackCountSize(itemsInInventoryToList: Item[][]): number
{
let total = 0;
for (const itemAndChildren of itemsInInventoryToList)
{
for (const item of itemAndChildren)
{
if (item.slotId === "hideout")
{
total += item.upd?.StackObjectsCount ?? 1;
}
}
}
return total;
}
/** /**
* Add amount to players ragfair rating * Add amount to players ragfair rating
* @param sessionId Profile to update * @param sessionId Profile to update
@ -422,64 +446,23 @@ export class RagfairOfferHelper
protected completeOffer(sessionID: string, offer: IRagfairOffer, boughtAmount: number): IItemEventRouterResponse protected completeOffer(sessionID: string, offer: IRagfairOffer, boughtAmount: number): IItemEventRouterResponse
{ {
const itemTpl = offer.items[0]._tpl; const itemTpl = offer.items[0]._tpl;
let itemsToSend = []; let paymentItemsToSendToPlayer: Item[] = [];
const offerStackCount = offer.items[0].upd.StackObjectsCount; const offerStackCount = offer.items[0].upd.StackObjectsCount;
// Pack or ALL items of a multi-offer were bought - remove entire ofer
if (offer.sellInOnePiece || boughtAmount === offerStackCount) if (offer.sellInOnePiece || boughtAmount === offerStackCount)
{ {
this.deleteOfferById(sessionID, offer._id); this.deleteOfferById(sessionID, offer._id);
} }
else else
{ {
offer.items[0].upd.StackObjectsCount -= boughtAmount; const offerRootItem = offer.items[0];
const rootItems = offer.items.filter((i) => i.parentId === "hideout");
rootItems.splice(0, 1);
let removeCount = boughtAmount; // Reduce offer root items stack count
let idsToRemove: string[] = []; offerRootItem.upd.StackObjectsCount -= boughtAmount;
while (removeCount > 0 && rootItems.length > 0)
{
const lastItem = rootItems[rootItems.length - 1];
if (lastItem.upd.StackObjectsCount > removeCount)
{
lastItem.upd.StackObjectsCount -= removeCount;
removeCount = 0;
}
else
{
removeCount -= lastItem.upd.StackObjectsCount;
idsToRemove.push(lastItem._id);
rootItems.splice(rootItems.length - 1, 1);
}
} }
let foundNewItems = true; // Assemble payment to send to seller now offer was purchased
while (foundNewItems)
{
foundNewItems = false;
for (const id of idsToRemove)
{
const newIds = offer.items
.filter((i) => !idsToRemove.includes(i._id) && idsToRemove.includes(i.parentId))
.map((i) => i._id);
if (newIds.length > 0)
{
foundNewItems = true;
idsToRemove = [...idsToRemove, ...newIds];
}
}
}
if (idsToRemove.length > 0)
{
offer.items = offer.items.filter((i) => !idsToRemove.includes(i._id));
}
}
// Assemble the payment item(s)
for (const requirement of offer.requirements) for (const requirement of offer.requirements)
{ {
// Create an item template item // Create an item template item
@ -504,7 +487,7 @@ export class RagfairOfferHelper
} }
} }
itemsToSend = [...itemsToSend, ...outItems]; paymentItemsToSendToPlayer = [...paymentItemsToSendToPlayer, ...outItems];
} }
} }
@ -519,7 +502,7 @@ export class RagfairOfferHelper
this.traderHelper.getTraderById(Traders.RAGMAN), this.traderHelper.getTraderById(Traders.RAGMAN),
MessageType.FLEAMARKET_MESSAGE, MessageType.FLEAMARKET_MESSAGE,
this.getLocalisedOfferSoldMessage(itemTpl, boughtAmount), this.getLocalisedOfferSoldMessage(itemTpl, boughtAmount),
itemsToSend, paymentItemsToSendToPlayer,
this.timeUtil.getHoursAsSeconds( this.timeUtil.getHoursAsSeconds(
this.questHelper.getMailItemRedeemTimeHoursForProfile(this.profileHelper.getPmcProfile(sessionID))), this.questHelper.getMailItemRedeemTimeHoursForProfile(this.profileHelper.getPmcProfile(sessionID))),
undefined, undefined,

View File

@ -65,9 +65,10 @@ export class RagfairSellHelper
* Get array of item count and sell time (empty array = no sell) * Get array of item count and sell time (empty array = no sell)
* @param sellChancePercent chance item will sell * @param sellChancePercent chance item will sell
* @param itemSellCount count of items to sell * @param itemSellCount count of items to sell
* @param sellInOneGo All items listed get sold at once
* @returns Array of purchases of item(s) listed * @returns Array of purchases of item(s) listed
*/ */
public rollForSale(sellChancePercent: number, itemSellCount: number): SellResult[] public rollForSale(sellChancePercent: number, itemSellCount: number, sellInOneGo = false): SellResult[]
{ {
const startTime = this.timeUtil.getTimestamp(); const startTime = this.timeUtil.getTimestamp();
@ -103,7 +104,9 @@ export class RagfairSellHelper
while (remainingCount > 0 && sellTime < endTime) while (remainingCount > 0 && sellTime < endTime)
{ {
const boughtAmount = this.randomUtil.getInt(1, remainingCount); const boughtAmount = (sellInOneGo)
? remainingCount
: this.randomUtil.getInt(1, remainingCount);
if (this.randomUtil.getChance100(effectiveSellChance)) if (this.randomUtil.getChance100(effectiveSellChance))
{ {
// Passed roll check, item will be sold // Passed roll check, item will be sold

View File

@ -1,4 +1,5 @@
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper"; import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { RagfairServerHelper } from "@spt/helpers/RagfairServerHelper"; import { RagfairServerHelper } from "@spt/helpers/RagfairServerHelper";
import { Item } from "@spt/models/eft/common/tables/IItem"; import { Item } from "@spt/models/eft/common/tables/IItem";
@ -11,6 +12,7 @@ import { ConfigServer } from "@spt/servers/ConfigServer";
import { SaveServer } from "@spt/servers/SaveServer"; import { SaveServer } from "@spt/servers/SaveServer";
import { DatabaseService } from "@spt/services/DatabaseService"; import { DatabaseService } from "@spt/services/DatabaseService";
import { LocalisationService } from "@spt/services/LocalisationService"; import { LocalisationService } from "@spt/services/LocalisationService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil"; import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { RagfairOfferHolder } from "@spt/utils/RagfairOfferHolder"; import { RagfairOfferHolder } from "@spt/utils/RagfairOfferHolder";
import { TimeUtil } from "@spt/utils/TimeUtil"; import { TimeUtil } from "@spt/utils/TimeUtil";
@ -31,11 +33,13 @@ export class RagfairOfferService
@inject("DatabaseService") protected databaseService: DatabaseService, @inject("DatabaseService") protected databaseService: DatabaseService,
@inject("SaveServer") protected saveServer: SaveServer, @inject("SaveServer") protected saveServer: SaveServer,
@inject("RagfairServerHelper") protected ragfairServerHelper: RagfairServerHelper, @inject("RagfairServerHelper") protected ragfairServerHelper: RagfairServerHelper,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("ProfileHelper") protected profileHelper: ProfileHelper, @inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder, @inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil, @inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer, @inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
) )
{ {
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR); this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
@ -276,7 +280,60 @@ export class RagfairOfferService
this.ragfairOfferHandler.removeOffer(playerOffer); this.ragfairOfferHandler.removeOffer(playerOffer);
// Send failed offer items to player in mail // Send failed offer items to player in mail
this.ragfairServerHelper.returnItems(profile.sessionId, playerOffer.items); const unstackedItems = this.unstackOfferItems(playerOffer.items);
this.ragfairServerHelper.returnItems(profile.sessionId, unstackedItems);
profile.RagfairInfo.offers.splice(offerinProfileIndex, 1); profile.RagfairInfo.offers.splice(offerinProfileIndex, 1);
} }
/**
* Flea offer items are stacked up often beyond the StackMaxSize limit
* Un stack the items into an array of root items and their children
* Will create new items equal to the
* @param items Offer items to unstack
* @returns Unstacked array of items
*/
protected unstackOfferItems(items: Item[]): Item[]
{
const result: Item[] = [];
const rootItem = items[0];
const itemDetails = this.itemHelper.getItem(rootItem._tpl);
const itemMaxStackSize = itemDetails[1]._props.StackMaxSize ?? 1;
const totalItemCount = rootItem.upd?.StackObjectsCount ?? 1;
// Items within stack tolerance, return existing data - no changes needed
if (totalItemCount <= itemMaxStackSize)
{
return items;
}
// Single item with no children e.g. ammo, use existing de-stacking code
if (items.length === 1)
{
return this.itemHelper.splitStack(rootItem);
}
// Item with children, needs special handling
// Force new item to have stack size of 1
for (let index = 0; index < totalItemCount; index++)
{
const itemAndChildrenClone = this.cloner.clone(items);
// Ensure upd object exits
itemAndChildrenClone[0].upd ||= {};
// Force item to be singular
itemAndChildrenClone[0].upd.StackObjectsCount = 1;
// Ensure items IDs are unique to prevent collisions when added to player inventory
const reparentedItemAndChildren = this.itemHelper.reparentItemAndChildren(
itemAndChildrenClone[0],
itemAndChildrenClone);
this.itemHelper.remapRootItemId(reparentedItemAndChildren);
result.push(...reparentedItemAndChildren);
}
return result;
}
} }