From d5c6515ef02e9276317424dec699bca3a17f307b Mon Sep 17 00:00:00 2001
From: KaenoDev <193943350+KaenoDev@users.noreply.github.com>
Date: Tue, 21 Jan 2025 01:21:48 +0000
Subject: [PATCH] Finish LocationLifecycleService. Getting into raid dies on
botgen now
---
Libraries/Core/Helpers/HealthHelper.cs | 4 +-
Libraries/Core/Models/Eft/Common/Location.cs | 2 +-
.../Core/Models/Eft/Common/LocationBase.cs | 6 +-
Libraries/Core/Services/BotNameService.cs | 2 +-
.../Core/Services/LocationLifecycleService.cs | 983 +++++++++++++++++-
5 files changed, 959 insertions(+), 38 deletions(-)
diff --git a/Libraries/Core/Helpers/HealthHelper.cs b/Libraries/Core/Helpers/HealthHelper.cs
index 09020663..cce83f4c 100644
--- a/Libraries/Core/Helpers/HealthHelper.cs
+++ b/Libraries/Core/Helpers/HealthHelper.cs
@@ -1,10 +1,10 @@
using SptCommon.Annotations;
using Core.Models.Eft.Common;
+using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Health;
using Core.Models.Eft.Profile;
using BodyPartHealth = Core.Models.Eft.Common.Tables.BodyPartHealth;
using Effects = Core.Models.Eft.Profile.Effects;
-using Health = Core.Models.Eft.Profile.Health;
namespace Core.Helpers;
@@ -32,7 +32,7 @@ public class HealthHelper
/// Should all prior effects be removed before apply new ones (default - true)
public void UpdateProfileHealthPostRaid(
PmcData pmcData,
- Health postRaidHealth,
+ BotBaseHealth postRaidHealth,
string sessionID,
bool isDead)
{
diff --git a/Libraries/Core/Models/Eft/Common/Location.cs b/Libraries/Core/Models/Eft/Common/Location.cs
index 1b4d3cd6..11b72db4 100644
--- a/Libraries/Core/Models/Eft/Common/Location.cs
+++ b/Libraries/Core/Models/Eft/Common/Location.cs
@@ -22,7 +22,7 @@ public record Location
public StaticContainerDetails? StaticContainers { get; set; }
[JsonPropertyName("staticAmmo")]
- public Dictionary StaticAmmo { get; set; }
+ public Dictionary> StaticAmmo { get; set; }
/** All possible static containers on map + their assign groupings */
[JsonPropertyName("statics")]
diff --git a/Libraries/Core/Models/Eft/Common/LocationBase.cs b/Libraries/Core/Models/Eft/Common/LocationBase.cs
index 764bd055..b2759745 100644
--- a/Libraries/Core/Models/Eft/Common/LocationBase.cs
+++ b/Libraries/Core/Models/Eft/Common/LocationBase.cs
@@ -575,7 +575,7 @@ public record AdditionalHostilitySettings
public List? AlwaysFriends { get; set; }
[JsonPropertyName("BearEnemyChance")]
- public int? BearEnemyChance { get; set; }
+ public double? BearEnemyChance { get; set; }
[JsonPropertyName("BearPlayerBehaviour")]
public string? BearPlayerBehaviour { get; set; }
@@ -593,10 +593,10 @@ public record AdditionalHostilitySettings
public string? SavagePlayerBehaviour { get; set; }
[JsonPropertyName("SavageEnemyChance")]
- public int? SavageEnemyChance { get; set; }
+ public double? SavageEnemyChance { get; set; }
[JsonPropertyName("UsecEnemyChance")]
- public int? UsecEnemyChance { get; set; }
+ public double? UsecEnemyChance { get; set; }
[JsonPropertyName("UsecPlayerBehaviour")]
public string? UsecPlayerBehaviour { get; set; }
diff --git a/Libraries/Core/Services/BotNameService.cs b/Libraries/Core/Services/BotNameService.cs
index 7cbd93c4..ca2ddb08 100644
--- a/Libraries/Core/Services/BotNameService.cs
+++ b/Libraries/Core/Services/BotNameService.cs
@@ -27,7 +27,7 @@ public class BotNameService(
///
public void ClearNameCache()
{
- throw new NotImplementedException();
+ _usedNameCache.Clear();
}
///
diff --git a/Libraries/Core/Services/LocationLifecycleService.cs b/Libraries/Core/Services/LocationLifecycleService.cs
index 87d89fcb..cc8384a2 100644
--- a/Libraries/Core/Services/LocationLifecycleService.cs
+++ b/Libraries/Core/Services/LocationLifecycleService.cs
@@ -1,9 +1,19 @@
+using Core.Context;
+using Core.Generators;
using Core.Helpers;
using SptCommon.Annotations;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Match;
+using Core.Models.Eft.Profile;
+using Core.Models.Eft.Quests;
+using Core.Models.Enums;
+using Core.Models.Spt.Config;
+using Core.Models.Spt.Location;
using Core.Models.Utils;
+using Core.Servers;
+using Core.Utils;
+using Core.Utils.Cloners;
namespace Core.Services;
@@ -12,19 +22,163 @@ public class LocationLifecycleService
{
private readonly ISptLogger _logger;
private readonly RewardHelper _rewardHelper;
+ private readonly ConfigServer _configServer;
+ private readonly TimeUtil _timeUtil;
+ private readonly DatabaseService _databaseService;
+ private readonly ProfileHelper _profileHelper;
+ private readonly HashUtil _hashUtil;
+ private readonly ApplicationContext _applicationContext;
+ private readonly BotGenerationCacheService _botGenerationCacheService;
+ private readonly BotNameService _botNameService;
+ private readonly PmcConfig _pmcConfig;
+ private readonly ICloner _cloner;
+ private readonly LocationConfig _locationConfig;
+ private readonly RaidTimeAdjustmentService _raidTimeAdjustmentService;
+ private readonly LocationLootGenerator _locationLootGenerator;
+ private readonly LocalisationService _localisationService;
+ private readonly BotLootCacheService _botLootCacheService;
+ private readonly RagfairConfig _ragfairConfig;
+ private readonly HideoutConfig _hideoutConfig;
+ private readonly TraderConfig _traderConfig;
+ private readonly LootGenerator _lootGenerator;
+ private readonly MailSendService _mailSendService;
+ private readonly TraderHelper _traderHelper;
+ private readonly RandomUtil _randomUtil;
+ private readonly InRaidConfig _inRaidConfig;
+ private readonly InRaidHelper _inRaidHelper;
+ private readonly PlayerScavGenerator _playerScavGenerator;
+ private readonly SaveServer _saveServer;
+ private readonly HealthHelper _healthHelper;
+ private readonly PmcChatResponseService _pmcChatResponseService;
+ private readonly QuestHelper _questHelper;
+ private readonly InsuranceService _insuranceService;
+ private readonly MatchBotDetailsCacheService _matchBotDetailsCacheService;
public LocationLifecycleService(
ISptLogger logger,
- RewardHelper rewardHelper)
+ RewardHelper rewardHelper,
+ ConfigServer configServer,
+ TimeUtil timeUtil,
+ DatabaseService databaseService,
+ ProfileHelper profileHelper,
+ HashUtil hashUtil,
+ ApplicationContext applicationContext,
+ BotGenerationCacheService botGenerationCacheService,
+ BotNameService botNameService,
+ ICloner cloner,
+ RaidTimeAdjustmentService raidTimeAdjustmentService,
+ LocationLootGenerator locationLootGenerator,
+ LocalisationService localisationService,
+ BotLootCacheService botLootCacheService,
+ LootGenerator lootGenerator,
+ MailSendService mailSendService,
+ TraderHelper traderHelper,
+ RandomUtil randomUtil,
+ InRaidHelper inRaidHelper,
+ PlayerScavGenerator playerScavGenerator,
+ SaveServer saveServer,
+ HealthHelper healthHelper,
+ PmcChatResponseService pmcChatResponseService,
+ QuestHelper questHelper,
+ InsuranceService insuranceService,
+ MatchBotDetailsCacheService matchBotDetailsCacheService
+ )
{
_logger = logger;
_rewardHelper = rewardHelper;
+ _configServer = configServer;
+ _timeUtil = timeUtil;
+ _databaseService = databaseService;
+ _profileHelper = profileHelper;
+ _hashUtil = hashUtil;
+ _applicationContext = applicationContext;
+ _botGenerationCacheService = botGenerationCacheService;
+ _botNameService = botNameService;
+ _cloner = cloner;
+ _raidTimeAdjustmentService = raidTimeAdjustmentService;
+ _locationLootGenerator = locationLootGenerator;
+ _localisationService = localisationService;
+ _botLootCacheService = botLootCacheService;
+ _lootGenerator = lootGenerator;
+ _mailSendService = mailSendService;
+ _traderHelper = traderHelper;
+ _randomUtil = randomUtil;
+ _inRaidHelper = inRaidHelper;
+ _playerScavGenerator = playerScavGenerator;
+ _saveServer = saveServer;
+ _healthHelper = healthHelper;
+ _pmcChatResponseService = pmcChatResponseService;
+ _questHelper = questHelper;
+ _insuranceService = insuranceService;
+ _matchBotDetailsCacheService = matchBotDetailsCacheService;
+
+ _locationConfig = _configServer.GetConfig();
+ _inRaidConfig = _configServer.GetConfig();
+ _traderConfig = _configServer.GetConfig();
+ _ragfairConfig = _configServer.GetConfig();
+ _hideoutConfig = _configServer.GetConfig();
+ _pmcConfig = _configServer.GetConfig();
}
/** Handle client/match/local/start */
public StartLocalRaidResponseData StartLocalRaid(string sessionId, StartLocalRaidRequestData request)
{
- throw new NotImplementedException();
+ _logger.Debug($"Starting: {request.Location}");
+
+ var playerProfile = _profileHelper.GetPmcProfile(sessionId);
+
+ var result = new StartLocalRaidResponseData
+ {
+ ServerId = $"{request.Location}.{request.PlayerSide} {_timeUtil.GetTimeStamp()}", // TODO - does this need to be more verbose - investigate client?
+ ServerSettings = _databaseService.GetLocationServices(), // TODO - is this per map or global?
+ Profile = new ProfileInsuredItems
+ {
+ InsuredItems = playerProfile.InsuredItems
+ },
+ LocationLoot = GenerateLocationAndLoot(request.Location, request.ShouldSkipLootGeneration == false),
+ TransitionType = TransitionType.NONE,
+ Transition = new Transition
+ {
+ TransitionType = TransitionType.NONE,
+ TransitionRaidId = _hashUtil.Generate(),
+ TransitionCount = 0,
+ VisitedLocations = []
+ }
+ };
+
+ // Only has value when transitioning into map from previous one
+ if (request.Transition is not null) {
+ // TODO - why doesnt the raid after transit have any transit data?
+ result.Transition = request.Transition;
+ }
+
+ // Get data stored at end of previous raid (if any)
+ var transitionData = _applicationContext
+ .GetLatestValue(ContextVariableType.TRANSIT_INFO)
+ ?.GetValue();
+ if (transitionData is not null) {
+ _logger.Success($"Player: {sessionId} is in transit to {request.Location}");
+ result.Transition.TransitionType = TransitionType.COMMON;
+ result.Transition.TransitionRaidId = transitionData.TransitionRaidId;
+ result.Transition.TransitionCount += 1;
+
+ // Used by client to determine infil location) - client adds the map player is transiting to later
+ result.Transition.VisitedLocations.Add(transitionData.SptLastVisitedLocation);
+
+ // Complete, clean up as no longer needed
+ _applicationContext.ClearValues(ContextVariableType.TRANSIT_INFO);
+ }
+
+ // Apply changes from pmcConfig to bot hostility values
+ AdjustBotHostilitySettings(result.LocationLoot);
+
+ AdjustExtracts(request.PlayerSide, request.Location, result.LocationLoot);
+
+ // Clear bot cache ready for a fresh raid
+ _botGenerationCacheService.ClearStoredBots();
+ _botNameService.ClearNameCache();
+
+ return result;
}
/**
@@ -35,7 +189,24 @@ public class LocationLifecycleService
*/
protected void AdjustExtracts(string playerSide, string location, LocationBase locationData)
{
- throw new NotImplementedException();
+ var playerIsScav = playerSide.ToLower() == "savage";
+ if (!playerIsScav)
+ return;
+
+ // Get relevant extract data for map
+ var mapExtracts = _databaseService.GetLocation(location)?.AllExtracts;
+ if (mapExtracts is null) {
+ _logger.Warning($"Unable to find map: {location} extract data, no adjustments made");
+
+ return;
+ }
+
+ // Find only scav extracts and overwrite existing exits with them
+ var scavExtracts = mapExtracts.Where(extract => extract.Side.ToLower() == "scav").ToList();
+ if (scavExtracts.Count() > 0) {
+ // Scav extracts found, use them
+ locationData.Exits.AddRange(scavExtracts);
+ }
}
/**
@@ -44,7 +215,73 @@ public class LocationLifecycleService
*/
protected void AdjustBotHostilitySettings(LocationBase location)
{
- throw new NotImplementedException();
+ foreach (var botId in _pmcConfig.HostilitySettings) {
+ var configHostilityChanges = _pmcConfig.HostilitySettings[botId.Key];
+ var locationBotHostilityDetails = location.BotLocationModifier.AdditionalHostilitySettings.FirstOrDefault(
+ botSettings => botSettings.BotRole.ToLower() == botId.Key);
+
+ // No matching bot in config, skip
+ if (locationBotHostilityDetails is null) {
+ _logger.Warning("No bot: ${botId} hostility values found on: ${location.Id}, can only edit existing. Skipping");
+
+ continue;
+ }
+
+ // Add new permanent enemies if they don't already exist
+ if (configHostilityChanges.AdditionalEnemyTypes is not null) {
+ foreach (var enemyTypeToAdd in configHostilityChanges.AdditionalEnemyTypes) {
+ if (!locationBotHostilityDetails.AlwaysEnemies.Contains(enemyTypeToAdd)) {
+ locationBotHostilityDetails.AlwaysEnemies.Add(enemyTypeToAdd);
+ }
+ }
+ }
+
+ // Add/edit chance settings
+ if (configHostilityChanges.ChancedEnemies is not null) {
+ locationBotHostilityDetails.ChancedEnemies = [];
+ foreach (var chanceDetailsToApply in configHostilityChanges.ChancedEnemies) {
+ var locationBotDetails = locationBotHostilityDetails.ChancedEnemies.FirstOrDefault(
+ botChance => botChance.Role == chanceDetailsToApply.Role);
+ if (locationBotDetails is not null) {
+ // Existing
+ locationBotDetails.EnemyChance = chanceDetailsToApply.EnemyChance;
+ } else {
+ // Add new
+ locationBotHostilityDetails.ChancedEnemies.Add(chanceDetailsToApply);
+ }
+ }
+ }
+
+ // Add new permanent friends if they don't already exist
+ if (configHostilityChanges.AdditionalFriendlyTypes is not null) {
+ locationBotHostilityDetails.AlwaysFriends = [];
+ foreach (var friendlyTypeToAdd in configHostilityChanges.AdditionalFriendlyTypes) {
+ if (!locationBotHostilityDetails.AlwaysFriends.Contains(friendlyTypeToAdd)) {
+ locationBotHostilityDetails.AlwaysFriends.Add(friendlyTypeToAdd);
+ }
+ }
+ }
+
+ // Adjust vs bear hostility chance
+ if (configHostilityChanges.BearEnemyChance is not null) {
+ locationBotHostilityDetails.BearEnemyChance = configHostilityChanges.BearEnemyChance;
+ }
+
+ // Adjust vs usec hostility chance
+ if (configHostilityChanges.UsecEnemyChance is not null) {
+ locationBotHostilityDetails.UsecEnemyChance = configHostilityChanges.UsecEnemyChance;
+ }
+
+ // Adjust vs savage hostility chance
+ if (configHostilityChanges.SavageEnemyChance is not null) {
+ locationBotHostilityDetails.SavageEnemyChance = configHostilityChanges.SavageEnemyChance;
+ }
+
+ // Adjust vs scav hostility behaviour
+ if (configHostilityChanges.SavagePlayerBehaviour is not null) {
+ locationBotHostilityDetails.SavagePlayerBehaviour = configHostilityChanges.SavagePlayerBehaviour;
+ }
+ }
}
/**
@@ -55,13 +292,169 @@ public class LocationLifecycleService
*/
protected LocationBase GenerateLocationAndLoot(string name, bool generateLoot = true)
{
- throw new NotImplementedException();
+ var location = _databaseService.GetLocation(name);
+ var locationBaseClone = _cloner.Clone(location.Base);
+
+ // Update datetime property to now
+ locationBaseClone.UnixDateTime = _timeUtil.GetTimeStamp();
+
+ // Don't generate loot for hideout
+ if (name.ToLower() == "hideout") {
+ return locationBaseClone;
+ }
+
+ // If new spawn system is enabled, clear the spawn waves to prevent x2 spawns
+ if (locationBaseClone.NewSpawn is true) {
+ locationBaseClone.Waves = [];
+ }
+
+ // Only requested base data, not loot
+ if (!generateLoot) {
+ return locationBaseClone;
+ }
+
+ // Check for a loot multipler adjustment in app context and apply if one is found
+ var locationConfigClone = new LocationConfig();
+ var raidAdjustments = _applicationContext
+ .GetLatestValue(ContextVariableType.RAID_ADJUSTMENTS)
+ ?.GetValue();
+ if (raidAdjustments is not null) {
+ locationConfigClone = _cloner.Clone(_locationConfig); // Clone values so they can be used to reset originals later
+ _raidTimeAdjustmentService.MakeAdjustmentsToMap(raidAdjustments, locationBaseClone);
+ }
+
+ var staticAmmoDist = _cloner.Clone(location.StaticAmmo);
+
+ // Create containers and add loot to them
+ var staticLoot = _locationLootGenerator.GenerateStaticContainers(locationBaseClone, staticAmmoDist);
+ locationBaseClone.Loot.AddRange(staticLoot);
+
+ // Add dynamic loot to output loot
+ var dynamicLootDistClone = _cloner.Clone(location.LooseLoot);
+ var dynamicSpawnPoints = _locationLootGenerator.GenerateDynamicLoot(
+ dynamicLootDistClone,
+ staticAmmoDist,
+ name.ToLower()
+ );
+
+ // Push chosen spawn points into returned object
+ foreach (var spawnPoint in dynamicSpawnPoints) {
+ locationBaseClone.Loot.Add(spawnPoint);
+ }
+
+ // Done generating, log results
+ _logger.Success(
+ _localisationService.GetText("location-dynamic_items_spawned_success", dynamicSpawnPoints.Count));
+ _logger.Success(_localisationService.GetText("location-generated_success", name));
+
+ // Reset loot multipliers back to original values
+ if (raidAdjustments is not null) {
+ _logger.Debug("Resetting loot multipliers back to their original values");
+ _locationConfig.StaticLootMultiplier = locationConfigClone.StaticLootMultiplier;
+ _locationConfig.LooseLootMultiplier = locationConfigClone.LooseLootMultiplier;
+
+ _applicationContext.ClearValues(ContextVariableType.RAID_ADJUSTMENTS);
+ }
+
+ return locationBaseClone;
}
/** Handle client/match/local/end */
public void EndLocalRaid(string sessionId, EndLocalRaidRequestData request)
{
- throw new NotImplementedException();
+ // Clear bot loot cache
+ _botLootCacheService.ClearCache();
+
+ var fullProfile = _profileHelper.GetFullProfile(sessionId);
+ var pmcProfile = fullProfile.CharacterData.PmcData;
+ var scavProfile = fullProfile.CharacterData.ScavData;
+
+ // TODO:
+ // Quest status?
+ // stats/eft/aggressor - weird values (EFT.IProfileDataContainer.Nickname)
+
+ _logger.Debug($"Raid: {request.ServerId} outcome: {request.Results.Result}");
+
+ // Reset flea interval time to out-of-raid value
+ _ragfairConfig.RunIntervalSeconds = _ragfairConfig.RunIntervalValues.OutOfRaid;
+ _hideoutConfig.RunIntervalSeconds = _hideoutConfig.RunIntervalValues.OutOfRaid;
+
+ // ServerId has various info stored in it, delimited by a period
+ var serverDetails = request.ServerId.Split(".");
+
+ var locationName = serverDetails[0].ToLower();
+ var isPmc = serverDetails[1].ToLower() == "pmc";
+ var mapBase = _databaseService.GetLocation(locationName).Base;
+ var isDead = IsPlayerDead(request.Results);
+ var isTransfer = IsMapToMapTransfer(request.Results);
+ var isSurvived = IsPlayerSurvived(request.Results);
+
+ // Handle items transferred via BTR or transit to player mailbox
+ HandleItemTransferEvent(sessionId, request);
+
+ // Player is moving between maps
+ if (isTransfer && request.LocationTransit is not null) {
+ // Manually store the map player just left
+ request.LocationTransit.SptLastVisitedLocation = locationName;
+ // TODO - Persist each players last visited location history over multiple transits, e.g using InMemoryCacheService, need to take care to not let data get stored forever
+ // Store transfer data for later use in `startLocalRaid()` when next raid starts
+ request.LocationTransit.SptExitName = request.Results.ExitName;
+ _applicationContext.AddValue(ContextVariableType.TRANSIT_INFO, request.LocationTransit);
+ }
+
+ if (!isPmc) {
+ HandlePostRaidPlayerScav(sessionId, pmcProfile, scavProfile, isDead, isTransfer, request);
+
+ return;
+ }
+
+ HandlePostRaidPmc(
+ sessionId,
+ fullProfile,
+ scavProfile,
+ isDead,
+ isSurvived,
+ isTransfer,
+ request,
+ locationName
+ );
+
+ // Handle car extracts
+ if (ExtractWasViaCar(request.Results.ExitName)) {
+ HandleCarExtract(request.Results.ExitName, pmcProfile, sessionId);
+ }
+
+ // Handle coop exit
+ if (
+ request.Results.ExitName is not null &&
+ ExtractTakenWasCoop(request.Results.ExitName) &&
+ _traderConfig.Fence.CoopExtractGift.SendGift
+ ) {
+ HandleCoopExtract(sessionId, pmcProfile, request.Results.ExitName);
+ SendCoopTakenFenceMessage(sessionId);
+ }
+ }
+
+ private void SendCoopTakenFenceMessage(string sessionId)
+ {
+ // Generate reward for taking coop extract
+ var loot = _lootGenerator.CreateRandomLoot(_traderConfig.Fence.CoopExtractGift);
+ var mailableLoot = new List- ();
+
+ var parentId = _hashUtil.Generate();
+ foreach (var item in loot) {
+ item.ParentId = parentId;
+ mailableLoot.Add(item);
+ }
+
+ // Send message from fence giving player reward generated above
+ _mailSendService.SendLocalisedNpcMessageToPlayer(
+ sessionId,
+ _traderHelper.GetValidTraderIdByEnumValue(Traders.FENCE),
+ MessageType.MESSAGE_WITH_ITEMS,
+ _randomUtil.GetArrayValue(_traderConfig.Fence.CoopExtractGift.MessageLocaleIds),
+ mailableLoot,
+ _timeUtil.GetHoursAsSeconds(_traderConfig.Fence.CoopExtractGift.GiftExpiryHours));
}
/**
@@ -71,7 +464,16 @@ public class LocationLifecycleService
*/
protected bool ExtractWasViaCar(string extractName)
{
- throw new NotImplementedException();
+ // exit name is undefined on death
+ if (extractName is null) {
+ return false;
+ }
+
+ if (extractName.ToLower().Contains("v-ex")) {
+ return true;
+ }
+
+ return _inRaidConfig.CarExtracts.Contains(extractName.Trim());
}
/**
@@ -82,7 +484,24 @@ public class LocationLifecycleService
*/
protected void HandleCarExtract(string extractName, PmcData pmcData, string sessionId)
{
- throw new NotImplementedException();
+ var newFenceStanding = GetFenceStandingAfterExtract(
+ pmcData,
+ _inRaidConfig.CarExtractBaseStandingGain,
+ pmcData.CarExtractCounts[extractName]);
+
+ var fenceId = Traders.FENCE;
+ pmcData.TradersInfo[fenceId].Standing = newFenceStanding;
+
+ // Check if new standing has leveled up trader
+ _traderHelper.LevelUp(fenceId, pmcData);
+ pmcData.TradersInfo[fenceId].LoyaltyLevel = Math.Max((int)pmcData.TradersInfo[fenceId].LoyaltyLevel, 1);
+
+ _logger.Debug($"Car extract: {extractName} used, total times taken: {pmcData.CarExtractCounts[extractName]}");
+
+ // Copy updated fence rep values into scav profile to ensure consistency
+ var scavData = _profileHelper.GetScavProfile(sessionId);
+ scavData.TradersInfo[fenceId].Standing = pmcData.TradersInfo[fenceId].Standing;
+ scavData.TradersInfo[fenceId].LoyaltyLevel = pmcData.TradersInfo[fenceId].LoyaltyLevel;
}
/**
@@ -93,7 +512,24 @@ public class LocationLifecycleService
*/
protected void HandleCoopExtract(string sessionId, PmcData pmcData, string extractName)
{
- throw new NotImplementedException();
+ var newFenceStanding = GetFenceStandingAfterExtract(
+ pmcData,
+ _inRaidConfig.CarExtractBaseStandingGain,
+ pmcData.CarExtractCounts[extractName]);
+
+ var fenceId = Traders.FENCE;
+ pmcData.TradersInfo[fenceId].Standing = newFenceStanding;
+
+ // Check if new standing has leveled up trader
+ _traderHelper.LevelUp(fenceId, pmcData);
+ pmcData.TradersInfo[fenceId].LoyaltyLevel = Math.Max((int)pmcData.TradersInfo[fenceId].LoyaltyLevel, 1);
+
+ _logger.Debug($"Car extract: {extractName} used, total times taken: {pmcData.CarExtractCounts[extractName]}");
+
+ // Copy updated fence rep values into scav profile to ensure consistency
+ var scavData = _profileHelper.GetScavProfile(sessionId);
+ scavData.TradersInfo[fenceId].Standing = pmcData.TradersInfo[fenceId].Standing;
+ scavData.TradersInfo[fenceId].LoyaltyLevel = pmcData.TradersInfo[fenceId].LoyaltyLevel;
}
/**
@@ -103,9 +539,19 @@ public class LocationLifecycleService
* @param extractCount Number of times extract was taken
* @returns Fence standing after taking extract
*/
- protected int GetFenceStandingAfterExtract(PmcData pmcData, int baseGain, int extractCount)
+ protected double GetFenceStandingAfterExtract(PmcData pmcData, double baseGain, double extractCount)
{
- throw new NotImplementedException();
+ var fenceId = Traders.FENCE;
+ var fenceStanding = pmcData.TradersInfo[fenceId].Standing;
+
+ // get standing after taking extract x times, x.xx format, gain from extract can be no smaller than 0.01
+ fenceStanding += Math.Max(baseGain / extractCount, 0.01);
+
+ // Ensure fence loyalty level is not above/below the range -7 to 15
+ var newFenceStanding = Math.Min(Math.Max((double)fenceStanding, -7), 15);
+ _logger.Debug($"Old vs new fence standing: {pmcData.TradersInfo[fenceId].Standing}, {newFenceStanding}");
+
+ return Math.Round(newFenceStanding, 2);
}
/**
@@ -115,7 +561,12 @@ public class LocationLifecycleService
*/
protected bool ExtractTakenWasCoop(string extractName)
{
- throw new NotImplementedException();
+ // No extract name, not a coop extract
+ if (extractName is null) {
+ return false;
+ }
+
+ return _inRaidConfig.CoopExtracts.Contains(extractName.Trim());
}
protected void HandlePostRaidPlayerScav(
@@ -126,7 +577,114 @@ public class LocationLifecycleService
bool isTransfer,
EndLocalRaidRequestData request)
{
- throw new NotImplementedException();
+ var postRaidProfile = request.Results.Profile;
+
+ if (isTransfer) {
+ // We want scav inventory to persist into next raid when pscav is moving between maps
+ _inRaidHelper.SetInventory(sessionId, scavProfile, postRaidProfile, true, isTransfer);
+ }
+
+ scavProfile.Info.Level = request.Results.Profile.Info.Level;
+ scavProfile.Skills = request.Results.Profile.Skills;
+ scavProfile.Stats = request.Results.Profile.Stats;
+ scavProfile.Encyclopedia = request.Results.Profile.Encyclopedia;
+ scavProfile.TaskConditionCounters = request.Results.Profile.TaskConditionCounters;
+ scavProfile.SurvivorClass = request.Results.Profile.SurvivorClass;
+
+ // Scavs dont have achievements, but copy anyway
+ scavProfile.Achievements = request.Results.Profile.Achievements;
+
+ scavProfile.Info.Experience = request.Results.Profile.Info.Experience;
+
+ // Must occur after experience is set and stats copied over
+ scavProfile.Stats.Eft.TotalSessionExperience = 0;
+
+ ApplyTraderStandingAdjustments(scavProfile.TradersInfo, request.Results.Profile.TradersInfo);
+
+ // Clamp fence standing within -7 to 15 range
+ var fenceMax = _traderConfig.Fence.PlayerRepMax; // 15
+ var fenceMin = _traderConfig.Fence.PlayerRepMin; //-7
+ var currentFenceStanding = request.Results.Profile.TradersInfo[Traders.FENCE].Standing;
+ scavProfile.TradersInfo[Traders.FENCE].Standing = Math.Min(Math.Max((double)currentFenceStanding, fenceMin), fenceMax);
+
+ // Successful extract as scav, give some rep
+ if (IsPlayerSurvived(request.Results) && scavProfile.TradersInfo[Traders.FENCE].Standing < fenceMax) {
+ scavProfile.TradersInfo[Traders.FENCE].Standing += _inRaidConfig.ScavExtractStandingGain;
+ }
+
+ // Copy scav fence values to PMC profile
+ pmcProfile.TradersInfo[Traders.FENCE] = scavProfile.TradersInfo[Traders.FENCE];
+
+ if (ProfileHasConditionCounters(scavProfile)) {
+ // Scav quest progress needs to be moved to pmc so player can see it in menu / hand them in
+ MigrateScavQuestProgressToPmcProfile(scavProfile, pmcProfile);
+ }
+
+ // Must occur after encyclopedia updated
+ MergePmcAndScavEncyclopedias(scavProfile, pmcProfile);
+
+ // Remove skill fatigue values
+ ResetSkillPointsEarnedDuringRaid(scavProfile.Skills.Common);
+
+ // Scav died, regen scav loadout and reset timer
+ if (isDead) {
+ _playerScavGenerator.Generate(sessionId);
+ }
+
+ // Update last played property
+ pmcProfile.Info.LastTimePlayedAsSavage = _timeUtil.GetTimeStamp();
+
+ // Force a profile save
+ _saveServer.SaveProfile(sessionId);
+ }
+
+ /**
+ * Scav quest progress isnt transferred automatically from scav to pmc, we do this manually
+ * @param scavProfile Scav profile with quest progress post-raid
+ * @param pmcProfile Server pmc profile to copy scav quest progress into
+ */
+ private void MigrateScavQuestProgressToPmcProfile(PmcData scavProfile, PmcData pmcProfile)
+ {
+ foreach (var scavQuest in scavProfile.Quests) {
+ var pmcQuest = pmcProfile.Quests.FirstOrDefault(quest => quest.QId == scavQuest.QId);
+ if (pmcQuest is null) {
+ _logger.Warning(_localisationService.GetText("inraid-unable_to_migrate_pmc_quest_not_found_in_profile",
+ scavQuest.QId));
+ continue;
+ }
+
+ // Get counters related to scav quest
+ var matchingCounters = scavProfile.TaskConditionCounters.Where(
+ counter => counter.Value.SourceId == scavQuest.QId);
+
+ if (matchingCounters is null) {
+ continue;
+ }
+
+ // insert scav quest counters into pmc profile
+ foreach (var counter in matchingCounters) {
+ pmcProfile.TaskConditionCounters[counter.Value.Id] = counter.Value;
+ }
+
+ // Find Matching PMC Quest
+ // Update Status and StatusTimer properties
+ pmcQuest.Status = scavQuest.Status;
+ pmcQuest.StatusTimers = scavQuest.StatusTimers;
+ }
+ }
+
+ /**
+ * Does provided profile contain any condition counters
+ * @param profile Profile to check for condition counters
+ * @returns Profile has condition counters
+ */
+ protected bool ProfileHasConditionCounters(PmcData profile)
+ {
+ if (profile.TaskConditionCounters is null) {
+ return false;
+ }
+
+ return profile.TaskConditionCounters.Count > 0;
}
/**
@@ -141,7 +699,7 @@ public class LocationLifecycleService
*/
protected void HandlePostRaidPmc(
string sessionId,
- PmcData pmcProfile,
+ SptProfile fullProfile,
PmcData scavProfile,
bool isDead,
bool isSurvived,
@@ -149,7 +707,94 @@ public class LocationLifecycleService
EndLocalRaidRequestData request,
string locationName)
{
- throw new NotImplementedException();
+ var pmcProfile = fullProfile.CharacterData.PmcData;
+ var postRaidProfile = request.Results.Profile;
+ var preRaidProfileQuestDataClone = _cloner.Clone(pmcProfile.Quests);
+
+ // MUST occur BEFORE inventory actions (setInventory()) occur
+ // Player died, get quest items they lost for use later
+ var lostQuestItems = _profileHelper.GetQuestItemsInProfile(postRaidProfile);
+
+ // Update inventory
+ _inRaidHelper.SetInventory(sessionId, pmcProfile, postRaidProfile, isSurvived, isTransfer);
+
+ pmcProfile.Info.Level = postRaidProfile.Info.Level;
+ pmcProfile.Skills = postRaidProfile.Skills;
+ pmcProfile.Stats.Eft = postRaidProfile.Stats.Eft;
+ pmcProfile.Encyclopedia = postRaidProfile.Encyclopedia;
+ pmcProfile.TaskConditionCounters = postRaidProfile.TaskConditionCounters;
+ pmcProfile.SurvivorClass = postRaidProfile.SurvivorClass;
+
+ // MUST occur prior to profile achievements being overwritten by post-raid achievements
+ ProcessAchievementRewards(fullProfile, postRaidProfile.Achievements);
+
+ pmcProfile.Achievements = postRaidProfile.Achievements;
+ pmcProfile.Quests = ProcessPostRaidQuests(postRaidProfile.Quests);
+
+ // Handle edge case - must occur AFTER processPostRaidQuests()
+ LightkeeperQuestWorkaround(sessionId, postRaidProfile.Quests, preRaidProfileQuestDataClone, pmcProfile);
+
+ pmcProfile.WishList = postRaidProfile.WishList;
+
+ pmcProfile.Info.Experience = postRaidProfile.Info.Experience;
+
+ ApplyTraderStandingAdjustments(pmcProfile.TradersInfo, postRaidProfile.TradersInfo);
+
+ // Must occur AFTER experience is set and stats copied over
+ pmcProfile.Stats.Eft.TotalSessionExperience = 0;
+
+ var fenceId = Traders.FENCE;
+
+ // Clamp fence standing
+ var currentFenceStanding = postRaidProfile.TradersInfo[fenceId].Standing;
+ pmcProfile.TradersInfo[fenceId].Standing =
+ Math.Min(Math.Max((double)currentFenceStanding, -7), 15); // Ensure it stays between -7 and 15
+
+ // Copy fence values to Scav
+ scavProfile.TradersInfo[fenceId] = pmcProfile.TradersInfo[fenceId];
+
+ // MUST occur AFTER encyclopedia updated
+ MergePmcAndScavEncyclopedias(pmcProfile, scavProfile);
+
+ // Remove skill fatigue values
+ ResetSkillPointsEarnedDuringRaid(pmcProfile.Skills.Common);
+
+ // Handle temp, hydration, limb hp/effects
+ _healthHelper.UpdateProfileHealthPostRaid(pmcProfile, postRaidProfile.Health, sessionId, isDead);
+
+ if (isDead)
+ {
+ if (lostQuestItems.Count > 0)
+ {
+ // MUST occur AFTER quests have post raid quest data has been merged "processPostRaidQuests()"
+ // Player is dead + had quest items, check and fix any broken find item quests
+ CheckForAndFixPickupQuestsAfterDeath(sessionId, lostQuestItems, pmcProfile.Quests);
+ }
+
+ _pmcChatResponseService.SendKillerResponse(sessionId, pmcProfile, postRaidProfile.Stats.Eft.Aggressor);
+
+ _inRaidHelper.DeleteInventory(pmcProfile, sessionId);
+
+ _inRaidHelper.RemoveFiRStatusFromItemsInContainer(sessionId, pmcProfile, "SecuredContainer");
+ }
+
+ // Must occur AFTER killer messages have been sent
+ _matchBotDetailsCacheService.ClearCache();
+
+ var roles = new List
+ {
+ "pmcbear",
+ "pmcusec"
+ };
+
+ var victims = postRaidProfile.Stats.Eft.Victims.Where(
+ victim => roles.Contains(victim.Role.ToLower())).ToList();
+ if (victims?.Count > 0) {
+ // Player killed PMCs, send some mail responses to them
+ _pmcChatResponseService.SendVictimResponse(sessionId, victims, pmcProfile);
+ }
+
+ HandleInsuredItemLostEvent(sessionId, pmcProfile, request, locationName);
}
protected void CheckForAndFixPickupQuestsAfterDeath(
@@ -158,7 +803,52 @@ public class LocationLifecycleService
List profileQuests
)
{
- throw new NotImplementedException();
+ // Exclude completed quests
+ var activeQuestIdsInProfile = profileQuests
+ .Where(quest => quest.Status is QuestStatusEnum.AvailableForStart or QuestStatusEnum.Success)
+ .Select(status => status.QId);
+
+ // Get db details of quests we found above
+ var questDb = _databaseService.GetQuests().Values.Where(quest =>
+ activeQuestIdsInProfile.Contains(quest.Id));
+
+ foreach (var lostItem in lostQuestItems)
+ {
+ string matchingConditionId = string.Empty;
+ // Find a quest that has a FindItem condition that has the list items tpl as a target
+ var matchingQuests = questDb.Where(quest => {
+ var matchingCondition = quest.Conditions.AvailableForFinish.FirstOrDefault(
+ questCondition => questCondition.ConditionType == "FindItem" && (questCondition.Target as List).Contains(lostItem.Template)
+ );
+ if (matchingCondition is null) {
+ // Quest doesnt have a matching condition
+ return false;
+ }
+
+ // We found a condition, save id for later
+ matchingConditionId = matchingCondition.Id;
+ return true;
+ }).ToList();
+
+ // Fail if multiple were found
+ if (matchingQuests.Count != 1) {
+ _logger.Error($"Unable to fix quest item: {lostItem}, {matchingQuests.Count()} matching quests found, expected 1");
+
+ continue;
+ }
+
+ var matchingQuest = matchingQuests[0];
+ // We have a match, remove the condition id from profile to reset progress and let player pick item up again
+ var profileQuestToUpdate = profileQuests.FirstOrDefault(questStatus => questStatus.QId == matchingQuest.Id);
+ if (profileQuestToUpdate is null) {
+ // Profile doesnt have a matching quest
+ continue;
+ }
+
+ // Filter out the matching condition we found
+ profileQuestToUpdate.CompletedConditions = profileQuestToUpdate.CompletedConditions.Where(
+ conditionId => conditionId != matchingConditionId).ToList();
+ }
}
/*
@@ -173,7 +863,31 @@ public class LocationLifecycleService
PmcData pmcProfile
)
{
- throw new NotImplementedException();
+ // LK quests that were not completed before raid but now are
+ var newlyCompletedLightkeeperQuests = postRaidQuests
+ .Where(postRaidQuest =>
+ postRaidQuest.Status == QuestStatusEnum.Success && // Quest is complete
+ preRaidQuests.Any(preRaidQuest =>
+ preRaidQuest.QId == postRaidQuest.QId && // Get matching pre-raid quest
+ preRaidQuest.Status != QuestStatusEnum.Success) && // Completed quest was not completed before raid started
+ _databaseService.GetQuests().TryGetValue(postRaidQuest.QId, out var quest) && quest?.TraderId == Traders.LIGHTHOUSEKEEPER) // Quest is from LK
+ .ToList();
+
+
+ // Run server complete quest process to ensure player gets rewards
+ foreach (var questToComplete in newlyCompletedLightkeeperQuests)
+ {
+ _questHelper.CompleteQuest(
+ pmcProfile,
+ new CompleteQuestRequestData
+ {
+ Action = "CompleteQuest",
+ QuestId = questToComplete.QId,
+ RemoveExcessItems = false
+ },
+ sessionId
+ );
+ }
}
/*
@@ -182,18 +896,41 @@ public class LocationLifecycleService
*/
protected List ProcessPostRaidQuests(List questsToProcess)
{
- throw new NotImplementedException();
+ var failedQuests = questsToProcess.Where(quest => quest.Status == QuestStatusEnum.MarkedAsFailed);
+ foreach (var failedQuest in failedQuests) {
+ var dbQuest = _databaseService.GetQuests()[failedQuest.QId];
+ if (dbQuest is null) {
+ continue;
+ }
+
+ if (dbQuest.Restartable is not null) {
+ failedQuest.Status = QuestStatusEnum.Fail;
+ }
+ }
+
+ return questsToProcess;
}
/*
* Adjust server trader settings if they differ from data sent by client
*/
protected void ApplyTraderStandingAdjustments(
- Dictionary tradersServerProfile,
- Dictionary tradersClientProfile
+ Dictionary? tradersServerProfile,
+ Dictionary? tradersClientProfile
)
{
- throw new NotImplementedException();
+ foreach (var traderId in tradersClientProfile) {
+ var serverProfileTrader = tradersServerProfile.FirstOrDefault(x => x.Key == traderId.Key).Value;
+ var clientProfileTrader = tradersClientProfile.FirstOrDefault(x => x.Key == traderId.Key).Value;
+ if (serverProfileTrader is null || clientProfileTrader is null) {
+ continue;
+ }
+
+ if (clientProfileTrader.Standing != serverProfileTrader.Standing) {
+ // Difference found, update server profile with values from client profile
+ tradersServerProfile[traderId.Key].Standing = clientProfileTrader.Standing;
+ }
+ }
}
/*
@@ -201,12 +938,60 @@ public class LocationLifecycleService
*/
protected void HandleItemTransferEvent(string sessionId, EndLocalRaidRequestData request)
{
- throw new NotImplementedException();
+ var transferTypes = new List
+ {
+ "btr",
+ "transit"
+ };
+
+ foreach (var trasferType in transferTypes) {
+ var rootId = $"{Traders.BTR}_{trasferType}";
+ var itemsToSend = request?.TransferItems?[rootId] ?? [];
+
+ // Filter out the btr container item from transferred items before delivering
+ itemsToSend = itemsToSend.Where(item => item.Id != Traders.BTR).ToList();
+ if (itemsToSend.Count == 0) {
+ continue;
+ }
+
+ TransferItemDelivery(sessionId, Traders.BTR, itemsToSend);
+ }
}
protected void TransferItemDelivery(string sessionId, string traderId, List
- items)
{
- throw new NotImplementedException();
+ var serverProfile = _saveServer.GetProfile(sessionId);
+ var pmcData = serverProfile.CharacterData.PmcData;
+
+ var dialogueTemplates = _databaseService.GetTrader(traderId).Dialogue;
+ if (dialogueTemplates is null) {
+ _logger.Error(_localisationService.GetText("inraid-unable_to_deliver_item_no_trader_found", traderId));
+
+ return;
+ }
+
+ if (!dialogueTemplates.TryGetValue("itemsDelivered", out var itemsDelivered))
+ {
+ _logger.Error("dialogueTemplates doesn't contain itemsDelivered");
+ return;
+ }
+ var messageId = _randomUtil.GetArrayValue(itemsDelivered);
+ var messageStoreTime = _timeUtil.GetHoursAsSeconds(_traderConfig.Fence.BtrDeliveryExpireHours);
+
+ // Remove any items that were returned by the item delivery, but also insured, from the player's insurance list
+ // This is to stop items being duplicated by being returned from both item delivery and insurance
+ var deliveredItemIds = items.Select(item => item.Id);
+ pmcData.InsuredItems = pmcData.InsuredItems.Where(
+ insuredItem => !deliveredItemIds.Contains(insuredItem.ItemId)).ToList();
+
+ // Send the items to the player
+ _mailSendService.SendLocalisedNpcMessageToPlayer(
+ sessionId,
+ _traderHelper.GetValidTraderIdByEnumValue(traderId),
+ MessageType.BTR_ITEMS_DELIVERY,
+ messageId,
+ items,
+ messageStoreTime);
}
protected void HandleInsuredItemLostEvent(
@@ -216,7 +1001,24 @@ public class LocationLifecycleService
string locationName
)
{
- throw new NotImplementedException();
+ if (request.LostInsuredItems?.Count > 0)
+ {
+ var mappedItems = _insuranceService.MapInsuredItemsToTrader(
+ sessionId,
+ request.LostInsuredItems,
+ request.Results.Profile
+ );
+
+ // Is possible to have items in lostInsuredItems but removed before reaching mappedItems
+ if (mappedItems.Count == 0)
+ {
+ return;
+ }
+
+ _insuranceService.StoreGearLostInRaidToSendLater(sessionId, mappedItems);
+
+ _insuranceService.StartPostRaidInsuranceLostProcess(preRaidPmcProfile, sessionId, locationName);
+ }
}
/*
@@ -224,7 +1026,62 @@ public class LocationLifecycleService
*/
protected List
- GetEquippedGear(List
- items)
{
- throw new NotImplementedException();
+ var inventorySlots = new List
+ {
+ "FirstPrimaryWeapon",
+ "SecondPrimaryWeapon",
+ "Holster",
+ "Scabbard",
+ "Compass",
+ "Headwear",
+ "Earpiece",
+ "Eyewear",
+ "FaceCover",
+ "ArmBand",
+ "ArmorVest",
+ "TacticalVest",
+ "Backpack",
+ "pocket1",
+ "pocket2",
+ "pocket3",
+ "pocket4",
+ "SpecialSlot1",
+ "SpecialSlot2",
+ "SpecialSlot3"
+ };
+
+ var inventoryItems = new List
- ();
+
+ // Get an array of root player items
+ foreach (var item in items) {
+ if (inventorySlots.Contains(item.SlotId)) {
+ inventoryItems.Add(item);
+ }
+ }
+
+ // Loop through these items and get all of their children
+ var newItems = inventoryItems;
+ while (newItems.Count > 0) {
+ var foundItems = new List
- ();
+
+ foreach (var item in newItems) {
+ // Find children of this item
+ foreach (var newItem in items) {
+ if (newItem.ParentId == item.Id) {
+ foundItems.Add(newItem);
+ }
+ }
+ }
+
+ // Add these new found items to our list of inventory items
+ inventoryItems.AddRange(inventoryItems);
+ inventoryItems.AddRange(foundItems);
+
+ // Now find the children of these items
+ newItems = foundItems;
+ }
+
+ return inventoryItems;
}
/*
@@ -232,7 +1089,7 @@ public class LocationLifecycleService
*/
protected bool IsPlayerSurvived(EndRaidResult results)
{
- throw new NotImplementedException();
+ return results.Result == ExitStatus.SURVIVED;
}
/*
@@ -240,7 +1097,14 @@ public class LocationLifecycleService
*/
protected bool IsPlayerDead(EndRaidResult results)
{
- throw new NotImplementedException();
+ var deathEnums = new List
+ {
+ ExitStatus.KILLED,
+ ExitStatus.MISSINGINACTION,
+ ExitStatus.LEFT
+
+ };
+ return deathEnums.Contains(results.Result.Value);
}
/*
@@ -248,15 +1112,17 @@ public class LocationLifecycleService
*/
protected bool IsMapToMapTransfer(EndRaidResult results)
{
- throw new NotImplementedException();
+ return results.Result == ExitStatus.TRANSIT;
}
/*
* Reset the skill points earned in a raid to 0, ready for next raid
*/
- protected void ResetSkillPointsEarnedDuringRaid(List commonSkills)
+ protected void ResetSkillPointsEarnedDuringRaid(List commonSkills)
{
- throw new NotImplementedException();
+ foreach (var skill in commonSkills) {
+ skill.PointsEarnedDuringSession = 0;
+ }
}
/*
@@ -265,6 +1131,61 @@ public class LocationLifecycleService
*/
protected void MergePmcAndScavEncyclopedias(PmcData primary, PmcData secondary)
{
- throw new NotImplementedException();
+ var mergedDicts = primary.Encyclopedia?.Union(secondary.Encyclopedia)
+ .GroupBy(kvp => kvp.Key)
+ .ToDictionary(
+ g => g.Key,
+ g => g.Any(kvp => kvp.Value)
+ );
+
+ primary.Encyclopedia = mergedDicts;
+ secondary.Encyclopedia = mergedDicts;
+ }
+
+ protected void ProcessAchievementRewards(SptProfile fullProfile, Dictionary? postRaidAchievements)
+ {
+ var sessionId = fullProfile.ProfileInfo.ProfileId;
+ var pmcProfile = fullProfile.CharacterData.PmcData;
+ var preRaidAchievementIds = fullProfile.CharacterData.PmcData.Achievements;
+ var postRaidAchievementIds = postRaidAchievements;
+ var achievementIdsAcquiredThisRaid = postRaidAchievementIds.Where(
+ id => !preRaidAchievementIds.Contains(id)
+ );
+
+ // Get achievement data from db
+ var achievementsDb = _databaseService.GetTemplates().Achievements;
+
+ // Map the achievement ids player obtained in raid with matching achievement data from db
+ var achievements = achievementIdsAcquiredThisRaid.Select(
+ achievementId =>
+ achievementsDb.FirstOrDefault((achievementDb) => achievementDb.Id == achievementId.Key)
+ );
+ if (achievements is null)
+ {
+ // No achievements found
+ return;
+ }
+
+ foreach (var achievement in achievements)
+ {
+ var rewardItems = _rewardHelper.ApplyRewards(
+ achievement.Rewards,
+ CustomisationSource.ACHIEVEMENT,
+ fullProfile,
+ pmcProfile,
+ achievement.Id
+ );
+
+ if (rewardItems?.Count > 0)
+ {
+ _mailSendService.SendLocalisedSystemMessageToPlayer(
+ sessionId,
+ "670547bb5fa0b1a7c30d5836 0",
+ rewardItems,
+ [],
+ _timeUtil.GetHoursAsSeconds(24 * 7)
+ );
+ }
+ }
}
}