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

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;
}