using SptCommon.Annotations; using Core.Helpers; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Models.Utils; using Core.Servers; namespace Core.Services; [Injectable(InjectionType.Singleton)] public class SeasonalEventService( ISptLogger _logger, DatabaseService _databaseService, GiftService _giftService, LocalisationService _localisationService, BotHelper _botHelper, ProfileHelper _profileHelper, ConfigServer _configServer ) { protected SeasonalEventConfig _seasonalEventConfig = _configServer.GetConfig(); protected QuestConfig _questConfig = _configServer.GetConfig(); protected HttpConfig _httpConfig = _configServer.GetConfig(); protected WeatherConfig _weatherConfig = _configServer.GetConfig(); protected LocationConfig _locationConfig = _configServer.GetConfig(); private List _currentlyActiveEvents = []; private bool _christmasEventActive = false; private bool _halloweenEventActive = false; protected IReadOnlyList _christmasEventItems = [ ItemTpl.FACECOVER_FAKE_WHITE_BEARD, ItemTpl.BARTER_CHRISTMAS_TREE_ORNAMENT_RED, ItemTpl.BARTER_CHRISTMAS_TREE_ORNAMENT_VIOLET, ItemTpl.BARTER_CHRISTMAS_TREE_ORNAMENT_SILVER, ItemTpl.HEADWEAR_DED_MOROZ_HAT, ItemTpl.HEADWEAR_SANTA_HAT, ItemTpl.BACKPACK_SANTAS_BAG, ItemTpl.RANDOMLOOTCONTAINER_NEW_YEAR_GIFT_BIG, ItemTpl.RANDOMLOOTCONTAINER_NEW_YEAR_GIFT_MEDIUM, ItemTpl.RANDOMLOOTCONTAINER_NEW_YEAR_GIFT_SMALL ]; protected IReadOnlyList _halloweenEventItems = [ ItemTpl.FACECOVER_SPOOKY_SKULL_MASK, ItemTpl.RANDOMLOOTCONTAINER_PUMPKIN_RAND_LOOT_CONTAINER, ItemTpl.HEADWEAR_JACKOLANTERN_TACTICAL_PUMPKIN_HELMET, ItemTpl.FACECOVER_FACELESS_MASK, ItemTpl.FACECOVER_JASON_MASK, ItemTpl.FACECOVER_MISHA_MAYOROV_MASK, ItemTpl.FACECOVER_SLENDER_MASK, ItemTpl.FACECOVER_GHOUL_MASK, ItemTpl.FACECOVER_HOCKEY_PLAYER_MASK_CAPTAIN, ItemTpl.FACECOVER_HOCKEY_PLAYER_MASK_BRAWLER, ItemTpl.FACECOVER_HOCKEY_PLAYER_MASK_QUIET ]; /// /// Get an array of christmas items found in bots inventories as loot /// /// array public IEnumerable GetChristmasEventItems() { return _christmasEventItems; } /// /// Get an array of halloween items found in bots inventories as loot /// /// array public IEnumerable GetHalloweenEventItems() { return _halloweenEventItems; } public bool ItemIsChristmasRelated(string itemTpl) { return _christmasEventItems.Contains(itemTpl); } public bool ItemIsHalloweenRelated(string itemTpl) { return _halloweenEventItems.Contains(itemTpl); } /// /// Check if item id exists in christmas or halloween event arrays /// /// item tpl to check for /// public bool ItemIsSeasonalRelated(string 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 List GetInactiveSeasonalEventItems() { var items = new List(); if (!ChristmasEventEnabled()) { items.AddRange(_christmasEventItems); } if (!HalloweenEventEnabled()) { items.AddRange(_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(string questId, SeasonalEventType eventType) { var eventQuestData = _questConfig.EventQuests.GetValueOrDefault(questId, null); if (eventQuestData?.Season == eventType) { return true; } return false; } /// /// Handle activating seasonal events /// public void EnableSeasonalEvents() { if (_currentlyActiveEvents.Count > 0) { 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 = new(); // Add active events to array foreach (var events in seasonalEvents) { if (!events.Enabled) { continue; } if (DateIsBetweenTwoDates(currentDate, 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 = new DateTime(); foreach (var seasonRange in _weatherConfig.SeasonDates) { if ( DateIsBetweenTwoDates( currentDate, seasonRange.StartMonth ?? 0, seasonRange.StartDay ?? 0, seasonRange.EndMonth ?? 0, seasonRange.EndDay ?? 0 ) ) { return seasonRange.SeasonType ?? Season.SUMMER; } } _logger.Warning(_localisationService.GetText("season-no_matching_season_found_for_date")); return Season.SUMMER; } /// /// Does the provided date fit between the two defined dates? /// Excludes year /// Inclusive of end date upto 23 hours 59 minutes /// /// Date to check is between 2 dates /// Lower bound for month /// Lower bound for day /// Upper bound for month /// Upper bound for day /// True when inside date range private bool DateIsBetweenTwoDates(DateTime dateToCheck, int startMonth, int startDay, int endMonth, int endDay) { var eventStartDate = new DateTime(dateToCheck.Year, startMonth, startDay); var eventEndDate = new DateTime(dateToCheck.Year, endMonth, endDay, 23, 59, 0); return dateToCheck >= eventStartDate && dateToCheck <= eventEndDate; } /// /// 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(); List equipmentSlotsToFilter = [EquipmentSlots.FaceCover, EquipmentSlots.Headwear, EquipmentSlots.Backpack, EquipmentSlots.TacticalVest]; List lootContainersToFilter = ["Backpack", "Pockets", "TacticalVest"]; // Remove christmas related equipment foreach (var equipmentSlotKey in equipmentSlotsToFilter) { if (botInventory.Equipment[equipmentSlotKey] is null) { _logger.Warning( _localisationService.GetText( "seasonal-missing_equipment_slot_on_bot", new { equipmentSlot = equipmentSlotKey, botRole = botRole, } ) ); } Dictionary equipment = botInventory.Equipment[equipmentSlotKey]; botInventory.Equipment[equipmentSlotKey] = equipment.Where(i => !_christmasEventItems.Contains(i.Key)).ToDictionary(); } // Remove christmas related loot from loot containers var props = botInventory.Items.GetType().GetProperties(); foreach (var lootContainerKey in lootContainersToFilter) { var prop = (Dictionary?)props.FirstOrDefault(p => p.Name.ToLower() == lootContainerKey.ToLower()).GetValue(botInventory.Items); if (prop is null) { _logger.Warning( _localisationService.GetText( "seasonal-missing_loot_container_slot_on_bot", new { lootContainer = lootContainerKey, botRole = botRole, } ) ); } List tplsToRemove = []; foreach (var tplKey in prop) { if (christmasItems.Contains(tplKey.Key)) { tplsToRemove.Add(tplKey.Key); } } foreach (var tplToRemove in tplsToRemove) { prop.Remove(tplToRemove); } // Get non-christmas items var nonChristmasTpls = prop.Where(tpl => !christmasItems.Contains(tpl.Key)); if (nonChristmasTpls.Count() == 0) { continue; } Dictionary intermediaryDict = new(); foreach (var tpl in nonChristmasTpls) { intermediaryDict[tpl.Key] = prop[tpl.Key]; } // Replace the original containerItems with the updated one prop = intermediaryDict; } } /// /// 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 private void UpdateGlobalEvents(Config globalConfig, SeasonalEvent eventType) { _logger.Success(_localisationService.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(SeasonalEventType.Halloween.ToString()); EnableHalloweenSummonEvent(); AddPumpkinsToScavBackpacks(); RenameBitcoin(); EnableSnow(); break; default: // Likely a mod event HandleModEvent(eventType, globalConfig); break; } } private void ApplyHalloweenEvent(SeasonalEvent eventType, Config globalConfig) { _halloweenEventActive = true; globalConfig.EventType = globalConfig.EventType.Where((x) => x != "None").ToList(); globalConfig.EventType.Add("Halloween"); globalConfig.EventType.Add("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); } private void ApplyChristmasEvent(SeasonalEvent eventType, Config globalConfig) { _christmasEventActive = true; if (eventType.Settings?.EnableChristmasHideout ?? false) { globalConfig.EventType = globalConfig.EventType.Where((x) => x != "None").ToList(); globalConfig.EventType.Add("Christmas"); } AddEventGearToBots(eventType.Type); AddEventLootToBots(eventType.Type); if (eventType.Settings?.EnableSanta ?? false) { AddGifterBotToMaps(); AddLootItemsToGifterDropItemsList(); } EnableDancingTree(); if (eventType.Settings?.AdjustBotAppearances ?? false) { AdjustBotAppearanceValues(eventType.Type); } } private void ApplyNewYearsEvent(SeasonalEvent eventType, Config globalConfig) { _christmasEventActive = true; if (eventType.Settings?.EnableChristmasHideout ?? false) { globalConfig.EventType = globalConfig.EventType.Where((x) => x != "None").ToList(); globalConfig.EventType.Add("Christmas"); } AddEventGearToBots(SeasonalEventType.Christmas); AddEventLootToBots(SeasonalEventType.Christmas); if (eventType.Settings?.EnableSanta ?? false) { AddGifterBotToMaps(); AddLootItemsToGifterDropItemsList(); } EnableDancingTree(); if (eventType.Settings?.AdjustBotAppearances ?? false) { AdjustBotAppearanceValues(SeasonalEventType.Christmas); } } private void AdjustBotAppearanceValues(SeasonalEventType season) { var adjustments = _seasonalEventConfig.BotAppearanceChanges[season]; if (adjustments is null) { return; } foreach (var botTypeKey in adjustments) { var botDb = _databaseService.GetBots().Types[botTypeKey.Key]; if (botDb is null) { continue; } var botAppearanceAdjustments = botTypeKey.Value; foreach (var appearanceKey in botAppearanceAdjustments) { var weightAdjustments = appearanceKey.Value; var props = botDb.BotAppearance.GetType().GetProperties(); foreach (var itemKey in weightAdjustments) { var prop = props.FirstOrDefault(x => x.Name.ToLower() == appearanceKey.Key.ToLower()); var propValue = (Dictionary)prop.GetValue(botDb.BotAppearance); propValue[itemKey.Key] = weightAdjustments[itemKey.Key]; prop.SetValue(botDb.BotAppearance, propValue); } } } } private void ReplaceBotHostility(Dictionary> hostilitySettings) { var locations = _databaseService.GetLocations(); var ignoreList = _locationConfig.NonMaps; var useDefault = hostilitySettings is null; var props = locations.GetType().GetProperties(); foreach (var locationProp in props) { if (ignoreList.Contains(locationProp.Name)) { continue; } Location location = (Location)locationProp.GetValue(locations); if (location?.Base?.BotLocationModifier?.AdditionalHostilitySettings is null) { continue; } List newHostilitySettings = useDefault ? new() : hostilitySettings[locationProp.Name]; if (newHostilitySettings is null) { continue; } location.Base.BotLocationModifier.AdditionalHostilitySettings = new(); } } private void RemoveEntryRequirement(List locationIds) { foreach (var locationId in locationIds) { var location = _databaseService.GetLocation(locationId); location.Base.AccessKeys = []; location.Base.AccessKeysPvE = []; } } public void GivePlayerSeasonalGifts(string 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 zyrach = _databaseService.GetBots().Types.FirstOrDefault(x => x.Key.ToLower() == "bosszryachiy"); var value = new Dictionary(); foreach (var chance in zyrach.Value.BotChances.EquipmentChances) { if (chance.Key.ToLower() == "Scabbard") { value.Add(chance.Key, 100); continue; } value.Add(chance.Key, chance.Value); } zyrach.Value.BotChances.EquipmentChances = value; } /// /// Enable the halloween zryachiy summon event /// protected void EnableHalloweenSummonEvent() { _databaseService.GetGlobals().Configuration.EventSettings.EventActive = true; } protected void ConfigureZombies(ZombieSettings zombieSettings) { throw new NotImplementedException(); } /// /// Get location ids of maps with an infection above 0 /// /// Dict of locations with their infection percentage /// List of location ids protected List GetLocationsWithZombies(Dictionary locationInfections) { throw new NotImplementedException(); } /// /// 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) { throw new NotImplementedException(); } protected void AddEventWavesToMaps(string eventType) { throw new NotImplementedException(); } /// /// Add event bosses to maps /// /// Seasonal event, e.g. HALLOWEEN/CHRISTMAS /// OPTIONAL - Maps to add bosses to protected void AddEventBossesToMaps(string eventType, List mapIdWhitelist = null) { throw new NotImplementedException(); } /// /// Change trader icons to be more event themed (Halloween only so far) /// /// What event is active protected void AdjustTraderIcons(SeasonalEventType eventType) { throw new NotImplementedException(); } /// /// Add lootable items from backpack into patrol.ITEMS_TO_DROP difficulty property /// protected void AddLootItemsToGifterDropItemsList() { var gifterBot = _databaseService.GetBots().Types["gifter"]; var items = gifterBot.BotInventory.Items.Backpack.Keys.ToList(); gifterBot.BotDifficulty["Easy"].Patrol["ITEMS_TO_DROP"] = items; gifterBot.BotDifficulty["Normal"].Patrol["ITEMS_TO_DROP"] = items; gifterBot.BotDifficulty["Hard"].Patrol["ITEMS_TO_DROP"] = items; gifterBot.BotDifficulty["Impossible"].Patrol["ITEMS_TO_DROP"] = items; } /// /// 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) { throw new NotImplementedException(); } /// /// 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) { throw new NotImplementedException(); } /// /// 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() { var enLocale = _databaseService.GetLocales().Global["en"]; enLocale.Value[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} Name"] = "Physical SPT Coin"; enLocale.Value[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} ShortName"] = "0.2SPT"; } /// /// Set Khorovod(dancing tree) chance to 100% on all maps that support it /// protected void EnableDancingTree() { throw new NotImplementedException(); } /// /// Add santa to maps /// protected void AddGifterBotToMaps() { throw new NotImplementedException(); } protected void HandleModEvent(SeasonalEvent seasonalEvent, Config globalConfig) { throw new NotImplementedException(); } /// /// Send gift to player if they have not already received it /// /// Player to send gift to /// Key of gift to give protected void GiveGift(string playerId, string giftKey) { var giftData = _giftService.GetGiftById(giftKey); if (!_profileHelper.PlayerHasRecievedMaxNumberOfGift(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; } }