mirror of
https://github.com/kwsch/PKHeX
synced 2024-12-22 10:23:09 +00:00
47071b41f3
Existing `get`/`set` logic is flawed in that it doesn't work on Big Endian operating systems, and it allocates heap objects when it doesn't need to. `System.Buffers.Binary.BinaryPrimitives` in the `System.Memory` NuGet package provides both Little Endian and Big Endian methods to read and write data; all the `get`/`set` operations have been reworked to use this new API. This removes the need for PKHeX's manual `BigEndian` class, as all functions are already covered by the BinaryPrimitives API. The `StringConverter` has now been rewritten to accept a Span to read from & write to, no longer requiring a temporary StringBuilder. Other Fixes included: - The Super Training UI for Gen6 has been reworked according to the latest block structure additions. - Cloning a Stadium2 Save File now works correctly (opening from the Folder browser list). - Checksum & Sanity properties removed from parent PKM class, and is now implemented via interface.
341 lines
11 KiB
C#
341 lines
11 KiB
C#
using System;
|
|
using static System.Buffers.Binary.BinaryPrimitives;
|
|
|
|
namespace PKHeX.Core
|
|
{
|
|
public sealed class SuperTrainBlock : SaveBlock
|
|
{
|
|
public SuperTrainBlock(SAV6XY sav, int offset) : base(sav) => Offset = offset;
|
|
public SuperTrainBlock(SAV6AO sav, int offset) : base(sav) => Offset = offset;
|
|
|
|
// Structure:
|
|
// 6 bytes stage unlock flags
|
|
// 1 byte distribution stage unlock flags
|
|
// 1 byte counter (max value = 10)
|
|
// float[48] recordScore1; // 0x08, 4byte/entry
|
|
// float[48] recordScore2; // 0xC8, 4byte/entry
|
|
// SS-F-G[48] recordHolder1; // 0x188, 4byte/entry
|
|
// SS-F-G[48] recordHolder2; // 0x248, 4byte/entry
|
|
// byte[12] bags // 0x308
|
|
// u32 tutorial tracker (max value = 6) // 0x314
|
|
|
|
// 0x318 total size
|
|
|
|
// SS-F-G = u16 species, u8 form, u8 gender
|
|
|
|
private const int MAX = 48;
|
|
private const int MAX_RELEASED = 32;
|
|
private const int MAX_DIST = 6;
|
|
private const int MAX_BAG = 12;
|
|
|
|
/// <summary>
|
|
/// Checks if a Regimen is unlocked.
|
|
/// </summary>
|
|
/// <param name="index">Index of regimen.</param>
|
|
/// <returns>Is Unlocked</returns>
|
|
public bool GetIsRegimenUnlocked(int index)
|
|
{
|
|
if ((uint)index >= MAX)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
return SAV.GetFlag(Offset, index);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a Regimen to the desired unlocked state.
|
|
/// </summary>
|
|
/// <param name="index">Index of regimen.</param>
|
|
/// <param name="value">Is Unlocked</param>
|
|
public void SetIsRegimenUnlocked(int index, bool value)
|
|
{
|
|
if ((uint)index >= MAX)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
SAV.SetFlag(Offset, index, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a Distribution Regimen is unlocked.
|
|
/// </summary>
|
|
/// <param name="index">Index of regimen.</param>
|
|
/// <returns>Is Unlocked</returns>
|
|
public bool GetIsDistributionUnlocked(int index)
|
|
{
|
|
if ((uint)index >= MAX_DIST)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
return SAV.GetFlag(Offset + 6, index);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a Distribution Regimen to the desired unlocked state.
|
|
/// </summary>
|
|
/// <param name="index">Index of regimen.</param>
|
|
/// <param name="value">Is Unlocked</param>
|
|
public void SetIsDistributionUnlocked(int index, bool value)
|
|
{
|
|
if ((uint)index >= MAX_DIST)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
SAV.SetFlag(Offset + 6, index, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Counts something up to 10 (overall stages unlocked?)
|
|
/// </summary>
|
|
public byte Counter
|
|
{
|
|
get => Data[Offset + 7];
|
|
set => Data[Offset + 7] = Math.Min((byte)10, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the record time.
|
|
/// </summary>
|
|
/// <param name="index">Index of the record.</param>
|
|
public float GetTime1(int index)
|
|
{
|
|
if ((uint) index >= MAX)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
|
|
return ReadSingleLittleEndian(Data.AsSpan(Offset + 0x08 + (4 * index)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the record time.
|
|
/// </summary>
|
|
/// <param name="index">Index of the record.</param>
|
|
/// <param name="value">Value to set.</param>
|
|
public void SetTime1(int index, float value = 0)
|
|
{
|
|
if ((uint)index >= MAX)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
|
|
WriteSingleLittleEndian(Data.AsSpan(Offset + 0x08 + (4 * index)), value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the record time.
|
|
/// </summary>
|
|
/// <param name="index">Index of the record.</param>
|
|
public float GetTime2(int index)
|
|
{
|
|
if ((uint)index >= MAX)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
|
|
return ReadSingleLittleEndian(Data.AsSpan(Offset + 0xC8 + (4 * index)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the record time.
|
|
/// </summary>
|
|
/// <param name="index">Index of the record.</param>
|
|
/// <param name="value">Value to set.</param>
|
|
public void SetTime2(int index, float value = 0)
|
|
{
|
|
if ((uint)index >= MAX)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
|
|
WriteSingleLittleEndian(Data.AsSpan(Offset + 0xC8 + (4 * index)), value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns an object which will edit the record directly from data.
|
|
/// </summary>
|
|
/// <param name="index">Index of the record.</param>
|
|
/// <returns>Object that will edit the record data if modified.</returns>
|
|
public SuperTrainingSpeciesRecord GetHolder1(int index)
|
|
{
|
|
if ((uint)index >= MAX)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
|
|
return new SuperTrainingSpeciesRecord(Data, Offset + 0x188 + (4 * index));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns an object which will edit the record directly from data.
|
|
/// </summary>
|
|
/// <param name="index">Index of the record.</param>
|
|
/// <returns>Object that will edit the record data if modified.</returns>
|
|
public SuperTrainingSpeciesRecord GetHolder2(int index)
|
|
{
|
|
if ((uint)index >= MAX)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
|
|
return new SuperTrainingSpeciesRecord(Data, Offset + 0x248 + (4 * index));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the bag from the desired <see cref="index"/>.
|
|
/// </summary>
|
|
/// <param name="index">Bag index</param>
|
|
public byte GetBag(int index)
|
|
{
|
|
if ((uint)index >= MAX_BAG)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
return Data[Offset + 0x308 + index];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a bag to the desired <see cref="index"/>.
|
|
/// </summary>
|
|
/// <param name="index">Bag index</param>
|
|
/// <param name="value">Bag ID</param>
|
|
public void SetBag(int index, byte value)
|
|
{
|
|
if ((uint)index >= MAX_BAG)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
Data[Offset + 0x308 + index] = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the next open bag index.
|
|
/// </summary>
|
|
/// <returns>Bag index that is empty</returns>
|
|
public int GetOpenBagIndex()
|
|
{
|
|
for (int i = 0; i < MAX_BAG; i++)
|
|
{
|
|
if (GetBag(i) != 0)
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a bag to the list of bags.
|
|
/// </summary>
|
|
/// <param name="bag">Bag type</param>
|
|
/// <returns>Bag was added or not</returns>
|
|
public bool AddBag(byte bag)
|
|
{
|
|
int index = GetOpenBagIndex();
|
|
if (index < 0)
|
|
return false;
|
|
SetBag(index, bag);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a bag from the list of bags.
|
|
/// </summary>
|
|
/// <param name="index">Bag index</param>
|
|
public void RemoveBag(int index)
|
|
{
|
|
if ((uint)index >= MAX_BAG)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
for (int i = index; i < MAX_BAG - 1; i++)
|
|
{
|
|
var next = GetBag(i + 1);
|
|
SetBag(i, next);
|
|
}
|
|
SetBag(MAX_BAG - 1, 0);
|
|
}
|
|
|
|
public uint TutorialIndex
|
|
{
|
|
get => ReadUInt32LittleEndian(Data.AsSpan(Offset + 0x314));
|
|
set => WriteUInt32LittleEndian(Data.AsSpan(Offset + 0x314), value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all data for the record.
|
|
/// </summary>
|
|
/// <param name="index">Index of the record.</param>
|
|
public void ClearRecord1(int index)
|
|
{
|
|
if ((uint)index >= MAX)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
|
|
SetTime1(index, 0f);
|
|
GetHolder1(index).Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all data for the record.
|
|
/// </summary>
|
|
/// <param name="index">Index of the record.</param>
|
|
public void ClearRecord2(int index)
|
|
{
|
|
if ((uint)index >= MAX)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
|
|
SetTime2(index, 0f);
|
|
GetHolder2(index).Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unlocks all stages.
|
|
/// </summary>
|
|
/// <param name="dist">Unlock all Distribution stages too.</param>
|
|
public void UnlockAllStages(bool dist)
|
|
{
|
|
for (int i = 0; i < MAX_RELEASED; i++)
|
|
SetIsRegimenUnlocked(i, true);
|
|
|
|
if (!dist)
|
|
return;
|
|
|
|
for (int i = 0; i < MAX_DIST; i++)
|
|
SetIsDistributionUnlocked(i, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all data in the block.
|
|
/// </summary>
|
|
public void ClearBlock() => Array.Clear(Data, Offset, 0x318);
|
|
}
|
|
|
|
public sealed class SuperTrainingSpeciesRecord : ISpeciesForm
|
|
{
|
|
private readonly byte[] Data;
|
|
private readonly int Offset;
|
|
|
|
public SuperTrainingSpeciesRecord(byte[] data, int offset)
|
|
{
|
|
Data = data;
|
|
Offset = offset;
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="PKM.Species"/> of the Record Holder.
|
|
/// </summary>
|
|
public int Species
|
|
{
|
|
get => ReadUInt16LittleEndian(Data.AsSpan(Offset + 0));
|
|
set => WriteUInt16LittleEndian(Data.AsSpan(Offset + 0), (ushort)value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="PKM.Form"/> of the Record Holder.
|
|
/// </summary>
|
|
public int Form
|
|
{
|
|
get => Data[Offset + 2];
|
|
set => Data[Offset + 2] = (byte)value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// <see cref="PKM.Gender"/> of the Record Holder.
|
|
/// </summary>
|
|
/// <seealso cref="Core.Gender"/>
|
|
public int Gender
|
|
{
|
|
get => Data[Offset + 3];
|
|
set => Data[Offset + 3] = (byte)value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wipes the record holder's pkm-related data.
|
|
/// </summary>
|
|
public void Clear() => Species = Form = Gender = 0;
|
|
|
|
/// <summary>
|
|
/// Sets the data to match what is in the provided reference.
|
|
/// </summary>
|
|
/// <param name="pkm">Reference to load from.</param>
|
|
public void LoadFrom(PKM pkm)
|
|
{
|
|
Species = pkm.Species;
|
|
Form = pkm.Form;
|
|
Gender = pkm.Gender;
|
|
}
|
|
}
|
|
}
|