using SPTarkov.DI.Annotations; 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.Customization; using SPTarkov.Server.Core.Models.Eft.Hideout; using SPTarkov.Server.Core.Models.Eft.ItemEvent; using SPTarkov.Server.Core.Models.Eft.Trade; using SPTarkov.Server.Core.Models.Enums; 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.Cloners; namespace SPTarkov.Server.Core.Controllers; [Injectable] public class CustomizationController( ISptLogger logger, EventOutputHolder eventOutputHolder, DatabaseService databaseService, SaveServer saveServer, ServerLocalisationService serverLocalisationService, ProfileHelper profileHelper, ICloner cloner, PaymentService paymentService ) { /// /// Get purchasable clothing items from trader that match players side (usec/bear) /// /// trader to look up clothing for /// Session id /// Suit array public List GetTraderSuits(MongoId traderId, MongoId sessionId) { var pmcData = profileHelper.GetPmcProfile(sessionId); var clothing = databaseService.GetCustomization(); var suits = databaseService.GetTrader(traderId).Suits; var matchingSuits = suits?.Where(s => clothing.ContainsKey(s.SuiteId)); matchingSuits = matchingSuits?.Where(s => clothing[s.SuiteId]?.Properties?.Side?.Contains(pmcData?.Info?.Side ?? string.Empty) ?? false ); if (matchingSuits == null) { throw new Exception( serverLocalisationService.GetText( "customisation-unable_to_get_trader_suits", traderId ) ); } return matchingSuits.ToList(); } /// /// Handle CustomizationBuy event /// Purchase/unlock a clothing item from a trader /// /// Player profile /// Request object /// Session id /// ItemEventRouterResponse public ItemEventRouterResponse BuyCustomisation( PmcData pmcData, BuyClothingRequestData buyClothingRequest, MongoId sessionId ) { var output = eventOutputHolder.GetOutput(sessionId); var traderOffer = GetTraderClothingOffer(sessionId, buyClothingRequest.Offer); if (traderOffer is null) { logger.Error( serverLocalisationService.GetText( "customisation-unable_to_find_suit_by_id", buyClothingRequest.Offer ) ); return output; } var suitId = traderOffer.SuiteId; if (OutfitAlreadyPurchased(traderOffer.SuiteId, sessionId)) { var suitDetails = databaseService.GetCustomization().GetValueOrDefault(suitId); logger.Error( serverLocalisationService.GetText( "customisation-item_already_purchased", new { itemId = suitDetails?.Id, itemName = suitDetails?.Name } ) ); return output; } // Charge player for buying item PayForClothingItems(sessionId, pmcData, buyClothingRequest.Items, output); var profile = saveServer.GetProfile(sessionId); // TODO: Merge with function _profileHelper.addHideoutCustomisationUnlock var rewardToStore = new CustomisationStorage { Id = suitId, Source = CustomisationSource.UNLOCKED_IN_GAME, Type = CustomisationType.SUITE, }; profile.CustomisationUnlocks.Add(rewardToStore); return output; } /// /// Has an outfit been purchased by a player /// /// clothing id /// Session id of profile to check for clothing in /// true if already purchased protected bool OutfitAlreadyPurchased(MongoId suitId, MongoId sessionId) { var fullProfile = profileHelper.GetFullProfile(sessionId); // Check if clothing can be found by id return fullProfile.CustomisationUnlocks.Exists(customisation => Equals(customisation.Id, suitId) ); } /// /// Get clothing offer from trader by suit id /// /// Session/Player id /// /// Suit protected Suit? GetTraderClothingOffer(MongoId sessionId, MongoId offerId) { var foundSuit = GetAllTraderSuits(sessionId).FirstOrDefault(s => s.Id == offerId); if (foundSuit is null) { logger.Error( serverLocalisationService.GetText( "customisation-unable_to_find_suit_with_id", offerId ) ); } return foundSuit; } /// /// Update output object and player profile with purchase details /// /// Session id /// Player profile /// Clothing purchased /// Client response protected void PayForClothingItems( MongoId sessionId, PmcData pmcData, List? itemsToPayForClothingWith, ItemEventRouterResponse output ) { if (itemsToPayForClothingWith is null || itemsToPayForClothingWith.Count == 0) { return; } foreach (var inventoryItemToProcess in itemsToPayForClothingWith) { var options = new ProcessBuyTradeRequestData { SchemeItems = [ new IdWithCount { Count = inventoryItemToProcess.Count.Value, Id = inventoryItemToProcess.Id, }, ], TransactionId = Traders.RAGMAN, Action = "BuyCustomization", Type = "", ItemId = "", Count = 0, SchemeId = 0, }; paymentService.PayMoney(pmcData, options, sessionId, output); } } /// /// Get all suits from Traders /// /// Session/Player id /// protected List GetAllTraderSuits(MongoId sessionId) { var traders = databaseService.GetTraders(); var result = new List(); foreach (var (traderId, trader) in traders) { if ( trader.Base?.CustomizationSeller is not null && trader.Base.CustomizationSeller.Value ) { result.AddRange(GetTraderSuits(traderId, sessionId)); } } return result; } /// /// Handle client/hideout/customization/offer/list /// /// Session/Player id /// public HideoutCustomisation GetHideoutCustomisation(MongoId sessionId) { return databaseService.GetHideout().Customisation!; } /// /// Handle client/customization/storage /// /// Session/Player id /// public List GetCustomisationStorage(MongoId sessionId) { var customisationResultsClone = cloner.Clone( databaseService.GetTemplates().CustomisationStorage ); var profile = profileHelper.GetFullProfile(sessionId); if (profile is null) { return customisationResultsClone!; } customisationResultsClone!.AddRange(profile.CustomisationUnlocks ?? []); return customisationResultsClone; } /// /// Handle CustomizationSet event /// /// Session/Player id /// /// Players PMC profile /// ItemEventRouterResponse public ItemEventRouterResponse SetCustomisation( MongoId sessionId, CustomizationSetRequest request, PmcData pmcData ) { foreach (var customisation in request.Customizations) { switch (customisation.Type) { case "dogTag": pmcData.Customization!.DogTag = customisation.Id; break; case "suite": ApplyClothingItemToProfile(customisation, pmcData); break; case "voice": pmcData.Customization.Voice = customisation.Id; break; default: logger.Error($"Unhandled customisation type: {customisation.Type}"); break; } } return eventOutputHolder.GetOutput(sessionId); } /// /// Applies a purchased suit to the players doll /// /// Suit to apply to profile /// Profile to update protected void ApplyClothingItemToProfile(CustomizationSetOption customisation, PmcData pmcData) { if (!databaseService.GetCustomization().TryGetValue(customisation.Id, out var dbSuit)) { logger.Error( $"Unable to find suit customisation id: {customisation.Id}, cannot apply clothing to player profile: {pmcData.Id}" ); return; } // Body if (dbSuit.Parent == CustomisationTypeId.UPPER) { pmcData.Customization.Body = dbSuit.Properties.Body; pmcData.Customization.Hands = dbSuit.Properties.Hands; return; } // Feet if (dbSuit.Parent == CustomisationTypeId.LOWER) { pmcData.Customization.Feet = dbSuit.Properties.Feet; } } }