diff --git a/PKHeX.Avalonia/Controls/SlotModel.cs b/PKHeX.Avalonia/Controls/SlotModel.cs
index 7715ca684..ad682924d 100644
--- a/PKHeX.Avalonia/Controls/SlotModel.cs
+++ b/PKHeX.Avalonia/Controls/SlotModel.cs
@@ -24,6 +24,20 @@ public partial class SlotModel : ObservableObject
[ObservableProperty]
private bool _isEmpty = true;
+ /// Whether the slot matches the current search filter.
+ [ObservableProperty]
+ private bool _isHighlighted;
+
+ /// Whether a search filter is currently active.
+ [ObservableProperty]
+ private bool _isSearchActive;
+
+ /// Opacity to use when a search is active (1.0 if highlighted or no search, 0.3 if not matching).
+ public double SearchOpacity => IsHighlighted ? 1.0 : (_isSearchActive ? 0.3 : 1.0);
+
+ partial void OnIsHighlightedChanged(bool value) => OnPropertyChanged(nameof(SearchOpacity));
+ partial void OnIsSearchActiveChanged(bool value) => OnPropertyChanged(nameof(SearchOpacity));
+
/// The PKM entity backing this slot, if any.
[ObservableProperty]
private PKM? _entity;
@@ -38,6 +52,10 @@ public partial class SlotModel : ObservableObject
? $"{SpeciesName.GetSpeciesNameGeneration(Entity.Species, 2, Entity.Format)} Lv.{Entity.CurrentLevel}"
: "Empty";
+ /// Label describing the slot type (e.g. "Daycare", "Fused Kyurem").
+ [ObservableProperty]
+ private string _slotLabel = string.Empty;
+
partial void OnEntityChanged(PKM? value)
{
OnPropertyChanged(nameof(ShowdownText));
diff --git a/PKHeX.Avalonia/ViewModels/PKMEditorViewModel.cs b/PKHeX.Avalonia/ViewModels/PKMEditorViewModel.cs
index a85ca2a89..019b2075f 100644
--- a/PKHeX.Avalonia/ViewModels/PKMEditorViewModel.cs
+++ b/PKHeX.Avalonia/ViewModels/PKMEditorViewModel.cs
@@ -11,6 +11,8 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PKHeX.Avalonia.Converters;
+using PKHeX.Avalonia.ViewModels.Subforms;
+using PKHeX.Avalonia.Views.Subforms;
using PKHeX.Core;
using PKHeX.Drawing.PokeSprite.Avalonia;
using SkiaSharp;
@@ -165,6 +167,19 @@ public partial class PKMEditorViewModel : ObservableObject
[ObservableProperty] private bool _isShadow;
[ObservableProperty] private bool _hasShadow;
+ // Form Argument
+ [ObservableProperty] private uint _formArgument;
+ [ObservableProperty] private bool _hasFormArgument;
+ [ObservableProperty] private uint _formArgumentMax;
+
+ // Alpha Mastered Move (Gen 8a - Legends Arceus)
+ [ObservableProperty] private ComboItem? _selectedAlphaMove;
+ [ObservableProperty] private bool _hasAlphaMove;
+
+ // Move Shop / Tech Record visibility
+ [ObservableProperty] private bool _hasMoveShop;
+ [ObservableProperty] private bool _hasTechRecords;
+
// Gen-specific: Catch Rate (Gen 1)
[ObservableProperty] private int _catchRate;
[ObservableProperty] private bool _hasCatchRate;
@@ -950,6 +965,41 @@ public void PopulateFields(PKM pk)
PokeStarFame = 0;
}
+ // Form Argument
+ if (pk is IFormArgument fa)
+ {
+ HasFormArgument = true;
+ FormArgument = fa.FormArgument;
+ FormArgumentMax = FormArgumentUtil.GetFormArgumentMax(pk.Species, pk.Form, pk.Context);
+ if (FormArgumentMax == 0)
+ FormArgumentMax = 255;
+ }
+ else
+ {
+ HasFormArgument = false;
+ FormArgument = 0;
+ FormArgumentMax = 255;
+ }
+
+ // Alpha Mastered Move (Legends Arceus)
+ if (pk is PA8 pa8)
+ {
+ HasAlphaMove = true;
+ var alphaMoveId = pa8.AlphaMove;
+ SelectedAlphaMove = MoveList.FirstOrDefault(m => m.Value == alphaMoveId);
+ }
+ else
+ {
+ HasAlphaMove = false;
+ SelectedAlphaMove = null;
+ }
+
+ // Move Shop (Legends Arceus)
+ HasMoveShop = pk is IMoveShop8Mastery;
+
+ // Tech Records (Gen 8+)
+ HasTechRecords = pk is ITechRecord;
+
// Origin Mark indicator
var gen = pk.Generation;
HasOriginMark = gen >= 3;
@@ -1231,9 +1281,51 @@ public void PopulateFields(PKM pk)
pb7Save.Mood = (byte)Math.Clamp(Mood7b, 0, 255);
}
+ // Form Argument
+ if (Entity is IFormArgument faSave)
+ faSave.FormArgument = FormArgument;
+
+ // Alpha Mastered Move (Legends Arceus)
+ if (Entity is PA8 pa8Save && SelectedAlphaMove is not null)
+ pa8Save.AlphaMove = (ushort)SelectedAlphaMove.Value;
+
return Entity;
}
+ // --- Move Shop / Tech Record editor commands ---
+
+ [RelayCommand]
+ private async Task OpenMoveShop()
+ {
+ if (Entity is not IMoveShop8Mastery master || Entity is not IMoveShop8 shop) return;
+ try
+ {
+ PreparePKM();
+ var vm = new MoveShopEditorViewModel(shop, master, Entity);
+ var view = new MoveShopEditorView { DataContext = vm };
+ var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
+ if (mainWindow != null)
+ await view.ShowDialog(mainWindow);
+ }
+ catch (Exception ex) { LegalityReport = $"Move Shop error: {ex.Message}"; }
+ }
+
+ [RelayCommand]
+ private async Task OpenTechRecords()
+ {
+ if (Entity is not ITechRecord tr) return;
+ try
+ {
+ PreparePKM();
+ var vm = new TechRecordEditorViewModel(tr, Entity);
+ var view = new TechRecordEditorView { DataContext = vm };
+ var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
+ if (mainWindow != null)
+ await view.ShowDialog(mainWindow);
+ }
+ catch (Exception ex) { LegalityReport = $"Tech Record error: {ex.Message}"; }
+ }
+
// --- IV/EV quick-set commands ---
[RelayCommand]
@@ -1402,6 +1494,46 @@ private string GetNatureColor(int statIndex)
};
}
+ // --- Nickname warning ---
+
+ [RelayCommand]
+ private void ShowNicknameWarning()
+ {
+ if (Entity is null) return;
+ var nickname = Nickname ?? string.Empty;
+ var species = Entity.Species;
+ var lang = Entity.Language;
+ var defaultName = SpeciesName.GetSpeciesNameGeneration(species, lang, Entity.Format);
+
+ // Check for non-ASCII characters that might not render in older games
+ bool hasSpecialChars = false;
+ foreach (var c in nickname)
+ {
+ if (c > 0x7E || c < 0x20)
+ {
+ hasSpecialChars = true;
+ break;
+ }
+ }
+
+ if (string.IsNullOrEmpty(nickname))
+ {
+ LegalityReport = "Nickname is empty.";
+ }
+ else if (nickname == defaultName)
+ {
+ LegalityReport = $"Nickname matches default species name: '{defaultName}'.";
+ }
+ else if (hasSpecialChars)
+ {
+ LegalityReport = $"Nickname '{nickname}' contains special characters that may not render correctly in-game. Default name: '{defaultName}'.";
+ }
+ else
+ {
+ LegalityReport = $"Nickname '{nickname}' appears to use standard characters. Default name: '{defaultName}'.";
+ }
+ }
+
// --- Sprite context menu commands ---
[RelayCommand]
diff --git a/PKHeX.Avalonia/ViewModels/SAVEditorViewModel.cs b/PKHeX.Avalonia/ViewModels/SAVEditorViewModel.cs
index 1633e64a0..4bae5f4e0 100644
--- a/PKHeX.Avalonia/ViewModels/SAVEditorViewModel.cs
+++ b/PKHeX.Avalonia/ViewModels/SAVEditorViewModel.cs
@@ -127,6 +127,18 @@ public void Redo()
[ObservableProperty]
private Bitmap? _boxWallpaper;
+ // Box search/filter
+ [ObservableProperty] private string _searchText = string.Empty;
+ [ObservableProperty] private string _searchResultText = string.Empty;
+
+ // Extra slots (Other tab)
+ public ObservableCollection ExtraSlots { get; } = [];
+ [ObservableProperty] private bool _hasExtraSlots;
+
+ // Save slot info
+ [ObservableProperty] private string _saveSlotInfo = string.Empty;
+ [ObservableProperty] private bool _hasSaveSlotInfo;
+
public ObservableCollection BoxSlots { get; } = [];
public ObservableCollection PartySlots { get; } = [];
public ObservableCollection BoxNames { get; } = [];
@@ -181,6 +193,9 @@ public void LoadSaveFile(SaveFile sav)
RefreshParty();
UpdateToolVisibility(sav);
RefreshDaycare(sav);
+ RefreshExtraSlots(sav);
+ RefreshSaveSlotInfo(sav);
+ SearchText = string.Empty;
}
private void UpdateToolVisibility(SaveFile sav)
@@ -455,6 +470,125 @@ private void RefreshDaycare(SaveFile sav)
}
}
+ #region Box Search
+
+ partial void OnSearchTextChanged(string value)
+ {
+ if (_sav is null) return;
+ var searchLower = value.ToLowerInvariant().Trim();
+ var isActive = !string.IsNullOrEmpty(searchLower);
+ int count = 0;
+
+ foreach (var slot in BoxSlots)
+ {
+ slot.IsSearchActive = isActive;
+ if (slot.Entity is null || slot.Entity.Species == 0)
+ {
+ slot.IsHighlighted = false;
+ continue;
+ }
+ if (!isActive)
+ {
+ slot.IsHighlighted = false;
+ continue;
+ }
+ var name = SpeciesName.GetSpeciesNameGeneration(slot.Entity.Species, 2, slot.Entity.Format).ToLowerInvariant();
+ var matches = name.Contains(searchLower);
+ slot.IsHighlighted = matches;
+ if (matches) count++;
+ }
+
+ SearchResultText = isActive ? $"{count} found" : string.Empty;
+ }
+
+ #endregion
+
+ #region Extra Slots
+
+ private void RefreshExtraSlots(SaveFile sav)
+ {
+ ExtraSlots.Clear();
+ try
+ {
+ var extras = sav.GetExtraSlots(true);
+ if (extras.Count == 0)
+ {
+ HasExtraSlots = false;
+ return;
+ }
+
+ HasExtraSlots = true;
+ foreach (var slotInfo in extras)
+ {
+ var pk = slotInfo.Read(sav);
+ var model = new SlotModel
+ {
+ Slot = slotInfo.Slot,
+ SlotLabel = GetSlotTypeLabel(slotInfo.Type, slotInfo.Slot),
+ };
+ model.Entity = pk;
+ if (pk.Species == 0)
+ {
+ model.SetImage(SpriteUtil.Spriter.None);
+ model.IsEmpty = true;
+ }
+ else
+ {
+ var sprite = pk.Sprite();
+ model.SetImage(sprite);
+ model.IsEmpty = false;
+ }
+ ExtraSlots.Add(model);
+ }
+ }
+ catch
+ {
+ HasExtraSlots = false;
+ }
+ }
+
+ private static string GetSlotTypeLabel(StorageSlotType type, int slot) => type switch
+ {
+ StorageSlotType.Daycare => $"Daycare #{slot + 1}",
+ StorageSlotType.GTS => "GTS Upload",
+ StorageSlotType.BattleBox => $"Battle Box #{slot + 1}",
+ StorageSlotType.FusedKyurem => "Fused Kyurem",
+ StorageSlotType.FusedNecrozmaS => "Fused Necrozma (Solgaleo)",
+ StorageSlotType.FusedNecrozmaM => "Fused Necrozma (Lunala)",
+ StorageSlotType.FusedCalyrex => "Fused Calyrex",
+ StorageSlotType.Resort => $"Resort #{slot + 1}",
+ StorageSlotType.Ride => "Ride Legend",
+ StorageSlotType.BattleAgency => $"Battle Agency #{slot + 1}",
+ StorageSlotType.SurpriseTrade => $"Surprise Trade #{slot + 1}",
+ StorageSlotType.Underground => $"Underground #{slot + 1}",
+ StorageSlotType.Scripted => $"Scripted #{slot + 1}",
+ StorageSlotType.PGL => "PGL Upload",
+ StorageSlotType.Pokéwalker => "Pokewalker",
+ StorageSlotType.Shiny => $"Shiny Cache #{slot + 1}",
+ _ => $"{type} #{slot + 1}",
+ };
+
+ #endregion
+
+ #region Save Slot Info
+
+ private void RefreshSaveSlotInfo(SaveFile sav)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine($"Type: {sav.GetType().Name}");
+ sb.AppendLine($"Generation: {sav.Generation}");
+ sb.AppendLine($"Version: {sav.Version}");
+ sb.AppendLine($"Boxes: {sav.BoxCount} ({sav.BoxSlotCount} slots each)");
+ if (sav.HasParty)
+ sb.AppendLine($"Party: {sav.PartyCount} Pokemon");
+ sb.AppendLine($"Checksums: {(sav.ChecksumsValid ? "Valid" : "Invalid")}");
+
+ SaveSlotInfo = sb.ToString().TrimEnd();
+ HasSaveSlotInfo = true;
+ }
+
+ #endregion
+
#region Box Management (Sort / Clear)
[RelayCommand]
diff --git a/PKHeX.Avalonia/Views/PKMEditorView.axaml b/PKHeX.Avalonia/Views/PKMEditorView.axaml
index 266f29c55..2711a5984 100644
--- a/PKHeX.Avalonia/Views/PKMEditorView.axaml
+++ b/PKHeX.Avalonia/Views/PKMEditorView.axaml
@@ -69,7 +69,12 @@
-
+
+
+
+
@@ -134,6 +139,12 @@
+
+
+
+
@@ -349,33 +360,50 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PKHeX.Avalonia/Views/SAVEditorView.axaml b/PKHeX.Avalonia/Views/SAVEditorView.axaml
index eb98601e6..29beba93a 100644
--- a/PKHeX.Avalonia/Views/SAVEditorView.axaml
+++ b/PKHeX.Avalonia/Views/SAVEditorView.axaml
@@ -9,6 +9,13 @@
+
+
+
+
+
+
diff --git a/PKHeX.Avalonia/Views/SlotControl.axaml b/PKHeX.Avalonia/Views/SlotControl.axaml
index 34bc66ee4..dbc5a8513 100644
--- a/PKHeX.Avalonia/Views/SlotControl.axaml
+++ b/PKHeX.Avalonia/Views/SlotControl.axaml
@@ -32,6 +32,7 @@
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="1" CornerRadius="0"
Cursor="Hand"
+ Opacity="{Binding SearchOpacity}"
DragDrop.AllowDrop="True"
AutomationProperties.Name="{Binding Summary}">