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, string 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, string 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 (_ragfairOfferHelper.OfferIsFromTrader(fleaOffer)) { 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( string 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( string 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 (IsPlayerOffer(fleaOffer.Id, fleaOffer.User?.Id)) { // 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); } /// /// Is the provided offerId and ownerId from a player made offer /// /// id of the offer /// Owner id /// true if offer was made by a player protected bool IsPlayerOffer(string offerId, string? offerOwnerId) { // No ownerId, not player offer if (offerOwnerId is null) { return false; } var offerCreatorProfile = _profileHelper.GetPmcProfile(offerOwnerId); if (offerCreatorProfile is null || offerCreatorProfile.RagfairInfo.Offers?.Count == 0) // No profile or no offers { return false; } // Does offer id exist in profile return offerCreatorProfile.RagfairInfo.Offers.Any(offer => offer.Id == offerId); } /// /// 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, string 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(string sessionId, int roublesToSend, string 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 curencyReward = _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 : new List() ), curencyReward.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( string parentItemId, List items, Dictionary handbookPrices, TraderBase traderDetails ) { var itemWithChildren = items.FindAndReturnChildrenAsItems(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; } }