From 77e34624af800b72c6c7e828bef7bb9e68610340 Mon Sep 17 00:00:00 2001 From: Loomeh Date: Thu, 11 Jul 2024 06:46:16 +0100 Subject: [PATCH 01/18] add signature scanning --- src/auto-splitter.c | 3 + src/signature.c | 185 ++++++++++++++++++++++++++++++++++++++++++++ src/signature.h | 21 +++++ 3 files changed, 209 insertions(+) create mode 100644 src/signature.c create mode 100644 src/signature.h diff --git a/src/auto-splitter.c b/src/auto-splitter.c index fd067de..1debfe1 100644 --- a/src/auto-splitter.c +++ b/src/auto-splitter.c @@ -15,6 +15,7 @@ #include "auto-splitter.h" #include "memory.h" #include "process.h" +#include "signature.h" #include "settings.h" char auto_splitter_file[PATH_MAX]; @@ -325,6 +326,8 @@ void run_auto_splitter() lua_setglobal(L, "readAddress"); lua_pushcfunction(L, getPid); lua_setglobal(L, "getPID"); + lua_pushcfunction(L, find_signature); + lua_setglobal(L, "signatureScan"); char current_file[PATH_MAX]; strcpy(current_file, auto_splitter_file); diff --git a/src/signature.c b/src/signature.c new file mode 100644 index 0000000..d3626eb --- /dev/null +++ b/src/signature.c @@ -0,0 +1,185 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Include your headers +#include "signature.h" +#include "memory.h" +#include "process.h" + +#include + +extern game_process process; + +MemoryRegion* get_memory_regions(pid_t pid, int* count) +{ + char maps_path[256]; + sprintf(maps_path, "/proc/%d/maps", pid); + FILE* maps_file = fopen(maps_path, "r"); + if (!maps_file) { + perror("Failed to open maps file"); + return NULL; + } + + MemoryRegion* regions = NULL; + int capacity = 0; + *count = 0; + + char line[256]; + while (fgets(line, sizeof(line), maps_file)) + { + if (*count >= capacity) + { + capacity = capacity == 0 ? 10 : capacity * 2; + regions = (MemoryRegion*)realloc(regions, capacity * sizeof(MemoryRegion)); + if (!regions) { + perror("Failed to allocate memory for regions"); + fclose(maps_file); + return NULL; + } + } + + uintptr_t start, end; + if (sscanf(line, "%lx-%lx", &start, &end) != 2) { + continue; // Skip lines that don't match the expected format + } + regions[*count].start = start; + regions[*count].end = end; + (*count)++; + } + + fclose(maps_file); + return regions; +} + +bool match_pattern(const uint8_t* data, const int* pattern, size_t pattern_size) +{ + for (size_t i = 0; i < pattern_size; ++i) + { + if (pattern[i] != -1 && data[i] != pattern[i]) + return false; + } + return true; +} + +int* convert_signature(const char* signature, size_t* pattern_size) +{ + char* signature_copy = strdup(signature); + if (!signature_copy) { + return NULL; + } + + char* token = strtok(signature_copy, " "); + size_t size = 0; + size_t capacity = 10; + int* pattern = (int*)malloc(capacity * sizeof(int)); + if (!pattern) { + free(signature_copy); + return NULL; + } + + while (token != NULL) + { + if (size >= capacity) + { + capacity *= 2; + int* temp = (int*)realloc(pattern, capacity * sizeof(int)); + if (!temp) { + free(pattern); + free(signature_copy); + return NULL; + } + pattern = temp; + } + + if (strcmp(token, "??") == 0) + pattern[size] = -1; + else + pattern[size] = strtol(token, NULL, 16); + size++; + token = strtok(NULL, " "); + } + + free(signature_copy); + *pattern_size = size; + return pattern; +} + +bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t size) +{ + struct iovec local_iov = { buffer, size }; + struct iovec remote_iov = { (void*)address, size }; + ssize_t nread = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0); + + return nread == size; +} + +int find_signature(lua_State* L) +{ + pid_t pid = process.pid; + const char* signature = lua_tostring(L, 1); + size_t pattern_length; + + int* pattern = convert_signature(signature, &pattern_length); + if (!pattern) { + printf("Failed to convert signature\n"); + return -1; // Return an error code + } + + int regions_count = 0; + MemoryRegion* regions = get_memory_regions(process.pid, ®ions_count); + if (!regions) { + printf("Failed to get memory regions\n"); + free(pattern); + return -1; // Return an error code + } + + for (int i = 0; i < regions_count; i++) + { + MemoryRegion region = regions[i]; + ssize_t region_size = region.end - region.start; + uint8_t* buffer = (uint8_t*)malloc(region_size); + if (!buffer) + { + printf("Failed to allocate memory for buffer\n"); + free(pattern); + free(regions); + return -1; // Return an error code + } + + if (!validate_process_memory(pid, region.start, buffer, region_size)) + { + printf("Failed to read memory\n"); + free(pattern); + free(regions); + free(buffer); + return -1; // Return an error code + } + else + { + for (size_t i = 0; i <= region_size - pattern_length; ++i) + { + if (match_pattern(buffer + i, pattern, pattern_length)) + { + uintptr_t result = region.start + i; + free(pattern); + free(regions); + free(buffer); + return result; + } + } + } + + free(buffer); + } + + free(pattern); + free(regions); + return -1; // Return an error code if no match is found +} \ No newline at end of file diff --git a/src/signature.h b/src/signature.h new file mode 100644 index 0000000..48bdf6d --- /dev/null +++ b/src/signature.h @@ -0,0 +1,21 @@ +#ifndef __SIGNATURE_H__ +#define __SIGNATURE_H__ + +#include +#include +#include +#include +#include + +typedef struct { + uintptr_t start; + uintptr_t end; +} MemoryRegion; + +MemoryRegion* get_memory_regions(pid_t pid, int* count); +bool match_pattern(const uint8_t* data, const int* pattern, size_t pattern_size); +int* convert_signature(const char* signature, size_t* pattern_size); +bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t size); +int find_signature(lua_State* L); + +#endif /* __SIGNATURE_H__ */ \ No newline at end of file From 59380708f4d5116d626df8b4e28de34f2b3b7e98 Mon Sep 17 00:00:00 2001 From: Loomeh Date: Thu, 11 Jul 2024 09:29:49 +0100 Subject: [PATCH 02/18] bug fixes --- src/auto-splitter.c | 6 ++-- src/signature.c | 79 +++++++++++++++++++-------------------------- src/signature.h | 8 ++--- 3 files changed, 40 insertions(+), 53 deletions(-) diff --git a/src/auto-splitter.c b/src/auto-splitter.c index 1debfe1..796b36a 100644 --- a/src/auto-splitter.c +++ b/src/auto-splitter.c @@ -15,8 +15,8 @@ #include "auto-splitter.h" #include "memory.h" #include "process.h" -#include "signature.h" #include "settings.h" +#include "signature.h" char auto_splitter_file[PATH_MAX]; int refresh_rate = 60; @@ -324,10 +324,10 @@ void run_auto_splitter() lua_setglobal(L, "process"); lua_pushcfunction(L, read_address); lua_setglobal(L, "readAddress"); + lua_pushcfunction(L, find_signature); + lua_setglobal(L, "sig_scan"); lua_pushcfunction(L, getPid); lua_setglobal(L, "getPID"); - lua_pushcfunction(L, find_signature); - lua_setglobal(L, "signatureScan"); char current_file[PATH_MAX]; strcpy(current_file, auto_splitter_file); diff --git a/src/signature.c b/src/signature.c index d3626eb..6c6e7a2 100644 --- a/src/signature.c +++ b/src/signature.c @@ -1,17 +1,16 @@ +#include +#include +#include +#include #include #include #include -#include -#include -#include #include -#include -#include +#include -// Include your headers -#include "signature.h" #include "memory.h" #include "process.h" +#include "signature.h" #include @@ -32,10 +31,8 @@ MemoryRegion* get_memory_regions(pid_t pid, int* count) *count = 0; char line[256]; - while (fgets(line, sizeof(line), maps_file)) - { - if (*count >= capacity) - { + while (fgets(line, sizeof(line), maps_file)) { + if (*count >= capacity) { capacity = capacity == 0 ? 10 : capacity * 2; regions = (MemoryRegion*)realloc(regions, capacity * sizeof(MemoryRegion)); if (!regions) { @@ -60,8 +57,7 @@ MemoryRegion* get_memory_regions(pid_t pid, int* count) bool match_pattern(const uint8_t* data, const int* pattern, size_t pattern_size) { - for (size_t i = 0; i < pattern_size; ++i) - { + for (size_t i = 0; i < pattern_size; ++i) { if (pattern[i] != -1 && data[i] != pattern[i]) return false; } @@ -84,10 +80,8 @@ int* convert_signature(const char* signature, size_t* pattern_size) return NULL; } - while (token != NULL) - { - if (size >= capacity) - { + while (token != NULL) { + if (size >= capacity) { capacity *= 2; int* temp = (int*)realloc(pattern, capacity * sizeof(int)); if (!temp) { @@ -128,51 +122,42 @@ int find_signature(lua_State* L) int* pattern = convert_signature(signature, &pattern_length); if (!pattern) { - printf("Failed to convert signature\n"); - return -1; // Return an error code + lua_pushinteger(L, 0); + return 1; } int regions_count = 0; MemoryRegion* regions = get_memory_regions(process.pid, ®ions_count); if (!regions) { - printf("Failed to get memory regions\n"); free(pattern); - return -1; // Return an error code + lua_pushinteger(L, 0); + return 1; } - for (int i = 0; i < regions_count; i++) - { + for (int i = 0; i < regions_count; i++) { MemoryRegion region = regions[i]; ssize_t region_size = region.end - region.start; uint8_t* buffer = (uint8_t*)malloc(region_size); - if (!buffer) - { - printf("Failed to allocate memory for buffer\n"); + if (!buffer) { free(pattern); free(regions); - return -1; // Return an error code + lua_pushinteger(L, 0); + return 1; } - if (!validate_process_memory(pid, region.start, buffer, region_size)) - { - printf("Failed to read memory\n"); - free(pattern); - free(regions); + if (!validate_process_memory(pid, region.start, buffer, region_size)) { free(buffer); - return -1; // Return an error code + continue; // Continue to next region } - else - { - for (size_t i = 0; i <= region_size - pattern_length; ++i) - { - if (match_pattern(buffer + i, pattern, pattern_length)) - { - uintptr_t result = region.start + i; - free(pattern); - free(regions); - free(buffer); - return result; - } + + for (size_t j = 0; j <= region_size - pattern_length; ++j) { + if (match_pattern(buffer + j, pattern, pattern_length)) { + uintptr_t result = region.start + j; + free(buffer); + free(pattern); + free(regions); + lua_pushinteger(L, result); // Ensure this pushes the correct address + return 1; // Return the number of values pushed onto the stack } } @@ -181,5 +166,7 @@ int find_signature(lua_State* L) free(pattern); free(regions); - return -1; // Return an error code if no match is found + + lua_pushinteger(L, 0); // Push 0 if no match is found in any region + return 1; // Return the number of values pushed onto the stack } \ No newline at end of file diff --git a/src/signature.h b/src/signature.h index 48bdf6d..68f016a 100644 --- a/src/signature.h +++ b/src/signature.h @@ -1,11 +1,11 @@ #ifndef __SIGNATURE_H__ #define __SIGNATURE_H__ -#include -#include -#include -#include #include +#include +#include +#include +#include typedef struct { uintptr_t start; From 1a87cbd2f91e89dd5142df851b57f9e11ef07ee9 Mon Sep 17 00:00:00 2001 From: Loomeh Date: Thu, 11 Jul 2024 10:23:15 +0100 Subject: [PATCH 03/18] offset handling --- src/signature.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/signature.c b/src/signature.c index 6c6e7a2..de97835 100644 --- a/src/signature.c +++ b/src/signature.c @@ -118,6 +118,13 @@ int find_signature(lua_State* L) { pid_t pid = process.pid; const char* signature = lua_tostring(L, 1); + const char* offset_str = lua_tostring(L, 2); + + int offset = 0; // Default offset value + if (offset_str != NULL) { + offset = strtol(offset_str, NULL, 0); + } + size_t pattern_length; int* pattern = convert_signature(signature, &pattern_length); @@ -126,6 +133,12 @@ int find_signature(lua_State* L) return 1; } + // Apply the offset to the first two bytes of the pattern + if (pattern_length >= 2 && pattern[0] != -1 && pattern[1] != -1) { + uint16_t* master_address = (uint16_t*)pattern; + *master_address += offset; + } + int regions_count = 0; MemoryRegion* regions = get_memory_regions(process.pid, ®ions_count); if (!regions) { @@ -146,6 +159,7 @@ int find_signature(lua_State* L) } if (!validate_process_memory(pid, region.start, buffer, region_size)) { + printf("Failed to read memory region: %lx-%lx\n", region.start, region.end); free(buffer); continue; // Continue to next region } @@ -169,4 +183,4 @@ int find_signature(lua_State* L) lua_pushinteger(L, 0); // Push 0 if no match is found in any region return 1; // Return the number of values pushed onto the stack -} \ No newline at end of file +} From 76311b4297f1a34f4b3b233027670140a98678b3 Mon Sep 17 00:00:00 2001 From: Loomeh Date: Thu, 11 Jul 2024 10:41:54 +0100 Subject: [PATCH 04/18] Update signature.c --- src/signature.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/signature.c b/src/signature.c index de97835..e667493 100644 --- a/src/signature.c +++ b/src/signature.c @@ -116,7 +116,7 @@ bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t int find_signature(lua_State* L) { - pid_t pid = process.pid; + pid_t p_pid = process.pid; const char* signature = lua_tostring(L, 1); const char* offset_str = lua_tostring(L, 2); @@ -140,7 +140,7 @@ int find_signature(lua_State* L) } int regions_count = 0; - MemoryRegion* regions = get_memory_regions(process.pid, ®ions_count); + MemoryRegion* regions = get_memory_regions(p_pid, ®ions_count); if (!regions) { free(pattern); lua_pushinteger(L, 0); @@ -158,7 +158,7 @@ int find_signature(lua_State* L) return 1; } - if (!validate_process_memory(pid, region.start, buffer, region_size)) { + if (!validate_process_memory(p_pid, region.start, buffer, region_size)) { printf("Failed to read memory region: %lx-%lx\n", region.start, region.end); free(buffer); continue; // Continue to next region From 4a70d1de626e88f82e3977bad72c9ff8286e0b41 Mon Sep 17 00:00:00 2001 From: Loomeh Date: Thu, 11 Jul 2024 23:04:34 +0100 Subject: [PATCH 05/18] convert sig scan result to hex before returning --- src/signature.c | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/signature.c b/src/signature.c index e667493..1f2e0d3 100644 --- a/src/signature.c +++ b/src/signature.c @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -118,27 +119,15 @@ int find_signature(lua_State* L) { pid_t p_pid = process.pid; const char* signature = lua_tostring(L, 1); - const char* offset_str = lua_tostring(L, 2); - - int offset = 0; // Default offset value - if (offset_str != NULL) { - offset = strtol(offset_str, NULL, 0); - } + int offset = lua_tointeger(L, 2); // Get the offset as an integer directly size_t pattern_length; - int* pattern = convert_signature(signature, &pattern_length); if (!pattern) { lua_pushinteger(L, 0); return 1; } - // Apply the offset to the first two bytes of the pattern - if (pattern_length >= 2 && pattern[0] != -1 && pattern[1] != -1) { - uint16_t* master_address = (uint16_t*)pattern; - *master_address += offset; - } - int regions_count = 0; MemoryRegion* regions = get_memory_regions(p_pid, ®ions_count); if (!regions) { @@ -159,18 +148,22 @@ int find_signature(lua_State* L) } if (!validate_process_memory(p_pid, region.start, buffer, region_size)) { - printf("Failed to read memory region: %lx-%lx\n", region.start, region.end); + // printf("Failed to read memory region: %lx-%lx\n", region.start, region.end); free(buffer); continue; // Continue to next region } for (size_t j = 0; j <= region_size - pattern_length; ++j) { if (match_pattern(buffer + j, pattern, pattern_length)) { - uintptr_t result = region.start + j; + uintptr_t result = region.start + j + offset; // Apply the offset here + char hex_str[20]; // Buffer to hold the hexadecimal string representation + sprintf(hex_str, "%lx", result); // Convert result to hexadecimal string + free(buffer); free(pattern); free(regions); - lua_pushinteger(L, result); // Ensure this pushes the correct address + + lua_pushstring(L, hex_str); // Push the hexadecimal string onto the Lua stack return 1; // Return the number of values pushed onto the stack } } From da0a1d66a2042d96dabefdfcae2201aa22cae595 Mon Sep 17 00:00:00 2001 From: Loomeh Date: Thu, 11 Jul 2024 23:13:43 +0100 Subject: [PATCH 06/18] add sig_scan to docs --- docs/auto-splitters.md | 617 +++++++++++++++++++++++++++++++---------- 1 file changed, 478 insertions(+), 139 deletions(-) diff --git a/docs/auto-splitters.md b/docs/auto-splitters.md index 344a0d5..f88ba5c 100644 --- a/docs/auto-splitters.md +++ b/docs/auto-splitters.md @@ -1,329 +1,668 @@ # Auto Splitters + + * Auto splitters are scripts that automate the process of timing your run in a game by making splits automatically, starting, resetting, pausing, etc. + + # How do they work? + + * These work by reading into game's memory and determining when the timer should do something, like make a split. + + * LibreSplit's autosplitting system works in a very similar way to LiveSplit's. The main difference is that LibreSplit uses Lua instead of C#. There are also some key differences: - * Runs an entire Lua system instead of only supporting specifically named C# blocks. - * This means you can run external functions outside of the ones LibreSplit executes. - * Support for the entire Lua language, including the importing of libraries for tasks such as performance monitoring. -# How to make LibreSplit auto splitters +* Runs an entire Lua system instead of only supporting specifically named C# blocks. + +* This means you can run external functions outside of the ones LibreSplit executes. + +* Support for the entire Lua language, including the importing of libraries for tasks such as performance monitoring. + + + +# How to make LibreSplit auto splitters + + * It's somewhat easy if you know what you are doing or are porting an already existing one. + + * First in the lua script goes a `process` function call with the name of the games process: + + ```lua + process('GameBlaBlaBla.exe') + ``` + * With this line, LibreSplit will repeatedly attempt to find this process and will not continue script execution until it is found. + + * Next we have to define the basic functions. Not all are required and the ones that are required may change depending on the game or end goal, like if loading screens are included or not. - * The order at which these run is the same as they are documented below. + +* The order at which these run is the same as they are documented below. + + ### `startup` - The purpose of this function is to specify how many times LibreSplit checks memory values and executes functions each second, the default is 60Hz. Usually, 60Hz is fine and this function can remain undefined. However, it's there if you need it. + +The purpose of this function is to specify how many times LibreSplit checks memory values and executes functions each second, the default is 60Hz. Usually, 60Hz is fine and this function can remain undefined. However, it's there if you need it. + ```lua + process('GameBlaBlaBla.exe') -function startup() - refreshRate = 120 + + +function startup() + +refreshRate = 120 + end + ``` + + ### `state` - The main purpose of this function is to assign memory values to Lua variables. + +The main purpose of this function is to assign memory values to Lua variables. + * Runs every 1000 / `refreshRate` milliseconds and when the script is enabled/loaded. + + ```lua + process('GameBlaBlaBla.exe') + + local isLoading = false; -function startup() - refreshRate = 120 + + +function startup() + +refreshRate = 120 + end -function state() - isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + + +function state() + +isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + end + ``` + + * You may have noticed that we're assigning this `isLoading` variable with the result of the function `readAddress`. This function is part of LibreSplit's Lua context and its purpose is to read memory values. It's explained in detail at the bottom of this document. + + ### `update` - The purpose of this function is to update local variables. + +The purpose of this function is to update local variables. + * Runs every 1000 / `refreshRate` milliseconds. + ```lua + process('GameBlaBlaBla.exe') + + local current = {isLoading = false}; + local old = {isLoading = false}; + local loadCount = 0 -function startup() - refreshRate = 120 + + +function startup() + +refreshRate = 120 + end -function state() - old.isLoading = current.isLoading; + + +function state() + +old.isLoading = current.isLoading; + + + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + +end + + + +function update() + +if not current.isLoading and old.isLoading then + +loadCount = loadCount + 1; - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); end -function update() - if not current.isLoading and old.isLoading then - loadCount = loadCount + 1; - end end + ``` + * We now have 3 variables, one represents the current state while the other the old state of isLoading, we also have loadCount getting updated in the `update` function which will store how many times we've entered the loading screen + + ### `start` + This tells LibreSplit when to start the timer.\ + _Note: LibreSplit will ignore any start calls if the timer is running._ + * Runs every 1000 / `refreshRate` milliseconds. + ```lua + process('GameBlaBlaBla.exe') + + local current = {isLoading = false}; + local old = {isLoading = false}; + local loadCount = 0 -function startup() - refreshRate = 120 + + +function startup() + +refreshRate = 120 + end -function state() - old.isLoading = current.isLoading; + + +function state() + +old.isLoading = current.isLoading; + + + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); end -function update() - if not current.isLoading and old.isLoading then - loadCount = loadCount + 1; - end + + +function update() + +if not current.isLoading and old.isLoading then + +loadCount = loadCount + 1; + end -function start() - return current.isLoading end + + + +function start() + +return current.isLoading + +end + ``` + + ### `split` + Tells LibreSplit to execute a split whenever it gets a true return. - * Runs every 1000 / `refreshRate` milliseconds. + +* Runs every 1000 / `refreshRate` milliseconds. + ```lua + process('GameBlaBlaBla.exe') + + local current = {isLoading = false}; + local old = {isLoading = false}; + local loadCount = 0 -function startup() - refreshRate = 120 + + +function startup() + +refreshRate = 120 + end -function state() - old.isLoading = current.isLoading; + + +function state() + +old.isLoading = current.isLoading; + + + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); end -function update() - if not current.isLoading and old.isLoading then - loadCount = loadCount + 1; - end + + +function update() + +if not current.isLoading and old.isLoading then + +loadCount = loadCount + 1; + end -function start() - return current.isLoading end -function split() - local shouldSplit = false; - if current.isLoading and not old.isLoading then - loadCount = loadCount + 1; - shouldSplit = loadCount > 1; - end + + +function start() + +return current.isLoading - return shouldSplit; end + + + +function split() + +local shouldSplit = false; + +if current.isLoading and not old.isLoading then + +loadCount = loadCount + 1; + +shouldSplit = loadCount > 1; + +end + + + +return shouldSplit; + +end + ``` + * Whoa lots of code, why didnt we just return if we are currently in a loading screen like in start? Because if we do, we will do multiple splits a second, the function runs multiple times and it would do lots of unwanted splits. + * To solve that, we only want to split when we enter a loading screen (old is false, current is true), but we also don't want to split on the first loading screen as we have the assumption that the first loading screen is when the run starts. So that's where our loadCount comes in handy, we can just check if we are on the first one and only split when we aren't. + + ### `isLoading` + Pauses the timer whenever true is being returned. + * Runs every 1000 / `refreshRate` milliseconds. + ```lua + process('GameBlaBlaBla.exe') + + local current = {isLoading = false, scene = ""}; + local old = {isLoading = false, scene = ""}; + local loadCount = 0 -function startup() - refreshRate = 120 + + +function startup() + +refreshRate = 120 + end -function state() - old.isLoading = current.isLoading; - old.scene = current.scene; + + +function state() + +old.isLoading = current.isLoading; + +old.scene = current.scene; + + + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + +current.scene = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xBB, 0xEE, 0x55, 0xDD, 0xBA, 0x6A); - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - current.scene = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xBB, 0xEE, 0x55, 0xDD, 0xBA, 0x6A); end -function update() - if not current.isLoading and old.isLoading then - loadCount = loadCount + 1; - end + + +function update() + +if not current.isLoading and old.isLoading then + +loadCount = loadCount + 1; + end -function start() - return current.isLoading end -function split() - local shouldSplit = false; - if current.isLoading and not old.isLoading then - loadCount = loadCount + 1; - shouldSplit = loadCount > 1; - end + + +function start() + +return current.isLoading - return shouldSplit; end -function isLoading() - return current.isLoading + + +function split() + +local shouldSplit = false; + +if current.isLoading and not old.isLoading then + +loadCount = loadCount + 1; + +shouldSplit = loadCount > 1; + +end + + + +return shouldSplit; + end + + + +function isLoading() + +return current.isLoading + +end + ``` + * Pretty self explanatory, since we want to return whenever we are currently in a loading screen, we can just send our current isLoading status, same as start. + + # `reset` + Instantly resets the timer. Use with caution. + * Runs every 1000 / `refreshRate` milliseconds. + ```lua + process('GameBlaBlaBla.exe') + + local current = {isLoading = false}; + local old = {isLoading = false}; + local loadCount = 0 + local didReset = false -function startup() - refreshRate = 120 + + +function startup() + +refreshRate = 120 + end -function state() - old.isLoading = current.isLoading; + + +function state() + +old.isLoading = current.isLoading; + + + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + +end + + + +function update() + +if not current.isLoading and old.isLoading then + +loadCount = loadCount + 1; - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); end -function update() - if not current.isLoading and old.isLoading then - loadCount = loadCount + 1; - end end -function start() - return current.isLoading + + +function start() + +return current.isLoading + +end + + + +function split() + +local shouldSplit = false; + +if current.isLoading and not old.isLoading then + +loadCount = loadCount + 1; + +shouldSplit = loadCount > 1; + +end + + + +return shouldSplit; + end -function split() - local shouldSplit = false; - if current.isLoading and not old.isLoading then - loadCount = loadCount + 1; - shouldSplit = loadCount > 1; - end + + +function isLoading() + +return current.isLoading - return shouldSplit; end -function isLoading() - return current.isLoading + + +function reset() + +if not old.scene == "MenuScene" and current.scene == "MenuScene" then + +return true + end -function reset() - if not old.scene == "MenuScene" and current.scene == "MenuScene" then - return true - end - return false +return false + end + ``` + * In this example we are checking for the scene, of course, the address is completely arbitrary and doesnt mean anything for this example. Specifically we are checking if we are entering the MenuScene scene. + + ## readAddress -* `readAddress` is the second function that LibreSplit defines for us and its globally available, its job is to read the memory value of a specified address. + +* `readAddress` is the second function that LibreSplit defines for us and its globally available, its job is to read the memory value of a specified address. + * The first value defines what kind of value we will read: - 1. `sbyte`: signed 8 bit integer - 2. `byte`: unsigned 8 bit integer - 3. `short`: signed 16 bit integer - 4. `ushort`: unsigned 16 bit integer - 5. `int`: signed 32 bit integer - 6. `uint`: unsigned 32 bit integer - 7. `long`: signed 64 bit integer - 8. `ulong`: unsigned 64 bit integer - 9. `float`: 32 bit floating point number - 10. `double`: 64 bit floating point number - 11. `bool`: Boolean (true or false) - 12. `stringX`, A string of characters. Its usage is different compared the rest, you type "stringX" where the X is how long the string can be plus 1, this is to allocate the NULL terminator which defines when the string ends, for example, if the longest possible string to return is "cheese", you would define it as "string7". Setting X lower can result in the string terminating incorrectly and getting an incorrect result, setting it higher doesnt have any difference (aside from wasting memory). + +1. `sbyte`: signed 8 bit integer + +2. `byte`: unsigned 8 bit integer + +3. `short`: signed 16 bit integer + +4. `ushort`: unsigned 16 bit integer + +5. `int`: signed 32 bit integer + +6. `uint`: unsigned 32 bit integer + +7. `long`: signed 64 bit integer + +8. `ulong`: unsigned 64 bit integer + +9. `float`: 32 bit floating point number + +10. `double`: 64 bit floating point number + +11. `bool`: Boolean (true or false) + +12. `stringX`, A string of characters. Its usage is different compared the rest, you type "stringX" where the X is how long the string can be plus 1, this is to allocate the NULL terminator which defines when the string ends, for example, if the longest possible string to return is "cheese", you would define it as "string7". Setting X lower can result in the string terminating incorrectly and getting an incorrect result, setting it higher doesnt have any difference (aside from wasting memory). + + * The second argument can be 2 things, a string or a number. - * If its a number: The value in that memory address of the main process will be used. - * If its a string: It will find the corresponding map of that string, for example "UnityPlayer.dll", This means that instead of reading the memory of the main map of the process (main binary .exe), it will instead read the memory of UnityPlayer.dll's memory space. - * Next you have to add another argument, this will be the offset at which to read from from the perspective of the base address of the module, meaning if the module is mapped to 0x1000 to 0xFFFF and you put 0x0100 in the offset, it will read the value in the address 0x1010. + +* If its a number: The value in that memory address of the main process will be used. + +* If its a string: It will find the corresponding map of that string, for example "UnityPlayer.dll", This means that instead of reading the memory of the main map of the process (main binary .exe), it will instead read the memory of UnityPlayer.dll's memory space. + +* Next you have to add another argument, this will be the offset at which to read from from the perspective of the base address of the module, meaning if the module is mapped to 0x1000 to 0xFFFF and you put 0x0100 in the offset, it will read the value in the address 0x1010. + + * The rest of arguments are memory offsets or pointer paths. - * A Pointer Path is a list of Offsets + a Base Address. The auto splitter reads the value at the base address and interprets the value as yet another address. It adds the first offset to this address and reads the value of the calculated address. It does this over and over until there are no more offsets. At that point, it has found the value it was searching for. This resembles the way objects are stored in memory. Every object has a clearly defined layout where each variable has a consistent offset within the object, so you basically follow these variables from object to object. - * Cheat Engine is a tool that allows you to easily find Addresses and Pointer Paths for those Addresses, so you don't need to debug the game to figure out the structure of the memory. +* A Pointer Path is a list of Offsets + a Base Address. The auto splitter reads the value at the base address and interprets the value as yet another address. It adds the first offset to this address and reads the value of the calculated address. It does this over and over until there are no more offsets. At that point, it has found the value it was searching for. This resembles the way objects are stored in memory. Every object has a clearly defined layout where each variable has a consistent offset within the object, so you basically follow these variables from object to object. + + + +* Cheat Engine is a tool that allows you to easily find Addresses and Pointer Paths for those Addresses, so you don't need to debug the game to figure out the structure of the memory. + + + +## sig_scan +sig_scan performs a signature/pattern scan on the provided IDA-style byte array and optional integer offset and returns the found address + +Example: +`signature = sig_scan("89 5C 24 ?? 89 44 24 ?? 74 ?? 48 8D 15", 4)` + +Returns: +`14123ce19` + +* Note: Until the address is found, sig_scan returns a 0. + + ## getPID + * Returns the current PID + + # Experimental stuff + ## `mapsCacheCycles` + * When a readAddress that uses a memory map the biggest bottleneck is reading every line of `/proc/pid/maps` and checking if that line is the corresponding module. This option allows you to set for how many cycles the cache of that file should be used. The cache is global so it gets reset every x number of cycles. - * `0` (default): Disabled completely - * `1`: Enabled for the current cycle - * `2`: Enabled for the current cycle and the next one - * `3`: Enabled for the current cycle and the 2 next ones - * You get the idea - + +* `0` (default): Disabled completely + +* `1`: Enabled for the current cycle + +* `2`: Enabled for the current cycle and the next one + +* `3`: Enabled for the current cycle and the 2 next ones + +* You get the idea + ### Performance + * Every uncached map finding takes around 1ms (depends a lot on your ram and cpu) + * Every cached map finding takes around 100us + + * Mainly useful for lots of readAddresses and the game has uncapped game state update rate, where literally every millisecond matters + + ### Example + ```lua -function startup() - refreshRate = 60; - mapsCacheCycles = 1; + +function startup() + +refreshRate = 60; + +mapsCacheCycles = 1; + end --- Assume all this readAddresses are different, + + +-- Assume all this readAddresses are different, + -- Instead of taking near 10ms it will instead take 1-2ms, because only this cycle is cached and the first readAddress is a cache miss, if the mapsCacheCycles were higher than 1 then a cycle could take less than half a millisecond -function state() - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + +function state() + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + +current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + end + + ``` From fb9573b9e045ca6ea157e69223c8440b800c402c Mon Sep 17 00:00:00 2001 From: Loomeh Date: Thu, 11 Jul 2024 23:28:39 +0100 Subject: [PATCH 07/18] add note about sig scan performance to docs --- docs/auto-splitters.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/auto-splitters.md b/docs/auto-splitters.md index f88ba5c..fa9df2e 100644 --- a/docs/auto-splitters.md +++ b/docs/auto-splitters.md @@ -584,6 +584,7 @@ Returns: `14123ce19` * Note: Until the address is found, sig_scan returns a 0. +* Note: Signature scanning is an expensive action. So, once an address has been found, it's recommended to reassign the sig_scan variable with the result of the sig_scan function to stop the scanning. From 3e30a3fbe74974c4627671457f13f83907b1fb66 Mon Sep 17 00:00:00 2001 From: Loomeh Date: Fri, 12 Jul 2024 00:31:13 +0100 Subject: [PATCH 08/18] fix docs formatting --- docs/auto-splitters.md | 606 ++++++++++------------------------------- 1 file changed, 139 insertions(+), 467 deletions(-) diff --git a/docs/auto-splitters.md b/docs/auto-splitters.md index fa9df2e..86656db 100644 --- a/docs/auto-splitters.md +++ b/docs/auto-splitters.md @@ -1,578 +1,290 @@ # Auto Splitters - - * Auto splitters are scripts that automate the process of timing your run in a game by making splits automatically, starting, resetting, pausing, etc. - - # How do they work? - - * These work by reading into game's memory and determining when the timer should do something, like make a split. - - * LibreSplit's autosplitting system works in a very similar way to LiveSplit's. The main difference is that LibreSplit uses Lua instead of C#. There are also some key differences: + * Runs an entire Lua system instead of only supporting specifically named C# blocks. + * This means you can run external functions outside of the ones LibreSplit executes. + * Support for the entire Lua language, including the importing of libraries for tasks such as performance monitoring. -* Runs an entire Lua system instead of only supporting specifically named C# blocks. - -* This means you can run external functions outside of the ones LibreSplit executes. - -* Support for the entire Lua language, including the importing of libraries for tasks such as performance monitoring. - - - -# How to make LibreSplit auto splitters - - +# How to make LibreSplit auto splitters * It's somewhat easy if you know what you are doing or are porting an already existing one. - - * First in the lua script goes a `process` function call with the name of the games process: - - ```lua - process('GameBlaBlaBla.exe') - ``` - * With this line, LibreSplit will repeatedly attempt to find this process and will not continue script execution until it is found. - - * Next we have to define the basic functions. Not all are required and the ones that are required may change depending on the game or end goal, like if loading screens are included or not. - -* The order at which these run is the same as they are documented below. - - + * The order at which these run is the same as they are documented below. ### `startup` - -The purpose of this function is to specify how many times LibreSplit checks memory values and executes functions each second, the default is 60Hz. Usually, 60Hz is fine and this function can remain undefined. However, it's there if you need it. - + The purpose of this function is to specify how many times LibreSplit checks memory values and executes functions each second, the default is 60Hz. Usually, 60Hz is fine and this function can remain undefined. However, it's there if you need it. ```lua - process('GameBlaBlaBla.exe') - - -function startup() - -refreshRate = 120 - +function startup() + refreshRate = 120 end - ``` - - ### `state` - -The main purpose of this function is to assign memory values to Lua variables. - + The main purpose of this function is to assign memory values to Lua variables. * Runs every 1000 / `refreshRate` milliseconds and when the script is enabled/loaded. - - ```lua - process('GameBlaBlaBla.exe') - - local isLoading = false; - - -function startup() - -refreshRate = 120 - +function startup() + refreshRate = 120 end - - -function state() - -isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - +function state() + isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); end - ``` - - * You may have noticed that we're assigning this `isLoading` variable with the result of the function `readAddress`. This function is part of LibreSplit's Lua context and its purpose is to read memory values. It's explained in detail at the bottom of this document. - - ### `update` - -The purpose of this function is to update local variables. - + The purpose of this function is to update local variables. * Runs every 1000 / `refreshRate` milliseconds. - ```lua - process('GameBlaBlaBla.exe') - - local current = {isLoading = false}; - local old = {isLoading = false}; - local loadCount = 0 - - -function startup() - -refreshRate = 120 - +function startup() + refreshRate = 120 end - - -function state() - -old.isLoading = current.isLoading; - - - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); +function state() + old.isLoading = current.isLoading; + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); end - - -function update() - -if not current.isLoading and old.isLoading then - -loadCount = loadCount + 1; - +function update() + if not current.isLoading and old.isLoading then + loadCount = loadCount + 1; + end end - -end - ``` - * We now have 3 variables, one represents the current state while the other the old state of isLoading, we also have loadCount getting updated in the `update` function which will store how many times we've entered the loading screen - - ### `start` - This tells LibreSplit when to start the timer.\ - _Note: LibreSplit will ignore any start calls if the timer is running._ - * Runs every 1000 / `refreshRate` milliseconds. - ```lua - process('GameBlaBlaBla.exe') - - local current = {isLoading = false}; - local old = {isLoading = false}; - local loadCount = 0 - - -function startup() - -refreshRate = 120 - +function startup() + refreshRate = 120 end - - -function state() - -old.isLoading = current.isLoading; - - - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); +function state() + old.isLoading = current.isLoading; + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); end - - -function update() - -if not current.isLoading and old.isLoading then - -loadCount = loadCount + 1; - +function update() + if not current.isLoading and old.isLoading then + loadCount = loadCount + 1; + end end +function start() + return current.isLoading end - - - -function start() - -return current.isLoading - -end - ``` - - ### `split` - Tells LibreSplit to execute a split whenever it gets a true return. - -* Runs every 1000 / `refreshRate` milliseconds. - + * Runs every 1000 / `refreshRate` milliseconds. ```lua - process('GameBlaBlaBla.exe') - - local current = {isLoading = false}; - local old = {isLoading = false}; - local loadCount = 0 - - -function startup() - -refreshRate = 120 - +function startup() + refreshRate = 120 end - - -function state() - -old.isLoading = current.isLoading; - - - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); +function state() + old.isLoading = current.isLoading; + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); end - - -function update() - -if not current.isLoading and old.isLoading then - -loadCount = loadCount + 1; - -end - -end - - - -function start() - -return current.isLoading - +function update() + if not current.isLoading and old.isLoading then + loadCount = loadCount + 1; + end end - - -function split() - -local shouldSplit = false; - -if current.isLoading and not old.isLoading then - -loadCount = loadCount + 1; - -shouldSplit = loadCount > 1; - +function start() + return current.isLoading end - - -return shouldSplit; +function split() + local shouldSplit = false; + if current.isLoading and not old.isLoading then + loadCount = loadCount + 1; + shouldSplit = loadCount > 1; + end + return shouldSplit; end - ``` - * Whoa lots of code, why didnt we just return if we are currently in a loading screen like in start? Because if we do, we will do multiple splits a second, the function runs multiple times and it would do lots of unwanted splits. - * To solve that, we only want to split when we enter a loading screen (old is false, current is true), but we also don't want to split on the first loading screen as we have the assumption that the first loading screen is when the run starts. So that's where our loadCount comes in handy, we can just check if we are on the first one and only split when we aren't. - - ### `isLoading` - Pauses the timer whenever true is being returned. - * Runs every 1000 / `refreshRate` milliseconds. - ```lua - process('GameBlaBlaBla.exe') - - local current = {isLoading = false, scene = ""}; - local old = {isLoading = false, scene = ""}; - local loadCount = 0 - - -function startup() - -refreshRate = 120 - +function startup() + refreshRate = 120 end - - -function state() - -old.isLoading = current.isLoading; - -old.scene = current.scene; - - - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - -current.scene = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xBB, 0xEE, 0x55, 0xDD, 0xBA, 0x6A); +function state() + old.isLoading = current.isLoading; + old.scene = current.scene; + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.scene = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xBB, 0xEE, 0x55, 0xDD, 0xBA, 0x6A); end - - -function update() - -if not current.isLoading and old.isLoading then - -loadCount = loadCount + 1; - +function update() + if not current.isLoading and old.isLoading then + loadCount = loadCount + 1; + end end +function start() + return current.isLoading end - - -function start() - -return current.isLoading +function split() + local shouldSplit = false; + if current.isLoading and not old.isLoading then + loadCount = loadCount + 1; + shouldSplit = loadCount > 1; + end + return shouldSplit; end - - -function split() - -local shouldSplit = false; - -if current.isLoading and not old.isLoading then - -loadCount = loadCount + 1; - -shouldSplit = loadCount > 1; - +function isLoading() + return current.isLoading end - - - -return shouldSplit; - -end - - - -function isLoading() - -return current.isLoading - -end - ``` - * Pretty self explanatory, since we want to return whenever we are currently in a loading screen, we can just send our current isLoading status, same as start. - - # `reset` - Instantly resets the timer. Use with caution. - * Runs every 1000 / `refreshRate` milliseconds. - ```lua - process('GameBlaBlaBla.exe') - - local current = {isLoading = false}; - local old = {isLoading = false}; - local loadCount = 0 - local didReset = false - - -function startup() - -refreshRate = 120 - -end - - - -function state() - -old.isLoading = current.isLoading; - - - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - +function startup() + refreshRate = 120 end - - -function update() - -if not current.isLoading and old.isLoading then - -loadCount = loadCount + 1; - -end +function state() + old.isLoading = current.isLoading; + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); end - - -function start() - -return current.isLoading - -end - - - -function split() - -local shouldSplit = false; - -if current.isLoading and not old.isLoading then - -loadCount = loadCount + 1; - -shouldSplit = loadCount > 1; - +function update() + if not current.isLoading and old.isLoading then + loadCount = loadCount + 1; + end end - - -return shouldSplit; - +function start() + return current.isLoading end - - -function isLoading() - -return current.isLoading +function split() + local shouldSplit = false; + if current.isLoading and not old.isLoading then + loadCount = loadCount + 1; + shouldSplit = loadCount > 1; + end + return shouldSplit; end - - -function reset() - -if not old.scene == "MenuScene" and current.scene == "MenuScene" then - -return true - +function isLoading() + return current.isLoading end -return false - +function reset() + if not old.scene == "MenuScene" and current.scene == "MenuScene" then + return true + end + return false end - ``` - * In this example we are checking for the scene, of course, the address is completely arbitrary and doesnt mean anything for this example. Specifically we are checking if we are entering the MenuScene scene. - - ## readAddress - -* `readAddress` is the second function that LibreSplit defines for us and its globally available, its job is to read the memory value of a specified address. - +* `readAddress` is the second function that LibreSplit defines for us and its globally available, its job is to read the memory value of a specified address. * The first value defines what kind of value we will read: - -1. `sbyte`: signed 8 bit integer - -2. `byte`: unsigned 8 bit integer - -3. `short`: signed 16 bit integer - -4. `ushort`: unsigned 16 bit integer - -5. `int`: signed 32 bit integer - -6. `uint`: unsigned 32 bit integer - -7. `long`: signed 64 bit integer - -8. `ulong`: unsigned 64 bit integer - -9. `float`: 32 bit floating point number - -10. `double`: 64 bit floating point number - -11. `bool`: Boolean (true or false) - -12. `stringX`, A string of characters. Its usage is different compared the rest, you type "stringX" where the X is how long the string can be plus 1, this is to allocate the NULL terminator which defines when the string ends, for example, if the longest possible string to return is "cheese", you would define it as "string7". Setting X lower can result in the string terminating incorrectly and getting an incorrect result, setting it higher doesnt have any difference (aside from wasting memory). - - + 1. `sbyte`: signed 8 bit integer + 2. `byte`: unsigned 8 bit integer + 3. `short`: signed 16 bit integer + 4. `ushort`: unsigned 16 bit integer + 5. `int`: signed 32 bit integer + 6. `uint`: unsigned 32 bit integer + 7. `long`: signed 64 bit integer + 8. `ulong`: unsigned 64 bit integer + 9. `float`: 32 bit floating point number + 10. `double`: 64 bit floating point number + 11. `bool`: Boolean (true or false) + 12. `stringX`, A string of characters. Its usage is different compared the rest, you type "stringX" where the X is how long the string can be plus 1, this is to allocate the NULL terminator which defines when the string ends, for example, if the longest possible string to return is "cheese", you would define it as "string7". Setting X lower can result in the string terminating incorrectly and getting an incorrect result, setting it higher doesnt have any difference (aside from wasting memory). * The second argument can be 2 things, a string or a number. - -* If its a number: The value in that memory address of the main process will be used. - -* If its a string: It will find the corresponding map of that string, for example "UnityPlayer.dll", This means that instead of reading the memory of the main map of the process (main binary .exe), it will instead read the memory of UnityPlayer.dll's memory space. - -* Next you have to add another argument, this will be the offset at which to read from from the perspective of the base address of the module, meaning if the module is mapped to 0x1000 to 0xFFFF and you put 0x0100 in the offset, it will read the value in the address 0x1010. - - + * If its a number: The value in that memory address of the main process will be used. + * If its a string: It will find the corresponding map of that string, for example "UnityPlayer.dll", This means that instead of reading the memory of the main map of the process (main binary .exe), it will instead read the memory of UnityPlayer.dll's memory space. + * Next you have to add another argument, this will be the offset at which to read from from the perspective of the base address of the module, meaning if the module is mapped to 0x1000 to 0xFFFF and you put 0x0100 in the offset, it will read the value in the address 0x1010. * The rest of arguments are memory offsets or pointer paths. + * A Pointer Path is a list of Offsets + a Base Address. The auto splitter reads the value at the base address and interprets the value as yet another address. It adds the first offset to this address and reads the value of the calculated address. It does this over and over until there are no more offsets. At that point, it has found the value it was searching for. This resembles the way objects are stored in memory. Every object has a clearly defined layout where each variable has a consistent offset within the object, so you basically follow these variables from object to object. -* A Pointer Path is a list of Offsets + a Base Address. The auto splitter reads the value at the base address and interprets the value as yet another address. It adds the first offset to this address and reads the value of the calculated address. It does this over and over until there are no more offsets. At that point, it has found the value it was searching for. This resembles the way objects are stored in memory. Every object has a clearly defined layout where each variable has a consistent offset within the object, so you basically follow these variables from object to object. - - - -* Cheat Engine is a tool that allows you to easily find Addresses and Pointer Paths for those Addresses, so you don't need to debug the game to figure out the structure of the memory. - - + * Cheat Engine is a tool that allows you to easily find Addresses and Pointer Paths for those Addresses, so you don't need to debug the game to figure out the structure of the memory. ## sig_scan sig_scan performs a signature/pattern scan on the provided IDA-style byte array and optional integer offset and returns the found address @@ -586,84 +298,44 @@ Returns: * Note: Until the address is found, sig_scan returns a 0. * Note: Signature scanning is an expensive action. So, once an address has been found, it's recommended to reassign the sig_scan variable with the result of the sig_scan function to stop the scanning. - - ## getPID - * Returns the current PID - - # Experimental stuff - ## `mapsCacheCycles` - * When a readAddress that uses a memory map the biggest bottleneck is reading every line of `/proc/pid/maps` and checking if that line is the corresponding module. This option allows you to set for how many cycles the cache of that file should be used. The cache is global so it gets reset every x number of cycles. - -* `0` (default): Disabled completely - -* `1`: Enabled for the current cycle - -* `2`: Enabled for the current cycle and the next one - -* `3`: Enabled for the current cycle and the 2 next ones - -* You get the idea - + * `0` (default): Disabled completely + * `1`: Enabled for the current cycle + * `2`: Enabled for the current cycle and the next one + * `3`: Enabled for the current cycle and the 2 next ones + * You get the idea + ### Performance - * Every uncached map finding takes around 1ms (depends a lot on your ram and cpu) - * Every cached map finding takes around 100us - - * Mainly useful for lots of readAddresses and the game has uncapped game state update rate, where literally every millisecond matters - - ### Example - ```lua - -function startup() - -refreshRate = 60; - -mapsCacheCycles = 1; - +function startup() + refreshRate = 60; + mapsCacheCycles = 1; end - - --- Assume all this readAddresses are different, - +-- Assume all this readAddresses are different, -- Instead of taking near 10ms it will instead take 1-2ms, because only this cycle is cached and the first readAddress is a cache miss, if the mapsCacheCycles were higher than 1 then a cycle could take less than half a millisecond - -function state() - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - -current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); - +function state() + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); end - - ``` From 520b7e43c770abd70dda12866e7c1b4d99f87458 Mon Sep 17 00:00:00 2001 From: Loomeh Date: Fri, 12 Jul 2024 00:50:54 +0100 Subject: [PATCH 09/18] remove unused include --- src/signature.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/signature.c b/src/signature.c index 1f2e0d3..9a778fd 100644 --- a/src/signature.c +++ b/src/signature.c @@ -1,6 +1,5 @@ #include #include -#include #include #include #include From 5dc47bb392056204de15a0bc5273469e90fb4d61 Mon Sep 17 00:00:00 2001 From: Loomeh Date: Sun, 14 Jul 2024 06:15:25 +0100 Subject: [PATCH 10/18] properly handle FF bytes/integer underflow --- src/signature.c | 38 +++++++++++++++++--------------------- src/signature.h | 4 ++-- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/signature.c b/src/signature.c index 9a778fd..87d303f 100644 --- a/src/signature.c +++ b/src/signature.c @@ -1,7 +1,6 @@ +#include "signature.h" #include #include -#include -#include #include #include #include @@ -10,14 +9,12 @@ #include "memory.h" #include "process.h" -#include "signature.h" #include extern game_process process; -MemoryRegion* get_memory_regions(pid_t pid, int* count) -{ +MemoryRegion* get_memory_regions(pid_t pid, int* count) { char maps_path[256]; sprintf(maps_path, "/proc/%d/maps", pid); FILE* maps_file = fopen(maps_path, "r"); @@ -55,17 +52,18 @@ MemoryRegion* get_memory_regions(pid_t pid, int* count) return regions; } -bool match_pattern(const uint8_t* data, const int* pattern, size_t pattern_size) -{ +bool match_pattern(const uint8_t* data, const uint16_t* pattern, size_t pattern_size) { for (size_t i = 0; i < pattern_size; ++i) { - if (pattern[i] != -1 && data[i] != pattern[i]) + uint8_t byte = pattern[i] & 0xFF; + bool ignore = (pattern[i] >> 8) & 0x1; + if (!ignore && data[i] != byte) { return false; + } } return true; } -int* convert_signature(const char* signature, size_t* pattern_size) -{ +uint16_t* convert_signature(const char* signature, size_t* pattern_size) { char* signature_copy = strdup(signature); if (!signature_copy) { return NULL; @@ -74,7 +72,7 @@ int* convert_signature(const char* signature, size_t* pattern_size) char* token = strtok(signature_copy, " "); size_t size = 0; size_t capacity = 10; - int* pattern = (int*)malloc(capacity * sizeof(int)); + uint16_t* pattern = (uint16_t*)malloc(capacity * sizeof(uint16_t)); if (!pattern) { free(signature_copy); return NULL; @@ -83,7 +81,7 @@ int* convert_signature(const char* signature, size_t* pattern_size) while (token != NULL) { if (size >= capacity) { capacity *= 2; - int* temp = (int*)realloc(pattern, capacity * sizeof(int)); + uint16_t* temp = (uint16_t*)realloc(pattern, capacity * sizeof(uint16_t)); if (!temp) { free(pattern); free(signature_copy); @@ -92,10 +90,11 @@ int* convert_signature(const char* signature, size_t* pattern_size) pattern = temp; } - if (strcmp(token, "??") == 0) - pattern[size] = -1; - else + if (strcmp(token, "??") == 0) { + pattern[size] = 0xFF00; // Set the upper byte to 1 to indicate ignoring this byte + } else { pattern[size] = strtol(token, NULL, 16); + } size++; token = strtok(NULL, " "); } @@ -105,8 +104,7 @@ int* convert_signature(const char* signature, size_t* pattern_size) return pattern; } -bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t size) -{ +bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t size) { struct iovec local_iov = { buffer, size }; struct iovec remote_iov = { (void*)address, size }; ssize_t nread = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0); @@ -114,14 +112,13 @@ bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t return nread == size; } -int find_signature(lua_State* L) -{ +int find_signature(lua_State* L) { pid_t p_pid = process.pid; const char* signature = lua_tostring(L, 1); int offset = lua_tointeger(L, 2); // Get the offset as an integer directly size_t pattern_length; - int* pattern = convert_signature(signature, &pattern_length); + uint16_t* pattern = convert_signature(signature, &pattern_length); if (!pattern) { lua_pushinteger(L, 0); return 1; @@ -147,7 +144,6 @@ int find_signature(lua_State* L) } if (!validate_process_memory(p_pid, region.start, buffer, region_size)) { - // printf("Failed to read memory region: %lx-%lx\n", region.start, region.end); free(buffer); continue; // Continue to next region } diff --git a/src/signature.h b/src/signature.h index 68f016a..9c56640 100644 --- a/src/signature.h +++ b/src/signature.h @@ -13,8 +13,8 @@ typedef struct { } MemoryRegion; MemoryRegion* get_memory_regions(pid_t pid, int* count); -bool match_pattern(const uint8_t* data, const int* pattern, size_t pattern_size); -int* convert_signature(const char* signature, size_t* pattern_size); +bool match_pattern(const uint8_t* data, const uint16_t* pattern, size_t pattern_size); +uint16_t* convert_signature(const char* signature, size_t* pattern_size); bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t size); int find_signature(lua_State* L); From 2376ebb066db760ee93ed68ff7d63444969c6c32 Mon Sep 17 00:00:00 2001 From: Loomeh Date: Sun, 14 Jul 2024 06:16:14 +0100 Subject: [PATCH 11/18] fix formatting --- src/signature.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/signature.c b/src/signature.c index 87d303f..1c7df9b 100644 --- a/src/signature.c +++ b/src/signature.c @@ -14,7 +14,8 @@ extern game_process process; -MemoryRegion* get_memory_regions(pid_t pid, int* count) { +MemoryRegion* get_memory_regions(pid_t pid, int* count) +{ char maps_path[256]; sprintf(maps_path, "/proc/%d/maps", pid); FILE* maps_file = fopen(maps_path, "r"); @@ -52,7 +53,8 @@ MemoryRegion* get_memory_regions(pid_t pid, int* count) { return regions; } -bool match_pattern(const uint8_t* data, const uint16_t* pattern, size_t pattern_size) { +bool match_pattern(const uint8_t* data, const uint16_t* pattern, size_t pattern_size) +{ for (size_t i = 0; i < pattern_size; ++i) { uint8_t byte = pattern[i] & 0xFF; bool ignore = (pattern[i] >> 8) & 0x1; @@ -63,7 +65,8 @@ bool match_pattern(const uint8_t* data, const uint16_t* pattern, size_t pattern_ return true; } -uint16_t* convert_signature(const char* signature, size_t* pattern_size) { +uint16_t* convert_signature(const char* signature, size_t* pattern_size) +{ char* signature_copy = strdup(signature); if (!signature_copy) { return NULL; @@ -104,7 +107,8 @@ uint16_t* convert_signature(const char* signature, size_t* pattern_size) { return pattern; } -bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t size) { +bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t size) +{ struct iovec local_iov = { buffer, size }; struct iovec remote_iov = { (void*)address, size }; ssize_t nread = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0); @@ -112,7 +116,8 @@ bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t return nread == size; } -int find_signature(lua_State* L) { +int find_signature(lua_State* L) +{ pid_t p_pid = process.pid; const char* signature = lua_tostring(L, 1); int offset = lua_tointeger(L, 2); // Get the offset as an integer directly From 164a01916d26c3e380718e3fdaea856c1f8b48fd Mon Sep 17 00:00:00 2001 From: Loomeh Date: Sun, 14 Jul 2024 17:52:27 +0100 Subject: [PATCH 12/18] update docs with sig_scan example --- docs/auto-splitters.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/auto-splitters.md b/docs/auto-splitters.md index 86656db..d0ed26b 100644 --- a/docs/auto-splitters.md +++ b/docs/auto-splitters.md @@ -298,6 +298,30 @@ Returns: * Note: Until the address is found, sig_scan returns a 0. * Note: Signature scanning is an expensive action. So, once an address has been found, it's recommended to reassign the sig_scan variable with the result of the sig_scan function to stop the scanning. + +Mini example script with the game SPRAWL: +```lua +process('Sprawl-Win64-Shipping.exe') + +local featuretest = 0 + +function state() + -- Perform the signature scan to find the initial address + featuretest = sig_scan("89 5C 24 ?? 89 44 24 ?? 74 ?? 48 8D 15", 4) + + if featuretest == 0 then + print("Signature scan did not find the address.") + else + -- Read an integer value from the found address + local readValue = readAddress('int', 'Sprawl-Win64-Shipping.exe', featuretest) + print("Feature test address: ", featuretest) + print("Read value: ", readValue) + end +end +``` + + + ## getPID * Returns the current PID From f3c2ae7863b71b982aed50b03328747851e9c907 Mon Sep 17 00:00:00 2001 From: Loomeh Date: Sun, 14 Jul 2024 18:55:39 +0100 Subject: [PATCH 13/18] Add info regarding Lua's string to number conversions. --- docs/auto-splitters.md | 10 ++++++---- src/signature.c | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/auto-splitters.md b/docs/auto-splitters.md index d0ed26b..4bbb9ee 100644 --- a/docs/auto-splitters.md +++ b/docs/auto-splitters.md @@ -287,16 +287,18 @@ end * Cheat Engine is a tool that allows you to easily find Addresses and Pointer Paths for those Addresses, so you don't need to debug the game to figure out the structure of the memory. ## sig_scan -sig_scan performs a signature/pattern scan on the provided IDA-style byte array and optional integer offset and returns the found address +sig_scan performs a signature/pattern scan on the provided IDA-style byte array and optional integer offset and returns a string representation of the found address Example: `signature = sig_scan("89 5C 24 ?? 89 44 24 ?? 74 ?? 48 8D 15", 4)` Returns: -`14123ce19` +`"14123ce19"` -* Note: Until the address is found, sig_scan returns a 0. -* Note: Signature scanning is an expensive action. So, once an address has been found, it's recommended to reassign the sig_scan variable with the result of the sig_scan function to stop the scanning. +### Notes +* Lua automatically handles the conversion of hexadecimal strings to numbers so parsing/casting it manually is not required. +* Until the address is found, sig_scan returns a 0. +* Signature scanning is an expensive action. So, once an address has been found, it's recommended to reassign the sig_scan variable with the result of the sig_scan function to stop the scanning. Mini example script with the game SPRAWL: diff --git a/src/signature.c b/src/signature.c index 1c7df9b..728369f 100644 --- a/src/signature.c +++ b/src/signature.c @@ -163,6 +163,8 @@ int find_signature(lua_State* L) free(pattern); free(regions); + + // Even though this function returns an integer, we can return a string as Lua will automatically handle the conversion. lua_pushstring(L, hex_str); // Push the hexadecimal string onto the Lua stack return 1; // Return the number of values pushed onto the stack } @@ -175,5 +177,5 @@ int find_signature(lua_State* L) free(regions); lua_pushinteger(L, 0); // Push 0 if no match is found in any region - return 1; // Return the number of values pushed onto the stack + return 1; } From 7b127319bdb8d00dd3dcbceefcde6316926cc955 Mon Sep 17 00:00:00 2001 From: Loomeh Date: Sun, 14 Jul 2024 18:56:25 +0100 Subject: [PATCH 14/18] fix formatting (again) --- src/signature.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/signature.c b/src/signature.c index 728369f..110fd78 100644 --- a/src/signature.c +++ b/src/signature.c @@ -163,7 +163,6 @@ int find_signature(lua_State* L) free(pattern); free(regions); - // Even though this function returns an integer, we can return a string as Lua will automatically handle the conversion. lua_pushstring(L, hex_str); // Push the hexadecimal string onto the Lua stack return 1; // Return the number of values pushed onto the stack From e2d4b511ae64e397bf7f241ab1e39498ba9af583 Mon Sep 17 00:00:00 2001 From: Loomeh Date: Wed, 31 Jul 2024 01:20:37 +0100 Subject: [PATCH 15/18] prefix return string with "0x" and improve error handling --- src/auto-splitter.c | 2 +- src/signature.c | 98 +++++++++++++++++++++++++++++++++++---------- src/signature.h | 2 +- 3 files changed, 78 insertions(+), 24 deletions(-) diff --git a/src/auto-splitter.c b/src/auto-splitter.c index 796b36a..530f48d 100644 --- a/src/auto-splitter.c +++ b/src/auto-splitter.c @@ -324,7 +324,7 @@ void run_auto_splitter() lua_setglobal(L, "process"); lua_pushcfunction(L, read_address); lua_setglobal(L, "readAddress"); - lua_pushcfunction(L, find_signature); + lua_pushcfunction(L, perform_sig_scan); lua_setglobal(L, "sig_scan"); lua_pushcfunction(L, getPid); lua_setglobal(L, "getPID"); diff --git a/src/signature.c b/src/signature.c index 110fd78..6e6b98c 100644 --- a/src/signature.c +++ b/src/signature.c @@ -1,6 +1,8 @@ #include "signature.h" #include #include +#include +#include #include #include #include @@ -14,14 +16,34 @@ extern game_process process; +// Error handling macro +#define HANDLE_ERROR(msg) \ + do { \ + perror(msg); \ + return NULL; \ + } while (0) + +// Error logging function +void log_error(const char* format, ...) +{ + va_list args; + va_start(args, format); + fprintf(stderr, "Error in sig_scan: "); + vfprintf(stderr, format, args); + fprintf(stderr, "\n"); + va_end(args); +} + MemoryRegion* get_memory_regions(pid_t pid, int* count) { char maps_path[256]; - sprintf(maps_path, "/proc/%d/maps", pid); + if (snprintf(maps_path, sizeof(maps_path), "/proc/%d/maps", pid) < 0) { + HANDLE_ERROR("Failed to create maps path"); + } + FILE* maps_file = fopen(maps_path, "r"); if (!maps_file) { - perror("Failed to open maps file"); - return NULL; + HANDLE_ERROR("Failed to open maps file"); } MemoryRegion* regions = NULL; @@ -32,16 +54,17 @@ MemoryRegion* get_memory_regions(pid_t pid, int* count) while (fgets(line, sizeof(line), maps_file)) { if (*count >= capacity) { capacity = capacity == 0 ? 10 : capacity * 2; - regions = (MemoryRegion*)realloc(regions, capacity * sizeof(MemoryRegion)); - if (!regions) { - perror("Failed to allocate memory for regions"); + MemoryRegion* temp = realloc(regions, capacity * sizeof(MemoryRegion)); + if (!temp) { + free(regions); fclose(maps_file); - return NULL; + HANDLE_ERROR("Failed to allocate memory for regions"); } + regions = temp; } uintptr_t start, end; - if (sscanf(line, "%lx-%lx", &start, &end) != 2) { + if (sscanf(line, "%" SCNxPTR "-%" SCNxPTR, &start, &end) != 2) { continue; // Skip lines that don't match the expected format } regions[*count].start = start; @@ -113,19 +136,39 @@ bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t struct iovec remote_iov = { (void*)address, size }; ssize_t nread = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0); - return nread == size; + return nread == (ssize_t)size; } -int find_signature(lua_State* L) +int perform_sig_scan(lua_State* L) { + if (lua_gettop(L) != 2) { + log_error("Invalid number of arguments: expected 2 (signature, offset)"); + lua_pushnil(L); + return 1; + } + + if (!lua_isstring(L, 1) || !lua_isnumber(L, 2)) { + log_error("Invalid argument types: expected (string, number)"); + lua_pushnil(L); + return 1; + } + pid_t p_pid = process.pid; const char* signature = lua_tostring(L, 1); - int offset = lua_tointeger(L, 2); // Get the offset as an integer directly + int offset = lua_tointeger(L, 2); + + // Validate signature string + if (strlen(signature) == 0) { + log_error("Signature string cannot be empty"); + lua_pushnil(L); + return 1; + } size_t pattern_length; uint16_t* pattern = convert_signature(signature, &pattern_length); if (!pattern) { - lua_pushinteger(L, 0); + log_error("Failed to convert signature"); + lua_pushnil(L); return 1; } @@ -133,18 +176,20 @@ int find_signature(lua_State* L) MemoryRegion* regions = get_memory_regions(p_pid, ®ions_count); if (!regions) { free(pattern); - lua_pushinteger(L, 0); + log_error("Failed to get memory regions"); + lua_pushnil(L); return 1; } for (int i = 0; i < regions_count; i++) { MemoryRegion region = regions[i]; ssize_t region_size = region.end - region.start; - uint8_t* buffer = (uint8_t*)malloc(region_size); + uint8_t* buffer = malloc(region_size); if (!buffer) { free(pattern); free(regions); - lua_pushinteger(L, 0); + log_error("Failed to allocate memory for region buffer"); + lua_pushnil(L); return 1; } @@ -155,17 +200,24 @@ int find_signature(lua_State* L) for (size_t j = 0; j <= region_size - pattern_length; ++j) { if (match_pattern(buffer + j, pattern, pattern_length)) { - uintptr_t result = region.start + j + offset; // Apply the offset here - char hex_str[20]; // Buffer to hold the hexadecimal string representation - sprintf(hex_str, "%lx", result); // Convert result to hexadecimal string + uintptr_t result = region.start + j + offset; + + char full_hex_str[32]; // Increased buffer size for safety + if (snprintf(full_hex_str, sizeof(full_hex_str), "0x%" PRIxPTR, result) < 0) { + free(buffer); + free(pattern); + free(regions); + log_error("Failed to convert result to string"); + lua_pushnil(L); + return 1; + } free(buffer); free(pattern); free(regions); - // Even though this function returns an integer, we can return a string as Lua will automatically handle the conversion. - lua_pushstring(L, hex_str); // Push the hexadecimal string onto the Lua stack - return 1; // Return the number of values pushed onto the stack + lua_pushstring(L, full_hex_str); + return 1; } } @@ -175,6 +227,8 @@ int find_signature(lua_State* L) free(pattern); free(regions); - lua_pushinteger(L, 0); // Push 0 if no match is found in any region + // No match found + log_error("No match found for the given signature"); + lua_pushnil(L); return 1; } diff --git a/src/signature.h b/src/signature.h index 9c56640..f44ac1a 100644 --- a/src/signature.h +++ b/src/signature.h @@ -16,6 +16,6 @@ MemoryRegion* get_memory_regions(pid_t pid, int* count); bool match_pattern(const uint8_t* data, const uint16_t* pattern, size_t pattern_size); uint16_t* convert_signature(const char* signature, size_t* pattern_size); bool validate_process_memory(pid_t pid, uintptr_t address, void* buffer, size_t size); -int find_signature(lua_State* L); +int perform_sig_scan(lua_State* L); #endif /* __SIGNATURE_H__ */ \ No newline at end of file From ee9e6b60c302d6e270140c9a0f4edce5ed5d7e4e Mon Sep 17 00:00:00 2001 From: Zane Holbrooke-Jones <67561520+Loomeh@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:09:08 +0100 Subject: [PATCH 16/18] Update build.yml --- .github/workflows/build.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bfc69f0..4ab2d52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,3 @@ -on: [push, pull_request] - jobs: tests: runs-on: ubuntu-latest @@ -26,8 +24,16 @@ jobs: shell: bash run: echo "git_short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - - name: "Upload artifacts" - uses: actions/upload-artifact@v4 - with: - name: LibreSplit-${{ env.build_number }}-${{ env.git_short_sha }} - path: libresplit + - name: "Prepare AppDir" + run: | + mkdir -p LibreSplit.AppDir/usr/bin + mkdir -p LibreSplit.AppDir/usr/share/applications + mkdir -p LibreSplit.AppDir/usr/share/icons/hicolor + mkdir -p LibreSplit.AppDir/usr/share/glib-2.0/schemas + cp libresplit LibreSplit.AppDir/usr/bin/ + cp libresplit.desktop LibreSplit.AppDir/usr/share/applications/ + for size in 16 22 24 32 36 48 64 72 96 128 256 512; do + mkdir -p LibreSplit.AppDir/usr/share/icons/hicolor/"${size}x${size}"/apps + rsvg-convert -w "$size" -h "$size" -f png -o LibreSplit.AppDir/usr/share/icons/hicolor/"${size}x${size}"/apps/libresplit.png libresplit.svg + done + cp libresplit.gschema.xml LibreSplit.AppDir/usr/share/glib-2.0/schemas/ From 3127706d0bd48a8b8a46186556596513a73a9b7a Mon Sep 17 00:00:00 2001 From: Zane Holbrooke-Jones <67561520+Loomeh@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:11:18 +0100 Subject: [PATCH 17/18] Revert build.yml --- .github/workflows/build.yml | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4ab2d52..bfc69f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,3 +1,5 @@ +on: [push, pull_request] + jobs: tests: runs-on: ubuntu-latest @@ -24,16 +26,8 @@ jobs: shell: bash run: echo "git_short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - - name: "Prepare AppDir" - run: | - mkdir -p LibreSplit.AppDir/usr/bin - mkdir -p LibreSplit.AppDir/usr/share/applications - mkdir -p LibreSplit.AppDir/usr/share/icons/hicolor - mkdir -p LibreSplit.AppDir/usr/share/glib-2.0/schemas - cp libresplit LibreSplit.AppDir/usr/bin/ - cp libresplit.desktop LibreSplit.AppDir/usr/share/applications/ - for size in 16 22 24 32 36 48 64 72 96 128 256 512; do - mkdir -p LibreSplit.AppDir/usr/share/icons/hicolor/"${size}x${size}"/apps - rsvg-convert -w "$size" -h "$size" -f png -o LibreSplit.AppDir/usr/share/icons/hicolor/"${size}x${size}"/apps/libresplit.png libresplit.svg - done - cp libresplit.gschema.xml LibreSplit.AppDir/usr/share/glib-2.0/schemas/ + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: LibreSplit-${{ env.build_number }}-${{ env.git_short_sha }} + path: libresplit From c68fa4b6506218fdb03233ee092993251675478b Mon Sep 17 00:00:00 2001 From: Loomeh Date: Fri, 9 Aug 2024 22:14:08 +0100 Subject: [PATCH 18/18] Update docs --- docs/auto-splitters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/auto-splitters.md b/docs/auto-splitters.md index 4bbb9ee..d7b6075 100644 --- a/docs/auto-splitters.md +++ b/docs/auto-splitters.md @@ -293,10 +293,10 @@ Example: `signature = sig_scan("89 5C 24 ?? 89 44 24 ?? 74 ?? 48 8D 15", 4)` Returns: -`"14123ce19"` +`"0x14123ce19"` ### Notes -* Lua automatically handles the conversion of hexadecimal strings to numbers so parsing/casting it manually is not required. +* Lua automatically handles the conversion of hexadecimal strings to numbers (as long as the '0x' prefix is present) so parsing/casting it manually is not required. * Until the address is found, sig_scan returns a 0. * Signature scanning is an expensive action. So, once an address has been found, it's recommended to reassign the sig_scan variable with the result of the sig_scan function to stop the scanning.