├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build.yml ├── icon.ico ├── GreenLuma-Manager.slnx ├── App.xaml ├── Models ├── Profile.cs ├── PluginManifest.cs ├── UpdateInfo.cs ├── PluginInfo.cs ├── Config.cs └── Game.cs ├── Plugins └── IPlugin.cs ├── AssemblyInfo.cs ├── Services ├── DepotService.cs ├── ConfigService.cs ├── ProfileService.cs ├── UpdateService.cs ├── SteamService.cs ├── PluginService.cs ├── GreenLumaService.cs ├── IconCacheService.cs └── SearchService.cs ├── LICENSE ├── .gitignore ├── Utilities ├── IconUrlConverter.cs ├── PathDetector.cs └── AutostartManager.cs ├── GreenLuma-Manager.csproj ├── Dialogs ├── CreateProfileDialog.xaml.cs ├── PluginsDialog.xaml.cs ├── CustomMessageBox.xaml.cs ├── CustomMessageBox.xaml ├── SettingsDialog.xaml.cs ├── CreateProfileDialog.xaml └── PluginsDialog.xaml ├── CONTRIBUTING.md ├── README.md └── App.xaml.cs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [3vil3vo] 2 | -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3vil3vo/GreenLuma-Manager/HEAD/icon.ico -------------------------------------------------------------------------------- /GreenLuma-Manager.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /App.xaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /Models/Profile.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace GreenLuma_Manager.Models; 4 | 5 | [DataContract] 6 | public class Profile 7 | { 8 | [DataMember] public string Name { get; set; } = "New Profile"; 9 | 10 | [DataMember] public List Games { get; set; } = []; 11 | } -------------------------------------------------------------------------------- /Models/PluginManifest.cs: -------------------------------------------------------------------------------- 1 | namespace GreenLuma_Manager.Models; 2 | 3 | public class PluginManifest 4 | { 5 | public string Name { get; set; } = string.Empty; 6 | public string Version { get; set; } = string.Empty; 7 | public string Author { get; set; } = string.Empty; 8 | public string Description { get; set; } = string.Empty; 9 | } -------------------------------------------------------------------------------- /Models/UpdateInfo.cs: -------------------------------------------------------------------------------- 1 | namespace GreenLuma_Manager.Models; 2 | 3 | public class UpdateInfo 4 | { 5 | public required string CurrentVersion { get; set; } 6 | public required string LatestVersion { get; set; } 7 | public required string LatestVersionTag { get; set; } 8 | public bool UpdateAvailable { get; set; } 9 | public required string DownloadUrl { get; set; } 10 | public required string ReleaseNotes { get; set; } 11 | } -------------------------------------------------------------------------------- /Plugins/IPlugin.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Media; 3 | 4 | namespace GreenLuma_Manager.Plugins; 5 | 6 | public interface IPlugin 7 | { 8 | string Name { get; } 9 | string Version { get; } 10 | string Author { get; } 11 | string Description { get; } 12 | Geometry Icon { get; } 13 | 14 | void Initialize(); 15 | void OnApplicationStartup(); 16 | void OnApplicationShutdown(); 17 | void ShowUi(Window owner); 18 | } -------------------------------------------------------------------------------- /Models/PluginInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace GreenLuma_Manager.Models; 4 | 5 | [DataContract] 6 | public class PluginInfo 7 | { 8 | [DataMember] public string Name { get; set; } = string.Empty; 9 | [DataMember] public string Version { get; set; } = string.Empty; 10 | [DataMember] public string Author { get; set; } = string.Empty; 11 | [DataMember] public string Description { get; set; } = string.Empty; 12 | [DataMember] public string FileName { get; set; } = string.Empty; 13 | [DataMember] public bool IsEnabled { get; set; } = true; 14 | [DataMember] public string Id { get; set; } = string.Empty; 15 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Services/DepotService.cs: -------------------------------------------------------------------------------- 1 | namespace GreenLuma_Manager.Services; 2 | 3 | public class AppPackageInfo 4 | { 5 | public string AppId { get; set; } = string.Empty; 6 | public List Depots { get; set; } = []; 7 | public List DlcAppIds { get; set; } = []; 8 | public Dictionary> DlcDepots { get; set; } = []; 9 | } 10 | 11 | public static class DepotService 12 | { 13 | public static async Task FetchAppPackageInfoAsync(string appId) 14 | { 15 | if (!uint.TryParse(appId, out var id)) 16 | return null; 17 | 18 | return await SteamService.Instance.GetAppPackageInfoAsync(id); 19 | } 20 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[REQUEST]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /Models/Config.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace GreenLuma_Manager.Models; 4 | 5 | [DataContract] 6 | public class Config 7 | { 8 | [DataMember] public string SteamPath { get; set; } = string.Empty; 9 | 10 | [DataMember] public string GreenLumaPath { get; set; } = string.Empty; 11 | 12 | [DataMember] public bool NoHook { get; set; } 13 | 14 | [DataMember] public bool DisableUpdateCheck { get; set; } 15 | 16 | [DataMember] public bool AutoUpdate { get; set; } = true; 17 | 18 | [DataMember] public string LastProfile { get; set; } = "default"; 19 | 20 | [DataMember] public bool CheckUpdate { get; set; } = true; 21 | 22 | [DataMember] public bool ReplaceSteamAutostart { get; set; } 23 | 24 | [DataMember] public bool FirstRun { get; set; } = true; 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 3vil3vo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build results 2 | [Dd]ebug/ 3 | [Dd]ebugPublic/ 4 | [Rr]elease/ 5 | [Rr]eleases/ 6 | x64/ 7 | x86/ 8 | [Ww][Ii][Nn]32/ 9 | [Aa][Rr][Mm]/ 10 | [Aa][Rr][Mm]64/ 11 | bld/ 12 | [Bb]in/ 13 | [Vv]sbin/ 14 | [Oo]bj/ 15 | [Ll]og/ 16 | [Ll]ogs/ 17 | 18 | # Visual Studio cache/options 19 | .vs/ 20 | .vscode/ 21 | *.suo 22 | *.user 23 | *.userosscache 24 | *.sln.docstates 25 | 26 | # NuGet Packages 27 | *.nupkg 28 | *.snupkg 29 | **/packages/* 30 | !**/packages/build/ 31 | *.nuget.props 32 | *.nuget.targets 33 | 34 | # Visual Studio profiler 35 | *.psess 36 | *.vsp 37 | *.vspx 38 | *.sap 39 | 40 | # ReSharper 41 | _ReSharper*/ 42 | *.[Rr]e[Ss]harper 43 | *.DotSettings.user 44 | 45 | # Build results 46 | [Bb]uild[Ll]og.* 47 | 48 | # MSTest test Results 49 | [Tt]est[Rr]esult*/ 50 | [Bb]uild[Ll]og.* 51 | 52 | # Files built by Visual Studio 53 | *_i.c 54 | *_p.c 55 | *_h.h 56 | *.ilk 57 | *.meta 58 | *.obj 59 | *.iobj 60 | *.pch 61 | *.pdb 62 | *.ipdb 63 | *.pgc 64 | *.pgd 65 | *.rsp 66 | *.sbr 67 | *.tlb 68 | *.tli 69 | *.tlh 70 | *.tmp 71 | *.tmp_proj 72 | *_wpftmp.csproj 73 | *.log 74 | *.tlog 75 | *.vspscc 76 | *.vssscc 77 | .builds 78 | *.pidb 79 | *.svclog 80 | *.scc 81 | 82 | # Backup & report files 83 | _UpgradeReport_Files/ 84 | Backup*/ 85 | UpgradeLog*.XML 86 | UpgradeLog*.htm 87 | ServiceFabricBackup/ 88 | *.rptproj.bak -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | build: 14 | runs-on: windows-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v4 21 | with: 22 | dotnet-version: "10.0.x" 23 | 24 | - name: Restore dependencies 25 | run: dotnet restore GreenLuma-Manager.csproj 26 | 27 | - name: Publish single-file executable 28 | run: dotnet publish GreenLuma-Manager.csproj -r win-x64 -p:PublishSingleFile=true -p:SelfContained=false -c Release -o dist 29 | 30 | - name: Upload artifact 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: GreenLuma-Manager 34 | path: dist/GreenLuma-Manager.exe 35 | 36 | - name: Get tag name 37 | if: startsWith(github.ref, 'refs/tags/') 38 | id: tag 39 | run: echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 40 | shell: bash 41 | 42 | - name: Create Release 43 | if: startsWith(github.ref, 'refs/tags/') 44 | uses: softprops/action-gh-release@v2 45 | with: 46 | files: dist/GreenLuma-Manager.exe 47 | generate_release_notes: true 48 | draft: false 49 | prerelease: false 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /Utilities/IconUrlConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.IO; 3 | using System.Windows.Data; 4 | using System.Windows.Media.Imaging; 5 | 6 | namespace GreenLuma_Manager.Utilities; 7 | 8 | public class IconUrlConverter : IValueConverter 9 | { 10 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 11 | { 12 | if (value is not string iconUrl || string.IsNullOrWhiteSpace(iconUrl)) 13 | return null; 14 | try 15 | { 16 | if (iconUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)) 17 | { 18 | var bmp = new BitmapImage(); 19 | bmp.BeginInit(); 20 | bmp.UriSource = new Uri(iconUrl, UriKind.Absolute); 21 | bmp.CacheOption = BitmapCacheOption.OnDemand; 22 | bmp.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; 23 | bmp.EndInit(); 24 | return bmp; 25 | } 26 | 27 | if (File.Exists(iconUrl)) 28 | { 29 | var bmp = new BitmapImage(); 30 | bmp.BeginInit(); 31 | bmp.UriSource = new Uri(iconUrl, UriKind.Absolute); 32 | bmp.CacheOption = BitmapCacheOption.OnLoad; 33 | bmp.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; 34 | bmp.EndInit(); 35 | bmp.Freeze(); 36 | return bmp; 37 | } 38 | 39 | return null; 40 | } 41 | catch 42 | { 43 | return null; 44 | } 45 | } 46 | 47 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 48 | { 49 | throw new NotImplementedException(); 50 | } 51 | } -------------------------------------------------------------------------------- /Models/Game.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.Serialization; 3 | 4 | namespace GreenLuma_Manager.Models; 5 | 6 | [DataContract] 7 | public class Game : INotifyPropertyChanged 8 | { 9 | [DataMember] public required string AppId { get; set; } 10 | 11 | [DataMember] 12 | public required string Name 13 | { 14 | get; 15 | set 16 | { 17 | if (field != value) 18 | { 19 | field = value; 20 | OnPropertyChanged(nameof(Name)); 21 | } 22 | } 23 | } = string.Empty; 24 | 25 | [DataMember] 26 | public required string Type 27 | { 28 | get; 29 | set 30 | { 31 | if (field != value) 32 | { 33 | field = value; 34 | OnPropertyChanged(nameof(Type)); 35 | } 36 | } 37 | } 38 | 39 | [DataMember] 40 | public string IconUrl 41 | { 42 | get; 43 | set 44 | { 45 | if (field != value) 46 | { 47 | field = value; 48 | OnPropertyChanged(nameof(IconUrl)); 49 | } 50 | } 51 | } = string.Empty; 52 | 53 | [DataMember] public List Depots { get; set; } = []; 54 | 55 | [IgnoreDataMember] 56 | public bool IsEditing 57 | { 58 | get; 59 | set 60 | { 61 | if (field != value) 62 | { 63 | field = value; 64 | OnPropertyChanged(nameof(IsEditing)); 65 | } 66 | } 67 | } 68 | 69 | public event PropertyChangedEventHandler? PropertyChanged; 70 | 71 | protected void OnPropertyChanged(string propertyName) 72 | { 73 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 74 | } 75 | } -------------------------------------------------------------------------------- /GreenLuma-Manager.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net10.0-windows 6 | GreenLuma-Manager 7 | GreenLuma_Manager 8 | enable 9 | enable 10 | true 11 | icon.ico 12 | 1.0.0-rc2.11 13 | 0.0.0.2 14 | 0.0.0.2 15 | Copyright © 3vil3vo 2025 16 | icon.ico 17 | README.md 18 | https://github.com/3vil3vo/GreenLuma-Manager 19 | LICENSE 20 | False 21 | x64 22 | Debug;Release 23 | AnyCPU 24 | true 25 | false 26 | win-x64 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | True 41 | \ 42 | 43 | 44 | True 45 | \ 46 | 47 | 48 | True 49 | \ 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Dialogs/CreateProfileDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Windows; 3 | using System.Windows.Input; 4 | using GreenLuma_Manager.Models; 5 | 6 | namespace GreenLuma_Manager.Dialogs; 7 | 8 | public partial class CreateProfileDialog 9 | { 10 | public CreateProfileDialog() 11 | { 12 | InitializeComponent(); 13 | Result = null; 14 | TxtProfileName.Focus(); 15 | PreviewKeyDown += OnPreviewKeyDown; 16 | } 17 | 18 | public Profile? Result { get; private set; } 19 | 20 | private void OnPreviewKeyDown(object sender, KeyEventArgs e) 21 | { 22 | if (e.Key == Key.Escape) Cancel_Click(sender, null); 23 | } 24 | 25 | private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) 26 | { 27 | if (e.LeftButton == MouseButtonState.Pressed) DragMove(); 28 | } 29 | 30 | private void ProfileName_PreviewTextInput(object sender, TextCompositionEventArgs e) 31 | { 32 | e.Handled = !IsValidProfileNameCharacter(e.Text); 33 | } 34 | 35 | private static bool IsValidProfileNameCharacter(string text) 36 | { 37 | return text.All(c => char.IsLetterOrDigit(c) || c == ' ' || c == '-' || c == '_'); 38 | } 39 | 40 | private void ProfileName_KeyDown(object sender, KeyEventArgs e) 41 | { 42 | switch (e.Key) 43 | { 44 | case Key.Return: 45 | Ok_Click(sender, null); 46 | break; 47 | case Key.Escape: 48 | Cancel_Click(sender, null); 49 | break; 50 | } 51 | } 52 | 53 | private void Ok_Click(object sender, RoutedEventArgs? e) 54 | { 55 | var profileName = TxtProfileName.Text.Trim(); 56 | 57 | if (!ValidateProfileName(profileName)) 58 | return; 59 | 60 | Result = new Profile { Name = profileName }; 61 | DialogResult = true; 62 | Close(); 63 | } 64 | 65 | private static bool ValidateProfileName(string? profileName) 66 | { 67 | if (string.IsNullOrWhiteSpace(profileName)) 68 | { 69 | CustomMessageBox.Show( 70 | "Profile name cannot be empty.", 71 | "Validation", 72 | icon: MessageBoxImage.Exclamation); 73 | return false; 74 | } 75 | 76 | if (profileName.Length > 50) 77 | { 78 | CustomMessageBox.Show( 79 | "Profile name is too long (max 50 characters).", 80 | "Validation", 81 | icon: MessageBoxImage.Exclamation); 82 | return false; 83 | } 84 | 85 | if (ContainsInvalidCharacters(profileName)) 86 | { 87 | CustomMessageBox.Show( 88 | "Profile name contains invalid characters.", 89 | "Validation", 90 | icon: MessageBoxImage.Exclamation); 91 | return false; 92 | } 93 | 94 | return true; 95 | } 96 | 97 | private static bool ContainsInvalidCharacters(string profileName) 98 | { 99 | return profileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 || 100 | profileName.Contains('/') || 101 | profileName.Contains('\\'); 102 | } 103 | 104 | private void Cancel_Click(object sender, RoutedEventArgs? e) 105 | { 106 | DialogResult = false; 107 | Close(); 108 | } 109 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to GreenLuma Manager 2 | 3 | Thank you for your interest in contributing to GreenLuma Manager! 4 | 5 | ## Development Setup 6 | 7 | 1. **Fork and clone the repository** 8 | ```bash 9 | git clone https://github.com/YOUR_USERNAME/GreenLuma-Manager.git 10 | cd GreenLuma-Manager 11 | ``` 12 | 13 | 2. **Open in Visual Studio** 14 | - Open `GreenLuma-Manager.slnx` in Visual Studio 2022 or later 15 | - Ensure you have .NET 10.0 SDK installed 16 | 17 | 3. **Restore NuGet packages** 18 | - Right-click the solution in Solution Explorer 19 | - Select "Restore NuGet Packages" 20 | - Package: Newtonsoft.Json 21 | 22 | 4. **Build and run** 23 | - Press F5 to build and run in debug mode 24 | - Or Build → Build Solution (Ctrl+Shift+B) 25 | 26 | ## Project Structure 27 | 28 | - `Services/` - Core application services 29 | - `ConfigService.cs` - Configuration management and RC3 migration 30 | - `ProfileService.cs` - Profile management and RC3 migration 31 | - `SearchService.cs` - Steam game search and icon fetching 32 | - `GreenLumaService.cs` - AppList generation and GreenLuma launch 33 | - `UpdateService.cs` - Auto-update functionality 34 | - `IconCacheService.cs` - Icon caching and management 35 | - `Models/` - Data models 36 | - `Config.cs` - Application configuration model 37 | - `Profile.cs` - Game profile model 38 | - `Game.cs` - Game data with INotifyPropertyChanged 39 | - `UpdateInfo.cs` - Update information model 40 | - `Dialogs/` - WPF dialog windows 41 | - `SettingsDialog.xaml` - Settings UI 42 | - `CreateProfileDialog.xaml` - Profile creation UI 43 | - `CustomMessageBox.xaml` - Custom message boxes 44 | - `Utilities/` - Helper classes 45 | - `PathDetector.cs` - Auto-detection of Steam/GreenLuma paths 46 | - `IconUrlConverter.cs` - WPF value converter for icons 47 | - `AutostartManager.cs` - Windows startup integration 48 | - `MainWindow.xaml` - Main application window 49 | 50 | ## Code Style 51 | 52 | - Follow C# naming conventions (PascalCase for public members, camelCase for private) 53 | - Use `async`/`await` for asynchronous operations 54 | - Implement `INotifyPropertyChanged` for data-bound properties 55 | - Keep methods focused and single-purpose 56 | - Use meaningful variable and method names 57 | - Add XML documentation comments for public APIs 58 | 59 | ## WPF Best Practices 60 | 61 | - Use MVVM patterns where appropriate 62 | - Leverage data binding over code-behind manipulation 63 | - Use `SynchronizationContext` for UI thread marshaling 64 | - Implement proper resource cleanup in `Dispose` methods 65 | - Use value converters for data transformation in bindings 66 | 67 | ## Pull Request Process 68 | 69 | 1. Create a new branch for your feature 70 | 2. Make your changes following code style guidelines 71 | 3. Test thoroughly in both Debug and Release builds 72 | 4. Commit with clear, descriptive messages 73 | 5. Push to your fork 74 | 6. Open a Pull Request with a clear description 75 | 76 | ## Testing 77 | 78 | Before submitting: 79 | - Build in both Debug and Release configurations 80 | - Test all modified features 81 | - Verify no binding errors in debug output 82 | - Test with both fresh install and RC3 migration scenarios 83 | - Check for memory leaks with long-running operations 84 | - Verify icon loading works correctly 85 | 86 | ## Reporting Bugs 87 | 88 | Use the GitHub Issues tab and include: 89 | - Clear description of the bug 90 | - Steps to reproduce 91 | - Expected vs actual behavior 92 | - Screenshots if applicable 93 | - Your Windows version 94 | - Application version 95 | 96 | ## Feature Requests 97 | 98 | Open an issue with: 99 | - Clear description of the feature 100 | - Why it would be useful 101 | - Any implementation ideas 102 | - Potential impact on existing features 103 | 104 | ## Questions? 105 | 106 | Feel free to open a discussion or issue for any questions! 107 | -------------------------------------------------------------------------------- /Dialogs/PluginsDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using System.Windows.Controls.Primitives; 4 | using System.Windows.Input; 5 | using GreenLuma_Manager.Models; 6 | using GreenLuma_Manager.Services; 7 | using Microsoft.Win32; 8 | 9 | namespace GreenLuma_Manager.Dialogs; 10 | 11 | public partial class PluginsDialog 12 | { 13 | public PluginsDialog() 14 | { 15 | InitializeComponent(); 16 | LoadPlugins(); 17 | PreviewKeyDown += OnPreviewKeyDown; 18 | } 19 | 20 | private void OnPreviewKeyDown(object sender, KeyEventArgs e) 21 | { 22 | if (e.Key == Key.Escape) Close(); 23 | } 24 | 25 | private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) 26 | { 27 | if (e.LeftButton == MouseButtonState.Pressed) DragMove(); 28 | } 29 | 30 | private void LoadPlugins() 31 | { 32 | var plugins = PluginService.GetAllPlugins(); 33 | LstPlugins.ItemsSource = plugins; 34 | 35 | PnlEmptyPlugins.Visibility = plugins.Count == 0 ? Visibility.Visible : Visibility.Collapsed; 36 | } 37 | 38 | private void ImportButton_Click(object sender, RoutedEventArgs e) 39 | { 40 | var dialog = new OpenFileDialog 41 | { 42 | Title = "Select Plugin DLL", 43 | Filter = "Plugin Files (*.dll)|*.dll|All Files (*.*)|*.*", 44 | Multiselect = false 45 | }; 46 | 47 | if (dialog.ShowDialog() != true) return; 48 | 49 | var error = PluginService.ImportPlugin(dialog.FileName); 50 | if (!string.IsNullOrEmpty(error)) 51 | { 52 | CustomMessageBox.Show(error, "Import Failed", icon: MessageBoxImage.Error); 53 | return; 54 | } 55 | 56 | CustomMessageBox.Show( 57 | "Plugin imported successfully. Restart the application to load the plugin.", 58 | "Success", 59 | icon: MessageBoxImage.Asterisk); 60 | 61 | LoadPlugins(); 62 | 63 | if (Owner is MainWindow mainWindow) 64 | mainWindow.UpdatePluginButtons(); 65 | } 66 | 67 | private void RemoveButton_Click(object sender, RoutedEventArgs e) 68 | { 69 | if (sender is not Button button || button.Tag is not PluginInfo plugin) return; 70 | 71 | var result = CustomMessageBox.Show( 72 | $"Remove plugin '{plugin.Name}'?\n\nThis will delete the plugin file and cannot be undone.", 73 | "Confirm Remove", 74 | MessageBoxButton.YesNo, 75 | MessageBoxImage.Warning); 76 | 77 | if (result != MessageBoxResult.Yes) return; 78 | 79 | PluginService.RemovePlugin(plugin); 80 | LoadPlugins(); 81 | 82 | CustomMessageBox.Show("Plugin removed successfully.", "Success", icon: MessageBoxImage.Asterisk); 83 | 84 | if (Owner is MainWindow mainWindow) 85 | mainWindow.UpdatePluginButtons(); 86 | } 87 | 88 | private void PluginToggle_Changed(object sender, RoutedEventArgs e) 89 | { 90 | if (sender is not ToggleButton toggle || toggle.Tag is not PluginInfo plugin) 91 | return; 92 | 93 | var enabled = toggle.IsChecked.GetValueOrDefault(); 94 | PluginService.TogglePlugin(plugin, enabled); 95 | 96 | if (enabled) 97 | CustomMessageBox.Show( 98 | $"Plugin '{plugin.Name}' enabled. Restart the application to load the plugin.", 99 | "Plugin Enabled", 100 | icon: MessageBoxImage.Asterisk); 101 | else 102 | CustomMessageBox.Show( 103 | $"Plugin '{plugin.Name}' disabled. Restart the application to unload the plugin.", 104 | "Plugin Disabled", 105 | icon: MessageBoxImage.Asterisk); 106 | 107 | if (Owner is MainWindow mainWindow) 108 | mainWindow.UpdatePluginButtons(); 109 | } 110 | 111 | private void CloseButton_Click(object sender, RoutedEventArgs e) 112 | { 113 | Close(); 114 | } 115 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GreenLuma Manager 2 | 3 | A modern desktop app for managing your GreenLuma AppList. No more entering app IDs one by one - just search, click, and launch. 4 | 5 | ![Version](https://img.shields.io/github/v/release/3vil3vo/GreenLuma-Manager?label=version) 6 | ![License](https://img.shields.io/badge/license-MIT-green) 7 | ![.NET](https://img.shields.io/badge/.NET-10.0-purple) 8 | 9 | ## Features 10 | 11 | - **Smart Search** - Find any Steam game or DLC instantly with real-time results from Steam's API 12 | - **Profile Management** - Keep different game lists organized with multiple profiles 13 | - **Auto-Detection** - Automatically finds your Steam and GreenLuma folders 14 | - **One-Click Launch** - Generate your AppList and fire up GreenLuma without the hassle 15 | - **Modern UI** - A clean WPF interface built with MaterialDesign 16 | - **Stealth Mode** - Configure GreenLuma's injection settings for discrete operation 17 | - **Auto-Updates** - Keeps you up to date with the latest features and fixes 18 | - **Auto-Start** - Option to launch with Windows and replace Steam startup 19 | 20 | ## Getting Started 21 | 22 | ### Download and Run 23 | 24 | 1. Grab the latest `GreenLuma-Manager.exe` from [Releases](../../releases) 25 | 2. Double-click and run it 26 | 3. The app will auto-detect your Steam and GreenLuma paths 27 | 4. If it doesn't find them, set them manually in Settings (⚙️) 28 | 29 | ### Building from Source 30 | 31 | #### Prerequisites 32 | - Visual Studio 2022 or later 33 | - .NET 10.0 or higher 34 | 35 | #### Steps 36 | 37 | 1. **Clone the repository** 38 | ```bash 39 | git clone https://github.com/3vil3vo/GreenLuma-Manager.git 40 | cd GreenLuma-Manager 41 | ``` 42 | 43 | 2. **Open in Visual Studio** 44 | - Open `GreenLuma-Manager.slnx` 45 | 46 | 3. **Restore NuGet packages** 47 | - Right-click solution → Restore NuGet Packages 48 | 49 | 4. **Build the project** 50 | - Build → Build Solution (Ctrl+Shift+B) 51 | - Find the executable in `bin/Debug/` or `bin/Release/` 52 | 53 | ## How to Use 54 | 55 | 1. **First Time Setup** 56 | - On first launch, the app auto-detects Steam and GreenLuma paths 57 | - If migrating from RC3, your settings and profiles are automatically imported 58 | - Adjust settings in Settings (⚙️) if needed 59 | 60 | 2. **Finding Games** 61 | - Type a game name or AppID into the search box 62 | - Results appear instantly from Steam's database with icons 63 | - Click the + button to add games to your current profile 64 | 65 | 3. **Managing Profiles** 66 | - Select a profile from the dropdown menu 67 | - Create new profiles with the + button 68 | - Add games with +, remove with the delete button 69 | - Each profile is saved automatically 70 | 71 | 4. **Launching GreenLuma** 72 | - Click "Generate AppList" to write files to your GreenLuma folder 73 | - Hit "Launch GreenLuma" - the app will close Steam and start GreenLuma 74 | - Enable stealth mode in settings for discrete injection 75 | 76 | ## Requirements 77 | 78 | - Windows 10/11 79 | - .NET 10.0 or higher 80 | - Steam installed 81 | - GreenLuma 2025 installed 82 | 83 | ## Project Structure 84 | 85 | - `Services/` - Core services (Config, Profile, Search, GreenLuma, Update, IconCache) 86 | - `Models/` - Data models (Config, Profile, Game, UpdateInfo) 87 | - `Dialogs/` - WPF dialogs (Settings, Create Profile, Message Box) 88 | - `Utilities/` - Helper classes (Path detection, Icon converter, Autostart) 89 | 90 | ## Special Thanks 91 | 92 | Shoutout to [BlueAmulet's GreenLuma-2025-Manager](https://github.com/BlueAmulet/GreenLuma-2025-Manager) for inspiration. 93 | 94 | ## Contributing 95 | 96 | Found a bug? Want to add a feature? Pull requests are always welcome! Check [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. 97 | 98 | ## License 99 | 100 | MIT License - See [LICENSE](LICENSE) for details. 101 | 102 | ## Disclaimer 103 | 104 | This is an educational project. Use it responsibly and at your own risk. We're not responsible if something goes wrong. 105 | 106 | ## Author 107 | 108 | Built with ☕ by [3vil3vo](https://github.com/3vil3vo) 109 | 110 | ## Need Help? 111 | 112 | Ran into an issue or have an idea? [Open an issue](../../issues) and let's fix it! 113 | -------------------------------------------------------------------------------- /Services/ConfigService.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | using System.Text.Json; 4 | using GreenLuma_Manager.Models; 5 | using GreenLuma_Manager.Utilities; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace GreenLuma_Manager.Services; 9 | 10 | public class ConfigService 11 | { 12 | private static readonly string ConfigDir = Path.Combine( 13 | Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 14 | "GLM_Manager"); 15 | 16 | private static readonly string ConfigPath = Path.Combine(ConfigDir, "config.json"); 17 | 18 | 19 | public static Config Load() 20 | { 21 | try 22 | { 23 | EnsureConfigDirectoryExists(); 24 | 25 | if (!File.Exists(ConfigPath)) return CreateDefaultConfig(); 26 | 27 | var configJson = File.ReadAllText(ConfigPath, Encoding.UTF8); 28 | 29 | var migratedConfig = TryMigrateFromOldVersion(configJson); 30 | if (migratedConfig != null) return migratedConfig; 31 | 32 | return DeserializeConfig(configJson) ?? new Config(); 33 | } 34 | catch 35 | { 36 | return new Config(); 37 | } 38 | } 39 | 40 | private static void EnsureConfigDirectoryExists() 41 | { 42 | if (!Directory.Exists(ConfigDir)) Directory.CreateDirectory(ConfigDir); 43 | } 44 | 45 | private static Config CreateDefaultConfig() 46 | { 47 | var config = new Config(); 48 | var (steamPath, greenLumaPath) = PathDetector.DetectPaths(); 49 | 50 | config.SteamPath = steamPath; 51 | config.GreenLumaPath = greenLumaPath; 52 | 53 | Save(config); 54 | return config; 55 | } 56 | 57 | private static Config? DeserializeConfig(string json) 58 | { 59 | try 60 | { 61 | return JsonSerializer.Deserialize(json); 62 | } 63 | catch 64 | { 65 | return null; 66 | } 67 | } 68 | 69 | private static Config? TryMigrateFromOldVersion(string configJson) 70 | { 71 | try 72 | { 73 | var jsonData = JObject.Parse(configJson); 74 | 75 | if (jsonData["steam_path"] == null && jsonData["SteamPath"] != null) return null; 76 | 77 | if (jsonData["steam_path"] != null) 78 | { 79 | var config = new Config 80 | { 81 | SteamPath = jsonData["steam_path"]?.ToString() ?? string.Empty, 82 | GreenLumaPath = jsonData["greenluma_path"]?.ToString() ?? string.Empty, 83 | NoHook = jsonData["no_hook"]?.ToObject() ?? false, 84 | DisableUpdateCheck = jsonData["disable_update_check"]?.ToObject() ?? false, 85 | AutoUpdate = jsonData["auto_update"]?.ToObject() ?? true, 86 | LastProfile = jsonData["last_profile"]?.ToString() ?? "default", 87 | CheckUpdate = jsonData["check_update"]?.ToObject() ?? true, 88 | ReplaceSteamAutostart = jsonData["replace_steam_autostart"]?.ToObject() ?? false, 89 | FirstRun = false 90 | }; 91 | 92 | SerializeConfig(config); 93 | return config; 94 | } 95 | 96 | return null; 97 | } 98 | catch 99 | { 100 | return null; 101 | } 102 | } 103 | 104 | public static void Save(Config config) 105 | { 106 | try 107 | { 108 | EnsureConfigDirectoryExists(); 109 | 110 | var json = SerializeConfig(config); 111 | File.WriteAllText(ConfigPath, json, Encoding.UTF8); 112 | } 113 | catch 114 | { 115 | // ignored 116 | } 117 | } 118 | 119 | private static string SerializeConfig(Config config) 120 | { 121 | return JsonSerializer.Serialize(config); 122 | } 123 | 124 | public static void WipeData() 125 | { 126 | try 127 | { 128 | AutostartManager.CleanupAll(); 129 | 130 | if (Directory.Exists(ConfigDir)) Directory.Delete(ConfigDir, true); 131 | } 132 | catch 133 | { 134 | // ignored 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /Utilities/PathDetector.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.Win32; 3 | 4 | namespace GreenLuma_Manager.Utilities; 5 | 6 | public class PathDetector 7 | { 8 | private static readonly string[] GreenLumaSignatureFiles = 9 | [ 10 | "DLLInjector.exe", 11 | "GreenLuma_2025_x64.dll", 12 | "GreenLuma_2025_x86.dll", 13 | "GreenLuma_2024_x86.dll", 14 | "GreenLuma_2023_x86.dll" 15 | ]; 16 | 17 | public static (string SteamPath, string GreenLumaPath) DetectPaths() 18 | { 19 | var steamPath = DetectSteamPath(); 20 | var greenLumaPath = DetectGreenLumaPath(steamPath); 21 | 22 | return (steamPath, greenLumaPath); 23 | } 24 | 25 | public static string DetectSteamPath() 26 | { 27 | var registryPath = TryDetectSteamFromRegistry(); 28 | if (!string.IsNullOrEmpty(registryPath)) 29 | return registryPath; 30 | 31 | return TryDetectSteamFromCommonLocations(); 32 | } 33 | 34 | private static string TryDetectSteamFromRegistry() 35 | { 36 | var path = TryGetRegistryPath("SOFTWARE\\WOW6432Node\\Valve\\Steam"); 37 | if (!string.IsNullOrEmpty(path)) 38 | return path; 39 | 40 | path = TryGetRegistryPath("SOFTWARE\\Valve\\Steam"); 41 | if (!string.IsNullOrEmpty(path)) 42 | return path; 43 | 44 | return string.Empty; 45 | } 46 | 47 | private static string? TryGetRegistryPath(string keyPath) 48 | { 49 | try 50 | { 51 | using var key = Registry.LocalMachine.OpenSubKey(keyPath); 52 | var installPath = key?.GetValue("InstallPath") as string; 53 | 54 | if (!string.IsNullOrWhiteSpace(installPath) && 55 | File.Exists(Path.Combine(installPath, "Steam.exe"))) 56 | return installPath; 57 | } 58 | catch 59 | { 60 | // ignored 61 | } 62 | 63 | return null; 64 | } 65 | 66 | private static string TryDetectSteamFromCommonLocations() 67 | { 68 | string[] commonPaths = 69 | [ 70 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Steam"), 71 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Steam"), 72 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Steam") 73 | ]; 74 | 75 | foreach (var path in commonPaths) 76 | if (File.Exists(Path.Combine(path, "Steam.exe"))) 77 | return path; 78 | 79 | return string.Empty; 80 | } 81 | 82 | public static string DetectGreenLumaPath(string steamPath) 83 | { 84 | if (!string.IsNullOrWhiteSpace(steamPath) && Directory.Exists(steamPath)) 85 | if (ContainsGreenLumaFiles(steamPath)) 86 | return steamPath; 87 | 88 | return TryDetectGreenLumaFromCommonLocations(); 89 | } 90 | 91 | private static string TryDetectGreenLumaFromCommonLocations() 92 | { 93 | string[] commonPaths = 94 | [ 95 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Steam"), 96 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Steam"), 97 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "GreenLuma"), 98 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "GreenLuma"), 99 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "GreenLuma"), 100 | "C:\\GreenLuma", 101 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads", "GreenLuma") 102 | ]; 103 | 104 | foreach (var path in commonPaths) 105 | if (Directory.Exists(path) && ContainsGreenLumaFiles(path)) 106 | return path; 107 | 108 | return string.Empty; 109 | } 110 | 111 | private static bool ContainsGreenLumaFiles(string directory) 112 | { 113 | foreach (var fileName in GreenLumaSignatureFiles) 114 | if (File.Exists(Path.Combine(directory, fileName))) 115 | return true; 116 | 117 | return false; 118 | } 119 | } -------------------------------------------------------------------------------- /Utilities/AutostartManager.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using GreenLuma_Manager.Models; 3 | using Microsoft.Win32; 4 | 5 | namespace GreenLuma_Manager.Utilities; 6 | 7 | public class AutostartManager 8 | { 9 | private const string RunKeyPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"; 10 | private const string BackupKeyPath = "SOFTWARE\\GLM_Manager"; 11 | private const string GreenLumaValueName = "GreenLumaManager"; 12 | private const string GreenLumaMonitorValueName = "GreenLumaMonitor"; 13 | 14 | public static void ManageAutostart(bool replaceSteam, Config? config) 15 | { 16 | try 17 | { 18 | using var runKey = Registry.CurrentUser.OpenSubKey(RunKeyPath, true); 19 | 20 | if (runKey == null) 21 | return; 22 | 23 | if (replaceSteam && !string.IsNullOrWhiteSpace(config?.GreenLumaPath)) 24 | ReplaceWithGreenLuma(runKey, config); 25 | else 26 | RestoreOriginalSteam(runKey, config?.GreenLumaPath); 27 | } 28 | catch 29 | { 30 | // ignored 31 | } 32 | } 33 | 34 | private static void ReplaceWithGreenLuma(RegistryKey runKey, Config? config) 35 | { 36 | if (config == null) 37 | return; 38 | 39 | var appPath = Environment.ProcessPath ?? 40 | Path.Combine(AppContext.BaseDirectory, AppDomain.CurrentDomain.FriendlyName); 41 | 42 | if (string.IsNullOrWhiteSpace(appPath)) 43 | return; 44 | 45 | runKey.SetValue(GreenLumaMonitorValueName, $"\"{appPath}\" --launch-greenluma"); 46 | } 47 | 48 | private static void RestoreOriginalSteam(RegistryKey runKey, string? greenlumaPath) 49 | { 50 | runKey.DeleteValue(GreenLumaMonitorValueName, false); 51 | CleanupVbsScript(greenlumaPath); 52 | } 53 | 54 | private static void CleanupVbsScript(string? greenlumaPath) 55 | { 56 | try 57 | { 58 | if (!string.IsNullOrWhiteSpace(greenlumaPath)) 59 | { 60 | var vbsPath = Path.Combine(greenlumaPath, "GLM_Autostart.vbs"); 61 | if (File.Exists(vbsPath)) File.Delete(vbsPath); 62 | } 63 | } 64 | catch 65 | { 66 | // ignored 67 | } 68 | } 69 | 70 | public static void CleanupAll() 71 | { 72 | try 73 | { 74 | RemoveGreenLumaAutostart(); 75 | RemoveGreenLumaMonitor(); 76 | DeleteBackupKey(); 77 | CleanupAllVbsScripts(); 78 | } 79 | catch 80 | { 81 | // ignored 82 | } 83 | } 84 | 85 | private static void CleanupAllVbsScripts() 86 | { 87 | try 88 | { 89 | string[] commonPaths = 90 | [ 91 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), 92 | "Steam"), 93 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), 94 | "Steam"), 95 | "C:\\GreenLuma" 96 | ]; 97 | 98 | foreach (var basePath in commonPaths) 99 | try 100 | { 101 | var vbsPath = Path.Combine(basePath, "GLM_Autostart.vbs"); 102 | if (File.Exists(vbsPath)) File.Delete(vbsPath); 103 | } 104 | catch 105 | { 106 | // ignored 107 | } 108 | } 109 | catch 110 | { 111 | // ignored 112 | } 113 | } 114 | 115 | private static void RemoveGreenLumaAutostart() 116 | { 117 | try 118 | { 119 | using var runKey = Registry.CurrentUser.OpenSubKey(RunKeyPath, true); 120 | runKey?.DeleteValue(GreenLumaValueName, false); 121 | } 122 | catch 123 | { 124 | // ignored 125 | } 126 | } 127 | 128 | private static void RemoveGreenLumaMonitor() 129 | { 130 | try 131 | { 132 | using var runKey = Registry.CurrentUser.OpenSubKey(RunKeyPath, true); 133 | runKey?.DeleteValue(GreenLumaMonitorValueName, false); 134 | } 135 | catch 136 | { 137 | // ignored 138 | } 139 | } 140 | 141 | private static void DeleteBackupKey() 142 | { 143 | try 144 | { 145 | Registry.CurrentUser.DeleteSubKeyTree(BackupKeyPath, false); 146 | } 147 | catch 148 | { 149 | // ignored 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Windows; 3 | using GreenLuma_Manager.Models; 4 | using GreenLuma_Manager.Services; 5 | 6 | namespace GreenLuma_Manager; 7 | 8 | public partial class App 9 | { 10 | protected override void OnStartup(StartupEventArgs e) 11 | { 12 | base.OnStartup(e); 13 | try 14 | { 15 | PluginService.Initialize(); 16 | PluginService.OnApplicationStartup(); 17 | 18 | if (e.Args.Length > 0) 19 | foreach (var arg in e.Args) 20 | if (string.Equals(arg, "--launch-greenluma", StringComparison.OrdinalIgnoreCase)) 21 | { 22 | try 23 | { 24 | var config = ConfigService.Load(); 25 | GreenLumaService.LaunchGreenLumaAsync(config).GetAwaiter().GetResult(); 26 | } 27 | catch 28 | { 29 | // ignored 30 | } 31 | 32 | Shutdown(); 33 | return; 34 | } 35 | 36 | var profiles = ProfileService.LoadAll(); 37 | var valid = new HashSet(profiles 38 | .SelectMany(p => p.Games) 39 | .Where(g => !string.IsNullOrWhiteSpace(g.AppId)) 40 | .Select(g => g.AppId)); 41 | IconCacheService.DeleteUnusedIcons(valid); 42 | _ = Task.Run(() => WarmupIconsAsync(profiles)); 43 | } 44 | catch 45 | { 46 | // ignored 47 | } 48 | } 49 | 50 | protected override void OnExit(ExitEventArgs e) 51 | { 52 | try 53 | { 54 | PluginService.OnApplicationShutdown(); 55 | } 56 | catch 57 | { 58 | // ignored 59 | } 60 | 61 | base.OnExit(e); 62 | } 63 | 64 | private static async Task WarmupIconsAsync(List profiles) 65 | { 66 | try 67 | { 68 | var semaphore = new SemaphoreSlim(6); 69 | var tasks = new List(); 70 | foreach (var profile in profiles) 71 | { 72 | var changed = false; 73 | foreach (var game in profile.Games) 74 | { 75 | if (string.IsNullOrWhiteSpace(game.AppId)) 76 | continue; 77 | var cached = IconCacheService.GetCachedIconPath(game.AppId); 78 | if (string.IsNullOrEmpty(cached)) 79 | { 80 | await semaphore.WaitAsync(); 81 | var t = Task.Run(async () => 82 | { 83 | try 84 | { 85 | string? path = null; 86 | if (!string.IsNullOrWhiteSpace(game.IconUrl)) 87 | path = await IconCacheService.DownloadAndCacheIconAsync(game.AppId, 88 | game.IconUrl); 89 | 90 | if (string.IsNullOrEmpty(path)) 91 | { 92 | await SearchService.FetchIconUrlAsync(game); 93 | if (!string.IsNullOrWhiteSpace(game.IconUrl)) 94 | path = await IconCacheService.DownloadAndCacheIconAsync(game.AppId, 95 | game.IconUrl); 96 | } 97 | 98 | if (!string.IsNullOrEmpty(path)) 99 | { 100 | game.IconUrl = path; 101 | changed = true; 102 | } 103 | } 104 | catch 105 | { 106 | // ignored 107 | } 108 | finally 109 | { 110 | semaphore.Release(); 111 | } 112 | }); 113 | tasks.Add(t); 114 | } 115 | else if (!string.IsNullOrWhiteSpace(game.IconUrl) && !File.Exists(cached)) 116 | { 117 | await semaphore.WaitAsync(); 118 | var t2 = Task.Run(async () => 119 | { 120 | try 121 | { 122 | var path = 123 | await IconCacheService.DownloadAndCacheIconAsync(game.AppId, game.IconUrl); 124 | if (!string.IsNullOrEmpty(path)) 125 | { 126 | game.IconUrl = path; 127 | changed = true; 128 | } 129 | } 130 | catch 131 | { 132 | // ignored 133 | } 134 | finally 135 | { 136 | semaphore.Release(); 137 | } 138 | }); 139 | tasks.Add(t2); 140 | } 141 | } 142 | 143 | await Task.WhenAll(tasks); 144 | if (changed) 145 | try 146 | { 147 | ProfileService.Save(profile); 148 | } 149 | catch 150 | { 151 | // ignored 152 | } 153 | 154 | tasks.Clear(); 155 | } 156 | } 157 | catch 158 | { 159 | // ignored 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /Dialogs/CustomMessageBox.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | using System.Windows.Input; 4 | using System.Windows.Media; 5 | using SolidColorBrush = System.Windows.Media.SolidColorBrush; 6 | 7 | namespace GreenLuma_Manager.Dialogs; 8 | 9 | public partial class CustomMessageBox 10 | { 11 | private CustomMessageBox(string message, string title, MessageBoxButton buttons, MessageBoxImage icon) 12 | { 13 | InitializeComponent(); 14 | 15 | TitleText.Text = title; 16 | MessageText.Text = message; 17 | 18 | SetIcon(icon); 19 | SetButtons(buttons); 20 | 21 | PreviewKeyDown += OnPreviewKeyDown; 22 | } 23 | 24 | public MessageBoxResult Result { get; private set; } 25 | 26 | private void OnPreviewKeyDown(object sender, KeyEventArgs e) 27 | { 28 | if (e.Key == Key.Escape) 29 | { 30 | Result = MessageBoxResult.Cancel; 31 | DialogResult = false; 32 | Close(); 33 | } 34 | } 35 | 36 | private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) 37 | { 38 | if (e.LeftButton == MouseButtonState.Pressed) DragMove(); 39 | } 40 | 41 | private void SetIcon(MessageBoxImage icon) 42 | { 43 | var iconData = GetIconData(icon); 44 | 45 | if (!string.IsNullOrEmpty(iconData.PathData)) 46 | { 47 | IconPath.Data = Geometry.Parse(iconData.PathData); 48 | IconPath.Fill = iconData.Brush; 49 | } 50 | else 51 | { 52 | IconPath.Visibility = Visibility.Collapsed; 53 | } 54 | } 55 | 56 | private (string PathData, SolidColorBrush Brush) GetIconData(MessageBoxImage icon) 57 | { 58 | return icon switch 59 | { 60 | MessageBoxImage.Hand => ( 61 | "M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z", 62 | (SolidColorBrush)FindResource("Danger") 63 | ), 64 | MessageBoxImage.Question => ( 65 | "M10,19H13V22H10V19M12,2C17.35,2.22 19.68,7.62 16.5,11.67C15.67,12.67 14.33,13.33 13.67,14.17C13,15 13,16 13,17H10C10,15.33 10,13.92 10.67,12.92C11.33,11.92 12.67,11.33 13.5,10.67C15.92,8.43 15.32,5.26 12,5A3,3 0 0,0 9,8H6A6,6 0 0,1 12,2Z", 66 | (SolidColorBrush)FindResource("Info") 67 | ), 68 | MessageBoxImage.Exclamation => ( 69 | "M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z", 70 | (SolidColorBrush)FindResource("Warning") 71 | ), 72 | MessageBoxImage.Asterisk => ( 73 | "M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z", 74 | (SolidColorBrush)FindResource("Info") 75 | ), 76 | _ => ( 77 | "M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z", 78 | (SolidColorBrush)FindResource("Info") 79 | ) 80 | }; 81 | } 82 | 83 | private void SetButtons(MessageBoxButton buttons) 84 | { 85 | ButtonPanel.Children.Clear(); 86 | 87 | switch (buttons) 88 | { 89 | case MessageBoxButton.OK: 90 | AddButton("OK", MessageBoxResult.OK, true); 91 | break; 92 | 93 | case MessageBoxButton.OKCancel: 94 | AddButton("CANCEL", MessageBoxResult.Cancel, false); 95 | AddButton("OK", MessageBoxResult.OK, true); 96 | break; 97 | 98 | case MessageBoxButton.YesNoCancel: 99 | AddButton("CANCEL", MessageBoxResult.Cancel, false); 100 | AddButton("NO", MessageBoxResult.No, false); 101 | AddButton("YES", MessageBoxResult.Yes, true); 102 | break; 103 | 104 | case MessageBoxButton.YesNo: 105 | AddButton("NO", MessageBoxResult.No, false); 106 | AddButton("YES", MessageBoxResult.Yes, true); 107 | break; 108 | } 109 | } 110 | 111 | private void AddButton(string text, MessageBoxResult result, bool isPrimary) 112 | { 113 | var button = new Button 114 | { 115 | Content = text, 116 | Style = isPrimary 117 | ? (Style)FindResource("MessageBtn") 118 | : (Style)FindResource("SecondaryBtn"), 119 | Margin = ButtonPanel.Children.Count > 0 120 | ? new Thickness(8, 0, 0, 0) 121 | : new Thickness(0) 122 | }; 123 | 124 | button.Click += (_, _) => 125 | { 126 | Result = result; 127 | DialogResult = result != MessageBoxResult.Cancel && result != MessageBoxResult.No; 128 | Close(); 129 | }; 130 | 131 | if (isPrimary) button.IsDefault = true; 132 | 133 | ButtonPanel.Children.Add(button); 134 | } 135 | 136 | public static MessageBoxResult Show( 137 | string message, 138 | string title = "Message", 139 | MessageBoxButton buttons = MessageBoxButton.OK, 140 | MessageBoxImage icon = MessageBoxImage.None) 141 | { 142 | var messageBox = new CustomMessageBox(message, title, buttons, icon); 143 | 144 | SetOwnerWindow(messageBox); 145 | 146 | messageBox.ShowDialog(); 147 | return messageBox.Result; 148 | } 149 | 150 | private static void SetOwnerWindow(CustomMessageBox messageBox) 151 | { 152 | var activeWindow = Application.Current.Windows 153 | .OfType() 154 | .FirstOrDefault(w => w.IsActive); 155 | 156 | if (activeWindow != null && activeWindow != messageBox) 157 | messageBox.Owner = activeWindow; 158 | else if (Application.Current.MainWindow != null && Application.Current.MainWindow != messageBox) 159 | messageBox.Owner = Application.Current.MainWindow; 160 | } 161 | } -------------------------------------------------------------------------------- /Services/ProfileService.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | using System.Text.Json; 4 | using GreenLuma_Manager.Models; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace GreenLuma_Manager.Services; 8 | 9 | public class ProfileService 10 | { 11 | private static readonly string ProfilesDir = Path.Combine( 12 | Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 13 | "GLM_Manager", 14 | "profiles"); 15 | 16 | public static List LoadAll() 17 | { 18 | var profiles = new List(); 19 | try 20 | { 21 | EnsureProfilesDirectoryExists(); 22 | TryMigrateProfilesFromOldVersion(); 23 | LoadProfilesFromDirectory(profiles); 24 | if (profiles.Count == 0) return CreateDefaultProfile(profiles); 25 | } 26 | catch 27 | { 28 | // ignored 29 | } 30 | 31 | return profiles; 32 | } 33 | 34 | private static void EnsureProfilesDirectoryExists() 35 | { 36 | if (!Directory.Exists(ProfilesDir)) Directory.CreateDirectory(ProfilesDir); 37 | } 38 | 39 | private static List CreateDefaultProfile(List profiles) 40 | { 41 | var defaultProfile = new Profile { Name = "default" }; 42 | Save(defaultProfile); 43 | profiles.Add(defaultProfile); 44 | return profiles; 45 | } 46 | 47 | private static void LoadProfilesFromDirectory(List profiles) 48 | { 49 | foreach (var file in Directory.GetFiles(ProfilesDir, "*.json")) 50 | try 51 | { 52 | var profile = DeserializeProfile(File.ReadAllText(file, Encoding.UTF8)); 53 | if (profile != null) profiles.Add(profile); 54 | } 55 | catch 56 | { 57 | // ignored 58 | } 59 | } 60 | 61 | public static Profile? Load(string profileName) 62 | { 63 | try 64 | { 65 | var filePath = GetProfileFilePath(profileName); 66 | if (!File.Exists(filePath)) 67 | return null; 68 | 69 | var json = File.ReadAllText(filePath, Encoding.UTF8); 70 | return DeserializeProfile(json); 71 | } 72 | catch 73 | { 74 | return null; 75 | } 76 | } 77 | 78 | public static void Save(Profile profile) 79 | { 80 | try 81 | { 82 | EnsureProfilesDirectoryExists(); 83 | var filePath = GetProfileFilePath(profile.Name); 84 | var json = SerializeProfile(profile); 85 | File.WriteAllText(filePath, json, Encoding.UTF8); 86 | } 87 | catch 88 | { 89 | // ignored 90 | } 91 | } 92 | 93 | public static void Delete(string profileName) 94 | { 95 | try 96 | { 97 | if (string.Equals(profileName, "default", StringComparison.OrdinalIgnoreCase)) 98 | return; 99 | 100 | var filePath = GetProfileFilePath(profileName); 101 | if (File.Exists(filePath)) File.Delete(filePath); 102 | } 103 | catch 104 | { 105 | // ignored 106 | } 107 | } 108 | 109 | public static void Export(Profile profile, string destinationPath) 110 | { 111 | try 112 | { 113 | var json = SerializeProfile(profile); 114 | File.WriteAllText(destinationPath, json, Encoding.UTF8); 115 | } 116 | catch 117 | { 118 | // ignored 119 | } 120 | } 121 | 122 | public static Profile? Import(string sourcePath) 123 | { 124 | try 125 | { 126 | var json = File.ReadAllText(sourcePath, Encoding.UTF8); 127 | return DeserializeProfile(json); 128 | } 129 | catch 130 | { 131 | return null; 132 | } 133 | } 134 | 135 | private static string GetProfileFilePath(string profileName) 136 | { 137 | var sanitizedName = SanitizeFileName(profileName); 138 | return Path.Combine(ProfilesDir, $"{sanitizedName}.json"); 139 | } 140 | 141 | private static Profile? DeserializeProfile(string json) 142 | { 143 | try 144 | { 145 | return JsonSerializer.Deserialize(json); 146 | } 147 | catch 148 | { 149 | return null; 150 | } 151 | } 152 | 153 | private static string SerializeProfile(Profile profile) 154 | { 155 | return JsonSerializer.Serialize(profile); 156 | } 157 | 158 | private static string SanitizeFileName(string name) 159 | { 160 | var invalidChars = Path.GetInvalidFileNameChars(); 161 | return new string([.. name.Select(c => invalidChars.Contains(c) ? '_' : c)]); 162 | } 163 | 164 | private static void TryMigrateProfilesFromOldVersion() 165 | { 166 | try 167 | { 168 | if (!Directory.Exists(ProfilesDir)) return; 169 | 170 | var filesToMigrate = Directory.GetFiles(ProfilesDir, "*.json"); 171 | 172 | foreach (var file in filesToMigrate) 173 | try 174 | { 175 | var rc3Json = File.ReadAllText(file, Encoding.UTF8); 176 | var rc3Data = JObject.Parse(rc3Json); 177 | 178 | if (rc3Data["games"] is not JArray gamesArray || gamesArray.Count == 0) continue; 179 | 180 | if (gamesArray[0] is not JObject firstGame || firstGame["id"] == null) continue; 181 | 182 | var profile = new Profile 183 | { 184 | Name = rc3Data["name"]?.ToString() ?? "default", 185 | Games = 186 | [ 187 | .. gamesArray 188 | .Select(gameToken => new Game 189 | { 190 | AppId = gameToken["id"]?.ToString() ?? string.Empty, 191 | Name = gameToken["name"]?.ToString() ?? string.Empty, 192 | Type = gameToken["type"]?.ToString() ?? "Game" 193 | }) 194 | .Where(g => !string.IsNullOrEmpty(g.AppId)) 195 | ] 196 | }; 197 | 198 | Save(profile); 199 | } 200 | catch 201 | { 202 | // ignored 203 | } 204 | } 205 | catch 206 | { 207 | // ignored 208 | } 209 | } 210 | } -------------------------------------------------------------------------------- /Dialogs/CustomMessageBox.xaml: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 62 | 63 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 105 | 106 | 107 | 113 | 119 | 120 | 121 | 122 | 123 | 130 | 131 | 137 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /Services/UpdateService.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Text.RegularExpressions; 5 | using GreenLuma_Manager.Models; 6 | 7 | namespace GreenLuma_Manager.Services; 8 | 9 | public partial class UpdateService 10 | { 11 | private const string GitHubApiUrl = "https://api.github.com/repos/3vil3vo/GreenLuma-Manager/releases/latest"; 12 | private static readonly string[] RcSeparator = ["-rc"]; 13 | 14 | private static readonly HttpClient Client; 15 | 16 | static UpdateService() 17 | { 18 | Client = new HttpClient(); 19 | Client.DefaultRequestHeaders.Add("User-Agent", "GreenLuma-Manager"); 20 | } 21 | 22 | private static string CurrentVersion => MainWindow.Version; 23 | 24 | [GeneratedRegex("\"tag_name\"\\s*:\\s*\"([^\"]+)\"")] 25 | private static partial Regex TagNameRegex(); 26 | 27 | [GeneratedRegex("\"browser_download_url\"\\s*:\\s*\"([^\"]+)\"")] 28 | private static partial Regex BrowserDownloadUrlRegex(); 29 | 30 | [GeneratedRegex("\"body\"\\s*:\\s*\"([^\"]*)\"")] 31 | private static partial Regex BodyRegex(); 32 | 33 | public static async Task CheckForUpdatesAsync() 34 | { 35 | try 36 | { 37 | var response = await Client.GetStringAsync(GitHubApiUrl); 38 | return ParseUpdateInfo(response); 39 | } 40 | catch 41 | { 42 | return null; 43 | } 44 | } 45 | 46 | private static UpdateInfo? ParseUpdateInfo(string jsonResponse) 47 | { 48 | var tagMatch = TagNameRegex().Match(jsonResponse); 49 | var downloadMatch = BrowserDownloadUrlRegex().Match(jsonResponse); 50 | var bodyMatch = BodyRegex().Match(jsonResponse); 51 | 52 | if (!tagMatch.Success) 53 | return null; 54 | 55 | var latestTag = tagMatch.Groups[1].Value; 56 | var downloadUrl = downloadMatch.Success ? downloadMatch.Groups[1].Value : string.Empty; 57 | var releaseNotes = bodyMatch.Success 58 | ? bodyMatch.Groups[1].Value.Replace("\\n", "\n").Replace("\\r", "\r") 59 | : string.Empty; 60 | 61 | var currentNormalized = NormalizeVersion(CurrentVersion); 62 | var latestNormalized = NormalizeVersion(latestTag); 63 | 64 | return new UpdateInfo 65 | { 66 | CurrentVersion = CurrentVersion, 67 | LatestVersion = FormatDisplayVersion(latestTag), 68 | LatestVersionTag = latestTag, 69 | UpdateAvailable = 70 | string.Compare(latestNormalized, currentNormalized, StringComparison.OrdinalIgnoreCase) > 0, 71 | DownloadUrl = downloadUrl, 72 | ReleaseNotes = releaseNotes 73 | }; 74 | } 75 | 76 | private static string NormalizeVersion(string version) 77 | { 78 | var cleaned = version.ToLower().Trim().Replace("v", ""); 79 | 80 | if (string.IsNullOrEmpty(cleaned)) 81 | return "00000.00000.00000.0000.0000"; 82 | 83 | if (cleaned.StartsWith("rc") && !cleaned.Contains(".0.0")) 84 | { 85 | var rcPart = cleaned[2..]; 86 | var rcParts = rcPart.Split('.'); 87 | 88 | var rcMajor = ParseVersionPart(rcParts, 0); 89 | var rcMinor = rcParts.Length > 1 ? ParseVersionPart(rcParts, 1) : 0; 90 | 91 | return $"00001.00000.00000.{rcMajor:D4}.{rcMinor:D4}"; 92 | } 93 | 94 | var parts = cleaned.Split('-', 2); 95 | var baseVersion = parts[0]; 96 | 97 | var versionParts = baseVersion.Split('.'); 98 | var major = ParseVersionPart(versionParts, 0); 99 | var minor = ParseVersionPart(versionParts, 1); 100 | var patch = ParseVersionPart(versionParts, 2); 101 | 102 | if (parts.Length == 1) return $"{major:D5}.{minor:D5}.{patch:D5}.9999.9999"; 103 | 104 | var prerelease = parts[1]; 105 | 106 | if (prerelease.StartsWith("rc")) 107 | { 108 | var rcPart = prerelease[2..]; 109 | var rcParts = rcPart.Split('.'); 110 | 111 | var rcMajor = ParseVersionPart(rcParts, 0); 112 | var rcMinor = rcParts.Length > 1 ? ParseVersionPart(rcParts, 1) : 0; 113 | 114 | return $"{major:D5}.{minor:D5}.{patch:D5}.{rcMajor:D4}.{rcMinor:D4}"; 115 | } 116 | 117 | return $"{major:D5}.{minor:D5}.{patch:D5}.0000.0000"; 118 | } 119 | 120 | private static int ParseVersionPart(string[] parts, int index) 121 | { 122 | if (index >= parts.Length) 123 | return 0; 124 | 125 | if (int.TryParse(parts[index], out var result)) 126 | return result; 127 | 128 | return 0; 129 | } 130 | 131 | private static string FormatDisplayVersion(string version) 132 | { 133 | var cleaned = version.ToLower().Replace("v", ""); 134 | 135 | if (cleaned.Contains("-rc")) 136 | { 137 | var parts = cleaned.Split(RcSeparator, StringSplitOptions.None); 138 | if (parts.Length > 1) return "RC" + parts[1].ToUpper(); 139 | } 140 | 141 | if (cleaned.StartsWith("rc")) return cleaned.ToUpper(); 142 | 143 | return cleaned; 144 | } 145 | 146 | public static async Task PerformAutoUpdateAsync(string downloadUrl) 147 | { 148 | try 149 | { 150 | var currentExePath = Environment.ProcessPath!; 151 | var tempExePath = await DownloadUpdate(downloadUrl); 152 | 153 | CreateAndExecuteUpdateScript(tempExePath, currentExePath); 154 | 155 | return true; 156 | } 157 | catch 158 | { 159 | return false; 160 | } 161 | } 162 | 163 | private static async Task DownloadUpdate(string downloadUrl) 164 | { 165 | var tempDir = Path.GetTempPath(); 166 | var tempExePath = Path.Combine(tempDir, "GreenLumaManager_Update.exe"); 167 | 168 | using var response = await Client.GetAsync(downloadUrl); 169 | response.EnsureSuccessStatusCode(); 170 | 171 | await using var fileStream = new FileStream(tempExePath, FileMode.Create, FileAccess.Write, FileShare.None); 172 | await response.Content.CopyToAsync(fileStream); 173 | 174 | return tempExePath; 175 | } 176 | 177 | private static void CreateAndExecuteUpdateScript(string tempExePath, string currentExePath) 178 | { 179 | var scriptPath = Path.Combine(Path.GetTempPath(), "update.bat"); 180 | var scriptContent = GenerateUpdateScript(tempExePath, currentExePath); 181 | 182 | File.WriteAllText(scriptPath, scriptContent); 183 | 184 | Process.Start(new ProcessStartInfo 185 | { 186 | FileName = scriptPath, 187 | CreateNoWindow = true, 188 | UseShellExecute = false, 189 | WindowStyle = ProcessWindowStyle.Hidden 190 | }); 191 | } 192 | 193 | private static string GenerateUpdateScript(string tempExePath, string currentExePath) 194 | { 195 | var currentExeName = Path.GetFileName(currentExePath); 196 | 197 | return $@"@echo off 198 | echo Waiting for application to close... 199 | timeout /t 2 /nobreak >nul 200 | 201 | taskkill /F /IM ""{currentExeName}"" >nul 2>&1 202 | 203 | echo Waiting for process to fully terminate... 204 | :wait_process 205 | tasklist /FI ""IMAGENAME eq {currentExeName}"" 2>NUL | find /I /N ""{currentExeName}"">NUL 206 | if ""%ERRORLEVEL%""==""0"" ( 207 | timeout /t 1 /nobreak >nul 208 | goto wait_process 209 | ) 210 | 211 | echo Process terminated. 212 | echo Updating... 213 | 214 | del ""{currentExePath}"" >nul 2>&1 215 | :wait_delete 216 | if exist ""{currentExePath}"" ( 217 | timeout /t 1 /nobreak >nul 218 | del ""{currentExePath}"" >nul 2>&1 219 | goto wait_delete 220 | ) 221 | 222 | move /y ""{tempExePath}"" ""{currentExePath}"" 223 | if errorlevel 1 ( 224 | echo Update failed! 225 | pause 226 | exit /b 1 227 | ) 228 | 229 | echo. 230 | echo ======================================== 231 | echo Update Complete! 232 | echo ======================================== 233 | echo. 234 | echo Restarting GreenLuma Manager... 235 | timeout /t 2 /nobreak >nul 236 | 237 | start """" ""{currentExePath}"" 238 | exit 239 | "; 240 | } 241 | } -------------------------------------------------------------------------------- /Services/SteamService.cs: -------------------------------------------------------------------------------- 1 | using SteamKit2; 2 | 3 | namespace GreenLuma_Manager.Services; 4 | 5 | public sealed class SteamService : IDisposable 6 | { 7 | private static readonly Lazy InstanceHolder = new(() => new SteamService()); 8 | 9 | private readonly Task _callbackLoop; 10 | private readonly CallbackManager _callbackManager; 11 | private readonly TaskCompletionSource _connectedTcs; 12 | private readonly CancellationTokenSource _cts; 13 | private readonly TaskCompletionSource _loggedOnTcs; 14 | private readonly SteamApps _steamApps; 15 | private readonly SteamClient _steamClient; 16 | private readonly SteamUser _steamUser; 17 | 18 | private bool _isConnected; 19 | private bool _isLoggedOn; 20 | private bool _isRunning; 21 | 22 | private SteamService() 23 | { 24 | _steamClient = new SteamClient(); 25 | _callbackManager = new CallbackManager(_steamClient); 26 | _steamUser = _steamClient.GetHandler()!; 27 | _steamApps = _steamClient.GetHandler()!; 28 | 29 | _cts = new CancellationTokenSource(); 30 | _connectedTcs = new TaskCompletionSource(); 31 | _loggedOnTcs = new TaskCompletionSource(); 32 | 33 | _callbackManager.Subscribe(OnConnected); 34 | _callbackManager.Subscribe(OnDisconnected); 35 | _callbackManager.Subscribe(OnLoggedOn); 36 | 37 | _isRunning = true; 38 | _callbackLoop = Task.Run(CallbackLoop); 39 | 40 | _steamClient.Connect(); 41 | } 42 | 43 | public static SteamService Instance => InstanceHolder.Value; 44 | 45 | public void Dispose() 46 | { 47 | _isRunning = false; 48 | _cts.Cancel(); 49 | _steamClient.Disconnect(); 50 | try 51 | { 52 | _callbackLoop.Wait(1000); 53 | } 54 | catch 55 | { 56 | // ignored 57 | } 58 | 59 | _cts.Dispose(); 60 | } 61 | 62 | public async Task GetGameDetailsAsync(uint appId) 63 | { 64 | var result = await GetAppInfoBatchAsync([appId]).ConfigureAwait(false); 65 | return result.GetValueOrDefault(appId); 66 | } 67 | 68 | public async Task> GetAppInfoBatchAsync(List appIds) 69 | { 70 | var results = new Dictionary(); 71 | var maxRetries = 2; 72 | 73 | for (var attempt = 0; attempt <= maxRetries; attempt++) 74 | try 75 | { 76 | await EnsureReadyAsync().ConfigureAwait(false); 77 | 78 | var requests = appIds.Select(id => new SteamApps.PICSRequest { ID = id, AccessToken = 0 }).ToList(); 79 | using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); 80 | 81 | var job = _steamApps.PICSGetProductInfo(requests, []); 82 | var task = job.ToTask(); 83 | 84 | if (await Task.WhenAny(task, Task.Delay(5000, cts.Token)) != task) 85 | { 86 | if (attempt < maxRetries) continue; 87 | break; 88 | } 89 | 90 | var result = await task.ConfigureAwait(false); 91 | 92 | if (result.Failed || result.Results == null) 93 | { 94 | if (attempt < maxRetries) 95 | { 96 | await Task.Delay(500, cts.Token).ConfigureAwait(false); 97 | continue; 98 | } 99 | 100 | return results; 101 | } 102 | 103 | foreach (var callback in result.Results) 104 | foreach (var (appId, appData) in callback.Apps) 105 | { 106 | var kv = appData.KeyValues; 107 | var common = kv["common"]; 108 | 109 | var name = common["name"].Value ?? $"App {appId}"; 110 | var type = MapSteamTypeToDisplayType(common["type"].Value ?? "Game"); 111 | 112 | var clientIconHash = common["clienticon"].Value; 113 | var parentId = common["parent"].Value; 114 | 115 | var libAssets = common["library_assets"]; 116 | var heroHash = libAssets["hero_capsule"]["image"].Value; 117 | 118 | var assets = common["assets"]; 119 | var mainHash = assets["main_capsule"]["image"].Value; 120 | 121 | var headerNode = common["header_image"]; 122 | var headerImage = headerNode.Value; 123 | if (string.IsNullOrEmpty(headerImage)) 124 | headerImage = headerNode["english"].Value; 125 | 126 | results[appId] = new GameDetails( 127 | appId.ToString(), 128 | type, 129 | name, 130 | clientIconHash, 131 | heroHash, 132 | mainHash, 133 | parentId, 134 | headerImage 135 | ); 136 | } 137 | 138 | if (results.Count > 0) return results; 139 | } 140 | catch 141 | { 142 | if (attempt == maxRetries) break; 143 | await Task.Delay(500).ConfigureAwait(false); 144 | } 145 | 146 | return results; 147 | } 148 | 149 | public async Task GetAppPackageInfoAsync(uint appId) 150 | { 151 | try 152 | { 153 | await EnsureReadyAsync().ConfigureAwait(false); 154 | 155 | var request = new SteamApps.PICSRequest { ID = appId, AccessToken = 0 }; 156 | var job = _steamApps.PICSGetProductInfo([request], []); 157 | 158 | var result = await job.ToTask().ConfigureAwait(false); 159 | 160 | if (result.Failed || result.Results == null) 161 | return null; 162 | 163 | foreach (var callback in result.Results) 164 | if (callback.Apps.TryGetValue(appId, out var appData)) 165 | return ParseAppPackageInfo(appId, appData); 166 | 167 | return null; 168 | } 169 | catch 170 | { 171 | return null; 172 | } 173 | } 174 | 175 | private static AppPackageInfo? ParseAppPackageInfo(uint appId, 176 | SteamApps.PICSProductInfoCallback.PICSProductInfo appData) 177 | { 178 | var kv = appData.KeyValues; 179 | 180 | var type = kv["common"]["type"].Value; 181 | if (string.Equals(type, "depot", StringComparison.OrdinalIgnoreCase)) 182 | return null; 183 | 184 | var info = new AppPackageInfo 185 | { 186 | AppId = appId.ToString() 187 | }; 188 | 189 | var dlcList = kv["common"]["extended"]["listofdlc"].Value; 190 | if (!string.IsNullOrEmpty(dlcList)) 191 | info.DlcAppIds = dlcList.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList(); 192 | 193 | foreach (var dlcId in info.DlcAppIds) 194 | info.DlcDepots[dlcId] = []; 195 | 196 | var depotsNode = kv["depots"]; 197 | foreach (var child in depotsNode.Children) 198 | { 199 | if (!uint.TryParse(child.Name, out var depotId)) 200 | continue; 201 | 202 | if (depotId == appId) 203 | continue; 204 | 205 | if (child["manifests"] == KeyValue.Invalid && child["depotfromapp"] == KeyValue.Invalid) 206 | continue; 207 | 208 | var dlcAppId = child["dlcappid"].Value; 209 | 210 | if (!string.IsNullOrEmpty(dlcAppId) && info.DlcDepots.TryGetValue(dlcAppId, out var dlcDepotList)) 211 | dlcDepotList.Add(depotId.ToString()); 212 | else 213 | info.Depots.Add(depotId.ToString()); 214 | } 215 | 216 | return info; 217 | } 218 | 219 | private async Task EnsureReadyAsync() 220 | { 221 | if (!_isConnected) 222 | await _connectedTcs.Task.ConfigureAwait(false); 223 | 224 | if (!_isLoggedOn) 225 | await _loggedOnTcs.Task.ConfigureAwait(false); 226 | } 227 | 228 | private async Task CallbackLoop() 229 | { 230 | while (_isRunning && !_cts.Token.IsCancellationRequested) 231 | { 232 | _callbackManager.RunWaitCallbacks(TimeSpan.FromMilliseconds(100)); 233 | await Task.Delay(100).ConfigureAwait(false); 234 | } 235 | } 236 | 237 | private void OnConnected(SteamClient.ConnectedCallback callback) 238 | { 239 | _isConnected = true; 240 | _connectedTcs.TrySetResult(); 241 | _steamUser.LogOnAnonymous(); 242 | } 243 | 244 | private void OnDisconnected(SteamClient.DisconnectedCallback callback) 245 | { 246 | _isConnected = false; 247 | _isLoggedOn = false; 248 | 249 | Task.Delay(TimeSpan.FromSeconds(5)).ContinueWith(_ => 250 | { 251 | if (_isRunning) _steamClient.Connect(); 252 | }); 253 | } 254 | 255 | private void OnLoggedOn(SteamUser.LoggedOnCallback callback) 256 | { 257 | if (callback.Result == EResult.OK) 258 | { 259 | _isLoggedOn = true; 260 | _loggedOnTcs.TrySetResult(); 261 | } 262 | } 263 | 264 | private static string MapSteamTypeToDisplayType(string steamType) 265 | { 266 | return steamType.ToLower() switch 267 | { 268 | "game" => "Game", 269 | "dlc" => "DLC", 270 | "demo" => "Demo", 271 | "mod" => "Mod", 272 | "video" => "Video", 273 | "music" => "Soundtrack", 274 | "bundle" => "Bundle", 275 | "episode" => "Episode", 276 | "tool" or "advertising" => "Software", 277 | _ => "Game" 278 | }; 279 | } 280 | } 281 | 282 | public record GameDetails( 283 | string AppId, 284 | string Type, 285 | string Name, 286 | string? ClientIconHash = null, 287 | string? HeroHash = null, 288 | string? MainHash = null, 289 | string? ParentAppId = null, 290 | string? HeaderImage = null 291 | ); -------------------------------------------------------------------------------- /Dialogs/SettingsDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Windows; 3 | using System.Windows.Input; 4 | using GreenLuma_Manager.Models; 5 | using GreenLuma_Manager.Services; 6 | using GreenLuma_Manager.Utilities; 7 | using Microsoft.Win32; 8 | 9 | namespace GreenLuma_Manager.Dialogs; 10 | 11 | public partial class SettingsDialog 12 | { 13 | private readonly Config _config; 14 | 15 | public SettingsDialog(Config config) 16 | { 17 | InitializeComponent(); 18 | _config = config; 19 | 20 | LoadSettings(); 21 | UpdateAutoUpdateVisibility(); 22 | 23 | PreviewKeyDown += OnPreviewKeyDown; 24 | } 25 | 26 | private void LoadSettings() 27 | { 28 | TxtSteamPath.Text = _config.SteamPath; 29 | TxtGreenLumaPath.Text = _config.GreenLumaPath; 30 | ChkReplaceSteamAutostart.IsChecked = _config.ReplaceSteamAutostart; 31 | ChkDisableUpdateCheck.IsChecked = _config.DisableUpdateCheck; 32 | ChkAutoUpdate.IsChecked = _config.AutoUpdate; 33 | } 34 | 35 | private void OnPreviewKeyDown(object sender, KeyEventArgs e) 36 | { 37 | if (e.Key == Key.Escape) Cancel_Click(this, new RoutedEventArgs()); 38 | } 39 | 40 | private void Nav_Checked(object sender, RoutedEventArgs e) 41 | { 42 | if (ViewGeneral == null || ViewSystem == null || ViewAdvanced == null) return; 43 | 44 | ViewGeneral.Visibility = Visibility.Collapsed; 45 | ViewSystem.Visibility = Visibility.Collapsed; 46 | ViewAdvanced.Visibility = Visibility.Collapsed; 47 | 48 | if (NavGeneral.IsChecked == true) ShowView(ViewGeneral); 49 | else if (NavSystem.IsChecked == true) ShowView(ViewSystem); 50 | else if (NavAdvanced.IsChecked == true) ShowView(ViewAdvanced); 51 | } 52 | 53 | private void ShowView(UIElement view) 54 | { 55 | view.Visibility = Visibility.Visible; 56 | } 57 | 58 | private void BrowseSteam_Click(object sender, RoutedEventArgs e) 59 | { 60 | var dialog = new OpenFolderDialog 61 | { 62 | Title = "Select Steam folder" 63 | }; 64 | 65 | if (!string.IsNullOrWhiteSpace(TxtSteamPath.Text) && Directory.Exists(TxtSteamPath.Text)) 66 | dialog.InitialDirectory = TxtSteamPath.Text; 67 | 68 | if (dialog.ShowDialog() == true) TxtSteamPath.Text = dialog.FolderName; 69 | } 70 | 71 | private void BrowseGreenLuma_Click(object sender, RoutedEventArgs e) 72 | { 73 | var dialog = new OpenFolderDialog 74 | { 75 | Title = "Select GreenLuma folder" 76 | }; 77 | 78 | if (!string.IsNullOrWhiteSpace(TxtGreenLumaPath.Text) && Directory.Exists(TxtGreenLumaPath.Text)) 79 | dialog.InitialDirectory = TxtGreenLumaPath.Text; 80 | 81 | if (dialog.ShowDialog() == true) TxtGreenLumaPath.Text = dialog.FolderName; 82 | } 83 | 84 | private void AutoDetect_Click(object sender, RoutedEventArgs e) 85 | { 86 | var (steamPath, greenLumaPath) = PathDetector.DetectPaths(); 87 | 88 | TxtSteamPath.Text = steamPath; 89 | TxtGreenLumaPath.Text = greenLumaPath; 90 | 91 | if (!string.IsNullOrWhiteSpace(steamPath) && !string.IsNullOrWhiteSpace(greenLumaPath)) 92 | CustomMessageBox.Show("Paths detected successfully!", "Success", icon: MessageBoxImage.Asterisk); 93 | else 94 | CustomMessageBox.Show("Could not detect all paths automatically.", "Detection", 95 | icon: MessageBoxImage.Exclamation); 96 | } 97 | 98 | private void DisableUpdateCheck_Changed(object sender, RoutedEventArgs e) 99 | { 100 | UpdateAutoUpdateVisibility(); 101 | } 102 | 103 | private void UpdateAutoUpdateVisibility() 104 | { 105 | if (ChkAutoUpdate == null || ChkDisableUpdateCheck == null) 106 | return; 107 | 108 | var isEnabled = !ChkDisableUpdateCheck.IsChecked.GetValueOrDefault(); 109 | ChkAutoUpdate.IsEnabled = isEnabled; 110 | 111 | if (!isEnabled) ChkAutoUpdate.IsChecked = false; 112 | } 113 | 114 | private void WipeData_Click(object sender, RoutedEventArgs e) 115 | { 116 | if (CustomMessageBox.Show("This will delete all profiles and settings. Continue?", "Wipe Data", 117 | MessageBoxButton.YesNo, MessageBoxImage.Exclamation) != MessageBoxResult.Yes) 118 | return; 119 | 120 | if (CustomMessageBox.Show("Are you absolutely sure? This cannot be undone.", "Confirm Wipe", 121 | MessageBoxButton.YesNo, MessageBoxImage.Exclamation) != MessageBoxResult.Yes) 122 | return; 123 | 124 | ConfigService.WipeData(); 125 | CustomMessageBox.Show("All data has been wiped. The application will now close.", "Complete", 126 | icon: MessageBoxImage.Asterisk); 127 | Application.Current.Shutdown(); 128 | } 129 | 130 | private void Ok_Click(object sender, RoutedEventArgs e) 131 | { 132 | var steamPath = NormalizePath(TxtSteamPath.Text); 133 | var greenLumaPath = NormalizePath(TxtGreenLumaPath.Text); 134 | 135 | if (!ValidatePaths(steamPath, greenLumaPath)) return; 136 | 137 | _config.SteamPath = steamPath; 138 | _config.GreenLumaPath = greenLumaPath; 139 | _config.ReplaceSteamAutostart = ChkReplaceSteamAutostart.IsChecked.GetValueOrDefault(); 140 | _config.DisableUpdateCheck = ChkDisableUpdateCheck.IsChecked.GetValueOrDefault(); 141 | _config.AutoUpdate = ChkAutoUpdate.IsChecked.GetValueOrDefault(); 142 | 143 | ConfigService.Save(_config); 144 | AutostartManager.ManageAutostart(_config.ReplaceSteamAutostart, _config); 145 | 146 | DialogResult = true; 147 | Close(); 148 | } 149 | 150 | private void Cancel_Click(object sender, RoutedEventArgs e) 151 | { 152 | DialogResult = false; 153 | Close(); 154 | } 155 | 156 | private static string NormalizePath(string? path) 157 | { 158 | return string.IsNullOrWhiteSpace(path) ? string.Empty : path.Trim().TrimEnd('\\', '/'); 159 | } 160 | 161 | private static bool ValidatePaths(string steamPath, string greenLumaPath) 162 | { 163 | if (string.IsNullOrWhiteSpace(steamPath)) 164 | { 165 | CustomMessageBox.Show("Steam path cannot be empty.", "Validation", icon: MessageBoxImage.Exclamation); 166 | return false; 167 | } 168 | 169 | if (string.IsNullOrWhiteSpace(greenLumaPath)) 170 | { 171 | CustomMessageBox.Show("GreenLuma path cannot be empty.", "Validation", icon: MessageBoxImage.Exclamation); 172 | return false; 173 | } 174 | 175 | if (!Directory.Exists(steamPath)) 176 | { 177 | CustomMessageBox.Show("Steam path does not exist.", "Validation", icon: MessageBoxImage.Exclamation); 178 | return false; 179 | } 180 | 181 | var steamExePath = Path.Combine(steamPath, "Steam.exe"); 182 | if (!File.Exists(steamExePath)) 183 | { 184 | CustomMessageBox.Show($"Steam.exe not found at:\n{steamExePath}", "Validation", 185 | icon: MessageBoxImage.Exclamation); 186 | return false; 187 | } 188 | 189 | if (!Directory.Exists(greenLumaPath)) 190 | { 191 | CustomMessageBox.Show($"GreenLuma path does not exist:\n{greenLumaPath}", "Validation", 192 | icon: MessageBoxImage.Exclamation); 193 | return false; 194 | } 195 | 196 | if (string.Equals(Path.GetFullPath(steamPath), Path.GetFullPath(greenLumaPath), 197 | StringComparison.OrdinalIgnoreCase)) 198 | { 199 | var result = CustomMessageBox.Show( 200 | "Installing GreenLuma in the Steam directory is not recommended. Some games scan this location for GreenLuma files, which may result in detection.\n\n" + 201 | "Do you want to continue anyway?", 202 | "Security Warning", 203 | MessageBoxButton.YesNo, 204 | MessageBoxImage.Warning); 205 | 206 | if (result != MessageBoxResult.Yes) 207 | return false; 208 | } 209 | 210 | if (IsPathReadOnly(greenLumaPath)) 211 | { 212 | CustomMessageBox.Show( 213 | $"The GreenLuma path is read-only.\nPlease ensure the folder is writable and not marked as Read-Only.\nPath: {greenLumaPath}", 214 | "Validation", 215 | icon: MessageBoxImage.Exclamation); 216 | return false; 217 | } 218 | 219 | var missingFiles = GetMissingGreenLumaFiles(greenLumaPath); 220 | if (missingFiles.Count > 0) 221 | { 222 | CustomMessageBox.Show( 223 | $"GreenLuma installation is incomplete.\nThe following files are missing:\n\n{string.Join("\n", missingFiles)}", 224 | "Validation", 225 | icon: MessageBoxImage.Exclamation); 226 | return false; 227 | } 228 | 229 | return true; 230 | } 231 | 232 | private static bool IsPathReadOnly(string path) 233 | { 234 | try 235 | { 236 | var info = new DirectoryInfo(path); 237 | if ((info.Attributes & FileAttributes.ReadOnly) != 0) 238 | return true; 239 | 240 | var tempFile = Path.Combine(path, Path.GetRandomFileName()); 241 | using (File.Create(tempFile, 1, FileOptions.DeleteOnClose)) 242 | { 243 | } 244 | 245 | return false; 246 | } 247 | catch 248 | { 249 | return true; 250 | } 251 | } 252 | 253 | private static List GetMissingGreenLumaFiles(string path) 254 | { 255 | string[] requiredFiles = 256 | [ 257 | "DLLInjector.exe", 258 | "DLLInjector.ini", 259 | "GreenLumaSettings_2025.exe", 260 | "GreenLuma_2025_x64.dll", 261 | "GreenLuma_2025_x86.dll", 262 | Path.Combine("GreenLuma2025_Files", "AchievementUnlocked.wav"), 263 | Path.Combine("GreenLuma2025_Files", "BootImage.bmp") 264 | ]; 265 | 266 | var missing = new List(); 267 | foreach (var file in requiredFiles) 268 | if (!File.Exists(Path.Combine(path, file))) 269 | missing.Add(file); 270 | 271 | var x86Launcher = Path.Combine(path, "bin", "x86launcher.exe"); 272 | var x64Launcher = Path.Combine(path, "bin", "x64launcher.exe"); 273 | 274 | if (!File.Exists(x86Launcher) && !File.Exists(x64Launcher)) 275 | missing.Add(Path.Combine("bin", "x86launcher.exe")); 276 | 277 | return missing; 278 | } 279 | } -------------------------------------------------------------------------------- /Dialogs/CreateProfileDialog.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 65 | 92 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 135 | 136 | 142 | 171 | 172 | 173 | 174 | 175 | 180 | 181 | 187 | 188 |