diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f735399 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# IDE stuff +.idea +.vs + +# Build directories +cmake-build-* +build \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a7887f0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "SokuLib"] + path = SokuLib + url = https://github.com/SokuDev/SokuLib diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1a839a6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.15) +cmake_policy(SET CMP0091 NEW) +set(PROJECT_NAME SaveRep) +project("${PROJECT_NAME}" C CXX) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) + +set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/install") +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + +add_definitions(-DWINVER=0x0501 -D_WIN32_WINNT=0x0501) +if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" AND "${CMAKE_CXX_SIMULATE_ID}" STREQUAL "MSVC") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-c++11-narrowing -Wno-microsoft-cast") +endif () +SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /Brepro") +SET(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} /Brepro") +SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /Brepro") + +# SokuLib +add_subdirectory(SokuLib) + +# Module +add_library( + "${PROJECT_NAME}" + MODULE + src/main.cpp + src/version.rc +) +target_compile_options("${PROJECT_NAME}" PRIVATE /Zi) +target_compile_definitions("${PROJECT_NAME}" PRIVATE DIRECTINPUT_VERSION=0x0800 CURL_STATICLIB _CRT_SECURE_NO_WARNINGS $<$:_DEBUG>) +target_link_directories("${PROJECT_NAME}" PRIVATE lib) +target_link_libraries( + "${PROJECT_NAME}" + SokuLib +) \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..03d4590 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 SokuDev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..440a0f3 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# SaveRep mod for Touhou Hisoutensoku + +This mod is to save replay when in one of the following situations: + +- the user is p1 or p2 in network battle, and: + - p1 or p2 presses ESC to end the game, or + - the game ends before one of the players wins because of desync, or + - the connection is lost +- the user is spectating, and: + - the user presses ESC to stop spectating, or + - the connection is lost + +## Build +Requires CMake, git and the VisualStudio compiler (MSVC). +Both git and cmake needs to be in the PATH environment variable. + +All the following commands are to be run inside the visual studio 32bits compiler +command prompt (called `x86 Native Tools Command Prompt for VS 20XX` in the start menu), unless stated otherwise. + +## Initialization +First go inside the folder you want the repository to be in. +In this example it will be C:\Users\PinkySmile\SokuProjects but remember to replace this +with the path for your machine. If you don't want to type the full path, you can drag and +drop the folder onto the console. + +`cd C:\Users\PinkySmile\SokuProjects` + +Now let's download the repository and initialize it for the first time +``` +git clone https://github.com/Hagb/SaveRep +cd SaveRep +git submodule init +git submodule update +mkdir build +cd build +cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Debug +``` +Note that if you want to build in Release, you should replace `-DCMAKE_BUILD_TYPE=Debug` with `-DCMAKE_BUILD_TYPE=Release`. + +## Compiling +Now, to build the mod, go to the build directory (if you did the previous step you already are) +`cd C:\Users\PinkySmile\SokuProjects\SaveRep\build` and invoke the compiler by running `cmake --build . --target SaveRep`. If you change the name of the mod (in the add_library statement in CMakeLists.txt), you will need to replace 'SaveRep' by the name of your mod in the previous command. + +You should find the resulting SaveRep.dll mod inside the build folder that can be to SWRSToys.ini. +In my case, I would add this line to it `SaveRep=C:/Users/PinkySmile/SokuProjects/SaveRep/build/SaveRep.dll`. diff --git a/SokuLib b/SokuLib new file mode 160000 index 0000000..a2372ee --- /dev/null +++ b/SokuLib @@ -0,0 +1 @@ +Subproject commit a2372eecde2830bc7091cb2fd5f1791ffde73a5b diff --git a/lib/d3d9.lib b/lib/d3d9.lib new file mode 100644 index 0000000..f6eddc6 Binary files /dev/null and b/lib/d3d9.lib differ diff --git a/lib/d3dx9.lib b/lib/d3dx9.lib new file mode 100644 index 0000000..11853bf Binary files /dev/null and b/lib/d3dx9.lib differ diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..def9430 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,151 @@ +// +// Created by PinkySmile on 31/10/2020 +// + +// #include +// clang-format off + +#include +#include +// clang-format on +#include "BattleManager.hpp" +#include "BattleMode.hpp" +#include "InputManager.hpp" +#include +// #include "Net" +#include "Hash.hpp" +#include "NetObject.hpp" +#include "Scenes.hpp" +#include "Tamper.hpp" +#include "VTables.hpp" + +static int /*SokuLib::Scene*/ (SokuLib::BattleWatch::*ogBattleWatchOnProcess)(); +static int /*SokuLib::Scene*/ (SokuLib::BattleClient::*ogBattleClientOnProcess)(); +static int /*SokuLib::Scene*/ (SokuLib::BattleServer::*ogBattleServerOnProcess)(); +static const auto spectatingSaveReplayIfAllow = (void(__thiscall *)(SokuLib::NetObject *))(0x454240); +static const auto battleSaveReplay = (void (*)())(0x43ebe0); +static const auto get00899840 = (char *(*)())(0x0043df40); +// static const auto getReplayPath +// = (void(__thiscall *)(SokuLib::InputManager *, char *replay_path, const char *profile1name, const char *profile2name))(0x42cb30); +// static const auto writeReplay = (void(__thiscall *)(SokuLib::InputManager *, const char *path))(0x42b2d0); + +static int /*SokuLib::Scene*/ __fastcall CBattleWatch_OnProcess(SokuLib::BattleWatch *This) { + int ret = (This->*ogBattleWatchOnProcess)(); + if (ret == SokuLib::SCENE_TITLE) { + std::cout << "Disconnect when spectating. Save replay if allowed." << std::endl; + spectatingSaveReplayIfAllow(&SokuLib::getNetObject()); + } + return ret; +} + +static void battleSaveReplayIfAllow() { + std::cout << "Save replay if allowed." << std::endl; + switch (get00899840()[0x73]) { + case 0: // always save replay + case 1: // save replay when as player + battleSaveReplay(); + case 2: // save replay when as spectator + case 3: // never save replay + case 4: // always ask + break; + } +} + +template static int /*SokuLib::Scene*/ __fastcall CBattlePlay_OnProcess(T *This) { + int ret = (This->**ogBattlePlayOnProcess)(); + if (ret == SokuLib::SCENE_TITLE) { + std::cout << "Disconnect. "; + battleSaveReplayIfAllow(); + } + return ret; +} + +template static void __declspec(naked) gameEndTooEarly() { + static const SokuLib::Scene retcode_ = retcode; // workaround for the template parameter is unusable in inline asm (why?) + std::cout << "Esc or desync causes the game ends too early. "; + battleSaveReplayIfAllow(); + __asm { + pop edi; + mov eax, retcode_; + pop esi; + ret; + } +} + +static void __declspec(naked) gameEndTooEarly2() { + static auto gameEndTooEarlyAddr = gameEndTooEarly; + static const void *fun004282d0 = (void *)0x004282d0; + __asm { + call fun004282d0; + jmp gameEndTooEarlyAddr; + } +} +static void __declspec(naked) gameEndTooEarly3() { + static const void *addr004283a2 = (void *)0x004283a2; + __asm { + push esi; + } + std::cout << "Esc or desync causes the game ends too early. "; + battleSaveReplayIfAllow(); + __asm { + pop esi; + cmp [esi+0x6c8], 0; + jmp addr004283a2; + } +} + +// We check if the game version is what we target (in our case, Soku 1.10a). +extern "C" __declspec(dllexport) bool CheckVersion(const BYTE hash[16]) { + return memcmp(hash, SokuLib::targetHash, sizeof(SokuLib::targetHash)) == 0; +} + +// Called when the mod loader is ready to initialize this module. +// All hooks should be placed here. It's also a good moment to load settings +// from the ini. +extern "C" __declspec(dllexport) bool Initialize(HMODULE hMyModule, HMODULE hParentModule) { + DWORD old; + +#ifdef _DEBUG + FILE *_; + + AllocConsole(); + freopen_s(&_, "CONOUT$", "w", stdout); + freopen_s(&_, "CONOUT$", "w", stderr); +#endif + VirtualProtect((PVOID)RDATA_SECTION_OFFSET, RDATA_SECTION_SIZE, PAGE_EXECUTE_WRITECOPY, &old); + ogBattleWatchOnProcess = SokuLib::TamperDword(&SokuLib::VTable_BattleWatch.onProcess, CBattleWatch_OnProcess); + ogBattleServerOnProcess + = SokuLib::TamperDword(&SokuLib::VTable_BattleServer.onProcess, CBattlePlay_OnProcess); + ogBattleClientOnProcess + = SokuLib::TamperDword(&SokuLib::VTable_BattleClient.onProcess, CBattlePlay_OnProcess); + VirtualProtect((PVOID)RDATA_SECTION_OFFSET, RDATA_SECTION_SIZE, old, &old); + VirtualProtect((PVOID)TEXT_SECTION_OFFSET, TEXT_SECTION_SIZE, PAGE_EXECUTE_WRITECOPY, &old); + SokuLib::TamperNearJmp(0x428663, gameEndTooEarly); + SokuLib::TamperNearJmp(0x428680, gameEndTooEarly); + SokuLib::TamperNearJmp(0x4283b0, gameEndTooEarly); + SokuLib::TamperNearJmp(0x42838e, gameEndTooEarly2); + VirtualProtect((PVOID)TEXT_SECTION_OFFSET, TEXT_SECTION_SIZE, old, &old); + + FlushInstructionCache(GetCurrentProcess(), nullptr, 0); + return true; +} + +extern "C" int APIENTRY DllMain(HMODULE hModule, DWORD fdwReason, LPVOID lpReserved) { + return TRUE; +} + +// New mod loader functions +// Loading priority. Mods are loaded in order by ascending level of priority +// (the highest first). When 2 mods define the same loading priority the loading +// order is undefined. +extern "C" __declspec(dllexport) int getPriority() { + return 0; +} + +// Not yet implemented in the mod loader, subject to change +// SokuModLoader::IValue **getConfig(); +// void freeConfig(SokuModLoader::IValue **v); +// bool commitConfig(SokuModLoader::IValue *); +// const char *getFailureReason(); +// bool hasChainedHooks(); +// void unHook(); \ No newline at end of file diff --git a/src/version.rc b/src/version.rc new file mode 100644 index 0000000..072b272 --- /dev/null +++ b/src/version.rc @@ -0,0 +1,62 @@ +#include +#define VER_FILEVERSION 0,1,0,0 +#define VER_FILEVERSION_STR "0.1\0" + +#define VER_PRODUCTVERSION 0,1,0,0 +#define VER_PRODUCTVERSION_STR "0.1\0" + +#define VER_COMPANYNAME_STR "SokuDev\0" +#define VER_FILEDESCRIPTION_STR "Mod for Touhou 12.3 to save replay when the battle ends too early (such as esc or desync)\0" +#define VER_INTERNALNAME_STR "SaveRep\0" +#define VER_LEGALCOPYRIGHT_STR "Hagb\0" +#define VER_LEGALTRADEMARKS1_STR "\0" +#define VER_LEGALTRADEMARKS2_STR "\0" +#define VER_ORIGINALFILENAME_STR "SaveRep.dll\0" +#define VER_PRODUCTNAME_STR "SaveRep\0" + +// Define this to 0 if not a pre release +#define VER_PRERELEASE VS_FF_PRERELEASE + +#ifndef DEBUG +#define VER_DEBUG 0 +#else +#define VER_DEBUG VS_FF_DEBUG +#endif + +VS_VERSION_INFO VERSIONINFO +FILEVERSION VER_FILEVERSION +PRODUCTVERSION VER_PRODUCTVERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS (VER_PRERELEASE|VER_DEBUG) +FILEOS VOS__WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "CompanyName", VER_COMPANYNAME_STR + VALUE "FileDescription", VER_FILEDESCRIPTION_STR + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "InternalName", VER_INTERNALNAME_STR + VALUE "LegalCopyright", VER_LEGALCOPYRIGHT_STR + VALUE "LegalTrademarks1", VER_LEGALTRADEMARKS1_STR + VALUE "LegalTrademarks2", VER_LEGALTRADEMARKS2_STR + VALUE "OriginalFilename", VER_ORIGINALFILENAME_STR + VALUE "ProductName", VER_PRODUCTNAME_STR + VALUE "ProductVersion", VER_PRODUCTVERSION_STR + END + END + + BLOCK "VarFileInfo" + BEGIN + /* The following line should only be modified for localized versions. */ + /* It consists of any number of WORD,WORD pairs, with each pair */ + /* describing a language,codepage combination supported by the file. */ + /* */ + /* For example, a file might have values "0x409,1252" indicating that it */ + /* supports English language (0x409) in the Windows ANSI codepage (1252). */ + VALUE "Translation", 0x409, 1252 + END +END \ No newline at end of file