Server/project/src/controllers/RagfairController.ts

783 lines
31 KiB
TypeScript
Raw Normal View History

2023-03-03 15:23:46 +00:00
import { inject, injectable } from "tsyringe";
import { RagfairOfferGenerator } from "@spt-aki/generators/RagfairOfferGenerator";
import { HandbookHelper } from "@spt-aki/helpers/HandbookHelper";
import { InventoryHelper } from "@spt-aki/helpers/InventoryHelper";
import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { PaymentHelper } from "@spt-aki/helpers/PaymentHelper";
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
import { RagfairHelper } from "@spt-aki/helpers/RagfairHelper";
import { RagfairOfferHelper } from "@spt-aki/helpers/RagfairOfferHelper";
import { RagfairSellHelper } from "@spt-aki/helpers/RagfairSellHelper";
import { RagfairSortHelper } from "@spt-aki/helpers/RagfairSortHelper";
import { TraderHelper } from "@spt-aki/helpers/TraderHelper";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { IBarterScheme, ITraderAssort } from "@spt-aki/models/eft/common/tables/ITrader";
import { IItemEventRouterResponse } from "@spt-aki/models/eft/itemEvent/IItemEventRouterResponse";
import { IAkiProfile } from "@spt-aki/models/eft/profile/IAkiProfile";
import { IAddOfferRequestData, Requirement } from "@spt-aki/models/eft/ragfair/IAddOfferRequestData";
import { IExtendOfferRequestData } from "@spt-aki/models/eft/ragfair/IExtendOfferRequestData";
import { IGetItemPriceResult } from "@spt-aki/models/eft/ragfair/IGetItemPriceResult";
import { IGetMarketPriceRequestData } from "@spt-aki/models/eft/ragfair/IGetMarketPriceRequestData";
import { IGetOffersResult } from "@spt-aki/models/eft/ragfair/IGetOffersResult";
import { IGetRagfairOfferByIdRequest } from "@spt-aki/models/eft/ragfair/IGetRagfairOfferByIdRequest";
import { IRagfairOffer } from "@spt-aki/models/eft/ragfair/IRagfairOffer";
import { IRemoveOfferRequestData } from "@spt-aki/models/eft/ragfair/IRemoveOfferRequestData";
import { ISearchRequestData } from "@spt-aki/models/eft/ragfair/ISearchRequestData";
import { IProcessBuyTradeRequestData } from "@spt-aki/models/eft/trade/IProcessBuyTradeRequestData";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { MemberCategory } from "@spt-aki/models/enums/MemberCategory";
import { RagfairSort } from "@spt-aki/models/enums/RagfairSort";
import { IRagfairConfig } from "@spt-aki/models/spt/config/IRagfairConfig";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder";
import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { RagfairServer } from "@spt-aki/servers/RagfairServer";
import { SaveServer } from "@spt-aki/servers/SaveServer";
import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { PaymentService } from "@spt-aki/services/PaymentService";
import { RagfairOfferService } from "@spt-aki/services/RagfairOfferService";
import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService";
import { RagfairRequiredItemsService } from "@spt-aki/services/RagfairRequiredItemsService";
import { RagfairTaxService } from "@spt-aki/services/RagfairTaxService";
import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil";
import { TimeUtil } from "@spt-aki/utils/TimeUtil";
2023-03-03 15:23:46 +00:00
/**
* Handle RagfairCallback events
*/
@injectable()
export class RagfairController
{
protected ragfairConfig: IRagfairConfig;
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
@inject("RagfairServer") protected ragfairServer: RagfairServer,
@inject("RagfairPriceService") protected ragfairPriceService: RagfairPriceService,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("SaveServer") protected saveServer: SaveServer,
@inject("RagfairSellHelper") protected ragfairSellHelper: RagfairSellHelper,
@inject("RagfairTaxService") protected ragfairTaxService: RagfairTaxService,
2023-03-03 15:23:46 +00:00
@inject("RagfairSortHelper") protected ragfairSortHelper: RagfairSortHelper,
@inject("RagfairOfferHelper") protected ragfairOfferHelper: RagfairOfferHelper,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("PaymentService") protected paymentService: PaymentService,
@inject("HandbookHelper") protected handbookHelper: HandbookHelper,
@inject("PaymentHelper") protected paymentHelper: PaymentHelper,
@inject("InventoryHelper") protected inventoryHelper: InventoryHelper,
@inject("TraderHelper") protected traderHelper: TraderHelper,
@inject("RagfairHelper") protected ragfairHelper: RagfairHelper,
@inject("RagfairOfferService") protected ragfairOfferService: RagfairOfferService,
@inject("RagfairRequiredItemsService") protected ragfairRequiredItemsService: RagfairRequiredItemsService,
@inject("RagfairOfferGenerator") protected ragfairOfferGenerator: RagfairOfferGenerator,
@inject("LocalisationService") protected localisationService: LocalisationService,
2023-11-10 16:49:29 -05:00
@inject("ConfigServer") protected configServer: ConfigServer,
2023-03-03 15:23:46 +00:00
)
{
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
}
public getOffers(sessionID: string, searchRequest: ISearchRequestData): IGetOffersResult
{
const profile = this.profileHelper.getFullProfile(sessionID);
2023-03-03 15:23:46 +00:00
const itemsToAdd = this.ragfairHelper.filterCategories(sessionID, searchRequest);
const traderAssorts = this.ragfairHelper.getDisplayableAssorts(sessionID);
const result: IGetOffersResult = {
offers: [],
offersCount: searchRequest.limit,
2023-11-10 16:49:29 -05:00
selectedCategory: searchRequest.handbookId,
2023-03-03 15:23:46 +00:00
};
result.offers = this.getOffersForSearchType(searchRequest, itemsToAdd, traderAssorts, profile.characters.pmc);
// Client requested a category refresh
if (searchRequest.updateOfferCount)
{
result.categories = this.getSpecificCategories(profile.characters.pmc, searchRequest, result.offers);
}
2023-03-03 15:23:46 +00:00
this.addIndexValueToOffers(result.offers);
// Sort offers
2023-11-10 16:49:29 -05:00
result.offers = this.ragfairSortHelper.sortOffers(
result.offers,
searchRequest.sortType,
searchRequest.sortDirection,
);
2023-03-03 15:23:46 +00:00
// Match offers with quests and lock unfinished quests
for (const offer of result.offers)
{
if (offer.user.memberType === MemberCategory.TRADER)
{
2023-11-10 16:49:29 -05:00
// for the items, check the barter schemes. The method getDisplayableAssorts sets a flag sptQuestLocked
// to true if the quest is not completed yet
2023-03-03 15:23:46 +00:00
if (this.ragfairOfferHelper.traderOfferItemQuestLocked(offer, traderAssorts))
{
offer.locked = true;
}
2023-11-10 16:49:29 -05:00
2023-03-03 15:23:46 +00:00
// Update offers BuyRestrictionCurrent/BuyRestrictionMax values
this.setTraderOfferPurchaseLimits(offer, profile);
this.setTraderOfferStackSize(offer);
}
}
result.offersCount = result.offers.length;
// Handle paging before returning results only if searching for general items, not preset items
2023-11-10 16:49:29 -05:00
if (searchRequest.buildCount === 0)
{
const start = searchRequest.page * searchRequest.limit;
2023-11-10 16:49:29 -05:00
const end = Math.min((searchRequest.page + 1) * searchRequest.limit, result.offers.length);
result.offers = result.offers.slice(start, end);
}
2023-03-03 15:23:46 +00:00
return result;
}
/**
* Handle client/ragfair/offer/findbyid
* @param sessionId Player id
* @param request Request data
* @returns IRagfairOffer
*/
public getOfferById(sessionId: string, request: IGetRagfairOfferByIdRequest): IRagfairOffer
{
const offers = this.ragfairOfferService.getOffers();
const offerToReturn = offers.find((x) => x.intId === request.id);
return offerToReturn;
}
2023-03-03 15:23:46 +00:00
/**
* Get offers for the client based on type of search being performed
* @param searchRequest Client search request data
* @param itemsToAdd comes from ragfairHelper.filterCategories()
2023-03-03 15:23:46 +00:00
* @param traderAssorts Trader assorts
* @param pmcProfile Player profile
* @returns array of offers
*/
2023-11-10 16:49:29 -05:00
protected getOffersForSearchType(
searchRequest: ISearchRequestData,
itemsToAdd: string[],
traderAssorts: Record<string, ITraderAssort>,
pmcProfile: IPmcData,
): IRagfairOffer[]
2023-03-03 15:23:46 +00:00
{
// Searching for items in preset menu
if (searchRequest.buildCount)
{
return this.ragfairOfferHelper.getOffersForBuild(searchRequest, itemsToAdd, traderAssorts, pmcProfile);
}
2024-02-02 19:23:08 +00:00
if (searchRequest.neededSearchId?.length > 0)
{
return this.ragfairOfferHelper.getOffersThatRequireItem(searchRequest, pmcProfile);
2024-02-02 19:23:08 +00:00
}
2023-03-03 15:23:46 +00:00
// Searching for general items
return this.ragfairOfferHelper.getValidOffers(searchRequest, itemsToAdd, traderAssorts, pmcProfile);
}
/**
* Get categories for the type of search being performed, linked/required/all
* @param searchRequest Client search request data
* @param offers ragfair offers to get categories for
2023-11-10 16:49:29 -05:00
* @returns record with templates + counts
2023-03-03 15:23:46 +00:00
*/
protected getSpecificCategories(
pmcProfile: IPmcData,
searchRequest: ISearchRequestData,
offers: IRagfairOffer[],
): Record<string, number>
2023-03-03 15:23:46 +00:00
{
// Linked/required search categories
const playerHasFleaUnlocked =
pmcProfile.Info.Level >= this.databaseServer.getTables().globals.config.RagFair.minUserLevel;
let offerPool = [];
2023-03-03 15:23:46 +00:00
if (this.isLinkedSearch(searchRequest) || this.isRequiredSearch(searchRequest))
{
offerPool = offers;
2023-03-03 15:23:46 +00:00
}
else if ((!(this.isLinkedSearch(searchRequest) || this.isRequiredSearch(searchRequest))))
{
// Get all categories
offerPool = this.ragfairOfferService.getOffers();
}
else
2023-03-03 15:23:46 +00:00
{
this.logger.error("Unable to get categories from search criteria, see log for request data");
this.logger.debug(JSON.stringify(searchRequest));
return {};
2023-03-03 15:23:46 +00:00
}
return this.ragfairServer.getAllActiveCategories(playerHasFleaUnlocked, searchRequest, offerPool);
2023-03-03 15:23:46 +00:00
}
/**
* Add index to all offers passed in (0-indexed)
* @param offers Offers to add index value to
*/
protected addIndexValueToOffers(offers: IRagfairOffer[]): void
{
let counter = 0;
for (const offer of offers)
{
offer.intId = ++counter;
2023-11-10 16:49:29 -05:00
offer.items[0].parentId = ""; // Without this it causes error: "Item deserialization error: No parent with id hideout found for item x"
2023-03-03 15:23:46 +00:00
}
}
/**
* Update a trader flea offer with buy restrictions stored in the traders assort
* @param offer flea offer to update
* @param fullProfile Players full profile
2023-03-03 15:23:46 +00:00
*/
protected setTraderOfferPurchaseLimits(offer: IRagfairOffer, fullProfile: IAkiProfile): void
2023-03-03 15:23:46 +00:00
{
// pre 3.6.x profiles lack this object, create it
if (!fullProfile.traderPurchases)
2023-03-03 15:23:46 +00:00
{
fullProfile.traderPurchases = {};
2023-03-03 15:23:46 +00:00
}
// No trader found, create a blank record for them
if (!fullProfile.traderPurchases[offer.user.id])
2023-03-03 15:23:46 +00:00
{
fullProfile.traderPurchases[offer.user.id] = {};
2023-03-03 15:23:46 +00:00
}
const traderAssorts = this.traderHelper.getTraderAssortsByTraderId(offer.user.id).items;
const assortId = offer.items[0]._id;
2023-11-10 16:49:29 -05:00
const assortData = traderAssorts.find((x) => x._id === assortId);
2023-03-03 15:23:46 +00:00
// Use value stored in profile, otherwise use value directly from in-memory trader assort data
offer.buyRestrictionCurrent = fullProfile.traderPurchases[offer.user.id][assortId]
? fullProfile.traderPurchases[offer.user.id][assortId].count
: assortData.upd.BuyRestrictionCurrent;
2023-03-03 15:23:46 +00:00
offer.buyRestrictionMax = assortData.upd.BuyRestrictionMax;
}
/**
* Adjust ragfair offer stack count to match same value as traders assort stack count
* @param offer Flea offer to adjust
*/
protected setTraderOfferStackSize(offer: IRagfairOffer): void
{
const firstItem = offer.items[0];
const traderAssorts = this.traderHelper.getTraderAssortsByTraderId(offer.user.id).items;
2023-03-03 15:23:46 +00:00
2023-11-10 16:49:29 -05:00
const assortPurchased = traderAssorts.find((x) => x._id === offer.items[0]._id);
2023-03-03 15:23:46 +00:00
if (!assortPurchased)
{
2023-11-10 16:49:29 -05:00
this.logger.warning(
this.localisationService.getText("ragfair-unable_to_adjust_stack_count_assort_not_found", {
offerId: offer.items[0]._id,
traderId: offer.user.id,
}),
);
2023-07-19 13:16:45 +01:00
2023-03-03 15:23:46 +00:00
return;
}
firstItem.upd.StackObjectsCount = assortPurchased.upd.StackObjectsCount;
}
protected isLinkedSearch(info: ISearchRequestData): boolean
{
return info.linkedSearchId !== "";
}
protected isRequiredSearch(info: ISearchRequestData): boolean
{
return info.neededSearchId !== "";
}
/**
* Check all profiles and sell player offers / send player money for listing if it sold
*/
2023-03-03 15:23:46 +00:00
public update(): void
{
for (const sessionID in this.saveServer.getProfiles())
{
if (this.saveServer.getProfile(sessionID).characters.pmc.RagfairInfo !== undefined)
{
this.ragfairOfferHelper.processOffersOnProfile(sessionID);
}
}
}
/**
* Called when creating an offer on flea, fills values in top right corner
2023-11-10 16:49:29 -05:00
* @param getPriceRequest
2023-03-03 15:23:46 +00:00
* @returns min/avg/max values for an item based on flea offers available
*/
public getItemMinAvgMaxFleaPriceValues(getPriceRequest: IGetMarketPriceRequestData): IGetItemPriceResult
{
// Get all items of tpl (sort by price)
let offers = this.ragfairOfferService.getOffersOfType(getPriceRequest.templateId);
// Offers exist for item, get averages of what's listed
2023-11-10 16:49:29 -05:00
if (typeof offers === "object" && offers.length > 0)
2023-03-03 15:23:46 +00:00
{
offers = this.ragfairSortHelper.sortOffers(offers, RagfairSort.PRICE);
const min = offers[0].requirementsCost; // Get first item from array as its pre-sorted
const max = offers.at(-1).requirementsCost; // Get last item from array as its pre-sorted
return { avg: (min + max) / 2, min: min, max: max };
2023-03-03 15:23:46 +00:00
}
// No offers listed, get price from live ragfair price list prices.json
const templatesDb = this.databaseServer.getTables().templates;
2023-03-03 15:23:46 +00:00
let tplPrice = templatesDb.prices[getPriceRequest.templateId];
if (!tplPrice)
{
// No flea price, get handbook price
tplPrice = this.handbookHelper.getTemplatePrice(getPriceRequest.templateId);
2023-03-03 15:23:46 +00:00
}
return { avg: tplPrice, min: tplPrice, max: tplPrice };
2023-03-03 15:23:46 +00:00
}
/**
* List item(s) on flea for sale
* @param pmcData Player profile
2023-07-25 14:04:21 +01:00
* @param offerRequest Flea list creation offer
* @param sessionID Session id
* @returns IItemEventRouterResponse
*/
2023-11-10 16:49:29 -05:00
public addPlayerOffer(
pmcData: IPmcData,
offerRequest: IAddOfferRequestData,
sessionID: string,
): IItemEventRouterResponse
2023-03-03 15:23:46 +00:00
{
const output = this.eventOutputHolder.getOutput(sessionID);
2023-03-03 15:23:46 +00:00
const validationMessage = "";
if (!this.isValidPlayerOfferRequest(offerRequest, validationMessage))
2023-03-03 15:23:46 +00:00
{
return this.httpResponse.appendErrorToOutput(output, validationMessage);
2023-03-03 15:23:46 +00:00
}
// Get an array of items from player inventory to list on flea
const { items: itemsInInventoryToList, errorMessage: itemsInInventoryError } = this
.getItemsToListOnFleaFromInventory(pmcData, offerRequest.items);
if (!itemsInInventoryToList || itemsInInventoryError)
2023-03-03 15:23:46 +00:00
{
this.httpResponse.appendErrorToOutput(output, itemsInInventoryError);
2023-03-03 15:23:46 +00:00
}
// Checks are done, create the offer
const playerListedPriceInRub = this.calculateRequirementsPriceInRub(offerRequest.requirements);
const fullProfile = this.saveServer.getProfile(sessionID);
2023-11-10 16:49:29 -05:00
const offer = this.createPlayerOffer(
fullProfile,
offerRequest.requirements,
this.ragfairHelper.mergeStackable(itemsInInventoryToList),
offerRequest.sellInOnePiece,
playerListedPriceInRub,
);
2023-03-03 15:23:46 +00:00
const rootItem = offer.items[0];
// Get average of items quality+children
const qualityMultiplier = this.itemHelper.getItemQualityModifierForOfferItems(offer.items);
let averageOfferPrice = this.ragfairPriceService.getFleaPriceForOfferItems(offer.items);
// Check for and apply item price modifer if it exists
const itemPriceModifer = this.ragfairConfig.dynamic.itemPriceMultiplier[rootItem._tpl];
if (itemPriceModifer)
{
averageOfferPrice *= itemPriceModifer;
}
// Multiply single item price by stack count and quality
averageOfferPrice *= rootItem.upd.StackObjectsCount * qualityMultiplier;
const itemStackCount = (offerRequest.sellInOnePiece) ? 1 : rootItem.upd.StackObjectsCount;
// Get averaged price of a single item being listed
const averageSingleItemPrice = (offerRequest.sellInOnePiece)
? averageOfferPrice / rootItem.upd.StackObjectsCount // Packs are a single offer made of many items
: averageOfferPrice / itemStackCount;
2023-11-10 16:49:29 -05:00
// Get averaged price of listing
const averagePlayerListedPriceInRub = (offerRequest.sellInOnePiece)
? playerListedPriceInRub / rootItem.upd.StackObjectsCount
: playerListedPriceInRub;
// Packs are reduced to the average price of a single item in the pack vs the averaged single price of an item
2023-11-10 16:49:29 -05:00
const sellChancePercent = this.ragfairSellHelper.calculateSellChance(
averageSingleItemPrice,
averagePlayerListedPriceInRub,
qualityMultiplier,
);
offer.sellResult = this.ragfairSellHelper.rollForSale(sellChancePercent, itemStackCount);
2023-03-03 15:23:46 +00:00
// Subtract flea market fee from stash
if (this.ragfairConfig.sell.fees)
{
2023-11-10 16:49:29 -05:00
const taxFeeChargeFailed = this.chargePlayerTaxFee(
sessionID,
rootItem,
pmcData,
playerListedPriceInRub,
itemStackCount,
offerRequest,
output,
);
if (taxFeeChargeFailed)
2023-03-03 15:23:46 +00:00
{
return output;
2023-03-03 15:23:46 +00:00
}
}
fullProfile.characters.pmc.RagfairInfo.offers.push(offer);
2023-03-03 15:23:46 +00:00
output.profileChanges[sessionID].ragFairOffers.push(offer);
// Remove items from inventory after creating offer
for (const itemToRemove of offerRequest.items)
2023-03-03 15:23:46 +00:00
{
this.inventoryHelper.removeItem(pmcData, itemToRemove, sessionID, output);
}
return output;
}
/**
* Charge player a listing fee for using flea, pulls charge from data previously sent by client
* @param sessionID Player id
* @param rootItem Base item being listed (used when client tax cost not found and must be done on server)
* @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 itemStackCount How many items were listed in player (used when client tax cost not found and must be done on server)
* @param offerRequest Add offer request object from client
* @param output IItemEventRouterResponse
* @returns True if charging tax to player failed
*/
2023-11-10 16:49:29 -05:00
protected chargePlayerTaxFee(
sessionID: string,
rootItem: Item,
pmcData: IPmcData,
requirementsPriceInRub: number,
itemStackCount: number,
offerRequest: IAddOfferRequestData,
output: IItemEventRouterResponse,
): boolean
{
// Get tax from cache hydrated earlier by client, if that's missing fall back to server calculation (inaccurate)
const storedClientTaxValue = this.ragfairTaxService.getStoredClientOfferTaxValueById(offerRequest.items[0]);
const tax = storedClientTaxValue
? storedClientTaxValue.fee
: this.ragfairTaxService.calculateTax(
2023-11-10 16:49:29 -05:00
rootItem,
pmcData,
requirementsPriceInRub,
itemStackCount,
offerRequest.sellInOnePiece,
);
this.logger.debug(`Offer tax to charge: ${tax}, pulled from client: ${(!!storedClientTaxValue)}`);
2023-11-10 16:49:29 -05:00
// cleanup of cache now we've used the tax value from it
this.ragfairTaxService.clearStoredOfferTaxById(offerRequest.items[0]);
const buyTradeRequest = this.createBuyTradeRequestObject("RUB", tax);
output = this.paymentService.payMoney(pmcData, buyTradeRequest, sessionID, output);
if (output.warnings.length > 0)
{
2023-11-10 16:49:29 -05:00
output = this.httpResponse.appendErrorToOutput(
output,
this.localisationService.getText("ragfair-unable_to_pay_commission_fee", tax),
);
return true;
}
return false;
}
/**
* Is the item to be listed on the flea valid
* @param offerRequest Client offer request
* @param errorMessage message to show to player when offer is invalid
* @returns Is offer valid
*/
protected isValidPlayerOfferRequest(offerRequest: IAddOfferRequestData, errorMessage: string): boolean
{
if (!offerRequest?.items || offerRequest.items.length === 0)
{
this.logger.error(this.localisationService.getText("ragfair-invalid_player_offer_request"));
return false;
}
2023-11-10 16:49:29 -05:00
if (!offerRequest.requirements)
{
this.logger.error(this.localisationService.getText("ragfair-unable_to_place_offer_with_no_requirements"));
return false;
}
2023-11-10 16:49:29 -05:00
return true;
}
/**
* Get the handbook price in roubles for the items being listed
2023-11-10 16:49:29 -05:00
* @param requirements
* @returns Rouble price
*/
protected calculateRequirementsPriceInRub(requirements: Requirement[]): number
{
let requirementsPriceInRub = 0;
for (const item of requirements)
{
const requestedItemTpl = item._tpl;
2023-11-10 16:49:29 -05:00
if (this.paymentHelper.isMoneyTpl(requestedItemTpl))
{
requirementsPriceInRub += this.handbookHelper.inRUB(item.count, requestedItemTpl);
}
else
{
requirementsPriceInRub += this.ragfairPriceService.getDynamicPriceForItem(requestedItemTpl)
* item.count;
}
}
2023-11-10 16:49:29 -05:00
return requirementsPriceInRub;
}
/**
2023-11-10 16:49:29 -05:00
* Using item ids from flea offer request, find corresponding items from player inventory and return as array
* @param pmcData Player profile
* @param itemIdsFromFleaOfferRequest Ids from request
* @returns Array of items from player inventory
*/
2023-11-10 16:49:29 -05:00
protected getItemsToListOnFleaFromInventory(
pmcData: IPmcData,
itemIdsFromFleaOfferRequest: string[],
): { items: Item[] | null; errorMessage: string | null; }
{
const itemsToReturn = [];
let errorMessage: string | null = null;
// Count how many items are being sold and multiply the requested amount accordingly
for (const itemId of itemIdsFromFleaOfferRequest)
{
2023-11-10 16:49:29 -05:00
let item = pmcData.Inventory.items.find((i) => i._id === itemId);
if (!item)
{
2023-11-10 16:49:29 -05:00
errorMessage = this.localisationService.getText("ragfair-unable_to_find_item_in_inventory", {
id: itemId,
});
this.logger.error(errorMessage);
return { items: null, errorMessage };
}
item = this.itemHelper.fixItemStackCount(item);
itemsToReturn.push(...this.itemHelper.findAndReturnChildrenAsItems(pmcData.Inventory.items, itemId));
}
if (!itemsToReturn?.length)
{
errorMessage = this.localisationService.getText("ragfair-unable_to_find_requested_items_in_inventory");
this.logger.error(errorMessage);
return { items: null, errorMessage };
}
return { items: itemsToReturn, errorMessage };
}
2023-11-10 16:49:29 -05:00
public createPlayerOffer(
profile: IAkiProfile,
requirements: Requirement[],
items: Item[],
sellInOnePiece: boolean,
amountToSend: number,
): IRagfairOffer
2023-03-03 15:23:46 +00:00
{
const loyalLevel = 1;
2023-11-10 16:49:29 -05:00
const formattedItems: Item[] = items.map((item) =>
2023-03-03 15:23:46 +00:00
{
2023-11-10 16:49:29 -05:00
const isChild = items.find((it) => it._id === item.parentId);
2023-03-03 15:23:46 +00:00
return {
_id: item._id,
_tpl: item._tpl,
2023-11-10 16:49:29 -05:00
parentId: isChild ? item.parentId : "hideout",
slotId: isChild ? item.slotId : "hideout",
upd: item.upd,
2023-03-03 15:23:46 +00:00
};
});
2023-11-10 16:49:29 -05:00
const formattedRequirements: IBarterScheme[] = requirements.map((item) =>
2023-03-03 15:23:46 +00:00
{
return { _tpl: item._tpl, count: item.count, onlyFunctional: item.onlyFunctional };
2023-03-03 15:23:46 +00:00
});
return this.ragfairOfferGenerator.createFleaOffer(
profile.characters.pmc.sessionId,
2023-03-03 15:23:46 +00:00
this.timeUtil.getTimestamp(),
formattedItems,
formattedRequirements,
loyalLevel,
2023-11-10 16:49:29 -05:00
sellInOnePiece,
2023-03-03 15:23:46 +00:00
);
}
public getAllFleaPrices(): Record<string, number>
{
return this.ragfairPriceService.getAllFleaPrices();
}
public getStaticPrices(): Record<string, number>
{
return this.ragfairPriceService.getAllStaticPrices();
}
/**
* User requested removal of the offer, actually reduces the time to 71 seconds,
* allowing for the possibility of extending the auction before it's end time
* @param removeRequest Remove offer request
* @param sessionId Players id
* @returns IItemEventRouterResponse
2023-03-03 15:23:46 +00:00
*/
public removeOffer(removeRequest: IRemoveOfferRequestData, sessionId: string): IItemEventRouterResponse
2023-03-03 15:23:46 +00:00
{
const output = this.eventOutputHolder.getOutput(sessionId);
const pmcData = this.saveServer.getProfile(sessionId).characters.pmc;
const playerProfileOffers = pmcData.RagfairInfo.offers;
if (!playerProfileOffers)
2023-03-03 15:23:46 +00:00
{
2023-11-10 16:49:29 -05:00
this.logger.warning(
this.localisationService.getText("ragfair-unable_to_remove_offer_not_found_in_profile", {
profileId: sessionId,
offerId: removeRequest.offerId,
2023-11-10 16:49:29 -05:00
}),
);
2023-07-19 13:16:45 +01:00
pmcData.RagfairInfo.offers = [];
2023-03-03 15:23:46 +00:00
}
const playerOfferIndex = playerProfileOffers.findIndex((offer) => offer._id === removeRequest.offerId);
if (playerOfferIndex === -1)
2023-03-03 15:23:46 +00:00
{
2023-11-10 16:49:29 -05:00
this.logger.error(
this.localisationService.getText("ragfair-offer_not_found_in_profile", {
offerId: removeRequest.offerId,
}),
2023-11-10 16:49:29 -05:00
);
return this.httpResponse.appendErrorToOutput(
output,
2023-11-10 16:49:29 -05:00
this.localisationService.getText("ragfair-offer_not_found_in_profile_short"),
);
2023-03-03 15:23:46 +00:00
}
const differenceInSeconds = playerProfileOffers[playerOfferIndex].endTime - this.timeUtil.getTimestamp();
2023-11-10 16:49:29 -05:00
if (differenceInSeconds > this.ragfairConfig.sell.expireSeconds)
2023-03-03 15:23:46 +00:00
{
// `expireSeconds` Default is 71 seconds
const newEndTime = this.ragfairConfig.sell.expireSeconds + this.timeUtil.getTimestamp();
playerProfileOffers[playerOfferIndex].endTime = Math.round(newEndTime);
2023-03-03 15:23:46 +00:00
}
return output;
2023-03-03 15:23:46 +00:00
}
/**
* Extend a ragfair offers listing time
* @param extendRequest Extend offer request
* @param sessionId Players id
* @returns IItemEventRouterResponse
*/
public extendOffer(extendRequest: IExtendOfferRequestData, sessionId: string): IItemEventRouterResponse
2023-03-03 15:23:46 +00:00
{
const output = this.eventOutputHolder.getOutput(sessionId);
const pmcData = this.saveServer.getProfile(sessionId).characters.pmc;
const playerOffers = pmcData.RagfairInfo.offers;
const playerOfferIndex = playerOffers.findIndex((offer) => offer._id === extendRequest.offerId);
const secondsToAdd = extendRequest.renewalTime * TimeUtil.ONE_HOUR_AS_SECONDS;
2023-03-03 15:23:46 +00:00
if (playerOfferIndex === -1)
2023-03-03 15:23:46 +00:00
{
2023-11-10 16:49:29 -05:00
this.logger.warning(
this.localisationService.getText("ragfair-offer_not_found_in_profile", {
offerId: extendRequest.offerId,
}),
2023-11-10 16:49:29 -05:00
);
return this.httpResponse.appendErrorToOutput(
output,
2023-11-10 16:49:29 -05:00
this.localisationService.getText("ragfair-offer_not_found_in_profile_short"),
);
2023-03-03 15:23:46 +00:00
}
// MOD: Pay flea market fee
if (this.ragfairConfig.sell.fees)
{
const count = playerOffers[playerOfferIndex].sellInOnePiece
? 1
: playerOffers[playerOfferIndex].items.reduce((sum, item) =>
{
return sum + item.upd.StackObjectsCount;
}, 0);
2023-11-10 16:49:29 -05:00
const tax = this.ragfairTaxService.calculateTax(
playerOffers[playerOfferIndex].items[0],
this.profileHelper.getPmcProfile(sessionId),
playerOffers[playerOfferIndex].requirementsCost,
2023-11-10 16:49:29 -05:00
count,
playerOffers[playerOfferIndex].sellInOnePiece,
2023-11-10 16:49:29 -05:00
);
2023-03-03 15:23:46 +00:00
const request = this.createBuyTradeRequestObject("RUB", tax);
this.paymentService.payMoney(pmcData, request, sessionId, output);
2023-03-03 15:23:46 +00:00
if (output.warnings.length > 0)
{
2023-11-10 16:49:29 -05:00
return this.httpResponse.appendErrorToOutput(
output,
this.localisationService.getText("ragfair-unable_to_pay_commission_fee"),
);
2023-03-03 15:23:46 +00:00
}
}
// Add extra time to offer
playerOffers[playerOfferIndex].endTime += Math.round(secondsToAdd);
2023-03-03 15:23:46 +00:00
return output;
2023-03-03 15:23:46 +00:00
}
/**
* Create a basic trader request object with price and currency type
* @param currency What currency: RUB, EURO, USD
* @param value Amount of currency
* @returns IProcessBuyTradeRequestData
*/
protected createBuyTradeRequestObject(currency: string, value: number): IProcessBuyTradeRequestData
{
return {
tid: "ragfair",
Action: "TradingConfirm",
scheme_items: [{ id: this.paymentHelper.getCurrency(currency), count: Math.round(value) }],
type: "",
item_id: "",
count: 0,
2023-11-10 16:49:29 -05:00
scheme_id: 0,
};
}
2023-11-10 16:49:29 -05:00
}