namespace Supercell.Laser.Server.Logic { using Newtonsoft.Json; using Supercell.Laser.Logic.Command.Home; using Supercell.Laser.Logic.Home; using Supercell.Laser.Logic.Home.Gatcha; using Supercell.Laser.Logic.Home.Items; using Supercell.Laser.Logic.Home.Structures; using Supercell.Laser.Logic.Listener; using Supercell.Laser.Logic.Message.Home; using Supercell.Laser.Server.Database; using Supercell.Laser.Server.Database.Models; using Supercell.Laser.Server.Networking.Session; using Supercell.Laser.Server.Settings; public static class SeasonResetManager { private static readonly string SourceStatePath = Supercell.Laser.Logic.Util.LogicHelperConfig.timersPath; private static readonly int[] TrophyRangesStart = { 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000, 1050, 1100, 1150, 1200, 1250, 1300, 1350, 1400, 1450, 1500, 1550, 1600, 1650, 1700, 1750, 1800, 1850, 1900, 1950, 2000, 2050, 2100, 2150, 2200, 2250, 2300, 2350, 2400, 2450, 2500, 2550, 2600, 2650, 2700, 2750, 2800, 2850, 2900, 2950, 3000, 3050, 3100, 3150, 3200, 3250, 3300, 3350, 3400, 3450, 3500, 3550, 3600, 3650, 3700, 3750, 3800, 3850, 3900, 3950, 4000, 4050, 4100, 4150, 4200, 4250, 4300, 4350, 4400, 4450, 4500 }; private static readonly int[] TrophyRangesEnd = { 599, 649, 699, 749, 799, 849, 899, 949, 999, 1049, 1099, 1149, 1199, 1249, 1299, 1349, 1399, 1449, 1499, 1549, 1599, 1649, 1699, 1749, 1799, 1849, 1899, 1949, 1999, 2049, 2099, 2149, 2199, 2249, 2299, 2349, 2399, 2449, 2499, 2549, 2599, 2649, 2699, 2749, 2799, 2849, 2899, 2949, 2999, 3049, 3099, 3149, 3199, 3249, 3299, 3349, 3399, 3449, 3499, 3549, 3599, 3649, 3699, 3749, 3799, 3849, 3899, 3949, 3999, 4049, 4099, 4149, 4199, 4249, 4299, 4349, 4399, 4449, 4499, 1000000 }; private static readonly int[] SeasonRewardAmounts = { 70, 120, 160, 200, 220, 240, 260, 280, 300, 320, 340, 360, 380, 400, 420, 440, 460, 480, 500, 520, 540, 560, 580, 600, 620, 640, 660, 680, 700, 720, 740, 760, 780, 800, 820, 840, 860, 880, 900, 920, 940, 960, 980, 1000, 1020, 1040, 1060, 1080, 1100, 1120, 1140, 1160, 1180, 1200, 1220, 1240, 1260, 1280, 1300, 1320, 1340, 1360, 1380, 1400, 1420, 1440, 1460, 1480, 1500, 1520, 1540, 1560, 1580, 1600, 1620, 1640, 1660, 1680, 1700, 1720 }; private static readonly int[] TrophyResetValues = { 525, 550, 600, 650, 700, 725, 750, 775, 800, 825, 850, 875, 900, 925, 950, 975, 1000, 1025, 1050, 1075, 1100, 1125, 1150, 1175, 1200, 1225, 1250, 1275, 1300, 1325, 1350, 1375, 1400, 1425, 1450, 1475, 1500, 1525, 1550, 1575, 1600, 1625, 1650, 1675, 1700, 1725, 1750, 1775, 1800, 1825, 1850, 1875, 1900, 1925, 1950, 1975, 2000, 2025, 2050, 2075, 2100, 2125, 2150, 2175, 2200, 2225, 2250, 2275, 2300, 2325, 2350, 2375, 2400, 2425, 2450, 2475, 2500, 2525, 2550, 2575, 2600, 2625, 2650, 2675, 2700, 2725, 2750, 2775, 2800, 2825, 2850, 2875, 2900, 2925, 2950, 2975, 3000, 3025, 3050, 3075, 3100, 3125, 3150, 3175, 3200, 3225, 3250, 3275, 3300, 3325, 3350, 3375, 3400, 3425, 3450, 3475, 3500, 3525, 3550, 3575, 3600, 3625, 3650, 3675, 3700, 3725, 3750, 3775, 3800, 3825, 3850, 3875, 3900, 3925, 3950, 3975, 4000 }; private static readonly object SyncRoot = new object(); private static Thread WorkerThread; private static bool Started; public static void Init() { if (Started) { return; } Started = true; EnsureStateFile(); WorkerThread = new Thread(RunLoop) { IsBackground = true, Name = "SeasonResetManager" }; WorkerThread.Start(); } private static void RunLoop() { while (Started) { try { SeasonResetState state = LoadState(); TimeSpan interval = ParseSeasonInterval(state.SeasonInterval); if (interval <= TimeSpan.Zero) { Thread.Sleep(TimeSpan.FromSeconds(30)); continue; } DateTime utcNow = DateTime.UtcNow; if (utcNow >= state.NextReset) { lock (SyncRoot) { state = LoadState(); interval = ParseSeasonInterval(state.SeasonInterval); if (interval <= TimeSpan.Zero) { continue; } utcNow = DateTime.UtcNow; if (utcNow >= state.NextReset) { Logger.Info($"Season reset started. nextReset={state.NextReset:O}"); long processedCount = ResetAllAccounts(); DateTime nextResetUtc = state.NextReset; while (utcNow >= nextResetUtc) { nextResetUtc = nextResetUtc.Add(interval); } SaveState(new SeasonResetState { LastResetUtc = utcNow, NextReset = nextResetUtc, SeasonInterval = state.SeasonInterval }); Logger.Success($"Season reset finished. Processed={processedCount}, nextReset={nextResetUtc:O}"); } } } } catch (Exception exception) { Logger.Exception("Season reset loop failed", exception); } Thread.Sleep(TimeSpan.FromSeconds(15)); } } private static long ResetAllAccounts() { long lastAccId = Accounts.GetMaxAvatarId(); if (lastAccId <= 0) { return 0; } long processedCount = 0; for (long accId = 1; accId <= lastAccId; accId++) { bool isOnline = Sessions.IsSessionActive(accId); Account account = isOnline ? Accounts.Load(accId) : Accounts.LoadNoCache(accId); if (account == null) { continue; } Notification notification = BuildSeasonResetNotification(account); if (notification != null) { account.Home.NotificationFactory.Add(notification); ApplySeasonStarPoints(account, isOnline, notification); } Accounts.Save(account); if (notification != null && isOnline) { AvailableServerCommandMessage serverCommandMessage = new AvailableServerCommandMessage(); serverCommandMessage.Command = new LogicAddNotificationCommand { Notification = notification }; LogicServerListener.Instance.GetGameListener(accId)?.SendTCPMessage(serverCommandMessage); } processedCount++; if (processedCount % 100 == 0 || processedCount == lastAccId) { double percent = (double)processedCount / lastAccId * 100; Logger.Info($"Season reset progress: {processedCount}/{lastAccId} ({percent:F2}%)"); } } return processedCount; } private static Notification BuildSeasonResetNotification(Account account) { if (account.Avatar.Trophies < 550) { return null; } List heroIds = new(); List heroTrophies = new(); List resetTrophies = new(); List starPointsAwarded = new(); foreach (Hero hero in account.Avatar.Heroes) { if (hero.Trophies < TrophyRangesStart[0]) { continue; } heroIds.Add(hero.CharacterId); heroTrophies.Add(hero.Trophies); for (int index = 0; index < TrophyRangesStart.Length; index++) { if (hero.Trophies < TrophyRangesStart[index] || hero.Trophies > TrophyRangesEnd[index]) { continue; } if (TrophyRangesStart[index] != 4500) { int trophiesReset = hero.Trophies - TrophyResetValues[index]; hero.Trophies = TrophyResetValues[index]; resetTrophies.Add(trophiesReset); starPointsAwarded.Add(SeasonRewardAmounts[index]); } else { int extraTrophies = hero.Trophies - 1440; extraTrophies /= 2; int trophiesReset = hero.Trophies - TrophyResetValues[index] - extraTrophies; hero.Trophies = TrophyResetValues[index] + extraTrophies; starPointsAwarded.Add(SeasonRewardAmounts[index] + (extraTrophies / 2)); resetTrophies.Add(trophiesReset); } break; } } if (heroIds.Count == 0) { return null; } return new Notification { Id = 79, IsViewed = false, RewardClaimed = true, MessageEntry = "Trophy season ended", HeroesIds = heroIds, HeroesTrophies = heroTrophies, HeroesTrophiesReseted = resetTrophies, StarpointsAwarded = starPointsAwarded, TimePassed = ToUnixSeconds(DateTime.UtcNow) }; } private static void ApplySeasonStarPoints(Account account, bool isOnline, Notification notification) { int totalStarPoints = notification.StarpointsAwarded.Sum(); if (totalStarPoints <= 0) { return; } if (isOnline) { HomeMode homeMode = LogicServerListener.Instance.GetHomeMode(account.AccountId); LogicGameListener gameListener = LogicServerListener.Instance.GetGameListener(account.AccountId); if (homeMode != null && gameListener != null) { LogicGiveDeliveryItemsCommand delivery = new LogicGiveDeliveryItemsCommand(); DeliveryUnit unit = new DeliveryUnit(100); GatchaDrop reward = new GatchaDrop(12) { Count = totalStarPoints }; unit.AddDrop(reward); delivery.DeliveryUnits.Add(unit); delivery.Execute(homeMode); AvailableServerCommandMessage deliveryMessage = new AvailableServerCommandMessage { Command = delivery }; gameListener.SendTCPMessage(deliveryMessage); return; } } account.Avatar.AddStarPoints(totalStarPoints); } private static int ToUnixSeconds(DateTime utcNow) { long timestamp = new DateTimeOffset(utcNow).ToUnixTimeSeconds(); return Convert.ToInt32(timestamp % int.MaxValue); } private static void EnsureStateFile() { SeasonResetState currentState = LoadState(); TimeSpan interval = ParseSeasonInterval(currentState.SeasonInterval); if (interval <= TimeSpan.Zero) { return; } if (File.Exists(GetStatePath()) && currentState.NextReset != default) { return; } DateTime utcNow = DateTime.UtcNow; SaveState(new SeasonResetState { LastResetUtc = utcNow, NextReset = utcNow.Add(interval), SeasonInterval = currentState.SeasonInterval }); } private static SeasonResetState LoadState() { string statePath = GetStatePath(); TimerStateDocument defaults = CreateDefaultDocument(); if (!File.Exists(statePath)) { return new SeasonResetState { LastResetUtc = defaults.LastReset ?? DateTime.UtcNow, NextReset = defaults.NextReset ?? DateTime.UtcNow.AddDays(7), SeasonInterval = defaults.SeasonInterval }; } TimerStateDocument document = JsonConvert.DeserializeObject(File.ReadAllText(statePath)) ?? defaults; if (string.IsNullOrWhiteSpace(document.SeasonInterval)) { document.SeasonInterval = defaults.SeasonInterval; } DateTime lastResetUtc = document.LastReset ?? DateTime.UtcNow; TimeSpan interval = ParseSeasonInterval(document.SeasonInterval); if (interval <= TimeSpan.Zero) { interval = TimeSpan.FromDays(7); document.SeasonInterval = "7d"; } DateTime nextResetUtc = document.NextReset ?? lastResetUtc.Add(interval); SeasonResetState state = new SeasonResetState { LastResetUtc = lastResetUtc, NextReset = nextResetUtc, SeasonInterval = document.SeasonInterval }; state.LastResetUtc = DateTime.SpecifyKind(state.LastResetUtc, DateTimeKind.Utc); state.NextReset = DateTime.SpecifyKind(state.NextReset, DateTimeKind.Utc); DateTime expectedNextResetUtc = state.LastResetUtc.Add(interval); bool invalidNextReset = state.NextReset <= state.LastResetUtc; bool mismatchedNextReset = Math.Abs((state.NextReset - expectedNextResetUtc).TotalSeconds) > 1 && document.NextReset.HasValue; if (invalidNextReset || mismatchedNextReset) { state.NextReset = expectedNextResetUtc; SaveState(state); } return state; } private static TimeSpan ParseSeasonInterval(string value) { if (string.IsNullOrWhiteSpace(value)) { return TimeSpan.Zero; } string normalized = value.Trim().ToLowerInvariant(); char suffix = normalized[^1]; if (!double.TryParse(normalized[..^1], out double amount) || amount <= 0) { return TimeSpan.Zero; } return suffix switch { 's' => TimeSpan.FromSeconds(amount), 'm' => TimeSpan.FromMinutes(amount), 'h' => TimeSpan.FromHours(amount), 'd' => TimeSpan.FromDays(amount), 'w' => TimeSpan.FromDays(amount * 7), _ => TimeSpan.Zero }; } private static void SaveState(SeasonResetState state) { string statePath = GetStatePath(); string directoryPath = Path.GetDirectoryName(statePath); if (!string.IsNullOrWhiteSpace(directoryPath)) { Directory.CreateDirectory(directoryPath); } TimerStateDocument document = File.Exists(statePath) ? JsonConvert.DeserializeObject(File.ReadAllText(statePath)) ?? CreateDefaultDocument() : CreateDefaultDocument(); document.LastReset = state.LastResetUtc; document.NextReset = state.NextReset; document.SeasonInterval = state.SeasonInterval; File.WriteAllText(statePath, JsonConvert.SerializeObject(document, Formatting.Indented)); } private static string GetStatePath() { string runtimePath = Path.Combine(AppContext.BaseDirectory, "Json", "season_reset_state.json"); if (File.Exists(runtimePath)) { return runtimePath; } return SourceStatePath; } private sealed class SeasonResetState { public DateTime LastResetUtc { get; set; } public DateTime NextReset { get; set; } public string SeasonInterval { get; set; } } private sealed class TimerStateDocument { [JsonProperty("seasonEnd")] public DateTime? SeasonEnd { get; set; } [JsonProperty("brawlPass")] public DateTime? BrawlPass { get; set; } [JsonProperty("powerPlay")] public DateTime? PowerPlay { get; set; } [JsonProperty("lastReset")] public DateTime? LastReset { get; set; } [JsonProperty("nextReset")] public DateTime? NextReset { get; set; } [JsonProperty("seasonInterval")] public string SeasonInterval { get; set; } } private static TimerStateDocument CreateDefaultDocument() { return new TimerStateDocument { SeasonEnd = new DateTime(2026, 12, 31, 12, 0, 0, DateTimeKind.Utc), BrawlPass = new DateTime(2026, 12, 31, 12, 0, 0, DateTimeKind.Utc), PowerPlay = new DateTime(2026, 5, 31, 12, 0, 0, DateTimeKind.Utc), LastReset = DateTime.UtcNow, NextReset = DateTime.UtcNow.AddDays(7), SeasonInterval = "7d" }; } } }