Push more trashbyte rework

This commit is contained in:
Kurt 2026-03-06 21:55:18 -06:00
parent 5a75fe4b89
commit d24d227df4
15 changed files with 194 additions and 60 deletions

View File

@ -421,8 +421,9 @@ public sealed class LegalityCheckLocalization
public string TrashBytesExpected { get; set; } = "Expected Trash Bytes.";
public string TrashBytesMismatchInitial { get; set; } = "Expected initial trash bytes to match the encounter.";
public string TrashBytesMissingTerminator { get; set; } = "Final terminator missing.";
public string TrashBytesMissingTerminatorFinal { get; set; } = "Final terminator missing.";
public string TrashBytesShouldBeEmpty { get; set; } = "Trash Bytes should be cleared.";
public string TrashBytesResetViaTransfer { get; set; } = "Trash Bytes were reset via transfer.";
#endregion

View File

@ -410,8 +410,9 @@ public static class LegalityCheckResultCodeExtensions
TransferTrackerShouldBeZero => localization.TransferTrackerShouldBeZero,
TrashBytesExpected => localization.TrashBytesExpected,
TrashBytesMismatchInitial => localization.TrashBytesMismatchInitial,
TrashBytesMissingTerminator => localization.TrashBytesMissingTerminator,
TrashBytesMissingTerminatorFinal => localization.TrashBytesMissingTerminatorFinal,
TrashBytesShouldBeEmpty => localization.TrashBytesShouldBeEmpty,
TrashBytesResetViaTransfer => localization.TrashBytesResetViaTransfer,
WordFilterInvalidCharacter_0 => localization.WordFilterInvalidCharacter_0,
WordFilterFlaggedPattern_01 => localization.WordFilterFlaggedPattern_01,
WordFilterTooManyNumbers_0 => localization.WordFilterTooManyNumbers_0,

View File

@ -363,10 +363,13 @@ public enum LegalityCheckResultCode : ushort
TransferEncryptGen6Xor,
TransferTrackerMissing,
TransferTrackerShouldBeZero,
// Trash Bytes
TrashBytesExpected,
TrashBytesMismatchInitial,
TrashBytesMissingTerminator,
TrashBytesMissingTerminatorFinal,
TrashBytesShouldBeEmpty,
TrashBytesResetViaTransfer,
// Bulk Cross-Comparison
BulkCloneDetectedDetails,

View File

@ -16,6 +16,8 @@ public override void Verify(LegalityAnalysis data)
internal void Verify(LegalityAnalysis data, G3PKM pk)
{
VerifyTrash(data, pk);
if (ParseSettings.AllowGBACrossTransferRSE(pk))
return;
@ -34,56 +36,173 @@ internal void Verify(LegalityAnalysis data, G3PKM pk)
if ((Ball)pk.Ball is Ball.Dive or Ball.Premier)
data.AddLine(GetInvalid(BallUnavailable));
}
private void VerifyTrash(LegalityAnalysis data, G3PKM pk)
{
if (pk is PK3 pk3)
VerifyTrash(data, pk3);
else
VerifyTrashCXD(data, pk);
}
private void VerifyTrashCXD(LegalityAnalysis data, G3PKM pk)
{
// Buffers should be entirely clean.
var ot = pk.OriginalTrainerTrash;
var result = TrashBytesUTF16.IsTrashNone(ot);
if (result.IsInvalid)
data.AddLine(Get(Trainer, Severity.Invalid, TrashBytesShouldBeEmpty));
var nick = pk.NicknameTrash;
result = TrashBytesUTF16.IsTrashNone(nick);
if (result.IsInvalid)
data.AddLine(Get(Nickname, Severity.Invalid, TrashBytesShouldBeEmpty));
}
private void VerifyTrash(LegalityAnalysis data, PK3 pk)
{
if (!pk.IsEgg && TrashByteRules3.IsResetTrash(pk))
{
data.AddLine(GetValid(TrashBytesResetViaTransfer));
return; // OK
}
var enc = data.EncounterOriginal;
if (enc is EncounterTrade3)
VerifyTrashTrade(data, pk);
else if (enc is EncounterGift3 g3)
VerifyTrashEvent3(data, pk, g3);
else if (enc is EncounterGift3JPN jp)
VerifyTrashEvent3(data, pk, jp);
else if (enc is EncounterGift3NY ny)
VerifyTrashEvent3(data, pk, ny);
else if (pk.Japanese && !(pk.IsEgg && pk.OriginalTrainerTrash[^1] == 0xFF))
VerifyTrashJPN(data, pk);
else
VerifyTrashINT(data, pk);
}
private void VerifyTrashTrade(LegalityAnalysis data, PK3 pk)
private static void VerifyTrashEvent3(LegalityAnalysis data, PK3 pk, EncounterGift3NY ny)
{
// For in-game trades, zeroes after the first terminator.
var trash = pk.OriginalTrainerTrash;
int len = TrashBytes8.GetStringLength(trash);
if (len == trash.Length)
return; // OK
if (trash[(len+1)..].ContainsAnyExcept<byte>(0))
data.AddLine(GetInvalid(TrashBytesMissingTerminator));
// todo
}
private void VerifyTrashJPN(LegalityAnalysis data, PK3 pk)
private static void VerifyTrashEvent3(LegalityAnalysis data, PK3 pk, EncounterGift3JPN jp)
{
// todo
}
private static void VerifyTrashEvent3(LegalityAnalysis data, PK3 pk, EncounterGift3 g3)
{
// todo
}
private static void VerifyTrashTrade(LegalityAnalysis data, PK3 pk)
{
if (!TrashByteRules3.IsTerminatedZero(pk.OriginalTrainerTrash))
data.AddLine(GetInvalid(Trainer, TrashBytesShouldBeEmpty));
if (!TrashByteRules3.IsTerminatedZero(pk.NicknameTrash))
data.AddLine(GetInvalid(Nickname, TrashBytesShouldBeEmpty));
}
private static void VerifyTrashJPN(LegalityAnalysis data, PK3 pk)
{
var trash = pk.OriginalTrainerTrash;
// OT name from save file is copied byte-for-byte. Byte 7 & 8 are always zero.
// PK3 do not store the 8th byte. Check the 7th explicitly.
if (trash[^1] != 0x00)
data.AddLine(GetInvalid(TrashBytesMissingTerminator));
int len = TrashBytes8.GetStringLength(trash);
if (len >= trash.Length - 2)
return; // OK -- invalid lengths will get warned elsewhere
if (trash[len..^2].ContainsAnyExcept<byte>(0xFF))
data.AddLine(GetInvalid(TrashBytesMissingTerminator));
if (!TrashByteRules3.IsTerminatedFFZero(trash, 6))
data.AddLine(GetInvalid(Trainer, TrashBytesMissingTerminatorFinal));
// Nickname can be all FF's (nicknamed) or whatever random garbage is in the buffer before filling. Unsure if we can reliably check this, but it should be "dirty" usually.
// If it is clean, flag as fishy.
FlagIsNicknameClean(data, pk);
}
private void VerifyTrashINT(LegalityAnalysis data, PK3 pk)
private static void VerifyTrashINT(LegalityAnalysis data, PK3 pk)
{
var trash = pk.OriginalTrainerTrash;
// OT name from save file is copied byte-for-byte. All 8 bytes are initialized to FF on new game.
if (!TrashByteRules3.IsTerminatedFFZero(trash, 7))
data.AddLine(GetInvalid(Trainer, TrashBytesMissingTerminatorFinal));
// Nickname can be all FF's (nicknamed) or whatever random garbage is in the buffer before filling. Unsure if we can reliably check this, but it should be "dirty" usually.
// If it is clean, flag as fishy.
FlagIsNicknameClean(data, pk);
}
int len = TrashBytes8.GetStringLength(trash);
if (trash[len..].ContainsAnyExcept<byte>(0xFF))
data.AddLine(GetInvalid(TrashBytesMissingTerminator));
private static void FlagIsNicknameClean(LegalityAnalysis data, PK3 pk)
{
if (!pk.IsNicknamed || pk.IsEgg)
return;
var nick = pk.NicknameTrash;
if (!TrashByteRules3.IsTerminatedFF(nick))
data.AddLine(GetInvalid(Trainer, TrashBytesMismatchInitial));
}
}
public static class TrashByteRules3
{
// PK3 stores u8[length] for OT name and Nickname.
// Due to how the game initializes the buffer for each, specific patterns in the unused bytes (after the string, within the allocated max buffer) can arise.
// 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.
// - 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:
// - OT Name: todo
// - Nickname: todo
// For event encounters directly injected into the save file via GBA multiboot:
// - OT Name: todo
// - Nickname: todo
private const byte Terminator = StringConverter3.TerminatorByte;
public static bool IsResetTrash(PK3 pk3)
{
if (!ParseSettings.AllowGBACrossTransferXD(pk3))
return false;
if (!IsTerminatedZero(pk3.OriginalTrainerTrash))
return false;
if (pk3.IsNicknamed)
return true;
if (!IsTerminatedZero(pk3.NicknameTrash))
return false;
return true;
}
public static bool IsTerminatedZero(ReadOnlySpan<byte> data)
{
var first = TrashBytes8.GetTerminatorIndex(data);
if (first == -1 || first >= data.Length - 2)
return true;
return !data[(first+1)..].ContainsAnyExcept<byte>(0);
}
public static bool IsTerminatedFF(ReadOnlySpan<byte> data)
{
var first = TrashBytes8.GetTerminatorIndex(data);
if (first == -1 || first >= data.Length - 2)
return true;
return !data[(first + 1)..].ContainsAnyExcept<byte>(0xFF);
}
public static bool IsTerminatedFFZero(ReadOnlySpan<byte> data, int preFill = 0)
{
if (preFill == 0)
return IsTerminatedZero(data);
var first = TrashBytes8.GetTerminatorIndex(data);
if (first == -1 || first == data.Length - 2)
return true;
if (first < preFill)
{
var inner = data[first..preFill];
if (inner.ContainsAnyExcept(Terminator))
return false;
first = preFill;
if (first >= data.Length - 2)
return true;
}
return !data[(first + 1)..].ContainsAnyExcept<byte>(0);
}
}

View File

@ -62,11 +62,11 @@ private void VerifyTrashBytesPCD(LegalityAnalysis data, PKM pk, PCD pcd)
private void VerifyTrashBytesHOME(LegalityAnalysis data, PKM pk)
{
if (!TrashBytesUTF16.IsFinalTerminatorPresent(pk.NicknameTrash))
data.AddLine(GetInvalid(CheckIdentifier.Nickname, TrashBytesMissingTerminator));
data.AddLine(GetInvalid(CheckIdentifier.Nickname, TrashBytesMissingTerminatorFinal));
if (!TrashBytesUTF16.IsFinalTerminatorPresent(pk.OriginalTrainerTrash))
data.AddLine(GetInvalid(CheckIdentifier.Trainer, TrashBytesMissingTerminator));
data.AddLine(GetInvalid(CheckIdentifier.Trainer, TrashBytesMissingTerminatorFinal));
if (!TrashBytesUTF16.IsFinalTerminatorPresent(pk.HandlingTrainerTrash))
data.AddLine(GetInvalid(CheckIdentifier.Handler, TrashBytesMissingTerminator));
data.AddLine(GetInvalid(CheckIdentifier.Handler, TrashBytesMissingTerminatorFinal));
if (pk.IsEgg)
{

View File

@ -335,10 +335,11 @@
"TransferPIDECXor": "Encryption Constant matches shinyxored PID.",
"TransferTrackerMissing": "Pokémon HOME Transfer Tracker is missing.",
"TransferTrackerShouldBeZero": "Pokémon HOME Transfer Tracker should be 0.",
"TrashBytesExpected": "Expected Trash Bytes.",
"TrashBytesMismatchInitial": "Expected initial trash bytes to match the encounter.",
"TrashBytesMissingTerminator": "Final terminator missing.",
"TrashBytesShouldBeEmpty": "Trash Bytes should be cleared.",
"TrashBytesExpected": "Erwartete Trash-Bytes.",
"TrashBytesMismatchInitial": "Die anfänglichen Trash-Bytes sollten der Begegnung entsprechen.",
"TrashBytesMissingTerminatorFinal": "Abschließender Terminator fehlt.",
"TrashBytesShouldBeEmpty": "Trash-Bytes sollten gelöscht sein.",
"TrashBytesResetViaTransfer": "Trash-Bytes wurden durch Transfer zurückgesetzt.",
"EncTradeShouldHaveEvolvedToSpecies_0": "Trade Encounter should have evolved to species: {0}.",
"EncGiftLanguageNotDistributed": "Gift Encounter was never distributed with this language.",
"EncGiftRegionNotDistributed": "Gift Encounter was never distributed to this Console Region.",

View File

@ -337,8 +337,9 @@
"TransferTrackerShouldBeZero": "Pokémon HOME Transfer Tracker should be 0.",
"TrashBytesExpected": "Expected Trash Bytes.",
"TrashBytesMismatchInitial": "Expected initial trash bytes to match the encounter.",
"TrashBytesMissingTerminator": "Final terminator missing.",
"TrashBytesMissingTerminatorFinal": "Final terminator missing.",
"TrashBytesShouldBeEmpty": "Trash Bytes should be cleared.",
"TrashBytesResetViaTransfer": "Trash Bytes were reset via transfer.",
"EncTradeShouldHaveEvolvedToSpecies_0": "Trade Encounter should have evolved to species: {0}.",
"EncGiftLanguageNotDistributed": "Gift Encounter was never distributed with this language.",
"EncGiftRegionNotDistributed": "Gift Encounter was never distributed to this Console Region.",

View File

@ -335,10 +335,11 @@
"TransferPIDECXor": "La constante de encriptado coincide con el PID brillante.",
"TransferTrackerMissing": "Falta el rastreador de transferencia de Pokémon HOME.",
"TransferTrackerShouldBeZero": "El rastreador de transferencia de Pokémon HOME debería ser 0.",
"TrashBytesExpected": "Expected Trash Bytes.",
"TrashBytesMismatchInitial": "Expected initial trash bytes to match the encounter.",
"TrashBytesMissingTerminator": "Final terminator missing.",
"TrashBytesShouldBeEmpty": "Trash Bytes should be cleared.",
"TrashBytesExpected": "Se esperaban bytes basura.",
"TrashBytesMismatchInitial": "Se esperaba que los bytes basura iniciales coincidieran con el encuentro.",
"TrashBytesMissingTerminatorFinal": "Falta el terminador final.",
"TrashBytesShouldBeEmpty": "Los bytes basura deberían quedar borrados.",
"TrashBytesResetViaTransfer": "Los bytes basura se restablecieron por transferencia.",
"EncTradeShouldHaveEvolvedToSpecies_0": "Trade Encounter should have evolved to species: {0}.",
"EncGiftLanguageNotDistributed": "Gift Encounter was never distributed with this language.",
"EncGiftRegionNotDistributed": "Gift Encounter was never distributed to this Console Region.",

View File

@ -335,10 +335,11 @@
"TransferPIDECXor": "La constante de encriptado coincide con el PID variocolor.",
"TransferTrackerMissing": "Falta el rastreador de transferencia de Pokémon HOME.",
"TransferTrackerShouldBeZero": "El rastreador de transferencia de Pokémon HOME debería ser 0.",
"TrashBytesExpected": "Expected Trash Bytes.",
"TrashBytesMismatchInitial": "Expected initial trash bytes to match the encounter.",
"TrashBytesMissingTerminator": "Final terminator missing.",
"TrashBytesShouldBeEmpty": "Trash Bytes should be cleared.",
"TrashBytesExpected": "Se esperaban bytes basura.",
"TrashBytesMismatchInitial": "Se esperaba que los bytes basura iniciales coincidieran con el encuentro.",
"TrashBytesMissingTerminatorFinal": "Falta el terminador final.",
"TrashBytesShouldBeEmpty": "Los bytes basura deberían estar borrados.",
"TrashBytesResetViaTransfer": "Los bytes basura se restablecieron mediante transferencia.",
"EncTradeShouldHaveEvolvedToSpecies_0": "Trade Encounter should have evolved to species: {0}.",
"EncGiftLanguageNotDistributed": "Gift Encounter was never distributed with this language.",
"EncGiftRegionNotDistributed": "Gift Encounter was never distributed to this Console Region.",

View File

@ -337,8 +337,9 @@
"TransferTrackerShouldBeZero": "Le Tracker de transfert Pokémon HOME doit être 0.",
"TrashBytesExpected": "Des octets de déchet sont attendus.",
"TrashBytesMismatchInitial": "Des octets de déchet sont attendus pour correspondre à la rencontre.",
"TrashBytesMissingTerminator": "Le terminateur final est manquant.",
"TrashBytesMissingTerminatorFinal": "Le terminateur final est manquant.",
"TrashBytesShouldBeEmpty": "Les octets de déchet ne devraient pas exister.",
"TrashBytesResetViaTransfer": "Les octets résiduels ont été réinitialisés lors du transfert.",
"EncTradeShouldHaveEvolvedToSpecies_0": "La Rencontre par Échange aurait dû evoluer en {0}.",
"EncGiftLanguageNotDistributed": "La Rencontre n'a jamais été distribuée avec cette langue.",
"EncGiftRegionNotDistributed": "La Rencontre n'a jamais été distribuée à cette région de console.",

View File

@ -335,10 +335,11 @@
"TransferPIDECXor": "La Encryption Constant corrisponde ad un PID shiny.",
"TransferTrackerMissing": "Il codice di monitoraggio di Pokémon Home è mancante.",
"TransferTrackerShouldBeZero": "Il codice di monitoraggio di Pokémon Home dovrebbe essere 0.",
"TrashBytesExpected": "Expected Trash Bytes.",
"TrashBytesMismatchInitial": "Expected initial trash bytes to match the encounter.",
"TrashBytesMissingTerminator": "Final terminator missing.",
"TrashBytesShouldBeEmpty": "Trash Bytes should be cleared.",
"TrashBytesExpected": "Previsti byte spazzatura.",
"TrashBytesMismatchInitial": "I byte spazzatura iniziali previsti devono corrispondere all'incontro.",
"TrashBytesMissingTerminatorFinal": "Manca il terminatore finale.",
"TrashBytesShouldBeEmpty": "I byte spazzatura dovrebbero essere azzerati.",
"TrashBytesResetViaTransfer": "I byte spazzatura sono stati reimpostati tramite trasferimento.",
"EncTradeShouldHaveEvolvedToSpecies_0": "Trade Encounter should have evolved to species: {0}.",
"EncGiftLanguageNotDistributed": "Gift Encounter was never distributed with this language.",
"EncGiftRegionNotDistributed": "Gift Encounter was never distributed to this Console Region.",

View File

@ -335,10 +335,11 @@
"TransferPIDECXor": "暗号化定数と色違い性格値が一致します。",
"TransferTrackerMissing": "HOME Trackerが見つかりません。",
"TransferTrackerShouldBeZero": "HOME Trackerをにしてください。",
"TrashBytesExpected": "Expected Trash Bytes.",
"TrashBytesMismatchInitial": "Expected initial trash bytes to match the encounter.",
"TrashBytesMissingTerminator": "Final terminator missing.",
"TrashBytesShouldBeEmpty": "Trash Bytes should be cleared.",
"TrashBytesExpected": "想定されるゴミバイトです。",
"TrashBytesMismatchInitial": "遭遇データと一致する初期ゴミバイトである必要があります。",
"TrashBytesMissingTerminatorFinal": "最終終端子がありません。",
"TrashBytesShouldBeEmpty": "ゴミバイトは消去されている必要があります。",
"TrashBytesResetViaTransfer": "転送によりゴミバイトがリセットされました。",
"EncTradeShouldHaveEvolvedToSpecies_0": "Trade Encounter should have evolved to species: {0}.",
"EncGiftLanguageNotDistributed": "Gift Encounter was never distributed with this language.",
"EncGiftRegionNotDistributed": "Gift Encounter was never distributed to this Console Region.",

View File

@ -335,10 +335,11 @@
"TransferPIDECXor": "암호화 상수가 색이 다르도록 계산된 PID와 일치합니다.",
"TransferTrackerMissing": "Pokémon HOME Transfer Tracker is missing.",
"TransferTrackerShouldBeZero": "Pokémon HOME Transfer Tracker should be 0.",
"TrashBytesExpected": "Expected Trash Bytes.",
"TrashBytesMismatchInitial": "Expected initial trash bytes to match the encounter.",
"TrashBytesMissingTerminator": "Final terminator missing.",
"TrashBytesShouldBeEmpty": "Trash Bytes should be cleared.",
"TrashBytesExpected": "예상된 쓰레기 바이트입니다.",
"TrashBytesMismatchInitial": "초기 쓰레기 바이트가 해당 조우 데이터와 일치해야 합니다.",
"TrashBytesMissingTerminatorFinal": "마지막 종료 문자가 없습니다.",
"TrashBytesShouldBeEmpty": "쓰레기 바이트는 지워져 있어야 합니다.",
"TrashBytesResetViaTransfer": "전송으로 인해 쓰레기 바이트가 초기화되었습니다.",
"EncTradeShouldHaveEvolvedToSpecies_0": "Trade Encounter should have evolved to species: {0}.",
"EncGiftLanguageNotDistributed": "Gift Encounter was never distributed with this language.",
"EncGiftRegionNotDistributed": "Gift Encounter was never distributed to this Console Region.",

View File

@ -337,8 +337,9 @@
"TransferTrackerShouldBeZero": "Pokémon HOME传输追踪信息应为0。",
"TrashBytesExpected": "预期的垃圾字节。",
"TrashBytesMismatchInitial": "期望的初始垃圾字节应与相遇匹配。",
"TrashBytesMissingTerminator": "缺少最终结束符。",
"TrashBytesMissingTerminatorFinal": "缺少最终结束符。",
"TrashBytesShouldBeEmpty": "垃圾字节应被清除。",
"TrashBytesResetViaTransfer": "垃圾字节已在传输时重置。",
"EncTradeShouldHaveEvolvedToSpecies_0": "通信交换获得的宝可梦应该已进化为:{0}。",
"EncGiftLanguageNotDistributed": "礼物相遇从未以该语言发放。",
"EncGiftRegionNotDistributed": "礼物相遇从未在该主机地区发放。",

View File

@ -335,10 +335,11 @@
"TransferPIDECXor": "加密常量與異色 PID 之 XOR 值匹配。",
"TransferTrackerMissing": "Pokémon HOME 傳輸跟蹤碼資訊丟失。",
"TransferTrackerShouldBeZero": "Pokémon HOME 傳輸跟蹤碼資訊應置 0。",
"TrashBytesExpected": "Expected Trash Bytes.",
"TrashBytesMismatchInitial": "Expected initial trash bytes to match the encounter.",
"TrashBytesMissingTerminator": "Final terminator missing.",
"TrashBytesShouldBeEmpty": "Trash Bytes should be cleared.",
"TrashBytesExpected": "預期的垃圾位元組。",
"TrashBytesMismatchInitial": "預期的初始垃圾位元組應與相遇相符。",
"TrashBytesMissingTerminatorFinal": "缺少最終終止符。",
"TrashBytesShouldBeEmpty": "垃圾位元組應被清除。",
"TrashBytesResetViaTransfer": "垃圾位元組已在傳輸時重設。",
"EncTradeShouldHaveEvolvedToSpecies_0": "Trade Encounter should have evolved to species: {0}.",
"EncGiftLanguageNotDistributed": "Gift Encounter was never distributed with this language.",
"EncGiftRegionNotDistributed": "Gift Encounter was never distributed to this Console Region.",