mirror of
https://github.com/kwsch/PKHeX
synced 2024-12-21 09:53:14 +00:00
b0da14b71e
Adds a basic editor for recorded Geonet/Unity Tower locations for the Gen 4/5 games, building on this [post by Danius88](https://projectpokemon.org/home/forums/topic/62055-bw-b2w2-unity-tower-geonet-and-passerby-research/). So far, I've implemented buttons that set all locations (including unused ones), set all legal locations, and clear all recorded locations; and checkboxes to toggle whether the whole globe is visible (used in Japanese games) and whether the ferry to Unity Tower is unlocked. Haven't implemented any UI for editing the status of individual locations, since I'm not sure how to lay it out. Also haven't implemented anything related to how the data of the other players in Unity Tower is stored.
582 lines
20 KiB
C#
582 lines
20 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using static System.Buffers.Binary.BinaryPrimitives;
|
|
|
|
namespace PKHeX.Core;
|
|
|
|
/// <summary>
|
|
/// Generation 4 abstract <see cref="SaveFile"/> object.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Storage data is stored in one contiguous block, and the remaining data is stored in another block.
|
|
/// </remarks>
|
|
public abstract class SAV4 : SaveFile, IEventFlag37
|
|
{
|
|
protected internal override string ShortSummary => $"{OT} ({Version}) - {PlayTimeString}";
|
|
public sealed override string Extension => ".sav";
|
|
|
|
// Blocks & Offsets
|
|
private const int PartitionSize = 0x40000;
|
|
|
|
// SaveData is chunked into two pieces.
|
|
private readonly Memory<byte> StorageBuffer;
|
|
protected internal readonly Memory<byte> GeneralBuffer;
|
|
protected Span<byte> Storage => StorageBuffer.Span;
|
|
public Span<byte> General => GeneralBuffer.Span;
|
|
protected sealed override Span<byte> BoxBuffer => Storage;
|
|
protected sealed override Span<byte> PartyBuffer => General;
|
|
|
|
public abstract Zukan4 Dex { get; }
|
|
|
|
protected abstract int EventFlag { get; }
|
|
protected abstract int EventWork { get; }
|
|
public sealed override bool GetFlag(int offset, int bitIndex) => FlagUtil.GetFlag(General, offset, bitIndex);
|
|
public sealed override void SetFlag(int offset, int bitIndex, bool value) => FlagUtil.SetFlag(General, offset, bitIndex, value);
|
|
|
|
protected SAV4(int gSize, int sSize)
|
|
{
|
|
GeneralBuffer = new byte[gSize];
|
|
StorageBuffer = new byte[sSize];
|
|
ClearBoxes();
|
|
}
|
|
|
|
protected SAV4(byte[] data, int gSize, int sSize, int sStart) : base(data)
|
|
{
|
|
var GeneralBlockPosition = GetActiveBlock(data, 0, gSize);
|
|
var StorageBlockPosition = GetActiveBlock(data, sStart, sSize);
|
|
|
|
var gbo = (GeneralBlockPosition == 0 ? 0 : PartitionSize);
|
|
var sbo = (StorageBlockPosition == 0 ? 0 : PartitionSize) + sStart;
|
|
GeneralBuffer = Data.AsMemory(gbo, gSize);
|
|
StorageBuffer = Data.AsMemory(sbo, sSize);
|
|
}
|
|
|
|
// Configuration
|
|
protected sealed override SAV4 CloneInternal()
|
|
{
|
|
var sav = CloneInternal4();
|
|
SetData(sav.General, General);
|
|
SetData(sav.Storage, Storage);
|
|
return sav;
|
|
}
|
|
|
|
protected abstract SAV4 CloneInternal4();
|
|
|
|
public sealed override void CopyChangesFrom(SaveFile sav)
|
|
{
|
|
SetData(sav.Data, 0);
|
|
var s4 = (SAV4)sav;
|
|
SetData(General, s4.General);
|
|
SetData(Storage, s4.Storage);
|
|
}
|
|
|
|
protected sealed override int SIZE_STORED => PokeCrypto.SIZE_4STORED;
|
|
protected sealed override int SIZE_PARTY => PokeCrypto.SIZE_4PARTY;
|
|
public sealed override PK4 BlankPKM => new();
|
|
public sealed override Type PKMType => typeof(PK4);
|
|
|
|
public sealed override int BoxCount => 18;
|
|
public sealed override int MaxEV => 255;
|
|
public sealed override int Generation => 4;
|
|
public override EntityContext Context => EntityContext.Gen4;
|
|
public int EventFlagCount => 0xB60; // 2912
|
|
public int EventWorkCount => (EventFlag - EventWork) >> 1;
|
|
protected sealed override int GiftCountMax => 11;
|
|
public sealed override int MaxStringLengthOT => 7;
|
|
public sealed override int MaxStringLengthNickname => 10;
|
|
public sealed override int MaxMoney => 999999;
|
|
public sealed override int MaxCoins => 50_000;
|
|
|
|
public sealed override ushort MaxMoveID => Legal.MaxMoveID_4;
|
|
public sealed override ushort MaxSpeciesID => Legal.MaxSpeciesID_4;
|
|
// MaxItemID
|
|
public sealed override int MaxAbilityID => Legal.MaxAbilityID_4;
|
|
public sealed override int MaxBallID => Legal.MaxBallID_4;
|
|
public sealed override int MaxGameID => Legal.MaxGameID_4; // Colo/XD
|
|
|
|
// Checksums
|
|
protected abstract int FooterSize { get; }
|
|
private ushort CalcBlockChecksum(ReadOnlySpan<byte> data) => Checksums.CRC16_CCITT(data[..^FooterSize]);
|
|
private static ushort GetBlockChecksumSaved(ReadOnlySpan<byte> data) => ReadUInt16LittleEndian(data[^2..]);
|
|
private bool GetBlockChecksumValid(ReadOnlySpan<byte> data) => CalcBlockChecksum(data) == GetBlockChecksumSaved(data);
|
|
|
|
protected sealed override void SetChecksums()
|
|
{
|
|
WriteUInt16LittleEndian(General[^2..], CalcBlockChecksum(General));
|
|
WriteUInt16LittleEndian(Storage[^2..], CalcBlockChecksum(Storage));
|
|
}
|
|
|
|
public sealed override bool ChecksumsValid
|
|
{
|
|
get
|
|
{
|
|
if (!GetBlockChecksumValid(General))
|
|
return false;
|
|
if (!GetBlockChecksumValid(Storage))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public sealed override string ChecksumInfo
|
|
{
|
|
get
|
|
{
|
|
var list = new List<string>();
|
|
if (!GetBlockChecksumValid(General))
|
|
list.Add("Small block checksum is invalid");
|
|
if (!GetBlockChecksumValid(Storage))
|
|
list.Add("Large block checksum is invalid");
|
|
|
|
return list.Count != 0 ? string.Join(Environment.NewLine, list) : "Checksums are valid.";
|
|
}
|
|
}
|
|
|
|
private static int GetActiveBlock(ReadOnlySpan<byte> data, int begin, int length)
|
|
{
|
|
int offset = begin + length - 0x14;
|
|
return SAV4BlockDetection.CompareFooters(data, offset, offset + PartitionSize);
|
|
}
|
|
|
|
protected int WondercardFlags = int.MinValue;
|
|
protected int AdventureInfo = int.MinValue;
|
|
protected int Seal = int.MinValue;
|
|
public int Geonet = int.MinValue;
|
|
protected int Trainer1;
|
|
public int GTS { get; protected set; } = int.MinValue;
|
|
|
|
// Storage
|
|
public override int PartyCount
|
|
{
|
|
get => General[Party - 4];
|
|
protected set => General[Party - 4] = (byte)value;
|
|
}
|
|
|
|
public sealed override int GetPartyOffset(int slot) => Party + (SIZE_PARTY * slot);
|
|
|
|
// Trainer Info
|
|
public override string OT
|
|
{
|
|
get => GetString(General.Slice(Trainer1, 16));
|
|
set => SetString(General.Slice(Trainer1, 16), value, MaxStringLengthOT, StringConverterOption.ClearZero);
|
|
}
|
|
|
|
public override uint ID32
|
|
{
|
|
get => ReadUInt32LittleEndian(General[(Trainer1 + 0x10)..]);
|
|
set => WriteUInt32LittleEndian(General[(Trainer1 + 0x10)..], value);
|
|
}
|
|
|
|
public override ushort TID16
|
|
{
|
|
get => ReadUInt16LittleEndian(General[(Trainer1 + 0x10)..]);
|
|
set => WriteUInt16LittleEndian(General[(Trainer1 + 0x10)..], value);
|
|
}
|
|
|
|
public override ushort SID16
|
|
{
|
|
get => ReadUInt16LittleEndian(General[(Trainer1 + 0x12)..]);
|
|
set => WriteUInt16LittleEndian(General[(Trainer1 + 0x12)..], value);
|
|
}
|
|
|
|
public override uint Money
|
|
{
|
|
get => ReadUInt32LittleEndian(General[(Trainer1 + 0x14)..]);
|
|
set => WriteUInt32LittleEndian(General[(Trainer1 + 0x14)..], value);
|
|
}
|
|
|
|
public override int Gender
|
|
{
|
|
get => General[Trainer1 + 0x18];
|
|
set => General[Trainer1 + 0x18] = (byte)value;
|
|
}
|
|
|
|
public override int Language
|
|
{
|
|
get => General[Trainer1 + 0x19];
|
|
set => General[Trainer1 + 0x19] = (byte)value;
|
|
}
|
|
|
|
public int Badges
|
|
{
|
|
get => General[Trainer1 + 0x1A];
|
|
set { if (value < 0) return; General[Trainer1 + 0x1A] = (byte)value; }
|
|
}
|
|
|
|
public int Sprite
|
|
{
|
|
get => General[Trainer1 + 0x1B];
|
|
set { if (value < 0) return; General[Trainer1 + 0x1B] = (byte)value; }
|
|
}
|
|
|
|
public uint Coin
|
|
{
|
|
get => ReadUInt16LittleEndian(General[(Trainer1 + 0x20)..]);
|
|
set => WriteUInt16LittleEndian(General[(Trainer1 + 0x20)..], (ushort)value);
|
|
}
|
|
|
|
public override int PlayedHours
|
|
{
|
|
get => ReadUInt16LittleEndian(General[(Trainer1 + 0x22)..]);
|
|
set => WriteUInt16LittleEndian(General[(Trainer1 + 0x22)..], (ushort)value);
|
|
}
|
|
|
|
public override int PlayedMinutes
|
|
{
|
|
get => General[Trainer1 + 0x24];
|
|
set => General[Trainer1 + 0x24] = (byte)value;
|
|
}
|
|
|
|
public override int PlayedSeconds
|
|
{
|
|
get => General[Trainer1 + 0x25];
|
|
set => General[Trainer1 + 0x25] = (byte)value;
|
|
}
|
|
|
|
public abstract int M { get; set; }
|
|
public abstract int X { get; set; }
|
|
public abstract int Y { get; set; }
|
|
|
|
public string Rival
|
|
{
|
|
get => GetString(Rival_Trash);
|
|
set => SetString(Rival_Trash, value, MaxStringLengthOT, StringConverterOption.ClearZero);
|
|
}
|
|
|
|
public abstract Span<byte> Rival_Trash { get; set; }
|
|
|
|
public abstract int X2 { get; set; }
|
|
public abstract int Y2 { get; set; }
|
|
public abstract int Z { get; set; }
|
|
|
|
public override uint SecondsToStart { get => ReadUInt32LittleEndian(General[(AdventureInfo + 0x34)..]); set => WriteUInt32LittleEndian(General[(AdventureInfo + 0x34)..], value); }
|
|
public override uint SecondsToFame { get => ReadUInt32LittleEndian(General[(AdventureInfo + 0x3C)..]); set => WriteUInt32LittleEndian(General[(AdventureInfo + 0x3C)..], value); }
|
|
|
|
public bool GeonetGlobalFlag { get => General[Geonet] != 0; set => General[Geonet] = (byte)(value ? 1 : 0); }
|
|
|
|
public int Country
|
|
{
|
|
get => General[Geonet + 1];
|
|
set { if (value < 0) return; General[Geonet + 1] = (byte)value; }
|
|
}
|
|
|
|
public int Region
|
|
{
|
|
get => General[Geonet + 2];
|
|
set { if (value < 0) return; General[Geonet + 2] = (byte)value; }
|
|
}
|
|
|
|
protected sealed override PK4 GetPKM(byte[] data) => new(data);
|
|
protected sealed override byte[] DecryptPKM(byte[] data) => PokeCrypto.DecryptArray45(data);
|
|
|
|
protected sealed override void SetPKM(PKM pk, bool isParty = false)
|
|
{
|
|
var pk4 = (PK4)pk;
|
|
// Apply to this Save File
|
|
DateTime Date = DateTime.Now;
|
|
if (pk4.Trade(OT, ID32, Gender, Date.Day, Date.Month, Date.Year))
|
|
pk.RefreshChecksum();
|
|
}
|
|
|
|
// Daycare
|
|
public override int GetDaycareSlotOffset(int loc, int slot) => DaycareOffset + (slot * SIZE_PARTY);
|
|
|
|
public override uint? GetDaycareEXP(int loc, int slot)
|
|
{
|
|
int ofs = DaycareOffset + ((slot+1)*SIZE_PARTY) - 4;
|
|
return ReadUInt32LittleEndian(General[ofs..]);
|
|
}
|
|
|
|
public override bool? IsDaycareOccupied(int loc, int slot) => null; // todo
|
|
|
|
public override void SetDaycareEXP(int loc, int slot, uint EXP)
|
|
{
|
|
int ofs = DaycareOffset + ((slot+1)*SIZE_PARTY) - 4;
|
|
WriteUInt32LittleEndian(General[ofs..], EXP);
|
|
}
|
|
|
|
public override void SetDaycareOccupied(int loc, int slot, bool occupied)
|
|
{
|
|
// todo
|
|
}
|
|
|
|
// Mystery Gift
|
|
private bool MysteryGiftActive { get => (General[72] & 1) == 1; set => General[72] = (byte)((General[72] & 0xFE) | (value ? 1 : 0)); }
|
|
|
|
private static bool IsMysteryGiftAvailable(DataMysteryGift[] value)
|
|
{
|
|
for (int i = 0; i < 8; i++) // 8 PGT
|
|
{
|
|
if (value[i] is PGT {CardType: not 0})
|
|
return true;
|
|
}
|
|
for (int i = 8; i < 11; i++) // 3 PCD
|
|
{
|
|
if (value[i] is PCD {Gift.CardType: not 0 })
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private bool MatchMysteryGifts(DataMysteryGift[] value, Span<byte> indexes)
|
|
{
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
if (value[i] is not PGT pgt)
|
|
continue;
|
|
|
|
if (pgt.CardType == 0) // empty
|
|
{
|
|
indexes[i] = pgt.Slot = 0;
|
|
continue;
|
|
}
|
|
|
|
indexes[i] = pgt.Slot = 3;
|
|
for (byte j = 0; j < 3; j++)
|
|
{
|
|
if (value[8 + j] is not PCD pcd)
|
|
continue;
|
|
|
|
// Check if data matches (except Slot @ 0x02)
|
|
if (!pcd.GiftEquals(pgt))
|
|
continue;
|
|
|
|
if (this is SAV4HGSS)
|
|
j++; // hgss 0,1,2; dppt 1,2,3
|
|
indexes[i] = pgt.Slot = j;
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public override MysteryGiftAlbum GiftAlbum
|
|
{
|
|
get => new(MysteryGiftCards, MysteryGiftReceivedFlags) {Flags = {[2047] = false}};
|
|
set
|
|
{
|
|
bool available = IsMysteryGiftAvailable(value.Gifts);
|
|
if (available && !MysteryGiftActive)
|
|
MysteryGiftActive = true;
|
|
value.Flags[2047] = available;
|
|
|
|
// Check encryption for each gift (decrypted wc4 sneaking in)
|
|
foreach (var g in value.Gifts)
|
|
{
|
|
if (g is PGT pgt)
|
|
{
|
|
pgt.VerifyPKEncryption();
|
|
}
|
|
else if (g is PCD pcd)
|
|
{
|
|
var dg = pcd.Gift;
|
|
if (dg.VerifyPKEncryption())
|
|
pcd.Gift = dg; // set encrypted gift back to PCD.
|
|
}
|
|
}
|
|
|
|
MysteryGiftReceivedFlags = value.Flags;
|
|
MysteryGiftCards = value.Gifts;
|
|
}
|
|
}
|
|
|
|
protected sealed override bool[] MysteryGiftReceivedFlags
|
|
{
|
|
get
|
|
{
|
|
bool[] result = new bool[GiftFlagMax];
|
|
for (int i = 0; i < result.Length; i++)
|
|
result[i] = ((General[WondercardFlags + (i >> 3)] >> (i & 7)) & 0x1) == 1;
|
|
return result;
|
|
}
|
|
set
|
|
{
|
|
if (GiftFlagMax != value.Length)
|
|
return;
|
|
|
|
Span<byte> data = General.Slice(WondercardFlags, value.Length / 8);
|
|
data.Clear();
|
|
for (int i = 0; i < value.Length; i++)
|
|
{
|
|
if (value[i])
|
|
data[i >> 3] |= (byte)(1 << (i & 7));
|
|
}
|
|
}
|
|
}
|
|
|
|
protected sealed override DataMysteryGift[] MysteryGiftCards
|
|
{
|
|
get
|
|
{
|
|
int pcd = this is SAV4HGSS ? 4 : 3;
|
|
DataMysteryGift[] cards = new DataMysteryGift[8 + pcd];
|
|
for (int i = 0; i < 8; i++) // 8 PGT
|
|
cards[i] = new PGT(General.Slice(WondercardData + (i * PGT.Size), PGT.Size).ToArray());
|
|
for (int i = 8; i < 11; i++) // 3 PCD
|
|
cards[i] = new PCD(General.Slice(WondercardData + (8 * PGT.Size) + ((i-8) * PCD.Size), PCD.Size).ToArray());
|
|
if (this is SAV4HGSS hgss)
|
|
cards[^1] = hgss.LockCapsuleSlot;
|
|
return cards;
|
|
}
|
|
set
|
|
{
|
|
Span<byte> indexes = stackalloc byte[8];
|
|
bool matchAny = MatchMysteryGifts(value, indexes); // automatically applied
|
|
if (!matchAny)
|
|
return;
|
|
|
|
for (int i = 0; i < 8; i++) // 8 PGT
|
|
{
|
|
if (value[i] is PGT)
|
|
{
|
|
var ofs = (WondercardData + (i * PGT.Size));
|
|
SetData(General[ofs..], value[i].Data);
|
|
}
|
|
}
|
|
for (int i = 8; i < 11; i++) // 3 PCD
|
|
{
|
|
if (value[i] is PCD)
|
|
{
|
|
var ofs = (WondercardData + (8 * PGT.Size) + ((i - 8) * PCD.Size));
|
|
SetData(General[ofs..], value[i].Data);
|
|
}
|
|
}
|
|
if (this is SAV4HGSS hgss && value.Length >= 11 && value[^1] is PCD capsule)
|
|
hgss.LockCapsuleSlot = capsule;
|
|
}
|
|
}
|
|
|
|
protected sealed override void SetDex(PKM pk) => Dex.SetDex(pk);
|
|
public sealed override bool GetCaught(ushort species) => Dex.GetCaught(species);
|
|
public sealed override bool GetSeen(ushort species) => Dex.GetSeen(species);
|
|
|
|
public int DexUpgraded
|
|
{
|
|
get
|
|
{
|
|
switch (Version)
|
|
{
|
|
case GameVersion.DP:
|
|
if (General[0x1413] != 0) return 4;
|
|
if (General[0x1415] != 0) return 3;
|
|
if (General[0x1404] != 0) return 2;
|
|
if (General[0x1414] != 0) return 1;
|
|
return 0;
|
|
case GameVersion.HGSS:
|
|
if (General[0x15ED] != 0) return 3;
|
|
if (General[0x15EF] != 0) return 2;
|
|
if (General[0x15EE] != 0 && (General[0x10D1] & 8) != 0) return 1;
|
|
return 0;
|
|
case GameVersion.Pt:
|
|
if (General[0x1641] != 0) return 4;
|
|
if (General[0x1643] != 0) return 3;
|
|
if (General[0x1640] != 0) return 2;
|
|
if (General[0x1642] != 0) return 1;
|
|
return 0;
|
|
default: return 0;
|
|
}
|
|
}
|
|
set
|
|
{
|
|
switch (Version)
|
|
{
|
|
case GameVersion.DP:
|
|
General[0x1413] = value == 4 ? (byte)1 : (byte)0;
|
|
General[0x1415] = value >= 3 ? (byte)1 : (byte)0;
|
|
General[0x1404] = value >= 2 ? (byte)1 : (byte)0;
|
|
General[0x1414] = value >= 1 ? (byte)1 : (byte)0;
|
|
break;
|
|
case GameVersion.HGSS:
|
|
General[0x15ED] = value == 3 ? (byte)1 : (byte)0;
|
|
General[0x15EF] = value >= 2 ? (byte)1 : (byte)0;
|
|
General[0x15EE] = value >= 1 ? (byte)1 : (byte)0;
|
|
General[0x10D1] = (byte)((General[0x10D1] & ~8) | (value >= 1 ? 8 : 0));
|
|
break;
|
|
case GameVersion.Pt:
|
|
General[0x1641] = value == 4 ? (byte)1 : (byte)0;
|
|
General[0x1643] = value >= 3 ? (byte)1 : (byte)0;
|
|
General[0x1640] = value >= 2 ? (byte)1 : (byte)0;
|
|
General[0x1642] = value >= 1 ? (byte)1 : (byte)0;
|
|
break;
|
|
default: return;
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed override string GetString(ReadOnlySpan<byte> data) => StringConverter4.GetString(data);
|
|
|
|
public sealed override int SetString(Span<byte> destBuffer, ReadOnlySpan<char> value, int maxLength, StringConverterOption option)
|
|
{
|
|
return StringConverter4.SetString(destBuffer, value, maxLength, option);
|
|
}
|
|
|
|
#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 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(General[(EventWork + (index * 2))..]);
|
|
public void SetWork(int index, ushort value) => WriteUInt16LittleEndian(General[EventWork..][(index * 2)..], value);
|
|
#endregion
|
|
|
|
// Seals
|
|
private const byte SealMaxCount = 99;
|
|
|
|
public Span<byte> GetSealCase() => General.Slice(Seal, (int)Seal4.MAX);
|
|
public void SetSealCase(ReadOnlySpan<byte> value) => SetData(General.Slice(Seal, (int)Seal4.MAX), value);
|
|
|
|
public byte GetSealCount(Seal4 id) => General[Seal + (int)id];
|
|
public byte SetSealCount(Seal4 id, byte count) => General[Seal + (int)id] = Math.Min(SealMaxCount, count);
|
|
|
|
public void SetAllSeals(byte count, bool unreleased = false)
|
|
{
|
|
var sealIndexCount = (int)(unreleased ? Seal4.MAX : Seal4.MAXLEGAL);
|
|
var clamped = Math.Min(count, SealMaxCount);
|
|
for (int i = 0; i < sealIndexCount; i++)
|
|
General[Seal + i] = clamped;
|
|
}
|
|
|
|
public int GetMailOffset(int index)
|
|
{
|
|
int ofs = (index * Mail4.SIZE);
|
|
return Version switch
|
|
{
|
|
GameVersion.DP => (ofs + 0x4BEC),
|
|
GameVersion.Pt => (ofs + 0x4E80),
|
|
_ => (ofs + 0x3FA8),
|
|
};
|
|
}
|
|
|
|
public byte[] GetMailData(int ofs) => General.Slice(ofs, Mail4.SIZE).ToArray();
|
|
|
|
public Mail4 GetMail(int mailIndex)
|
|
{
|
|
int ofs = GetMailOffset(mailIndex);
|
|
return new Mail4(GetMailData(ofs), ofs);
|
|
}
|
|
|
|
public abstract uint SwarmSeed { get; set; }
|
|
public abstract uint SwarmMaxCountModulo { get; }
|
|
|
|
public uint SwarmIndex
|
|
{
|
|
get => SwarmSeed % SwarmMaxCountModulo;
|
|
set
|
|
{
|
|
value %= SwarmMaxCountModulo;
|
|
while (SwarmIndex != value)
|
|
++SwarmSeed;
|
|
}
|
|
}
|
|
}
|