using EonaCat.FirstLight.SaveTransfer.VdfGenerator.Models; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; 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; } PanelInstructionsSteam.Visibility = Visibility.Collapsed; PanelInstructionsCracked.Visibility = Visibility.Collapsed; PanelResign.Visibility = Visibility.Collapsed; PanelDecrypt.Visibility = Visibility.Collapsed; PanelVdf.Visibility = Visibility.Collapsed; PanelLog.Visibility = Visibility.Collapsed; if (tab == TabInstructionsSteam) { PanelInstructionsSteam.Visibility = Visibility.Visible; } if (tab == TabInstructionsCracked) { PanelInstructionsCracked.Visibility = Visibility.Visible; } if (tab == TabResign) { PanelResign.Visibility = Visibility.Visible; } if (tab == TabDecrypt) { PanelDecrypt.Visibility = Visibility.Visible; } if (tab == TabVdf) { PanelVdf.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 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() { var dlg = new Microsoft.Win32.OpenFolderDialog { Title = "Select your save folder (e.g. …\\userdata\\\\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(); 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 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(); 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 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 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 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 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 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; }