diff --git a/.gitmodules b/.gitmodules index 5ba57aa2d..e043508bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -38,3 +38,9 @@ [submodule "libs/cryptopp-cmake"] path = libs/cryptopp-cmake url = https://github.com/abdes/cryptopp-cmake +[submodule "libs/libui-ng"] + path = libs/libui-ng + url = git@github.com:libui-ng/libui-ng.git +[submodule "libs/wxWidgets"] + path = libs/wxWidgets + url = git@github.com:wxWidgets/wxWidgets.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 68ab82eaa..2af1a0655 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,8 @@ project(RepExtender) find_package(OpenGL REQUIRED) +include_directories ("${CMAKE_CURRENT_SOURCE_DIR}") + include_directories(${CMAKE_SOURCE_DIR}/include "${CMAKE_SOURCE_DIR}/libs/lua" "${CMAKE_SOURCE_DIR}/libs/rapidxml" "${CMAKE_SOURCE_DIR}/libs/rapidjson/include/rapidjson" "${CMAKE_SOURCE_DIR}/libs/LuaBridge/Source/LuaBridge" @@ -23,6 +25,19 @@ option(ZYDIS_BUILD_DOXYGEN "" OFF) add_subdirectory("libs/zydis") target_compile_options("Zydis" PUBLIC "/MD") +# wxWidgets +set(wxBUILD_CXX_STANDARD 17) +option(wxBUILD_INSTALL "" OFF) +option(wxBUILD_OPTIMISE "" ON) +option(wxBUILD_PIC "" OFF) +option(wxBUILD_SHARED "" OFF) +option(wxBUILD_STRIPPED_RELEASE "" ON) +option(wxBUILD_VENDOR "" "repentogon") +add_subdirectory ("libs/wxWidgets") +include_directories ("${CMAKE_CURRENT_SOURCE_DIR}/libs/wxWidgets/" "${CMAKE_CURRENT_SOURCE_DIR}/libs/wxWidgets/include") +target_compile_options(wxbase PUBLIC "/MD") +target_compile_options(wxcore PUBLIC "/MD") + # ANTLR option(WITH_STATIC_CRT "" OFF) option(ANTLR_BUILD_CPP_TESTS "" OFF) @@ -93,11 +108,11 @@ target_compile_options(libzhl PUBLIC "/MD" "/wd4251" "/wd4996") # Injected DLL FILE(GLOB LAUNCHER_SRC ${CMAKE_SOURCE_DIR}/launcher/*.cpp) -add_library(dsound SHARED ${LAUNCHER_SRC}) -target_include_directories(dsound PUBLIC ${CURL_INCLUDE_DIR}) -target_link_libraries(dsound libcurl cryptopp) -target_compile_options(dsound PUBLIC "/MD" "/wd4996") -target_link_libraries(dsound Imagehlp) +add_library(launcher SHARED ${LAUNCHER_SRC}) +target_include_directories(launcher PUBLIC ${CURL_INCLUDE_DIR}) +target_link_libraries(launcher libcurl cryptopp) +target_compile_options(launcher PUBLIC "/MD" "/wd4996") +target_link_libraries(launcher Imagehlp) # ImGUI enable_language(RC) @@ -188,10 +203,16 @@ add_custom_command(TARGET zhlREPENTOGON POST_BUILD COMMAND ${CMAKE_COMMAND} -E c add_custom_command(TARGET zhlREPENTOGON POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${PROJECT_SOURCE_DIR}/repentogon/resources-repentogon" "$/resources-repentogon") # add_custom_command(TARGET zhlDelirium POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${PROJECT_SOURCE_DIR}/delirium/resources-delirium" "$/resources-delirium") +# Injector +add_executable(injector WIN32 injector/injector.cpp injector/window.cpp injector/window.h) +target_compile_options(injector PUBLIC "/MD") +target_link_libraries(injector wxbase wxcore cryptopp bcrypt) + if(NOT ISAAC_DIRECTORY STREQUAL "") message (STATUS "Files will be installed to " ${ISAAC_DIRECTORY}) - add_custom_command(TARGET Lua5.4 POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$/Lua5.4.dll" "${ISAAC_DIRECTORY}") - add_custom_command(TARGET dsound POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$/dsound.dll" "${ISAAC_DIRECTORY}") + add_custom_command(TARGET Lua5.4 POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$/Lua5.4.dll" "${ISAAC_DIRECTORY}") + add_custom_command(TARGET injector POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$/injector.exe" "${ISAAC_DIRECTORY}") + add_custom_command(TARGET launcher POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$/launcher.dll" "${ISAAC_DIRECTORY}") add_custom_command(TARGET libzhl POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$/libzhl.dll" "${ISAAC_DIRECTORY}") add_custom_command(TARGET zhlREPENTOGON POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$/freetype.dll" "${ISAAC_DIRECTORY}") add_custom_command(TARGET zhlREPENTOGON POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "$/zhlREPENTOGON.dll" "${ISAAC_DIRECTORY}") @@ -218,4 +239,5 @@ target_link_libraries(libzhl Lua5.4 "Zydis" dbghelp) set_property(GLOBAL PROPERTY USE_FOLDERS ON) set_target_properties(freetype antlr4_shared imgui Lua5.4 stb stbc Zydis Zycore cryptopp libcurl_object libcurl_static curl_uninstall PROPERTIES FOLDER "External Libs") set_target_properties(libzhl zhlparser PROPERTIES FOLDER "libzhl") -set_target_properties(dsound zhlDelirium zhlREPENTOGON PROPERTIES FOLDER "Repentogon") +set_target_properties(launcher zhlDelirium zhlREPENTOGON PROPERTIES FOLDER "Repentogon") + diff --git a/injector/injector.cpp b/injector/injector.cpp new file mode 100644 index 000000000..fc9767bc8 --- /dev/null +++ b/injector/injector.cpp @@ -0,0 +1,309 @@ +#define _CRT_SECURE_NO_WARNINGS + +#include +#include +#include + +#include + +#include +#include "injector/window.h" + +/* Perform the early setup for the injection: create the Isaac process, + * allocate memory for the remote thread function etc. + * + * No extra thread is created in the process. ImGui should be initialized + * afterwards to setup the injector, and then the remote thread can be + * created. + * + * Return true of the initialization was sucessful, false otherwise. + */ +static int FirstStageInit(struct IsaacOptions const* options, HANDLE* process, void** page, size_t* functionOffset, PROCESS_INFORMATION* processInfo); + +static void Log(const char* fmt, ...); + +static void GenerateCLI(const struct IsaacOptions* options, char cli[256]); + +void GenerateCLI(const struct IsaacOptions* options, char cli[256]) { + memset(cli, 0, sizeof(cli)); + if (options->console) { + strcat(cli, "--console "); + } + + if (!options->updates) { + strcat(cli, "--skipupdates "); + } + + if (options->lua_debug) { + strcat(cli, "--luadebug "); + } + + if (options->level_stage) { + strcat(cli, "--set-stage="); + char buffer[13]; // 11 chars for a max int (including sign) + 1 char for space + 1 char for '\0' + sprintf(buffer, "%d ", options->level_stage); + strcat(cli, buffer); + } + + if (options->stage_type) { + strcat(cli, "--set-stage-type="); + char buffer[13]; // 11 chars for a max int (including sign) + 1 char for space + 1 char for '\0' + sprintf(buffer, "%d ", options->stage_type); + strcat(cli, buffer); + } + + if (options->lua_heap_size) { + strcat(cli, "--luaheapsize="); + strcat(cli, options->lua_heap_size); + } +} + +DWORD CreateIsaac(struct IsaacOptions const* options, PROCESS_INFORMATION* processInfo) { + STARTUPINFOA startupInfo; + memset(&startupInfo, 0, sizeof(startupInfo)); + + memset(processInfo, 0, sizeof(*processInfo)); + + char cli[256]; + GenerateCLI(options, cli); + + DWORD result = CreateProcessA("isaac-ng.exe", cli, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &startupInfo, processInfo); + if (result == 0) { + Log("Failed to create process: %d\n", GetLastError()); + return -1; + } + else { + Log("Started isaac-ng.exe in suspended state, processID = %d\n", processInfo->dwProcessId); + } + + return result; +} + +int UpdateMemory(HANDLE process, PROCESS_INFORMATION const* processInfo, void** page, size_t* functionOffset) { + void* remotePage = VirtualAllocEx(process, NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE); + if (!remotePage) { + Log("Failed to allocate memory in isaac-ng.exe to load the dsound DLL: %d\n", GetLastError()); + return -1; + } + else { + Log("Allocated memory for remote thread at %p\n", remotePage); + } + + SIZE_T bytesWritten = 0; + char zeroBuffer[4096]; + memset(zeroBuffer, 0, sizeof(zeroBuffer)); + WriteProcessMemory(process, remotePage, zeroBuffer, 4096, &bytesWritten); + + HMODULE kernel32 = GetModuleHandleA("kernel32.dll"); + if (!kernel32) { + Log("Unable to find kernel32.dll, WTF\n"); + return -1; + } + else { + Log("Acquired kernel32.dll at %p\n", kernel32); + } + + FARPROC getProcAddress = GetProcAddress(kernel32, "GetProcAddress"); + FARPROC loadLibraryA = GetProcAddress(kernel32, "LoadLibraryA"); + + if (!getProcAddress) { + Log("Unable to find GetProcAddress\n"); + return -1; + } + + if (!loadLibraryA) { + Log("Unable to find LoadLibraryA\n"); + return -1; + } + + Log("Acquired GetProcAddress at %p, LoadLibraryA at %p\n", getProcAddress, loadLibraryA); + + const char* dllName = "launcher.dll"; + const char* functionName = "LaunchZHL"; + size_t offset = 0; + // 0x0 + WriteProcessMemory(process, remotePage, &getProcAddress, sizeof(getProcAddress), &bytesWritten); + offset += bytesWritten; + // 0x4 + WriteProcessMemory(process, (char*)remotePage + offset, &loadLibraryA, sizeof(loadLibraryA), &bytesWritten); + offset += bytesWritten; + // 0x8 + WriteProcessMemory(process, (char*)remotePage + offset, dllName, strlen(dllName), &bytesWritten); + offset += (bytesWritten + 1); + // 0x16 (0x15 is a '\0') + WriteProcessMemory(process, (char*)remotePage + offset, functionName, strlen(functionName), &bytesWritten); + offset += (bytesWritten + 1); + + *functionOffset = offset; + + /* 0x21 (0x20 is a '\0') + * Call LoadLibraryA in the remote thread. + * The thread will push the name of the DLL from its stack. + * It will then call LoadLibraryA. + * + * This function is a THREAD_START_ROUTINE, with the following signature : + * DWORD WINAPI(LPVOID); + * + * WINAPI is __stdcall: arguments are pushed in reverse order on the stack and callee cleans the stack. + */ + char hook[128] = { + "\x55" // push ebp + "\x89\xe5" // mov ebp, esp + "\x53" // push ebx + "\x56" // push esi + "\x57" // push edi + "\x3e\x8b\x5d\x08" // mov ebx, dword ptr ds:[ebp+8], put parameter in ebx + "\x8b\x73\x04" // mov esi, dword ptr ds:[ebx+4], put LoadLibraryA in esi + "\x8d\x7b\x08" // lea edi, [ebx+8], put dllname in edi + "\x57" // push edi, push dllname on stack + "\xff\xd6" // call esi, call LoadLibraryA("launcher.dll") + "\x8d\x7b\x15" // lea edi, [ebx + 0x15], put function name in edi + "\x57" // push edi, put function name on stack + "\x50" // push eax, put library on stack + "\x8b\x33" // mov esi, dword ptr ds:[ebx], put GetProcAddress in esi + "\xff\xd6" // call esi, call GetProcAddress(lib, "LaunchZHL") + "\xff\xd0" // call eax, call LaunchZHL() + "\x5f" // pop edi + "\x5e" // pop esi + "\x5b" // pop ebx + "\x89\xec" // mov esp, ebp + "\xb8\x01\x00\x00\x00" // mov eax, 1 + "\x5d" // pop ebp + "\xc2\x04\x00" // ret 4 + }; + WriteProcessMemory(process, (char*)remotePage + offset, hook, 128, &bytesWritten); + + *page = remotePage; + return 0; +} + +int FirstStageInit(struct IsaacOptions const* options, HANDLE* outProcess, void** page, size_t* functionOffset, PROCESS_INFORMATION* processInfo) { + { + FILE* f = fopen("injector.log", "w"); + if (f) { + fclose(f); + } + } + + Log("Starting injector\n"); + DWORD processId = CreateIsaac(options, processInfo); + + HANDLE process = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, + FALSE, processInfo->dwProcessId); + if (!process) { + Log("Failed to open process: %d\n", GetLastError()); + return -1; + } + else { + Log("Acquired handle to isaac-ng.exe, process ID = %d\n", processInfo->dwProcessId); + } + + if (UpdateMemory(process, processInfo, page, functionOffset)) { + return -1; + } + + *outProcess = process; + + return 0; +} + +int CreateAndWait(HANDLE process, void* remotePage, size_t functionOffset) { + HANDLE remoteThread = CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)((char*)remotePage + functionOffset), remotePage, 0, NULL); + if (!remoteThread) { + Log("Error while creating remote thread: %d\n", GetLastError()); + return -1; + } + else { + Log("Created remote thread in isaac-ng.exe\n"); + } + + Log("Waiting for remote thread to complete\n"); + DWORD result = WaitForSingleObject(remoteThread, 60 * 1000); + switch (result) { + case WAIT_OBJECT_0: + Log("RemoteThread completed\n"); + break; + + case WAIT_ABANDONED: + Log("This shouldn't happened: RemoteThread returned WAIT_ABANDONNED\n"); + return -1; + + case WAIT_TIMEOUT: + Log("RemoteThread timed out\n"); + return -1; + + case WAIT_FAILED: + Log("WaitForSingleObject on RemoteThread failed: %d\n", GetLastError()); + return -1; + } + + return 0; +} + +void Log(const char* fmt, ...) { + va_list va; + va_start(va, fmt); + + FILE* f = fopen("injector.log", "a"); + if (!f) { + f = stderr; + } + + char buffer[4096]; + time_t now = time(NULL); + struct tm* tm = localtime(&now); + strftime(buffer, 4095, "%Y-%m-%d %H:%M:%S", tm); + fprintf(f, "[%s] ", buffer); + vfprintf(f, fmt, va); + va_end(va); +} + +int InjectIsaac(int updates, int console, int lua_debug, int level_stage, int stage_type, const char* lua_heap_size) { + HANDLE process; + void* remotePage; + size_t functionOffset; + PROCESS_INFORMATION processInfo; + + struct IsaacOptions options; + options.updates = updates; + options.console = console; + options.lua_debug = lua_debug; + options.level_stage = level_stage; + options.stage_type = stage_type; + options.lua_heap_size = lua_heap_size; + + if (FirstStageInit(&options, &process, &remotePage, &functionOffset, &processInfo)) { + return -1; + } + + if (CreateAndWait(process, remotePage, functionOffset)) { + return -1; + } + + DWORD result = ResumeThread(processInfo.hThread); + if (result == -1) { + Log("Failed to resume isaac-ng.exe main thread: %d\n", GetLastError()); + return -1; + } + else { + Log("Resumed main thread of isaac-ng.exe, previous supend count was %d\n", result); + } + + Log("Waiting for isaac-ng.exe main thread to return\n"); + WaitForSingleObject(processInfo.hProcess, INFINITE); + Log("isaac-ng.exe completed, shutting down injector\n"); + + CloseHandle(processInfo.hProcess); + CloseHandle(processInfo.hThread); + + return 0; +} + +bool IsaacLauncher::App::OnInit() { + MainFrame* frame = new MainFrame(); + frame->Show(); + frame->PostInit(); + return true; +} + +wxIMPLEMENT_APP(IsaacLauncher::App); \ No newline at end of file diff --git a/injector/window.cpp b/injector/window.cpp new file mode 100755 index 000000000..88ba3cf75 --- /dev/null +++ b/injector/window.cpp @@ -0,0 +1,499 @@ +// #include + +#include +#include + +// #include +// #include + +#include "injector/window.h" + +wxBEGIN_EVENT_TABLE(IsaacLauncher::MainFrame, wxFrame) + EVT_COMBOBOX(IsaacLauncher::WINDOW_COMBOBOX_LEVEL, IsaacLauncher::MainFrame::OnLevelSelect) + EVT_COMBOBOX(IsaacLauncher::WINDOW_COMBOBOX_LAUNCH_MODE, IsaacLauncher::MainFrame::OnLauchModeSelect) + EVT_CHECKBOX(IsaacLauncher::WINDOW_CHECKBOX_REPENTOGON_CONSOLE, IsaacLauncher::MainFrame::OnOptionSelected) + EVT_CHECKBOX(IsaacLauncher::WINDOW_CHECKBOX_REPENTOGON_UPDATES, IsaacLauncher::MainFrame::OnOptionSelected) + EVT_CHECKBOX(IsaacLauncher::WINDOW_CHECKBOX_VANILLA_LUADEBUG, IsaacLauncher::MainFrame::OnOptionSelected) + EVT_TEXT(IsaacLauncher::WINDOW_TEXT_VANILLA_LUAHEAPSIZE, IsaacLauncher::MainFrame::OnCharacterWritten) + EVT_BUTTON(IsaacLauncher::WINDOW_BUTTON_LAUNCH_BUTTON, IsaacLauncher::MainFrame::Launch) +wxEND_EVENT_TABLE() + +class LuaHeapSizeValidator : public wxValidator { +public: + LuaHeapSizeValidator() { } + LuaHeapSizeValidator(const LuaHeapSizeValidator& ) { } + + wxObject* Clone() const override { + return new LuaHeapSizeValidator(); + } + +private: + void OnCharacterWritten(wxKeyEvent& event) { + wxTextCtrl* ctrl = dynamic_cast(event.GetEventObject()); + wxString original = ctrl->GetValue(); + + } + + wxDECLARE_EVENT_TABLE(); +}; + +wxBEGIN_EVENT_TABLE(LuaHeapSizeValidator, wxValidator) + // EVT_CHAR(LuaHeapSizeValidator::OnCharacterWritten) +wxEND_EVENT_TABLE() + +namespace IsaacLauncher { + static std::tuple MakeBoldFont(wxFrame* frame); + static wxComboBox* CreateLevelsComboBox(wxFrame* frame); + + static const char* mandatoryFileNames[] = { + "libzhl.dll", + "zhlRepentogon.dll", + "freetype.dll", + NULL + }; + + Version const knownVersions[] = { + { "04469d0c3d3581936fcf85bea5f9f4f3a65b2ccf96b36310456c9626bac36dc6", "v1.7.9b.J835 (Steam)", true }, + { NULL, NULL, false } + }; + + Version const* const validVersions[] = { + NULL + }; + + Version const* MainFrame::GetVersion(const char* hash) { + Version const* version = knownVersions; + while (version->version) { + if (!strcmp(hash, version->hash)) { + return version; + } + + ++version; + } + + return NULL; + } + + bool MainFrame::FileExists(const char* name) { + WIN32_FIND_DATAA data; + memset(&data, 0, sizeof(data)); + HANDLE result = FindFirstFileA(name, &data); + bool ret = result != INVALID_HANDLE_VALUE; + if (ret) { + FindClose(result); + } + + return ret; + } + + MainFrame::MainFrame() : wxFrame(nullptr, wxID_ANY, "REPENTOGON Launcher") { + memset(&_options, 0, sizeof(_options)); + _grid = new wxGridBagSizer(0, 20); + _updates = _console = nullptr; + _luaHeapSize = nullptr; + _enabledRepentogon = false; + + AddLaunchOptions(); + AddRepentogonOptions(); + AddVanillaOptions(); + + wxSizer* horizontalSizer = new wxBoxSizer(wxHORIZONTAL); + horizontalSizer->Add(_grid, 0, wxLEFT, 20); + + wxSizer* verticalSizer = new wxBoxSizer(wxVERTICAL); + wxTextCtrl* logWindow = new wxTextCtrl(this, -1, wxEmptyString, wxDefaultPosition, wxSize(-1, 200), wxTE_READONLY | wxTE_MULTILINE | wxTE_RICH); + logWindow->SetBackgroundColour(*wxWHITE); + _logWindow = logWindow; + verticalSizer->Add(logWindow, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 20); + verticalSizer->Add(horizontalSizer); + SetSizer(verticalSizer); + + SetBackgroundColour(wxColour(237, 237, 237)); + } + + void MainFrame::AddLaunchOptions() { + auto [sourceFont, boldFont] = MakeBoldFont(this); + boldFont.SetPointSize(14); + SetFont(boldFont); + wxStaticText* text = new wxStaticText(this, -1, "Launch mode"); + SetFont(sourceFont); + + wxSizer* box = new wxBoxSizer(wxHORIZONTAL); + box->Add(new wxStaticText(this, -1, "Launch mode: ")); + + wxComboBox* modes = new wxComboBox(this, WINDOW_COMBOBOX_LAUNCH_MODE); + modes->Insert("Repentogon", 0, (void*)nullptr); + modes->Insert("Vanilla", 0, (void*)nullptr); + modes->SetValue("Repentogon"); + + box->Add(modes); + + _grid->Add(text, wxGBPosition(0, 2), wxDefaultSpan, wxALIGN_CENTER); + _grid->Add(box, wxGBPosition(1, 2)); + + wxButton* launchButton = new wxButton(this, WINDOW_BUTTON_LAUNCH_BUTTON, "Launch game"); + _grid->Add(launchButton, wxGBPosition(2, 2), wxDefaultSpan, wxEXPAND); + } + + void MainFrame::AddRepentogonOptions() { + auto [sourceFont, boldFont] = MakeBoldFont(this); + boldFont.SetPointSize(14); + SetFont(boldFont); + wxStaticText* text = new wxStaticText(this, -1, "REPENTOGON Options"); + SetFont(sourceFont); + + wxCheckBox* updates = new wxCheckBox(this, WINDOW_CHECKBOX_REPENTOGON_UPDATES, "Check for updates"); + updates->SetValue(true); + wxCheckBox* console = new wxCheckBox(this, WINDOW_CHECKBOX_REPENTOGON_CONSOLE, "Enable console window"); + console->SetValue(false); + + _updates = updates; + _console = console; + + _grid->Add(text, wxGBPosition(0, 0), wxDefaultSpan, wxALIGN_CENTER); + _grid->Add(updates, wxGBPosition(1, 0)); + _grid->Add(console, wxGBPosition(2, 0)); + } + + void MainFrame::AddVanillaOptions() { + auto [sourceFont, boldFont] = MakeBoldFont(this); + boldFont.SetPointSize(14); + SetFont(boldFont); + wxStaticText* text = new wxStaticText(this, -1, "Universal Options"); + SetFont(sourceFont); + + wxSizer* levelSelectSizer = new wxBoxSizer(wxHORIZONTAL); + levelSelectSizer->Add(new wxStaticText(this, -1, "Starting stage: ")); + wxComboBox* levelSelect = CreateLevelsComboBox(this); + levelSelectSizer->Add(levelSelect); + + _grid->Add(text, wxGBPosition(0, 1), wxDefaultSpan, wxALIGN_CENTER); + _grid->Add(levelSelectSizer, wxGBPosition(1, 1)); + _grid->Add(new wxCheckBox(this, WINDOW_CHECKBOX_VANILLA_LUADEBUG, "Enable luadebug (unsafe)"), wxGBPosition(2, 1)); + + LuaHeapSizeValidator validator; + wxSizer* heapSizeBox = new wxBoxSizer(wxHORIZONTAL); + wxTextCtrl* heapSizeCtrl = new wxTextCtrl(this, WINDOW_TEXT_VANILLA_LUAHEAPSIZE, "1024M"); + heapSizeCtrl->SetValidator(validator); + _luaHeapSize = heapSizeCtrl; + wxStaticText* heapSizeText = new wxStaticText(this, -1, "Lua heap size: "); + heapSizeBox->Add(heapSizeText); + heapSizeBox->Add(heapSizeCtrl); + _grid->Add(heapSizeBox, wxGBPosition(3, 1)); + } + + void MainFrame::OnLevelSelect(wxCommandEvent& event) { + wxComboBox* box = dynamic_cast(event.GetEventObject()); + wxString string = box->GetValue(); + std::cmatch match; + if (std::regex_search(string.c_str().AsChar(), match, std::basic_regex("([0-9])\\.([0-9])"))) { + int stage = std::stoi(match[1].str(), NULL, 0); + int type = std::stoi(match[2].str(), NULL, 0); + _options.level_stage = stage; + _options.stage_type = type; + } + } + + void MainFrame::OnLauchModeSelect(wxCommandEvent& event) { + wxComboBox* box = dynamic_cast(event.GetEventObject()); + if (box->GetValue() == "Vanilla") { + _updates->Disable(); + _console->Disable(); + _options.mode = LAUNCH_MODE_VANILLA; + } + else { + _updates->Enable(); + _console->Enable(); + _options.mode = LAUNCH_MODE_REPENTOGON; + } + } + + void MainFrame::OnCharacterWritten(wxCommandEvent& event) { + wxTextCtrl* object = dynamic_cast(event.GetEventObject()); + wxString content = object->GetValue(); + } + + void MainFrame::OnOptionSelected(wxCommandEvent& event) { + wxCheckBox* box = dynamic_cast(event.GetEventObject()); + switch (box->GetId()) { + case WINDOW_CHECKBOX_REPENTOGON_CONSOLE: + _options.console = box->GetValue(); + break; + + case WINDOW_CHECKBOX_REPENTOGON_UPDATES: + _options.updates = box->GetValue(); + break; + + case WINDOW_CHECKBOX_VANILLA_LUADEBUG: + _options.lua_debug = box->GetValue(); + break; + + default: + return; + } + } + + void MainFrame::Launch(wxCommandEvent& event) { + + } + + void MainFrame::PostInit() { + Log("Welcome to the REPENTOGON Launcher version %s", IsaacLauncher::version); + if (!CheckIsaacVersion()) { + return; + } + + if (!CheckRepentogonInstallation()) { + return; + } + + if (!CheckRepentogonVersion()) { + return; + } + } + + bool MainFrame::CheckIsaacVersion() { + Log("Checking Isaac version..."); + WIN32_FIND_DATAA isaac; + memset(&isaac, 0, sizeof(isaac)); + HANDLE isaacFile = FindFirstFileA("isaac-ng.exe", &isaac); + if (isaacFile == INVALID_HANDLE_VALUE) { + LogError("Unable to find isaac-ng.exe"); + return false; + } + FindClose(isaacFile); + + DWORD size = isaac.nFileSizeLow; + char* content = (char*)malloc(size + 2); + if (!content) { + LogError("Unable to allocate memory to read game's executable, aborting"); + return false; + } + + FILE* exe = fopen(isaac.cFileName, "rb"); + fread(content, 1, size, exe); + content[size] = '\0'; + /* std::string sha256Str; + CryptoPP::SHA256 sha256; + CryptoPP::StringSource(content, true, + new CryptoPP::HashFilter(sha256, + new CryptoPP::HexEncoder( + new CryptoPP::StringSink(sha256Str) + ) + ) + ); */ + BCRYPT_ALG_HANDLE alg; + NTSTATUS err = BCryptOpenAlgorithmProvider(&alg, BCRYPT_SHA256_ALGORITHM, NULL, 0); + DWORD buffSize; + DWORD dummy; + err = BCryptGetProperty(alg, BCRYPT_OBJECT_LENGTH, (unsigned char*)&buffSize, sizeof(buffSize), &dummy, 0); + BCRYPT_HASH_HANDLE hashHandle; + unsigned char* hashBuffer = (unsigned char*)malloc(buffSize); + err = BCryptCreateHash(alg, &hashHandle, hashBuffer, buffSize, NULL, 0, 0); + err = BCryptHashData(hashHandle, (unsigned char*)content, size, 0); + DWORD hashSize; + err = BCryptGetProperty(alg, BCRYPT_HASH_LENGTH, (unsigned char*)&hashSize, sizeof(hashSize), &dummy, 0); + unsigned char* hash = (unsigned char*)malloc(hashSize); + char* hashHex = (char*)malloc(hashSize * 2 + 1); + err = BCryptFinishHash(hashHandle, hash, hashSize, 0); + free(hashBuffer); + err = BCryptCloseAlgorithmProvider(&alg, 0); + + for (int i = 0; i < hashSize; ++i) { + sprintf(hashHex + 2 * i, "%02hhx", hash[i]); + } + + // const char* sha256p = sha256Str.c_str(); + Log("\tFound isaac-ng.exe. Hash: %s", hashHex); + Version const* version = GetVersion(hashHex); + if (!version) { + LogError("Unknown version. REPENTOGON will not launch."); + return false; + } + else if (!version->valid) { + LogError("This version of the game does not support REPENTOGON."); + return false; + } + + free(hash); + + Log("\tIdentified REPENTOGON compatible version %s", version->version); + + return true; + } + + bool MainFrame::CheckRepentogonInstallation() { + Log("Checking Repentogon installation..."); + const char** file = mandatoryFileNames; + bool ok = true; + while (*file) { + if (FileExists(*file)) { + Log("\t%s: found", *file); + } + else { + ok = false; + LogError("\t%s: not found", *file); + } + + ++file; + } + + return ok; + } + + bool MainFrame::CheckRepentogonVersion() { + Log("Checking Repentogon version..."); + HMODULE repentogon = LoadLibraryExA("zhlRepentogon.dll", NULL, DONT_RESOLVE_DLL_REFERENCES); + if (!repentogon) { + LogError("Unable to open zhlRepentogon.dll"); + return false; + } + + HMODULE zhl = LoadLibraryExA("libzhl.dll", NULL, DONT_RESOLVE_DLL_REFERENCES); + if (!zhl) { + FreeLibrary(repentogon); + LogError("Unable to open libzhl.dll"); + return false; + } + + bool result = false; + FARPROC repentogonVersion = GetProcAddress(repentogon, "__REPENTOGON_VERSION"); + FARPROC zhlVersion = GetProcAddress(zhl, "__ZHL_VERSION"); + + if (!repentogonVersion) { + LogError("Unable to get Repentogon's version (%d)", GetLastError()); + result = false; + goto end; + } + + if (!zhlVersion) { + LogError("Unable to get ZHL's version"); + result = false; + goto end; + } + + const char* repentogonVersionStr = *(const char**)repentogonVersion; + const char* zhlVersionStr = *(const char**)zhlVersion; + + Log("\tFound Repentogon version %s", repentogonVersionStr); + Log("\tFound ZHL version %s", zhlVersionStr); + + if (strcmp(repentogonVersionStr, zhlVersionStr)) { + LogError("Repentogon/ZHL version mismatch"); + result = false; + goto end; + } + + Log("\tRepentogon and ZHL versions match"); + result = true; + + end: + FreeLibrary(repentogon); + FreeLibrary(zhl); + return result; + } + + void MainFrame::Log(const char* fmt, ...) { + char buffer[4096]; + va_list va; + va_start(va, fmt); + int count = vsnprintf(buffer, 4096, fmt, va); + va_end(va); + + if (count == 0) + return; + + wxString text(buffer); + if (buffer[count] != '\n') + text += '\n'; + _logWindow->AppendText(text); + } + + void MainFrame::LogError(const char* fmt, ...) { + char buffer[4096]; + va_list va; + va_start(va, fmt); + int count = vsnprintf(buffer, 4096, fmt, va); + va_end(va); + + if (count == 0) + return; + + wxString text(buffer); + text.Prepend("[ERROR] "); + if (buffer[count] != '\n') + text += '\n'; + wxTextAttr attr = _logWindow->GetDefaultStyle(); + _logWindow->SetDefaultStyle(wxTextAttr(*wxRED)); + _logWindow->AppendText(text); + _logWindow->SetDefaultStyle(attr); + } + + std::tuple MakeBoldFont(wxFrame* frame) { + wxFont source = frame->GetFont(); + wxFont bold = source; + bold.MakeBold(); + return std::make_tuple(source, bold); + } + + wxComboBox* CreateLevelsComboBox(wxFrame* frame) { + wxComboBox* box = new wxComboBox(frame, WINDOW_COMBOBOX_LEVEL, "Start level"); + const char* names[] = { + "Basement", + "Cellar", + "Burning Basement", + "Downpour", + "Dross", + "Caves", + "Catacombs", + "Flooded Caves", + "Mines", + "Ashpit", + "Depths", + "Necropolis", + "Dank Depths", + "Mausoleum", + "Gehenna", + "Womb", + "Utero", + "Scarred Womb", + "Corpse", + NULL + }; + + const char* uniqueNames[] = { + "??? (9.0)", + "Sheol (10.0)", + "Cathedral (10.1)", + "Dark Room (11.0)", + "Chest (11.1)", + "The Void (12.0)", + "Home (13.0)", + NULL + }; + + int pos = 0; + box->Insert(wxString("--"), pos++); + box->SetValue("--"); + int variant = 0; + for (const char* name : names) { + if (!name) + continue; + + wxString s; + int level = 1 + 2 * (variant / 5); + box->Insert(s.Format("%s I (%d.%d)", name, level, variant % 5), pos++, (void*)nullptr); + box->Insert(s.Format("%s II (%d.%d)", name, level + 1, variant % 5), pos++, (void*)nullptr); + + ++variant; + } + + for (const char* name : uniqueNames) { + if (!name) + continue; + box->Insert(wxString(name), pos++, (void*)nullptr); + } + + return box; + } +} \ No newline at end of file diff --git a/injector/window.h b/injector/window.h new file mode 100755 index 000000000..f1d3f58cc --- /dev/null +++ b/injector/window.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include + +enum LaunchMode { + LAUNCH_MODE_VANILLA, + LAUNCH_MODE_REPENTOGON +}; + +struct IsaacOptions { + LaunchMode mode; + + // Repentogon options + bool updates; + bool console; + + // Game options + bool lua_debug; + int level_stage; + int stage_type; + const char* lua_heap_size; +}; + +namespace IsaacLauncher { + static constexpr const char* version = "alpha"; + + struct Version { + const char* hash; + const char* version; + bool valid; + }; + + extern Version const knownVersions[]; + + enum Windows { + WINDOW_COMBOBOX_LEVEL, + WINDOW_COMBOBOX_LAUNCH_MODE, + WINDOW_CHECKBOX_REPENTOGON_CONSOLE, + WINDOW_CHECKBOX_REPENTOGON_UPDATES, + WINDOW_CHECKBOX_VANILLA_LUADEBUG, + WINDOW_TEXT_VANILLA_LUAHEAPSIZE, + WINDOW_BUTTON_LAUNCH_BUTTON + }; + + class App : public wxApp { + public: + bool OnInit() override; + }; + + class MainFrame : public wxFrame { + public: + MainFrame(); + + void Log(const char* fmt, ...); + void LogError(const char* fmt, ...); + void PostInit(); + + static Version const* GetVersion(const char* hash); + static bool FileExists(const char* name); + + private: + /* Window building. */ + void AddLaunchOptions(); + void AddRepentogonOptions(); + void AddVanillaOptions(); + + /* Event handlers. */ + void OnLevelSelect(wxCommandEvent& event); + void OnLauchModeSelect(wxCommandEvent& event); + void OnCharacterWritten(wxCommandEvent& event); + void OnOptionSelected(wxCommandEvent& event); + void Launch(wxCommandEvent& event); + + /* Post init stuff. */ + bool CheckIsaacVersion(); + bool CheckRepentogonInstallation(); + bool CheckRepentogonVersion(); + + IsaacOptions _options; + wxGridBagSizer* _grid; + wxCheckBox* _updates; + wxCheckBox* _console; + wxTextCtrl* _luaHeapSize; + wxTextCtrl* _logWindow; + + bool _enabledRepentogon; + + wxDECLARE_EVENT_TABLE(); + }; +} \ No newline at end of file diff --git a/launcher/dllmain.cpp b/launcher/dllmain.cpp index f4b632ae7..d42e2e7ef 100644 --- a/launcher/dllmain.cpp +++ b/launcher/dllmain.cpp @@ -117,13 +117,12 @@ DWORD RedirectLua(HMODULE* outLua) { static HMODULE luaHandle = NULL; -BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) -{ - if(ul_reason_for_call == DLL_PROCESS_ATTACH) - { +extern "C" { + void __declspec(dllexport) LaunchZHL() { sLogger->SetOutputFile("dsound.log", "w", true); sLogger->SetFlushOnLog(true); sLogger->Info("Loaded REPENTOGON dsound.dll\n"); + if (HasCommandLineArgument("-repentogonoff")) { FILE* f = fopen("repentogon.log", "a"); if (f) { @@ -132,7 +131,7 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReser fprintf(f, "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"); fclose(f); } - return TRUE; + return; } sLogger->Info("dsound: Overriding Lua 5.3.3 with Lua 5.4\n"); @@ -181,16 +180,17 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReser WIN32_FIND_DATA findData; HANDLE hFind = FindFirstFile("*", &findData); - if(hFind != INVALID_HANDLE_VALUE) + LoadLibrary("libzhl.dll"); + if (hFind != INVALID_HANDLE_VALUE) { do { - if(!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { - const char *fileName = findData.cFileName; + const char* fileName = findData.cFileName; int n = strlen(fileName); - if(n >= 4 && !_stricmp(fileName + n - 4, ".dll") && !_strnicmp(fileName, "zhl", 3)) + if (n >= 4 && !_stricmp(fileName + n - 4, ".dll") && !_strnicmp(fileName, "zhl", 3)) { HMODULE library = LoadLibraryA(fileName); if (!library) @@ -206,7 +206,7 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReser //printf("loaded mod %s\n", fileName); } } - } while(FindNextFile(hFind, &findData)); + } while (FindNextFile(hFind, &findData)); FindClose(hFind); } @@ -226,7 +226,11 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReser sLogger->Info("dsound: Loaded all ZHL mods in %d msecs\n", GetTickCount() - startTime); } - else if(ul_reason_for_call == DLL_PROCESS_DETACH) +} + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) +{ + if(ul_reason_for_call == DLL_PROCESS_DETACH) { sLogger->Info("dsound: Unloading dsound.dll\n"); /* printf("unloading libs\n"); diff --git a/libs/libui-ng b/libs/libui-ng new file mode 160000 index 000000000..8de4a5c83 --- /dev/null +++ b/libs/libui-ng @@ -0,0 +1 @@ +Subproject commit 8de4a5c8336f82310df1c6dad51cb732113ea114 diff --git a/libs/wxWidgets b/libs/wxWidgets new file mode 160000 index 000000000..ac3cea5bc --- /dev/null +++ b/libs/wxWidgets @@ -0,0 +1 @@ +Subproject commit ac3cea5bcd5a3fc3d52648efb4dceacbcc9d0698 diff --git a/libzhl/SigScan.cpp b/libzhl/SigScan.cpp index 008ad4598..9b6fe79b5 100644 --- a/libzhl/SigScan.cpp +++ b/libzhl/SigScan.cpp @@ -1,3 +1,4 @@ +#include "Log.h" #include "SigScan.h" #include #include @@ -197,6 +198,12 @@ bool SigScan::Scan(Callback callback) } s_pLastAddress = s_pBase; + ZHL::Logger logger; + logger.Log("[ERROR] Unable to find signature "); + FILE* f = logger.GetFile(); + for (size_t i = 0; i < m_iLength; ++i) + fprintf(f, "%hhx", m_sig[i]); + fprintf(f, "\n"); return false; } diff --git a/libzhl/Version.cpp b/libzhl/Version.cpp new file mode 100755 index 000000000..217046485 --- /dev/null +++ b/libzhl/Version.cpp @@ -0,0 +1,5 @@ +#include "libzhl.h" + +extern "C" { + LIBZHL_API const char* __ZHL_VERSION = "1.1.0"; +} \ No newline at end of file diff --git a/repentogon/ImGuiFeatures/ImGui.cpp b/repentogon/ImGuiFeatures/ImGui.cpp index afa598572..47617db40 100644 --- a/repentogon/ImGuiFeatures/ImGui.cpp +++ b/repentogon/ImGuiFeatures/ImGui.cpp @@ -274,7 +274,7 @@ static std::vector pressedKeys; LRESULT CALLBACK windowProc_hook(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { - if (shutdownInitiated) + if (shutdownInitiated || !g_Game) return CallWindowProc(windowProc, hWnd, uMsg, wParam, lParam); // Enable the overlay using the grave key, disable using ESC diff --git a/repentogon/Version.cpp b/repentogon/Version.cpp new file mode 100755 index 000000000..25601948b --- /dev/null +++ b/repentogon/Version.cpp @@ -0,0 +1,3 @@ +extern "C" { + __declspec(dllexport) const char* __REPENTOGON_VERSION = "1.1.0"; +} \ No newline at end of file