using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Eft.ItemEvent; using SPTarkov.Server.Core.Models.Eft.Ragfair; using SPTarkov.Server.Core.Models.Eft.Trade; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Routers; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils; using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; namespace SPTarkov.Server.Core.Controllers; [Injectable] public class TradeController( ISptLogger logger, DatabaseService databaseService, EventOutputHolder eventOutputHolder, TradeHelper tradeHelper, TimeUtil timeUtil, RandomUtil randomUtil, ItemHelper itemHelper, RagfairOfferHelper ragfairOfferHelper, RagfairServer ragfairServer, HttpResponseUtil httpResponseUtil, ServerLocalisationService serverLocalisationService, MailSendService mailSendService, ConfigServer configServer ) { protected readonly RagfairConfig RagfairConfig = configServer.GetConfig(); protected readonly TraderConfig TraderConfig = configServer.GetConfig(); /// /// Handle TradingConfirm event /// /// Players PMC profile /// /// Session/Player id /// public ItemEventRouterResponse ConfirmTrading(PmcData pmcData, ProcessBaseTradeRequestData request, MongoId sessionID) { var output = eventOutputHolder.GetOutput(sessionID); // Buying if (request.Type == "buy_from_trader") { var foundInRaid = TraderConfig.PurchasesAreFoundInRaid; var buyData = (ProcessBuyTradeRequestData)request; tradeHelper.BuyItem(pmcData, buyData, sessionID, foundInRaid, output); return output; } // Selling if (request.Type == "sell_to_trader") { var sellData = (ProcessSellTradeRequestData)request; tradeHelper.SellItem(pmcData, pmcData, sellData, sessionID, output); return output; } var errorMessage = $"Unhandled trade event: {request.Type}"; logger.Error(errorMessage); return httpResponseUtil.AppendErrorToOutput(output, errorMessage, BackendErrorCodes.RagfairUnavailable); } /// /// Handle RagFairBuyOffer event /// /// Players PMC profile /// /// Session/Player id /// public ItemEventRouterResponse ConfirmRagfairTrading(PmcData pmcData, ProcessRagfairTradeRequestData request, MongoId sessionID) { var output = eventOutputHolder.GetOutput(sessionID); foreach (var offer in request.Offers) { var fleaOffer = ragfairServer.GetOffer(offer.Id); if (fleaOffer is null) { return httpResponseUtil.AppendErrorToOutput(output, $"Offer with ID {offer.Id} not found", BackendErrorCodes.OfferNotFound); } if (offer.Count == 0) { var errorMessage = serverLocalisationService.GetText( "ragfair-unable_to_purchase_0_count_item", itemHelper.GetItem(fleaOffer.Items[0].Template).Value.Name ); return httpResponseUtil.AppendErrorToOutput(output, errorMessage, BackendErrorCodes.OfferOutOfStock); } if (fleaOffer.IsTraderOffer()) { BuyTraderItemFromRagfair(sessionID, pmcData, fleaOffer, offer, output); } else { BuyPmcItemFromRagfair(sessionID, pmcData, fleaOffer, offer, output); } // Exit loop early if problem found if (output.Warnings?.Count > 0) { return output; } } return output; } /// /// Buy an item off the flea sold by a trader /// /// Session id /// Player profile /// Offer being purchased /// request data from client /// Output to send back to client protected void BuyTraderItemFromRagfair( MongoId sessionId, PmcData pmcData, RagfairOffer fleaOffer, OfferRequest requestOffer, ItemEventRouterResponse output ) { // Skip buying items when player doesn't have needed loyalty if (!pmcData.PlayerMeetsTraderLoyaltyLevelToBuyOffer(fleaOffer)) { var errorMessage = $"Unable to buy item: {fleaOffer.Items[0].Template} from trader: {fleaOffer.User.Id} as loyalty level too low, skipping"; if (logger.IsLogEnabled(LogLevel.Debug)) { logger.Debug(errorMessage); } httpResponseUtil.AppendErrorToOutput(output, errorMessage, BackendErrorCodes.RagfairUnavailable); return; } // Trigger purchase of item from trader var buyData = new ProcessBuyTradeRequestData { Action = "TradingConfirm", Type = "buy_from_ragfair", TransactionId = fleaOffer.User.Id, ItemId = fleaOffer.Root, Count = requestOffer.Count, SchemeId = 0, SchemeItems = requestOffer.Items, }; tradeHelper.BuyItem(pmcData, buyData, sessionId, TraderConfig.PurchasesAreFoundInRaid, output); // Remove/lower offer quantity of item purchased from trader flea offer ragfairServer.ReduceOfferQuantity(fleaOffer.Id, requestOffer.Count ?? 0); } /// /// Buy an item off the flea sold by a PMC /// /// Session id /// Player profile /// Offer being purchased /// request data from client /// Output to send back to client protected void BuyPmcItemFromRagfair( MongoId sessionId, PmcData pmcData, RagfairOffer fleaOffer, OfferRequest requestOffer, ItemEventRouterResponse output ) { var buyData = new ProcessBuyTradeRequestData { Action = "TradingConfirm", Type = "buy_from_ragfair", TransactionId = "ragfair", ItemId = fleaOffer.Id, // Store ragfair offerId in buyRequestData.item_id Count = requestOffer.Count, SchemeId = 0, SchemeItems = requestOffer.Items, }; // buyItem() must occur prior to removing the offer stack, otherwise item inside offer doesn't exist for confirmTrading() to use tradeHelper.BuyItem(pmcData, buyData, sessionId, RagfairConfig.Dynamic.PurchasesAreFoundInRaid, output); if (output.Warnings?.Count > 0) { return; } // resolve when a profile buy another profile's offer var offerOwnerId = fleaOffer.User.Id; var offerBuyCount = requestOffer.Count; if (fleaOffer.IsPlayerOffer()) { // Complete selling the offer now it has been purchased ragfairOfferHelper.CompleteOffer(offerOwnerId, fleaOffer, offerBuyCount ?? 0); return; } // Remove/lower offer quantity of item purchased from PMC flea offer ragfairServer.ReduceOfferQuantity(fleaOffer.Id, requestOffer.Count ?? 0); } /// /// Handle SellAllFromSavage event /// /// Players PMC profile /// /// Session/Player id /// public ItemEventRouterResponse SellScavItemsToFence(PmcData pmcData, SellScavItemsToFenceRequestData request, MongoId sessionId) { var output = eventOutputHolder.GetOutput(sessionId); MailMoneyToPlayer(sessionId, (int)request.TotalValue, Traders.FENCE); return output; } /// /// Send the specified rouble total to player as mail /// /// Session id /// amount of roubles to send /// Trader to sell items to protected void MailMoneyToPlayer(MongoId sessionId, int roublesToSend, MongoId trader) { if (logger.IsLogEnabled(LogLevel.Debug)) { logger.Debug($"Selling scav items to fence for {roublesToSend} roubles"); } // Create single currency item with all currency on it var rootCurrencyReward = new Item { Id = new MongoId(), Template = Money.ROUBLES, Upd = new Upd { StackObjectsCount = roublesToSend }, }; // Ensure money is properly split to follow its max stack size limit var currencyReward = itemHelper.SplitStackIntoSeparateItems(rootCurrencyReward); // Send mail from trader mailSendService.SendLocalisedNpcMessageToPlayer( sessionId, trader, MessageType.MessageWithItems, randomUtil.GetArrayValue(databaseService.GetTrader(trader).Dialogue.TryGetValue("soldItems", out var items) ? items : []), currencyReward.SelectMany(x => x).ToList(), timeUtil.GetHoursAsSeconds(72) ); } /// /// Looks up an items children and gets total handbook price for them /// /// parent item that has children we want to sum price of /// All items (parent + children) /// Prices of items from handbook /// Trader being sold to, to perform buy category check against /// Rouble price protected int GetPriceOfItemAndChildren( MongoId parentItemId, IEnumerable items, Dictionary handbookPrices, TraderBase traderDetails ) { var itemWithChildren = items.GetItemWithChildren(parentItemId); var totalPrice = 0; foreach (var itemToSell in itemWithChildren) { var itemDetails = itemHelper.GetItem(itemToSell.Template); if (!(itemDetails.Key && itemHelper.IsOfBaseclasses(itemDetails.Value.Id, traderDetails.ItemsBuy.Category))) // Skip if tpl isn't item OR item doesn't fulfil match traders buy categories { continue; } // Get price of item multiplied by how many are in stack totalPrice += (int)((handbookPrices[itemToSell.Template] ?? 0) * (itemToSell.Upd?.StackObjectsCount ?? 1)); } return totalPrice; } }