mirror of
https://github.com/kwsch/NHSE.git
synced 2026-04-25 07:37:02 -05:00
Merge branch 'master' into tileshift
This commit is contained in:
commit
f443add432
|
|
@ -1,32 +1,24 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using static System.Buffers.Binary.BinaryPrimitives;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public sealed class EncryptedInt32
|
||||
/// <summary>
|
||||
/// Represents an encrypted 32-bit integer with associated encryption parameters.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Value}")]
|
||||
public sealed record EncryptedInt32(uint OriginalEncrypted, ushort Adjust = 0, byte Shift = 0, byte Checksum = 0)
|
||||
{
|
||||
// Encryption constant used to encrypt the int.
|
||||
private const uint ENCRYPTION_CONSTANT = 0x80E32B11;
|
||||
// Base shift count used in the encryption.
|
||||
private const byte SHIFT_BASE = 3;
|
||||
|
||||
public readonly uint OriginalEncrypted;
|
||||
public readonly ushort Adjust;
|
||||
public readonly byte Shift;
|
||||
public readonly byte Checksum;
|
||||
public uint Value { get; set; } = Decrypt(OriginalEncrypted, Shift, Adjust);
|
||||
|
||||
public uint Value;
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
|
||||
public EncryptedInt32(uint encryptedValue, ushort adjust = 0, byte shift = 0, byte checksum = 0)
|
||||
{
|
||||
OriginalEncrypted = encryptedValue;
|
||||
Adjust = adjust;
|
||||
Shift = shift;
|
||||
Checksum = checksum;
|
||||
Value = Decrypt(encryptedValue, shift, adjust);
|
||||
}
|
||||
public static implicit operator uint(EncryptedInt32 encryptedInt32) => encryptedInt32.Value;
|
||||
|
||||
public void Write(Span<byte> data) => Write(this, data);
|
||||
public void Write(byte[] data, int offset) => Write(data.AsSpan(offset));
|
||||
|
|
@ -41,16 +33,14 @@ public static byte CalculateChecksum(uint value)
|
|||
|
||||
public static uint Decrypt(uint encrypted, byte shift, ushort adjust)
|
||||
{
|
||||
// Decrypt the encrypted int using the given params.
|
||||
ulong val = ((ulong) encrypted) << ((32 - SHIFT_BASE - shift) & 0x3F);
|
||||
val += val >> 32;
|
||||
return ENCRYPTION_CONSTANT - adjust + (uint)val;
|
||||
var rotated = BitOperations.RotateRight(encrypted, shift + SHIFT_BASE);
|
||||
return rotated + ENCRYPTION_CONSTANT - adjust;
|
||||
}
|
||||
|
||||
public static uint Encrypt(uint value, byte shift, ushort adjust)
|
||||
{
|
||||
ulong val = (ulong) (value + unchecked(adjust - ENCRYPTION_CONSTANT)) << (shift + SHIFT_BASE);
|
||||
return (uint) ((val >> 32) + val);
|
||||
var adjusted = value + adjust - ENCRYPTION_CONSTANT;
|
||||
return BitOperations.RotateLeft(adjusted, shift + SHIFT_BASE);
|
||||
}
|
||||
|
||||
public static EncryptedInt32 ReadVerify(ReadOnlySpan<byte> data, int offset)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace NHSE.Core;
|
|||
public sealed class MainSave : EncryptedFilePair
|
||||
{
|
||||
public readonly MainSaveOffsets Offsets;
|
||||
public MainSave(string folder) : base(folder, "main") => Offsets = MainSaveOffsets.GetOffsets(Info);
|
||||
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; }
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace NHSE.Core;
|
|||
public sealed class Personal : EncryptedFilePair, IVillagerOrigin
|
||||
{
|
||||
public readonly PersonalOffsets Offsets;
|
||||
public Personal(string folder) : base(folder, "personal") => Offsets = PersonalOffsets.GetOffsets(Info);
|
||||
public Personal(ISaveFileProvider provider) : base(provider, "personal") => Offsets = PersonalOffsets.GetOffsets(Info);
|
||||
public override string ToString() => PlayerName;
|
||||
|
||||
public uint TownID
|
||||
|
|
@ -173,5 +173,10 @@ public bool ProfileIsMakeVillage
|
|||
set => Data[Offsets.ProfileIsMakeVillage] = (byte)(value ? 1 : 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appended structure added in 3.0.0 for Hotel data.
|
||||
/// </summary>
|
||||
public Personal30? Data30 => (Offsets as IPersonal30)?.Get30s_064c1881(Raw);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
@ -3,7 +3,4 @@
|
|||
/// <summary>
|
||||
/// photo_studio_island.dat
|
||||
/// </summary>
|
||||
public sealed class PhotoStudioIsland : EncryptedFilePair
|
||||
{
|
||||
public PhotoStudioIsland(string folder) : base(folder, "photo_studio_island") { }
|
||||
}
|
||||
public sealed class PhotoStudioIsland(ISaveFileProvider provider) : EncryptedFilePair(provider, "photo_studio_island");
|
||||
|
|
@ -3,7 +3,4 @@
|
|||
/// <summary>
|
||||
/// postbox.dat
|
||||
/// </summary>
|
||||
public sealed class PostBox : EncryptedFilePair
|
||||
{
|
||||
public PostBox(string folder) : base(folder, "postbox") { }
|
||||
}
|
||||
public sealed class PostBox(ISaveFileProvider provider) : EncryptedFilePair(provider, "postbox");
|
||||
|
|
@ -3,9 +3,7 @@
|
|||
/// <summary>
|
||||
/// profile.dat
|
||||
/// </summary>
|
||||
public sealed class Profile : EncryptedFilePair
|
||||
{
|
||||
public Profile(string folder) : base(folder, "profile") { }
|
||||
|
||||
// pretty much just a jpeg -- which is also stored in Personal.
|
||||
}
|
||||
/// <remarks>
|
||||
/// pretty much just a jpeg -- which is also stored in Personal.
|
||||
/// </remarks>
|
||||
public sealed class Profile(ISaveFileProvider provider) : EncryptedFilePair(provider, "profile");
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Data structures stored for the HappyHomeDesigner DLC.
|
||||
/// </summary>
|
||||
public sealed class WhereAreN : EncryptedFilePair
|
||||
{
|
||||
public const string FileName = "wherearen";
|
||||
|
||||
public readonly WhereAreNOffsets Offsets;
|
||||
public WhereAreN(string folder) : base(folder, FileName) => Offsets = WhereAreNOffsets.GetOffsets(Info);
|
||||
public WhereAreN(ISaveFileProvider provider) : base(provider, FileName) => Offsets = WhereAreNOffsets.GetOffsets(Info);
|
||||
|
||||
public EncryptedInt32 Poki
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,53 +1,50 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using static System.Buffers.Binary.BinaryPrimitives;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents two files -- <see cref="DataPath"/> and <see cref="HeaderPath"/> and their decrypted data.
|
||||
/// Represents two files -- <see cref="NameData"/> and <see cref="NameHeader"/> and their decrypted data.
|
||||
/// </summary>
|
||||
public abstract class EncryptedFilePair
|
||||
{
|
||||
private readonly byte[] RawData;
|
||||
private readonly byte[] RawHeader;
|
||||
private readonly ISaveFileProvider Provider;
|
||||
|
||||
protected Memory<byte> Raw => RawData;
|
||||
public Span<byte> Data => RawData;
|
||||
public Span<byte> Header => RawHeader;
|
||||
|
||||
public readonly FileHeaderInfo Info;
|
||||
|
||||
public readonly string DataPath;
|
||||
public readonly string HeaderPath;
|
||||
public readonly string NameData;
|
||||
public readonly string NameHeader;
|
||||
|
||||
public static bool Exists(string folder, string name)
|
||||
/// <summary>
|
||||
/// Checks if the file pair exists in the specified provider.
|
||||
/// </summary>
|
||||
public static bool Exists(ISaveFileProvider provider, string name)
|
||||
{
|
||||
var NameData = $"{name}.dat";
|
||||
var NameHeader = $"{name}Header.dat";
|
||||
var hdr = Path.Combine(folder, NameHeader);
|
||||
var dat = Path.Combine(folder, NameData);
|
||||
return File.Exists(hdr) && File.Exists(dat);
|
||||
var nameData = $"{name}.dat";
|
||||
var nameHeader = $"{name}Header.dat";
|
||||
return provider.FileExists(nameHeader) && provider.FileExists(nameData);
|
||||
}
|
||||
|
||||
protected EncryptedFilePair(string folder, string name)
|
||||
protected EncryptedFilePair(ISaveFileProvider provider, string name)
|
||||
{
|
||||
Provider = provider;
|
||||
NameData = $"{name}.dat";
|
||||
NameHeader = $"{name}Header.dat";
|
||||
var hdr = Path.Combine(folder, NameHeader);
|
||||
var dat = Path.Combine(folder, NameData);
|
||||
|
||||
var hd = File.ReadAllBytes(hdr);
|
||||
var md = File.ReadAllBytes(dat);
|
||||
var hd = provider.ReadFile(NameHeader);
|
||||
var md = provider.ReadFile(NameData);
|
||||
|
||||
Encryption.Decrypt(hd, md);
|
||||
|
||||
RawHeader = hd;
|
||||
RawData = md;
|
||||
DataPath = dat;
|
||||
HeaderPath = hdr;
|
||||
|
||||
Info = RawHeader[..FileHeaderInfo.SIZE].ToClass<FileHeaderInfo>();
|
||||
}
|
||||
|
|
@ -55,10 +52,11 @@ protected EncryptedFilePair(string folder, string name)
|
|||
public void Save(uint seed)
|
||||
{
|
||||
var encrypt = Encryption.Encrypt(RawData, seed, RawHeader);
|
||||
File.WriteAllBytes(DataPath, encrypt.Data.Span);
|
||||
File.WriteAllBytes(HeaderPath, encrypt.Header.Span);
|
||||
Provider.WriteFile(NameData, encrypt.Data.Span);
|
||||
Provider.WriteFile(NameHeader, encrypt.Header.Span);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Updates all hashes of <see cref="Data"/>.
|
||||
/// </summary>
|
||||
|
|
|
|||
52
NHSE.Core/Save/Meta/FolderSaveFileProvider.cs
Normal file
52
NHSE.Core/Save/Meta/FolderSaveFileProvider.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Provides file access from a filesystem folder.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Creates a provider rooted at the specified folder path.
|
||||
/// </remarks>
|
||||
/// <param name="rootPath">Absolute path to the save folder.</param>
|
||||
public sealed class FolderSaveFileProvider(string rootPath) : ISaveFileProvider
|
||||
{
|
||||
public byte[] ReadFile(string relativePath)
|
||||
{
|
||||
var fullPath = Path.Combine(rootPath, relativePath);
|
||||
return File.ReadAllBytes(fullPath);
|
||||
}
|
||||
|
||||
public void WriteFile(string relativePath, ReadOnlySpan<byte> data)
|
||||
{
|
||||
var fullPath = Path.Combine(rootPath, relativePath);
|
||||
File.WriteAllBytes(fullPath, data);
|
||||
}
|
||||
|
||||
public bool FileExists(string relativePath)
|
||||
{
|
||||
var fullPath = Path.Combine(rootPath, relativePath);
|
||||
return File.Exists(fullPath);
|
||||
}
|
||||
|
||||
public string[] GetDirectories(string searchPattern)
|
||||
{
|
||||
var dirs = Directory.GetDirectories(rootPath, searchPattern, SearchOption.TopDirectoryOnly);
|
||||
var result = new string[dirs.Length];
|
||||
for (int i = 0; i < dirs.Length; i++)
|
||||
result[i] = new DirectoryInfo(dirs[i]).Name;
|
||||
return result;
|
||||
}
|
||||
|
||||
public ISaveFileProvider GetSubdirectoryProvider(string subdirectory)
|
||||
{
|
||||
var subPath = Path.Combine(rootPath, subdirectory);
|
||||
return new FolderSaveFileProvider(subPath);
|
||||
}
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
// No-op for folder provider; writes are immediate.
|
||||
}
|
||||
}
|
||||
|
|
@ -7,16 +7,38 @@ namespace NHSE.Core;
|
|||
/// <summary>
|
||||
/// Represents all saved data that is stored on the device for the New Horizon's game.
|
||||
/// </summary>
|
||||
public class HorizonSave
|
||||
/// <remarks>
|
||||
/// Creates a HorizonSave from a file provider.
|
||||
/// </remarks>
|
||||
/// <param name="provider">Provider for reading/writing save files.</param>
|
||||
public class HorizonSave(ISaveFileProvider provider)
|
||||
{
|
||||
public readonly MainSave Main;
|
||||
public readonly Player[] Players;
|
||||
public readonly MainSave Main = new(provider);
|
||||
public readonly Player[] Players = Player.ReadMany(provider);
|
||||
private readonly ISaveFileProvider Provider = provider;
|
||||
|
||||
public override string ToString() => $"{Players[0].Personal.TownName} - {Players[0]}";
|
||||
|
||||
public HorizonSave(string folder)
|
||||
/// <summary>
|
||||
/// Creates a HorizonSave from an unpacked file path.
|
||||
/// </summary>
|
||||
/// <param name="folder">Path to the folder containing save files.</param>
|
||||
/// <returns>HorizonSave loaded from the folder.</returns>
|
||||
public static HorizonSave FromFolder(string folder)
|
||||
{
|
||||
Main = new MainSave(folder);
|
||||
Players = Player.ReadMany(folder);
|
||||
var provider = new FolderSaveFileProvider(folder);
|
||||
return new HorizonSave(provider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a HorizonSave from a ZIP file path.
|
||||
/// </summary>
|
||||
/// <param name="zipPath">Path to the ZIP archive containing save files.</param>
|
||||
/// <returns>HorizonSave loaded from the ZIP archive.</returns>
|
||||
public static HorizonSave FromZip(string zipPath)
|
||||
{
|
||||
var provider = new ZipSaveFileProvider(zipPath);
|
||||
return new HorizonSave(provider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -35,6 +57,7 @@ public void Save(uint seed)
|
|||
pair.Save(seed);
|
||||
}
|
||||
}
|
||||
Provider.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
50
NHSE.Core/Save/Meta/ISaveFileProvider.cs
Normal file
50
NHSE.Core/Save/Meta/ISaveFileProvider.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using System;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction for reading and writing save files from different storage backends (folder, ZIP archive, etc.).
|
||||
/// </summary>
|
||||
public interface ISaveFileProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads all bytes from the specified file path relative to the root.
|
||||
/// </summary>
|
||||
/// <param name="relativePath">Relative path to the file (e.g., "main.dat" or "Villager0/personal.dat").</param>
|
||||
/// <returns>Byte array containing the file contents.</returns>
|
||||
byte[] ReadFile(string relativePath);
|
||||
|
||||
/// <summary>
|
||||
/// Writes all bytes to the specified file path relative to the root.
|
||||
/// </summary>
|
||||
/// <param name="relativePath">Relative path to the file.</param>
|
||||
/// <param name="data">Byte data to write.</param>
|
||||
void WriteFile(string relativePath, ReadOnlySpan<byte> data);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a file exists at the specified relative path.
|
||||
/// </summary>
|
||||
/// <param name="relativePath">Relative path to the file.</param>
|
||||
/// <returns>True if the file exists; otherwise false.</returns>
|
||||
bool FileExists(string relativePath);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all subdirectory names matching the specified pattern relative to the root.
|
||||
/// </summary>
|
||||
/// <param name="searchPattern">Search pattern (e.g., "Villager*").</param>
|
||||
/// <returns>Array of matching directory names (not full paths).</returns>
|
||||
string[] GetDirectories(string searchPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a child provider scoped to the specified subdirectory.
|
||||
/// </summary>
|
||||
/// <param name="subdirectory">Subdirectory name to scope to.</param>
|
||||
/// <returns>A new provider rooted at the subdirectory.</returns>
|
||||
ISaveFileProvider GetSubdirectoryProvider(string subdirectory);
|
||||
|
||||
/// <summary>
|
||||
/// Flushes any pending writes to the underlying storage.
|
||||
/// For folder providers, this may be a no-op. For ZIP providers, this rebuilds and saves the archive.
|
||||
/// </summary>
|
||||
void Flush();
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
|
@ -36,29 +35,32 @@ public IEnumerator<EncryptedFilePair> GetEnumerator()
|
|||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Imports Player data from the requested <see cref="folder"/>.
|
||||
/// Imports Player data from the requested provider.
|
||||
/// </summary>
|
||||
/// <param name="folder">Folder that contains the Player Villager sub-folders.</param>
|
||||
/// <returns>Player object array loaded from the <see cref="folder"/>.</returns>
|
||||
public static Player[] ReadMany(string folder)
|
||||
/// <param name="provider">Provider that contains the Player Villager sub-folders.</param>
|
||||
/// <returns>Player object array loaded from the provider.</returns>
|
||||
public static Player[] ReadMany(ISaveFileProvider provider)
|
||||
{
|
||||
var dirs = Directory.GetDirectories(folder, "Villager*", SearchOption.TopDirectoryOnly);
|
||||
var dirs = provider.GetDirectories("Villager*");
|
||||
var result = new Player[dirs.Length];
|
||||
for (int i = 0; i < result.Length; i++)
|
||||
result[i] = new Player(dirs[i]);
|
||||
{
|
||||
var subProvider = provider.GetSubdirectoryProvider(dirs[i]);
|
||||
result[i] = new Player(subProvider, dirs[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Player(string folder)
|
||||
private Player(ISaveFileProvider provider, string directoryName)
|
||||
{
|
||||
DirectoryName = new DirectoryInfo(folder).Name;
|
||||
DirectoryName = directoryName;
|
||||
|
||||
Personal = new Personal(folder);
|
||||
Photo = new PhotoStudioIsland(folder);
|
||||
PostBox = new PostBox(folder);
|
||||
Profile = new Profile(folder);
|
||||
Personal = new Personal(provider);
|
||||
Photo = new PhotoStudioIsland(provider);
|
||||
PostBox = new PostBox(provider);
|
||||
Profile = new Profile(provider);
|
||||
|
||||
if (EncryptedFilePair.Exists(folder, WhereAreN.FileName))
|
||||
WhereAreN = new WhereAreN(folder);
|
||||
if (EncryptedFilePair.Exists(provider, WhereAreN.FileName))
|
||||
WhereAreN = new WhereAreN(provider);
|
||||
}
|
||||
}
|
||||
151
NHSE.Core/Save/Meta/ZipSaveFileProvider.cs
Normal file
151
NHSE.Core/Save/Meta/ZipSaveFileProvider.cs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Provides file access from a ZIP archive. Loads contents into memory and rebuilds the archive on Flush.
|
||||
/// </summary>
|
||||
public sealed class ZipSaveFileProvider : ISaveFileProvider
|
||||
{
|
||||
private string ZipPath { get; }
|
||||
private string BasePath { get; } = string.Empty;
|
||||
private Dictionary<string, byte[]> Files { get; } = [];
|
||||
private ZipSaveFileProvider? Root { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a provider from a ZIP file path. The ZIP contents are loaded into memory.
|
||||
/// </summary>
|
||||
/// <param name="zipPath">Absolute path to the ZIP file.</param>
|
||||
public ZipSaveFileProvider(string zipPath)
|
||||
{
|
||||
ZipPath = zipPath;
|
||||
using var archive = ZipFile.OpenRead(ZipPath);
|
||||
LoadFromZip(archive);
|
||||
}
|
||||
|
||||
private ZipSaveFileProvider(ZipSaveFileProvider root, string basePath)
|
||||
{
|
||||
ZipPath = root.ZipPath;
|
||||
BasePath = basePath;
|
||||
Files = root.Files;
|
||||
Root = root;
|
||||
}
|
||||
|
||||
private void LoadFromZip(ZipArchive archive)
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
continue; // Skip directory entries
|
||||
|
||||
using var stream = entry.Open();
|
||||
using var ms = new MemoryStream((int)entry.Length);
|
||||
stream.CopyTo(ms);
|
||||
var key = NormalizePath(entry.FullName);
|
||||
Files[key] = ms.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFullKey(string relativePath)
|
||||
{
|
||||
var combined = string.IsNullOrEmpty(BasePath) ? relativePath : $"{BasePath}/{relativePath}";
|
||||
return NormalizePath(combined);
|
||||
}
|
||||
|
||||
private static string NormalizePath(string path)
|
||||
{
|
||||
return path.Replace('\\', '/').TrimStart('/');
|
||||
}
|
||||
|
||||
public byte[] ReadFile(string relativePath)
|
||||
{
|
||||
var key = GetFullKey(relativePath);
|
||||
if (!Files.TryGetValue(key, out var data))
|
||||
throw new FileNotFoundException($"File not found in ZIP: {key}");
|
||||
return data;
|
||||
}
|
||||
|
||||
public void WriteFile(string relativePath, ReadOnlySpan<byte> data)
|
||||
{
|
||||
var key = GetFullKey(relativePath);
|
||||
Files[key] = data.ToArray();
|
||||
}
|
||||
|
||||
public bool FileExists(string relativePath)
|
||||
{
|
||||
var key = GetFullKey(relativePath);
|
||||
return Files.ContainsKey(key);
|
||||
}
|
||||
|
||||
public string[] GetDirectories(string searchPattern)
|
||||
{
|
||||
var prefix = string.IsNullOrEmpty(BasePath) ? string.Empty : BasePath + "/";
|
||||
var pattern = searchPattern.Replace("*", string.Empty);
|
||||
|
||||
var directories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var key in Files.Keys)
|
||||
{
|
||||
if (!key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var remainder = key[prefix.Length..];
|
||||
var slashIndex = remainder.IndexOf('/');
|
||||
if (slashIndex <= 0)
|
||||
continue;
|
||||
|
||||
var dirName = remainder[..slashIndex];
|
||||
if (dirName.Contains(pattern, StringComparison.OrdinalIgnoreCase))
|
||||
directories.Add(dirName);
|
||||
}
|
||||
|
||||
return [.. directories.Order()];
|
||||
}
|
||||
|
||||
public ISaveFileProvider GetSubdirectoryProvider(string subdirectory)
|
||||
{
|
||||
var newBase = string.IsNullOrEmpty(BasePath) ? subdirectory : $"{BasePath}/{subdirectory}";
|
||||
var rootProvider = Root ?? this;
|
||||
return new ZipSaveFileProvider(rootProvider, NormalizePath(newBase));
|
||||
}
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
// Only the root provider can flush
|
||||
if (Root is not null)
|
||||
{
|
||||
Root.Flush();
|
||||
return;
|
||||
}
|
||||
|
||||
// Write to a temporary file first, then replace the original
|
||||
var tempPath = ZipPath + ".tmp";
|
||||
try
|
||||
{
|
||||
using (var stream = new FileStream(tempPath, FileMode.Create, FileAccess.Write))
|
||||
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create))
|
||||
{
|
||||
foreach (var (key, data) in Files.OrderBy(x => x.Key))
|
||||
{
|
||||
var entry = archive.CreateEntry(key, CompressionLevel.Optimal);
|
||||
using var entryStream = entry.Open();
|
||||
entryStream.Write(data);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace original with temp
|
||||
File.Delete(ZipPath);
|
||||
File.Move(tempPath, ZipPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Clean up temp file on failure
|
||||
if (File.Exists(tempPath))
|
||||
File.Delete(tempPath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
NHSE.Core/Save/Offsets/Personal30.cs
Normal file
31
NHSE.Core/Save/Offsets/Personal30.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using System;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public sealed class Personal30(Memory<byte> raw)
|
||||
{
|
||||
public Span<byte> Data => raw.Span;
|
||||
|
||||
public bool IsInitialized30 => System.Buffers.Binary.BinaryPrimitives.ReadInt64LittleEndian(raw.Span) != 0;
|
||||
|
||||
public EncryptedInt32 HotelTickets // @0x0 size 0x8, align 4
|
||||
{
|
||||
get => EncryptedInt32.ReadVerify(Data, 0);
|
||||
set => value.Write(Data);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IPersonal30
|
||||
{
|
||||
public int Offset30s_064c1881 { get; }
|
||||
public int Length30s_064c1881 { get; }
|
||||
}
|
||||
|
||||
public static class Personal30Extensions
|
||||
{
|
||||
extension(IPersonal30 personal)
|
||||
{
|
||||
public Personal30 Get30s_064c1881(Memory<byte> data)
|
||||
=> new(data.Slice(personal.Offset30s_064c1881, personal.Length30s_064c1881));
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ namespace NHSE.Core;
|
|||
/// <summary>
|
||||
/// <inheritdoc cref="PersonalOffsets"/>
|
||||
/// </summary>
|
||||
public sealed class PersonalOffsets30 : PersonalOffsets
|
||||
public sealed class PersonalOffsets30 : PersonalOffsets, IPersonal30
|
||||
{
|
||||
// GSavePlayer
|
||||
private const int Player = 0x110;
|
||||
|
|
@ -50,6 +50,10 @@ public sealed class PersonalOffsets30 : PersonalOffsets
|
|||
public override int MaxRecipeID => 0x430; // unchanged
|
||||
public override int MaxRemakeBitFlag => 0x7D0 * 32;
|
||||
|
||||
// Additional struct added in 3.0.0 for Hotel; fetch via Offset's interface extension method.
|
||||
public int Offset30s_064c1881 => PlayerOther + 0x3C6D0;
|
||||
public int Length30s_064c1881 => 0x790;
|
||||
|
||||
public override IReactionStore ReadReactions(ReadOnlySpan<byte> data) => data.Slice(Manpu, GSavePlayerManpu15.SIZE).ToStructure<GSavePlayerManpu15>();
|
||||
public override void SetReactions(Span<byte> data, IReactionStore value) => ((GSavePlayerManpu15)value).ToBytes().CopyTo(data[Manpu..]);
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using static System.Buffers.Binary.BinaryPrimitives;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using static System.Buffers.Binary.BinaryPrimitives;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
|
|
|||
1279
NHSE.WinForms/Editor.Designer.cs
generated
1279
NHSE.WinForms/Editor.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -1,14 +1,14 @@
|
|||
using System;
|
||||
using NHSE.Core;
|
||||
using NHSE.Injection;
|
||||
using NHSE.Sprites;
|
||||
using NHSE.WinForms.Properties;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using NHSE.Core;
|
||||
using NHSE.Injection;
|
||||
using NHSE.Sprites;
|
||||
using NHSE.WinForms.Properties;
|
||||
|
||||
namespace NHSE.WinForms;
|
||||
|
||||
|
|
@ -17,6 +17,11 @@ public sealed partial class Editor : Form
|
|||
private readonly HorizonSave SAV;
|
||||
private readonly VillagerEditor Villagers;
|
||||
|
||||
/// <summary>
|
||||
/// Currently loaded player index.
|
||||
/// </summary>
|
||||
private int PlayerIndex = -1;
|
||||
|
||||
public Editor(HorizonSave file)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
|
@ -212,8 +217,6 @@ private void LoadPlayers()
|
|||
PlayerIndex = -1;
|
||||
CB_Players.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private int PlayerIndex = -1;
|
||||
private void LoadPlayer(object sender, EventArgs e) => LoadPlayer(CB_Players.SelectedIndex);
|
||||
|
||||
private void B_EditPlayerItems_Click(object sender, EventArgs e)
|
||||
|
|
@ -292,19 +295,28 @@ private void LoadPlayer(int index)
|
|||
var pers = player.Personal;
|
||||
TB_Name.Text = pers.PlayerName;
|
||||
TB_TownName.Text = pers.TownName;
|
||||
NUD_BankBells.Value = Math.Min(int.MaxValue, pers.Bank.Value);
|
||||
NUD_NookMiles.Value = Math.Min(int.MaxValue, pers.NookMiles.Value);
|
||||
NUD_TotalNookMiles.Value = Math.Min(int.MaxValue, pers.TotalNookMiles.Value);
|
||||
NUD_Wallet.Value = Math.Min(int.MaxValue, pers.Wallet.Value);
|
||||
NUD_BankBells.Value = Math.Min(int.MaxValue, pers.Bank);
|
||||
NUD_NookMiles.Value = Math.Min(int.MaxValue, pers.NookMiles);
|
||||
NUD_TotalNookMiles.Value = Math.Min(int.MaxValue, pers.TotalNookMiles);
|
||||
NUD_Wallet.Value = Math.Min(int.MaxValue, pers.Wallet);
|
||||
|
||||
// swapped on purpose -- first count is the first two rows of items
|
||||
NUD_PocketCount1.Value = Math.Min(int.MaxValue, pers.PocketCount);
|
||||
NUD_PocketCount2.Value = Math.Min(int.MaxValue, pers.BagCount);
|
||||
NUD_StorageCount.Value = Math.Min(int.MaxValue, pers.ItemChestCount);
|
||||
|
||||
if (pers.Data30 is { IsInitialized30: true } addition)
|
||||
{
|
||||
NUD_HotelTickets.Value = Math.Min(int.MaxValue, addition.HotelTickets);
|
||||
}
|
||||
else
|
||||
{
|
||||
L_HotelTickets.Visible = NUD_HotelTickets.Visible = false;
|
||||
}
|
||||
|
||||
if (player.WhereAreN is not null)
|
||||
{
|
||||
NUD_Poki.Value = Math.Min(int.MaxValue, player.WhereAreN.Poki.Value);
|
||||
NUD_Poki.Value = Math.Min(int.MaxValue, player.WhereAreN.Poki);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -347,21 +359,10 @@ private void SavePlayer(int index)
|
|||
SAV.ChangeIdentity(orig, updated);
|
||||
}
|
||||
|
||||
var bank = pers.Bank;
|
||||
bank.Value = (uint)NUD_BankBells.Value;
|
||||
pers.Bank = bank;
|
||||
|
||||
var nook = pers.NookMiles;
|
||||
nook.Value = (uint)NUD_NookMiles.Value;
|
||||
pers.NookMiles = nook;
|
||||
|
||||
var tnook = pers.TotalNookMiles;
|
||||
tnook.Value = (uint)NUD_TotalNookMiles.Value;
|
||||
pers.TotalNookMiles = tnook;
|
||||
|
||||
var wallet = pers.Wallet;
|
||||
wallet.Value = (uint)NUD_Wallet.Value;
|
||||
pers.Wallet = wallet;
|
||||
pers.Bank = pers.Bank with { Value = (uint)NUD_BankBells.Value };
|
||||
pers.NookMiles = pers.NookMiles with { Value = (uint)NUD_NookMiles.Value };
|
||||
pers.TotalNookMiles = pers.TotalNookMiles with { Value = (uint)NUD_TotalNookMiles.Value };
|
||||
pers.Wallet = pers.Wallet with { Value = (uint)NUD_Wallet.Value };
|
||||
|
||||
// swapped on purpose -- first count is the first two rows of items
|
||||
pers.PocketCount = (uint)NUD_PocketCount1.Value;
|
||||
|
|
@ -369,11 +370,14 @@ private void SavePlayer(int index)
|
|||
|
||||
pers.ItemChestCount = (uint)NUD_StorageCount.Value;
|
||||
|
||||
if (player.Personal.Data30 is { IsInitialized30: true } addition)
|
||||
{
|
||||
addition.HotelTickets = addition.HotelTickets with { Value = (uint)NUD_HotelTickets.Value };
|
||||
}
|
||||
|
||||
if (player.WhereAreN is { } x)
|
||||
{
|
||||
var poki = x.Poki;
|
||||
poki.Value = (uint)NUD_Poki.Value;
|
||||
x.Poki = poki;
|
||||
x.Poki = x.Poki with { Value = (uint)NUD_Poki.Value };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
|
|
@ -26,36 +26,36 @@
|
|||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
|
|
|
|||
|
|
@ -123,6 +123,15 @@ private static void OpenFileOrPath(string path)
|
|||
return;
|
||||
}
|
||||
|
||||
// Load zip files differently
|
||||
var ext = Path.GetExtension(path);
|
||||
if (ext.Equals(".zip", StringComparison.OrdinalIgnoreCase) && new FileInfo(path).Length < 20 * 1024 * 1024) // less than 20MB
|
||||
{
|
||||
var file = HorizonSave.FromZip(path);
|
||||
Open(file);
|
||||
return;
|
||||
}
|
||||
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
if (dir is null || !Directory.Exists(dir)) // ya never know
|
||||
{
|
||||
|
|
@ -135,7 +144,7 @@ private static void OpenFileOrPath(string path)
|
|||
|
||||
private static void OpenSaveFile(string path)
|
||||
{
|
||||
var file = new HorizonSave(path);
|
||||
var file = HorizonSave.FromFolder(path);
|
||||
Open(file);
|
||||
|
||||
var settings = Settings.Default;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using NHSE.Sprites;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using NHSE.Sprites;
|
||||
|
||||
namespace NHSE.WinForms;
|
||||
|
||||
public sealed partial class ImageFetcher : Form
|
||||
{
|
||||
private const string Filename = "image.zip";
|
||||
private const int MaxBufferSize = 8192;
|
||||
private static string ZipFilePath => Path.Combine(ItemSprite.PlatformAppDataPath, Filename);
|
||||
|
||||
private readonly string[] AllHosts;
|
||||
|
|
@ -49,14 +48,39 @@ private async void B_Download_Click(object sender, EventArgs e)
|
|||
|
||||
SetUIDownloadState(false);
|
||||
|
||||
L_Status.Text = "Downloading...";
|
||||
L_Status.PerformSafely(() => L_Status.Text = "Downloading...");
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
using var httpClient = new System.Net.Http.HttpClient();
|
||||
await using var stream = await httpClient.GetStreamAsync(hostSelected).ConfigureAwait(false);
|
||||
await using var fileStream = new FileStream(ZipFilePath, FileMode.CreateNew);
|
||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
using var response = await httpClient.GetAsync(hostSelected, System.Net.Http.HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
|
||||
var canReportProgress = totalBytes != -1 && PBar_MultiUse != null;
|
||||
|
||||
using (var contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
using (var fileStream = new FileStream(ZipFilePath, FileMode.Create, FileAccess.Write, FileShare.None, MaxBufferSize, true))
|
||||
{
|
||||
var buffer = new byte[MaxBufferSize];
|
||||
long totalRead = 0;
|
||||
int read;
|
||||
while ((read = await contentStream.ReadAsync(buffer).ConfigureAwait(false)) > 0)
|
||||
{
|
||||
await fileStream.WriteAsync(buffer.AsMemory(0, read)).ConfigureAwait(false);
|
||||
totalRead += read;
|
||||
if (canReportProgress && PBar_MultiUse != null)
|
||||
{
|
||||
int progress = (int)((totalRead * 100) / totalBytes);
|
||||
PBar_MultiUse.PerformSafely(() => PBar_MultiUse.Value = Math.Min(progress, 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PBar_MultiUse?.PerformSafely(() => PBar_MultiUse.Value = 100);
|
||||
L_Status.Invoke((Action)(() => L_Status.Text = "Unzipping..."));
|
||||
UnzipFile();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -65,22 +89,6 @@ private async void B_Download_Click(object sender, EventArgs e)
|
|||
}
|
||||
}
|
||||
|
||||
private void ProgressChanged(object sender, DownloadProgressChangedEventArgs e) => PBar_MultiUse.Value = e.ProgressPercentage;
|
||||
|
||||
private void Completed(object? sender, AsyncCompletedEventArgs e)
|
||||
{
|
||||
if (e.Error != null)
|
||||
{
|
||||
WinFormsUtil.Error(e.Error.Message, e.Error.InnerException == null ? string.Empty : e.Error.InnerException.Message);
|
||||
SetUIDownloadState(true);
|
||||
return;
|
||||
}
|
||||
|
||||
PBar_MultiUse.Value = 100;
|
||||
L_Status.Text = "Unzipping...";
|
||||
UnzipFile();
|
||||
}
|
||||
|
||||
private async void UnzipFile()
|
||||
{
|
||||
try
|
||||
|
|
@ -105,11 +113,11 @@ private async void UnzipFile()
|
|||
|
||||
private void SetUIDownloadState(bool val, bool success = false)
|
||||
{
|
||||
ControlBox = val;
|
||||
B_Download.Enabled = val;
|
||||
PBar_MultiUse.Value = 0;
|
||||
this.PerformSafely(() => ControlBox = val);
|
||||
B_Download.PerformSafely(() => B_Download.Enabled = val);
|
||||
PBar_MultiUse.PerformSafely(() => PBar_MultiUse.Value = 0);
|
||||
|
||||
L_Status.Text = success ? "Images installed successfully." : string.Empty;
|
||||
L_Status.PerformSafely(() => L_Status.Text = success ? "Images installed successfully." : string.Empty);
|
||||
|
||||
CheckFileStatusLabel();
|
||||
|
||||
|
|
@ -129,30 +137,24 @@ private async void CheckNetworkFileSizeAsync()
|
|||
{
|
||||
try
|
||||
{
|
||||
L_FileSize.Text = string.Empty;
|
||||
L_FileSize.PerformSafely(() => L_FileSize.Text = string.Empty);
|
||||
var host = AllHosts[CB_HostSelect.SelectedIndex];
|
||||
#if NETFRAMEWORK
|
||||
using var webClient = new WebClient();
|
||||
await webClient.OpenReadTaskAsync(new Uri(host, UriKind.Absolute)).ConfigureAwait(false);
|
||||
var hdr = webClient.ResponseHeaders?["Content-Length"];
|
||||
#elif NETCOREAPP
|
||||
using var httpClient = new System.Net.Http.HttpClient();
|
||||
var httpInitialResponse = await httpClient.GetAsync(host).ConfigureAwait(false);
|
||||
var hdr = httpInitialResponse.Content.Headers.ContentLength;
|
||||
#endif
|
||||
|
||||
if (hdr == null)
|
||||
{
|
||||
L_FileSize.Text = "Failed.";
|
||||
L_FileSize.PerformSafely(() => L_FileSize.Text = "Failed.");
|
||||
return;
|
||||
}
|
||||
var totalSizeBytes = Convert.ToInt64(hdr);
|
||||
var totalSizeMb = totalSizeBytes / 1e+6;
|
||||
L_FileSize.Text = $"{totalSizeMb:0.##}MB";
|
||||
L_FileSize.PerformSafely(() => L_FileSize.Text = $"{totalSizeMb:0.##}MB");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
L_FileSize.Text = ex.Message;
|
||||
L_FileSize.PerformSafely(() => L_FileSize.Text = ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,5 +164,5 @@ private static string CleanUrl(string url)
|
|||
return uri.Segments.Length < 2 ? url : $"{uri.Host}/{uri.Segments[1]}";
|
||||
}
|
||||
|
||||
private bool CheckFileStatusLabel() => L_ImgStatus.Visible = ItemSprite.SingleSpriteExists;
|
||||
private void CheckFileStatusLabel() => L_ImgStatus.PerformSafely(() => L_ImgStatus.Visible = ItemSprite.SingleSpriteExists);
|
||||
}
|
||||
22
NHSE.WinForms/Util/CrossThreadExtensions.cs
Normal file
22
NHSE.WinForms/Util/CrossThreadExtensions.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace NHSE.WinForms;
|
||||
|
||||
public static class CrossThreadExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper function to perform an action on a Control's thread safely.
|
||||
/// </summary>
|
||||
public static void PerformSafely(this Control target, Action action)
|
||||
{
|
||||
if (target.InvokeRequired)
|
||||
{
|
||||
target.Invoke(action);
|
||||
}
|
||||
else
|
||||
{
|
||||
action();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,7 +67,7 @@ private static void LoadSpecialForms()
|
|||
{
|
||||
// For forms that require more complete initialization (dynamically added user controls)
|
||||
var path = Settings.Default.LastFilePath;
|
||||
var sav = new HorizonSave(path);
|
||||
var sav = HorizonSave.FromFolder(path);
|
||||
using var editor = new Editor(sav);
|
||||
using var so = new SingleObjectEditor<object>(new object(), PropertySort.NoSort, false);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user