Added VDF Generator
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
|||||||
@@ -25,9 +25,7 @@
|
|||||||
<!-- status bar -->
|
<!-- status bar -->
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════
|
|
||||||
HEADER
|
|
||||||
════════════════════════════════════════════════════════════════ -->
|
|
||||||
<Border Grid.Row="0" Background="{StaticResource BgMidBrush}"
|
<Border Grid.Row="0" Background="{StaticResource BgMidBrush}"
|
||||||
BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,0,0,1">
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,0,0,1">
|
||||||
<Grid Margin="28,18,28,18">
|
<Grid Margin="28,18,28,18">
|
||||||
@@ -65,9 +63,6 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════
|
|
||||||
TAB BAR
|
|
||||||
════════════════════════════════════════════════════════════════ -->
|
|
||||||
<Border Grid.Row="1" Background="{StaticResource BgMidBrush}"
|
<Border Grid.Row="1" Background="{StaticResource BgMidBrush}"
|
||||||
BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,0,0,1">
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,0,0,1">
|
||||||
<TabControl x:Name="MainTabs" Style="{StaticResource DarkTabControl}"
|
<TabControl x:Name="MainTabs" Style="{StaticResource DarkTabControl}"
|
||||||
@@ -80,17 +75,16 @@
|
|||||||
x:Name="TabResign"/>
|
x:Name="TabResign"/>
|
||||||
<TabItem Header="🔓 Decrypt / Inspect" Style="{StaticResource DarkTabItem}"
|
<TabItem Header="🔓 Decrypt / Inspect" Style="{StaticResource DarkTabItem}"
|
||||||
x:Name="TabDecrypt"/>
|
x:Name="TabDecrypt"/>
|
||||||
|
<TabItem Header="📦 VDF Generator" Style="{StaticResource DarkTabItem}"
|
||||||
|
x:Name="TabVdf"/>
|
||||||
<TabItem Header="📋 Log" Style="{StaticResource DarkTabItem}"
|
<TabItem Header="📋 Log" Style="{StaticResource DarkTabItem}"
|
||||||
x:Name="TabLog"/>
|
x:Name="TabLog"/>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════
|
|
||||||
CONTENT PANELS
|
|
||||||
════════════════════════════════════════════════════════════════ -->
|
|
||||||
<Grid Grid.Row="2">
|
<Grid Grid.Row="2">
|
||||||
|
|
||||||
<!-- ── RESIGN PANEL ──────────────────────────────────────── -->
|
<!-- RESIGN PANEL -->
|
||||||
<ScrollViewer x:Name="PanelResign" VerticalScrollBarVisibility="Auto"
|
<ScrollViewer x:Name="PanelResign" VerticalScrollBarVisibility="Auto"
|
||||||
HorizontalScrollBarVisibility="Disabled">
|
HorizontalScrollBarVisibility="Disabled">
|
||||||
<StackPanel Margin="32,28,32,28">
|
<StackPanel Margin="32,28,32,28">
|
||||||
@@ -182,7 +176,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<!-- ── DECRYPT PANEL ─────────────────────────────────────── -->
|
<!-- DECRYPT PANEL -->
|
||||||
<ScrollViewer x:Name="PanelDecrypt" Visibility="Collapsed"
|
<ScrollViewer x:Name="PanelDecrypt" Visibility="Collapsed"
|
||||||
VerticalScrollBarVisibility="Auto"
|
VerticalScrollBarVisibility="Auto"
|
||||||
HorizontalScrollBarVisibility="Disabled">
|
HorizontalScrollBarVisibility="Disabled">
|
||||||
@@ -236,7 +230,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<!-- ── LOG PANEL ─────────────────────────────────────────── -->
|
<!-- LOG PANEL -->
|
||||||
<Grid x:Name="PanelLog" Visibility="Collapsed" Margin="32,28,32,28">
|
<Grid x:Name="PanelLog" Visibility="Collapsed" Margin="32,28,32,28">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
@@ -265,11 +259,62 @@
|
|||||||
HorizontalAlignment="Right" Margin="0,10,0,0"
|
HorizontalAlignment="Right" Margin="0,10,0,0"
|
||||||
Click="ClearLog_Click"/>
|
Click="ClearLog_Click"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<!-- VDF GENERATOR PANEL -->
|
||||||
|
<ScrollViewer x:Name="PanelVdf" Visibility="Collapsed"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollBarVisibility="Disabled">
|
||||||
|
<StackPanel Margin="32,28,32,28">
|
||||||
|
|
||||||
|
<!-- Remote Folder -->
|
||||||
|
<TextBlock Text="REMOTE FOLDER" Style="{StaticResource FieldLabel}"/>
|
||||||
|
<Grid Margin="0,0,0,18">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox x:Name="TxtVdfRemoteFolder" Style="{StaticResource DarkTextBox}"
|
||||||
|
IsReadOnly="True" Grid.Column="0"
|
||||||
|
Text="Click Browse to select the 'remote' folder…"
|
||||||
|
Foreground="{StaticResource TextMutedBrush}"/>
|
||||||
|
<Button Grid.Column="1" Content="Browse…"
|
||||||
|
Style="{StaticResource OutlineButton}"
|
||||||
|
Margin="10,0,0,0" Click="BrowseVdfRemoteFolder_Click"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<TextBlock Foreground="{StaticResource TextMutedBrush}" FontSize="11"
|
||||||
|
Margin="0,0,0,24">
|
||||||
|
Select the <Run FontFamily="Consolas">remote</Run> folder from your Steam save directory
|
||||||
|
(e.g., <Run FontFamily="Consolas">…\userdata\<AccountId>\3768760\remote</Run>)
|
||||||
|
to generate the <Run FontFamily="Consolas">remotecache.vdf</Run> file.
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<!-- Action Button -->
|
||||||
|
<Button x:Name="BtnGenerateVdf" Content="📦 Generate VDF"
|
||||||
|
Style="{StaticResource GoldButton}"
|
||||||
|
HorizontalAlignment="Stretch" Height="48"
|
||||||
|
Click="BtnGenerateVdf_Click"/>
|
||||||
|
|
||||||
|
<!-- Progress -->
|
||||||
|
<ProgressBar x:Name="VdfProgress" Height="4" Margin="0,14,0,0"
|
||||||
|
Background="#0D1E30" Foreground="{StaticResource AccentGoldBrush}"
|
||||||
|
BorderThickness="0" Visibility="Collapsed" IsIndeterminate="True"/>
|
||||||
|
|
||||||
|
<!-- Inline result banner -->
|
||||||
|
<Border x:Name="VdfResultBanner" Visibility="Collapsed"
|
||||||
|
CornerRadius="8" Padding="16,12" Margin="0,14,0,0">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock x:Name="VdfResultText"
|
||||||
|
Foreground="White" FontSize="13"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════
|
<!-- STATUS BAR -->
|
||||||
STATUS BAR
|
|
||||||
════════════════════════════════════════════════════════════════ -->
|
|
||||||
<Border Grid.Row="3" Background="{StaticResource BgMidBrush}"
|
<Border Grid.Row="3" Background="{StaticResource BgMidBrush}"
|
||||||
BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,1,0,0"
|
BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,1,0,0"
|
||||||
Padding="28,8">
|
Padding="28,8">
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
using EonaCat.FirstLight.SaveTransfer.VdfGenerator.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
@@ -31,6 +34,7 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
PanelResign.Visibility = Visibility.Collapsed;
|
PanelResign.Visibility = Visibility.Collapsed;
|
||||||
PanelDecrypt.Visibility = Visibility.Collapsed;
|
PanelDecrypt.Visibility = Visibility.Collapsed;
|
||||||
|
PanelVdf.Visibility = Visibility.Collapsed;
|
||||||
PanelLog.Visibility = Visibility.Collapsed;
|
PanelLog.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
if (tab == TabResign)
|
if (tab == TabResign)
|
||||||
@@ -43,6 +47,11 @@ public partial class MainWindow : Window
|
|||||||
PanelDecrypt.Visibility = Visibility.Visible;
|
PanelDecrypt.Visibility = Visibility.Visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tab == TabVdf)
|
||||||
|
{
|
||||||
|
PanelVdf.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
if (tab == TabLog)
|
if (tab == TabLog)
|
||||||
{
|
{
|
||||||
PanelLog.Visibility = Visibility.Visible;
|
PanelLog.Visibility = Visibility.Visible;
|
||||||
@@ -73,6 +82,18 @@ public partial class MainWindow : Window
|
|||||||
TxtDecryptFolder.Foreground = (Brush)FindResource("TextPrimaryBrush");
|
TxtDecryptFolder.Foreground = (Brush)FindResource("TextPrimaryBrush");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BrowseVdfRemoteFolder_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
string? path = BrowseForFolder();
|
||||||
|
if (path is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TxtVdfRemoteFolder.Text = path;
|
||||||
|
TxtVdfRemoteFolder.Foreground = (Brush)FindResource("TextPrimaryBrush");
|
||||||
|
}
|
||||||
|
|
||||||
private static string? BrowseForFolder()
|
private static string? BrowseForFolder()
|
||||||
{
|
{
|
||||||
var dlg = new Microsoft.Win32.OpenFolderDialog
|
var dlg = new Microsoft.Win32.OpenFolderDialog
|
||||||
@@ -341,6 +362,86 @@ public partial class MainWindow : Window
|
|||||||
return (processedCount, summary);
|
return (processedCount, summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void BtnGenerateVdf_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
string folder = TxtVdfRemoteFolder.Text.Trim();
|
||||||
|
if (string.IsNullOrEmpty(folder) || folder.StartsWith("Click"))
|
||||||
|
{ ShowBanner(VdfResultBanner, VdfResultText, "Please select a remote folder first.", BannerKind.Error); return; }
|
||||||
|
|
||||||
|
if (!Directory.Exists(folder))
|
||||||
|
{ ShowBanner(VdfResultBanner, VdfResultText, "The selected folder does not exist.", BannerKind.Error); return; }
|
||||||
|
|
||||||
|
SetBusy(true, BtnGenerateVdf, VdfProgress);
|
||||||
|
HideBanner(VdfResultBanner);
|
||||||
|
|
||||||
|
var log = new List<string>();
|
||||||
|
string summary;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
summary = await Task.Run(() => RunGenerateVdf(folder, log));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
summary = $"Error generating VDF: {ex.Message}";
|
||||||
|
log.Add($"[ERROR] {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SetBusy(false, BtnGenerateVdf, VdfProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendLog(log);
|
||||||
|
UpdateStatusBar("VDF generation completed.");
|
||||||
|
|
||||||
|
bool success = !summary.StartsWith("Error");
|
||||||
|
ShowBanner(VdfResultBanner, VdfResultText, summary,
|
||||||
|
success ? BannerKind.Success : BannerKind.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RunGenerateVdf(string remoteFolderPath, List<string> log)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Add("[INFO] Generating VDF from remote folder...");
|
||||||
|
var remoteCacheVdfFile = new RemoteCacheVdfFile(remoteFolderPath);
|
||||||
|
log.Add($"[INFO] Found {remoteCacheVdfFile.CachedFiles.Count} files in remote folder.");
|
||||||
|
|
||||||
|
var outputDirectory = remoteFolderPath;
|
||||||
|
if (string.IsNullOrEmpty(outputDirectory))
|
||||||
|
{
|
||||||
|
outputDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we already have a remotecache.vdf in the target location
|
||||||
|
string outputPath = Path.Combine(outputDirectory, $"{RemoteCacheVdfFile.FileName}{RemoteCacheVdfFile.FileExtension}");
|
||||||
|
if (File.Exists(outputPath))
|
||||||
|
{
|
||||||
|
log.Add($"[WARNING] A file named {RemoteCacheVdfFile.FileName}{RemoteCacheVdfFile.FileExtension} already exists in the target location.");
|
||||||
|
|
||||||
|
// Create a backup of the existing file
|
||||||
|
string backupPath = Path.Combine(outputDirectory, $"{RemoteCacheVdfFile.FileName}_backup_{DateTime.Now:yyyyMMddHHmmss}{RemoteCacheVdfFile.FileExtension}");
|
||||||
|
if (File.Exists(backupPath))
|
||||||
|
{
|
||||||
|
log.Add($"[WARNING] Backup file {backupPath} already exists. Overwriting backup.");
|
||||||
|
File.Delete(backupPath);
|
||||||
|
}
|
||||||
|
File.Move(outputPath, backupPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteCacheVdfFile.ExportAsFile(outputDirectory);
|
||||||
|
log.Add($"[SUCCESS] VDF file generated successfully.");
|
||||||
|
log.Add($"[INFO] Output: {Path.Combine(outputDirectory, "remotecache.vdf")}");
|
||||||
|
return $"✓ VDF generated successfully with {remoteCacheVdfFile.CachedFiles.Count} files.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Add($"[ERROR] {ex.Message}");
|
||||||
|
return $"Error: {ex.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private void AppendLog(IEnumerable<string> lines)
|
private void AppendLog(IEnumerable<string> lines)
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() =>
|
Dispatcher.Invoke(() =>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace EonaCat.FirstLight.SaveTransfer.VdfGenerator.KeyValue.Interfaces;
|
||||||
|
|
||||||
|
public interface IKeyValueNode
|
||||||
|
{
|
||||||
|
string Key { get; }
|
||||||
|
}
|
||||||
55
007SaveTool/VdfGenerator/KeyValue/KeyValueDeserializer.cs
Normal file
55
007SaveTool/VdfGenerator/KeyValue/KeyValueDeserializer.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using EonaCat.FirstLight.SaveTransfer.VdfGenerator.KeyValue.Models;
|
||||||
|
|
||||||
|
namespace EonaCat.FirstLight.SaveTransfer.VdfGenerator.KeyValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides static methods for deserializing text representations of key-value groups into KvGroup objects.
|
||||||
|
/// </summary>
|
||||||
|
public static class KeyValueDeserializer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Deserializes the specified text into a KvGroup object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The input string containing the serialized representation of a KvGroup. Cannot be null.</param>
|
||||||
|
/// <returns>A KvGroup object that represents the data contained in the input text.</returns>
|
||||||
|
public static KeyValueGroup Deserialize(string text)
|
||||||
|
{
|
||||||
|
var tokenizer = new KeyValueTokenizer(text);
|
||||||
|
return ParseGroup(tokenizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a group from the provided tokenizer, including any nested groups or key-value pairs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="t">The tokenizer used to read group names, symbols, and key-value pairs from the input stream. Must not be null and must be positioned at the start of a group.</param>
|
||||||
|
/// <param name="k">The optional name of the group to parse. If null, the group name is read from the tokenizer.</param>
|
||||||
|
/// <returns>A KvGroup representing the parsed group, including all nested groups and key-value pairs.</returns>
|
||||||
|
private static KeyValueGroup ParseGroup(KeyValueTokenizer t, string? k = null)
|
||||||
|
{
|
||||||
|
// Expect group name
|
||||||
|
var groupName = k ?? t.ReadString();
|
||||||
|
var group = new KeyValueGroup(groupName);
|
||||||
|
|
||||||
|
t.ReadSymbol('{');
|
||||||
|
|
||||||
|
while (!t.PeekSymbol('}'))
|
||||||
|
{
|
||||||
|
var key = t.ReadString();
|
||||||
|
|
||||||
|
if (t.PeekSymbol('{'))
|
||||||
|
{
|
||||||
|
// Nested group
|
||||||
|
group.Nodes.Add(ParseGroup(t, key));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Key-value pair
|
||||||
|
var value = t.ReadString();
|
||||||
|
group.Nodes.Add(new Models.KeyValuePair(key, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.ReadSymbol('}');
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
007SaveTool/VdfGenerator/KeyValue/KeyValueSerializer.cs
Normal file
53
007SaveTool/VdfGenerator/KeyValue/KeyValueSerializer.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using EonaCat.FirstLight.SaveTransfer.VdfGenerator.KeyValue.Models;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace EonaCat.FirstLight.SaveTransfer.VdfGenerator.KeyValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides functionality to serialize key–value groups into a text-based format.
|
||||||
|
/// </summary>
|
||||||
|
public class KeyValueSerializer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Serializes the given key–value group into a text format.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="group">The key–value group to serialize. Cannot be null.</param>
|
||||||
|
/// <returns>A string representing the serialized form of the key–value group.</returns>
|
||||||
|
public static string Serialize(KeyValueGroup group)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
SerializeGroup(group, sb, 0);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializes the specified key-value group and its child elements into a formatted string representation, appending the result to the provided StringBuilder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="group">The key-value group to serialize. Cannot be null.</param>
|
||||||
|
/// <param name="sb">The StringBuilder to which the serialized output is appended. Cannot be null.</param>
|
||||||
|
/// <param name="indent">The indentation level to apply to the serialized output.</param>
|
||||||
|
private static void SerializeGroup(KeyValueGroup group, StringBuilder sb, int indent)
|
||||||
|
{
|
||||||
|
const char padChar = '\t';
|
||||||
|
var pad = new string(padChar, indent);
|
||||||
|
|
||||||
|
sb.Append($"{pad}\"{group.Key}\"\n");
|
||||||
|
sb.Append($"{pad}{{\n");
|
||||||
|
|
||||||
|
foreach (var node in group.Nodes)
|
||||||
|
{
|
||||||
|
switch (node)
|
||||||
|
{
|
||||||
|
case Models.KeyValuePair pair:
|
||||||
|
sb.Append($"{pad}{padChar}\"{pair.Key}\"{padChar}{padChar}\"{pair.Value}\"\n");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyValueGroup childGroup:
|
||||||
|
SerializeGroup(childGroup, sb, indent + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append($"{pad}}}\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
81
007SaveTool/VdfGenerator/KeyValue/KeyValueTokenizer.cs
Normal file
81
007SaveTool/VdfGenerator/KeyValue/KeyValueTokenizer.cs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
namespace EonaCat.FirstLight.SaveTransfer.VdfGenerator.KeyValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides functionality for tokenizing a key-value formatted string input, allowing sequential reading and validation of symbols and quoted strings.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The input string to tokenize. Must not be null.</param>
|
||||||
|
public class KeyValueTokenizer(string text)
|
||||||
|
{
|
||||||
|
private int _pos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the next non-whitespace character in the input matches the specified character without advancing the current position.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="c">The character to compare with the next non-whitespace character in the input.</param>
|
||||||
|
/// <returns><see langword="true"/> if the next non-whitespace character matches the specified character; otherwise, <see langword="false"/>.</returns>
|
||||||
|
public bool PeekSymbol(char c)
|
||||||
|
{
|
||||||
|
SkipWhitespace();
|
||||||
|
return _pos < text.Length && text[_pos] == c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the next non-whitespace character from the input and verifies that it matches the specified symbol.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="c">The character to match at the current position in the input.</param>
|
||||||
|
/// <exception cref="Exception">Thrown if the next non-whitespace character does not match the specified symbol.</exception>
|
||||||
|
public void ReadSymbol(char c)
|
||||||
|
{
|
||||||
|
SkipWhitespace();
|
||||||
|
if (_pos >= text.Length || text[_pos] != c)
|
||||||
|
throw new Exception($"Expected '{c}' at position {_pos}");
|
||||||
|
|
||||||
|
_pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads a string enclosed in quotation marks from the current position in the input text.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns> The string located between the opening and closing quotation marks or an empty string if there are no characters between the quotes. </returns>
|
||||||
|
/// <exception cref="Exception"> Thrown when the current position does not contain an opening quotation mark or when a closing quotation mark cannot be found. </exception>
|
||||||
|
public string ReadString()
|
||||||
|
{
|
||||||
|
SkipWhitespace();
|
||||||
|
|
||||||
|
if (_pos >= text.Length || text[_pos] != '"')
|
||||||
|
throw new Exception($"Expected '\"' at position {_pos}, but found '{text[_pos]}'!");
|
||||||
|
|
||||||
|
_pos++; // skip opening quote
|
||||||
|
var start = _pos;
|
||||||
|
|
||||||
|
while (_pos < text.Length && text[_pos] != '"')
|
||||||
|
_pos++;
|
||||||
|
|
||||||
|
if (_pos >= text.Length)
|
||||||
|
throw new Exception("Unterminated string literal!");
|
||||||
|
|
||||||
|
var result = text.Substring(start, _pos - start);
|
||||||
|
_pos++; // skip closing quote
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Advances the current position past any consecutive whitespace characters in the input text.
|
||||||
|
/// </summary>
|
||||||
|
private void SkipWhitespace()
|
||||||
|
{
|
||||||
|
while (_pos < text.Length)
|
||||||
|
{
|
||||||
|
var c = text[_pos];
|
||||||
|
|
||||||
|
if (c is ' ' or '\t' or '\n' or '\r')
|
||||||
|
{
|
||||||
|
_pos++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
007SaveTool/VdfGenerator/KeyValue/Models/KeyValueGroup.cs
Normal file
36
007SaveTool/VdfGenerator/KeyValue/Models/KeyValueGroup.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using EonaCat.FirstLight.SaveTransfer.VdfGenerator.KeyValue.Interfaces;
|
||||||
|
|
||||||
|
namespace EonaCat.FirstLight.SaveTransfer.VdfGenerator.KeyValue.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a hierarchical group of key-value nodes, allowing organization of nested key-value pairs and groups.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key that identifies this group. Cannot be null.</param>
|
||||||
|
public class KeyValueGroup(string key) : IKeyValueNode
|
||||||
|
{
|
||||||
|
public string Key { get; } = key;
|
||||||
|
public List<IKeyValueNode> Nodes { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a new key-value pair to the group.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key to associate with the value. Cannot be null.</param>
|
||||||
|
/// <param name="value">The value to associate with the key. Cannot be null.</param>
|
||||||
|
/// <returns>The current instance with the new key-value pair added.</returns>
|
||||||
|
public KeyValueGroup Add(string key, string value)
|
||||||
|
{
|
||||||
|
Nodes.Add(new KeyValuePair(key, value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the specified group to the collection of nodes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="group">The group to add to the collection. Cannot be null.</param>
|
||||||
|
/// <returns>The current instance with the added group, enabling method chaining.</returns>
|
||||||
|
public KeyValueGroup Add(KeyValueGroup group)
|
||||||
|
{
|
||||||
|
Nodes.Add(group);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
007SaveTool/VdfGenerator/KeyValue/Models/KeyValuePair.cs
Normal file
14
007SaveTool/VdfGenerator/KeyValue/Models/KeyValuePair.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using EonaCat.FirstLight.SaveTransfer.VdfGenerator.KeyValue.Interfaces;
|
||||||
|
|
||||||
|
namespace EonaCat.FirstLight.SaveTransfer.VdfGenerator.KeyValue.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a key-value pair node with immutable key and value properties.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key associated with the pair. If not specified, an empty string is used.</param>
|
||||||
|
/// <param name="value">The value associated with the pair. If not specified, an empty string is used.</param>
|
||||||
|
public class KeyValuePair(string key = "", string value = "") : IKeyValueNode
|
||||||
|
{
|
||||||
|
public string Key { get; } = key;
|
||||||
|
public string Value { get; } = value;
|
||||||
|
}
|
||||||
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
20
007SaveTool/VdfGenerator/NumberParser.cs
Normal file
20
007SaveTool/VdfGenerator/NumberParser.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace EonaCat.FirstLight.SaveTransfer.VdfGenerator;
|
||||||
|
|
||||||
|
public static class NumberParser
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parses the specified string as a 32-bit signed integer value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="s">The string representation of the number to parse.</param>
|
||||||
|
/// <returns>The parsed 32-bit signed integer value if the parse operation succeeds; otherwise, 0.</returns>
|
||||||
|
public static int ParseInt(string s)
|
||||||
|
=> int.TryParse(s, out var v) ? v : 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses the specified string representation of a number to a 64-bit signed integer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="s">The string containing the number to parse.</param>
|
||||||
|
/// <returns>The parsed 64-bit signed integer value if parsing succeeds; otherwise, 0.</returns>
|
||||||
|
public static long ParseLong(string s)
|
||||||
|
=> long.TryParse(s, out var v) ? v : 0;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 18
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 18.5.11801.241 oobstable
|
VisualStudioVersion = 18.5.11801.241
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.FirstLight.SaveTransfer", "007SaveTool\EonaCat.FirstLight.SaveTransfer.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.FirstLight.SaveTransfer", "007SaveTool\EonaCat.FirstLight.SaveTransfer.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|||||||
Reference in New Issue
Block a user