using BetterGenshinImpact.Service; using LazyCache; using LazyCache.Providers; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging.Abstractions; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using System; using System.Collections.Concurrent; using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; namespace BetterGenshinImpact.ViewModel; internal static class MapIconImageCache { private const string CacheType = "map-icon-image"; private static readonly HttpClient _http = new(); private static readonly TimeSpan _ttl = TimeSpan.FromDays(20); private static readonly ConcurrentDictionary _decodedCache = new(StringComparer.Ordinal); private static readonly ConcurrentDictionary> _inflight = new(StringComparer.Ordinal); private static readonly TimeProvider _timeProvider; private static readonly MemoryFileCache _fileCache; static MapIconImageCache() { _timeProvider = App.GetService() ?? TimeProvider.System; _fileCache = App.GetService() ?? CreateDefaultMemoryFileCache(); } public static event EventHandler? ImageUpdated; public static ImageSource? TryGet(string url) { if (string.IsNullOrWhiteSpace(url)) { return null; } if (!_decodedCache.TryGetValue(url, out var entry)) { return null; } if (entry.ExpiresAtUtc <= _timeProvider.GetUtcNow()) { _decodedCache.TryRemove(url, out _); return null; } return entry.Image; } public static Task GetAsync(string url, CancellationToken ct) { if (string.IsNullOrWhiteSpace(url)) { return Task.FromResult(null); } var cached = TryGet(url); if (cached != null) { return Task.FromResult(cached); } var task = _inflight.GetOrAdd(url, u => LoadAndDecodeAsync(u, CancellationToken.None)); return task.WaitAsync(ct); } private static async Task LoadAndDecodeAsync(string url, CancellationToken ct) { try { var bytes = await _fileCache.GetOrAddAsync( CacheType, url, _ttl, token => LoadBytesAsync(url, token), obj => obj, payload => payload, ct); if (bytes is not { Length: > 0 }) { return null; } var image = await StaRunner.Instance.InvokeAsync(() => { if (LooksLikeWebp(bytes)) { return LoadWebpFromBytes(bytes); } return LoadBitmapImageFromBytes(bytes); }); if (image == null) { return null; } var entry = new CacheEntry(image, _timeProvider.GetUtcNow().Add(_ttl)); _decodedCache[url] = entry; ImageUpdated?.Invoke(null, url); return image; } catch { return null; } finally { _inflight.TryRemove(url, out _); } } private static async Task LoadBytesAsync(string url, CancellationToken ct) { if (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { return await _http.GetByteArrayAsync(url, ct); } var uri = ToAbsoluteOrRelativeUri(url); return await StaRunner.Instance.InvokeAsync(() => TryReadBytesFromUri(uri)); } private static MemoryFileCache CreateDefaultMemoryFileCache() { var memoryCache = new MemoryCache(new MemoryCacheOptions()); var provider = new MemoryCacheProvider(memoryCache); var appCache = new CachingService(new Lazy(() => provider)); return new MemoryFileCache(appCache, TimeProvider.System, NullLogger.Instance); } private static ImageSource LoadBitmapImageFromBytes(byte[] bytes) { using var ms = new MemoryStream(bytes, writable: false); var bmp = new BitmapImage(); bmp.BeginInit(); bmp.CacheOption = BitmapCacheOption.OnLoad; bmp.StreamSource = ms; bmp.EndInit(); bmp.Freeze(); return bmp; } private static ImageSource LoadWebpFromBytes(byte[] bytes) { using var img = Image.Load(bytes); var width = img.Width; var height = img.Height; var stride = width * 4; var buffer = new byte[stride * height]; img.ProcessPixelRows(accessor => { for (var y = 0; y < height; y++) { var row = accessor.GetRowSpan(y); var rowOffset = y * stride; for (var x = 0; x < width; x++) { var p = row[x]; var a = p.A; var i = rowOffset + x * 4; buffer[i + 0] = Premultiply(p.B, a); buffer[i + 1] = Premultiply(p.G, a); buffer[i + 2] = Premultiply(p.R, a); buffer[i + 3] = a; } } }); var bmp = BitmapSource.Create(width, height, 96, 96, PixelFormats.Pbgra32, null, buffer, stride); bmp.Freeze(); return bmp; } private static byte Premultiply(byte c, byte a) { return (byte)((c * a + 127) / 255); } private static byte[]? TryReadBytesFromUri(Uri uri) { try { if (uri.IsFile && File.Exists(uri.LocalPath)) { return File.ReadAllBytes(uri.LocalPath); } if (Application.GetResourceStream(uri) is { } res) { using var s = res.Stream; using var ms = new MemoryStream(); s.CopyTo(ms); return ms.ToArray(); } if (Application.GetContentStream(uri) is { } content) { using var s = content.Stream; using var ms = new MemoryStream(); s.CopyTo(ms); return ms.ToArray(); } } catch { } return null; } private static bool LooksLikeWebp(byte[] bytes) { if (bytes.Length < 12) { return false; } try { return bytes[0] == (byte)'R' && bytes[1] == (byte)'I' && bytes[2] == (byte)'F' && bytes[3] == (byte)'F' && bytes[8] == (byte)'W' && bytes[9] == (byte)'E' && bytes[10] == (byte)'B' && bytes[11] == (byte)'P'; } catch { return false; } } private static Uri ToAbsoluteOrRelativeUri(string iconUrl) { if (iconUrl.StartsWith("pack://", StringComparison.OrdinalIgnoreCase)) { return new Uri(iconUrl, UriKind.Absolute); } if (Uri.TryCreate(iconUrl, UriKind.Absolute, out var abs)) { return abs; } var basePath = AppContext.BaseDirectory; var fullPath = Path.Combine(basePath, iconUrl); return new Uri(fullPath, UriKind.Absolute); } private readonly record struct CacheEntry(ImageSource Image, DateTimeOffset ExpiresAtUtc); } file sealed class StaRunner { public static StaRunner Instance { get; } = new(); private readonly BlockingCollection _queue = new(); private readonly Thread _thread; private StaRunner() { _thread = new Thread(Run) { IsBackground = true }; _thread.SetApartmentState(ApartmentState.STA); _thread.Start(); } private void Run() { foreach (var action in _queue.GetConsumingEnumerable()) { action(); } } public Task InvokeAsync(Func func) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _queue.Add(() => { try { tcs.SetResult(func()); } catch (Exception ex) { tcs.SetException(ex); } }); return tcs.Task; } }