using System.Collections.Immutable; using System.Diagnostics; using Phantom.Common.Data.Minecraft; using Phantom.Utils.Logging; using Serilog; namespace Phantom.Controller.Minecraft; public sealed class MinecraftVersions : IDisposable { private static readonly ILogger Logger = PhantomLogger.Create<MinecraftVersions>(); private static readonly TimeSpan CacheRetentionTime = TimeSpan.FromMinutes(10); private readonly MinecraftVersionApi api = new (); private readonly Stopwatch cacheTimer = new (); private readonly SemaphoreSlim cacheSemaphore = new (1, 1); private bool IsCacheNotExpired => cacheTimer.IsRunning && cacheTimer.Elapsed < CacheRetentionTime; private ImmutableArray<MinecraftVersion>? cachedVersions; private readonly Dictionary<string, FileDownloadInfo?> cachedServerExecutables = new (); public void Dispose() { api.Dispose(); cacheSemaphore.Dispose(); } public async Task<ImmutableArray<MinecraftVersion>> GetVersions(CancellationToken cancellationToken) { return await GetCachedObject(() => cachedVersions != null, () => cachedVersions.GetValueOrDefault(), v => cachedVersions = v, LoadVersions, cancellationToken); } private async Task<ImmutableArray<MinecraftVersion>> LoadVersions(CancellationToken cancellationToken) { ImmutableArray<MinecraftVersion> versions = await api.GetVersions(cancellationToken); Logger.Information("Refreshed Minecraft version cache, {Versions} version(s) found.", versions.Length); return versions; } public async Task<FileDownloadInfo?> GetServerExecutableInfo(string version, CancellationToken cancellationToken) { var versions = await GetVersions(cancellationToken); return await GetCachedObject(() => cachedServerExecutables.ContainsKey(version), () => cachedServerExecutables[version], v => cachedServerExecutables[version] = v, ct => LoadServerExecutableInfo(versions, version, ct), cancellationToken); } private async Task<FileDownloadInfo?> LoadServerExecutableInfo(ImmutableArray<MinecraftVersion> versions, string version, CancellationToken cancellationToken) { var info = await api.GetServerExecutableInfo(versions, version, cancellationToken); if (info == null) { Logger.Information("Refreshed Minecraft {Version} server executable cache, no file found.", version); } else { Logger.Information("Refreshed Minecraft {Version} server executable cache, found file: {Url}.", version, info.DownloadUrl); } return info; } private async Task<T> GetCachedObject<T>(Func<bool> isLoaded, Func<T> fieldGetter, Action<T> fieldSetter, Func<CancellationToken, Task<T>> fieldLoader, CancellationToken cancellationToken) { if (IsCacheNotExpired && isLoaded()) { return fieldGetter(); } await cacheSemaphore.WaitAsync(cancellationToken); try { if (IsCacheNotExpired && isLoaded()) { return fieldGetter(); } T result = await fieldLoader(cancellationToken); fieldSetter(result); cacheTimer.Restart(); return result; } finally { cacheSemaphore.Release(); } } }