2023-03-03 15:23:46 +00:00
|
|
|
import { inject, injectable } from "tsyringe";
|
2024-07-15 18:24:23 +00:00
|
|
|
import { ItemHelper } from "@spt/helpers/ItemHelper";
|
2024-05-21 17:59:04 +00:00
|
|
|
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
|
|
|
|
import { RagfairServerHelper } from "@spt/helpers/RagfairServerHelper";
|
|
|
|
import { Item } from "@spt/models/eft/common/tables/IItem";
|
|
|
|
import { IRagfairOffer } from "@spt/models/eft/ragfair/IRagfairOffer";
|
|
|
|
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
|
|
|
|
import { IRagfairConfig } from "@spt/models/spt/config/IRagfairConfig";
|
|
|
|
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
|
|
|
import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
|
|
|
|
import { ConfigServer } from "@spt/servers/ConfigServer";
|
|
|
|
import { SaveServer } from "@spt/servers/SaveServer";
|
2024-05-29 15:15:45 +01:00
|
|
|
import { DatabaseService } from "@spt/services/DatabaseService";
|
2024-05-21 17:59:04 +00:00
|
|
|
import { LocalisationService } from "@spt/services/LocalisationService";
|
2024-07-15 18:24:23 +00:00
|
|
|
import { ICloner } from "@spt/utils/cloners/ICloner";
|
2024-05-21 17:59:04 +00:00
|
|
|
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
|
|
|
|
import { RagfairOfferHolder } from "@spt/utils/RagfairOfferHolder";
|
|
|
|
import { TimeUtil } from "@spt/utils/TimeUtil";
|
2023-03-03 15:23:46 +00:00
|
|
|
|
|
|
|
@injectable()
|
|
|
|
export class RagfairOfferService
|
|
|
|
{
|
|
|
|
protected playerOffersLoaded = false;
|
2024-01-23 14:17:01 +00:00
|
|
|
/** Offer id + offer object */
|
2023-03-03 15:23:46 +00:00
|
|
|
protected expiredOffers: Record<string, IRagfairOffer> = {};
|
|
|
|
|
|
|
|
protected ragfairConfig: IRagfairConfig;
|
2024-05-02 08:56:40 +00:00
|
|
|
protected ragfairOfferHandler: RagfairOfferHolder;
|
2023-03-03 15:23:46 +00:00
|
|
|
|
|
|
|
constructor(
|
2024-05-28 14:04:20 +00:00
|
|
|
@inject("PrimaryLogger") protected logger: ILogger,
|
2023-03-03 15:23:46 +00:00
|
|
|
@inject("TimeUtil") protected timeUtil: TimeUtil,
|
2024-05-29 15:15:45 +01:00
|
|
|
@inject("DatabaseService") protected databaseService: DatabaseService,
|
2023-03-03 15:23:46 +00:00
|
|
|
@inject("SaveServer") protected saveServer: SaveServer,
|
|
|
|
@inject("RagfairServerHelper") protected ragfairServerHelper: RagfairServerHelper,
|
2024-07-15 18:24:23 +00:00
|
|
|
@inject("ItemHelper") protected itemHelper: ItemHelper,
|
2023-03-03 15:23:46 +00:00
|
|
|
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
|
|
|
|
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
|
|
|
|
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
|
|
|
|
@inject("LocalisationService") protected localisationService: LocalisationService,
|
2023-11-13 11:13:25 -05:00
|
|
|
@inject("ConfigServer") protected configServer: ConfigServer,
|
2024-07-15 18:24:23 +00:00
|
|
|
@inject("PrimaryCloner") protected cloner: ICloner,
|
2023-03-03 15:23:46 +00:00
|
|
|
)
|
|
|
|
{
|
|
|
|
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
|
2024-05-02 08:56:40 +00:00
|
|
|
this.ragfairOfferHandler = new RagfairOfferHolder(
|
|
|
|
this.ragfairConfig.dynamic.offerItemCount.max,
|
|
|
|
ragfairServerHelper,
|
2024-05-27 20:06:07 +00:00
|
|
|
profileHelper,
|
2024-05-02 08:56:40 +00:00
|
|
|
);
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all offers
|
|
|
|
* @returns IRagfairOffer array
|
|
|
|
*/
|
|
|
|
public getOffers(): IRagfairOffer[]
|
|
|
|
{
|
|
|
|
return this.ragfairOfferHandler.getOffers();
|
|
|
|
}
|
|
|
|
|
2024-05-27 16:05:16 +00:00
|
|
|
public getOfferByOfferId(offerId: string): IRagfairOffer | undefined
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
return this.ragfairOfferHandler.getOfferById(offerId);
|
|
|
|
}
|
|
|
|
|
2024-05-27 16:05:16 +00:00
|
|
|
public getOffersOfType(templateId: string): IRagfairOffer[] | undefined
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
return this.ragfairOfferHandler.getOffersByTemplate(templateId);
|
|
|
|
}
|
|
|
|
|
|
|
|
public addOffer(offer: IRagfairOffer): void
|
|
|
|
{
|
|
|
|
this.ragfairOfferHandler.addOffer(offer);
|
|
|
|
}
|
|
|
|
|
|
|
|
public addOfferToExpired(staleOffer: IRagfairOffer): void
|
|
|
|
{
|
|
|
|
this.expiredOffers[staleOffer._id] = staleOffer;
|
|
|
|
}
|
|
|
|
|
2024-01-23 14:17:01 +00:00
|
|
|
/**
|
|
|
|
* Get total count of current expired offers
|
|
|
|
* @returns Number of expired offers
|
|
|
|
*/
|
2023-03-03 15:23:46 +00:00
|
|
|
public getExpiredOfferCount(): number
|
|
|
|
{
|
|
|
|
return Object.keys(this.expiredOffers).length;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-01-23 14:17:01 +00:00
|
|
|
* Get an array of arrays of expired offer items + children
|
|
|
|
* @returns Expired offer assorts
|
2023-03-03 15:23:46 +00:00
|
|
|
*/
|
2024-01-23 14:17:01 +00:00
|
|
|
public getExpiredOfferAssorts(): Item[][]
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2024-01-23 14:17:01 +00:00
|
|
|
const expiredItems: Item[][] = [];
|
2023-03-03 15:23:46 +00:00
|
|
|
|
|
|
|
for (const expiredOfferId in this.expiredOffers)
|
|
|
|
{
|
2024-01-23 14:17:01 +00:00
|
|
|
const expiredOffer = this.expiredOffers[expiredOfferId];
|
|
|
|
expiredItems.push(expiredOffer.items);
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return expiredItems;
|
|
|
|
}
|
|
|
|
|
2024-01-23 14:17:01 +00:00
|
|
|
/**
|
|
|
|
* Clear out internal expiredOffers dictionary of all items
|
|
|
|
*/
|
2023-03-03 15:23:46 +00:00
|
|
|
public resetExpiredOffers(): void
|
|
|
|
{
|
|
|
|
this.expiredOffers = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Does the offer exist on the ragfair
|
|
|
|
* @param offerId offer id to check for
|
|
|
|
* @returns offer exists - true
|
|
|
|
*/
|
|
|
|
public doesOfferExist(offerId: string): boolean
|
|
|
|
{
|
|
|
|
return this.ragfairOfferHandler.getOfferById(offerId) !== undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove an offer from ragfair by offer id
|
|
|
|
* @param offerId Offer id to remove
|
|
|
|
*/
|
2023-11-13 11:13:25 -05:00
|
|
|
public removeOfferById(offerId: string): void
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const offer = this.ragfairOfferHandler.getOfferById(offerId);
|
|
|
|
if (!offer)
|
|
|
|
{
|
2023-11-13 11:13:25 -05:00
|
|
|
this.logger.warning(
|
|
|
|
this.localisationService.getText("ragfair-unable_to_remove_offer_doesnt_exist", offerId),
|
|
|
|
);
|
2023-03-03 15:23:46 +00:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.ragfairOfferHandler.removeOffer(offer);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reduce size of an offer stack by specified amount
|
|
|
|
* @param offerId Offer to adjust stack size of
|
|
|
|
* @param amount How much to deduct from offers stack size
|
|
|
|
*/
|
|
|
|
public removeOfferStack(offerId: string, amount: number): void
|
|
|
|
{
|
|
|
|
const offer = this.ragfairOfferHandler.getOfferById(offerId);
|
2024-05-27 16:05:16 +00:00
|
|
|
if (offer)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2024-05-27 16:05:16 +00:00
|
|
|
offer.items[0].upd!.StackObjectsCount! -= amount;
|
|
|
|
if (offer.items[0].upd!.StackObjectsCount! <= 0)
|
|
|
|
{
|
|
|
|
this.processStaleOffer(offer);
|
|
|
|
}
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public removeAllOffersByTrader(traderId: string): void
|
|
|
|
{
|
2024-01-26 10:49:06 +00:00
|
|
|
this.ragfairOfferHandler.removeAllOffersByTrader(traderId);
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Do the trader offers on flea need to be refreshed
|
|
|
|
* @param traderID Trader to check
|
|
|
|
* @returns true if they do
|
|
|
|
*/
|
|
|
|
public traderOffersNeedRefreshing(traderID: string): boolean
|
|
|
|
{
|
2024-05-29 15:15:45 +01:00
|
|
|
const trader = this.databaseService.getTrader(traderID);
|
2024-05-05 11:50:24 +01:00
|
|
|
if (!trader || !trader.base)
|
|
|
|
{
|
2024-05-21 12:40:16 +01:00
|
|
|
this.logger.error(this.localisationService.getText("ragfair-trader_missing_base_file", traderID));
|
2024-05-05 11:50:24 +01:00
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-03-03 15:23:46 +00:00
|
|
|
// No value, occurs when first run, trader offers need to be added to flea
|
|
|
|
if (typeof trader.base.refreshTraderRagfairOffers !== "boolean")
|
|
|
|
{
|
|
|
|
trader.base.refreshTraderRagfairOffers = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return trader.base.refreshTraderRagfairOffers;
|
|
|
|
}
|
|
|
|
|
|
|
|
public addPlayerOffers(): void
|
|
|
|
{
|
|
|
|
if (!this.playerOffersLoaded)
|
|
|
|
{
|
|
|
|
for (const sessionID in this.saveServer.getProfiles())
|
|
|
|
{
|
|
|
|
const pmcData = this.saveServer.getProfile(sessionID).characters.pmc;
|
2023-11-13 11:13:25 -05:00
|
|
|
|
2023-03-03 15:23:46 +00:00
|
|
|
if (pmcData.RagfairInfo === undefined || pmcData.RagfairInfo.offers === undefined)
|
|
|
|
{
|
|
|
|
// Profile is wiped
|
|
|
|
continue;
|
|
|
|
}
|
2023-11-13 11:13:25 -05:00
|
|
|
|
2023-03-03 15:23:46 +00:00
|
|
|
this.ragfairOfferHandler.addOffers(pmcData.RagfairInfo.offers);
|
|
|
|
}
|
|
|
|
this.playerOffersLoaded = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public expireStaleOffers(): void
|
|
|
|
{
|
|
|
|
const time = this.timeUtil.getTimestamp();
|
2023-10-31 17:46:14 +00:00
|
|
|
for (const staleOffer of this.ragfairOfferHandler.getStaleOffers(time))
|
|
|
|
{
|
|
|
|
this.processStaleOffer(staleOffer);
|
|
|
|
}
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove stale offer from flea
|
|
|
|
* @param staleOffer Stale offer to process
|
|
|
|
*/
|
|
|
|
protected processStaleOffer(staleOffer: IRagfairOffer): void
|
|
|
|
{
|
|
|
|
const staleOfferUserId = staleOffer.user.id;
|
|
|
|
const isTrader = this.ragfairServerHelper.isTrader(staleOfferUserId);
|
2024-05-27 20:06:07 +00:00
|
|
|
const isPlayer = this.profileHelper.isPlayer(staleOfferUserId.replace(/^pmc/, ""));
|
2023-03-03 15:23:46 +00:00
|
|
|
|
|
|
|
// Skip trader offers, managed by RagfairServer.update()
|
|
|
|
if (isTrader)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle dynamic offer
|
|
|
|
if (!(isTrader || isPlayer))
|
|
|
|
{
|
|
|
|
// Dynamic offer
|
|
|
|
this.addOfferToExpired(staleOffer);
|
|
|
|
}
|
|
|
|
|
2023-04-06 16:23:52 +00:00
|
|
|
// Handle player offer - items need returning/XP adjusting. Checking if offer has actually expired or not.
|
|
|
|
if (isPlayer && staleOffer.endTime <= this.timeUtil.getTimestamp())
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
this.returnPlayerOffer(staleOffer);
|
2024-02-27 23:45:47 +00:00
|
|
|
return;
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Remove expired existing offer from global offers
|
|
|
|
this.removeOfferById(staleOffer._id);
|
|
|
|
}
|
|
|
|
|
2024-01-26 10:49:06 +00:00
|
|
|
protected returnPlayerOffer(playerOffer: IRagfairOffer): void
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2024-01-26 10:49:06 +00:00
|
|
|
const pmcId = String(playerOffer.user.id);
|
|
|
|
const profile = this.profileHelper.getProfileByPmcId(pmcId);
|
2024-07-18 09:18:33 +01:00
|
|
|
if (!profile)
|
|
|
|
{
|
|
|
|
this.logger.error(`Unable to return flea offer ${playerOffer._id} as the profile: ${pmcId} could not be found`);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
2023-03-03 15:23:46 +00:00
|
|
|
|
2024-05-17 15:32:41 -04:00
|
|
|
const offerinProfileIndex = profile.RagfairInfo.offers.findIndex((o) => o._id === playerOffer._id);
|
2024-01-26 10:49:06 +00:00
|
|
|
if (offerinProfileIndex === -1)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2024-02-02 13:54:07 -05:00
|
|
|
this.logger.warning(
|
|
|
|
this.localisationService.getText("ragfair-unable_to_find_offer_to_remove", playerOffer._id),
|
|
|
|
);
|
2024-01-26 10:49:06 +00:00
|
|
|
return;
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
|
2023-12-16 15:50:35 +00:00
|
|
|
// Reduce player ragfair rep
|
2024-05-29 15:15:45 +01:00
|
|
|
profile.RagfairInfo.rating -= this.databaseService.getGlobals().config.RagFair.ratingDecreaseCount;
|
2023-11-14 16:09:45 +00:00
|
|
|
profile.RagfairInfo.isRatingGrowing = false;
|
|
|
|
|
2024-01-26 10:49:06 +00:00
|
|
|
const firstOfferItem = playerOffer.items[0];
|
2024-05-27 16:05:16 +00:00
|
|
|
if (firstOfferItem.upd!.StackObjectsCount! > firstOfferItem.upd!.OriginalStackObjectsCount!)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2024-05-27 16:05:16 +00:00
|
|
|
playerOffer.items[0].upd!.StackObjectsCount = firstOfferItem.upd!.OriginalStackObjectsCount;
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
2024-05-27 16:05:16 +00:00
|
|
|
delete playerOffer.items[0].upd!.OriginalStackObjectsCount;
|
2024-01-26 10:49:06 +00:00
|
|
|
// Remove player offer from flea
|
|
|
|
this.ragfairOfferHandler.removeOffer(playerOffer);
|
2023-03-03 15:23:46 +00:00
|
|
|
|
2023-12-16 15:50:35 +00:00
|
|
|
// Send failed offer items to player in mail
|
2024-07-15 18:24:23 +00:00
|
|
|
const unstackedItems = this.unstackOfferItems(playerOffer.items);
|
|
|
|
this.ragfairServerHelper.returnItems(profile.sessionId, unstackedItems);
|
2024-01-26 10:49:06 +00:00
|
|
|
profile.RagfairInfo.offers.splice(offerinProfileIndex, 1);
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
2024-07-15 18:24:23 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
{
|
2024-07-18 09:19:44 +01:00
|
|
|
// Edge case - Ensure items stack count isnt < 1
|
|
|
|
if (items[0]?.upd?.StackObjectsCount < 1)
|
|
|
|
{
|
|
|
|
items[0].upd.StackObjectsCount = 1;
|
|
|
|
}
|
|
|
|
|
2024-07-15 18:24:23 +00:00
|
|
|
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;
|
|
|
|
}
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|