using System.Collections.Frozen; using SPTarkov.Common.Extensions; 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.Enums; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Utils; using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; namespace SPTarkov.Server.Core.Services; [Injectable(InjectionType.Singleton)] public class SeasonalEventService( ISptLogger logger, TimeUtil timeUtil, DatabaseService databaseService, GiftService giftService, ServerLocalisationService serverLocalisationService, ProfileHelper profileHelper, ConfigServer configServer, RandomUtil randomUtil ) { private bool _christmasEventActive; protected readonly FrozenSet _christmasEventItems = [ ItemTpl.ARMOR_6B13_M_ASSAULT_ARMOR_CHRISTMAS_EDITION, ItemTpl.BACKPACK_SANTAS_BAG, ItemTpl.BARTER_CHRISTMAS_TREE_ORNAMENT_RED, ItemTpl.BARTER_CHRISTMAS_TREE_ORNAMENT_SILVER, ItemTpl.BARTER_CHRISTMAS_TREE_ORNAMENT_VIOLET, ItemTpl.BARTER_JAR_OF_PICKLES, ItemTpl.BARTER_OLIVIER_SALAD_BOX, ItemTpl.BARTER_SPECIAL_40DEGREE_FUEL, ItemTpl.HEADWEAR_DED_MOROZ_HAT, ItemTpl.HEADWEAR_ELF_HAT, ItemTpl.HEADWEAR_HAT_WITH_HORNS, ItemTpl.HEADWEAR_MASKA1SCH_BULLETPROOF_HELMET_CHRISTMAS_EDITION, ItemTpl.HEADWEAR_SANTA_HAT, ItemTpl.RANDOMLOOTCONTAINER_NEW_YEAR_GIFT_BIG, ItemTpl.RANDOMLOOTCONTAINER_NEW_YEAR_GIFT_MEDIUM, ItemTpl.RANDOMLOOTCONTAINER_NEW_YEAR_GIFT_SMALL, ItemTpl.FACECOVER_ASTRONOMER_MASK, ItemTpl.FACECOVER_AYBOLIT_MASK, ItemTpl.FACECOVER_CIPOLLINO_MASK, ItemTpl.FACECOVER_FAKE_WHITE_BEARD, ItemTpl.FACECOVER_FOX_MASK, ItemTpl.FACECOVER_GRINCH_MASK, ItemTpl.FACECOVER_HARE_MASK, ItemTpl.FACECOVER_ROOSTER_MASK, ItemTpl.FLARE_RSP30_REACTIVE_SIGNAL_CARTRIDGE_FIREWORK, ItemTpl.BARTER_SHYSHKA_CHRISTMAS_TREE_LIFE_EXTENDER, ItemTpl.BACKPACK_MYSTERY_RANCH_TERRAFRAME_BACKPACK_CHRISTMAS_EDITION, ]; private List _currentlyActiveEvents = []; protected readonly FrozenSet _equipmentSlotsToFilter = [ EquipmentSlots.FaceCover, EquipmentSlots.Headwear, EquipmentSlots.Backpack, EquipmentSlots.TacticalVest, ]; private bool _halloweenEventActive; protected readonly FrozenSet _halloweenEventItems = [ ItemTpl.HEADWEAR_JACKOLANTERN_TACTICAL_PUMPKIN_HELMET, ItemTpl.FACECOVER_FACELESS_MASK, ItemTpl.FACECOVER_GHOUL_MASK, ItemTpl.FACECOVER_SPOOKY_SKULL_MASK, ItemTpl.FACECOVER_SPOOKY_SKULL_MASK_2, ItemTpl.FACECOVER_GHOUL_MASK_2, ItemTpl.FACECOVER_JASON_MASK, ItemTpl.FACECOVER_MISHA_MAYOROV_MASK, ItemTpl.FACECOVER_SLENDER_MASK, ItemTpl.FACECOVER_SLENDER_MASK_2, ItemTpl.RANDOMLOOTCONTAINER_PUMPKIN_RAND_LOOT_CONTAINER, ]; protected readonly HttpConfig _httpConfig = configServer.GetConfig(); protected readonly LocationConfig _locationConfig = configServer.GetConfig(); protected readonly QuestConfig _questConfig = configServer.GetConfig(); protected readonly SeasonalEventConfig _seasonalEventConfig = configServer.GetConfig(); protected readonly WeatherConfig _weatherConfig = configServer.GetConfig(); /// /// Get an array of christmas items found in bots inventories as loot /// /// array public FrozenSet GetChristmasEventItems() { return _christmasEventItems; } /// /// Get an array of halloween items found in bots inventories as loot /// /// array public FrozenSet GetHalloweenEventItems() { return _halloweenEventItems; } public bool ItemIsChristmasRelated(MongoId itemTpl) { return _christmasEventItems.Contains(itemTpl); } public bool ItemIsHalloweenRelated(MongoId itemTpl) { return _halloweenEventItems.Contains(itemTpl); } /// /// Check if item id exists in christmas or halloween event arrays /// /// item tpl to check for /// public bool ItemIsSeasonalRelated(MongoId itemTpl) { return _christmasEventItems.Contains(itemTpl) || _halloweenEventItems.Contains(itemTpl); } /// /// Get active seasonal events /// /// Array of active events public List GetActiveEvents() { return _currentlyActiveEvents; } /// /// Get an array of seasonal items that should not appear /// e.g. if halloween is active, only return christmas items /// or, if halloween and christmas are inactive, return both sets of items /// /// array of tpl strings public HashSet GetInactiveSeasonalEventItems() { var items = new HashSet(); if (!ChristmasEventEnabled()) { items.UnionWith(_christmasEventItems); } if (!HalloweenEventEnabled()) { items.UnionWith(_halloweenEventItems); } return items; } /// /// Is a seasonal event currently active /// /// true if event is active public bool SeasonalEventEnabled() { return _christmasEventActive || _halloweenEventActive; } /// /// Is christmas event active /// /// true if active public bool ChristmasEventEnabled() { return _christmasEventActive; } /// /// is halloween event active /// /// true if active public bool HalloweenEventEnabled() { return _halloweenEventActive; } /// /// Is detection of seasonal events enabled (halloween / christmas) /// /// true if seasonal events should be checked for public bool IsAutomaticEventDetectionEnabled() { return _seasonalEventConfig.EnableSeasonalEventDetection; } /// /// Get a dictionary of gear changes to apply to bots for a specific event e.g. Christmas/Halloween /// /// Name of event to get gear changes for /// bots with equipment changes protected Dictionary>>? GetEventBotGear(SeasonalEventType eventType) { return _seasonalEventConfig.EventGear.GetValueOrDefault(eventType, null); } /// /// Get a dictionary of loot changes to apply to bots for a specific event e.g. Christmas/Halloween /// /// Name of event to get gear changes for /// bots with loot changes protected Dictionary>> GetEventBotLoot(SeasonalEventType eventType) { return _seasonalEventConfig.EventLoot.GetValueOrDefault(eventType, null); } /// /// Get the dates each seasonal event starts and ends at /// /// Record with event name + start/end date public List GetEventDetails() { return _seasonalEventConfig.Events; } /// /// Look up quest in configs/quest.json /// /// Quest to look up /// event type (Christmas/Halloween/None) /// true if related public bool IsQuestRelatedToEvent(MongoId questId, SeasonalEventType eventType) { var eventQuestData = _questConfig.EventQuests.GetValueOrDefault(questId, null); return eventQuestData?.Season == eventType; } /// /// Handle activating seasonal events /// public void EnableSeasonalEvents() { if (_currentlyActiveEvents.Any()) { var globalConfig = databaseService.GetGlobals().Configuration; foreach (var activeEvent in _currentlyActiveEvents) { UpdateGlobalEvents(globalConfig, activeEvent); } } } /// /// Force a seasonal event to be active /// /// Event to force active /// True if event was successfully force enabled public bool ForceSeasonalEvent(SeasonalEventType eventType) { var globalConfig = databaseService.GetGlobals().Configuration; var seasonEvent = _seasonalEventConfig.Events.FirstOrDefault(e => e.Type == eventType); if (seasonEvent is null) { logger.Warning($"Unable to force event: {eventType} as it cannot be found in events config"); return false; } UpdateGlobalEvents(globalConfig, seasonEvent); return true; } /// /// Store active events inside class list property `currentlyActiveEvents` + set class properties: christmasEventActive/halloweenEventActive /// public void CacheActiveEvents() { var currentDate = DateTimeOffset.UtcNow.DateTime; var seasonalEvents = GetEventDetails(); // reset existing data _currentlyActiveEvents = []; // Add active events to array foreach (var events in seasonalEvents) { if (!events.Enabled) { continue; } if (currentDate.DateIsBetweenTwoDates(events.StartMonth, events.StartDay, events.EndMonth, events.EndDay)) { _currentlyActiveEvents.Add(events); } } } /// /// Get the currently active weather season e.g. SUMMER/AUTUMN/WINTER /// /// Season enum value public Season GetActiveWeatherSeason() { if (_weatherConfig.OverrideSeason.HasValue) { return _weatherConfig.OverrideSeason.Value; } var currentDate = timeUtil.GetDateTimeNow(); foreach (var seasonRange in _weatherConfig.SeasonDates) { if ( currentDate.DateIsBetweenTwoDates( seasonRange.StartMonth ?? 0, seasonRange.StartDay ?? 0, seasonRange.EndMonth ?? 0, seasonRange.EndDay ?? 0 ) ) { return seasonRange.SeasonType ?? Season.SUMMER; } } logger.Warning(serverLocalisationService.GetText("season-no_matching_season_found_for_date")); return Season.SUMMER; } /// /// Iterate through bots inventory and loot to find and remove christmas items (as defined in SeasonalEventService) /// /// Bots inventory to iterate over /// the role of the bot being processed public void RemoveChristmasItemsFromBotInventory(BotTypeInventory botInventory, string botRole) { var christmasItems = GetChristmasEventItems(); // Remove christmas related equipment foreach (var equipmentSlotKey in _equipmentSlotsToFilter) { if (!botInventory.Equipment.TryGetValue(equipmentSlotKey, out var equipment)) { logger.Warning( serverLocalisationService.GetText( "seasonal-missing_equipment_slot_on_bot", new { equipmentSlot = equipmentSlotKey, botRole } ) ); continue; } botInventory.Equipment[equipmentSlotKey] = equipment.Where(i => !_christmasEventItems.Contains(i.Key)).ToDictionary(); } var containersToCheck = new List> { botInventory.Items.Backpack, botInventory.Items.Pockets, botInventory.Items.SecuredContainer, botInventory.Items.TacticalVest, botInventory.Items.SpecialLoot, }; foreach (var container in containersToCheck) { // Find all Christmas items in container and remove container.RemoveItems(christmasItems); } } /// /// Make adjusted to server code based on the name of the event passed in /// /// globals.json /// Name of the event to enable. e.g. Christmas protected void UpdateGlobalEvents(Config globalConfig, SeasonalEvent eventType) { logger.Success(serverLocalisationService.GetText("season-event_is_active", eventType.Type)); _christmasEventActive = false; _halloweenEventActive = false; switch (eventType.Type) { case SeasonalEventType.Halloween: ApplyHalloweenEvent(eventType, globalConfig); break; case SeasonalEventType.Christmas: ApplyChristmasEvent(eventType, globalConfig); break; case SeasonalEventType.NewYears: ApplyNewYearsEvent(eventType, globalConfig); break; case SeasonalEventType.AprilFools: AddGifterBotToMaps(); AddLootItemsToGifterDropItemsList(); AddEventGearToBots(SeasonalEventType.Halloween); AddEventGearToBots(SeasonalEventType.Christmas); AddEventLootToBots(SeasonalEventType.Christmas); AddEventBossesToMaps("halloweensummon"); EnableHalloweenSummonEvent(); AddPumpkinsToScavBackpacks(); RenameBitcoin(); if (eventType.Settings is not null && eventType.Settings.ReplaceBotHostility.GetValueOrDefault(false)) { if (_seasonalEventConfig.HostilitySettingsForEvent.TryGetValue("AprilFools", out var botData)) { ReplaceBotHostility(botData); } } if (eventType.Settings?.ForceSeason != null) { _weatherConfig.OverrideSeason = eventType.Settings.ForceSeason; } break; default: // Likely a mod event HandleModEvent(eventType, globalConfig); break; } } protected void ApplyHalloweenEvent(SeasonalEvent eventType, Config globalConfig) { _halloweenEventActive = true; globalConfig.EventType = globalConfig.EventType.Where(x => x != EventType.None).ToList(); globalConfig.EventType.Add(EventType.Halloween); globalConfig.EventType.Add(EventType.HalloweenIllumination); globalConfig.Health.ProfileHealthSettings.DefaultStimulatorBuff = "Buffs_Halloween"; AddEventGearToBots(eventType.Type); AdjustZryachiyMeleeChance(); if (eventType.Settings?.EnableSummoning ?? false) { EnableHalloweenSummonEvent(); AddEventBossesToMaps("halloweensummon"); } if (eventType.Settings?.ZombieSettings?.Enabled ?? false) { ConfigureZombies(eventType.Settings.ZombieSettings); } if (eventType.Settings?.RemoveEntryRequirement is not null) { RemoveEntryRequirement(eventType.Settings.RemoveEntryRequirement); } if (eventType.Settings?.ReplaceBotHostility ?? false) { ReplaceBotHostility(_seasonalEventConfig.HostilitySettingsForEvent.FirstOrDefault(x => x.Key == "zombies").Value); } if (eventType.Settings?.AdjustBotAppearances ?? false) { AdjustBotAppearanceValues(eventType.Type); } AddPumpkinsToScavBackpacks(); AdjustTraderIcons(eventType.Type); } protected void ApplyChristmasEvent(SeasonalEvent eventType, Config globalConfig) { _christmasEventActive = true; if (eventType.Settings?.EnableChristmasHideout ?? false) { globalConfig.EventType = globalConfig.EventType.Where(x => x != EventType.None).ToList(); globalConfig.EventType.Add(EventType.Christmas); } AddEventGearToBots(eventType.Type); AddEventLootToBots(eventType.Type); if (eventType.Settings?.EnableSanta ?? false) { AddGifterBotToMaps(); AddLootItemsToGifterDropItemsList(); } EnableDancingTree(); if (eventType.Settings?.AdjustBotAppearances ?? false) { AdjustBotAppearanceValues(eventType.Type); } } protected void ApplyNewYearsEvent(SeasonalEvent eventType, Config globalConfig) { _christmasEventActive = true; if (eventType.Settings?.EnableChristmasHideout ?? false) { globalConfig.EventType = globalConfig.EventType.Where(x => x != EventType.None).ToList(); globalConfig.EventType.Add(EventType.Christmas); } AddEventGearToBots(SeasonalEventType.Christmas); AddEventLootToBots(SeasonalEventType.Christmas); if (eventType.Settings?.EnableSanta ?? false) { AddGifterBotToMaps(); AddLootItemsToGifterDropItemsList(); } EnableDancingTree(); if (eventType.Settings?.AdjustBotAppearances ?? false) { AdjustBotAppearanceValues(SeasonalEventType.Christmas); } } /// /// Adjust the weights for all bots body part appearances, based on data inside /// seasonalevents.json/botAppearanceChanges /// /// Season to apply changes for protected void AdjustBotAppearanceValues(SeasonalEventType season) { if (!_seasonalEventConfig.BotAppearanceChanges.TryGetValue(season, out var appearanceAdjustments)) { // No changes found for this season return; } foreach (var (botType, botAppearanceAdjustments) in appearanceAdjustments) { if (!databaseService.GetBots().Types.TryGetValue(botType, out var bot)) { // Bot defined in config doesn't exist continue; } foreach (var (bodyPart, weightAdjustments) in botAppearanceAdjustments) { // Get the matching bots appearance pool by key var partPool = bodyPart switch { "body" => bot.BotAppearance.Body, "feet" => bot.BotAppearance.Feet, "hands" => bot.BotAppearance.Hands, "head" => bot.BotAppearance.Head, _ => null, }; if (partPool is null) { logger.Warning($"Unable to adjust bot: {botType} body part appearance: {bodyPart}"); continue; } // Apply new weights to values from config foreach (var (itemId, weighting) in weightAdjustments) { partPool[itemId] = weighting; } } } } protected void ReplaceBotHostility(Dictionary> hostilitySettings) { var locations = databaseService.GetLocations().GetDictionary(); var ignoreList = _locationConfig.NonMaps; foreach (var (locationName, locationBase) in locations) { if (ignoreList.Contains(locationName)) { continue; } if (locationBase?.Base?.BotLocationModifier?.AdditionalHostilitySettings is null) { continue; } // Try to get map 'default' first if it exists if (!hostilitySettings.TryGetValue("default", out var newHostilitySettings)) { // No 'default', try for location name if (!hostilitySettings.TryGetValue(locationName, out newHostilitySettings)) { // no settings for map by name, skip map continue; } } foreach (var settings in newHostilitySettings) { var matchingBaseSettings = locationBase.Base.BotLocationModifier.AdditionalHostilitySettings.FirstOrDefault(x => x.BotRole == settings.BotRole ); if (matchingBaseSettings is null) { continue; } if (settings.AlwaysEnemies is not null) { matchingBaseSettings.AlwaysEnemies = settings.AlwaysEnemies; } if (settings.AlwaysFriends is not null) { matchingBaseSettings.AlwaysFriends = settings.AlwaysFriends; } if (settings.BearEnemyChance is not null) { matchingBaseSettings.BearEnemyChance = settings.BearEnemyChance; } if (settings.ChancedEnemies is not null) { matchingBaseSettings.ChancedEnemies = settings.ChancedEnemies; } if (settings.Neutral is not null) { matchingBaseSettings.Neutral = settings.Neutral; } if (settings.SavageEnemyChance is not null) { matchingBaseSettings.SavageEnemyChance = settings.SavageEnemyChance; } if (settings.SavagePlayerBehaviour is not null) { matchingBaseSettings.SavagePlayerBehaviour = settings.SavagePlayerBehaviour; } if (settings.UsecEnemyChance is not null) { matchingBaseSettings.UsecEnemyChance = settings.UsecEnemyChance; } if (settings.UsecPlayerBehaviour is not null) { matchingBaseSettings.UsecPlayerBehaviour = settings.UsecPlayerBehaviour; } if (settings.Warn is not null) { matchingBaseSettings.Warn = settings.Warn; } } } } protected void RemoveEntryRequirement(IEnumerable locationIds) { foreach (var locationId in locationIds) { var location = databaseService.GetLocation(locationId); location.Base.AccessKeys = []; location.Base.AccessKeysPvE = []; } } public void GivePlayerSeasonalGifts(MongoId sessionId) { if (_currentlyActiveEvents is null) { return; } foreach (var seasonEvent in _currentlyActiveEvents) { switch (seasonEvent.Type) { case SeasonalEventType.Christmas: GiveGift(sessionId, "Christmas2022"); break; case SeasonalEventType.NewYears: GiveGift(sessionId, "NewYear2023"); GiveGift(sessionId, "NewYear2024"); break; } } } /// /// Force zryachiy to always have a melee weapon /// protected void AdjustZryachiyMeleeChance() { var zryachiyKvP = databaseService .GetBots() .Types.FirstOrDefault(x => string.Equals(x.Key, "bosszryachiy", StringComparison.OrdinalIgnoreCase)); var value = new Dictionary(); foreach (var chance in zryachiyKvP.Value.BotChances.EquipmentChances) { if (string.Equals(chance.Key, "Scabbard", StringComparison.OrdinalIgnoreCase)) { value.Add(chance.Key, 100); continue; } value.Add(chance.Key, chance.Value); } zryachiyKvP.Value.BotChances.EquipmentChances = value; } /// /// Enable the halloween zryachiy summon event /// protected void EnableHalloweenSummonEvent() { databaseService.GetGlobals().Configuration.EventSettings.EventActive = true; } protected void ConfigureZombies(ZombieSettings zombieSettings) { // Flag zombies as being enabled var botData = databaseService.GetBots(); if (!botData.Core.TryAdd("ACTIVE_HALLOWEEN_ZOMBIES_EVENT", true)) { botData.Core["ACTIVE_HALLOWEEN_ZOMBIES_EVENT"] = true; } var globals = databaseService.GetGlobals(); var infectionHalloween = globals.Configuration.SeasonActivity.InfectionHalloween; infectionHalloween.DisplayUIEnabled = true; infectionHalloween.Enabled = true; var globalInfectionDict = globals.LocationInfection.GetAllPropsAsDict(); foreach (var (locationId, infectionPercentage) in zombieSettings.MapInfectionAmount) { // calculate a random value unless the rate is 100 double randomInfectionPercentage = infectionPercentage == 100 ? infectionPercentage : Convert.ToDouble(randomUtil.GetInt(Convert.ToInt32(infectionPercentage), 100)); if (logger.IsLogEnabled(LogLevel.Debug)) logger.Debug($"Percent infected from map {locationId} is {randomInfectionPercentage}"); // Infection rates sometimes apply to multiple maps, e.g. Factory day/night or Sandbox/sandbox_high // Get the list of maps that should have infection value applied to their base // 90% of locations are just 1 map e.g. bigmap = customs var mappedLocations = GetLocationFromInfectedLocation(locationId); foreach (var locationKey in mappedLocations) { databaseService.GetLocation(locationKey).Base.Events.Halloween2024.InfectionPercentage = randomInfectionPercentage; } // Globals data needs value updated too globalInfectionDict[locationId] = randomInfectionPercentage; } foreach (var locationId in zombieSettings.DisableBosses) { databaseService.GetLocation(locationId).Base.BossLocationSpawn = []; } foreach (var locationId in zombieSettings.DisableWaves) { databaseService.GetLocation(locationId).Base.Waves = []; } var locationsWithActiveInfection = GetLocationsWithZombies(zombieSettings.MapInfectionAmount); AddEventBossesToMaps("halloweenzombies", locationsWithActiveInfection); } /// /// Get location ids of maps with an infection above 0 /// /// Dict of locations with their infection percentage /// List of location ids protected HashSet GetLocationsWithZombies(Dictionary locationInfections) { var result = new HashSet(); // Get only the locations with an infection above 0 var infectionKeys = locationInfections.Where(location => locationInfections[location.Key] > 0); // Convert the infected location id into its generic location id foreach (var location in infectionKeys) { result.UnionWith(GetLocationFromInfectedLocation(location.Key)); } return result; } /// /// BSG store the location ids differently inside `LocationInfection`, need to convert to matching location IDs /// /// Key to convert /// List of locations protected List GetLocationFromInfectedLocation(string infectedLocationKey) { return infectedLocationKey switch { "factory4" => ["factory4_day", "factory4_night"], "Sandbox" => ["sandbox", "sandbox_high"], _ => [infectedLocationKey], }; } protected void AddEventWavesToMaps(string eventType) { var wavesToAddByMap = _seasonalEventConfig.EventWaves[eventType.ToLowerInvariant()]; if (wavesToAddByMap is null) { logger.Warning($"Unable to add: {eventType} waves, eventWaves is missing"); return; } var locations = databaseService.GetLocations().GetAllPropsAsDict(); foreach (var map in wavesToAddByMap) { var wavesToAdd = wavesToAddByMap[map.Key]; if (wavesToAdd is null) { logger.Warning($"Unable to add: {eventType} wave to: {map.Key}"); continue; } ((Location)locations[map.Key]).Base.Waves = []; ((Location)locations[map.Key]).Base.Waves.AddRange(wavesToAdd); } } /// /// Add event bosses to maps /// /// Seasonal event, e.g. HALLOWEEN/CHRISTMAS /// OPTIONAL - Maps to add bosses to protected void AddEventBossesToMaps(string eventType, HashSet? mapIdWhitelist = null) { if (!_seasonalEventConfig.EventBossSpawns.TryGetValue(eventType.ToLowerInvariant(), out var botsToAddPerMap)) { logger.Warning($"Unable to add: {eventType} bosses, eventBossSpawns is missing"); return; } var locations = databaseService.GetLocations().GetAllPropsAsDict(); foreach (var (locationKey, bossesToAdd) in botsToAddPerMap) { if (bossesToAdd.Count == 0) { continue; } if (mapIdWhitelist is null || !mapIdWhitelist.Contains(locationKey)) { continue; } var locationName = databaseService.GetLocations().GetMappedKey(locationKey); var mapBosses = ((Location)locations[locationName]).Base.BossLocationSpawn; foreach (var boss in bossesToAdd) { if (mapBosses.All(bossSpawn => bossSpawn.BossName != boss.BossName)) { // Zombie doesn't exist in maps boss list yet, add mapBosses.Add(boss); } } } } /// /// Change trader icons to be more event themed (Halloween only so far) /// /// What event is active protected void AdjustTraderIcons(SeasonalEventType eventType) { switch (eventType) { case SeasonalEventType.Halloween: _httpConfig.ServerImagePathOverride["./assets/images/traders/5a7c2ebb86f7746e324a06ab.png"] = "./assets/images/traders/halloween/5a7c2ebb86f7746e324a06ab.png"; _httpConfig.ServerImagePathOverride["./assets/images/traders/5ac3b86a86f77461491d1ad8.png"] = "./assets/images/traders/halloween/5ac3b86a86f77461491d1ad8.png"; _httpConfig.ServerImagePathOverride["./assets/images/traders/5c06531a86f7746319710e1b.png"] = "./assets/images/traders/halloween/5c06531a86f7746319710e1b.png"; _httpConfig.ServerImagePathOverride["./assets/images/traders/59b91ca086f77469a81232e4.png"] = "./assets/images/traders/halloween/59b91ca086f77469a81232e4.png"; _httpConfig.ServerImagePathOverride["./assets/images/traders/59b91cab86f77469aa5343ca.png"] = "./assets/images/traders/halloween/59b91cab86f77469aa5343ca.png"; _httpConfig.ServerImagePathOverride["./assets/images/traders/59b91cb486f77469a81232e5.png"] = "./assets/images/traders/halloween/59b91cb486f77469a81232e5.png"; _httpConfig.ServerImagePathOverride["./assets/images/traders/59b91cbd86f77469aa5343cb.png"] = "./assets/images/traders/halloween/59b91cbd86f77469aa5343cb.png"; _httpConfig.ServerImagePathOverride["./assets/images/traders/579dc571d53a0658a154fbec.png"] = "./assets/images/traders/halloween/579dc571d53a0658a154fbec.png"; break; case SeasonalEventType.Christmas: // TODO: find christmas trader icons break; } // TODO: implement this properly as new function //_databaseImporter.LoadImages($"{ _databaseImporter.GetSptDataPath()} images /" // ,["traders"] // ,["/files/trader/avatar/"]); } /// /// Add lootable items from backpack into patrol.ITEMS_TO_DROP difficulty property /// protected void AddLootItemsToGifterDropItemsList() { var gifterBot = databaseService.GetBots().Types["gifter"]; var itemsCSV = string.Join(",", gifterBot.BotInventory.Items.Backpack.Keys); string[] difficulties = ["easy", "normal", "hard", "impossible"]; foreach (var difficulty in difficulties) { gifterBot.BotDifficulty[difficulty].Patrol.TryAdd("ITEMS_TO_DROP", ""); gifterBot.BotDifficulty[difficulty].Patrol["ITEMS_TO_DROP"] = itemsCSV; } } /// /// Read in data from seasonalEvents.json and add found equipment items to bots /// /// Name of the event to read equipment in from config protected void AddEventGearToBots(SeasonalEventType eventType) { var botGearChanges = GetEventBotGear(eventType); if (botGearChanges is null) { logger.Warning(serverLocalisationService.GetText("gameevent-no_gear_data", eventType)); return; } // Iterate over bots with changes to apply foreach (var botKvP in botGearChanges) { var botToUpdate = databaseService.GetBots().Types[botKvP.Key.ToLowerInvariant()]; if (botToUpdate is null) { logger.Warning(serverLocalisationService.GetText("gameevent-bot_not_found", botKvP)); continue; } // Iterate over each equipment slot change var gearAmendmentsBySlot = botGearChanges[botKvP.Key]; foreach (var equipmentKvP in gearAmendmentsBySlot) { // Adjust slots spawn chance to be at least 75% botToUpdate.BotChances.EquipmentChances[equipmentKvP.Key] = Math.Max( botToUpdate.BotChances.EquipmentChances[equipmentKvP.Key], 75 ); // Grab gear to add and loop over it foreach (var itemToAddKvP in equipmentKvP.Value) { var equipmentSlot = (EquipmentSlots)Enum.Parse(typeof(EquipmentSlots), equipmentKvP.Key); var equipmentDict = botToUpdate.BotInventory.Equipment[equipmentSlot]; equipmentDict[itemToAddKvP.Key] = equipmentKvP.Value[itemToAddKvP.Key]; } } } } /// /// Read in data from seasonalEvents.json and add found loot items to bots /// /// Name of the event to read loot in from config protected void AddEventLootToBots(SeasonalEventType eventType) { var botLootChanges = GetEventBotLoot(eventType); if (botLootChanges is null) { logger.Warning(serverLocalisationService.GetText("gameevent-no_gear_data", eventType)); return; } // Iterate over bots with changes to apply foreach (var botKvpP in botLootChanges) { var botToUpdate = databaseService.GetBots().Types[botKvpP.Key.ToLowerInvariant()]; if (botToUpdate is null) { logger.Warning(serverLocalisationService.GetText("gameevent-bot_not_found", botKvpP)); continue; } // Iterate over each loot slot change var lootAmendmentsBySlot = botLootChanges[botKvpP.Key]; foreach (var slotKvP in lootAmendmentsBySlot) { // Grab loot to add and loop over it var itemTplsToAdd = slotKvP.Value; foreach (var itemKvP in itemTplsToAdd) { var dict = botToUpdate.BotInventory.Items.GetAllPropsAsDict(); dict[itemKvP.Key] = itemTplsToAdd[itemKvP.Key]; } } } } /// /// Add pumpkin loot boxes to scavs /// protected void AddPumpkinsToScavBackpacks() { databaseService.GetBots().Types["assault"].BotInventory.Items.Backpack[ItemTpl.RANDOMLOOTCONTAINER_PUMPKIN_RAND_LOOT_CONTAINER] = 400; } protected void RenameBitcoin() { if (databaseService.GetLocales().Global.TryGetValue("en", out var lazyLoad)) { lazyLoad.AddTransformer(localeData => { localeData[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} Name"] = "Physical SPT Coin"; localeData[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} ShortName"] = "0.2SPT"; return localeData; }); } } /// /// Set Khorovod(dancing tree) chance to 100% on all maps that support it /// protected void EnableDancingTree() { var maps = databaseService.GetLocations(); HashSet mapsToCheck = ["hideout", "base", "privatearea"]; foreach (var mapKvP in maps.GetDictionary()) { // Skip maps that have no tree if (mapsToCheck.Contains(mapKvP.Key)) { continue; } var mapData = mapKvP.Value; if (mapData.Base?.Events?.Khorovod?.Chance is not null) { mapData.Base.Events.Khorovod.Chance = 100; mapData.Base.BotLocationModifier.KhorovodChance = 100; } } } /// /// Add santa to maps /// protected void AddGifterBotToMaps() { var gifterSettings = _seasonalEventConfig.GifterSettings; var maps = databaseService.GetLocations().GetDictionary(); foreach (var gifterMapSettings in gifterSettings) { if (!maps.TryGetValue(databaseService.GetLocations().GetMappedKey(gifterMapSettings.Map), out var mapData)) { logger.Warning($"AddGifterBotToMaps() Map not found {gifterMapSettings.Map}"); continue; } // Don't add gifter to map twice var existingGifter = mapData.Base.BossLocationSpawn.FirstOrDefault(boss => boss.BossName == "gifter"); if (existingGifter is not null) { existingGifter.BossChance = gifterMapSettings.SpawnChance; continue; } mapData.Base.BossLocationSpawn.Add( new BossLocationSpawn { BossName = "gifter", BossChance = gifterMapSettings.SpawnChance, BossZone = gifterMapSettings.Zones, IsBossPlayer = false, BossDifficulty = "normal", BossEscortType = "gifter", BossEscortDifficulty = "normal", BossEscortAmount = "0", ForceSpawn = true, SpawnMode = ["regular", "pve"], Time = -1, TriggerId = "", TriggerName = "", Delay = 0, IsRandomTimeSpawn = false, IgnoreMaxBots = true, } ); } } protected void HandleModEvent(SeasonalEvent seasonalEvent, Config globalConfig) { if (seasonalEvent.Settings?.EnableChristmasHideout ?? false) { globalConfig.EventType = globalConfig.EventType.Where(x => x != EventType.None).ToList(); globalConfig.EventType.Add(EventType.Christmas); } if (seasonalEvent.Settings?.EnableHalloweenHideout ?? false) { globalConfig.EventType = globalConfig.EventType.Where(x => x != EventType.None).ToList(); globalConfig.EventType.Add(EventType.Halloween); globalConfig.EventType.Add(EventType.HalloweenIllumination); } if (seasonalEvent.Settings?.AddEventGearToBots ?? false) { AddEventGearToBots(seasonalEvent.Type); } if (seasonalEvent.Settings?.AddEventLootToBots ?? false) { AddEventLootToBots(seasonalEvent.Type); } if (seasonalEvent.Settings?.EnableSummoning ?? false) { EnableHalloweenSummonEvent(); AddEventBossesToMaps("halloweensummon"); } if (seasonalEvent.Settings?.ZombieSettings?.Enabled ?? false) { ConfigureZombies(seasonalEvent.Settings.ZombieSettings); } if (seasonalEvent.Settings?.ForceSeason != null) { _weatherConfig.OverrideSeason = seasonalEvent.Settings.ForceSeason; } if (seasonalEvent.Settings?.AdjustBotAppearances ?? false) { AdjustBotAppearanceValues(seasonalEvent.Type); } } /// /// Send gift to player if they have not already received it /// /// Player to send gift to /// Key of gift to give protected void GiveGift(MongoId playerId, string giftKey) { var giftData = giftService.GetGiftById(giftKey); if (!profileHelper.PlayerHasReceivedMaxNumberOfGift(playerId, giftKey, giftData.MaxToSendPlayer ?? 5)) { giftService.SendGiftToPlayer(playerId, giftKey); } } /// /// Get the underlying bot type for an event bot e.g. `peacefullZryachiyEvent` will return `bossZryachiy` /// /// Event bot role type /// Bot role as string public string GetBaseRoleForEventBot(string? eventBotRole) { return _seasonalEventConfig.EventBotMapping.GetValueOrDefault(eventBotRole, null); } /// /// Force the weather to be snow /// public void EnableSnow() { _weatherConfig.OverrideSeason = Season.WINTER; } }