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, ProfileHelper profileHelper, 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 (PlayerLacksTraderLoyaltyLevelToBuyOffer(fleaOffer, pmcData)) { 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); } /// /// Does Player have necessary trader loyalty to purchase flea offer /// /// Flea offer being bought /// Player profile /// True if player can buy offer protected bool PlayerLacksTraderLoyaltyLevelToBuyOffer(RagfairOffer fleaOffer, PmcData pmcData) { return fleaOffer.LoyaltyLevel > pmcData.TradersInfo[fleaOffer.User.Id].LoyaltyLevel; } /// /// 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, List 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; } }