Allow edits for SAV OT trash bytes for gen1-5

This commit is contained in:
Kurt 2026-03-11 00:15:58 -05:00
parent 662c3db7dc
commit 301a1e7664
6 changed files with 48 additions and 10 deletions

View File

@ -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<byte> 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<byte>(0);
}
@ -183,7 +187,7 @@ public static bool IsTerminatedZero(ReadOnlySpan<byte> data)
public static bool IsTerminatedFF(ReadOnlySpan<byte> 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<byte>(0xFF);
}
@ -194,17 +198,20 @@ public static bool IsTerminatedFFZero(ReadOnlySpan<byte> 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<byte>(0);
return !data[first..].ContainsAnyExcept<byte>(0);
}
}

View File

@ -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<byte> 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); }

View File

@ -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<byte> 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); }

View File

@ -255,10 +255,13 @@ public override int PartyCount
public sealed override int GetPartyOffset(int slot) => Party + (SIZE_PARTY * slot);
#region Trainer Info
public Span<byte> 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

View File

@ -8,7 +8,7 @@ namespace PKHeX.Core;
/// </summary>
public abstract class PlayerData5(SAV5 sav, Memory<byte> raw) : SaveBlock<SAV5>(sav, raw)
{
private Span<byte> OriginalTrainerTrash => Data.Slice(4, 0x10);
public Span<byte> OriginalTrainerTrash => Data.Slice(4, 0x10);
public string OT
{

View File

@ -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<byte> 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;