├── unlockfps_gui
├── Assets
│ └── icon.ico
├── AssemblyInfo.cs
├── FodyWeavers.xml
├── Converters
│ ├── BooleanConverters.cs
│ ├── FullPath2NameConverter.cs
│ ├── Enum2ListConverter.cs
│ ├── HasItemsConverter.cs
│ └── DelegateConverter.cs
├── OSVersionExt
│ ├── Win32API
│ │ ├── IWin32API.cs
│ │ ├── Win32ApiProvider.cs
│ │ └── Win32ApiEnums.cs
│ ├── VersionInfo.cs
│ ├── Environment
│ │ ├── IEnvironment.cs
│ │ └── EnvironmentProvider.cs
│ ├── MajorVersion10
│ │ ├── RegistryProviderDefault.cs
│ │ └── MajorVersion10Properties.cs
│ └── Registry
│ │ └── IRegistry.cs
├── ViewModels
│ └── ViewModelBase.cs
├── Views
│ ├── AboutWindow.axaml.cs
│ ├── AlertWindow.axaml.cs
│ ├── AboutWindow.axaml
│ ├── AlertWindow.axaml
│ ├── MainWindow.axaml
│ ├── InitializationWindow.axaml
│ ├── SettingsWindow.axaml.cs
│ └── MainWindow.axaml.cs
├── Utils
│ ├── AssemblyAttributeUtil.cs
│ ├── WineHelper.cs
│ ├── ReflectionUtil.cs
│ ├── ProcessUtils.cs
│ └── ConsoleManager.cs
├── AppBuilderExtensions.cs
├── Program.cs
├── app.manifest
├── App.axaml
├── unlockfps_gui.csproj
├── WindowChromeExtensions.cs
├── App.axaml.cs
└── Styles
│ └── TabStyles.axaml
├── unlockfps_nc
├── Resources
│ ├── icon.ico
│ └── app.manifest
├── Model
│ └── Config.cs
├── AboutForm.cs
├── unlockfps_nc.csproj
├── Program.cs
├── Service
│ └── ConfigService.cs
├── MainForm.cs
├── AboutForm.Designer.cs
├── Utility
│ └── ProcessUtils.cs
├── SetupForm.Designer.cs
├── AboutForm.resx
├── SettingsForm.cs
├── SetupForm.resx
├── SettingsForm.resx
├── SetupForm.cs
└── MainForm.Designer.cs
├── unlockfps
├── FodyWeavers.xml
├── Properties
│ └── launchSettings.json
├── GameConstants.cs
├── Logging
│ ├── ILoggerFactory.cs
│ ├── LogLevel.cs
│ ├── ILogger.cs
│ ├── LogManager.cs
│ └── ConsoleLogger.cs
├── AssemblyInfo.cs
├── NativeMethods.txt
├── Utils
│ ├── TaskUtils.cs
│ ├── WineHelper.cs
│ ├── NativeMethods.cs
│ ├── Win32Window.cs
│ └── ProcessUtils.cs
├── Config.cs
├── unlockfps.csproj
├── Services
│ ├── ConfigService.cs
│ └── ProcessService.cs
├── Program.cs
├── app.manifest
└── FpsPatterns.cs
├── .github
└── workflows
│ └── publish.yml
├── unlockfps_nc.sln
├── .gitattributes
├── README.md
└── .gitignore
/unlockfps_gui/Assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Milkitic/genshin-fps-unlock-universal/HEAD/unlockfps_gui/Assets/icon.ico
--------------------------------------------------------------------------------
/unlockfps_nc/Resources/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Milkitic/genshin-fps-unlock-universal/HEAD/unlockfps_nc/Resources/icon.ico
--------------------------------------------------------------------------------
/unlockfps_gui/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | [assembly: PropertyChanged.FilterType(@".*ViewModel")]
2 | [assembly: PropertyChanged.FilterType(@".*\.Config")]
--------------------------------------------------------------------------------
/unlockfps/FodyWeavers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/unlockfps/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "unlockfps": {
4 | "commandName": "Project",
5 | "commandLineArgs": "-m"
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/unlockfps_gui/FodyWeavers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/unlockfps/GameConstants.cs:
--------------------------------------------------------------------------------
1 | namespace UnlockFps;
2 |
3 | public static class GameConstants
4 | {
5 | public static string[] GameNames { get; } = ["YuanShen", "GenshinImpact"];
6 | }
--------------------------------------------------------------------------------
/unlockfps/Logging/ILoggerFactory.cs:
--------------------------------------------------------------------------------
1 | namespace UnlockFps.Logging;
2 |
3 | public interface ILoggerFactory
4 | {
5 | ILogger CreateLogger(string name);
6 | ILogger CreateLogger();
7 | }
--------------------------------------------------------------------------------
/unlockfps/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | [assembly: PropertyChanged.FilterType(@".*ViewModel")]
2 | [assembly: PropertyChanged.FilterType(@".*\.Config")]
3 | [assembly: PropertyChanged.FilterType(@".*\.LaunchOptions")]
--------------------------------------------------------------------------------
/unlockfps/Logging/LogLevel.cs:
--------------------------------------------------------------------------------
1 | namespace UnlockFps.Logging;
2 |
3 | public enum LogLevel
4 | {
5 | Trace = 0,
6 | Debug = 1,
7 | Information = 2,
8 | Warning = 3,
9 | Error = 4,
10 | Critical = 5,
11 | None = 6,
12 | }
--------------------------------------------------------------------------------
/unlockfps_gui/Converters/BooleanConverters.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Data.Converters;
2 |
3 | namespace UnlockFps.Gui.Converters;
4 |
5 | public static class BooleanConverters
6 | {
7 | public static readonly IValueConverter Not =
8 | new DelegateConverter((x, _, _, _) => !(bool)x, (x, _, _, _) => !(bool)x);
9 | }
--------------------------------------------------------------------------------
/unlockfps_gui/OSVersionExt/Win32API/IWin32API.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace OSVersionExt.Win32API
7 | {
8 | public interface IWin32API
9 | {
10 | NTSTATUS RtlGetVersion(ref OSVERSIONINFOEX versionInfo);
11 | int GetSystemMetrics(SystemMetric smIndex);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/unlockfps/NativeMethods.txt:
--------------------------------------------------------------------------------
1 | CreateProcess
2 | DispatchMessage
3 | GetClassName
4 | GetForegroundWindow
5 | GetMessage
6 | GetProcessImageFileName
7 | GetWindowText
8 | GetWindowThreadProcessId
9 | ResumeThread
10 | SetWinEventHook
11 | TranslateMessage
12 |
13 | EVENT_SYSTEM_FOREGROUND
14 | WINEVENT_OUTOFCONTEXT
15 | WINEVENT_SKIPOWNPROCESS
16 |
17 | OpenProcess
18 | QueryFullProcessImageName
19 |
20 | EnumWindows
--------------------------------------------------------------------------------
/unlockfps_gui/OSVersionExt/VersionInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 |
7 | namespace OSVersionExt
8 | {
9 | public class VersionInfo
10 | {
11 | public Version Version { get; private set; }
12 |
13 | public VersionInfo(int major, int minor, int build)
14 | {
15 | this.Version = new Version(major, minor, build);
16 | }
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/unlockfps_gui/OSVersionExt/Environment/IEnvironment.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace OSVersionExt.Environment
7 | {
8 | public interface IEnvironment
9 | {
10 | ///
11 | /// Determines whether the current operating system is a 64-bit operating system.
12 | ///
13 | ///
14 | bool Is64BitOperatingSystem();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/unlockfps_nc/Resources/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/unlockfps_gui/OSVersionExt/MajorVersion10/RegistryProviderDefault.cs:
--------------------------------------------------------------------------------
1 | using OSVersionExt.Registry;
2 |
3 | namespace OSVersionExt.MajorVersion10
4 | {
5 | public class RegistryProviderDefault : IRegistry
6 | {
7 | public RegistryProviderDefault()
8 | {
9 | // NOP
10 | }
11 | public object GetValue(string keyName, string valueName, object defaultValue)
12 | {
13 | return Microsoft.Win32.Registry.GetValue(keyName, valueName, defaultValue);
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/unlockfps/Logging/ILogger.cs:
--------------------------------------------------------------------------------
1 | namespace UnlockFps.Logging;
2 |
3 | public interface ILogger
4 | {
5 | void Log(LogLevel logLevel, string message);
6 | void LogInformation(string message);
7 | void LogDebug(string message);
8 | void LogError(string message);
9 | void LogWarning(string message);
10 | void LogInformation(Exception exception, string message);
11 | void LogDebug(Exception exception, string message);
12 | void LogError(Exception exception, string message);
13 | void LogWarning(Exception exception, string message);
14 | }
15 |
16 | public interface ILogger : ILogger
17 | {
18 | }
--------------------------------------------------------------------------------
/unlockfps_gui/OSVersionExt/Environment/EnvironmentProvider.cs:
--------------------------------------------------------------------------------
1 | using OSVersionExt.Environment;
2 |
3 |
4 | namespace OSVersionExt
5 | {
6 | public class EnvironmentProvider : IEnvironment
7 | {
8 | public EnvironmentProvider()
9 | {
10 | // NOP
11 | }
12 |
13 | ///
14 | /// Determines whether the current operating system is a 64-bit operating system.
15 | ///
16 | /// true if the operating system is 64-bit; otherwise, false.
17 | public bool Is64BitOperatingSystem()
18 | {
19 | return System.Environment.Is64BitOperatingSystem;
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/unlockfps_gui/Converters/FullPath2NameConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using Avalonia.Data.Converters;
4 |
5 | namespace UnlockFps.Gui.Converters;
6 |
7 | internal sealed class FullPath2NameConverter : IValueConverter
8 | {
9 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
10 | {
11 | if (value is string s)
12 | {
13 | return System.IO.Path.GetFileName(s);
14 | }
15 |
16 | return value?.ToString();
17 | }
18 |
19 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
20 | {
21 | throw new NotImplementedException();
22 | }
23 | }
--------------------------------------------------------------------------------
/unlockfps_gui/ViewModels/ViewModelBase.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Runtime.CompilerServices;
3 | using ReactiveUI;
4 |
5 | namespace UnlockFps.Gui.ViewModels;
6 |
7 | public class ViewModelBase : ReactiveObject
8 | {
9 | protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
10 | {
11 | this.RaisePropertyChanged(propertyName);
12 | }
13 |
14 | protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null)
15 | {
16 | if (EqualityComparer.Default.Equals(field, value)) return false;
17 | field = value;
18 | OnPropertyChanged(propertyName);
19 | return true;
20 | }
21 | }
--------------------------------------------------------------------------------
/unlockfps_gui/Views/AboutWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using Avalonia.Controls;
3 | using Avalonia.Input;
4 | using UnlockFps.Gui.Utils;
5 |
6 | namespace UnlockFps.Gui.Views;
7 |
8 | public partial class AboutWindow : Window
9 | {
10 | public AboutWindow()
11 | {
12 | this.SetSystemChrome();
13 | InitializeComponent();
14 | Run_Version.Text = "v" + ReflectionUtil.GetInformationalVersion();
15 | }
16 |
17 | private void HyperLink_OnTapped(object? sender, TappedEventArgs e)
18 | {
19 | if (sender is TextBlock { Text: { } text })
20 | {
21 | Process.Start(new ProcessStartInfo(text) { UseShellExecute = true });
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/unlockfps_gui/Converters/Enum2ListConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Linq;
4 | using Avalonia.Data.Converters;
5 |
6 | namespace UnlockFps.Gui.Converters;
7 |
8 | internal sealed class Enum2ListConverter : IValueConverter
9 | {
10 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
11 | {
12 | if (parameter is Type t && t.IsSubclassOf(typeof(Enum)))
13 | return GetTypeList(t);
14 | if (value is Enum)
15 | return GetTypeList(value.GetType());
16 | return value;
17 | }
18 |
19 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
20 | {
21 | throw new NotImplementedException();
22 | }
23 |
24 | private static object GetTypeList(Type t)
25 | {
26 | var list = Enum.GetValues(t).Cast().ToList();
27 | return list;
28 | }
29 | }
--------------------------------------------------------------------------------
/unlockfps_gui/OSVersionExt/Registry/IRegistry.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace OSVersionExt.Registry
7 | {
8 | public interface IRegistry
9 | {
10 | ///
11 | /// Retrieves the value associated with the specified name, in the specified registry key.
12 | /// If the name is not found in the specified key, returns a default value that you provide, or null if the specified key does not exist.
13 | ///
14 | /// The full registry path of the key, beginning with a valid registry root, such as "HKEY_CURRENT_USER".
15 | /// The name of the name/value pair.
16 | /// The value to return if valueName does not exist.
17 | ///
18 | object GetValue(string keyName, string valueName, object defaultValue);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/unlockfps/Utils/TaskUtils.cs:
--------------------------------------------------------------------------------
1 | namespace UnlockFps.Utils;
2 |
3 | public static class TaskUtils
4 | {
5 | public static bool TaskSleep(double milliseconds, CancellationTokenSource cts)
6 | {
7 | return TaskSleep(TimeSpan.FromMilliseconds(milliseconds), cts);
8 | }
9 |
10 | public static bool TaskSleep(double milliseconds, CancellationToken token)
11 | {
12 | return TaskSleep(TimeSpan.FromMilliseconds(milliseconds), token);
13 | }
14 |
15 | public static bool TaskSleep(TimeSpan delay, CancellationTokenSource cts)
16 | {
17 | return TaskSleep(delay, cts.Token);
18 | }
19 |
20 | public static bool TaskSleep(TimeSpan delay, in CancellationToken token)
21 | {
22 | try
23 | {
24 | Task.Delay(delay).Wait(token);
25 | }
26 | catch (TaskCanceledException)
27 | {
28 | return false;
29 | }
30 | catch (OperationCanceledException)
31 | {
32 | return false;
33 | }
34 |
35 | return true;
36 | }
37 | }
--------------------------------------------------------------------------------
/unlockfps_gui/Views/AlertWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Interactivity;
4 |
5 | namespace UnlockFps.Gui.Views;
6 |
7 | public partial class AlertWindow : Window
8 | {
9 | public static readonly StyledProperty IsErrorProperty =
10 | AvaloniaProperty.Register(nameof(IsError), true);
11 |
12 | public bool IsError
13 | {
14 | get => GetValue(IsErrorProperty);
15 | set => SetValue(IsErrorProperty, value);
16 | }
17 |
18 | public static readonly StyledProperty TextProperty =
19 | AvaloniaProperty.Register(nameof(Text), "Info");
20 |
21 | public string Text
22 | {
23 | get => GetValue(TextProperty);
24 | set => SetValue(TextProperty, value);
25 | }
26 |
27 | public AlertWindow()
28 | {
29 | this.SetSystemChrome();
30 | DataContext = this;
31 | InitializeComponent();
32 | }
33 |
34 | private void Button_OnClick(object? sender, RoutedEventArgs e)
35 | {
36 | Close();
37 | }
38 | }
--------------------------------------------------------------------------------
/unlockfps_gui/Utils/AssemblyAttributeUtil.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Reflection;
4 | using System.Runtime.Loader;
5 |
6 | namespace UnlockFps.Gui.Utils;
7 |
8 | internal static class AssemblyAttributeUtil
9 | {
10 | public static T? GetAssemblyAttribute(out T? coreAttribute) where T : Attribute
11 | {
12 | var versionAsm = Assembly.GetCallingAssembly();
13 | var entryAsm = Assembly.GetEntryAssembly();
14 | var desiredEntryAsmName = versionAsm.GetName().Name?.Split('.')[0];
15 | var desiredEntryAsm =
16 | AssemblyLoadContext.Default.Assemblies.FirstOrDefault(k => k.GetName().Name == desiredEntryAsmName);
17 | if (desiredEntryAsm != null)
18 | {
19 | versionAsm = desiredEntryAsm;
20 | }
21 |
22 | if (entryAsm == versionAsm)
23 | {
24 | coreAttribute = null;
25 | return versionAsm.GetCustomAttribute();
26 | }
27 | else
28 | {
29 | coreAttribute = versionAsm.GetCustomAttribute();
30 | return entryAsm?.GetCustomAttribute();
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/unlockfps_nc/Model/Config.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace unlockfps_nc.Model
8 | {
9 | public class Config
10 | {
11 | public string GamePath { get; set; } = "";
12 |
13 | public bool AutoStart { get; set; }
14 | public bool AutoClose { get; set; }
15 | public bool PopupWindow { get; set; }
16 | public bool Fullscreen { get; set; } = true;
17 | public bool UseCustomRes { get; set; }
18 | public bool IsExclusiveFullscreen { get; set; }
19 | public bool StartMinimized { get; set; }
20 | public bool UsePowerSave { get; set; }
21 | public bool SuspendLoad { get; set; }
22 | public bool UseMobileUI { get; set; }
23 |
24 | public int FPSTarget { get; set; } = 120;
25 | public int CustomResX { get; set; } = 1920;
26 | public int CustomResY { get; set; } = 1080;
27 | public int MonitorNum { get; set; } = 1;
28 | public int Priority { get; set; } = 3;
29 |
30 | public List DllList { get; set; } = new();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/unlockfps_nc/AboutForm.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Data;
5 | using System.Diagnostics;
6 | using System.Drawing;
7 | using System.Linq;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 | using System.Windows.Forms;
11 |
12 | namespace unlockfps_nc
13 | {
14 | public partial class AboutForm : Form
15 | {
16 | public AboutForm()
17 | {
18 | InitializeComponent();
19 | }
20 |
21 | private void LinkLabelSource_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
22 | {
23 | OpenLink("https://github.com/34736384/genshin-fps-unlock");
24 | }
25 |
26 | private void LinkLabelIssues_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
27 | {
28 | OpenLink("https://github.com/34736384/genshin-fps-unlock/issues");
29 | }
30 |
31 | private void OpenLink(string url)
32 | {
33 | var psi = new ProcessStartInfo
34 | {
35 | FileName = url,
36 | UseShellExecute = true
37 | };
38 |
39 | Process.Start(psi);
40 | }
41 |
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/unlockfps_gui/Converters/HasItemsConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Globalization;
4 | using System.Linq;
5 | using Avalonia;
6 | using Avalonia.Data.Converters;
7 |
8 | namespace UnlockFps.Gui.Converters;
9 |
10 | internal sealed class HasItemsConverter : IValueConverter
11 | {
12 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
13 | {
14 | if (value is IEnumerable enumerable)
15 | {
16 | var enumerator = enumerable.GetEnumerator();
17 | var moveNext = enumerator.MoveNext();
18 | if (enumerator is IDisposable disposable)
19 | {
20 | disposable.Dispose();
21 | }
22 |
23 | return moveNext;
24 | }
25 |
26 | if (value is int i)
27 | {
28 | return i > 0;
29 | }
30 |
31 | return AvaloniaProperty.UnsetValue;
32 | }
33 |
34 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
35 | {
36 | throw new NotImplementedException();
37 | }
38 |
39 | private static object GetTypeList(Type t)
40 | {
41 | var list = Enum.GetValues(t).Cast().ToList();
42 | return list;
43 | }
44 | }
--------------------------------------------------------------------------------
/unlockfps_gui/Utils/WineHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.Runtime.InteropServices;
4 |
5 | namespace UnlockFps.Gui.Utils;
6 |
7 | internal static partial class WineHelper
8 | {
9 | public static bool DetectWine(
10 | [NotNullWhen(true)] out string? version,
11 | [NotNullWhen(true)] out string? buildId)
12 | {
13 | try
14 | {
15 | version = GetVersion();
16 | Console.WriteLine("Wine version: " + version);
17 | buildId = GetBuildId();
18 | Console.WriteLine("Wine build id: " + buildId);
19 | return true;
20 | }
21 | catch (EntryPointNotFoundException)
22 | {
23 | version = null;
24 | buildId = null;
25 | return false;
26 | }
27 | catch (DllNotFoundException)
28 | {
29 | version = null;
30 | buildId = null;
31 | return false;
32 | }
33 | }
34 |
35 | [LibraryImport("ntdll", EntryPoint = "wine_get_version", StringMarshalling = StringMarshalling.Utf8)]
36 | private static partial string GetVersion();
37 |
38 | [LibraryImport("ntdll", EntryPoint = "wine_get_build_id", StringMarshalling = StringMarshalling.Utf8)]
39 | private static partial string GetBuildId();
40 | }
--------------------------------------------------------------------------------
/unlockfps/Config.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.ObjectModel;
2 | using System.ComponentModel;
3 |
4 | namespace UnlockFps;
5 |
6 | public partial class LaunchOptions : INotifyPropertyChanged
7 | {
8 | public string? GamePath { get; set; }
9 |
10 | public bool IsWindowBorderless { get; set; }
11 | public bool Fullscreen { get; set; } = true;
12 | public bool IsExclusiveFullscreen { get; set; }
13 | public bool UseCustomResolution { get; set; }
14 | public int CustomResolutionX { get; set; } = 1920;
15 | public int CustomResolutionY { get; set; } = 1080;
16 | public bool UseMobileUI { get; set; }
17 |
18 | public int MonitorId { get; set; } = 1;
19 |
20 | public bool SuspendLoad { get; set; }
21 | public ObservableCollection DllList { get; set; } = new();
22 | }
23 |
24 | public partial class Config : INotifyPropertyChanged
25 | {
26 | public LaunchOptions LaunchOptions { get; set; } = new();
27 |
28 | public bool AutoLaunch { get; set; }
29 | public bool AutoClose { get; set; }
30 | public bool UsePowerSave { get; set; }
31 | public int FpsTarget { get; set; } = 120;
32 | public int FpsPowerSave { get; set; } = 10;
33 | public int ProcessPriority { get; set; } = 3;
34 | public bool ShowDebugConsole { get; set; }
35 | public bool WindowQueryUseEvent { get; set; } = true;
36 | }
--------------------------------------------------------------------------------
/unlockfps_gui/AppBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Avalonia;
3 | using Avalonia.Media;
4 | using SkiaSharp;
5 |
6 | namespace UnlockFps.Gui;
7 |
8 | public static class AppBuilderExtensions
9 | {
10 | public static AppBuilder WithNativeFonts(this AppBuilder appBuilder, string? specificFontFamily = null)
11 | {
12 | string? familyName = null;
13 |
14 | if (specificFontFamily != null)
15 | {
16 | var family = SKFontManager.Default.MatchFamily(specificFontFamily);
17 | familyName = family?.FamilyName;
18 | }
19 |
20 | FontManagerOptions options = new();
21 |
22 | if (familyName == null)
23 | {
24 | familyName = SKFontManager.Default.MatchCharacter('a')?.FamilyName;
25 | if (familyName == null)
26 | {
27 | Console.Error.WriteLine("Cannot find default font.");
28 | }
29 | }
30 |
31 | if (familyName != null)
32 | {
33 | options.DefaultFamilyName = familyName;
34 |
35 | var fontFallbacks = new FontFallback[]
36 | {
37 | new() { FontFamily = FontFamily.Parse(familyName) },
38 | };
39 | options.FontFallbacks = fontFallbacks;
40 | }
41 |
42 | return appBuilder.With(options);
43 | }
44 | }
--------------------------------------------------------------------------------
/unlockfps_nc/unlockfps_nc.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | net8.0-windows
6 | enable
7 | true
8 | enable
9 | Resources\icon.ico
10 | Resources\app.manifest
11 | x64
12 | False
13 | true
14 | 7.0
15 |
16 |
17 |
18 | full
19 |
20 |
21 |
22 | full
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/unlockfps_gui/Converters/DelegateConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using Avalonia.Data.Converters;
4 |
5 | namespace UnlockFps.Gui.Converters;
6 |
7 | public class DelegateConverter : IValueConverter
8 | {
9 | public delegate object? ConvertDelegate(object? value, Type targetType, object? parameter, CultureInfo culture);
10 |
11 | public delegate object? ConvertBackDelegate(object? value, Type targetType, object? parameter,
12 | CultureInfo culture);
13 |
14 | private readonly ConvertDelegate _convert;
15 | private readonly ConvertBackDelegate? _convertBack;
16 |
17 | public DelegateConverter(ConvertDelegate convert, ConvertBackDelegate? convertBack = null)
18 | {
19 | _convert = convert ?? throw new ArgumentNullException(nameof(convert));
20 | _convertBack = convertBack;
21 | }
22 |
23 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
24 | {
25 | return _convert(value, targetType, parameter, culture);
26 | }
27 |
28 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
29 | {
30 | if (_convertBack == null)
31 | throw new NotImplementedException($"ConvertBack() of {GetType().Name} is not implemented.");
32 | return _convertBack(value, targetType, parameter, culture);
33 | }
34 | }
--------------------------------------------------------------------------------
/unlockfps_nc/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using System.Runtime.InteropServices;
3 | using unlockfps_nc.Service;
4 | using unlockfps_nc.Utility;
5 |
6 | namespace unlockfps_nc
7 | {
8 | internal static class Program
9 | {
10 | private static IntPtr MutexHandle = IntPtr.Zero;
11 | public static IServiceProvider ServiceProvider { get; private set; }
12 |
13 | [STAThread]
14 | static void Main()
15 | {
16 | MutexHandle = Native.CreateMutex(IntPtr.Zero, true, @"GenshinFPSUnlocker");
17 | if (Marshal.GetLastWin32Error() == 183)
18 | {
19 | MessageBox.Show(@"Another unlocker is already running.", @"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
20 | return;
21 | }
22 |
23 | var services = new ServiceCollection();
24 | services.AddTransient();
25 | services.AddTransient();
26 | services.AddTransient();
27 | services.AddSingleton();
28 | services.AddSingleton();
29 |
30 | ServiceProvider = services.BuildServiceProvider();
31 |
32 | ApplicationConfiguration.Initialize();
33 | Application.Run(ServiceProvider.GetRequiredService());
34 | }
35 |
36 |
37 | }
38 | }
--------------------------------------------------------------------------------
/unlockfps_gui/OSVersionExt/Win32API/Win32ApiProvider.cs:
--------------------------------------------------------------------------------
1 | using OSVersionExt.Win32API;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Runtime.InteropServices;
6 | using System.Security;
7 | using System.Text;
8 |
9 | namespace OSVersionExt
10 | {
11 | ///
12 | /// Win32 API Provider
13 | ///
14 | /// CLR wrapper https://github.com/microsoft/referencesource/blob/master/mscorlib/microsoft/win32/win32native.cs
15 | public class Win32ApiProvider : IWin32API
16 | {
17 | private const String NTDLL = "ntdll.dll";
18 | private const String USER32 = "user32.dll";
19 |
20 | [SecurityCritical]
21 | [DllImport(NTDLL, EntryPoint = "RtlGetVersion", SetLastError = true, CharSet = CharSet.Unicode)]
22 | internal static extern NTSTATUS ntdll_RtlGetVersion(ref OSVERSIONINFOEX versionInfo);
23 |
24 | [DllImport(USER32, EntryPoint = "GetSystemMetrics")]
25 | internal static extern int ntdll_GetSystemMetrics(SystemMetric smIndex);
26 |
27 |
28 | public NTSTATUS RtlGetVersion(ref OSVERSIONINFOEX versionInfo)
29 | {
30 | return ntdll_RtlGetVersion(ref versionInfo);
31 | }
32 |
33 | public int GetSystemMetrics(SystemMetric smIndex)
34 | {
35 | return ntdll_GetSystemMetrics(smIndex);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/unlockfps/Utils/WineHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Runtime.InteropServices;
3 | using UnlockFps.Logging;
4 |
5 | namespace UnlockFps.Utils;
6 |
7 | internal static partial class WineHelper
8 | {
9 | private static readonly ILogger Logger = LogManager.GetLogger(nameof(WineHelper));
10 |
11 | public static bool DetectWine(
12 | [NotNullWhen(true)] out string? version,
13 | [NotNullWhen(true)] out string? buildId)
14 | {
15 | try
16 | {
17 | version = GetVersion();
18 | Logger.LogInformation($"Wine version: {version}");
19 | buildId = GetBuildId();
20 | Logger.LogInformation($"Wine build id: {buildId}");
21 | return true;
22 | }
23 | catch (EntryPointNotFoundException)
24 | {
25 | version = null;
26 | buildId = null;
27 | return false;
28 | }
29 | catch (DllNotFoundException)
30 | {
31 | version = null;
32 | buildId = null;
33 | return false;
34 | }
35 | }
36 |
37 | [LibraryImport("ntdll", EntryPoint = "wine_get_version", StringMarshalling = StringMarshalling.Utf8)]
38 | private static partial string GetVersion();
39 |
40 | [LibraryImport("ntdll", EntryPoint = "wine_get_build_id", StringMarshalling = StringMarshalling.Utf8)]
41 | private static partial string GetBuildId();
42 | }
--------------------------------------------------------------------------------
/unlockfps_nc/Service/ConfigService.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 | using unlockfps_nc.Model;
8 |
9 | namespace unlockfps_nc.Service
10 | {
11 | public class ConfigService
12 | {
13 | private const string ConfigName = "fps_config.json";
14 |
15 | public Config Config { get; private set; } = new();
16 |
17 | public ConfigService()
18 | {
19 | Load();
20 | Sanitize();
21 | }
22 |
23 | private void Load()
24 | {
25 | if (!File.Exists(ConfigName))
26 | return;
27 |
28 | var json = File.ReadAllText(ConfigName);
29 | Config = JsonConvert.DeserializeObject(json);
30 | }
31 |
32 | private void Sanitize()
33 | {
34 | Config.FPSTarget = Math.Clamp(Config.FPSTarget, 1, 420);
35 | Config.Priority = Math.Clamp(Config.Priority, 0, 5);
36 | Config.CustomResX = Math.Clamp(Config.CustomResX, 200, 7680);
37 | Config.CustomResY = Math.Clamp(Config.CustomResY, 200, 4320);
38 | Config.MonitorNum = Math.Clamp(Config.MonitorNum, 1, 100);
39 | }
40 |
41 | public void Save()
42 | {
43 | var json = JsonConvert.SerializeObject(Config, Formatting.Indented);
44 | File.WriteAllText(ConfigName, json);
45 | }
46 |
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/unlockfps/Utils/NativeMethods.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace UnlockFps.Utils;
4 |
5 | internal static partial class NativeMethods
6 | {
7 | public delegate void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
8 |
9 | [LibraryImport("user32.dll")]
10 | public static partial nint GetForegroundWindow();
11 |
12 | [LibraryImport("user32.dll")]
13 | public static partial uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
14 |
15 | [LibraryImport("kernel32.dll", SetLastError = true)]
16 | [return: MarshalAs(UnmanagedType.Bool)]
17 | public static partial bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, Span lpBuffer, int nSize, out int lpNumberOfBytesWritten);
18 |
19 | [LibraryImport("kernel32.dll", SetLastError = true)]
20 | [return: MarshalAs(UnmanagedType.Bool)]
21 | public static partial bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, Span lpBuffer, int nSize, out int lpNumberOfBytesRead);
22 |
23 | [LibraryImport("kernel32.dll", EntryPoint = "LoadLibraryExW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
24 | public static partial nint LoadLibraryEx(string lpLibFileName, IntPtr hFile, uint dwFlags);
25 |
26 | [LibraryImport("user32.dll", SetLastError = true)]
27 | public static partial IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc,
28 | WinEventProc lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
29 |
30 | }
--------------------------------------------------------------------------------
/unlockfps_gui/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using Avalonia;
4 | using Avalonia.ReactiveUI;
5 | using UnlockFps.Gui.Utils;
6 |
7 | namespace UnlockFps.Gui;
8 |
9 | internal sealed class Program
10 | {
11 | // Initialization code. Don't use any Avalonia, third-party APIs or any
12 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
13 | // yet and stuff might break.
14 | [STAThread]
15 | public static void Main(string[] args)
16 | {
17 | using (new Mutex(true, @"GenshinFPSUnlocker", out var createdNew))
18 | {
19 | DuplicatedInstance = !createdNew;
20 | BuildAvaloniaApp()
21 | .StartWithClassicDesktopLifetime(args);
22 | }
23 | }
24 |
25 | public static bool DuplicatedInstance { get; private set; }
26 |
27 | // Avalonia configuration, don't remove; also used by visual designer.
28 | public static AppBuilder BuildAvaloniaApp()
29 | {
30 | var appBuilder = AppBuilder.Configure()
31 | .UsePlatformDetect()
32 | .WithNativeFonts()
33 | .LogToTrace()
34 | .UseReactiveUI();
35 | if (WineHelper.DetectWine(out _, out _))
36 | {
37 | return appBuilder
38 | .With(new Win32PlatformOptions
39 | {
40 | CompositionMode = [Win32CompositionMode.RedirectionSurface],
41 | RenderingMode = [Win32RenderingMode.Software],
42 | OverlayPopups = true
43 | });
44 | }
45 | else
46 | {
47 | return appBuilder;
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/unlockfps/unlockfps.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | enable
7 | enable
8 | true
9 | app.manifest
10 | UnlockFps
11 | unlockfps_cli
12 |
13 |
14 |
15 | true
16 |
17 | true
18 | true
19 | true
20 |
21 |
22 |
23 |
24 |
25 | all
26 |
27 |
28 |
29 | all
30 | compile; runtime; build; native; contentfiles; analyzers; buildtransitive
31 |
32 |
33 | all
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/unlockfps/Logging/LogManager.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 |
3 | namespace UnlockFps.Logging;
4 |
5 | public static class LogManager
6 | {
7 | private static ILoggerFactory? _loggerFactory;
8 | public static void SetLoggerFactory(ILoggerFactory loggerFactory) => _loggerFactory = loggerFactory;
9 | public static ILogger GetLogger(string name) => _loggerFactory?.CreateLogger(name) ?? new ConsoleLogger(name);
10 | //public static void Info(string message)
11 | //{
12 | // var name = GetClassName() ?? nameof(Logger);
13 | // if (_loggerFactory is null) Console.WriteLine(message);
14 | // else _loggerFactory.CreateLogger(name).LogInformation(message);
15 | //}
16 |
17 | //public static void Debug(string message)
18 | //{
19 | // var name = GetClassName() ?? nameof(Logger);
20 | // if (_loggerFactory is null) Console.WriteLine(message);
21 | // else _loggerFactory.CreateLogger(name).LogDebug(message);
22 | //}
23 |
24 | //public static void Error(string message)
25 | //{
26 | // var name = GetClassName() ?? nameof(Logger);
27 | // if (_loggerFactory is null) Console.WriteLine(message);
28 | // else _loggerFactory.CreateLogger(name).LogError(message);
29 | //}
30 |
31 | //public static void Warn(string message)
32 | //{
33 | // var name = GetClassName() ?? nameof(Logger);
34 | // if (_loggerFactory is null) Console.WriteLine(message);
35 | // else _loggerFactory.CreateLogger(name).LogWarning(message);
36 | //}
37 |
38 | //private static string? GetClassName()
39 | //{
40 | // var methodInfo = new StackTrace().GetFrame(2)?.GetMethod();
41 | // return methodInfo?.ReflectedType?.Name;
42 | //}
43 | }
--------------------------------------------------------------------------------
/unlockfps_gui/Views/AboutWindow.axaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
36 |
37 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 | env:
3 | ProjectName: genshin-fps-unlock
4 | on:
5 | create:
6 | tags:
7 | - "v*.*.*"
8 |
9 | jobs:
10 | build:
11 | name: Build and Release
12 | if: ${{ StartsWith(github.ref, 'refs/tags/') }}
13 | runs-on: windows-latest
14 |
15 | steps:
16 | - name: Checkout code
17 | uses: actions/checkout@v2
18 | with:
19 | fetch-depth: '0'
20 | submodules: 'recursive'
21 |
22 | - name: Setup .NET
23 | uses: actions/setup-dotnet@v1
24 | with:
25 | dotnet-version: 8.0.x
26 |
27 | - name: Publish
28 | if: ${{ success() }}
29 | shell: pwsh
30 | run: |
31 | echo ${{ github.ref }}
32 | dotnet publish unlockfps_gui --runtime win-x64 --configuration Release --output ci-publish-win64
33 | rm ./ci-publish-win64/*.pdb
34 | rm ./ci-publish-win64/unlockfps_cli.*
35 |
36 | - name: Get tag
37 | uses: dawidd6/action-get-tag@v1
38 | if: ${{ success() && startsWith(github.ref, 'refs/tags/') }}
39 | id: tag
40 |
41 | - name: Pack via 7z
42 | if: ${{ success() && startsWith(github.ref, 'refs/tags/') }}
43 | run: |
44 | mkdir -p ./ci-pack/
45 | 7z a -mx9 -mfb=273 -ms -md=31 -myx=9 -mtm=- -mmt -mmtf -md=1536m -mmf=bt3 -mmc=10000 -mpb=0 -mlc=0 "./ci-pack/${{ env.ProjectName }}-${{ steps.tag.outputs.tag }}-win64.7z" "./ci-publish-win64/*" -x!"${{ env.ProjectName }}" -r
46 |
47 | - name: Create a new GitHub release if a new tag is pushed
48 | uses: softprops/action-gh-release@v1
49 | if: ${{ success() && startsWith(github.ref, 'refs/tags/') }}
50 | env:
51 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
52 | with:
53 | name: ${{ steps.tag.outputs.tag }}
54 | prerelease: true
55 | draft: false
56 | files: |
57 | ./ci-pack/${{ env.ProjectName }}-${{ steps.tag.outputs.tag }}-win64.7z
--------------------------------------------------------------------------------
/unlockfps_gui/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | true/PM
30 | PerMonitorV2, PerMonitor
31 |
32 |
33 |
34 |
35 |
36 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/unlockfps_nc.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.8.34330.188
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "unlockfps_nc", "unlockfps_nc\unlockfps_nc.csproj", "{AB706C74-000D-4A43-909C-D0EE906003B8}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "unlockfps_gui", "unlockfps_gui\unlockfps_gui.csproj", "{2E8443BA-176B-4586-A393-7B6A0EB07E40}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "unlockfps", "unlockfps\unlockfps.csproj", "{3487E54A-20D4-496E-8764-1BE6AB4CCD61}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Release|Any CPU = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {AB706C74-000D-4A43-909C-D0EE906003B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {AB706C74-000D-4A43-909C-D0EE906003B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {AB706C74-000D-4A43-909C-D0EE906003B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {AB706C74-000D-4A43-909C-D0EE906003B8}.Release|Any CPU.Build.0 = Release|Any CPU
22 | {2E8443BA-176B-4586-A393-7B6A0EB07E40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {2E8443BA-176B-4586-A393-7B6A0EB07E40}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {2E8443BA-176B-4586-A393-7B6A0EB07E40}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {2E8443BA-176B-4586-A393-7B6A0EB07E40}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {3487E54A-20D4-496E-8764-1BE6AB4CCD61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {3487E54A-20D4-496E-8764-1BE6AB4CCD61}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {3487E54A-20D4-496E-8764-1BE6AB4CCD61}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {3487E54A-20D4-496E-8764-1BE6AB4CCD61}.Release|Any CPU.Build.0 = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(SolutionProperties) = preSolution
32 | HideSolutionNode = FALSE
33 | EndGlobalSection
34 | GlobalSection(ExtensibilityGlobals) = postSolution
35 | SolutionGuid = {BA9973A2-2E7A-4D69-A824-D9A1B1CCF56E}
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/unlockfps_gui/Views/AlertWindow.axaml:
--------------------------------------------------------------------------------
1 |
15 |
19 |
20 |
21 |
26 |
32 |
33 |
38 |
39 |
40 |
44 |
45 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/unlockfps_gui/App.axaml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
22 |
25 |
26 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/unlockfps/Services/ConfigService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.ObjectModel;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace UnlockFps.Services;
6 |
7 | public class ConfigService
8 | {
9 | private const string ConfigName = "fps_config.json";
10 |
11 | public Config Config { get; private set; } = new();
12 |
13 | public ConfigService()
14 | {
15 | Load();
16 | StandardizeValues();
17 | }
18 |
19 | private void Load()
20 | {
21 | if (!File.Exists(ConfigName))
22 | return;
23 |
24 | var json = File.ReadAllText(ConfigName);
25 | Config = JsonSerializer.Deserialize(json, ConfigJsonContext.Default.Config)!;
26 | }
27 |
28 | private void StandardizeValues()
29 | {
30 | if (Config.LaunchOptions == null!)
31 | {
32 | Config.LaunchOptions = new LaunchOptions();
33 | }
34 |
35 | if (!string.IsNullOrWhiteSpace(Config.LaunchOptions.GamePath))
36 | {
37 | Config.LaunchOptions.GamePath = File.Exists(Config.LaunchOptions.GamePath)
38 | ? Path.GetFullPath(Config.LaunchOptions.GamePath)
39 | : null;
40 | }
41 |
42 | Config.FpsTarget = Math.Clamp(Config.FpsTarget, 1, 420);
43 | Config.ProcessPriority = Math.Clamp(Config.ProcessPriority, 0, 5);
44 | Config.LaunchOptions.CustomResolutionX = Math.Clamp(Config.LaunchOptions.CustomResolutionX, 200, 7680);
45 | Config.LaunchOptions.CustomResolutionY = Math.Clamp(Config.LaunchOptions.CustomResolutionY, 200, 4320);
46 | Config.LaunchOptions.MonitorId = Math.Clamp(Config.LaunchOptions.MonitorId, 1, 100);
47 | Config.FpsPowerSave = Math.Clamp(Config.FpsPowerSave, 1, 30);
48 |
49 | if (Config.LaunchOptions.DllList == null!)
50 | {
51 | Config.LaunchOptions.DllList = new ObservableCollection();
52 | }
53 | else
54 | {
55 | Config.LaunchOptions.DllList = new ObservableCollection(
56 | Config.LaunchOptions.DllList
57 | .Where(k => !string.IsNullOrWhiteSpace(k) && File.Exists(k))
58 | .Select(Path.GetFullPath)
59 | );
60 | }
61 | }
62 |
63 | public void Save()
64 | {
65 | var json = JsonSerializer.Serialize(Config, ConfigJsonContext.Default.Config);
66 | File.WriteAllText(ConfigName, json);
67 | }
68 | }
69 |
70 | [JsonSourceGenerationOptions(WriteIndented = true, ReadCommentHandling = JsonCommentHandling.Skip)]
71 | [JsonSerializable(typeof(Config))]
72 | internal partial class ConfigJsonContext : JsonSerializerContext;
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/unlockfps_gui/Utils/ReflectionUtil.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using Semver;
3 |
4 | namespace UnlockFps.Gui.Utils;
5 |
6 | internal static class ReflectionUtil
7 | {
8 | private static string? _version;
9 | private static string? _company;
10 |
11 | public static string GetInformationalVersion()
12 | {
13 | if (_version != null) return _version;
14 |
15 | //var runner = AssemblyAttributeUtil.GetAssemblyAttribute(out var core);
16 | var runner = typeof(ReflectionUtil).Assembly.GetCustomAttribute();
17 | var core = default(AssemblyInformationalVersionAttribute);
18 | var runnerVersion = runner!.InformationalVersion;
19 | FixCommit(ref runnerVersion);
20 |
21 | if (core == null)
22 | {
23 | return _version ??= runnerVersion;
24 | }
25 |
26 | var coreVersion = core.InformationalVersion;
27 | FixCommit(ref coreVersion);
28 | if (coreVersion == runnerVersion)
29 | {
30 | return _version ??= runnerVersion;
31 | }
32 |
33 | return _version ??= $"{runnerVersion} (core: {coreVersion})";
34 | }
35 |
36 | public static string GetCompany()
37 | {
38 | if (_company != null) return _company;
39 |
40 | var runner = AssemblyAttributeUtil.GetAssemblyAttribute(out var core);
41 | var runnerCompany = runner!.Company;
42 | if (core == null)
43 | {
44 | return _company ??= runnerCompany;
45 | }
46 |
47 | var coreCompany = core.Company;
48 | if (coreCompany == runnerCompany)
49 | {
50 | return _company ??= runnerCompany;
51 | }
52 |
53 | return _company ??= $"{runnerCompany} (core: {coreCompany})";
54 | }
55 |
56 | private static void FixCommit(ref string version)
57 | {
58 | if (!SemVersion.TryParse(version, SemVersionStyles.Strict, out var semVer)) return;
59 |
60 | if (!semVer.IsPrerelease)
61 | {
62 | var lastIndexOf = version.LastIndexOf('+');
63 |
64 | var lastIndexOfDot = version.LastIndexOf('.');
65 | var subStr = version.Substring(lastIndexOfDot + 1);
66 | if (subStr.Length == 40)
67 | {
68 | version = version.Substring(0, lastIndexOfDot);
69 | }
70 | else
71 | {
72 | version = version.Substring(0, lastIndexOf);
73 | }
74 | }
75 | else if (semVer.Metadata.Length > 7)
76 | {
77 | var lastIndexOf = version.LastIndexOf('+');
78 | version = version.Substring(0, lastIndexOf + 8);
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/unlockfps/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using CommandLine;
3 | using UnlockFps.Logging;
4 | using UnlockFps.Services;
5 |
6 | namespace UnlockFps;
7 |
8 | internal class Program
9 | {
10 | public class Options
11 | {
12 | [Option('m', "monitor-only", Default = false, Required = false)]
13 | public bool MonitorOnly { get; set; }
14 | }
15 |
16 | private static readonly ILogger Logger = LogManager.GetLogger(nameof(Program));
17 |
18 | [STAThread]
19 | [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Options))]
20 | static async Task Main(string[] args)
21 | {
22 | await Parser.Default.ParseArguments(args)
23 | .WithParsedAsync(async o =>
24 | {
25 | if (o.MonitorOnly)
26 | {
27 | CreateMonitorOnly();
28 | }
29 | else
30 | {
31 | await CreateProcessWithMonitor();
32 | }
33 | });
34 | }
35 |
36 | private static void CreateMonitorOnly()
37 | {
38 | var configService = new ConfigService();
39 | configService.Save();
40 |
41 | using var cts = new CancellationTokenSource();
42 | var gameInstanceService = new GameInstanceService(configService);
43 | Console.CancelKeyPress += (_, e) =>
44 | {
45 | Exit(gameInstanceService);
46 | };
47 | Logger.LogInformation("Monitor mode. Press 'Ctrl+C' to exit.");
48 | gameInstanceService.Start();
49 | while (Console.ReadLine() != "exit")
50 | {
51 |
52 | }
53 |
54 | Exit(gameInstanceService);
55 | }
56 |
57 | private static async ValueTask CreateProcessWithMonitor()
58 | {
59 | var configService = new ConfigService();
60 | configService.Save();
61 |
62 | using var cts = new CancellationTokenSource();
63 | var gameInstanceService = new GameInstanceService(configService);
64 | gameInstanceService.Start();
65 |
66 | var processService = new ProcessService(configService);
67 | processService.Start();
68 |
69 | gameInstanceService.ProcessExit += (p) =>
70 | {
71 | Exit(gameInstanceService);
72 | };
73 | Console.CancelKeyPress += (_, e) =>
74 | {
75 | processService.KillLastProcess();
76 | Exit(gameInstanceService);
77 | };
78 |
79 |
80 | while (Console.ReadLine() != "exit")
81 | {
82 |
83 | }
84 |
85 | processService.KillLastProcess();
86 | Exit(gameInstanceService);
87 | }
88 |
89 | private static void Exit(GameInstanceService gameInstanceService)
90 | {
91 | gameInstanceService.Stop();
92 | Environment.Exit(0);
93 | }
94 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Genshin Impact FPS Unlocker
2 |
3 | 注:本fork已长期停更,原因是作者(我)早已不玩原神,没有自用价值失去维护动力
4 | 如果需要使用,请使用上游项目https://github.com/34736384/genshin-fps-unlock
5 | 如果需要在linux上跑,相信愿意折腾的你也绝对会自己合并上游的内存地址相关的修改!
6 |
7 | A forked version which rewrites GUI and supports linux with WINE.
8 |
9 | 
10 |
11 | > Running in Windows 11 & Linux KDE
12 |
13 | - This tool helps you to unlock the 60 fps limit in the game
14 | - This is an external program which uses **WriteProcessMemory** to write the desired fps to the game
15 | - Handle protection bypass is already included
16 | - Does not require a driver for R/W access
17 | - Supports OS and CN version
18 | - Should work for future updates
19 | - If the source needs to be updated, I'll try to do it as soon as possible
20 | - You can download the compiled binary over at '[Release](https://github.com/34736384/genshin-fps-unlock/releases)' if you don't want to compile it yourself
21 |
22 | ## Compiling
23 | 1. Install Visual Studio 2022 with Desktop C++ workload in Visual Studio Installer.
24 | 2. Install .NET 8 SDK.
25 | 3. Use `dotnet build ./unlockfps_gui` for regular compiling. Use `dotnet publish ./unlockfps_gui -c Release -r win-x64` for AOT publish.
26 |
27 | ## Usage
28 |
29 | ### Running on Windows
30 |
31 | - Run the exe and click 'Launch'
32 | - If it is your first time running, unlocker will attempt to find your game through the registry. If it fails, then it will ask you to either browse or run the game.
33 | - Place the compiled exe anywhere you want (except for the game folder)
34 | - Make sure your game is closed—the unlocker will automatically start the game for you
35 | - Run the exe as administrator, and leave the exe running
36 | >It requires adminstrator because the game needs to be started by the unlocker and the game requires such permission
37 | - To load other third-party plugins, go to `Options->Settings->DLLs` and click add
38 |
39 | ### Running with wine
40 |
41 | #### Prerequisite
42 |
43 | ```
44 | WINEPREFIX=... winetricks dotnet45
45 | WINEPREFIX=... winecfg -v win7
46 | ```
47 | #### Running
48 |
49 | ```
50 | WINEPREFIX=... wine unlockfps.exe
51 | ```
52 | 
53 |
54 | ## Version 3.0.0 Changes
55 | - Rewritten the project in .NET 8
56 | - Added a launch option to use mobile UI (for streaming from mobile devices or touchscreen laptops)
57 | ## Notes
58 | - HoYoverse (miHoYo) is well aware of this tool, and you will not get banned for using **ONLY** fps unlock.
59 | - If you are using other third-party plugins, you are doing it at your own risk.
60 | - Any artifacts from unlocking fps (e.g. stuttering) is **NOT** a bug of the unlocker
61 |
62 |
--------------------------------------------------------------------------------
/unlockfps_gui/Views/MainWindow.axaml:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
35 |
36 |
37 |
38 |
39 |
48 |
49 |
53 |
54 |
55 |
61 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/unlockfps_gui/unlockfps_gui.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | net8.0
5 | enable
6 | true
7 | app.manifest
8 | true
9 | Assets\icon.ico
10 | win-x64
11 | UnlockFps.Gui
12 | unlockfps
13 | true
14 |
15 | 1.0
16 | v
17 |
18 | Milkitic
19 | Genshin FPS Unlocker
20 | Genshin FPS Unlocker
21 | Copyright © Milkitic $([System.DateTime]::Now.Year)
22 |
23 |
24 |
25 |
26 | true
27 |
28 | true
29 | true
30 | true
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | all
47 | runtime; build; native; contentfiles; analyzers; buildtransitive
48 |
49 |
50 | all
51 | compile; runtime; build; native; contentfiles; analyzers; buildtransitive
52 |
53 |
54 | all
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/unlockfps/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 |
62 |
63 |
64 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/unlockfps/Utils/Win32Window.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using System.Runtime.Versioning;
3 | using Windows.Win32;
4 | using Windows.Win32.Foundation;
5 | using Windows.Win32.System.Threading;
6 |
7 | namespace UnlockFps.Utils;
8 |
9 | [SupportedOSPlatform("windows5.0")]
10 | public class Win32Window
11 | {
12 | private readonly HWND _hWnd;
13 | private string? _className;
14 | private string? _title;
15 | private string? _processName;
16 | private uint _pid;
17 |
18 | public Win32Window(nint handle)
19 | {
20 | _hWnd = (HWND)handle;
21 | }
22 |
23 | public nint Handle => _hWnd;
24 |
25 | public string ClassName => _className ??= CallWin32ToGetPWSTR(512, (p, l) => PInvoke.GetClassName(_hWnd, p, l));
26 |
27 | public string Title => _title ??= CallWin32ToGetPWSTR(512, (p, l) => PInvoke.GetWindowText(_hWnd, p, l));
28 |
29 | public uint ProcessId => _pid is 0 ? (_pid = GetProcessIdCore()) : _pid;
30 |
31 | //public string ProcessName => _processName ??= Process.GetProcessById((int)ProcessId).ProcessName;
32 | public unsafe string ProcessName
33 | {
34 | get
35 | {
36 | if (_processName == null)
37 | {
38 | var hProcess =
39 | PInvoke.OpenProcess(
40 | PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_LIMITED_INFORMATION |
41 | PROCESS_ACCESS_RIGHTS.PROCESS_TERMINATE | PROCESS_ACCESS_RIGHTS.PROCESS_SYNCHRONIZE, false,
42 | ProcessId);
43 | try
44 | {
45 | uint bufferSize = 512;
46 | Span span = stackalloc char[(int)bufferSize];
47 |
48 | fixed (char* o = span)
49 | {
50 | if (!PInvoke.QueryFullProcessImageName(hProcess, 0, new PWSTR(o), &bufferSize))
51 | {
52 | return "";
53 | }
54 | }
55 |
56 | var path = new string(span.Slice(0, (int)bufferSize));
57 | var processName = Path.GetFileNameWithoutExtension(path);
58 | _processName = processName;
59 | }
60 | finally
61 | {
62 | PInvoke.CloseHandle(hProcess);
63 | }
64 | }
65 |
66 | return _processName;
67 | }
68 | }
69 |
70 | private unsafe uint GetProcessIdCore()
71 | {
72 | uint pid = 0;
73 | PInvoke.GetWindowThreadProcessId(_hWnd, &pid);
74 | return pid;
75 | }
76 |
77 | private unsafe string CallWin32ToGetPWSTR(int bufferLength, Func getter)
78 | {
79 | var buffer = ArrayPool.Shared.Rent(bufferLength);
80 | try
81 | {
82 | fixed (char* ptr = buffer)
83 | {
84 | getter(ptr, bufferLength);
85 | return new string(ptr);
86 | }
87 | }
88 | finally
89 | {
90 | ArrayPool.Shared.Return(buffer);
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/unlockfps_gui/Views/InitializationWindow.axaml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
23 |
24 |
28 |
45 |
46 |
50 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/unlockfps_gui/WindowChromeExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 | using System.Threading.Tasks;
4 | using Avalonia.Controls;
5 | using Avalonia.Controls.Primitives;
6 | using Avalonia.Media;
7 | using Avalonia.Platform;
8 | using OSVersionExtension;
9 |
10 | namespace UnlockFps.Gui;
11 |
12 | public static class WindowChromeExtensions
13 | {
14 | public static void SetSystemChrome(this Window window)
15 | {
16 | window.Loaded += WindowLoaded;
17 | window.Unloaded += WindowUnloaded;
18 |
19 | SetWindowChrome(window);
20 |
21 | void OnPlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e)
22 | {
23 | SetWindowChrome(window, e);
24 | }
25 |
26 | void WindowLoaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
27 | {
28 | window.Activated += WindowActivated;
29 | window.Deactivated += WindowDeactivated;
30 | if (window.PlatformSettings != null)
31 | {
32 | window.PlatformSettings.ColorValuesChanged += OnPlatformSettingsOnColorValuesChanged;
33 | }
34 | }
35 |
36 | void WindowUnloaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
37 | {
38 | window.Activated -= WindowActivated;
39 | window.Deactivated -= WindowDeactivated;
40 | if (window.PlatformSettings != null)
41 | {
42 | window.PlatformSettings.ColorValuesChanged -= OnPlatformSettingsOnColorValuesChanged;
43 | }
44 | }
45 |
46 | void WindowActivated(object? sender, EventArgs e)
47 | {
48 | SetWindowChrome(window);
49 | }
50 |
51 | void WindowDeactivated(object? sender, EventArgs e)
52 | {
53 | SetWindowChrome(window);
54 | }
55 | }
56 |
57 | private static async void SetWindowChrome(Window window, PlatformColorValues? platformSettings = null)
58 | {
59 | await Task.Delay(1);
60 | var platformColorValues = platformSettings ?? window.PlatformSettings?.GetColorValues();
61 | var isDark = platformColorValues?.ThemeVariant == PlatformThemeVariant.Dark;
62 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
63 |
64 | var version = OSVersion.GetOperatingSystem();
65 | if (window.IsActive)
66 | {
67 | if (version is OSVersionExtension.OperatingSystem.Windows11)
68 | {
69 | window.Background = isDark
70 | ? SolidColorBrush.Parse("#80202020")
71 | : SolidColorBrush.Parse("#DDF3F3F3");
72 | window.TransparencyLevelHint = new[] { WindowTransparencyLevel.Mica };
73 | }
74 | else if (version is OSVersionExtension.OperatingSystem.Windows10)
75 | {
76 | window.Background = isDark
77 | ? SolidColorBrush.Parse("#80202020")
78 | : SolidColorBrush.Parse("#DDF3F3F3");
79 | window.TransparencyLevelHint = new[] { WindowTransparencyLevel.AcrylicBlur };
80 | }
81 | }
82 | else
83 | {
84 | window.Background = isDark
85 | ? SolidColorBrush.Parse("#202020")
86 | : SolidColorBrush.Parse("#F3F3F3");
87 | await Task.Delay(100);
88 | window.ClearValue(TopLevel.TransparencyLevelHintProperty);
89 | await Task.Delay(1);
90 | window.ClearValue(TemplatedControl.BackgroundProperty);
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/unlockfps/Logging/ConsoleLogger.cs:
--------------------------------------------------------------------------------
1 | using Milki.Extensions.Threading;
2 |
3 | namespace UnlockFps.Logging;
4 |
5 | public class ConsoleLogger(string name) : ILogger
6 | {
7 | private static readonly SynchronizationContext LoggerSynchronizationContext =
8 | new SingleSynchronizationContext("Default ConsoleLogger");
9 |
10 | public void Log(LogLevel logLevel, string message)
11 | {
12 | LoggerSynchronizationContext.Post(_ =>
13 | {
14 | if (logLevel == LogLevel.Trace)
15 | {
16 | Console.ForegroundColor = ConsoleColor.DarkGray;
17 | }
18 | else if (logLevel == LogLevel.Debug)
19 | {
20 | Console.ForegroundColor = ConsoleColor.DarkGray;
21 | }
22 | else if (logLevel == LogLevel.Information)
23 | {
24 | Console.ForegroundColor = ConsoleColor.White;
25 | }
26 | else if (logLevel == LogLevel.Warning)
27 | {
28 | Console.ForegroundColor = ConsoleColor.DarkYellow;
29 | }
30 | else if (logLevel == LogLevel.Error)
31 | {
32 | Console.ForegroundColor = ConsoleColor.Red;
33 | }
34 | else if (logLevel == LogLevel.Critical)
35 | {
36 | Console.ForegroundColor = ConsoleColor.White;
37 | Console.BackgroundColor = ConsoleColor.Red;
38 | }
39 |
40 | Console.Write($"[{DateTime.Now:HH:mm:ss.fff}] ");
41 | if (logLevel == LogLevel.Trace)
42 | {
43 | Console.Write("TRACE ");
44 | }
45 | else if (logLevel == LogLevel.Debug)
46 | {
47 | Console.Write("DEBUG ");
48 | }
49 | else if (logLevel == LogLevel.Information)
50 | {
51 | Console.Write("INFO ");
52 | }
53 | else if (logLevel == LogLevel.Warning)
54 | {
55 | Console.Write("WARN ");
56 | }
57 | else if (logLevel == LogLevel.Error)
58 | {
59 | Console.Write("ERROR ");
60 | }
61 | else if (logLevel == LogLevel.Critical)
62 | {
63 | Console.Write("CRITICAL ");
64 | }
65 |
66 | Console.Write($"{name}: ");
67 | Console.WriteLine(message);
68 | Console.ResetColor();
69 | }, null);
70 | }
71 |
72 | public void LogInformation(string message)
73 | {
74 | Log(LogLevel.Information, message);
75 | }
76 |
77 | public void LogDebug(string message)
78 | {
79 | Log(LogLevel.Debug, message);
80 | }
81 |
82 | public void LogError(string message)
83 | {
84 | Log(LogLevel.Error, message);
85 | }
86 |
87 | public void LogWarning(string message)
88 | {
89 | Log(LogLevel.Warning, message);
90 | }
91 |
92 | public void LogInformation(Exception exception, string message)
93 | {
94 | Log(LogLevel.Information, message + "\r\n" + exception);
95 | }
96 |
97 | public void LogDebug(Exception exception, string message)
98 | {
99 | Log(LogLevel.Debug, message + "\r\n" + exception);
100 | }
101 |
102 | public void LogError(Exception exception, string message)
103 | {
104 | Log(LogLevel.Error, message + "\r\n" + exception);
105 | }
106 |
107 | public void LogWarning(Exception exception, string message)
108 | {
109 | Log(LogLevel.Warning, message + "\r\n" + exception);
110 | }
111 | }
--------------------------------------------------------------------------------
/unlockfps_gui/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Avalonia;
4 | using Avalonia.Controls;
5 | using Avalonia.Controls.ApplicationLifetimes;
6 | using Avalonia.Markup.Xaml;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using UnlockFps.Gui.Utils;
9 | using UnlockFps.Gui.Views;
10 | using UnlockFps.Services;
11 |
12 | namespace UnlockFps.Gui;
13 |
14 | public partial class App : Application
15 | {
16 | public static ServiceProvider DefaultServices { get; private set; } = null!;
17 |
18 | public static Window? CurrentMainWindow =>
19 | (App.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
20 |
21 | public override void Initialize()
22 | {
23 | AvaloniaXamlLoader.Load(this);
24 | }
25 |
26 | public override void RegisterServices()
27 | {
28 | base.RegisterServices();
29 |
30 | var services = new ServiceCollection();
31 | services.AddTransient();
32 | services.AddTransient();
33 | services.AddTransient();
34 | services.AddTransient();
35 | services.AddTransient();
36 | services.AddSingleton();
37 | services.AddSingleton();
38 | services.AddSingleton();
39 | DefaultServices = services.BuildServiceProvider();
40 | }
41 |
42 | public override void OnFrameworkInitializationCompleted()
43 | {
44 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
45 | {
46 | TaskScheduler.UnobservedTaskException += TaskSchedulerOnUnobservedTaskException;
47 | var configService = DefaultServices.GetRequiredService();
48 | configService.Config.PropertyChanged += Config_PropertyChanged;
49 | ToggleConsole(configService.Config.ShowDebugConsole);
50 | if (!Program.DuplicatedInstance)
51 | {
52 | desktop.MainWindow = DefaultServices.GetRequiredService();
53 | }
54 | else
55 | {
56 | var alertWindow = DefaultServices.GetRequiredService();
57 | alertWindow.Text = "Another unlocker is already running.";
58 | desktop.MainWindow = alertWindow;
59 | }
60 | }
61 |
62 | base.OnFrameworkInitializationCompleted();
63 | }
64 |
65 | private static void Config_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
66 | {
67 | if (sender is Config config && e.PropertyName == nameof(Config.ShowDebugConsole))
68 | {
69 | ToggleConsole(config.ShowDebugConsole);
70 | }
71 | }
72 |
73 | private static void TaskSchedulerOnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
74 | {
75 | if (e.Exception.InnerExceptions.Count == 1)
76 | {
77 | Console.WriteLine("Unobserved task exception: " + e.Exception.InnerException);
78 | }
79 | else
80 | {
81 | Console.WriteLine("Unobserved task exception: " + e.Exception);
82 | }
83 | }
84 |
85 | private static void ToggleConsole(bool show)
86 | {
87 | try
88 | {
89 | if (show)
90 | {
91 | ConsoleManager.Show();
92 | }
93 | else
94 | {
95 | ConsoleManager.Hide();
96 | }
97 | }
98 | catch
99 | {
100 | // ignored
101 | }
102 | }
103 | }
--------------------------------------------------------------------------------
/unlockfps_nc/MainForm.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using unlockfps_nc.Model;
3 | using unlockfps_nc.Service;
4 |
5 | namespace unlockfps_nc
6 | {
7 | public partial class MainForm : Form
8 | {
9 | private Point _windowLocation;
10 | private Size _windowSize;
11 |
12 | private readonly ConfigService _configService;
13 | private readonly Config _config;
14 | private readonly ProcessService _processService;
15 |
16 | public MainForm(
17 | ConfigService configService,
18 | ProcessService processService)
19 | {
20 | InitializeComponent();
21 | _configService = configService;
22 | _config = _configService.Config;
23 | _processService = processService;
24 | SetupBindings();
25 | }
26 |
27 | private void SettingsMenuItem_Click(object sender, EventArgs e)
28 | {
29 | var settingsForm = Program.ServiceProvider.GetRequiredService();
30 | settingsForm.ShowDialog();
31 | }
32 |
33 | private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
34 | {
35 | _configService.Save();
36 | _processService.OnFormClosing();
37 | NotifyIconMain.Visible = false;
38 | }
39 |
40 | private void MainForm_Load(object sender, EventArgs e)
41 | {
42 | _windowLocation = Location;
43 | _windowSize = Size;
44 | if (_config.AutoStart)
45 | BtnStartGame_Click(null, null);
46 | }
47 |
48 | private void SetupBindings()
49 | {
50 | InputFPS.DataBindings.Add("Value", _config, "FPSTarget", true, DataSourceUpdateMode.OnPropertyChanged);
51 | SliderFPS.DataBindings.Add("Value", _config, "FPSTarget", true, DataSourceUpdateMode.OnPropertyChanged);
52 | CBAutoStart.DataBindings.Add("Checked", _config, "AutoStart", true, DataSourceUpdateMode.OnPropertyChanged);
53 | }
54 |
55 | private void SetupMenuItem_Click(object sender, EventArgs e)
56 | {
57 | ShowSetupForm();
58 | }
59 |
60 | private void BtnStartGame_Click(object sender, EventArgs e)
61 | {
62 | if (!File.Exists(_config.GamePath))
63 | ShowSetupForm();
64 |
65 | if (_processService.Start())
66 | WindowState = FormWindowState.Minimized;
67 | }
68 |
69 | private void ShowSetupForm()
70 | {
71 | var setupForm = Program.ServiceProvider.GetRequiredService();
72 | setupForm.ShowDialog();
73 | }
74 |
75 | private void ExitMenuItem_Click(object sender, EventArgs e)
76 | {
77 | Application.Exit();
78 | }
79 |
80 | private void MainForm_Resize(object sender, EventArgs e)
81 | {
82 | if (WindowState == FormWindowState.Minimized)
83 | NotifyAndHide();
84 | }
85 |
86 | private void NotifyAndHide()
87 | {
88 | NotifyIconMain.Visible = true;
89 | NotifyIconMain.Text = $@"FPS Unlocker (FPS: {_config.FPSTarget})";
90 | NotifyIconMain.ShowBalloonTip(500);
91 |
92 | ShowInTaskbar = false;
93 | Hide();
94 | }
95 |
96 | private void NotifyIconMain_DoubleClick(object sender, EventArgs e)
97 | {
98 | WindowState = FormWindowState.Normal;
99 | ShowInTaskbar = true;
100 | Show();
101 | Activate();
102 |
103 | Location = _windowLocation;
104 | Size = _windowSize;
105 | }
106 |
107 | private void AboutMenuItem_Click(object sender, EventArgs e)
108 | {
109 | var aboutForm = new AboutForm();
110 | aboutForm.ShowDialog();
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/unlockfps/FpsPatterns.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Runtime.InteropServices;
3 | using UnlockFps.Logging;
4 | using UnlockFps.Utils;
5 |
6 | namespace UnlockFps;
7 |
8 | internal static class FpsPatterns
9 | {
10 | private static readonly ILogger Logger = LogManager.GetLogger(nameof(FpsPatterns));
11 |
12 | public static unsafe nint ProvideAddress(ProcessModule mdUnityPlayer, ProcessModule mdUserAssembly, Process process)
13 | {
14 | var unityPlayerPath = mdUnityPlayer.FileName;
15 | var userAssemblyPath = mdUserAssembly.FileName;
16 |
17 | using ModuleGuard shUnityPlayer = Utils.NativeMethods.LoadLibraryEx(unityPlayerPath, nint.Zero, 0x20);
18 | using ModuleGuard shUserAssembly = Utils.NativeMethods.LoadLibraryEx(userAssemblyPath, nint.Zero, 0x20);
19 |
20 | var pUnityPlayer = shUnityPlayer.BaseAddress;
21 | var pUserAssembly = shUserAssembly.BaseAddress;
22 |
23 | var dosHeader = Marshal.PtrToStructure(pUnityPlayer);
24 | var ntHeader =
25 | Marshal.PtrToStructure((nint)(pUnityPlayer.ToInt64() + dosHeader.e_lfanew));
26 |
27 | if (ntHeader.FileHeader.TimeDateStamp < 0x656FFAF7U) // < 3.7
28 | {
29 | Logger.LogDebug($"TimeDateStamp: {ntHeader.FileHeader.TimeDateStamp}, <3.7");
30 | var addressPtr = ProcessUtils.PatternScan(pUnityPlayer, "7F 0F 8B 05 ?? ?? ?? ??");
31 | byte* address = (byte*)addressPtr;
32 | if (address == null) throw new Exception("Unrecognized FPS pattern.");
33 |
34 | Logger.LogDebug($"Scanned pattern successfully: 0x{addressPtr:X16}");
35 | byte* rip = address + 2;
36 | int rel = *(int*)(rip + 2);
37 | var localVa = rip + rel + 6;
38 | var rva = localVa - pUnityPlayer.ToInt64();
39 | return (nint)(pUnityPlayer.ToInt64() + rva);
40 | }
41 | else
42 | {
43 | byte* rip = null;
44 | if (ntHeader.FileHeader.TimeDateStamp < 0x656FFAF7U) // < 4.3
45 | {
46 | Logger.LogDebug($"TimeDateStamp: {ntHeader.FileHeader.TimeDateStamp}, <4.3");
47 | var addressPtr =
48 | ProcessUtils.PatternScan(pUserAssembly, "E8 ?? ?? ?? ?? 85 C0 7E 07 E8 ?? ?? ?? ?? EB 05");
49 | byte* address = (byte*)addressPtr;
50 | if (address == null) throw new Exception("Unrecognized FPS pattern.");
51 |
52 | Logger.LogDebug($"Scanned pattern successfully: 0x{addressPtr:X16}");
53 | rip = address;
54 | rip += *(int*)(rip + 1) + 5;
55 | rip += *(int*)(rip + 3) + 7;
56 | }
57 | else
58 | {
59 | Logger.LogDebug($"TimeDateStamp: {ntHeader.FileHeader.TimeDateStamp}");
60 | var addressPtr = ProcessUtils.PatternScan(pUserAssembly, "B9 3C 00 00 00 FF 15");
61 | byte* address = (byte*)addressPtr;
62 | if (address == null) throw new Exception("Unrecognized FPS pattern.");
63 |
64 | Logger.LogDebug($"Scanned pattern successfully: 0x{addressPtr:X16}");
65 | rip = address;
66 | rip += 5;
67 | rip += *(int*)(rip + 2) + 6;
68 | }
69 |
70 | byte* remoteVa = rip - pUserAssembly.ToInt64() + mdUserAssembly.BaseAddress.ToInt64();
71 | byte* dataPtr = null;
72 |
73 | Span readResult = stackalloc byte[8];
74 | while (dataPtr == null)
75 | {
76 | Utils.NativeMethods.ReadProcessMemory(process.Handle, (nint)remoteVa, readResult, readResult.Length, out _);
77 | ulong value = BitConverter.ToUInt64(readResult);
78 | dataPtr = (byte*)value;
79 | }
80 |
81 | byte* localVa = dataPtr - mdUnityPlayer.BaseAddress.ToInt64() + pUnityPlayer.ToInt64();
82 | while (localVa[0] == 0xE8 || localVa[0] == 0xE9)
83 | localVa += *(int*)(localVa + 1) + 5;
84 |
85 | localVa += *(int*)(localVa + 2) + 6;
86 | var rva = localVa - pUnityPlayer.ToInt64();
87 | return (nint)(mdUnityPlayer.BaseAddress.ToInt64() + rva);
88 | }
89 |
90 | }
91 | }
--------------------------------------------------------------------------------
/unlockfps/Utils/ProcessUtils.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 | using System.Text;
3 |
4 | namespace UnlockFps.Utils;
5 |
6 | internal class ProcessUtils
7 | {
8 | public static string GetProcessPathFromPid(uint pid, out nint processHandle)
9 | {
10 | var hProcess = Native.OpenProcess(
11 | ProcessAccess.QUERY_LIMITED_INFORMATION |
12 | ProcessAccess.TERMINATE |
13 | StandardAccess.SYNCHRONIZE, false, pid);
14 |
15 | processHandle = hProcess;
16 |
17 | if (hProcess == nint.Zero)
18 | return string.Empty;
19 |
20 | StringBuilder sb = new StringBuilder(1024);
21 | uint bufferSize = (uint)sb.Capacity;
22 | if (!Native.QueryFullProcessImageName(hProcess, 0, sb, ref bufferSize))
23 | return string.Empty;
24 |
25 | return sb.ToString();
26 | }
27 |
28 | public static bool InjectDlls(nint processHandle, IReadOnlyList dllPaths)
29 | {
30 | if (dllPaths.Count == 0)
31 | return true;
32 |
33 | Native.RtlAdjustPrivilege(20, true, false, out var _);
34 |
35 | var kernel32 = Native.LoadLibrary("kernel32.dll");
36 | var loadLibrary = Native.GetProcAddress(kernel32, "LoadLibraryW");
37 |
38 | var remoteVa = Native.VirtualAllocEx(processHandle, nint.Zero, 0x1000,
39 | AllocationType.COMMIT | AllocationType.RESERVE, MemoryProtection.READWRITE);
40 | if (remoteVa == nint.Zero)
41 | return false;
42 |
43 | foreach (var dllPath in dllPaths)
44 | {
45 | var nativeString = Marshal.StringToHGlobalUni(dllPath);
46 | var bytes = Encoding.Unicode.GetBytes(dllPath);
47 | Marshal.FreeHGlobal(nativeString);
48 |
49 | if (!Native.WriteProcessMemory(processHandle, remoteVa, bytes, bytes.Length, out var bytesWritten))
50 | return false;
51 |
52 | var thread = Native.CreateRemoteThread(processHandle, nint.Zero, 0, loadLibrary, remoteVa, 0, out var threadId);
53 | if (thread == nint.Zero)
54 | return false;
55 |
56 | Native.WaitForSingleObject(thread, uint.MaxValue);
57 | Native.CloseHandle(thread);
58 | Native.WriteProcessMemory(processHandle, remoteVa, new byte[bytes.Length], bytes.Length, out _);
59 | }
60 |
61 | Native.VirtualFreeEx(processHandle, remoteVa, 0, FreeType.RELEASE);
62 |
63 | return true;
64 | }
65 |
66 | public static unsafe nint PatternScan(nint module, string signature)
67 | {
68 | var dosHeader = Marshal.PtrToStructure(module);
69 | var ntHeader = Marshal.PtrToStructure((nint)(module.ToInt64() + dosHeader.e_lfanew));
70 |
71 | var sizeOfImage = ntHeader.OptionalHeader.SizeOfImage;
72 |
73 | using var scanner = new Reloaded.Memory.Sigscan.Scanner((byte*)module.ToPointer(), (int)sizeOfImage);
74 |
75 | var result = scanner.FindPattern(signature);
76 | if (result.Found)
77 | {
78 | return (nint)(module.ToInt64() + result.Offset);
79 | }
80 |
81 | return nint.Zero;
82 | }
83 |
84 | public static nint GetModuleBase(nint hProcess, string moduleName)
85 | {
86 | var modules = new nint[1024];
87 |
88 | if (!Native.EnumProcessModules(hProcess, modules, (uint)(modules.Length * nint.Size), out var bytesNeeded))
89 | {
90 | if (Marshal.GetLastWin32Error() != 299)
91 | return nint.Zero;
92 | }
93 |
94 | foreach (var module in modules.Where(x => x != nint.Zero))
95 | {
96 | StringBuilder sb = new StringBuilder(1024);
97 | if (Native.GetModuleBaseName(hProcess, module, sb, (uint)sb.Capacity) == 0)
98 | continue;
99 |
100 | if (sb.ToString() != moduleName)
101 | continue;
102 |
103 | if (!Native.GetModuleInformation(hProcess, module, out var moduleInfo, (uint)Marshal.SizeOf()))
104 | continue;
105 |
106 | return moduleInfo.lpBaseOfDll;
107 | }
108 |
109 | return nint.Zero;
110 | }
111 | }
--------------------------------------------------------------------------------
/unlockfps_gui/Styles/TabStyles.axaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
64 |
65 |
66 |
67 |
68 |
71 |
72 |
73 |
77 |
80 |
81 |
82 | 14
83 | Normal
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/unlockfps_nc/AboutForm.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace unlockfps_nc
2 | {
3 | partial class AboutForm
4 | {
5 | ///
6 | /// Required designer variable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Clean up any resources being used.
12 | ///
13 | /// true if managed resources should be disposed; otherwise, false.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Windows Form Designer generated code
24 |
25 | ///
26 | /// Required method for Designer support - do not modify
27 | /// the contents of this method with the code editor.
28 | ///
29 | private void InitializeComponent()
30 | {
31 | LabelTitle = new Label();
32 | LabelDescription = new Label();
33 | LinkLabelSource = new LinkLabel();
34 | LinkLabelIssues = new LinkLabel();
35 | SuspendLayout();
36 | //
37 | // LabelTitle
38 | //
39 | LabelTitle.Font = new Font("Segoe UI", 9F, FontStyle.Bold, GraphicsUnit.Point, 0);
40 | LabelTitle.Location = new Point(12, 10);
41 | LabelTitle.Name = "LabelTitle";
42 | LabelTitle.Size = new Size(320, 41);
43 | LabelTitle.TabIndex = 0;
44 | LabelTitle.Text = "Genshin FPS Unlocker\r\nv3.0.3";
45 | LabelTitle.TextAlign = ContentAlignment.TopCenter;
46 | //
47 | // LabelDescription
48 | //
49 | LabelDescription.Location = new Point(12, 51);
50 | LabelDescription.Name = "LabelDescription";
51 | LabelDescription.Size = new Size(320, 23);
52 | LabelDescription.TabIndex = 1;
53 | LabelDescription.Text = "This program is free and open source";
54 | LabelDescription.TextAlign = ContentAlignment.MiddleCenter;
55 | //
56 | // LinkLabelSource
57 | //
58 | LinkLabelSource.LinkArea = new LinkArea(8, 46);
59 | LinkLabelSource.Location = new Point(12, 74);
60 | LinkLabelSource.Name = "LinkLabelSource";
61 | LinkLabelSource.Size = new Size(320, 23);
62 | LinkLabelSource.TabIndex = 2;
63 | LinkLabelSource.TabStop = true;
64 | LinkLabelSource.Text = "Source: https://github.com/34736384/genshin-fps-unlock";
65 | LinkLabelSource.TextAlign = ContentAlignment.MiddleCenter;
66 | LinkLabelSource.UseCompatibleTextRendering = true;
67 | LinkLabelSource.LinkClicked += LinkLabelSource_LinkClicked;
68 | //
69 | // LinkLabelIssues
70 | //
71 | LinkLabelIssues.LinkArea = new LinkArea(84, 53);
72 | LinkLabelIssues.Location = new Point(12, 96);
73 | LinkLabelIssues.Name = "LinkLabelIssues";
74 | LinkLabelIssues.Size = new Size(320, 76);
75 | LinkLabelIssues.TabIndex = 3;
76 | LinkLabelIssues.TabStop = true;
77 | LinkLabelIssues.Text = "If you encounter any problems or have a suggestion\r\nGo ahead and submit an issue at\r\n\r\nhttps://github.com/34736384/genshin-fps-unlock/issues\r\n\r\n";
78 | LinkLabelIssues.TextAlign = ContentAlignment.MiddleCenter;
79 | LinkLabelIssues.UseCompatibleTextRendering = true;
80 | LinkLabelIssues.LinkClicked += LinkLabelIssues_LinkClicked;
81 | //
82 | // AboutForm
83 | //
84 | AutoScaleDimensions = new SizeF(7F, 17F);
85 | AutoScaleMode = AutoScaleMode.Font;
86 | ClientSize = new Size(344, 194);
87 | Controls.Add(LinkLabelIssues);
88 | Controls.Add(LinkLabelSource);
89 | Controls.Add(LabelDescription);
90 | Controls.Add(LabelTitle);
91 | FormBorderStyle = FormBorderStyle.FixedDialog;
92 | MaximizeBox = false;
93 | MinimizeBox = false;
94 | Name = "AboutForm";
95 | StartPosition = FormStartPosition.CenterParent;
96 | Text = "About";
97 | ResumeLayout(false);
98 | }
99 |
100 | #endregion
101 |
102 | private Label LabelTitle;
103 | private Label LabelDescription;
104 | private LinkLabel LinkLabelSource;
105 | private LinkLabel LinkLabelIssues;
106 | }
107 | }
--------------------------------------------------------------------------------
/unlockfps_gui/Utils/ProcessUtils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Runtime.InteropServices;
5 | using System.Text;
6 |
7 | namespace UnlockFps.Gui.Utils
8 | {
9 | internal class ProcessUtils
10 | {
11 | public static string GetProcessPathFromPid(uint pid, out IntPtr processHandle)
12 | {
13 | var hProcess = Native.OpenProcess(
14 | ProcessAccess.QUERY_LIMITED_INFORMATION |
15 | ProcessAccess.TERMINATE |
16 | StandardAccess.SYNCHRONIZE, false, pid);
17 |
18 | processHandle = hProcess;
19 |
20 | if (hProcess == IntPtr.Zero)
21 | return string.Empty;
22 |
23 | StringBuilder sb = new StringBuilder(1024);
24 | uint bufferSize = (uint)sb.Capacity;
25 | if (!Native.QueryFullProcessImageName(hProcess, 0, sb, ref bufferSize))
26 | return string.Empty;
27 |
28 | return sb.ToString();
29 | }
30 |
31 | public static bool InjectDlls(IntPtr processHandle, IReadOnlyList dllPaths)
32 | {
33 | if (dllPaths.Count == 0)
34 | return true;
35 |
36 | Native.RtlAdjustPrivilege(20, true, false, out var _);
37 |
38 | var kernel32 = Native.LoadLibrary("kernel32.dll");
39 | var loadLibrary = Native.GetProcAddress(kernel32, "LoadLibraryW");
40 |
41 | var remoteVa = Native.VirtualAllocEx(processHandle, IntPtr.Zero, 0x1000,
42 | AllocationType.COMMIT | AllocationType.RESERVE, MemoryProtection.READWRITE);
43 | if (remoteVa == IntPtr.Zero)
44 | return false;
45 |
46 | foreach (var dllPath in dllPaths)
47 | {
48 | var nativeString = Marshal.StringToHGlobalUni(dllPath);
49 | var bytes = Encoding.Unicode.GetBytes(dllPath);
50 | Marshal.FreeHGlobal(nativeString);
51 |
52 | if (!Native.WriteProcessMemory(processHandle, remoteVa, bytes, bytes.Length, out var bytesWritten))
53 | return false;
54 |
55 | var thread = Native.CreateRemoteThread(processHandle, IntPtr.Zero, 0, loadLibrary, remoteVa, 0, out var threadId);
56 | if (thread == IntPtr.Zero)
57 | return false;
58 |
59 | Native.WaitForSingleObject(thread, uint.MaxValue);
60 | Native.CloseHandle(thread);
61 | Native.WriteProcessMemory(processHandle, remoteVa, new byte[bytes.Length], bytes.Length, out _);
62 | }
63 |
64 | Native.VirtualFreeEx(processHandle, remoteVa, 0, FreeType.RELEASE);
65 |
66 | return true;
67 | }
68 |
69 | public static unsafe IntPtr PatternScan(IntPtr module, string signature)
70 | {
71 | var tokens = signature.Split(' ');
72 | var patternBytes = tokens
73 | .ToList()
74 | .Select(x => x == "?" ? (byte)0xFF : Convert.ToByte(x, 16))
75 | .ToArray();
76 |
77 | var dosHeader = Marshal.PtrToStructure(module);
78 | var ntHeader = Marshal.PtrToStructure((nint)(module.ToInt64() + dosHeader.e_lfanew));
79 |
80 | var sizeOfImage = ntHeader.OptionalHeader.SizeOfImage;
81 | var scanBytes = (byte*)module;
82 |
83 | var s = patternBytes.Length;
84 | var d = patternBytes;
85 |
86 | for (var i = 0U; i < sizeOfImage - s; i++)
87 | {
88 | var found = true;
89 | for (var j = 0; j < s; j++)
90 | {
91 | if (d[j] != scanBytes[i + j] && d[j] != 0xFF)
92 | {
93 | found = false;
94 | break;
95 | }
96 | }
97 |
98 | if (found)
99 | return (nint)(module.ToInt64() + i);
100 | }
101 |
102 | return IntPtr.Zero;
103 | }
104 |
105 | public static IntPtr GetModuleBase(IntPtr hProcess, string moduleName)
106 | {
107 | var modules = new IntPtr[1024];
108 |
109 | if (!Native.EnumProcessModules(hProcess, modules, (uint)(modules.Length * IntPtr.Size), out var bytesNeeded))
110 | {
111 | if (Marshal.GetLastWin32Error() != 299)
112 | return IntPtr.Zero;
113 | }
114 |
115 | foreach (var module in modules.Where(x => x != IntPtr.Zero))
116 | {
117 | StringBuilder sb = new StringBuilder(1024);
118 | if (Native.GetModuleBaseName(hProcess, module, sb, (uint)sb.Capacity) == 0)
119 | continue;
120 |
121 | if (sb.ToString() != moduleName)
122 | continue;
123 |
124 | if (!Native.GetModuleInformation(hProcess, module, out var moduleInfo, (uint)Marshal.SizeOf()))
125 | continue;
126 |
127 | return moduleInfo.lpBaseOfDll;
128 | }
129 |
130 | return IntPtr.Zero;
131 | }
132 |
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/unlockfps_gui/Views/SettingsWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Reflection.PortableExecutable;
4 | using System.Windows.Input;
5 | using Avalonia.Controls;
6 | using Avalonia.Interactivity;
7 | using Avalonia.Platform.Storage;
8 | using Microsoft.Extensions.DependencyInjection;
9 | using ReactiveUI;
10 | using UnlockFps.Gui.ViewModels;
11 | using UnlockFps.Gui.Views;
12 | using UnlockFps.Services;
13 |
14 | namespace UnlockFps.Gui.ViewModels
15 | {
16 | public class SettingsWindowViewModel : ViewModelBase
17 | {
18 | private ICommand? _addDllCommand;
19 | private ICommand? _removeDllCommand;
20 |
21 | public required SettingsWindow Window { get; init; }
22 | public required Config Config { get; init; }
23 |
24 | public string? SelectedDll { get; set; }
25 |
26 | public ICommand AddDllCommand => _addDllCommand ??= ReactiveCommand.CreateFromTask(async () =>
27 | {
28 | var selectedFiles = await Window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
29 | {
30 | FileTypeFilter =
31 | [
32 | new FilePickerFileType("DLL (*.dll)") { Patterns = ["*.dll"] },
33 | new FilePickerFileType("All files (*.*)") { Patterns = ["*.*"] },
34 | ],
35 | AllowMultiple = true
36 | });
37 |
38 | foreach (var selectedFile in selectedFiles)
39 | {
40 | var localPath = selectedFile.Path.LocalPath;
41 | if (!VerifyDll(localPath))
42 | {
43 | var alertWindow = App.DefaultServices.GetRequiredService();
44 | alertWindow.Text =
45 | $"""
46 | Invalid File:
47 | {localPath}
48 |
49 | Only native x64 dlls are supported.
50 | """;
51 | await alertWindow.ShowDialog(Window);
52 | }
53 | else
54 | {
55 | Config.LaunchOptions.DllList.Add(localPath);
56 | }
57 | }
58 | });
59 |
60 | public ICommand RemoveDllCommand => _removeDllCommand ??= ReactiveCommand.Create(() =>
61 | {
62 | if (SelectedDll != null)
63 | {
64 | Config.LaunchOptions.DllList.Remove(SelectedDll);
65 | }
66 | });
67 |
68 | private static bool VerifyDll(string fullPath)
69 | {
70 | if (!File.Exists(fullPath))
71 | return false;
72 |
73 | using var fs = new FileStream(fullPath, FileMode.Open, FileAccess.Read);
74 | using var peReader = new PEReader(fs);
75 | if (peReader.HasMetadata)
76 | return false;
77 |
78 | return peReader.PEHeaders.CoffHeader.Machine == Machine.Amd64;
79 | }
80 | }
81 | }
82 |
83 | namespace UnlockFps.Gui.Views
84 | {
85 | public partial class SettingsWindow : Window
86 | {
87 | private readonly ConfigService? _configService;
88 | private readonly SettingsWindowViewModel _viewModel;
89 |
90 | public SettingsWindow(ConfigService configService)
91 | {
92 | this.SetSystemChrome();
93 | _configService = configService;
94 | _viewModel = new SettingsWindowViewModel
95 | {
96 | Config = _configService.Config,
97 | Window = this,
98 | };
99 | DataContext = _viewModel;
100 | InitializeComponent();
101 | }
102 |
103 | #if DEBUG
104 | public SettingsWindow()
105 | {
106 | if (!Design.IsDesignMode) throw new InvalidOperationException();
107 | InitializeComponent();
108 | }
109 | #endif
110 |
111 | private void Control_OnLoaded(object? sender, RoutedEventArgs e)
112 | {
113 | if (_configService != null)
114 | {
115 | _configService.Config.PropertyChanged += Config_PropertyChanged;
116 | _configService.Config.LaunchOptions.PropertyChanged += Config_PropertyChanged;
117 | _configService.Config.LaunchOptions.DllList.CollectionChanged += DllList_CollectionChanged;
118 | }
119 | }
120 |
121 | private void Control_OnUnloaded(object? sender, RoutedEventArgs e)
122 | {
123 | if (_configService != null)
124 | {
125 | _configService.Config.PropertyChanged -= Config_PropertyChanged;
126 | _configService.Config.LaunchOptions.PropertyChanged -= Config_PropertyChanged;
127 | _configService.Config.LaunchOptions.DllList.CollectionChanged -= DllList_CollectionChanged;
128 | }
129 | }
130 |
131 | private void Config_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
132 | {
133 | _configService?.Save();
134 | }
135 |
136 | private void DllList_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
137 | {
138 | _configService?.Save();
139 | }
140 | }
141 | }
--------------------------------------------------------------------------------
/unlockfps_nc/Utility/ProcessUtils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reflection.PortableExecutable;
5 | using System.Runtime.InteropServices;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace unlockfps_nc.Utility
10 | {
11 | internal class ProcessUtils
12 | {
13 | public static string GetProcessPathFromPid(uint pid, out IntPtr processHandle)
14 | {
15 | var hProcess = Native.OpenProcess(
16 | ProcessAccess.QUERY_LIMITED_INFORMATION |
17 | ProcessAccess.TERMINATE |
18 | StandardAccess.SYNCHRONIZE, false, pid);
19 |
20 | processHandle = hProcess;
21 |
22 | if (hProcess == IntPtr.Zero)
23 | return string.Empty;
24 |
25 | StringBuilder sb = new StringBuilder(1024);
26 | uint bufferSize = (uint)sb.Capacity;
27 | if (!Native.QueryFullProcessImageName(hProcess, 0, sb, ref bufferSize))
28 | return string.Empty;
29 |
30 | return sb.ToString();
31 | }
32 |
33 | public static bool InjectDlls(IntPtr processHandle, List dllPaths)
34 | {
35 | if (dllPaths.Count == 0)
36 | return true;
37 |
38 | Native.RtlAdjustPrivilege(20, true, false, out var _);
39 |
40 | var kernel32 = Native.LoadLibrary("kernel32.dll");
41 | var loadLibrary = Native.GetProcAddress(kernel32, "LoadLibraryW");
42 |
43 | var remoteVa = Native.VirtualAllocEx(processHandle, IntPtr.Zero, 0x1000,
44 | AllocationType.COMMIT | AllocationType.RESERVE, MemoryProtection.READWRITE);
45 | if (remoteVa == IntPtr.Zero)
46 | return false;
47 |
48 | foreach (var dllPath in dllPaths)
49 | {
50 | var nativeString = Marshal.StringToHGlobalUni(dllPath);
51 | var bytes = Encoding.Unicode.GetBytes(dllPath);
52 | Marshal.FreeHGlobal(nativeString);
53 |
54 | if (!Native.WriteProcessMemory(processHandle, remoteVa, bytes, bytes.Length, out var bytesWritten))
55 | return false;
56 |
57 | var thread = Native.CreateRemoteThread(processHandle, IntPtr.Zero, 0, loadLibrary, remoteVa, 0, out var threadId);
58 | if (thread == IntPtr.Zero)
59 | return false;
60 |
61 | Native.WaitForSingleObject(thread, uint.MaxValue);
62 | Native.CloseHandle(thread);
63 | Native.WriteProcessMemory(processHandle, remoteVa, new byte[bytes.Length], bytes.Length, out _);
64 | }
65 |
66 | Native.VirtualFreeEx(processHandle, remoteVa, 0, FreeType.RELEASE);
67 |
68 | return true;
69 | }
70 |
71 | public static unsafe IntPtr PatternScan(IntPtr module, string signature)
72 | {
73 | var tokens = signature.Split(' ');
74 | var patternBytes = tokens
75 | .ToList()
76 | .Select(x => x == "?" ? (byte)0xFF : Convert.ToByte(x, 16))
77 | .ToArray();
78 |
79 | var dosHeader = Marshal.PtrToStructure(module);
80 | var ntHeader = Marshal.PtrToStructure((IntPtr)(module.ToInt64() + dosHeader.e_lfanew));
81 |
82 | var sizeOfImage = ntHeader.OptionalHeader.SizeOfImage;
83 | var scanBytes = (byte*)module;
84 |
85 | var s = patternBytes.Length;
86 | var d = patternBytes;
87 |
88 | for (var i = 0U; i < sizeOfImage - s; i++)
89 | {
90 | var found = true;
91 | for (var j = 0; j < s; j++)
92 | {
93 | if (d[j] != scanBytes[i + j] && d[j] != 0xFF)
94 | {
95 | found = false;
96 | break;
97 | }
98 | }
99 |
100 | if (found)
101 | return (IntPtr)(module.ToInt64() + i);
102 | }
103 |
104 | return IntPtr.Zero;
105 | }
106 |
107 | public static IntPtr GetModuleBase(IntPtr hProcess, string moduleName)
108 | {
109 | var modules = new IntPtr[1024];
110 |
111 | if (!Native.EnumProcessModules(hProcess, modules, (uint)(modules.Length * IntPtr.Size), out var bytesNeeded))
112 | {
113 | if (Marshal.GetLastWin32Error() != 299)
114 | return IntPtr.Zero;
115 | }
116 |
117 | foreach (var module in modules.Where(x => x != IntPtr.Zero))
118 | {
119 | StringBuilder sb = new StringBuilder(1024);
120 | if (Native.GetModuleBaseName(hProcess, module, sb, (uint)sb.Capacity) == 0)
121 | continue;
122 |
123 | if (sb.ToString() != moduleName)
124 | continue;
125 |
126 | if (!Native.GetModuleInformation(hProcess, module, out var moduleInfo, (uint)Marshal.SizeOf()))
127 | continue;
128 |
129 | return moduleInfo.lpBaseOfDll;
130 | }
131 |
132 | return IntPtr.Zero;
133 | }
134 |
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/unlockfps/Services/ProcessService.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using System.ComponentModel;
3 | using System.Diagnostics;
4 | using System.Runtime.InteropServices;
5 | using System.Runtime.Versioning;
6 | using System.Text;
7 | using UnlockFps.Logging;
8 | using UnlockFps.Utils;
9 | using Windows.Win32.System.Threading;
10 |
11 | using static Windows.Win32.PInvoke;
12 | using PROCESS_INFORMATION = Windows.Win32.System.Threading.PROCESS_INFORMATION;
13 |
14 | namespace UnlockFps.Services;
15 |
16 | [SupportedOSPlatform("windows5.1.2600")]
17 | public class ProcessService
18 | {
19 | private static readonly ILogger Logger = LogManager.GetLogger(nameof(ProcessService));
20 |
21 | private readonly Config _config;
22 | private int _lastProcessId;
23 |
24 | public ProcessService(ConfigService configService)
25 | {
26 | _config = configService.Config;
27 | }
28 |
29 | public void Start()
30 | {
31 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
32 | {
33 | throw new PlatformNotSupportedException("Only windows or wine is supported.");
34 | }
35 |
36 | var runningProcess = Process.GetProcesses()
37 | .FirstOrDefault(x => Array.IndexOf(GameConstants.GameNames, x.ProcessName) != -1);
38 |
39 | if (runningProcess is not null)
40 | {
41 | throw new Exception("An instance of the game is already running: " + runningProcess.Id);
42 | }
43 |
44 | var launchOptions = _config.LaunchOptions;
45 | using var disposable = CreateProcessRaw(launchOptions, out var lpProcessInformation);
46 |
47 | if (!ProcessUtils.InjectDlls(lpProcessInformation.hProcess, launchOptions.DllList))
48 | {
49 | throw new Win32Exception(Marshal.GetLastWin32Error(),
50 | $"Dll Injection failed. ({Marshal.GetLastPInvokeErrorMessage()})");
51 | }
52 |
53 | if (launchOptions.SuspendLoad)
54 | {
55 | var retCode = ResumeThread(lpProcessInformation.hThread);
56 | if (retCode == 0xFFFFFFFF)
57 | {
58 | throw new Win32Exception(Marshal.GetLastWin32Error(),
59 | $"ResumeThread failed. ({Marshal.GetLastPInvokeErrorMessage()})");
60 | }
61 | }
62 |
63 | _lastProcessId = (int)lpProcessInformation.dwProcessId;
64 | }
65 |
66 | private static unsafe IDisposable CreateProcessRaw(LaunchOptions launchOptions, out PROCESS_INFORMATION lpProcessInformation)
67 | {
68 | var lpCurrentDirectory = Path.GetDirectoryName(launchOptions.GamePath);
69 | var commandLine = BuildCommandLine(launchOptions);
70 | var lpStartupInfo = new STARTUPINFOW();
71 | var dwCreationFlags = launchOptions.SuspendLoad ? PROCESS_CREATION_FLAGS.CREATE_SUSPENDED : default;
72 |
73 | var array = ArrayPool.Shared.Rent(commandLine.Length + 1);
74 | try
75 | {
76 | var lpCommandLine = new Span(array, 0, commandLine.Length + 1);
77 | commandLine.CopyTo(lpCommandLine);
78 | lpCommandLine[^1] = '\0';
79 |
80 | if (!CreateProcess(launchOptions.GamePath, ref lpCommandLine,
81 | default, default, false,
82 | dwCreationFlags, default, lpCurrentDirectory,
83 | in lpStartupInfo, out lpProcessInformation))
84 | {
85 | throw new Win32Exception(Marshal.GetLastWin32Error(),
86 | $"CreateProcess failed. ({Marshal.GetLastPInvokeErrorMessage()})");
87 | }
88 | }
89 | finally
90 | {
91 | ArrayPool.Shared.Return(array);
92 | }
93 |
94 | return new ThreadGuard(lpProcessInformation.hThread);
95 | }
96 |
97 | private static string BuildCommandLine(LaunchOptions launchOptions)
98 | {
99 | var commandLine = new StringBuilder($"{launchOptions.GamePath} ");
100 | if (launchOptions.IsWindowBorderless)
101 | {
102 | commandLine.Append("-popupwindow ");
103 | }
104 |
105 | if (launchOptions.UseCustomResolution)
106 | {
107 | commandLine.Append(
108 | $"-screen-width {launchOptions.CustomResolutionX} -screen-height {launchOptions.CustomResolutionY} ");
109 | }
110 |
111 | commandLine.Append($"-screen-fullscreen {(launchOptions.Fullscreen ? 1 : 0)} ");
112 | if (launchOptions.Fullscreen)
113 | {
114 | commandLine.Append($"-window-mode {(launchOptions.IsExclusiveFullscreen ? "exclusive" : "borderless")} ");
115 | }
116 |
117 | if (launchOptions.UseMobileUI)
118 | {
119 | commandLine.Append("use_mobile_platform -is_cloud 1 -platform_type CLOUD_THIRD_PARTY_MOBILE ");
120 | }
121 |
122 | commandLine.Append($"-monitor {launchOptions.MonitorId} ");
123 | return commandLine.ToString();
124 | }
125 |
126 | public void KillLastProcess()
127 | {
128 | try
129 | {
130 | var process = Process.GetProcessById(_lastProcessId);
131 | if (Array.IndexOf(GameConstants.GameNames, process.ProcessName) != -1)
132 | {
133 | process.Kill();
134 | }
135 | }
136 | catch (Exception ex)
137 | {
138 | Logger.LogWarning(ex, "Kill process failed");
139 | }
140 | }
141 | }
--------------------------------------------------------------------------------
/unlockfps_gui/Utils/ConsoleManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Reflection.Metadata;
5 | using System.Runtime.InteropServices;
6 | using System.Security;
7 |
8 | namespace UnlockFps.Gui.Utils;
9 |
10 | [SuppressUnmanagedCodeSecurity]
11 | internal static class ConsoleManager
12 | {
13 | [Flags]
14 | public enum CharacterAttributes
15 | {
16 | FOREGROUND_BLUE = 0x0001,
17 | FOREGROUND_GREEN = 0x0002,
18 | FOREGROUND_RED = 0x0004,
19 | FOREGROUND_INTENSITY = 0x0008,
20 | BACKGROUND_BLUE = 0x0010,
21 | BACKGROUND_GREEN = 0x0020,
22 | BACKGROUND_RED = 0x0040,
23 | BACKGROUND_INTENSITY = 0x0080,
24 | COMMON_LVB_LEADING_BYTE = 0x0100,
25 | COMMON_LVB_TRAILING_BYTE = 0x0200,
26 | COMMON_LVB_GRID_HORIZONTAL = 0x0400,
27 | COMMON_LVB_GRID_LVERTICAL = 0x0800,
28 | COMMON_LVB_GRID_RVERTICAL = 0x1000,
29 | COMMON_LVB_REVERSE_VIDEO = 0x4000,
30 | COMMON_LVB_UNDERSCORE = 0x8000
31 | }
32 |
33 | private static ConsoleEventDelegate? _handler;
34 | private delegate bool ConsoleEventDelegate(int eventType);
35 | public static bool HasConsole => GetConsoleWindow() != IntPtr.Zero;
36 |
37 | private const string Kernel32_DllName = "kernel32";
38 |
39 | [DllImport(Kernel32_DllName)]
40 | private static extern bool AllocConsole();
41 |
42 | [DllImport(Kernel32_DllName)]
43 | private static extern bool FreeConsole();
44 |
45 | [DllImport(Kernel32_DllName)]
46 | private static extern IntPtr GetConsoleWindow();
47 |
48 | [DllImport(Kernel32_DllName)]
49 | private static extern int GetConsoleOutputCP();
50 | [DllImport(Kernel32_DllName)]
51 | private static extern int SetConsoleTextAttribute(IntPtr hConsoleOutput,
52 | CharacterAttributes wAttributes);
53 | [DllImport(Kernel32_DllName, SetLastError = true)]
54 | private static extern bool SetConsoleCtrlHandler(ConsoleEventDelegate callback, bool add);
55 |
56 | private const int MF_BYCOMMAND = 0x00000000;
57 | public const int SC_CLOSE = 0xF060;
58 |
59 | [DllImport("user32.dll")]
60 | public static extern int DeleteMenu(IntPtr hMenu, int nPosition, int wFlags);
61 |
62 | [DllImport("user32.dll")]
63 | private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
64 |
65 | ///
66 | /// Creates a new console instance if the process is not attached to a console already.
67 | ///
68 | public static void Show()
69 | {
70 | if (HasConsole) return;
71 | AllocConsole();
72 | //var intPtr = GetConsoleWindow();
73 | //SetConsoleTextAttribute(intPtr,
74 | // CharacterAttributes.BACKGROUND_INTENSITY | CharacterAttributes.FOREGROUND_INTENSITY);
75 | InvalidateOutAndError();
76 |
77 | Console.Title = "Genshin FPS Unlocker Debugging Console";
78 | Console.ForegroundColor = ConsoleColor.Yellow;
79 | Console.WriteLine("Note: Closing this window will lead to program exiting.");
80 | Console.ResetColor();
81 | var hMenu = GetSystemMenu(GetConsoleWindow(), false);
82 | DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);
83 | }
84 |
85 | public static void BindExitAction(Action? exitAction)
86 | {
87 | if (exitAction == null || _handler != null) return;
88 | _handler = eventType =>
89 | {
90 | if (eventType != 2) return false;
91 | exitAction();
92 | return true;
93 | };
94 |
95 | SetConsoleCtrlHandler(_handler, true);
96 | }
97 |
98 | ///
99 | /// If the process has a console attached to it, it will be detached and no longer visible. Writing to the System.Console is still possible, but no output will be shown.
100 | ///
101 | public static void Hide()
102 | {
103 | if (!HasConsole) return;
104 | SetOutAndErrorNull();
105 | FreeConsole();
106 | }
107 |
108 | //public static void Toggle()
109 | //{
110 | // if (HasConsole) Hide();
111 | // else Show();
112 | //}
113 |
114 | private static void InvalidateOutAndError()
115 | {
116 | Type type = typeof(System.Console);
117 | System.Reflection.FieldInfo? _out = type.GetField(
118 | #if !NETSTANDARD2_0
119 | "s_out",
120 | #else
121 | "_out",
122 | #endif
123 |
124 | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
125 |
126 | System.Reflection.FieldInfo? _error = type.GetField(
127 | #if !NETSTANDARD2_0
128 | "s_error",
129 | #else
130 | "_error",
131 | #endif
132 | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
133 |
134 | //System.Reflection.MethodInfo? _InitializeStdOutError = type.GetMethod("InitializeStdOutError",
135 | // System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
136 |
137 | Debug.Assert(_out != null);
138 | Debug.Assert(_error != null);
139 |
140 | //Debug.Assert(_InitializeStdOutError != null);
141 |
142 | _out.SetValue(null, null);
143 | _error.SetValue(null, null);
144 |
145 | var o = Console.Out;
146 | o = Console.Error;
147 | //_InitializeStdOutError.Invoke(null, new object[] { true });
148 | }
149 |
150 | private static void SetOutAndErrorNull()
151 | {
152 | Console.SetOut(TextWriter.Null);
153 | Console.SetError(TextWriter.Null);
154 | }
155 | }
--------------------------------------------------------------------------------
/unlockfps_gui/OSVersionExt/MajorVersion10/MajorVersion10Properties.cs:
--------------------------------------------------------------------------------
1 | using OSVersionExt.Registry;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 |
7 | namespace OSVersionExt.MajorVersion10
8 | {
9 | public readonly struct RegistryEntry
10 | {
11 | ///
12 | /// The full registry path of the key, beginning with a valid registry root, such as "HKEY_CURRENT_USER".
13 | ///
14 | public string FullPathToKey { get; }
15 |
16 | ///
17 | /// The name of the name/value pair.
18 | ///
19 | public string ValueName { get; }
20 |
21 | ///
22 | /// The value to return if ValueName does not exist.
23 | ///
24 | public string DefaultValueNotFound {get;}
25 |
26 | public RegistryEntry(string fullPathToKey, string valueName, string defaultValue)
27 | {
28 | FullPathToKey = fullPathToKey;
29 | ValueName = valueName;
30 | DefaultValueNotFound = defaultValue;
31 | }
32 | }
33 |
34 | ///
35 | /// Get the release id and UBR (Update Build Revision) on Windows system having major version 10.
36 | ///
37 | public class MajorVersion10Properties
38 | {
39 | private const string FullPathToCurrentVersion = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion";
40 | private readonly RegistryEntry _releaseIdRegistry = new RegistryEntry(FullPathToCurrentVersion, "ReleaseId", null);
41 | private readonly RegistryEntry _ubrRegistry = new RegistryEntry(FullPathToCurrentVersion, "UBR", null);
42 | private readonly RegistryEntry _displayVersionRegistry = new RegistryEntry(FullPathToCurrentVersion, "DisplayVersion", null);
43 |
44 | private IRegistry _registryProvider;
45 |
46 | private string _releaseId = null;
47 | private string _UBR = null;
48 | private string _displayVersion = null;
49 |
50 | ///
51 | /// Returns the Windows numeric release ID (e.g. 1909, 2004, 2009). For versions like 20H2 use DisplayVersion.
52 | ///
53 | /// returns the release id or null, if detection has failed.
54 | [Obsolete("Works until Windows 10 version 2009/20H2 (build 19042) only. Use DisplayVersion instead.")]
55 | public string ReleaseId { get => _releaseId; }
56 |
57 | ///
58 | /// Gets the Update Build Revision of a Windows 10 system
59 | ///
60 | /// returns null, if detection has failed.
61 | public string UBR { get => _UBR; }
62 |
63 | ///
64 | /// Gets the Display Version such as 1909, 2004, 20H2.
65 | ///
66 | public string DisplayVersion { get => _displayVersion; }
67 |
68 | ///
69 | /// Create instance with custom registry provider.
70 | ///
71 | ///
72 | ///
73 | public MajorVersion10Properties(IRegistry registryProvider)
74 | {
75 | _ = registryProvider ?? throw new ArgumentNullException();
76 |
77 | _registryProvider = registryProvider;
78 | GetAllProperties();
79 | }
80 |
81 | public MajorVersion10Properties()
82 | {
83 | _registryProvider = new RegistryProviderDefault();
84 | GetAllProperties();
85 | }
86 |
87 | private void GetAllProperties()
88 | {
89 | _releaseId = GetReleaseId();
90 | _UBR = GetUBR();
91 | _displayVersion = GetDisplayVersion();
92 | }
93 |
94 | ///
95 | /// The version number representing feature updates, is referred as the release id, such as 1903, 1909.
96 | /// Works until Windows 10 version 2009/20H2 (build 19042) only.
97 | ///
98 | /// Returns the release id or null, if value is not available.
99 | /// Feature updates for Windows 10 are released twice a year, around March and September, via the Semi-Annual Channel.
100 | private string GetReleaseId()
101 | {
102 | return _registryProvider.GetValue(_releaseIdRegistry.FullPathToKey, _releaseIdRegistry.ValueName, _releaseIdRegistry.DefaultValueNotFound)?.ToString();
103 | }
104 |
105 | ///
106 | /// Gets the UBR (Update Build Revision).
107 | ///
108 | ///
109 | /// E.g, it returns 778 for Microsoft Windows [Version 10.0.18363.778]
110 | private string GetUBR()
111 | {
112 | return _registryProvider.GetValue(_ubrRegistry.FullPathToKey, _ubrRegistry.ValueName, _ubrRegistry.DefaultValueNotFound)?.ToString();
113 | }
114 |
115 |
116 | ///
117 | /// Returns the DisplayVersion such as 20H2 (for ReleaseId 2009). If value is not found, it will return the ReleaseId.
118 | ///
119 | ///
120 | private string GetDisplayVersion()
121 | {
122 | string displayVersion = _registryProvider.GetValue(_displayVersionRegistry.FullPathToKey, _displayVersionRegistry.ValueName, _displayVersionRegistry.DefaultValueNotFound)?.ToString();
123 |
124 | return displayVersion ?? GetReleaseId();
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/unlockfps_nc/SetupForm.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace unlockfps_nc
2 | {
3 | partial class SetupForm
4 | {
5 | ///
6 | /// Required designer variable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Clean up any resources being used.
12 | ///
13 | /// true if managed resources should be disposed; otherwise, false.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Windows Form Designer generated code
24 |
25 | ///
26 | /// Required method for Designer support - do not modify
27 | /// the contents of this method with the code editor.
28 | ///
29 | private void InitializeComponent()
30 | {
31 | LabelResult = new Label();
32 | LabelSelect = new Label();
33 | ComboResult = new ComboBox();
34 | LabelHint = new Label();
35 | BtnBrowse = new Button();
36 | BtnConfirm = new Button();
37 | LabelCurrentPath = new Label();
38 | BrowseDialog = new OpenFileDialog();
39 | SuspendLayout();
40 | //
41 | // LabelResult
42 | //
43 | LabelResult.AutoSize = true;
44 | LabelResult.Location = new Point(12, 48);
45 | LabelResult.Name = "LabelResult";
46 | LabelResult.Size = new Size(67, 15);
47 | LabelResult.TabIndex = 0;
48 | LabelResult.Text = "LabelResult";
49 | //
50 | // LabelSelect
51 | //
52 | LabelSelect.AutoSize = true;
53 | LabelSelect.Location = new Point(12, 63);
54 | LabelSelect.Name = "LabelSelect";
55 | LabelSelect.Size = new Size(38, 15);
56 | LabelSelect.TabIndex = 1;
57 | LabelSelect.Text = "Select";
58 | //
59 | // ComboResult
60 | //
61 | ComboResult.FormattingEnabled = true;
62 | ComboResult.Location = new Point(12, 81);
63 | ComboResult.Name = "ComboResult";
64 | ComboResult.Size = new Size(435, 23);
65 | ComboResult.TabIndex = 2;
66 | //
67 | // LabelHint
68 | //
69 | LabelHint.AutoSize = true;
70 | LabelHint.Location = new Point(12, 117);
71 | LabelHint.Name = "LabelHint";
72 | LabelHint.Size = new Size(359, 45);
73 | LabelHint.TabIndex = 3;
74 | LabelHint.Text = "If your game is not listed above, you can either:\r\n1. Open the game now and the unlocker will try to find it's location\r\n2. Use the browse button below";
75 | //
76 | // BtnBrowse
77 | //
78 | BtnBrowse.Location = new Point(12, 176);
79 | BtnBrowse.Name = "BtnBrowse";
80 | BtnBrowse.Size = new Size(75, 23);
81 | BtnBrowse.TabIndex = 4;
82 | BtnBrowse.Text = "Browse";
83 | BtnBrowse.UseVisualStyleBackColor = true;
84 | BtnBrowse.Click += BtnBrowse_Click;
85 | //
86 | // BtnConfirm
87 | //
88 | BtnConfirm.Location = new Point(372, 176);
89 | BtnConfirm.Name = "BtnConfirm";
90 | BtnConfirm.Size = new Size(75, 23);
91 | BtnConfirm.TabIndex = 5;
92 | BtnConfirm.Text = "Confirm";
93 | BtnConfirm.UseVisualStyleBackColor = true;
94 | BtnConfirm.Click += BtnConfirm_Click;
95 | //
96 | // LabelCurrentPath
97 | //
98 | LabelCurrentPath.Location = new Point(12, 9);
99 | LabelCurrentPath.Name = "LabelCurrentPath";
100 | LabelCurrentPath.Size = new Size(435, 39);
101 | LabelCurrentPath.TabIndex = 6;
102 | LabelCurrentPath.Text = "LabelCurrentPath";
103 | //
104 | // BrowseDialog
105 | //
106 | BrowseDialog.Filter = "Executable Files (*.exe)|GenshinImpact.exe;YuanShen.exe";
107 | BrowseDialog.RestoreDirectory = true;
108 | BrowseDialog.Title = "Select GenshinImpact.exe or YuanShen.exe";
109 | //
110 | // SetupForm
111 | //
112 | AutoScaleDimensions = new SizeF(7F, 15F);
113 | AutoScaleMode = AutoScaleMode.Font;
114 | ClientSize = new Size(459, 211);
115 | Controls.Add(LabelCurrentPath);
116 | Controls.Add(BtnConfirm);
117 | Controls.Add(BtnBrowse);
118 | Controls.Add(LabelHint);
119 | Controls.Add(ComboResult);
120 | Controls.Add(LabelSelect);
121 | Controls.Add(LabelResult);
122 | FormBorderStyle = FormBorderStyle.FixedDialog;
123 | MaximizeBox = false;
124 | MinimizeBox = false;
125 | Name = "SetupForm";
126 | StartPosition = FormStartPosition.CenterParent;
127 | Text = "Setup";
128 | FormClosing += SetupForm_FormClosing;
129 | Load += SetupForm_Load;
130 | ResumeLayout(false);
131 | PerformLayout();
132 | }
133 |
134 | #endregion
135 |
136 | private Label LabelResult;
137 | private Label LabelSelect;
138 | private ComboBox ComboResult;
139 | private Label LabelHint;
140 | private Button BtnBrowse;
141 | private Button BtnConfirm;
142 | private Label LabelCurrentPath;
143 | private OpenFileDialog BrowseDialog;
144 | }
145 | }
--------------------------------------------------------------------------------
/unlockfps_gui/OSVersionExt/Win32API/Win32ApiEnums.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Runtime.InteropServices;
5 | using System.Text;
6 |
7 | namespace OSVersionExt.Win32API
8 | {
9 | ///
10 | /// Holds information, whether the Windows is a server, workstation or domain controller.
11 | ///
12 | public enum ProductType : byte
13 | {
14 | ///
15 | /// The operating system is Windows 10, Windows 8, Windows 7,...
16 | ///
17 | /// VER_NT_WORKSTATION
18 | Workstation = 0x0000001,
19 | ///
20 | /// The system is a domain controller and the operating system is Windows Server.
21 | ///
22 | /// VER_NT_DOMAIN_CONTROLLER
23 | DomainController = 0x0000002,
24 | ///
25 | /// The operating system is Windows Server. Note that a server that is also a domain controller
26 | /// is reported as VER_NT_DOMAIN_CONTROLLER, not VER_NT_SERVER.
27 | ///
28 | /// VER_NT_SERVER
29 | Server = 0x0000003
30 | }
31 |
32 |
33 | ///
34 | /// Holds specific information for certain Windows variants (e.g. Small Business, Datacenter,...)
35 | ///
36 | [Flags]
37 | public enum SuiteMask : ushort
38 | {
39 | ///
40 | /// Microsoft BackOffice components are installed.
41 | ///
42 | VER_SUITE_BACKOFFICE = 0x00000004,
43 | ///
44 | /// Windows Server 2003, Web Edition is installed
45 | ///
46 | VER_SUITE_BLADE = 0x00000400,
47 | ///
48 | /// Windows Server 2003, Compute Cluster Edition is installed.
49 | ///
50 | VER_SUITE_COMPUTE_SERVER = 0x00004000,
51 | ///
52 | /// Windows Server 2008 Datacenter, Windows Server 2003, Datacenter Edition, or Windows 2000 Datacenter Server is installed.
53 | ///
54 | VER_SUITE_DATACENTER = 0x00000080,
55 | ///
56 | /// Windows Server 2008 Enterprise, Windows Server 2003, Enterprise Edition, or Windows 2000 Advanced Server is installed.
57 | /// Refer to the Remarks section for more information about this bit flag.
58 | ///
59 | VER_SUITE_ENTERPRISE = 0x00000002,
60 | ///
61 | /// Windows XP Embedded is installed.
62 | ///
63 | VER_SUITE_EMBEDDEDNT = 0x00000040,
64 | ///
65 | /// Windows Vista Home Premium, Windows Vista Home Basic, or Windows XP Home Edition is installed.
66 | ///
67 | VER_SUITE_PERSONAL = 0x00000200,
68 | ///
69 | /// Remote Desktop is supported, but only one interactive session is supported. This value is set unless the system is running in application server mode.
70 | ///
71 | VER_SUITE_SINGLEUSERTS = 0x00000100,
72 | ///
73 | /// Microsoft Small Business Server was once installed on the system, but may have been upgraded to another version of Windows.
74 | /// Refer to the Remarks section for more information about this bit flag.
75 | ///
76 | VER_SUITE_SMALLBUSINESS = 0x00000001,
77 | ///
78 | /// Microsoft Small Business Server is installed with the restrictive client license in force. Refer to the Remarks section for more information about this bit flag.
79 | ///
80 | VER_SUITE_SMALLBUSINESS_RESTRICTED = 0x00000020,
81 | ///
82 | /// Windows Storage Server 2003 R2 or Windows Storage Server 2003is installed.
83 | ///
84 | VER_SUITE_STORAGE_SERVER = 0x00002000,
85 | ///
86 | /// Terminal Services is installed. This value is always set.
87 | /// If VER_SUITE_TERMINAL is set but VER_SUITE_SINGLEUSERTS is not set, the system is running in application server mode.
88 | ///
89 | VER_SUITE_TERMINAL = 0x00000010,
90 | ///
91 | /// Windows Home Server is installed.
92 | ///
93 | VER_SUITE_WH_SERVER = 0x00008000
94 |
95 | //VER_SUITE_MULTIUSERTS = 0x00020000
96 | }
97 |
98 | public enum NTSTATUS : uint
99 | {
100 | ///
101 | /// The operation completed successfully.
102 | ///
103 | STATUS_SUCCESS = 0x00000000
104 | }
105 |
106 | ///
107 | /// Contains operating system version information. The information includes major and
108 | /// minor version numbers, a build number, a platform identifier, and information about
109 | /// product suites and the latest Service Pack installed on the system.
110 | ///
111 | ///
112 | ///
113 | /// var osVersionInfo = new OSVERSIONINFOEX { OSVersionInfoSize = Marshal.SizeOf(typeof(OSVERSIONINFOEX)) };
114 | ///
115 | ///
116 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
117 | public struct OSVERSIONINFOEX
118 | {
119 | // The OSVersionInfoSize field must be set to Marshal.SizeOf(typeof(OSVERSIONINFOEX))
120 | public int OSVersionInfoSize;
121 | public int MajorVersion;
122 | public int MinorVersion;
123 | public int BuildNumber;
124 | public int PlatformId;
125 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
126 | public string CSDVersion;
127 | public ushort ServicePackMajor;
128 | public ushort ServicePackMinor;
129 | public SuiteMask SuiteMask;
130 | public ProductType ProductType;
131 | public byte Reserved;
132 | }
133 |
134 | }
135 |
--------------------------------------------------------------------------------
/unlockfps_nc/AboutForm.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 |
--------------------------------------------------------------------------------
/unlockfps_nc/SettingsForm.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Data;
5 | using System.Drawing;
6 | using System.Linq;
7 | using System.Reflection.PortableExecutable;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 | using System.Windows.Forms;
11 | using unlockfps_nc.Model;
12 | using unlockfps_nc.Service;
13 |
14 | namespace unlockfps_nc
15 | {
16 | public partial class SettingsForm : Form
17 | {
18 | private readonly ConfigService _configService;
19 | private readonly Config _config;
20 |
21 | public SettingsForm(ConfigService configService)
22 | {
23 | InitializeComponent();
24 | _configService = configService;
25 | _config = _configService.Config;
26 |
27 | SetupBindings();
28 | }
29 |
30 | private void SetupBindings()
31 | {
32 | // General
33 | CBStartMinimized.DataBindings.Add("Checked", _config, "StartMinimized", true, DataSourceUpdateMode.OnPropertyChanged);
34 | CBAutoClose.DataBindings.Add("Checked", _config, "AutoClose", true, DataSourceUpdateMode.OnPropertyChanged);
35 | CBPowerSave.DataBindings.Add("Checked", _config, "UsePowerSave", true, DataSourceUpdateMode.OnPropertyChanged);
36 | ComboPriority.DataBindings.Add("SelectedIndex", _config, "Priority", true, DataSourceUpdateMode.OnPropertyChanged);
37 |
38 | // Launch Options
39 | CBPopup.DataBindings.Add("Checked", _config, "PopupWindow", true, DataSourceUpdateMode.OnPropertyChanged);
40 | CBFullscreen.DataBindings.Add("Checked", _config, "Fullscreen", true, DataSourceUpdateMode.OnPropertyChanged);
41 | CBCustomRes.DataBindings.Add("Checked", _config, "UseCustomRes", true, DataSourceUpdateMode.OnPropertyChanged);
42 | CBUseMobileUI.DataBindings.Add("Checked", _config, "UseMobileUI", true, DataSourceUpdateMode.OnPropertyChanged);
43 | InputResX.DataBindings.Add("Value", _config, "CustomResX", true, DataSourceUpdateMode.OnPropertyChanged);
44 | InputResY.DataBindings.Add("Value", _config, "CustomResY", true, DataSourceUpdateMode.OnPropertyChanged);
45 | ComboFullscreenMode.DataBindings.Add("SelectedIndex", _config, "IsExclusiveFullscreen", true, DataSourceUpdateMode.OnPropertyChanged);
46 | InputMonitorNum.DataBindings.Add("Value", _config, "MonitorNum", true, DataSourceUpdateMode.OnPropertyChanged);
47 |
48 | // DLLs
49 | RefreshDllList();
50 | CBSuspendLoad.DataBindings.Add("Checked", _config, "SuspendLoad", true, DataSourceUpdateMode.OnPropertyChanged);
51 | }
52 |
53 | private void RefreshDllList()
54 | {
55 | _config.DllList = _config.DllList
56 | .Where(VerifyDll)
57 | .ToList();
58 |
59 | ListBoxDlls.Items.Clear();
60 | ListBoxDlls.Items.AddRange(_config.DllList.ToArray());
61 | }
62 |
63 | private void UpdateControlState()
64 | {
65 | if (_config.PopupWindow) // they can't coexist (?) so disable the other
66 | _config.Fullscreen = false;
67 |
68 | CBPopup.Enabled = !_config.Fullscreen;
69 | CBFullscreen.Enabled = !_config.PopupWindow;
70 | InputResX.Enabled = _config.UseCustomRes;
71 | InputResY.Enabled = _config.UseCustomRes;
72 | ComboFullscreenMode.Enabled = _config is { Fullscreen: true, PopupWindow: false };
73 | }
74 |
75 | public void LaunchOptionsChanged(object sender, EventArgs e)
76 | {
77 | UpdateControlState();
78 | }
79 |
80 | private void SettingsForm_Load(object sender, EventArgs e)
81 | {
82 | UpdateControlState();
83 | }
84 |
85 | private void SettingsForm_FormClosing(object sender, FormClosingEventArgs e)
86 | {
87 | _configService.Save();
88 | }
89 |
90 | private void BtnAddDll_Click(object sender, EventArgs e)
91 | {
92 | if (DllAddDialog.ShowDialog() != DialogResult.OK)
93 | return;
94 |
95 | var selectedFiles = DllAddDialog.FileNames.ToList();
96 | selectedFiles = selectedFiles
97 | .Where(x => VerifyDll(x) || MessageBox.Show(
98 | $@"Invalid File: {Environment.NewLine}{x}{Environment.NewLine}{Environment.NewLine}Only native x64 dlls are supported",
99 | @"Error", MessageBoxButtons.OK, MessageBoxIcon.Error) != DialogResult.OK)
100 | .Where(x => !_config.DllList.Contains(x))
101 | .ToList();
102 |
103 | _config.DllList.AddRange(selectedFiles);
104 | RefreshDllList();
105 | }
106 |
107 | private bool VerifyDll(string fullPath)
108 | {
109 | if (!File.Exists(fullPath))
110 | return false;
111 |
112 | using var fs = new FileStream(fullPath, FileMode.Open, FileAccess.Read);
113 | using var peReader = new PEReader(fs);
114 |
115 | if (peReader.HasMetadata)
116 | return false;
117 |
118 | return peReader.PEHeaders.CoffHeader.Machine == Machine.Amd64;
119 | }
120 |
121 | private void ListBoxDlls_Format(object sender, ListControlConvertEventArgs e)
122 | {
123 | e.Value = Path.GetFileName(e.Value as string);
124 | }
125 |
126 | private void ListBoxDlls_MouseMove(object sender, MouseEventArgs e)
127 | {
128 | var index = ListBoxDlls.IndexFromPoint(e.Location);
129 | if (index == -1)
130 | return;
131 |
132 | var toolTipText = _config.DllList[index];
133 | ToolTipSettings.SetToolTip(ListBoxDlls, toolTipText);
134 | }
135 |
136 | private void BtnRemoveDll_Click(object sender, EventArgs e)
137 | {
138 | var selectedIndex = ListBoxDlls.SelectedIndex;
139 | if (selectedIndex == -1)
140 | return;
141 |
142 | _config.DllList.RemoveAt(selectedIndex);
143 | RefreshDllList();
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/unlockfps_nc/SetupForm.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 | 17, 17
122 |
123 |
--------------------------------------------------------------------------------
/unlockfps_nc/SettingsForm.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 | 17, 17
122 |
123 |
124 | 17, 17
125 |
126 |
127 | 151, 17
128 |
129 |
--------------------------------------------------------------------------------
/unlockfps_nc/SetupForm.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Data;
5 | using System.Drawing;
6 | using System.Linq;
7 | using System.Runtime.InteropServices;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 | using System.Windows.Forms;
11 | using Microsoft.Win32;
12 | using unlockfps_nc.Model;
13 | using unlockfps_nc.Service;
14 | using unlockfps_nc.Utility;
15 |
16 | namespace unlockfps_nc
17 | {
18 | public partial class SetupForm : Form
19 | {
20 | private CancellationTokenSource _cts;
21 |
22 | private readonly ConfigService _configService;
23 | private readonly Config _config;
24 |
25 | public SetupForm(ConfigService configService)
26 | {
27 | InitializeComponent();
28 | _configService = configService;
29 | _config = _configService.Config;
30 | }
31 |
32 | private void SetupForm_Load(object sender, EventArgs e)
33 | {
34 | _cts = new();
35 | Task.Run(PollProcess, _cts.Token);
36 |
37 | LabelCurrentPath.Text = $@"Current Path: {_config.GamePath}";
38 | LabelResult.Text = @"Searching...";
39 | LabelResult.ForeColor = Color.Orange;
40 | Task.Run(SearchGamePath, _cts.Token);
41 | }
42 |
43 | private void SetupForm_FormClosing(object sender, FormClosingEventArgs e)
44 | {
45 | _cts.Cancel();
46 | _configService.Save();
47 | }
48 |
49 | private async Task PollProcess()
50 | {
51 | // System.Diagnostics.Process will throw access denied
52 | // use native win32 api instead
53 |
54 | while (!_cts.Token.IsCancellationRequested)
55 | {
56 | await Task.Delay(1000);
57 | IntPtr windowHandle = IntPtr.Zero;
58 | IntPtr processHandle = IntPtr.Zero;
59 | string processPath = string.Empty;
60 |
61 | Native.EnumWindows((hWnd, lParam) =>
62 | {
63 | const int maxCount = 256;
64 | StringBuilder sb = new StringBuilder(maxCount);
65 |
66 | Native.GetClassName(hWnd, sb, maxCount);
67 | if (sb.ToString() == "UnityWndClass")
68 | {
69 | windowHandle = hWnd;
70 | Native.GetWindowThreadProcessId(hWnd, out var pid);
71 | processPath = ProcessUtils.GetProcessPathFromPid(pid, out processHandle);
72 | return false;
73 | }
74 |
75 | return true;
76 | }, IntPtr.Zero);
77 |
78 | if (windowHandle == IntPtr.Zero)
79 | continue;
80 |
81 | Native.TerminateProcess(processHandle, 0);
82 | Native.CloseHandle(processHandle);
83 |
84 | if (string.IsNullOrEmpty(processPath))
85 | {
86 | MessageBox.Show(@"Failed to find process path\nPlease use ""Browse"" instead", @"Error",
87 | MessageBoxButtons.OK, MessageBoxIcon.Error);
88 | return;
89 | }
90 |
91 | MessageBox.Show($@"Game Found!{Environment.NewLine}{processPath}", @"Success", MessageBoxButtons.OK,
92 | MessageBoxIcon.Information);
93 |
94 | _config.GamePath = processPath;
95 | Invoke(Close);
96 | }
97 |
98 | }
99 |
100 | private void SearchGamePath()
101 | {
102 | List openedSubKeys = new();
103 |
104 | try
105 | {
106 | using var uninstallKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall");
107 | if (uninstallKey == null)
108 | return;
109 |
110 | var subKeys = uninstallKey.GetSubKeyNames()
111 | .ToList()
112 | .Where(keyName => keyName is "Genshin Impact" or "原神")
113 | .Select(uninstallKey.OpenSubKey)
114 | .Where(key => key != null)
115 | .ToList();
116 |
117 | subKeys.ForEach(openedSubKeys.Add);
118 |
119 | var launcherIniPaths = subKeys
120 | .Select(key => (string)key.GetValue("InstallPath"))
121 | .Where(path => !string.IsNullOrEmpty(path) && Directory.Exists(path))
122 | .Select(launcherPath => $@"{launcherPath}\config.ini")
123 | .ToList();
124 |
125 | List gamePaths = new();
126 | foreach (var configPath in launcherIniPaths)
127 | {
128 | var configLines = File.ReadLines(configPath);
129 | Dictionary ini = new();
130 | foreach (var line in configLines)
131 | {
132 | var split = line.Split('=', StringSplitOptions.RemoveEmptyEntries);
133 | if (split.Length < 2)
134 | continue;
135 |
136 | ini[split[0]] = split[1];
137 | }
138 |
139 | if (!ini.TryGetValue("game_install_path", out var gamePath))
140 | continue;
141 |
142 | if (!ini.TryGetValue("game_start_name", out var gameName))
143 | continue;
144 |
145 | gamePaths.Add($@"{gamePath}\{gameName}".Replace('/', '\\'));
146 | }
147 |
148 | Invoke(() =>
149 | {
150 | LabelResult.ForeColor = gamePaths.Count > 0 ? Color.Green : Color.Red;
151 | LabelResult.Text = $@"Found {gamePaths.Count} installation of the game";
152 | ComboResult.Items.AddRange(gamePaths.ToArray());
153 | if (gamePaths.Count > 0)
154 | ComboResult.SelectedIndex = 0;
155 | });
156 | }
157 | finally
158 | {
159 | openedSubKeys.ForEach(x => x.Close());
160 | }
161 | }
162 |
163 | private void BtnBrowse_Click(object sender, EventArgs e)
164 | {
165 | if (BrowseDialog.ShowDialog() != DialogResult.OK)
166 | return;
167 |
168 | var selectedFile = BrowseDialog.FileName;
169 | var fileName = Path.GetFileNameWithoutExtension(selectedFile);
170 | var directory = Path.GetDirectoryName(selectedFile);
171 |
172 | if (fileName != "GenshinImpact" && fileName != "YuanShen")
173 | {
174 | MessageBox.Show(
175 | $@"Please select the game exe{Environment.NewLine}GenshinImpact.exe or YuanShen.exe",
176 | @"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
177 | return;
178 | }
179 |
180 | var unityPlayer = Path.Combine(directory, "UnityPlayer.dll");
181 | if (!File.Exists(unityPlayer))
182 | {
183 | MessageBox.Show(@"That's not the right place", @"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
184 | return;
185 | }
186 |
187 | _config.GamePath = selectedFile;
188 | Close();
189 | }
190 |
191 | private void BtnConfirm_Click(object sender, EventArgs e)
192 | {
193 | var selectedPath = (string)ComboResult.SelectedItem;
194 | if (string.IsNullOrEmpty(selectedPath))
195 | return;
196 |
197 | _config.GamePath = selectedPath;
198 | Close();
199 | }
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/unlockfps_gui/Views/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Runtime.InteropServices;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using System.Windows.Input;
9 | using Avalonia;
10 | using Avalonia.Controls;
11 | using Avalonia.Controls.ApplicationLifetimes;
12 | using Avalonia.Interactivity;
13 | using Avalonia.Threading;
14 | using Microsoft.Extensions.DependencyInjection;
15 | using ReactiveUI;
16 | using UnlockFps.Gui.Utils;
17 | using UnlockFps.Gui.ViewModels;
18 | using UnlockFps.Gui.Views;
19 | using UnlockFps.Services;
20 |
21 | namespace UnlockFps.Gui.ViewModels
22 | {
23 | public class MainWindowViewModel : ViewModelBase
24 | {
25 | public required ProcessService ProcessService { get; init; }
26 | public required Config Config { get; init; }
27 | public required GameInstanceService GameInstanceService { get; init; }
28 |
29 | public int MinimumFps { get; set; } = 1;
30 | public int MaximumFps { get; set; } = 420;
31 |
32 | public ICommand OpenInitializationWindowCommand { get; } =
33 | ReactiveCommand.CreateFromTask(ShowWindow);
34 |
35 | public ICommand OpenSettingsWindowCommand { get; } = ReactiveCommand.CreateFromTask(ShowWindow);
36 | public ICommand OpenAboutWindowCommand { get; } = ReactiveCommand.CreateFromTask(ShowWindow);
37 |
38 | public static async Task ShowWindow() where T : Window
39 | {
40 | var window = App.DefaultServices.GetRequiredService();
41 | window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
42 | await window.ShowDialog(MainWindow);
43 | }
44 |
45 | private static Window MainWindow =>
46 | ((IClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!).MainWindow!;
47 | }
48 | }
49 |
50 | namespace UnlockFps.Gui.Views
51 | {
52 | public partial class MainWindow : Window
53 | {
54 | private readonly MainWindowViewModel _viewModel;
55 | private readonly ConfigService _configService;
56 | private readonly ProcessService _processService;
57 | private readonly TrayIcon _trayIcon;
58 |
59 | #if DEBUG
60 | public MainWindow()
61 | {
62 | if (!Design.IsDesignMode) throw new InvalidOperationException();
63 | InitializeComponent();
64 | }
65 | #endif
66 |
67 | public MainWindow(ConfigService configService, ProcessService processService, GameInstanceService gameInstanceService)
68 | {
69 | this.SetSystemChrome();
70 | DataContext = _viewModel = new MainWindowViewModel()
71 | {
72 | Config = configService.Config,
73 | ProcessService = processService,
74 | GameInstanceService = gameInstanceService
75 | };
76 | _configService = configService;
77 | _processService = processService;
78 | InitializeComponent();
79 |
80 | gameInstanceService.Start();
81 | if (WineHelper.DetectWine(out var version, out var buildId))
82 | {
83 | Title += $" (Wine {version})";
84 | }
85 | else if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
86 | {
87 | Title += $" ({Environment.OSVersion})";
88 | }
89 |
90 | _trayIcon = TrayIcon.GetIcons(Application.Current!)![0];
91 | _trayIcon.Clicked += (_, _) =>
92 | {
93 | if (WindowState == WindowState.Minimized)
94 | {
95 | Show();
96 | WindowState = WindowState.Normal;
97 | }
98 | };
99 | if (_trayIcon.Menu is { } menu)
100 | {
101 | var items = menu.Items
102 | .Where(k => k is not NativeMenuItemSeparator)
103 | .OfType()
104 | .ToArray();
105 | items[0].Click += (_, _) =>
106 | {
107 | Show();
108 | WindowState = WindowState.Normal;
109 | };
110 | items[1].Click += (_, _) => Close();
111 | }
112 | }
113 |
114 | protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
115 | {
116 | base.OnPropertyChanged(e);
117 | if (e.Property != WindowStateProperty) return;
118 | if (WindowState == WindowState.Minimized)
119 | {
120 | this.Hide();
121 | _trayIcon.IsVisible = true;
122 | _trayIcon.ToolTipText = $"{Title} (FPS: {_viewModel.Config.FpsTarget})";
123 | }
124 | else
125 | {
126 | _trayIcon.IsVisible = false;
127 | }
128 | }
129 |
130 | private async void Window_OnLoaded(object? sender, RoutedEventArgs e)
131 | {
132 | if (Design.IsDesignMode) return;
133 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
134 | {
135 | ConsoleManager.BindExitAction(() =>
136 | {
137 | Console.ForegroundColor = ConsoleColor.Yellow;
138 | Console.WriteLine("User manually closes debug window. Program will now exit.");
139 | Console.ResetColor();
140 | Thread.Sleep(1000);
141 | Dispatcher.UIThread.Invoke(Close);
142 | });
143 | }
144 |
145 | if (_viewModel.Config.AutoLaunch)
146 | {
147 | await LaunchGame(true);
148 | }
149 | }
150 |
151 | private void Window_OnClosing(object? sender, WindowClosingEventArgs e)
152 | {
153 | _configService.Save();
154 | }
155 |
156 | private void Window_OnClosed(object? sender, EventArgs e)
157 | {
158 | try
159 | {
160 | ConsoleManager.Hide();
161 | }
162 | catch
163 | {
164 | // ignored
165 | }
166 | }
167 |
168 | private async void BtnLaunchGame_OnClick(object? sender, RoutedEventArgs e)
169 | {
170 | await LaunchGame(false);
171 | }
172 |
173 | private async Task LaunchGame(bool isAutoStart)
174 | {
175 | if (!File.Exists(_viewModel.Config.LaunchOptions.GamePath))
176 | {
177 | if (isAutoStart) return;
178 | await MainWindowViewModel.ShowWindow();
179 | }
180 |
181 | if (!File.Exists(_viewModel.Config.LaunchOptions.GamePath)) return;
182 |
183 | try
184 | {
185 | _processService.Start();
186 | _viewModel.GameInstanceService.PropertyChanged += ProcessServiceOnPropertyChanged;
187 | WindowState = WindowState.Minimized;
188 | }
189 | catch (Exception ex)
190 | {
191 | await ShowErrorMessage(ex.Message);
192 | }
193 |
194 | return;
195 |
196 | void ProcessServiceOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
197 | {
198 | if (e.PropertyName != nameof(GameInstanceService.IsRunning)) return;
199 | if (!_viewModel.GameInstanceService.IsRunning)
200 | {
201 | _viewModel.GameInstanceService.PropertyChanged -= ProcessServiceOnPropertyChanged;
202 | Dispatcher.UIThread.Invoke(() =>
203 | {
204 | if (_viewModel.Config.AutoClose)
205 | {
206 | Close();
207 | }
208 | else
209 | {
210 | Show();
211 | WindowState = WindowState.Normal;
212 | }
213 | });
214 | }
215 | }
216 | }
217 |
218 | private static async Task ShowErrorMessage(string infoWindowText)
219 | {
220 | await Dispatcher.UIThread.InvokeAsync(async () =>
221 | {
222 | var infoWindow = App.DefaultServices.GetRequiredService();
223 | infoWindow.Text = infoWindowText;
224 | await infoWindow.ShowDialog(App.CurrentMainWindow!);
225 | });
226 | }
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/unlockfps_nc/MainForm.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace unlockfps_nc
2 | {
3 | partial class MainForm
4 | {
5 | ///
6 | /// Required designer variable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Clean up any resources being used.
12 | ///
13 | /// true if managed resources should be disposed; otherwise, false.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Windows Form Designer generated code
24 |
25 | ///
26 | /// Required method for Designer support - do not modify
27 | /// the contents of this method with the code editor.
28 | ///
29 | private void InitializeComponent()
30 | {
31 | components = new System.ComponentModel.Container();
32 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
33 | OptionsMenuStrip = new MenuStrip();
34 | optionsToolStripMenuItem = new ToolStripMenuItem();
35 | SettingsMenuItem = new ToolStripMenuItem();
36 | SetupMenuItem = new ToolStripMenuItem();
37 | AboutMenuItem = new ToolStripMenuItem();
38 | LabelFPS = new Label();
39 | InputFPS = new NumericUpDown();
40 | SliderFPS = new TrackBar();
41 | CBAutoStart = new CheckBox();
42 | BtnStartGame = new Button();
43 | ToolTipMain = new ToolTip(components);
44 | NotifyIconMain = new NotifyIcon(components);
45 | ContextNotify = new ContextMenuStrip(components);
46 | ExitMenuItem = new ToolStripMenuItem();
47 | OptionsMenuStrip.SuspendLayout();
48 | ((System.ComponentModel.ISupportInitialize)InputFPS).BeginInit();
49 | ((System.ComponentModel.ISupportInitialize)SliderFPS).BeginInit();
50 | ContextNotify.SuspendLayout();
51 | SuspendLayout();
52 | //
53 | // OptionsMenuStrip
54 | //
55 | OptionsMenuStrip.Items.AddRange(new ToolStripItem[] { optionsToolStripMenuItem });
56 | OptionsMenuStrip.Location = new Point(0, 0);
57 | OptionsMenuStrip.Name = "OptionsMenuStrip";
58 | OptionsMenuStrip.Size = new Size(284, 24);
59 | OptionsMenuStrip.TabIndex = 0;
60 | OptionsMenuStrip.Text = "menuStrip1";
61 | //
62 | // optionsToolStripMenuItem
63 | //
64 | optionsToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { SettingsMenuItem, SetupMenuItem, AboutMenuItem });
65 | optionsToolStripMenuItem.Name = "optionsToolStripMenuItem";
66 | optionsToolStripMenuItem.Size = new Size(61, 20);
67 | optionsToolStripMenuItem.Text = "Options";
68 | //
69 | // SettingsMenuItem
70 | //
71 | SettingsMenuItem.Name = "SettingsMenuItem";
72 | SettingsMenuItem.Size = new Size(116, 22);
73 | SettingsMenuItem.Text = "Settings";
74 | SettingsMenuItem.Click += SettingsMenuItem_Click;
75 | //
76 | // SetupMenuItem
77 | //
78 | SetupMenuItem.Name = "SetupMenuItem";
79 | SetupMenuItem.Size = new Size(116, 22);
80 | SetupMenuItem.Text = "Setup";
81 | SetupMenuItem.Click += SetupMenuItem_Click;
82 | //
83 | // AboutMenuItem
84 | //
85 | AboutMenuItem.Name = "AboutMenuItem";
86 | AboutMenuItem.Size = new Size(116, 22);
87 | AboutMenuItem.Text = "About";
88 | AboutMenuItem.Click += AboutMenuItem_Click;
89 | //
90 | // LabelFPS
91 | //
92 | LabelFPS.AutoSize = true;
93 | LabelFPS.Location = new Point(12, 33);
94 | LabelFPS.Name = "LabelFPS";
95 | LabelFPS.Size = new Size(29, 15);
96 | LabelFPS.TabIndex = 1;
97 | LabelFPS.Text = "FPS:";
98 | //
99 | // InputFPS
100 | //
101 | InputFPS.Location = new Point(47, 31);
102 | InputFPS.Maximum = new decimal(new int[] { 420, 0, 0, 0 });
103 | InputFPS.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
104 | InputFPS.Name = "InputFPS";
105 | InputFPS.Size = new Size(225, 23);
106 | InputFPS.TabIndex = 2;
107 | InputFPS.Value = new decimal(new int[] { 120, 0, 0, 0 });
108 | //
109 | // SliderFPS
110 | //
111 | SliderFPS.Location = new Point(12, 60);
112 | SliderFPS.Maximum = 420;
113 | SliderFPS.Minimum = 1;
114 | SliderFPS.Name = "SliderFPS";
115 | SliderFPS.Size = new Size(260, 45);
116 | SliderFPS.TabIndex = 3;
117 | SliderFPS.TickStyle = TickStyle.None;
118 | SliderFPS.Value = 120;
119 | //
120 | // CBAutoStart
121 | //
122 | CBAutoStart.AutoSize = true;
123 | CBAutoStart.Location = new Point(12, 90);
124 | CBAutoStart.Name = "CBAutoStart";
125 | CBAutoStart.Size = new Size(161, 19);
126 | CBAutoStart.TabIndex = 4;
127 | CBAutoStart.Text = "Start Game Automatically";
128 | ToolTipMain.SetToolTip(CBAutoStart, "This will take effect on subsequent launch");
129 | CBAutoStart.UseVisualStyleBackColor = true;
130 | //
131 | // BtnStartGame
132 | //
133 | BtnStartGame.Location = new Point(197, 86);
134 | BtnStartGame.Name = "BtnStartGame";
135 | BtnStartGame.Size = new Size(75, 23);
136 | BtnStartGame.TabIndex = 5;
137 | BtnStartGame.Text = "Start Game";
138 | BtnStartGame.UseVisualStyleBackColor = true;
139 | BtnStartGame.Click += BtnStartGame_Click;
140 | //
141 | // NotifyIconMain
142 | //
143 | NotifyIconMain.BalloonTipIcon = ToolTipIcon.Info;
144 | NotifyIconMain.BalloonTipText = "Minimized to tray";
145 | NotifyIconMain.BalloonTipTitle = "FPS Unlcoker";
146 | NotifyIconMain.ContextMenuStrip = ContextNotify;
147 | NotifyIconMain.Icon = (Icon)resources.GetObject("NotifyIconMain.Icon");
148 | NotifyIconMain.Text = "FPS Unlocker";
149 | NotifyIconMain.Visible = true;
150 | NotifyIconMain.DoubleClick += NotifyIconMain_DoubleClick;
151 | //
152 | // ContextNotify
153 | //
154 | ContextNotify.Items.AddRange(new ToolStripItem[] { ExitMenuItem });
155 | ContextNotify.Name = "ContextNotify";
156 | ContextNotify.Size = new Size(94, 26);
157 | //
158 | // ExitMenuItem
159 | //
160 | ExitMenuItem.Name = "ExitMenuItem";
161 | ExitMenuItem.Size = new Size(93, 22);
162 | ExitMenuItem.Text = "Exit";
163 | ExitMenuItem.Click += ExitMenuItem_Click;
164 | //
165 | // MainForm
166 | //
167 | AutoScaleDimensions = new SizeF(7F, 15F);
168 | AutoScaleMode = AutoScaleMode.Font;
169 | ClientSize = new Size(284, 121);
170 | Controls.Add(BtnStartGame);
171 | Controls.Add(CBAutoStart);
172 | Controls.Add(SliderFPS);
173 | Controls.Add(InputFPS);
174 | Controls.Add(LabelFPS);
175 | Controls.Add(OptionsMenuStrip);
176 | FormBorderStyle = FormBorderStyle.FixedSingle;
177 | Icon = (Icon)resources.GetObject("$this.Icon");
178 | MainMenuStrip = OptionsMenuStrip;
179 | MaximizeBox = false;
180 | Name = "MainForm";
181 | StartPosition = FormStartPosition.CenterScreen;
182 | Text = "Genshin FPS Unlocker";
183 | FormClosing += MainForm_FormClosing;
184 | Load += MainForm_Load;
185 | Resize += MainForm_Resize;
186 | OptionsMenuStrip.ResumeLayout(false);
187 | OptionsMenuStrip.PerformLayout();
188 | ((System.ComponentModel.ISupportInitialize)InputFPS).EndInit();
189 | ((System.ComponentModel.ISupportInitialize)SliderFPS).EndInit();
190 | ContextNotify.ResumeLayout(false);
191 | ResumeLayout(false);
192 | PerformLayout();
193 | }
194 |
195 | #endregion
196 |
197 | private MenuStrip OptionsMenuStrip;
198 | private ToolStripMenuItem optionsToolStripMenuItem;
199 | private ToolStripMenuItem SettingsMenuItem;
200 | private ToolStripMenuItem SetupMenuItem;
201 | private ToolStripMenuItem AboutMenuItem;
202 | private Label LabelFPS;
203 | private NumericUpDown InputFPS;
204 | private TrackBar SliderFPS;
205 | private CheckBox CBAutoStart;
206 | private Button BtnStartGame;
207 | private ToolTip ToolTipMain;
208 | private NotifyIcon NotifyIconMain;
209 | private ContextMenuStrip ContextNotify;
210 | private ToolStripMenuItem exitToolStripMenuItem;
211 | private ToolStripMenuItem ExitMenuItem;
212 | }
213 | }
214 |
--------------------------------------------------------------------------------