Added VDF Generator
This commit is contained in:
193
007SaveTool/VdfGenerator/Models/CachedFile.cs
Normal file
193
007SaveTool/VdfGenerator/Models/CachedFile.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.IO;
|
||||
using EonaCat.FirstLight.SaveTransfer.VdfGenerator.KeyValue.Models;
|
||||
using EonaCat.FirstLight.SaveTransfer.VdfGenerator;
|
||||
|
||||
namespace EonaCat.FirstLight.SaveTransfer.VdfGenerator.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a file metadata cached for synchronization or tracking purposes. Provides properties for file path, size, timestamps, hash, and synchronization state.
|
||||
/// </summary>
|
||||
/// <param name="relativePath">The relative path of the file within the root directory. Used to identify and locate the file in the cache.</param>
|
||||
public class CachedFileMetadata(string relativePath)
|
||||
{
|
||||
private const string DefaultSha = "0000000000000000000000000000000000000000";
|
||||
|
||||
public string RelativePath { get; set; } = relativePath;
|
||||
public int Root { get; set; }
|
||||
public int Size { get; set; }
|
||||
public long LocalTime { get; set; }
|
||||
public long Time { get; set; }
|
||||
public long RemoteTime { get; set; }
|
||||
public string Sha { get; set; } = DefaultSha;
|
||||
public int SyncState { get; set; }
|
||||
public int PersistState { get; set; }
|
||||
public int PlatformsToSync2 { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current time as the number of seconds that have elapsed since the Unix epoch (January 1, 1970, 00:00:00 UTC).
|
||||
/// </summary>
|
||||
private static long Now => DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
/// <summary>
|
||||
/// Computes the SHA-1 hash of the specified byte span and returns its hexadecimal string representation.
|
||||
/// </summary>
|
||||
/// <param name="data">The input data to hash as a read-only span of bytes.</param>
|
||||
/// <returns>A lowercase hexadecimal string representing the SHA-1 hash of the input data.</returns>
|
||||
private static string Sha1FromSpan(ReadOnlySpan<byte> data)
|
||||
{
|
||||
Span<byte> hash = stackalloc byte[20]; // SHA‑1 = 20 bytes
|
||||
SHA1.HashData(data, hash);
|
||||
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the relative path from the specified root directory to the given file path, using forward slashes as directory separators.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The absolute path to the target file. Cannot be null.</param>
|
||||
/// <param name="rootPath">The absolute path to the root directory from which to calculate the relative path. Cannot be null.</param>
|
||||
/// <returns>A relative path from the root directory to the file, using forward slashes ('/') as directory separators.</returns>
|
||||
private static string GetRelativePath(string filePath, string rootPath)
|
||||
=> Path.GetRelativePath(rootPath, filePath).Replace(Path.DirectorySeparatorChar, '/');
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachedFileMetadata"/> class using the specified file path and root directory. Loads the file's data and metadata into the cache.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The full path to the file to be cached. Must refer to an existing file.</param>
|
||||
/// <param name="rootPath">The root directory path used to compute the relative path for the cached file.</param>
|
||||
/// <exception cref="FileNotFoundException">Thrown if the file specified by filePath does not exist.</exception>
|
||||
public CachedFileMetadata(string filePath, string rootPath) : this(GetRelativePath(filePath, rootPath))
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
throw new FileNotFoundException("File not found", filePath);
|
||||
|
||||
var data = File.ReadAllBytes(filePath);
|
||||
Size = data.Length;
|
||||
|
||||
SetLocalTimeAndTimeToNow();
|
||||
Sha = Sha1FromSpan(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CachedFileMetadata"/> class using data from the provided key–value group.
|
||||
/// </summary>
|
||||
/// <param name="group">A key–value group containing the file data used to initialize the object's properties.</param>
|
||||
public CachedFileMetadata(KeyValueGroup group) : this(group.Key)
|
||||
{
|
||||
foreach (var node in group.Nodes.Cast<KeyValue.Models.KeyValuePair?>())
|
||||
{
|
||||
switch (node?.Key)
|
||||
{
|
||||
case "root":
|
||||
Root = NumberParser.ParseInt(node.Value);
|
||||
break;
|
||||
case "size":
|
||||
Size = NumberParser.ParseInt(node.Value);
|
||||
break;
|
||||
case "localtime":
|
||||
LocalTime = NumberParser.ParseLong(node.Value);
|
||||
break;
|
||||
case "time":
|
||||
Time = NumberParser.ParseLong(node.Value);
|
||||
break;
|
||||
case "remotetime":
|
||||
RemoteTime = NumberParser.ParseLong(node.Value);
|
||||
break;
|
||||
case "sha":
|
||||
Sha = node.Value;
|
||||
break;
|
||||
case "syncstate":
|
||||
SyncState = NumberParser.ParseInt(node.Value);
|
||||
break;
|
||||
case "persiststate":
|
||||
PersistState = NumberParser.ParseInt(node.Value);
|
||||
break;
|
||||
case "platformstosync2":
|
||||
PlatformsToSync2 = NumberParser.ParseInt(node.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the local time property to the current system time.
|
||||
/// </summary>
|
||||
public void SetLocalTimeToNow() => LocalTime = Now;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current time to the system's current date and time.
|
||||
/// </summary>
|
||||
public void SetTimeToNow() => Time = Now;
|
||||
|
||||
/// <summary>
|
||||
/// Sets both the local time and the time properties to the current value of the system clock.
|
||||
/// </summary>
|
||||
public void SetLocalTimeAndTimeToNow()
|
||||
{
|
||||
var epoch = Now;
|
||||
LocalTime = epoch;
|
||||
Time = epoch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the local time using the specified timestamp.
|
||||
/// </summary>
|
||||
/// <param name="timestamp">The point in time, expressed as a DateTimeOffset, to set as the local time. The value is converted to Unix time in seconds.</param>
|
||||
public void SetLocalTime(DateTimeOffset timestamp)
|
||||
=> LocalTime = timestamp.ToUnixTimeSeconds();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current time value using the specified timestamp.
|
||||
/// </summary>
|
||||
/// <param name="timestamp">The point in time to set, represented as a DateTimeOffset. The value is converted to Unix time in seconds.</param>
|
||||
public void SetTime(DateTimeOffset timestamp)
|
||||
=> Time = timestamp.ToUnixTimeSeconds();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the local time and time properties using the specified timestamp.
|
||||
/// </summary>
|
||||
/// <param name="timestamp">The date and time value to use, including the offset from Coordinated Universal Time (UTC).</param>
|
||||
public void SetLocalTimeAndTime(DateTimeOffset timestamp)
|
||||
{
|
||||
LocalTime = timestamp.ToUnixTimeSeconds();
|
||||
Time = timestamp.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local date and time represented by the current Unix timestamp value.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="DateTimeOffset"/> that represents the local date and time corresponding to the stored Unix time in seconds.</returns>
|
||||
public DateTimeOffset GetLocalDateTime()
|
||||
=> DateTimeOffset.FromUnixTimeSeconds(LocalTime);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the date and time represented by the current Unix timestamp value.
|
||||
/// </summary>
|
||||
/// <returns>A DateTimeOffset value corresponding to the Unix timestamp stored in the current instance.</returns>
|
||||
public DateTimeOffset GetDateTime()
|
||||
=> DateTimeOffset.FromUnixTimeSeconds(Time);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current remote date and time as a DateTimeOffset value.
|
||||
/// </summary>
|
||||
/// <returns>A DateTimeOffset representing the remote time, converted from the stored Unix timestamp.</returns>
|
||||
public DateTimeOffset GetRemoteDateTime()
|
||||
=> DateTimeOffset.FromUnixTimeSeconds(RemoteTime);
|
||||
|
||||
/// <summary>
|
||||
/// Exports the current object as a key–value group containing essential information about its state and properties.
|
||||
/// </summary>
|
||||
/// <returns> A <see cref="KeyValueGroup"/> instance populated with core data such as the relative path, size, timestamps, SHA checksum, and the current synchronization and persistence states.</returns>
|
||||
public KeyValueGroup ExportAsKvGroup()
|
||||
=> new KeyValueGroup(RelativePath)
|
||||
.Add("root", Root.ToString())
|
||||
.Add("size", Size.ToString())
|
||||
.Add("localtime", LocalTime.ToString())
|
||||
.Add("time", Time.ToString())
|
||||
.Add("remotetime", RemoteTime.ToString())
|
||||
.Add("sha", Sha)
|
||||
.Add("syncstate", SyncState.ToString())
|
||||
.Add("persiststate", PersistState.ToString())
|
||||
.Add("platformstosync2", PlatformsToSync2.ToString());
|
||||
}
|
||||
99
007SaveTool/VdfGenerator/Models/RemoteCacheVdfFile.cs
Normal file
99
007SaveTool/VdfGenerator/Models/RemoteCacheVdfFile.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using EonaCat.FirstLight.SaveTransfer.VdfGenerator;
|
||||
using EonaCat.FirstLight.SaveTransfer.VdfGenerator.KeyValue;
|
||||
using EonaCat.FirstLight.SaveTransfer.VdfGenerator.KeyValue.Models;
|
||||
|
||||
namespace EonaCat.FirstLight.SaveTransfer.VdfGenerator.Models;
|
||||
|
||||
public class RemoteCacheVdfFile(int appId)
|
||||
{
|
||||
public const string FileName = "remotecache";
|
||||
public const string FileExtension = ".vdf";
|
||||
|
||||
public int AppId { get; set; } = appId;
|
||||
public int ChangeNumber { get; set; }
|
||||
public int OsType { get; set; }
|
||||
public List<CachedFileMetadata> CachedFiles { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the application identifier from the specified file path.
|
||||
/// </summary>
|
||||
/// <param name="path">The file system path from which to extract the application identifier. Must contain a parent directory whose name is a valid integer.</param>
|
||||
/// <returns>The application identifier parsed from the parent directory name of the specified path.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the parent directory name of the specified path is not a valid integer.</exception>
|
||||
private static int GetAppIdFromPath(string path)
|
||||
{
|
||||
var parent = Path.GetFileName(Path.GetDirectoryName(path));
|
||||
return int.TryParse(parent, out var result)
|
||||
? result
|
||||
: throw new InvalidOperationException("Invalid AppId in path");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RemoteCacheVdfFile"/> class using the specified remote folder path and loads metadata for all files within the folder and its subdirectories.
|
||||
/// </summary>
|
||||
/// <param name="remoteFolderPath">The full path to the remote folder containing the files to be cached. Must not be null or empty.</param>
|
||||
public RemoteCacheVdfFile(string remoteFolderPath) : this(GetAppIdFromPath(remoteFolderPath))
|
||||
{
|
||||
var files = Directory.GetFiles(remoteFolderPath, "*", SearchOption.AllDirectories);
|
||||
foreach (var file in files)
|
||||
CachedFiles.Add(new CachedFileMetadata(file, remoteFolderPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RemoteCacheVdfFile"/> class based on the provided key–value (KV) group, copying the relevant metadata.
|
||||
/// </summary>
|
||||
/// <param name="group">The KV group from which metadata and the list of cached files are read. Must not be null.</param>
|
||||
public RemoteCacheVdfFile(KeyValueGroup group) : this(group.Key)
|
||||
{
|
||||
foreach (var node in group.Nodes)
|
||||
{
|
||||
// If the node is a KvGroup, we create a new CachedFileMetadata object using the group and add it to the CachedFiles list.
|
||||
if (node is KeyValueGroup fileGroup)
|
||||
{
|
||||
CachedFiles.Add(new CachedFileMetadata(fileGroup));
|
||||
continue;
|
||||
}
|
||||
// If the node is not a KvGroup, we attempt to cast it to a KvPair to extract the key and value for the properties of RemoteCacheVdfFile.
|
||||
var kvPair = node as KeyValue.Models.KeyValuePair;
|
||||
switch (kvPair?.Key)
|
||||
{
|
||||
case "ChangeNumber":
|
||||
ChangeNumber = NumberParser.ParseInt(kvPair.Value);
|
||||
break;
|
||||
case "OSType":
|
||||
OsType = NumberParser.ParseInt(kvPair.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports the current object and its cached files as a key-value group representation.
|
||||
/// </summary>
|
||||
/// <returns>A KvGroup containing the key-value pairs for the current object and its cached files.</returns>
|
||||
public KeyValueGroup ExportAsKvGroup()
|
||||
{
|
||||
var kvGroup = new KeyValueGroup(AppId.ToString())
|
||||
.Add("ChangeNumber", ChangeNumber.ToString())
|
||||
.Add("OSType", OsType.ToString());
|
||||
|
||||
foreach (var cachedFile in CachedFiles)
|
||||
kvGroup.Add(cachedFile.ExportAsKvGroup());
|
||||
|
||||
return kvGroup;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports the current data to a file in the specified destination folder.
|
||||
/// </summary>
|
||||
/// <param name="destinationFolder">The path to the folder where the exported file will be created. Must be a valid, writable directory.</param>
|
||||
public void ExportAsFile(string destinationFolder)
|
||||
{
|
||||
var kv = ExportAsKvGroup();
|
||||
var serialized = KeyValueSerializer.Serialize(kv);
|
||||
var outputFilePath = Path.Combine(destinationFolder, $"{FileName}{FileExtension}");
|
||||
File.WriteAllText(outputFilePath, serialized, new UTF8Encoding());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user