namespace Supercell.Laser.Server.Logic.Game { using Supercell.Laser.Logic.Data; using Supercell.Laser.Logic.Home.Items; using Supercell.Laser.Server.Database; using Supercell.Laser.Titan.Json; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Supercell.Laser.Logic.Command.Home; using Supercell.Laser.Logic.Message.Home; using Supercell.Laser.Server.Networking.Session; using EventData = Supercell.Laser.Logic.Home.Items.EventData; using System.Configuration; using Supercell.Laser.Logic.Data.Helper; using Supercell.Laser.Server.PlayerMapUtils; using Supercell.Laser.Logic.Home.Structures; using Supercell.Laser.Logic.Battle.Structures; using System.IO; public static class Events { public const int REFRESH_MINUTES = 120; private static readonly string GameplayJsonPath = "/root/36.218/Supercell.Laser.Server/gameplay.json"; private static Timer RefreshTimer; private static Timer FileWatcherTimer; private static EventSlotConfig[] ConfigSlots; private static Dictionary Slots; private static Dictionary> SlotsCollected = new(); private static DateTime LastFileWriteTime; private static string CachedGameplayJson; private static DateTime GetMSKTimeToRefresh(string gameMode) { int hour = gameMode switch { "Bounty" or "Heist" => 17, "Showdown" or "DuoShowdown" => 0, "GemGrab" => 12, "KingOfHill" => 22, "BigGame" => 0, "HoldTheBall" => 0, "BasketBrawl" => 0, "VolleyBrawl" => 0, "BrawlBall" => 0, "ProtectKing" => 0, "Payload" => 0, "Invasion" => 0, "TagTeam" => 0, "BossFight_TownCrush" => 0, "CTF" => 0, _ => 4 }; TimeZoneInfo mskZone = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time"); DateTime mskNow = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, mskZone); DateTime targetTimeToday = new DateTime(mskNow.Year, mskNow.Month, mskNow.Day, hour, 0, 0); if (mskNow >= targetTimeToday) { targetTimeToday = targetTimeToday.AddDays(1); } return targetTimeToday; } public static void Init() { LoadSettings(); Slots = new Dictionary(); GenerateEvents(); CachedGameplayJson = File.ReadAllText(GameplayJsonPath); LastFileWriteTime = File.GetLastWriteTimeUtc(GameplayJsonPath); RefreshTimer = new Timer(async _ => await CheckEventsRefresh(), null, TimeSpan.Zero, TimeSpan.FromMinutes(0.1)); FileWatcherTimer = new Timer(async _ => await CheckFileChanges(), null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); } private static async Task CheckFileChanges() { try { DateTime currentWriteTime = File.GetLastWriteTimeUtc(GameplayJsonPath); if (currentWriteTime <= LastFileWriteTime) return; string currentJson = File.ReadAllText(GameplayJsonPath); if (currentJson == CachedGameplayJson) return; CachedGameplayJson = currentJson; LastFileWriteTime = currentWriteTime; EventSlotConfig[] newConfigs = ParseSlotsFromJson(currentJson); HashSet affectedSlots = new HashSet(); for (int i = 0; i < newConfigs.Length; i++) { EventSlotConfig newConfig = newConfigs[i]; EventSlotConfig oldConfig = ConfigSlots.FirstOrDefault(c => c.Slot == newConfig.Slot); if (oldConfig == null) { ConfigSlots = ConfigSlots.Append(newConfig).ToArray(); RegenerateSlotInternal(newConfig); affectedSlots.Add(newConfig.Slot); continue; } if (HasConfigChanged(oldConfig, newConfig)) { ConfigSlots[Array.IndexOf(ConfigSlots, oldConfig)] = newConfig; RegenerateSlotInternal(newConfig); affectedSlots.Add(newConfig.Slot); } } HashSet newSlotIds = new HashSet(newConfigs.Select(c => c.Slot)); List removedConfigs = ConfigSlots.Where(c => !newSlotIds.Contains(c.Slot)).ToList(); foreach (var removed in removedConfigs) { Slots.Remove(removed.Slot); affectedSlots.Add(removed.Slot); } ConfigSlots = ConfigSlots.Where(c => newSlotIds.Contains(c.Slot)).ToArray(); if (affectedSlots.Count > 0) { await NotifyPlayers(); } } catch (Exception ex) { Logger.Error($"Error checking file changes: {ex}"); } } private static EventSlotConfig[] ParseSlotsFromJson(string json) { LogicJSONObject settings = LogicJSONParser.ParseObject(json); LogicJSONArray slots = settings.GetJSONArray("slots"); EventSlotConfig[] configs = new EventSlotConfig[slots.Size()]; for (int i = 0; i < slots.Size(); i++) { EventSlotConfig config = new EventSlotConfig(); LogicJSONObject slot = slots.GetJSONObject(i); config.Slot = slot.GetJSONNumber("slot").GetIntValue(); LogicJSONArray gameModes = slot.GetJSONArray("game_modes"); LogicJSONArray locations = slot.GetJSONArray("locations"); LogicJSONArray modifi = slot.GetJSONArray("modificators"); config.AllowedModes = new string[gameModes.Size()]; if (locations != null && locations.Size() > 0) { for (int l = 0; l < locations.Size(); l++) { LogicJSONNumber locationNode = locations.GetJSONNumber(l); if (locationNode != null) config.Locations.Add(locationNode.GetIntValue()); } } else { LogicJSONNumber locationNode = slot.GetJSONNumber("location"); if (locationNode != null) config.location = locationNode.GetIntValue(); } LogicJSONNumber playerMapIdNode = slot.GetJSONNumber("player_map_id"); if (playerMapIdNode != null) { config.PlayerMapId = playerMapIdNode.GetIntValue(); } for (int j = 0; j < gameModes.Size(); j++) { config.AllowedModes[j] = gameModes.GetJSONString(j).GetStringValue(); } if (modifi != null) { for (int k = 0; k < modifi.Size(); k++) { LogicJSONNumber numberNode = modifi.GetJSONNumber(k); if (numberNode != null) config.modifi.Add(numberNode.GetIntValue()); } } configs[i] = config; } return configs; } private static bool HasConfigChanged(EventSlotConfig oldConfig, EventSlotConfig newConfig) { if (oldConfig.Slot != newConfig.Slot) return true; if (oldConfig.location != newConfig.location) return true; if (oldConfig.PlayerMapId != newConfig.PlayerMapId) return true; if (oldConfig.AllowedModes == null && newConfig.AllowedModes != null) return true; if (oldConfig.AllowedModes != null && newConfig.AllowedModes == null) return true; if (oldConfig.AllowedModes != null && newConfig.AllowedModes != null) { if (!oldConfig.AllowedModes.SequenceEqual(newConfig.AllowedModes)) return true; } if (oldConfig.Locations == null && newConfig.Locations != null) return true; if (oldConfig.Locations != null && newConfig.Locations == null) return true; if (oldConfig.Locations != null && newConfig.Locations != null) { if (!oldConfig.Locations.SequenceEqual(newConfig.Locations)) return true; } if (oldConfig.modifi == null && newConfig.modifi != null) return true; if (oldConfig.modifi != null && newConfig.modifi == null) return true; if (oldConfig.modifi != null && newConfig.modifi != null) { if (!oldConfig.modifi.SetEquals(newConfig.modifi)) return true; } return false; } private static async Task CheckEventsRefresh(object state = null) { try { DateTime now = DateTime.UtcNow; foreach (var config in ConfigSlots) { if (!Slots.TryGetValue(config.Slot, out var currentEvent)) { RegenerateSlotInternal(config); continue; } } } catch (Exception ex) { Logger.Error($"error refreshing events: {ex}"); } } private static void GenerateEvents() { foreach (var config in ConfigSlots) { var newEvent = GenerateEvent(config); if (newEvent == null) continue; Slots[config.Slot] = newEvent; } } private static void RegenerateSlotInternal(EventSlotConfig config) { var newEvent = GenerateEvent(config); if (newEvent == null) return; Slots[config.Slot] = newEvent; Matchmaking.UpdateSlot(newEvent); } private static async Task NotifyPlayers() { var events = GetEvents(); Parallel.ForEach(Sessions.ActiveSessions.Values.ToArray(), session => { try { session.Home.Home.Events = events; var command = new LogicDayChangedCommand { Home = session.Home.Home }; session.Connection.Send(new AvailableServerCommandMessage { Command = command }); } catch { } }); } private static EventData GenerateEvent(EventSlotConfig config) { string[] gameModes = config.AllowedModes; int slot = config.Slot; int presetlocation = config.location; List presetLocations = config.Locations; HashSet modifi = config.modifi; // Fix 11: Сбрасываем список игроков, собравших бонус, при каждой генерации // нового EventData для данного слота. Раньше SlotsCollected[slot] никогда // не очищался при ротации ивента — список рос бессрочно, постепенно накапливая // id всех когда-либо заходивших игроков и не освобождая память никогда. SlotsCollected[slot] = new List(); if (config.PlayerMapId > 0) { var playerMap = PlayerMapManager.GetPlayerMap(config.PlayerMapId); if (playerMap != null) { var eventData = new EventData { Slot = slot, // LocationId = null, EndTime = TimeZoneInfo.ConvertTimeToUtc(GetMSKTimeToRefresh("Bounty"), TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time")), BattlePlayerMap = CreateBattlePlayerMap(playerMap) }; return eventData; } } LocationData location = null; if (presetLocations != null && presetLocations.Count > 0) { Random rand = new Random(); int tries = 0; while (tries < presetLocations.Count && location == null) { int locationId = presetLocations[rand.Next(0, presetLocations.Count)]; location = DataTables.Get(DataType.Location).GetDataWithId(locationId); tries++; } if (location == null) { foreach (int locationId in presetLocations) { location = DataTables.Get(DataType.Location).GetDataWithId(locationId); if (location != null) break; } } if (location == null) { Logger.Error($"preset locations for slot {slot} not found!"); return null; } } else if (presetlocation != 0) { location = DataTables.Get(DataType.Location).GetDataWithId(presetlocation); if (location == null) { Logger.Error($"preset location {presetlocation} not found!"); return null; } } else { location = GetRandomLocationForModes(gameModes); } if (location == null) { Logger.Error($" slot not found {slot}"); return new EventData { Slot = slot, LocationId = GlobalId.CreateGlobalId(15, 14), EndTime = TimeZoneInfo.ConvertTimeToUtc(GetMSKTimeToRefresh("Bounty"), TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time")), }; } DateTime refreshTime = GetMSKTimeToRefresh(location.GameMode); DateTime refreshTimeUTC = TimeZoneInfo.ConvertTimeToUtc(refreshTime, TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time")); return new EventData { Slot = slot, LocationId = location.GetGlobalId(), EndTime = refreshTimeUTC, }; } private static BattlePlayerMap CreateBattlePlayerMap(LogicPlayerMap playerMap) { var battleMap = new BattlePlayerMap(); battleMap.MapId = playerMap.MapId; battleMap.MapName = playerMap.MapName; battleMap.AvatarName = playerMap.AvatarName; battleMap.GMV = playerMap.GameModeVariation; battleMap.MapEnvironmentData = playerMap.MapEnvironmentData; battleMap.MapData = playerMap.CompressedMapData; battleMap.State = playerMap.State; battleMap.AccountId = playerMap.AccountId; return battleMap; } private static void LoadSettings() { LogicJSONObject settings = LogicJSONParser.ParseObject(File.ReadAllText(GameplayJsonPath)); LogicJSONArray slots = settings.GetJSONArray("slots"); ConfigSlots = new EventSlotConfig[slots.Size()]; for (int i = 0; i < slots.Size(); i++) { EventSlotConfig config = new EventSlotConfig(); LogicJSONObject slot = slots.GetJSONObject(i); config.Slot = slot.GetJSONNumber("slot").GetIntValue(); LogicJSONArray gameModes = slot.GetJSONArray("game_modes"); LogicJSONArray locations = slot.GetJSONArray("locations"); LogicJSONArray modifi = slot.GetJSONArray("modificators"); config.AllowedModes = new string[gameModes.Size()]; if (locations != null && locations.Size() > 0) { for (int l = 0; l < locations.Size(); l++) { LogicJSONNumber locationNode = locations.GetJSONNumber(l); if (locationNode != null) config.Locations.Add(locationNode.GetIntValue()); } } else { LogicJSONNumber locationNode = slot.GetJSONNumber("location"); if (locationNode != null) config.location = locationNode.GetIntValue(); } LogicJSONNumber playerMapIdNode = slot.GetJSONNumber("player_map_id"); if (playerMapIdNode != null) { config.PlayerMapId = playerMapIdNode.GetIntValue(); } for (int j = 0; j < gameModes.Size(); j++) { config.AllowedModes[j] = gameModes.GetJSONString(j).GetStringValue(); } if (modifi != null) { for (int k = 0; k < modifi.Size(); k++) { LogicJSONNumber numberNode = modifi.GetJSONNumber(k); if (numberNode != null) config.modifi.Add(numberNode.GetIntValue()); } } ConfigSlots[i] = config; } } public static EventData[] GetEventsById(int a, long id) { try { EventData[] events = Slots.Values.ToArray(); int index = 0; foreach (EventData e in events) { if (SlotsCollected.ContainsKey(e.Slot) && SlotsCollected[e.Slot].Contains(id)) { e.IsBonusCollected = true; } else { e.IsBonusCollected = false; } e.PowerPlayGamesPlayed = a; events[index] = e; index++; } return events; } catch { return Slots.Values.ToArray(); } } private static LocationData GetRandomLocationForModes(string[] gameModes) { List locations = new(); var table = DataTables.Get(DataType.Location); for (int i = 0; i < table.Count; i++) { var location = table.GetDataWithId(i); if (location == null) continue; if (location.Disabled) continue; if (gameModes.Contains(location.GameMode)) { locations.Add(location); } } if (locations.Count == 0) return null; return locations[new Random().Next(locations.Count)]; } public static bool CollectEvent(long id, int slot) { if (!HasSlot(slot)) return false; if (SlotsCollected[slot].Contains(id)) return false; SlotsCollected[slot].Add(id); return true; } public static EventData GetEvent(int slot) => Slots.TryGetValue(slot, out var eventData) ? eventData : null; public static bool HasSlot(int slot) => Slots.ContainsKey(slot); public static EventData[] GetEvents() => Slots.Values.ToArray(); private class EventSlotConfig { public int Slot { get; set; } public string[] AllowedModes { get; set; } public int location; public List Locations = new(); public HashSet modifi = new(); public long PlayerMapId; } private class ServerEventData { public int Slot { get; set; } public int LocationId { get; set; } public DateTime EndTime { get; set; } } } }