https://github.com/speedrun-program/load-extender
This is the previous version: lengthening the time it takes to access files REVISION #2
To compile this, you need to download and include robin_hood.h, and on Windows you also need to install EasyHook.
I made this because it helps us save non-load time in a speedrun.
In this version, it uses unique_ptr int arrays instead of a class, it has a way to include leading and trailing spaces in file names, and it has a way to deal with if there’s already a file named files_and_delays.txt. I also made it so the txt files are in utf-16 LE on Windows because that’s how file paths are encoded on that OS.
load_extender.cpp
#include <chrono>
#include <thread>
#include <fstream>
#include <string>
#include <vector>
#include <mutex>
#include <filesystem>
#include <memory>
#include <climits>
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
#define THIS_IS_NOT_THE_TEST_PROGRAM
#endif
#ifdef _WIN32
using wstring_or_string = std::wstring;
#include <Windows.h>
// easyhook.h installed with NuGet
// https://easyhook.github.io/documentation.html
#include <easyhook.h>
#else
using wstring_or_string = std::string;
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <dlfcn.h>
#endif
#include "robin_hood.h"
#include "other_functions.h"
// thank you for making this hash map, martinus.
// here's his github: https://github.com/martinus/robin-hood-hashing
static robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>> rh_map;
static std::mutex delay_array_mutex = read_delays_file(rh_map);
#ifdef _WIN32
static NTSTATUS WINAPI NtCreateFileHook(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength)
{
const std::wstring& file_path = ObjectAttributes->ObjectName->Buffer;
// +1 so '\' isn't included. If '\' isn't found, the whole wstring is checked because npos + 1 is 0
const std::wstring& file_name = file_path.substr(file_path.rfind(L"\\") + 1);
delay_file(rh_map, file_name, delay_array_mutex);
return NtCreateFile(
FileHandle,
DesiredAccess,
ObjectAttributes,
IoStatusBlock,
AllocationSize,
FileAttributes,
ShareAccess,
CreateDisposition,
CreateOptions,
EaBuffer,
EaLength
);
}
extern "C" void __declspec(dllexport) __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO * inRemoteInfo);
void __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO* inRemoteInfo)
{
HOOK_TRACE_INFO hHook1 = {NULL};
LhInstallHook(
GetProcAddress(GetModuleHandle(TEXT("ntdll")), "NtCreateFile"),
NtCreateFileHook,
NULL,
&hHook1
);
ULONG ACLEntries[1] = {0};
LhSetExclusiveACL(ACLEntries, 1, &hHook1);
}
#else
static auto original_fopen = reinterpret_cast<FILE * (*)(const char* path, const char* mode)>(dlsym(RTLD_NEXT, "fopen"));
FILE* fopen(const char* path, const char* mode)
{
// finding part of path which shows file name
int file_name_index = -1; // increases to 0 and checks entire path if no slash is found
for (int i = 0; path[i] != '\0'; i++)
{
if (path[i] == '/')
{
file_name_index = i;
}
}
const std::string file_name(&path[file_name_index + 1]);
delay_file(rh_map, file_name, delay_array_mutex);
return original_fopen(path, mode);
}
#endif
load_extender_test.cpp
#include <mutex>
#include <memory>
#include <fstream>
#include <string>
#include <vector>
#include <filesystem>
#include <climits>
#include "robin_hood.h"
// thank you for making this hash map, martinus.
// here's his github: https://github.com/martinus/robin-hood-hashing
#ifdef _WIN32
using wstring_or_string = std::wstring; // file paths are UTF-16LE on Windows
#else
using wstring_or_string = std::string;
#endif
#include "other_functions.h"
static void print_rh_map(robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>>& rh_map)
{
printf("\n---------- hash map current state ----------\n");
for (auto& it : rh_map)
{
#ifdef _WIN32
printf("%ls /", it.first.c_str());
#else
printf("%s /", it.first.c_str());
#endif
int i = 1; // used after loop to check if -1 is at the end
for (; it.second[i] >= 0; i++)
{
printf(" %d", it.second[i]);
}
if (it.second[i] == -1)
{
if (i == 1)
{
printf(" RESET ALL");
}
else
{
printf(" REPEAT");
}
}
printf(": INDEX %d\n", it.second[0]);
}
printf("---------------------------------------------\n\n");
}
static void test_all_inputs()
{
#ifdef _WIN32
wchar_t path_separator = L'\\';
#else
char path_separator = '/';
#endif
wstring_or_string test_path;
std::ifstream input_test_file;
input_test_file.open("test_input.txt", std::ios::binary);
if (input_test_file.fail())
{
printf("couldn't open test_input.txt\n");
return;
}
std::string file_content(
(std::istreambuf_iterator<char>(input_test_file)),
(std::istreambuf_iterator<char>())
);
if (file_content.length() > 0 && file_content[file_content.length() - 1] == '\n')
{
file_content.erase(file_content.length() - 1);
}
input_test_file.close();
#ifdef _WIN32
if (file_content.length() < 2)
{
printf("test_input.txt byte order mark is missing\nsave test_input.txt as UTF-16 LE\n");
return;
}
size_t start_index = 2; // first two bytes are BOM bytes
#else
size_t start_index = 0;
#endif
size_t stop_index = 0;
robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>> rh_map;
std::mutex delay_array_mutex = read_delays_file(rh_map);
print_rh_map(rh_map);
do // go through each line
{
#ifdef _WIN32
stop_index = file_content.find("\n\0", start_index);
if (stop_index != std::string::npos)
{
// - 1 because of carriage return character
test_path = string_to_wstring(file_content.substr(start_index, stop_index - start_index - 1));
}
else
{
test_path = string_to_wstring(file_content.substr(start_index));
}
start_index = stop_index + 2; // + 2 to go past newline character and null character
printf("testing path: %ls\n", test_path.c_str());
#else
stop_index = file_content.find("\n", start_index);
if (stop_index != std::string::npos)
{
test_path = file_content.substr(start_index, stop_index - start_index);
}
else
{
test_path = file_content.substr(start_index);
}
start_index = stop_index + 1; // + 1 to go past newline character
printf("testing path: %s\n", test_path.c_str());
#endif
// +1 so separator isn't included
// If separator isn't found, the whole wstring is checked because npos + 1 is 0
const wstring_or_string& test_file = &test_path[test_path.rfind(path_separator) + 1];
delay_file(rh_map, test_file, delay_array_mutex);
print_rh_map(rh_map);
test_path.clear();
}
while (stop_index != std::string::npos);
}
int main(int argc, char* argv[])
{
printf("\ntest start\n");
test_all_inputs();
printf("test finished, press Enter to exit\n");
std::string x;
std::getline(std::cin, x);
return 0;
}
other_functions.h
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
#include <iostream>
#endif
#ifdef _WIN32
static std::wstring string_to_wstring(std::string line)
{
std::wstring ws_line;
for (size_t i = 1; i < line.length(); i += 2)
{
ws_line.push_back((line[i - 1]) + (line[i] << 8));
}
return ws_line;
}
#endif
static void remove_start_and_end_whitespace(wstring_or_string& file_name)
{
wstring_or_string white_space = {9, 11, 12, 13, 32}; // whitespace character values
size_t first_not_whitespace = file_name.find_first_not_of(white_space);
size_t last_not_whitespace = file_name.find_last_not_of(white_space);
if (first_not_whitespace == std::string::npos)
{
file_name.erase(0);
}
else
{
file_name = file_name.substr(first_not_whitespace, last_not_whitespace - first_not_whitespace + 1);
}
}
static void set_key_and_value(robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>>& rh_map, wstring_or_string& line)
{
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
static bool too_big_delay_seen = false; // this is used so the message will only be printed once
#endif
// 47 is forward slash, 45 is dash character, 10 is newline
// numbers used so it will work with both string and wstring
int current_delay = 0;
size_t current_index = (line.length() > 0 && line[0] == 47); // start at 1 if first character is forward slash
wstring_or_string file_name;
std::vector<int> delay_sequence;
delay_sequence.push_back(1); // first index keeps track of next index to get delay time from
// get file name
for (; current_index < line.length() && line[current_index] != 47 && line[current_index] != 10; current_index++)
{
file_name.push_back(line[current_index]);
}
if (line[0] != 47) // remove leading and trailing whitespace if first character wasn't forward slash
{
remove_start_and_end_whitespace(file_name);
}
// get delay sequence
for (current_index++; current_index < line.length() && line[current_index] != 10; current_index++)
{
if (line[current_index] >= 48 && line[current_index] <= 57) // character is a number
{
int current_digit = line[current_index] - 48;
// avoid going over INT_MAX
if (((INT_MAX / 10) > current_delay) || ((INT_MAX / 10) == current_delay && (INT_MAX % 10) >= current_digit))
{
current_delay = ((current_delay * 10) + current_digit);
}
else
{
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
if (!too_big_delay_seen)
{
too_big_delay_seen = true;
printf("delay time can only be %d milliseconds\n", INT_MAX);
}
#endif
current_delay = INT_MAX;
}
}
else if (line[current_index] == 47)
{
delay_sequence.push_back(current_delay);
current_delay = 0;
}
else if (line[current_index] == 45)
{
delay_sequence.push_back(-1); // sequence starts over after reaching the end
current_delay = 0;
break;
}
}
if (current_delay != 0)
{
delay_sequence.push_back(current_delay);
}
if (delay_sequence[delay_sequence.size() - 1] != -1)
{
delay_sequence.push_back(-2); // sequence ends without starting over
}
if (delay_sequence[1] != -2) // no delay sequence written on this line
{
std::unique_ptr<int[]> delay_sequence_unique_ptr = std::make_unique<int[]>(delay_sequence.size());
// copy delays into std::unique_ptr<int[]>
for (size_t i = 0; i < delay_sequence.size() && i < INT_MAX; i++)
{
delay_sequence_unique_ptr[i] = delay_sequence[i];
}
// delay sequence can't be longer that INT_MAX because first index keeps track of next index to get delay time from
if ((delay_sequence.size() > INT_MAX) || ((delay_sequence.size() == INT_MAX) && (delay_sequence[INT_MAX] >= 0)))
{
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
printf("only %d delays can be stored in a delay sequence\n", INT_MAX - 2);
#endif
delay_sequence_unique_ptr[INT_MAX] = -2;
}
rh_map[wstring_or_string(file_name)] = std::move(delay_sequence_unique_ptr);
}
}
static std::mutex read_delays_file(robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>>& rh_map)
{
std::ifstream delays_file;
std::string txt_file_name = "files_and_delays.txt";
// find files_and_delays.txt file with highest number at the end
// this is used in case the directory already has a file named files_and_delays.txt
for (int file_number = 0; std::filesystem::exists("files_and_delays" + std::to_string(file_number) + ".txt"); file_number++)
{
txt_file_name = "files_and_delays" + std::to_string(file_number) + ".txt";
}
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
printf("opening %s\n", txt_file_name.c_str());
#endif
delays_file.open(txt_file_name, std::ios::binary);
if (delays_file.fail())
{
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
printf("couldn't open %s\n", txt_file_name.c_str());
#endif
return std::mutex();
}
std::string file_content(
(std::istreambuf_iterator<char>(delays_file)),
(std::istreambuf_iterator<char>())
);
delays_file.close();
#ifdef _WIN32
if (file_content.length() < 2)
{
printf("files_and_delays.txt byte order mark is missing\nsave files_and_delays.txt as UTF-16 LE\n");
return std::mutex();
}
size_t start_index = 2; // first two bytes are BOM bytes
#else
size_t start_index = 0;
#endif
size_t stop_index = 0;
do // go through each line
{
#ifdef _WIN32
stop_index = file_content.find("\n\0", start_index);
std::wstring line = L"";
if (stop_index != std::string::npos)
{
// - 1 because of carriage return character
line = string_to_wstring(file_content.substr(start_index, stop_index - start_index - 1));
}
else
{
line = string_to_wstring(file_content.substr(start_index));
}
start_index = stop_index + 2;
#else
stop_index = file_content.find("\n", start_index);
std::string line = "";
if (stop_index != std::string::npos)
{
line = file_content.substr(start_index, stop_index - start_index);
}
else
{
line = file_content.substr(start_index);
}
start_index = stop_index + 1;
#endif
set_key_and_value(rh_map, line);
}
while (stop_index != std::string::npos);
return std::mutex();
}
void delay_file(robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>>& rh_map, const wstring_or_string& file_name, std::mutex& delay_array_mutex)
{
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
#ifdef _WIN32
char print_format[] = "%ls";
#else
char print_format[] = "%s";
#endif
#endif
robin_hood::unordered_node_map<wstring_or_string, std::unique_ptr<int[]>>::iterator rh_map_iter = rh_map.find(file_name);
if (rh_map_iter != rh_map.end())
{
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
printf(print_format, file_name.c_str());
printf(" found in hash map\n");
#endif
int delay = 0;
{
std::lock_guard<std::mutex> delay_array_mutex_lock(delay_array_mutex);
std::unique_ptr<int[]>& delay_sequence = rh_map_iter->second;
if (delay_sequence[1] == -1)
{
for (auto& it : rh_map)
{
it.second[0] = 1;
}
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
printf("all delay sequences reset\n");
#endif
}
else if (delay_sequence[delay_sequence[0]] == -1)
{
delay_sequence[0] = 1;
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
printf(print_format, file_name.c_str());
printf(" delay sequence reset\n");
#endif
}
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
else if (delay_sequence[delay_sequence[0]] == -2)
{
printf(print_format, file_name.c_str());
printf(" delay sequence already finished\n");
}
#endif
delay = delay_sequence[delay_sequence[0]];
delay_sequence[0] += (delay_sequence[delay_sequence[0]] >= 0); // if it's -1 or -2, it's at the end already
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
delay *= (delay >= 0); // it might be set to -1 or -2
printf(print_format, file_name.c_str());
printf(" access delayed for %d second(s) and %d millisecond(s)\n", delay / 1000, delay % 1000);
#endif
}
#ifdef THIS_IS_NOT_THE_TEST_PROGRAM
if (delay > 0) // it might be set to -1 or -2
{
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
}
#endif
}
#ifndef THIS_IS_NOT_THE_TEST_PROGRAM
else
{
printf(print_format, file_name.c_str());
printf(" not found in hash map\n");
}
#endif
}
load_extender_injector.cpp (only used on Windows because LD_PRELOAD and DYLD_INSERT_LIBRARIES can be used on Linux and Mac OS)
#include <tchar.h>
#include <iostream>
#include <string>
#include <Windows.h>
// easyhook.h installed with NuGet
// https://easyhook.github.io/documentation.html
#include <easyhook.h>
void get_exit_input()
{
std::wcout << "Press Enter to exit";
std::wstring input;
std::getline(std::wcin, input);
std::getline(std::wcin, input);
}
int _tmain(int argc, _TCHAR* argv[])
{
WCHAR* dllToInject32 = NULL;
WCHAR* dllToInject64 = NULL;
LPCWSTR lpApplicationName = argv[0];
DWORD lpBinaryType;
if (GetBinaryType(lpApplicationName, &lpBinaryType) == 0 || (lpBinaryType != 0 && lpBinaryType != 6))
{
std::wcout << "ERROR: This exe wasn't identified as 32-bit or as 64-bit";
get_exit_input();
return 1;
}
else if (lpBinaryType == 0)
{
dllToInject32 = (WCHAR*)L"load_extender_32.dll";
}
else
{
dllToInject64 = (WCHAR*)L"load_extender_64.dll";
}
DWORD processId;
std::wcout << "Enter the target process Id: ";
std::cin >> processId;
wprintf(L"Attempting to inject dll\n\n");
// Inject dllToInject into the target process Id, passing
// freqOffset as the pass through data.
NTSTATUS nt = RhInjectLibrary(
processId, // The process to inject into
0, // ThreadId to wake up upon injection
EASYHOOK_INJECT_DEFAULT,
dllToInject32, // 32-bit
dllToInject64, // 64-bit
NULL, // data to send to injected DLL entry point
0 // size of data to send
);
if (nt != 0)
{
printf("RhInjectLibrary failed with error code = %d\n", nt);
PWCHAR err = RtlGetLastErrorString();
std::wcout << err << "\n";
get_exit_input();
return 1;
}
else
{
std::wcout << L"Library injected successfully.\n";
}
get_exit_input();
return 0;
}