Fix all 9 critical audit issues: data loss, legality, buttons, labels

Critical fixes:
- Data loss: write back IsShiny/IsInfected/IsCured in PreparePKM
- IV/EV changes now trigger UpdateLegality (12 handlers fixed)
- EXP changes now trigger UpdateLegality
- FriendshipLabel swaps to "Hatch Counter:" when IsEgg=true
- IsEgg now refreshes sprite display
- Shiny toggle (☆) and RerollPID buttons added to Main tab
- IsNicknamed checkbox added to Nickname row
- RTC Editor now supports Gen 2 saves (new SAVRTC2 ViewModel/View)
This commit is contained in:
montanon 2026-03-16 15:00:17 -03:00
parent 0ca0ea0404
commit 6652a8d337
6 changed files with 171 additions and 20 deletions

View File

@ -89,6 +89,11 @@ public partial class PKMEditorViewModel : ObservableObject
[ObservableProperty] private bool _isEgg;
[ObservableProperty] private bool _isNicknamed;
/// <summary>
/// Returns "Hatch Counter:" when the PKM is an egg, otherwise "Friendship:".
/// </summary>
public string FriendshipLabel => IsEgg ? "Hatch Counter:" : "Friendship:";
// Pokerus
[ObservableProperty] private bool _isInfected;
[ObservableProperty] private bool _isCured;
@ -429,6 +434,41 @@ private void RerollEc()
EncryptionConstantHex = ec.ToString("X8");
}
[RelayCommand]
private void Shinytize()
{
if (Entity is null) return;
if (Entity.IsShiny)
{
Entity.SetPIDGender(Entity.Gender);
IsShiny = false;
}
else
{
Entity.SetShiny();
IsShiny = true;
}
PidHex = Entity.PID.ToString("X8");
IsShinyDisplay = Entity.IsShiny;
IsSquareShiny = Entity.IsShiny && Entity.ShinyXor == 0;
UpdateSprite();
UpdateLegality();
}
[RelayCommand]
private void RerollPid()
{
if (Entity is null) return;
var rng = new Random();
Entity.PID = (uint)rng.Next();
PidHex = Entity.PID.ToString("X8");
IsShiny = Entity.IsShiny;
IsShinyDisplay = Entity.IsShiny;
IsSquareShiny = Entity.IsShiny && Entity.ShinyXor == 0;
UpdateSprite();
UpdateLegality();
}
// Home Tracker
[ObservableProperty] private string _homeTrackerHex = "0000000000000000";
[ObservableProperty] private bool _hasHomeTracker;
@ -1609,7 +1649,15 @@ public void PopulateFields(PKM pk)
Entity.IsEgg = IsEgg;
Entity.IsNicknamed = IsNicknamed;
// Shiny
if (IsShiny && !Entity.IsShiny)
Entity.SetShiny();
else if (!IsShiny && Entity.IsShiny)
Entity.SetPIDGender(Entity.Gender);
// Pokerus
Entity.IsPokerusInfected = IsInfected;
Entity.IsPokerusCured = IsCured;
Entity.PokerusStrain = PkrsStrain;
Entity.PokerusDays = PkrsDays;
@ -1721,6 +1769,7 @@ public void PopulateFields(PKM pk)
{
scSave.ShadowID = (ushort)ShadowId;
scSave.Purification = Purification;
// IsShadow is derived from ShadowID/Purification, no separate write needed
}
// Gen-specific: Catch Rate (Gen 1)
@ -1953,14 +2002,19 @@ private async Task ShowFullLegalityReport()
try { Level = Experience.GetLevel(value, growth); }
finally { _isPopulating = false; }
RecalcStats();
UpdateLegality();
}
// --- IsEgg change triggers legality ---
// --- IsEgg change triggers legality + sprite + label ---
partial void OnIsEggChanged(bool value)
{
if (!_isPopulating)
{
UpdateLegality();
UpdateSprite();
}
OnPropertyChanged(nameof(FriendshipLabel));
}
partial void OnIsShinyChanged(bool value)
@ -1971,21 +2025,21 @@ private async Task ShowFullLegalityReport()
// --- IV changed handlers → recalc stats + legality ---
partial void OnIv_HPChanged(int value) { if (!_isPopulating) RecalcStats(); }
partial void OnIv_ATKChanged(int value) { if (!_isPopulating) RecalcStats(); }
partial void OnIv_DEFChanged(int value) { if (!_isPopulating) RecalcStats(); }
partial void OnIv_SPAChanged(int value) { if (!_isPopulating) RecalcStats(); }
partial void OnIv_SPDChanged(int value) { if (!_isPopulating) RecalcStats(); }
partial void OnIv_SPEChanged(int value) { if (!_isPopulating) RecalcStats(); }
partial void OnIv_HPChanged(int value) { if (!_isPopulating) { RecalcStats(); UpdateLegality(); } }
partial void OnIv_ATKChanged(int value) { if (!_isPopulating) { RecalcStats(); UpdateLegality(); } }
partial void OnIv_DEFChanged(int value) { if (!_isPopulating) { RecalcStats(); UpdateLegality(); } }
partial void OnIv_SPAChanged(int value) { if (!_isPopulating) { RecalcStats(); UpdateLegality(); } }
partial void OnIv_SPDChanged(int value) { if (!_isPopulating) { RecalcStats(); UpdateLegality(); } }
partial void OnIv_SPEChanged(int value) { if (!_isPopulating) { RecalcStats(); UpdateLegality(); } }
// --- EV changed handlers → recalc stats ---
// --- EV changed handlers → recalc stats + legality ---
partial void OnEv_HPChanged(int value) { if (!_isPopulating) RecalcStats(); }
partial void OnEv_ATKChanged(int value) { if (!_isPopulating) RecalcStats(); }
partial void OnEv_DEFChanged(int value) { if (!_isPopulating) RecalcStats(); }
partial void OnEv_SPAChanged(int value) { if (!_isPopulating) RecalcStats(); }
partial void OnEv_SPDChanged(int value) { if (!_isPopulating) RecalcStats(); }
partial void OnEv_SPEChanged(int value) { if (!_isPopulating) RecalcStats(); }
partial void OnEv_HPChanged(int value) { if (!_isPopulating) { RecalcStats(); UpdateLegality(); } }
partial void OnEv_ATKChanged(int value) { if (!_isPopulating) { RecalcStats(); UpdateLegality(); } }
partial void OnEv_DEFChanged(int value) { if (!_isPopulating) { RecalcStats(); UpdateLegality(); } }
partial void OnEv_SPAChanged(int value) { if (!_isPopulating) { RecalcStats(); UpdateLegality(); } }
partial void OnEv_SPDChanged(int value) { if (!_isPopulating) { RecalcStats(); UpdateLegality(); } }
partial void OnEv_SPEChanged(int value) { if (!_isPopulating) { RecalcStats(); UpdateLegality(); } }
// --- Stat auto-recalculation ---

View File

@ -129,8 +129,10 @@ public static List<SAVToolDescriptor> GetAllTools()
// --- RTC ---
new("RTC Editor",
sav => sav is SAV3 { SmallBlock: ISaveBlock3SmallHoenn },
sav => WithView<SAVRTC3ViewModel, SAVRTC3View>(new SAVRTC3ViewModel((SAV3)sav))),
sav => (sav.Generation == 2 && sav is not SAV2Stadium) || sav is SAV3 { SmallBlock: ISaveBlock3SmallHoenn },
sav => sav is SAV3 s3
? WithView<SAVRTC3ViewModel, SAVRTC3View>(new SAVRTC3ViewModel(s3))
: WithView<SAVRTC2ViewModel, SAVRTC2View>(new SAVRTC2ViewModel((SAV2)sav))),
// --- DLC (Gen 5) ---
new("DLC Content",

View File

@ -0,0 +1,35 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PKHeX.Core;
namespace PKHeX.Avalonia.ViewModels.Subforms;
/// <summary>
/// ViewModel for the Gen 2 Real-Time Clock editor.
/// Gen 2 Crystal has an RTC flag that can be reset to force re-initialization.
/// </summary>
public partial class SAVRTC2ViewModel : SaveEditorViewModelBase
{
private readonly SAV2 _sav;
[ObservableProperty]
private string _statusText = "RTC is running normally. Use Reset to force clock re-initialization on next boot.";
public SAVRTC2ViewModel(SAV2 sav) : base(sav)
{
_sav = sav;
}
[RelayCommand]
private void ResetRtc()
{
_sav.ResetRTC();
StatusText = "RTC reset flag has been SET. The clock will be re-initialized on next boot.";
}
[RelayCommand]
private void Save()
{
Modified = true;
}
}

View File

@ -60,7 +60,11 @@
ColumnDefinitions="104,*">
<!-- PID -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="PID:" TextAlignment="Right" VerticalAlignment="Center" Margin="0,0,6,0" />
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding PidHex}" Height="25" Width="72" HorizontalAlignment="Left" FontFamily="Courier New,Consolas,monospace" />
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<TextBox Text="{Binding PidHex}" Height="25" Width="72" FontFamily="Courier New,Consolas,monospace" />
<Button Content="&#x2606;" Command="{Binding ShinytizeCommand}" Width="24" Height="25" Padding="0" FontSize="14" Margin="4,0,0,0" ToolTip.Tip="Toggle Shiny" />
<Button Content="Reroll" Command="{Binding RerollPidCommand}" Height="25" Padding="4,0" FontSize="9" Margin="2,0,0,0" />
</StackPanel>
<!-- Species + Gender -->
<TextBlock Grid.Row="1" Grid.Column="0" Text="Species:" TextAlignment="Right" VerticalAlignment="Center" Margin="0,0,6,0" />
@ -74,7 +78,8 @@
<!-- Nickname -->
<TextBlock Grid.Row="2" Grid.Column="0" Text="Nickname:" TextAlignment="Right" VerticalAlignment="Center" Margin="0,0,6,0" />
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<TextBox Text="{Binding Nickname}" Height="25" Width="144" />
<CheckBox IsChecked="{Binding IsNicknamed}" Margin="0,0,4,0" ToolTip.Tip="Is Nicknamed" />
<TextBox Text="{Binding Nickname}" Height="25" Width="130" />
<Button Content="?" Command="{Binding ShowNicknameWarningCommand}"
Width="20" Height="20" Padding="0" FontSize="10" Margin="2,0,0,0"
ToolTip.Tip="Check nickname font compatibility" />
@ -115,8 +120,8 @@
<ComboBox Grid.Row="8" Grid.Column="1" ItemsSource="{Binding AbilityList}" SelectedItem="{Binding SelectedAbility}"
DisplayMemberBinding="{Binding Text, DataType=core:ComboItem}" Height="25" Width="144" HorizontalAlignment="Left" />
<!-- Friendship -->
<TextBlock Grid.Row="9" Grid.Column="0" Text="Friendship:" TextAlignment="Right" VerticalAlignment="Center" Margin="0,0,6,0" />
<!-- Friendship / Hatch Counter -->
<TextBlock Grid.Row="9" Grid.Column="0" Text="{Binding FriendshipLabel}" TextAlignment="Right" VerticalAlignment="Center" Margin="0,0,6,0" />
<NumericUpDown Grid.Row="9" Grid.Column="1" Value="{Binding Friendship}" Minimum="0" Maximum="255" Height="25" Width="60" HorizontalAlignment="Left" />
<!-- Language -->

View File

@ -0,0 +1,31 @@
<views:SubformWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="using:PKHeX.Avalonia.Views"
xmlns:vm="using:PKHeX.Avalonia.ViewModels.Subforms"
x:Class="PKHeX.Avalonia.Views.Subforms.SAVRTC2View"
x:DataType="vm:SAVRTC2ViewModel"
Title="RTC Editor (Gen 2)"
Width="380" Height="200"
MinWidth="300" MinHeight="160"
FontFamily="Microsoft Sans Serif,Geneva,Helvetica,Arial,sans-serif"
FontSize="11"
WindowStartupLocation="CenterOwner"
CanResize="False">
<DockPanel Margin="8">
<!-- Bottom buttons -->
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"
HorizontalAlignment="Right" Spacing="8" Margin="0,8,0,0">
<Button Content="Reset RTC" Command="{Binding ResetRtcCommand}" MinWidth="85" Padding="8,4"
ToolTip.Tip="Set the RTC reset flag so the clock re-initializes on next boot" />
<Button Content="OK" Click="OnOkClick" MinWidth="75" Padding="8,4" />
<Button Content="Cancel" Click="OnCancelClick" MinWidth="75" Padding="8,4" />
</StackPanel>
<StackPanel Spacing="8" VerticalAlignment="Center">
<TextBlock Text="Gen 2 Crystal Real-Time Clock" FontWeight="SemiBold" FontSize="12" />
<TextBlock Text="{Binding StatusText}" TextWrapping="Wrap" FontSize="11" />
</StackPanel>
</DockPanel>
</views:SubformWindow>

View File

@ -0,0 +1,24 @@
using Avalonia.Interactivity;
using PKHeX.Avalonia.ViewModels.Subforms;
namespace PKHeX.Avalonia.Views.Subforms;
public partial class SAVRTC2View : SubformWindow
{
public SAVRTC2View()
{
InitializeComponent();
}
private void OnOkClick(object? sender, RoutedEventArgs e)
{
if (DataContext is SAVRTC2ViewModel vm)
vm.SaveCommand.Execute(null);
CloseWithResult(true);
}
private void OnCancelClick(object? sender, RoutedEventArgs e)
{
CloseWithResult(false);
}
}