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> /// <summary>
/// Calculates differences in the Event Blocks between two <see cref="SaveFile"/>. /// Calculates differences in the Event Blocks between two <see cref="SaveFile"/>.
/// </summary> /// </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> SetFlags { get; } = [];
public List<int> ClearedFlags { 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 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) public EventBlockDiff(string f1, string f2)
{ {
@ -37,16 +39,16 @@ public sealed class EventBlockDiff<T, T2> : IEventWorkDiff where T : class, IEve
Diff(t1, t2); 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; return t1;
if (s1 is IEventFlagProvider37 p1) if (s1 is IEventFlagProvider37 p1)
return p1.EventWork as T; return p1.EventWork as TSave;
return null; return null;
} }
private static EventWorkDiffCompatibility SanityCheckSaveInfo(T s1, T s2) private static EventWorkDiffCompatibility SanityCheckSaveInfo(TSave s1, TSave s2)
{ {
if (s1.GetType() != s2.GetType()) if (s1.GetType() != s2.GetType())
return DifferentGameGroup; return DifferentGameGroup;
@ -57,7 +59,7 @@ public sealed class EventBlockDiff<T, T2> : IEventWorkDiff where T : class, IEve
return Valid; return Valid;
} }
private void Diff(T s1, T s2) private void Diff(TSave s1, TSave s2)
{ {
Message = SanityCheckSaveInfo(s1, s2); Message = SanityCheckSaveInfo(s1, s2);
if (Message != Valid) if (Message != Valid)

View file

@ -13,5 +13,5 @@ public interface ISaveBlock8LA
MyItem8a Items { get; } MyItem8a Items { get; }
Epoch1970Value AdventureStart { get; } Epoch1970Value AdventureStart { get; }
Epoch1900DateTimeValue LastSaved { 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 Coordinates8a Coordinates { get; } = new(sav, Block(sav, KCoordinates));
public Epoch1900DateTimeValue LastSaved { get; } = new(Block(sav, KLastSaved)); public Epoch1900DateTimeValue LastSaved { get; } = new(Block(sav, KLastSaved));
public PlayerFashion8a FashionPlayer { get; } = new(sav, Block(sav, KFashionPlayer)); 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; 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 string OT { get => MyStatus.OT; set => MyStatus.OT = value; }
public override uint Money { get => MyStatus.Money; set => MyStatus.Money = 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 PlayedHours { get => Played.PlayedHours; set => Played.PlayedHours = value; }
public override int PlayedMinutes { get => Played.PlayedMinutes; set => Played.PlayedMinutes = (byte)value; } public override int PlayedMinutes { get => Played.PlayedMinutes; set => Played.PlayedMinutes = value; }
public override int PlayedSeconds { get => Played.PlayedSeconds; set => Played.PlayedSeconds = (byte)value; } public override int PlayedSeconds { get => Played.PlayedSeconds; set => Played.PlayedSeconds = value; }
// Inventory // Inventory
public override IReadOnlyList<InventoryPouch> Inventory { get => Items.Inventory; set => Items.Inventory = value; } 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 Epoch1970Value AdventureStart => Blocks.AdventureStart;
public Coordinates8a Coordinates => Blocks.Coordinates; public Coordinates8a Coordinates => Blocks.Coordinates;
public Epoch1900DateTimeValue LastSaved => Blocks.LastSaved; public Epoch1900DateTimeValue LastSaved => Blocks.LastSaved;
public PlayTime8a Played => Blocks.Played; public PlayTime8b Played => Blocks.Played;
public AreaSpawnerSet8a AreaSpawners => new(Blocks.GetBlock(SaveBlockAccessor8LA.KSpawners)); public AreaSpawnerSet8a AreaSpawners => new(Blocks.GetBlock(SaveBlockAccessor8LA.KSpawners));
#endregion #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 uint Money { get => (uint)Blocks.GetBlockValue(SaveBlockAccessor8LA.KMoney); set => Blocks.SetBlockValue(SaveBlockAccessor8LA.KMoney, value); }
public override int MaxMoney => 9_999_999; public override int MaxMoney => 9_999_999;
public override int PlayedHours { get => Played.PlayedHours; set => Played.PlayedHours = (ushort)value; } public override int PlayedHours { get => Played.PlayedHours; set => Played.PlayedHours = value; }
public override int PlayedMinutes { get => Played.PlayedMinutes; set => Played.PlayedMinutes = (byte)value; } public override int PlayedMinutes { get => Played.PlayedMinutes; set => Played.PlayedMinutes = value; }
public override int PlayedSeconds { get => Played.PlayedSeconds; set => Played.PlayedSeconds = (byte)value; } public override int PlayedSeconds { get => Played.PlayedSeconds; set => Played.PlayedSeconds = value; }
protected override Span<byte> BoxBuffer => BoxInfo.Data; protected override Span<byte> BoxBuffer => BoxInfo.Data;
protected override Span<byte> PartyBuffer => PartyInfo.Data; protected override Span<byte> PartyBuffer => PartyInfo.Data;

View file

@ -3,11 +3,12 @@ using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core; 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 public int PlayedHours
{ {
get => ReadUInt16LittleEndian(Data); get => ReadUInt16LittleEndian(Data);
@ -26,39 +27,53 @@ public sealed class PlayTime6 : SaveBlock<SaveFile>
set => Data[3] = (byte)value; set => Data[3] = (byte)value;
} }
private uint LastSaved { get => ReadUInt32LittleEndian(Data[0x4..]); set => WriteUInt32LittleEndian(Data[0x4..], value); } public string PlayedTime => $"{PlayedHours:0000}ː{PlayedMinutes:00}ː{PlayedSeconds:00}"; // not :
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); } /// <summary>
private int LastSavedHour { get => (int)((LastSaved >> 21) & 0x1F); set => LastSaved = (LastSaved & 0xFC1FFFFF) | (((uint)value & 0x1F) << 21); } /// Object storing the playtime of a save file as well as the last saved date.
private int LastSavedMinute { get => (int)((LastSaved >> 26) & 0x3F); set => LastSaved = (LastSaved & 0x03FFFFFF) | (((uint)value & 0x3F) << 26); } /// </summary>
public string LastSavedTime => $"{LastSavedYear:0000}-{LastSavedMonth:00}-{LastSavedDay:00} {LastSavedHour:00}ː{LastSavedMinute:00}"; // not : /// <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 public DateTime? LastSavedDate
{ {
get => !DateUtil.IsDateValid(LastSavedYear, LastSavedMonth, LastSavedDay) get => !DateUtil.IsDateValid(LastSaved.Year, LastSaved.Month, LastSaved.Day)
? null ? null
: new DateTime(LastSavedYear, LastSavedMonth, LastSavedDay, LastSavedHour, LastSavedMinute, 0); : LastSaved.Timestamp;
set set
{ {
// Only update the properties if a value is provided. // Only update the properties if a value is provided.
if (value is { } dt) if (value is { } dt)
{ {
LastSavedYear = dt.Year; LastSaved.Timestamp = dt;
LastSavedMonth = dt.Month;
LastSavedDay = dt.Day;
LastSavedHour = dt.Hour;
LastSavedMinute = dt.Minute;
} }
else // Clear the date. else // Clear the date.
{ {
// If code tries to access MetDate again, null will be returned. // If code tries to access MetDate again, null will be returned.
LastSavedYear = 0; LastSaved.Year = 0;
LastSavedMonth = 0; LastSaved.Month = 0;
LastSavedDay = 0; LastSaved.Day = 0;
LastSavedHour = 0; LastSaved.Hour = 0;
LastSavedMinute = 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 System;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core; 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(SAV7b sav, Memory<byte> raw) : base(sav, raw) { }
public PlayTime7b(SAV8SWSH sav, SCBlock block) : base(sav, block.Data) { } public PlayTime7b(SAV8SWSH sav, SCBlock block) : base(sav, block.Data) { }
public int PlayedHours protected override Epoch1900DateTimeValue LastSaved => new(Raw.Slice(0x4, 4));
{
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;
}
}
}
} }

View file

@ -1,22 +1,14 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core; namespace PKHeX.Core;
/// <summary> /// <summary>
/// Playtime storage /// PlayTime object without a Last Saved timestamp.
/// </summary> /// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))] [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 public PlayTime8b(SAV8BS sav, Memory<byte> raw) : base(sav, raw) { }
{ public PlayTime8b(SAV8LA sav, SCBlock block) : base(sav, block.Data) { }
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 :
} }

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; using static System.Buffers.Binary.BinaryPrimitives;
namespace PKHeX.Core; 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. /// Stores the <see cref="Timestamp"/> to indicate the seconds since 1900 that an event occurred.
/// </summary> /// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))] [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 // 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 // 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) { } public Epoch1900DateTimeValue(SCBlock block) : this(block.Data) { }
private static DateTime Epoch => new(1900, 1, 1); private static DateTime Epoch => new(1900, 1, 1);
private uint RawDate { get => ReadUInt32LittleEndian(Span); set => WriteUInt32LittleEndian(Span, value); } public override int Year { get => (int)(RawDate & 0xFFF) + Epoch.Year; set => RawDate = (RawDate & 0xFFFFF000) | (uint)(value - Epoch.Year); }
public 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 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 bool HasSeconds => Span.Length > 4;
public int Second { public int Second {
get => HasSeconds ? Span[4] : 0; 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); get => new(Year, Month, Day, Hour, Minute, Second);
set 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> /// <summary>
/// time_t (seconds since 1900 Epoch) /// time_t (seconds since 1900 Epoch)
/// </summary> /// </summary>
public ulong TotalSeconds public override ulong TotalSeconds
{ {
get => (ulong)(Timestamp - Epoch).TotalSeconds; get => (ulong)(Timestamp - Epoch).TotalSeconds;
set => Timestamp = Epoch.AddSeconds(value); 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; }
}