├── RemnantSaveGuardian ├── Assets │ ├── 1024.ico │ ├── 256.ico │ ├── applicationIcon-256.png │ └── applicationIcon-1024.png ├── Models │ ├── DataColor.cs │ └── AppConfig.cs ├── ViewModels │ ├── BackupsViewModel.cs │ ├── LogViewModel.cs │ ├── SettingsViewModel.cs │ ├── WorldAnalyzerViewModel.cs │ └── MainWindowViewModel.cs ├── EventTransfer.cs ├── AssemblyInfo.cs ├── Helpers │ ├── CalculateConverter.cs │ ├── EnumToBooleanConverter.cs │ └── WindowDwmHelper.cs ├── Views │ ├── Pages │ │ ├── LogPage.xaml │ │ ├── LogPage.xaml.cs │ │ ├── BackupsPage.xaml │ │ ├── SettingsPage.xaml │ │ └── WorldAnalyzerPage.xaml │ ├── UserControls │ │ ├── TextBlockPlus.xaml │ │ └── TextBlockPlus.xaml.cs │ └── Windows │ │ └── MainWindow.xaml ├── Services │ ├── PageService.cs │ └── ApplicationHostService.cs ├── WindowsSave.cs ├── SaveWatcher.cs ├── Properties │ ├── Resources.Designer.cs │ ├── Resources.resx │ └── Settings.settings ├── Logger.cs ├── app.manifest ├── RemnantSaveGuardian.csproj ├── LocalizationProvider.cs ├── UpdateCheck.cs ├── SaveBackup.cs ├── App.xaml.cs ├── App.config ├── RemnantItem.cs ├── App.xaml ├── RemnantSave.cs ├── RemnantCharacter.cs ├── GameInfo.cs └── locales │ └── Strings.ko.resx ├── Directory.Build.props ├── .github └── workflows │ └── dotnet-desktop.yml ├── RemnantSaveGuardian.sln ├── README.md └── .gitignore /RemnantSaveGuardian/Assets/1024.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Razzmatazzz/RemnantSaveGuardian/HEAD/RemnantSaveGuardian/Assets/1024.ico -------------------------------------------------------------------------------- /RemnantSaveGuardian/Assets/256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Razzmatazzz/RemnantSaveGuardian/HEAD/RemnantSaveGuardian/Assets/256.ico -------------------------------------------------------------------------------- /RemnantSaveGuardian/Assets/applicationIcon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Razzmatazzz/RemnantSaveGuardian/HEAD/RemnantSaveGuardian/Assets/applicationIcon-256.png -------------------------------------------------------------------------------- /RemnantSaveGuardian/Assets/applicationIcon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Razzmatazzz/RemnantSaveGuardian/HEAD/RemnantSaveGuardian/Assets/applicationIcon-1024.png -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MSBuildProjectDirectory)=$(MSBuildProjectName) 4 | 5 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Models/DataColor.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Media; 2 | 3 | namespace RemnantSaveGuardian.Models 4 | { 5 | public struct DataColor 6 | { 7 | public Brush Color { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Models/AppConfig.cs: -------------------------------------------------------------------------------- 1 | namespace RemnantSaveGuardian.Models 2 | { 3 | public class AppConfig 4 | { 5 | public string ConfigurationsFolder { get; set; } 6 | 7 | public string AppPropertiesFileName { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/ViewModels/BackupsViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | using Wpf.Ui.Common.Interfaces; 4 | 5 | namespace RemnantSaveGuardian.ViewModels 6 | { 7 | public partial class BackupsViewModel : ObservableObject, INavigationAware 8 | { 9 | public void OnNavigatedTo() 10 | { 11 | } 12 | 13 | public void OnNavigatedFrom() 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/EventTransfer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RemnantSaveGuardian 4 | { 5 | internal class EventTransfer 6 | { 7 | internal static event EventHandler? Event; 8 | internal class MessageArgs : EventArgs 9 | { 10 | internal MessageArgs(object message) 11 | { 12 | _message = message; 13 | } 14 | internal object _message { get; set; } 15 | 16 | } 17 | internal static void Transfer(object s) 18 | { 19 | Event?.Invoke(null, new MessageArgs(s)); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/ViewModels/LogViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | using Wpf.Ui.Common.Interfaces; 4 | 5 | namespace RemnantSaveGuardian.ViewModels 6 | { 7 | public partial class LogViewModel : ObservableObject, INavigationAware 8 | { 9 | private bool _isInitialized = false; 10 | 11 | public void OnNavigatedTo() 12 | { 13 | if (!_isInitialized) 14 | InitializeViewModel(); 15 | } 16 | 17 | public void OnNavigatedFrom() 18 | { 19 | } 20 | 21 | private void InitializeViewModel() 22 | { 23 | _isInitialized = true; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/ViewModels/SettingsViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | using Wpf.Ui.Common.Interfaces; 4 | 5 | namespace RemnantSaveGuardian.ViewModels 6 | { 7 | public partial class SettingsViewModel : ObservableObject, INavigationAware 8 | { 9 | private bool _isInitialized = false; 10 | 11 | public void OnNavigatedTo() 12 | { 13 | if (!_isInitialized) 14 | InitializeViewModel(); 15 | } 16 | 17 | public void OnNavigatedFrom() 18 | { 19 | } 20 | 21 | private void InitializeViewModel() 22 | { 23 | _isInitialized = true; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/ViewModels/WorldAnalyzerViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | using Wpf.Ui.Common.Interfaces; 4 | 5 | namespace RemnantSaveGuardian.ViewModels 6 | { 7 | public partial class WorldAnalyzerViewModel : ObservableObject, INavigationAware 8 | { 9 | private bool _isInitialized = false; 10 | 11 | public void OnNavigatedTo() 12 | { 13 | if (!_isInitialized) 14 | InitializeViewModel(); 15 | } 16 | 17 | public void OnNavigatedFrom() 18 | { 19 | } 20 | 21 | private void InitializeViewModel() 22 | { 23 | _isInitialized = true; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Helpers/CalculateConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Data; 3 | 4 | namespace RemnantSaveGuardian.Helpers 5 | { 6 | internal class CalculateConverter : IValueConverter 7 | { 8 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 9 | { 10 | var intX = Math.Round((double)value); 11 | var intY = Int32.Parse((string)parameter); 12 | return (intX + intY); 13 | } 14 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 15 | { 16 | throw new NotSupportedException(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Views/Pages/LogPage.xaml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Views/UserControls/TextBlockPlus.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Helpers/EnumToBooleanConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows.Data; 4 | 5 | namespace RemnantSaveGuardian.Helpers 6 | { 7 | internal class EnumToBooleanConverter : IValueConverter 8 | { 9 | public EnumToBooleanConverter() 10 | { 11 | } 12 | 13 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 14 | { 15 | if (parameter is not String enumString) 16 | throw new ArgumentException("ExceptionEnumToBooleanConverterParameterMustBeAnEnumName"); 17 | 18 | if (!Enum.IsDefined(typeof(Wpf.Ui.Appearance.ThemeType), value)) 19 | throw new ArgumentException("ExceptionEnumToBooleanConverterValueMustBeAnEnum"); 20 | 21 | var enumValue = Enum.Parse(typeof(Wpf.Ui.Appearance.ThemeType), enumString); 22 | 23 | return enumValue.Equals(value); 24 | } 25 | 26 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 27 | { 28 | if (parameter is not String enumString) 29 | throw new ArgumentException("ExceptionEnumToBooleanConverterParameterMustBeAnEnumName"); 30 | 31 | return Enum.Parse(typeof(Wpf.Ui.Appearance.ThemeType), enumString); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-desktop.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core Desktop 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: windows-latest 13 | 14 | env: 15 | Project: RemnantSaveGuardian/RemnantSaveGuardian.csproj 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Install .NET Core 24 | uses: actions/setup-dotnet@v4 25 | with: 26 | dotnet-version: 8.0.x 27 | 28 | - name: Setup MSBuild.exe 29 | uses: microsoft/setup-msbuild@v2 30 | 31 | # Just in case there will be some unit tests in future 32 | - name: Execute unit tests 33 | run: dotnet test 34 | 35 | - name: Restore the application 36 | run: msbuild $env:Project /t:Restore /p:Configuration=Release 37 | 38 | - uses: kzrnm/get-net-sdk-project-versions-action@v2 39 | id: get-version 40 | with: 41 | proj-path: ${{ env.Project }} 42 | 43 | - name: publish 44 | run: dotnet publish RemnantSaveGuardian/RemnantSaveGuardian.csproj -o "./publish" 45 | 46 | - name: Upload build artifacts 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: 'RemnantSaveGuardian_${{steps.get-version.outputs.assembly-version}}' 50 | path: ./publish 51 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Services/PageService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using Wpf.Ui.Mvvm.Contracts; 4 | 5 | namespace RemnantSaveGuardian.Services 6 | { 7 | /// 8 | /// Service that provides pages for navigation. 9 | /// 10 | public class PageService : IPageService 11 | { 12 | /// 13 | /// Service which provides the instances of pages. 14 | /// 15 | private readonly IServiceProvider _serviceProvider; 16 | 17 | /// 18 | /// Creates new instance and attaches the . 19 | /// 20 | public PageService(IServiceProvider serviceProvider) 21 | { 22 | _serviceProvider = serviceProvider; 23 | } 24 | 25 | /// 26 | public T? GetPage() where T : class 27 | { 28 | if (!typeof(FrameworkElement).IsAssignableFrom(typeof(T))) 29 | throw new InvalidOperationException("The page should be a WPF control."); 30 | 31 | return (T?)_serviceProvider.GetService(typeof(T)); 32 | } 33 | 34 | /// 35 | public FrameworkElement? GetPage(Type pageType) 36 | { 37 | if (!typeof(FrameworkElement).IsAssignableFrom(pageType)) 38 | throw new InvalidOperationException("The page should be a WPF control."); 39 | 40 | return _serviceProvider.GetService(pageType) as FrameworkElement; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/WindowsSave.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace RemnantSaveGuardian 6 | { 7 | class WindowsSave 8 | { 9 | public string Container { get; set; } 10 | public string Profile { get; set; } 11 | public List Worlds { get; set; } 12 | private bool isValid; 13 | public bool Valid { get { return isValid; } } 14 | 15 | public WindowsSave(string containerPath) 16 | { 17 | Worlds = new List(); 18 | Container = containerPath; 19 | var folderPath = new FileInfo(containerPath).Directory.FullName; 20 | var offset = 136; 21 | byte[] byteBuffer = File.ReadAllBytes(Container); 22 | var profileBytes = new byte[16]; 23 | Array.Copy(byteBuffer, offset, profileBytes, 0, 16); 24 | var profileGuid = new Guid(profileBytes); 25 | Profile = profileGuid.ToString().ToUpper().Replace("-", ""); 26 | isValid = File.Exists($@"{folderPath}\{Profile}"); 27 | offset += 160; 28 | while (offset + 16 < byteBuffer.Length) 29 | { 30 | var worldBytes = new byte[16]; 31 | Array.Copy(byteBuffer, offset, worldBytes, 0, 16); 32 | var worldGuid = new Guid(worldBytes); 33 | Worlds.Add(folderPath + "\\" + worldGuid.ToString().ToUpper().Replace("-", "")); 34 | offset += 160; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /RemnantSaveGuardian.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.6.33815.320 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemnantSaveGuardian", "RemnantSaveGuardian\RemnantSaveGuardian.csproj", "{61D7EC72-AB05-43AE-943E-066C4F1F1520}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D6BEAB10-FA9E-421D-A7E6-ED1F5203B96F}" 9 | ProjectSection(SolutionItems) = preProject 10 | Directory.Build.props = Directory.Build.props 11 | .github\workflows\dotnet-desktop.yml = .github\workflows\dotnet-desktop.yml 12 | LICENSE = LICENSE 13 | README.md = README.md 14 | EndProjectSection 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {61D7EC72-AB05-43AE-943E-066C4F1F1520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {61D7EC72-AB05-43AE-943E-066C4F1F1520}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {61D7EC72-AB05-43AE-943E-066C4F1F1520}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {61D7EC72-AB05-43AE-943E-066C4F1F1520}.Release|Any CPU.Build.0 = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ExtensibilityGlobals) = postSolution 31 | SolutionGuid = {E8790D64-B056-44FA-898D-43526F95A868} 32 | EndGlobalSection 33 | EndGlobal 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remnant Save Guardian 2 | Back up your Remnant 2 saves and view your world rolls. 3 | 4 | **This project is a work in progress. Lots of features are broken or partially implemented.** 5 | 6 | ## Installation 7 | 1. Download and install [Microsoft .NET 8.0](https://dotnet.microsoft.com/en-us/download) or greater 8 | 2. Download the [latest release](https://github.com/Razzmatazzz/RemnantSaveGuardian/releases/latest/) 9 | 3. Unzip the latest release to a folder of your choosing (probably not the same folder where you have the game installed) 10 | 4. Run RemnantSaveGuardian.exe 11 | 12 | ## Screenshots 13 | ![image](https://github.com/Razzmatazzz/RemnantSaveGuardian/assets/35779878/cc428b0b-7573-4128-a2ae-02ef25ebda36) 14 | ![image](https://github.com/Razzmatazzz/RemnantSaveGuardian/assets/35779878/48fc0eae-fc87-47be-bea3-89af08f102a6) 15 | ![image](https://github.com/Razzmatazzz/RemnantSaveGuardian/assets/35779878/45d46b50-f3d2-4341-847e-ae3dd4f6df2c) 16 | 17 | ## Known Issues 18 | - [Some items are missing from the world analyzer](https://github.com/Razzmatazzz/RemnantSaveGuardian/issues/43) 19 | - [Many events and items do not have well-formatted names](https://github.com/Razzmatazzz/RemnantSaveGuardian/issues/45) 20 | - [Some events not displaying or are erroneously displaying](https://github.com/Razzmatazzz/RemnantSaveGuardian/issues/44) 21 | - [If you are using Norton Antivirus, it may cause weirdness with your game saves and RemnantSaveGuardian](https://github.com/Razzmatazzz/RemnantSaveGuardian/issues/70) 22 | 23 | Thanks for [crackedmind](https://github.com/crackedmind) for the inflation code to convert saves into partial plaintext. 24 | 25 | Thanks to [AuriCrystal](https://github.com/Auricrystal) for event/item list. 26 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Services/ApplicationHostService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using System; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using Wpf.Ui.Mvvm.Contracts; 8 | 9 | namespace RemnantSaveGuardian.Services 10 | { 11 | /// 12 | /// Managed host of the application. 13 | /// 14 | public class ApplicationHostService : IHostedService 15 | { 16 | private readonly IServiceProvider _serviceProvider; 17 | private INavigationWindow _navigationWindow; 18 | 19 | public ApplicationHostService(IServiceProvider serviceProvider) 20 | { 21 | _serviceProvider = serviceProvider; 22 | } 23 | 24 | /// 25 | /// Triggered when the application host is ready to start the service. 26 | /// 27 | /// Indicates that the start process has been aborted. 28 | public async Task StartAsync(CancellationToken cancellationToken) 29 | { 30 | await HandleActivationAsync(); 31 | } 32 | 33 | /// 34 | /// Triggered when the application host is performing a graceful shutdown. 35 | /// 36 | /// Indicates that the shutdown process should no longer be graceful. 37 | public async Task StopAsync(CancellationToken cancellationToken) 38 | { 39 | await Task.CompletedTask; 40 | } 41 | 42 | /// 43 | /// Creates main window during activation. 44 | /// 45 | private async Task HandleActivationAsync() 46 | { 47 | await Task.CompletedTask; 48 | 49 | if (!Application.Current.Windows.OfType().Any()) 50 | { 51 | _navigationWindow = (_serviceProvider.GetService(typeof(INavigationWindow)) as INavigationWindow)!; 52 | _navigationWindow!.ShowWindow(); 53 | 54 | _navigationWindow.Navigate(typeof(Views.Pages.BackupsPage)); 55 | } 56 | 57 | await Task.CompletedTask; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Views/Pages/LogPage.xaml.cs: -------------------------------------------------------------------------------- 1 | //using System.Drawing; 2 | using System; 3 | 4 | using Wpf.Ui.Common.Interfaces; 5 | using Wpf.Ui.Controls; 6 | 7 | namespace RemnantSaveGuardian.Views.Pages 8 | { 9 | /// 10 | /// Interaction logic for LogView.xaml 11 | /// 12 | public partial class LogPage : INavigableView 13 | { 14 | public ViewModels.LogViewModel ViewModel 15 | { 16 | get; 17 | } 18 | 19 | public LogPage(ViewModels.LogViewModel viewModel) 20 | { 21 | ViewModel = viewModel; 22 | 23 | InitializeComponent(); 24 | Logger.MessageLogged += Logger_MessageLogged; 25 | foreach (var logMessage in Logger.Messages) 26 | { 27 | addMessage(logMessage.Message, logMessage.LogType); 28 | } 29 | } 30 | 31 | private void Logger_MessageLogged(object? sender, MessageLoggedEventArgs e) 32 | { 33 | addMessage(e.Message, e.LogType); 34 | } 35 | 36 | private void addMessage(string message, LogType logType) 37 | { 38 | Dispatcher.Invoke(delegate { 39 | var infoBar = new InfoBar() 40 | { 41 | Message = message, 42 | IsOpen = true, 43 | Title = DateTime.Now.ToString(), 44 | }; 45 | if (logType == LogType.Error) 46 | { 47 | infoBar.Severity = InfoBarSeverity.Error; 48 | } 49 | if (logType == LogType.Warning) 50 | { 51 | infoBar.Severity = InfoBarSeverity.Warning; 52 | } 53 | if (logType == LogType.Success) 54 | { 55 | infoBar.Severity = InfoBarSeverity.Success; 56 | } 57 | infoBar.ContextMenu = new System.Windows.Controls.ContextMenu(); 58 | var menuCopyMessage = new Wpf.Ui.Controls.MenuItem(); 59 | menuCopyMessage.Header = Loc.T("Copy"); 60 | menuCopyMessage.SymbolIcon = Wpf.Ui.Common.SymbolRegular.Copy24; 61 | menuCopyMessage.Click += (s, e) => 62 | { 63 | System.Windows.Clipboard.SetDataObject(message); 64 | }; 65 | infoBar.ContextMenu.Items.Add(menuCopyMessage); 66 | stackLogs.Children.Insert(0, infoBar); 67 | }); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/SaveWatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace RemnantSaveGuardian 5 | { 6 | internal static class SaveWatcher 7 | { 8 | public static event EventHandler SaveUpdated; 9 | private static FileSystemWatcher watcher = new () 10 | { 11 | //NotifyFilter = NotifyFilters.LastWrite, 12 | Filter = "profile.sav", 13 | }; 14 | private static System.Timers.Timer saveTimer = new() 15 | { 16 | Interval = 2000, 17 | AutoReset = false, 18 | }; 19 | 20 | static SaveWatcher() 21 | { 22 | watcher.Changed += OnSaveFileChanged; 23 | watcher.Created += OnSaveFileChanged; 24 | watcher.Deleted += OnSaveFileChanged; 25 | 26 | saveTimer.Elapsed += SaveTimer_Elapsed; 27 | 28 | Properties.Settings.Default.PropertyChanged += Default_PropertyChanged; 29 | } 30 | 31 | private static void Default_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) 32 | { 33 | if (e.PropertyName != "SaveFolder") 34 | { 35 | return; 36 | } 37 | Watch(Properties.Settings.Default.SaveFolder); 38 | } 39 | 40 | private static void SaveTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) 41 | { 42 | SaveUpdated?.Invoke(sender, e); 43 | } 44 | 45 | private static void OnSaveFileChanged(object sender, FileSystemEventArgs e) 46 | { 47 | try 48 | { 49 | //When the save files are modified, they are modified 50 | //multiple times in relatively rapid succession. 51 | //This timer is refreshed each time the save is modified, 52 | //and a backup only occurs after the timer expires. 53 | saveTimer.Stop(); 54 | saveTimer.Start(); 55 | } 56 | catch (Exception ex) 57 | { 58 | Logger.Error($"{ex.GetType()} {Loc.T("setting save file timer")}: {ex.Message} ({ex.StackTrace})"); 59 | } 60 | } 61 | 62 | public static void Watch(string path) 63 | { 64 | if (Directory.Exists(path)) 65 | { 66 | if (watcher.Path != path) 67 | { 68 | watcher.Path = path; 69 | } 70 | watcher.EnableRaisingEvents = true; 71 | } 72 | else 73 | { 74 | watcher.EnableRaisingEvents = false; 75 | } 76 | } 77 | 78 | public static void Pause() 79 | { 80 | watcher.EnableRaisingEvents = false; 81 | } 82 | public static void Resume() 83 | { 84 | watcher.EnableRaisingEvents = true; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace RemnantSaveGuardian.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("RemnantSaveGuardian.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/ViewModels/MainWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | using System; 4 | using System.Collections.ObjectModel; 5 | 6 | using Wpf.Ui.Common; 7 | using Wpf.Ui.Controls; 8 | using Wpf.Ui.Controls.Interfaces; 9 | using Wpf.Ui.Mvvm.Contracts; 10 | 11 | namespace RemnantSaveGuardian.ViewModels 12 | { 13 | public partial class MainWindowViewModel : ObservableObject 14 | { 15 | private bool _isInitialized = false; 16 | 17 | [ObservableProperty] 18 | private string _applicationTitle = String.Empty; 19 | 20 | [ObservableProperty] 21 | private ObservableCollection _navigationItems = new(); 22 | 23 | [ObservableProperty] 24 | private ObservableCollection _navigationFooter = new(); 25 | 26 | [ObservableProperty] 27 | private ObservableCollection _trayMenuItems = new(); 28 | 29 | public MainWindowViewModel(INavigationService navigationService) 30 | { 31 | if (!_isInitialized) 32 | InitializeViewModel(); 33 | } 34 | 35 | private void InitializeViewModel() 36 | { 37 | ApplicationTitle = "Remnant Save Guardian"; 38 | 39 | NavigationItems = new ObservableCollection 40 | { 41 | new NavigationItem() 42 | { 43 | Content = Loc.T("Save Backups"), 44 | ToolTip = Loc.T("Save Backups"), 45 | PageTag = "backups", 46 | Icon = SymbolRegular.Database24, 47 | PageType = typeof(Views.Pages.BackupsPage) 48 | }, 49 | new NavigationItem() 50 | { 51 | Content = Loc.T("World Analyzer"), 52 | ToolTip = Loc.T("World Analyzer"), 53 | PageTag = "world-analyzer", 54 | Icon = SymbolRegular.Globe24, 55 | PageType = typeof(Views.Pages.WorldAnalyzerPage) 56 | } 57 | }; 58 | 59 | NavigationFooter = new ObservableCollection 60 | { 61 | new NavigationItem() 62 | { 63 | Content = Loc.T("Log"), 64 | ToolTip = Loc.T("Log"), 65 | PageTag = "log", 66 | Icon = SymbolRegular.Notebook24, 67 | PageType = typeof(Views.Pages.LogPage) 68 | }, 69 | new NavigationItem() 70 | { 71 | Content = Loc.T("Settings"), 72 | ToolTip = Loc.T("Settings"), 73 | PageTag = "settings", 74 | Icon = SymbolRegular.Settings24, 75 | PageType = typeof(Views.Pages.SettingsPage) 76 | } 77 | }; 78 | 79 | TrayMenuItems = new ObservableCollection 80 | { 81 | new MenuItem 82 | { 83 | Header = Loc.T("Home"), 84 | Tag = "tray_home" 85 | } 86 | }; 87 | 88 | _isInitialized = true; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Logger.cs: -------------------------------------------------------------------------------- 1 | using RemnantSaveGuardian.Views.Windows; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | 6 | namespace RemnantSaveGuardian 7 | { 8 | internal static class Logger 9 | { 10 | public static event EventHandler MessageLogged; 11 | private static List messages = new (); 12 | public static List Messages { get { return messages; } } 13 | static Logger() 14 | { 15 | if (Properties.Settings.Default.CreateLogFile) 16 | { 17 | CreateLog(); 18 | } 19 | Properties.Settings.Default.PropertyChanged += Default_PropertyChanged; 20 | } 21 | 22 | private static void Default_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) 23 | { 24 | if (e.PropertyName != "CreateLogFile") 25 | { 26 | return; 27 | } 28 | if (!Properties.Settings.Default.CreateLogFile) 29 | { 30 | return; 31 | } 32 | CreateLog(); 33 | } 34 | 35 | private static void CreateLog() 36 | { 37 | File.WriteAllText("log.txt", DateTime.Now.ToString() + ": Version " + typeof(MainWindow).Assembly.GetName().Version + "\r\n"); 38 | } 39 | 40 | public static void Log(object message, LogType logType) 41 | { 42 | if (message == null) 43 | { 44 | message = "null"; 45 | } 46 | MessageLogged?.Invoke(null, new (message.ToString(), logType)); 47 | messages.Add(new(message.ToString(), logType)); 48 | if (Properties.Settings.Default.CreateLogFile) 49 | { 50 | StreamWriter writer = File.AppendText("log.txt"); 51 | writer.WriteLine(DateTime.Now.ToString() + ": " + message); 52 | writer.Close(); 53 | } 54 | //Debug.WriteLine(message); 55 | } 56 | public static void Log(object message) 57 | { 58 | Log(message, LogType.Normal); 59 | } 60 | public static void Success(object message) 61 | { 62 | Log(message, LogType.Success); 63 | } 64 | public static void Error(object message) 65 | { 66 | Log(message, LogType.Error); 67 | } 68 | public static void Warn(object message) 69 | { 70 | Log(message, LogType.Warning); 71 | } 72 | } 73 | 74 | public enum LogType 75 | { 76 | Normal, 77 | Success, 78 | Error, 79 | Warning, 80 | } 81 | 82 | public class MessageLoggedEventArgs : EventArgs 83 | { 84 | public string Message { get; set; } 85 | public LogType LogType { get; set;} 86 | public MessageLoggedEventArgs(string message, LogType logType) 87 | { 88 | Message = message; 89 | LogType = logType; 90 | } 91 | public MessageLoggedEventArgs(string message) 92 | { 93 | Message = message; 94 | LogType = LogType.Normal; 95 | } 96 | } 97 | public class LogMessage 98 | { 99 | public string Message { get; set; } 100 | public LogType LogType { get; set; } 101 | public LogMessage(string message, LogType logType) 102 | { 103 | Message = message; 104 | LogType = logType; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 54 | 55 | 56 | 57 | PerMonitor 58 | true/PM 59 | true 60 | 61 | 62 | 63 | 64 | 65 | 66 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/RemnantSaveGuardian.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net8.0-windows 6 | enable 7 | true 8 | en;ru;de;es;fr;it;ja;ko;pt-BR;zh-Hans;zh-Hant 9 | app.manifest 10 | Assets\256.ico 11 | 1.4.2.0 12 | OnOutputUpdated 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Never 39 | 40 | 41 | 42 | 43 | 44 | GameStrings.resx 45 | True 46 | True 47 | 48 | 49 | Strings.resx 50 | True 51 | True 52 | 53 | 54 | True 55 | True 56 | Resources.resx 57 | 58 | 59 | True 60 | True 61 | Settings.settings 62 | 63 | 64 | 65 | 66 | 67 | GameStrings.Designer.cs 68 | PublicResXFileCodeGenerator 69 | 70 | 71 | PublicResXFileCodeGenerator 72 | 73 | 74 | Strings.Designer.cs 75 | PublicResXFileCodeGenerator 76 | 77 | 78 | PublicResXFileCodeGenerator 79 | 80 | 81 | PublicResXFileCodeGenerator 82 | 83 | 84 | ResXFileCodeGenerator 85 | Resources.Designer.cs 86 | 87 | 88 | 89 | 90 | 91 | SettingsSingleFileGenerator 92 | Settings.Designer.cs 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/LocalizationProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Globalization; 3 | using System.Reflection; 4 | using System.Text.RegularExpressions; 5 | 6 | using WPFLocalizeExtension.Extensions; 7 | 8 | namespace RemnantSaveGuardian 9 | { 10 | internal class Loc 11 | { 12 | public static T GetLocalizedValue(string key, LocalizationOptions options) 13 | { 14 | var ns = "Strings"; 15 | if (options.Has("namespace") && options["namespace"] != "") 16 | { 17 | ns = options["namespace"]; 18 | } 19 | var currentCulture = WPFLocalizeExtension.Engine.LocalizeDictionary.Instance.Culture; 20 | if (options.Has("locale") && options["locale"] != currentCulture.ToString()) 21 | { 22 | WPFLocalizeExtension.Engine.LocalizeDictionary.Instance.SetCurrentThreadCulture = false; 23 | WPFLocalizeExtension.Engine.LocalizeDictionary.Instance.Culture = new CultureInfo(options["locale"]); 24 | var translation = LocExtension.GetLocalizedValue(Assembly.GetCallingAssembly().GetName().Name + $":{ns}:" + key); 25 | WPFLocalizeExtension.Engine.LocalizeDictionary.Instance.Culture = currentCulture; 26 | return translation; 27 | } 28 | //Debug.WriteLine($"{ns}:{key}"); 29 | return LocExtension.GetLocalizedValue(Assembly.GetCallingAssembly().GetName().Name + $":{ns}:" + key); 30 | } 31 | public static string T(string key, LocalizationOptions options) 32 | { 33 | var val = GetLocalizedValue(key, options); 34 | if (val == null || val == "") 35 | { 36 | return key; 37 | /*if (resourceFile != "GameStrings") 38 | { 39 | return key; 40 | } 41 | if (key == null) 42 | { 43 | return key; 44 | } 45 | return Regex.Replace(key.Replace("_", " "), "([A-Z0-9]+)", " $1").Trim();*/ 46 | } 47 | var matches = new Regex(@"{(?:(?\w+?):)?(?\w+?)}").Matches(val); 48 | foreach (Match match in matches) 49 | { 50 | var valueToSub = match.Groups["sub"].Value; 51 | if (options.Has(valueToSub) && options[valueToSub] != "") 52 | { 53 | valueToSub = options[valueToSub]; 54 | } else 55 | { 56 | var optionsToUse = options; 57 | if (match.Groups.ContainsKey("namespace") && match.Groups["namespace"].Value != "") 58 | { 59 | optionsToUse = new LocalizationOptions(options); 60 | optionsToUse["namespace"] = match.Groups["namespace"].Value; 61 | } 62 | valueToSub = T(valueToSub, optionsToUse); 63 | } 64 | val = val.Replace(match.Value, valueToSub); 65 | } 66 | return val; 67 | } 68 | public static string T(string key) 69 | { 70 | return T(key, new LocalizationOptions { { "namespace", "Strings" } }); 71 | } 72 | public static string GameT(string key, LocalizationOptions options) 73 | { 74 | options["namespace"] = "GameStrings"; 75 | return T(key, options); 76 | } 77 | public static string GameT(string key) 78 | { 79 | return T(key, new LocalizationOptions { { "namespace", "GameStrings" } }); 80 | } 81 | public static bool Has(string key, LocalizationOptions options) 82 | { 83 | var val = GetLocalizedValue(key, options); 84 | if (val == null || val == "") 85 | { 86 | return false; 87 | } 88 | return true; 89 | } 90 | public static bool GameTHas(string key) 91 | { 92 | return Has(key, new LocalizationOptions { { "namespace", "GameStrings" } }); 93 | } 94 | } 95 | 96 | public class LocalizationOptions : Dictionary 97 | { 98 | public LocalizationOptions(LocalizationOptions source) 99 | { 100 | foreach (var kvp in source) 101 | { 102 | this.Add(kvp.Key, kvp.Value); 103 | } 104 | } 105 | public LocalizationOptions() { } 106 | 107 | public bool Has(string key) 108 | { 109 | if (!ContainsKey(key)) return false; 110 | return this[key] != null; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/UpdateCheck.cs: -------------------------------------------------------------------------------- 1 | using AutoUpdaterDotNET; 2 | using System; 3 | using System.Windows.Documents; 4 | using System.Diagnostics; 5 | using System.Net.Http; 6 | using System.Text.Json.Nodes; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | 10 | namespace RemnantSaveGuardian 11 | { 12 | internal class UpdateCheck 13 | { 14 | private static string repo = "Razzmatazzz/RemnantSaveGuardian"; 15 | private static readonly HttpClient client = new(); 16 | private static DateTime lastUpdateCheck = DateTime.MinValue; 17 | 18 | public static event EventHandler? NewVersion; 19 | 20 | public static async void CheckForNewVersion() 21 | { 22 | try 23 | { 24 | if (lastUpdateCheck.AddMinutes(5) > DateTime.Now) 25 | { 26 | Logger.Warn(Loc.T("You must wait 5 minutes between update checks")); 27 | return; 28 | } 29 | lastUpdateCheck = DateTime.Now; 30 | GameInfo.CheckForNewGameInfo(); 31 | var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.github.com/repos/{repo}/releases/latest"); 32 | request.Headers.Add("user-agent", "remnant-save-guardian"); 33 | var response = await client.SendAsync(request); 34 | response.EnsureSuccessStatusCode(); 35 | JsonNode latestRelease = JsonNode.Parse(await response.Content.ReadAsStringAsync()); 36 | 37 | Version remoteVersion = new Version(latestRelease["tag_name"].ToString()); 38 | Version localVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; 39 | localVersion = new Version(localVersion.Major, localVersion.Minor, localVersion.Build); 40 | if (localVersion.CompareTo(remoteVersion) == -1) 41 | { 42 | NewVersion?.Invoke(null, new() { Version = remoteVersion, Uri = new(latestRelease["html_url"].ToString()) }); 43 | var messageBox = new Wpf.Ui.Controls.MessageBox(); 44 | messageBox.Title = Loc.T("Update available"); 45 | Hyperlink hyperLink = new() 46 | { 47 | NavigateUri = new Uri($"https://github.com/Razzmatazzz/RemnantSaveGuardian/releases/tag/{remoteVersion}") 48 | }; 49 | hyperLink.Inlines.Add(Loc.T("Changelog")); 50 | hyperLink.RequestNavigate += (o, e) => Process.Start("explorer.exe", e.Uri.ToString()); 51 | var txtBlock = new TextBlock() 52 | { 53 | Text = Loc.T("The latest version of Remnant Save Guardian is {CurrentVersion}. You are using version {LocalVersion}. Do you want to upgrade the application now?", 54 | new LocalizationOptions() 55 | { 56 | { 57 | "CurrentVersion", remoteVersion.ToString() 58 | }, 59 | { 60 | "LocalVersion", localVersion.ToString() 61 | } 62 | } 63 | ) + "\n", 64 | TextWrapping = System.Windows.TextWrapping.WrapWithOverflow, 65 | }; 66 | txtBlock.Inlines.Add(hyperLink); 67 | messageBox.Content = txtBlock; 68 | messageBox.ButtonLeftName = Loc.T("Update"); 69 | messageBox.ButtonLeftClick += (send, updatedEvent) => { 70 | UpdateInfoEventArgs args = new() 71 | { 72 | InstalledVersion = localVersion, 73 | CurrentVersion = remoteVersion.ToString(), 74 | DownloadURL = latestRelease["assets"].AsArray()[0]["browser_download_url"].ToString() 75 | }; 76 | messageBox.Close(); 77 | AutoUpdater.DownloadUpdate(args); 78 | Application.Current.Shutdown(); 79 | }; 80 | messageBox.ButtonRightName = Loc.T("Cancel"); 81 | messageBox.ButtonRightClick += (send, updatedEvent) => { 82 | messageBox.Close(); 83 | }; 84 | messageBox.ShowDialog(); 85 | } 86 | } 87 | catch (Exception ex) 88 | { 89 | Logger.Error($"{Loc.T("Error checking for new version")}: {ex.Message}"); 90 | } 91 | } 92 | } 93 | 94 | public class NewVersionEventArgs : EventArgs 95 | { 96 | public Version Version { get; set; } 97 | public Uri Uri { get; set; } 98 | } 99 | public class UpdateCheckErrorEventArgs : EventArgs 100 | { 101 | public Exception Exception { get; set; } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Views/Pages/BackupsPage.xaml: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/SaveBackup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.IO; 4 | 5 | namespace RemnantSaveGuardian 6 | { 7 | public class SaveBackup : IEditableObject 8 | { 9 | struct SaveData 10 | { 11 | internal string name; 12 | internal DateTime date; 13 | internal bool keep; 14 | internal bool active; 15 | } 16 | 17 | public event EventHandler Updated; 18 | private SaveData saveData; 19 | private SaveData backupData; 20 | private bool inTxn = false; 21 | //private int[] progression; 22 | //private List charData; 23 | private RemnantSave save; 24 | public string Name 25 | { 26 | get 27 | { 28 | return this.saveData.name; 29 | } 30 | set 31 | { 32 | if (value.Equals("")) 33 | { 34 | this.saveData.name = this.saveData.date.Ticks.ToString(); 35 | } 36 | else 37 | { 38 | this.saveData.name = value; 39 | } 40 | //OnUpdated(new UpdatedEventArgs("Name")); 41 | } 42 | } 43 | public DateTime SaveDate 44 | { 45 | get 46 | { 47 | return this.saveData.date; 48 | } 49 | set 50 | { 51 | this.saveData.date = value; 52 | //OnUpdated(new UpdatedEventArgs("SaveDate")); 53 | } 54 | } 55 | public string Progression 56 | { 57 | get 58 | { 59 | return string.Join(", ", this.save.Characters); 60 | } 61 | } 62 | public bool Keep 63 | { 64 | get 65 | { 66 | return this.saveData.keep; 67 | } 68 | set 69 | { 70 | if (this.saveData.keep != value) 71 | { 72 | this.saveData.keep = value; 73 | OnUpdated(new UpdatedEventArgs("Keep")); 74 | } 75 | } 76 | } 77 | public bool Active 78 | { 79 | get 80 | { 81 | return this.saveData.active; 82 | } 83 | set 84 | { 85 | this.saveData.active = value; 86 | //OnUpdated(new UpdatedEventArgs("Active")); 87 | } 88 | } 89 | 90 | public RemnantSave Save 91 | { 92 | get 93 | { 94 | return save; 95 | } 96 | } 97 | 98 | //public SaveBackup(DateTime saveDate) 99 | public SaveBackup(string savePath) 100 | { 101 | this.save = new RemnantSave(savePath); 102 | this.saveData = new SaveData(); 103 | this.saveData.name = this.SaveDateTime.Ticks.ToString(); 104 | this.saveData.date = this.SaveDateTime; 105 | this.saveData.keep = false; 106 | } 107 | 108 | /*public void setProgression(List> allItemList) 109 | { 110 | 111 | int[] prog = new int[allItemList.Count]; 112 | for (int i=0; i < allItemList.Count; i++) 113 | { 114 | prog[i] = allItemList[i].Count; 115 | } 116 | this.progression = prog; 117 | } 118 | public List GetCharacters() 119 | { 120 | return this.charData; 121 | } 122 | public void LoadCharacterData(string saveFolder) 123 | { 124 | this.charData = RemnantCharacter.GetCharactersFromSave(saveFolder, RemnantCharacter.CharacterProcessingMode.NoEvents); 125 | }*/ 126 | 127 | // Implements IEditableObject 128 | void IEditableObject.BeginEdit() 129 | { 130 | if (!inTxn) 131 | { 132 | this.backupData = saveData; 133 | inTxn = true; 134 | } 135 | } 136 | 137 | void IEditableObject.CancelEdit() 138 | { 139 | if (inTxn) 140 | { 141 | this.saveData = backupData; 142 | inTxn = false; 143 | } 144 | } 145 | 146 | void IEditableObject.EndEdit() 147 | { 148 | if (inTxn) 149 | { 150 | if (!backupData.name.Equals(saveData.name)) 151 | { 152 | OnUpdated(new UpdatedEventArgs("Name")); 153 | } 154 | if (!backupData.date.Equals(saveData.date)) 155 | { 156 | OnUpdated(new UpdatedEventArgs("SaveDate")); 157 | } 158 | if (!backupData.keep.Equals(saveData.keep)) 159 | { 160 | OnUpdated(new UpdatedEventArgs("Keep")); 161 | } 162 | if (!backupData.active.Equals(saveData.active)) 163 | { 164 | OnUpdated(new UpdatedEventArgs("Active")); 165 | } 166 | backupData = new SaveData(); 167 | inTxn = false; 168 | } 169 | } 170 | 171 | public void OnUpdated(UpdatedEventArgs args) 172 | { 173 | EventHandler handler = Updated; 174 | if (null != handler) handler(this, args); 175 | } 176 | 177 | private DateTime SaveDateTime 178 | { 179 | get 180 | { 181 | return File.GetLastWriteTime(save.SaveProfilePath); 182 | } 183 | } 184 | } 185 | 186 | public class UpdatedEventArgs : EventArgs 187 | { 188 | private readonly string _fieldName; 189 | 190 | public UpdatedEventArgs(string fieldName) 191 | { 192 | _fieldName = fieldName; 193 | } 194 | 195 | public string FieldName 196 | { 197 | get { return _fieldName; } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | using RemnantSaveGuardian.Models; 6 | using RemnantSaveGuardian.Properties; 7 | using RemnantSaveGuardian.Services; 8 | 9 | using System; 10 | using System.Globalization; 11 | using System.IO; 12 | using System.Linq; 13 | using System.Reflection; 14 | using System.Runtime.InteropServices; 15 | using System.Threading; 16 | using System.Windows; 17 | using System.Windows.Markup; 18 | using System.Windows.Threading; 19 | 20 | using Wpf.Ui.Mvvm.Contracts; 21 | using Wpf.Ui.Mvvm.Services; 22 | 23 | namespace RemnantSaveGuardian 24 | { 25 | /// 26 | /// Interaction logic for App.xaml 27 | /// 28 | public partial class App 29 | { 30 | // The.NET Generic Host provides dependency injection, configuration, logging, and other services. 31 | // https://docs.microsoft.com/dotnet/core/extensions/generic-host 32 | // https://docs.microsoft.com/dotnet/core/extensions/dependency-injection 33 | // https://docs.microsoft.com/dotnet/core/extensions/configuration 34 | // https://docs.microsoft.com/dotnet/core/extensions/logging 35 | private static readonly IHost _host = Host 36 | .CreateDefaultBuilder() 37 | .ConfigureAppConfiguration(c => { c.SetBasePath(Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)); }) 38 | .ConfigureServices((context, services) => 39 | { 40 | // App Host 41 | services.AddHostedService(); 42 | 43 | // Page resolver service 44 | services.AddSingleton(); 45 | 46 | // Theme manipulation 47 | services.AddSingleton(); 48 | 49 | // TaskBar manipulation 50 | services.AddSingleton(); 51 | 52 | // Service containing navigation, same as INavigationWindow... but without window 53 | services.AddSingleton(); 54 | 55 | // Main window with navigation 56 | services.AddScoped(); 57 | services.AddScoped(); 58 | 59 | // Views and ViewModels 60 | services.AddScoped(); 61 | services.AddScoped(); 62 | services.AddScoped(); 63 | services.AddScoped(); 64 | services.AddScoped(); 65 | services.AddScoped(); 66 | services.AddScoped(); 67 | services.AddScoped(); 68 | 69 | // Configuration 70 | services.Configure(context.Configuration.GetSection(nameof(AppConfig))); 71 | }).Build(); 72 | 73 | /// 74 | /// Gets registered service. 75 | /// 76 | /// Type of the service to get. 77 | /// Instance of the service or . 78 | public static T GetService() 79 | where T : class 80 | { 81 | return _host.Services.GetService(typeof(T)) as T; 82 | } 83 | 84 | /// 85 | /// Occurs when the application is loading. 86 | /// 87 | private async void OnStartup(object sender, StartupEventArgs e) 88 | { 89 | var culture = CultureInfo.GetCultureInfo(GetUserDefaultUILanguage()); 90 | var cultures = EnumerateSupportedCultures(); 91 | Current.Properties["langs"] = cultures; 92 | if (!cultures.Contains(culture) && cultures.Contains(culture.Parent)) 93 | { 94 | culture = culture.Parent; 95 | } 96 | if (Settings.Default.Language != "") 97 | { 98 | culture = cultures.First(e => e.Name == Settings.Default.Language); 99 | } 100 | 101 | Thread.CurrentThread.CurrentCulture = culture; 102 | WPFLocalizeExtension.Engine.LocalizeDictionary.Instance.Culture = culture; 103 | 104 | FrameworkElement.LanguageProperty.OverrideMetadata( 105 | typeof(FrameworkElement), 106 | new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(culture.IetfLanguageTag))); 107 | await _host.StartAsync(); 108 | } 109 | 110 | [DllImport("Kernel32.dll", CharSet = CharSet.Auto)] 111 | static extern ushort GetUserDefaultUILanguage(); 112 | 113 | private CultureInfo[] EnumerateSupportedCultures() 114 | { 115 | CultureInfo[] culture = CultureInfo.GetCultures(CultureTypes.AllCultures); 116 | 117 | string exeLocation = Path.GetDirectoryName(Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path)) ?? ""; 118 | 119 | var c = culture.Where(cultureInfo => Directory.Exists(Path.Combine(exeLocation, cultureInfo.Name)) && cultureInfo.Name != "") 120 | .Prepend(CultureInfo.GetCultureInfo("en")) 121 | .ToArray(); 122 | 123 | return c; 124 | } 125 | 126 | /// 127 | /// Occurs when the application is closing. 128 | /// 129 | private async void OnExit(object sender, ExitEventArgs e) 130 | { 131 | await _host.StopAsync(); 132 | 133 | _host.Dispose(); 134 | } 135 | 136 | /// 137 | /// Occurs when an exception is thrown by an application but not handled. 138 | /// 139 | private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) 140 | { 141 | // For more info see https://docs.microsoft.com/en-us/dotnet/api/system.windows.application.dispatcherunhandledexception?view=windowsdesktop-6.0 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /RemnantSaveGuardian/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | True 7 | 8 | 9 | 10 10 | 11 | 12 | 100 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | True 25 | 26 | 27 | 18 28 | 29 | 30 | True 31 | 32 | 33 | True 34 | 35 | 36 | True 37 | 38 | 39 | True 40 | 41 | 42 | True 43 | 44 | 45 | True 46 | 47 | 48 | True 49 | 50 | 51 | True 52 | 53 | 54 | True 55 | 56 | 57 | True 58 | 59 | 60 | True 61 | 62 | 63 | True 64 | 65 | 66 | True 67 | 68 | 69 | True 70 | 71 | 72 | True 73 | 74 | 75 | True 76 | 77 | 78 | True 79 | 80 | 81 | False 82 | 83 | 84 | Highlight 85 | 86 | 87 | False 88 | 89 | 90 | True 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | Dark 100 | 101 | 102 | backups 103 | 104 | 105 | True 106 | 107 | 108 | 1100 109 | 110 | 111 | 650 112 | 113 | 114 | 115 | 116 | 117 | False 118 | 119 | 120 | False 121 | 122 | 123 | False 124 | 125 | 126 | 1 127 | 128 | 129 | True 130 | 131 | 132 | remwiki 133 | 134 | 135 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | True 12 | 13 | 14 | 10 15 | 16 | 17 | 100 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | True 30 | 31 | 32 | 18 33 | 34 | 35 | True 36 | 37 | 38 | True 39 | 40 | 41 | True 42 | 43 | 44 | True 45 | 46 | 47 | True 48 | 49 | 50 | True 51 | 52 | 53 | True 54 | 55 | 56 | True 57 | 58 | 59 | True 60 | 61 | 62 | True 63 | 64 | 65 | True 66 | 67 | 68 | True 69 | 70 | 71 | True 72 | 73 | 74 | True 75 | 76 | 77 | True 78 | 79 | 80 | True 81 | 82 | 83 | True 84 | 85 | 86 | False 87 | 88 | 89 | Highlight 90 | 91 | 92 | False 93 | 94 | 95 | True 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Dark 105 | 106 | 107 | backups 108 | 109 | 110 | True 111 | 112 | 113 | 1100 114 | 115 | 116 | 650 117 | 118 | 119 | 120 | 121 | 122 | False 123 | 124 | 125 | False 126 | 127 | 128 | False 129 | 130 | 131 | 1 132 | 133 | 134 | True 135 | 136 | 137 | remwiki 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Views/Windows/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 39 | 40 | 48 | 49 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Helpers/WindowDwmHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Windows; 4 | using System.Windows.Interop; 5 | using System.Windows.Media; 6 | using Wpf.Ui.Appearance; 7 | using Wpf.Ui.Interop; 8 | 9 | namespace RemnantSaveGuardian.Helpers 10 | { 11 | internal class WindowDwmHelper 12 | { 13 | internal class Win32API 14 | { 15 | /// 16 | /// Determines whether the specified window handle identifies an existing window. 17 | /// 18 | /// A handle to the window to be tested. 19 | /// If the window handle identifies an existing window, the return value is nonzero. 20 | [DllImport("user32.dll", CharSet = CharSet.Auto)] 21 | [return: MarshalAs(UnmanagedType.Bool)] 22 | public static extern bool IsWindow([In] IntPtr hWnd); 23 | } 24 | internal class Utilities 25 | { 26 | private static readonly PlatformID _osPlatform = Environment.OSVersion.Platform; 27 | 28 | private static readonly Version _osVersion = Environment.OSVersion.Version; 29 | 30 | /// 31 | /// Whether the operating system is NT or newer. 32 | /// 33 | public static bool IsNT => _osPlatform == PlatformID.Win32NT; 34 | 35 | /// 36 | /// Whether the operating system version is greater than or equal to 6.0. 37 | /// 38 | public static bool IsOSVistaOrNewer => _osVersion >= new Version(6, 0); 39 | 40 | /// 41 | /// Whether the operating system version is greater than or equal to 6.1. 42 | /// 43 | public static bool IsOSWindows7OrNewer => _osVersion >= new Version(6, 1); 44 | 45 | /// 46 | /// Whether the operating system version is greater than or equal to 6.2. 47 | /// 48 | public static bool IsOSWindows8OrNewer => _osVersion >= new Version(6, 2); 49 | 50 | /// 51 | /// Whether the operating system version is greater than or equal to 10.0* (build 10240). 52 | /// 53 | public static bool IsOSWindows10OrNewer => _osVersion.Build >= 10240; 54 | 55 | /// 56 | /// Whether the operating system version is greater than or equal to 10.0* (build 22000). 57 | /// 58 | public static bool IsOSWindows11OrNewer => _osVersion.Build >= 22000; 59 | 60 | /// 61 | /// Whether the operating system version is greater than or equal to 10.0* (build 22523). 62 | /// 63 | public static bool IsOSWindows11Insider1OrNewer => _osVersion.Build >= 22523; 64 | 65 | /// 66 | /// Whether the operating system version is greater than or equal to 10.0* (build 22557). 67 | /// 68 | public static bool IsOSWindows11Insider2OrNewer => _osVersion.Build >= 22557; 69 | } 70 | internal enum UXMaterials 71 | { 72 | None = BackgroundType.None, 73 | Mica = BackgroundType.Mica, 74 | Acrylic = BackgroundType.Acrylic 75 | } 76 | internal static Color transparentColor = Color.FromArgb(0x1, 0x80, 0x80, 0x80); 77 | internal static Brush transparentBrush = new SolidColorBrush(transparentColor); 78 | internal static bool IsSupported(UXMaterials type) 79 | { 80 | return type switch 81 | { 82 | UXMaterials.Mica => Utilities.IsOSWindows11OrNewer, 83 | UXMaterials.Acrylic => Utilities.IsOSWindows7OrNewer, 84 | UXMaterials.None => true, 85 | _ => false 86 | }; 87 | } 88 | internal static WindowInteropHelper GetWindow(Window window) 89 | { 90 | return new WindowInteropHelper(window); 91 | } 92 | internal static bool ApplyDwm(Window window, UXMaterials type) 93 | { 94 | IntPtr handle = GetWindow(window).Handle; 95 | 96 | if (type == UXMaterials.Mica && !Utilities.IsOSWindows11Insider1OrNewer) 97 | { 98 | type = UXMaterials.Acrylic; 99 | } 100 | 101 | if (!IsSupported(type)) 102 | return false; 103 | 104 | if (handle == IntPtr.Zero) 105 | return false; 106 | 107 | if (!Win32API.IsWindow(handle)) 108 | return false; 109 | 110 | if (type == UXMaterials.None) 111 | { 112 | RestoreBackground(window); 113 | return UnsafeNativeMethods.RemoveWindowBackdrop(handle); 114 | } 115 | // First release of Windows 11 116 | if (!Utilities.IsOSWindows11Insider1OrNewer) 117 | { 118 | if (type == UXMaterials.Mica) 119 | { 120 | RemoveBackground(window); 121 | return UnsafeNativeMethods.ApplyWindowLegacyMicaEffect(handle); 122 | } 123 | 124 | if (type == UXMaterials.Acrylic) 125 | { 126 | return UnsafeNativeMethods.ApplyWindowLegacyMicaEffect(handle); 127 | } 128 | 129 | return false; 130 | } 131 | 132 | // Newer Windows 11 versions 133 | RemoveBackground(window); 134 | return UnsafeNativeMethods.ApplyWindowBackdrop(handle, (BackgroundType)type); 135 | } 136 | 137 | /// 138 | /// Tries to remove background from and it's composition area. 139 | /// 140 | /// Window to manipulate. 141 | /// if operation was successful. 142 | internal static void RemoveBackground(Window window) 143 | { 144 | if (window == null) 145 | return; 146 | 147 | // Remove background from visual root 148 | window.Background = transparentBrush; 149 | } 150 | internal static void RestoreBackground(Window window) 151 | { 152 | if (window == null) 153 | return; 154 | 155 | var backgroundBrush = window.Resources["ApplicationBackgroundBrush"]; 156 | 157 | // Manual fallback 158 | if (backgroundBrush is not SolidColorBrush) 159 | backgroundBrush = GetFallbackBackgroundBrush(); 160 | 161 | window.Background = (SolidColorBrush)backgroundBrush; 162 | } 163 | private static Brush GetFallbackBackgroundBrush() 164 | { 165 | return Theme.GetAppTheme() == ThemeType.Dark 166 | ? new SolidColorBrush(Color.FromArgb(0xFF, 0x20, 0x20, 0x20)) 167 | : new SolidColorBrush(Color.FromArgb(0xFF, 0xFA, 0xFA, 0xFA)); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd 364 | 365 | secrets/ 366 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/RemnantItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace RemnantSaveGuardian 6 | { 7 | public class RemnantItem : IEquatable, IComparable 8 | { 9 | private static readonly List _itemKeyPatterns = new() { 10 | @"/Items/Trinkets/(?\w+)/(?:\w+/)+(?\w+)(?:\.|$)", // rings and amulets 11 | @"/Items/(?Mods)/\w+/(?\w+)(?:\.|$)", // weapon mods 12 | @"/Items/(?Archetypes)/\w+/(?Archetype_\w+)(?:\.|$)", // archetypes 13 | @"/Items/Archetypes/(?\w+)/(?\w+)/\w+/(?\w+)(?:\.|$)", // perks and skills 14 | @"/Items/(?Traits)/(?\w+?/)?\w+?/(?\w+)(?:\.|$)", // traits 15 | @"/Items/Archetypes/(?\w+)/(?:PerksAnd)?(?Traits)/(?\w+)", // archetype traits dlc2 and dlc3 16 | @"/Items/Archetypes/(?\w+)/(?Armor)/(?\w+)(?:\.|$)", // armors 17 | @"/Items/(?Armor)/(?:\w+/)?(?:(?\w+)/)?(?\w+)(?:\.|$)", // armor 18 | @"/Items/(?Weapons)/(?:\w+/)+(?\w+)(?:\.|$)", // weapons 19 | @"/Items/(?Gems)/(?:\w+/)+(?\w+)(?:\.|$)", // gems 20 | @"/Items/Armor/(?:\w+/)?(?Relic)Testing/(?:\w+/)+(?\w+)(?:\.|$)", // relics 21 | @"/Items/(?Relic)s/(?:\w+/)+(?\w+)(?:\.|$)", // relics 22 | @"/Items/Materials/(?Engrams)/(?\w+)(?:\.|$)", // engrams 23 | @"/Items/Archetypes/(?Warden)/(?Item_HiddenContainer_Material_Engram_Warden)", // warden engram 24 | @"/(?Quests)/Quest_\w+/Items/(?:Quest_Hidden_Item\w+/)?(?\w+)(?:\.|$)", // quest items 25 | @"/Items/(?Materials)/World/\w+/(?\w+)(?:\.|$)", // materials 26 | }; 27 | public static List ItemKeyPatterns { get { return _itemKeyPatterns; } } 28 | public enum RemnantItemMode 29 | { 30 | Normal, 31 | Hardcore, 32 | Survival 33 | } 34 | 35 | private string _key; 36 | private string _name; 37 | private string _type; 38 | private string _set; 39 | private string _part; 40 | public string Name { 41 | get 42 | { 43 | if (this._set != "" && this._part != "") 44 | { 45 | return $"{Loc.GameT($"Armor_{this._set}")} ({Loc.GameT($"Armor_{this._part}")})"; 46 | } 47 | if (_type == "Armor") 48 | { 49 | var armorMatch = Regex.Match(_name, @"\w+_(?(?:Head|Body|Gloves|Legs))_\w+"); 50 | if (armorMatch.Success) 51 | { 52 | return $"{Loc.GameT(_name.Replace($"{armorMatch.Groups["armorPart"].Value}_", ""))} ({Loc.GameT($"Armor_{armorMatch.Groups["armorPart"].Value}")})"; 53 | } 54 | } 55 | return Loc.GameT(_name); 56 | } 57 | } 58 | public string RawName 59 | { 60 | get 61 | { 62 | return _name; 63 | } 64 | } 65 | public string Type 66 | { 67 | get 68 | { 69 | return Loc.GameT(_type); 70 | } 71 | } 72 | public string RawType 73 | { 74 | get 75 | { 76 | return _type; 77 | } 78 | } 79 | public string Key 80 | { 81 | get 82 | { 83 | return _key; 84 | } 85 | } 86 | public RemnantItemMode ItemMode { get; set; } 87 | public string ItemNotes { get; set; } 88 | public bool Coop { get; set; } 89 | public string TileSet { get; set; } 90 | public bool IsArmorSet { get; set; } 91 | public RemnantItem(string nameOrKey) 92 | { 93 | this._key = nameOrKey; 94 | this._name = nameOrKey; 95 | this._type = "Unknown"; 96 | this._set = ""; 97 | this._part = ""; 98 | this.ItemMode = RemnantItemMode.Normal; 99 | this.ItemNotes = ""; 100 | this.Coop = false; 101 | TileSet = ""; 102 | IsArmorSet = true; 103 | foreach (string pattern in ItemKeyPatterns) { 104 | var nameMatch = Regex.Match(nameOrKey, pattern); 105 | if (!nameMatch.Success) 106 | { 107 | continue; 108 | } 109 | this._key = this._key.Replace(".", ""); 110 | if (nameMatch.Groups["archetypeName"].Value == "Warden") { 111 | this._type = "Engrams"; 112 | } else { 113 | this._type = nameMatch.Groups["itemType"].Value; 114 | } 115 | this._name = nameMatch.Groups["itemName"].Value; 116 | if (nameMatch.Groups.ContainsKey("armorSet")) 117 | { 118 | //this._type = "Armor"; 119 | this._set = nameMatch.Groups["armorSet"].Value; 120 | var armorMatch = Regex.Match(this._name, @"Armor_(?\w+)_\w+"); 121 | if (armorMatch.Success) 122 | { 123 | this._part = armorMatch.Groups["armorPart"].Value; 124 | } 125 | } 126 | break; 127 | } 128 | } 129 | 130 | public override string ToString() 131 | { 132 | return Name; 133 | } 134 | 135 | public string TypeName() 136 | { 137 | if (_type == "") 138 | { 139 | return Name; 140 | } 141 | return Type + ": " + Name; 142 | } 143 | 144 | public override bool Equals(Object? obj) 145 | { 146 | //Check for null and compare run-time types. 147 | if ((obj == null)) 148 | { 149 | return false; 150 | } 151 | else if (!this.GetType().Equals(obj.GetType())) 152 | { 153 | if (obj.GetType() == typeof(string)) 154 | { 155 | return (this.Key.Equals(obj)); 156 | } 157 | return false; 158 | } 159 | else 160 | { 161 | RemnantItem rItem = (RemnantItem)obj; 162 | return (this.Key.Equals(rItem.Key) && this.ItemMode == rItem.ItemMode); 163 | } 164 | } 165 | 166 | public override int GetHashCode() 167 | { 168 | return this._name.GetHashCode(); 169 | } 170 | 171 | public int CompareTo(Object? obj) 172 | { 173 | //Check for null and compare run-time types. 174 | if ((obj == null)) 175 | { 176 | return 1; 177 | } 178 | else if (!this.GetType().Equals(obj.GetType())) 179 | { 180 | if (obj.GetType() == typeof(string)) 181 | { 182 | return (this.Key.CompareTo(obj)); 183 | } 184 | return this.ToString().CompareTo(obj.ToString()); 185 | } 186 | else 187 | { 188 | RemnantItem rItem = (RemnantItem)obj; 189 | if (this.ItemMode != rItem.ItemMode) 190 | { 191 | var modeCompare = this.ItemMode.CompareTo(rItem.ItemMode); 192 | if (modeCompare != 0) 193 | { 194 | return modeCompare; 195 | } 196 | } 197 | return this._key.CompareTo(rItem.Key); 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/App.xaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 57 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/RemnantSave.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.IO; 5 | using System.IO.Compression; 6 | using System.Runtime.InteropServices; 7 | 8 | namespace RemnantSaveGuardian 9 | { 10 | public class RemnantSave 11 | { 12 | private List? _Characters; 13 | public List Characters { 14 | get => _Characters ??= RemnantCharacter.GetCharactersFromSave(this, RemnantCharacter.CharacterProcessingMode.NoEvents); 15 | } 16 | 17 | public static readonly string DefaultWgsSaveFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Packages\PerfectWorldEntertainment.RemnantFromtheAshes_jrajkyc4tsa6w\SystemAppData\wgs"; 18 | private string savePath; 19 | private string profileFile; 20 | private RemnantSaveType saveType; 21 | private WindowsSave winSave; 22 | 23 | public static readonly Guid FOLDERID_SavedGames = new(0x4C5C32FF, 0xBB9D, 0x43B0, 0xB5, 0xB4, 0x2D, 0x72, 0xE5, 0x4E, 0xAA, 0xA4); 24 | [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] 25 | static extern string SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags, IntPtr hToken = default); 26 | 27 | public RemnantSave(string path) 28 | { 29 | if (!Directory.Exists(path)) 30 | { 31 | throw new Exception(path + " does not exist."); 32 | } 33 | 34 | if (File.Exists(path + @"\profile.sav")) 35 | { 36 | this.saveType = RemnantSaveType.Normal; 37 | this.profileFile = "profile.sav"; 38 | } 39 | else 40 | { 41 | var winFiles = Directory.GetFiles(path, "container.*"); 42 | if (winFiles.Length > 0) 43 | { 44 | this.winSave = new WindowsSave(winFiles[0]); 45 | this.saveType = RemnantSaveType.WindowsStore; 46 | profileFile = winSave.Profile; 47 | } 48 | else 49 | { 50 | throw new Exception(path + " is not a valid save."); 51 | } 52 | } 53 | this.savePath = path; 54 | } 55 | 56 | public string SaveFolderPath 57 | { 58 | get 59 | { 60 | return this.savePath; 61 | } 62 | } 63 | 64 | public string SaveProfilePath 65 | { 66 | get 67 | { 68 | return this.savePath + $@"\{this.profileFile}"; 69 | } 70 | } 71 | public RemnantSaveType SaveType 72 | { 73 | get { return this.saveType; } 74 | } 75 | public string[] WorldSaves 76 | { 77 | get 78 | { 79 | if (this.saveType == RemnantSaveType.Normal) 80 | { 81 | return Directory.GetFiles(SaveFolderPath, "save_*.sav"); 82 | } 83 | else 84 | { 85 | System.Console.WriteLine(this.winSave.Worlds.ToArray()); 86 | return this.winSave.Worlds.ToArray(); 87 | } 88 | } 89 | } 90 | 91 | public bool Valid 92 | { 93 | get 94 | { 95 | return this.saveType == RemnantSaveType.Normal || this.winSave.Valid; 96 | } 97 | } 98 | 99 | public static bool ValidSaveFolder(string folder) 100 | { 101 | if (!Directory.Exists(folder)) 102 | { 103 | return false; 104 | } 105 | 106 | if (File.Exists(folder + "\\profile.sav")) 107 | { 108 | return true; 109 | } 110 | else 111 | { 112 | var winFiles = Directory.GetFiles(folder, "container.*"); 113 | if (winFiles.Length > 0) 114 | { 115 | return true; 116 | } 117 | } 118 | return false; 119 | } 120 | 121 | public void UpdateCharacters() 122 | { 123 | Characters.Clear(); 124 | Characters.AddRange(RemnantCharacter.GetCharactersFromSave(this)); 125 | } 126 | 127 | public string GetProfileData() 128 | { 129 | return DecompressSaveAsString(this.SaveProfilePath); 130 | } 131 | 132 | public static string DefaultSaveFolder() 133 | { 134 | var saveFolder = SHGetKnownFolderPath(FOLDERID_SavedGames, 0) + @"\Remnant2"; 135 | if (Directory.Exists($@"{saveFolder}\Steam")) 136 | { 137 | saveFolder += @"\Steam"; 138 | var userFolders = Directory.GetDirectories(saveFolder); 139 | if (userFolders.Length > 0) 140 | { 141 | return userFolders[0]; 142 | } 143 | } 144 | else 145 | { 146 | var folders = Directory.GetDirectories(saveFolder); 147 | if (folders.Length > 0) 148 | { 149 | return folders[0]; 150 | } 151 | } 152 | return saveFolder; 153 | } 154 | 155 | // Credit to https://gist.github.com/crackedmind 156 | 157 | internal class FileHeader 158 | { 159 | public uint Crc32; 160 | public uint TotalSize; 161 | public uint Unknown; 162 | 163 | public static FileHeader ReadFromStream(Stream stream) 164 | { 165 | FileHeader header = new(); 166 | using var reader = new BinaryReader(stream, Encoding.UTF8, true); 167 | header.Crc32 = reader.ReadUInt32(); 168 | header.TotalSize = reader.ReadUInt32(); 169 | header.Unknown = reader.ReadUInt32(); 170 | return header; 171 | } 172 | } 173 | internal class ChunkHeader 174 | { 175 | public ulong ChunkHeaderTag; // always 0x222222229E2A83C1 176 | public ulong ChunkSize; // always 0x20000 177 | public byte DecompressionMethod; // 3 - zlib 178 | public ulong CompressedSize1; 179 | public ulong DecompressedSize1; // <= ChunkSize 180 | public ulong CompressedSize2; 181 | public ulong DecompressedSize2; // <= ChunkSize 182 | 183 | public static ChunkHeader ReadFromStream(Stream stream) 184 | { 185 | ChunkHeader header = new ChunkHeader(); 186 | using var reader = new BinaryReader(stream, Encoding.UTF8, true); 187 | header.ChunkHeaderTag = reader.ReadUInt64(); 188 | header.ChunkSize = reader.ReadUInt64(); 189 | header.DecompressionMethod = reader.ReadByte(); 190 | header.CompressedSize1 = reader.ReadUInt64(); 191 | header.DecompressedSize1 = reader.ReadUInt64(); 192 | header.CompressedSize2 = reader.ReadUInt64(); 193 | header.DecompressedSize2 = reader.ReadUInt64(); 194 | return header; 195 | } 196 | } 197 | 198 | public static byte[] DecompressSave(string saveFilePath) 199 | { 200 | if (File.Exists(saveFilePath)) 201 | { 202 | using var fileStream = File.Open(saveFilePath, FileMode.Open); 203 | var fileHeader = FileHeader.ReadFromStream(fileStream); 204 | 205 | var saveContent = new byte[fileHeader.TotalSize]; 206 | using var memstream = new MemoryStream(saveContent); 207 | while (fileStream.Position < fileStream.Length) 208 | { 209 | ChunkHeader header = ChunkHeader.ReadFromStream(fileStream); 210 | byte[] buffer = new byte[header.CompressedSize1]; 211 | fileStream.Read(buffer); 212 | 213 | using var bufferStream = new MemoryStream(buffer); 214 | using var decompressor = new ZLibStream(bufferStream, CompressionMode.Decompress); 215 | decompressor.CopyTo(memstream); 216 | } 217 | 218 | return saveContent; 219 | } 220 | return Array.Empty(); 221 | } 222 | public static string DecompressSaveAsString(string saveFilePath) 223 | { 224 | return Encoding.ASCII.GetString(DecompressSave(saveFilePath)); 225 | } 226 | } 227 | 228 | public enum RemnantSaveType 229 | { 230 | Normal, 231 | WindowsStore 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/RemnantCharacter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using System.IO; 6 | 7 | namespace RemnantSaveGuardian 8 | { 9 | public class RemnantCharacter 10 | { 11 | public List Archetypes { get; set; } 12 | public string SecondArchetype { get; set; } 13 | public List Inventory { get; set; } 14 | public List CampaignEvents { get; } 15 | public List AdventureEvents { get; } 16 | public RemnantSave Save { get; } 17 | public int WorldIndex { get; } = -1; 18 | 19 | public int Progression 20 | { 21 | get 22 | { 23 | return this.Inventory.Count; 24 | } 25 | } 26 | 27 | private List missingItems; 28 | 29 | public enum ProcessMode { Campaign, Adventure }; 30 | 31 | public override string ToString() 32 | { 33 | return string.Join(", ", Archetypes.Select(arch => Loc.GameT(arch))) + " (" + this.Progression + ")"; 34 | } 35 | 36 | public string ToFullString() 37 | { 38 | string str = "CharacterData{ Archetypes: [" + string.Join(", ", Archetypes.Select(arch => Loc.T(arch))) + "], Inventory: [" + string.Join(", ", this.Inventory) + "], CampaignEvents: [" + string.Join(", ", this.CampaignEvents) + "], AdventureEvents: [" + string.Join(", ", this.AdventureEvents) + "] }"; 39 | return str; 40 | } 41 | 42 | public RemnantCharacter(RemnantSave remnantSave, int index) 43 | { 44 | this.Archetypes = new List(); 45 | this.Inventory = new List(); 46 | this.CampaignEvents = new List(); 47 | this.AdventureEvents = new List(); 48 | this.missingItems = new List(); 49 | this.Save = remnantSave; 50 | this.WorldIndex = index; 51 | } 52 | 53 | public enum CharacterProcessingMode { All, NoEvents }; 54 | 55 | public static List GetCharactersFromSave(RemnantSave remnantSave) 56 | { 57 | return GetCharactersFromSave(remnantSave, CharacterProcessingMode.All); 58 | } 59 | 60 | public static List GetCharactersFromSave(RemnantSave remnantSave, CharacterProcessingMode mode) 61 | { 62 | List charData = new List(); 63 | try 64 | { 65 | string profileData = remnantSave.GetProfileData(); 66 | #if DEBUG 67 | File.WriteAllText(remnantSave.SaveProfilePath.Replace(".sav", ".txt"), profileData); 68 | #endif 69 | var archetypes = Regex.Matches(profileData, @"/Game/World_(Base|DLC\d+)/Items/Archetypes/(?\w+)/Archetype_\w+_UI\.Archetype_\w+_UI_C"); 70 | var inventoryStarts = Regex.Matches(profileData, "/Game/Characters/Player/Base/Character_Master_Player.Character_Master_Player_C"); 71 | var inventoryEnds = Regex.Matches(profileData, "[^.]Character_Master_Player_C"); 72 | for (var i = 0; i < inventoryStarts.Count; i++) 73 | { 74 | //Debug.WriteLine($"character {i}"); 75 | Match invMatch = inventoryStarts[i]; 76 | Match invEndMatch = inventoryEnds.First(m => m.Index > invMatch.Index); 77 | var inventory = profileData.Substring(invMatch.Index, invEndMatch.Index - invMatch.Index); 78 | RemnantCharacter cd = new RemnantCharacter(remnantSave, i); 79 | for (var m = 0; m < archetypes.Count; m++) 80 | { 81 | Match archMatch = archetypes[m]; 82 | int prevCharEnd = 0; 83 | if (i > 0) 84 | { 85 | Match prevInvStart = inventoryStarts[i - 1]; 86 | prevCharEnd = inventoryEnds.First(m => m.Index > prevInvStart.Index).Index; 87 | } 88 | if (archMatch.Index > prevCharEnd && archMatch.Index < invMatch.Index) 89 | { 90 | cd.Archetypes.Add(archMatch.Groups["archetype"].Value); 91 | } 92 | } 93 | if (cd.Archetypes.Count == 0) 94 | { 95 | cd.Archetypes.Add("Unknown"); 96 | } 97 | 98 | foreach (string pattern in RemnantItem.ItemKeyPatterns) 99 | { 100 | var itemMatches = new Regex(pattern).Matches(inventory); 101 | foreach (Match itemMatch in itemMatches) 102 | { 103 | cd.Inventory.Add(itemMatch.Value.Replace(".", "").ToLower()); 104 | } 105 | } 106 | 107 | /*rx = new Regex(@"/Items/QuestItems(/[a-zA-Z0-9_]+)+/[a-zA-Z0-9_]+"); 108 | matches = rx.Matches(inventory); 109 | foreach (Match match in matches) 110 | { 111 | saveItems.Add(match.Value); 112 | } 113 | 114 | rx = new Regex(@"/Quests/[a-zA-Z0-9_]+/[a-zA-Z0-9_]+"); 115 | matches = rx.Matches(inventory); 116 | foreach (Match match in matches) 117 | { 118 | saveItems.Add(match.Value); 119 | } 120 | 121 | rx = new Regex(@"/Player/Emotes/Emote_[a-zA-Z0-9]+"); 122 | matches = rx.Matches(inventory); 123 | foreach (Match match in matches) 124 | { 125 | saveItems.Add(match.Value); 126 | }*/ 127 | 128 | if (mode == CharacterProcessingMode.All) 129 | { 130 | cd.LoadWorldData(); 131 | } 132 | charData.Add(cd); 133 | } 134 | } 135 | catch (IOException ex) 136 | { 137 | if (ex.Message.Contains("being used by another process")) 138 | { 139 | if (Views.Pages.BackupsPage.isDataLoaded == true) 140 | { 141 | Logger.Warn(Loc.T("Save file in use; waiting 0.5 seconds and retrying.")); 142 | } 143 | System.Threading.Thread.Sleep(500); 144 | charData = GetCharactersFromSave(remnantSave, mode); 145 | } 146 | } 147 | return charData; 148 | } 149 | 150 | public void LoadWorldData() 151 | { 152 | if (this.Save == null) 153 | { 154 | return; 155 | } 156 | /*if (this.CampaignEvents.Count != 0) 157 | { 158 | return; 159 | }*/ 160 | if (this.WorldIndex >= Save.WorldSaves.Length) 161 | { 162 | return; 163 | } 164 | try 165 | { 166 | RemnantWorldEvent.ProcessEvents(this); 167 | 168 | missingItems.Clear(); 169 | foreach (List eventItems in GameInfo.EventItem.Values) 170 | { 171 | foreach (RemnantItem item in eventItems) 172 | { 173 | if (!this.Inventory.Contains(item.Key.ToLower())) 174 | { 175 | if (!missingItems.Contains(item)) 176 | { 177 | missingItems.Add(item); 178 | } 179 | } 180 | } 181 | } 182 | missingItems.Sort(); 183 | } 184 | catch (IOException ex) 185 | { 186 | if (ex.Message.Contains("being used by another process")) 187 | { 188 | Logger.Warn(Loc.T("Save file in use; waiting 0.5 seconds and retrying.")); 189 | System.Threading.Thread.Sleep(500); 190 | LoadWorldData(); 191 | } 192 | } 193 | catch (Exception ex) 194 | { 195 | Logger.Error($"Error loading world Data in CharacterData.LoadWorldData: {ex.Message} {ex.StackTrace}"); 196 | } 197 | } 198 | 199 | public List GetMissingItems() 200 | { 201 | return missingItems; 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Views/UserControls/TextBlockPlus.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xaml.Behaviors; 2 | using System; 3 | using System.ComponentModel; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | using System.Windows.Input; 7 | using System.Windows.Media; 8 | using System.Windows.Media.Animation; 9 | 10 | namespace RemnantSaveGuardian.Views.UserControls 11 | { 12 | /// 13 | /// TextBlockPlus Control Interface 14 | /// 15 | public partial class TextBlockPlus : UserControl 16 | { 17 | public TextBlockPlus() 18 | { 19 | InitializeComponent(); 20 | } 21 | #region DependencyProperties 22 | [Category("Extend Properties")] 23 | public string Text 24 | { 25 | get { return (string)GetValue(TextProperty); } 26 | set { SetValue(TextProperty, value); } 27 | } 28 | public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(TextBlockPlus), new PropertyMetadata("")); 29 | [Category("Extend Properties")] 30 | public int RollingSpeed 31 | { 32 | get { return (int)GetValue(RollingSpeedProperty); } 33 | set { SetValue(RollingSpeedProperty, value); } 34 | } 35 | public static readonly DependencyProperty RollingSpeedProperty = DependencyProperty.Register("RollingSpeed", typeof(int), typeof(TextBlockPlus), new PropertyMetadata(250)); 36 | 37 | [Category("Extend Properties")] 38 | public int RollbackSpeed 39 | { 40 | get { return (int)GetValue(RollbackSpeedProperty); } 41 | set { SetValue(RollbackSpeedProperty, value); } 42 | } 43 | public static readonly DependencyProperty RollbackSpeedProperty = DependencyProperty.Register("RollbackSpeed", typeof(int), typeof(TextBlockPlus), new PropertyMetadata(1000)); 44 | #endregion 45 | } 46 | 47 | /// 48 | /// Rolling TextBlock Behavior 49 | /// 50 | public sealed class RollingTextBlockBehavior : Behavior 51 | { 52 | public int rollingSpeed 53 | { 54 | get { return (int)GetValue(rollingSpeedProperty); } 55 | set { SetValue(rollingSpeedProperty, value); } 56 | } 57 | public static readonly DependencyProperty rollingSpeedProperty = DependencyProperty.Register("rollingSpeed", typeof(int), typeof(RollingTextBlockBehavior), new PropertyMetadata(250)); 58 | public int rollbackSpeed 59 | { 60 | get { return (int)GetValue(rollbackSpeedProperty); } 61 | set { SetValue(rollbackSpeedProperty, value); } 62 | } 63 | public static readonly DependencyProperty rollbackSpeedProperty = DependencyProperty.Register("rollbackSpeed", typeof(int), typeof(RollingTextBlockBehavior), new PropertyMetadata(1000)); 64 | 65 | private TextBlock? textBlock; 66 | private Storyboard storyBoard = new Storyboard(); 67 | private DoubleAnimation animation = new DoubleAnimation(); 68 | 69 | protected override void OnAttached() 70 | { 71 | base.OnAttached(); 72 | AssociatedObject.MouseEnter += AssociatedObject_MouseEnter; 73 | AssociatedObject.MouseLeave += AssociatedObject_MouseLeave; 74 | AssociatedObject.MouseDown += AssociatedObject_MouseDown; 75 | AssociatedObject.MouseUp += AssociatedObject_MouseUp; 76 | AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel; 77 | 78 | DependencyProperty[] propertyChain = new DependencyProperty[] 79 | { 80 | ScrollViewerBehavior.HorizontalOffsetProperty 81 | }; 82 | 83 | Storyboard.SetTargetProperty(animation, new PropertyPath("(0)", propertyChain)); 84 | storyBoard.Children.Add(animation); 85 | } 86 | protected override void OnDetaching() 87 | { 88 | base.OnDetaching(); 89 | AssociatedObject.MouseEnter -= AssociatedObject_MouseEnter; 90 | AssociatedObject.MouseLeave -= AssociatedObject_MouseLeave; 91 | AssociatedObject.MouseDown -= AssociatedObject_MouseDown; 92 | AssociatedObject.MouseUp -= AssociatedObject_MouseUp; 93 | AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel; 94 | } 95 | private void AssociatedObject_MouseEnter(object sender, RoutedEventArgs e) 96 | { 97 | if (AssociatedObject is not null) 98 | { 99 | var textBlock = this.AssociatedObject as TextBlock; 100 | if (textBlock != null) 101 | { 102 | var scrollViewer = textBlock.Parent as ScrollViewer; 103 | double textWidth = textBlock.ActualWidth - scrollViewer.ActualWidth; 104 | double scrollValue = scrollViewer.HorizontalOffset; 105 | double scrollWidth = scrollViewer.ScrollableWidth; 106 | if (scrollWidth > 0 && rollingSpeed > 0) 107 | { 108 | double time = (scrollWidth - scrollValue) / scrollWidth * (textWidth / rollingSpeed); 109 | animation.To = scrollWidth; 110 | animation.Duration = TimeSpan.FromSeconds(time); 111 | animation.BeginTime = TimeSpan.FromMilliseconds(200); 112 | storyBoard.Begin(scrollViewer, true); 113 | } 114 | } 115 | } 116 | } 117 | private void AssociatedObject_MouseLeave(object sender, RoutedEventArgs e) 118 | { 119 | if (AssociatedObject is not null) 120 | { 121 | var textBlock = this.AssociatedObject as TextBlock; 122 | if (textBlock != null) 123 | { 124 | var scrollViewer = textBlock.Parent as ScrollViewer; 125 | double textWidth = textBlock.ActualWidth - scrollViewer.ActualWidth; 126 | double scrollValue = scrollViewer.HorizontalOffset; 127 | double scrollWidth = scrollViewer.ScrollableWidth; 128 | if (scrollWidth > 0 && rollingSpeed > 0) 129 | { 130 | double time = scrollValue / scrollWidth * (textWidth / rollbackSpeed); 131 | animation.To = 0; 132 | animation.Duration = TimeSpan.FromSeconds(time); 133 | animation.BeginTime = TimeSpan.FromMilliseconds(200); 134 | storyBoard.Begin(scrollViewer, true); 135 | } 136 | } 137 | } 138 | } 139 | private void AssociatedObject_MouseDown(object sender, MouseButtonEventArgs e) 140 | { 141 | if (AssociatedObject is not null) 142 | { 143 | var textBlock = this.AssociatedObject as TextBlock; 144 | if (textBlock != null && e.LeftButton == MouseButtonState.Pressed) 145 | { 146 | var scrollViewer = textBlock.Parent as ScrollViewer; 147 | storyBoard.Pause(scrollViewer); 148 | } 149 | 150 | MouseButton button = MouseButton.Middle; 151 | if (e.LeftButton == MouseButtonState.Pressed) 152 | { 153 | button = MouseButton.Left; 154 | } else if (e.RightButton == MouseButtonState.Pressed) { 155 | button = MouseButton.Right; 156 | } 157 | var eBack = new MouseButtonEventArgs(e.MouseDevice, e.Timestamp, button); 158 | eBack.RoutedEvent = UIElement.MouseDownEvent; 159 | 160 | var ui = VisualUpwardSearch(AssociatedObject) as TextBlockPlus; 161 | ui.RaiseEvent(eBack); 162 | } 163 | } 164 | private void AssociatedObject_MouseUp(object sender, MouseButtonEventArgs e) 165 | { 166 | if (AssociatedObject is not null) 167 | { 168 | var textBlock = this.AssociatedObject as TextBlock; 169 | if (textBlock != null) 170 | { 171 | var scrollViewer = textBlock.Parent as ScrollViewer; 172 | storyBoard.Resume(scrollViewer); 173 | } 174 | 175 | MouseButton button = MouseButton.Middle; 176 | if (e.LeftButton == MouseButtonState.Released) 177 | { 178 | button = MouseButton.Left; 179 | } 180 | else if (e.RightButton == MouseButtonState.Released) 181 | { 182 | button = MouseButton.Right; 183 | } 184 | 185 | var eBack = new MouseButtonEventArgs(e.MouseDevice, e.Timestamp, button); 186 | eBack.RoutedEvent = UIElement.MouseUpEvent; 187 | 188 | var ui = VisualUpwardSearch(AssociatedObject) as TextBlockPlus; 189 | ui.RaiseEvent(eBack); 190 | } 191 | } 192 | private void AssociatedObject_PreviewMouseWheel(object sender, MouseWheelEventArgs e) 193 | { 194 | e.Handled = true; 195 | 196 | var eBack = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta); 197 | eBack.RoutedEvent = UIElement.MouseWheelEvent; 198 | 199 | var ui = VisualUpwardSearch(AssociatedObject) as TextBlockPlus; 200 | ui.RaiseEvent(eBack); 201 | } 202 | private DependencyObject VisualUpwardSearch(DependencyObject source) 203 | { 204 | while (source != null && source.GetType() != typeof(T)) 205 | { 206 | source = VisualTreeHelper.GetParent(source); 207 | } 208 | return source; 209 | } 210 | } 211 | public static class ScrollViewerBehavior 212 | { 213 | public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.RegisterAttached("HorizontalOffset", typeof(double), typeof(ScrollViewerBehavior), new UIPropertyMetadata(0.0, OnHorizontalOffsetChanged)); 214 | public static void SetHorizontalOffset(FrameworkElement target, double value) 215 | { 216 | target.SetValue(HorizontalOffsetProperty, value); 217 | } 218 | public static double GetHorizontalOffset(FrameworkElement target) 219 | { 220 | return (double)target.GetValue(HorizontalOffsetProperty); 221 | } 222 | private static void OnHorizontalOffsetChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) 223 | { 224 | var view = target as ScrollViewer; 225 | if (view != null) 226 | { 227 | view.ScrollToHorizontalOffset((double)e.NewValue); 228 | } 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/GameInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.IO; 5 | using System.Text.Json.Nodes; 6 | using System.Net.Http; 7 | using System.Reflection; 8 | 9 | namespace RemnantSaveGuardian 10 | { 11 | class GameInfo 12 | { 13 | public static event EventHandler GameInfoUpdate; 14 | private static Dictionary zones = new Dictionary(); 15 | private static Dictionary events = new Dictionary(); 16 | private static Dictionary> eventItem = new Dictionary>(); 17 | private static Dictionary subLocations = new Dictionary(); 18 | private static Dictionary injectables = new Dictionary(); 19 | private static List mainLocations = new List(); 20 | private static Dictionary archetypes = new Dictionary(); 21 | private static Dictionary injectableParents = new(); 22 | public static Dictionary Events 23 | { 24 | get 25 | { 26 | if (events.Count == 0) 27 | { 28 | RefreshGameInfo(); 29 | } 30 | 31 | return events; 32 | } 33 | } 34 | public static Dictionary> EventItem 35 | { 36 | get 37 | { 38 | if (eventItem.Count == 0) 39 | { 40 | RefreshGameInfo(); 41 | } 42 | 43 | return eventItem; 44 | } 45 | } 46 | public static Dictionary Zones 47 | { 48 | get 49 | { 50 | if (zones.Count == 0) 51 | { 52 | RefreshGameInfo(); 53 | } 54 | 55 | return zones; 56 | } 57 | } 58 | public static Dictionary SubLocations 59 | { 60 | get 61 | { 62 | if (subLocations.Count == 0) 63 | { 64 | RefreshGameInfo(); 65 | } 66 | 67 | return subLocations; 68 | } 69 | } 70 | public static Dictionary Injectables 71 | { 72 | get 73 | { 74 | if (injectables.Count == 0) 75 | { 76 | RefreshGameInfo(); 77 | } 78 | 79 | return injectables; 80 | } 81 | } 82 | public static Dictionary InjectableParents 83 | { 84 | get 85 | { 86 | if (injectableParents.Count == 0) 87 | { 88 | RefreshGameInfo(); 89 | } 90 | return injectableParents; 91 | } 92 | } 93 | public static List MainLocations 94 | { 95 | get 96 | { 97 | if (mainLocations.Count == 0) 98 | { 99 | RefreshGameInfo(); 100 | } 101 | 102 | return mainLocations; 103 | } 104 | } 105 | 106 | public static Dictionary Archetypes 107 | { 108 | get 109 | { 110 | if (archetypes.Count == 0) 111 | { 112 | RefreshGameInfo(); 113 | } 114 | 115 | return archetypes; 116 | } 117 | } 118 | 119 | public static void RefreshGameInfo() 120 | { 121 | zones.Clear(); 122 | events.Clear(); 123 | eventItem.Clear(); 124 | subLocations.Clear(); 125 | injectables.Clear(); 126 | mainLocations.Clear(); 127 | archetypes.Clear(); 128 | injectableParents.Clear(); 129 | //var json = JsonNode.Parse(File.ReadAllText("game.json")); 130 | var json = GetGameInfoJson(); 131 | var gameEvents = json["events"].AsObject(); 132 | foreach (var worldkvp in gameEvents.AsEnumerable()) 133 | { 134 | foreach (var kvp in worldkvp.Value.AsObject().AsEnumerable()) 135 | { 136 | List eventItems = new List(); 137 | if (kvp.Value == null) 138 | { 139 | Logger.Warn($"Event {kvp.Key} has no items"); 140 | continue; 141 | } 142 | foreach (var item in kvp.Value.AsArray()) 143 | { 144 | if (item["ignore"] != null && item["ignore"].GetValue() == true) 145 | { 146 | continue; 147 | } 148 | RemnantItem rItem = new RemnantItem(item["name"].ToString()); 149 | if (item["notes"] != null) 150 | { 151 | rItem.ItemNotes = Loc.GameTHas($"{rItem.RawName}_Notes") ? Loc.GameT($"{rItem.RawName}_Notes") : item["notes"].ToString(); 152 | } 153 | if (item["mode"] != null) 154 | { 155 | rItem.ItemMode = (RemnantItem.RemnantItemMode)Enum.Parse(typeof(RemnantItem.RemnantItemMode), item["mode"].ToString(), true); 156 | } 157 | if (item["coop"] != null) 158 | { 159 | rItem.Coop = item["coop"].GetValue(); 160 | } 161 | if (item["tileSet"] != null) 162 | { 163 | rItem.TileSet = item["tileSet"].ToString(); 164 | } 165 | if (item["armorSet"] != null) 166 | { 167 | rItem.IsArmorSet = item["armorSet"].GetValue(); 168 | } 169 | eventItems.Add(rItem); 170 | } 171 | eventItem.Add(kvp.Key, eventItems); 172 | } 173 | } 174 | var locations = json["mainLocations"].AsArray(); 175 | foreach (var location in locations) 176 | { 177 | mainLocations.Add(location.ToString()); 178 | } 179 | var subLocs = json["subLocations"].AsObject(); 180 | foreach (var worldkvp in subLocs.AsEnumerable()) 181 | { 182 | foreach (var kvp in worldkvp.Value.AsObject().AsEnumerable()) 183 | { 184 | subLocations.Add(kvp.Key, kvp.Value.ToString()); 185 | } 186 | } 187 | var injects = json["injectables"].AsObject(); 188 | foreach (var worldkvp in injects.AsEnumerable()) 189 | { 190 | foreach (var kvp in worldkvp.Value.AsObject().AsEnumerable()) 191 | { 192 | injectables.Add(kvp.Key, kvp.Value.ToString()); 193 | } 194 | } 195 | var injectParents = json["injectableParents"].AsObject(); 196 | foreach (var kvp in injectParents.AsEnumerable()) 197 | { 198 | injectableParents.Add(kvp.Key, kvp.Value.ToString()); 199 | } 200 | } 201 | public static JsonNode GetGameInfoJson() 202 | { 203 | var assembly = Assembly.GetExecutingAssembly(); 204 | var resourceName = "RemnantSaveGuardian.game.json"; 205 | 206 | string jsonFile; 207 | using (Stream stream = assembly.GetManifestResourceStream(resourceName)) 208 | using (StreamReader reader = new StreamReader(stream)) 209 | { 210 | jsonFile = reader.ReadToEnd(); //Make string equal to full file 211 | } 212 | var embedJson = JsonNode.Parse(jsonFile); 213 | if (File.Exists("game.json")) 214 | { 215 | var json = JsonNode.Parse(File.ReadAllText("game.json")); 216 | if (json["version"].GetValue() > embedJson["version"].GetValue()) 217 | { 218 | return json; 219 | } 220 | } 221 | return embedJson; 222 | } 223 | public static async void CheckForNewGameInfo() 224 | { 225 | GameInfoUpdateEventArgs args = new GameInfoUpdateEventArgs() 226 | { 227 | Result = GameInfoUpdateResult.NoUpdate, 228 | }; 229 | try 230 | { 231 | var request = new HttpRequestMessage(HttpMethod.Get, $"https://raw.githubusercontent.com/Razzmatazzz/RemnantSaveGuardian/main/RemnantSaveGuardian/game.json"); 232 | request.Headers.Add("user-agent", "remnant-save-guardian"); 233 | HttpClient client = new(); 234 | var response = await client.SendAsync(request); 235 | response.EnsureSuccessStatusCode(); 236 | JsonNode gameJson = JsonNode.Parse(await response.Content.ReadAsStringAsync()); 237 | args.RemoteVersion = int.Parse(gameJson["version"].ToString()); 238 | 239 | //var json = JsonNode.Parse(File.ReadAllText("game.json")); 240 | var json = GetGameInfoJson(); 241 | args.LocalVersion = int.Parse(json["version"].ToString()); 242 | 243 | if (args.RemoteVersion > args.LocalVersion) 244 | { 245 | File.WriteAllText("game.json", gameJson.ToJsonString()); 246 | try { 247 | RefreshGameInfo(); 248 | args.Result = GameInfoUpdateResult.Updated; 249 | args.Message = Loc.T("Game info updated."); 250 | } catch (Exception ex) { 251 | //File.WriteAllText("game.json", json.ToString()); 252 | Logger.Error(Loc.T("Could not parse updated game data; check for new version of this app")); 253 | args.Result = GameInfoUpdateResult.Failed; 254 | args.Message = $"{Loc.T("Error checking for new game info")}: {ex.Message}"; 255 | } 256 | } 257 | } 258 | catch (Exception ex) 259 | { 260 | args.Result = GameInfoUpdateResult.Failed; 261 | args.Message = $"{Loc.T("Error checking for new game info")}: {ex.Message}"; 262 | } 263 | 264 | GameInfoUpdate?.Invoke(typeof(GameInfo), args); 265 | } 266 | } 267 | public class GameInfoUpdateEventArgs : EventArgs 268 | { 269 | public int LocalVersion { get; set; } 270 | public int RemoteVersion { get; set; } 271 | public string Message { get; set; } 272 | public GameInfoUpdateResult Result { get; set; } 273 | 274 | public GameInfoUpdateEventArgs() 275 | { 276 | this.LocalVersion = 0; 277 | this.RemoteVersion = 0; 278 | this.Message = "No new game info found."; 279 | this.Result = GameInfoUpdateResult.NoUpdate; 280 | } 281 | } 282 | 283 | public enum GameInfoUpdateResult 284 | { 285 | Updated, 286 | Failed, 287 | NoUpdate 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/Views/Pages/SettingsPage.xaml: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | -------------------------------------------------------------------------------- /RemnantSaveGuardian/locales/Strings.ko.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 렘넌트 세이브 가디언에 대하여 122 | 123 | 124 | 활성화 125 | 126 | 127 | 어드벤처 128 | 129 | 130 | 정말 {backupName}을/를 백업을 삭제합니까? 131 | 132 | 133 | 백업 완료 134 | 135 | 136 | 백업 폴더 137 | 138 | 139 | 백업 폴더가 설정되지 않았습니다; 기본값으로 복구합니다 140 | 141 | 142 | 백업 복원 완료 143 | 144 | 145 | 백업 폴더가 발견되지 않았습니다. 생성중... 146 | 147 | 148 | 백업 발견 149 | 150 | 151 | 현 상태 백업 152 | 153 | 154 | 지금 확인하기 155 | 156 | 157 | 백업 폳더 열기 158 | 159 | 160 | 게임 실행 161 | 162 | 163 | 캠페인 164 | 165 | 166 | 취소 167 | 168 | 169 | 캐릭터 170 | 171 | 172 | 캐릭터 173 | 174 | 175 | 자동 세이브 백업 176 | 177 | 178 | 세이브 파일을 관찰하다 변경이 감지되면 자동으로 백업합니다 179 | 180 | 181 | 프로그램 업데이트 자동 확인 182 | 183 | 184 | 로그 파일 생성 185 | 186 | 187 | log.txt 파일을 생성합니다 188 | 189 | 190 | 월드 판독기에 co-op 아이템 표시하기 191 | 192 | 193 | 판독기에 습득 가능 아이템 열 추가 194 | 195 | 196 | 목록에서 복원하고자 하는 백업을 선택하세요. 197 | 198 | 199 | 삭제 확인 200 | 201 | 202 | 복사 203 | 204 | 205 | 세이브 파일 경로를 찾지 못 했습니다; 수동으로 설정해 주세요. 206 | 207 | 208 | 다크 모드 209 | 210 | 211 | 날짜 212 | 213 | 214 | 삭제 215 | 216 | 217 | 넘치는 세이브 삭제 218 | 219 | 220 | 백업을 새 폴더로 옮길까요? 221 | 222 | 223 | 새 게임 정보 오류 체크중 224 | 225 | 226 | 새 버전 오류 체크중 227 | 228 | 229 | 백업을 복원하기 전에 게임을 종료해 주세요. 230 | 231 | 232 | 세이브 파일을 단순 텍스트로 추출하기 233 | 234 | 235 | 게임 폴더 236 | 237 | 238 | 게임 정보 업데이트 완료 239 | 240 | 241 | 하드코어 242 | 243 | 244 | 245 | 246 | 247 | 정보 248 | 249 | 250 | 유효하지 않은 백업 복원 타입 251 | 252 | 253 | 유효하지 않은 폴더 254 | 255 | 256 | 유효하지 않은 폴더; 백업 폴더는 게임 세이브 폴더와 같을 수 없습니다. 257 | 258 | 259 | 유효하지 않은 폴더; Remnant II 게임이 설치된 폴더를 지정해주세요. 260 | 261 | 262 | 유효하지 않은 폴더; Remnant II 세이브가 저장되어있는 폴더를 지정해주세요. 263 | 264 | 265 | 유효하지 않은 폴더; 백업 폴더와는 다른 폴더를 지정해주세요. 266 | 267 | 268 | 보존 269 | 270 | 271 | 마지막 세이브 백업 시간 272 | 273 | 274 | 저장할 백업 수 (0 : 무제한) 275 | 276 | 277 | 백업 수가 이 한도를 넘으면 자동으로 오래된 백업부터 삭제합니다. 278 | 279 | 280 | 분 마다 백업 281 | 282 | 283 | 다음 백업을 하기 위해 반드시 경과해야하는 최소 시간 284 | 285 | 286 | 월드 판독기 미획득 아이템 색 287 | 288 | 289 | 월드 판독기 미획득 아이템 색 290 | 291 | 292 | 시작 페이지 293 | 294 | 295 | 앱 시작 시 표시할 페이지 296 | 297 | 298 | 라이트 모드 299 | 300 | 301 | 로그 302 | 303 | 304 | 전부 복원 305 | 306 | 307 | 캐릭터만 복원 308 | 309 | 310 | 월드만 복원 311 | 312 | 313 | 백업 복원 314 | 315 | 316 | 미획득 아이템 317 | 318 | 319 | 백업 이동 320 | 321 | 322 | 이름 323 | 324 | 325 | 새 버전이 사용 가능합니다! 326 | 327 | 328 | 보통 329 | 330 | 331 | 폴더 열기 332 | 333 | 334 | 위키 열기 335 | 336 | 337 | 획득 가능 아이템 338 | 339 | 340 | 기본 설정 341 | 342 | 343 | 세이브 파일 변경 진행중 344 | 345 | 346 | 진행 상황 347 | 348 | 349 | Red 350 | 351 | 352 | 백업 저장 353 | 354 | 355 | 세이브 변경이 감지되었습니다; 다음 백업 전 {numMinutes} 분 동안 기다려주세요 356 | 357 | 358 | 세이브 파일이 사용 중입니다; 0.5 초 후 다시 시도해주세요. 359 | 360 | 361 | 세이브 폴더 362 | 363 | 364 | 단순 텍스트로 저장 365 | 366 | 367 | 세이브 데이터 368 | 369 | 370 | 세이브 371 | 372 | 373 | 게임 세이브 폴더와 다른 폴더를 선택하세요. 374 | 375 | 376 | 세이브 파일 타이머 설정 377 | 378 | 379 | 설정 380 | 381 | 382 | 서바이벌 383 | 384 | 385 | 하이라이트 386 | 387 | 388 | 보통 389 | 390 | 391 | 테마 392 | 393 | 394 | White 395 | 396 | 397 | 월드 판독기 398 | 399 | 400 | 이벤트나 아이템으로 정렬하기 401 | 402 | 403 | 다시 체크하려면 5분 기다려주세요 404 | 405 | 406 | 407 | 408 | --------------------------------------------------------------------------------