Refactor And Deduplicate Some Switch Games Common Logic (#4187)

- Improve Epoch 1900 classes using similar logic from PlayTime7b.
- Move Time Classes into non Gen specific folder since it appears the logic is shared across a few of them.
- Use Epoch1900DateTimeValue for LastSaved in PlayTime7b since the logic is the same.
- Remove TeamIndexes9 since it is a duplicate of TeamIndexes9. Use the similar pattern like Box8 where it is reused in multiple locations.
- Add BlueberryClubRoom9 to ISaveBlock9Main since it wasn't added when the class was introduced.
- Simplify RaidSevenStar9 creation since GetBlockSafe does basically what the if-else block does.
- Change SAV8SWSH Base Raid to RaidGalar to match the same way the SAV9SV does for its Base Raid.
This commit is contained in:
Jonathan Herbert 2024-02-23 15:19:17 -04:00 committed by GitHub
parent cf274bba70
commit 92c964d6fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 119 additions and 178 deletions

View file

@ -17,7 +17,7 @@ public interface ISaveBlock8Main
Daycare8 Daycare { get; }
Record8 Records { get; }
TrainerCard8 TrainerCard { get; }
RaidSpawnList8 Raid { get; }
RaidSpawnList8 RaidGalar { get; }
RaidSpawnList8 RaidArmor { get; }
RaidSpawnList8 RaidCrown { get; }
TitleScreen8 TitleScreen { get; }

View file

@ -13,7 +13,7 @@ public interface ISaveBlock9Main
PlayTime9 Played { get; }
Zukan9 Zukan { get; }
ConfigSave9 Config { get; }
TeamIndexes9 TeamIndexes { get; }
TeamIndexes8 TeamIndexes { get; }
Epoch1900DateTimeValue LastSaved { get; }
Epoch1970Value LastDateCycle { get; }
PlayerFashion9 PlayerFashion { get; }
@ -22,4 +22,5 @@ public interface ISaveBlock9Main
RaidSpawnList9 RaidKitakami { get; }
RaidSpawnList9 RaidBlueberry { get; }
BlueberryQuestRecord9 BlueberryQuestRecord { get; }
BlueberryClubRoom9 BlueberryClubRoom { get; }
}

View file

@ -24,7 +24,7 @@ public sealed class SaveBlockAccessor8SWSH : SCBlockAccessor, ISaveBlock8Main
public Record8 Records { get; }
public TrainerCard8 TrainerCard{ get; }
public FashionUnlock8 Fashion { get; }
public RaidSpawnList8 Raid { get; }
public RaidSpawnList8 RaidGalar { get; }
public RaidSpawnList8 RaidArmor { get; }
public RaidSpawnList8 RaidCrown { get; }
public TitleScreen8 TitleScreen { get; }
@ -48,7 +48,7 @@ public sealed class SaveBlockAccessor8SWSH : SCBlockAccessor, ISaveBlock8Main
Daycare = new Daycare8(sav, GetBlock(KDaycare));
Records = new Record8(sav, GetBlock(KRecord));
Fashion = new FashionUnlock8(sav, GetBlock(KFashionUnlock));
Raid = new RaidSpawnList8(sav, GetBlock(KRaidSpawnList), RaidSpawnList8.RaidCountLegal_O0);
RaidGalar = new RaidSpawnList8(sav, GetBlock(KRaidSpawnList), RaidSpawnList8.RaidCountLegal_O0);
RaidArmor = new RaidSpawnList8(sav, GetBlockSafe(KRaidSpawnListR1), RaidSpawnList8.RaidCountLegal_R1);
RaidCrown = new RaidSpawnList8(sav, GetBlockSafe(KRaidSpawnListR2), RaidSpawnList8.RaidCountLegal_R2);
TitleScreen = new TitleScreen8(sav, GetBlock(KTitleScreenTeam));

View file

@ -21,7 +21,7 @@ public sealed class SaveBlockAccessor9SV : SCBlockAccessor, ISaveBlock9Main
public Zukan9 Zukan { get; }
public ConfigSave9 Config { get; }
public ConfigCamera9 ConfigCamera { get; }
public TeamIndexes9 TeamIndexes { get; }
public TeamIndexes8 TeamIndexes { get; }
public Epoch1900DateTimeValue LastSaved { get; }
public Epoch1970Value LastDateCycle { get; }
public PlayerFashion9 PlayerFashion { get; }
@ -46,7 +46,7 @@ public sealed class SaveBlockAccessor9SV : SCBlockAccessor, ISaveBlock9Main
Zukan = new Zukan9(sav, GetBlock(KZukan), GetBlockSafe(KZukanT1));
Config = new ConfigSave9(sav, GetBlock(KConfig));
ConfigCamera = new ConfigCamera9(sav, GetBlockSafe(KConfigCamera));
TeamIndexes = new TeamIndexes9(sav, GetBlock(KTeamIndexes), GetBlock(KTeamLocks));
TeamIndexes = new TeamIndexes8(sav, GetBlock(KTeamIndexes), GetBlock(KTeamLocks));
LastSaved = new Epoch1900DateTimeValue(GetBlock(KLastSaved));
LastDateCycle = new Epoch1970Value(GetBlock(KLastDateCycle));
PlayerFashion = new PlayerFashion9(sav, GetBlock(KCurrentClothing));
@ -70,12 +70,7 @@ public sealed class SaveBlockAccessor9SV : SCBlockAccessor, ISaveBlock9Main
RaidBlueberry = new RaidSpawnList9(sav, fake, default, RaidSpawnList9.RaidCountLegal_T2, false);
}
var captured = GetBlock(KSevenStarRaidsCapture);
var s7 = TryGetBlock(KSevenStarRaidsDefeat, out var defeated);
if (!s7)
defeated = GetFakeBlock();
RaidSevenStar = new RaidSevenStar9(sav, captured, defeated!);
RaidSevenStar = new RaidSevenStar9(sav, GetBlock(KSevenStarRaidsCapture), GetBlockSafe(KSevenStarRaidsDefeat));
EnrollmentDate = new Epoch1900DateValue(GetBlock(KEnrollmentDate));
BlueberryQuestRecord = new BlueberryQuestRecord9(sav, GetBlockSafe(KBlueberryQuestRecords));
BlueberryClubRoom = new BlueberryClubRoom9(sav, GetBlockSafe(KBlueberryClubRoom));

View file

@ -76,7 +76,7 @@ public sealed class SAV8SWSH : SaveFile, ISaveBlock8SWSH, ITrainerStatRecord, IS
public Record8 Records => Blocks.Records;
public TrainerCard8 TrainerCard => Blocks.TrainerCard;
public FashionUnlock8 Fashion => Blocks.Fashion;
public RaidSpawnList8 Raid => Blocks.Raid;
public RaidSpawnList8 RaidGalar => Blocks.RaidGalar;
public RaidSpawnList8 RaidArmor => Blocks.RaidArmor;
public RaidSpawnList8 RaidCrown => Blocks.RaidCrown;
public TitleScreen8 TitleScreen => Blocks.TitleScreen;

View file

@ -74,7 +74,7 @@ public sealed class SAV9SV : SaveFile, ISaveBlock9Main, ISCBlockArray, ISaveFile
public BoxLayout9 BoxLayout => Blocks.BoxLayout;
public PlayTime9 Played => Blocks.Played;
public ConfigSave9 Config => Blocks.Config;
public TeamIndexes9 TeamIndexes => Blocks.TeamIndexes;
public TeamIndexes8 TeamIndexes => Blocks.TeamIndexes;
public Epoch1900DateTimeValue LastSaved => Blocks.LastSaved;
public Epoch1970Value LastDateCycle => Blocks.LastDateCycle;
public PlayerFashion9 PlayerFashion => Blocks.PlayerFashion;

View file

@ -25,39 +25,34 @@ public sealed class PlayTime7b : SaveBlock<SAV7b>
set => Data[Offset + 3] = (byte)value;
}
private uint LastSaved { get => ReadUInt32LittleEndian(Data.AsSpan(Offset + 0x4)); set => WriteUInt32LittleEndian(Data.AsSpan(Offset + 0x4), value); }
private int LastSavedYear { get => (int)(LastSaved & 0xFFF) + 1900; set => LastSaved = (LastSaved & 0xFFFFF000) | (uint)(value - 1900); }
private int LastSavedMonth { get => (int)((LastSaved >> 12) & 0xF) + 1; set => LastSaved = (LastSaved & 0xFFFF0FFF) | (((uint)(value - 1) & 0xF) << 12); }
private int LastSavedDay { get => (int)((LastSaved >> 16) & 0x1F); set => LastSaved = (LastSaved & 0xFFE0FFFF) | (((uint)value & 0x1F) << 16); }
private int LastSavedHour { get => (int)((LastSaved >> 21) & 0x1F); set => LastSaved = (LastSaved & 0xFC1FFFFF) | (((uint)value & 0x1F) << 21); }
private int LastSavedMinute { get => (int)((LastSaved >> 26) & 0x3F); set => LastSaved = (LastSaved & 0x03FFFFFF) | (((uint)value & 0x3F) << 26); }
public string LastSavedTime => $"{LastSavedYear:0000}-{LastSavedMonth:00}-{LastSavedDay:00} {LastSavedHour:00}ː{LastSavedMinute:00}"; // not :
private Epoch1900DateTimeValue LastSaved => new(Data.AsMemory(Offset + 0x4));
public string LastSavedTime => $"{LastSaved.Year:0000}-{LastSaved.Month:00}-{LastSaved.Day:00} {LastSaved.Hour:00}ː{LastSaved.Minute:00}"; // not :
public DateTime? LastSavedDate
{
get => !DateUtil.IsDateValid(LastSavedYear, LastSavedMonth, LastSavedDay)
get => !DateUtil.IsDateValid(LastSaved.Year, LastSaved.Month, LastSaved.Day)
? null
: new DateTime(LastSavedYear, LastSavedMonth, LastSavedDay, LastSavedHour, LastSavedMinute, 0);
: new DateTime(LastSaved.Year, LastSaved.Month, LastSaved.Day, LastSaved.Hour, LastSaved.Minute, 0);
set
{
// Only update the properties if a value is provided.
if (value.HasValue)
{
var dt = value.Value;
LastSavedYear = dt.Year;
LastSavedMonth = dt.Month;
LastSavedDay = dt.Day;
LastSavedHour = dt.Hour;
LastSavedMinute = dt.Minute;
LastSaved.Year = dt.Year;
LastSaved.Month = dt.Month;
LastSaved.Day = dt.Day;
LastSaved.Hour = dt.Hour;
LastSaved.Minute = dt.Minute;
}
else // Clear the date.
{
// If code tries to access MetDate again, null will be returned.
LastSavedYear = 0;
LastSavedMonth = 0;
LastSavedDay = 0;
LastSavedHour = 0;
LastSavedMinute = 0;
LastSaved.Year = 0;
LastSaved.Month = 0;
LastSaved.Day = 0;
LastSaved.Hour = 0;
LastSaved.Minute = 0;
}
}
}

View file

@ -3,15 +3,28 @@ using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public sealed class TeamIndexes8(SAV8SWSH sav, SCBlock indexes, SCBlock locks) : ITeamIndexSet
public sealed class TeamIndexes8 : ITeamIndexSet
{
private const int TeamCount = 6;
private const int NONE_SELECTED = -1;
private readonly SaveFile SAV;
private readonly SCBlock Indexes;
private readonly SCBlock Locks;
public readonly int[] TeamSlots = new int[TeamCount * 6];
private TeamIndexes8(SaveFile sav, SCBlock indexes, SCBlock locks)
{
SAV = sav;
Indexes = indexes;
Locks = locks;
}
public TeamIndexes8(SAV8SWSH sav, SCBlock indexes, SCBlock locks) : this((SaveFile)sav, indexes, locks) { }
public TeamIndexes8(SAV9SV sav, SCBlock indexes, SCBlock locks) : this((SaveFile)sav, indexes, locks) { }
public void LoadBattleTeams()
{
if (!sav.State.Exportable)
if (!SAV.State.Exportable)
{
ClearBattleTeams();
return;
@ -19,7 +32,7 @@ public sealed class TeamIndexes8(SAV8SWSH sav, SCBlock indexes, SCBlock locks) :
for (int i = 0; i < TeamCount * 6; i++)
{
short val = ReadInt16LittleEndian(indexes.Data.AsSpan((i * 2)));
short val = ReadInt16LittleEndian(Indexes.Data.AsSpan(i * 2));
if (val < 0)
{
TeamSlots[i] = NONE_SELECTED;
@ -28,7 +41,7 @@ public sealed class TeamIndexes8(SAV8SWSH sav, SCBlock indexes, SCBlock locks) :
int box = val >> 8;
int slot = val & 0xFF;
int index = (sav.BoxSlotCount * box) + slot;
int index = (SAV.BoxSlotCount * box) + slot;
TeamSlots[i] = index & 0xFFFF;
}
}
@ -47,7 +60,7 @@ public sealed class TeamIndexes8(SAV8SWSH sav, SCBlock indexes, SCBlock locks) :
public void SaveBattleTeams()
{
var span = indexes.Data.AsSpan();
var span = Indexes.Data.AsSpan();
for (int i = 0; i < TeamCount * 6; i++)
{
int index = TeamSlots[i];
@ -57,12 +70,12 @@ public sealed class TeamIndexes8(SAV8SWSH sav, SCBlock indexes, SCBlock locks) :
continue;
}
sav.GetBoxSlotFromIndex(index, out var box, out var slot);
SAV.GetBoxSlotFromIndex(index, out var box, out var slot);
index = (box << 8) | slot;
WriteInt16LittleEndian(span[(i * 2)..], (short)index);
}
}
public bool GetIsTeamLocked(int team) => FlagUtil.GetFlag(locks.Data, 0, team);
public void SetIsTeamLocked(int team, bool value) => FlagUtil.SetFlag(locks.Data, 0, team, value);
public bool GetIsTeamLocked(int team) => FlagUtil.GetFlag(Locks.Data, 0, team);
public void SetIsTeamLocked(int team, bool value) => FlagUtil.SetFlag(Locks.Data, 0, team, value);
}

View file

@ -1,55 +0,0 @@
using System;
using System.ComponentModel;
namespace PKHeX.Core;
/// <summary>
/// Stores the <see cref="Timestamp"/> to indicate the seconds since 1900 that an event occurred.
/// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed class Epoch1900DateTimeValue(Memory<byte> Data)
{
// Data should be 8 bytes where we only care about the first 4.5 bytes i.e. 36 bits
// First 12 bits are year from 1900, next 4 bits are 0 indexed month, next 5 are days, next 5 are hours, next 6 bits are minutes, last 4 bits are seconds
private Span<byte> Span => Data.Span;
public Epoch1900DateTimeValue(SCBlock block) : this(block.Data) { }
private static DateTime Epoch => new(1900, 1, 1);
public DateTime Timestamp
{
get => Epoch
.AddSeconds(Span[4])
.AddMinutes(((Span[3] & 0b1111_1100) >> 2))
.AddHours((Span[3] & 0b0000_0011) << 3 | (Span[2] & 0b1110_0000) >> 5)
.AddDays((Span[2] & 0b0001_1111) - 1)
.AddMonths((Span[1] & 0b1111_0000) >> 4)
.AddYears(((Span[1] & 0b0000_1111) << 8) | Span[0]);
set
{
int day = value.Day;
int month = value.Month - Epoch.Month;
int year = value.Year - Epoch.Year;
int hour = value.Hour;
int minute = value.Minute;
int second = value.Second;
Span[4] = (byte)second;
Span[3] = (byte)(((minute & 0b0011_1111) << 2) | ((hour & 0b0001_1000) >> 3));
Span[2] = (byte)(((hour & 0b0000_0111) << 5) | (day & 0b0001_1111));
Span[1] = (byte)(((month & 0b0000_1111) << 4) | ((year & 0b1111_0000_00000) >> 8));
Span[0] = (byte)(year & 0b1111_1111);
}
}
public string DisplayValue => $"{Timestamp.Year:0000}-{Timestamp.Month:00}-{Timestamp.Day:00} {Timestamp.Hour:00}ː{Timestamp.Minute:00}ː{Timestamp.Second:00}"; // not :
/// <summary>
/// time_t (seconds since 1900 Epoch)
/// </summary>
public ulong Seconds
{
get => (ulong)(Timestamp - Epoch).TotalSeconds;
set => Timestamp = Epoch.AddSeconds(value);
}
}

View file

@ -1,68 +0,0 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public sealed class TeamIndexes9(SAV9SV sav, SCBlock indexes, SCBlock locks) : ITeamIndexSet
{
private const int TeamCount = 6;
private const int NONE_SELECTED = -1;
public readonly int[] TeamSlots = new int[TeamCount * 6];
public void LoadBattleTeams()
{
if (!sav.State.Exportable)
{
ClearBattleTeams();
return;
}
for (int i = 0; i < TeamCount * 6; i++)
{
short val = ReadInt16LittleEndian(indexes.Data.AsSpan(i * 2));
if (val < 0)
{
TeamSlots[i] = NONE_SELECTED;
continue;
}
int box = val >> 8;
int slot = val & 0xFF;
int index = (sav.BoxSlotCount * box) + slot;
TeamSlots[i] = index & 0xFFFF;
}
}
public void ClearBattleTeams()
{
for (int i = 0; i < TeamSlots.Length; i++)
TeamSlots[i] = NONE_SELECTED;
}
public void UnlockAllTeams()
{
for (int i = 0; i < TeamCount; i++)
SetIsTeamLocked(i, false);
}
public void SaveBattleTeams()
{
var span = indexes.Data.AsSpan();
for (int i = 0; i < TeamCount * 6; i++)
{
int index = TeamSlots[i];
if (index < 0)
{
WriteInt16LittleEndian(span[(i * 2)..], (short)index);
continue;
}
sav.GetBoxSlotFromIndex(index, out var box, out var slot);
index = (box << 8) | slot;
WriteInt16LittleEndian(span[(i * 2)..], (short)index);
}
}
public bool GetIsTeamLocked(int team) => FlagUtil.GetFlag(locks.Data, 0, team);
public void SetIsTeamLocked(int team, bool value) => FlagUtil.SetFlag(locks.Data, 0, team, value);
}

View file

@ -0,0 +1,59 @@
using System;
using System.ComponentModel;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
/// <summary>
/// Stores the <see cref="Timestamp"/> to indicate the seconds since 1900 that an event occurred.
/// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed class Epoch1900DateTimeValue(Memory<byte> Data)
{
// Data should be 4 or 8 bytes where we only care about the first 4 or 4.5 bytes i.e. 32 or 36 bits
// First 12 bits are year from 1900, next 4 bits are 0 indexed month, next 5 are days, next 5 are hours, next 6 bits are minutes, (optional) last 4 bits are seconds
private Span<byte> Span => Data.Span;
public Epoch1900DateTimeValue(SCBlock block) : this(block.Data) { }
private static DateTime Epoch => new(1900, 1, 1);
private uint RawDate { get => ReadUInt32LittleEndian(Span); set => WriteUInt32LittleEndian(Span, value); }
public int Year { get => (int)(RawDate & 0xFFF) + Epoch.Year; set => RawDate = (RawDate & 0xFFFFF000) | (uint)(value - Epoch.Year); }
public int Month { get => (int)((RawDate >> 12) & 0xF) + 1; set => RawDate = (RawDate & 0xFFFF0FFF) | (((uint)(value - 1) & 0xF) << 12); }
public int Day { get => (int)((RawDate >> 16) & 0x1F); set => RawDate = (RawDate & 0xFFE0FFFF) | (((uint)value & 0x1F) << 16); }
public int Hour { get => (int)((RawDate >> 21) & 0x1F); set => RawDate = (RawDate & 0xFC1FFFFF) | (((uint)value & 0x1F) << 21); }
public int Minute { get => (int)((RawDate >> 26) & 0x3F); set => RawDate = (RawDate & 0x03FFFFFF) | (((uint)value & 0x3F) << 26); }
public bool HasSeconds => Span.Length > 4;
public int Second {
get => HasSeconds ? Span[4] : 0;
set {
if (HasSeconds) Span[4] = (byte)value;
}
}
public DateTime Timestamp
{
get => new(Year, Month, Day, Hour, Minute, Second);
set
{
Year = value.Year;
Month = value.Month;
Day = value.Day;
Hour = value.Hour;
Minute = value.Minute;
Second = value.Second;
}
}
public string DisplayValue => $"{Timestamp.Year:0000}-{Timestamp.Month:00}-{Timestamp.Day:00} {Timestamp.Hour:00}ː{Timestamp.Minute:00}ː{Timestamp.Second:00}"; // not :
/// <summary>
/// time_t (seconds since 1900 Epoch)
/// </summary>
public ulong TotalSeconds
{
get => (ulong)(Timestamp - Epoch).TotalSeconds;
set => Timestamp = Epoch.AddSeconds(value);
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
@ -10,26 +11,26 @@ namespace PKHeX.Core;
public sealed class Epoch1900DateValue(Memory<byte> Data)
{
// Data should be 4 bytes where we only care about the first 3 bytes i.e. 24 bits
// First 6 bits are day, next 6 bits are 0 indexed month, last 12 bits are year from 1900
// First 12 bits are year from 1900, next 6 bits are 0 indexed month, next 6 are days
private Span<byte> Span => Data.Span;
public Epoch1900DateValue(SCBlock block) : this(block.Data) { }
private static DateTime Epoch => new(1900, 1, 1);
private uint RawDate { get => ReadUInt32LittleEndian(Span); set => WriteUInt32LittleEndian(Span, value); }
public int Year { get => (int)(RawDate & 0xFFF) + Epoch.Year; set => RawDate = (RawDate & 0xFFFFF000) | (uint)(value - Epoch.Year); }
public int Month { get => (int)((RawDate >> 12) & 0x3F) + 1; set => RawDate = (RawDate & 0xFFFC0FFF) | (((uint)(value - 1) & 0x3F) << 12); }
public int Day { get => (int)((RawDate >> 18) & 0x3F); set => RawDate = (RawDate & 0xFF03FFFF) | (((uint)value & 0x3F) << 18); }
public DateTime Timestamp
{
get => Epoch
.AddDays((Span[2] >> 2) - 1)
.AddMonths(((Span[2] & 0b0000_0011) << 2) | ((Span[1] & 0b1111_0000) >> 4))
.AddYears(((Span[1] & 0b0000_1111) << 8) | Span[0]);
set {
int day = value.Day;
int month = value.Month - Epoch.Month;
int year = value.Year - Epoch.Year;
Span[2] = (byte)(((day & 0b0011_1111) << 2) | ((month & 0b0011_0000) >> 4));
Span[1] = (byte)(((month & 0b0000_1111) << 4) | ((year & 0b1111_0000_00000) >> 8));
Span[0] = (byte)(year & 0b1111_1111);
get => new(Year, Month, Day);
set
{
Year = value.Year;
Month = value.Month;
Day = value.Day;
}
}
@ -38,7 +39,7 @@ public sealed class Epoch1900DateValue(Memory<byte> Data)
/// <summary>
/// time_t (seconds since 1900 Epoch rounded to days)
/// </summary>
public ulong Seconds
public ulong TotalSeconds
{
get => (ulong)(Timestamp - Epoch).TotalSeconds;
set => Timestamp = Epoch.AddSeconds(value);

View file

@ -18,7 +18,7 @@ public partial class SAV_Raid8 : Form
SAV = (SAV8SWSH)(Origin = sav).Clone();
Raids = raidOrigin switch
{
MaxRaidOrigin.Galar => SAV.Raid,
MaxRaidOrigin.Galar => SAV.RaidGalar,
MaxRaidOrigin.IsleOfArmor => SAV.RaidArmor,
MaxRaidOrigin.CrownTundra => SAV.RaidCrown,
_ => throw new ArgumentOutOfRangeException($"Raid Origin {raidOrigin} is not valid for Sword and Shield")