From 301a1e7664f7031539975d03a04091b1ac2a8156 Mon Sep 17 00:00:00 2001 From: Kurt Date: Wed, 11 Mar 2026 00:15:58 -0500 Subject: [PATCH] Allow edits for SAV OT trash bytes for gen1-5 --- .../Legality/Verifiers/Misc/MiscVerifierG3.cs | 17 ++++++++---- PKHeX.Core/Saves/SAV3Colosseum.cs | 3 ++- PKHeX.Core/Saves/SAV3XD.cs | 3 ++- PKHeX.Core/Saves/SAV4.cs | 7 +++-- .../Saves/Substructures/Gen5/PlayerData5.cs | 2 +- .../Save Editors/SAV_SimpleTrainer.cs | 26 +++++++++++++++++++ 6 files changed, 48 insertions(+), 10 deletions(-) diff --git a/PKHeX.Core/Legality/Verifiers/Misc/MiscVerifierG3.cs b/PKHeX.Core/Legality/Verifiers/Misc/MiscVerifierG3.cs index 16a959ed0..b22e69cfe 100644 --- a/PKHeX.Core/Legality/Verifiers/Misc/MiscVerifierG3.cs +++ b/PKHeX.Core/Legality/Verifiers/Misc/MiscVerifierG3.cs @@ -147,6 +147,10 @@ public static class TrashByteRules3 // When transferred to Colosseum/XD, the encoding method switches to u16[length], thus discarding the original buffer along with its "trash". // For original encounters from a mainline save file, // - OT Name: the game copies the entire buffer from the save file OT as the PK3's OT. Thus, that must match exactly. + // - - Japanese OT names are 5 chars, international is 7 chars. Manually entered strings are FF terminated to max length + 1. + // - - Default OT (Japanese) names were padded with FF to len=6, so they always match manually entered names (no trash). + // - - Default OT (International) names from the character select screen can have trash bytes due to being un-padded (single FF end of string, saves ROM space). + // - - verification of Default OTs todo (if OT dirty, check if is default with expected trash pattern) // - Nickname: the buffer has garbage RAM data leftover in the nickname field, thus it should be "dirty" usually. // - Nicknamed: when nicknamed, the game fills the buffer with FFs then applies the nickname. // For event encounters from GameCube: @@ -175,7 +179,7 @@ public static bool IsResetTrash(PK3 pk3) public static bool IsTerminatedZero(ReadOnlySpan data) { var first = TrashBytes8.GetTerminatorIndex(data); - if (first == -1 || first >= data.Length - 2) + if (first == -1 || first >= data.Length - 1) return true; return !data[(first+1)..].ContainsAnyExcept(0); } @@ -183,7 +187,7 @@ public static bool IsTerminatedZero(ReadOnlySpan data) public static bool IsTerminatedFF(ReadOnlySpan data) { var first = TrashBytes8.GetTerminatorIndex(data); - if (first == -1 || first >= data.Length - 2) + if (first == -1 || first >= data.Length - 1) return true; return !data[(first + 1)..].ContainsAnyExcept(0xFF); } @@ -194,17 +198,20 @@ public static bool IsTerminatedFFZero(ReadOnlySpan data, int preFill = 0) return IsTerminatedZero(data); var first = TrashBytes8.GetTerminatorIndex(data); - if (first == -1 || first == data.Length - 2) + if (first == -1 || first >= data.Length - 1) return true; + + first++; if (first < preFill) { var inner = data[first..preFill]; if (inner.ContainsAnyExcept(Terminator)) return false; first = preFill; - if (first >= data.Length - 2) + first++; + if (first >= data.Length - 1) return true; } - return !data[(first + 1)..].ContainsAnyExcept(0); + return !data[first..].ContainsAnyExcept(0); } } diff --git a/PKHeX.Core/Saves/SAV3Colosseum.cs b/PKHeX.Core/Saves/SAV3Colosseum.cs index 7ae7c5c3e..d03937a34 100644 --- a/PKHeX.Core/Saves/SAV3Colosseum.cs +++ b/PKHeX.Core/Saves/SAV3Colosseum.cs @@ -256,7 +256,8 @@ public override int PlayedSeconds } // Trainer Info (offset 0x78, length 0xB18, end @ 0xB90) - public override string OT { get => GetString(Data.Slice(0x78, 20)); set { SetString(Data.Slice(0x78, 20), value, 10, StringConverterOption.ClearZero); OT2 = value; } } + public Span OriginalTrainerTrash => Data.Slice(0x78, 20); + public override string OT { get => GetString(OriginalTrainerTrash); set { SetString(OriginalTrainerTrash, value, 10, StringConverterOption.ClearZero); OT2 = value; } } public string OT2 { get => GetString(Data.Slice(0x8C, 20)); set => SetString(Data.Slice(0x8C, 20), value, 10, StringConverterOption.ClearZero); } public override uint ID32 { get => ReadUInt32BigEndian(Data[0xA4..]); set => WriteUInt32BigEndian(Data[0xA4..], value); } diff --git a/PKHeX.Core/Saves/SAV3XD.cs b/PKHeX.Core/Saves/SAV3XD.cs index 6e4fad8bd..f6c9bf2f5 100644 --- a/PKHeX.Core/Saves/SAV3XD.cs +++ b/PKHeX.Core/Saves/SAV3XD.cs @@ -234,7 +234,8 @@ public override int PlayedSeconds // Trainer Info public override GameVersion Version { get => GameVersion.XD; set { } } - public override string OT { get => GetString(Data.Slice(Trainer1 + 0x00, 20)); set => SetString(Data.Slice(Trainer1 + 0x00, 20), value, 10, StringConverterOption.ClearZero); } + public Span OriginalTrainerTrash => Data.Slice(Trainer1 + 0x00, 20); + public override string OT { get => GetString(OriginalTrainerTrash); set => SetString(OriginalTrainerTrash, value, 10, StringConverterOption.ClearZero); } public override uint ID32 { get => ReadUInt32BigEndian(Data[(Trainer1 + 0x2C)..]); set => WriteUInt32BigEndian(Data[(Trainer1 + 0x2C)..], value); } public override ushort SID16 { get => ReadUInt16BigEndian(Data[(Trainer1 + 0x2C)..]); set => WriteUInt16BigEndian(Data[(Trainer1 + 0x2C)..], value); } public override ushort TID16 { get => ReadUInt16BigEndian(Data[(Trainer1 + 0x2E)..]); set => WriteUInt16BigEndian(Data[(Trainer1 + 0x2E)..], value); } diff --git a/PKHeX.Core/Saves/SAV4.cs b/PKHeX.Core/Saves/SAV4.cs index 32647da21..726d79801 100644 --- a/PKHeX.Core/Saves/SAV4.cs +++ b/PKHeX.Core/Saves/SAV4.cs @@ -255,10 +255,13 @@ public override int PartyCount public sealed override int GetPartyOffset(int slot) => Party + (SIZE_PARTY * slot); #region Trainer Info + + public Span OriginalTrainerTrash => General.Slice(Trainer1, 16); + public override string OT { - get => GetString(General.Slice(Trainer1, 16)); - set => SetString(General.Slice(Trainer1, 16), value, MaxStringLengthTrainer, StringConverterOption.ClearZero); + get => GetString(OriginalTrainerTrash); + set => SetString(OriginalTrainerTrash, value, MaxStringLengthTrainer, StringConverterOption.ClearZero); } public override uint ID32 diff --git a/PKHeX.Core/Saves/Substructures/Gen5/PlayerData5.cs b/PKHeX.Core/Saves/Substructures/Gen5/PlayerData5.cs index 1ab856d2b..877bb6647 100644 --- a/PKHeX.Core/Saves/Substructures/Gen5/PlayerData5.cs +++ b/PKHeX.Core/Saves/Substructures/Gen5/PlayerData5.cs @@ -8,7 +8,7 @@ namespace PKHeX.Core; /// public abstract class PlayerData5(SAV5 sav, Memory raw) : SaveBlock(sav, raw) { - private Span OriginalTrainerTrash => Data.Slice(4, 0x10); + public Span OriginalTrainerTrash => Data.Slice(4, 0x10); public string OT { diff --git a/PKHeX.WinForms/Subforms/Save Editors/SAV_SimpleTrainer.cs b/PKHeX.WinForms/Subforms/Save Editors/SAV_SimpleTrainer.cs index 2bc5948e3..3258e3778 100644 --- a/PKHeX.WinForms/Subforms/Save Editors/SAV_SimpleTrainer.cs +++ b/PKHeX.WinForms/Subforms/Save Editors/SAV_SimpleTrainer.cs @@ -73,6 +73,8 @@ public SAV_SimpleTrainer(SaveFile sav) L_PikaBeach.Visible = MT_PikaBeach.Visible = false; CB_SoundType.Visible = LBL_SoundType.Visible = false; } + + TB_OTName.Click += (_, _) => ClickOT(sav1.OriginalTrainerTrash, TB_OTName); } if (SAV is SAV2 sav2) @@ -94,6 +96,8 @@ public SAV_SimpleTrainer(SaveFile sav) CB_TextSpeed.SelectedIndex = sav2.TextSpeed; badgeval = sav2.Badges; cba = [CHK_1, CHK_2, CHK_3, CHK_4, CHK_6, CHK_5, CHK_7, CHK_8, CHK_H1, CHK_H2, CHK_H3, CHK_H4, CHK_H5, CHK_H6, CHK_H7, CHK_H8]; + + TB_OTName.Click += (_, _) => ClickOT(sav2.OriginalTrainerTrash, TB_OTName); } if (SAV is SAV3 sav3) @@ -114,6 +118,8 @@ public SAV_SimpleTrainer(SaveFile sav) CB_BattleStyle.SelectedIndex = sav3.OptionBattleStyle ? 1 : 0; CB_SoundType.SelectedIndex = sav3.OptionSoundStereo ? 0 : 1; CHK_BattleEffects.Checked = sav3.OptionBattleScene; + + TB_OTName.Click += (_, _) => ClickOT(sav3.OriginalTrainerTrash, TB_OTName); } if (SAV is SAV3Colosseum or SAV3XD) { @@ -123,6 +129,11 @@ public SAV_SimpleTrainer(SaveFile sav) CAL_AdventureStartDate.Visible = CAL_HoFDate.Visible = false; CAL_AdventureStartTime.Visible = CAL_HoFTime.Visible = false; GB_Adventure.Visible = false; + + if (SAV is SAV3Colosseum colo) + TB_OTName.Click += (_, _) => ClickOT(colo.OriginalTrainerTrash, TB_OTName); + else if (SAV is SAV3XD xd) + TB_OTName.Click += (_, _) => ClickOT(xd.OriginalTrainerTrash, TB_OTName); return; } @@ -143,6 +154,8 @@ public SAV_SimpleTrainer(SaveFile sav) Main.SetCountrySubRegion(CB_Country, "gen4_countries"); CB_Country.SelectedValue = sav4.Country; CB_Region.SelectedValue = sav4.Region; + + TB_OTName.Click += (_, _) => ClickOT(sav4.OriginalTrainerTrash, TB_OTName); } else if (SAV is SAV5 s) { @@ -166,6 +179,8 @@ public SAV_SimpleTrainer(SaveFile sav) Main.SetCountrySubRegion(CB_Country, "gen5_countries"); CB_Country.SelectedValue = s.Country; CB_Region.SelectedValue = s.Region; + + TB_OTName.Click += (_, _) => ClickOT(s.PlayerData.OriginalTrainerTrash, TB_OTName); } for (int i = 0; i < cba.Length; i++) @@ -189,6 +204,17 @@ public SAV_SimpleTrainer(SaveFile sav) private readonly bool Loading; private bool MapUpdated; + private void ClickOT(Span text, TextBox tb) + { + // Special Character Form + if (ModifierKeys != Keys.Control) + return; + + var d = new TrashEditor(tb, text, SAV, SAV.Generation, SAV.Context); + d.ShowDialog(); + tb.Text = d.FinalString; + } + private void ChangeFFFF(object sender, EventArgs e) { MaskedTextBox box = (MaskedTextBox)sender;