Compare commits

..

30 Commits

Author SHA1 Message Date
间辞
66df00d038
Add files via upload (#4760) 2026-03-21 02:20:20 -05:00
Kurt
4784e2de82 Update ItemStorage3E.cs
Closes #4759
2026-03-21 01:43:10 -05:00
Kurt
a43b6a5d32 Update 26.03.20 2026-03-20 22:10:14 -05:00
Kurt
48938c5e14 Minor clean 2026-03-19 20:35:58 -05:00
Kurt
0e097b1fc6 Minor slot hover performance improvements
Skip repaint on cursor moving the hover window
Cache reference to the slot interaction types and "nothing" slot image
Dispose of slot sprites when updating with a new one
If scrolling box/group, auto-update hover with the newly displayed slot's content instead of hiding
2026-03-19 17:25:06 -05:00
Kurt
56ba92b68b Handle ZA's EOL revision (2)
When throwing arg out of range, pass the value so it's obvious the number
2026-03-19 15:39:07 -05:00
Kurt
3ad376be44 Misc tweaks
Add rejections for shiny criteria for gen8+ generators

Unrelated: use recent c# lang feature for settings property field
2026-03-19 09:38:02 -05:00
Kurt
ca6fbf024c Add new virtual method for pre-applying Nickname
Results in correct trash bytes for user-Nickname'd mons as if they were nicknamed via the in-game menu.
2026-03-19 03:20:42 -05:00
Kurt
3c1e7bdc6c Misc tweaks
Overworld8a: acknowledge Nature request
8U/8N: remove unnecessary auto-mint (not applied anywhere else)
Nature: use extension properties, use the `IsFixed` check throughout codebase
Wallpaper: add PLA default to pasture (not-obvious prior behavior was removed in refactor).
Tests: fix ck3 file with OT trash bytes (now cleared)
Tests: fix pk3 file with OT trash bytes now passes (added 1 trash pattern, future work)
Trash3: initial stubs for default OT trash recognition (one included to pass above ^)
2026-03-18 01:17:17 -05:00
Kurt
b1dbc6a82b Update SAV4.cs 2026-03-17 02:23:03 -05:00
Kurt
94ad477703 Fix rs/e inventory length
Closes #4756
2026-03-16 08:20:56 -05:00
Kurt
42496def98 PKM Editor BK4 don't set BallHGSS 2026-03-16 01:57:21 -05:00
Kurt
8950613422 Add party import for hall of fame 3
Closes #4754
2026-03-15 18:25:13 -05:00
Kurt
2faa1b10b1 Revise translation form scrape
Iterate through all constructor paths
2026-03-14 22:33:44 -05:00
Professor Dirty
ff69f82234
Add CHS translation and correct frlg flags format (#4753) 2026-03-14 22:28:18 -05:00
Kurt
3317a8bfda Add trash view/edit to all Trainer forms
Make PID text entry uppercase across the program
Standardize RivalTrash => RivalNameTrash, same for Rival => RivalName
2026-03-14 22:28:00 -05:00
Kurt
94f3937f2f Refactor: extract gen3 save block structures
Changed: Inventory editor no longer needs to clone the save file on GUI open
Changed: some method signatures have moved from SAV3* to the specific block
Allows the block structures to be used without a SAV3 object
Allows the Inventory editor to open from a blank save file.
2026-03-14 13:40:17 -05:00
Victor Borges
a3e0ed29b4
Fix YlwApricorn and BluApricorn gen 2 sprite IDs (#4752) 2026-03-13 00:39:02 -05:00
Kurt
d3a4ed6ad3 Update PKMEditor.Designer.cs 2026-03-12 01:34:05 -05:00
Kurt
bb363a7a3d Re-flow Stat editor for alignment/showing all text
Some languages have localized these labels to long strings; previously they were truncated (probably frustrating); now, they all show (wrapped text is the best I can do -- better than truncating?)
2026-03-12 01:29:58 -05:00
Professor Dirty
2f95536b13
Update CHS translation of FRLG flags (#4751) 2026-03-11 12:15:47 -05:00
Kurt
301a1e7664 Allow edits for SAV OT trash bytes for gen1-5 2026-03-11 00:15:58 -05:00
Kurt
662c3db7dc Faster box hover preview
Render it manually rather than let controls be goofy with draw calls.
2026-03-09 20:28:43 -05:00
Kurt
5b42ff746d Handle handle leaks on dragdrop cursor icon
Also pass the tooltips to the components container so that they dispose of anything if needed too.
A user had a long-running script/session that drag-dropped a few thousand times, which exhausted the Windows GDI handle limit (10,000 per process).
2026-03-09 12:25:15 -05:00
sora10pls
c7f02bcc20 GO: more tweaks to 30th Anniversary event handling 2026-03-09 13:11:23 -04:00
Kurt
7617f6dfa7 Revise trash check for Japanese nickname
Closes #4750
2026-03-08 23:41:20 -05:00
Kurt
8b08f263e5 Minor tweaks
Remove GameSync/SecureValue from SAV tab (still lives in Block Data)
Remove inaccessible items from FRLG/E key items
2026-03-08 23:40:56 -05:00
Professor Dirty
d827cec5a7
Update Crystal Event Flags translation in Chinese (#4749) 2026-03-07 23:39:32 -06:00
sora10pls
e5e7cc914c Pokémon 30th Anniversary: All Out event handling 2026-03-07 18:44:33 -05:00
Kurt
ff72af52ad Update 26.03.06 2026-03-06 23:15:14 -06:00
190 changed files with 4565 additions and 4040 deletions

View File

@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>26.03.06</Version>
<Version>26.03.20</Version>
<LangVersion>14</LangVersion>
<Nullable>enable</Nullable>
<NeutralLanguage>en</NeutralLanguage>

View File

@ -342,12 +342,12 @@ private bool ParseLineNature(ReadOnlySpan<char> input, ReadOnlySpan<string> natu
return false;
var nature = (Nature)index;
if (!nature.IsFixed())
if (!nature.IsFixed)
{
LogError(NatureUnrecognized, input);
return false;
}
if (Nature != Nature.Random && Nature != nature)
if (Nature.IsFixed && Nature != nature)
{
LogError(NatureAlreadySpecified, input);
return false;
@ -627,7 +627,7 @@ private void AddEVs(List<string> result, in BattleTemplateExportSettings setting
BattleTemplateToken.EVsAppendNature => GetStringStatsNatureAmp(EVs, 0, nameEVs, Nature),
_ => GetStringStats(EVs, 0, nameEVs),
};
if (token is BattleTemplateToken.EVsAppendNature && Nature.IsFixed())
if (token is BattleTemplateToken.EVsAppendNature && Nature.IsFixed)
line += $" ({settings.Localization.Strings.natures[(int)Nature]})";
result.Add(cfg.Push(BattleTemplateToken.EVs, line));
}
@ -1081,7 +1081,7 @@ private bool ParseLineEVs(ReadOnlySpan<char> line, BattleTemplateLocalization lo
return false; // invalid line
}
if (Nature != Nature.Random) // specified in a separate Nature line
if (Nature.IsFixed) // specified in a separate Nature line
LogError(NatureEffortAmpAlreadySpecified, natureName);
else
Nature = (Nature)natureIndex;
@ -1100,7 +1100,7 @@ private bool ParseLineEVs(ReadOnlySpan<char> line, BattleTemplateLocalization lo
result.TreatAmpsAsSpeedNotLast();
var ampNature = AdjustNature(result.Plus, result.Minus);
success &= ampNature;
if (ampNature && currentNature != Nature.Random && currentNature != Nature)
if (ampNature && currentNature.IsFixed && currentNature != Nature)
{
LogError(NatureEffortAmpConflictNature);
Nature = currentNature; // revert to original

View File

@ -205,10 +205,10 @@ public ModifyResult TryModify(TObject entity, IEnumerable<StringInstruction> fil
return result;
}
private static Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[] GetPropertyDictionaries(IReadOnlyList<Type> types, int expectedMax)
private static Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[] GetPropertyDictionaries(ReadOnlySpan<Type> types, int expectedMax)
{
var result = new Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[types.Count];
for (int i = 0; i < types.Count; i++)
var result = new Dictionary<string, PropertyInfo>.AlternateLookup<ReadOnlySpan<char>>[types.Length];
for (int i = 0; i < types.Length; i++)
result[i] = GetPropertyDictionary(types[i], ReflectUtil.GetAllPropertyInfoPublic, expectedMax).GetAlternateLookup<ReadOnlySpan<char>>();
return result;
}

View File

@ -30,8 +30,10 @@ public void SetNickname(string nick)
pk.ClearNickname();
return;
}
pk.IsNicknamed = true;
pk.PrepareNickname();
pk.Nickname = nick;
pk.IsNicknamed = true;
}
/// <summary>
@ -137,7 +139,7 @@ public bool SetUnshiny()
/// <param name="nature">Desired <see cref="PKM.Nature"/> value to set.</param>
public void SetNature(Nature nature)
{
if (!nature.IsFixed())
if (!nature.IsFixed)
nature = 0; // default valid
var format = pk.Format;

View File

@ -1,21 +1,12 @@
using System;
using System;
namespace PKHeX.Core;
/// <summary>
/// Base class for defining a manipulation of box data.
/// </summary>
public abstract class BoxManipBase : IBoxManip
public abstract record BoxManipBase(BoxManipType Type, Func<SaveFile, bool> Usable) : IBoxManip
{
public BoxManipType Type { get; }
public Func<SaveFile, bool> Usable { get; }
protected BoxManipBase(BoxManipType type, Func<SaveFile, bool> usable)
{
Type = type;
Usable = usable;
}
public abstract string GetPrompt(bool all);
public abstract string GetFail(bool all);
public abstract string GetSuccess(bool all);

View File

@ -5,7 +5,7 @@ namespace PKHeX.Core;
/// <summary>
/// Clears contents of boxes by deleting all that satisfy a criteria.
/// </summary>
public sealed class BoxManipClear(BoxManipType Type, Func<PKM, bool> criteria, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
public sealed record BoxManipClear(BoxManipType Type, Func<PKM, bool> Criteria, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
{
public BoxManipClear(BoxManipType Type, Func<PKM, bool> Criteria) : this(Type, Criteria, _ => true) { }
@ -18,6 +18,6 @@ public override int Execute(SaveFile sav, BoxManipParam param)
var (start, stop, reverse) = param;
return sav.ClearBoxes(start, stop, Method);
bool Method(PKM p) => reverse ^ criteria(p);
bool Method(PKM p) => reverse ^ Criteria(p);
}
}

View File

@ -5,7 +5,7 @@ namespace PKHeX.Core;
/// <summary>
/// Clears contents of boxes by deleting all that satisfy a criteria based on a <see cref="SaveFile"/>.
/// </summary>
public sealed class BoxManipClearComplex(BoxManipType Type, Func<PKM, SaveFile, bool> criteria, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
public sealed record BoxManipClearComplex(BoxManipType Type, Func<PKM, SaveFile, bool> Criteria, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
{
public BoxManipClearComplex(BoxManipType Type, Func<PKM, SaveFile, bool> Criteria) : this(Type, Criteria, _ => true) { }
@ -18,6 +18,6 @@ public override int Execute(SaveFile sav, BoxManipParam param)
var (start, stop, reverse) = param;
return sav.ClearBoxes(start, stop, Method);
bool Method(PKM p) => reverse ^ criteria(p, sav);
bool Method(PKM p) => reverse ^ Criteria(p, sav);
}
}

View File

@ -7,7 +7,7 @@ namespace PKHeX.Core;
/// Clears contents of boxes by deleting all but the first duplicate detected.
/// </summary>
/// <typeparam name="T">Base type of the "is duplicate" hash for the duplicate detection.</typeparam>
public sealed class BoxManipClearDuplicate<T> : BoxManipBase
public sealed record BoxManipClearDuplicate<T> : BoxManipBase
{
private readonly HashSet<T> HashSet = [];
private readonly Func<PKM, bool> Criteria;

View File

@ -5,8 +5,8 @@ namespace PKHeX.Core;
/// <summary>
/// Modifies contents of boxes by using an <see cref="Action"/> to change data.
/// </summary>
public sealed class BoxManipModify(BoxManipType type, Action<PKM> Action, Func<SaveFile, bool> Usable)
: BoxManipBase(type, Usable)
public sealed record BoxManipModify(BoxManipType Type, Action<PKM> Action, Func<SaveFile, bool> Usable)
: BoxManipBase(Type, Usable)
{
public BoxManipModify(BoxManipType type, Action<PKM> Action) : this(type, Action, _ => true) { }

View File

@ -5,7 +5,7 @@ namespace PKHeX.Core;
/// <summary>
/// Modifies contents of boxes by using an <see cref="Action"/> (referencing a Save File) to change data.
/// </summary>
public sealed class BoxManipModifyComplex(BoxManipType Type, Action<PKM, SaveFile> Action, Func<SaveFile, bool> Usable)
public sealed record BoxManipModifyComplex(BoxManipType Type, Action<PKM, SaveFile> Action, Func<SaveFile, bool> Usable)
: BoxManipBase(Type, Usable)
{
public BoxManipModifyComplex(BoxManipType Type, Action<PKM, SaveFile> Action) : this(Type, Action, _ => true) { }

View File

@ -6,7 +6,7 @@ namespace PKHeX.Core;
/// <summary>
/// Sorts contents of boxes by using a Sorter to determine the order.
/// </summary>
public sealed class BoxManipSort(BoxManipType Type, Func<IEnumerable<PKM>, IEnumerable<PKM>> Sorter, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
public sealed record BoxManipSort(BoxManipType Type, Func<IEnumerable<PKM>, IEnumerable<PKM>> Sorter, Func<SaveFile, bool> Usable) : BoxManipBase(Type, Usable)
{
public BoxManipSort(BoxManipType Type, Func<IEnumerable<PKM>, IEnumerable<PKM>> Sorter) : this(Type, Sorter, _ => true) { }

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
namespace PKHeX.Core;
@ -6,7 +6,7 @@ namespace PKHeX.Core;
/// <summary>
/// Sorts contents of boxes by using a <see cref="Sorter"/> (referencing a Save File) to determine the order.
/// </summary>
public sealed class BoxManipSortComplex : BoxManipBase
public sealed record BoxManipSortComplex : BoxManipBase
{
private readonly Func<IEnumerable<PKM>, SaveFile, int, IEnumerable<PKM>> Sorter;
public BoxManipSortComplex(BoxManipType type, Func<IEnumerable<PKM>, SaveFile, IEnumerable<PKM>> sorter) : this(type, sorter, _ => true) { }

View File

@ -61,11 +61,11 @@ private static List<SlotInfoMisc> GetExtraSlots2(SAV2 sav)
private static List<SlotInfoMisc> GetExtraSlots3(SAV3 sav)
{
if (sav is not SAV3FRLG)
if (sav is not SAV3FRLG frlg)
return None;
return
[
new(sav.LargeBuffer[0x3C98..], 0) {Type = StorageSlotType.Daycare},
new(frlg.LargeBlock.SingleDaycareRoute5, 0) {Type = StorageSlotType.Daycare},
];
}

View File

@ -51,7 +51,7 @@ public enum StorageSlotType : byte
/// Shiny Overworld Cache
/// </summary>
/// <remarks>
/// <see cref="GameVersion.ZA"/>
/// <see cref="GameVersion.ZA"/>
/// </remarks>
Shiny,

View File

@ -55,7 +55,7 @@ public static class NatureUtil
/// Checks if the provided <see cref="value"/> is a valid stored <see cref="Nature"/> value.
/// </summary>
/// <returns>True if value is an actual nature.</returns>
public bool IsFixed() => value < Nature.Random;
public bool IsFixed => value != Nature.Random;
/// <summary>
/// Checks if the provided <see cref="value"/> is a possible mint nature.
@ -63,12 +63,12 @@ public static class NatureUtil
/// <remarks>
/// The only valid mint natures are those which have a stat amp applied, or neutral nature being Serious.
/// </remarks>
public bool IsMint() => (value.IsFixed() && (byte)value % 6 != 0) || value == Nature.Serious;
public bool IsMint => (value.IsFixed && (byte)value % 6 != 0) || value == Nature.Serious;
/// <summary>
/// Checks if the provided <see cref="value"/> is a neutral nature which has no stat amps applied.
/// </summary>
public bool IsNeutral() => value.IsFixed() && (byte)value % 6 == 0;
public bool IsNeutral => value.IsFixed && (byte)value % 6 == 0;
/// <summary>
/// Converts the provided <see cref="value"/> to a neutral nature.

View File

@ -6,8 +6,6 @@ namespace PKHeX.Core;
public sealed class PlayerBag3E : PlayerBag, IPlayerBag3
{
private const int BaseOffset = 0x0498;
public override IReadOnlyList<InventoryPouch3> Pouches { get; } = GetPouches(ItemStorage3E.Instance);
public override ItemStorage3E Info => ItemStorage3E.Instance;
@ -21,7 +19,7 @@ public sealed class PlayerBag3E : PlayerBag, IPlayerBag3
new(0x000, 50, 999, info, PCItems),
];
public PlayerBag3E(SAV3E sav) : this(sav.Large[BaseOffset..], sav.SecurityKey) { }
public PlayerBag3E(SAV3E sav) : this(sav.LargeBlock.Inventory, sav.SmallBlock.SecurityKey) { }
public PlayerBag3E(ReadOnlySpan<byte> data, uint security)
{
UpdateSecurityKey(security);
@ -29,7 +27,7 @@ public PlayerBag3E(ReadOnlySpan<byte> data, uint security)
}
public override void CopyTo(SaveFile sav) => CopyTo((SAV3E)sav);
public void CopyTo(SAV3E sav) => CopyTo(sav.Large[BaseOffset..]);
public void CopyTo(SAV3E sav) => CopyTo(sav.LargeBlock.Inventory);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)

View File

@ -6,7 +6,6 @@ namespace PKHeX.Core;
public sealed class PlayerBag3FRLG(bool VC) : PlayerBag, IPlayerBag3
{
private const int BaseOffset = 0x0298;
public override IItemStorage Info => GetInfo(VC);
private static IItemStorage GetInfo(bool vc) => vc ? ItemStorage3FRLG_VC.Instance : ItemStorage3FRLG.Instance;
public override IReadOnlyList<InventoryPouch3> Pouches { get; } = GetPouches(GetInfo(VC));
@ -21,7 +20,7 @@ public sealed class PlayerBag3FRLG(bool VC) : PlayerBag, IPlayerBag3
new(0x000, 30, 999, info, PCItems),
];
public PlayerBag3FRLG(SAV3FRLG sav) : this(sav.Large[BaseOffset..], sav.SecurityKey, sav.IsVirtualConsole) { }
public PlayerBag3FRLG(SAV3FRLG sav) : this(sav.LargeBlock.Inventory, sav.SmallBlock.SecurityKey, sav.IsVirtualConsole) { }
public PlayerBag3FRLG(ReadOnlySpan<byte> data, uint security, bool vc) : this(vc)
{
UpdateSecurityKey(security);
@ -29,7 +28,7 @@ public PlayerBag3FRLG(ReadOnlySpan<byte> data, uint security, bool vc) : this(vc
}
public override void CopyTo(SaveFile sav) => CopyTo((SAV3FRLG)sav);
public void CopyTo(SAV3FRLG sav) => CopyTo(sav.Large[BaseOffset..]);
public void CopyTo(SAV3FRLG sav) => CopyTo(sav.LargeBlock.Inventory);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)

View File

@ -6,8 +6,6 @@ namespace PKHeX.Core;
public sealed class PlayerBag3RS : PlayerBag
{
private const int BaseOffset = 0x0498;
public override IReadOnlyList<InventoryPouch3> Pouches { get; } = GetPouches(ItemStorage3RS.Instance);
public override ItemStorage3RS Info => ItemStorage3RS.Instance;
@ -21,11 +19,11 @@ public sealed class PlayerBag3RS : PlayerBag
new(0x000, 50, 999, info, PCItems),
];
public PlayerBag3RS(SAV3RS sav) : this(sav.Large[BaseOffset..]) { }
public PlayerBag3RS(SAV3RS sav) : this(sav.LargeBlock.Inventory) { }
public PlayerBag3RS(ReadOnlySpan<byte> data) => Pouches.LoadAll(data);
public override void CopyTo(SaveFile sav) => CopyTo((SAV3RS)sav);
public void CopyTo(SAV3RS sav) => CopyTo(sav.Large[BaseOffset..]);
public void CopyTo(SAV3RS sav) => CopyTo(sav.LargeBlock.Inventory);
public void CopyTo(Span<byte> data) => Pouches.SaveAll(data);
public override int GetMaxCount(InventoryType type, int itemIndex)

View File

@ -13,9 +13,9 @@ public sealed class ItemStorage3E : IItemStorage
public static ReadOnlySpan<ushort> Key =>
[
// R/S
259, 260, 261, 262, 263, 264, 265, 266, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288,
259, 260, 261, 262, 263, 264, 265, 266, 268, 269, 270, 271, 272, 273, 274, 275, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288,
// FR/LG
349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374,
370, 371, 372,
// E
375, 376,
];

View File

@ -13,7 +13,7 @@ public sealed class ItemStorage3FRLG : IItemStorage
public static ReadOnlySpan<ushort> Key =>
[
// R/S
259, 260, 261, 262, 263, 264, 265, 266, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288,
260, 261, 262, 263, 264, 265,
// FR/LG
349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374,
];
@ -55,7 +55,7 @@ public sealed class ItemStorage3FRLG_VC : IItemStorage // TODO VC RSE: delete me
// Unobtainable
044, // Berry Juice
// TODO RSE VC: Remove these
046, 047, // Shoal Salt, Shoal Shell
048, 049, 050, 051, // Shards
@ -71,7 +71,7 @@ public sealed class ItemStorage3FRLG_VC : IItemStorage // TODO VC RSE: delete me
173, // Lansat Berry (Event)
174, // Starf Berry (Event)
175, // Enigma Berry (Event)
// TODO RSE VC: Remove these
179, // BrightPowder
180, // White Herb

View File

@ -112,7 +112,7 @@ public EncounterCriteria()
/// Determines whether a specific Nature is specified in the criteria or if complex nature mutations are allowed.
/// </summary>
/// <returns>><see langword="true"/> if a Nature is specified or complex nature mutations are allowed; otherwise, <see langword="false"/>.</returns>
public bool IsSpecifiedNature() => Nature != Nature.Random || Mutations.IsComplexNature();
public bool IsSpecifiedNature() => Nature.IsFixed || Mutations.IsComplexNature();
/// <summary>
/// Determines whether a level range is specified in the criteria.
@ -126,6 +126,12 @@ public EncounterCriteria()
/// <returns>><see langword="true"/> if an Ability is specified; otherwise, <see langword="false"/>.</returns>
public bool IsSpecifiedAbility() => Ability != Any12H;
/// <summary>
/// Determines whether the shiny value is explicitly specified rather than set to random.
/// </summary>
/// <returns>><see langword="true"/> if a Shiny is specified; otherwise, <see langword="false"/>.</returns>
public bool IsSpecifiedShiny() => Shiny != Shiny.Random;
/// <summary>
/// Determines whether all IVs are specified in the criteria.
/// </summary>
@ -183,6 +189,20 @@ public int GetCountSpecifiedIVs() => Convert.ToInt32(IV_HP != RandomIV)
_ => throw new ArgumentOutOfRangeException(nameof(ability), ability, null),
};
/// <summary>
/// Determines whether the specified shiny properties satisfy the shiny criteria based on the current <see cref="Shiny"/> setting.
/// </summary>
/// <returns>><see langword="true"/> if the index satisfies the shiny criteria; otherwise, <see langword="false"/>.</returns>
public bool IsSatisfiedShiny(uint xor, uint cmp) => Shiny switch
{
Shiny.Random => true,
Shiny.Never => xor > cmp, // not shiny
Shiny.AlwaysSquare => xor == 0, // square shiny
Shiny.AlwaysStar => xor < cmp && xor != 0, // star shiny
Shiny.Always => xor < cmp, // shiny
_ => false, // shouldn't be set
};
/// <summary>
/// Determines whether the specified Nature satisfies the criteria.
/// </summary>
@ -191,7 +211,7 @@ public int GetCountSpecifiedIVs() => Convert.ToInt32(IV_HP != RandomIV)
public bool IsSatisfiedNature(Nature nature)
{
if (Mutations.HasFlag(AllowOnlyNeutralNature))
return nature.IsNeutral();
return nature.IsNeutral;
if (Nature == Nature.Random)
return true;
return nature == Nature || Mutations.HasFlag(CanMintNature);
@ -300,7 +320,7 @@ public Nature GetNature(Nature encValue)
/// </summary>
public Nature GetNature()
{
if (Nature != Nature.Random)
if (Nature.IsFixed)
return Nature;
var result = (Nature)Util.Rand.Next(25);
if (Mutations.HasFlag(AllowOnlyNeutralNature))

View File

@ -12,6 +12,8 @@ public enum PogoType : byte
// Pokémon captured in the wild.
Wild,
WildLevel20,
WildLevel25,
// Pokémon hatched from Eggs.
Egg,
@ -137,6 +139,8 @@ public static class PogoTypeExtensions
public byte LevelMin => encounterType switch
{
Wild => 1,
WildLevel20 => 20,
WildLevel25 => 25,
Egg => 1,
Egg12km => 8,
Raid => 20,
@ -204,6 +208,8 @@ public static class PogoTypeExtensions
public int MinimumIV => encounterType switch
{
Wild => 0,
WildLevel20 => 0,
WildLevel25 => 0,
RaidMythical => 10,
RaidShadowMythical => 8,
RaidShadowMythicalGOWA => 8,
@ -324,6 +330,6 @@ public bool IsBallValid(Ball ball)
_ => Ball.None, // Poké, Great, Ultra
};
public bool IsSpecialResearch => encounterType is >= SpecialMythical and < TimedMythical;
public bool IsSpecialResearch => encounterType is SpecialResearch or >= SpecialMythical and < TimedMythical;
}
}

View File

@ -155,7 +155,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
return false;
if (Gender != FixedGenderUtil.GenderRandom && Gender != pk.Gender)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;
}

View File

@ -153,7 +153,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (IVs.IsSpecified && !Legal.GetIsFixedIVSequenceValidSkipRand(IVs, pk))
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;

View File

@ -167,7 +167,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
return false;
if (Gender != pk.Gender)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;
}

View File

@ -145,7 +145,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (IVs.IsSpecified && !Legal.GetIsFixedIVSequenceValidSkipRand(IVs, pk))
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;

View File

@ -166,7 +166,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
return false;
if (Gender != pk.Gender)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;
}

View File

@ -122,8 +122,6 @@ protected virtual void SetPINGA(PK8 pk, in EncounterCriteria criteria, PersonalI
}
FinishCorrelation(pk, seed);
if (criteria.IsSpecifiedNature() && criteria.Nature != pk.Nature && criteria.Nature.IsMint())
pk.StatNature = criteria.Nature;
}
protected GenerateParam8 GetParam() => GetParam(Info);

View File

@ -57,6 +57,10 @@ protected override void SetPINGA(PK8 pk, in EncounterCriteria criteria, Personal
{
Span<int> iv = stackalloc int[6];
// Honor a shiny request only at the end; generate as never-shiny to avoid shiny PID rejection in main RNG method.
var isShinyRequested = criteria.Shiny.IsShiny();
var iterCriteria = criteria with { Shiny = ShinyMethod };
int ctr = 0;
var rand = new Xoroshiro128Plus(Util.Rand.Rand64());
var param = GetParam(pi);
@ -64,23 +68,22 @@ protected override void SetPINGA(PK8 pk, in EncounterCriteria criteria, Personal
const int max = 100_000;
do
{
if (TryApply(pk, seed = rand.Next(), iv, param, criteria))
if (TryApply(pk, seed = rand.Next(), iv, param, iterCriteria))
break;
} while (++ctr < max);
if (ctr == max) // fail
{
if (!TryApply(pk, seed = rand.Next(), iv, param, criteria.WithoutIVs()))
iterCriteria = iterCriteria.WithoutIVs();
if (!TryApply(pk, seed = rand.Next(), iv, param, iterCriteria))
{
var tmp = EncounterCriteria.Unrestricted with { Shiny = ShinyMethod };
while (!TryApply(pk, seed = rand.Next(), iv, param, tmp)) { }
iterCriteria = EncounterCriteria.Unrestricted with { Shiny = ShinyMethod };
while (!TryApply(pk, seed = rand.Next(), iv, param, iterCriteria)) { }
}
}
FinishCorrelation(pk, seed);
if (criteria.IsSpecifiedNature() && criteria.Nature != pk.Nature && criteria.Nature.IsMint())
pk.StatNature = criteria.Nature;
if (criteria.Shiny.IsShiny())
if (isShinyRequested)
pk.PID = ShinyUtil.GetShinyPID(pk.TID16, pk.SID16, pk.PID, ShinyXor);
}

View File

@ -209,7 +209,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
{
if (!Shiny.IsValid(pk))
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (Gender != FixedGenderUtil.GenderRandom && pk.Gender != Gender)
return false;

View File

@ -154,7 +154,7 @@ private void SetPINGA(PK9 pk, in EncounterCriteria criteria, PersonalInfo9SV pi)
if (Gender != FixedGenderUtil.GenderRandom)
pk.Gender = Gender;
if (Nature != Nature.Random)
if (Nature.IsFixed)
pk.Nature = pk.StatNature = Nature;
}
#endregion
@ -178,7 +178,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (TeraType != GemType.Random && pk is ITeraType t && !Tera9RNG.IsMatchTeraType(TeraType, Species, Form, (byte)t.TeraTypeOriginal))
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;

View File

@ -158,7 +158,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (pk is IAlphaReadOnly a && a.IsAlpha != IsAlpha)
return false;

View File

@ -135,7 +135,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (pk is IAlphaReadOnly a && a.IsAlpha != IsAlpha)
return false;

View File

@ -155,7 +155,7 @@ private bool IsMatchNatureGenderShiny(PKM pk)
return false;
if (pk.Gender != Gender)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
return true;
}
@ -192,7 +192,7 @@ public bool IsMatchExact(PKM pk, EvoCriteria evo)
return false;
if (FlawlessIVCount != 0 && pk.FlawlessIVCount < FlawlessIVCount)
return false;
if (Nature != Nature.Random && pk.Nature != Nature)
if (Nature.IsFixed && pk.Nature != Nature)
return false;
if (pk is IAlphaReadOnly a && a.IsAlpha != IsAlpha)
return false;

View File

@ -36,10 +36,10 @@ public static bool GenerateData(PA9 pk, in GenerateParam9a enc, in EncounterCrit
{
var rand = new Xoroshiro128Plus(seed);
pk.EncryptionConstant = (uint)rand.NextInt(uint.MaxValue);
pk.PID = GetAdaptedPID(ref rand, pk, enc);
if (enc.Shiny is Shiny.Random && criteria.Shiny.IsShiny() != pk.IsShiny)
var pid = GetAdaptedPID(ref rand, pk, enc);
if (enc.Shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(ShinyUtil.GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
Span<int> ivs = [UNSET, UNSET, UNSET, UNSET, UNSET, UNSET];
if (enc.IVs.IsSpecified)
@ -104,7 +104,7 @@ public static bool GenerateData(PA9 pk, in GenerateParam9a enc, in EncounterCrit
return false;
pk.Gender = gender;
var nature = enc.Nature != Nature.Random ? enc.Nature
var nature = enc.Nature.IsFixed ? enc.Nature
: (Nature)rand.NextInt(25);
// Compromise on Nature -- some are fixed, some are random. If the request wants a specific nature, just mint it.
@ -264,7 +264,7 @@ private static bool IsMatchIVsAndFollowing(PKM pk, in GenerateParam9a enc, Xoros
if (pk.Gender != gender)
return false;
var nature = enc.Nature != Nature.Random ? enc.Nature
var nature = enc.Nature.IsFixed ? enc.Nature
: (Nature)rand.NextInt(25);
if (pk.Nature != nature)
return false;

View File

@ -13,5 +13,5 @@ public interface IFixedNature
/// <summary>
/// Indicates if the nature is a single value (not random).
/// </summary>
bool IsFixedNature => Nature != Nature.Random;
bool IsFixedNature => Nature.IsFixed;
}

View File

@ -20,7 +20,8 @@ public static class Legal
internal const int MaxSpeciesID_3 = 386;
internal const int MaxMoveID_3 = 354;
internal const int MaxItemID_3 = 374;
internal const int MaxItemID_3_RS = 346;
internal const int MaxItemID_3_FRLG = 374;
internal const int MaxItemID_3_E = 376;
internal const int MaxItemID_3_COLO = 547;
internal const int MaxItemID_3_XD = 593;

View File

@ -55,7 +55,8 @@ private static bool TryApplyFromSeed(PK8 pk, in EncounterCriteria criteria, Shin
if (shiny == Shiny.AlwaysStar && type != Shiny.AlwaysStar)
return false;
}
if (shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
// IVs

View File

@ -114,7 +114,7 @@ public static bool Verify(PKM pk, ulong seed, Span<int> ivs, in GenerateParam8 p
break;
}
var nature = param.Nature != Nature.Random ? param.Nature
var nature = param.Nature.IsFixed ? param.Nature
: param.Species == (int)Species.Toxtricity
? ToxtricityUtil.GetRandomNature(ref rng, pk.Form)
: (Nature)rng.NextInt(25);
@ -160,6 +160,8 @@ public static bool TryApply(PK8 pk, ulong seed, Span<int> ivs, in GenerateParam8
var trID = (uint)rng.NextInt();
var pid = (uint)rng.NextInt();
// Battle
var xor = GetShinyXor(pid, trID);
bool isShiny = xor < 16;
if (isShiny && param.Shiny == Shiny.Never)
@ -167,9 +169,8 @@ public static bool TryApply(PK8 pk, ulong seed, Span<int> ivs, in GenerateParam8
ForceShinyState(false, ref pid, trID, 0);
isShiny = false;
}
if (param.Shiny is Shiny.Random && isShiny != criteria.Shiny.IsShiny())
return false;
// Captured
if (isShiny)
{
if (!GetIsShiny6(pk.ID32, pid))
@ -181,6 +182,8 @@ public static bool TryApply(PK8 pk, ulong seed, Span<int> ivs, in GenerateParam8
pid ^= 0x1000_0000;
}
if (param.Shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
const int UNSET = -1;
@ -236,7 +239,7 @@ public static bool TryApply(PK8 pk, ulong seed, Span<int> ivs, in GenerateParam8
return false;
pk.Gender = gender;
var nature = param.Nature != Nature.Random ? param.Nature
var nature = param.Nature.IsFixed ? param.Nature
: param.Species == (int)Species.Toxtricity
? ToxtricityUtil.GetRandomNature(ref rng, pk.Form)
: (Nature)rng.NextInt(25);

View File

@ -129,6 +129,8 @@ public static bool TryApplyFromSeed(PA8 pk, in EncounterCriteria criteria, in Ov
if (para.Shiny == Shiny.AlwaysStar && type != Shiny.AlwaysStar)
return false;
}
if (para.Shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
Span<int> ivs = [UNSET, UNSET, UNSET, UNSET, UNSET, UNSET];
@ -172,6 +174,8 @@ public static bool TryApplyFromSeed(PA8 pk, in EncounterCriteria criteria, in Ov
pk.Gender = gender;
var nature = (Nature)rand.NextInt(25);
if (criteria.IsSpecifiedNature() && !criteria.IsSatisfiedNature(nature))
return false;
pk.Nature = pk.StatNature = nature;
var (height, weight) = para.IsAlpha
@ -179,16 +183,10 @@ public static bool TryApplyFromSeed(PA8 pk, in EncounterCriteria criteria, in Ov
: ((byte)(rand.NextInt(0x81) + rand.NextInt(0x80)),
(byte)(rand.NextInt(0x81) + rand.NextInt(0x80)));
if (pk is IScaledSize s)
{
s.HeightScalar = height;
s.WeightScalar = weight;
if (pk is IScaledSizeValue a)
{
a.ResetHeight();
a.ResetWeight();
}
}
pk.HeightScalar = height;
pk.WeightScalar = weight;
pk.ResetHeight();
pk.ResetWeight();
return true;
}

View File

@ -65,6 +65,8 @@ public static bool TryApplyFromSeed(PB8 pk, in EncounterCriteria criteria, Shiny
if (shiny == Shiny.AlwaysStar && type != Shiny.AlwaysStar)
return false;
}
if (shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
// Check IVs: Create flawless IVs at random indexes, then the random IVs for not flawless.

View File

@ -65,6 +65,8 @@ public static bool TryApplyFromSeed(PB8 pk, in EncounterCriteria criteria, Shiny
if (shiny == Shiny.AlwaysStar && type != Shiny.AlwaysStar)
return false;
}
if (shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
// Check IVs: Create flawless IVs at random indexes, then the random IVs for not flawless.

View File

@ -62,10 +62,10 @@ public static bool GenerateData(PK9 pk, in GenerateParam9 enc, in EncounterCrite
{
var rand = new Xoroshiro128Plus(seed);
pk.EncryptionConstant = (uint)rand.NextInt(uint.MaxValue);
pk.PID = GetAdaptedPID(ref rand, pk, enc);
if (enc.Shiny is Shiny.Random && criteria.Shiny.IsShiny() != pk.IsShiny)
var pid = GetAdaptedPID(ref rand, pk, enc);
if (enc.Shiny is Shiny.Random && criteria.IsSpecifiedShiny() && !criteria.IsSatisfiedShiny(GetShinyXor(pid, pk.ID32), 16))
return false;
pk.PID = pid;
const int UNSET = -1;
const int MAX = 31;
@ -121,7 +121,7 @@ public static bool GenerateData(PK9 pk, in GenerateParam9 enc, in EncounterCrite
return false;
pk.Gender = gender;
var nature = enc.Nature != Nature.Random ? enc.Nature : enc.Species == (int)Species.Toxtricity
var nature = enc.Nature.IsFixed ? enc.Nature : enc.Species == (int)Species.Toxtricity
? ToxtricityUtil.GetRandomNature(ref rand, pk.Form)
: (Nature)rand.NextInt(25);
@ -205,7 +205,7 @@ public static bool IsMatch(PKM pk, in GenerateParam9 enc, in ulong seed)
if (pk.Gender != gender)
return false;
var nature = enc.Nature != Nature.Random ? enc.Nature : enc.Species == (int)Species.Toxtricity
var nature = enc.Nature.IsFixed ? enc.Nature : enc.Species == (int)Species.Toxtricity
? ToxtricityUtil.GetRandomNature(ref rand, pk.Form)
: (Nature)rand.NextInt(25);
if (pk.Nature != nature)

View File

@ -4,7 +4,7 @@ namespace PKHeX.Core;
/// Parameters used to generate data for an encounter.
/// </summary>
/// <param name="Species">Species to generate.</param>
/// <param name="GenderRatio">Gender ratio byte.</param>
/// <param name="GenderRatio">Gender ratio byte from Personal Info.</param>
/// <param name="FlawlessIVs">Count of IVs that are perfect.</param>
/// <param name="RollCount">Count of shiny rolls allowed for the PID calculation.</param>
/// <param name="Height">Height value to generate. If zero, full random.</param>

View File

@ -51,7 +51,7 @@ public static bool IsHeldItemAllowed(int item, EntityContext context)
// Combined bitflags for released held items across generations.
private static readonly bool[] ReleasedHeldItems_2 = GetPermitList(MaxItemID_2, HeldItems_GSC);
private static readonly bool[] ReleasedHeldItems_3 = GetPermitList(MaxItemID_3, HeldItems_RS, ItemStorage3RS.Unreleased); // Safari Ball
private static readonly bool[] ReleasedHeldItems_3 = GetPermitList(MaxItemID_3_RS, HeldItems_RS, ItemStorage3RS.Unreleased); // Safari Ball
private static readonly bool[] ReleasedHeldItems_4 = GetPermitList(MaxItemID_4_HGSS, HeldItems_HGSS, ItemStorage4.Unreleased);
private static readonly bool[] ReleasedHeldItems_5 = GetPermitList(MaxItemID_5_B2W2, HeldItems_BW, ItemStorage5.Unreleased);
private static readonly bool[] ReleasedHeldItems_6 = GetPermitList(MaxItemID_6_AO, HeldItems_AO, ItemStorage6XY.Unreleased);

View File

@ -46,7 +46,7 @@ private void VerifyTrash(LegalityAnalysis data, G3PKM pk)
VerifyTrashCXD(data, pk);
}
private void VerifyTrashCXD(LegalityAnalysis data, G3PKM pk)
private static void VerifyTrashCXD(LegalityAnalysis data, G3PKM pk)
{
// Buffers should be entirely clean.
var ot = pk.OriginalTrainerTrash;
@ -122,7 +122,10 @@ 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));
{
if (!TrashByteRules3.IsTrashPatternDefaultTrainer(trash, pk.Version, (LanguageID)pk.Language))
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);
@ -132,7 +135,9 @@ private static void FlagIsNicknameClean(LegalityAnalysis data, PK3 pk)
{
if (!pk.IsNicknamed || pk.IsEgg)
return;
var nick = pk.NicknameTrash;
// Japanese only fills the first 5+1 bytes; everything else is trash.
// International games are 10 chars (full buffer) max; implicit terminator if full.
var nick = pk.GetNicknamePrefillRegion();
if (!TrashByteRules3.IsTerminatedFF(nick))
data.AddLine(GetInvalid(Trainer, TrashBytesMismatchInitial));
}
@ -145,6 +150,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:
@ -173,7 +182,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);
}
@ -181,7 +190,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);
}
@ -192,17 +201,77 @@ 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);
}
// TRASH BYTES: New Game Default OTs
// Default OT names in International (not JPN) Gen3 mainline games memcpy 7 chars and FF from the "default OT name" table, regardless of strlen.
// Copied strings therefore contain trash from the next string entry that is encoded into the rom's string table.
// Below is a list of possible (version, language, trash) default OTs, as initialized by the game. An `*` is used to denote the terminator.
/// <summary>
/// Checks if the specified trash byte pattern matches a default trainer name pattern for the given game <see cref="version"/> and <see cref="language"/>.
/// </summary>
/// <remarks>Default trainer names in certain Generation 3 Pokémon games may include trailing bytes ("trash") due to how names are stored in the game's ROM.
/// This method checks if the provided pattern matches any of these known default patterns for the specified version and language.
/// </remarks>
public static bool IsTrashPatternDefaultTrainer(ReadOnlySpan<byte> trash, GameVersion version, LanguageID language) => version switch
{
GameVersion.R or GameVersion.S => IsTrashPatternDefaultTrainerRS(trash, language),
GameVersion.E => IsTrashPatternDefaultTrainerE(trash, language),
GameVersion.FR => IsTrashPatternDefaultTrainerFR(trash, language),
GameVersion.LG => IsTrashPatternDefaultTrainerLG(trash, language),
_ => false,
};
/// <inheritdoc cref="IsTrashPatternDefaultTrainer"/>
/// <remarks>Default OT names present in <see cref="GameVersion.R"/> and <see cref="GameVersion.S"/> based on the language of the game.</remarks>
public static bool IsTrashPatternDefaultTrainerRS(ReadOnlySpan<byte> trash, LanguageID language) => language switch
{
// TODO
LanguageID.English => trash switch
{
[0xCD, 0xBB, 0xCC, 0xBB, 0xFF, 0xCE, 0xDC] => true, // SARA*Th
_ => false,
},
_ => false,
};
/// <inheritdoc cref="IsTrashPatternDefaultTrainer"/>
/// <remarks>Default OT names present in <see cref="GameVersion.E"/> based on the language of the game.</remarks>
public static bool IsTrashPatternDefaultTrainerE(ReadOnlySpan<byte> trash, LanguageID language) => language switch
{
// TODO
_ => false,
};
/// <inheritdoc cref="IsTrashPatternDefaultTrainer"/>
/// <remarks>Default OT names present in <see cref="GameVersion.FR"/> based on the language of the game.</remarks>
public static bool IsTrashPatternDefaultTrainerFR(ReadOnlySpan<byte> trash, LanguageID language) => language switch
{
// TODO
_ => false,
};
/// <inheritdoc cref="IsTrashPatternDefaultTrainer"/>
/// <remarks>Default OT names present in <see cref="GameVersion.LG"/> based on the language of the game.</remarks>
public static bool IsTrashPatternDefaultTrainerLG(ReadOnlySpan<byte> trash, LanguageID language) => language switch
{
// TODO
_ => false,
};
}

View File

@ -17,7 +17,7 @@ internal static void VerifyStatNature(LegalityAnalysis data, PKM pk)
return;
// Must be a valid mint nature.
if (!statNature.IsMint())
if (!statNature.IsMint)
data.AddLine(Get(Invalid, Misc, StatNatureInvalid));
}

View File

@ -24,7 +24,7 @@ public override void Verify(LegalityAnalysis data)
if (pk.PID == 0)
data.AddLine(Get(Severity.Fishy, PIDZero));
if (!pk.Nature.IsFixed()) // out of range
if (!pk.Nature.IsFixed) // out of range
data.AddLine(GetInvalid(PIDNatureMismatch));
if (data.Info.EncounterMatch is IEncounterEgg egg)
VerifyEggPID(data, pk, egg);

View File

@ -626,7 +626,7 @@ public override bool IsMatchExact(PKM pk, EvoCriteria evo)
if (MetLevel != 0 && MetLevel != pk.MetLevel) return false;
if ((Ball == 0 ? 4 : Ball) != pk.Ball) return false;
if (OTGender < 2 && OTGender != pk.OriginalTrainerGender) return false;
if (Nature != Nature.Random && pk.Nature != Nature) return false;
if (Nature.IsFixed && pk.Nature != Nature) return false;
if (Gender != 3 && Gender != pk.Gender) return false;
if (pk is IScaledSize s)

View File

@ -398,4 +398,15 @@ public override int GetStringTerminatorIndex(ReadOnlySpan<byte> data)
public override int GetStringLength(ReadOnlySpan<byte> data)
=> TrashBytes8.GetStringLength(data);
public override int GetBytesPerChar() => 1;
public override void PrepareNickname() => GetNicknamePrefillRegion().Fill(StringConverter3.TerminatorByte);
public Span<byte> GetNicknamePrefillRegion()
{
// Japanese only fills the first 5+1 bytes; everything else is trash.
// International games are 10 chars (full buffer) max; implicit terminator if full.
if (Japanese)
return NicknameTrash[..6];
return NicknameTrash;
}
}

View File

@ -11,7 +11,7 @@ namespace PKHeX.Core;
/// Object representing a <see cref="PKM"/>'s data and derived properties.
/// </summary>
[DynamicallyAccessedMembers(PublicProperties | NonPublicProperties | PublicParameterlessConstructor)]
public abstract class PKM : ISpeciesForm, ITrainerID32, IGeneration, IShiny, ILangNick, IGameValueLimit, INature, IFatefulEncounter, IStringConverter, ITrashIntrospection
public abstract class PKM : ISpeciesForm, ITrainerID32, IGeneration, IShiny, ILangNick, IGameValueLimit, INature, IFatefulEncounter, IStringConverter, ITrashIntrospection, IContext
{
public abstract int SIZE_PARTY { get; }
public abstract int SIZE_STORED { get; }
@ -44,6 +44,11 @@ public abstract class PKM : ISpeciesForm, ITrainerID32, IGeneration, IShiny, ILa
public abstract Span<byte> OriginalTrainerTrash { get; }
public virtual Span<byte> HandlingTrainerTrash => [];
/// <summary>
/// Conditions the <see cref="NicknameTrash"/> data to safely terminate the Nickname string from the text entry screen.
/// </summary>
public virtual void PrepareNickname() { }
protected abstract byte[] Encrypt();
public abstract EntityContext Context { get; }
public byte Format => Context.Generation;

View File

@ -168,7 +168,7 @@ private bool SearchSimple(PKM pk)
return false;
if (Ability > -1 && pk.Ability != Ability)
return false;
if (Nature.IsFixed() && pk.StatNature != Nature)
if (Nature.IsFixed && pk.StatNature != Nature)
return false;
if (Item > -1 && pk.HeldItem != Item)
return false;

View File

@ -17,7 +17,7 @@ public abstract class G3PKM : PKM, IRibbonSetEvent3, IRibbonSetCommon3, IRibbonS
public sealed override ushort MaxMoveID => Legal.MaxMoveID_3;
public sealed override ushort MaxSpeciesID => Legal.MaxSpeciesID_3;
public sealed override int MaxAbilityID => Legal.MaxAbilityID_3;
public sealed override int MaxItemID => Legal.MaxItemID_3;
public sealed override int MaxItemID => Legal.MaxItemID_3_E;
public sealed override int MaxBallID => Legal.MaxBallID_3;
public sealed override GameVersion MaxGameID => Legal.MaxGameID_3;
public sealed override int MaxIV => 31;

View File

@ -289,7 +289,7 @@ public sealed override byte Ball
BallDPPt = Clamp(value, Core.Ball.Cherish);
// Only set the HG/SS value if it originated in HG/SS and was not an event.
if (WasCreatedInHGSS)
if (WasCreatedInHGSS && this is not BK4)
BallHGSS = Clamp(value, Core.Ball.Sport);
else
BallHGSS = 0;

View File

@ -35,3 +35,20 @@ public interface IStringConverter
/// <returns>Count of bytes written to <see cref="data"/>.</returns>
int SetString(Span<byte> data, ReadOnlySpan<char> text, int length, StringConverterOption option);
}
public delegate string StringGetter(ReadOnlySpan<byte> data);
public delegate int StringLoader(ReadOnlySpan<byte> data, Span<char> text);
public delegate int StringSetter(Span<byte> data, ReadOnlySpan<char> text, int length, StringConverterOption option);
public sealed class CustomStringConverter : IStringConverter, IGeneration, IContext
{
public required StringGetter Get { get; init; }
public required StringLoader Load { get; init; }
public required StringSetter Set { get; init; }
public required byte Generation { get; init; }
public required EntityContext Context { get; init; }
public string GetString(ReadOnlySpan<byte> data) => Get(data);
public int LoadString(ReadOnlySpan<byte> data, Span<char> text) => Load(data, text);
public int SetString(Span<byte> data, ReadOnlySpan<char> text, int length, StringConverterOption option) => Set(data, text, length, option);
}

View File

@ -73,8 +73,8 @@ public static byte GetItemOld2(ushort item)
NaN, 058, 059, 061, 444, NaN, NaN, 216, 445, 446, // 5
NaN, 447, 051, 038, 039, 040, 478, 464, 456, 484, // 6
NaN, 482, 033, 217, 151, NaN, 237, 244, 149, 153, // 7
152, 245, 221, 156, 150, 485, 086, 087, 222, 487, // 8
NaN, 223, 486, 488, 224, 243, 248, 490, 241, 491, // 9
152, 245, 221, 156, 150, 485, 086, 087, 222, 486, // 8
NaN, 223, 487, 488, 224, 243, 248, 490, 241, 491, // 9
NaN, 489, 240, 473, NaN, 259, 228, 246, 242, 157, // 10
088, 089, 229, 247, 504, NaN, NaN, 239, 258, 230, // 11
NaN, 034, 035, 036, 037, 238, 231, 475, 481, NaN, // 12

View File

@ -14,7 +14,7 @@
0190 e 钢铁能检查GS球
0191 e 钢铁能归还GS球 (与上述旗标绑定)
0192 e GS球能放入桐树林祠堂
1809 * No Mystery Gift item is waiting in the Pokémon Center
1809 * 宝可梦中心没有神秘礼物等待领取
0672 * 已解决 阿露福遗迹凤王拼图谜题
0673 * 已解决 阿露福遗迹化石盔拼图谜题
0674 * 已解决 阿露福遗迹菊石兽拼图谜题

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ namespace PKHeX.Core;
/// <summary>
/// Minimal Trainer Information necessary for generating a <see cref="PKM"/>.
/// </summary>
public interface ITrainerInfo : ITrainerID32ReadOnly, IVersion, IGeneration
public interface ITrainerInfo : ITrainerID32ReadOnly, IVersion, IGeneration, IContext
{
string OT { get; }
byte Gender { get; }
@ -13,7 +13,6 @@ public interface ITrainerInfo : ITrainerID32ReadOnly, IVersion, IGeneration
int Language { get; }
new byte Generation { get; }
EntityContext Context { get; }
}
/// <summary>

View File

@ -0,0 +1,43 @@
using System;
using System.ComponentModel;
namespace PKHeX.Core;
[TypeConverter(typeof(ExpandableObjectConverter))]
public interface ISaveBlock3Large
{
Memory<byte> Raw { get; }
Span<byte> Data { get; }
ushort X { get; set; }
ushort Y { get; set; }
byte PartyCount { get; set; }
Span<byte> PartyBuffer { get; }
uint Money { get; set; }
ushort Coin { get; set; }
ushort RegisteredItem { get; set; }
Span<byte> EReaderBerry { get; }
Gen3MysteryData MysteryData { get; set; }
int DaycareOffset { get; }
int DaycareSlotSize { get; }
int BadgeFlagStart { get; }
int EventFlagCount { get; }
int EventWorkCount { get; }
int EggEventFlag { get; }
Memory<byte> RoamerData { get; }
uint GetRecord(int record);
void SetRecord(int record, uint value);
Mail3 GetMail(int mailIndex);
void SetMail(int mailIndex, Mail3 value);
bool GetEventFlag(int flagNumber);
void SetEventFlag(int flagNumber, bool value);
ushort GetWork(int index);
void SetWork(int index, ushort value);
int SeenOffset2 { get; }
int ExternalEventData { get; }
int SeenOffset3 { get; }
Span<byte> GiftRibbons { get; }
}

View File

@ -0,0 +1,13 @@
using System;
namespace PKHeX.Core;
public interface ISaveBlock3LargeExpansion : ISaveBlock3Large
{
WonderNews3 GetWonderNews(bool isJapanese);
void SetWonderNews(bool isJapanese, ReadOnlySpan<byte> data);
WonderCard3 GetWonderCard(bool isJapanese);
void SetWonderCard(bool isJapanese, ReadOnlySpan<byte> data);
WonderCard3Extra GetWonderCardExtra(bool isJapanese);
void SetWonderCardExtra(bool isJapanese, ReadOnlySpan<byte> data);
}

View File

@ -3,13 +3,13 @@
namespace PKHeX.Core;
/// <summary>
/// Properties common to RS &amp; Emerald save files.
/// Large-block properties common to RS & Emerald save files.
/// </summary>
public interface IGen3Hoenn
public interface ISaveBlock3LargeHoenn : ISaveBlock3Large
{
RTC3 ClockInitial { get; set; }
RTC3 ClockElapsed { get; set; }
PokeBlock3Case PokeBlocks { get; set; }
ushort GetBerryBlenderRPMRecord(int index);
void SetBerryBlenderRPMRecord(int index, ushort value);
DecorationInventory3 Decorations { get; }
Swarm3 Swarm { get; set; }
@ -19,6 +19,6 @@ public interface IGen3Hoenn
RecordMixing3Gift RecordMixingGift { get; set; }
SecretBaseManager3 SecretBases { get; }
Paintings3 GetPainting(int index);
Paintings3 GetPainting(int index, bool japanese);
void SetPainting(int index, Paintings3 value);
}

View File

@ -0,0 +1,36 @@
using System;
using System.ComponentModel;
namespace PKHeX.Core;
[TypeConverter(typeof(ExpandableObjectConverter))]
public interface ISaveBlock3Small
{
Memory<byte> Raw { get; }
Span<byte> Data { get; }
Span<byte> OriginalTrainerTrash { get; }
byte Gender { get; set; }
uint ID32 { get; set; }
ushort TID16 { get; set; }
ushort SID16 { get; set; }
int PlayedHours { get; set; }
int PlayedMinutes { get; set; }
int PlayedSeconds { get; set; }
byte PlayedFrames { get; set; }
byte OptionsButtonMode { get; set; }
int TextSpeed { get; set; }
byte OptionWindowFrame { get; set; }
bool OptionSoundStereo { get; set; }
bool OptionBattleStyle { get; set; }
bool OptionBattleScene { get; set; }
bool OptionIsRegionMapZoom { get; set; }
byte PokedexSort { get; set; }
byte PokedexMode { get; set; }
byte PokedexNationalMagicRSE { get; set; }
byte PokedexNationalMagicFRLG { get; set; }
uint DexPIDUnown { get; set; }
uint DexPIDSpinda { get; set; }
Span<byte> EReaderTrainer { get; }
uint SecurityKey { get; }
}

View File

@ -1,6 +1,8 @@
using System.ComponentModel.DataAnnotations;
namespace PKHeX.Core;
public interface IGen3Joyful
public interface ISaveBlock3SmallExpansion : ISaveBlock3Small
{
ushort JoyfulJumpInRow { get; set; }
ushort JoyfulJump5InRow { get; set; }
@ -11,6 +13,9 @@ public interface IGen3Joyful
ushort JoyfulBerriesInRow { get; set; }
ushort JoyfulBerries5InRow { get; set; }
ushort GetBerryPressSpeed([Range(0, 3)] int index);
void SetBerryPressSpeed([Range(0, 3)] int index, ushort value);
uint BerryPowder { get; set; }
uint SecurityKey { get; set; }
new uint SecurityKey { get; set; }
uint LinkFlags { get; set; }
}

View File

@ -0,0 +1,17 @@
namespace PKHeX.Core;
/// <summary>
/// Small-block properties common to RS & Emerald save files.
/// </summary>
public interface ISaveBlock3SmallHoenn : ISaveBlock3Small
{
/// <summary>
/// localTimeOffset
/// </summary>
RTC3 ClockInitial { get; set; }
/// <summary>
/// lastBerryTreeUpdate
/// </summary>
RTC3 ClockElapsed { get; set; }
}

View File

@ -0,0 +1,204 @@
using System;
using System.Collections.Generic;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public sealed record SaveBlock3LargeE(Memory<byte> Raw) : ISaveBlock3LargeExpansion, ISaveBlock3LargeHoenn, IRecordStatStorage<RecID3Emerald, uint>
{
public Span<byte> Data => Raw.Span;
public ushort X { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); }
public ushort Y { get => ReadUInt16LittleEndian(Data[2..]); set => WriteUInt16LittleEndian(Data[2..], value); }
public byte PartyCount { get => Data[0x234]; set => Data[0x234] = value; }
public Span<byte> PartyBuffer => Data.Slice(0x238, 6 * PokeCrypto.SIZE_3PARTY);
public uint Money { get => ReadUInt32LittleEndian(Data[0x0490..]); set => WriteUInt32LittleEndian(Data[0x0490..], value); }
public ushort Coin { get => ReadUInt16LittleEndian(Data[0x0494..]); set => WriteUInt16LittleEndian(Data[0x0494..], value); }
public ushort RegisteredItem { get => ReadUInt16LittleEndian(Data[0x0496..]); set => WriteUInt16LittleEndian(Data[0x0496..], value); }
public Span<byte> Inventory => Data.Slice(0x0498, 0x3B0);
private Span<byte> PokeBlockData => Data.Slice(0x848, PokeBlock3Case.SIZE);
public PokeBlock3Case PokeBlocks { get => new(PokeBlockData); set => value.Write(PokeBlockData); }
public int SeenOffset2 => 0x988;
private const int OFS_BerryBlenderRecord = 0x9BC;
/// <summary>
/// Max RPM for 2, 3 and 4 players. Each value unit represents 0.01 RPM. Value 0 if no record.
/// </summary>
/// <remarks>2 players: index 0, 3 players: index 1, 4 players: index 2</remarks>
public const int BerryBlenderRPMRecordCount = 3;
private Span<byte> GetBlenderRPMSpan(int index)
{
if ((uint)index >= BerryBlenderRPMRecordCount)
throw new ArgumentOutOfRangeException(nameof(index));
return Data[(OFS_BerryBlenderRecord + (index * 2))..];
}
public ushort GetBerryBlenderRPMRecord(int index) => ReadUInt16LittleEndian(GetBlenderRPMSpan(index));
public void SetBerryBlenderRPMRecord(int index, ushort value) => WriteUInt16LittleEndian(GetBlenderRPMSpan(index), value);
private const int EventFlag = 0x1270;
private const int EventWork = 0x139C;
public int EventFlagCount => 8 * 300;
public int EventWorkCount => 0x100;
public int EggEventFlag => 0x86;
public int BadgeFlagStart => 0x867;
public bool GetEventFlag(int flagNumber)
{
if ((uint)flagNumber >= EventFlagCount)
throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to get ({flagNumber}) is greater than max ({EventFlagCount}).");
return FlagUtil.GetFlag(Data, EventFlag + (flagNumber >> 3), flagNumber & 7);
}
public void SetEventFlag(int flagNumber, bool value)
{
if ((uint)flagNumber >= EventFlagCount)
throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to set ({flagNumber}) is greater than max ({EventFlagCount}).");
FlagUtil.SetFlag(Data, EventFlag + (flagNumber >> 3), flagNumber & 7, value);
}
public ushort GetWork(int index) => ReadUInt16LittleEndian(Data[(EventWork + (index * 2))..]);
public void SetWork(int index, ushort value) => WriteUInt16LittleEndian(Data[EventWork..][(index * 2)..], value);
private const int RecordOffset = 0x159C;
private static int GetRecordOffset(RecID3Emerald record)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)record, (uint)RecID3Emerald.NUM_GAME_STATS);
return RecordOffset + (sizeof(uint) * (int)record);
}
public uint GetRecord(int record) => GetRecord((RecID3Emerald)record);
public void SetRecord(int record, uint value) => SetRecord((RecID3Emerald)record, value);
public uint GetRecord(RecID3Emerald record) => ReadUInt32LittleEndian(Data[GetRecordOffset(record)..]);
public void SetRecord(RecID3Emerald record, uint value) => WriteUInt32LittleEndian(Data[GetRecordOffset(record)..], value);
public void AddRecord(RecID3Emerald record, uint value) => SetRecord(record, GetRecord(record) + value);
private Memory<byte> SecretBaseData => Raw.Slice(0x1A9C, SecretBaseManager3.BaseCount * SecretBase3.SIZE);
public SecretBaseManager3 SecretBases => new(SecretBaseData);
public DecorationInventory3 Decorations => new(Data.Slice(0x2734, DecorationInventory3.SIZE));
private Span<byte> SwarmData => Data.Slice(0x2B90, Swarm3.SIZE);
public Swarm3 Swarm { get => new(SwarmData.ToArray()); set => value.Data.CopyTo(SwarmData); }
private void ClearSwarm() => SwarmData.Clear();
public IReadOnlyList<Swarm3> DefaultSwarms => Swarm3Details.Swarms_E;
public int SwarmIndex
{
get
{
var map = Swarm.MapNum;
for (int i = 0; i < DefaultSwarms.Count; i++)
{
if (DefaultSwarms[i].MapNum == map)
return i;
}
return -1;
}
set
{
var arr = DefaultSwarms;
if ((uint)value >= arr.Count)
ClearSwarm();
else
Swarm = arr[value];
}
}
private const int MailOffset = 0x2BE0;
private static int GetMailOffset(int index) => (index * Mail3.SIZE) + MailOffset;
private Span<byte> GetMailSpan(int ofs) => Data.Slice(ofs, Mail3.SIZE);
public Mail3 GetMail(int mailIndex)
{
var ofs = GetMailOffset(mailIndex);
var span = Data.Slice(ofs, Mail3.SIZE);
return new Mail3(span.ToArray(), ofs);
}
public void SetMail(int mailIndex, Mail3 value)
{
var ofs = GetMailOffset(mailIndex);
value.CopyTo(GetMailSpan(ofs));
}
private const int OFS_TrendyWord = 0x2E20;
public bool GetTrendyWordUnlocked(TrendyWord3E word) => FlagUtil.GetFlag(Data, OFS_TrendyWord + ((byte)word >> 3), (byte)word & 7);
public void SetTrendyWordUnlocked(TrendyWord3E word, bool value) => FlagUtil.SetFlag(Data, OFS_TrendyWord + ((byte)word >> 3), (byte)word & 7, value);
private const int Painting = 0x2F90;
private const int PaintingCount = 5;
private Span<byte> GetPaintingSpan(int index)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual<uint>((uint)index, PaintingCount);
return Data.Slice(Painting + (Paintings3.SIZE * index), Paintings3.SIZE * PaintingCount);
}
public Paintings3 GetPainting(int index, bool japanese) => new(GetPaintingSpan(index).ToArray(), japanese);
public void SetPainting(int index, Paintings3 value) => value.Data.CopyTo(GetPaintingSpan(index));
public int DaycareOffset => 0x3030;
public int DaycareSlotSize => PokeCrypto.SIZE_3STORED + 0x3C; // 0x38 mail + 4 exp
public uint DaycareSeed
{
get => ReadUInt32LittleEndian(Data[0x3148..]);
set => WriteUInt32LittleEndian(Data[0x3148..], value);
}
public Span<byte> GiftRibbons => Data.Slice(0x31B3, 11);
public int ExternalEventData => 0x31B3;
public Memory<byte> RoamerData => Raw.Slice(0x31DC, Roamer3.SIZE);
private const int OFFSET_EBERRY = 0x31F8;
private const int SIZE_EBERRY = 0x34;
public Span<byte> EReaderBerry => Data.Slice(OFFSET_EBERRY, SIZE_EBERRY);
public const int WonderNewsOffset = 0x322C;
// RAM Script
private Span<byte> MysterySpan => Data.Slice(0x3728, MysteryEvent3.SIZE);
public Gen3MysteryData MysteryData
{
get => new MysteryEvent3(MysterySpan.ToArray());
set => value.Data.CopyTo(MysterySpan);
}
private static int WonderCardOffset(bool isJapanese) => WonderNewsOffset + (isJapanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE);
private static int WonderCardExtraOffset(bool isJapanese) => WonderCardOffset(isJapanese) + (isJapanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE);
private Span<byte> WonderNewsData(bool isJapanese) => Data.Slice(WonderNewsOffset, isJapanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE);
private Span<byte> WonderCardData(bool isJapanese) => Data.Slice(WonderCardOffset(isJapanese), isJapanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE);
private Span<byte> WonderCardExtraData(bool isJapanese) => Data.Slice(WonderCardExtraOffset(isJapanese), WonderCard3Extra.SIZE);
public WonderNews3 GetWonderNews(bool isJapanese) => new(WonderNewsData(isJapanese).ToArray());
public void SetWonderNews(bool isJapanese, ReadOnlySpan<byte> data) => data.CopyTo(WonderNewsData(isJapanese));
public WonderCard3 GetWonderCard(bool isJapanese) => new(WonderCardData(isJapanese).ToArray());
public void SetWonderCard(bool isJapanese, ReadOnlySpan<byte> data) => data.CopyTo(WonderCardData(isJapanese));
public WonderCard3Extra GetWonderCardExtra(bool isJapanese) => new(WonderCardExtraData(isJapanese).ToArray());
public void SetWonderCardExtra(bool isJapanese, ReadOnlySpan<byte> data) => data.CopyTo(WonderCardExtraData(isJapanese));
private const int OFS_TrainerHillRecord = 0x3718;
/** Each value unit represents 1/60th of a second. Value 0 if no record. */
public uint GetTrainerHillRecord(TrainerHillMode3E mode) => ReadUInt32LittleEndian(Data[(OFS_TrainerHillRecord + ((byte)mode * 4))..]);
public void SetTrainerHillRecord(TrainerHillMode3E mode, uint value) => WriteUInt32LittleEndian(Data[(OFS_TrainerHillRecord + ((byte)mode * 4))..], value);
private Span<byte> RecordMixingData => Data.Slice(0x3B14, RecordMixing3Gift.SIZE);
public RecordMixing3Gift RecordMixingGift
{
get => new(RecordMixingData.ToArray());
set => value.Data.CopyTo(RecordMixingData);
}
public int SeenOffset3 => 0x3B24;
private const int Walda = 0x3D70;
public ushort WaldaBackgroundColor { get => ReadUInt16LittleEndian(Data[(Walda + 0)..]); set => WriteUInt16LittleEndian(Data[(Walda + 0)..], value); }
public ushort WaldaForegroundColor { get => ReadUInt16LittleEndian(Data[(Walda + 2)..]); set => WriteUInt16LittleEndian(Data[(Walda + 2)..], value); }
public byte WaldaIconID { get => Data[Walda + 0x14]; set => Data[Walda + 0x14] = value; }
public byte WaldaPatternID { get => Data[Walda + 0x15]; set => Data[Walda + 0x15] = value; }
public bool WaldaUnlocked { get => Data[Walda + 0x16] != 0; set => Data[Walda + 0x16] = (byte)(value ? 1 : 0); }
}

View File

@ -0,0 +1,121 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public sealed record SaveBlock3LargeFRLG(Memory<byte> Raw) : ISaveBlock3LargeExpansion, IRecordStatStorage<RecID3FRLG, uint>
{
public Span<byte> Data => Raw.Span;
public ushort X { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); }
public ushort Y { get => ReadUInt16LittleEndian(Data[2..]); set => WriteUInt16LittleEndian(Data[2..], value); }
public byte PartyCount { get => Data[0x034]; set => Data[0x034] = value; }
public Span<byte> PartyBuffer => Data.Slice(0x038, 6 * PokeCrypto.SIZE_3PARTY);
public uint Money { get => ReadUInt32LittleEndian(Data[0x0290..]); set => WriteUInt32LittleEndian(Data[0x0290..], value); }
public ushort Coin { get => ReadUInt16LittleEndian(Data[0x0294..]); set => WriteUInt16LittleEndian(Data[0x0294..], value); }
public ushort RegisteredItem { get => ReadUInt16LittleEndian(Data[0x0296..]); set => WriteUInt16LittleEndian(Data[0x0296..], value); }
public Span<byte> Inventory => Data.Slice(0x0298, 0x360);
public int SeenOffset2 => 0x5F8;
private const int EventFlag = 0xEE0;
private const int EventWork = 0x1000;
public int EventFlagCount => 8 * 288;
public int EventWorkCount => 0x100;
public int EggEventFlag => 0x266;
public int BadgeFlagStart => 0x820;
public bool GetEventFlag(int flagNumber)
{
if ((uint)flagNumber >= EventFlagCount)
throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to get ({flagNumber}) is greater than max ({EventFlagCount}).");
return FlagUtil.GetFlag(Data, EventFlag + (flagNumber >> 3), flagNumber & 7);
}
public void SetEventFlag(int flagNumber, bool value)
{
if ((uint)flagNumber >= EventFlagCount)
throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to set ({flagNumber}) is greater than max ({EventFlagCount}).");
FlagUtil.SetFlag(Data, EventFlag + (flagNumber >> 3), flagNumber & 7, value);
}
public ushort GetWork(int index) => ReadUInt16LittleEndian(Data[(EventWork + (index * 2))..]);
public void SetWork(int index, ushort value) => WriteUInt16LittleEndian(Data[EventWork..][(index * 2)..], value);
private const int RecordOffset = 0x1200;
private static int GetRecordOffset(RecID3FRLG record)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)record, (uint)RecID3FRLG.NUM_GAME_STATS);
return RecordOffset + (sizeof(uint) * (int)record);
}
public uint GetRecord(int record) => GetRecord((RecID3FRLG)record);
public void SetRecord(int record, uint value) => SetRecord((RecID3FRLG)record, value);
public uint GetRecord(RecID3FRLG record) => ReadUInt32LittleEndian(Data[GetRecordOffset(record)..]);
public void SetRecord(RecID3FRLG record, uint value) => WriteUInt32LittleEndian(Data[GetRecordOffset(record)..], value);
public void AddRecord(RecID3FRLG record, uint value) => SetRecord(record, GetRecord(record) + value);
private const int MailOffset = 0x2CD0;
private static int GetMailOffset(int index) => (index * Mail3.SIZE) + MailOffset;
private Span<byte> GetMailSpan(int ofs) => Data.Slice(ofs, Mail3.SIZE);
public Mail3 GetMail(int mailIndex)
{
var ofs = GetMailOffset(mailIndex);
var span = Data.Slice(ofs, Mail3.SIZE);
return new Mail3(span.ToArray(), ofs);
}
public void SetMail(int mailIndex, Mail3 value)
{
var ofs = GetMailOffset(mailIndex);
value.CopyTo(GetMailSpan(ofs));
}
public int DaycareOffset => 0x2F80;
public int DaycareSlotSize => PokeCrypto.SIZE_3STORED + 0x3C; // 0x38 mail + 4 exp
public ushort DaycareSeed
{
get => ReadUInt16LittleEndian(Data[0x3098..]);
set => WriteUInt16LittleEndian(Data[0x3098..], value);
}
public Span<byte> GiftRibbons => Data.Slice(0x309C, 11);
public int ExternalEventData => 0x30A7;
public Memory<byte> RoamerData => Raw.Slice(0x30D0, Roamer3.SIZE);
private const int OFFSET_EBERRY = 0x30EC;
private const int SIZE_EBERRY = 0x34;
public Span<byte> EReaderBerry => Data.Slice(OFFSET_EBERRY, SIZE_EBERRY);
public const int WonderNewsOffset = 0x3120;
private static int WonderCardOffset(bool isJapanese) => WonderNewsOffset + (isJapanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE);
private static int WonderCardExtraOffset(bool isJapanese) => WonderCardOffset(isJapanese) + (isJapanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE);
private Span<byte> WonderNewsData(bool isJapanese) => Data.Slice(WonderNewsOffset, isJapanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE);
private Span<byte> WonderCardData(bool isJapanese) => Data.Slice(WonderCardOffset(isJapanese), isJapanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE);
private Span<byte> WonderCardExtraData(bool isJapanese) => Data.Slice(WonderCardExtraOffset(isJapanese), WonderCard3Extra.SIZE);
public WonderNews3 GetWonderNews(bool isJapanese) => new(WonderNewsData(isJapanese).ToArray());
public void SetWonderNews(bool isJapanese, ReadOnlySpan<byte> data) => data.CopyTo(WonderNewsData(isJapanese));
public WonderCard3 GetWonderCard(bool isJapanese) => new(WonderCardData(isJapanese).ToArray());
public void SetWonderCard(bool isJapanese, ReadOnlySpan<byte> data) => data.CopyTo(WonderCardData(isJapanese));
public WonderCard3Extra GetWonderCardExtra(bool isJapanese) => new(WonderCardExtraData(isJapanese).ToArray());
public void SetWonderCardExtra(bool isJapanese, ReadOnlySpan<byte> data) => data.CopyTo(WonderCardExtraData(isJapanese));
public Span<byte> RivalNameTrash => Data.Slice(0x3A4C, 8);
private Span<byte> MysterySpan => Data.Slice(0x361C, MysteryEvent3.SIZE);
public Gen3MysteryData MysteryData
{
get => new MysteryEvent3(MysterySpan.ToArray());
set => value.Data.CopyTo(MysterySpan);
}
public int SeenOffset3 => 0x3A18;
public Memory<byte> SingleDaycareRoute5 => Raw.Slice(0x3C98, PokeCrypto.SIZE_3STORED); // 0x38 mail + 4 exp
}

View File

@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public sealed record SaveBlock3LargeRS(Memory<byte> Raw) : ISaveBlock3LargeHoenn, IRecordStatStorage<RecID3RuSa, uint>
{
public Span<byte> Data => Raw.Span;
public ushort X { get => ReadUInt16LittleEndian(Data); set => WriteUInt16LittleEndian(Data, value); }
public ushort Y { get => ReadUInt16LittleEndian(Data[2..]); set => WriteUInt16LittleEndian(Data[2..], value); }
public byte PartyCount { get => Data[0x234]; set => Data[0x234] = value; }
public Span<byte> PartyBuffer => Data.Slice(0x238, 6 * PokeCrypto.SIZE_3PARTY);
public uint Money { get => ReadUInt32LittleEndian(Data[0x0490..]); set => WriteUInt32LittleEndian(Data[0x0490..], value); }
public ushort Coin { get => ReadUInt16LittleEndian(Data[0x0494..]); set => WriteUInt16LittleEndian(Data[0x0494..], value); }
public ushort RegisteredItem { get => ReadUInt16LittleEndian(Data[0x0496..]); set => WriteUInt16LittleEndian(Data[0x0496..], value); }
public Span<byte> Inventory => Data.Slice(0x498, 0x360);
private Span<byte> PokeBlockData => Data.Slice(0x7F8, PokeBlock3Case.SIZE);
public PokeBlock3Case PokeBlocks { get => new(PokeBlockData); set => value.Write(PokeBlockData); }
public int SeenOffset2 => 0x938;
private const int OFS_BerryBlenderRecord = 0x96C;
/// <summary>
/// Max RPM for 2, 3 and 4 players. Each value unit represents 0.01 RPM. Value 0 if no record.
/// </summary>
/// <remarks>2 players: index 0, 3 players: index 1, 4 players: index 2</remarks>
public const int BerryBlenderRPMRecordCount = 3;
private Span<byte> GetBlenderRPMSpan(int index)
{
if ((uint)index >= BerryBlenderRPMRecordCount)
throw new ArgumentOutOfRangeException(nameof(index));
return Data[(OFS_BerryBlenderRecord + (index * 2))..];
}
public ushort GetBerryBlenderRPMRecord(int index) => ReadUInt16LittleEndian(GetBlenderRPMSpan(index));
public void SetBerryBlenderRPMRecord(int index, ushort value) => WriteUInt16LittleEndian(GetBlenderRPMSpan(index), value);
private const int EventFlag = 0x1220;
private const int EventWork = 0x1340;
public int EventFlagCount => 8 * 288;
public int EventWorkCount => 0x100;
public int EggEventFlag => 0x86;
public int BadgeFlagStart => 0x807;
public bool GetEventFlag(int flagNumber)
{
if ((uint)flagNumber >= EventFlagCount)
throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to get ({flagNumber}) is greater than max ({EventFlagCount}).");
return FlagUtil.GetFlag(Data, EventFlag + (flagNumber >> 3), flagNumber & 7);
}
public void SetEventFlag(int flagNumber, bool value)
{
if ((uint)flagNumber >= EventFlagCount)
throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to set ({flagNumber}) is greater than max ({EventFlagCount}).");
FlagUtil.SetFlag(Data, EventFlag + (flagNumber >> 3), flagNumber & 7, value);
}
public ushort GetWork(int index) => ReadUInt16LittleEndian(Data[(EventWork + (index * 2))..]);
public void SetWork(int index, ushort value) => WriteUInt16LittleEndian(Data[EventWork..][(index * 2)..], value);
private const int RecordOffset = 0x1540;
private static int GetRecordOffset(RecID3RuSa record)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)record, (uint)RecID3RuSa.NUM_GAME_STATS);
return RecordOffset + (sizeof(uint) * (int)record);
}
public uint GetRecord(int record) => GetRecord((RecID3RuSa)record);
public void SetRecord(int record, uint value) => SetRecord((RecID3RuSa)record, value);
public uint GetRecord(RecID3RuSa record) => ReadUInt32LittleEndian(Data[GetRecordOffset(record)..]);
public void SetRecord(RecID3RuSa record, uint value) => WriteUInt32LittleEndian(Data[GetRecordOffset(record)..], value);
public void AddRecord(RecID3RuSa record, uint value) => SetRecord(record, GetRecord(record) + value);
private Memory<byte> SecretBaseData => Raw.Slice(0x1A08, SecretBaseManager3.BaseCount * SecretBase3.SIZE);
public SecretBaseManager3 SecretBases => new(SecretBaseData);
public DecorationInventory3 Decorations => new(Data.Slice(0x26A0, DecorationInventory3.SIZE));
private Span<byte> SwarmData => Data.Slice(0x2AFC, Swarm3.SIZE);
public Swarm3 Swarm { get => new(SwarmData.ToArray()); set => value.Data.CopyTo(SwarmData); }
private void ClearSwarm() => SwarmData.Clear();
public IReadOnlyList<Swarm3> DefaultSwarms => Swarm3Details.Swarms_RS;
public int SwarmIndex
{
get
{
var map = Swarm.MapNum;
for (int i = 0; i < DefaultSwarms.Count; i++)
{
if (DefaultSwarms[i].MapNum == map)
return i;
}
return -1;
}
set
{
var arr = DefaultSwarms;
if ((uint)value >= arr.Count)
ClearSwarm();
else
Swarm = arr[value];
}
}
private const int MailOffset = 0x2B4C;
private static int GetMailOffset(int index) => (index * Mail3.SIZE) + MailOffset;
private Span<byte> GetMailSpan(int ofs) => Data.Slice(ofs, Mail3.SIZE);
public Mail3 GetMail(int mailIndex)
{
var ofs = GetMailOffset(mailIndex);
var span = Data.Slice(ofs, Mail3.SIZE);
return new Mail3(span.ToArray(), ofs);
}
public void SetMail(int mailIndex, Mail3 value)
{
var ofs = GetMailOffset(mailIndex);
value.CopyTo(GetMailSpan(ofs));
}
private const int Painting = 0x2EFC;
private const int PaintingCount = 5;
private Span<byte> GetPaintingSpan(int index)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual<uint>((uint)index, PaintingCount);
return Data.Slice(Painting + (Paintings3.SIZE * index), Paintings3.SIZE * PaintingCount);
}
public Paintings3 GetPainting(int index, bool japanese) => new(GetPaintingSpan(index).ToArray(), japanese);
public void SetPainting(int index, Paintings3 value) => value.Data.CopyTo(GetPaintingSpan(index));
public int DaycareOffset => 0x2F9C;
public int DaycareSlotSize => PokeCrypto.SIZE_3STORED; // mail stored separate from box mons
public ushort DaycareSeed
{
get => ReadUInt16LittleEndian(Data[0x30B4..]);
set => WriteUInt16LittleEndian(Data[0x30B4..], value);
}
public Span<byte> GiftRibbons => Data.Slice(ExternalEventData - 11, 11);
public int ExternalEventData => 0x311B;
public Memory<byte> RoamerData => Raw.Slice(0x3144, Roamer3.SIZE);
private const int OFFSET_EBERRY = 0x3160;
private const int SIZE_EBERRY = 0x530;
public Span<byte> EReaderBerry => Data.Slice(OFFSET_EBERRY, SIZE_EBERRY);
private Span<byte> MysterySpan => Data.Slice(0x3690, MysteryEvent3.SIZE);
public Gen3MysteryData MysteryData
{
get => new MysteryEvent3(MysterySpan.ToArray());
set => value.Data.CopyTo(MysterySpan);
}
private Span<byte> RecordSpan => Data.Slice(0x3A7C, RecordMixing3Gift.SIZE);
public RecordMixing3Gift RecordMixingGift
{
get => new(RecordSpan.ToArray());
set => value.Data.CopyTo(RecordSpan);
}
public int SeenOffset3 => 0x3A8C;
}

View File

@ -0,0 +1,67 @@
using System;
using System.ComponentModel.DataAnnotations;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public sealed record SaveBlock3SmallE(Memory<byte> Raw) : ISaveBlock3SmallExpansion, ISaveBlock3SmallHoenn
{
public Span<byte> Data => Raw.Span;
public Span<byte> OriginalTrainerTrash => Data[..8];
public byte Gender { get => Data[8]; set => Data[8] = value; }
public uint ID32 { get => ReadUInt32LittleEndian(Data[0x0A..]); set => WriteUInt32LittleEndian(Data[0x0A..], value); }
public ushort TID16 { get => ReadUInt16LittleEndian(Data[0xA..]); set => WriteUInt16LittleEndian(Data[0xA..], value); }
public ushort SID16 { get => ReadUInt16LittleEndian(Data[0xC..]); set => WriteUInt16LittleEndian(Data[0xC..], value); }
public int PlayedHours { get => ReadUInt16LittleEndian(Data[0xE..]); set => WriteUInt16LittleEndian(Data[0xE..], (ushort)value); }
public int PlayedMinutes { get => Data[0x10]; set => Data[0x10] = (byte)value; }
public int PlayedSeconds { get => Data[0x11]; set => Data[0x11] = (byte)value; }
public byte PlayedFrames { get => Data[0x12]; set => Data[0x12] = value; }
public byte OptionsButtonMode { get => Data[0x13]; set => Data[0x13] = value; }
private uint OptionsConfig { get => ReadUInt32LittleEndian(Data[0x14..]); set => WriteUInt32LittleEndian(Data[0x14..], value); }
public int TextSpeed { get => (int)(OptionsConfig & 0b11); set => OptionsConfig = (uint)((byte)value & 0b11) | (OptionsConfig & ~0b11u); }
public byte OptionWindowFrame { get => (byte)((OptionsConfig >> 2) & 0b11111); set => OptionsConfig = (uint)((value & 0b11111) << 2) | (OptionsConfig & ~(0b11111u << 2)); }
public bool OptionSoundStereo { get => (OptionsConfig & 0b100000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b100000) : (OptionsConfig & ~0b100000u); }
public bool OptionBattleStyle { get => (OptionsConfig & 0b1000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b1000000) : (OptionsConfig & ~0b1000000u); }
public bool OptionBattleScene { get => (OptionsConfig & 0b10000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b10000000) : (OptionsConfig & ~0b10000000u); }
public bool OptionIsRegionMapZoom { get => (OptionsConfig & 0b100000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b100000000) : (OptionsConfig & ~0b100000000u); }
public byte PokedexSort { get => Data[0x18]; set => Data[0x18] = value; }
public byte PokedexMode { get => Data[0x19]; set => Data[0x19] = value; }
public byte PokedexNationalMagicRSE { get => Data[0x1A]; set => Data[0x1A] = value; }
public byte PokedexNationalMagicFRLG { get => Data[0x1B]; set => Data[0x1B] = value; }
public uint DexPIDUnown { get => ReadUInt32LittleEndian(Data[0x1C..]); set => WriteUInt32LittleEndian(Data[0x1C..], value); }
public uint DexPIDSpinda { get => ReadUInt32LittleEndian(Data[0x20..]); set => WriteUInt32LittleEndian(Data[0x20..], value); }
private Span<byte> ClockInitialSpan => Data.Slice(0x098, RTC3.Size);
private Span<byte> ClockElapsedSpan => Data.Slice(0x0A0, RTC3.Size);
public RTC3 ClockInitial { get => new(ClockInitialSpan.ToArray()); set => value.Data.CopyTo(ClockInitialSpan); }
public RTC3 ClockElapsed { get => new(ClockElapsedSpan.ToArray()); set => value.Data.CopyTo(ClockElapsedSpan); }
public uint LinkFlags { get => ReadUInt32LittleEndian(Data[0x0A8..]); set => WriteUInt32LittleEndian(Data[0x0A8..], value); }
public uint SecurityKey { get => ReadUInt32LittleEndian(Data[0x0AC..]); set => WriteUInt32LittleEndian(Data[0x0AC..], value); }
// 0xB0: Player Apprentice
// 0xDC: Apprentices
// 0x1EC: Berry Crush
public ushort GetBerryPressSpeed([Range(0, 3)] int index) => ReadUInt16LittleEndian(Data[(0x1EC + (index * 2))..]);
public void SetBerryPressSpeed([Range(0, 3)] int index, ushort value) => WriteUInt16LittleEndian(Data[(0x1EC + (index * 2))..], value);
public uint BerryPowder { get => ReadUInt32LittleEndian(Data[0x1F4..]) ^ SecurityKey; set => WriteUInt32LittleEndian(Data[0x1F4..], value ^ SecurityKey); }
public ushort JoyfulJumpInRow { get => ReadUInt16LittleEndian(Data[0x1FC..]); set => WriteUInt16LittleEndian(Data[0x1FC..], Math.Min((ushort)9999, value)); }
public ushort JoyfulJump5InRow { get => ReadUInt16LittleEndian(Data[0x200..]); set => WriteUInt16LittleEndian(Data[0x200..], Math.Min((ushort)9999, value)); }
public ushort JoyfulJumpGamesMaxPlayers { get => ReadUInt16LittleEndian(Data[0x202..]); set => WriteUInt16LittleEndian(Data[0x202..], Math.Min((ushort)9999, value)); }
public uint JoyfulJumpScore { get => ReadUInt16LittleEndian(Data[0x208..]); set => WriteUInt32LittleEndian(Data[0x208..], Math.Min(99990, value)); }
public uint JoyfulBerriesScore { get => ReadUInt16LittleEndian(Data[0x20C..]); set => WriteUInt32LittleEndian(Data[0x20C..], Math.Min(99990, value)); }
public ushort JoyfulBerriesInRow { get => ReadUInt16LittleEndian(Data[0x210..]); set => WriteUInt16LittleEndian(Data[0x210..], Math.Min((ushort)9999, value)); }
public ushort JoyfulBerries5InRow { get => ReadUInt16LittleEndian(Data[0x212..]); set => WriteUInt16LittleEndian(Data[0x212..], Math.Min((ushort)9999, value)); }
// Battle Frontier: 0x64C
public Span<byte> EReaderTrainer => Data.Slice(0xBEC, 0xBC);
public BattleFrontier3 BattleFrontier => new(Data.Slice(0xCDC, BattleFrontier3.SIZE));
public ushort BP { get => ReadUInt16LittleEndian(Data[0xEB8..]); set => WriteUInt16LittleEndian(Data[0xEB8..], Math.Min((ushort)9999, value)); }
public ushort BPEarned { get => ReadUInt16LittleEndian(Data[0xEBA..]); set => WriteUInt16LittleEndian(Data[0xEBA..], value); }
}

View File

@ -0,0 +1,79 @@
using System;
using System.ComponentModel.DataAnnotations;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public sealed record SaveBlock3SmallFRLG(Memory<byte> Raw) : ISaveBlock3SmallExpansion
{
public Span<byte> Data => Raw.Span;
public Span<byte> OriginalTrainerTrash => Data[..8];
public byte Gender { get => Data[8]; set => Data[8] = value; }
public uint ID32 { get => ReadUInt32LittleEndian(Data[0x0A..]); set => WriteUInt32LittleEndian(Data[0x0A..], value); }
public ushort TID16 { get => ReadUInt16LittleEndian(Data[0xA..]); set => WriteUInt16LittleEndian(Data[0xA..], value); }
public ushort SID16 { get => ReadUInt16LittleEndian(Data[0xC..]); set => WriteUInt16LittleEndian(Data[0xC..], value); }
public int PlayedHours { get => ReadUInt16LittleEndian(Data[0xE..]); set => WriteUInt16LittleEndian(Data[0xE..], (ushort)value); }
public int PlayedMinutes { get => Data[0x10]; set => Data[0x10] = (byte)value; }
public int PlayedSeconds { get => Data[0x11]; set => Data[0x11] = (byte)value; }
public byte PlayedFrames { get => Data[0x12]; set => Data[0x12] = value; }
public byte OptionsButtonMode { get => Data[0x13]; set => Data[0x13] = value; }
private uint OptionsConfig { get => ReadUInt32LittleEndian(Data[0x14..]); set => WriteUInt32LittleEndian(Data[0x14..], value); }
public int TextSpeed { get => (int)(OptionsConfig & 0b11); set => OptionsConfig = (uint)((byte)value & 0b11) | (OptionsConfig & ~0b11u); }
public byte OptionWindowFrame { get => (byte)((OptionsConfig >> 2) & 0b11111); set => OptionsConfig = (uint)((value & 0b11111) << 2) | (OptionsConfig & ~(0b11111u << 2)); }
public bool OptionSoundStereo { get => (OptionsConfig & 0b100000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b100000) : (OptionsConfig & ~0b100000u); }
public bool OptionBattleStyle { get => (OptionsConfig & 0b1000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b1000000) : (OptionsConfig & ~0b1000000u); }
public bool OptionBattleScene { get => (OptionsConfig & 0b10000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b10000000) : (OptionsConfig & ~0b10000000u); }
public bool OptionIsRegionMapZoom { get => (OptionsConfig & 0b100000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b100000000) : (OptionsConfig & ~0b100000000u); }
public byte PokedexSort { get => Data[0x18]; set => Data[0x18] = value; }
public byte PokedexMode { get => Data[0x19]; set => Data[0x19] = value; }
public byte PokedexNationalMagicRSE { get => Data[0x1A]; set => Data[0x1A] = value; }
public byte PokedexNationalMagicFRLG { get => Data[0x1B]; set => Data[0x1B] = value; }
public uint DexPIDUnown { get => ReadUInt32LittleEndian(Data[0x1C..]); set => WriteUInt32LittleEndian(Data[0x1C..], value); }
public uint DexPIDSpinda { get => ReadUInt32LittleEndian(Data[0x20..]); set => WriteUInt32LittleEndian(Data[0x20..], value); }
public uint LinkFlags { get => ReadUInt32LittleEndian(Data[0x0A8..]); set => WriteUInt32LittleEndian(Data[0x0A8..], value); }
public bool DummyFlagTrue
{
get => ReadUInt16LittleEndian(Data[0x0AC..]) != 0;
set => WriteUInt16LittleEndian(Data[0x0AC..], value ? (ushort)1 : (ushort)0);
}
public bool DummyFlagFalse
{
get => ReadUInt16LittleEndian(Data[0x0AE..]) == 0;
set => WriteUInt16LittleEndian(Data[0x0AE..], value ? (ushort)0 : (ushort)1);
}
public void FixDummyFlags()
{
DummyFlagTrue = true;
DummyFlagFalse = false;
}
// Battle Tower: 0xB0
public Span<byte> EReaderTrainer => Data.Slice(0x4A0, 0xBC);
// 0xAF0: Berry Crush
public ushort GetBerryPressSpeed([Range(0,3)] int index) => ReadUInt16LittleEndian(Data[(0xAF0 + (index * 2))..]);
public void SetBerryPressSpeed([Range(0, 3)] int index, ushort value) => WriteUInt16LittleEndian(Data[(0xAF0 + (index * 2))..], value);
public uint BerryPowder { get => ReadUInt32LittleEndian(Data[0xAF8..]) ^ SecurityKey; set => WriteUInt32LittleEndian(Data[0xAF8..], value ^ SecurityKey); }
public ushort JoyfulJumpInRow { get => ReadUInt16LittleEndian(Data[0xB00..]); set => WriteUInt16LittleEndian(Data[0xB00..], Math.Min((ushort)9999, value)); }
public ushort JoyfulJump5InRow { get => ReadUInt16LittleEndian(Data[0xB04..]); set => WriteUInt16LittleEndian(Data[0xB04..], Math.Min((ushort)9999, value)); }
public ushort JoyfulJumpGamesMaxPlayers { get => ReadUInt16LittleEndian(Data[0xB06..]); set => WriteUInt16LittleEndian(Data[0xB06..], Math.Min((ushort)9999, value)); }
public uint JoyfulJumpScore { get => ReadUInt16LittleEndian(Data[0xB0C..]); set => WriteUInt32LittleEndian(Data[0xB0C..], Math.Min(99990, value)); }
public uint JoyfulBerriesScore { get => ReadUInt16LittleEndian(Data[0xB10..]); set => WriteUInt32LittleEndian(Data[0xB10..], Math.Min(99990, value)); }
public ushort JoyfulBerriesInRow { get => ReadUInt16LittleEndian(Data[0xB14..]); set => WriteUInt16LittleEndian(Data[0xB14..], Math.Min((ushort)9999, value)); }
public ushort JoyfulBerries5InRow { get => ReadUInt16LittleEndian(Data[0xB16..]); set => WriteUInt16LittleEndian(Data[0xB16..], Math.Min((ushort)9999, value)); }
public uint SecurityKey
{
get => ReadUInt32LittleEndian(Data[0xF20..]);
set => WriteUInt32LittleEndian(Data[0xF20..], value);
}
}

View File

@ -0,0 +1,45 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public sealed record SaveBlock3SmallRS(Memory<byte> Raw) : ISaveBlock3SmallHoenn
{
public Span<byte> Data => Raw.Span;
public Span<byte> OriginalTrainerTrash => Data[..8];
public byte Gender { get => Data[8]; set => Data[8] = value; }
public uint ID32 { get => ReadUInt32LittleEndian(Data[0x0A..]); set => WriteUInt32LittleEndian(Data[0x0A..], value); }
public ushort TID16 { get => ReadUInt16LittleEndian(Data[0xA..]); set => WriteUInt16LittleEndian(Data[0xA..], value); }
public ushort SID16 { get => ReadUInt16LittleEndian(Data[0xC..]); set => WriteUInt16LittleEndian(Data[0xC..], value); }
public int PlayedHours { get => ReadUInt16LittleEndian(Data[0xE..]); set => WriteUInt16LittleEndian(Data[0xE..], (ushort)value); }
public int PlayedMinutes { get => Data[0x10]; set => Data[0x10] = (byte)value; }
public int PlayedSeconds { get => Data[0x11]; set => Data[0x11] = (byte)value; }
public byte PlayedFrames { get => Data[0x12]; set => Data[0x12] = value; }
public byte OptionsButtonMode { get => Data[0x13]; set => Data[0x13] = value; }
private uint OptionsConfig { get => ReadUInt32LittleEndian(Data[0x14..]); set => WriteUInt32LittleEndian(Data[0x14..], value); }
public int TextSpeed { get => (int)(OptionsConfig & 0b11); set => OptionsConfig = (uint)((byte)value & 0b11) | (OptionsConfig & ~0b11u); }
public byte OptionWindowFrame { get => (byte)((OptionsConfig >> 2) & 0b11111); set => OptionsConfig = (uint)((value & 0b11111) << 2) | (OptionsConfig & ~(0b11111u << 2)); }
public bool OptionSoundStereo { get => (OptionsConfig & 0b100000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b100000) : (OptionsConfig & ~0b100000u); }
public bool OptionBattleStyle { get => (OptionsConfig & 0b1000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b1000000) : (OptionsConfig & ~0b1000000u); }
public bool OptionBattleScene { get => (OptionsConfig & 0b10000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b10000000) : (OptionsConfig & ~0b10000000u); }
public bool OptionIsRegionMapZoom { get => (OptionsConfig & 0b100000000) != 0; set => OptionsConfig = value ? (OptionsConfig | 0b100000000) : (OptionsConfig & ~0b100000000u); }
public byte PokedexSort { get => Data[0x18]; set => Data[0x18] = value; }
public byte PokedexMode { get => Data[0x19]; set => Data[0x19] = value; }
public byte PokedexNationalMagicRSE { get => Data[0x1A]; set => Data[0x1A] = value; }
public byte PokedexNationalMagicFRLG { get => Data[0x1B]; set => Data[0x1B] = value; }
public uint DexPIDUnown { get => ReadUInt32LittleEndian(Data[0x1C..]); set => WriteUInt32LittleEndian(Data[0x1C..], value); }
public uint DexPIDSpinda { get => ReadUInt32LittleEndian(Data[0x20..]); set => WriteUInt32LittleEndian(Data[0x20..], value); }
private Span<byte> ClockInitialSpan => Data.Slice(0x098, RTC3.Size);
private Span<byte> ClockElapsedSpan => Data.Slice(0x0A0, RTC3.Size);
public RTC3 ClockInitial { get => new(ClockInitialSpan.ToArray()); set => value.Data.CopyTo(ClockInitialSpan); }
public RTC3 ClockElapsed { get => new(ClockElapsedSpan.ToArray()); set => value.Data.CopyTo(ClockElapsedSpan); }
public uint SecurityKey => 0;
// 0xA8: Battle Tower
public Span<byte> EReaderTrainer => Data.Slice(0x498, 0xBC);
}

View File

@ -293,13 +293,13 @@ public override ushort TID16
public override ushort SID16 { get => 0; set { } }
public string Rival
public string RivalName
{
get => GetString(Data.Slice(Offsets.Rival, MaxStringLengthTrainer));
set => SetString(Data.Slice(Offsets.Rival, MaxStringLengthTrainer), value, MaxStringLengthTrainer, StringConverterOption.Clear50);
}
public Span<byte> RivalTrash { get => Data.Slice(Offsets.Rival, StringLength); set { if (value.Length == StringLength) value.CopyTo(Data[Offsets.Rival..]); } }
public Span<byte> RivalNameTrash { get => Data.Slice(Offsets.Rival, StringLength); set { if (value.Length == StringLength) value.CopyTo(Data[Offsets.Rival..]); } }
public byte RivalStarter { get => Data[Offsets.Starter - 2]; set => Data[Offsets.Starter - 2] = value; }
public byte Starter { get => Data[Offsets.Starter]; set => Data[Offsets.Starter] = value; }

View File

@ -325,13 +325,13 @@ public Span<byte> OriginalTrainerTrash
set { if (value.Length == StringLength) value.CopyTo(Data[(Offsets.Trainer1 + 2)..]); }
}
public string Rival
public string RivalName
{
get => GetString(Data.Slice(Offsets.Rival, (Korean ? 2 : 1) * MaxStringLengthTrainer));
set => SetString(Data.Slice(Offsets.Rival, (Korean ? 2 : 1) * MaxStringLengthTrainer), value, 8, StringConverterOption.Clear50);
}
public Span<byte> RivalTrash
public Span<byte> RivalNameTrash
{
get => Data.Slice(Offsets.Rival, StringLength);
set { if (value.Length == StringLength) value.CopyTo(Data[Offsets.Rival..]); }

View File

@ -18,8 +18,14 @@ public abstract class SAV3 : SaveFile, ILangDeviantSave, IEventFlag37, IBoxDetai
public bool Japanese { get; }
public bool Korean => false;
public bool IsVirtualConsole => State.Exportable && Metadata.FileName is { } s && s.Contains(".sav")
&& (s.StartsWith("FireRed_", StringComparison.Ordinal) || s.StartsWith("LeafGreen_")); // default to Mainline-Era for non-exportable
public bool IsVirtualConsole => State.Exportable && Metadata.FileName is { } s && IsVirtualConsoleFileName(s); // default to Mainline-Era for non-exportable
public static bool IsVirtualConsoleFileName(string s)
{
if (!s.Contains(".sav"))
return false;
return s.StartsWith("FireRed_") || s.StartsWith("LeafGreen_");
}
// Similar to future games, the Generation 3 Mainline save files are comprised of separate objects:
// Object 1 - Small, containing misc configuration data & the Pokédex.
@ -46,6 +52,8 @@ public abstract class SAV3 : SaveFile, ILangDeviantSave, IEventFlag37, IBoxDetai
public Span<byte> Small => SmallBuffer.Span;
public Span<byte> Large => LargeBuffer.Span;
public Span<byte> Storage => StorageBuffer.Span;
public abstract ISaveBlock3Small SmallBlock { get; }
public abstract ISaveBlock3Large LargeBlock { get; }
private readonly int ActiveSlot;
public sealed override int Language { get; set; }
@ -177,14 +185,11 @@ public void WriteBothSaveSlots(Span<byte> data)
public sealed override ushort MaxMoveID => Legal.MaxMoveID_3;
public sealed override ushort MaxSpeciesID => Legal.MaxSpeciesID_3;
public sealed override int MaxAbilityID => Legal.MaxAbilityID_3;
public override int MaxItemID => Legal.MaxItemID_3;
public sealed override int MaxBallID => Legal.MaxBallID_3;
public sealed override GameVersion MaxGameID => Legal.MaxGameID_3;
public abstract int EventFlagCount { get; }
public abstract int EventWorkCount { get; }
protected abstract int EventFlag { get; }
protected abstract int EventWork { get; }
public int EventFlagCount => LargeBlock.EventFlagCount;
public int EventWorkCount => LargeBlock.EventWorkCount;
/// <summary>
/// Force loads a new <see cref="SAV3"/> object to the requested <see cref="version"/>.
@ -210,13 +215,15 @@ public void WriteBothSaveSlots(Span<byte> data)
public sealed override int MaxMoney => 999999;
public sealed override bool HasParty => true;
public sealed override int PartyCount { get => LargeBlock.PartyCount; protected set => LargeBlock.PartyCount = (byte)value; }
public sealed override int GetPartyOffset(int slot) => SIZE_PARTY * slot;
public sealed override bool IsPKMPresent(ReadOnlySpan<byte> data) => EntityDetection.IsPresentGBA(data);
protected sealed override PK3 GetPKM(byte[] data) => new(data);
protected sealed override byte[] DecryptPKM(byte[] data) => PokeCrypto.DecryptArray3(data);
protected sealed override Span<byte> BoxBuffer => Storage;
protected sealed override Span<byte> PartyBuffer => Large;
protected sealed override Span<byte> PartyBuffer => LargeBlock.PartyBuffer;
private const int COUNT_BOX = 14;
private const int COUNT_SLOTSPERBOX = 30;
@ -328,7 +335,7 @@ public sealed override string ChecksumInfo
public static bool IsMail(int itemID) => (uint)(itemID - 121) <= (132 - 121);
protected override void SetPartyValues(PKM pk, bool isParty)
protected sealed override void SetPartyValues(PKM pk, bool isParty)
{
if (pk is not PK3 p3)
return;
@ -341,170 +348,82 @@ protected override void SetPartyValues(PKM pk, bool isParty)
base.SetPartyValues(pk, isParty);
}
public abstract uint SecurityKey { get; set; }
public Span<byte> OriginalTrainerTrash => Small[..8];
public sealed override string OT
{
get
{
int len = Japanese ? 5 : MaxStringLengthTrainer;
return GetString(OriginalTrainerTrash[..len]);
return GetString(SmallBlock.OriginalTrainerTrash[..len]);
}
set
{
int len = Japanese ? 5 : MaxStringLengthTrainer;
SetString(OriginalTrainerTrash[..len], value, len, StringConverterOption.ClearFF); // match the game-init FF terminating pattern
SetString(SmallBlock.OriginalTrainerTrash[..len], value, len, StringConverterOption.None); // Preserve original pattern
}
}
public sealed override byte Gender
{
get => Small[8];
set => Small[8] = value;
get => SmallBlock.Gender;
set => SmallBlock.Gender = value;
}
public sealed override uint ID32
{
get => ReadUInt32LittleEndian(Small[0x0A..]);
set => WriteUInt32LittleEndian(Small[0x0A..], value);
get => SmallBlock.ID32;
set => SmallBlock.ID32 = value;
}
public sealed override ushort TID16
{
get => ReadUInt16LittleEndian(Small[0xA..]);
set => WriteUInt16LittleEndian(Small[0xA..], value);
get => SmallBlock.TID16;
set => SmallBlock.TID16 = value;
}
public sealed override ushort SID16
{
get => ReadUInt16LittleEndian(Small[0xC..]);
set => WriteUInt16LittleEndian(Small[0xC..], value);
get => SmallBlock.SID16;
set => SmallBlock.SID16 = value;
}
public sealed override int PlayedHours
{
get => ReadUInt16LittleEndian(Small[0xE..]);
set => WriteUInt16LittleEndian(Small[0xE..], (ushort)value);
get => SmallBlock.PlayedHours;
set => SmallBlock.PlayedHours = value;
}
public sealed override int PlayedMinutes
{
get => Small[0x10];
set => Small[0x10] = (byte)value;
get => SmallBlock.PlayedMinutes;
set => SmallBlock.PlayedMinutes = value;
}
public sealed override int PlayedSeconds
{
get => Small[0x11];
set => Small[0x11] = (byte)value;
}
public byte PlayedFrames
{
get => Small[0x12];
set => Small[0x12] = value;
}
public byte OptionsButtonMode
{
get => Small[0x13];
set => Small[0x13] = value;
}
private uint OptionsConfig
{
get => ReadUInt32LittleEndian(Small[0x14..]);
set => WriteUInt32LittleEndian(Small[0x14..], value);
}
// 2 bits: Text Speed
// 5 bits: Window Frame
// 1 bit: sound
// 1 bit: battle style (shift vs set)
// 1 bit: battle scene off toggle (animations enabled/disabled)
// 1 bit: regionMapZoom (on/off)
// 4 bits unused
// 16 bits unused
public int TextSpeed
{
get => (int)(OptionsConfig & 0b11);
set => OptionsConfig = (uint)((byte)value & 0b11) | (OptionsConfig & ~0b11u);
}
public byte OptionWindowFrame
{
get => (byte)((OptionsConfig >> 2) & 0b11111);
set => OptionsConfig = (uint)((value & 0b11111) << 2) | (OptionsConfig & ~(0b11111u << 2));
}
public bool OptionSoundStereo
{
get => (OptionsConfig & 0b100000) != 0;
set => OptionsConfig = value ? (OptionsConfig | 0b100000) : (OptionsConfig & ~0b100000u);
}
public bool OptionBattleStyle
{
get => (OptionsConfig & 0b1000000) != 0;
set => OptionsConfig = value ? (OptionsConfig | 0b1000000) : (OptionsConfig & ~0b1000000u);
}
public bool OptionBattleScene
{
get => (OptionsConfig & 0b10000000) != 0;
set => OptionsConfig = value ? (OptionsConfig | 0b10000000) : (OptionsConfig & ~0b10000000u);
}
public bool OptionIsRegionMapZoom
{
get => (OptionsConfig & 0b100000000) != 0;
set => OptionsConfig = value ? (OptionsConfig | 0b100000000) : (OptionsConfig & ~0b100000000u);
}
public ushort X
{
get => ReadUInt16LittleEndian(Large);
set => WriteUInt16LittleEndian(Large, value);
}
public ushort Y
{
get => ReadUInt16LittleEndian(Large[2..]);
set => WriteUInt16LittleEndian(Large[2..], value);
get => SmallBlock.PlayedSeconds;
set => SmallBlock.PlayedSeconds = value;
}
#region Event Flag/Event Work
public bool GetEventFlag(int flagNumber)
{
if ((uint)flagNumber >= EventFlagCount)
throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to get ({flagNumber}) is greater than max ({EventFlagCount}).");
return GetFlag(EventFlag + (flagNumber >> 3), flagNumber & 7);
}
public bool GetEventFlag(int flagNumber) => LargeBlock.GetEventFlag(flagNumber);
public void SetEventFlag(int flagNumber, bool value) => LargeBlock.SetEventFlag(flagNumber, value);
public void SetEventFlag(int flagNumber, bool value)
{
if ((uint)flagNumber >= EventFlagCount)
throw new ArgumentOutOfRangeException(nameof(flagNumber), $"Event Flag to set ({flagNumber}) is greater than max ({EventFlagCount}).");
SetFlag(EventFlag + (flagNumber >> 3), flagNumber & 7, value);
}
public ushort GetWork(int index) => ReadUInt16LittleEndian(Large[(EventWork + (index * 2))..]);
public void SetWork(int index, ushort value) => WriteUInt16LittleEndian(Large[EventWork..][(index * 2)..], value);
public ushort GetWork(int index) => LargeBlock.GetWork(index);
public void SetWork(int index, ushort value) => LargeBlock.SetWork(index, value);
#endregion
public sealed override bool GetFlag(int offset, int bitIndex) => GetFlag(Large, offset, bitIndex);
public sealed override void SetFlag(int offset, int bitIndex, bool value) => SetFlag(Large, offset, bitIndex, value);
public uint GetRecord(int record) => LargeBlock.GetRecord(record) ^ SmallBlock.SecurityKey;
public void SetRecord(int record, uint value) => LargeBlock.SetRecord(record, value ^ SmallBlock.SecurityKey);
protected abstract int BadgeFlagStart { get; }
public abstract uint Coin { get; set; }
public int Badges
{
get
{
int startFlag = BadgeFlagStart;
int startFlag = LargeBlock.BadgeFlagStart;
int val = 0;
for (int i = 0; i < 8; i++)
{
@ -516,30 +435,27 @@ public int Badges
}
set
{
int startFlag = BadgeFlagStart;
int startFlag = LargeBlock.BadgeFlagStart;
for (int i = 0; i < 8; i++)
SetEventFlag(startFlag + i, (value & (1 << i)) != 0);
}
}
protected abstract int PokeDex { get; }
public override bool HasPokeDex => true;
public sealed override bool HasPokeDex => true;
public int DaycareSlotCount => 2;
protected abstract int DaycareSlotSize { get; }
protected abstract int DaycareOffset { get; }
protected abstract int GetDaycareEXPOffset(int slot);
public Memory<byte> GetDaycareSlot(int slot) => LargeBuffer.Slice(GetDaycareSlotOffset(slot), DaycareSlotSize);
public Memory<byte> GetDaycareSlot(int slot) => LargeBuffer.Slice(GetDaycareSlotOffset(slot), LargeBlock.DaycareSlotSize);
public uint GetDaycareEXP(int index) => ReadUInt32LittleEndian(Large[GetDaycareEXPOffset(index)..]);
public void SetDaycareEXP(int index, uint value) => WriteUInt32LittleEndian(Large[GetDaycareEXPOffset(index)..], value);
public bool IsDaycareOccupied(int slot) => IsPKMPresent(Large[GetDaycareSlotOffset(slot)..]);
public void SetDaycareOccupied(int slot, bool occupied) { /* todo */ }
public int GetDaycareSlotOffset(int slot) => DaycareOffset + (slot * DaycareSlotSize);
protected abstract int EggEventFlag { get; }
public bool IsEggAvailable { get => GetEventFlag(EggEventFlag); set => SetEventFlag(EggEventFlag, value); }
public int GetDaycareSlotOffset(int slot) => LargeBlock.DaycareOffset + (slot * LargeBlock.DaycareSlotSize);
public bool IsEggAvailable { get => GetEventFlag(LargeBlock.EggEventFlag); set => SetEventFlag(LargeBlock.EggEventFlag, value); }
#region Storage
public sealed override int GetBoxOffset(int box) => Box + 4 + (SIZE_STORED * box * COUNT_SLOTSPERBOX);
public sealed override bool HasBox => true;
public sealed override int CurrentBox
{
@ -547,6 +463,8 @@ public sealed override int CurrentBox
set => Storage[0] = (byte)value;
}
public sealed override int GetBoxOffset(int box) => 4 + (SIZE_STORED * box * COUNT_SLOTSPERBOX);
public int GetBoxWallpaper(int box)
{
if (box >= COUNT_BOX)
@ -598,25 +516,24 @@ protected sealed override void SetDex(PKM pk)
switch (species)
{
case (int)Species.Unown when !GetSeen(species): // Unown
DexPIDUnown = pk.PID;
SmallBlock.DexPIDUnown = pk.PID;
break;
case (int)Species.Spinda when !GetSeen(species): // Spinda
DexPIDSpinda = pk.PID;
SmallBlock.DexPIDSpinda = pk.PID;
break;
}
SetCaught(species, true);
SetSeen(species, true);
}
public uint DexPIDUnown { get => ReadUInt32LittleEndian(Small[(PokeDex + 0x4)..]); set => WriteUInt32LittleEndian(Small[(PokeDex + 0x4)..], value); }
public uint DexPIDSpinda { get => ReadUInt32LittleEndian(Small[(PokeDex + 0x8)..]); set => WriteUInt32LittleEndian(Small[(PokeDex + 0x8)..], value); }
public int DexUnownForm => EntityPID.GetUnownForm3(DexPIDUnown);
private const int PokeDex = 0x18; // small
public sealed override bool GetCaught(ushort species)
{
int bit = species - 1;
int ofs = bit >> 3;
int caughtOffset = PokeDex + 0x10;
const int caughtOffset = PokeDex + 0x10;
return FlagUtil.GetFlag(Small, caughtOffset + ofs, bit & 7);
}
@ -624,7 +541,7 @@ public sealed override void SetCaught(ushort species, bool caught)
{
int bit = species - 1;
int ofs = bit >> 3;
int caughtOffset = PokeDex + 0x10;
const int caughtOffset = PokeDex + 0x10;
FlagUtil.SetFlag(Small, caughtOffset + ofs, bit & 7, caught);
}
@ -632,46 +549,37 @@ public sealed override bool GetSeen(ushort species)
{
int bit = species - 1;
int ofs = bit >> 3;
int seenOffset = PokeDex + 0x44;
const int seenOffset = PokeDex + 0x44;
return FlagUtil.GetFlag(Small, seenOffset + ofs, bit & 7);
}
protected abstract int SeenOffset2 { get; }
protected abstract int SeenOffset3 { get; }
public sealed override void SetSeen(ushort species, bool seen)
{
int bit = species - 1;
int ofs = bit >> 3;
int seenOffset = PokeDex + 0x44;
const int seenOffset = PokeDex + 0x44;
FlagUtil.SetFlag(Small, seenOffset + ofs, bit & 7, seen);
FlagUtil.SetFlag(Large, SeenOffset2 + ofs, bit & 7, seen);
FlagUtil.SetFlag(Large, SeenOffset3 + ofs, bit & 7, seen);
FlagUtil.SetFlag(Large, LargeBlock.SeenOffset2 + ofs, bit & 7, seen);
FlagUtil.SetFlag(Large, LargeBlock.SeenOffset3 + ofs, bit & 7, seen);
}
public byte PokedexSort
/// <summary>
/// In Gen 3, the seen flags are stored in three different places. Mirror them to each other to ensure consistency.
/// </summary>
/// <remarks>
/// Only really use this if you are allowing users to manually edit the seen flags in the first (normal) section; then trigger this on saving all.
/// </remarks>
public void MirrorSeenFlags()
{
get => Small[PokeDex + 0x01];
set => Small[PokeDex + 0x01] = value;
}
public byte PokedexMode
{
get => Small[PokeDex + 0x01];
set => Small[PokeDex + 0x01] = value;
}
public byte PokedexNationalMagicRSE
{
get => Small[PokeDex + 0x02];
set => Small[PokeDex + 0x02] = value;
}
public byte PokedexNationalMagicFRLG
{
get => Small[PokeDex + 0x03];
set => Small[PokeDex + 0x03] = value;
for (ushort species = 1; species <= Legal.MaxSpeciesID_3; species++)
{
int bit = species - 1;
int ofs = bit >> 3;
bool seen = FlagUtil.GetFlag(Small, PokeDex + 0x44 + ofs, bit & 7);
FlagUtil.SetFlag(Large, LargeBlock.SeenOffset2 + ofs, bit & 7, seen);
FlagUtil.SetFlag(Large, LargeBlock.SeenOffset3 + ofs, bit & 7, seen);
}
}
protected const byte PokedexNationalUnlockRSE = 0xDA;
@ -684,32 +592,18 @@ public byte PokedexNationalMagicFRLG
public sealed override string GetString(ReadOnlySpan<byte> data)
=> StringConverter3.GetString(data, Japanese);
public override int LoadString(ReadOnlySpan<byte> data, Span<char> destBuffer)
public sealed override int LoadString(ReadOnlySpan<byte> data, Span<char> destBuffer)
=> StringConverter3.LoadString(data, destBuffer, Japanese);
public sealed override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
=> StringConverter3.SetString(destBuffer, value, maxLength, Japanese, option);
protected abstract int MailOffset { get; }
public int GetMailOffset(int index) => (index * Mail3.SIZE) + MailOffset;
public string EBerryName => GetString(LargeBlock.EReaderBerry[..7]);
public bool IsEBerryEngima => LargeBlock.EReaderBerry[0] is 0 or 0xFF;
public MailDetail GetMail(int mailIndex)
{
var ofs = GetMailOffset(mailIndex);
var data = Large.Slice(ofs, Mail3.SIZE).ToArray();
return new Mail3(data, ofs);
}
#region eBerry
public abstract Span<byte> EReaderBerry();
public string EBerryName => GetString(EReaderBerry()[..7]);
public bool IsEBerryEngima => EReaderBerry()[0] is 0 or 0xFF;
#endregion
#region eTrainer
public abstract Span<byte> EReaderTrainer();
#endregion
public abstract Gen3MysteryData MysteryData { get; set; }
/// <summary>
/// Indicates if the extdata sections of the save file are available for get/set.
/// </summary>
public bool IsFullSaveFile => Data.Length >= SaveUtil.SIZE_G3RAW;
/// <summary>
/// Hall of Fame data is split across two sectors.
@ -741,14 +635,15 @@ public void SetHallOfFameData(ReadOnlySpan<byte> value)
/// </summary>
public Memory<byte> GetEReaderData() => Buffer.Slice(0x1E000, SIZE_SECTOR_USED);
/// <summary> Only used in Emerald. </summary>
/// <summary> Only used in Emerald for storing the Battle Video. </summary>
public Memory<byte> GetFinalExternalData() => Buffer.Slice(0x1F000, SIZE_SECTOR_USED);
public bool IsCorruptPokedexFF() => MemoryMarshal.Read<ulong>(Small[0xAC..]) == ulong.MaxValue;
public override void CopyChangesFrom(SaveFile sav)
public sealed override void CopyChangesFrom(SaveFile sav)
{
SetData(sav.Data, 0);
if (Data.Length != 0)
SetData(sav.Data, 0);
var s3 = (SAV3)sav;
SetData(Small, s3.Small);
SetData(Large, s3.Large);
@ -757,7 +652,7 @@ public override void CopyChangesFrom(SaveFile sav)
#region External Connections
public Span<byte> GiftRibbons => Large.Slice(ExternalEventData - 11, 11);
public Span<byte> GiftRibbons => LargeBlock.GiftRibbons;
public void GiftRibbonsImport(ReadOnlySpan<byte> trade)
{
@ -773,7 +668,7 @@ public void GiftRibbonsImport(ReadOnlySpan<byte> trade)
}
public void GiftRibbonsClear() => GiftRibbons.Clear();
protected abstract int ExternalEventData { get; }
private int ExternalEventData => LargeBlock.ExternalEventData;
protected int ExternalEventFlags => ExternalEventData + 0x14;
public uint ColosseumRaw1

View File

@ -6,13 +6,21 @@ namespace PKHeX.Core;
/// <summary>
/// Generation 3 <see cref="SaveFile"/> object for Pokémon Colosseum saves.
/// </summary>
public sealed class SAV3Colosseum : SaveFile, IGCSaveFile, IBoxDetailName, IDaycareStorage, IDaycareExperience, IGCRegion
public sealed class SAV3Colosseum : SaveFile, IGCSaveFile, IBoxDetailName, IDaycareStorage, IDaycareExperience, IGCRegion, ISaveFileRevision
{
protected internal override string ShortSummary => $"{OT} ({Version}) - {PlayTimeString}";
public override string Extension => this.GCExtension();
public override PersonalTable3 Personal => PersonalTable.RS;
public override ReadOnlySpan<ushort> HeldItems => Legal.HeldItems_RS;
public SAV3GCMemoryCard? MemoryCard { get; init; }
public int SaveRevision => 0;
public string SaveRevisionString => OriginalRegion switch
{
GCRegion.NTSC_J => "-J",
GCRegion.NTSC_U => "-U",
GCRegion.PAL => "-PAL",
_ => "-?",
};
private readonly Memory<byte> Container;
protected override Span<byte> BoxBuffer => Data;
@ -256,7 +264,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

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
@ -8,280 +7,70 @@ namespace PKHeX.Core;
/// Generation 3 <see cref="SaveFile"/> object for <see cref="GameVersion.E"/>.
/// </summary>
/// <inheritdoc cref="SAV3" />
public sealed class SAV3E : SAV3, IGen3Hoenn, IGen3Joyful, IGen3Wonder, IDaycareRandomState<uint>
public sealed class SAV3E : SAV3, IDaycareRandomState<uint>
{
// Configuration
protected override SAV3E CloneInternal() => new(GetFinalData()) { Language = Language };
public override SaveBlock3SmallE SmallBlock { get; }
public override SaveBlock3LargeE LargeBlock { get; }
public override GameVersion Version { get => GameVersion.E; set { } }
public override PersonalTable3 Personal => PersonalTable.E;
public override int EventFlagCount => 8 * 300;
public override int EventWorkCount => 0x100;
protected override int DaycareSlotSize => SIZE_STORED + 0x3C; // 0x38 mail + 4 exp
protected override int EggEventFlag => 0x86;
protected override int BadgeFlagStart => 0x867;
public SAV3E(Memory<byte> data) : base(data) => Initialize();
public SAV3E(bool japanese = false) : base(japanese) => Initialize();
public SAV3E(Memory<byte> data) : base(data)
{
SmallBlock = new SaveBlock3SmallE(SmallBuffer[..0xF2C]);
LargeBlock = new SaveBlock3LargeE(LargeBuffer[..0x3D88]);
}
public SAV3E(bool japanese = false) : base(japanese)
{
SmallBlock = new SaveBlock3SmallE(SmallBuffer[..0xF2C]);
LargeBlock = new SaveBlock3LargeE(LargeBuffer[..0x3D88]);
}
public override PlayerBag3E Inventory => new(this);
protected override int EventFlag => 0x1270;
protected override int EventWork => 0x139C;
public override int MaxItemID => Legal.MaxItemID_3_E;
protected override int PokeDex => 0x18; // small
protected override int DaycareOffset => 0x3030; // large
// storage
private void Initialize() => Box = 0;
#region Small
public override bool NationalDex
{
get => PokedexNationalMagicRSE == PokedexNationalUnlockRSE;
get => SmallBlock.PokedexNationalMagicRSE == PokedexNationalUnlockRSE;
set
{
PokedexMode = value ? (byte)1 : (byte)0; // mode
PokedexNationalMagicRSE = value ? PokedexNationalUnlockRSE : (byte)0; // magic
SmallBlock.PokedexMode = value ? (byte)1 : (byte)0; // mode
SmallBlock.PokedexNationalMagicRSE = value ? PokedexNationalUnlockRSE : (byte)0; // magic
SetEventFlag(0x896, value);
SetWork(0x46, PokedexNationalUnlockWorkRSE);
}
}
public override uint SecurityKey
{
get => ReadUInt32LittleEndian(Small[0xAC..]);
set => WriteUInt32LittleEndian(Small[0xAC..], value);
}
public RTC3 ClockInitial
{
get => new(Small.Slice(0x98, RTC3.Size).ToArray());
set => SetData(Small[0x98..], value.Data);
}
public RTC3 ClockElapsed
{
get => new(Small.Slice(0xA0, RTC3.Size).ToArray());
set => SetData(Small[0xA0..], value.Data);
}
public uint BerryPowder
{
get => ReadUInt32LittleEndian(Small[0x1F4..]) ^ SecurityKey;
set => WriteUInt32LittleEndian(Small[0x1F4..], value ^ SecurityKey);
}
public ushort JoyfulJumpInRow { get => ReadUInt16LittleEndian(Small[0x1FC..]); set => WriteUInt16LittleEndian(Small[0x1FC..], Math.Min((ushort)9999, value)); }
// u16 field2;
public ushort JoyfulJump5InRow { get => ReadUInt16LittleEndian(Small[0x200..]); set => WriteUInt16LittleEndian(Small[0x200..], Math.Min((ushort)9999, value)); }
public ushort JoyfulJumpGamesMaxPlayers { get => ReadUInt16LittleEndian(Small[0x202..]); set => WriteUInt16LittleEndian(Small[0x202..], Math.Min((ushort)9999, value)); }
// u32 field8;
public uint JoyfulJumpScore { get => ReadUInt16LittleEndian(Small[0x208..]); set => WriteUInt32LittleEndian(Small[0x208..], Math.Min(99990, value)); }
public uint JoyfulBerriesScore { get => ReadUInt16LittleEndian(Small[0x20C..]); set => WriteUInt32LittleEndian(Small[0x20C..], Math.Min(99990, value)); }
public ushort JoyfulBerriesInRow { get => ReadUInt16LittleEndian(Small[0x210..]); set => WriteUInt16LittleEndian(Small[0x210..], Math.Min((ushort)9999, value)); }
public ushort JoyfulBerries5InRow { get => ReadUInt16LittleEndian(Small[0x212..]); set => WriteUInt16LittleEndian(Small[0x212..], Math.Min((ushort)9999, value)); }
public BattleFrontier3 BattleFrontier => new(Small.Slice(0xCDC, BattleFrontier3.SIZE));
public uint BP
{
get => ReadUInt16LittleEndian(Small[0xEB8..]);
set
{
if (value > 9999)
value = 9999;
WriteUInt16LittleEndian(Small[0xEB8..], (ushort)value);
}
}
public uint BPEarned
{
get => ReadUInt16LittleEndian(Small[0xEBA..]);
set
{
if (value > 65535)
value = 65535;
WriteUInt16LittleEndian(Small[0xEBA..], (ushort)value);
}
}
#endregion
#region Large
public override int PartyCount { get => Large[0x234]; protected set => Large[0x234] = (byte)value; }
public override int GetPartyOffset(int slot) => 0x238 + (SIZE_PARTY * slot);
public override uint Money
{
get => ReadUInt32LittleEndian(Large[0x0490..]) ^ SecurityKey;
set => WriteUInt32LittleEndian(Large[0x0490..], value ^ SecurityKey);
get => LargeBlock.Money ^ SmallBlock.SecurityKey;
set => LargeBlock.Money = value ^ SmallBlock.SecurityKey;
}
public override uint Coin
{
get => (ushort)(ReadUInt16LittleEndian(Large[0x0494..]) ^ SecurityKey);
set => WriteUInt16LittleEndian(Large[0x0494..], (ushort)(value ^ SecurityKey));
get => (ushort)(LargeBlock.Coin ^ SmallBlock.SecurityKey);
set => LargeBlock.Coin = (ushort)(value ^ SmallBlock.SecurityKey);
}
private const int OFS_BerryBlenderRecord = 0x9BC;
private const int OFS_TrendyWord = 0x2E20;
private const int OFS_TrainerHillRecord = 0x3718;
private Span<byte> PokeBlockData => Large.Slice(0x848, PokeBlock3Case.SIZE);
public PokeBlock3Case PokeBlocks
{
get => new(PokeBlockData);
set => value.Write(PokeBlockData);
}
protected override int SeenOffset2 => 0x988;
public DecorationInventory3 Decorations => new(Large.Slice(0x2734, DecorationInventory3.SIZE));
private Span<byte> SwarmSpan => Large.Slice(0x2B90, Swarm3.SIZE);
public Swarm3 Swarm
{
get => new(SwarmSpan.ToArray());
set => SetData(SwarmSpan, value.Data);
}
private void ClearSwarm() => SwarmSpan.Clear();
public IReadOnlyList<Swarm3> DefaultSwarms => Swarm3Details.Swarms_E;
public int SwarmIndex
{
get => Array.FindIndex(Swarm3Details.Swarms_E, z => z.MapNum == Swarm.MapNum);
set
{
var arr = DefaultSwarms;
if ((uint)value >= arr.Count)
ClearSwarm();
else
Swarm = arr[value];
}
}
protected override int MailOffset => 0x2BE0;
protected override int GetDaycareEXPOffset(int slot) => GetDaycareSlotOffset(slot + 1) - 4; // @ end of each pk slot
uint IDaycareRandomState<uint>.Seed // after the 2 slots, before the step counter
{
get => ReadUInt32LittleEndian(Large[GetDaycareEXPOffset(2)..]);
set => WriteUInt32LittleEndian(Large[GetDaycareEXPOffset(2)..], value);
get => LargeBlock.DaycareSeed;
set => LargeBlock.DaycareSeed = value;
}
protected override int ExternalEventData => 0x31B3;
/// <summary>
/// Max RPM for 2, 3 and 4 players. Each value unit represents 0.01 RPM. Value 0 if no record.
/// </summary>
/// <remarks>2 players: index 0, 3 players: index 1, 4 players: index 2</remarks>
public const int BerryBlenderRPMRecordCount = 3;
private Span<byte> GetBlenderRPMSpan(int index)
{
if ((uint)index >= BerryBlenderRPMRecordCount)
throw new ArgumentOutOfRangeException(nameof(index));
return Large[(OFS_BerryBlenderRecord + (index * 2))..];
}
public ushort GetBerryBlenderRPMRecord(int index) => ReadUInt16LittleEndian(GetBlenderRPMSpan(index));
public void SetBerryBlenderRPMRecord(int index, ushort value)
{
WriteUInt16LittleEndian(GetBlenderRPMSpan(index), value);
State.Edited = true;
}
public bool GetTrendyWordUnlocked(TrendyWord3E word)
{
return GetFlag(OFS_TrendyWord + ((byte)word >> 3), (byte)word & 7);
}
public void SetTrendyWordUnlocked(TrendyWord3E word, bool value)
{
SetFlag(OFS_TrendyWord + ((byte)word >> 3), (byte)word & 7, value);
State.Edited = true;
}
/** Each value unit represents 1/60th of a second. Value 0 if no record. */
public uint GetTrainerHillRecord(TrainerHillMode3E mode)
{
return ReadUInt32LittleEndian(Large[(OFS_TrainerHillRecord + ((byte)mode * 4))..]);
}
public void SetTrainerHillRecord(TrainerHillMode3E mode, uint value)
{
WriteUInt32LittleEndian(Large[(OFS_TrainerHillRecord + ((byte)mode * 4))..], value);
State.Edited = true;
}
#region eBerry
private const int OFFSET_EBERRY = 0x31F8;
private const int SIZE_EBERRY = 0x34;
public override Span<byte> EReaderBerry() => Large.Slice(OFFSET_EBERRY, SIZE_EBERRY);
#endregion
#region eTrainer
public override Span<byte> EReaderTrainer() => Small.Slice(0xBEC, 0xBC);
#endregion
public int WonderOffset => WonderNewsOffset;
private const int WonderNewsOffset = 0x322C;
private int WonderCardOffset => WonderNewsOffset + (Japanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE);
private int WonderCardExtraOffset => WonderCardOffset + (Japanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE);
private Span<byte> WonderNewsData => Large.Slice(WonderNewsOffset, Japanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE);
private Span<byte> WonderCardData => Large.Slice(WonderCardOffset, Japanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE);
private Span<byte> WonderCardExtraData => Large.Slice(WonderCardExtraOffset, WonderCard3Extra.SIZE);
public WonderNews3 WonderNews { get => new(WonderNewsData.ToArray()); set => SetData(WonderNewsData, value.Data); }
public WonderCard3 WonderCard { get => new(WonderCardData.ToArray()); set => SetData(WonderCardData, value.Data); }
public WonderCard3Extra WonderCardExtra { get => new(WonderCardExtraData.ToArray()); set => SetData(WonderCardExtraData, value.Data); }
// 0x338: 4 easy chat words
// 0x340: news MENewsJisanStruct
// 0x344: uint[5], uint[5] tracking?
private Span<byte> MysterySpan => Large.Slice(0x3728, MysteryEvent3.SIZE);
public override Gen3MysteryData MysteryData
{
get => new MysteryEvent3(MysterySpan.ToArray());
set => SetData(MysterySpan, value.Data);
}
private Span<byte> RecordMixingData => Large.Slice(0x3B14, RecordMixing3Gift.SIZE);
public RecordMixing3Gift RecordMixingGift { get => new(RecordMixingData.ToArray()); set => SetData(RecordMixingData, value.Data); }
protected override int SeenOffset3 => 0x3B24;
private const int Walda = 0x3D70;
public ushort WaldaBackgroundColor { get => ReadUInt16LittleEndian(Large[(Walda + 0)..]); set => WriteUInt16LittleEndian(Large[(Walda + 0)..], value); }
public ushort WaldaForegroundColor { get => ReadUInt16LittleEndian(Large[(Walda + 2)..]); set => WriteUInt16LittleEndian(Large[(Walda + 2)..], value); }
public byte WaldaIconID { get => Large[Walda + 0x14]; set => Large[Walda + 0x14] = value; }
public byte WaldaPatternID { get => Large[Walda + 0x15]; set => Large[Walda + 0x15] = value; }
public bool WaldaUnlocked { get => Large[Walda + 0x16] != 0; set => Large[Walda + 0x16] = (byte)(value ? 1 : 0); }
private Memory<byte> SecretBaseData => LargeBuffer.Slice(0x1A9C, SecretBaseManager3.BaseCount * SecretBase3.SIZE);
public SecretBaseManager3 SecretBases => new(SecretBaseData);
private const int Painting = 0x2F90;
private const int CountPaintings = 5;
private Span<byte> GetPaintingSpan(int index)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, CountPaintings);
return Large.Slice(Painting + (Paintings3.SIZE * index), Paintings3.SIZE * CountPaintings);
}
public Paintings3 GetPainting(int index) => new(GetPaintingSpan(index).ToArray(), Japanese);
public void SetPainting(int index, Paintings3 value) => value.Data.CopyTo(GetPaintingSpan(index));
#endregion
private const uint EXTRADATA_SENTINEL = 0x0000B39D;
public bool HasBattleVideo => Data.Length > SaveUtil.SIZE_G3RAWHALF && ReadUInt32LittleEndian(GetFinalExternalData().Span) == EXTRADATA_SENTINEL;
public bool HasBattleVideo => IsFullSaveFile && ReadUInt32LittleEndian(GetFinalExternalData().Span) == EXTRADATA_SENTINEL;
public void SetExtraDataSentinelBattleVideo() => WriteUInt32LittleEndian(GetFinalExternalData().Span, EXTRADATA_SENTINEL);
public Memory<byte> BattleVideoData => GetFinalExternalData().Slice(4, BattleVideo3.SIZE);

View File

@ -1,5 +1,4 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
@ -7,41 +6,39 @@ namespace PKHeX.Core;
/// Generation 5 <see cref="SaveFile"/> object for <see cref="GameVersion.FRLG"/>.
/// </summary>
/// <inheritdoc cref="SAV3" />
public sealed class SAV3FRLG : SAV3, IGen3Joyful, IGen3Wonder, IDaycareRandomState<ushort>
public sealed class SAV3FRLG : SAV3, IDaycareRandomState<ushort>
{
// Configuration
protected override SAV3FRLG CloneInternal() => new(GetFinalData()) { Language = Language };
public override GameVersion Version { get; set; } = GameVersion.FR; // allow mutation
public override SaveBlock3SmallFRLG SmallBlock { get; }
public override SaveBlock3LargeFRLG LargeBlock { get; }
public override GameVersion Version
{
get;
set => field = value is GameVersion.FR or GameVersion.LG ? value : GameVersion.FRLG;
} = GameVersion.FR; // allow mutation
private PersonalTable3 _personal = PersonalTable.FR;
public override PersonalTable3 Personal => _personal;
public override int MaxItemID => Legal.MaxItemID_3_FRLG;
public override int EventFlagCount => 8 * 288;
public override int EventWorkCount => 0x100;
protected override int DaycareSlotSize => SIZE_STORED + 0x3C; // 0x38 mail + 4 exp
protected override int EggEventFlag => 0x266;
protected override int BadgeFlagStart => 0x820;
public SAV3FRLG(bool japanese = false) : base(japanese) => Initialize();
public override PlayerBag3FRLG Inventory => new(this);
public SAV3FRLG(bool japanese = false) : base(japanese)
{
SmallBlock = new SaveBlock3SmallFRLG(SmallBuffer[..0xF24]);
LargeBlock = new SaveBlock3LargeFRLG(LargeBuffer[..0x3D68]);
}
public SAV3FRLG(Memory<byte> data) : base(data)
{
Initialize();
SmallBlock = new SaveBlock3SmallFRLG(SmallBuffer[..0xF24]);
LargeBlock = new SaveBlock3LargeFRLG(LargeBuffer[..0x3D68]);
// Fix save files that have an overflow corruption with the Pokédex.
// Future loads of this save file will cause it to be recognized as FR/LG correctly.
if (IsCorruptPokedexFF())
WriteUInt32LittleEndian(Small[0xAC..], 1);
SmallBlock.FixDummyFlags();
}
protected override int EventFlag => 0xEE0;
protected override int EventWork => 0x1000;
protected override int PokeDex => 0x18; // small
protected override int DaycareOffset => 0x2F80; // large
// storage
private void Initialize() => Box = 0;
public override PlayerBag3FRLG Inventory => new(this);
public bool ResetPersonal(GameVersion g)
{
@ -55,105 +52,40 @@ public bool ResetPersonal(GameVersion g)
#region Small
public override bool NationalDex
{
get => PokedexNationalMagicFRLG == PokedexNationalUnlockFRLG;
get => SmallBlock.PokedexNationalMagicFRLG == PokedexNationalUnlockFRLG;
set
{
PokedexNationalMagicFRLG = value ? PokedexNationalUnlockFRLG : (byte)0; // magic
SmallBlock.PokedexNationalMagicFRLG = value ? PokedexNationalUnlockFRLG : (byte)0;
SetEventFlag(0x840, value);
SetWork(0x4E, PokedexNationalUnlockWorkFRLG);
}
}
public uint BerryPowder
{
get => ReadUInt32LittleEndian(Small[0xAF8..]) ^ SecurityKey;
set => WriteUInt32LittleEndian(Small[0xAF8..], value ^ SecurityKey);
}
public ushort JoyfulJumpInRow { get => ReadUInt16LittleEndian(Small[0xB00..]); set => WriteUInt16LittleEndian(Small[0xB00..], Math.Min((ushort)9999, value)); }
// u16 field2;
public ushort JoyfulJump5InRow { get => ReadUInt16LittleEndian(Small[0xB04..]); set => WriteUInt16LittleEndian(Small[0xB04..], Math.Min((ushort)9999, value)); }
public ushort JoyfulJumpGamesMaxPlayers { get => ReadUInt16LittleEndian(Small[0xB06..]); set => WriteUInt16LittleEndian(Small[0xB06..], Math.Min((ushort)9999, value)); }
// u32 field8;
public uint JoyfulJumpScore { get => ReadUInt16LittleEndian(Small[0xB0C..]); set => WriteUInt32LittleEndian(Small[0xB0C..], Math.Min(99990, value)); }
public uint JoyfulBerriesScore { get => ReadUInt16LittleEndian(Small[0xB10..]); set => WriteUInt32LittleEndian(Small[0xB10..], Math.Min(99990, value)); }
public ushort JoyfulBerriesInRow { get => ReadUInt16LittleEndian(Small[0xB14..]); set => WriteUInt16LittleEndian(Small[0xB14..], Math.Min((ushort)9999, value)); }
public ushort JoyfulBerries5InRow { get => ReadUInt16LittleEndian(Small[0xB16..]); set => WriteUInt16LittleEndian(Small[0xB16..], Math.Min((ushort)9999, value)); }
public override uint SecurityKey
{
get => ReadUInt32LittleEndian(Small[0xF20..]);
set => WriteUInt32LittleEndian(Small[0xF20..], value);
}
#endregion
#region Large
public override int PartyCount { get => Large[0x034]; protected set => Large[0x034] = (byte)value; }
public override int GetPartyOffset(int slot) => 0x038 + (SIZE_PARTY * slot);
public override uint Money
{
get => ReadUInt32LittleEndian(Large[0x0290..]) ^ SecurityKey;
set => WriteUInt32LittleEndian(Large[0x0290..], value ^ SecurityKey);
get => LargeBlock.Money ^ SmallBlock.SecurityKey;
set => LargeBlock.Money = value ^ SmallBlock.SecurityKey;
}
public override uint Coin
{
get => (ushort)(ReadUInt16LittleEndian(Large[0x0294..]) ^ SecurityKey);
set => WriteUInt16LittleEndian(Large[0x0294..], (ushort)(value ^ SecurityKey));
get => (ushort)(LargeBlock.Coin ^ SmallBlock.SecurityKey);
set => LargeBlock.Coin = (ushort)(value ^ SmallBlock.SecurityKey);
}
protected override int SeenOffset2 => 0x5F8;
protected override int MailOffset => 0x2CD0;
protected override int GetDaycareEXPOffset(int slot) => GetDaycareSlotOffset(slot + 1) - 4; // @ end of each pk slot
ushort IDaycareRandomState<ushort>.Seed
{
get => ReadUInt16LittleEndian(Large[GetDaycareEXPOffset(2)..]);
set => WriteUInt16LittleEndian(Large[GetDaycareEXPOffset(2)..], value);
get => LargeBlock.DaycareSeed;
set => LargeBlock.DaycareSeed = value;
}
protected override int ExternalEventData => 0x30A7;
#region eBerry
private const int OFFSET_EBERRY = 0x30EC;
private const int SIZE_EBERRY = 0x34;
public override Span<byte> EReaderBerry() => Large.Slice(OFFSET_EBERRY, SIZE_EBERRY);
#endregion
#region eTrainer
public override Span<byte> EReaderTrainer() => Small.Slice(0x4A0, 0xBC);
#endregion
public int WonderOffset => WonderNewsOffset;
private const int WonderNewsOffset = 0x3120;
private int WonderCardOffset => WonderNewsOffset + (Japanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE);
private int WonderCardExtraOffset => WonderCardOffset + (Japanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE);
private Span<byte> WonderNewsData => Large.Slice(WonderNewsOffset, Japanese ? WonderNews3.SIZE_JAP : WonderNews3.SIZE);
private Span<byte> WonderCardData => Large.Slice(WonderCardOffset, Japanese ? WonderCard3.SIZE_JAP : WonderCard3.SIZE);
private Span<byte> WonderCardExtraData => Large.Slice(WonderCardExtraOffset, WonderCard3Extra.SIZE);
public WonderNews3 WonderNews { get => new(WonderNewsData.ToArray()); set => SetData(WonderNewsData, value.Data); }
public WonderCard3 WonderCard { get => new(WonderCardData.ToArray()); set => SetData(WonderCardData, value.Data); }
public WonderCard3Extra WonderCardExtra { get => new(WonderCardExtraData.ToArray()); set => SetData(WonderCardExtraData, value.Data); }
// 0x338: 4 easy chat words
// 0x340: news MENewsJisanStruct
// 0x344: uint[5], uint[5] tracking?
private Span<byte> MysterySpan => Large.Slice(0x361C, MysteryEvent3.SIZE);
public override Gen3MysteryData MysteryData { get => new MysteryEvent3(MysterySpan.ToArray()); set => SetData(MysterySpan, value.Data); }
protected override int SeenOffset3 => 0x3A18;
public string RivalName
{
get => GetString(Large.Slice(0x3A4C, 8));
set => SetString(Large.Slice(0x3A4C, 8), value, 7, StringConverterOption.ClearZero);
get => GetString(LargeBlock.RivalNameTrash);
set => SetString(LargeBlock.RivalNameTrash, value, 7, StringConverterOption.ClearZero);
}
#endregion
}

View File

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
@ -8,160 +6,68 @@ namespace PKHeX.Core;
/// Generation 3 <see cref="SaveFile"/> object for <see cref="GameVersion.RS"/>.
/// </summary>
/// <inheritdoc cref="SAV3" />
public sealed class SAV3RS : SAV3, IGen3Hoenn, IDaycareRandomState<ushort>
public sealed class SAV3RS : SAV3, IDaycareRandomState<ushort>
{
// Configuration
protected override SAV3RS CloneInternal() => new(GetFinalData()) { Language = Language };
public override SaveBlock3SmallRS SmallBlock { get; }
public override SaveBlock3LargeRS LargeBlock { get; }
public override GameVersion Version
{
get;
set => field = value is GameVersion.RS or GameVersion.R or GameVersion.S ? value : GameVersion.RS;
set => field = value is GameVersion.R or GameVersion.S ? value : GameVersion.RS;
} = GameVersion.RS;
public override PersonalTable3 Personal => PersonalTable.RS;
public override int MaxItemID => Legal.MaxItemID_3_RS;
public override int EventFlagCount => 8 * 288;
public override int EventWorkCount => 0x100;
protected override int DaycareSlotSize => SIZE_STORED;
protected override int EggEventFlag => 0x86;
protected override int BadgeFlagStart => 0x807;
public SAV3RS(Memory<byte> data) : base(data)
{
SmallBlock = new SaveBlock3SmallRS(SmallBuffer[..0x890]);
LargeBlock = new SaveBlock3LargeRS(LargeBuffer[..0x3AC0]);
}
public SAV3RS(Memory<byte> data) : base(data) => Initialize();
public SAV3RS(bool japanese = false) : base(japanese) => Initialize();
public SAV3RS(bool japanese = false) : base(japanese)
{
SmallBlock = new SaveBlock3SmallRS(SmallBuffer[..0x890]);
LargeBlock = new SaveBlock3LargeRS(LargeBuffer[..0x3AC0]);
}
public override PlayerBag3RS Inventory => new(this);
protected override int EventFlag => 0x1220;
protected override int EventWork => 0x1340;
protected override int PokeDex => 0x18; // small
protected override int DaycareOffset => 0x2F9C; // large
// storage
private void Initialize() => Box = 0;
#region Small
public override bool NationalDex
{
get => PokedexNationalMagicRSE == PokedexNationalUnlockRSE;
get => SmallBlock.PokedexNationalMagicRSE == PokedexNationalUnlockRSE;
set
{
PokedexMode = value ? (byte)1 : (byte)0; // mode
PokedexNationalMagicRSE = value ? PokedexNationalUnlockRSE : (byte)0; // magic
SmallBlock.PokedexMode = value ? (byte)1 : (byte)0;
SmallBlock.PokedexNationalMagicRSE = value ? PokedexNationalUnlockRSE : (byte)0;
SetEventFlag(0x836, value);
SetWork(0x46, PokedexNationalUnlockWorkRSE);
}
}
public override uint SecurityKey { get => 0; set { } }
public RTC3 ClockInitial
{
get => new(Small.Slice(0x98, RTC3.Size).ToArray());
set => SetData(Small.Slice(0x98, RTC3.Size), value.Data);
}
public RTC3 ClockElapsed
{
get => new(Small.Slice(0xA0, RTC3.Size).ToArray());
set => SetData(Small.Slice(0xA0, RTC3.Size), value.Data);
}
#endregion
#region Large
public override int PartyCount { get => Large[0x234]; protected set => Large[0x234] = (byte)value; }
public override int GetPartyOffset(int slot) => 0x238 + (SIZE_PARTY * slot);
public override uint Money
{
get => ReadUInt32LittleEndian(Large[0x0490..]);
set => WriteUInt32LittleEndian(Large[0x0490..], value);
get => LargeBlock.Money;
set => LargeBlock.Money = value;
}
public override uint Coin
{
get => ReadUInt16LittleEndian(Large[0x0494..]);
set => WriteUInt16LittleEndian(Large[0x0494..], (ushort)(value));
get => LargeBlock.Coin;
set => LargeBlock.Coin = (ushort)value;
}
private Span<byte> PokeBlockData => Large.Slice(0x7F8, PokeBlock3Case.SIZE);
public PokeBlock3Case PokeBlocks
{
get => new(PokeBlockData);
set => value.Write(PokeBlockData);
}
protected override int SeenOffset2 => 0x938;
public DecorationInventory3 Decorations => new(Large.Slice(0x26A0, DecorationInventory3.SIZE));
private Span<byte> SwarmData => Large.Slice(0x2AFC, Swarm3.SIZE);
public Swarm3 Swarm
{
get => new(SwarmData.ToArray());
set => SetData(SwarmData, value.Data);
}
private void ClearSwarm() => Large.Slice(0x2AFC, Swarm3.SIZE).Clear();
public IReadOnlyList<Swarm3> DefaultSwarms => Swarm3Details.Swarms_RS;
public int SwarmIndex
{
get => Array.FindIndex(Swarm3Details.Swarms_RS, z => z.MapNum == Swarm.MapNum);
set
{
var arr = DefaultSwarms;
if ((uint)value >= arr.Count)
ClearSwarm();
else
Swarm = arr[value];
}
}
protected override int MailOffset => 0x2B4C;
protected override int GetDaycareEXPOffset(int slot) => GetDaycareSlotOffset(2) + (2 * 0x38) + (4 * slot); // consecutive vals, after both consecutive slots & 2 mail
ushort IDaycareRandomState<ushort>.Seed
{
get => ReadUInt16LittleEndian(Large[GetDaycareEXPOffset(2)..]);
set => WriteUInt16LittleEndian(Large[GetDaycareEXPOffset(2)..], value);
get => LargeBlock.DaycareSeed;
set => LargeBlock.DaycareSeed = value;
}
protected override int ExternalEventData => 0x311B;
#region eBerry
private const int OFFSET_EBERRY = 0x3160;
private const int SIZE_EBERRY = 0x530;
public override Span<byte> EReaderBerry() => Large.Slice(OFFSET_EBERRY, SIZE_EBERRY);
#endregion
#region eTrainer
public override Span<byte> EReaderTrainer() => Small.Slice(0x498, 0xBC);
#endregion
private Span<byte> MysterySpan => Large.Slice(0x3690, MysteryEvent3.SIZE);
public override Gen3MysteryData MysteryData { get => new MysteryEvent3(MysterySpan.ToArray()); set => SetData(MysterySpan, value.Data); }
private Span<byte> RecordSpan => Large.Slice(0x3A7C, RecordMixing3Gift.SIZE);
public RecordMixing3Gift RecordMixingGift { get => new(RecordSpan.ToArray()); set => SetData(RecordSpan, value.Data); }
protected override int SeenOffset3 => 0x3A8C;
private Memory<byte> SecretBaseData => LargeBuffer.Slice(0x1A08, SecretBaseManager3.BaseCount * SecretBase3.SIZE);
public SecretBaseManager3 SecretBases => new(SecretBaseData);
private const int Painting = 0x2EFC;
private const int CountPaintings = 5;
private Span<byte> GetPaintingSpan(int index)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, CountPaintings);
return Large.Slice(Painting + (Paintings3.SIZE * index), Paintings3.SIZE * CountPaintings);
}
public Paintings3 GetPainting(int index) => new(GetPaintingSpan(index).ToArray(), Japanese);
public void SetPainting(int index, Paintings3 value) => value.Data.CopyTo(GetPaintingSpan(index));
#endregion
}

View File

@ -133,7 +133,7 @@ public override void CopyChangesFrom(SaveFile sav)
public override ushort MaxMoveID => Legal.MaxMoveID_3;
public override ushort MaxSpeciesID => Legal.MaxSpeciesID_3;
public override int MaxAbilityID => Legal.MaxAbilityID_3;
public override int MaxItemID => Legal.MaxItemID_3;
public override int MaxItemID => Legal.MaxItemID_3_RS;
public override int MaxBallID => Legal.MaxBallID_3;
public override GameVersion MaxGameID => Legal.MaxGameID_3;

View File

@ -7,9 +7,18 @@ namespace PKHeX.Core;
/// <summary>
/// Generation 3 <see cref="SaveFile"/> object for Pokémon XD saves.
/// </summary>
public sealed class SAV3XD : SaveFile, IGCSaveFile, IBoxDetailName, IDaycareStorage, IDaycareExperience, IGCRegion
public sealed class SAV3XD : SaveFile, IGCSaveFile, IBoxDetailName, IDaycareStorage, IDaycareExperience, IGCRegion, ISaveFileRevision
{
protected internal override string ShortSummary => $"{OT} ({Version}) {PlayTimeString}";
public int SaveRevision => 0;
public string SaveRevisionString => OriginalRegion switch
{
GCRegion.NTSC_J => "-J",
GCRegion.NTSC_U => "-U",
GCRegion.PAL => "-PAL",
_ => "-?",
};
public override string Extension => this.GCExtension();
public SAV3GCMemoryCard? MemoryCard { get; init; }
@ -234,7 +243,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

@ -238,10 +238,10 @@ private int GetActiveExtraBlock(BlockInfo4 block)
private int OFS_Backdrop => FashionCase + 0x28;
protected int OFS_Chatter = int.MinValue;
public Chatter4 Chatter => new(this, Buffer[OFS_Chatter..]);
public Chatter4 Chatter => new(this, GeneralBuffer[OFS_Chatter..]);
protected int OFS_Record = int.MinValue;
public Record4 Records => new(this, Buffer.Slice(OFS_Record, Record4.GetSize(this)));
public Record4 Records => new(this, GeneralBuffer.Slice(OFS_Record, Record4.GetSize(this)));
protected int OFS_Groups = int.MinValue;
@ -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
@ -337,13 +340,13 @@ public override int PlayedSeconds
public abstract int X { get; set; }
public abstract int Y { get; set; }
public string Rival
public string RivalName
{
get => GetString(RivalTrash);
set => SetString(RivalTrash, value, MaxStringLengthTrainer, StringConverterOption.ClearZero);
get => GetString(RivalNameTrash);
set => SetString(RivalNameTrash, value, MaxStringLengthTrainer, StringConverterOption.ClearZero);
}
public abstract Span<byte> RivalTrash { get; set; }
public abstract Span<byte> RivalNameTrash { get; set; }
public abstract int X2 { get; set; }
public abstract int Y2 { get; set; }

View File

@ -97,7 +97,7 @@ public override void SetBoxWallpaper(int box, int value)
public override int X { get => ReadUInt16LittleEndian(General[0x1240..]); set => WriteUInt16LittleEndian(General[0x1240..], (ushort)(X2 = value)); }
public override int Y { get => ReadUInt16LittleEndian(General[0x1244..]); set => WriteUInt16LittleEndian(General[0x1244..], (ushort)(Y2 = value)); }
public override Span<byte> RivalTrash
public override Span<byte> RivalNameTrash
{
get => General.Slice(0x25A8, MaxStringLengthTrainer * 2);
set { if (value.Length == MaxStringLengthTrainer * 2) value.CopyTo(General[0x25A8..]); }

View File

@ -190,7 +190,7 @@ protected override void SetPKM(PKM pk, bool isParty = false)
public override int X { get => ReadUInt16LittleEndian(General[0x123C..]); set => WriteUInt16LittleEndian(General[0x123C..], (ushort)(X2 = value)); }
public override int Y { get => ReadUInt16LittleEndian(General[0x1240..]); set => WriteUInt16LittleEndian(General[0x1240..], (ushort)(Y2 = value)); }
public override Span<byte> RivalTrash
public override Span<byte> RivalNameTrash
{
get => RivalSpan;
set { if (value.Length == MaxStringLengthTrainer * 2) value.CopyTo(RivalSpan); }

View File

@ -137,7 +137,7 @@ public void SetWallpaperUnlocked(Wallpaper4Pt wallpaperId, bool value)
public override int X { get => ReadUInt16LittleEndian(General[0x1288..]); set => WriteUInt16LittleEndian(General[0x1288..], (ushort)(X2 = value)); }
public override int Y { get => ReadUInt16LittleEndian(General[0x128C..]); set => WriteUInt16LittleEndian(General[0x128C..], (ushort)(Y2 = value)); }
public override Span<byte> RivalTrash
public override Span<byte> RivalNameTrash
{
get => RivalSpan;
set { if (value.Length == MaxStringLengthTrainer * 2) value.CopyTo(RivalSpan); }

View File

@ -51,13 +51,13 @@ public sealed class SAV5B2W2 : SAV5, ISaveBlock5B2W2
public MedalList5 Medals => Blocks.Medals;
public KeySystem5 Keys => Blocks.Keys;
public string Rival
public string RivalName
{
get => GetString(RivalTrash);
set => SetString(RivalTrash, value, MaxStringLengthTrainer, StringConverterOption.ClearZero);
get => GetString(RivalNameTrash);
set => SetString(RivalNameTrash, value, MaxStringLengthTrainer, StringConverterOption.ClearZero);
}
public Span<byte> RivalTrash
public Span<byte> RivalNameTrash
{
get => Data.Slice(0x23BA4, MaxStringLengthTrainer * 2);
set { if (value.Length == MaxStringLengthTrainer * 2) value.CopyTo(Data[0x23BA4..]); }

View File

@ -280,10 +280,12 @@ public override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, i
public override int CurrentBox { get => BoxLayout.CurrentBox; set => BoxLayout.CurrentBox = (byte)value; }
public override int BoxesUnlocked { get => BoxLayout.BoxesUnlocked; set => BoxLayout.BoxesUnlocked = (byte)value; }
public string Rival
public Span<byte> RivalNameTrash => Data.Slice(0x55F4, 0x1A);
public string RivalName
{
get => GetString(Data.Slice(0x55F4, 0x1A));
set => SetString(Data.Slice(0x55F4, 0x1A), value, MaxStringLengthTrainer, StringConverterOption.ClearZero);
get => GetString(RivalNameTrash);
set => SetString(RivalNameTrash, value, MaxStringLengthTrainer, StringConverterOption.ClearZero);
}
public short ZoneID // map

View File

@ -36,7 +36,7 @@ public SAV8LA()
{
0 => "-Base", // Vanilla
1 => "-DB", // DLC 1: Daybreak
_ => throw new ArgumentOutOfRangeException(nameof(SaveRevision)),
_ => throw new ArgumentOutOfRangeException(nameof(SaveRevision), SaveRevision, null),
};
public override string GetString(ReadOnlySpan<byte> data)

View File

@ -45,7 +45,7 @@ public override void CopyChangesFrom(SaveFile sav)
0 => "-Base", // Vanilla
1 => "-IoA", // DLC 1: Isle of Armor
2 => "-CT", // DLC 2: Crown Tundra
_ => throw new ArgumentOutOfRangeException(nameof(SaveRevision)),
_ => throw new ArgumentOutOfRangeException(nameof(SaveRevision), SaveRevision, null),
};
public override bool ChecksumsValid => true;
@ -111,7 +111,7 @@ private void Initialize()
0 => (Legal.MaxMoveID_8_O0, Legal.MaxSpeciesID_8_O0, Legal.MaxItemID_8_O0, Legal.MaxAbilityID_8_O0),
1 => (Legal.MaxMoveID_8_R1, Legal.MaxSpeciesID_8_R1, Legal.MaxItemID_8_R1, Legal.MaxAbilityID_8_R1),
2 => (Legal.MaxMoveID_8_R2, Legal.MaxSpeciesID_8_R2, Legal.MaxItemID_8_R2, Legal.MaxAbilityID_8_R2),
_ => throw new ArgumentOutOfRangeException(nameof(SaveRevision)),
_ => throw new ArgumentOutOfRangeException(nameof(SaveRevision), SaveRevision, null),
};
}

View File

@ -50,7 +50,7 @@ public override void CopyChangesFrom(SaveFile sav)
0 => "-Base", // Vanilla
1 => "-TM", // Teal Mask
2 => "-ID", // Indigo Disk
_ => throw new ArgumentOutOfRangeException(nameof(SaveRevision)),
_ => throw new ArgumentOutOfRangeException(nameof(SaveRevision), SaveRevision, null),
};
public override bool ChecksumsValid => true;
@ -118,7 +118,7 @@ private void Initialize()
0 => (Legal.MaxMoveID_9_T0, Legal.MaxSpeciesID_9_T0, Legal.MaxItemID_9_T0, Legal.MaxAbilityID_9_T0),
1 => (Legal.MaxMoveID_9_T1, Legal.MaxSpeciesID_9_T1, Legal.MaxItemID_9_T1, Legal.MaxAbilityID_9_T1),
2 => (Legal.MaxMoveID_9_T2, Legal.MaxSpeciesID_9_T2, Legal.MaxItemID_9_T2, Legal.MaxAbilityID_9_T2),
_ => throw new ArgumentOutOfRangeException(nameof(SaveRevision)),
_ => throw new ArgumentOutOfRangeException(nameof(SaveRevision), SaveRevision, null),
};
}

View File

@ -48,7 +48,8 @@ public override void CopyChangesFrom(SaveFile sav)
{
0 => "-Base", // Vanilla
1 => "-MD", // Mega Dimension
_ => throw new ArgumentOutOfRangeException(nameof(SaveRevision)),
2 => "-EOL", // End of Life
_ => throw new ArgumentOutOfRangeException(nameof(SaveRevision), SaveRevision, null),
};
public override bool ChecksumsValid => true;

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using static System.Buffers.Binary.BinaryPrimitives;
@ -33,6 +34,7 @@ protected SAV_BEEF([ConstantExpected] int size, [ConstantExpected] int biOffset)
/// <summary>
/// Timestamp that the save file was last saved at (Secure Value)
/// </summary>
[TypeConverter(typeof(TypeConverterU64))]
public ulong TimeStampCurrent
{
get => ReadUInt64LittleEndian(Data[BlockInfoOffset..]);
@ -42,6 +44,7 @@ public ulong TimeStampCurrent
/// <summary>
/// Timestamp that the save file was saved at prior to the <see cref="TimeStampCurrent"/> (Secure Value)
/// </summary>
[TypeConverter(typeof(TypeConverterU64))]
public ulong TimeStampPrevious
{
get => ReadUInt64LittleEndian(Data[(BlockInfoOffset + 8)..]);

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using static System.Buffers.Binary.BinaryPrimitives;
@ -19,13 +20,14 @@ private static int GetMemberOffset(int index)
private Memory<byte> GetMemberSlice(int index)
=> Raw.Slice(GetMemberOffset(index), HallFame3PKM.SIZE);
private HallFame3PKM GetMember(int index) => new(GetMemberSlice(index), Japanese);
public HallFame3PKM GetMember(int index) => new(GetMemberSlice(index), Japanese);
public HallFame3PKM[] Team
{
get
{
var team = new HallFame3PKM[6];
var team = new HallFame3PKM[Count];
for (int i = 0; i < Count; i++)
team[i] = GetMember(i);
return team;
@ -49,13 +51,26 @@ public static HallFame3Entry[] GetEntries(SAV3 sav)
public static void SetEntries(SAV3 sav, HallFame3Entry[] entries)
{
byte[] data = sav.GetHallOfFameData();
Span<byte> data = sav.GetHallOfFameData();
Debug.Assert(data.Length >= MaxLength);
for (int i = 0; i < entries.Length; i++)
entries[i].Data.CopyTo(data.AsSpan(i * SIZE));
entries[i].Data.CopyTo(data[(i * SIZE)..]);
sav.SetHallOfFameData(data);
}
public void CopyFrom(IReadOnlyList<PKM> party)
{
var length = Math.Min(Count, party.Count);
for (int i = 0; i < length; i++)
GetMember(i).CopyFrom(party[i]);
}
public void CopyFrom(HallFame3Entry entry)
{
for (int i = 0; i < Count; i++)
GetMember(i).CopyFrom(entry.GetMember(i));
}
}
public sealed class HallFame3PKM(Memory<byte> Raw, bool Japanese) : ISpeciesForm
@ -104,4 +119,22 @@ public ushort Species
};
public bool IsShiny => ShinyUtil.GetIsShiny3(ID32, PID);
public void CopyFrom(PKM pk)
{
Species = pk.Species;
Level = pk.CurrentLevel;
PID = pk.EncryptionConstant;
ID32 = pk.ID32;
pk.NicknameTrash.CopyTo(NicknameTrash);
}
public void CopyFrom(HallFame3PKM pk)
{
Species = pk.Species;
Level = pk.Level;
PID = pk.PID;
ID32 = pk.ID32;
pk.NicknameTrash.CopyTo(NicknameTrash);
}
}

View File

@ -1,9 +0,0 @@
namespace PKHeX.Core;
public interface IGen3Wonder
{
int WonderOffset { get; }
WonderNews3 WonderNews { get; set; }
WonderCard3 WonderCard { get; set; }
WonderCard3Extra WonderCardExtra { get; set; }
}

View File

@ -1,29 +1,10 @@
using System;
using System.Collections.Generic;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public sealed class Record3(SAV3 SAV)
public static class Record3
{
public uint GetRecord(int record) => ReadUInt32LittleEndian(SAV.Large[GetRecordOffset(record)..]) ^ SAV.SecurityKey;
public void SetRecord(int record, uint value) => WriteUInt32LittleEndian(SAV.Large[GetRecordOffset(record)..], value ^ SAV.SecurityKey);
private int GetRecordOffset(int record)
{
var baseOffset = GetOffset(SAV.Version);
var offset = baseOffset + (4 * record);
return offset;
}
public static int GetOffset(GameVersion version) => version switch
{
GameVersion.RS or GameVersion.R or GameVersion.S => 0x1540,
GameVersion.E => 0x159C,
GameVersion.FRLG or GameVersion.FR or GameVersion.LG => 0x1200,
_ => throw new ArgumentOutOfRangeException(nameof(version), version, null),
};
private static Type GetEnumType(GameVersion version) => version switch
{
GameVersion.RS or GameVersion.R or GameVersion.S => typeof(RecID3RuSa),
@ -41,7 +22,7 @@ public static IList<ComboItem> GetItems(SAV3 sav)
var names = GetEnumNames(version);
var values = GetEnumValues(version);
var result = new ComboItem[values.Length];
var result = new ComboItem[values.Length - 1]; // exclude NUM_GAME_STATS
for (int i = 0; i < result.Length; i++)
{
var replaced = names[i].Replace('_', ' ');
@ -110,7 +91,7 @@ public enum RecID3RuSa
USED_DAYCARE = 47,
RODE_CABLE_CAR = 48,
ENTERED_HOT_SPRINGS = 49,
// NUM_GAME_STATS = 50
NUM_GAME_STATS = 50,
}
/// <summary>
@ -175,7 +156,7 @@ public enum RecID3Emerald
BERRY_CRUSH_WITH_FRIENDS = 51,
// NUM_USED_GAME_STATS = 52,
// NUM_GAME_STATS = 64
NUM_GAME_STATS = 64,
}
/// <summary>
@ -239,5 +220,5 @@ public enum RecID3FRLG
UNION_WITH_FRIENDS = 50,
BERRY_CRUSH_WITH_FRIENDS = 51,
// NUM_GAME_STATS = 64,
NUM_GAME_STATS = 64,
}

View File

@ -3,24 +3,13 @@
namespace PKHeX.Core;
public sealed class Roamer3 : IContestStats
public sealed record Roamer3(Memory<byte> Raw, bool IsGlitched) : IContestStats
{
public const int SIZE = 0x14;
public bool IsGlitched { get; }
private readonly Memory<byte> Raw;
public const int SIZE = 0x14; // +8 bytes of unused
private Span<byte> Data => Raw.Span;
public Roamer3(SAV3 sav)
public Roamer3(ISaveBlock3Large large) : this(large.RoamerData, large is not SaveBlock3LargeE)
{
var offset = sav switch
{
SAV3RS => 0x3144,
SAV3E => 0x31DC,
_ => 0x30D0, // FRLG
};
var buffer = sav.LargeBuffer;
Raw = buffer.Slice(offset, SIZE);
IsGlitched = sav is not SAV3E;
}
public uint IV32
@ -53,7 +42,7 @@ public byte CurrentLevel
set => Data[12] = value;
}
public int Status { get => Data[0x0D]; set => Data[0x0D] = (byte)value; }
public byte Status { get => Data[0x0D]; set => Data[0x0D] = (byte)value; }
public byte ContestCool { get => Data[0x0E]; set => Data[0x0E] = value; }
public byte ContestBeauty { get => Data[0x0F]; set => Data[0x0F] = value; }

View File

@ -51,7 +51,7 @@ public int RegistryStatus
public string OriginalTrainerName
{
get => StringConverter3.GetString(OriginalTrainerTrash, Language);
set => StringConverter3.SetString(OriginalTrainerTrash, value, 7, Language, StringConverterOption.ClearFF);
set => StringConverter3.SetString(OriginalTrainerTrash, value, 7, Language, StringConverterOption.None);
}
public int OriginalTrainerClass => Data[9] % 5;

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

@ -101,7 +101,7 @@ public int Language
set => Data[0x2D] = (byte)value;
}
private Span<byte> OriginalTrainerTrash => Data.Slice(0x48, 0x1A);
public Span<byte> OriginalTrainerTrash => Data.Slice(0x48, 0x1A);
public string OT
{

Some files were not shown because too many files have changed in this diff Show More