Add handling for revised player house and room structure (v2.0) (#547)

* Add handling for revised player house and room structure (v2.0)

* Update player room load/dump file type filters
This commit is contained in:
hp3721 2021-11-19 21:53:04 -06:00 committed by GitHub
parent 8a2a562cdb
commit 8aed43a67f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 569 additions and 76 deletions

View File

@ -66,29 +66,19 @@ public IReadOnlyList<Building> Buildings
set => Building.SetArray(value).CopyTo(Data, Offsets.MainFieldStructure);
}
public PlayerHouse GetPlayerHouse(int index)
public IPlayerHouse GetPlayerHouse(int index) => Offsets.ReadPlayerHouse(Data, index);
public void SetPlayerHouse(IPlayerHouse value, int index) => Offsets.WritePlayerHouse(value, Data, index);
public IPlayerHouse[] GetPlayerHouses()
{
if ((uint)index >= MainSaveOffsets.PlayerCount)
throw new ArgumentOutOfRangeException(nameof(index));
return new PlayerHouse(Data.Slice(Offsets.PlayerHouseList + (index * PlayerHouse.SIZE), PlayerHouse.SIZE));
var players = new IPlayerHouse[MainSaveOffsets.PlayerCount];
for (int i = 0; i < players.Length; i++)
players[i] = GetPlayerHouse(i);
return players;
}
public void SetPlayerHouse(PlayerHouse h, int index)
{
if ((uint)index >= MainSaveOffsets.PlayerCount)
throw new ArgumentOutOfRangeException(nameof(index));
h.Data.CopyTo(Data, Offsets.PlayerHouseList + (index * PlayerHouse.SIZE));
}
public PlayerHouse[] GetPlayerHouses()
{
var villagers = new PlayerHouse[MainSaveOffsets.PlayerCount];
for (int i = 0; i < villagers.Length; i++)
villagers[i] = GetPlayerHouse(i);
return villagers;
}
public void SetPlayerHouses(IReadOnlyList<PlayerHouse> houses)
public void SetPlayerHouses(IReadOnlyList<IPlayerHouse> houses)
{
for (int i = 0; i < houses.Count; i++)
SetPlayerHouse(houses[i], i);

View File

@ -48,9 +48,13 @@ public abstract class MainSaveOffsets
public abstract int VillagerSize { get; }
public abstract int VillagerHouseSize { get; }
public abstract int PlayerHouseSize { get; }
public abstract int PlayerRoomSize { get; }
public abstract IVillager ReadVillager(byte[] data);
public abstract IVillagerHouse ReadVillagerHouse(byte[] data);
public abstract IPlayerHouse ReadPlayerHouse(byte[] data);
public abstract IPlayerRoom ReadPlayerRoom(byte[] data);
public static MainSaveOffsets GetOffsets(FileHeaderInfo Info)
{
@ -162,5 +166,23 @@ public void WriteVillagerHouse(IVillagerHouse v, byte[] data, int index)
var size = VillagerHouseSize;
v.Write().CopyTo(data, NpcHouseList + (index * size));
}
public IPlayerHouse ReadPlayerHouse(byte[] data, int index)
{
if ((uint)index >= PlayerCount)
throw new ArgumentOutOfRangeException(nameof(index));
var size = PlayerHouseSize;
var v = data.Slice(PlayerHouseList + (index * size), size);
return ReadPlayerHouse(v);
}
public void WritePlayerHouse(IPlayerHouse v, byte[] data, int index)
{
if ((uint)index >= PlayerCount)
throw new ArgumentOutOfRangeException(nameof(index));
var size = PlayerHouseSize;
v.Write().CopyTo(data, PlayerHouseList + (index * size));
}
}
}

View File

@ -52,5 +52,11 @@ public class MainSaveOffsets10 : MainSaveOffsets
public override int VillagerHouseSize => VillagerHouse1.SIZE;
public override IVillagerHouse ReadVillagerHouse(byte[] data) => new VillagerHouse1(data);
public override int PlayerHouseSize => PlayerHouse1.SIZE;
public override IPlayerHouse ReadPlayerHouse(byte[] data) => new PlayerHouse1(data);
public override int PlayerRoomSize => PlayerHouse1.SIZE;
public override IPlayerRoom ReadPlayerRoom(byte[] data) => new PlayerRoom1(data);
}
}

View File

@ -52,5 +52,10 @@ public class MainSaveOffsets11 : MainSaveOffsets
public override int VillagerHouseSize => VillagerHouse1.SIZE;
public override IVillagerHouse ReadVillagerHouse(byte[] data) => new VillagerHouse1(data);
public override int PlayerHouseSize => PlayerHouse1.SIZE;
public override IPlayerHouse ReadPlayerHouse(byte[] data) => new PlayerHouse1(data);
public override int PlayerRoomSize => PlayerHouse1.SIZE;
public override IPlayerRoom ReadPlayerRoom(byte[] data) => new PlayerRoom1(data);
}
}

View File

@ -54,5 +54,10 @@ public class MainSaveOffsets110 : MainSaveOffsets
public override int VillagerHouseSize => VillagerHouse1.SIZE;
public override IVillagerHouse ReadVillagerHouse(byte[] data) => new VillagerHouse1(data);
public override int PlayerHouseSize => PlayerHouse1.SIZE;
public override IPlayerHouse ReadPlayerHouse(byte[] data) => new PlayerHouse1(data);
public override int PlayerRoomSize => PlayerHouse1.SIZE;
public override IPlayerRoom ReadPlayerRoom(byte[] data) => new PlayerRoom1(data);
}
}

View File

@ -55,5 +55,10 @@ public class MainSaveOffsets111 : MainSaveOffsets
public override int VillagerHouseSize => VillagerHouse1.SIZE;
public override IVillagerHouse ReadVillagerHouse(byte[] data) => new VillagerHouse1(data);
public override int PlayerHouseSize => PlayerHouse1.SIZE;
public override IPlayerHouse ReadPlayerHouse(byte[] data) => new PlayerHouse1(data);
public override int PlayerRoomSize => PlayerHouse1.SIZE;
public override IPlayerRoom ReadPlayerRoom(byte[] data) => new PlayerRoom1(data);
}
}

View File

@ -52,5 +52,10 @@ public class MainSaveOffsets12 : MainSaveOffsets
public override int VillagerHouseSize => VillagerHouse1.SIZE;
public override IVillagerHouse ReadVillagerHouse(byte[] data) => new VillagerHouse1(data);
public override int PlayerHouseSize => PlayerHouse1.SIZE;
public override IPlayerHouse ReadPlayerHouse(byte[] data) => new PlayerHouse1(data);
public override int PlayerRoomSize => PlayerHouse1.SIZE;
public override IPlayerRoom ReadPlayerRoom(byte[] data) => new PlayerRoom1(data);
}
}

View File

@ -52,5 +52,10 @@ public class MainSaveOffsets13 : MainSaveOffsets
public override int VillagerHouseSize => VillagerHouse1.SIZE;
public override IVillagerHouse ReadVillagerHouse(byte[] data) => new VillagerHouse1(data);
public override int PlayerHouseSize => PlayerHouse1.SIZE;
public override IPlayerHouse ReadPlayerHouse(byte[] data) => new PlayerHouse1(data);
public override int PlayerRoomSize => PlayerHouse1.SIZE;
public override IPlayerRoom ReadPlayerRoom(byte[] data) => new PlayerRoom1(data);
}
}

View File

@ -52,5 +52,10 @@ public class MainSaveOffsets14 : MainSaveOffsets
public override int VillagerHouseSize => VillagerHouse1.SIZE;
public override IVillagerHouse ReadVillagerHouse(byte[] data) => new VillagerHouse1(data);
public override int PlayerHouseSize => PlayerHouse1.SIZE;
public override IPlayerHouse ReadPlayerHouse(byte[] data) => new PlayerHouse1(data);
public override int PlayerRoomSize => PlayerHouse1.SIZE;
public override IPlayerRoom ReadPlayerRoom(byte[] data) => new PlayerRoom1(data);
}
}

View File

@ -52,5 +52,10 @@ public class MainSaveOffsets15 : MainSaveOffsets
public override int VillagerHouseSize => VillagerHouse1.SIZE;
public override IVillagerHouse ReadVillagerHouse(byte[] data) => new VillagerHouse1(data);
public override int PlayerHouseSize => PlayerHouse1.SIZE;
public override IPlayerHouse ReadPlayerHouse(byte[] data) => new PlayerHouse1(data);
public override int PlayerRoomSize => PlayerHouse1.SIZE;
public override IPlayerRoom ReadPlayerRoom(byte[] data) => new PlayerRoom1(data);
}
}

View File

@ -52,5 +52,10 @@ public class MainSaveOffsets16 : MainSaveOffsets
public override int VillagerHouseSize => VillagerHouse1.SIZE;
public override IVillagerHouse ReadVillagerHouse(byte[] data) => new VillagerHouse1(data);
public override int PlayerHouseSize => PlayerHouse1.SIZE;
public override IPlayerHouse ReadPlayerHouse(byte[] data) => new PlayerHouse1(data);
public override int PlayerRoomSize => PlayerHouse1.SIZE;
public override IPlayerRoom ReadPlayerRoom(byte[] data) => new PlayerRoom1(data);
}
}

View File

@ -52,5 +52,10 @@ public class MainSaveOffsets17 : MainSaveOffsets
public override int VillagerHouseSize => VillagerHouse1.SIZE;
public override IVillagerHouse ReadVillagerHouse(byte[] data) => new VillagerHouse1(data);
public override int PlayerHouseSize => PlayerHouse1.SIZE;
public override IPlayerHouse ReadPlayerHouse(byte[] data) => new PlayerHouse1(data);
public override int PlayerRoomSize => PlayerHouse1.SIZE;
public override IPlayerRoom ReadPlayerRoom(byte[] data) => new PlayerRoom1(data);
}
}

View File

@ -53,5 +53,10 @@ public class MainSaveOffsets18 : MainSaveOffsets
public override int VillagerHouseSize => VillagerHouse1.SIZE;
public override IVillagerHouse ReadVillagerHouse(byte[] data) => new VillagerHouse1(data);
public override int PlayerHouseSize => PlayerHouse1.SIZE;
public override IPlayerHouse ReadPlayerHouse(byte[] data) => new PlayerHouse1(data);
public override int PlayerRoomSize => PlayerHouse1.SIZE;
public override IPlayerRoom ReadPlayerRoom(byte[] data) => new PlayerRoom1(data);
}
}

View File

@ -54,5 +54,10 @@ public class MainSaveOffsets19 : MainSaveOffsets
public override int VillagerHouseSize => VillagerHouse1.SIZE;
public override IVillagerHouse ReadVillagerHouse(byte[] data) => new VillagerHouse1(data);
public override int PlayerHouseSize => PlayerHouse1.SIZE;
public override IPlayerHouse ReadPlayerHouse(byte[] data) => new PlayerHouse1(data);
public override int PlayerRoomSize => PlayerHouse1.SIZE;
public override IPlayerRoom ReadPlayerRoom(byte[] data) => new PlayerRoom1(data);
}
}

View File

@ -55,5 +55,10 @@ public class MainSaveOffsets20 : MainSaveOffsets
public override int VillagerHouseSize => VillagerHouse2.SIZE;
public override IVillagerHouse ReadVillagerHouse(byte[] data) => new VillagerHouse2(data);
public override int PlayerHouseSize => PlayerHouse2.SIZE;
public override IPlayerHouse ReadPlayerHouse(byte[] data) => new PlayerHouse2(data);
public override int PlayerRoomSize => PlayerHouse2.SIZE;
public override IPlayerRoom ReadPlayerRoom(byte[] data) => new PlayerRoom2(data);
}
}

View File

@ -0,0 +1,33 @@
namespace NHSE.Core
{
/// <summary>
/// Ambient sound for a house.
/// </summary>
public enum SoundAmbientKind : byte
{
Silence = 0x0,
Rain = 0x1,
Sea = 0x2,
InWater = 0x3,
Wind = 0x4,
Plateau = 0x5,
Jungle = 0x6,
Crowd = 0x7,
Cheers = 0x8,
City = 0x9,
Train = 0xA,
Construction = 0xB,
Space = 0xC,
Echo = 0xD,
Storm = 0xE,
Cave = 0x10,
Earthquake = 0x11,
Squeak = 0x12,
Country = 0x14,
Factory = 0x15,
Cyber = 0x1A,
Healing = 0x1B,
Forest = 0x1C,
Duct = 0x1D,
}
}

View File

@ -59,7 +59,7 @@ private static void Dump(string path, byte[] data, string name)
/// <param name="houses"></param>
/// <param name="players"></param>
/// <param name="path">Path to dump to</param>
public static void DumpPlayerHouses(this IReadOnlyList<PlayerHouse> houses, IReadOnlyList<Player> players, string path)
public static void DumpPlayerHouses(this IReadOnlyList<IPlayerHouse> houses, IReadOnlyList<Player> players, string path)
{
for (int i = 0; i < houses.Count; i++)
{
@ -84,12 +84,12 @@ public static void DumpPlayerHouses(this HorizonSave sav, string path)
}
}
private static void Dump(this PlayerHouse h, string path, IVillagerOrigin p) => h.Dump(p.PlayerName, path);
private static void Dump(this IPlayerHouse h, string path, IVillagerOrigin p) => h.Dump(p.PlayerName, path);
private static void Dump(this PlayerHouse h, string player, string path)
private static void Dump(this IPlayerHouse h, string player, string path)
{
var dest = Path.Combine(path, $"{player}.nhph");
var data = h.Data;
var dest = Path.Combine(path, $"{player}.{h.Extension}");
var data = h.Write();
File.WriteAllBytes(dest, data);
}

View File

@ -8,10 +8,10 @@ public class RoomItemManager
{
public readonly RoomItemLayer[] Layers;
public readonly PlayerRoom Room;
public readonly IPlayerRoom Room;
private const int LayerCount = 8;
public RoomItemManager(PlayerRoom room)
public RoomItemManager(IPlayerRoom room)
{
Layers = room.GetItemLayers();
Room = room;

View File

@ -0,0 +1,16 @@
namespace NHSE.Core
{
public interface IPlayerHouse : IHouseInfo
{
/// <summary> Returns the inner data buffer of the object, flushing any pending changes. </summary>
byte[] Write();
sbyte NPC1 { get; set; }
sbyte NPC2 { get; set; }
string Extension { get; }
short[] GetEventFlags();
void SetEventFlags(short[] value);
IPlayerRoom GetRoom(int v);
void SetRoom(int roomIndex, IPlayerRoom room);
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace NHSE.Core
{
public interface IPlayerRoom
{
/// <summary> Returns the inner data buffer of the object, flushing any pending changes. </summary>
byte[] Write();
string Extension { get; }
RoomItemLayer[] GetItemLayers();
void SetItemLayers(IReadOnlyList<RoomItemLayer> value);
IPlayerRoom Upgrade();
}
}

View File

@ -2,12 +2,15 @@
namespace NHSE.Core
{
public class PlayerHouse : IHouseInfo
public class PlayerHouse1 : IPlayerHouse
{
public const int SIZE = 0x26400;
public virtual string Extension => "nhph";
public readonly byte[] Data;
public PlayerHouse(byte[] data) => Data = data;
public PlayerHouse1(byte[] data) => Data = data;
public byte[] Write() => Data;
public uint HouseLevel { get => BitConverter.ToUInt32(Data, 0x00); set => BitConverter.GetBytes(value).CopyTo(Data, 0x00); }
public WallType WallUniqueID { get => (WallType)BitConverter.ToUInt16(Data, 0x04); set => BitConverter.GetBytes((ushort)value).CopyTo(Data, 0x04); }
@ -39,29 +42,29 @@ public short[] GetEventFlags()
public void SetEventFlags(short[] value) => Buffer.BlockCopy(value, 0, Data, 0x20, value.Length * sizeof(short));
public const int MaxRoom = 6;
private const int RoomStart = 0x120;
public const int RoomStart = 0x120;
public PlayerRoom GetRoom(int roomIndex)
public IPlayerRoom GetRoom(int roomIndex)
{
if ((uint)roomIndex >= MaxRoom)
throw new ArgumentOutOfRangeException(nameof(roomIndex));
var data = Data.Slice(RoomStart + (roomIndex * PlayerRoom.SIZE), PlayerRoom.SIZE);
return new PlayerRoom(data);
var data = Data.Slice(RoomStart + (roomIndex * PlayerRoom1.SIZE), PlayerRoom1.SIZE);
return new PlayerRoom1(data);
}
public void SetRoom(int roomIndex, PlayerRoom room)
public void SetRoom(int roomIndex, IPlayerRoom room)
{
if ((uint)roomIndex >= MaxRoom)
throw new ArgumentOutOfRangeException(nameof(roomIndex));
room.Data.CopyTo(Data, RoomStart + (roomIndex * PlayerRoom.SIZE));
room.Write().CopyTo(Data, RoomStart + (roomIndex * PlayerRoom1.SIZE));
}
public sbyte NPC1 { get => (sbyte)Data[0x263D0]; set => Data[0x263D0] = (byte)value; }
public sbyte NPC2 { get => (sbyte)Data[0x263D1]; set => Data[0x263D1] = (byte)value; }
// 2bytes padding
// 2 bytes padding
public Item DoorDecoItemName
{
@ -84,5 +87,15 @@ public Item OrderPostItemName
}
// cockroach @ 0x263f0 -- meh
public PlayerHouse2 Upgrade()
{
var data = new byte[PlayerHouse2.SIZE];
Data.Slice(0x0, 0x120).CopyTo(data, 0); // HouseLevel -> EventFlag
for (int i = 0; i < MaxRoom; i++)
GetRoom(i).Upgrade().Write().CopyTo(data, 0x120 + i * PlayerRoom2.SIZE); // RoomList
Data.Slice(0x263D0, 0x30).CopyTo(data, 0x289F8); // PlayerList -> Cockroach
return new PlayerHouse2(data);
}
}
}

View File

@ -0,0 +1,66 @@
using System;
namespace NHSE.Core
{
public class PlayerHouse2 : PlayerHouse1
{
public new const int SIZE = 0x28A28;
public override string Extension => "nhph2";
public PlayerHouse2(byte[] data) : base(data) { }
public new PlayerRoom2 GetRoom(int roomIndex)
{
if ((uint)roomIndex >= MaxRoom)
throw new ArgumentOutOfRangeException(nameof(roomIndex));
var data = Data.Slice(RoomStart + (roomIndex * PlayerRoom2.SIZE), PlayerRoom2.SIZE);
return new PlayerRoom2(data);
}
public void SetRoom(int roomIndex, PlayerRoom2 room)
{
if ((uint)roomIndex >= MaxRoom)
throw new ArgumentOutOfRangeException(nameof(roomIndex));
room.Data.CopyTo(Data, RoomStart + (roomIndex * PlayerRoom2.SIZE));
}
public new sbyte NPC1 { get => (sbyte)Data[0x289F8]; set => Data[0x289F8] = (byte)value; }
public new sbyte NPC2 { get => (sbyte)Data[0x289F9]; set => Data[0x289F9] = (byte)value; }
// 2 bytes padding
public new Item DoorDecoItemName
{
get => Data.Slice(0x289FC, 8).ToClass<Item>();
set => value.ToBytesClass().CopyTo(Data, 0x289FC);
}
public new bool PlayerHouseFlag { get => Data[0x28A04] != 0; set => Data[0x28A04] = (byte)(value ? 1 : 0); }
public new Item PostItemName
{
get => Data.Slice(0x28A08, 8).ToClass<Item>();
set => value.ToBytesClass().CopyTo(Data, 0x28A08);
}
public new Item OrderPostItemName
{
get => Data.Slice(0x28A10, 8).ToClass<Item>();
set => value.ToBytesClass().CopyTo(Data, 0x28A10);
}
// cockroach @ 0x28A18 -- meh
public PlayerHouse1 Downgrade()
{
var data = new byte[PlayerHouse1.SIZE];
Data.Slice(0x0, 0x120).CopyTo(data, 0); // HouseLevel -> EventFlag
for (int i = 0; i < MaxRoom; i++)
GetRoom(i).Downgrade().Data.CopyTo(data, 0x120 + i * PlayerRoom2.SIZE); // RoomList
Data.Slice(0x289F8, 0x30).CopyTo(data, 0x263D0); // PlayerList -> Cockroach
return new PlayerHouse1(data);
}
}
}

View File

@ -0,0 +1,73 @@
namespace NHSE.Core
{
/// <summary>
/// Converts Player House objects to different revisions.
/// </summary>
public static class PlayerHouseConverter
{
/// <summary>
/// Checks to see if the <see cref="size"/> matches any of the House object sizes.
/// </summary>
/// <param name="size">Size of the byte array that might represent a House object.</param>
/// <returns>True if it matches any size.</returns>
public static bool IsHouse(int size)
{
return size == PlayerHouse1.SIZE || size == PlayerHouse2.SIZE;
}
/// <summary>
/// Checks to see if the input <see cref="size"/> can be converted to the <see cref="expect"/> size.
/// </summary>
/// <param name="size">Current house file size</param>
/// <param name="expect">Expected house file size</param>
/// <returns>True if can be converted, false if no conversion available.</returns>
public static bool IsCompatible(int size, int expect)
{
return expect switch
{
// Can convert to any format
PlayerHouse1.SIZE or PlayerHouse2.SIZE => IsHouse(size),
// No conversion available
_ => false,
};
}
/// <summary>
/// Converts the House data format to another format.
/// </summary>
/// <remarks>
/// Before calling this method, check that a conversion method exists via <see cref="IsCompatible"/> and that the length of the <see cref="data"/> is not the same as what you <see cref="expect"/>.
/// If the sizes are the same, it will return the input <see cref="data"/>.
/// If no conversion path exists, it will return the input <see cref="data"/>.
/// </remarks>
/// <param name="data">Current format</param>
/// <param name="expect">Target size</param>
/// <returns>Converted data</returns>
public static byte[] GetCompatible(byte[] data, int expect)
{
if (data.Length == expect)
return data;
return expect switch
{
PlayerHouse1.SIZE when data.Length == PlayerHouse2.SIZE => Convert21(data),
PlayerHouse2.SIZE when data.Length == PlayerHouse1.SIZE => Convert12(data),
_ => data,
};
}
/// <summary>
/// Converts a <see cref="PlayerHouse1"/> object byte array to a <see cref="PlayerHouse2"/>
/// </summary>
/// <param name="h1"><see cref="PlayerHouse1"/> object byte array</param>
/// <returns><see cref="PlayerHouse2"/> object byte array</returns>
private static byte[] Convert12(byte[] h1) => new PlayerHouse1(h1).Upgrade().Data;
/// <summary>
/// Converts a <see cref="PlayerHouse2"/> object byte array to a <see cref="PlayerHouse1"/>
/// </summary>
/// <param name="h2"><see cref="PlayerHouse2"/> object byte array</param>
/// <returns><see cref="PlayerHouse1"/> object byte array</returns>
private static byte[] Convert21(byte[] h2) => new PlayerHouse2(h2).Downgrade().Data;
}
}

View File

@ -2,12 +2,15 @@
namespace NHSE.Core
{
public class PlayerRoom
public class PlayerRoom1 : IPlayerRoom
{
public const int SIZE = 0x65C8;
public virtual string Extension => "nhpr";
public readonly byte[] Data;
public PlayerRoom(byte[] data) => Data = data;
public PlayerRoom1(byte[] data) => Data = data;
public byte[] Write() => Data;
/*
s_d8bc748b ItemLayerList[8]; // @0x0 size 0xc80, align 4
@ -34,5 +37,12 @@ public GSaveRoomFloorWall RoomFloorWall
get => Data.Slice(0x65A4, GSaveRoomFloorWall.SIZE).ToStructure<GSaveRoomFloorWall>();
set => value.ToBytes().CopyTo(Data, 0x65A4);
}
public IPlayerRoom Upgrade()
{
var data = new byte[PlayerRoom2.SIZE];
Data.CopyTo(data, 0);
return new PlayerRoom2(data);
}
}
}

View File

@ -0,0 +1,91 @@
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace NHSE.Core
{
public class PlayerRoom2 : PlayerRoom1
{
public new const int SIZE = 0x6C24;
public new virtual string Extension => "nhpr2";
public PlayerRoom2(byte[] data) : base(data) { }
/*
s_665e9093 ExtraEffectLayerList[2]; // @0x65c8 size 0x320, align 2
GSaveMusicBoxInfo MusicBoxInfo; // @0x6c08 size 0x4, align 2
u8 SoundAmbient; // @0x6c0c size 0x1, align 1
u8 gap_6c0d[3];
s_e13a81f4 _cfb139b9; // @0x6c10 size 0x14, align 4
*/
public s_665e9093[] ExtraEffectLayerList
{
get
{
var result = new s_665e9093[2];
for (int i = 0; i < result.Length; i++)
{
var slice = Data.Slice(0x65C8 + i * s_665e9093.SIZE, s_665e9093.SIZE);
result[i] = slice.ToStructure<s_665e9093>();
}
return result;
}
set => value.ToBytesClass().CopyTo(Data, 0x65C8);
}
public GSaveMusicBoxInfo MusicBoxInfo
{
get => Data.Slice(0x6C08, GSaveMusicBoxInfo.SIZE).ToStructure<GSaveMusicBoxInfo>();
set => value.ToBytes().CopyTo(Data, 0x6C08);
}
public SoundAmbientKind SoundAmbientUniqueID { get => (SoundAmbientKind)Data[0x6C0C]; set => Data[0x6C0C]= (byte)value; }
// 3 bytes padding
public s_e13a81f4 _cfb139b9
{
get => Data.Slice(0x6C10, s_e13a81f4.SIZE).ToStructure<s_e13a81f4>();
set => value.ToBytes().CopyTo(Data, 0x6C10);
}
public PlayerRoom1 Downgrade()
{
var result = new byte[PlayerRoom1.SIZE];
Data.CopyTo(result, 0);
return new PlayerRoom1(result);
}
}
#pragma warning disable CS8618, CA1815, CA1819, IDE1006, RCS1170
[StructLayout(LayoutKind.Sequential, Pack = 2, Size = SIZE)]
[TypeConverter(typeof(ValueTypeTypeConverter))]
public struct s_665e9093
{
public const int SIZE = 0x320; /* 0x320 big, align 2 */
[field: MarshalAs(UnmanagedType.LPArray, SizeConst = 400)]
public GSaveItemExtraEffect[] ItemExtraEffectBuffer { get; set; } // @0x0 size 0x2, align 2
}
[StructLayout(LayoutKind.Sequential, Pack = 2, Size = SIZE)]
[TypeConverter(typeof(ValueTypeTypeConverter))]
public struct GSaveItemExtraEffect
{
public const int SIZE = 0x2; /* 0x2 big, align 2 */
public ushort InnerData { get; set; } // @0x0 size 0x2, align 2
}
[StructLayout(LayoutKind.Sequential, Pack = 2, Size = SIZE)]
[TypeConverter(typeof(ValueTypeTypeConverter))]
public struct GSaveMusicBoxInfo
{
public const int SIZE = 0x4; /* 0x4 big, align 2 */
public ushort PlayingAudioMusicID { get; set; } // @0x0 size 0x2, align 2
public byte _749b78c2 { get; set; } // @0x2 size 0x1, align 1
}
}

View File

@ -0,0 +1,73 @@
namespace NHSE.Core
{
/// <summary>
/// Converts Player Room objects to different revisions.
/// </summary>
public static class PlayerRoomConverter
{
/// <summary>
/// Checks to see if the <see cref="size"/> matches any of the Room object sizes.
/// </summary>
/// <param name="size">Size of the byte array that might represent a Room object.</param>
/// <returns>True if it matches any size.</returns>
public static bool IsRoom(int size)
{
return size == PlayerRoom1.SIZE || size == PlayerRoom2.SIZE;
}
/// <summary>
/// Checks to see if the input <see cref="size"/> can be converted to the <see cref="expect"/> size.
/// </summary>
/// <param name="size">Current room file size</param>
/// <param name="expect">Expected room file size</param>
/// <returns>True if can be converted, false if no conversion available.</returns>
public static bool IsCompatible(int size, int expect)
{
return expect switch
{
// Can convert to any format
PlayerRoom1.SIZE or PlayerRoom2.SIZE => IsRoom(size),
// No conversion available
_ => false,
};
}
/// <summary>
/// Converts the Room data format to another format.
/// </summary>
/// <remarks>
/// Before calling this method, check that a conversion method exists via <see cref="IsCompatible"/> and that the length of the <see cref="data"/> is not the same as what you <see cref="expect"/>.
/// If the sizes are the same, it will return the input <see cref="data"/>.
/// If no conversion path exists, it will return the input <see cref="data"/>.
/// </remarks>
/// <param name="data">Current format</param>
/// <param name="expect">Target size</param>
/// <returns>Converted data</returns>
public static byte[] GetCompatible(byte[] data, int expect)
{
if (data.Length == expect)
return data;
return expect switch
{
PlayerRoom1.SIZE when data.Length == PlayerRoom2.SIZE => Convert21(data),
PlayerRoom2.SIZE when data.Length == PlayerRoom1.SIZE => Convert12(data),
_ => data,
};
}
/// <summary>
/// Converts a <see cref="PlayerRoom1"/> object byte array to a <see cref="PlayerRoom2"/>
/// </summary>
/// <param name="h1"><see cref="PlayerRoom1"/> object byte array</param>
/// <returns><see cref="PlayerRoom2"/> object byte array</returns>
private static byte[] Convert12(byte[] h1) => new PlayerRoom1(h1).Upgrade().Write();
/// <summary>
/// Converts a <see cref="PlayerRoom2"/> object byte array to a <see cref="PlayerRoom1"/>
/// </summary>
/// <param name="h2"><see cref="PlayerRoom2"/> object byte array</param>
/// <returns><see cref="PlayerRoom1"/> object byte array</returns>
private static byte[] Convert21(byte[] h2) => new PlayerRoom2(h2).Downgrade().Write();
}
}

View File

@ -470,7 +470,7 @@ private void B_EditDesignsTailor_Click(object sender, EventArgs e)
private void B_EditPlayerHouses_Click(object sender, EventArgs e)
{
var houses = SAV.Main.GetPlayerHouses();
using var editor = new PlayerHouseEditor(houses, SAV.Players, PlayerIndex);
using var editor = new PlayerHouseEditor(houses, SAV.Players, SAV.Main, PlayerIndex);
if (editor.ShowDialog() == DialogResult.OK)
SAV.Main.SetPlayerHouses(houses);
}

View File

@ -96,7 +96,7 @@ public static bool LoadMuseum(Museum museum)
return true;
}
public static void DumpHouse(IReadOnlyList<Player> players, IReadOnlyList<PlayerHouse> houses, int index, bool dumpAll)
public static void DumpHouse(IReadOnlyList<Player> players, IReadOnlyList<IPlayerHouse> houses, int index, bool dumpAll)
{
if (dumpAll)
DumpAllPlayerHouses(houses, players);
@ -104,23 +104,25 @@ public static void DumpHouse(IReadOnlyList<Player> players, IReadOnlyList<Player
DumpPlayerHouse(players, houses, index);
}
private static void DumpPlayerHouse(IReadOnlyList<Player> players, IReadOnlyList<PlayerHouse> houses, int index)
private static void DumpPlayerHouse(IReadOnlyList<Player> players, IReadOnlyList<IPlayerHouse> houses, int index)
{
var house = houses[index];
var name = PlayerHouseEditor.GetHouseSummary(players, house, index);
using var sfd = new SaveFileDialog
{
Filter = "New Horizons Player House (*.nhph)|*.nhph|All files (*.*)|*.*",
FileName = $"{name}.nhph",
Filter = "New Horizons Player House (*.nhph)|*.nhph|" +
"New Horizons Player House (*.nhph2)|*.nhph2|" +
"All files (*.*)|*.*",
FileName = $"{name}.{house.Extension}",
};
if (sfd.ShowDialog() != DialogResult.OK)
return;
var data = house.Data;
var data = house.Write();
File.WriteAllBytes(sfd.FileName, data);
}
private static void DumpAllPlayerHouses(IReadOnlyList<PlayerHouse> houses, IReadOnlyList<Player> players)
private static void DumpAllPlayerHouses(IReadOnlyList<IPlayerHouse> houses, IReadOnlyList<Player> players)
{
using var fbd = new FolderBrowserDialog();
if (fbd.ShowDialog() != DialogResult.OK)
@ -132,69 +134,90 @@ private static void DumpAllPlayerHouses(IReadOnlyList<PlayerHouse> houses, IRead
houses.DumpPlayerHouses(players, fbd.SelectedPath);
}
public static bool LoadHouse(IReadOnlyList<Player> players, PlayerHouse[] houses, int index)
public static bool LoadHouse(MainSaveOffsets offsets, IReadOnlyList<Player> players, IPlayerHouse[] houses, int index)
{
var h = houses[index];
var name = PlayerHouseEditor.GetHouseSummary(players, houses[index], index);
using var ofd = new OpenFileDialog
{
Filter = "New Horizons Player House (*.nhph)|*.nhph|All files (*.*)|*.*",
FileName = $"{name}.nhph",
Filter = "New Horizons Player House (*.nhph)|*.nhph|" +
"New Horizons Player House (*.nhph2)|*.nhph2|" +
"All files (*.*)|*.*",
FileName = $"{name}.{h.Extension}",
};
if (ofd.ShowDialog() != DialogResult.OK)
return false;
var file = ofd.FileName;
var fi = new FileInfo(file);
const int expectLength = PlayerHouse.SIZE;
if (fi.Length != expectLength)
var path = ofd.FileName;
var expectLength = offsets.PlayerHouseSize;
var fi = new FileInfo(path);
if (!VillagerHouseConverter.IsCompatible((int)fi.Length, expectLength))
{
WinFormsUtil.Error(MessageStrings.MsgCanceling, string.Format(MessageStrings.MsgDataSizeMismatchImport, fi.Length, expectLength));
WinFormsUtil.Error(string.Format(MessageStrings.MsgDataSizeMismatchImport, fi.Length, expectLength), path);
return false;
}
var data = File.ReadAllBytes(file);
var h = new PlayerHouse(data);
var data = File.ReadAllBytes(ofd.FileName);
data = PlayerHouseConverter.GetCompatible(data, expectLength);
if (fi.Length != expectLength)
{
WinFormsUtil.Error(MessageStrings.MsgCanceling, string.Format(MessageStrings.MsgDataSizeMismatchImport, fi.Length, expectLength), path);
return false;
}
h = offsets.ReadPlayerHouse(data);
var current = houses[index];
h.NPC1 = current.NPC1;
houses[index] = h;
return true;
}
public static void DumpRoom(PlayerRoom room, int index)
public static void DumpRoom(IPlayerRoom room, int index)
{
using var sfd = new SaveFileDialog
{
Filter = "New Horizons Player House Room (*.nhpr)|*.nhpr|All files (*.*)|*.*",
FileName = $"Room {index + 1}.nhpr",
Filter = "New Horizons Player House Room (*.nhpr)|*.nhpr|" +
"New Horizons Player House Room (*.nhpr2)|*.nhpr2|" +
"All files (*.*)|*.*",
FileName = $"Room {index + 1}.{room.Extension}",
};
if (sfd.ShowDialog() != DialogResult.OK)
return;
var data = room.Data;
var data = room.Write();
File.WriteAllBytes(sfd.FileName, data);
}
public static bool LoadRoom(PlayerRoom room, int index)
public static bool LoadRoom(MainSaveOffsets offsets, IPlayerRoom room, int index)
{
using var ofd = new OpenFileDialog
{
Filter = "New Horizons Player House Room (*.nhpr)|*.nhpr|All files (*.*)|*.*",
FileName = $"Room {index + 1}.nhpr",
Filter = "New Horizons Player House Room (*.nhpr)|*.nhpr|" +
"New Horizons Player House Room (*.nhpr2)|*.nhpr2|" +
"All files (*.*)|*.*",
FileName = $"Room {index + 1}.{room.Extension}",
};
if (ofd.ShowDialog() != DialogResult.OK)
return false;
var file = ofd.FileName;
var fi = new FileInfo(file);
const int expectLength = PlayerRoom.SIZE;
if (fi.Length != expectLength)
var path = ofd.FileName;
var expectLength = offsets.PlayerRoomSize;
var fi = new FileInfo(path);
if (!PlayerRoomConverter.IsCompatible((int)fi.Length, expectLength))
{
WinFormsUtil.Error(MessageStrings.MsgCanceling, string.Format(MessageStrings.MsgDataSizeMismatchImport, fi.Length, expectLength));
WinFormsUtil.Error(string.Format(MessageStrings.MsgDataSizeMismatchImport, fi.Length, expectLength), path);
return false;
}
var data = File.ReadAllBytes(file);
data.CopyTo(room.Data, 0);
var data = File.ReadAllBytes(ofd.FileName);
data = PlayerRoomConverter.GetCompatible(data, offsets.PlayerRoomSize);
if (fi.Length != expectLength)
{
WinFormsUtil.Error(MessageStrings.MsgCanceling, string.Format(MessageStrings.MsgDataSizeMismatchImport, fi.Length, expectLength), path);
return false;
}
room = offsets.ReadPlayerRoom(data);
return true;
}

View File

@ -9,7 +9,8 @@ namespace NHSE.WinForms
{
public partial class PlayerHouseEditor : Form
{
private readonly PlayerHouse[] Houses;
private readonly MainSave SAV;
private readonly IPlayerHouse[] Houses;
private readonly IReadOnlyList<Player> Players;
private RoomItemManager Manager;
private const int scale = 24;
@ -17,10 +18,11 @@ public partial class PlayerHouseEditor : Form
private int Index = -1;
private int RoomIndex = -1;
public PlayerHouseEditor(PlayerHouse[] houses, IReadOnlyList<Player> players, int index)
public PlayerHouseEditor(IPlayerHouse[] houses, IReadOnlyList<Player> players, MainSave sav, int index)
{
InitializeComponent();
this.TranslateInterface(GameInfo.CurrentLanguage);
SAV = sav;
Houses = houses;
Players = players;
Manager = new RoomItemManager(houses[0].GetRoom(0));
@ -73,7 +75,7 @@ private void PG_Item_PropertyValueChanged(object s, PropertyValueChangedEventArg
LB_Items.Items[Index] = GetHouseSummary(Players, Houses[Index], Index);
}
public static string GetHouseSummary(IReadOnlyList<Player> players, PlayerHouse house, int index)
public static string GetHouseSummary(IReadOnlyList<Player> players, IPlayerHouse house, int index)
{
var houseName = index >= players.Count ? $"House {index}" : $"{players[index].Personal.PlayerName}'s House";
return $"{houseName} (lv {house.HouseLevel})";
@ -86,7 +88,7 @@ private void B_DumpHouse_Click(object sender, EventArgs e)
private void B_LoadHouse_Click(object sender, EventArgs e)
{
if (!MiscDumpHelper.LoadHouse(Players, Houses, Index))
if (!MiscDumpHelper.LoadHouse(SAV.Offsets, Players, Houses, Index))
return;
RoomIndex = -1;
@ -141,14 +143,14 @@ private static void GetCoordinates(MouseEventArgs e, out int x, out int y)
y = e.Y / scale;
}
private void ChangeRoom(PlayerHouse house)
private void ChangeRoom(IPlayerHouse house)
{
RoomIndex = (int) NUD_Room.Value - 1;
ReloadManager(house);
DrawLayer();
}
private void ReloadManager(PlayerHouse house)
private void ReloadManager(IPlayerHouse house)
{
var unsupported = Manager.GetUnsupportedTiles();
if (unsupported.Count != 0)
@ -300,7 +302,7 @@ private void DeleteTile(Item tile, int x, int y)
private void B_LoadRoom_Click(object sender, EventArgs e)
{
var room = Manager.Room;
MiscDumpHelper.LoadRoom(room, RoomIndex);
MiscDumpHelper.LoadRoom(SAV.Offsets, room, RoomIndex);
var house = Houses[Index];
house.SetRoom(RoomIndex, room);