Initial version

This commit is contained in:
2026-05-31 07:38:10 +02:00
parent e363eb1491
commit ec7031d9fd
8 changed files with 1360 additions and 0 deletions

198
007SaveTool/App.xaml Normal file
View File

@@ -0,0 +1,198 @@
<Application x:Class="EonaCat.FirstLight.SaveTransfer.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<!-- ── Colour Palette ─────────────────────────────────────────── -->
<Color x:Key="BgDark">#1A1A2E</Color>
<Color x:Key="BgMid">#16213E</Color>
<Color x:Key="BgCard">#0F3460</Color>
<Color x:Key="AccentGold">#D4AF37</Color>
<Color x:Key="AccentGoldHover">#F0C840</Color>
<Color x:Key="TextPrimary">#E8E8E8</Color>
<Color x:Key="TextSecondary">#A0A8B8</Color>
<Color x:Key="TextMuted">#6A7280</Color>
<Color x:Key="Success">#22C55E</Color>
<Color x:Key="Error">#EF4444</Color>
<Color x:Key="Warning">#F59E0B</Color>
<Color x:Key="Info">#3B82F6</Color>
<Color x:Key="BorderColor">#1E3A5F</Color>
<SolidColorBrush x:Key="BgDarkBrush" Color="{StaticResource BgDark}"/>
<SolidColorBrush x:Key="BgMidBrush" Color="{StaticResource BgMid}"/>
<SolidColorBrush x:Key="BgCardBrush" Color="{StaticResource BgCard}"/>
<SolidColorBrush x:Key="AccentGoldBrush" Color="{StaticResource AccentGold}"/>
<SolidColorBrush x:Key="TextPrimaryBrush" Color="{StaticResource TextPrimary}"/>
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondary}"/>
<SolidColorBrush x:Key="TextMutedBrush" Color="{StaticResource TextMuted}"/>
<SolidColorBrush x:Key="SuccessBrush" Color="{StaticResource Success}"/>
<SolidColorBrush x:Key="ErrorBrush" Color="{StaticResource Error}"/>
<SolidColorBrush x:Key="WarningBrush" Color="{StaticResource Warning}"/>
<SolidColorBrush x:Key="InfoBrush" Color="{StaticResource Info}"/>
<SolidColorBrush x:Key="BorderBrush" Color="{StaticResource BorderColor}"/>
<!-- ── TextBox Style ─────────────────────────────────────────── -->
<Style x:Key="DarkTextBox" TargetType="TextBox">
<Setter Property="Background" Value="#0D1B2A"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontFamily" Value="Consolas"/>
<Setter Property="CaretBrush" Value="{StaticResource AccentGoldBrush}"/>
<Setter Property="SelectionBrush" Value="{StaticResource AccentGoldBrush}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6">
<ScrollViewer x:Name="PART_ContentHost"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="BorderBrush" Value="{StaticResource AccentGoldBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ── Primary Gold Button ───────────────────────────────────── -->
<Style x:Key="GoldButton" TargetType="Button">
<Setter Property="Background" Value="{StaticResource AccentGoldBrush}"/>
<Setter Property="Foreground" Value="#1A1A2E"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="24,12"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
CornerRadius="8"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background"
Value="{StaticResource AccentGoldBrush}"/>
<Setter TargetName="Bd" Property="Opacity" Value="0.85"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bd" Property="Opacity" Value="0.7"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Bd" Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ── Secondary/Outline Button ─────────────────────────────── -->
<Style x:Key="OutlineButton" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource AccentGoldBrush}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Padding" Value="16,10"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{StaticResource AccentGoldBrush}"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#1A3050"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Bd" Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ── Tab Control Styles ────────────────────────────────────── -->
<Style x:Key="DarkTabControl" TargetType="TabControl">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
</Style>
<Style x:Key="DarkTabItem" TargetType="TabItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="20,10"/>
<Setter Property="BorderThickness" Value="0,0,0,2"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabItem">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ContentPresenter x:Name="ContentSite"
VerticalAlignment="Center"
HorizontalAlignment="Center"
ContentSource="Header"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="BorderBrush"
Value="{StaticResource AccentGoldBrush}"/>
<Setter Property="Foreground"
Value="{StaticResource AccentGoldBrush}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground"
Value="{StaticResource TextPrimaryBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ── ScrollBar Style ───────────────────────────────────────── -->
<Style TargetType="ScrollBar">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Width" Value="6"/>
</Style>
<!-- ── Label Style ───────────────────────────────────────────── -->
<Style x:Key="FieldLabel" TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="0,0,0,4"/>
</Style>
</Application.Resources>
</Application>

5
007SaveTool/App.xaml.cs Normal file
View File

@@ -0,0 +1,5 @@
using System.Windows;
namespace EonaCat.FirstLight.SaveTransfer;
public partial class App : Application { }

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<AssemblyName>EonaCat.FirstLight.SaveTransfer</AssemblyName>
<RootNamespace>EonaCat.FirstLight.SaveTransfer</RootNamespace>
<ApplicationIcon>icon.ico</ApplicationIcon>
<Platforms>AnyCPU</Platforms>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<Optimize>true</Optimize>
</PropertyGroup>
</Project>

292
007SaveTool/MainWindow.xaml Normal file
View File

@@ -0,0 +1,292 @@
<Window x:Class="EonaCat.FirstLight.SaveTransfer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EonaCat.FirstLight.SaveTransfer"
Title="EonaCat FirstLight SaveTransfer"
Height="760" Width="920"
MinHeight="680" MinWidth="820"
WindowStartupLocation="CenterScreen"
Background="{StaticResource BgDarkBrush}"
FontFamily="Segoe UI">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolVis"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<!-- header -->
<RowDefinition Height="Auto"/>
<!-- tabs bar -->
<RowDefinition Height="*"/>
<!-- content -->
<RowDefinition Height="Auto"/>
<!-- status bar -->
</Grid.RowDefinitions>
<!-- ═══════════════════════════════════════════════════════════
HEADER
════════════════════════════════════════════════════════════════ -->
<Border Grid.Row="0" Background="{StaticResource BgMidBrush}"
BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,0,0,1">
<Grid Margin="28,18,28,18">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<!-- Bond logo / bullet icon -->
<Border Width="36" Height="36" CornerRadius="18"
Background="{StaticResource AccentGoldBrush}"
Margin="0,0,14,0">
<TextBlock Text="007" FontWeight="Bold" FontSize="11"
Foreground="#1A1A2E"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="EonaCat FirstLight SaveTransfer"
Foreground="{StaticResource TextPrimaryBrush}"
FontSize="18" FontWeight="Bold"/>
<TextBlock Text="Resign · Decrypt · Inspect"
Foreground="{StaticResource TextMutedBrush}"
FontSize="11" Margin="0,2,0,0"/>
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Border Background="#0D2035" CornerRadius="6" Padding="10,5" Margin="0,0,10,0">
<TextBlock x:Name="TxtStatus" Text="Ready"
Foreground="{StaticResource TextSecondaryBrush}"
FontSize="12" VerticalAlignment="Center"/>
</Border>
</StackPanel>
</Grid>
</Border>
<!-- ═══════════════════════════════════════════════════════════
TAB BAR
════════════════════════════════════════════════════════════════ -->
<Border Grid.Row="1" Background="{StaticResource BgMidBrush}"
BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,0,0,1">
<TabControl x:Name="MainTabs" Style="{StaticResource DarkTabControl}"
Background="Transparent" Margin="14,0,14,0">
<TabControl.ContentTemplate>
<DataTemplate><!-- tabs only used for header switching --></DataTemplate>
</TabControl.ContentTemplate>
<TabItem Header="⚙ Resign Save" Style="{StaticResource DarkTabItem}"
x:Name="TabResign"/>
<TabItem Header="🔓 Decrypt / Inspect" Style="{StaticResource DarkTabItem}"
x:Name="TabDecrypt"/>
<TabItem Header="📋 Log" Style="{StaticResource DarkTabItem}"
x:Name="TabLog"/>
</TabControl>
</Border>
<!-- ═══════════════════════════════════════════════════════════
CONTENT PANELS
════════════════════════════════════════════════════════════════ -->
<Grid Grid.Row="2">
<!-- ── RESIGN PANEL ──────────────────────────────────────── -->
<ScrollViewer x:Name="PanelResign" VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="32,28,32,28">
<!-- Save Folder -->
<TextBlock Text="SAVE FOLDER" Style="{StaticResource FieldLabel}"/>
<Grid Margin="0,0,0,18">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="TxtResignFolder" Style="{StaticResource DarkTextBox}"
IsReadOnly="True" Grid.Column="0"
Text="Click Browse to select your save folder…"
Foreground="{StaticResource TextMutedBrush}"/>
<Button Grid.Column="1" Content="Browse…"
Style="{StaticResource OutlineButton}"
Margin="10,0,0,0" Click="BrowseResignFolder_Click"/>
</Grid>
<!-- Target SteamID64 -->
<TextBlock Text="TARGET STEAMID64 (the account you want to resign TO)"
Style="{StaticResource FieldLabel}"/>
<TextBox x:Name="TxtTargetSID" Style="{StaticResource DarkTextBox}"
Margin="0,0,0,6"
ToolTip="Your SteamID64 — find it at steamid.io"
PreviewTextInput="NumericOnly_PreviewInput"/>
<TextBlock Foreground="{StaticResource TextMutedBrush}" FontSize="11"
Margin="0,0,0,18">
Find your SteamID64 at
<Hyperlink NavigateUri="https://steamid.io"
RequestNavigate="Hyperlink_RequestNavigate"
Foreground="{StaticResource AccentGoldBrush}">
steamid.io
</Hyperlink>
— it looks like 76561197960272671
</TextBlock>
<!-- Source SteamID64 (optional) -->
<TextBlock Text="SOURCE STEAMID64 (optional — leave blank to auto-detect)"
Style="{StaticResource FieldLabel}"/>
<TextBox x:Name="TxtSourceSID" Style="{StaticResource DarkTextBox}"
Margin="0,0,0,6"
ToolTip="Only needed if auto-detect fails. The original account's SteamID64."
PreviewTextInput="NumericOnly_PreviewInput"/>
<TextBlock Foreground="{StaticResource TextMutedBrush}" FontSize="11"
Margin="0,0,0,24">
When left blank the tool will auto-detect from <Run FontFamily="Consolas">index.save</Run>
and bruteforce the <Run FontFamily="Consolas">data.save</Run> key.
</TextBlock>
<!-- Mismatch option -->
<Border Background="#0D1E30" CornerRadius="8" Padding="16,12"
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"
Margin="0,0,0,24">
<StackPanel>
<CheckBox x:Name="ChkAutoConfirm"
Foreground="{StaticResource TextPrimaryBrush}"
FontSize="13" IsChecked="False">
<TextBlock TextWrapping="Wrap">
Auto-confirm SteamID mismatches between
<Run FontFamily="Consolas">index.save</Run> and
<Run FontFamily="Consolas">data.save</Run>
</TextBlock>
</CheckBox>
</StackPanel>
</Border>
<!-- Action Button -->
<Button x:Name="BtnResign" Content="🔑 Resign Save Files"
Style="{StaticResource GoldButton}"
HorizontalAlignment="Stretch" Height="48"
Click="BtnResign_Click"/>
<!-- Progress -->
<ProgressBar x:Name="ResignProgress" Height="4" Margin="0,14,0,0"
Background="#0D1E30" Foreground="{StaticResource AccentGoldBrush}"
BorderThickness="0" Visibility="Collapsed" IsIndeterminate="True"/>
<!-- Inline result banner -->
<Border x:Name="ResignResultBanner" Visibility="Collapsed"
CornerRadius="8" Padding="16,12" Margin="0,14,0,0">
<StackPanel>
<TextBlock x:Name="ResignResultText"
Foreground="White" FontSize="13"
TextWrapping="Wrap"/>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<!-- ── DECRYPT PANEL ─────────────────────────────────────── -->
<ScrollViewer x:Name="PanelDecrypt" Visibility="Collapsed"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="32,28,32,28">
<!-- Save Folder -->
<TextBlock Text="SAVE FOLDER" Style="{StaticResource FieldLabel}"/>
<Grid Margin="0,0,0,18">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="TxtDecryptFolder" Style="{StaticResource DarkTextBox}"
IsReadOnly="True" Grid.Column="0"
Text="Click Browse to select your save folder…"
Foreground="{StaticResource TextMutedBrush}"/>
<Button Grid.Column="1" Content="Browse…"
Style="{StaticResource OutlineButton}"
Margin="10,0,0,0" Click="BrowseDecryptFolder_Click"/>
</Grid>
<!-- SteamID64 -->
<TextBlock Text="STEAMID64 (optional — leave blank to auto-detect)"
Style="{StaticResource FieldLabel}"/>
<TextBox x:Name="TxtDecryptSID" Style="{StaticResource DarkTextBox}"
Margin="0,0,0,6"
PreviewTextInput="NumericOnly_PreviewInput"/>
<TextBlock Foreground="{StaticResource TextMutedBrush}" FontSize="11"
Margin="0,0,0,24">
Leave blank to auto-detect from <Run FontFamily="Consolas">index.save</Run>.
Output files will be saved next to the originals with a <Run FontFamily="Consolas">.decrypted</Run> extension.
</TextBlock>
<!-- Action Button -->
<Button x:Name="BtnDecrypt" Content="🔓 Decrypt Save Files"
Style="{StaticResource GoldButton}"
HorizontalAlignment="Stretch" Height="48"
Click="BtnDecrypt_Click"/>
<!-- Progress -->
<ProgressBar x:Name="DecryptProgress" Height="4" Margin="0,14,0,0"
Background="#0D1E30" Foreground="{StaticResource AccentGoldBrush}"
BorderThickness="0" Visibility="Collapsed" IsIndeterminate="True"/>
<!-- Inline result -->
<Border x:Name="DecryptResultBanner" Visibility="Collapsed"
CornerRadius="8" Padding="16,12" Margin="0,14,0,0">
<TextBlock x:Name="DecryptResultText" Foreground="White"
FontSize="13" TextWrapping="Wrap"/>
</Border>
</StackPanel>
</ScrollViewer>
<!-- ── LOG PANEL ─────────────────────────────────────────── -->
<Grid x:Name="PanelLog" Visibility="Collapsed" Margin="32,28,32,28">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="OPERATION LOG"
Style="{StaticResource FieldLabel}" Margin="0,0,0,10"/>
<Border Grid.Row="1" Background="#080F18" CornerRadius="8"
BorderBrush="{StaticResource BorderBrush}" BorderThickness="1">
<ScrollViewer x:Name="LogScroll"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
Padding="4">
<TextBlock x:Name="TxtLog"
FontFamily="Consolas" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}"
Padding="12" TextWrapping="Wrap"/>
</ScrollViewer>
</Border>
<Button Grid.Row="2" Content="🗑 Clear Log"
Style="{StaticResource OutlineButton}"
HorizontalAlignment="Right" Margin="0,10,0,0"
Click="ClearLog_Click"/>
</Grid>
</Grid>
<!-- ═══════════════════════════════════════════════════════════
STATUS BAR
════════════════════════════════════════════════════════════════ -->
<Border Grid.Row="3" Background="{StaticResource BgMidBrush}"
BorderBrush="{StaticResource BorderBrush}" BorderThickness="0,1,0,0"
Padding="28,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="TxtStatusBar"
Text="Select a folder and enter your Target SteamID64 to get started."
Foreground="{StaticResource TextMutedBrush}" FontSize="11"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="1"
Text="EonaCat.FirstLight.SaveTransfer"
Foreground="{StaticResource TextMutedBrush}" FontSize="11"
VerticalAlignment="Center"/>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,428 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
namespace EonaCat.FirstLight.SaveTransfer;
public partial class MainWindow : Window
{
private readonly StringBuilder _logBuffer = new();
public MainWindow()
{
InitializeComponent();
MainTabs.SelectionChanged += MainTabs_SelectionChanged;
}
private void MainTabs_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (MainTabs.SelectedItem is not TabItem tab)
{
return;
}
PanelResign.Visibility = Visibility.Collapsed;
PanelDecrypt.Visibility = Visibility.Collapsed;
PanelLog.Visibility = Visibility.Collapsed;
if (tab == TabResign)
{
PanelResign.Visibility = Visibility.Visible;
}
if (tab == TabDecrypt)
{
PanelDecrypt.Visibility = Visibility.Visible;
}
if (tab == TabLog)
{
PanelLog.Visibility = Visibility.Visible;
}
}
private void BrowseResignFolder_Click(object sender, RoutedEventArgs e)
{
string? path = BrowseForFolder();
if (path is null)
{
return;
}
TxtResignFolder.Text = path;
TxtResignFolder.Foreground = (Brush)FindResource("TextPrimaryBrush");
}
private void BrowseDecryptFolder_Click(object sender, RoutedEventArgs e)
{
string? path = BrowseForFolder();
if (path is null)
{
return;
}
TxtDecryptFolder.Text = path;
TxtDecryptFolder.Foreground = (Brush)FindResource("TextPrimaryBrush");
}
private static string? BrowseForFolder()
{
var dlg = new Microsoft.Win32.OpenFolderDialog
{
Title = "Select your save folder (e.g. …\\userdata\\<AccountId>\\3768760\\remote)",
};
return dlg.ShowDialog() == true ? dlg.FolderName : null;
}
private void NumericOnly_PreviewInput(object sender, TextCompositionEventArgs e)
{
foreach (char c in e.Text)
{
if (!char.IsDigit(c)) { e.Handled = true; return; }
}
}
private async void BtnResign_Click(object sender, RoutedEventArgs e)
{
string folder = TxtResignFolder.Text.Trim();
if (string.IsNullOrEmpty(folder) || folder.StartsWith("Click"))
{ ShowBanner(ResignResultBanner, ResignResultText, "Please select a save folder first.", BannerKind.Error); return; }
string targetText = TxtTargetSID.Text.Trim();
if (!ulong.TryParse(targetText, out ulong targetSid))
{ ShowBanner(ResignResultBanner, ResignResultText, "Please enter a valid Target SteamID64.", BannerKind.Error); return; }
ulong? userSourceSid = null;
string sourceText = TxtSourceSID.Text.Trim();
if (!string.IsNullOrEmpty(sourceText))
{
if (!ulong.TryParse(sourceText, out ulong s))
{ ShowBanner(ResignResultBanner, ResignResultText, "Source SteamID64 is invalid — leave blank to auto-detect.", BannerKind.Error); return; }
userSourceSid = s;
}
bool autoConfirm = ChkAutoConfirm.IsChecked == true;
SetBusy(true, BtnResign, ResignProgress);
HideBanner(ResignResultBanner);
var log = new List<string>();
int resignedDirs = 0;
string summary;
try
{
(resignedDirs, summary) = await Task.Run(() =>
RunResign(folder, targetSid, userSourceSid, autoConfirm, log));
}
finally
{
SetBusy(false, BtnResign, ResignProgress);
}
// Flush to log tab
AppendLog(log);
UpdateStatusBar($"Resigned {resignedDirs} save container(s).");
bool success = resignedDirs > 0;
ShowBanner(ResignResultBanner, ResignResultText, summary,
success ? BannerKind.Success : BannerKind.Warning);
}
private static (int resigned, string summary) RunResign(
string rootDir, ulong targetSid, ulong? userSourceSid,
bool autoConfirm, List<string> log)
{
var containers = SaveCore.FindSaveContainers(rootDir);
if (containers.Count == 0)
{
log.Add("[INFO] No save containers found (no index.save / data.save).");
return (0, "No save containers found in the selected folder.");
}
int resignedDirs = 0;
foreach (var container in containers)
{
log.Add($"\nFound save container: {container.Directory}");
string indexPath = System.IO.Path.Combine(container.Directory, "index.save");
string dataPath = System.IO.Path.Combine(container.Directory, "data.save");
if (container.HasIndex && container.HasData)
{
ulong? indexSid = SaveCore.DetectSourceSteamIdFromIndex(indexPath);
log.Add(" [AUTO] Bruteforcing data.save encryption key…");
var sw = System.Diagnostics.Stopwatch.StartNew();
ulong? dataSid = SaveCore.BruteforceDataSaveKey(dataPath);
sw.Stop();
if (dataSid.HasValue)
{
log.Add($" [AUTO] data.save key cracked in {sw.Elapsed.TotalSeconds:F3}s: {dataSid}");
}
else
{
log.Add(" [ERROR] Bruteforce failed. Skipping. Use manual Source SteamID.");
continue;
}
if (!indexSid.HasValue)
{
log.Add(" [ERROR] index.save auto-detect failed. Skipping.");
continue;
}
if (indexSid == dataSid)
{
log.Add($" [OK] Both files bound to SteamID64: {indexSid}");
ulong src = userSourceSid ?? indexSid.Value;
SaveCore.ResignIndexFile(indexPath, src, targetSid, log);
SaveCore.ResignDataFile(dataPath, src, targetSid, log);
}
else
{
log.Add($" [WARNING] STEAMID MISMATCH!");
log.Add($" index.save → {indexSid}");
log.Add($" data.save → {dataSid}");
if (userSourceSid.HasValue)
{
log.Add($" Using manual override: {userSourceSid}");
SaveCore.ResignIndexFile(indexPath, userSourceSid.Value, targetSid, log);
SaveCore.ResignDataFile(dataPath, userSourceSid.Value, targetSid, log);
}
else if (autoConfirm)
{
log.Add(" Proceeding with split re-signing (auto-confirm).");
SaveCore.ResignIndexFile(indexPath, indexSid.Value, targetSid, log);
SaveCore.ResignDataFile(dataPath, dataSid.Value, targetSid, log);
}
else
{
// In GUI mode without auto-confirm, default to split signing
log.Add(" Proceeding with dynamic split re-signing.");
SaveCore.ResignIndexFile(indexPath, indexSid.Value, targetSid, log);
SaveCore.ResignDataFile(dataPath, dataSid.Value, targetSid, log);
}
}
resignedDirs++;
}
else if (container.HasIndex)
{
ulong? indexSid = SaveCore.DetectSourceSteamIdFromIndex(indexPath);
if (!indexSid.HasValue)
{ log.Add(" [ERROR] index.save auto-detect failed. Skipping."); continue; }
ulong src = userSourceSid ?? indexSid.Value;
SaveCore.ResignIndexFile(indexPath, src, targetSid, log);
log.Add(" [INFO] No data.save present.");
resignedDirs++;
}
else if (container.HasData)
{
log.Add(" [AUTO] Bruteforcing data.save…");
ulong? dataSid = SaveCore.BruteforceDataSaveKey(dataPath);
if (!dataSid.HasValue)
{ log.Add(" [ERROR] Bruteforce failed. Skipping."); continue; }
ulong src = userSourceSid ?? dataSid.Value;
SaveCore.ResignDataFile(dataPath, src, targetSid, log);
log.Add(" [INFO] No index.save present.");
resignedDirs++;
}
}
string summary = resignedDirs > 0
? $"✓ Resigned {resignedDirs} save container(s) successfully. Backups created."
: "No containers were resigned. Check the Log tab for details.";
return (resignedDirs, summary);
}
private async void BtnDecrypt_Click(object sender, RoutedEventArgs e)
{
string folder = TxtDecryptFolder.Text.Trim();
if (string.IsNullOrEmpty(folder) || folder.StartsWith("Click"))
{ ShowBanner(DecryptResultBanner, DecryptResultText, "Please select a save folder first.", BannerKind.Error); return; }
ulong? manualSid = null;
string sidText = TxtDecryptSID.Text.Trim();
if (!string.IsNullOrEmpty(sidText))
{
if (!ulong.TryParse(sidText, out ulong s))
{ ShowBanner(DecryptResultBanner, DecryptResultText, "SteamID64 is invalid.", BannerKind.Error); return; }
manualSid = s;
}
SetBusy(true, BtnDecrypt, DecryptProgress);
HideBanner(DecryptResultBanner);
var log = new List<string>();
int processed;
string summary;
try
{
(processed, summary) = await Task.Run(() =>
RunDecrypt(folder, manualSid, log));
}
finally
{
SetBusy(false, BtnDecrypt, DecryptProgress);
}
AppendLog(log);
UpdateStatusBar($"Decrypted {processed} container(s).");
ShowBanner(DecryptResultBanner, DecryptResultText, summary,
processed > 0 ? BannerKind.Success : BannerKind.Warning);
}
private static (int processed, string summary) RunDecrypt(
string rootDir, ulong? manualSid, List<string> log)
{
var containers = SaveCore.FindSaveContainers(rootDir);
if (containers.Count == 0)
{
log.Add("[INFO] No save containers found.");
return (0, "No save containers found in the selected folder.");
}
int processedCount = 0;
foreach (var container in containers)
{
log.Add($"\nSave container: {container.Directory}");
string indexPath = System.IO.Path.Combine(container.Directory, "index.save");
string dataPath = System.IO.Path.Combine(container.Directory, "data.save");
ulong? decSid = manualSid;
if (!decSid.HasValue)
{
if (container.HasIndex)
{
decSid = SaveCore.DetectSourceSteamIdFromIndex(indexPath);
if (decSid.HasValue)
{
log.Add($" [AUTO] Detected SteamID64: {decSid}");
}
else
{ log.Add(" [ERROR] Auto-detect failed. Skipping."); continue; }
}
else
{ log.Add(" [ERROR] No index.save for auto-detection. Skipping."); continue; }
}
if (container.HasIndex)
{
SaveCore.DecryptIndexFile(indexPath, decSid.Value, log);
}
if (container.HasData)
{
SaveCore.DecryptDataFile(dataPath, decSid.Value, log);
}
processedCount++;
}
string summary = processedCount > 0
? $"✓ Decrypted {processedCount} container(s). .decrypted files written."
: "Nothing was decrypted. Check the Log tab for details.";
return (processedCount, summary);
}
private void AppendLog(IEnumerable<string> lines)
{
Dispatcher.Invoke(() =>
{
foreach (string line in lines)
{
_logBuffer.AppendLine(line);
// Colour-code lines
var run = new Run(line + "\n");
if (line.Contains("[SUCCESS]") || line.Contains("[OK]") || line.StartsWith(" ✓"))
{
run.Foreground = (Brush)FindResource("SuccessBrush");
}
else if (line.Contains("[ERROR]"))
{
run.Foreground = (Brush)FindResource("ErrorBrush");
}
else if (line.Contains("[WARNING]") || line.Contains("[WARN]"))
{
run.Foreground = (Brush)FindResource("WarningBrush");
}
else if (line.Contains("[AUTO]") || line.Contains("[INFO]"))
{
run.Foreground = (Brush)FindResource("InfoBrush");
}
else
{
run.Foreground = (Brush)FindResource("TextSecondaryBrush");
}
TxtLog.Inlines.Add(run);
}
LogScroll.ScrollToBottom();
});
}
private void ClearLog_Click(object sender, RoutedEventArgs e)
{
TxtLog.Inlines.Clear();
_logBuffer.Clear();
}
private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true });
e.Handled = true;
}
private void SetBusy(bool busy, Button btn, System.Windows.Controls.ProgressBar progress)
{
btn.IsEnabled = !busy;
progress.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
TxtStatus.Text = busy ? "Working…" : "Ready";
}
private void UpdateStatusBar(string msg) =>
TxtStatusBar.Text = msg;
private enum BannerKind { Success, Warning, Error, Info }
private void ShowBanner(Border banner, TextBlock text, string message, BannerKind kind)
{
banner.Visibility = Visibility.Visible;
text.Text = message;
banner.Background = kind switch
{
BannerKind.Success => new SolidColorBrush(Color.FromArgb(40, 34, 197, 94)),
BannerKind.Warning => new SolidColorBrush(Color.FromArgb(40, 245, 158, 11)),
BannerKind.Error => new SolidColorBrush(Color.FromArgb(40, 239, 68, 68)),
_ => new SolidColorBrush(Color.FromArgb(40, 59, 130, 246)),
};
banner.BorderBrush = kind switch
{
BannerKind.Success => (Brush)FindResource("SuccessBrush"),
BannerKind.Warning => (Brush)FindResource("WarningBrush"),
BannerKind.Error => (Brush)FindResource("ErrorBrush"),
_ => (Brush)FindResource("InfoBrush"),
};
banner.BorderThickness = new Thickness(1);
text.Foreground = banner.BorderBrush;
}
private static void HideBanner(Border banner) =>
banner.Visibility = Visibility.Collapsed;
}

399
007SaveTool/SaveCore.cs Normal file
View File

@@ -0,0 +1,399 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text;
namespace EonaCat.FirstLight.SaveTransfer;
/// <summary>
/// All encryption uses XOR with the little-endian SteamID64 (8-byte repeating key).
/// </summary>
public static class SaveCore
{
private static byte[] GetSteamIdKeyBytes(ulong steamId64)
{
// struct.pack("<Q", steam_id) → 8 bytes, little-endian
byte[] key = new byte[8];
ulong v = steamId64;
for (int i = 0; i < 8; i++) { key[i] = (byte)(v & 0xFF); v >>= 8; }
return key;
}
/// <summary>XOR every byte with the 8-byte repeating SteamID key.</summary>
public static byte[] XorWithSteamId(ReadOnlySpan<byte> data, ulong steamId64)
{
byte[] key = GetSteamIdKeyBytes(steamId64);
byte[] result = new byte[data.Length];
for (int i = 0; i < data.Length; i++)
{
result[i] = (byte)(data[i] ^ key[i % 8]);
}
return result;
}
/// <summary>
/// Read bytes 24..27 (little-endian uint32) from index.save, convert to SteamID64.
/// </summary>
public static ulong? DetectSourceSteamIdFromIndex(string indexPath)
{
// Use the Backup copy if available
string backupPath = Path.Combine(Path.GetDirectoryName(indexPath)!, "Backup",
Path.GetFileName(indexPath));
string targetPath = File.Exists(backupPath) ? backupPath : indexPath;
if (!File.Exists(targetPath))
{
return null;
}
try
{
byte[] data = File.ReadAllBytes(targetPath);
if (data.Length < 28)
{
return null;
}
uint accountId = BitConverter.ToUInt32(data, 24); // little-endian uint32
return 76561197960265728UL + accountId;
}
catch { return null; }
}
/// <summary>
/// Recover the SteamID64 used to encrypt data.save by brute-forcing bytes 2 and 3
/// of the key while exploiting known zlib header magic (CMF=0x78).
/// </summary>
public static ulong? BruteforceDataSaveKey(string filePath, IProgress<string>? progress = null)
{
if (!File.Exists(filePath))
{
return null;
}
string backupPath = Path.Combine(Path.GetDirectoryName(filePath)!, "Backup",
Path.GetFileName(filePath));
string targetPath = File.Exists(backupPath) ? backupPath : filePath;
byte[] ciphertext;
try { ciphertext = File.ReadAllBytes(targetPath); }
catch { return null; }
if (ciphertext.Length < 32)
{
return null;
}
// b0 comes from knowing first plaintext byte is 0x78 (zlib CMF)
byte b0 = (byte)(ciphertext[0] ^ 0x78);
// b1 candidates from valid zlib FLG values that make (cmf*256+flg) % 31 == 0
// where cmf = 0x78: 0x78*256 + flg must be divisible by 31
// valid_flgs = [0x01, 0x5E, 0x9C, 0xDA]
byte[] validFlgs = [0x01, 0x5E, 0x9C, 0xDA];
byte[] b1Candidates = new byte[validFlgs.Length];
for (int i = 0; i < validFlgs.Length; i++)
{
b1Candidates[i] = (byte)(ciphertext[1] ^ validFlgs[i]);
}
byte[] headerChunk = ciphertext[..16];
foreach (byte b1 in b1Candidates)
{
for (int b2 = 0; b2 < 256; b2++)
{
for (int b3 = 0; b3 < 256; b3++)
{
// Key bytes 0..7 — tail bytes 4..7 fixed as [0x01,0x00,0x10,0x01]
// (matches the game's SteamID encoding pattern)
byte[] key = [b0, b1, (byte)b2, (byte)b3, 0x01, 0x00, 0x10, 0x01];
// Decrypt first 2 bytes only to check zlib header
byte cmf = (byte)(headerChunk[0] ^ key[0]);
byte flg = (byte)(headerChunk[1] ^ key[1]);
if (cmf != 0x78)
{
continue;
}
if ((cmf * 256 + flg) % 31 != 0)
{
continue;
}
// Verify with first 128 bytes
try
{
byte[] verifyChunk = new byte[Math.Min(128, ciphertext.Length)];
for (int i = 0; i < verifyChunk.Length; i++)
{
verifyChunk[i] = (byte)(ciphertext[i] ^ key[i % 8]);
}
using var ms = new MemoryStream(verifyChunk);
using var zs = new ZLibStream(ms, CompressionMode.Decompress);
byte[] dummy = new byte[256];
zs.Read(dummy, 0, dummy.Length);
}
catch { continue; }
// Full decompression to confirm
try
{
byte[] fullDecrypted = new byte[ciphertext.Length];
for (int i = 0; i < ciphertext.Length; i++)
{
fullDecrypted[i] = (byte)(ciphertext[i] ^ key[i % 8]);
}
using var ms2 = new MemoryStream(fullDecrypted);
using var zs2 = new ZLibStream(ms2, CompressionMode.Decompress);
using var output = new MemoryStream();
zs2.CopyTo(output);
// success — reconstruct ulong SteamID64 from key bytes
ulong steamId = BitConverter.ToUInt64(key, 0);
return steamId;
}
catch { continue; }
}
}
}
return null;
}
/// <summary>
/// Resign index.save: XOR out the old key and XOR in the new key in one pass.
/// Creates a Backup/ copy before modifying.
/// </summary>
public static void ResignIndexFile(string filePath, ulong fromSid, ulong toSid,
List<string> log)
{
log.Add($" Resigning index.save: {filePath}");
byte[] originalCiphertext = File.ReadAllBytes(filePath);
byte[] fromKey = GetSteamIdKeyBytes(fromSid);
byte[] toKey = GetSteamIdKeyBytes(toSid);
byte[] newCiphertext = new byte[originalCiphertext.Length];
for (int i = 0; i < originalCiphertext.Length; i++)
{
newCiphertext[i] = (byte)(originalCiphertext[i] ^ fromKey[i % 8] ^ toKey[i % 8]);
}
EnsureBackup(filePath, log);
File.WriteAllBytes(filePath, newCiphertext);
log.Add(" [SUCCESS] index.save resigned successfully.");
}
/// <summary>
/// Resign data.save: decrypt (XOR + zlib decompress), recompress (zlib level 4), re-encrypt.
/// Returns false if decompression fails.
/// </summary>
public static bool ResignDataFile(string filePath, ulong fromSid, ulong toSid,
List<string> log)
{
log.Add($" Resigning data.save: {filePath}");
byte[] originalCiphertext = File.ReadAllBytes(filePath);
byte[] fromKey = GetSteamIdKeyBytes(fromSid);
byte[] toKey = GetSteamIdKeyBytes(toSid);
// Step 1: XOR-decrypt with source key
byte[] decryptedXor = new byte[originalCiphertext.Length];
for (int i = 0; i < originalCiphertext.Length; i++)
{
decryptedXor[i] = (byte)(originalCiphertext[i] ^ fromKey[i % 8]);
}
// Step 2: Zlib decompress
byte[] decompressedPayload;
try
{
using var ms = new MemoryStream(decryptedXor);
using var zs = new ZLibStream(ms, CompressionMode.Decompress);
using var output = new MemoryStream();
zs.CopyTo(output);
decompressedPayload = output.ToArray();
log.Add($" [OK] Decompressed payload: {decompressedPayload.Length:N0} bytes.");
}
catch (Exception ex)
{
log.Add($" [ERROR] Decompression failed — source SteamID may be wrong. {ex.Message}");
return false;
}
// Step 3: Recompress at level 4
byte[] compressedPayload;
using (var msOut = new MemoryStream())
{
using var zOut = new ZLibStream(msOut, CompressionLevel.Optimal);
zOut.Write(decompressedPayload, 0, decompressedPayload.Length);
zOut.Flush();
compressedPayload = msOut.ToArray();
}
// Step 4: XOR-encrypt with target key
byte[] newCiphertext = new byte[compressedPayload.Length];
for (int i = 0; i < compressedPayload.Length; i++)
{
newCiphertext[i] = (byte)(compressedPayload[i] ^ toKey[i % 8]);
}
EnsureBackup(filePath, log);
File.WriteAllBytes(filePath, newCiphertext);
log.Add($" [SUCCESS] data.save resigned. ({originalCiphertext.Length:N0} → {newCiphertext.Length:N0} bytes)");
return true;
}
/// <summary>
/// Decrypt index.save and write a .decrypted file alongside it.
/// </summary>
public static void DecryptIndexFile(string filePath, ulong steamSid, List<string> log)
{
log.Add($" Processing index.save: {filePath}");
byte[] data = File.ReadAllBytes(filePath);
if (data.Length < 8) { log.Add(" [ERROR] File too short."); return; }
byte[] decrypted = XorWithSteamId(data, steamSid);
// Check for header structure marker
int idx = IndexOf(decrypted, "SSaveGameHeader"u8);
if (idx >= 0)
{
log.Add(" [OK] Header verified: found 'SSaveGameHeader'");
}
else
{
log.Add(" [WARN] 'SSaveGameHeader' not found — SteamID may be incorrect.");
}
string outPath = filePath + ".decrypted";
File.WriteAllBytes(outPath, decrypted);
log.Add($" [SUCCESS] → {Path.GetFileName(outPath)}");
}
/// <summary>
/// Decrypt data.save (XOR + zlib decompress) and write a .decrypted file.
/// </summary>
public static bool DecryptDataFile(string filePath, ulong steamSid, List<string> log)
{
log.Add($" Processing data.save: {filePath}");
byte[] ciphertext = File.ReadAllBytes(filePath);
byte[] decryptedXor = XorWithSteamId(ciphertext, steamSid);
try
{
byte[] decompressed;
using (var ms = new MemoryStream(decryptedXor))
using (var zs = new ZLibStream(ms, CompressionMode.Decompress))
using (var output = new MemoryStream())
{
zs.CopyTo(output);
decompressed = output.ToArray();
}
log.Add($" [OK] Decompressed: {decompressed.Length:N0} bytes.");
// Extract save class type name (bytes 8..8+head_len), head_len at offset 4
if (decompressed.Length >= 32)
{
uint rawLen = BitConverter.ToUInt32(decompressed, 4);
int headLen = (int)(rawLen & 0x3FFFFFFF);
if (headLen < 100 && headLen > 0 && 8 + headLen <= decompressed.Length)
{
string typeName = Encoding.Latin1.GetString(decompressed, 8, headLen);
log.Add($" Save class type: '{typeName}'");
}
}
string outPath = filePath + ".decrypted";
File.WriteAllBytes(outPath, decompressed);
log.Add($" [SUCCESS] → {Path.GetFileName(outPath)}");
return true;
}
catch (Exception ex)
{
log.Add($" [ERROR] Decompression failed — SteamID {steamSid} may be incorrect. {ex.Message}");
return false;
}
}
private static void EnsureBackup(string filePath, List<string> log)
{
string backupDir = Path.Combine(Path.GetDirectoryName(filePath)!, "Backup");
string backupPath = Path.Combine(backupDir, Path.GetFileName(filePath));
if (!Directory.Exists(backupDir))
{
Directory.CreateDirectory(backupDir);
}
if (!File.Exists(backupPath))
{
File.Copy(filePath, backupPath);
log.Add($" [OK] Backup → {backupPath}");
}
}
private static int IndexOf(ReadOnlySpan<byte> haystack, ReadOnlySpan<byte> needle)
{
for (int i = 0; i <= haystack.Length - needle.Length; i++)
{
if (haystack.Slice(i, needle.Length).SequenceEqual(needle))
{
return i;
}
}
return -1;
}
public record SaveContainer(string Directory, bool HasIndex, bool HasData);
/// <summary>
/// Walk a root directory recursively looking for folders that contain
/// index.save and/or data.save (ignoring Backup sub-dirs).
/// </summary>
public static List<SaveContainer> FindSaveContainers(string rootDir)
{
var results = new List<SaveContainer>();
ScanDir(rootDir, results);
return results;
}
private static void ScanDir(string dir, List<SaveContainer> results)
{
try
{
string dirName = Path.GetFileName(dir);
if (string.Equals(dirName, "Backup", StringComparison.OrdinalIgnoreCase))
{
return;
}
string[] files = Directory.GetFiles(dir);
bool hasIndex = Array.Exists(files, f => Path.GetFileName(f).Equals("index.save", StringComparison.OrdinalIgnoreCase));
bool hasData = Array.Exists(files, f => Path.GetFileName(f).Equals("data.save", StringComparison.OrdinalIgnoreCase));
if (hasIndex || hasData)
{
results.Add(new SaveContainer(dir, hasIndex, hasData));
}
foreach (string sub in Directory.GetDirectories(dir))
{
ScanDir(sub, results);
}
}
catch { /* access denied, skip */ }
}
}

BIN
007SaveTool/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

View File

@@ -0,0 +1,21 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.5.11801.241 oobstable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.FirstLight.SaveTransfer", "007SaveTool\EonaCat.FirstLight.SaveTransfer.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|AnyCPU = Debug|AnyCPU
Release|AnyCPU = Release|AnyCPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|AnyCPU.Build.0 = Debug|AnyCPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|AnyCPU.ActiveCfg = Release|AnyCPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|AnyCPU.Build.0 = Release|AnyCPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal