├── .gitignore ├── .cargo └── config.toml ├── rust-toolchain.toml ├── .editorconfig ├── src ├── lib.rs ├── logger.rs ├── process │ └── module.rs ├── process.rs └── main.rs ├── Cargo.toml ├── LICENSE ├── Cargo.lock ├── README.md ├── flake.lock └── flake.nix /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /result 3 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "x86_64-pc-windows-gnu" 3 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | targets = ["x86_64-pc-windows-gnu"] 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod logger; 2 | pub mod process; 3 | 4 | pub mod utils { 5 | pub fn str_to_w_vec(s: &str) -> Vec { 6 | s.encode_utf16().chain(::core::iter::once(0)).collect() 7 | } 8 | 9 | pub fn w_to_str(wide: &[u16]) -> String { 10 | let i = wide.iter().cloned().take_while(|&c| c != 0); 11 | char::decode_utf16(i) 12 | .map(|r| r.unwrap_or(char::REPLACEMENT_CHARACTER)) 13 | .collect() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | use log::{LevelFilter, Log, Metadata, Record, SetLoggerError}; 2 | 3 | pub struct TinyLogger; 4 | 5 | impl Log for TinyLogger { 6 | fn enabled(&self, _metadata: &Metadata) -> bool { 7 | true 8 | } 9 | 10 | fn log(&self, record: &Record) { 11 | eprintln!( 12 | "[{}][{}] {}", 13 | record.level(), 14 | record.target(), 15 | record.args() 16 | ); 17 | } 18 | 19 | fn flush(&self) {} 20 | } 21 | 22 | impl TinyLogger { 23 | pub fn init(self) -> Result<(), SetLoggerError> { 24 | log::set_boxed_logger(Box::new(self)).map(|()| log::set_max_level(LevelFilter::Trace)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "genshin-force-fps" 3 | version = "0.2.3" 4 | authors = ["y0soro "] 5 | edition = "2021" 6 | rust-version = "1.56" 7 | repository = "https://github.com/y0soro/genshin-force-fps-rs" 8 | license = "MIT" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [profile.release] 13 | strip = true 14 | lto = true 15 | codegen-units = 1 16 | # panic = "abort" 17 | 18 | [dependencies] 19 | lexopt = "0.2.1" 20 | patternscan = "1.2.0" 21 | 22 | [dependencies.log] 23 | version = "0.4.17" 24 | features = ["std"] 25 | 26 | [dependencies.windows] 27 | version = "0.37.0" 28 | features = [ 29 | "alloc", 30 | "Win32_Foundation", 31 | "Win32_Security", 32 | "Win32_Storage_FileSystem", 33 | "Win32_System_Diagnostics_Debug", 34 | "Win32_System_Memory", 35 | "Win32_System_ProcessStatus", 36 | "Win32_System_Registry", 37 | "Win32_System_Threading", 38 | ] 39 | 40 | [build-dependencies] 41 | embed-manifest = "1.3.1" 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 y0soro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/process/module.rs: -------------------------------------------------------------------------------- 1 | use core::ffi::c_void; 2 | use std::io::Cursor; 3 | 4 | use patternscan; 5 | use windows::Win32::System::Memory::{VirtualFree, MEM_RELEASE}; 6 | 7 | #[derive(Debug)] 8 | pub struct Module { 9 | pub(super) base_addr: *mut c_void, 10 | pub(super) base_size: usize, 11 | pub(super) snapshot_mem: *mut c_void, 12 | } 13 | 14 | impl Module { 15 | pub fn pattern_scan(&self, pattern: &str) -> Option<*mut u8> { 16 | unsafe { 17 | let mem_slice = 18 | ::core::slice::from_raw_parts_mut(self.snapshot_mem as *mut u8, self.base_size); 19 | 20 | let offset = patternscan::scan_first_match(Cursor::new(mem_slice), pattern).ok()??; 21 | Some(self.base_addr.add(offset) as _) 22 | } 23 | } 24 | 25 | #[allow(clippy::not_unsafe_ptr_arg_deref)] 26 | pub fn snapshot_addr(&self, ps_addr: *mut u8) -> *mut u8 { 27 | unsafe { 28 | let offset = ps_addr.offset_from(self.base_addr as _); 29 | if offset < 0 || offset >= self.base_size as isize { 30 | panic!( 31 | "{:?} out of bounds, [{:?}, {:?}]", 32 | ps_addr, 33 | self.base_addr, 34 | self.base_addr.add(self.base_size) 35 | ); 36 | } 37 | self.snapshot_mem.offset(offset) as *mut u8 38 | } 39 | } 40 | } 41 | 42 | impl Drop for Module { 43 | fn drop(&mut self) { 44 | unsafe { 45 | VirtualFree(self.snapshot_mem, 0, MEM_RELEASE); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "embed-manifest" 7 | version = "1.4.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "41cd446c890d6bed1d8b53acef5f240069ebef91d6fae7c5f52efe61fe8b5eae" 10 | 11 | [[package]] 12 | name = "genshin-force-fps" 13 | version = "0.2.3" 14 | dependencies = [ 15 | "embed-manifest", 16 | "lexopt", 17 | "log", 18 | "patternscan", 19 | "windows", 20 | ] 21 | 22 | [[package]] 23 | name = "lexopt" 24 | version = "0.2.1" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "478ee9e62aaeaf5b140bd4138753d1f109765488581444218d3ddda43234f3e8" 27 | 28 | [[package]] 29 | name = "log" 30 | version = "0.4.20" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 33 | 34 | [[package]] 35 | name = "patternscan" 36 | version = "1.2.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "cbf9ac94ae7c3d7f743ec57e0b6b05077631c1a90c6cea9b162a69efa6d6bbde" 39 | 40 | [[package]] 41 | name = "windows" 42 | version = "0.37.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" 45 | dependencies = [ 46 | "windows_aarch64_msvc", 47 | "windows_i686_gnu", 48 | "windows_i686_msvc", 49 | "windows_x86_64_gnu", 50 | "windows_x86_64_msvc", 51 | ] 52 | 53 | [[package]] 54 | name = "windows_aarch64_msvc" 55 | version = "0.37.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" 58 | 59 | [[package]] 60 | name = "windows_i686_gnu" 61 | version = "0.37.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" 64 | 65 | [[package]] 66 | name = "windows_i686_msvc" 67 | version = "0.37.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" 70 | 71 | [[package]] 72 | name = "windows_x86_64_gnu" 73 | version = "0.37.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" 76 | 77 | [[package]] 78 | name = "windows_x86_64_msvc" 79 | version = "0.37.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Genshin Force FPS 2 | 3 | This is almost a RIIR(rewrite it in Rust) for [genshin-fps-unlock](https://github.com/34736384/genshin-fps-unlock) but without GUI. 4 | 5 | ## Features 6 | - Unlock the 30/60 FPS limit in game, you can force any frame rate limit as you want 7 | - CLI, i.e. no overhead 8 | - Cross build 9 | 10 | ## Usage 11 | 12 | ``` 13 | Genshin Force FPS 14 | 15 | USAGE: 16 | genshin-force-fps.exe [OPTIONS] -- [GAME_ARGS] 17 | OPTIONS: 18 | -h, --help Prints help information 19 | --hdr Force enable HDR support 20 | -f, --fps NUMBER Force game FPS, defaults to 120 21 | -c, --cwd PATH Path to working dir that game process runs on 22 | -o, --open PATH Path to GenshinImpact.exe/YuanShen.exe, can be 23 | omitted if it's installed on default location (C:) 24 | ARGS: 25 | [GAME_ARGS] Unity player arguments passing to game executable, 26 | https://docs.unity3d.com/Manual/PlayerCommandLineArguments.html 27 | EXAMPLE: 28 | # Force FPS to 120 and specify game path 29 | genshin-force-fps.exe -f 120 -o C:\path\to\GenshinImpact.exe 30 | # Force FPS to 144 and append Unity cli arguments, assuming the game was 31 | # installed on default location 32 | genshin-force-fps.exe -f 144 -- -screen-width 1600 -screen-height 900 -screen-fullscreen 0 33 | ``` 34 | 35 | The option `-o/--open` can be omitted if the game was installed on "C:\Program Files\Genshin Impact\Genshin Impact Game\". 36 | 37 | After launching, the tool will first start the game and sniffing the memory addresses of fps value, then monitor those values using `ReadProcessMemory` and force them using `WriteProcessMemory` if not equal to what user specified at 1 second interval respectively . 38 | 39 | ### Windows 40 | 41 | Create a file shortcut with arguments appended to target path or launch a terminal to specify the arguments. Or use batch script. 42 | 43 | If the game was installed on default location and you are fine with default 120 fps setting, then just double click the "genshin-force-fps.exe". 44 | 45 | ### Lutris/Linux 46 | 47 | Change game executable path to path of genshin-force-fps.exe, and specifying the game path with option `-o/--open` instead. For example, 48 | 49 | - Executable: `/path/to/genshin-force-fps.exe` 50 | - Arguments: `-f 144 -o 'C:\\Program Files\Genshin Impact\Genshin Impact Game\GenshinImpact.exe'` 51 | 52 | The game path has to be Windows path in current WINEPREFIX environment instead of Unix path on host machine since this tool is still a Windows program. 53 | 54 | ## Cross Build on Linux 55 | 56 | ### Generic 57 | 58 | Install `mingw-w64-gcc` and follow instructions in https://wiki.archlinux.org/title/Rust#Windows to setup build environment. 59 | 60 | ```bash 61 | $ cargo build --target x86_64-pc-windows-gnu 62 | $ ls ./target/x86_64-pc-windows-gnu/*/*.exe 63 | ``` 64 | 65 | ### Nix 66 | 67 | 1. Follow https://nixos.org/download.html#download-nix to setup Nix environment or install `nix` from your package manager 68 | 2. Enable Nix flakes experimental features, see https://nixos.wiki/wiki/Flakes 69 | 70 | ```bash 71 | $ nix build 72 | $ # or in fully qualified path 73 | $ nix build ".#packages.x86_64-linux.default" 74 | $ ls ./result/bin 75 | ``` 76 | 77 | ## Troubleshooting 78 | 79 | ### Game crashes on event screen 80 | 81 | Change current working dir to somewhere other than parent dir of game executable. 82 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fenix": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ], 8 | "rust-analyzer-src": "rust-analyzer-src" 9 | }, 10 | "locked": { 11 | "lastModified": 1703312464, 12 | "narHash": "sha256-eGFvUZCK5F56+A1NmJ1kqAMBlajsr33lgOmSwlpvFec=", 13 | "owner": "nix-community", 14 | "repo": "fenix", 15 | "rev": "7c6af11a6d9d4929fcaedd94eec9007795e52778", 16 | "type": "github" 17 | }, 18 | "original": { 19 | "owner": "nix-community", 20 | "repo": "fenix", 21 | "type": "github" 22 | } 23 | }, 24 | "flake-utils": { 25 | "inputs": { 26 | "systems": "systems" 27 | }, 28 | "locked": { 29 | "lastModified": 1701680307, 30 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", 31 | "owner": "numtide", 32 | "repo": "flake-utils", 33 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", 34 | "type": "github" 35 | }, 36 | "original": { 37 | "owner": "numtide", 38 | "repo": "flake-utils", 39 | "type": "github" 40 | } 41 | }, 42 | "naersk": { 43 | "inputs": { 44 | "nixpkgs": [ 45 | "nixpkgs" 46 | ] 47 | }, 48 | "locked": { 49 | "lastModified": 1698420672, 50 | "narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=", 51 | "owner": "nix-community", 52 | "repo": "naersk", 53 | "rev": "aeb58d5e8faead8980a807c840232697982d47b9", 54 | "type": "github" 55 | }, 56 | "original": { 57 | "owner": "nix-community", 58 | "repo": "naersk", 59 | "type": "github" 60 | } 61 | }, 62 | "nixpkgs": { 63 | "locked": { 64 | "lastModified": 1703134684, 65 | "narHash": "sha256-SQmng1EnBFLzS7WSRyPM9HgmZP2kLJcPAz+Ug/nug6o=", 66 | "owner": "NixOS", 67 | "repo": "nixpkgs", 68 | "rev": "d6863cbcbbb80e71cecfc03356db1cda38919523", 69 | "type": "github" 70 | }, 71 | "original": { 72 | "owner": "NixOS", 73 | "ref": "nixpkgs-unstable", 74 | "repo": "nixpkgs", 75 | "type": "github" 76 | } 77 | }, 78 | "root": { 79 | "inputs": { 80 | "fenix": "fenix", 81 | "flake-utils": "flake-utils", 82 | "naersk": "naersk", 83 | "nixpkgs": "nixpkgs" 84 | } 85 | }, 86 | "rust-analyzer-src": { 87 | "flake": false, 88 | "locked": { 89 | "lastModified": 1703247226, 90 | "narHash": "sha256-ZXWDkDwD2QXWrMe+n7H5fhAfqs0entHszLib+iEMN58=", 91 | "owner": "rust-lang", 92 | "repo": "rust-analyzer", 93 | "rev": "afbb8f31ff0fb66cb7f6ae89606727d01c0a8153", 94 | "type": "github" 95 | }, 96 | "original": { 97 | "owner": "rust-lang", 98 | "ref": "nightly", 99 | "repo": "rust-analyzer", 100 | "type": "github" 101 | } 102 | }, 103 | "systems": { 104 | "locked": { 105 | "lastModified": 1681028828, 106 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 107 | "owner": "nix-systems", 108 | "repo": "default", 109 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 110 | "type": "github" 111 | }, 112 | "original": { 113 | "owner": "nix-systems", 114 | "repo": "default", 115 | "type": "github" 116 | } 117 | } 118 | }, 119 | "root": "root", 120 | "version": 7 121 | } 122 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | naersk = { 5 | url = "github:nix-community/naersk"; 6 | inputs.nixpkgs.follows = "nixpkgs"; 7 | }; 8 | fenix = { 9 | url = "github:nix-community/fenix"; 10 | inputs.nixpkgs.follows = "nixpkgs"; 11 | }; 12 | flake-utils.url = "github:numtide/flake-utils"; 13 | }; 14 | 15 | outputs = { self, nixpkgs, naersk, fenix, flake-utils }: 16 | flake-utils.lib.eachDefaultSystem ( 17 | system: 18 | let 19 | pkgs = nixpkgs.legacyPackages.${system}; 20 | toolchain = with fenix.packages.${system}; 21 | combine [ 22 | minimal.rustc 23 | minimal.cargo 24 | targets.x86_64-pc-windows-gnu.latest.rust-std 25 | ]; 26 | naersk-lib = naersk.lib.${system}.override { 27 | cargo = toolchain; 28 | rustc = toolchain; 29 | }; 30 | in 31 | rec { 32 | packages.default = packages.x86_64-pc-windows-gnu; 33 | # The rust compiler is internally a cross compiler, so a single 34 | # toolchain can be used to compile multiple targets. In a hermetic 35 | # build system like nix flakes, there's effectively one package for 36 | # every permutation of the supported hosts and targets. 37 | # i.e.: nix build .#packages.x86_64-linux.x86_64-pc-windows-gnu 38 | # where x86_64-linux is the host and x86_64-pc-windows-gnu is the 39 | # target 40 | packages.x86_64-pc-windows-gnu = naersk-lib.buildPackage { 41 | src = ./.; 42 | 43 | nativeBuildInputs = with pkgs; [ 44 | pkgsCross.mingwW64.stdenv.cc 45 | # Used for running tests. 46 | #wineWowPackages.stable 47 | # wineWowPackages is overkill, but it's built in CI for nixpkgs, 48 | # so it doesn't need to be built from source. It needs to provide 49 | # wine64 not just wine. An alternative would be this: 50 | # (wineMinimal.override { wineBuild = "wine64"; }) 51 | ]; 52 | 53 | buildInputs = with pkgs.pkgsCross.mingwW64.windows; [ mingw_w64_pthreads pthreads ]; 54 | 55 | # See: https://github.com/nix-community/naersk/issues/181#issuecomment-874352470 56 | preBuild = '' 57 | export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-C link-args=''$(echo $NIX_LDFLAGS | tr ' ' '\n' | grep -- '^-L' | tr '\n' ' ')" 58 | export NIX_LDFLAGS= 59 | ''; 60 | 61 | # Configures the target which will be built. 62 | # ref: https://doc.rust-lang.org/cargo/reference/config.html#buildtarget 63 | CARGO_BUILD_TARGET = "x86_64-pc-windows-gnu"; 64 | 65 | # Configures the linker which will be used. cc.targetPrefix is 66 | # sometimes different than the targets used by rust. i.e.: the 67 | # mingw-w64 linker is "x86_64-w64-mingw32-gcc" whereas the rust 68 | # target is "x86_64-pc-windows-gnu". 69 | # 70 | # This is only necessary if rustc doesn't already know the correct linker to use. 71 | # 72 | # ref: https://doc.rust-lang.org/cargo/reference/config.html#targettriplelinker 73 | #CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER = with pkgs.pkgsCross.mingwW64.stdenv; 74 | # "${cc}/bin/${cc.targetPrefix}gcc"; 75 | 76 | # Configures the script which should be used to run tests. Since 77 | # this is compiled for 64-bit Windows, use wine64 to run the tests. 78 | # ref: https://doc.rust-lang.org/cargo/reference/config.html#targettriplerunner 79 | #CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUNNER = pkgs.writeScript "wine-wrapper" '' 80 | # # Without this, wine will error out when attempting to create the 81 | # # prefix in the build's homeless shelter. 82 | # export WINEPREFIX="$(mktemp -d)" 83 | # exec wine64 $@ 84 | #''; 85 | 86 | doCheck = false; 87 | 88 | # Multi-stage builds currently fail for mingwW64. 89 | singleStep = true; 90 | }; 91 | } 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/process.rs: -------------------------------------------------------------------------------- 1 | pub mod module; 2 | 3 | use core::ffi::c_void; 4 | use core::mem; 5 | use core::ptr; 6 | 7 | use windows::core::{PCWSTR, PWSTR}; 8 | use windows::Win32::Foundation::{ 9 | CloseHandle, GetLastError, HANDLE, HINSTANCE, MAX_PATH, STILL_ACTIVE, 10 | }; 11 | use windows::Win32::System::Diagnostics::Debug::{ReadProcessMemory, WriteProcessMemory}; 12 | use windows::Win32::System::Memory::{ 13 | VirtualAlloc, VirtualFree, MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE, 14 | }; 15 | use windows::Win32::System::ProcessStatus::{ 16 | K32EnumProcessModules, K32GetModuleBaseNameW, K32GetModuleInformation, MODULEINFO, 17 | }; 18 | use windows::Win32::System::Threading::{ 19 | CreateProcessW, GetExitCodeProcess, PROCESS_CREATION_FLAGS, PROCESS_INFORMATION, STARTUPINFOW, 20 | }; 21 | 22 | use crate::utils::*; 23 | 24 | #[derive(Debug)] 25 | pub struct Process { 26 | handle: HANDLE, 27 | _pid: u32, 28 | } 29 | 30 | impl Process { 31 | pub fn create(exec_path: &str, exec_wd: Option<&str>, args: &str) -> Result { 32 | let exec_wd_holder: Vec; 33 | let exec_wd = if let Some(s) = exec_wd { 34 | exec_wd_holder = str_to_w_vec(s); 35 | PCWSTR(exec_wd_holder.as_ptr()) 36 | } else { 37 | PCWSTR::default() 38 | }; 39 | 40 | let mut args = str_to_w_vec(args); 41 | let ps_info = &mut PROCESS_INFORMATION::default(); 42 | 43 | unsafe { 44 | let ok = CreateProcessW( 45 | exec_path, 46 | PWSTR(args.as_mut_ptr()), 47 | ptr::null(), 48 | ptr::null(), 49 | false, 50 | PROCESS_CREATION_FLAGS(0), 51 | ptr::null(), 52 | exec_wd, 53 | &STARTUPINFOW::default(), 54 | ps_info, 55 | ) 56 | .as_bool(); 57 | if !ok { 58 | return Err(format!("failed to create process: {:?}", GetLastError())); 59 | } 60 | CloseHandle(ps_info.hThread); 61 | }; 62 | 63 | Ok(Self { 64 | handle: ps_info.hProcess, 65 | _pid: ps_info.dwProcessId, 66 | }) 67 | } 68 | 69 | unsafe fn enum_modules(&self) -> Result, String> { 70 | let mut lpcbneeded: u32 = 1024; 71 | let mut lphmodule = vec![]; 72 | 73 | while lphmodule.len() < lpcbneeded as _ { 74 | lphmodule.resize(lpcbneeded as _, HINSTANCE::default()); 75 | 76 | let ok = K32EnumProcessModules( 77 | self.handle, 78 | lphmodule.as_mut_ptr(), 79 | (lphmodule.len() * mem::size_of::()) as u32, 80 | &mut lpcbneeded, 81 | ) 82 | .as_bool(); 83 | if !ok { 84 | return Err("failed to enum modules".to_owned()); 85 | } 86 | } 87 | lphmodule.truncate(lpcbneeded as _); 88 | Ok(lphmodule) 89 | } 90 | 91 | pub fn get_module(&self, name: &str) -> Result { 92 | unsafe { 93 | let hmodule = 'outer: { 94 | for hmodule in self.enum_modules()? { 95 | let mut module_name: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize]; 96 | K32GetModuleBaseNameW(self.handle, hmodule, &mut module_name); 97 | let module_name = w_to_str(&module_name); 98 | if module_name == name { 99 | break 'outer hmodule; 100 | } 101 | } 102 | return Err("module not found".to_owned()); 103 | }; 104 | 105 | let mut info = MODULEINFO::default(); 106 | let ok = K32GetModuleInformation( 107 | self.handle, 108 | hmodule, 109 | &mut info, 110 | mem::size_of_val(&info) as _, 111 | ) 112 | .as_bool(); 113 | if !ok { 114 | return Err("failed to get module info".to_owned()); 115 | } 116 | let base_addr = info.lpBaseOfDll; 117 | let base_size = info.SizeOfImage as usize; 118 | 119 | let snapshot_mem = VirtualAlloc( 120 | ptr::null(), 121 | base_size, 122 | MEM_COMMIT | MEM_RESERVE, 123 | PAGE_READWRITE, 124 | ); 125 | if snapshot_mem.is_null() { 126 | return Err("failed to allocate snapshot mem".to_owned()); 127 | } 128 | 129 | let ok = ReadProcessMemory( 130 | self.handle, 131 | base_addr, 132 | snapshot_mem, 133 | base_size, 134 | ptr::null_mut(), 135 | ) 136 | .as_bool(); 137 | if !ok { 138 | VirtualFree(snapshot_mem, 0, MEM_RELEASE); 139 | return Err("failed to read module memory".to_owned()); 140 | } 141 | 142 | Ok(module::Module { 143 | base_addr, 144 | base_size, 145 | snapshot_mem, 146 | }) 147 | } 148 | } 149 | 150 | pub fn read(&self, base: *const u8) -> Result { 151 | unsafe { 152 | let mut buffer: T = mem::zeroed(); 153 | 154 | let ok = ReadProcessMemory( 155 | self.handle, 156 | base as *const c_void, 157 | &mut buffer as *mut _ as *mut c_void, 158 | mem::size_of_val(&buffer), 159 | ptr::null_mut(), 160 | ) 161 | .as_bool(); 162 | if !ok { 163 | return Err("failed to read memory".to_owned()); 164 | } 165 | Ok(buffer) 166 | } 167 | } 168 | 169 | pub fn write(&self, base: *const u8, value: &T) -> Result<(), String> { 170 | unsafe { 171 | let ok = WriteProcessMemory( 172 | self.handle, 173 | base as *const c_void, 174 | value as *const _ as *const c_void, 175 | mem::size_of_val(value), 176 | ptr::null_mut(), 177 | ) 178 | .as_bool(); 179 | if !ok { 180 | return Err("failed to write memory".to_owned()); 181 | } 182 | } 183 | Ok(()) 184 | } 185 | 186 | pub fn is_active(&self) -> bool { 187 | unsafe { 188 | let mut exitcode: u32 = 0; 189 | let ok = GetExitCodeProcess(self.handle, &mut exitcode as *mut _).as_bool(); 190 | if !ok { 191 | return false; 192 | } 193 | exitcode == STILL_ACTIVE.0 as u32 194 | } 195 | } 196 | } 197 | 198 | impl Drop for Process { 199 | fn drop(&mut self) { 200 | unsafe { 201 | CloseHandle(self.handle); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use core::time::Duration; 2 | use std::error::Error; 3 | use std::thread::sleep; 4 | 5 | use genshin_force_fps::logger::TinyLogger; 6 | use genshin_force_fps::process::module::Module; 7 | use genshin_force_fps::process::Process; 8 | use genshin_force_fps::utils::*; 9 | 10 | use log::{error, info, warn}; 11 | use windows::core::PCWSTR; 12 | use windows::Win32::Storage::FileSystem::{GetFileAttributesW, INVALID_FILE_ATTRIBUTES}; 13 | use windows::Win32::System::Registry::{ 14 | RegOpenCurrentUser, RegSetKeyValueW, HKEY, KEY_WRITE, REG_DWORD, 15 | }; 16 | 17 | const HELP: &str = "\ 18 | Genshin Force FPS 19 | 20 | USAGE: 21 | genshin-force-fps.exe [OPTIONS] -- [GAME_ARGS] 22 | OPTIONS: 23 | -h, --help Prints help information 24 | --hdr Force enable HDR support 25 | -f, --fps NUMBER Force game FPS, defaults to 120 26 | -c, --cwd PATH Path to working dir that game process runs on 27 | -o, --open PATH Path to GenshinImpact.exe/YuanShen.exe, can be 28 | omitted if it's installed on default location (C:) 29 | ARGS: 30 | [GAME_ARGS] Unity player arguments passing to game executable, 31 | https://docs.unity3d.com/Manual/PlayerCommandLineArguments.html 32 | EXAMPLE: 33 | # Force FPS to 120 and specify game path 34 | genshin-force-fps.exe -f 120 -o C:\\path\\to\\GenshinImpact.exe 35 | # Force FPS to 144 and append Unity cli arguments, assuming the game was 36 | # installed on default location 37 | genshin-force-fps.exe -f 144 -- -screen-width 1600 -screen-height 900 -screen-fullscreen 0 38 | "; 39 | 40 | const DEFAULT_GAME_PATHS: &[&str] = &[ 41 | "C:\\Program Files\\Genshin Impact\\Genshin Impact Game\\GenshinImpact.exe", 42 | "C:\\Program Files\\Genshin Impact\\Genshin Impact Game\\YuanShen.exe", 43 | ]; 44 | 45 | struct Args { 46 | game_path: Option, 47 | game_cwd: Option, 48 | enable_hdr: bool, 49 | fps: i32, 50 | game_args: Vec, 51 | } 52 | 53 | fn parse_env_args() -> Result { 54 | use lexopt::prelude::*; 55 | 56 | let mut game_path: Option = None; 57 | let mut game_cwd: Option = None; 58 | let mut enable_hdr = false; 59 | let mut fps: i32 = 120; 60 | let mut game_args: Vec = vec![]; 61 | 62 | let mut parser = lexopt::Parser::from_env(); 63 | while let Some(arg) = parser.next()? { 64 | match arg { 65 | Short('h') | Long("help") => { 66 | println!("{}", HELP); 67 | std::process::exit(0); 68 | } 69 | Long("hdr") => enable_hdr = true, 70 | Short('n') | Long("no-disable-vsync") => { 71 | warn!( 72 | "VSYNC disabling has been removed, this flag {:?} has no effect", 73 | arg 74 | ); 75 | } 76 | Short('f') | Long("fps") => { 77 | fps = parser.value()?.parse()?; 78 | fps = ::core::cmp::max(1, fps); 79 | } 80 | Short('c') | Long("cwd") => { 81 | game_cwd = Some(parser.value()?.parse()?); 82 | } 83 | Short('o') | Long("open") => { 84 | game_path = Some(parser.value()?.parse()?); 85 | } 86 | Value(val) => { 87 | game_args.push(val.into_string()?); 88 | } 89 | _ => return Err(arg.unexpected()), 90 | } 91 | } 92 | 93 | Ok(Args { 94 | game_path, 95 | game_cwd, 96 | enable_hdr, 97 | fps, 98 | game_args, 99 | }) 100 | } 101 | 102 | fn main() -> Result<(), Box> { 103 | TinyLogger.init()?; 104 | let Args { 105 | game_path, 106 | game_cwd, 107 | enable_hdr, 108 | mut game_args, 109 | fps, 110 | } = parse_env_args()?; 111 | 112 | let game_path = match game_path { 113 | Some(s) => { 114 | if !path_exists(&s) { 115 | eprintln!("{}", HELP); 116 | eprintln!("Game path {} doesn't exists!", s); 117 | std::process::exit(1); 118 | } 119 | s 120 | } 121 | None => loop { 122 | if let Some(possible_path) = game_args.first() { 123 | if path_exists(possible_path) { 124 | break game_args.remove(0); 125 | } 126 | } 127 | let res = DEFAULT_GAME_PATHS.iter().find(|i| path_exists(i)); 128 | if let Some(&s) = res { 129 | break s.to_owned(); 130 | } else { 131 | eprintln!("{}", HELP); 132 | eprintln!("Please specify the game path with option -o"); 133 | std::process::exit(1); 134 | } 135 | }, 136 | }; 137 | 138 | if enable_hdr { 139 | set_hdr_reg()?; 140 | info!("set HDR registry value") 141 | } 142 | 143 | let game_args = game_args.join(" "); 144 | if !game_args.is_empty() { 145 | info!("launching {} {}", game_path, game_args); 146 | } else { 147 | info!("launching {}", game_path); 148 | } 149 | let ps = Process::create(&game_path, game_cwd.as_deref(), &game_args)?; 150 | let m_up = loop { 151 | sleep(Duration::from_millis(200)); 152 | match ps.get_module("UnityPlayer.dll") { 153 | Ok(m) => break m, 154 | Err(s) => { 155 | error!("{}", s); 156 | } 157 | } 158 | if !ps.is_active() { 159 | return Ok(()); 160 | } 161 | }; 162 | 163 | sleep(Duration::from_millis(5000)); 164 | let m_ua = loop { 165 | sleep(Duration::from_millis(200)); 166 | match ps.get_module("UserAssembly.dll") { 167 | Ok(m) => break m, 168 | Err(s) => { 169 | error!("{}", s); 170 | } 171 | } 172 | if !ps.is_active() { 173 | return Ok(()); 174 | } 175 | }; 176 | 177 | let p_fps = scan_fps_ptr(&ps, &m_up, &m_ua) 178 | .map_err(|e| error!("{}", e)) 179 | .ok(); 180 | 181 | info!( 182 | "scan result: p_fps:{}", 183 | p_fps.map_or("failure".to_string(), |v| format!("{:?}", v)), 184 | ); 185 | drop(m_up); 186 | 187 | let Some(p_fps) = p_fps else { 188 | info!("failed to scan pointer of fps value, exit in 10s"); 189 | sleep(Duration::from_secs(10)); 190 | std::process::exit(1); 191 | }; 192 | 193 | loop { 194 | if !ps.is_active() { 195 | return Ok(()); 196 | } 197 | sleep(Duration::from_secs(1)); 198 | 199 | let res = ps.read::(p_fps); 200 | if let Ok(v) = res { 201 | if v != fps && v >= 0 { 202 | let res = ps.write::(p_fps, &fps); 203 | if res.is_err() { 204 | error!("failed to write FPS"); 205 | } else { 206 | info!("force FPS: {} -> {}", v, fps); 207 | } 208 | } 209 | } 210 | } 211 | } 212 | 213 | fn set_hdr_reg() -> windows::core::Result<()> { 214 | unsafe { 215 | let mut h_key = HKEY::default(); 216 | RegOpenCurrentUser(KEY_WRITE.0, &mut h_key).ok()?; 217 | 218 | let sub_keys = [r"SOFTWARE\miHoYo\原神", r"SOFTWARE\miHoYo\Genshin Impact"]; 219 | 220 | for sub_key in sub_keys { 221 | let sub_key = str_to_w_vec(sub_key); 222 | let value_name = str_to_w_vec("WINDOWS_HDR_ON_h3132281285"); 223 | let value = 1i32; 224 | RegSetKeyValueW( 225 | h_key, 226 | PCWSTR(sub_key.as_ptr()), 227 | PCWSTR(value_name.as_ptr()), 228 | REG_DWORD.0, 229 | &value as *const _ as _, 230 | core::mem::size_of_val(&value) as _, 231 | ) 232 | .ok()?; 233 | } 234 | } 235 | Ok(()) 236 | } 237 | 238 | #[inline] 239 | unsafe fn extract_address( 240 | m: &Module, 241 | p_inst: *mut u8, 242 | address_offset: usize, 243 | inst_len: usize, 244 | ) -> *mut u8 { 245 | let rel = (m.snapshot_addr(p_inst.add(address_offset)) as *mut i32).read_unaligned() as isize; 246 | p_inst.offset(rel + inst_len as isize) 247 | } 248 | 249 | fn scan_fps_ptr(ps: &Process, m_up: &Module, m_ua: &Module) -> Result<*mut u8, Box> { 250 | let p_fps_anchor = m_ua 251 | .pattern_scan("B9 3C 00 00 00 FF 15") 252 | .ok_or("FPS anchor pattern not found, try updating this tools")?; 253 | unsafe { 254 | let pp_func_fps = extract_address(m_ua, p_fps_anchor.offset(5), 2, 6); 255 | 256 | let mut p_func_fps = loop { 257 | let p = ps.read::(pp_func_fps)?; 258 | if p == 0 { 259 | sleep(Duration::from_millis(200)); 260 | continue; 261 | } 262 | break (p as *mut u8); 263 | }; 264 | 265 | loop { 266 | let inst = *m_up.snapshot_addr(p_func_fps); 267 | match inst { 268 | // CALL 269 | 0xe8 | 0xe9 => { 270 | p_func_fps = extract_address(m_up, p_func_fps, 1, 5); 271 | continue; 272 | } 273 | _ => break, 274 | } 275 | } 276 | 277 | let p_fps = extract_address(m_up, p_func_fps, 2, 6); 278 | Ok(p_fps) 279 | } 280 | } 281 | 282 | // 4.2 or before 283 | fn _scan_fps_ptr_legacy_4_3( 284 | ps: &Process, 285 | m_up: &Module, 286 | m_ua: &Module, 287 | ) -> Result<*mut u8, Box> { 288 | let p_fps_anchor = m_ua 289 | .pattern_scan("E8 ? ? ? ? 85 C0 7E 07 E8 ? ? ? ? EB 05") 290 | .ok_or("FPS anchor pattern not found, try updating this tools")?; 291 | unsafe { 292 | let p_func_indirect = extract_address(m_ua, p_fps_anchor, 1, 5); 293 | 294 | let pp_func_fps = extract_address(m_ua, p_func_indirect, 3, 7); 295 | 296 | let mut p_func_fps = loop { 297 | let p = ps.read::(pp_func_fps)?; 298 | if p == 0 { 299 | sleep(Duration::from_millis(200)); 300 | continue; 301 | } 302 | break (p as *mut u8); 303 | }; 304 | 305 | loop { 306 | let inst = *m_up.snapshot_addr(p_func_fps); 307 | match inst { 308 | // CALL 309 | 0xe8 | 0xe9 => { 310 | p_func_fps = extract_address(m_up, p_func_fps, 1, 5); 311 | continue; 312 | } 313 | _ => break, 314 | } 315 | } 316 | 317 | let p_fps = extract_address(m_up, p_func_fps, 2, 6); 318 | Ok(p_fps) 319 | } 320 | } 321 | 322 | // 3.6 or before 323 | fn _scan_fps_ptr_legacy_3_6(m_up: &Module) -> Result<*mut u8, Box> { 324 | let p_fps_anchor = m_up 325 | .pattern_scan("7F 0F 8B 05 ? ? ? ?") 326 | .ok_or("FPS anchor pattern not found")?; 327 | unsafe { 328 | let p_fps = extract_address(m_up, p_fps_anchor, 4, 8); 329 | Ok(p_fps) 330 | } 331 | } 332 | 333 | // broken 334 | fn _scan_vsync_ptr(ps: &Process, m_up: &Module) -> Result<*mut u8, Box> { 335 | let p_vsync_anchor = m_up 336 | .pattern_scan("E8 ? ? ? ? 8B E8 49 8B 1E") 337 | .ok_or("VSync anchor pattern not found, try updating this tools")?; 338 | unsafe { 339 | let p_func_read_vsync = extract_address(m_up, p_vsync_anchor, 1, 5); 340 | 341 | let pp_vsync_base = extract_address(m_up, p_func_read_vsync, 3, 7); 342 | 343 | let vsync_offset = 344 | (m_up.snapshot_addr(p_func_read_vsync.add(9)) as *mut i32).read_unaligned() as isize; 345 | 346 | let p_vsync_base = loop { 347 | let p = ps.read::(pp_vsync_base)?; 348 | if p == 0 { 349 | sleep(Duration::from_millis(200)); 350 | continue; 351 | } 352 | break (p as *mut u8); 353 | }; 354 | Ok(p_vsync_base.offset(vsync_offset)) 355 | } 356 | } 357 | 358 | fn path_exists(path: &str) -> bool { 359 | unsafe { 360 | let attrs = GetFileAttributesW(path); 361 | attrs != INVALID_FILE_ATTRIBUTES 362 | } 363 | } 364 | --------------------------------------------------------------------------------