From 5520bc345ee4fb2f76eacc9cc9b02f784337189f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CRiceCake=E2=80=9D?= <“1250839773a@gmail.com”> Date: Thu, 25 May 2023 14:15:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 38 +++ unlockfps.sln | 31 ++ unlockfps/inireader.h | 463 ++++++++++++++++++++++++++++ unlockfps/main.cpp | 415 +++++++++++++++++++++++++ unlockfps/unlockfps.vcxproj | 156 ++++++++++ unlockfps/unlockfps.vcxproj.filters | 25 ++ unlockfps/unlockfps.vcxproj.user | 4 + 7 files changed, 1132 insertions(+) create mode 100644 README.md create mode 100644 unlockfps.sln create mode 100644 unlockfps/inireader.h create mode 100644 unlockfps/main.cpp create mode 100644 unlockfps/unlockfps.vcxproj create mode 100644 unlockfps/unlockfps.vcxproj.filters create mode 100644 unlockfps/unlockfps.vcxproj.user diff --git a/README.md b/README.md new file mode 100644 index 0000000..6846a1b --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# 原神解锁FPS限制 + +**!重要:原git已经不再维护,3.7失效,简单的维护了一下 3.7可用。这小东西我挺喜欢的** + +**感谢Euphony_Facetious以及34736384两位作者的开源** + + - 工作原理通过**WriteProcessMemory**把FPS数值写进游戏 + - 不需要通过驱动进行读写操作 + - 支持国服和外服 + - 理论上支持后续版本,不需要更新源码 + - 如果需要更新我会尽快更新 + - [下载](https://gitee.com/xiaonian233/genshin-fps-unlock/releases/) + +## 编译 + + - 用VS2019编译,其他版本的也应该可以,没测试过 +## 食用指南 + - 第一次运行的话先以管理员运行,然后手动打开游戏,这样解锁器能够获取到游戏路经并保存在配置文件里,这只需要执行一次,以后就可以直接用解锁器启动游戏了 + - 解锁器放哪都行 + - 运行之前确保游戏是关闭的 + - 用管理员运行解锁器 + - 解锁器不能关掉 +>使用管理员运行是因为游戏必须由解锁器启动,游戏本身就需要管理员权限了,所以负责启动的也是需要的 +### 默认热键 PS:按键要按一次改一次,不是长按 +- **END** 开/关 +- **右ctrl + 上方向键** 增大FPS上限 (+20) +- **右ctrl + 右方向键** 增大FPS上限 (+2) +- **右ctrl + 下方向键** 减少FPS上限 (-20) +- **右ctrl + 左方向键** 减少FPS上限 (-2) +- 源里默认的FPS数值是120 + +## 注意 +- 已经在新号上测试了两星期,目前并没有任何异常,冒险等级30 +- 使用未认证的第三方软件修改游戏数据是违反了协议条款的,后果自负 +- 想要更改热键的话,修改下源里开头的定义 ([热键码](http://cherrytree.at/misc/vk.htm)) +- 至于为啥我没写成能在和游戏同一个目录下运行是因为游戏登录的时候会进行文件完整性检测,如果游戏目录内有其他文件也会当做是游戏的文件进行检测。如果把解锁器和游戏放一起的话游戏会把解锁器当成游戏文件检测,从而导致报错(31-4302) +- 要转载的话随便,毕竟开源,可以的话就注明下出处 +- 这么个破工具请不要拿去倒卖 \ No newline at end of file diff --git a/unlockfps.sln b/unlockfps.sln new file mode 100644 index 0000000..f18b4bb --- /dev/null +++ b/unlockfps.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30907.101 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "unlockfps", "unlockfps\unlockfps.vcxproj", "{3E9E0888-3ED0-4E9C-BFEC-471549FDBD27}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3E9E0888-3ED0-4E9C-BFEC-471549FDBD27}.Debug|x64.ActiveCfg = Debug|x64 + {3E9E0888-3ED0-4E9C-BFEC-471549FDBD27}.Debug|x64.Build.0 = Debug|x64 + {3E9E0888-3ED0-4E9C-BFEC-471549FDBD27}.Debug|x86.ActiveCfg = Debug|Win32 + {3E9E0888-3ED0-4E9C-BFEC-471549FDBD27}.Debug|x86.Build.0 = Debug|Win32 + {3E9E0888-3ED0-4E9C-BFEC-471549FDBD27}.Release|x64.ActiveCfg = Release|x64 + {3E9E0888-3ED0-4E9C-BFEC-471549FDBD27}.Release|x64.Build.0 = Release|x64 + {3E9E0888-3ED0-4E9C-BFEC-471549FDBD27}.Release|x86.ActiveCfg = Release|Win32 + {3E9E0888-3ED0-4E9C-BFEC-471549FDBD27}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8D8D2DB4-C1DD-4A25-B605-F7591421E733} + EndGlobalSection +EndGlobal diff --git a/unlockfps/inireader.h b/unlockfps/inireader.h new file mode 100644 index 0000000..e7a8915 --- /dev/null +++ b/unlockfps/inireader.h @@ -0,0 +1,463 @@ +// Read an INI file into easy-to-access name/value pairs. + +// inih and INIReader are released under the New BSD license (see LICENSE.txt). +// Go to the project home page for more info: +// +// https://github.com/benhoyt/inih +/* inih -- simple .INI file parser + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ +#pragma warning( disable : 4996 ) + +#ifndef __INI_H__ +#define __INI_H__ + +/* Make this header file easier to include in C++ code */ +#ifdef __cplusplus +extern "C" { +#endif + +#include + + /* Typedef for prototype of handler function. */ + typedef int (*ini_handler)(void* user, const char* section, + const char* name, const char* value); + + /* Typedef for prototype of fgets-style reader function. */ + typedef char* (*ini_reader)(char* str, int num, void* stream); + + /* Parse given INI-style file. May have [section]s, name=value pairs + (whitespace stripped), and comments starting with ';' (semicolon). Section + is "" if name=value pair parsed before any section heading. name:value + pairs are also supported as a concession to Python's configparser. + + For each name=value pair parsed, call handler function with given user + pointer as well as section, name, and value (data only valid for duration + of handler call). Handler should return nonzero on success, zero on error. + + Returns 0 on success, line number of first error on parse error (doesn't + stop on first error), -1 on file open error, or -2 on memory allocation + error (only when INI_USE_STACK is zero). + */ + int ini_parse(const char* filename, ini_handler handler, void* user); + + /* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't + close the file when it's finished -- the caller must do that. */ + int ini_parse_file(FILE* file, ini_handler handler, void* user); + + /* Same as ini_parse(), but takes an ini_reader function pointer instead of + filename. Used for implementing custom or string-based I/O. */ + int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user); + + /* Nonzero to allow multi-line value parsing, in the style of Python's + configparser. If allowed, ini_parse() will call the handler with the same + name for each subsequent line parsed. */ +#ifndef INI_ALLOW_MULTILINE +#define INI_ALLOW_MULTILINE 1 +#endif + + /* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of + the file. See http://code.google.com/p/inih/issues/detail?id=21 */ +#ifndef INI_ALLOW_BOM +#define INI_ALLOW_BOM 1 +#endif + + /* Nonzero to allow inline comments (with valid inline comment characters + specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match + Python 3.2+ configparser behaviour. */ +#ifndef INI_ALLOW_INLINE_COMMENTS +#define INI_ALLOW_INLINE_COMMENTS 1 +#endif +#ifndef INI_INLINE_COMMENT_PREFIXES +#define INI_INLINE_COMMENT_PREFIXES ";" +#endif + + /* Nonzero to use stack, zero to use heap (malloc/free). */ +#ifndef INI_USE_STACK +#define INI_USE_STACK 1 +#endif + +/* Stop parsing on first error (default is to keep parsing). */ +#ifndef INI_STOP_ON_FIRST_ERROR +#define INI_STOP_ON_FIRST_ERROR 0 +#endif + +/* Maximum line length for any line in INI file. */ +#ifndef INI_MAX_LINE +#define INI_MAX_LINE 200 +#endif + +#ifdef __cplusplus +} +#endif + +/* inih -- simple .INI file parser + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include +#include +#include + +#if !INI_USE_STACK +#include +#endif + +#define MAX_SECTION 50 +#define MAX_NAME 50 + +/* Strip whitespace chars off end of given string, in place. Return s. */ +inline static char* rstrip(char* s) +{ + char* p = s + strlen(s); + while (p > s && isspace((unsigned char)(*--p))) + *p = '\0'; + return s; +} + +/* Return pointer to first non-whitespace char in given string. */ +inline static char* lskip(const char* s) +{ + while (*s && isspace((unsigned char)(*s))) + s++; + return (char*)s; +} + +/* Return pointer to first char (of chars) or inline comment in given string, + or pointer to null at end of string if neither found. Inline comment must + be prefixed by a whitespace character to register as a comment. */ +inline static char* find_chars_or_comment(const char* s, const char* chars) +{ +#if INI_ALLOW_INLINE_COMMENTS + int was_space = 0; + while (*s && (!chars || !strchr(chars, *s)) && + !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) { + was_space = isspace((unsigned char)(*s)); + s++; + } +#else + while (*s && (!chars || !strchr(chars, *s))) { + s++; + } +#endif + return (char*)s; +} + +/* Version of strncpy that ensures dest (size bytes) is null-terminated. */ +inline static char* strncpy0(char* dest, const char* src, size_t size) +{ + strncpy(dest, src, size); + dest[size - 1] = '\0'; + return dest; +} + +/* See documentation in header file. */ +inline int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user) +{ + /* Uses a fair bit of stack (use heap instead if you need to) */ +#if INI_USE_STACK + char line[INI_MAX_LINE]; +#else + char* line; +#endif + char section[MAX_SECTION] = ""; + char prev_name[MAX_NAME] = ""; + + char* start; + char* end; + char* name; + char* value; + int lineno = 0; + int error = 0; + +#if !INI_USE_STACK + line = (char*)malloc(INI_MAX_LINE); + if (!line) { + return -2; + } +#endif + + /* Scan through stream line by line */ + while (reader(line, INI_MAX_LINE, stream) != NULL) { + lineno++; + + start = line; +#if INI_ALLOW_BOM + if (lineno == 1 && (unsigned char)start[0] == 0xEF && + (unsigned char)start[1] == 0xBB && + (unsigned char)start[2] == 0xBF) { + start += 3; + } +#endif + start = lskip(rstrip(start)); + + if (*start == ';' || *start == '#') { + /* Per Python configparser, allow both ; and # comments at the + start of a line */ + } +#if INI_ALLOW_MULTILINE + else if (*prev_name && *start && start > line) { + +#if INI_ALLOW_INLINE_COMMENTS + end = find_chars_or_comment(start, NULL); + if (*end) + *end = '\0'; + rstrip(start); +#endif + + /* Non-blank line with leading whitespace, treat as continuation + of previous name's value (as per Python configparser). */ + if (!handler(user, section, prev_name, start) && !error) + error = lineno; + } +#endif + else if (*start == '[') { + /* A "[section]" line */ + end = find_chars_or_comment(start + 1, "]"); + if (*end == ']') { + *end = '\0'; + strncpy0(section, start + 1, sizeof(section)); + *prev_name = '\0'; + } + else if (!error) { + /* No ']' found on section line */ + error = lineno; + } + } + else if (*start) { + /* Not a comment, must be a name[=:]value pair */ + end = find_chars_or_comment(start, "=:"); + if (*end == '=' || *end == ':') { + *end = '\0'; + name = rstrip(start); + value = lskip(end + 1); +#if INI_ALLOW_INLINE_COMMENTS + end = find_chars_or_comment(value, NULL); + if (*end) + *end = '\0'; +#endif + rstrip(value); + + /* Valid name[=:]value pair found, call handler */ + strncpy0(prev_name, name, sizeof(prev_name)); + if (!handler(user, section, name, value) && !error) + error = lineno; + } + else if (!error) { + /* No '=' or ':' found on name[=:]value line */ + error = lineno; + } + } + +#if INI_STOP_ON_FIRST_ERROR + if (error) + break; +#endif + } + +#if !INI_USE_STACK + free(line); +#endif + + return error; +} + +/* See documentation in header file. */ +inline int ini_parse_file(FILE* file, ini_handler handler, void* user) +{ + return ini_parse_stream((ini_reader)fgets, file, handler, user); +} + +/* See documentation in header file. */ +inline int ini_parse(const char* filename, ini_handler handler, void* user) +{ + FILE* file = nullptr; + int error; + + file = fopen(filename, "r"); + if (!file) + return -1; + error = ini_parse_file(file, handler, user); + fclose(file); + return error; +} + +#endif /* __INI_H__ */ + + +#ifndef __INIREADER_H__ +#define __INIREADER_H__ + +#include +#include +#include + +// Read an INI file into easy-to-access name/value pairs. (Note that I've gone +// for simplicity here rather than speed, but it should be pretty decent.) +class INIReader +{ +public: + // Empty Constructor + INIReader() {}; + + // Construct INIReader and parse given filename. See ini.h for more info + // about the parsing. + INIReader(std::string filename); + + // Construct INIReader and parse given file. See ini.h for more info + // about the parsing. + INIReader(FILE* file); + + // Return the result of ini_parse(), i.e., 0 on success, line number of + // first error on parse error, or -1 on file open error. + int ParseError() const; + + // Return the list of sections found in ini file + const std::set& Sections() const; + + // Get a string value from INI file, returning default_value if not found. + std::string Get(std::string section, std::string name, + std::string default_value) const; + + // Get an integer (long) value from INI file, returning default_value if + // not found or not a valid integer (decimal "1234", "-1234", or hex "0x4d2"). + long GetInteger(std::string section, std::string name, long default_value) const; + + // Get a real (floating point double) value from INI file, returning + // default_value if not found or not a valid floating point value + // according to strtod(). + double GetReal(std::string section, std::string name, double default_value) const; + + // Get a single precision floating point number value from INI file, returning + // default_value if not found or not a valid floating point value + // according to strtof(). + float GetFloat(std::string section, std::string name, float default_value) const; + + // Get a boolean value from INI file, returning default_value if not found or if + // not a valid true/false value. Valid true values are "true", "yes", "on", "1", + // and valid false values are "false", "no", "off", "0" (not case sensitive). + bool GetBoolean(std::string section, std::string name, bool default_value) const; + +protected: + int _error; + std::map _values; + std::set _sections; + static std::string MakeKey(std::string section, std::string name); + static int ValueHandler(void* user, const char* section, const char* name, + const char* value); +}; + +#endif // __INIREADER_H__ + + +#ifndef __INIREADER__ +#define __INIREADER__ + +#include +#include +#include + +inline INIReader::INIReader(std::string filename) +{ + _error = ini_parse(filename.c_str(), ValueHandler, this); +} + +inline INIReader::INIReader(FILE* file) +{ + _error = ini_parse_file(file, ValueHandler, this); +} + +inline int INIReader::ParseError() const +{ + return _error; +} + +inline const std::set& INIReader::Sections() const +{ + return _sections; +} + +inline std::string INIReader::Get(std::string section, std::string name, std::string default_value) const +{ + std::string key = MakeKey(section, name); + return _values.count(key) ? _values.at(key) : default_value; +} + +inline long INIReader::GetInteger(std::string section, std::string name, long default_value) const +{ + std::string valstr = Get(section, name, ""); + const char* value = valstr.c_str(); + char* end; + // This parses "1234" (decimal) and also "0x4D2" (hex) + long n = strtol(value, &end, 0); + return end > value ? n : default_value; +} + +inline double INIReader::GetReal(std::string section, std::string name, double default_value) const +{ + std::string valstr = Get(section, name, ""); + const char* value = valstr.c_str(); + char* end; + double n = strtod(value, &end); + return end > value ? n : default_value; +} + +inline float INIReader::GetFloat(std::string section, std::string name, float default_value) const +{ + std::string valstr = Get(section, name, ""); + const char* value = valstr.c_str(); + char* end; + float n = strtof(value, &end); + return end > value ? n : default_value; +} + +inline bool INIReader::GetBoolean(std::string section, std::string name, bool default_value) const +{ + std::string valstr = Get(section, name, ""); + // Convert to lower case to make string comparisons case-insensitive + std::transform(valstr.begin(), valstr.end(), valstr.begin(), ::tolower); + if (valstr == "true" || valstr == "yes" || valstr == "on" || valstr == "1") + return true; + else if (valstr == "false" || valstr == "no" || valstr == "off" || valstr == "0") + return false; + else + return default_value; +} + +inline std::string INIReader::MakeKey(std::string section, std::string name) +{ + std::string key = section + "=" + name; + // Convert to lower case to make section/name lookups case-insensitive + std::transform(key.begin(), key.end(), key.begin(), ::tolower); + return key; +} + +inline int INIReader::ValueHandler(void* user, const char* section, const char* name, + const char* value) +{ + INIReader* reader = (INIReader*)user; + std::string key = MakeKey(section, name); + if (reader->_values[key].size() > 0) + reader->_values[key] += "\n"; + reader->_values[key] += value; + reader->_sections.insert(section); + return 1; +} + +#endif // __INIREADER__ \ No newline at end of file diff --git a/unlockfps/main.cpp b/unlockfps/main.cpp new file mode 100644 index 0000000..f49d4ed --- /dev/null +++ b/unlockfps/main.cpp @@ -0,0 +1,415 @@ +#define KEY_TOGGLE VK_END +#define KEY_INCREASE VK_UP +#define KEY_INCREASE_SMALL VK_RIGHT +#define KEY_DECREASE VK_DOWN +#define KEY_DECREASE_SMALL VK_LEFT +#define FPS_TARGET 120 + +#include +#include +#include +#include +#include +#include +#include "inireader.h" + +std::string GamePath{}; +int FpsValue = FPS_TARGET; + +// - д - Ŀ +uintptr_t PatternScan(void* module, const char* signature) +{ + static auto pattern_to_byte = [](const char* pattern) { + auto bytes = std::vector{}; + auto start = const_cast(pattern); + auto end = const_cast(pattern) + strlen(pattern); + + for (auto current = start; current < end; ++current) { + if (*current == '?') { + ++current; + if (*current == '?') + ++current; + bytes.push_back(-1); + } + else { + bytes.push_back(strtoul(current, ¤t, 16)); + } + } + return bytes; + }; + + auto dosHeader = (PIMAGE_DOS_HEADER)module; + auto ntHeaders = (PIMAGE_NT_HEADERS)((std::uint8_t*)module + dosHeader->e_lfanew); + + auto sizeOfImage = ntHeaders->OptionalHeader.SizeOfImage; + auto patternBytes = pattern_to_byte(signature); + auto scanBytes = reinterpret_cast(module); + + auto s = patternBytes.size(); + auto d = patternBytes.data(); + + for (auto i = 0ul; i < sizeOfImage - s; ++i) { + bool found = true; + for (auto j = 0ul; j < s; ++j) { + if (scanBytes[i + j] != d[j] && d[j] != -1) { + found = false; + break; + } + } + if (found) { + return (uintptr_t)&scanBytes[i]; + } + } + return 0; +} + +std::string GetLastErrorAsString(DWORD code) +{ + LPSTR buf = nullptr; + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&buf, 0, NULL); + std::string ret = buf; + LocalFree(buf); + return ret; +} + +// ȡĿDLLϢ +bool GetModule(DWORD pid, std::string ModuleName, PMODULEENTRY32 pEntry) +{ + if (!pEntry) + return false; + + MODULEENTRY32 mod32{}; + mod32.dwSize = sizeof(mod32); + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid); + for (Module32First(snap, &mod32); Module32Next(snap, &mod32);) + { + if (mod32.th32ProcessID != pid) + continue; + + if (mod32.szModule == ModuleName) + { + *pEntry = mod32; + break; + } + } + CloseHandle(snap); + + return pEntry->modBaseAddr; +} + +// ͨID +DWORD GetPID(std::string ProcessName) +{ + DWORD pid = 0; + PROCESSENTRY32 pe32{}; + pe32.dwSize = sizeof(pe32); + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + for (Process32First(snap, &pe32); Process32Next(snap, &pe32);) + { + if (pe32.szExeFile == ProcessName) + { + pid = pe32.th32ProcessID; + break; + } + } + CloseHandle(snap); + return pid; +} + +bool WriteConfig(std::string GamePath, int fps) +{ + HANDLE hFile = CreateFileA("fps_config.ini", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hFile == INVALID_HANDLE_VALUE) + { + DWORD code = GetLastError(); + printf("CreateFileA failed (%d): %s\n", code, GetLastErrorAsString(code).c_str()); + return false; + } + + std::string content{}; + content = "[Setting]\n"; + content += "Path=" + GamePath + "\n"; + content += "FPS=" + std::to_string(fps); + + DWORD written = 0; + WriteFile(hFile, content.data(), content.size(), &written, nullptr); + CloseHandle(hFile); +} + +void LoadConfig() +{ + if (GetFileAttributesA("config") != INVALID_FILE_ATTRIBUTES) + DeleteFileA("config"); + + INIReader reader("fps_config.ini"); + if (reader.ParseError() != 0) + { + printf("ò\n벻Ҫرմ˽ - ȻֶϷ\nֻҪһ - ڻȡϷ·\n"); + printf("\nȴϷ...\n"); + + DWORD pid = 0; + while (!(pid = GetPID("YuanShen.exe")) && !(pid = GetPID("GenshinImpact.exe"))) + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // ȡ̾ - Ȩ޺ܵ͵ - Ӧûȡ + // PROCESS_QUERY_LIMITED_INFORMATION - ڲѯ· (K32GetModuleFileNameExA) + // SYNCHRONIZE - ڵȴ̽ (WaitForSingleObject) + HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, pid); + if (!hProcess) + { + DWORD code = GetLastError(); + printf("OpenProcess failed (%d): %s", code, GetLastErrorAsString(code).c_str()); + return; + } + + char szPath[MAX_PATH]{}; + DWORD length = sizeof(szPath); + QueryFullProcessImageNameA(hProcess, 0, szPath, &length); + + GamePath = szPath; + WriteConfig(GamePath, FpsValue); + + HWND hwnd = nullptr; + while (!(hwnd = FindWindowA("UnityWndClass", nullptr))) + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + DWORD ExitCode = STILL_ACTIVE; + while (ExitCode == STILL_ACTIVE) + { + SendMessageA(hwnd, WM_CLOSE, 0, 0); + GetExitCodeProcess(hProcess, &ExitCode); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + } + + // wait for the game to close then continue + WaitForSingleObject(hProcess, -1); + CloseHandle(hProcess); + + system("cls"); + return; + } + + GamePath = reader.Get("Setting", "Path", ""); + FpsValue = reader.GetInteger("Setting", "FPS", FpsValue); + + if (GetFileAttributesA(GamePath.c_str()) == INVALID_FILE_ATTRIBUTES) + { + printf("Ϸ·ı - ʼ\n"); + DeleteFileA("config.ini"); + LoadConfig(); + } +} + +// ȼ߳ +DWORD __stdcall Thread1(LPVOID p) +{ + if (!p) + return 0; + + int* pTargetFPS = (int*)p; + int fps = *pTargetFPS; + int prev = fps; + while (true) + { + std::this_thread::sleep_for(std::chrono::milliseconds(16)); + if (GetAsyncKeyState(KEY_DECREASE) & 1 && GetAsyncKeyState(VK_RCONTROL) & 0x8000) + fps -= 20; + if (GetAsyncKeyState(KEY_DECREASE_SMALL) & 1 && GetAsyncKeyState(VK_RCONTROL) & 0x8000) + fps -= 2; + if (GetAsyncKeyState(KEY_INCREASE) & 1 && GetAsyncKeyState(VK_RCONTROL) & 0x8000) + fps += 20; + if (GetAsyncKeyState(KEY_INCREASE_SMALL) & 1 && GetAsyncKeyState(VK_RCONTROL) & 0x8000) + fps += 2; + if (GetAsyncKeyState(KEY_TOGGLE) & 1) + fps = fps != 60 ? 60 : prev; + if (prev != fps) + WriteConfig(GamePath, fps); + if (fps > 60) + prev = fps; + if (fps < 60) + fps = 60; + printf("\rFPS: %d - %s ", fps, fps > 60 ? "ON" : "OFF"); + *pTargetFPS = fps; + } + + return 0; +} + +int main(int argc, char** argv) +{ + std::atexit([] { + system("pause"); + }); + + SetConsoleTitleA(""); + + std::string CommandLine{}; + if (argc > 1) + { + for (int i = 1; i < argc; i++) + CommandLine += argv[i] + std::string(" "); + } + + // ȡ + LoadConfig(); + int TargetFPS = FpsValue; + std::string ProcessPath = GamePath; + std::string ProcessDir{}; + + if (ProcessPath.length() < 8) + return 0; + + printf("FPS v1.4.2\n"); + printf("Ϸ·: %s\n\n", ProcessPath.c_str()); + ProcessDir = ProcessPath.substr(0, ProcessPath.find_last_of("\\")); + + DWORD pid = GetPID(ProcessPath.substr(ProcessPath.find_last_of("\\") + 1)); + if (pid) + { + printf("⵽ϷУ\n"); + printf("ֶϷᵼʧЧ\n"); + printf("ֶرϷ - ԶϷ\n"); + return 0; + } + + STARTUPINFOA si{}; + PROCESS_INFORMATION pi{}; + if (!CreateProcessA(ProcessPath.c_str(), (LPSTR)CommandLine.c_str(), nullptr, nullptr, FALSE, 0, nullptr, ProcessDir.c_str(), &si, &pi)) + { + DWORD code = GetLastError(); + printf("CreateProcess failed (%d): %s", code, GetLastErrorAsString(code).c_str()); + return 0; + } + + CloseHandle(pi.hThread); + printf("PID: %d\n", pi.dwProcessId); + + // ȴUnityPlayer.dllغͻȡDLLϢ + MODULEENTRY32 hUnityPlayer{}; + MODULEENTRY32 hUserAssembly{}; + while (!GetModule(pi.dwProcessId, "UnityPlayer.dll", &hUnityPlayer)) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + while (!GetModule(pi.dwProcessId, "UserAssembly.dll", &hUserAssembly)) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + printf("UnityPlayer: %X%X\n", (uintptr_t)hUnityPlayer.modBaseAddr >> 32 & -1, hUnityPlayer.modBaseAddr); + printf("UserAssembly: %X%X\n", (uintptr_t)hUnityPlayer.modBaseAddr >> 32 & -1, hUserAssembly.modBaseAddr); + + // ڱUnityPlayer.dllСڴ - + //LPVOID mem = VirtualAlloc(nullptr, hUnityPlayer.modBaseSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + LPVOID up = VirtualAlloc(nullptr, hUnityPlayer.modBaseSize + hUserAssembly.modBaseSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + if (!up) + { + DWORD code = GetLastError(); + printf("VirtualAlloc failed (%d): %s", code, GetLastErrorAsString(code).c_str()); + return 0; + } + + // ģ + ReadProcessMemory(pi.hProcess, hUnityPlayer.modBaseAddr, up, hUnityPlayer.modBaseSize, nullptr); + LPVOID ua = (LPVOID)((uintptr_t)up + hUnityPlayer.modBaseSize); + ReadProcessMemory(pi.hProcess, hUserAssembly.modBaseAddr, ua, hUserAssembly.modBaseSize, nullptr); + printf("Searching for pattern...\n"); + /* + 7F 0F jg 0x11 + 8B 05 ? ? ? ? mov eax, dword ptr[rip+?] + */ + uintptr_t address = PatternScan(ua, "E8 ? ? ? ? 85 C0 7E 07 E8 ? ? ? ? EB 05"); + if (!address) + { + printf("outdated pattern\n"); + return 0; + } + + // Եַ (FPS) + uintptr_t pfps = 0; + { + uintptr_t rip = address; + rip += *(int32_t*)(rip + 1) + 5; + rip += *(int32_t*)(rip + 3) + 7; + uintptr_t ptr = 0; + uintptr_t data = rip - (uintptr_t)ua + (uintptr_t)hUserAssembly.modBaseAddr; + while (!ptr) + { + ReadProcessMemory(pi.hProcess, (LPCVOID)data, &ptr, sizeof(uintptr_t), nullptr); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + rip = ptr - (uintptr_t)hUnityPlayer.modBaseAddr + (uintptr_t)up; + while (*(uint8_t*)rip == 0xE8 || *(uint8_t*)rip == 0xE9) + rip += *(int32_t*)(rip + 1) + 5; + pfps = rip + *(int32_t*)(rip + 2) + 6; + pfps -= (uintptr_t)up; + printf("FPS Offset: %X\n", pfps); + pfps = (uintptr_t)hUnityPlayer.modBaseAddr + pfps; + } + + // Եַ (ֱͬ) + address = PatternScan(up, "E8 ? ? ? ? 8B E8 49 8B 1E"); + uintptr_t pvsync = 0; + if (address) + { + uintptr_t ppvsync = 0; + uintptr_t rip = address; + int32_t rel = *(int32_t*)(rip + 1); + rip = rip + rel + 5; + uint64_t rax = *(uint32_t*)(rip + 3); + ppvsync = rip + rax + 7; + ppvsync -= (uintptr_t)up; + printf("VSync Offset: %X\n", ppvsync); + ppvsync = (uintptr_t)hUnityPlayer.modBaseAddr + ppvsync; + + uintptr_t buffer = 0; + while (!buffer) + { + ReadProcessMemory(pi.hProcess, (LPCVOID)ppvsync, &buffer, sizeof(buffer), nullptr); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + rip += 7; + pvsync = *(uint32_t*)(rip + 2); + pvsync = buffer + pvsync; + } + + VirtualFree(up, 0, MEM_RELEASE); + printf("Done\n\n"); + printf("ctrl + ͷ:\n"); + printf(" ctrl + : +20\n"); + printf(" ctrl + : -20\n"); + printf(" ctrl + : -2\n"); + printf(" ctrl + : +2\n\n"); + + // ȼ߳ + HANDLE hThread = CreateThread(nullptr, 0, Thread1, &TargetFPS, 0, nullptr); + if (hThread) + CloseHandle(hThread); + + DWORD dwExitCode = STILL_ACTIVE; + while (dwExitCode == STILL_ACTIVE) + { + GetExitCodeProcess(pi.hProcess, &dwExitCode); + + // ÿһ + std::this_thread::sleep_for(std::chrono::seconds(2)); + int fps = 0; + ReadProcessMemory(pi.hProcess, (LPVOID)pfps, &fps, sizeof(fps), nullptr); + if (fps == -1) + continue; + if (fps != TargetFPS) + WriteProcessMemory(pi.hProcess, (LPVOID)pfps, &TargetFPS, sizeof(TargetFPS), nullptr); + + int vsync = 0; + ReadProcessMemory(pi.hProcess, (LPVOID)pvsync, &vsync, sizeof(vsync), nullptr); + if (vsync) + { + vsync = 0; + // رմֱͬ + WriteProcessMemory(pi.hProcess, (LPVOID)pvsync, &vsync, sizeof(vsync), nullptr); + } + } + + CloseHandle(pi.hProcess); + TerminateProcess((HANDLE)-1, 0); + + return 0; +} \ No newline at end of file diff --git a/unlockfps/unlockfps.vcxproj b/unlockfps/unlockfps.vcxproj new file mode 100644 index 0000000..3b4544c --- /dev/null +++ b/unlockfps/unlockfps.vcxproj @@ -0,0 +1,156 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {3e9e0888-3ed0-4e9c-bfec-471549fdbd27} + unlockfps + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + MultiByte + + + Application + false + v143 + true + MultiByte + + + + + + + + + + + + + + + + + + + + + true + + + false + + + true + + + false + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + TurnOffAllWarnings + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + stdcpp17 + + + Console + true + RequireAdministrator + + + + + TurnOffAllWarnings + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + stdcpp17 + + + Console + true + true + false + RequireAdministrator + + + + + + + + + + + + \ No newline at end of file diff --git a/unlockfps/unlockfps.vcxproj.filters b/unlockfps/unlockfps.vcxproj.filters new file mode 100644 index 0000000..4e9e0aa --- /dev/null +++ b/unlockfps/unlockfps.vcxproj.filters @@ -0,0 +1,25 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + + + + \ No newline at end of file diff --git a/unlockfps/unlockfps.vcxproj.user b/unlockfps/unlockfps.vcxproj.user new file mode 100644 index 0000000..88a5509 --- /dev/null +++ b/unlockfps/unlockfps.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file