diff --git a/DXMainClient/DXGUI/GameClass.cs b/DXMainClient/DXGUI/GameClass.cs index 25df2e507..31af0bbea 100644 --- a/DXMainClient/DXGUI/GameClass.cs +++ b/DXMainClient/DXGUI/GameClass.cs @@ -257,7 +257,8 @@ private IServiceProvider BuildServiceProvider(WindowManager windowManager) .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); // singleton xna controls - same instance on each request services diff --git a/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs b/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs index 6c0910c75..48455b943 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs @@ -28,7 +28,7 @@ public GameInformationPanel(WindowManager windowManager, MapLoader mapLoader) DrawMode = ControlDrawMode.UNIQUE_RENDER_TARGET; } - private MapLoader mapLoader; + private readonly MapLoader mapLoader; private XNALabel lblGameInformation; private XNALabel lblGameMode; @@ -43,7 +43,7 @@ public GameInformationPanel(WindowManager windowManager, MapLoader mapLoader) private GenericHostedGame game = null; - private bool disposeTextures = false; + private bool disposeTextures = true; private Texture2D mapTexture = null; private Texture2D noMapPreviewTexture = null; @@ -194,17 +194,28 @@ public void SetInfo(GenericHostedGame game) if (mapLoader != null) { - mapTexture = mapLoader.GameModeMaps.Find(m => m.Map.UntranslatedName.Equals(game.Map, StringComparison.InvariantCultureIgnoreCase) && m.Map.IsPreviewTextureCached())?.Map?.LoadPreviewTexture(); + Map map = mapLoader.GameModeMaps.Find(m => m.Map.UntranslatedName.Equals(game.Map, StringComparison.InvariantCultureIgnoreCase))?.Map; + + if (map != null) + { + if (map.IsPreviewTextureAvailableAsFile()) + { + mapTexture = map.LoadPreviewTexture(); + disposeTextures = true; + } + else + { + mapTexture = AssetLoader.TextureFromImage(mapLoader.MapTextureCacheManager.GetMapTextureIfAvailable(map)); + disposeTextures = true; + } + } + if (mapTexture == null && noMapPreviewTexture != null) { Debug.Assert(!noMapPreviewTexture.IsDisposed, "noMapPreviewTexture should not be disposed."); mapTexture = noMapPreviewTexture; disposeTextures = false; } - else - { - disposeTextures = true; - } } } diff --git a/DXMainClient/DXGUI/Multiplayer/MapTextureCacheManager.cs b/DXMainClient/DXGUI/Multiplayer/MapTextureCacheManager.cs new file mode 100644 index 000000000..56df189c0 --- /dev/null +++ b/DXMainClient/DXGUI/Multiplayer/MapTextureCacheManager.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using DTAClient.Domain.Multiplayer; + +using SixLabors.ImageSharp; + +namespace DTAClient.DXGUI.Multiplayer +{ + public class MapTextureCacheManager : IDisposable + { + public const int MaxCacheSize = 100; + public const int SleepIntervalMS = 100; + + private readonly ConcurrentDictionary mapTextures = []; + + private readonly ConcurrentDictionary missedMaps = []; + + private readonly CancellationTokenSource cancellationTokenSource = new(); + + public MapTextureCacheManager() => + Task.Run(() => MapTextureLoadingService(cancellationTokenSource.Token)); + + public void Dispose() => + cancellationTokenSource?.Cancel(); + + public Image GetMapTextureIfAvailable(Map map) + { + if (mapTextures.TryGetValue(map, out Image image)) + return image; + + if (missedMaps.Count < MaxCacheSize) + missedMaps.TryAdd(map, 0); + + return null; + } + + private async Task MapTextureLoadingService(CancellationToken cancellationToken) + { + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Clear cache if it's too big + if (mapTextures.Count > MaxCacheSize) + mapTextures.Clear(); + + if (!missedMaps.IsEmpty) + { + var missedMapCopy = missedMaps.ToArray(); + foreach ((Map missedMap, _) in missedMapCopy) + { + if (mapTextures.Count > MaxCacheSize) + break; + + missedMaps.TryRemove(missedMap, out _); + + if (mapTextures.ContainsKey(missedMap)) + continue; + + Image image = await Task.Run(missedMap.ExtractMapPreview); + mapTextures.TryAdd(missedMap, image); + } + + } + + await Task.Delay(SleepIntervalMS); + } + } + + } +} diff --git a/DXMainClient/Domain/Multiplayer/GameModeMap.cs b/DXMainClient/Domain/Multiplayer/GameModeMap.cs index b28d2e791..211a1230a 100644 --- a/DXMainClient/Domain/Multiplayer/GameModeMap.cs +++ b/DXMainClient/Domain/Multiplayer/GameModeMap.cs @@ -16,6 +16,9 @@ public GameModeMap(GameMode gameMode, Map map, bool isFavorite) IsFavorite = isFavorite; } + public override string ToString() + => $"{GameMode?.Name} - {Map?.Name}"; + protected bool Equals(GameModeMap other) => Equals(GameMode, other.GameMode) && Equals(Map, other.Map); public override int GetHashCode() diff --git a/DXMainClient/Domain/Multiplayer/Map.cs b/DXMainClient/Domain/Multiplayer/Map.cs index 7e1b4cffd..67b17a567 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -13,7 +13,6 @@ using Color = Microsoft.Xna.Framework.Color; using Point = Microsoft.Xna.Framework.Point; using Utilities = Rampastring.Tools.Utilities; -using static System.Collections.Specialized.BitVector32; using System.Diagnostics; namespace DTAClient.Domain.Multiplayer @@ -680,9 +679,12 @@ private void ParseSpawnIniOptions(IniFile forcedOptionsIni, string spawnIniOptio } } - public bool IsPreviewTextureCached() => + public bool IsPreviewTextureAvailableAsFile() => SafePath.GetFile(ProgramConstants.GamePath, PreviewPath).Exists; + public Image ExtractMapPreview() => + MapPreviewExtractor.ExtractMapPreview(GetCustomMapIniFile()); + /// /// Loads and returns the map preview texture. /// @@ -694,7 +696,7 @@ public Texture2D LoadPreviewTexture() if (!Official) { // Extract preview from the map itself - using Image preview = MapPreviewExtractor.ExtractMapPreview(GetCustomMapIniFile()); + using Image preview = ExtractMapPreview(); if (preview != null) { @@ -917,6 +919,6 @@ private static Point GetIsoTilePixelCoord(int isoTileX, int isoTileY, string[] a protected bool Equals(Map other) => string.Equals(SHA1, other?.SHA1, StringComparison.InvariantCultureIgnoreCase); - public override int GetHashCode() => SHA1 != null ? SHA1.GetHashCode() : 0; + public override int GetHashCode() => SHA1 != null ? SHA1.GetHashCode() : BaseFilePath.GetHashCode(); } } diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index 04c91afa8..7f08cd015 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -6,7 +6,11 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; + using ClientCore; + +using DTAClient.DXGUI.Multiplayer; + using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer @@ -55,6 +59,13 @@ public class MapLoader /// private string[] AllowedGameModes = ClientConfiguration.Instance.AllowedCustomGameModes.Split(','); + public readonly MapTextureCacheManager MapTextureCacheManager; + + public MapLoader(MapTextureCacheManager mapTextureCacheManager) + { + MapTextureCacheManager = mapTextureCacheManager; + } + /// /// Loads multiplayer map info asynchonously. ///