mirror of
https://github.com/kwsch/NHSE.git
synced 2026-03-22 01:34:51 -05:00
Add: Campsite Editor to Map tab. Allows users to set whether there is or is not a villager visiting the campsite, allows selecting that visitor and to edit the visit timestamp (defaults to current timestamp override so visitor is there on same day load). Back-tracked offsets from old Cylindircal Earth save schemas for old save revs. If campsite is not unlocked on the save, the UI is locked out with a message. Cleanup: Moved UpdateFruitsFlag to misc editor class over save class because that was bad form. Oops.
280 lines
12 KiB
C#
280 lines
12 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 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 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, Span<byte> playerID, Span<byte> townID) => Offsets.WritePattern(value, Data, index, playerID, townID);
|
|
public DesignPatternPRO GetDesignPRO(int index) => Offsets.ReadPatternPRO(Data, index);
|
|
public void SetDesignPRO(DesignPatternPRO value, int index, Span<byte> playerID, Span<byte> townID) => Offsets.WritePatternPRO(value, Data, index, playerID, townID);
|
|
|
|
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, Span<byte> playerID, Span<byte> townID)
|
|
{
|
|
var count = Math.Min(Offsets.PatternCount, value.Count);
|
|
for (int i = 0; i < count; i++)
|
|
SetDesign(value[i], i, playerID, townID);
|
|
}
|
|
|
|
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, Span<byte> playerID, Span<byte> townID)
|
|
{
|
|
var count = Math.Min(Offsets.PatternCount, value.Count);
|
|
for (int i = 0; i < count; i++)
|
|
SetDesignPRO(value[i], i, playerID, townID);
|
|
}
|
|
|
|
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..]);
|
|
}
|
|
|
|
public 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..]);
|
|
}
|
|
|
|
public TerrainTile[] GetTerrainTiles() => TerrainTile.GetArray(Data.Slice(Offsets.LandMakingMap, MapGrid.MapTileCount16x16 * TerrainTile.SIZE));
|
|
public void SetTerrainTiles(IReadOnlyList<TerrainTile> array) => TerrainTile.SetArray(array).CopyTo(Data[Offsets.LandMakingMap..]);
|
|
|
|
public const int MapDesignNone = 0xF800;
|
|
|
|
public Memory<byte> MapDesignTileData => Raw.Slice(Offsets.MyDesignMap, 112 * 96 * sizeof(ushort));
|
|
public ushort[] GetMapDesignTiles() => MemoryMarshal.Cast<byte, ushort>(MapDesignTileData.Span).ToArray();
|
|
public void SetMapDesignTiles(ReadOnlySpan<ushort> value) => MemoryMarshal.Cast<ushort, byte>(value).CopyTo(MapDesignTileData.Span);
|
|
|
|
private const int FieldItemLayerSize = MapGrid.MapTileCount32x32 * Item.SIZE;
|
|
private const int FieldItemFlagSize = MapGrid.MapTileCount32x32 / 8; // bitflags
|
|
|
|
private int FieldItemLayer1 => Offsets.FieldItem;
|
|
private int FieldItemLayer2 => Offsets.FieldItem + FieldItemLayerSize;
|
|
public int FieldItemFlag1 => Offsets.FieldItem + (FieldItemLayerSize * 2);
|
|
public int FieldItemFlag2 => Offsets.FieldItem + (FieldItemLayerSize * 2) + FieldItemFlagSize;
|
|
|
|
public Item[] GetFieldItemLayer1() => Item.GetArray(Data.Slice(FieldItemLayer1, FieldItemLayerSize));
|
|
public void SetFieldItemLayer1(IReadOnlyList<Item> array) => Item.SetArray(array).CopyTo(Data[FieldItemLayer1..]);
|
|
|
|
public Item[] GetFieldItemLayer2() => Item.GetArray(Data.Slice(FieldItemLayer2, FieldItemLayerSize));
|
|
public void SetFieldItemLayer2(IReadOnlyList<Item> array) => Item.SetArray(array).CopyTo(Data[FieldItemLayer2..]);
|
|
|
|
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..]);
|
|
}
|
|
} |