using System.Text.RegularExpressions; 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.Profile; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Utils; namespace SPTarkov.Server.Core.Services; [Injectable(InjectionType.Singleton)] public class PmcChatResponseService( ISptLogger logger, RandomUtil randomUtil, NotificationSendHelper notificationSendHelper, WeightedRandomHelper weightedRandomHelper, ServerLocalisationService serverLocalisationService, GiftService giftService, LocaleService localeService, MatchBotDetailsCacheService matchBotDetailsCacheService, ConfigServer configServer ) { protected readonly PmcChatResponse _pmcResponsesConfig = configServer.GetConfig(); /// /// For each PMC victim of the player, have a chance to send a message to the player, can be positive or negative /// /// Session ID /// List of bots killed by player /// Player profile public void SendVictimResponse(MongoId sessionId, IEnumerable pmcVictims, PmcData pmcData) { foreach (var victim in pmcVictims) { if (!randomUtil.GetChance100(_pmcResponsesConfig.Victim.ResponseChancePercent)) { continue; } if (string.IsNullOrEmpty(victim.Name)) { logger.Warning($"Victim: {victim.ProfileId.ToString()} does not have a nickname, skipping pmc response message send"); continue; } var victimDetails = GetVictimDetails(victim); var message = ChooseMessage(true, pmcData, victim); if (message is not null) { notificationSendHelper.SendMessageToPlayer(sessionId, victimDetails, message, MessageType.UserMessage); } } } /// /// Not fully implemented yet, needs method of acquiring killers details after raid /// /// Session id /// Players profile /// The bot who killed the player public void SendKillerResponse(MongoId sessionId, PmcData pmcData, Aggressor killer) { if (!randomUtil.GetChance100(_pmcResponsesConfig.Killer.ResponseChancePercent)) { return; } // find bot by id in cache var killerDetailsInCache = matchBotDetailsCacheService.GetBotById(killer.ProfileId); if (killerDetailsInCache is null) { return; } // Because we've cached PMC sides as "Savage" for the client, // we need to figure out what side it really is var side = killerDetailsInCache.Side == DogtagSide.Usec ? "Usec" : "Bear"; var killerDetails = new UserDialogInfo { Id = killer.ProfileId.Value, Aid = killerDetailsInCache.Aid, Info = new UserDialogDetails { Nickname = killerDetailsInCache.Nickname, Side = side, Level = killerDetailsInCache.Level, MemberCategory = killerDetailsInCache.Type, }, }; var message = ChooseMessage(false, pmcData); if (message is null) { return; } notificationSendHelper.SendMessageToPlayer(sessionId, killerDetails, message, MessageType.UserMessage); } /// /// Choose a localised message to send the player (different if sender was killed or killed player) /// /// Is the message coming from a bot killed by the player /// Player profile /// OPTIONAL - details of the pmc killed /// Message from PMC to player protected string? ChooseMessage(bool isVictim, PmcData pmcData, Victim? victimData = null) { // Positive/negative etc var responseType = ChooseResponseType(isVictim); // Get all locale keys var possibleResponseLocaleKeys = GetResponseLocaleKeys(responseType, isVictim); if (possibleResponseLocaleKeys.Count == 0) { logger.Warning(serverLocalisationService.GetText("pmcresponse-unable_to_find_key", responseType)); return null; } // Choose random response from above list and request it from localisation service var responseText = serverLocalisationService.GetText( randomUtil.GetArrayValue(possibleResponseLocaleKeys), new { playerName = pmcData.Info.Nickname, playerLevel = pmcData.Info.Level, playerSide = pmcData.Info.Side, victimDeathLocation = victimData is not null ? GetLocationName(victimData.Location) : string.Empty, } ); // Give the player a gift code if they were killed and response is 'pity'. if (responseType == "pity") { var giftKeys = giftService.GetGiftIds(); var randomGiftKey = randomUtil.GetArrayValue(giftKeys); Regex.Replace(responseText, "/(%giftcode%)/gi", randomGiftKey); // TODO: does regex still work } if (AppendSuffixToMessageEnd(isVictim)) { var suffixText = serverLocalisationService.GetText(randomUtil.GetArrayValue(GetResponseSuffixLocaleKeys())); responseText += $" {suffixText}"; } if (StripCapitalisation(isVictim)) { responseText = responseText.ToLowerInvariant(); } if (AllCaps(isVictim)) { responseText = responseText.ToUpper(); } return responseText; } /// /// use map key to get a localised location name /// e.g. factory4_day becomes "Factory" /// /// Location key to localise /// Localised location name protected string GetLocationName(string locationKey) { return localeService.GetLocaleDb().GetValueOrDefault(locationKey, locationKey); } /// /// Should capitalisation be stripped from the message response before sending /// /// Was responder a victim of player /// True = should be stripped protected bool StripCapitalisation(bool isVictim) { var chance = isVictim ? _pmcResponsesConfig.Victim.StripCapitalisationChancePercent : _pmcResponsesConfig.Killer.StripCapitalisationChancePercent; return randomUtil.GetChance100(chance); } /// /// Should capitalisation be stripped from the message response before sending /// /// Was responder a victim of player /// True = should be stripped protected bool AllCaps(bool isVictim) { var chance = isVictim ? _pmcResponsesConfig.Victim.AllCapsChancePercent : _pmcResponsesConfig.Killer.AllCapsChancePercent; return randomUtil.GetChance100(chance); } /// /// Should a suffix be appended to the end of the message being sent to player /// /// Was responder a victim of player /// True = should be appended protected bool AppendSuffixToMessageEnd(bool isVictim) { var chance = isVictim ? _pmcResponsesConfig.Victim.AppendBroToMessageEndChancePercent : _pmcResponsesConfig.Killer.AppendBroToMessageEndChancePercent; return randomUtil.GetChance100(chance); } /// /// Choose a type of response based on the weightings in pmc response config /// /// Was responder killed by player /// Response type (positive/negative) protected string ChooseResponseType(bool isVictim = true) { var responseWeights = isVictim ? _pmcResponsesConfig.Victim.ResponseTypeWeights : _pmcResponsesConfig.Killer.ResponseTypeWeights; return weightedRandomHelper.GetWeightedValue(responseWeights); } /// /// Get locale keys related to the type of response to send (victim/killer) /// /// Positive/negative /// Was responder killed by player /// List of response locale keys protected List GetResponseLocaleKeys(string keyType, bool isVictim = true) { var keyBase = isVictim ? "pmcresponse-victim_" : "pmcresponse-killer_"; var keys = serverLocalisationService.GetLocaleKeys(); return keys.Where(x => x.StartsWith($"{keyBase}{keyType}")).ToList(); } /// /// Get all locale keys that start with `pmcresponse-suffix` /// /// List of keys protected List GetResponseSuffixLocaleKeys() { var keys = serverLocalisationService.GetLocaleKeys(); return keys.Where(x => x.StartsWith("pmcresponse-suffix")).ToList(); } /// /// Convert a victim object into a IUserDialogInfo object /// /// Victim to convert /// UserDialogInfo object protected UserDialogInfo GetVictimDetails(Victim pmcVictim) { var categories = new List { MemberCategory.UniqueId, MemberCategory.Default, MemberCategory.Default, MemberCategory.Default, MemberCategory.Default, MemberCategory.Default, MemberCategory.Default, MemberCategory.Default, MemberCategory.Default, MemberCategory.Default, MemberCategory.Default, MemberCategory.Default, MemberCategory.Sherpa, MemberCategory.Developer, MemberCategory.Unheard, MemberCategory.Emissary, }; // Randomly choose a category for the victim var chosenCategory = randomUtil.GetArrayValue(categories); return new UserDialogInfo { Id = pmcVictim.ProfileId.Value, Aid = int.Parse(pmcVictim.AccountId), Info = new UserDialogDetails { Nickname = pmcVictim.Name, Level = pmcVictim.Level, Side = pmcVictim.Side, MemberCategory = chosenCategory, // TODO: get this data from a stored PMC profile when generated pre-raid instead of randomly generating it SelectedMemberCategory = chosenCategory, }, }; } }