Differentiate villager types

v1.5 increased the size of GSaveLightMemory by 0xC each; with 160 entries, everything stored after this field (at 0x2F84) increased its offset by 0x780.

  GSaveItemName                     ClothesPTops;                           // @0x14c size 0x8, align 4
  s16                               _58b5e808;                              // @0x154 size 0x2, align 2
  s8                                ClothesPTarget;                         // @0x156 size 0x1, align 1

Should be possible to convert the first revision of villager data to the current format and vice versa. Someone would need to document how they pre-fill these new fields (listed above).
This commit is contained in:
Kurt 2020-09-29 22:20:53 -07:00
parent e652d00d45
commit 8ca3877bfa
16 changed files with 241 additions and 56 deletions

View File

@ -15,18 +15,18 @@ public sealed class MainSave : EncryptedFilePair
public AirportColor AirportThemeColor { get => (AirportColor)Data[Offsets.AirportThemeColor]; set => Data[Offsets.AirportThemeColor] = (byte)value; }
public uint WeatherSeed { get => BitConverter.ToUInt32(Data, Offsets.WeatherRandSeed); set => BitConverter.GetBytes(value).CopyTo(Data, Offsets.WeatherRandSeed); }
public Villager GetVillager(int index) => Offsets.ReadVillager(Data, index);
public void SetVillager(Villager value, int index) => Offsets.WriteVillager(value, Data, index);
public IVillager GetVillager(int index) => Offsets.ReadVillager(Data, index);
public void SetVillager(IVillager value, int index) => Offsets.WriteVillager(value, Data, index);
public Villager[] GetVillagers()
public IVillager[] GetVillagers()
{
var villagers = new Villager[MainSaveOffsets.VillagerCount];
var villagers = new IVillager[MainSaveOffsets.VillagerCount];
for (int i = 0; i < villagers.Length; i++)
villagers[i] = GetVillager(i);
return villagers;
}
public void SetVillagers(IReadOnlyList<Villager> villagers)
public void SetVillagers(IReadOnlyList<IVillager> villagers)
{
for (int i = 0; i < villagers.Count; i++)
SetVillager(villagers[i], i);

View File

@ -44,6 +44,9 @@ public abstract class MainSaveOffsets
public abstract int LostItemBox { get; }
public abstract int LastSavedTime { get; }
public abstract int VillagerSize { get; }
public abstract IVillager ReadVillager(byte[] data);
public static MainSaveOffsets GetOffsets(FileHeaderInfo Info)
{
var rev = Info.GetKnownRevisionIndex();
@ -108,19 +111,22 @@ public void WritePatternPRO(DesignPatternPRO p, byte[] data, int index)
p.Data.CopyTo(data, PatternsPRO + (index * DesignPatternPRO.SIZE));
}
public Villager ReadVillager(byte[] data, int index)
public IVillager ReadVillager(byte[] data, int index)
{
if ((uint)index >= VillagerCount)
throw new ArgumentOutOfRangeException(nameof(index));
var v = data.Slice(Animal + (index * Villager.SIZE), Villager.SIZE);
return new Villager(v);
var size = VillagerSize;
var v = data.Slice(Animal + (index * size), size);
return ReadVillager(v);
}
public void WriteVillager(Villager v, byte[] data, int index)
public void WriteVillager(IVillager v, byte[] data, int index)
{
if ((uint)index >= VillagerCount)
throw new ArgumentOutOfRangeException(nameof(index));
v.Data.CopyTo(data, Animal + (index * Villager.SIZE));
var size = VillagerSize;
v.Write().CopyTo(data, Animal + (index * size));
}
}
}

View File

@ -46,5 +46,8 @@ public class MainSaveOffsets10 : MainSaveOffsets
public override int LostItemBox => GSaveLandOtherStart + 0x5C1E20;
public override int LastSavedTime => GSaveLandOtherStart + 0x5C6748;
#endregion
public override int VillagerSize => Villager1.SIZE;
public override IVillager ReadVillager(byte[] data) => new Villager1(data);
}
}
}

View File

@ -46,5 +46,8 @@ public class MainSaveOffsets11 : MainSaveOffsets
public override int LostItemBox => GSaveLandOtherStart + 0x5C33F0;
public override int LastSavedTime => GSaveLandOtherStart + 0x5C7D48;
#endregion
public override int VillagerSize => Villager1.SIZE;
public override IVillager ReadVillager(byte[] data) => new Villager1(data);
}
}

View File

@ -46,5 +46,8 @@ public class MainSaveOffsets12 : MainSaveOffsets
public override int LostItemBox => GSaveLandOtherStart + 0x5CF370;
public override int LastSavedTime => GSaveLandOtherStart + 0x5D3CC8;
#endregion
public override int VillagerSize => Villager1.SIZE;
public override IVillager ReadVillager(byte[] data) => new Villager1(data);
}
}

View File

@ -46,5 +46,8 @@ public class MainSaveOffsets13 : MainSaveOffsets
public override int LostItemBox => GSaveLandOtherStart + 0x5CF3F0;
public override int LastSavedTime => GSaveLandOtherStart + 0x5D3D48;
#endregion
public override int VillagerSize => Villager1.SIZE;
public override IVillager ReadVillager(byte[] data) => new Villager1(data);
}
}

View File

@ -46,5 +46,8 @@ public class MainSaveOffsets14 : MainSaveOffsets
public override int LostItemBox => GSaveLandOtherStart + 0x605E70;
public override int LastSavedTime => GSaveLandOtherStart + 0x60A708;
#endregion
public override int VillagerSize => Villager1.SIZE;
public override IVillager ReadVillager(byte[] data) => new Villager1(data);
}
}

View File

@ -46,5 +46,8 @@ public class MainSaveOffsets15 : MainSaveOffsets
public override int LostItemBox => GSaveLandOtherStart + 0x6159f0;
public override int LastSavedTime => GSaveLandOtherStart + 0x61a288;
#endregion
public override int VillagerSize => Villager2.SIZE;
public override IVillager ReadVillager(byte[] data) => new Villager2(data);
}
}

View File

@ -108,7 +108,7 @@ public static void DumpVillagerHouses(this MainSave sav, string path)
}
}
private static void Dump(this VillagerHouse h, string path, Villager v)
private static void Dump(this VillagerHouse h, string path, IVillager v)
{
var name = GameInfo.Strings.GetVillager(v.InternalName);
var dest = Path.Combine(path, $"{name}.nhvh");
@ -121,17 +121,17 @@ private static void Dump(this VillagerHouse h, string path, Villager v)
/// </summary>
/// <param name="villagers">Data to dump from</param>
/// <param name="path">Path to dump to</param>
public static void Dump(this IEnumerable<Villager> villagers, string path)
public static void Dump(this IEnumerable<IVillager> villagers, string path)
{
foreach (var v in villagers)
v.Dump(path);
}
private static void Dump(this Villager v, string path)
private static void Dump(this IVillager v, string path)
{
var name = GameInfo.Strings.GetVillager(v.InternalName);
var dest = Path.Combine(path, $"{name}.nhv");
File.WriteAllBytes(dest, v.Data);
var dest = Path.Combine(path, $"{name}.{v.Extension}");
File.WriteAllBytes(dest, v.Write());
}
/// <summary>

View File

@ -0,0 +1,42 @@
using System.Collections.Generic;
namespace NHSE.Core
{
/// <summary>
/// Exposes all interact-able values for a villager.
/// </summary>
public interface IVillager : IVillagerOrigin
{
byte Species { get; set; }
byte Variant { get; set; }
VillagerPersonality Personality { get; set; }
string InternalName { get; }
string CatchPhrase { get; set; }
IReadOnlyList<VillagerItem> WearStockList { get; set; }
IReadOnlyList<VillagerItem> FtrStockList { get; set; }
byte BirthType { get; set; }
byte InducementType { get; set; }
byte MoveType { get; set; }
bool MovingOut { get; set; }
int Gender { get; }
GSaveRoomFloorWall Room { get; set; }
DesignPatternPRO Design { get; set; }
GSaveMemory GetMemory(int index);
GSaveMemory[] GetMemories();
void SetMemory(GSaveMemory memory, int index);
void SetMemories(IReadOnlyList<GSaveMemory> memories);
ushort[] GetEventFlagsSave();
void SetEventFlagsSave(ushort[] value);
void SetFriendshipAll(byte value = byte.MaxValue);
/// <summary> Returns the inner data buffer of the object, flushing any pending changes. </summary>
byte[] Write();
string Extension { get; }
}
}

View File

@ -3,30 +3,18 @@
namespace NHSE.Core
{
public class Villager : IVillagerOrigin
public sealed class Villager1 : IVillager
{
public const int SIZE = 0x12AB0;
public string Extension => "nhv";
public readonly byte[] Data;
public Villager(byte[] data) => Data = data;
public Villager1(byte[] data) => Data = data;
public byte[] Write() => Data;
public byte Species
{
get => Data[0];
set => Data[0] = value;
}
public byte Variant
{
get => Data[1];
set => Data[1] = value;
}
public VillagerPersonality Personality
{
get => (VillagerPersonality)Data[2];
set => Data[2] = (byte)value;
}
public byte Species { get => Data[0]; set => Data[0] = value; }
public byte Variant { get => Data[1]; set => Data[1] = value; }
public VillagerPersonality Personality { get => (VillagerPersonality)Data[2]; set => Data[2] = (byte)value; }
public string TownName => GetMemory(0).TownName;
public byte[] GetTownIdentity() => GetMemory(0).GetTownIdentity();
@ -68,8 +56,8 @@ public void SetMemories(IReadOnlyList<GSaveMemory> memories)
public string CatchPhrase
{
get => StringUtil.GetString(Data, 0x10014, 2 * 12);
set => StringUtil.GetBytes(value, 2 * 12).CopyTo(Data, 0x10014);
get => StringUtil.GetString(Data, 0xFFE8 + 0x2C, 2 * 12);
set => StringUtil.GetBytes(value, 2 * 12).CopyTo(Data, 0xFFE8 + 0x2C);
}
private const int WearCount = 24;
@ -137,4 +125,4 @@ public void SetFriendshipAll(byte value = byte.MaxValue)
}
}
}
}
}

View File

@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
namespace NHSE.Core
{
public sealed class Villager2 : IVillager
{
public const int SIZE = 0x13230; // + 160*0xC (0x780) -- GSaveLightMemory increased size.
public string Extension => "nhv2";
public readonly byte[] Data;
public Villager2(byte[] data) => Data = data;
public byte[] Write() => Data;
public byte Species { get => Data[0]; set => Data[0] = value; }
public byte Variant { get => Data[1]; set => Data[1] = value; }
public VillagerPersonality Personality { get => (VillagerPersonality)Data[2]; set => Data[2] = (byte)value; }
public string TownName => GetMemory(0).TownName;
public byte[] GetTownIdentity() => GetMemory(0).GetTownIdentity();
public string PlayerName => GetMemory(0).PlayerName;
public byte[] GetPlayerIdentity() => GetMemory(0).GetPlayerIdentity();
public const int PlayerMemoryCount = 8;
public GSaveMemory GetMemory(int index)
{
if ((uint)index >= PlayerMemoryCount)
throw new ArgumentOutOfRangeException(nameof(index));
var bytes = Data.Slice(0x4 + (index * GSaveMemory.SIZE), GSaveMemory.SIZE);
return new GSaveMemory(bytes);
}
public GSaveMemory[] GetMemories()
{
var memories = new GSaveMemory[PlayerMemoryCount];
for (int i = 0; i < memories.Length; i++)
memories[i] = GetMemory(i);
return memories;
}
public void SetMemory(GSaveMemory memory, int index)
{
if ((uint)index >= PlayerMemoryCount)
throw new ArgumentOutOfRangeException(nameof(index));
memory.Data.CopyTo(Data, 0x4 + (index * GSaveMemory.SIZE));
}
public void SetMemories(IReadOnlyList<GSaveMemory> memories)
{
for (int i = 0; i < memories.Count; i++)
SetMemory(memories[i], i);
}
public string CatchPhrase
{
get => StringUtil.GetString(Data, 0x10768 + 0x2C, 2 * 12);
set => StringUtil.GetBytes(value, 2 * 12).CopyTo(Data, 0x10768 + 0x2C);
}
private const int WearCount = 24;
public IReadOnlyList<VillagerItem> WearStockList
{
get => VillagerItem.GetArray(Data.Slice(0x1094c, WearCount * VillagerItem.SIZE));
set => VillagerItem.SetArray(value).CopyTo(Data, 0x1094c);
}
private const int FurnitureCount = 32;
public IReadOnlyList<VillagerItem> FtrStockList
{
get => VillagerItem.GetArray(Data.Slice(0x10d6c, FurnitureCount * VillagerItem.SIZE));
set => VillagerItem.SetArray(value).CopyTo(Data, 0x10d6c);
}
// State Flags
public byte BirthType { get => Data[0x12678]; set => Data[0x12678] = value; }
public byte InducementType { get => Data[0x12679]; set => Data[0x12679] = value; }
public byte MoveType { get => Data[0x1267a]; set => Data[0x1267a] = value; }
public bool MovingOut { get => (MoveType & 2) == 2; set => MoveType = (byte)((MoveType & ~2) | (value ? 2 : 0)); }
// EventFlagsNPCSaveParam
private const int EventFlagsSaveCount = 0x100; // Future-proof allocation! Release version used <20% of the amount allocated.
public ushort[] GetEventFlagsSave()
{
var value = new ushort[EventFlagsSaveCount];
Buffer.BlockCopy(Data, 0x1267c, value, 0, sizeof(ushort) * value.Length);
return value;
}
public void SetEventFlagsSave(ushort[] value)
{
Buffer.BlockCopy(value, 0, Data, 0x1267c, sizeof(ushort) * value.Length);
}
public override string ToString() => InternalName;
public string InternalName => VillagerUtil.GetInternalVillagerName((VillagerSpecies)Species, Variant);
public int Gender => ((int)Personality / 4) & 1; // 0 = M, 1 = F
public GSaveRoomFloorWall Room
{
get => Data.Slice(0x12880, GSaveRoomFloorWall.SIZE).ToStructure<GSaveRoomFloorWall>();
set => value.ToBytes().CopyTo(Data, 0x12880);
}
public DesignPatternPRO Design
{
get => new DesignPatternPRO(Data.Slice(0x128a8, DesignPatternPRO.SIZE));
set => value.Data.CopyTo(Data, 0x128a8);
}
public void SetFriendshipAll(byte value = byte.MaxValue)
{
for (int i = 0; i < PlayerMemoryCount; i++)
{
var m = GetMemory(i);
if (string.IsNullOrEmpty(m.PlayerName))
continue;
m.Friendship = value;
SetMemory(m, i);
}
}
}
}

View File

@ -8,13 +8,13 @@ namespace NHSE.WinForms
{
public partial class VillagerEditor : UserControl
{
public Villager[] Villagers;
public IVillager[] Villagers;
public IVillagerOrigin Origin;
private readonly MainSave SAV;
private int VillagerIndex = -1;
private bool Loading;
public VillagerEditor(Villager[] villagers, IVillagerOrigin origin, MainSave sav, bool hasHouses)
public VillagerEditor(IVillager[] villagers, IVillagerOrigin origin, MainSave sav, bool hasHouses)
{
InitializeComponent();
Villagers = villagers;
@ -52,7 +52,7 @@ private void LoadVillager(int index)
VillagerIndex = index;
}
private void LoadVillager(Villager v)
private void LoadVillager(IVillager v)
{
Loading = true;
NUD_Species.Value = v.Species;
@ -103,15 +103,17 @@ private void B_DumpVillager_Click(object sender, EventArgs e)
var name = L_ExternalName.Text;
using var sfd = new SaveFileDialog
{
Filter = "New Horizons Villager (*.nhv)|*.nhv|All files (*.*)|*.*",
FileName = $"{name}.nhv",
Filter = "New Horizons Villager (*.nhv)|*.nhv|" +
"New Horizons Villager (*.nhv2)|*.nhv2|" +
"All files (*.*)|*.*",
FileName = $"{name}.{Villagers[VillagerIndex].Extension}",
};
if (sfd.ShowDialog() != DialogResult.OK)
return;
SaveVillager(VillagerIndex);
var v = Villagers[VillagerIndex];
File.WriteAllBytes(sfd.FileName, v.Data);
File.WriteAllBytes(sfd.FileName, v.Write());
}
private void B_LoadVillager_Click(object sender, EventArgs e)
@ -119,15 +121,16 @@ private void B_LoadVillager_Click(object sender, EventArgs e)
var name = L_ExternalName.Text;
using var ofd = new OpenFileDialog
{
Filter = "New Horizons Villager (*.nhv)|*.nhv|All files (*.*)|*.*",
FileName = $"{name}.nhv",
Filter = "New Horizons Villager (*.nhv)|*.nhv|" +
"New Horizons Villager (*.nhv2)|*.nhv2|" +
"All files (*.*)|*.*",
FileName = $"{name}.{Villagers[VillagerIndex].Extension}",
};
if (ofd.ShowDialog() != DialogResult.OK)
return;
var path = ofd.FileName;
var original = Villagers[VillagerIndex];
var expectLength = original.Data.Length;
var expectLength = SAV.Offsets.VillagerSize;
var fi = new FileInfo(path);
if (fi.Length != expectLength)
{
@ -136,7 +139,7 @@ private void B_LoadVillager_Click(object sender, EventArgs e)
}
var data = File.ReadAllBytes(ofd.FileName);
var v = new Villager(data);
var v = SAV.Offsets.ReadVillager(data);
var player0 = Origin;
if (!v.IsOriginatedFrom(player0))
{
@ -145,7 +148,7 @@ private void B_LoadVillager_Click(object sender, EventArgs e)
if (result == DialogResult.Cancel)
return;
if (result == DialogResult.Yes)
v.ChangeOrigins(player0, v.Data);
v.ChangeOrigins(player0, v.Write());
}
LoadVillager(Villagers[VillagerIndex] = v);

View File

@ -7,7 +7,7 @@ namespace NHSE.WinForms
{
public static class MiscDumpHelper
{
public static void DumpVillagerMemoryPlayer(Villager v, GSaveMemory memory)
public static void DumpVillagerMemoryPlayer(IVillager v, GSaveMemory memory)
{
using var sfd = new SaveFileDialog
{
@ -21,7 +21,7 @@ public static void DumpVillagerMemoryPlayer(Villager v, GSaveMemory memory)
File.WriteAllBytes(sfd.FileName, data);
}
public static bool LoadVillagerMemoryPlayer(Villager v, GSaveMemory[] memories, int index)
public static bool LoadVillagerMemoryPlayer(IVillager v, GSaveMemory[] memories, int index)
{
using var ofd = new OpenFileDialog
{

View File

@ -10,11 +10,11 @@ public partial class VillagerHouseEditor : Form
{
private readonly MainSave SAV;
private readonly VillagerHouse[] Houses;
private readonly IReadOnlyList<Villager> Villagers;
private readonly IReadOnlyList<IVillager> Villagers;
private int Index;
public VillagerHouseEditor(VillagerHouse[] houses, IReadOnlyList<Villager> villagers, MainSave sav, int index)
public VillagerHouseEditor(VillagerHouse[] houses, IReadOnlyList<IVillager> villagers, MainSave sav, int index)
{
InitializeComponent();
this.TranslateInterface(GameInfo.CurrentLanguage);

View File

@ -7,7 +7,7 @@ namespace NHSE.WinForms
{
public partial class VillagerMemoryEditor : Form
{
private readonly Villager Villager;
private readonly IVillager Villager;
private readonly GSaveMemory[] Memories;
private int PlayerIndex = -1;
@ -16,7 +16,7 @@ public partial class VillagerMemoryEditor : Form
private readonly TextBox[] Greetings;
public VillagerMemoryEditor(Villager villager)
public VillagerMemoryEditor(IVillager villager)
{
InitializeComponent();
this.TranslateInterface(GameInfo.CurrentLanguage);