mirror of
https://github.com/kwsch/NHSE.git
synced 2026-03-22 01:34:51 -05:00
Previous fix for bad map design imports had an incorrect range and no way to manage v2.0.8- and v3.0.0+ dumps. Import/Export for map design patterns now supports both map sizes (old and new). It will accept 2.0.8 dumps and pad them to work on 3.0.0 as well as trim previously mangled ones, or 3.0.0 dumps down to fit on 2.0.8 saves. Byte padding methods added to ArrayUtil for this padding and for future map flag import padding fixes.
309 lines
13 KiB
C#
309 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using static System.Buffers.Binary.BinaryPrimitives;
|
|
|
|
namespace NHSE.Core;
|
|
|
|
/// <summary>
|
|
/// main.dat
|
|
/// </summary>
|
|
public sealed class MainSave : EncryptedFilePair
|
|
{
|
|
public readonly MainSaveOffsets Offsets;
|
|
public MainSave(ISaveFileProvider provider) : base(provider, "main") => Offsets = MainSaveOffsets.GetOffsets(Info);
|
|
|
|
public Hemisphere Hemisphere { get => (Hemisphere)Data[Offsets.WeatherArea]; set => Data[Offsets.WeatherArea] = (byte)value; }
|
|
public Hemisphere TourHemisphere { get => (Hemisphere)Data[Offsets.TourHemisphere]; set => Data[Offsets.TourHemisphere] = (byte)value; }
|
|
public AirportColor AirportThemeColor { get => (AirportColor)Data[Offsets.AirportThemeColor]; set => Data[Offsets.AirportThemeColor] = (byte)value; }
|
|
public uint WeatherSeed
|
|
{
|
|
get => ReadUInt32LittleEndian(Data[Offsets.WeatherRandSeed..]);
|
|
set => WriteUInt32LittleEndian(Data[Offsets.WeatherRandSeed..], value);
|
|
}
|
|
public uint TourWeatherSeed
|
|
{
|
|
get => ReadUInt32LittleEndian(Data[Offsets.TourWeatherRandSeed..]);
|
|
set => WriteUInt32LittleEndian(Data[Offsets.TourWeatherRandSeed..], value);
|
|
}
|
|
|
|
public byte CampsiteStatus { get => Data[Offsets.GSaveCampSite]; set => Data[Offsets.GSaveCampSite] = value; }
|
|
public byte CampsiteVillagerVariantID { get => Data[Offsets.GSaveNpcCamp]; set => Data[Offsets.GSaveNpcCamp] = value; }
|
|
public byte CampsiteVillagerSpeciesID { get => Data[Offsets.GSaveNpcCamp+1]; set => Data[Offsets.GSaveNpcCamp+1] = value; }
|
|
public GSaveDate CampTimestamp
|
|
{
|
|
get => Data.ToStructure<GSaveDate>(Offsets.CampLastVisitTime, GSaveDate.SIZE);
|
|
set => value.ToBytes().CopyTo(Data[Offsets.CampLastVisitTime..]);
|
|
}
|
|
|
|
public IVillager GetVillager(int index) => Offsets.ReadVillager(Data, index);
|
|
public void SetVillager(IVillager value, int index) => Offsets.WriteVillager(value, Data, index);
|
|
|
|
public IVillagerHouse GetVillagerHouse(int index) => Offsets.ReadVillagerHouse(Data, index);
|
|
public void SetVillagerHouse(IVillagerHouse value, int index) => Offsets.WriteVillagerHouse(value, Data, index);
|
|
|
|
public IVillager[] GetVillagers()
|
|
{
|
|
var villagers = new IVillager[MainSaveOffsets.VillagerCount];
|
|
for (int i = 0; i < villagers.Length; i++)
|
|
villagers[i] = GetVillager(i);
|
|
return villagers;
|
|
}
|
|
|
|
public void SetVillagers(IReadOnlyList<IVillager> villagers)
|
|
{
|
|
for (int i = 0; i < villagers.Count; i++)
|
|
SetVillager(villagers[i], i);
|
|
}
|
|
|
|
public IVillagerHouse[] GetVillagerHouses()
|
|
{
|
|
var villagers = new IVillagerHouse[MainSaveOffsets.VillagerCount];
|
|
for (int i = 0; i < villagers.Length; i++)
|
|
villagers[i] = GetVillagerHouse(i);
|
|
return villagers;
|
|
}
|
|
|
|
public void SetVillagerHouses(IReadOnlyList<IVillagerHouse> villagers)
|
|
{
|
|
for (int i = 0; i < villagers.Count; i++)
|
|
SetVillagerHouse(villagers[i], i);
|
|
}
|
|
|
|
public DesignPattern GetDesign(int index) => Offsets.ReadPattern(Data, index);
|
|
public void SetDesign(DesignPattern value, int index) => Offsets.WritePattern(value, Data, index);
|
|
public DesignPatternPRO GetDesignPRO(int index) => Offsets.ReadPatternPRO(Data, index);
|
|
public void SetDesignPRO(DesignPatternPRO value, int index) => Offsets.WritePatternPRO(value, Data, index);
|
|
|
|
public IReadOnlyList<Item> RecycleBin
|
|
{
|
|
get => Item.GetArray(Data.Slice(Offsets.LostItemBox, MainSaveOffsets.RecycleBinCount * Item.SIZE));
|
|
set => Item.SetArray(value).CopyTo(Data[Offsets.LostItemBox..]);
|
|
}
|
|
|
|
public IReadOnlyList<Building> Buildings
|
|
{
|
|
get => Building.GetArray(Data.Slice(Offsets.MainFieldStructure, MainSaveOffsets.BuildingCount * Building.SIZE));
|
|
set => Building.SetArray(value).CopyTo(Data[Offsets.MainFieldStructure..]);
|
|
}
|
|
|
|
public IPlayerHouse GetPlayerHouse(int index) => Offsets.ReadPlayerHouse(Data, index);
|
|
public void SetPlayerHouse(IPlayerHouse value, int index) => Offsets.WritePlayerHouse(value, Data, index);
|
|
|
|
public IPlayerHouse[] GetPlayerHouses()
|
|
{
|
|
var players = new IPlayerHouse[MainSaveOffsets.PlayerCount];
|
|
for (int i = 0; i < players.Length; i++)
|
|
players[i] = GetPlayerHouse(i);
|
|
return players;
|
|
}
|
|
|
|
public void SetPlayerHouses(IReadOnlyList<IPlayerHouse> houses)
|
|
{
|
|
for (int i = 0; i < houses.Count; i++)
|
|
SetPlayerHouse(houses[i], i);
|
|
}
|
|
|
|
public DesignPattern[] GetDesigns()
|
|
{
|
|
var result = new DesignPattern[Offsets.PatternCount];
|
|
for (int i = 0; i <result.Length; i++)
|
|
result[i] = GetDesign(i);
|
|
return result;
|
|
}
|
|
|
|
public void SetDesigns(IReadOnlyList<DesignPattern> value)
|
|
{
|
|
var count = Math.Min(Offsets.PatternCount, value.Count);
|
|
for (int i = 0; i < count; i++)
|
|
SetDesign(value[i], i);
|
|
}
|
|
|
|
public DesignPatternPRO[] GetDesignsPRO()
|
|
{
|
|
var result = new DesignPatternPRO[Offsets.PatternCount];
|
|
for (int i = 0; i < result.Length; i++)
|
|
result[i] = GetDesignPRO(i);
|
|
return result;
|
|
}
|
|
|
|
public void SetDesignsPRO(IReadOnlyList<DesignPatternPRO> value)
|
|
{
|
|
var count = Math.Min(Offsets.PatternCount, value.Count);
|
|
for (int i = 0; i < count; i++)
|
|
SetDesignPRO(value[i], i);
|
|
}
|
|
|
|
public DesignPattern FlagMyDesign
|
|
{
|
|
get => MainSaveOffsets.ReadPatternAtOffset(Data, Offsets.PatternFlag);
|
|
set => value.Data.CopyTo(Data[Offsets.PatternFlag..]);
|
|
}
|
|
|
|
public DesignPatternPRO[] GetDesignsTailor()
|
|
{
|
|
var result = new DesignPatternPRO[MainSaveOffsets.PatternTailorCount];
|
|
for (int i = 0; i < result.Length; i++)
|
|
result[i] = MainSaveOffsets.ReadPatternPROAtOffset(Data, Offsets.PatternTailor + (i * DesignPatternPRO.SIZE));
|
|
return result;
|
|
}
|
|
|
|
public void SetDesignsTailor(IReadOnlyList<DesignPatternPRO> value)
|
|
{
|
|
var count = Math.Min(Offsets.PatternCount, value.Count);
|
|
for (int i = 0; i < count; i++)
|
|
value[i].Data.CopyTo(Data[(Offsets.PatternTailor + (i * DesignPatternPRO.SIZE))..]);
|
|
}
|
|
|
|
private const int EventFlagsSaveCount = 0x400;
|
|
|
|
public Span<short> EventFlagLand => MemoryMarshal.Cast<byte, short>(Data.Slice(Offsets.EventFlagLand, sizeof(short) * EventFlagsSaveCount));
|
|
|
|
public TurnipStonk Turnips
|
|
{
|
|
get => Data.Slice(Offsets.ShopKabu, TurnipStonk.SIZE).ToArray().ToClass<TurnipStonk>();
|
|
set => value.ToBytesClass().CopyTo(Data[Offsets.ShopKabu..]);
|
|
}
|
|
|
|
public Museum Museum
|
|
{
|
|
get => new(Data.Slice(Offsets.Museum, Museum.SIZE).ToArray());
|
|
set => value.Data.CopyTo(Data[Offsets.Museum..]);
|
|
}
|
|
|
|
// Acre Layout/Selection of which baselayer is selected for an acre.
|
|
private const int AcreWidth = 7 + (2 * 1); // 1 on each side cannot be traversed
|
|
private const int AcreHeight = 6 + (2 * 1); // 1 on each side cannot be traversed
|
|
private const int AcreMax = AcreWidth * AcreHeight;
|
|
private const int AcreSizeAll = AcreMax * 2;
|
|
|
|
public ushort GetAcre(int index)
|
|
{
|
|
if ((uint)index > AcreMax)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
return ReadUInt16LittleEndian(Data[(Offsets.OutsideField + (index * 2))..]);
|
|
}
|
|
|
|
public void SetAcre(int index, ushort value)
|
|
{
|
|
if ((uint)index > AcreMax)
|
|
throw new ArgumentOutOfRangeException(nameof(index));
|
|
WriteUInt16LittleEndian(Data[(Offsets.OutsideField + (index * 2))..], value);
|
|
}
|
|
|
|
public byte[] GetAcreBytes() => Data.Slice(Offsets.OutsideField, AcreSizeAll).ToArray();
|
|
|
|
public void SetAcreBytes(ReadOnlySpan<byte> data)
|
|
{
|
|
if (data.Length != AcreSizeAll)
|
|
throw new ArgumentOutOfRangeException(nameof(data.Length));
|
|
data.CopyTo(Data[Offsets.OutsideField..]);
|
|
}
|
|
|
|
|
|
#pragma warning disable CA1822 // Mark members as static
|
|
public byte FieldItemAcreWidth => Offsets.FieldItemAcreWidth; // 3.0.0 updated from 7 => 9
|
|
// ReSharper disable once MemberCanBeMadeStatic.Global
|
|
public byte FieldItemAcreHeight => 6; // always 6
|
|
private int FieldItemAcreCount => FieldItemAcreWidth * FieldItemAcreHeight;
|
|
#pragma warning restore CA1822 // Mark members as static
|
|
|
|
|
|
private const int TotalTerrainTileCount = LayerTerrain.TilesPerAcreDim * LayerTerrain.TilesPerAcreDim * (7 * 6);
|
|
private int TotalFieldItemTileCount => LayerFieldItem.TilesPerAcreDim * LayerFieldItem.TilesPerAcreDim * FieldItemAcreCount;
|
|
|
|
public TerrainTile[] GetTerrainTiles() => TerrainTile.GetArray(Data.Slice(Offsets.LandMakingMap, TotalTerrainTileCount * TerrainTile.SIZE));
|
|
public void SetTerrainTiles(IReadOnlyList<TerrainTile> array) => TerrainTile.SetArray(array).CopyTo(Data[Offsets.LandMakingMap..]);
|
|
|
|
public const ushort MapDesignNone = 0xF800;
|
|
|
|
public Memory<byte> MapDesignTileData(int w, int h) => Raw.Slice(Offsets.MyDesignMap, (w * 16) * (h * 16) * sizeof(ushort));
|
|
public ushort[] GetMapDesignTiles(int w, int h) => MemoryMarshal.Cast<byte, ushort>(MapDesignTileData(w, h).Span).ToArray();
|
|
public void SetMapDesignTiles(ReadOnlySpan<ushort> value, int w, int h) => MemoryMarshal.Cast<ushort, byte>(value).CopyTo(MapDesignTileData(w, h).Span);
|
|
|
|
public void ClearDesignTiles(int w, int h)
|
|
{
|
|
var tiles = GetMapDesignTiles(w, h);
|
|
tiles.AsSpan().Fill(MapDesignNone);
|
|
SetMapDesignTiles(tiles, w, h);
|
|
}
|
|
|
|
private int FieldItemLayerSize => TotalFieldItemTileCount * Item.SIZE;
|
|
private int FieldItemFlagSize => TotalFieldItemTileCount / sizeof(byte); // bitflags
|
|
|
|
private int FieldItemLayer0 => Offsets.FieldItem;
|
|
private int FieldItemLayer1 => Offsets.FieldItem + FieldItemLayerSize;
|
|
public int FieldItemFlag0 => Offsets.FieldItem + (FieldItemLayerSize * 2);
|
|
public int FieldItemFlag1 => Offsets.FieldItem + (FieldItemLayerSize * 2) + FieldItemFlagSize;
|
|
public Memory<byte> FieldItemFlag0Data => Data.Slice(FieldItemFlag0, FieldItemFlagSize).ToArray();
|
|
public Memory<byte> FieldItemFlag1Data => Data.Slice(FieldItemFlag1, FieldItemFlagSize).ToArray();
|
|
|
|
public Item[] GetFieldItemLayer0() => Item.GetArray(Data.Slice(FieldItemLayer0, FieldItemLayerSize));
|
|
public void SetFieldItemLayer0(IReadOnlyList<Item> array) => Item.SetArray(array).CopyTo(Data[FieldItemLayer0..]);
|
|
|
|
public Item[] GetFieldItemLayer1() => Item.GetArray(Data.Slice(FieldItemLayer1, FieldItemLayerSize));
|
|
public void SetFieldItemLayer1(IReadOnlyList<Item> array) => Item.SetArray(array).CopyTo(Data[FieldItemLayer1..]);
|
|
|
|
public ushort OutsideFieldTemplateUniqueId
|
|
{
|
|
get => ReadUInt16LittleEndian(Data[(Offsets.OutsideField + AcreSizeAll)..]);
|
|
set => WriteUInt16LittleEndian(Data[(Offsets.OutsideField + AcreSizeAll)..], value);
|
|
}
|
|
|
|
public ushort MainFieldParamUniqueID
|
|
{
|
|
get => ReadUInt16LittleEndian(Data[(Offsets.OutsideField + AcreSizeAll + 2)..]);
|
|
set => WriteUInt16LittleEndian(Data[(Offsets.OutsideField + AcreSizeAll + 2)..], value);
|
|
}
|
|
|
|
public uint EventPlazaLeftUpX
|
|
{
|
|
get => ReadUInt32LittleEndian(Data[(Offsets.OutsideField + AcreSizeAll + 4)..]);
|
|
set => WriteUInt32LittleEndian(Data[(Offsets.OutsideField + AcreSizeAll + 4)..], value);
|
|
}
|
|
|
|
public uint EventPlazaLeftUpZ
|
|
{
|
|
get => ReadUInt32LittleEndian(Data[(Offsets.OutsideField + AcreSizeAll + 8)..]);
|
|
set => WriteUInt32LittleEndian(Data[(Offsets.OutsideField + AcreSizeAll + 8)..], value);
|
|
}
|
|
|
|
public GSaveVisitorNpc Visitor
|
|
{
|
|
get => Data.Slice(Offsets.Visitor, GSaveVisitorNpc.SIZE).ToArray().ToClass<GSaveVisitorNpc>();
|
|
set => value.ToBytesClass().CopyTo(Data[Offsets.Visitor..]);
|
|
}
|
|
|
|
public GSaveFg SaveFg
|
|
{
|
|
get => Data.Slice(Offsets.SaveFg, GSaveFg.SIZE).ToArray().ToClass<GSaveFg>();
|
|
set => value.ToBytesClass().CopyTo(Data[Offsets.SaveFg..]);
|
|
}
|
|
|
|
public ushort SpecialtyFruit
|
|
{
|
|
get => ReadUInt16LittleEndian(Data[Offsets.SpecialtyFruit..]);
|
|
set => WriteUInt16LittleEndian(Data[Offsets.SpecialtyFruit..], value);
|
|
}
|
|
public ushort SisterFruit
|
|
{
|
|
get => ReadUInt16LittleEndian(Data[Offsets.SisterFruit..]);
|
|
set => WriteUInt16LittleEndian(Data[Offsets.SisterFruit..], value);
|
|
}
|
|
public IslandFlowers SpecialtyFlower { get => (IslandFlowers)Data[Offsets.SpecialtyFlower]; set => Data[Offsets.SpecialtyFlower] = (byte)value; }
|
|
public IslandFlowers SisterFlower { get => (IslandFlowers)Data[Offsets.SisterFlower]; set => Data[Offsets.SisterFlower] = (byte)value; }
|
|
public Span<byte> FruitFlags
|
|
{
|
|
get => Data.Slice(Offsets.FruitFlags, 5);
|
|
set => value.ToArray().CopyTo(Data[Offsets.FruitFlags..]);
|
|
}
|
|
|
|
public GSaveTime LastSaved => Data.Slice(Offsets.LastSavedTime, GSaveTime.SIZE).ToStructure<GSaveTime>();
|
|
|
|
public GSaveBulletinBoard Bulletin
|
|
{
|
|
get => Data.Slice(Offsets.BulletinBoard, GSaveBulletinBoard.SIZE).ToStructure<GSaveBulletinBoard>();
|
|
set => value.ToBytes().CopyTo(Data[Offsets.BulletinBoard..]);
|
|
}
|
|
}
|