mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-03-21 17:48:28 -05:00
Fix bitmap memory leaks, load concurrency, money clamp
Bitmap disposal (6 files): - SlotModel.SetImage: dispose old Avalonia Bitmap + input SKBitmap - PKMEditorVM: dispose old SpriteImage, LegalityImage, BallSprite and intermediate SKBitmaps on every update - SAVEditorVM: dispose old BoxWallpaper + SKBitmap on box navigation - WondercardVM: dispose old GiftSlotModel.Sprite on refresh - QRDialogVM: dispose intermediate SKBitmaps during QR generation - Added ToAvaloniaBitmapAndDispose helper for owned SKBitmap conversion Concurrency: - MainWindowVM: add _isLoading guard to prevent concurrent LoadFileAsync calls from drag-drop or rapid Open clicks Money clamp: - Trainer8/8a/8b/9/9a: clamp Money to sav.MaxMoney on save (was allowing values exceeding game maximums)
This commit is contained in:
parent
ae206b80a0
commit
807007ad8a
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,4 +46,22 @@ public class SKBitmapToAvaloniaBitmapConverter : IValueConverter
|
|||
stream.Position = 0;
|
||||
return new Bitmap(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static Bitmap? ToAvaloniaBitmapAndDispose(SKBitmap? skBitmap)
|
||||
{
|
||||
if (skBitmap is null)
|
||||
return null;
|
||||
try
|
||||
{
|
||||
return ToAvaloniaBitmap(skBitmap);
|
||||
}
|
||||
finally
|
||||
{
|
||||
skBitmap.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ public partial class MainWindowViewModel : ObservableObject
|
|||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
|
||||
/// <summary>
|
||||
/// Guards against concurrent invocations of <see cref="LoadFileAsync"/>.
|
||||
/// </summary>
|
||||
private bool _isLoading;
|
||||
|
||||
/// <summary>
|
||||
/// 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]);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user