diff --git a/PKHeX.Avalonia/Controls/SlotModel.cs b/PKHeX.Avalonia/Controls/SlotModel.cs index ad682924d..063a01d34 100644 --- a/PKHeX.Avalonia/Controls/SlotModel.cs +++ b/PKHeX.Avalonia/Controls/SlotModel.cs @@ -64,7 +64,10 @@ public partial class SlotModel : ObservableObject public void SetImage(SKBitmap? skBitmap) { + var old = Image; Image = SKBitmapToAvaloniaBitmapConverter.ToAvaloniaBitmap(skBitmap); + old?.Dispose(); + skBitmap?.Dispose(); IsEmpty = skBitmap is null; } } diff --git a/PKHeX.Avalonia/Converters/SKBitmapToAvaloniaBitmapConverter.cs b/PKHeX.Avalonia/Converters/SKBitmapToAvaloniaBitmapConverter.cs index 3d609f8f4..4f6a78b1c 100644 --- a/PKHeX.Avalonia/Converters/SKBitmapToAvaloniaBitmapConverter.cs +++ b/PKHeX.Avalonia/Converters/SKBitmapToAvaloniaBitmapConverter.cs @@ -46,4 +46,22 @@ public class SKBitmapToAvaloniaBitmapConverter : IValueConverter stream.Position = 0; return new Bitmap(stream); } + + /// + /// Converts an SKBitmap to an Avalonia Bitmap and disposes the input SKBitmap. + /// Use this when the caller owns the SKBitmap and will not reuse it after conversion. + /// + public static Bitmap? ToAvaloniaBitmapAndDispose(SKBitmap? skBitmap) + { + if (skBitmap is null) + return null; + try + { + return ToAvaloniaBitmap(skBitmap); + } + finally + { + skBitmap.Dispose(); + } + } } diff --git a/PKHeX.Avalonia/ViewModels/MainWindowViewModel.cs b/PKHeX.Avalonia/ViewModels/MainWindowViewModel.cs index 8bb1160bc..0a06d40db 100644 --- a/PKHeX.Avalonia/ViewModels/MainWindowViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/MainWindowViewModel.cs @@ -27,6 +27,11 @@ public partial class MainWindowViewModel : ObservableObject { private readonly IDialogService _dialogService; + /// + /// Guards against concurrent invocations of . + /// + private bool _isLoading; + /// /// The file path from which the current save file was loaded. /// Used to create an automatic backup before overwriting. @@ -345,6 +350,9 @@ private async Task DumpAllBoxesAsync() public async Task LoadFileAsync(string path) { + if (_isLoading) + return; + _isLoading = true; try { var data = await File.ReadAllBytesAsync(path); @@ -370,6 +378,10 @@ public async Task LoadFileAsync(string path) { await _dialogService.ShowErrorAsync("Load Error", ex.Message); } + finally + { + _isLoading = false; + } } private void LoadSaveFile(SaveFile sav, string path) @@ -937,7 +949,7 @@ private async Task OpenQRDialogAsync() public void HandleFileDrop(string[] files) { - if (files.Length == 0) + if (files.Length == 0 || _isLoading) return; _ = LoadFileAsync(files[0]); diff --git a/PKHeX.Avalonia/ViewModels/PKMEditorViewModel.cs b/PKHeX.Avalonia/ViewModels/PKMEditorViewModel.cs index bed1a7bab..90f3ffc52 100644 --- a/PKHeX.Avalonia/ViewModels/PKMEditorViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/PKMEditorViewModel.cs @@ -994,11 +994,15 @@ private void UpdateBallSprite() try { var skBitmap = SpriteUtil.GetBallSprite(Ball); - BallSprite = SKBitmapToAvaloniaBitmapConverter.ToAvaloniaBitmap(skBitmap); + var old = BallSprite; + BallSprite = SKBitmapToAvaloniaBitmapConverter.ToAvaloniaBitmapAndDispose(skBitmap); + old?.Dispose(); } catch { + var old = BallSprite; BallSprite = null; + old?.Dispose(); } } @@ -2855,7 +2859,9 @@ private void UpdateLegality() { if (Entity is null) { + var old = LegalityImage; LegalityImage = null; + old?.Dispose(); Move1Legal = Move2Legal = Move3Legal = Move4Legal = true; Relearn1Legal = Relearn2Legal = Relearn3Legal = Relearn4Legal = true; return; @@ -2868,7 +2874,13 @@ private void UpdateLegality() var color = valid ? SKColors.Green : SKColors.Red; using var surface = SKSurface.Create(new SKImageInfo(24, 24)); - if (surface is null) { LegalityImage = null; return; } + if (surface is null) + { + var old2 = LegalityImage; + LegalityImage = null; + old2?.Dispose(); + return; + } var canvas = surface.Canvas; canvas.Clear(SKColors.Transparent); using var paint = new SKPaint { Color = color, IsAntialias = true }; @@ -2877,7 +2889,9 @@ private void UpdateLegality() using var image = surface.Snapshot(); using var data = image.Encode(SKEncodedImageFormat.Png, 100); using var ms = new MemoryStream(data.ToArray()); + var oldLegality = LegalityImage; LegalityImage = new Bitmap(ms); + oldLegality?.Dispose(); // Move legality var moves = la.Info.Moves; @@ -2909,7 +2923,9 @@ private void UpdateLegality() } catch { + var old = LegalityImage; LegalityImage = null; + old?.Dispose(); Move1Legal = Move2Legal = Move3Legal = Move4Legal = true; Relearn1Legal = Relearn2Legal = Relearn3Legal = Relearn4Legal = true; } @@ -2921,6 +2937,9 @@ private void UpdateSprite() return; var sprite = Entity.Sprite(); + var old = SpriteImage; SpriteImage = SKBitmapToAvaloniaBitmapConverter.ToAvaloniaBitmap(sprite); + old?.Dispose(); + sprite.Dispose(); } } diff --git a/PKHeX.Avalonia/ViewModels/SAVEditorViewModel.cs b/PKHeX.Avalonia/ViewModels/SAVEditorViewModel.cs index 5ea8ef209..de915feff 100644 --- a/PKHeX.Avalonia/ViewModels/SAVEditorViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/SAVEditorViewModel.cs @@ -303,11 +303,15 @@ private void RefreshBox() try { var wpBitmap = _sav.WallpaperImage(CurrentBox); - BoxWallpaper = SKBitmapToAvaloniaBitmapConverter.ToAvaloniaBitmap(wpBitmap); + var oldWp = BoxWallpaper; + BoxWallpaper = SKBitmapToAvaloniaBitmapConverter.ToAvaloniaBitmapAndDispose(wpBitmap); + oldWp?.Dispose(); } catch { + var oldWp = BoxWallpaper; BoxWallpaper = null; + oldWp?.Dispose(); } int slotCount = Math.Min(30, _sav.BoxSlotCount); diff --git a/PKHeX.Avalonia/ViewModels/Subforms/QRDialogViewModel.cs b/PKHeX.Avalonia/ViewModels/Subforms/QRDialogViewModel.cs index 721027211..e6e8ddd6d 100644 --- a/PKHeX.Avalonia/ViewModels/Subforms/QRDialogViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/Subforms/QRDialogViewModel.cs @@ -34,9 +34,14 @@ private void GenerateQR(PKM pk) var qr = QREncode.GenerateQRCode(pk); var sprite = pk.Sprite(); var composed = QRImageUtil.GetQRImage(qr, sprite); + qr.Dispose(); + sprite.Dispose(); + _qrBitmap?.Dispose(); _qrBitmap = composed; + var old = QrImage; QrImage = SKBitmapToAvaloniaBitmapConverter.ToAvaloniaBitmap(composed); + old?.Dispose(); var lines = pk.GetQRLines(); SummaryText = string.Join("\n", lines); diff --git a/PKHeX.Avalonia/ViewModels/Subforms/Trainer8ViewModel.cs b/PKHeX.Avalonia/ViewModels/Subforms/Trainer8ViewModel.cs index 2e8a3f95e..15159a8a5 100644 --- a/PKHeX.Avalonia/ViewModels/Subforms/Trainer8ViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/Subforms/Trainer8ViewModel.cs @@ -119,7 +119,7 @@ private void Save() _sav.Blocks.TrainerCard.TrainerID = int.TryParse(TrainerCardId, out var tcid) ? tcid : 0; _sav.Blocks.TrainerCard.RotoRallyScore = int.TryParse(RotoRallyScore, out var rr) ? rr : 0; - _sav.Money = uint.TryParse(Money, out var money) ? money : 0u; + _sav.Money = uint.TryParse(Money, out var money) ? (uint)Math.Min(money, (uint)_sav.MaxMoney) : 0u; var watt = uint.TryParse(Watt, out var w) ? w : 0u; _sav.MyStatus.Watt = watt; diff --git a/PKHeX.Avalonia/ViewModels/Subforms/Trainer8aViewModel.cs b/PKHeX.Avalonia/ViewModels/Subforms/Trainer8aViewModel.cs index 2b1b3708b..66657228c 100644 --- a/PKHeX.Avalonia/ViewModels/Subforms/Trainer8aViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/Subforms/Trainer8aViewModel.cs @@ -65,7 +65,7 @@ private void Save() { _sav.Gender = (byte)Gender; _sav.OT = OtName; - _sav.Money = uint.TryParse(Money, out var money) ? money : 0u; + _sav.Money = uint.TryParse(Money, out var money) ? (uint)Math.Min(money, (uint)_sav.MaxMoney) : 0u; _sav.PlayedHours = (ushort)Math.Clamp(PlayedHours, 0, ushort.MaxValue); _sav.PlayedMinutes = (ushort)Math.Clamp(PlayedMinutes, 0, 59); diff --git a/PKHeX.Avalonia/ViewModels/Subforms/Trainer8bViewModel.cs b/PKHeX.Avalonia/ViewModels/Subforms/Trainer8bViewModel.cs index 957ca3c55..2320132e3 100644 --- a/PKHeX.Avalonia/ViewModels/Subforms/Trainer8bViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/Subforms/Trainer8bViewModel.cs @@ -76,7 +76,7 @@ private void Save() _sav.Gender = (byte)Gender; _sav.OT = OtName; _sav.Rival = Rival; - _sav.Money = uint.TryParse(Money, out var money) ? money : 0u; + _sav.Money = uint.TryParse(Money, out var money) ? (uint)Math.Min(money, (uint)_sav.MaxMoney) : 0u; _sav.BattleTower.BP = (uint)Math.Max(0, Bp); _sav.PlayedHours = (ushort)Math.Clamp(PlayedHours, 0, ushort.MaxValue); diff --git a/PKHeX.Avalonia/ViewModels/Subforms/Trainer9ViewModel.cs b/PKHeX.Avalonia/ViewModels/Subforms/Trainer9ViewModel.cs index f73692cbd..9abde5841 100644 --- a/PKHeX.Avalonia/ViewModels/Subforms/Trainer9ViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/Subforms/Trainer9ViewModel.cs @@ -182,7 +182,7 @@ private void Save() _sav.Version = (GameVersion)(GameIndex + (byte)GameVersion.SL); _sav.Gender = (byte)Gender; _sav.OT = OtName; - _sav.Money = uint.TryParse(Money, out var m) ? m : 0u; + _sav.Money = uint.TryParse(Money, out var m) ? (uint)Math.Min(m, (uint)_sav.MaxMoney) : 0u; _sav.LeaguePoints = uint.TryParse(LeaguePoints, out var lp) ? lp : 0u; _sav.PlayedHours = (ushort)Math.Clamp(PlayedHours, 0, ushort.MaxValue); diff --git a/PKHeX.Avalonia/ViewModels/Subforms/Trainer9aViewModel.cs b/PKHeX.Avalonia/ViewModels/Subforms/Trainer9aViewModel.cs index 1fc36f757..cac207c51 100644 --- a/PKHeX.Avalonia/ViewModels/Subforms/Trainer9aViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/Subforms/Trainer9aViewModel.cs @@ -113,7 +113,7 @@ private void Save() } _sav.OT = OtName; - _sav.Money = uint.TryParse(Money, out var m) ? m : 0u; + _sav.Money = uint.TryParse(Money, out var m) ? (uint)Math.Min(m, (uint)_sav.MaxMoney) : 0u; _sav.PlayedHours = (ushort)Math.Clamp(PlayedHours, 0, ushort.MaxValue); _sav.PlayedMinutes = (ushort)Math.Clamp(PlayedMinutes, 0, 59); diff --git a/PKHeX.Avalonia/ViewModels/Subforms/WondercardViewModel.cs b/PKHeX.Avalonia/ViewModels/Subforms/WondercardViewModel.cs index 1cbf95232..fd2b51918 100644 --- a/PKHeX.Avalonia/ViewModels/Subforms/WondercardViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/Subforms/WondercardViewModel.cs @@ -37,7 +37,9 @@ public void Refresh() IsEmpty = Gift.IsEmpty; Description = IsEmpty ? $"Slot {Index + 1}: (empty)" : $"Slot {Index + 1}: {Gift.CardHeader}"; var skBitmap = Gift.Sprite(); - Sprite = SKBitmapToAvaloniaBitmapConverter.ToAvaloniaBitmap(skBitmap); + var old = Sprite; + Sprite = SKBitmapToAvaloniaBitmapConverter.ToAvaloniaBitmapAndDispose(skBitmap); + old?.Dispose(); } public void SetGift(DataMysteryGift gift)