Deduplicate Gen 6-8 PlayTime Logic (#4208)

Slightly overengineered but a fun experiment to de-duplicate some logic.
This commit is contained in:
Jonathan Herbert 2024-03-10 10:50:32 -04:00 committed by GitHub
parent 25c7b3cc8c
commit c651c6f6cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 129 additions and 131 deletions

View file

@ -9,7 +9,9 @@ namespace PKHeX.Core;
/// <summary>
/// Calculates differences in the Event Blocks between two <see cref="SaveFile"/>.
/// </summary>
public sealed class EventBlockDiff<T, T2> : IEventWorkDiff where T : class, IEventFlagArray, IEventWorkArray<T2> where T2 : unmanaged, IEquatable<T2>
public sealed class EventBlockDiff<TSave, TWorkValue> : IEventWorkDiff
where TSave : class, IEventFlagArray, IEventWorkArray<TWorkValue>
where TWorkValue : unmanaged, IEquatable<TWorkValue>
{
public List<int> SetFlags { get; } = [];
public List<int> ClearedFlags { get; } = [];
@ -19,7 +21,7 @@ public sealed class EventBlockDiff<T, T2> : IEventWorkDiff where T : class, IEve
private const int MAX_SAVEFILE_SIZE = 0x10_0000; // 1 MB
public EventBlockDiff(T s1, T s2) => Diff(s1, s2);
public EventBlockDiff(TSave s1, TSave s2) => Diff(s1, s2);
public EventBlockDiff(string f1, string f2)
{
@ -37,16 +39,16 @@ public sealed class EventBlockDiff<T, T2> : IEventWorkDiff where T : class, IEve
Diff(t1, t2);
}
private static T? GetBlock(SaveFile s1)
private static TSave? GetBlock(SaveFile s1)
{
if (s1 is T t1)
if (s1 is TSave t1)
return t1;
if (s1 is IEventFlagProvider37 p1)
return p1.EventWork as T;
return p1.EventWork as TSave;
return null;
}
private static EventWorkDiffCompatibility SanityCheckSaveInfo(T s1, T s2)
private static EventWorkDiffCompatibility SanityCheckSaveInfo(TSave s1, TSave s2)
{
if (s1.GetType() != s2.GetType())
return DifferentGameGroup;
@ -57,7 +59,7 @@ public sealed class EventBlockDiff<T, T2> : IEventWorkDiff where T : class, IEve
return Valid;
}
private void Diff(T s1, T s2)
private void Diff(TSave s1, TSave s2)
{
Message = SanityCheckSaveInfo(s1, s2);
if (Message != Valid)

View file

@ -13,5 +13,5 @@ public interface ISaveBlock8LA
MyItem8a Items { get; }
Epoch1970Value AdventureStart { get; }
Epoch1900DateTimeValue LastSaved { get; }
PlayTime8a Played { get; }
PlayTime8b Played { get; }
}

View file

@ -17,7 +17,7 @@ public sealed class SaveBlockAccessor8LA(SAV8LA sav) : SCBlockAccessor, ISaveBlo
public Coordinates8a Coordinates { get; } = new(sav, Block(sav, KCoordinates));
public Epoch1900DateTimeValue LastSaved { get; } = new(Block(sav, KLastSaved));
public PlayerFashion8a FashionPlayer { get; } = new(sav, Block(sav, KFashionPlayer));
public PlayTime8a Played { get; } = new(sav, Block(sav, KPlayTime));
public PlayTime8b Played { get; } = new(sav, Block(sav, KPlayTime));
public int DetectRevision() => HasBlock(0x8184EFB4) ? 1 : 0;

View file

@ -261,9 +261,9 @@ public sealed class SAV8BS : SaveFile, ISaveFileRevision, ITrainerStatRecord, IE
public override string OT { get => MyStatus.OT; set => MyStatus.OT = value; }
public override uint Money { get => MyStatus.Money; set => MyStatus.Money = value; }
public override int PlayedHours { get => Played.PlayedHours; set => Played.PlayedHours = (ushort)value; }
public override int PlayedMinutes { get => Played.PlayedMinutes; set => Played.PlayedMinutes = (byte)value; }
public override int PlayedSeconds { get => Played.PlayedSeconds; set => Played.PlayedSeconds = (byte)value; }
public override int PlayedHours { get => Played.PlayedHours; set => Played.PlayedHours = value; }
public override int PlayedMinutes { get => Played.PlayedMinutes; set => Played.PlayedMinutes = value; }
public override int PlayedSeconds { get => Played.PlayedSeconds; set => Played.PlayedSeconds = value; }
// Inventory
public override IReadOnlyList<InventoryPouch> Inventory { get => Items.Inventory; set => Items.Inventory = value; }

View file

@ -115,7 +115,7 @@ public sealed class SAV8LA : SaveFile, ISaveBlock8LA, ISCBlockArray, ISaveFileRe
public Epoch1970Value AdventureStart => Blocks.AdventureStart;
public Coordinates8a Coordinates => Blocks.Coordinates;
public Epoch1900DateTimeValue LastSaved => Blocks.LastSaved;
public PlayTime8a Played => Blocks.Played;
public PlayTime8b Played => Blocks.Played;
public AreaSpawnerSet8a AreaSpawners => new(Blocks.GetBlock(SaveBlockAccessor8LA.KSpawners));
#endregion
@ -123,9 +123,9 @@ public sealed class SAV8LA : SaveFile, ISaveBlock8LA, ISCBlockArray, ISaveFileRe
public override uint Money { get => (uint)Blocks.GetBlockValue(SaveBlockAccessor8LA.KMoney); set => Blocks.SetBlockValue(SaveBlockAccessor8LA.KMoney, value); }
public override int MaxMoney => 9_999_999;
public override int PlayedHours { get => Played.PlayedHours; set => Played.PlayedHours = (ushort)value; }
public override int PlayedMinutes { get => Played.PlayedMinutes; set => Played.PlayedMinutes = (byte)value; }
public override int PlayedSeconds { get => Played.PlayedSeconds; set => Played.PlayedSeconds = (byte)value; }
public override int PlayedHours { get => Played.PlayedHours; set => Played.PlayedHours = value; }
public override int PlayedMinutes { get => Played.PlayedMinutes; set => Played.PlayedMinutes = value; }
public override int PlayedSeconds { get => Played.PlayedSeconds; set => Played.PlayedSeconds = value; }
protected override Span<byte> BoxBuffer => BoxInfo.Data;
protected override Span<byte> PartyBuffer => PartyInfo.Data;

View file

@ -3,11 +3,12 @@ using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public sealed class PlayTime6 : SaveBlock<SaveFile>
/// <summary>
/// Simple 4-byte block storing time played in a save file.
/// </summary>
public abstract class PlayTime<TSave>(TSave sav, Memory<byte> raw) : SaveBlock<TSave>(sav, raw)
where TSave : SaveFile
{
public PlayTime6(SAV6 sav, Memory<byte> raw) : base(sav, raw) { }
public PlayTime6(SAV7 sav, Memory<byte> raw) : base(sav, raw) { }
public int PlayedHours
{
get => ReadUInt16LittleEndian(Data);
@ -26,39 +27,53 @@ public sealed class PlayTime6 : SaveBlock<SaveFile>
set => Data[3] = (byte)value;
}
private uint LastSaved { get => ReadUInt32LittleEndian(Data[0x4..]); set => WriteUInt32LittleEndian(Data[0x4..], value); }
private int LastSavedYear { get => (int)(LastSaved & 0xFFF); set => LastSaved = (LastSaved & 0xFFFFF000) | (uint)value; }
private int LastSavedMonth { get => (int)((LastSaved >> 12) & 0xF); set => LastSaved = (LastSaved & 0xFFFF0FFF) | (((uint)value & 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 :
public string PlayedTime => $"{PlayedHours:0000}ː{PlayedMinutes:00}ː{PlayedSeconds:00}"; // not :
}
/// <summary>
/// Object storing the playtime of a save file as well as the last saved date.
/// </summary>
/// <typeparam name="TSave">Type of Save File</typeparam>
/// <typeparam name="TEpoch">Type of Epoch for the <see cref="LastSaved"/> timestamp.</typeparam>
public abstract class PlayTimeLastSaved<TSave, TEpoch>(TSave sav, Memory<byte> raw) : PlayTime<TSave>(sav, raw)
where TSave : SaveFile
where TEpoch : EpochDateTime
{
protected abstract TEpoch LastSaved { get; }
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);
: LastSaved.Timestamp;
set
{
// Only update the properties if a value is provided.
if (value is { } dt)
{
LastSavedYear = dt.Year;
LastSavedMonth = dt.Month;
LastSavedDay = dt.Day;
LastSavedHour = dt.Hour;
LastSavedMinute = dt.Minute;
LastSaved.Timestamp = dt;
}
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;
}
}
}
}
/// <summary>
/// PlayTime object with a zero-epoch Last Saved timestamp.
/// </summary>
public sealed class PlayTime6 : PlayTimeLastSaved<SaveFile, Epoch0000DateTime>
{
public PlayTime6(SAV6 sav, Memory<byte> raw) : base(sav, raw) { }
public PlayTime6(SAV7 sav, Memory<byte> raw) : base(sav, raw) { }
protected override Epoch0000DateTime LastSaved => new(Raw.Slice(0x4, 4));
}

View file

@ -1,55 +1,14 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public sealed class PlayTime7b : SaveBlock<SaveFile>
/// <summary>
/// PlayTime object with a 1900-epoch Last Saved timestamp.
/// </summary>
public sealed class PlayTime7b : PlayTimeLastSaved<SaveFile, Epoch1900DateTimeValue>
{
public PlayTime7b(SAV7b sav, Memory<byte> raw) : base(sav, raw) { }
public PlayTime7b(SAV8SWSH sav, SCBlock block) : base(sav, block.Data) { }
public int PlayedHours
{
get => ReadUInt16LittleEndian(Data);
set => WriteUInt16LittleEndian(Data, (ushort)value);
}
public int PlayedMinutes
{
get => Data[2];
set => Data[2] = (byte)value;
}
public int PlayedSeconds
{
get => Data[3];
set => Data[3] = (byte)value;
}
private Epoch1900DateTimeValue LastSaved => new(Raw.Slice(0x4, 4));
public string LastSavedTime => LastSaved.DisplayValue;
public DateTime? LastSavedDate
{
get => !DateUtil.IsDateValid(LastSaved.Year, LastSaved.Month, LastSaved.Day)
? null
: LastSaved.Timestamp;
set
{
// Only update the properties if a value is provided.
if (value is { } dt)
{
LastSaved.Timestamp = dt;
}
else // Clear the date.
{
// If code tries to access MetDate again, null will be returned.
LastSaved.Year = 0;
LastSaved.Month = 0;
LastSaved.Day = 0;
LastSaved.Hour = 0;
LastSaved.Minute = 0;
}
}
}
protected override Epoch1900DateTimeValue LastSaved => new(Raw.Slice(0x4, 4));
}

View file

@ -1,22 +1,14 @@
using System;
using System.ComponentModel;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
/// <summary>
/// Playtime storage
/// PlayTime object without a Last Saved timestamp.
/// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed class PlayTime8b(SAV8BS sav, Memory<byte> raw) : SaveBlock<SAV8BS>(sav, raw)
public sealed class PlayTime8b : PlayTime<SaveFile>
{
public ushort PlayedHours
{
get => ReadUInt16LittleEndian(Data);
set => WriteUInt16LittleEndian(Data, value);
}
public byte PlayedMinutes { get => Data[2]; set => Data[2] = value; }
public byte PlayedSeconds { get => Data[3]; set => Data[3] = value; }
public string LastSavedTime => $"{PlayedHours:0000}ː{PlayedMinutes:00}ː{PlayedSeconds:00}"; // not :
public PlayTime8b(SAV8BS sav, Memory<byte> raw) : base(sav, raw) { }
public PlayTime8b(SAV8LA sav, SCBlock block) : base(sav, block.Data) { }
}

View file

@ -1,22 +0,0 @@
using System;
using System.ComponentModel;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
/// <summary>
/// Stores the amount of time the save file has been played.
/// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed class PlayTime8a(SAV8LA sav, SCBlock block) : SaveBlock<SAV8LA>(sav, block.Data)
{
public ushort PlayedHours
{
get => ReadUInt16LittleEndian(Data);
set => WriteUInt16LittleEndian(Data, value);
}
public byte PlayedMinutes { get => Data[2]; set => Data[2] = value; }
public byte PlayedSeconds { get => Data[3]; set => Data[3] = value; }
public string PlayedTime => $"{PlayedHours:0000}ː{PlayedMinutes:00}ː{PlayedSeconds:00}"; // not :
}

View file

@ -1,4 +1,3 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;

View file

@ -0,0 +1,40 @@
using System;
using System.ComponentModel;
namespace PKHeX.Core;
[TypeConverter(typeof(ExpandableObjectConverter))]
public sealed class Epoch0000DateTime(Memory<byte> Data): EpochDateTime(Data)
{
// Data should be 4 or 8 bytes where we only care about the first 4 bytes i.e. 32 bits
// First 12 bits are year from 0000, next 4 bits are month, next 5 are days, next 5 are hours, next 6 bits are minutes
private static DateTime Epoch => new(0, 1, 1);
public override int Year { get => (int)(RawDate & 0xFFF); set => RawDate = (RawDate & 0xFFFFF000) | (uint)(value); }
public override int Month { get => (int)((RawDate >> 12) & 0xF); set => RawDate = (RawDate & 0xFFFF0FFF) | (((uint)value & 0xF) << 12); }
public override DateTime Timestamp
{
get => new(Year, Month, Day, Hour, Minute, 0);
set
{
Year = value.Year;
Month = value.Month;
Day = value.Day;
Hour = value.Hour;
Minute = value.Minute;
}
}
public override string DisplayValue => $"{Timestamp.Year:0000}-{Timestamp.Month:00}-{Timestamp.Day:00} {Timestamp.Hour:00}ː{Timestamp.Minute:00}ː{Timestamp.Second:00}"; // not :
/// <summary>
/// time_t
/// </summary>
public override ulong TotalSeconds
{
get => (ulong)(Timestamp - Epoch).TotalSeconds;
set => Timestamp = Epoch.AddSeconds(value);
}
}

View file

@ -8,22 +8,17 @@ namespace PKHeX.Core;
/// 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)
public sealed class Epoch1900DateTimeValue(Memory<byte> Data) : EpochDateTime(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 override int Year { get => (int)(RawDate & 0xFFF) + Epoch.Year; set => RawDate = (RawDate & 0xFFFFF000) | (uint)(value - Epoch.Year); }
public override int Month { get => (int)((RawDate >> 12) & 0xF) + 1; set => RawDate = (RawDate & 0xFFFF0FFF) | (((uint)(value - 1) & 0xF) << 12); }
public bool HasSeconds => Span.Length > 4;
public int Second {
get => HasSeconds ? Span[4] : 0;
@ -32,7 +27,7 @@ public sealed class Epoch1900DateTimeValue(Memory<byte> Data)
}
}
public DateTime Timestamp
public override DateTime Timestamp
{
get => new(Year, Month, Day, Hour, Minute, Second);
set
@ -46,12 +41,12 @@ public sealed class Epoch1900DateTimeValue(Memory<byte> Data)
}
}
public string DisplayValue => $"{Timestamp.Year:0000}-{Timestamp.Month:00}-{Timestamp.Day:00} {Timestamp.Hour:00}ː{Timestamp.Minute:00}" + (HasSeconds ? $"ː{Timestamp.Second:00}" : ""); // not :
public override string DisplayValue => $"{Timestamp.Year:0000}-{Timestamp.Month:00}-{Timestamp.Day:00} {Timestamp.Hour:00}ː{Timestamp.Minute:00}" + (HasSeconds ? $"ː{Timestamp.Second:00}" : ""); // not :
/// <summary>
/// time_t (seconds since 1900 Epoch)
/// </summary>
public ulong TotalSeconds
public override ulong TotalSeconds
{
get => (ulong)(Timestamp - Epoch).TotalSeconds;
set => Timestamp = Epoch.AddSeconds(value);

View file

@ -0,0 +1,18 @@
using System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core;
public abstract class EpochDateTime(Memory<byte> Data)
{
protected Span<byte> Span => Data.Span;
protected uint RawDate { get => ReadUInt32LittleEndian(Span); set => WriteUInt32LittleEndian(Span, value); }
public abstract int Year { get; set;}
public abstract int Month { get; set;}
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 abstract DateTime Timestamp { get; set; }
public abstract string DisplayValue { get; }
public abstract ulong TotalSeconds { get; set; }
}