using System.IO; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Configuration; using Torch.API.Managers; using Torch.Managers; namespace Maintenance.Managers; public class MaintenanceScheduleManager(string storagePath) : IManager { [Manager.Dependency] private readonly MaintenanceManager _maintenanceManager = null!; [Manager.Dependency] private readonly ConfigManager _configManager = null!; [Manager.Dependency] private readonly LanguageManager _languageManager = null!; [Manager.Dependency] private readonly IChatManagerServer _chatManager = null!; private MaintenanceSchedule _currentSchedule = MaintenanceSchedule.Default; private readonly FileInfo _scheduleFile = new(Path.Combine(storagePath, ".schedule")); public MaintenanceSchedule CurrentSchedule { get => _currentSchedule; private set { if (value != _currentSchedule) { using var file = _scheduleFile.Create(); JsonSerializer.Serialize(file, value); } _currentSchedule = value; } } private readonly CancellationTokenSource _cancellationTokenSource = new(); public void Attach() { Scheduler(); if (!_scheduleFile.Exists) return; using (var file = _scheduleFile.OpenRead()) _currentSchedule = JsonSerializer.Deserialize(file)!; _scheduleFile.Delete(); if (_currentSchedule is not { StartTime: null, EndTime: not null }) return; if (!_configManager.Configuration.GetValue(ConfigKeys.ContinueEndTimerAfterRestart)) CurrentSchedule = MaintenanceSchedule.Default; _maintenanceManager.MaintenanceEnabled = true; } private async void Scheduler() { var token = _cancellationTokenSource.Token; var timerSecondsSection = _configManager.Configuration.GetSection(ConfigKeys.TimerBroadcastForSeconds); var timerSeconds = timerSecondsSection.GetChildren().Select(b => b.Get()).ToArray(); using var disposable = timerSecondsSection.GetReloadToken() .RegisterChangeCallback(_ => timerSeconds = timerSecondsSection.GetChildren().Select(b => b.Get()).ToArray(), null); while (!token.IsCancellationRequested) { await Task.Delay(2000, token); switch (_maintenanceManager.MaintenanceEnabled) { case false when _currentSchedule.StartTime.HasValue: { if (timerSeconds.Any(b => Math.Abs(_currentSchedule.Time.TotalSeconds - b) <= 1)) { _chatManager.SendMessageAsOther("Maintenance", Format(_currentSchedule.EndTime.HasValue ? LangKeys.ScheduleTimerBroadcast : LangKeys.StartTimerBroadcast)); } if (_currentSchedule.Time.TotalSeconds <= 1) { _chatManager.SendMessageAsOther("Maintenance", Format(LangKeys.MaintenanceActivated)); _maintenanceManager.MaintenanceEnabled = true; CurrentSchedule = CurrentSchedule with { StartTime = null }; } break; } case true when _currentSchedule.EndTime.HasValue: { if (timerSeconds.Any(b => Math.Abs(_currentSchedule.Duration.TotalSeconds - b) <= 1)) { _chatManager.SendMessageAsOther("Maintenance", Format(LangKeys.EndTimerBroadcast)); } if (_currentSchedule.Duration.TotalSeconds <= 1) { _chatManager.SendMessageAsOther("Maintenance", Format(LangKeys.MaintenanceDeactivated)); _maintenanceManager.MaintenanceEnabled = false; CurrentSchedule = CurrentSchedule with { EndTime = null }; } break; } } } string Format(string key) => _languageManager.Format(key, _currentSchedule); } public void Detach() { _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); } public void ScheduleTimer(TimerType type, TimeSpan duration) { if (_maintenanceManager.MaintenanceEnabled && type == TimerType.Start) throw new InvalidOperationException("Maintenance is already enabled"); if (type == TimerType.Start) CurrentSchedule = CurrentSchedule with { StartTime = DateTimeOffset.Now + duration }; else CurrentSchedule = CurrentSchedule with { EndTime = DateTimeOffset.Now + duration }; } public void ScheduleMaintenance(TimeSpan startTime, TimeSpan endTime) { if (_maintenanceManager.MaintenanceEnabled) throw new InvalidOperationException("Maintenance is already enabled"); var startDateTime = DateTimeOffset.Now + startTime; CurrentSchedule = new(startDateTime, startDateTime + endTime); } public void CancelTimer(TimerType? type) { CurrentSchedule = type switch { TimerType.Start when !_currentSchedule.StartTime.HasValue => throw new InvalidOperationException( "No start timer running"), TimerType.Start => MaintenanceSchedule.Default, TimerType.End when !_currentSchedule.EndTime.HasValue => throw new InvalidOperationException( "No end timer running"), TimerType.End => CurrentSchedule with { EndTime = null }, _ => MaintenanceSchedule.Default }; } } public record MaintenanceSchedule(DateTimeOffset? StartTime, DateTimeOffset? EndTime) { public static MaintenanceSchedule Default => new(null, null); [JsonIgnore] public TimeSpan Time => Round((StartTime ?? throw new InvalidOperationException("No start timer running")) - DateTimeOffset.Now); [JsonIgnore] public TimeSpan Duration => Round((EndTime ?? throw new InvalidOperationException("No end timer running")) - (StartTime ?? DateTimeOffset.Now)); private static TimeSpan Round(TimeSpan timeSpan) => TimeSpan.FromSeconds(Math.Round(timeSpan.TotalSeconds, 0)); } public enum TimerType : byte { Start, End }