├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.yaml └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE.md ├── OutRun2006Tweaks.ini ├── OutRun2006Tweaks.lods.ini ├── README.md ├── build_vs2022.bat ├── cmake.toml ├── cmkr.cmake ├── docs └── file_formats │ ├── README.md │ ├── pmt_c2c.bt │ ├── xmt_lindbergh.bt │ └── xst.bt ├── external ├── ini-cpp │ └── ini │ │ └── ini.h └── miniz │ ├── miniz.c │ └── miniz.h ├── generate_vs2022.bat └── src ├── Proxy.cpp ├── Proxy.def ├── Proxy.hpp ├── Resource.aps ├── Resource.rc ├── dllmain.cpp ├── exception.hpp ├── game.hpp ├── game_addrs.hpp ├── hook_mgr.cpp ├── hook_mgr.hpp ├── hooks_audio.cpp ├── hooks_bugfixes.cpp ├── hooks_drawdistance.cpp ├── hooks_exceptions.cpp ├── hooks_flac.cpp ├── hooks_forcefeedback.cpp ├── hooks_framerate.cpp ├── hooks_graphics.cpp ├── hooks_input.cpp ├── hooks_misc.cpp ├── hooks_textures.cpp ├── hooks_uiscaling.cpp ├── input_manager.cpp ├── network.cpp ├── overlay ├── chatroom.cpp ├── course_editor.cpp ├── hooks_overlay.cpp ├── notifications.hpp ├── overlay.cpp ├── overlay.hpp ├── server_notifications.cpp └── update_check.cpp ├── plugin.hpp └── resource.h /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: emoose # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Let us know about bugs found with the game/tweaks (for feature requests, please use Discussions tab) 3 | title: "[Bug]: " 4 | labels: bug 5 | body: 6 | - id: disclaimer 7 | type: markdown 8 | attributes: 9 | value: | 10 | If you've ran into an issue/bug with the game or tweaks let us know about it here. 11 | 12 | **For feature requests/suggestions**, please write on the [Suggestions discussion board](https://github.com/emoose/OutRun2006Tweaks/discussions/categories/suggestions). 13 | - id: version 14 | type: input 15 | attributes: 16 | label: Tweaks version 17 | description: Which version of OutRun2006Tweaks were you using? (you can usually find this by looking at the filename of ZIP you downloaded) 18 | placeholder: outrun2006tweaks-abcdefghijklmnopqrstuvwxyz 19 | validations: 20 | required: true 21 | - id: problem 22 | type: textarea 23 | attributes: 24 | label: Describe your issue here (please attach the OutRun2006Tweaks.log file with your report, you can drag+drop it into the text area) 25 | validations: 26 | required: true 27 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | 5 | jobs: 6 | build: 7 | runs-on: windows-2022 8 | strategy: 9 | matrix: 10 | arch: [ Win32 ] 11 | build_type: [ Release ] 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4.1.7 16 | with: 17 | submodules: recursive 18 | 19 | - name: Configure 20 | run: | 21 | mkdir build 22 | cd build 23 | cmake -G "Visual Studio 17 2022" -A ${{matrix.arch}} .. 24 | 25 | - name: Add older VC2022 redist workaround to safetyhook 26 | shell: pwsh 27 | run: | 28 | (Get-Content build/_deps/safetyhook-build/safetyhook.vcxproj) -replace 'ZYCORE_STATIC_BUILD', '_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR;ZYCORE_STATIC_BUILD' | Set-Content build/_deps/safetyhook-build/safetyhook.vcxproj 29 | (Get-Content build/_deps/zydis-build/Zydis.vcxproj) -replace 'ZYCORE_STATIC_BUILD', '_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR;ZYCORE_STATIC_BUILD' | Set-Content build/_deps/zydis-build/Zydis.vcxproj 30 | (Get-Content build/_deps/zydis-build/zycore/Zycore.vcxproj) -replace 'ZYCORE_STATIC_BUILD', '_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR;ZYCORE_STATIC_BUILD' | Set-Content build/_deps/zydis-build/zycore/Zycore.vcxproj 31 | 32 | - name: Zero out SDL_gameinputjoystick.c (https://github.com/libsdl-org/SDL/issues/11487 / https://github.com/actions/runner-images/issues/10980) 33 | shell: pwsh 34 | run: | 35 | Set-Content -Path build/_deps/sdl-src/src/joystick/gdk/SDL_gameinputjoystick.c -Value "" 36 | (Get-Content build/_deps/sdl-build/include-config-release/build_config/SDL_build_config.h) -replace '#define SDL_JOYSTICK_GAMEINPUT 1', '/* #undef SDL_JOYSTICK_GAMEINPUT */' | Set-Content build/_deps/sdl-build/include-config-release/build_config/SDL_build_config.h 37 | 38 | - name: Build 39 | run: | 40 | cmake --build build --config ${{matrix.build_type}} 41 | 42 | - name: Copy settings 43 | shell: pwsh 44 | run: | 45 | cp OutRun2006Tweaks.ini build/bin/OutRun2006Tweaks.ini 46 | cp OutRun2006Tweaks.lods.ini build/bin/OutRun2006Tweaks.lods.ini 47 | 48 | - name: Download OR2006C2C 49 | shell: pwsh 50 | run: | 51 | Invoke-WebRequest -Uri https://github.com/emoose/OutRun2006Tweaks/releases/download/v0.1/OR2006C2C.EXE -OutFile build/bin/OR2006C2C.exe 52 | 53 | - name: Upload 54 | uses: actions/upload-artifact@v4.3.3 55 | with: 56 | name: outrun2006tweaks-${{ github.sha }} 57 | path: build/bin/ 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Compiled Static libraries 20 | *.lai 21 | *.la 22 | *.a 23 | *.lib 24 | 25 | # Executables 26 | *.exe 27 | *.out 28 | *.app 29 | 30 | build/* 31 | build_x86/* 32 | .vscode/* 33 | out/* 34 | .vs/* 35 | rel/* -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/spdlog"] 2 | path = external/spdlog 3 | url = https://github.com/gabime/spdlog 4 | [submodule "external/ModUtils"] 5 | path = external/ModUtils 6 | url = https://github.com/CookiePLMonster/ModUtils 7 | [submodule "external/xxHash"] 8 | path = external/xxHash 9 | url = https://github.com/Cyan4973/xxHash.git 10 | [submodule "external/imgui"] 11 | path = external/imgui 12 | url = https://github.com/ocornut/imgui.git 13 | [submodule "external/IXWebSocket"] 14 | path = external/IXWebSocket 15 | url = https://github.com/machinezone/IXWebSocket.git 16 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file is automatically generated from cmake.toml - DO NOT EDIT 2 | # See https://github.com/build-cpp/cmkr for more information 3 | 4 | cmake_minimum_required(VERSION 3.15) 5 | 6 | if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) 7 | message(FATAL_ERROR "In-tree builds are not supported. Run CMake from a separate directory: cmake -B build") 8 | endif() 9 | 10 | set(CMKR_ROOT_PROJECT OFF) 11 | if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) 12 | set(CMKR_ROOT_PROJECT ON) 13 | 14 | # Bootstrap cmkr and automatically regenerate CMakeLists.txt 15 | include(cmkr.cmake OPTIONAL RESULT_VARIABLE CMKR_INCLUDE_RESULT) 16 | if(CMKR_INCLUDE_RESULT) 17 | cmkr() 18 | endif() 19 | 20 | # Enable folder support 21 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 22 | 23 | # Create a configure-time dependency on cmake.toml to improve IDE support 24 | configure_file(cmake.toml cmake.toml COPYONLY) 25 | endif() 26 | 27 | project(outrun2006tweaks-proj) 28 | 29 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") 30 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") 31 | 32 | set(ASMJIT_STATIC ON CACHE BOOL "" FORCE) 33 | 34 | # disable unneeded FLAC stuff 35 | set(INSTALL_MANPAGES OFF CACHE BOOL "" FORCE) 36 | set(BUILD_CXXLIBS OFF CACHE BOOL "" FORCE) 37 | set(BUILD_DOCS OFF CACHE BOOL "" FORCE) 38 | set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) 39 | set(BUILD_PROGRAMS OFF CACHE BOOL "" FORCE) 40 | set(BUILD_TESTING OFF CACHE BOOL "" FORCE) 41 | set(UPNPC_BUILD_TESTS OFF CACHE BOOL "" FORCE) 42 | set(UPNPC_BUILD_SAMPLE OFF CACHE BOOL "" FORCE) 43 | set(UPNPC_BUILD_SHARED OFF CACHE BOOL "" FORCE) 44 | set(JSONCPP_STATIC_WINDOWS_RUNTIME OFF CACHE BOOL "" FORCE) 45 | set(JSONCPP_WITH_TESTS OFF CACHE BOOL "" FORCE) 46 | set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) 47 | set(BUILD_STATIC_LIBS ON CACHE BOOL "" FORCE) 48 | set(BUILD_OBJECT_LIBS ON CACHE BOOL "" FORCE) 49 | set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE) 50 | 51 | option(ZYDIS_BUILD_TOOLS "" OFF) 52 | option(ZYDIS_BUILD_EXAMPLES "" OFF) 53 | option(JSONCPP_WITH_TESTS "" OFF) 54 | option(JSONCPP_STATIC_WINDOWS_RUNTIME "" OFF) 55 | 56 | if ("${CMAKE_BUILD_TYPE}" MATCHES "Release") 57 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MT") 58 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MT") 59 | 60 | # Statically compile runtime 61 | string(REGEX REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") 62 | string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") 63 | string(REGEX REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}") 64 | string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}") 65 | 66 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded") 67 | message(NOTICE "Building in Release mode") 68 | endif() 69 | 70 | include(FetchContent) 71 | 72 | # Fix warnings about DOWNLOAD_EXTRACT_TIMESTAMP 73 | if(POLICY CMP0135) 74 | cmake_policy(SET CMP0135 NEW) 75 | endif() 76 | message(STATUS "Fetching zydis (v4.0.0)...") 77 | FetchContent_Declare(zydis 78 | GIT_REPOSITORY 79 | "https://github.com/zyantific/zydis" 80 | GIT_TAG 81 | v4.0.0 82 | ) 83 | FetchContent_MakeAvailable(zydis) 84 | 85 | message(STATUS "Fetching safetyhook (629558c64009a7291ba6ed5cfb49187086a27a47)...") 86 | FetchContent_Declare(safetyhook 87 | GIT_REPOSITORY 88 | "https://github.com/cursey/safetyhook" 89 | GIT_TAG 90 | 629558c64009a7291ba6ed5cfb49187086a27a47 91 | ) 92 | FetchContent_MakeAvailable(safetyhook) 93 | 94 | message(STATUS "Fetching ogg (v1.3.5)...") 95 | FetchContent_Declare(ogg 96 | GIT_REPOSITORY 97 | "https://github.com/xiph/ogg" 98 | GIT_TAG 99 | v1.3.5 100 | ) 101 | FetchContent_MakeAvailable(ogg) 102 | 103 | message(STATUS "Fetching flac (1.4.3)...") 104 | FetchContent_Declare(flac 105 | GIT_REPOSITORY 106 | "https://github.com/xiph/flac" 107 | GIT_TAG 108 | 1.4.3 109 | ) 110 | FetchContent_MakeAvailable(flac) 111 | 112 | message(STATUS "Fetching miniupnpc (miniupnpd_2_3_7)...") 113 | FetchContent_Declare(miniupnpc 114 | GIT_REPOSITORY 115 | "https://github.com/miniupnp/miniupnp" 116 | GIT_TAG 117 | miniupnpd_2_3_7 118 | SOURCE_SUBDIR 119 | miniupnpc 120 | ) 121 | FetchContent_MakeAvailable(miniupnpc) 122 | 123 | message(STATUS "Fetching jsoncpp (1.9.6)...") 124 | FetchContent_Declare(jsoncpp 125 | GIT_REPOSITORY 126 | "https://github.com/open-source-parsers/jsoncpp.git" 127 | GIT_TAG 128 | 1.9.6 129 | ) 130 | FetchContent_MakeAvailable(jsoncpp) 131 | 132 | message(STATUS "Fetching zlib (v1.3.1)...") 133 | FetchContent_Declare(zlib 134 | GIT_REPOSITORY 135 | "https://github.com/madler/zlib" 136 | GIT_TAG 137 | v1.3.1 138 | ) 139 | FetchContent_MakeAvailable(zlib) 140 | 141 | message(STATUS "Fetching sdl (preview-3.1.8)...") 142 | FetchContent_Declare(sdl 143 | GIT_REPOSITORY 144 | "https://github.com/libsdl-org/SDL" 145 | GIT_TAG 146 | preview-3.1.8 147 | ) 148 | FetchContent_MakeAvailable(sdl) 149 | 150 | # Target: spdlog 151 | set(spdlog_SOURCES 152 | cmake.toml 153 | "external/spdlog/src/async.cpp" 154 | "external/spdlog/src/bundled_fmtlib_format.cpp" 155 | "external/spdlog/src/cfg.cpp" 156 | "external/spdlog/src/color_sinks.cpp" 157 | "external/spdlog/src/file_sinks.cpp" 158 | "external/spdlog/src/spdlog.cpp" 159 | "external/spdlog/src/stdout_sinks.cpp" 160 | ) 161 | 162 | add_library(spdlog STATIC) 163 | 164 | target_sources(spdlog PRIVATE ${spdlog_SOURCES}) 165 | source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${spdlog_SOURCES}) 166 | 167 | target_compile_definitions(spdlog PUBLIC 168 | SPDLOG_COMPILED_LIB 169 | _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR 170 | ) 171 | 172 | target_include_directories(spdlog PUBLIC 173 | "external/spdlog/include" 174 | ) 175 | 176 | # Target: outrun2006tweaks 177 | set(outrun2006tweaks_SOURCES 178 | OutRun2006Tweaks.ini 179 | OutRun2006Tweaks.lods.ini 180 | cmake.toml 181 | "external/IXWebSocket/ixwebsocket/IXBase64.h" 182 | "external/IXWebSocket/ixwebsocket/IXBench.cpp" 183 | "external/IXWebSocket/ixwebsocket/IXBench.h" 184 | "external/IXWebSocket/ixwebsocket/IXCancellationRequest.cpp" 185 | "external/IXWebSocket/ixwebsocket/IXCancellationRequest.h" 186 | "external/IXWebSocket/ixwebsocket/IXConnectionState.cpp" 187 | "external/IXWebSocket/ixwebsocket/IXConnectionState.h" 188 | "external/IXWebSocket/ixwebsocket/IXDNSLookup.cpp" 189 | "external/IXWebSocket/ixwebsocket/IXDNSLookup.h" 190 | "external/IXWebSocket/ixwebsocket/IXExponentialBackoff.cpp" 191 | "external/IXWebSocket/ixwebsocket/IXExponentialBackoff.h" 192 | "external/IXWebSocket/ixwebsocket/IXGetFreePort.cpp" 193 | "external/IXWebSocket/ixwebsocket/IXGetFreePort.h" 194 | "external/IXWebSocket/ixwebsocket/IXGzipCodec.cpp" 195 | "external/IXWebSocket/ixwebsocket/IXGzipCodec.h" 196 | "external/IXWebSocket/ixwebsocket/IXHttp.cpp" 197 | "external/IXWebSocket/ixwebsocket/IXHttp.h" 198 | "external/IXWebSocket/ixwebsocket/IXHttpClient.cpp" 199 | "external/IXWebSocket/ixwebsocket/IXHttpClient.h" 200 | "external/IXWebSocket/ixwebsocket/IXHttpServer.cpp" 201 | "external/IXWebSocket/ixwebsocket/IXHttpServer.h" 202 | "external/IXWebSocket/ixwebsocket/IXNetSystem.cpp" 203 | "external/IXWebSocket/ixwebsocket/IXNetSystem.h" 204 | "external/IXWebSocket/ixwebsocket/IXProgressCallback.h" 205 | "external/IXWebSocket/ixwebsocket/IXSelectInterrupt.cpp" 206 | "external/IXWebSocket/ixwebsocket/IXSelectInterrupt.h" 207 | "external/IXWebSocket/ixwebsocket/IXSelectInterruptEvent.cpp" 208 | "external/IXWebSocket/ixwebsocket/IXSelectInterruptEvent.h" 209 | "external/IXWebSocket/ixwebsocket/IXSelectInterruptFactory.cpp" 210 | "external/IXWebSocket/ixwebsocket/IXSelectInterruptFactory.h" 211 | "external/IXWebSocket/ixwebsocket/IXSelectInterruptPipe.cpp" 212 | "external/IXWebSocket/ixwebsocket/IXSelectInterruptPipe.h" 213 | "external/IXWebSocket/ixwebsocket/IXSetThreadName.cpp" 214 | "external/IXWebSocket/ixwebsocket/IXSetThreadName.h" 215 | "external/IXWebSocket/ixwebsocket/IXSocket.cpp" 216 | "external/IXWebSocket/ixwebsocket/IXSocket.h" 217 | "external/IXWebSocket/ixwebsocket/IXSocketAppleSSL.cpp" 218 | "external/IXWebSocket/ixwebsocket/IXSocketAppleSSL.h" 219 | "external/IXWebSocket/ixwebsocket/IXSocketConnect.cpp" 220 | "external/IXWebSocket/ixwebsocket/IXSocketConnect.h" 221 | "external/IXWebSocket/ixwebsocket/IXSocketFactory.cpp" 222 | "external/IXWebSocket/ixwebsocket/IXSocketFactory.h" 223 | "external/IXWebSocket/ixwebsocket/IXSocketMbedTLS.cpp" 224 | "external/IXWebSocket/ixwebsocket/IXSocketMbedTLS.h" 225 | "external/IXWebSocket/ixwebsocket/IXSocketOpenSSL.cpp" 226 | "external/IXWebSocket/ixwebsocket/IXSocketOpenSSL.h" 227 | "external/IXWebSocket/ixwebsocket/IXSocketServer.cpp" 228 | "external/IXWebSocket/ixwebsocket/IXSocketServer.h" 229 | "external/IXWebSocket/ixwebsocket/IXSocketTLSOptions.cpp" 230 | "external/IXWebSocket/ixwebsocket/IXSocketTLSOptions.h" 231 | "external/IXWebSocket/ixwebsocket/IXStrCaseCompare.cpp" 232 | "external/IXWebSocket/ixwebsocket/IXStrCaseCompare.h" 233 | "external/IXWebSocket/ixwebsocket/IXUdpSocket.cpp" 234 | "external/IXWebSocket/ixwebsocket/IXUdpSocket.h" 235 | "external/IXWebSocket/ixwebsocket/IXUniquePtr.h" 236 | "external/IXWebSocket/ixwebsocket/IXUrlParser.cpp" 237 | "external/IXWebSocket/ixwebsocket/IXUrlParser.h" 238 | "external/IXWebSocket/ixwebsocket/IXUserAgent.cpp" 239 | "external/IXWebSocket/ixwebsocket/IXUserAgent.h" 240 | "external/IXWebSocket/ixwebsocket/IXUtf8Validator.h" 241 | "external/IXWebSocket/ixwebsocket/IXUuid.cpp" 242 | "external/IXWebSocket/ixwebsocket/IXUuid.h" 243 | "external/IXWebSocket/ixwebsocket/IXWebSocket.cpp" 244 | "external/IXWebSocket/ixwebsocket/IXWebSocket.h" 245 | "external/IXWebSocket/ixwebsocket/IXWebSocketCloseConstants.cpp" 246 | "external/IXWebSocket/ixwebsocket/IXWebSocketCloseConstants.h" 247 | "external/IXWebSocket/ixwebsocket/IXWebSocketCloseInfo.h" 248 | "external/IXWebSocket/ixwebsocket/IXWebSocketErrorInfo.h" 249 | "external/IXWebSocket/ixwebsocket/IXWebSocketHandshake.cpp" 250 | "external/IXWebSocket/ixwebsocket/IXWebSocketHandshake.h" 251 | "external/IXWebSocket/ixwebsocket/IXWebSocketHandshakeKeyGen.h" 252 | "external/IXWebSocket/ixwebsocket/IXWebSocketHttpHeaders.cpp" 253 | "external/IXWebSocket/ixwebsocket/IXWebSocketHttpHeaders.h" 254 | "external/IXWebSocket/ixwebsocket/IXWebSocketInitResult.h" 255 | "external/IXWebSocket/ixwebsocket/IXWebSocketMessage.h" 256 | "external/IXWebSocket/ixwebsocket/IXWebSocketMessageType.h" 257 | "external/IXWebSocket/ixwebsocket/IXWebSocketOpenInfo.h" 258 | "external/IXWebSocket/ixwebsocket/IXWebSocketPerMessageDeflate.cpp" 259 | "external/IXWebSocket/ixwebsocket/IXWebSocketPerMessageDeflate.h" 260 | "external/IXWebSocket/ixwebsocket/IXWebSocketPerMessageDeflateCodec.cpp" 261 | "external/IXWebSocket/ixwebsocket/IXWebSocketPerMessageDeflateCodec.h" 262 | "external/IXWebSocket/ixwebsocket/IXWebSocketPerMessageDeflateOptions.cpp" 263 | "external/IXWebSocket/ixwebsocket/IXWebSocketPerMessageDeflateOptions.h" 264 | "external/IXWebSocket/ixwebsocket/IXWebSocketProxyServer.cpp" 265 | "external/IXWebSocket/ixwebsocket/IXWebSocketProxyServer.h" 266 | "external/IXWebSocket/ixwebsocket/IXWebSocketSendData.h" 267 | "external/IXWebSocket/ixwebsocket/IXWebSocketSendInfo.h" 268 | "external/IXWebSocket/ixwebsocket/IXWebSocketServer.cpp" 269 | "external/IXWebSocket/ixwebsocket/IXWebSocketServer.h" 270 | "external/IXWebSocket/ixwebsocket/IXWebSocketTransport.cpp" 271 | "external/IXWebSocket/ixwebsocket/IXWebSocketTransport.h" 272 | "external/IXWebSocket/ixwebsocket/IXWebSocketVersion.h" 273 | "external/ModUtils/MemoryMgr.h" 274 | "external/ModUtils/Patterns.cpp" 275 | "external/ModUtils/Patterns.h" 276 | "external/imgui/backends/imgui_impl_dx9.cpp" 277 | "external/imgui/backends/imgui_impl_win32.cpp" 278 | "external/imgui/imgui.cpp" 279 | "external/imgui/imgui_demo.cpp" 280 | "external/imgui/imgui_draw.cpp" 281 | "external/imgui/imgui_tables.cpp" 282 | "external/imgui/imgui_widgets.cpp" 283 | "external/ini-cpp/ini/ini.h" 284 | "external/miniz/miniz.c" 285 | "external/miniz/miniz.h" 286 | "external/xxHash/xxhash.c" 287 | "external/xxHash/xxhash.h" 288 | "src/Proxy.cpp" 289 | "src/Proxy.def" 290 | "src/Proxy.hpp" 291 | "src/Resource.rc" 292 | "src/dllmain.cpp" 293 | "src/exception.hpp" 294 | "src/game.hpp" 295 | "src/game_addrs.hpp" 296 | "src/hook_mgr.cpp" 297 | "src/hook_mgr.hpp" 298 | "src/hooks_audio.cpp" 299 | "src/hooks_bugfixes.cpp" 300 | "src/hooks_drawdistance.cpp" 301 | "src/hooks_exceptions.cpp" 302 | "src/hooks_flac.cpp" 303 | "src/hooks_forcefeedback.cpp" 304 | "src/hooks_framerate.cpp" 305 | "src/hooks_graphics.cpp" 306 | "src/hooks_input.cpp" 307 | "src/hooks_misc.cpp" 308 | "src/hooks_textures.cpp" 309 | "src/hooks_uiscaling.cpp" 310 | "src/input_manager.cpp" 311 | "src/network.cpp" 312 | "src/overlay/chatroom.cpp" 313 | "src/overlay/course_editor.cpp" 314 | "src/overlay/hooks_overlay.cpp" 315 | "src/overlay/notifications.hpp" 316 | "src/overlay/overlay.cpp" 317 | "src/overlay/overlay.hpp" 318 | "src/overlay/server_notifications.cpp" 319 | "src/overlay/update_check.cpp" 320 | "src/plugin.hpp" 321 | "src/resource.h" 322 | ) 323 | 324 | add_library(outrun2006tweaks SHARED) 325 | 326 | target_sources(outrun2006tweaks PRIVATE ${outrun2006tweaks_SOURCES}) 327 | source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${outrun2006tweaks_SOURCES}) 328 | 329 | target_compile_definitions(outrun2006tweaks PUBLIC 330 | _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING 331 | _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR 332 | DIRECTINPUT_VERSION=0x0800 333 | ) 334 | 335 | target_compile_features(outrun2006tweaks PUBLIC 336 | cxx_std_20 337 | ) 338 | 339 | target_compile_options(outrun2006tweaks PUBLIC 340 | "/GS-" 341 | "/bigobj" 342 | "/EHa" 343 | "/MP" 344 | ) 345 | 346 | target_include_directories(outrun2006tweaks PUBLIC 347 | "shared/" 348 | "src/" 349 | "include/" 350 | "external/ModUtils/" 351 | "external/ini-cpp/ini/" 352 | "external/xxHash/" 353 | "external/miniz/" 354 | "external/imgui/" 355 | "external/IXWebSocket/" 356 | ) 357 | 358 | target_link_libraries(outrun2006tweaks PUBLIC 359 | spdlog 360 | safetyhook 361 | ogg 362 | FLAC 363 | jsoncpp_static 364 | version.lib 365 | xinput9_1_0.lib 366 | Hid.lib 367 | libminiupnpc-static 368 | SDL3-static 369 | Winmm.lib 370 | Setupapi.lib 371 | Crypt32.lib 372 | ) 373 | 374 | target_link_options(outrun2006tweaks PUBLIC 375 | "/DEBUG" 376 | "/OPT:REF" 377 | "/OPT:ICF" 378 | ) 379 | 380 | set_target_properties(outrun2006tweaks PROPERTIES 381 | OUTPUT_NAME 382 | dinput8 383 | SUFFIX 384 | .dll 385 | RUNTIME_OUTPUT_DIRECTORY_RELEASE 386 | "${CMAKE_BINARY_DIR}/bin/${CMKR_TARGET}" 387 | RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO 388 | "${CMAKE_BINARY_DIR}/bin/${CMKR_TARGET}" 389 | LIBRARY_OUTPUT_DIRECTORY_RELEASE 390 | "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 391 | LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO 392 | "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 393 | ARCHIVE_OUTPUT_DIRECTORY_RELEASE 394 | "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 395 | ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO 396 | "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 397 | ) 398 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 emoose 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OutRun2006Tweaks.lods.ini: -------------------------------------------------------------------------------- 1 | # This file contains known-bad object IDs that will be excluded from being drawn by the DrawDistanceIncrease tweak 2 | # Such as LOD models or other bad stage models 3 | # 4 | # OutRun2006Tweaks includes an overlay that can help to find the IDs for bad objects if they appear 5 | # Just make sure [Overlay] Enabled = true is set, then press F11 when you see a bad model while playing a stage 6 | # 7 | # Please post any bad objects you find to https://github.com/emoose/OutRun2006Tweaks/issues/97! 8 | 9 | # PALM BEACH 10 | [Stage 0] 11 | # Bad geo when entering from left-side bunki 12 | 0x1 = 0xD 13 | 14 | # ALPINE 15 | [Stage 3] 16 | # See-through mountains in distance from bunki 17 | 0x1 = 0x1, 0xD, 0x22 18 | 19 | # SNOW MOUNTAIN 20 | [Stage 4] 21 | # Piece of floating mountain geometry when entering the stage 22 | 0x1 = 0x8 23 | # Random piece of floating geometry above the lift cables, didn't get a close look 24 | 0x6 = 0x1E 25 | 26 | # TULIP GARDEN 27 | [Stage 10] 28 | # Hill on the road, DD222 29 | 0x1 = 0x1F 30 | 31 | # BEACH 32 | [Stage 15] 33 | # Black patch above water 34 | SkipQuickSort = True 35 | 36 | # LAS VEGAS 37 | [Stage 18] 38 | # Building geo above winding section 39 | 0x1 = 0x1F 40 | 0x3 = 0x1, 0x3, 0x10, 0x11, 0x1F, 0x21, 0x4E, 0x50 41 | 42 | # ALASKA 43 | [Stage 19] 44 | # Odd white backdrop visible in bunki 45 | 0x1 = 0x15 46 | 47 | # MAYA 48 | [Stage 25] 49 | # Tree sprite on bunki transition 50 | 0x1 = 0x34 51 | # Floating temple over entrance 52 | 0x2 = 0x2B8, 0x2B9, 0x2BB, 0x2BC, 0x2BE, 0x2D9, 0x2DB 53 | 0x3 = 0x2 54 | 0x4 = 0x24 55 | 56 | # NEW YORK 57 | [Stage 26] 58 | # Odd flat plane over track when entering from right-side bunki 59 | 0x1 = 0x25 60 | # Bushes when entering from bunki 61 | 0x5 = 0xBB 62 | 63 | # PALM BEACH (T) 64 | [Stage 60] 65 | # Bad polygon when entering from bunki 66 | 0x1 = 0x5, 0x6 67 | 68 | # BEACH (T) 69 | [Stage 61] 70 | # Black patch above water 71 | SkipQuickSort = True 72 | 73 | # BEACH (BR) 74 | [Stage 65] 75 | # Bad polygon when entering from bunki 76 | 0x1 = 0x27 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OutRun2006Tweaks 2 | [![GitHub Downloads](https://img.shields.io/github/downloads/emoose/OutRun2006Tweaks/total)](https://github.com/emoose/OutRun2006Tweaks/releases) 3 | 4 | A wrapper DLL that can patch in some minor fixes & tweaks into OutRun 2006: Coast 2 Coast. 5 | 6 | Latest builds can be found under the releases section: https://github.com/emoose/OutRun2006Tweaks/releases 7 | 8 | **Tweaks will also point the game to new multiplayer servers**, just head to the multiplayer section in-game and pick a username & password there! 9 | 10 | Online games are regularly setup on the OutRun2006Tweaks discord: https://discord.gg/GFjKAMg83t 11 | 12 | ### Features 13 | **Graphics:** 14 | - UI can now scale to different aspect ratios without stretching 15 | - Game scene & UI textures can be extracted from game, and replaced with higher-resolution versions 16 | - Allows disabling vehicle LODs, reducing the ugly pop-in as they get closer 17 | - Fixed Z-buffer precision issues that caused heavy Z-fighting and distant object pop-in 18 | - Lens flare effect now loads from correct path without needing to change game files 19 | - Stage objects such as traffic cones now only disappear once they're actually off-screen 20 | - Fixes certain effects like engine backfiring which failed to appear when using controllers 21 | - Anisotropic filtering & transparency supersampling can be forced, greatly reducing aliasing around the edges of the track 22 | - Reflection rendering resolution can be increased from the default 128x128 23 | - Restores the car base shadow from the C2C console ports, which was missing on PC for some reason 24 | - Allows using higher-quality models for Alberto/Clarissa/Jennifer, which were otherwise left unused 25 | 26 | **Gameplay:** 27 | - Points game toward new online servers, restoring the online multiplayer modes 28 | - Restored XInput rumble code from the Xbox release can be enabled inside INI, allowing gear shifts/drifts/crashes/etc to give feedback 29 | - Xbox Series impulse triggers are supported and can be tweaked inside INI 30 | - Steering deadzone can be customized from the default 20% 31 | - Horn button can be made functional during normal gameplay, outside of the "honk your horn!" girl requests 32 | - Allows randomizing the set of highway animations to use, instead of only using the set for the game mode being played 33 | - In-game HUD can be optionally toggled via bindable keypress 34 | - Manual Transmission (MT) can be set as the default for C2C menus 35 | - Passing all the C2C missions might unlock something new 🐱 36 | 37 | **Bugfixes:** 38 | - Built-in framelimiter to prevent speedups, framerate can be partially unlocked with game running at 60FPS internally 39 | - Prevents save corruption bug when remapping controls with many input devices connected 40 | - Fixed C2C ranking scoreboards not updating on Steam and other releases due to faulty anti-piracy checks 41 | - Pegasus animation's clopping sound effect will now end correctly 42 | - Text related to the online service can optionally be hidden 43 | - Automatically disables DPI scaling on the game window to fix scaling issues 44 | - Fixes issues with shading on certain character/stage models (eg. the ending cutscene models) 45 | - Allows particles like grass/gravel to be drawn correctly, like in the console versions 46 | - Game can be forced to run on a single core, to help with freezing issues on some modern systems 47 | - Bink movie files larger than 1024 pixels will now play without crashes 48 | - Game crashes will now write a crash report into CrashDumps folder (please feel free to post any crash reports to the issues page!) 49 | 50 | **Enhancements:** 51 | - Game can now run in borderless windowed mode; mouse cursor will now be hidden while game is active 52 | - Will use desktop resolution for the game if outrun2006.ini isn't present 53 | - Load times heavily reduced via improved framelimiter 54 | - Draw distance for the stage can be increased, greatly reducing pop-in/fade-ins on the level 55 | - Music can now be loaded from uncompressed WAV or lossless FLAC files, if they exist with the same filename 56 | - Allows intro splash screens to be skipped 57 | - Music track can be changed mid-race via Z and X buttons, or Back/RS+Back on controller (`CDSwitcher` must be enabled in INI first) 58 | 59 | All the above can be customized via the OutRun2006Tweaks.ini file. 60 | 61 | The partial FPS unlock allows game to render out at higher FPS, **but will still run at 60FPS internally**. 62 | This won't give as much benefit as a true framerate unlock since frames will be repeated, but it can help reduce load times & improve some effects like the reflections update rate. 63 | (high refresh rate monitors that have poor 60Hz response times may also benefit from this too) 64 | 65 | ### Setup 66 | Since Steam/DVD releases are packed with ancient DRM that doesn't play well with DLL wrappers, this pack includes a replacement game EXE to run the game with. 67 | 68 | This EXE should be compatible with both the Steam release & the original DVD version, along with most OR2006 mods. 69 | 70 | To set it up: 71 | 72 | - Extract the files from the release ZIP into your **Outrun2006 Coast 2 Coast** folder, where **OR2006C2C.EXE** is located, replacing the original EXE. 73 | - Edit **OutRun2006Tweaks.ini** to customize the tweaks to your liking (by default all tweaks are enabled, other than `CDSwitcher`) 74 | - **Important:** Install the latest x86 VC redist from (https://aka.ms/vs/17/release/vc_redist.x86.exe), a redist from 2024 is needed for Tweaks to launch correctly (**even if you already have it installed please try installing it again**) 75 | - Run the game, your desktop resolution will be used by default if `outrun2006.ini` file isn't present. 76 | - (optional) the [SoundtrackFix package](https://github.com/emoose/OutRun2006Tweaks/releases/download/v0.3.0-release/OutRun2006Tweaks-SoundtrackFix-1.0.zip) can be applied to fix the missing first 2 seconds in "Rush a Difficulty" 77 | - (optional) texture improvements can be found in the texture pack releases thread (please feel free to create your own too!): https://github.com/emoose/OutRun2006Tweaks/issues/20 78 | 79 | Steam Deck/Linux users may need to run the game with `WINEDLLOVERRIDES="dinput8=n,b" %command%` launch parameters for the mod to load in. 80 | 81 | ### Building 82 | Building requires Visual Studio 2022, CMake & git to be installed, with those setup just clone this repo and then run `generate_2022.bat`. 83 | 84 | If the batch script succeeds you should see a `build\outrun2006tweaks-proj.sln` solution file, just open that in VS and build it. 85 | 86 | (if you have issues building with this setup please let me know) 87 | 88 | ### Thanks 89 | Thanks to [debugging.games](http://debugging.games) for hosting debug symbols for OutRun 2 SP (Lindburgh), very useful for looking into Outrun2006. 90 | 91 | (**if you own any prototype of Coast 2 Coast or Online Arcade** it may also contain debug symbols inside, which would let us improve even more on the C2C side of the game - please consider getting in touch at my email: lucknut.xbl at gmail dot com) 92 | -------------------------------------------------------------------------------- /build_vs2022.bat: -------------------------------------------------------------------------------- 1 | git pull --recurse-submodules 2 | git submodule update --init --recursive 3 | mkdir build 4 | cd build 5 | cmake .. -G "Visual Studio 17 2022" -A Win32 6 | cmake --build . --config Release 7 | -------------------------------------------------------------------------------- /cmake.toml: -------------------------------------------------------------------------------- 1 | # Reference: https://build-cpp.github.io/cmkr/cmake-toml 2 | # to build: 3 | # > cmake -B build 4 | # > cmake --build build --config Release 5 | [project] 6 | name = "outrun2006tweaks-proj" 7 | cmake-after = """ 8 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") 9 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") 10 | 11 | set(ASMJIT_STATIC ON CACHE BOOL "" FORCE) 12 | 13 | # disable unneeded FLAC stuff 14 | set(INSTALL_MANPAGES OFF CACHE BOOL "" FORCE) 15 | set(BUILD_CXXLIBS OFF CACHE BOOL "" FORCE) 16 | set(BUILD_DOCS OFF CACHE BOOL "" FORCE) 17 | set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) 18 | set(BUILD_PROGRAMS OFF CACHE BOOL "" FORCE) 19 | set(BUILD_TESTING OFF CACHE BOOL "" FORCE) 20 | set(UPNPC_BUILD_TESTS OFF CACHE BOOL "" FORCE) 21 | set(UPNPC_BUILD_SAMPLE OFF CACHE BOOL "" FORCE) 22 | set(UPNPC_BUILD_SHARED OFF CACHE BOOL "" FORCE) 23 | set(JSONCPP_STATIC_WINDOWS_RUNTIME OFF CACHE BOOL "" FORCE) 24 | set(JSONCPP_WITH_TESTS OFF CACHE BOOL "" FORCE) 25 | set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) 26 | set(BUILD_STATIC_LIBS ON CACHE BOOL "" FORCE) 27 | set(BUILD_OBJECT_LIBS ON CACHE BOOL "" FORCE) 28 | set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE) 29 | 30 | option(ZYDIS_BUILD_TOOLS "" OFF) 31 | option(ZYDIS_BUILD_EXAMPLES "" OFF) 32 | option(JSONCPP_WITH_TESTS "" OFF) 33 | option(JSONCPP_STATIC_WINDOWS_RUNTIME "" OFF) 34 | 35 | if ("${CMAKE_BUILD_TYPE}" MATCHES "Release") 36 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MT") 37 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MT") 38 | 39 | # Statically compile runtime 40 | string(REGEX REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") 41 | string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") 42 | string(REGEX REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}") 43 | string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}") 44 | 45 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded") 46 | message(NOTICE "Building in Release mode") 47 | endif() 48 | """ 49 | 50 | [target.spdlog] 51 | type = "static" 52 | sources = ["external/spdlog/src/*.cpp"] 53 | include-directories = ["external/spdlog/include"] 54 | compile-definitions = ["SPDLOG_COMPILED_LIB", "_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR"] 55 | compile-options = [] 56 | 57 | [fetch-content] 58 | zydis = { git = "https://github.com/zyantific/zydis", tag = "v4.0.0" } 59 | safetyhook = { git = "https://github.com/cursey/safetyhook", tag = "629558c64009a7291ba6ed5cfb49187086a27a47" } 60 | ogg = { git = "https://github.com/xiph/ogg", tag = "v1.3.5" } 61 | flac = { git = "https://github.com/xiph/flac", tag = "1.4.3" } 62 | miniupnpc = { git = "https://github.com/miniupnp/miniupnp", tag = "miniupnpd_2_3_7", subdir = "miniupnpc" } 63 | jsoncpp = { git = "https://github.com/open-source-parsers/jsoncpp.git", tag = "1.9.6" } 64 | zlib = { git = "https://github.com/madler/zlib", tag = "v1.3.1" } 65 | sdl = { git = "https://github.com/libsdl-org/SDL", tag = "preview-3.1.8" } 66 | 67 | [target.outrun2006tweaks] 68 | type = "shared" 69 | sources = ["*.ini", "src/**.cpp", "src/**.c", "src/**.def", "src/Resource.rc", 70 | "external/ModUtils/Patterns.cpp", 71 | "external/xxHash/xxhash.c", 72 | "external/miniz/miniz.c", 73 | "external/imgui/backends/imgui_impl_win32.cpp", 74 | "external/imgui/backends/imgui_impl_dx9.cpp", 75 | "external/imgui/imgui.cpp", 76 | "external/imgui/imgui_demo.cpp", 77 | "external/imgui/imgui_draw.cpp", 78 | "external/imgui/imgui_tables.cpp", 79 | "external/imgui/imgui_widgets.cpp", 80 | "external/IXWebSocket/ixwebsocket/**" 81 | ] 82 | headers = ["src/**.hpp", "src/**.h", "external/ModUtils/Patterns.h", "external/ModUtils/MemoryMgr.h", "external/xxHash/xxhash.h", "external/miniz/miniz.h", "external/ini-cpp/ini/ini.h"] 83 | include-directories = ["shared/", "src/", "include/", 84 | "external/ModUtils/", 85 | "external/ini-cpp/ini/", 86 | "external/xxHash/", 87 | "external/miniz/", 88 | "external/imgui/", 89 | "external/IXWebSocket/" 90 | ] 91 | compile-options = ["/GS-", "/bigobj", "/EHa", "/MP"] 92 | link-options = ["/DEBUG", "/OPT:REF", "/OPT:ICF"] 93 | compile-features = ["cxx_std_20"] 94 | compile-definitions = ["_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING", "_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR", "DIRECTINPUT_VERSION=0x0800"] 95 | link-libraries = [ 96 | "spdlog", 97 | "safetyhook", 98 | "ogg", 99 | "FLAC", 100 | "jsoncpp_static", 101 | "version.lib", 102 | "xinput9_1_0.lib", 103 | "Hid.lib", 104 | "libminiupnpc-static", 105 | "SDL3-static", 106 | "Winmm.lib", 107 | "Setupapi.lib", 108 | "Crypt32.lib" 109 | ] 110 | 111 | [target.outrun2006tweaks.properties] 112 | OUTPUT_NAME = "dinput8" 113 | SUFFIX = ".dll" 114 | RUNTIME_OUTPUT_DIRECTORY_RELEASE = "${CMAKE_BINARY_DIR}/bin/${CMKR_TARGET}" 115 | RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO = "${CMAKE_BINARY_DIR}/bin/${CMKR_TARGET}" 116 | LIBRARY_OUTPUT_DIRECTORY_RELEASE = "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 117 | LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO = "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 118 | ARCHIVE_OUTPUT_DIRECTORY_RELEASE = "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 119 | ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO = "${CMAKE_BINARY_DIR}/lib/${CMKR_TARGET}" 120 | 121 | -------------------------------------------------------------------------------- /cmkr.cmake: -------------------------------------------------------------------------------- 1 | include_guard() 2 | 3 | # Change these defaults to point to your infrastructure if desired 4 | set(CMKR_REPO "https://github.com/build-cpp/cmkr" CACHE STRING "cmkr git repository" FORCE) 5 | set(CMKR_TAG "v0.2.34" CACHE STRING "cmkr git tag (this needs to be available forever)" FORCE) 6 | set(CMKR_COMMIT_HASH "" CACHE STRING "cmkr git commit hash (optional)" FORCE) 7 | 8 | # To bootstrap/generate a cmkr project: cmake -P cmkr.cmake 9 | if(CMAKE_SCRIPT_MODE_FILE) 10 | set(CMAKE_BINARY_DIR "${CMAKE_BINARY_DIR}/build") 11 | set(CMAKE_CURRENT_BINARY_DIR "${CMAKE_BINARY_DIR}") 12 | file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}") 13 | endif() 14 | 15 | # Set these from the command line to customize for development/debugging purposes 16 | set(CMKR_EXECUTABLE "" CACHE FILEPATH "cmkr executable") 17 | set(CMKR_SKIP_GENERATION OFF CACHE BOOL "skip automatic cmkr generation") 18 | set(CMKR_BUILD_TYPE "Debug" CACHE STRING "cmkr build configuration") 19 | mark_as_advanced(CMKR_REPO CMKR_TAG CMKR_COMMIT_HASH CMKR_EXECUTABLE CMKR_SKIP_GENERATION CMKR_BUILD_TYPE) 20 | 21 | # Disable cmkr if generation is disabled 22 | if(DEFINED ENV{CI} OR CMKR_SKIP_GENERATION OR CMKR_BUILD_SKIP_GENERATION) 23 | message(STATUS "[cmkr] Skipping automatic cmkr generation") 24 | unset(CMKR_BUILD_SKIP_GENERATION CACHE) 25 | macro(cmkr) 26 | endmacro() 27 | return() 28 | endif() 29 | 30 | # Disable cmkr if no cmake.toml file is found 31 | if(NOT CMAKE_SCRIPT_MODE_FILE AND NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake.toml") 32 | message(AUTHOR_WARNING "[cmkr] Not found: ${CMAKE_CURRENT_SOURCE_DIR}/cmake.toml") 33 | macro(cmkr) 34 | endmacro() 35 | return() 36 | endif() 37 | 38 | # Convert a Windows native path to CMake path 39 | if(CMKR_EXECUTABLE MATCHES "\\\\") 40 | string(REPLACE "\\" "/" CMKR_EXECUTABLE_CMAKE "${CMKR_EXECUTABLE}") 41 | set(CMKR_EXECUTABLE "${CMKR_EXECUTABLE_CMAKE}" CACHE FILEPATH "" FORCE) 42 | unset(CMKR_EXECUTABLE_CMAKE) 43 | endif() 44 | 45 | # Helper macro to execute a process (COMMAND_ERROR_IS_FATAL ANY is 3.19 and higher) 46 | function(cmkr_exec) 47 | execute_process(COMMAND ${ARGV} RESULT_VARIABLE CMKR_EXEC_RESULT) 48 | if(NOT CMKR_EXEC_RESULT EQUAL 0) 49 | message(FATAL_ERROR "cmkr_exec(${ARGV}) failed (exit code ${CMKR_EXEC_RESULT})") 50 | endif() 51 | endfunction() 52 | 53 | # Windows-specific hack (CMAKE_EXECUTABLE_PREFIX is not set at the moment) 54 | if(WIN32) 55 | set(CMKR_EXECUTABLE_NAME "cmkr.exe") 56 | else() 57 | set(CMKR_EXECUTABLE_NAME "cmkr") 58 | endif() 59 | 60 | # Use cached cmkr if found 61 | if(DEFINED ENV{CMKR_CACHE}) 62 | set(CMKR_DIRECTORY_PREFIX "$ENV{CMKR_CACHE}") 63 | string(REPLACE "\\" "/" CMKR_DIRECTORY_PREFIX "${CMKR_DIRECTORY_PREFIX}") 64 | if(NOT CMKR_DIRECTORY_PREFIX MATCHES "\\/$") 65 | set(CMKR_DIRECTORY_PREFIX "${CMKR_DIRECTORY_PREFIX}/") 66 | endif() 67 | # Build in release mode for the cache 68 | set(CMKR_BUILD_TYPE "Release") 69 | else() 70 | set(CMKR_DIRECTORY_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/_cmkr_") 71 | endif() 72 | set(CMKR_DIRECTORY "${CMKR_DIRECTORY_PREFIX}${CMKR_TAG}") 73 | set(CMKR_CACHED_EXECUTABLE "${CMKR_DIRECTORY}/bin/${CMKR_EXECUTABLE_NAME}") 74 | 75 | # Helper function to check if a string starts with a prefix 76 | # Cannot use MATCHES, see: https://github.com/build-cpp/cmkr/issues/61 77 | function(cmkr_startswith str prefix result) 78 | string(LENGTH "${prefix}" prefix_length) 79 | string(LENGTH "${str}" str_length) 80 | if(prefix_length LESS_EQUAL str_length) 81 | string(SUBSTRING "${str}" 0 ${prefix_length} str_prefix) 82 | if(prefix STREQUAL str_prefix) 83 | set("${result}" ON PARENT_SCOPE) 84 | return() 85 | endif() 86 | endif() 87 | set("${result}" OFF PARENT_SCOPE) 88 | endfunction() 89 | 90 | # Handle upgrading logic 91 | if(CMKR_EXECUTABLE AND NOT CMKR_CACHED_EXECUTABLE STREQUAL CMKR_EXECUTABLE) 92 | cmkr_startswith("${CMKR_EXECUTABLE}" "${CMAKE_CURRENT_BINARY_DIR}/_cmkr" CMKR_STARTSWITH_BUILD) 93 | cmkr_startswith("${CMKR_EXECUTABLE}" "${CMKR_DIRECTORY_PREFIX}" CMKR_STARTSWITH_CACHE) 94 | if(CMKR_STARTSWITH_BUILD) 95 | if(DEFINED ENV{CMKR_CACHE}) 96 | message(AUTHOR_WARNING "[cmkr] Switching to cached cmkr: '${CMKR_CACHED_EXECUTABLE}'") 97 | if(EXISTS "${CMKR_CACHED_EXECUTABLE}") 98 | set(CMKR_EXECUTABLE "${CMKR_CACHED_EXECUTABLE}" CACHE FILEPATH "Full path to cmkr executable" FORCE) 99 | else() 100 | unset(CMKR_EXECUTABLE CACHE) 101 | endif() 102 | else() 103 | message(AUTHOR_WARNING "[cmkr] Upgrading '${CMKR_EXECUTABLE}' to '${CMKR_CACHED_EXECUTABLE}'") 104 | unset(CMKR_EXECUTABLE CACHE) 105 | endif() 106 | elseif(DEFINED ENV{CMKR_CACHE} AND CMKR_STARTSWITH_CACHE) 107 | message(AUTHOR_WARNING "[cmkr] Upgrading cached '${CMKR_EXECUTABLE}' to '${CMKR_CACHED_EXECUTABLE}'") 108 | unset(CMKR_EXECUTABLE CACHE) 109 | endif() 110 | endif() 111 | 112 | if(CMKR_EXECUTABLE AND EXISTS "${CMKR_EXECUTABLE}") 113 | message(VERBOSE "[cmkr] Found cmkr: '${CMKR_EXECUTABLE}'") 114 | elseif(CMKR_EXECUTABLE AND NOT CMKR_EXECUTABLE STREQUAL CMKR_CACHED_EXECUTABLE) 115 | message(FATAL_ERROR "[cmkr] '${CMKR_EXECUTABLE}' not found") 116 | elseif(NOT CMKR_EXECUTABLE AND EXISTS "${CMKR_CACHED_EXECUTABLE}") 117 | set(CMKR_EXECUTABLE "${CMKR_CACHED_EXECUTABLE}" CACHE FILEPATH "Full path to cmkr executable" FORCE) 118 | message(STATUS "[cmkr] Found cached cmkr: '${CMKR_EXECUTABLE}'") 119 | else() 120 | set(CMKR_EXECUTABLE "${CMKR_CACHED_EXECUTABLE}" CACHE FILEPATH "Full path to cmkr executable" FORCE) 121 | message(VERBOSE "[cmkr] Bootstrapping '${CMKR_EXECUTABLE}'") 122 | 123 | message(STATUS "[cmkr] Fetching cmkr...") 124 | if(EXISTS "${CMKR_DIRECTORY}") 125 | cmkr_exec("${CMAKE_COMMAND}" -E rm -rf "${CMKR_DIRECTORY}") 126 | endif() 127 | find_package(Git QUIET REQUIRED) 128 | cmkr_exec("${GIT_EXECUTABLE}" 129 | clone 130 | --config advice.detachedHead=false 131 | --branch ${CMKR_TAG} 132 | --depth 1 133 | ${CMKR_REPO} 134 | "${CMKR_DIRECTORY}" 135 | ) 136 | if(CMKR_COMMIT_HASH) 137 | execute_process( 138 | COMMAND "${GIT_EXECUTABLE}" checkout -q "${CMKR_COMMIT_HASH}" 139 | RESULT_VARIABLE CMKR_EXEC_RESULT 140 | WORKING_DIRECTORY "${CMKR_DIRECTORY}" 141 | ) 142 | if(NOT CMKR_EXEC_RESULT EQUAL 0) 143 | message(FATAL_ERROR "Tag '${CMKR_TAG}' hash is not '${CMKR_COMMIT_HASH}'") 144 | endif() 145 | endif() 146 | message(STATUS "[cmkr] Building cmkr (using system compiler)...") 147 | cmkr_exec("${CMAKE_COMMAND}" 148 | --no-warn-unused-cli 149 | "${CMKR_DIRECTORY}" 150 | "-B${CMKR_DIRECTORY}/build" 151 | "-DCMAKE_BUILD_TYPE=${CMKR_BUILD_TYPE}" 152 | "-DCMAKE_UNITY_BUILD=ON" 153 | "-DCMAKE_INSTALL_PREFIX=${CMKR_DIRECTORY}" 154 | "-DCMKR_GENERATE_DOCUMENTATION=OFF" 155 | ) 156 | cmkr_exec("${CMAKE_COMMAND}" 157 | --build "${CMKR_DIRECTORY}/build" 158 | --config "${CMKR_BUILD_TYPE}" 159 | --parallel 160 | ) 161 | cmkr_exec("${CMAKE_COMMAND}" 162 | --install "${CMKR_DIRECTORY}/build" 163 | --config "${CMKR_BUILD_TYPE}" 164 | --prefix "${CMKR_DIRECTORY}" 165 | --component cmkr 166 | ) 167 | if(NOT EXISTS ${CMKR_EXECUTABLE}) 168 | message(FATAL_ERROR "[cmkr] Failed to bootstrap '${CMKR_EXECUTABLE}'") 169 | endif() 170 | cmkr_exec("${CMKR_EXECUTABLE}" version) 171 | message(STATUS "[cmkr] Bootstrapped ${CMKR_EXECUTABLE}") 172 | endif() 173 | execute_process(COMMAND "${CMKR_EXECUTABLE}" version 174 | RESULT_VARIABLE CMKR_EXEC_RESULT 175 | ) 176 | if(NOT CMKR_EXEC_RESULT EQUAL 0) 177 | message(FATAL_ERROR "[cmkr] Failed to get version, try clearing the cache and rebuilding") 178 | endif() 179 | 180 | # Use cmkr.cmake as a script 181 | if(CMAKE_SCRIPT_MODE_FILE) 182 | if(NOT EXISTS "${CMAKE_SOURCE_DIR}/cmake.toml") 183 | execute_process(COMMAND "${CMKR_EXECUTABLE}" init 184 | RESULT_VARIABLE CMKR_EXEC_RESULT 185 | ) 186 | if(NOT CMKR_EXEC_RESULT EQUAL 0) 187 | message(FATAL_ERROR "[cmkr] Failed to bootstrap cmkr project. Please report an issue: https://github.com/build-cpp/cmkr/issues/new") 188 | else() 189 | message(STATUS "[cmkr] Modify cmake.toml and then configure using: cmake -B build") 190 | endif() 191 | else() 192 | execute_process(COMMAND "${CMKR_EXECUTABLE}" gen 193 | RESULT_VARIABLE CMKR_EXEC_RESULT 194 | ) 195 | if(NOT CMKR_EXEC_RESULT EQUAL 0) 196 | message(FATAL_ERROR "[cmkr] Failed to generate project.") 197 | else() 198 | message(STATUS "[cmkr] Configure using: cmake -B build") 199 | endif() 200 | endif() 201 | endif() 202 | 203 | # This is the macro that contains black magic 204 | macro(cmkr) 205 | # When this macro is called from the generated file, fake some internal CMake variables 206 | get_source_file_property(CMKR_CURRENT_LIST_FILE "${CMAKE_CURRENT_LIST_FILE}" CMKR_CURRENT_LIST_FILE) 207 | if(CMKR_CURRENT_LIST_FILE) 208 | set(CMAKE_CURRENT_LIST_FILE "${CMKR_CURRENT_LIST_FILE}") 209 | get_filename_component(CMAKE_CURRENT_LIST_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) 210 | endif() 211 | 212 | # File-based include guard (include_guard is not documented to work) 213 | get_source_file_property(CMKR_INCLUDE_GUARD "${CMAKE_CURRENT_LIST_FILE}" CMKR_INCLUDE_GUARD) 214 | if(NOT CMKR_INCLUDE_GUARD) 215 | set_source_files_properties("${CMAKE_CURRENT_LIST_FILE}" PROPERTIES CMKR_INCLUDE_GUARD TRUE) 216 | 217 | file(SHA256 "${CMAKE_CURRENT_LIST_FILE}" CMKR_LIST_FILE_SHA256_PRE) 218 | 219 | # Generate CMakeLists.txt 220 | cmkr_exec("${CMKR_EXECUTABLE}" gen 221 | WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" 222 | ) 223 | 224 | file(SHA256 "${CMAKE_CURRENT_LIST_FILE}" CMKR_LIST_FILE_SHA256_POST) 225 | 226 | # Delete the temporary file if it was left for some reason 227 | set(CMKR_TEMP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/CMakerLists.txt") 228 | if(EXISTS "${CMKR_TEMP_FILE}") 229 | file(REMOVE "${CMKR_TEMP_FILE}") 230 | endif() 231 | 232 | if(NOT CMKR_LIST_FILE_SHA256_PRE STREQUAL CMKR_LIST_FILE_SHA256_POST) 233 | # Copy the now-generated CMakeLists.txt to CMakerLists.txt 234 | # This is done because you cannot include() a file you are currently in 235 | configure_file(CMakeLists.txt "${CMKR_TEMP_FILE}" COPYONLY) 236 | 237 | # Add the macro required for the hack at the start of the cmkr macro 238 | set_source_files_properties("${CMKR_TEMP_FILE}" PROPERTIES 239 | CMKR_CURRENT_LIST_FILE "${CMAKE_CURRENT_LIST_FILE}" 240 | ) 241 | 242 | # 'Execute' the newly-generated CMakeLists.txt 243 | include("${CMKR_TEMP_FILE}") 244 | 245 | # Delete the generated file 246 | file(REMOVE "${CMKR_TEMP_FILE}") 247 | 248 | # Do not execute the rest of the original CMakeLists.txt 249 | return() 250 | endif() 251 | # Resume executing the unmodified CMakeLists.txt 252 | endif() 253 | endmacro() 254 | -------------------------------------------------------------------------------- /docs/file_formats/README.md: -------------------------------------------------------------------------------- 1 | This folder contains some templates for [010 Editor](https://www.sweetscape.com/010editor/) to define formats used in C2C / OR2, these formats can then be viewed/edited inside 010 Editors template view. 2 | 3 | GZ/SZ files must be decompressed first (GZ seems to decompress with normal GZip tools, SZ requires `mkdir output; offzip -A file.sz output`) 4 | 5 | After that the decompressed file can be loaded into 010 Editor, and the template can be opened via `Templates > Open Template`, & ran via `Templates > Run Template` 6 | 7 | Values inside the file can be changed inside the template results window by double-clicking the existing value shown. 8 | -------------------------------------------------------------------------------- /docs/file_formats/xmt_lindbergh.bt: -------------------------------------------------------------------------------- 1 | //------------------------------------------------ 2 | //--- 010 Editor v14.0.1 Binary Template 3 | // 4 | // File: OutRun 2 SP SDX (Lindbergh) XMT model package 5 | // Authors: emoose 6 | // Version: 1.0 7 | // Purpose: Decode structures used in XMT model packages 8 | // Category: 9 | // File Mask: mdl.bin 10 | // ID Bytes: 11 | // History: 12 | //------------------------------------------------ 13 | 14 | // TODO: unsure how XMTHEAD is used, model files seem to start with XMDLHEAD header instead 15 | // looks like carview reads this from the start of the compressed GZ but that doesn't seem to give valid data? 16 | // maybe it's a struct constructed at runtime... 17 | struct XMTHEAD 18 | { 19 | unsigned int flag; 20 | unsigned int objnum; 21 | unsigned int texnum; 22 | unsigned int mdldata; // offset 23 | unsigned int texdata; // offset 24 | }; 25 | 26 | struct XMDLHEAD 27 | { 28 | unsigned int flag; 29 | unsigned int mdlnum; 30 | unsigned int mdldata_table; 31 | unsigned int mdlname_table; 32 | }; 33 | 34 | struct CullingNode 35 | { 36 | unsigned int flags; 37 | float center[3]; 38 | float radius; 39 | int offset_lod; 40 | unsigned int tweening_factor; 41 | int index_matrix; 42 | int index_child; 43 | int index_sibling; 44 | int index_model[4]; 45 | }; 46 | 47 | struct ModelHeader 48 | { 49 | int index_vtx_groups; 50 | int numof_vtx_groups; 51 | }; 52 | 53 | struct VtxGroupInfo 54 | { 55 | int index_vtxfmt; 56 | unsigned int index_mat_groups[2]; 57 | int numof_mat_groups[2]; 58 | }; 59 | 60 | struct MatGroupInfo 61 | { 62 | unsigned int base_index; 63 | int index_material; 64 | int index_primitives; 65 | int numof_primitives; 66 | float center[3]; 67 | float radius; 68 | }; 69 | 70 | struct PrimitiveList 71 | { 72 | unsigned int primitive_type; 73 | unsigned int start_index; 74 | unsigned int primitive_count; 75 | unsigned int polygon_count; 76 | }; 77 | 78 | enum VertexShaderType 79 | { 80 | VSTYPE_FIXED_FUNCTION = 0x0, 81 | VSTYPE_BUMPDOTMAP = 0x1, 82 | VSTYPE_TWEENING = 0x2, 83 | VSTYPE_BUMPCUBEMAP = 0x3, 84 | VSTYPE_MAX = 0x4, 85 | }; 86 | 87 | enum FvfTbl_xyz 88 | { 89 | D3DFVF_XYZ = 1, 90 | D3DFVF_XYZRHW = 2, 91 | D3DFVF_XYZB1 = 3, 92 | D3DFVF_XYZB2 = 4, 93 | D3DFVF_XYZB3 = 5, 94 | D3DFVF_XYZB4 = 6 95 | }; 96 | 97 | enum FvfTbl_tex 98 | { 99 | D3DFVF_TEX1 = 1, 100 | D3DFVF_TEX2 = 2, 101 | D3DFVF_TEX3 = 3, 102 | D3DFVF_TEX4 = 4 103 | }; 104 | 105 | struct vertex_format_member // custom 106 | { 107 | int unk_0 : 1; 108 | FvfTbl_xyz vtxType_1 : 3; 109 | int fvf_normal_4 : 1; // uses D3DFVF_NORMAL 110 | int fvf_diffuse_5 : 1; // uses D3DFVF_DIFFUSE 111 | int fvf_specular_6 : 1; // uses D3DFVF_SPECULAR 112 | int unk_7 : 1; 113 | FvfTbl_tex texType_8 : 4; 114 | }; 115 | 116 | union vertex_format_bf 117 | { 118 | vertex_format_member m; 119 | unsigned int w; 120 | }; 121 | 122 | struct VtxFormatList(int header_offset) 123 | { 124 | int numof_stream; 125 | unsigned int offset_indices; 126 | unsigned int offset_vertices[4]; 127 | unsigned int index_buffer_size; 128 | unsigned int vertex_buffer_size; 129 | vertex_format_bf vertex_format; 130 | unsigned int vertex_format_size; 131 | VertexShaderType vertex_shader_type; 132 | 133 | local long pos = FTell(); 134 | 135 | FSeek(header_offset + offset_indices); 136 | char index_buffer_data[index_buffer_size]; 137 | 138 | // TODO: theres sometimes data between offset_indices & offset_vertices[0], what is it? 139 | // TODO: offset_vertices has 4 entries, but only a single vertex_buffer_size, how should it be handled? 140 | 141 | FSeek(header_offset + offset_vertices[0]); 142 | char vertex_buffer_data[vertex_buffer_size]; 143 | 144 | FSeek(pos); 145 | }; 146 | 147 | struct M_MatAttrFlags 148 | { 149 | int M_MA_SPECULAR_1 : 1; 150 | int M_MA_DOUBLESIDE_2 : 1; 151 | int M_MA_DOUBLESIDE_LIGHTING_4 : 1; 152 | int M_MA_ZBIAS_8 : 1; 153 | int M_MA_NOFOG_10 : 1; 154 | int unused_20 : 1; 155 | int unused_40 : 1; 156 | int unused_80 : 1; 157 | }; 158 | 159 | struct MatAttrib_member 160 | { 161 | M_MatAttrFlags flags; 162 | unsigned int src_blend_factor : 4; 163 | unsigned int dst_blend_factor : 4; 164 | unsigned int blend_operation : 3; 165 | unsigned int pixelshader : 4; 166 | unsigned int zbias : 4; 167 | unsigned int ftype : 2; 168 | }; 169 | 170 | union MatAttrib 171 | { 172 | MatAttrib_member m; 173 | unsigned int w; 174 | }; 175 | 176 | struct M_TexAttrFlags 177 | { 178 | int M_TA_SPECULARMAP_1 : 1; 179 | int M_TA_ENVMAP_LIGHTING_2 : 1; 180 | int M_TA_ENVMAP_SPHERE_4 : 1; 181 | int M_TA_ENVMAP_CUBE_8 : 1; 182 | int M_TA_VOLUMETEX_10 : 1; 183 | int M_TA_COMBINE_BUMPSPHEREMAP_20 : 1; 184 | int M_TA_PROJECTION_40 : 1; 185 | int M_TA_FRESNEL_80 : 1; 186 | // unsure why these are here, only 8 bits of flags above but flags field is 10 bits for some reason? 187 | int unused_100 : 1; 188 | int unused_200 : 1; 189 | }; 190 | 191 | struct TexAttrib_member 192 | { 193 | M_TexAttrFlags flags; 194 | unsigned int addr_u : 3; 195 | unsigned int addr_v : 3; 196 | unsigned int filter : 3; 197 | unsigned int mipmap : 2; 198 | unsigned int blend : 5; 199 | unsigned int alpha_blend : 2; 200 | unsigned int coord_index : 4; 201 | }; 202 | 203 | union TexAttrib 204 | { 205 | TexAttrib_member m; 206 | unsigned int w; 207 | }; 208 | 209 | struct MatTexInfo 210 | { 211 | TexAttrib attrib; 212 | unsigned int blendcolor; 213 | float mipmap_bias; 214 | float bump_depth; 215 | int index; 216 | }; 217 | 218 | struct MaterialList 219 | { 220 | int index_color; 221 | MatAttrib attrib; 222 | MatTexInfo texture[4]; 223 | }; 224 | 225 | struct MaterialColor 226 | { 227 | float diffuse[4]; 228 | float ambient[4]; 229 | float specular[4]; 230 | float emissive[4]; 231 | float power; 232 | float intensity; 233 | }; 234 | 235 | struct ObjectHeader 236 | { 237 | unsigned int offset_cull_nodes; 238 | unsigned int offset_matrices; 239 | unsigned int offset_models; 240 | unsigned int offset_vtx_groups; 241 | unsigned int offset_mat_groups; 242 | unsigned int offset_primitives; 243 | unsigned int offset_vtx_formats; 244 | unsigned int offset_materials; 245 | unsigned int offset_mat_colors; 246 | int numof_vtx_formats; 247 | int numof_mat_groups; 248 | int numof_materials; 249 | int numof_mat_colors; 250 | }; 251 | 252 | struct OBJECT(int namePos) 253 | { 254 | local long pos = FTell(); 255 | 256 | FSeek(namePos); 257 | string Name; 258 | 259 | FSeek(pos); 260 | 261 | ObjectHeader Header; 262 | 263 | // At runtime the game takes all the offsets from ObjectHeader and sets up pointers to each section 264 | // Those pointers just point to the start of the data for that section, and doesn't try to work out any kind of count 265 | // For loading into 010 Editor we can kinda work out the count by doing math against the next sections offset 266 | // but there's a good chance some malformed file could break this... 267 | 268 | FSeek(pos + Header.offset_cull_nodes); 269 | CullingNode CullingNodes[(Header.offset_matrices - Header.offset_cull_nodes) / 0x38]; 270 | 271 | FSeek(pos + Header.offset_models); 272 | ModelHeader ModelHeaders[(Header.offset_vtx_groups - Header.offset_models) / 8]; 273 | 274 | FSeek(pos + Header.offset_vtx_groups); 275 | VtxGroupInfo VtxGroups[(Header.offset_mat_groups - Header.offset_vtx_groups) / 0x14]; 276 | 277 | FSeek(pos + Header.offset_mat_groups); 278 | MatGroupInfo MatGroups[Header.numof_mat_groups]; //[(Header.offset_primitives - Header.offset_mat_groups) / 0x20]; 279 | 280 | FSeek(pos + Header.offset_primitives); 281 | PrimitiveList Primitives[Header.numof_mat_groups]; //[(Header.offset_vtx_formats - Header.offset_primitives) / 0x10]; 282 | 283 | FSeek(pos + Header.offset_vtx_formats); 284 | VtxFormatList VtxFormats(pos)[Header.numof_vtx_formats]; //[(Header.offset_materials - Header.offset_vtx_formats) / 0x2C]; 285 | 286 | FSeek(pos + Header.offset_materials); 287 | MaterialList Materials[Header.numof_materials]; //[(Header.offset_mat_colors - Header.offset_materials) / 0x58]; 288 | 289 | FSeek(pos + Header.offset_mat_colors); 290 | MaterialColor MaterialColors[Header.numof_mat_colors]; 291 | }; 292 | 293 | XMDLHEAD XMdlHead; 294 | 295 | FSeek(XMdlHead.mdldata_table); 296 | int MdlDataOffsets[XMdlHead.mdlnum]; 297 | FSeek(XMdlHead.mdlname_table); 298 | int MdlNameOffsets[XMdlHead.mdlnum]; 299 | 300 | local int i = 0; 301 | for(i = 0; i < XMdlHead.mdlnum; i++) 302 | { 303 | FSeek(MdlDataOffsets[i]); 304 | OBJECT Objects(MdlNameOffsets[i]); 305 | } 306 | -------------------------------------------------------------------------------- /docs/file_formats/xst.bt: -------------------------------------------------------------------------------- 1 | //------------------------------------------------ 2 | //--- 010 Editor v14.0.1 Binary Template 3 | // 4 | // File: OutRun 2 XST sprite package 5 | // Authors: emoose 6 | // Version: 1.0 7 | // Purpose: Decode structures used in XST sprite packages 8 | // Category: 9 | // File Mask: *.xst 10 | // ID Bytes: 11 | // History: 12 | //------------------------------------------------ 13 | 14 | // Lindbergh only contains XST headers + XPR0, with tex data following after XPRHeader.HeaderSize (usually 0x800 bytes) 15 | 16 | // Xbox/C2C added a memory header at the beginning that gave the system memory size / video memory size used by the file 17 | // Tex data then starts immediately after the system memory data (and in C2C the 0x800 XPRHeader.HeaderSize bytes are skipped entirely) 18 | // (C2C also sets XPR0 headers to nonsense values for some reason) 19 | 20 | // TODO: on Xbox fetching the tex data after system memory data seems to leave 4 bytes missed at the start 21 | // the orig XPR0 header is included there, maybe should just use XPRHeader.HeaderSize for it like in Lindbergh? 22 | // need to actually extract the texdata out and see which method gives a valid texture... 23 | 24 | struct DSPDATA 25 | { 26 | unsigned int scr_idx; 27 | unsigned short sx; 28 | unsigned short sy; 29 | unsigned short rot; 30 | unsigned short flip; 31 | float scale; 32 | }; 33 | 34 | struct DSPTBL(int xstOffset) 35 | { 36 | unsigned int nb_dspdata; 37 | int dspdata; // DSPDATA * 38 | 39 | local long pos = FTell(); 40 | 41 | FSeek(xstOffset + dspdata); 42 | DSPDATA DspData[nb_dspdata]; 43 | 44 | FSeek(pos); 45 | }; 46 | 47 | struct SCRTBL 48 | { 49 | unsigned int spr_idx; 50 | float su; // start 51 | float sv; 52 | float eu; // end 53 | float ev; 54 | unsigned short sx; // start 55 | unsigned short sy; 56 | unsigned short ex; // end 57 | unsigned short ey; 58 | }; 59 | 60 | struct SCRTBL2 // unused? 61 | { 62 | unsigned int spr_idx; 63 | unsigned int rotate; 64 | float su; // start 65 | float sv; 66 | float eu; // end 67 | float ev; 68 | unsigned short sx; // start 69 | unsigned short sy; 70 | unsigned short ex; // end 71 | unsigned short ey; 72 | }; 73 | 74 | struct MEMHEAD // probably not actual struct name 75 | { 76 | unsigned int SysMemSize_0; 77 | unsigned int VidMemSize_4; 78 | }; 79 | 80 | // In C2C these seem set to nonsense values, 1 / 2 / 3 ? 81 | struct XPR0_Header 82 | { 83 | uint32 Magic; 84 | uint32 TotalSize; 85 | uint32 HeaderSize; 86 | }; 87 | 88 | enum X_D3DFMT 89 | { 90 | X_D3DFMT_L8 = 0x00000000, 91 | X_D3DFMT_AL8 = 0x00000001, 92 | X_D3DFMT_A1R5G5B5 = 0x00000002, 93 | X_D3DFMT_X1R5G5B5 = 0x00000003, 94 | X_D3DFMT_A4R4G4B4 = 0x00000004, 95 | X_D3DFMT_R5G6B5 = 0x00000005, 96 | X_D3DFMT_A8R8G8B8 = 0x00000006, 97 | X_D3DFMT_X8R8G8B8 = 0x00000007, 98 | X_D3DFMT_X8L8V8U8 = 0x00000007, 99 | X_D3DFMT_P8 = 0x0000000B, 100 | X_D3DFMT_DXT1 = 0x0000000C, 101 | //X_D3DFMT_DXT2 = 0x0000000E, 102 | X_D3DFMT_DXT3 = 0x0000000E, 103 | //X_D3DFMT_DXT4 = 0x0000000F, 104 | X_D3DFMT_DXT5 = 0x0000000F, 105 | X_D3DFMT_LIN_A1R5G5B5 = 0x00000010, 106 | X_D3DFMT_LIN_R5G6B5 = 0x00000011, 107 | X_D3DFMT_LIN_A8R8G8B8 = 0x00000012, 108 | X_D3DFMT_LIN_Q8W8V8U8 = 0x00000012, 109 | X_D3DFMT_LIN_L8 = 0x00000013, 110 | X_D3DFMT_LIN_R8B8 = 0x00000016, 111 | X_D3DFMT_LIN_V8U8 = 0x00000017, 112 | X_D3DFMT_LIN_G8B8 = 0x00000017, 113 | X_D3DFMT_A8 = 0x00000019, 114 | X_D3DFMT_A8L8 = 0x0000001A, 115 | X_D3DFMT_LIN_AL8 = 0x0000001B, 116 | X_D3DFMT_LIN_X1R5G5B5 = 0x0000001C, 117 | X_D3DFMT_LIN_A4R4G4B4 = 0x0000001D, 118 | X_D3DFMT_LIN_X8R8G8B8 = 0x0000001E, 119 | X_D3DFMT_LIN_X8L8V8U8 = 0x0000001E, 120 | X_D3DFMT_LIN_A8 = 0x0000001F, 121 | X_D3DFMT_LIN_A8L8 = 0x00000020, 122 | X_D3DFMT_R6G5B5 = 0x00000027, 123 | X_D3DFMT_L6V5U5 = 0x00000027, 124 | X_D3DFMT_G8B8 = 0x00000028, 125 | X_D3DFMT_V8U8 = 0x00000028, 126 | X_D3DFMT_R8B8 = 0x00000029, 127 | X_D3DFMT_L16 = 0x00000032, 128 | X_D3DFMT_V16U16 = 0x00000033, 129 | X_D3DFMT_LIN_L16 = 0x00000035, 130 | X_D3DFMT_LIN_V16U16 = 0x00000036, 131 | X_D3DFMT_LIN_R6G5B5 = 0x00000037, 132 | X_D3DFMT_LIN_L6V5U5 = 0x00000037, 133 | X_D3DFMT_R5G5B5A1 = 0x00000038, 134 | X_D3DFMT_R4G4B4A4 = 0x00000039, 135 | X_D3DFMT_A8B8G8R8 = 0x0000003A, 136 | X_D3DFMT_Q8W8V8U8 = 0x0000003A, 137 | X_D3DFMT_B8G8R8A8 = 0x0000003B, 138 | X_D3DFMT_R8G8B8A8 = 0x0000003C, 139 | X_D3DFMT_LIN_R5G5B5A1 = 0x0000003D, 140 | X_D3DFMT_LIN_R4G4B4A4 = 0x0000003E, 141 | X_D3DFMT_LIN_A8B8G8R8 = 0x0000003F, 142 | X_D3DFMT_LIN_B8G8R8A8 = 0x00000040, 143 | X_D3DFMT_LIN_R8G8B8A8 = 0x00000041, 144 | }; 145 | 146 | struct D3DFORMAT 147 | { 148 | int DMACHANNEL_0 : 2; 149 | int CUBEMAP_2 : 1; 150 | int BORDERSOURCE_COLOR_3 : 1; 151 | int DIMENSIONS_4 : 4; 152 | X_D3DFMT FORMAT_8 : 8; 153 | int MIPMAPS_16 : 4; 154 | int USIZE_20 : 4; 155 | int VSIZE_24 : 4; 156 | int PSIZE_28 : 4; 157 | }; 158 | 159 | enum X_D3DCOMMON_TYPE 160 | { 161 | D3DCOMMON_TYPE_VERTEXBUFFER = 0, 162 | D3DCOMMON_TYPE_INDEXBUFFER = 1, 163 | D3DCOMMON_TYPE_PUSHBUFFER = 2, 164 | D3DCOMMON_TYPE_PALETTE = 3, 165 | D3DCOMMON_TYPE_TEXTURE = 4, 166 | D3DCOMMON_TYPE_SURFACE = 5, 167 | D3DCOMMON_TYPE_FIXUP = 6 168 | }; 169 | 170 | struct D3DCOMMON 171 | { 172 | int REFCOUNT_0 : 16; 173 | X_D3DCOMMON_TYPE TYPE_16 : 3; 174 | int INTREFCOUNT_19 : 5; 175 | int D3DCREATED_24 : 1; 176 | int UNUSED_25 : 7; 177 | }; 178 | 179 | struct XPR0_Entry 180 | { 181 | D3DCOMMON common; 182 | uint32 data; 183 | uint32 lock; 184 | D3DFORMAT format; 185 | uint32 size; 186 | }; 187 | 188 | struct XSTHEAD 189 | { 190 | unsigned int flag; 191 | unsigned int tex_ofs; 192 | unsigned int nb_tex; 193 | int dummy; 194 | unsigned int nb_dsptbl; 195 | int dsptbl; 196 | unsigned int nb_scrtbl; 197 | int scrtbl; 198 | }; 199 | 200 | struct TexData(int size) 201 | { 202 | char data[size]; 203 | }; 204 | 205 | // Check if this file starts with an Xbox/C2C mem header, the values should add up to the file size if it's valid 206 | MEMHEAD MemHeaderTest; 207 | FSeek(FTell() - 8); 208 | 209 | local int hasMemHeader = 0; 210 | if (FileSize() == (MemHeaderTest.SysMemSize_0 + MemHeaderTest.VidMemSize_4 + 8)) 211 | { 212 | MEMHEAD MemHeader; 213 | hasMemHeader = 1; 214 | } 215 | 216 | local long xstPos = FTell(); 217 | XSTHEAD Head; 218 | 219 | FSeek(xstPos + Head.dsptbl); 220 | DSPTBL DspTbl(xstPos)[Head.nb_dsptbl]; 221 | 222 | FSeek(xstPos + Head.scrtbl); 223 | SCRTBL ScrTbl[Head.nb_scrtbl]; 224 | 225 | FSeek(xstPos + Head.tex_ofs); 226 | 227 | XPR0_Header XPRHeader; 228 | XPR0_Entry XPREntries[Head.nb_tex]; 229 | 230 | local long texDataStart = xstPos + Head.tex_ofs + XPRHeader.HeaderSize; 231 | if (hasMemHeader == 1) 232 | { 233 | texDataStart = MemHeader.SysMemSize_0 + 8; 234 | } 235 | 236 | FSeek(texDataStart); 237 | 238 | local int i = 0; 239 | local int size = 0; 240 | for(i = 0; i < Head.nb_tex; i++) 241 | { 242 | size = XPREntries[i].size; 243 | // "true" XPR0 seems to leave size field empty, have to work it out from the offset of the next entry (or EOF) 244 | if (size == 0 && i+1 < Head.nb_tex) 245 | size = XPREntries[i+1].data - XPREntries[i].data; 246 | if (size == 0) 247 | size = FileSize() - FTell(); 248 | TexData Texture(size); 249 | } 250 | -------------------------------------------------------------------------------- /generate_vs2022.bat: -------------------------------------------------------------------------------- 1 | git submodule update --init --recursive 2 | mkdir build 3 | cd build 4 | cmake .. -G "Visual Studio 17 2022" -A Win32 5 | -------------------------------------------------------------------------------- /src/Proxy.def: -------------------------------------------------------------------------------- 1 | EXPORTS 2 | DirectInput8Create @9 3 | DXGIDumpJournal @10 4 | CreateDXGIFactory @11 5 | CreateDXGIFactory1 @12 6 | CreateDXGIFactory2 @13 7 | DXGID3D10CreateDevice @14 8 | DXGID3D10CreateLayeredDevice @15 9 | DXGID3D10GetLayeredDeviceSize @17 10 | DXGID3D10RegisterLayers @18 11 | DXGIGetDebugInterface1 @19 12 | DXGIReportAdapterConfiguration @20 13 | X3DAudioInitialize @21 14 | X3DAudioCalculate @22 15 | CreateFX @23 16 | -------------------------------------------------------------------------------- /src/Proxy.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace proxy 4 | { 5 | extern HMODULE origModule; 6 | bool on_attach(HMODULE ourModule); 7 | void on_detach(); 8 | }; 9 | 10 | #define PLUGIN_API extern "C" __declspec(dllexport) 11 | -------------------------------------------------------------------------------- /src/Resource.aps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emoose/OutRun2006Tweaks/02d03ea5521877096ec8774c57e1d9ba708d3ef3/src/Resource.aps -------------------------------------------------------------------------------- /src/Resource.rc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emoose/OutRun2006Tweaks/02d03ea5521877096ec8774c57e1d9ba708d3ef3/src/Resource.rc -------------------------------------------------------------------------------- /src/game_addrs.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "plugin.hpp" 4 | #include 5 | #include 6 | #include "game.hpp" 7 | 8 | typedef void(__stdcall* D3DXVec4Transform_fn)(D3DXVECTOR4*, D3DXVECTOR4*, D3DMATRIX*); 9 | 10 | typedef void(__cdecl* mxPushLoadMatrix_fn)(D3DMATRIX*); 11 | typedef void(__cdecl* mxTranslate_fn)(float, float, float); 12 | typedef void(__cdecl* DrawObject_Internal_fn)(int, int, uint16_t*, int, int, int); 13 | typedef void(__cdecl* DrawObjectAlpha_Internal_fn)(int, float, void*, int); 14 | typedef void(__cdecl* mxPopMatrix_fn)(); 15 | 16 | namespace Game 17 | { 18 | inline D3DXVECTOR2 original_resolution{ 640, 480 }; 19 | 20 | inline int* game_mode = nullptr; 21 | inline GameState* current_mode = nullptr; 22 | inline int* game_start_progress_code = nullptr; 23 | inline int* file_load_progress_code = nullptr; 24 | 25 | static_assert(sizeof(bool) == sizeof(uint8_t)); // the following bools take 1 byte each 26 | inline bool* Sumo_CountdownTimerEnable = nullptr; 27 | inline bool* Sumo_IntroLogosEnable = nullptr; 28 | 29 | inline D3DPRESENT_PARAMETERS* D3DPresentParams = nullptr; 30 | inline IDirect3DDevice9** D3DDevice_ptr = nullptr; 31 | 32 | inline IDirectInput8A** DirectInput8_ptr = nullptr; 33 | 34 | inline HWND* hWnd_ptr = nullptr; 35 | 36 | inline IDirect3DDevice9* D3DDevice() { 37 | return *D3DDevice_ptr; 38 | } 39 | inline IDirectInput8A* DirectInput8() { 40 | return *DirectInput8_ptr; 41 | } 42 | inline HWND GameHwnd() { 43 | return *hWnd_ptr; 44 | } 45 | 46 | inline uint32_t* navipub_disp_flg = nullptr; 47 | 48 | inline int* sel_bgm_kind_buf = nullptr; 49 | 50 | inline s_chrset_info* chrset_info = nullptr; 51 | 52 | inline int* app_time = nullptr; // used by SetTweeningTable etc 53 | inline int* sprani_num_ticks = nullptr; // number of game ticks being ran in the current frame (can be 0 if above 60FPS) 54 | 55 | inline GameStage* stg_stage_num = nullptr; 56 | 57 | inline D3DXVECTOR2* screen_scale = nullptr; 58 | 59 | inline DrawBuffer* s_ImmDrawBuffer = nullptr; 60 | inline DrawBuffer* s_AftDrawBuffer = nullptr; 61 | 62 | // ini cfg 63 | inline D3DXVECTOR2* screen_resolution = nullptr; 64 | inline int* D3DFogEnabled = nullptr; 65 | inline int* D3DAdapterNum = nullptr; 66 | inline int* D3DAntialiasing = nullptr; 67 | inline uint8_t* D3DWindowed = nullptr; 68 | inline int* CfgLanguage = nullptr; 69 | 70 | // player data 71 | inline float* Sumo_NumOutRunMiles = nullptr; 72 | 73 | // game functions 74 | inline fn_0args SetFrameStartCpuTime = nullptr; 75 | inline fn_1arg_int CalcNumUpdatesToRun = nullptr; 76 | 77 | inline LPDIENUMDEVICESCALLBACKA DInput_EnumJoysticksCallback = nullptr; 78 | 79 | inline fn_0args Sumo_D3DResourcesRelease = nullptr; 80 | inline fn_0args Sumo_D3DResourcesCreate = nullptr; 81 | 82 | inline fn_1arg fn43FA10 = nullptr; 83 | 84 | inline fn_0args ReadIO = nullptr; 85 | inline fn_0args SoundControl_mb = nullptr; 86 | inline fn_0args LinkControlReceive = nullptr; 87 | inline fn_0args ModeControl = nullptr; 88 | inline fn_0args EventControl = nullptr; 89 | inline fn_0args GhostCarExecServer = nullptr; 90 | inline fn_0args fn4666A0 = nullptr; 91 | inline fn_0args FileLoad_Ctrl = nullptr; 92 | 93 | inline fn_1arg PrjSndRequest = nullptr; 94 | inline fn_1arg SetSndQueue = nullptr; 95 | 96 | inline fn_1arg_int SwitchNow = nullptr; 97 | inline int(*Sumo_CalcSteerSensitivity)(int cur, int prev) = nullptr; 98 | 99 | inline fn_1arg_int GetNowStageNum = nullptr; 100 | inline fn_1arg_int GetStageUniqueNum = nullptr; 101 | inline fn_1arg_int GetMaxCsLen = nullptr; 102 | inline fn_1arg_char GetStageUniqueName = nullptr; 103 | 104 | inline void(*QuickSort)(void*, int, int) = nullptr; 105 | inline void(*DrawStoredModel_Internal)(DrawBuffer*) = nullptr; 106 | 107 | inline fn_stdcall_1arg_int Sumo_CheckRacerUnlocked = nullptr; 108 | 109 | inline const char* SumoNet_OnlineUserName = nullptr; 110 | inline sSumoNet_LobbyInfo* SumoNet_LobbyInfo = nullptr; 111 | inline SumoNet_NetDriver** SumoNet_CurNetDriver = nullptr; 112 | 113 | // 2d sprite drawing 114 | inline fn_1arg sprSetFontPriority = nullptr; 115 | inline fn_1arg sprSetPrintFont = nullptr; 116 | inline fn_1arg sprSetFontColor = nullptr; 117 | inline fn_2floats sprSetFontScale = nullptr; 118 | inline fn_2args sprLocateP = nullptr; 119 | inline fn_printf sprPrintf = nullptr; 120 | 121 | inline fn_1arg_char Sumo_GetStringFromId = nullptr; 122 | inline fn_printf Sumo_Printf = nullptr; 123 | 124 | inline fn_0args_void SumoFrontEnd_GetSingleton_4035F0 = nullptr; 125 | inline fn_0args_class SumoFrontEnd_animate_443110 = nullptr; 126 | 127 | // 3d drawing 128 | inline DrawObject_Internal_fn DrawObject_Internal = nullptr; 129 | inline DrawObjectAlpha_Internal_fn DrawObjectAlpha_Internal = nullptr; 130 | inline int* power_on_timer = nullptr; 131 | 132 | // math 133 | inline mxPushLoadMatrix_fn mxPushLoadMatrix = (mxPushLoadMatrix_fn)0x409F90; 134 | inline mxTranslate_fn mxTranslate = (mxTranslate_fn)0x40A290; 135 | inline mxPopMatrix_fn mxPopMatrix = (mxPopMatrix_fn)0x40A010; 136 | 137 | // audio 138 | inline fn_3args adxPlay = nullptr; 139 | inline fn_0args adxStopAll = nullptr; 140 | 141 | // D3DX 142 | inline D3DXVec4Transform_fn D3DXVec4Transform = nullptr; 143 | 144 | inline sEventWork* event(int event_id) 145 | { 146 | sEventWork* s_EventWork = Module::exe_ptr(0x399B30); 147 | return &s_EventWork[event_id]; 148 | } 149 | 150 | inline EVWORK_CAR* pl_car() 151 | { 152 | return event(8)->data(); 153 | } 154 | 155 | inline bool is_in_game() 156 | { 157 | return 158 | *Game::current_mode == GameState::STATE_GAME || 159 | *Game::current_mode == GameState::STATE_GOAL || 160 | *Game::current_mode == GameState::STATE_TIMEUP || 161 | *Game::current_mode == GameState::STATE_TRYAGAIN || 162 | *Game::current_mode == GameState::STATE_OUTRUNMILES || 163 | *Game::current_mode == GameState::STATE_SMPAUSEMENU || 164 | (*Game::current_mode == GameState::STATE_START && *Game::game_start_progress_code == 65); 165 | } 166 | 167 | inline const char* StageNames[] = { 168 | "Palm Beach", 169 | "Deep Lake", "Industrial Complex", 170 | "Alpine", "Snowy Mountain", "Cloudy Highland", 171 | "Castle Wall", "Ghost Forest", "Coniferous Forest", "Desert", 172 | "Tulip Garden", "Metropolis", "Ancient Ruins", "Cape Way", "Imperial Avenue", 173 | 174 | "Sunny Beach", 175 | "Big Forest", "Waterfalls", 176 | "Casino Town", "Ice Scape", "Canyon", 177 | "Bay Area", "Jungle", "Lost City", "National Park", 178 | "Legend", "Skyscrapers", "Floral Village", "Milky Way", "Giant Statues", 179 | 180 | "(R) Palm Beach", 181 | "(R) Deep Lake", "(R) Industrial Complex", 182 | "(R) Alpine", "(R) Snowy Mountain", "(R) Cloudy Highland", 183 | "(R) Castle Wall", "(R) Ghost Forest", "(R) Coniferous Forest", "(R) Desert", 184 | "(R) Tulip Garden", "(R) Metropolis", "(R) Ancient Ruins", "(R) Cape Way", "(R) Imperial Avenue", 185 | 186 | "(R) Sunny Beach", 187 | "(R) Big Forest", "(R) Waterfalls", 188 | "(R) Casino Town", "(R) Ice Scape", "(R) Canyon", 189 | "(R) Bay Area", "(R) Jungle", "(R) Lost City", "(R) National Park", 190 | "(R) Legend", "(R) Skyscrapers", "(R) Floral Village", "(R) Milky Way", "(R) Giant Statues", 191 | 192 | "(T) Palm Beach", "(T) Sunny Beach", 193 | "(Night) Palm Beach", "(Night) Sunny Beach", 194 | "(R-Night) Palm Beach", "(R-Night) Sunny Beach" 195 | }; 196 | 197 | inline const char* GetStageFriendlyName(GameStage stage) 198 | { 199 | if (int(stage) < 0x42) 200 | return StageNames[int(stage)]; 201 | return StageNames[0]; 202 | } 203 | 204 | inline void init() 205 | { 206 | game_mode = Module::exe_ptr(0x380258); 207 | current_mode = Module::exe_ptr(0x38026C); 208 | game_start_progress_code = Module::exe_ptr(0x4367A8); 209 | file_load_progress_code = Module::exe_ptr(0x436718); 210 | Sumo_CountdownTimerEnable = Module::exe_ptr(0x237911); 211 | Sumo_IntroLogosEnable = Module::exe_ptr(0x2319A1); 212 | 213 | D3DPresentParams = Module::exe_ptr(0x49BD64); 214 | D3DDevice_ptr = Module::exe_ptr(0x49BD60); 215 | DirectInput8_ptr = Module::exe_ptr(0x4606E8); 216 | hWnd_ptr = Module::exe_ptr(0x4A8C88); 217 | 218 | navipub_disp_flg = Module::exe_ptr(0x4447F8); 219 | 220 | sel_bgm_kind_buf = Module::exe_ptr(0x430364); 221 | 222 | chrset_info = Module::exe_ptr(0x254860); 223 | 224 | app_time = Module::exe_ptr(0x49EDB8); 225 | sprani_num_ticks = Module::exe_ptr(0x380278); 226 | 227 | stg_stage_num = Module::exe_ptr(0x3D2E8C); 228 | 229 | screen_scale = Module::exe_ptr(0x340C94); 230 | 231 | s_ImmDrawBuffer = Module::exe_ptr(0x00464EF8); 232 | s_AftDrawBuffer = Module::exe_ptr(0x004612D8); 233 | 234 | screen_resolution = Module::exe_ptr(0x340C8C); 235 | 236 | D3DFogEnabled = Module::exe_ptr(0x340C88); 237 | D3DAdapterNum = Module::exe_ptr(0x55AF00); 238 | D3DAntialiasing = Module::exe_ptr(0x55AF04); 239 | D3DWindowed = Module::exe_ptr(0x55AF08); 240 | CfgLanguage = Module::exe_ptr(0x340CA0); 241 | 242 | Sumo_NumOutRunMiles = Module::exe_ptr(0x3C2404); 243 | 244 | SetFrameStartCpuTime = Module::fn_ptr(0x49430); 245 | CalcNumUpdatesToRun = Module::fn_ptr(0x17890); 246 | 247 | DInput_EnumJoysticksCallback = Module::fn_ptr(0x3EF0); 248 | 249 | Sumo_D3DResourcesRelease = Module::fn_ptr(0x17970); 250 | Sumo_D3DResourcesCreate = Module::fn_ptr(0x17A20); 251 | 252 | fn43FA10 = Module::fn_ptr(0x3FA10); 253 | 254 | ReadIO = Module::fn_ptr(0x53BB0); // ReadIO 255 | SoundControl_mb = Module::fn_ptr(0x2F330); // SoundControl_mb 256 | LinkControlReceive = Module::fn_ptr(0x55130); // LinkControlReceive 257 | ModeControl = Module::fn_ptr(0x3FA20); // ModeControl 258 | EventControl = Module::fn_ptr(0x3FAB0); // EventControl 259 | GhostCarExecServer = Module::fn_ptr(0x80F80); // GhostCarExecServer 260 | fn4666A0 = Module::fn_ptr(0x666A0); 261 | FileLoad_Ctrl = Module::fn_ptr(0x4FBA0); 262 | 263 | PrjSndRequest = Module::fn_ptr(0x249F0); 264 | SetSndQueue = Module::fn_ptr(0x24940); 265 | 266 | SwitchNow = Module::fn_ptr(0x536C0); 267 | Sumo_CalcSteerSensitivity = Module::fn_ptr(0x537C0, Sumo_CalcSteerSensitivity); 268 | 269 | GetNowStageNum = Module::fn_ptr(0x50380); 270 | GetStageUniqueNum = Module::fn_ptr(0x4DC50); 271 | GetMaxCsLen = Module::fn_ptr(0x3D470); 272 | GetStageUniqueName = Module::fn_ptr(0x4BE80); 273 | 274 | QuickSort = Module::fn_ptr(0x499E0, QuickSort); 275 | DrawStoredModel_Internal = Module::fn_ptr(0x5890, DrawStoredModel_Internal); 276 | 277 | Sumo_CheckRacerUnlocked = Module::fn_ptr(0xE8410); 278 | 279 | SumoNet_OnlineUserName = Module::exe_ptr(0x430C20); 280 | SumoNet_LobbyInfo = Module::exe_ptr(0x25A7A4); 281 | SumoNet_CurNetDriver = Module::exe_ptr(0x3D68AC); 282 | 283 | sprSetFontPriority = Module::fn_ptr(0x2CCB0); 284 | sprSetPrintFont = Module::fn_ptr(0x2CA60); 285 | sprSetFontColor = Module::fn_ptr(0x2CCA0); 286 | sprSetFontScale = Module::fn_ptr(0x2CC60); 287 | sprLocateP = Module::fn_ptr(0x2CC00); 288 | sprPrintf = Module::fn_ptr(0x2CCE0); 289 | 290 | Sumo_GetStringFromId = Module::fn_ptr(0x65EB0); 291 | Sumo_Printf = Module::fn_ptr(0x2CDD0); 292 | 293 | SumoFrontEnd_GetSingleton_4035F0 = Module::fn_ptr(0x35F0); 294 | SumoFrontEnd_animate_443110 = Module::fn_ptr(0x43110); 295 | 296 | DrawObject_Internal = Module::fn_ptr(0x5360); 297 | DrawObjectAlpha_Internal = Module::fn_ptr(0x56D0); 298 | power_on_timer = Module::exe_ptr(0x55AF0C); 299 | 300 | mxPushLoadMatrix = Module::fn_ptr(0x9F90); 301 | mxTranslate = Module::fn_ptr(0xA290); 302 | mxPopMatrix = Module::fn_ptr(0xA010); 303 | 304 | adxPlay = Module::fn_ptr(0x1000); 305 | adxStopAll = Module::fn_ptr(0x1050); 306 | 307 | D3DXVec4Transform = Module::fn_ptr(0x393B2); 308 | } 309 | }; 310 | -------------------------------------------------------------------------------- /src/hook_mgr.cpp: -------------------------------------------------------------------------------- 1 | #include "hook_mgr.hpp" 2 | 3 | Hook::Hook() 4 | { 5 | HookManager::RegisterHook(this); 6 | } 7 | 8 | void HookManager::ApplyHooks() 9 | { 10 | for (const auto& hook : s_hooks) 11 | { 12 | hook->is_active_ = false; 13 | if (hook->validate()) 14 | { 15 | hook->is_active_ = hook->apply(); 16 | auto desc = hook->description(); 17 | if (!desc.empty()) 18 | { 19 | spdlog::log(hook->is_active_ ? 20 | spdlog::level::info : spdlog::level::err, 21 | "{}: apply {}", desc, hook->is_active_ ? "successful" : "failed"); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/hook_mgr.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | // Base class for hooks 17 | class Hook 18 | { 19 | friend class HookManager; 20 | 21 | public: 22 | Hook(); 23 | 24 | virtual ~Hook() = default; 25 | 26 | // name/description of hook, for debug logging/tracing 27 | virtual std::string_view description() = 0; 28 | 29 | // check if user has enabled this hook, and any prerequisites are satisfied 30 | virtual bool validate() = 0; 31 | 32 | // applies the hook/patch 33 | virtual bool apply() = 0; 34 | 35 | bool active() 36 | { 37 | return is_active_; 38 | } 39 | 40 | bool error() 41 | { 42 | return has_error_; 43 | } 44 | 45 | private: 46 | bool is_active_ = false; 47 | bool has_error_ = false; 48 | }; 49 | 50 | // Static HookManager class 51 | class HookManager 52 | { 53 | private: 54 | inline static std::vector s_hooks; 55 | 56 | public: 57 | static void RegisterHook(Hook* hook) 58 | { 59 | s_hooks.emplace_back(hook); 60 | } 61 | 62 | static void ApplyHooks(); 63 | }; 64 | -------------------------------------------------------------------------------- /src/hooks_audio.cpp: -------------------------------------------------------------------------------- 1 | #include "hook_mgr.hpp" 2 | #include "plugin.hpp" 3 | #include "game_addrs.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | std::string BGMOverridePath; 10 | 11 | class BGMLoaderHook : public Hook 12 | { 13 | // Hook games BGM loader to check the type of the audio file being loaded 14 | // Game already has CWaveFile code to load in uncompressed WAV audio, but it goes left unused 15 | // We'll just check if any WAV file exists with the same filename, and switch over to it if so 16 | // (fortunately games BGM code seems based on DirectX DXUTsound.cpp sample, which included the CWaveFile class) 17 | const static int CSoundManager__CreateStreaming_HookAddr = 0x11FA3; 18 | 19 | inline static char CurWavFilePath[4096]; 20 | 21 | inline static SafetyHookMid hook = {}; 22 | static void destination(safetyhook::Context& ctx) 23 | { 24 | std::filesystem::path strWaveFileName = *(const char**)(ctx.esp + 0x54); 25 | if (!BGMOverridePath.empty()) 26 | { 27 | if (!std::filesystem::exists(BGMOverridePath)) 28 | BGMOverridePath = ".\\Sound\\" + BGMOverridePath; 29 | 30 | if (std::filesystem::exists(BGMOverridePath)) 31 | { 32 | strcpy_s(CurWavFilePath, BGMOverridePath.c_str()); 33 | *(const char**)(ctx.esp + 0x54) = CurWavFilePath; 34 | strWaveFileName = BGMOverridePath; 35 | } 36 | BGMOverridePath.clear(); 37 | } 38 | 39 | // If a .wav file exists with the same filename, let's try checking that file instead 40 | std::filesystem::path fileNameAsWav = strWaveFileName; 41 | fileNameAsWav = fileNameAsWav.replace_extension(".wav"); 42 | std::filesystem::path fileNameAsFlac = strWaveFileName; 43 | fileNameAsFlac = fileNameAsFlac.replace_extension(".flac"); 44 | 45 | enum class WaveFileType 46 | { 47 | WAV = 1, 48 | FLAC = 2, 49 | OGG = 3 50 | }; 51 | 52 | // Normally hardcoded to 3/OGG, but changing to 1/WAV allows using CWaveFile 53 | // 2/FLAC is checked for by our hooks_flac.cpp code 54 | WaveFileType waveFileType = WaveFileType(ctx.eax); 55 | if (Settings::AllowFLAC && std::filesystem::exists(fileNameAsFlac)) 56 | { 57 | waveFileType = WaveFileType::FLAC; 58 | 59 | // Switch the file game is trying to load to the flac instead 60 | strcpy_s(CurWavFilePath, fileNameAsFlac.string().c_str()); 61 | *(const char**)(ctx.esp + 0x54) = CurWavFilePath; 62 | } 63 | else if (Settings::AllowWAV && std::filesystem::exists(fileNameAsWav)) 64 | { 65 | waveFileType = WaveFileType::WAV; 66 | 67 | // Switch the file game is trying to load to the wav instead 68 | strcpy_s(CurWavFilePath, fileNameAsWav.string().c_str()); 69 | *(const char**)(ctx.esp + 0x54) = CurWavFilePath; 70 | } 71 | ctx.eax = int(waveFileType); 72 | } 73 | 74 | public: 75 | std::string_view description() override 76 | { 77 | return "BGMLoaderHook"; 78 | } 79 | 80 | bool validate() override 81 | { 82 | return (Settings::AllowWAV || Settings::AllowFLAC || Settings::CDSwitcherEnable); 83 | } 84 | 85 | bool apply() override 86 | { 87 | hook = safetyhook::create_mid(Module::exe_ptr(CSoundManager__CreateStreaming_HookAddr), destination); 88 | return !!hook; 89 | } 90 | 91 | static BGMLoaderHook instance; 92 | }; 93 | BGMLoaderHook BGMLoaderHook::instance; 94 | 95 | uint32_t ParseButtonCombination(std::string_view combo) 96 | { 97 | int retval = 0; 98 | std::string cur_token; 99 | 100 | // Parse combo tokens into buttons bitfield (tokens seperated by any non-alphabetical char, eg. +) 101 | for (char c : combo) 102 | { 103 | if (!isalpha(c) && c != '-') 104 | { 105 | if (cur_token.length() && XInputButtonMap.count(cur_token)) 106 | retval |= XInputButtonMap.at(cur_token); 107 | 108 | cur_token.clear(); 109 | continue; 110 | } 111 | cur_token += ::tolower(c); 112 | } 113 | 114 | if (cur_token.length() && XInputButtonMap.count(cur_token)) 115 | retval |= XInputButtonMap.at(cur_token); 116 | 117 | return retval; 118 | } 119 | 120 | class CDSwitcher : public Hook 121 | { 122 | constexpr static int SongTitleDisplaySeconds = 2; 123 | constexpr static int SongTitleDisplayFrames = SongTitleDisplaySeconds * 60; 124 | constexpr static float SongTitleFadeBeginSeconds = 0.75; 125 | constexpr static int SongTitleFadeBeginFrame = int(SongTitleFadeBeginSeconds * 60.f); 126 | 127 | inline static int SongTitleDisplayTimer = 0; 128 | inline static bool PrevKeyStatePrev = false; 129 | inline static bool PrevKeyStateNext = false; 130 | 131 | inline static uint32_t PadButtonCombo_Next = XINPUT_GAMEPAD_BACK; 132 | inline static uint32_t PadButtonCombo_Prev = XINPUT_GAMEPAD_RIGHT_THUMB | XINPUT_GAMEPAD_BACK; 133 | inline static uint32_t PadButtonCombo_Next_BitCount = 0; 134 | inline static uint32_t PadButtonCombo_Prev_BitCount = 0; 135 | 136 | inline static SafetyHookInline Game_Ctrl = {}; 137 | static void destination() 138 | { 139 | Game_Ctrl.call(); 140 | 141 | bool PadStateNext = Input::PadReleased(PadButtonCombo_Next); 142 | bool PadStatePrev = Input::PadReleased(PadButtonCombo_Prev); 143 | if (PadStateNext && PadStatePrev) 144 | { 145 | // Whichever combination has the most number of buttons takes precedence 146 | // (else there could be issues if one binding is a subset of another one) 147 | if (PadButtonCombo_Prev_BitCount > PadButtonCombo_Next_BitCount) 148 | PadStateNext = false; 149 | else 150 | PadStatePrev = false; 151 | } 152 | 153 | bool KeyStateNext = ((GetAsyncKeyState('X') & 1) || PadStateNext); 154 | bool KeyStatePrev = ((GetAsyncKeyState('Z') & 1) || PadStatePrev); 155 | 156 | bool BGMChanged = false; 157 | 158 | if (KeyStatePrev != PrevKeyStatePrev) 159 | { 160 | PrevKeyStatePrev = KeyStatePrev; 161 | if (KeyStatePrev) 162 | { 163 | *Game::sel_bgm_kind_buf = *Game::sel_bgm_kind_buf - 1; 164 | if (*Game::sel_bgm_kind_buf < 0) 165 | *Game::sel_bgm_kind_buf = Settings::CDTracks.size() - 1; 166 | BGMChanged = true; 167 | } 168 | } 169 | if (KeyStateNext != PrevKeyStateNext) 170 | { 171 | PrevKeyStateNext = KeyStateNext; 172 | if (KeyStateNext) 173 | { 174 | *Game::sel_bgm_kind_buf = *Game::sel_bgm_kind_buf + 1; 175 | if (*Game::sel_bgm_kind_buf >= Settings::CDTracks.size()) 176 | *Game::sel_bgm_kind_buf = 0; 177 | BGMChanged = true; 178 | } 179 | } 180 | 181 | if (BGMChanged) 182 | { 183 | BGMOverridePath = Settings::CDTracks[*Game::sel_bgm_kind_buf].first; 184 | Game::adxPlay(0, 0, 0); 185 | 186 | SongTitleDisplayTimer = SongTitleDisplayFrames; 187 | } 188 | } 189 | 190 | static void __cdecl PettyAutosceneCmdTblAnalysis_adxPlay_dest(int a1, uint32_t bgmIdx, int a3) 191 | { 192 | if (!Settings::CDTracks.size()) 193 | { 194 | Game::adxPlay(a1, bgmIdx, a3); 195 | return; 196 | } 197 | 198 | if (bgmIdx >= Settings::CDTracks.size()) 199 | bgmIdx = 0; 200 | 201 | BGMOverridePath = Settings::CDTracks[bgmIdx].first; 202 | Game::adxPlay(0, 0, 0); 203 | } 204 | 205 | public: 206 | static void draw(int numUpdates) 207 | { 208 | if (SongTitleDisplayTimer > 0 && *Game::sel_bgm_kind_buf < Settings::CDTracks.size()) 209 | { 210 | Game::sprSetFontPriority(4); 211 | Game::sprSetPrintFont(Settings::CDSwitcherTitleFont); 212 | Game::sprSetFontScale(Settings::CDSwitcherTitleFontSizeX, Settings::CDSwitcherTitleFontSizeY); 213 | Game::sprLocateP(Settings::CDSwitcherTitlePositionX, Settings::CDSwitcherTitlePositionY); 214 | 215 | uint32_t color = 0xFFFFFF; 216 | if (SongTitleDisplayTimer > SongTitleFadeBeginFrame) 217 | color |= 0xFF000000; 218 | else 219 | { 220 | uint8_t alpha = uint8_t((float(SongTitleDisplayTimer) / SongTitleFadeBeginFrame) * 255.f); 221 | color |= (alpha << 24); 222 | } 223 | Game::sprSetFontColor(color); 224 | 225 | const auto& song = Settings::CDTracks[*Game::sel_bgm_kind_buf].second; 226 | Game::sprPrintf("#%02d. %s", (*Game::sel_bgm_kind_buf) + 1, song.c_str()); 227 | 228 | SongTitleDisplayTimer -= numUpdates; 229 | if (SongTitleDisplayTimer <= 0) 230 | SongTitleDisplayTimer = 0; 231 | } 232 | } 233 | 234 | std::string_view description() override 235 | { 236 | return "CDSwitcher"; 237 | } 238 | 239 | bool validate() override 240 | { 241 | return Settings::CDSwitcherEnable; 242 | } 243 | 244 | bool apply() override 245 | { 246 | constexpr int Game_Ctrl_Addr = 0x9C840; 247 | constexpr int PettyAutosceneCmdTblAnalysis_adxPlay_CallAddr1 = 0x8687F; 248 | constexpr int PettyAutosceneCmdTblAnalysis_adxPlay_CallAddr2 = 0x868D3; 249 | 250 | PadButtonCombo_Next = ParseButtonCombination(Settings::CDSwitcherTrackNext); 251 | PadButtonCombo_Prev = ParseButtonCombination(Settings::CDSwitcherTrackPrevious); 252 | PadButtonCombo_Next_BitCount = Util::BitCount(PadButtonCombo_Next); 253 | PadButtonCombo_Prev_BitCount = Util::BitCount(PadButtonCombo_Prev); 254 | 255 | Memory::VP::InjectHook(Module::exe_ptr(PettyAutosceneCmdTblAnalysis_adxPlay_CallAddr1), PettyAutosceneCmdTblAnalysis_adxPlay_dest, Memory::HookType::Call); 256 | Memory::VP::InjectHook(Module::exe_ptr(PettyAutosceneCmdTblAnalysis_adxPlay_CallAddr2), PettyAutosceneCmdTblAnalysis_adxPlay_dest, Memory::HookType::Call); 257 | 258 | Game_Ctrl = safetyhook::create_inline(Module::exe_ptr(Game_Ctrl_Addr), destination); 259 | return !!Game_Ctrl; 260 | } 261 | 262 | static CDSwitcher instance; 263 | }; 264 | CDSwitcher CDSwitcher::instance; 265 | 266 | void AudioHooks_Update(int numUpdates) 267 | { 268 | if (numUpdates > 0 && Settings::AllowHorn) 269 | { 270 | static bool hornActive = false; 271 | if (Game::SwitchNow(0x10) && // Check if horn button is pressed 272 | *Game::current_mode == GameState::STATE_GAME) 273 | { 274 | Game::SetSndQueue(0x1BC | SND_LOOP); 275 | hornActive = true; 276 | } 277 | else 278 | { 279 | if (hornActive) 280 | { 281 | Game::SetSndQueue(0x1BC | SND_STOP); 282 | hornActive = false; 283 | } 284 | } 285 | } 286 | 287 | if (Settings::CDSwitcherDisplayTitle) 288 | CDSwitcher::draw(numUpdates); 289 | } 290 | 291 | void CDSwitcher_ReadIni(const std::filesystem::path& iniPath) 292 | { 293 | if (!std::filesystem::exists(iniPath)) 294 | { 295 | // TODO: fill in defaults if no INI found? for now we'll just disable switcher 296 | Settings::CDSwitcherEnable = false; 297 | } 298 | 299 | std::vector> iniTracks; 300 | 301 | std::ifstream file(iniPath); 302 | if (file && file.is_open()) 303 | { 304 | std::string line; 305 | bool inCDTracksSection = false; 306 | 307 | while (std::getline(file, line)) 308 | { 309 | if (inCDTracksSection) 310 | { 311 | if (line.empty()) 312 | continue; 313 | line = Util::trim(line); 314 | if (line.front() == '[' && line.back() == ']') // Reached a new section 315 | break; 316 | if (line.front() == '#' || line.front() == ';') 317 | continue; 318 | 319 | auto delimiterPos = line.find('='); 320 | if (delimiterPos != std::string::npos) 321 | { 322 | std::string path = Util::trim(line.substr(0, delimiterPos)); 323 | std::string name = Util::trim(line.substr(delimiterPos + 1)); 324 | iniTracks.emplace_back(path, name); 325 | spdlog::info(" - CDTracks: Added track {} ({})", name, path); 326 | 327 | // Check both paths that the CDSwitcher code looks in 328 | if (!std::filesystem::exists(path) && !std::filesystem::exists(".\\Sound\\" + path)) 329 | spdlog::warn("^ File \"{}\" not found for track, likely won't play properly in-game!", path); 330 | } 331 | } 332 | else if (line == "[CDTracks]") 333 | { 334 | inCDTracksSection = true; 335 | } 336 | } 337 | 338 | file.close(); 339 | } 340 | 341 | if (iniTracks.size() <= 0) 342 | return; 343 | 344 | if (Settings::CDSwitcherShuffleTracks) 345 | { 346 | std::random_device rd; 347 | std::mt19937 g(rd()); 348 | 349 | // Use std::shuffle to shuffle the vector 350 | std::shuffle(iniTracks.begin(), iniTracks.end(), g); 351 | } 352 | 353 | // replace old tracklist with the one from this INI 354 | // (if user had specified tracks in user.ini they likely meant to replace the defaults) 355 | Settings::CDTracks = iniTracks; 356 | } 357 | -------------------------------------------------------------------------------- /src/hooks_exceptions.cpp: -------------------------------------------------------------------------------- 1 | #define WIN32_LEAN_AND_MEAN 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "hook_mgr.hpp" 9 | #include "plugin.hpp" 10 | 11 | // miniz unfortunately doesn't include wchar versions of its functions, but fortunately does allow passing FILE* to it 12 | mz_bool mz_zip_writer_add_file(mz_zip_archive* pZip, const char* pArchive_name, const wchar_t* pSrc_filename, const void* pComment, mz_uint16 comment_size, mz_uint level_and_flags) 13 | { 14 | MZ_FILE* pSrc_file = NULL; 15 | mz_uint64 uncomp_size = 0; 16 | MZ_TIME_T file_modified_time; 17 | MZ_TIME_T* pFile_time = NULL; 18 | mz_bool status; 19 | 20 | memset(&file_modified_time, 0, sizeof(file_modified_time)); 21 | 22 | _wfopen_s(&pSrc_file, pSrc_filename, L"rb"); 23 | if (!pSrc_file) 24 | return false; 25 | 26 | _fseeki64(pSrc_file, 0, SEEK_END); 27 | uncomp_size = _ftelli64(pSrc_file); 28 | _fseeki64(pSrc_file, 0, SEEK_SET); 29 | 30 | status = mz_zip_writer_add_cfile(pZip, pArchive_name, pSrc_file, uncomp_size, pFile_time, pComment, comment_size, level_and_flags, NULL, 0, NULL, 0); 31 | 32 | fclose(pSrc_file); 33 | 34 | return status; 35 | } 36 | 37 | LONG WINAPI CustomUnhandledExceptionFilter(LPEXCEPTION_POINTERS ExceptionInfo) 38 | { 39 | wchar_t modulename[MAX_PATH]; 40 | wchar_t dump_filename[MAX_PATH]; 41 | wchar_t crash_log_filename[MAX_PATH]; 42 | wchar_t re4t_log_filename[MAX_PATH]; 43 | wchar_t save_filename[MAX_PATH]; 44 | wchar_t zip_filename[MAX_PATH]; 45 | wchar_t timestamp[128]; 46 | wchar_t* modulenameptr{}; 47 | bool bDumpSuccess; 48 | __time64_t time; 49 | struct tm ltime; 50 | HWND hWnd; 51 | HANDLE hFile; 52 | 53 | // Write minidump 54 | if (GetModuleFileNameW(GetModuleHandle(NULL), modulename, _countof(modulename)) != 0) 55 | { 56 | modulenameptr = wcsrchr(modulename, '\\'); 57 | *modulenameptr = L'\0'; 58 | modulenameptr += 1; 59 | } 60 | else 61 | { 62 | modulenameptr = (wchar_t*)L"err.err"; 63 | } 64 | 65 | _time64(&time); 66 | _localtime64_s(<ime, &time); 67 | wcsftime(timestamp, _countof(timestamp), L"%Y%m%d%H%M%S", <ime); 68 | swprintf_s(dump_filename, L"%s\\%s\\%s.%s.dmp", modulename, L"CrashDumps", modulenameptr, timestamp); 69 | 70 | hFile = CreateFileW(dump_filename, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); 71 | 72 | if (hFile != INVALID_HANDLE_VALUE) 73 | { 74 | MINIDUMP_EXCEPTION_INFORMATION ex; 75 | memset(&ex, 0, sizeof(ex)); 76 | ex.ThreadId = GetCurrentThreadId(); 77 | ex.ExceptionPointers = ExceptionInfo; 78 | ex.ClientPointers = TRUE; 79 | 80 | if (!(MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, MiniDumpWithDataSegs, &ex, NULL, NULL))) 81 | bDumpSuccess = false; 82 | 83 | CloseHandle(hFile); 84 | } 85 | 86 | // Logs exception into buffer and writes to file 87 | swprintf_s(crash_log_filename, L"%s\\%s\\%s.%s.log", modulename, L"CrashDumps", modulenameptr, timestamp); 88 | hFile = CreateFileW(crash_log_filename, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); 89 | 90 | if (hFile != INVALID_HANDLE_VALUE) 91 | { 92 | auto Log = [ExceptionInfo, hFile](char* buffer, size_t size, bool reg, bool stack, bool trace) 93 | { 94 | if (LogException(buffer, size, (LPEXCEPTION_POINTERS)ExceptionInfo, reg, stack, trace)) 95 | { 96 | // Write log file 97 | DWORD NumberOfBytesWritten = 0; 98 | WriteFile(hFile, buffer, strlen(buffer), &NumberOfBytesWritten, NULL); 99 | } 100 | }; 101 | 102 | // Try to make a very descriptive exception, for that we need to malloc a huge buffer... 103 | if (auto buffer = (char*)malloc(max_logsize_ever)) 104 | { 105 | Log(buffer, max_logsize_ever, true, true, true); 106 | free(buffer); 107 | } 108 | else 109 | { 110 | // Use a static buffer, no need for any allocation 111 | static const auto size = max_logsize_basic + max_logsize_regs + max_logsize_stackdump; 112 | static char static_buf[size]; 113 | static_assert(size <= max_static_buffer, "Static buffer is too big"); 114 | 115 | Log(buffer = static_buf, sizeof(static_buf), true, true, false); 116 | } 117 | CloseHandle(hFile); 118 | } 119 | 120 | // Copy re4_tweaks log file to CrashDumps 121 | { 122 | swprintf_s(re4t_log_filename, L"%s\\%s\\OutRun2006Tweaks.log", modulename, L"CrashDumps"); 123 | 124 | if (std::filesystem::exists(Module::LogPath)) 125 | std::filesystem::copy_file(Module::LogPath, re4t_log_filename); 126 | } 127 | 128 | // ZIP up the dump/log/save 129 | { 130 | swprintf_s(zip_filename, L"%s\\%s\\%s.%s.zip", modulename, L"CrashDumps", modulenameptr, timestamp); 131 | 132 | bool zip_created = false; 133 | 134 | FILE* zip_file; 135 | if (_wfopen_s(&zip_file, zip_filename, L"wb") == 0 && zip_file) 136 | { 137 | mz_zip_archive zip_archive; 138 | mz_zip_zero_struct(&zip_archive); 139 | if (mz_zip_writer_init_cfile(&zip_archive, zip_file, 3)) 140 | { 141 | // Even if one of these fails we want all to attempt being added, so don't think we can do any assignment trick here... 142 | zip_created = true; 143 | 144 | if (!mz_zip_writer_add_file(&zip_archive, "dump.dmp", dump_filename, nullptr, 0, 3)) 145 | zip_created = false; 146 | if (!mz_zip_writer_add_file(&zip_archive, "crash.log", crash_log_filename, nullptr, 0, 3)) 147 | zip_created = false; 148 | if (!mz_zip_writer_add_file(&zip_archive, "OutRun2006Tweaks.log", re4t_log_filename, nullptr, 0, 3)) 149 | zip_created = false; 150 | 151 | mz_zip_writer_finalize_archive(&zip_archive); 152 | mz_zip_writer_end(&zip_archive); 153 | } 154 | fclose(zip_file); 155 | } 156 | 157 | if (zip_created) 158 | { 159 | DeleteFileW(dump_filename); 160 | DeleteFileW(crash_log_filename); 161 | DeleteFileW(re4t_log_filename); 162 | } 163 | } 164 | 165 | // Exit the application 166 | wchar_t error[1024]; 167 | swprintf_s(error, L"Fatal error (0x%08X) at 0x%08X.\n\nA crash log has been saved to \"%s\".", (int)ExceptionInfo->ExceptionRecord->ExceptionCode, (int)ExceptionInfo->ExceptionRecord->ExceptionAddress, zip_filename); 168 | MessageBoxW(NULL, error, L"OutRun2006Tweaks", MB_ICONERROR | MB_OK); 169 | 170 | ShowCursor(TRUE); 171 | hWnd = FindWindowW(0, L""); 172 | SetForegroundWindow(hWnd); 173 | 174 | return EXCEPTION_CONTINUE_SEARCH; 175 | } 176 | 177 | void InitExceptionHandler() 178 | { 179 | std::filesystem::path dumpPath = Module::ExePath.parent_path() / L"CrashDumps"; 180 | 181 | if (!std::filesystem::exists(dumpPath)) 182 | std::filesystem::create_directories(dumpPath); 183 | 184 | SetUnhandledExceptionFilter(CustomUnhandledExceptionFilter); 185 | 186 | // Now stub out SetUnhandledExceptionFilter so NO ONE ELSE can set it! 187 | Memory::VP::Patch(&SetUnhandledExceptionFilter, { 0xC2, 0x04, 0x00 }); 188 | } 189 | -------------------------------------------------------------------------------- /src/hooks_flac.cpp: -------------------------------------------------------------------------------- 1 | #include "hook_mgr.hpp" 2 | #include "plugin.hpp" 3 | #include "game_addrs.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // CWaveFile class used in C2C, seems based on DirectX DXUTsound.cpp code 10 | class CWaveFile 11 | { 12 | public: 13 | virtual ~CWaveFile() = 0; 14 | virtual HRESULT Open(LPSTR strFileName, WAVEFORMATEX* a3, DWORD dwFlags) = 0; 15 | virtual HRESULT OpenFromMemory(BYTE* pbData, ULONG ulDataSize, WAVEFORMATEX* pwfx, DWORD dwFlags) = 0; 16 | virtual HRESULT Close() = 0; 17 | virtual HRESULT Read(BYTE* pBuffer, DWORD dwSizeToRead, DWORD* pdwSizeRead) = 0; 18 | virtual HRESULT Write(UINT nSizeToWrite, BYTE* pbSrcData, UINT* pnSizeWrote) = 0; 19 | virtual int GetSize() = 0; 20 | virtual HRESULT ResetFile() = 0; 21 | 22 | WAVEFORMATEX* m_pwfx_4; 23 | DWORD m_dwSize_8; 24 | HMMIO m_hmmio_C; 25 | MMCKINFO m_ck_10; 26 | MMCKINFO m_ckRiff_24; 27 | MMIOINFO m_mmioinfoOut_38; 28 | DWORD m_dwFlags_80; 29 | BOOL m_bIsReadingFromMemory_84; 30 | BYTE* m_pbData_88; 31 | BYTE* m_pbDataCur_8C; 32 | ULONG m_ulDataSize_90; 33 | CHAR* m_pResourceBuffer_94; 34 | }; 35 | 36 | CWaveFile::~CWaveFile() {} 37 | 38 | class CFLACFile : public CWaveFile 39 | { 40 | public: 41 | CFLACFile(); 42 | ~CFLACFile(); 43 | HRESULT Open(LPSTR strFileName, WAVEFORMATEX* a3, DWORD dwFlags); 44 | HRESULT OpenFromMemory(BYTE* pbData, ULONG ulDataSize, WAVEFORMATEX* pwfx, DWORD dwFlags); 45 | HRESULT Close(); 46 | HRESULT Read(BYTE* pBuffer, DWORD dwSizeToRead, DWORD* pdwSizeRead); 47 | HRESULT Write(UINT nSizeToWrite, BYTE* pbSrcData, UINT* pnSizeWrote); 48 | int GetSize(); 49 | HRESULT ResetFile(); 50 | 51 | private: 52 | FLAC__StreamDecoder* m_pDecoder; 53 | std::vector m_pDecodedData; 54 | DWORD m_dwDecodedDataSize; 55 | DWORD m_dwCurrentPosition; 56 | 57 | FLAC__uint64 m_currentSample; 58 | 59 | bool m_loopEnabled; 60 | FLAC__uint64 m_loopStart; 61 | FLAC__uint64 m_loopEnd; 62 | 63 | // FLAC callbacks 64 | static FLAC__StreamDecoderWriteStatus WriteCallback(const FLAC__StreamDecoder* decoder, const FLAC__Frame* frame, const FLAC__int32* const buffer[], void* client_data); 65 | static void MetadataCallback(const FLAC__StreamDecoder* decoder, const FLAC__StreamMetadata* metadata, void* client_data); 66 | static void ErrorCallback(const FLAC__StreamDecoder* decoder, FLAC__StreamDecoderErrorStatus status, void* client_data); 67 | 68 | // Helper functions 69 | HRESULT DecodeFile(); 70 | void EnsureBufferSize(size_t requiredSize); 71 | bool SeekToSample(FLAC__uint64 sample); 72 | }; 73 | 74 | CFLACFile::CFLACFile() : m_pDecoder(NULL), m_dwDecodedDataSize(0), m_dwCurrentPosition(0), m_loopEnabled(false), m_currentSample(0) 75 | { 76 | } 77 | 78 | CFLACFile::~CFLACFile() 79 | { 80 | Close(); 81 | } 82 | 83 | HRESULT CFLACFile::Open(LPSTR strFileName, WAVEFORMATEX* pwfx, DWORD dwFlags) 84 | { 85 | // Initialize FLAC decoder 86 | m_pDecoder = FLAC__stream_decoder_new(); 87 | if (m_pDecoder == NULL) 88 | return E_FAIL; 89 | 90 | // Set up callbacks 91 | FLAC__stream_decoder_set_metadata_ignore_all(m_pDecoder); 92 | FLAC__stream_decoder_set_metadata_respond(m_pDecoder, FLAC__METADATA_TYPE_STREAMINFO); 93 | FLAC__stream_decoder_set_metadata_respond(m_pDecoder, FLAC__METADATA_TYPE_VORBIS_COMMENT); 94 | FLAC__stream_decoder_init_file(m_pDecoder, strFileName, WriteCallback, MetadataCallback, ErrorCallback, this); 95 | 96 | // Decode the entire file 97 | return DecodeFile(); 98 | } 99 | 100 | HRESULT CFLACFile::OpenFromMemory(BYTE* pbData, ULONG ulDataSize, WAVEFORMATEX* pwfx, DWORD dwFlags) 101 | { 102 | OutputDebugString("CFLACFile::OpenFromMemory not implementamundo"); 103 | return 0; 104 | } 105 | 106 | HRESULT CFLACFile::Read(BYTE* pBuffer, DWORD dwSizeToRead, DWORD* pdwSizeRead) 107 | { 108 | if (!m_pDecodedData.size()) 109 | return E_FAIL; 110 | 111 | DWORD dwRead = 0; 112 | DWORD dwRemaining = dwSizeToRead; 113 | 114 | while (dwRemaining > 0) 115 | { 116 | // Check if we've reached the loop end 117 | if (m_loopEnabled && m_currentSample >= m_loopEnd) 118 | { 119 | // Seek back to loop start 120 | if (!SeekToSample(m_loopStart)) 121 | break; 122 | } 123 | 124 | DWORD numRead = dwRemaining; 125 | if (m_loopEnabled) 126 | { 127 | FLAC__uint64 samplesUntilLoopEnd = m_loopEnd - m_currentSample; 128 | DWORD bytesUntilLoopEnd = static_cast(samplesUntilLoopEnd * m_pwfx_4->nBlockAlign); 129 | numRead = min(bytesUntilLoopEnd, numRead); 130 | } 131 | 132 | while (m_dwCurrentPosition + numRead > m_dwDecodedDataSize) 133 | { 134 | if (FLAC__stream_decoder_get_state(m_pDecoder) == FLAC__STREAM_DECODER_END_OF_STREAM) 135 | break; 136 | 137 | // Try to read two frames at a time, hopefully reduce any skipping... 138 | if (!FLAC__stream_decoder_process_single(m_pDecoder)) 139 | break; 140 | 141 | if (!FLAC__stream_decoder_process_single(m_pDecoder)) 142 | break; 143 | } 144 | 145 | DWORD dwAvailable = m_dwDecodedDataSize - m_dwCurrentPosition; 146 | DWORD dwToRead = min(dwAvailable, numRead); 147 | if (dwToRead == 0) 148 | break; 149 | 150 | memcpy(pBuffer + dwRead, m_pDecodedData.data() + m_dwCurrentPosition, dwToRead); 151 | m_dwCurrentPosition += dwToRead; 152 | m_currentSample += dwToRead / m_pwfx_4->nBlockAlign; 153 | dwRead += dwToRead; 154 | dwRemaining -= dwToRead; 155 | } 156 | 157 | if (pdwSizeRead) 158 | *pdwSizeRead = dwRead; 159 | 160 | return S_OK; 161 | } 162 | 163 | bool CFLACFile::SeekToSample(FLAC__uint64 sample) 164 | { 165 | m_currentSample = sample; 166 | m_dwCurrentPosition = static_cast(sample * m_pwfx_4->nBlockAlign); 167 | return true; 168 | } 169 | 170 | HRESULT CFLACFile::Write(UINT nSizeToWrite, BYTE* pbSrcData, UINT* pnSizeWrote) 171 | { 172 | OutputDebugString("Negatory on the CFLACFile::Write"); 173 | return 0; 174 | } 175 | 176 | int CFLACFile::GetSize() 177 | { 178 | return this->m_dwSize_8; 179 | } 180 | 181 | HRESULT CFLACFile::Close() 182 | { 183 | if (m_pDecoder) 184 | { 185 | FLAC__stream_decoder_finish(m_pDecoder); 186 | FLAC__stream_decoder_delete(m_pDecoder); 187 | m_pDecoder = NULL; 188 | } 189 | 190 | m_pDecodedData.clear(); 191 | m_dwDecodedDataSize = 0; 192 | m_dwCurrentPosition = 0; 193 | 194 | return S_OK; 195 | } 196 | 197 | HRESULT CFLACFile::ResetFile() 198 | { 199 | m_dwCurrentPosition = 0; 200 | return S_OK; 201 | } 202 | 203 | HRESULT CFLACFile::DecodeFile() 204 | { 205 | if (!FLAC__stream_decoder_process_until_end_of_metadata(m_pDecoder)) 206 | return E_FAIL; 207 | 208 | if (!FLAC__stream_decoder_process_single(m_pDecoder)) 209 | return E_FAIL; 210 | 211 | return S_OK; 212 | } 213 | 214 | void CFLACFile::EnsureBufferSize(size_t requiredSize) 215 | { 216 | if (m_pDecodedData.size() < requiredSize) 217 | { 218 | size_t newCapacity = m_pDecodedData.size(); 219 | if (!newCapacity) 220 | newCapacity = requiredSize; 221 | while (newCapacity < requiredSize) 222 | newCapacity *= 2; // Double the capacity 223 | m_pDecodedData.resize(newCapacity); 224 | } 225 | } 226 | 227 | FLAC__StreamDecoderWriteStatus CFLACFile::WriteCallback(const FLAC__StreamDecoder* decoder, const FLAC__Frame* frame, const FLAC__int32* const buffer[], void* client_data) 228 | { 229 | CFLACFile* pThis = static_cast(client_data); 230 | 231 | const unsigned int bps = frame->header.bits_per_sample; 232 | const DWORD totalSamples = frame->header.blocksize * frame->header.channels; 233 | const DWORD bytesPerSample = bps / 8; 234 | const DWORD totalBytes = totalSamples * bytesPerSample; 235 | 236 | // Ensure buffer is large enough 237 | size_t requiredSize = pThis->m_dwDecodedDataSize + totalBytes; 238 | pThis->EnsureBufferSize(requiredSize); 239 | 240 | // Append decoded FLAC samples to m_pDecodedData 241 | unsigned char* outBuffer = pThis->m_pDecodedData.data() + pThis->m_dwDecodedDataSize; 242 | 243 | for (unsigned int i = 0; i < frame->header.blocksize; i++) 244 | { 245 | for (unsigned int channel = 0; channel < frame->header.channels; channel++) 246 | { 247 | FLAC__int32 sample = buffer[channel][i]; 248 | 249 | // Handle different bit depths 250 | switch (bps) { 251 | case 16: 252 | { 253 | INT16 pcm = static_cast(max(-32768, min(32767, sample))); 254 | *(INT16*)outBuffer = pcm; 255 | outBuffer += sizeof(INT16); 256 | } 257 | break; 258 | case 24: 259 | { 260 | outBuffer[0] = (sample & 0xFF); 261 | outBuffer[1] = ((sample >> 8) & 0xFF); 262 | outBuffer[2] = ((sample >> 16) & 0xFF); 263 | outBuffer += 3; 264 | } 265 | break; 266 | case 32: 267 | *(FLAC__int32*)outBuffer = sample; 268 | outBuffer += sizeof(FLAC__int32); 269 | break; 270 | default: 271 | // Unsupported bit depth 272 | return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; 273 | } 274 | } 275 | } 276 | 277 | pThis->m_dwDecodedDataSize += totalBytes; 278 | 279 | return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; 280 | } 281 | 282 | void CFLACFile::MetadataCallback(const FLAC__StreamDecoder* decoder, const FLAC__StreamMetadata* metadata, void* client_data) 283 | { 284 | CFLACFile* pThis = static_cast(client_data); 285 | 286 | if (metadata->type == FLAC__METADATA_TYPE_STREAMINFO) 287 | { 288 | // Fill in WAVEFORMATEX structure 289 | pThis->m_pwfx_4 = new WAVEFORMATEX; 290 | pThis->m_pwfx_4->wFormatTag = WAVE_FORMAT_PCM; 291 | pThis->m_pwfx_4->nChannels = metadata->data.stream_info.channels; 292 | pThis->m_pwfx_4->nSamplesPerSec = metadata->data.stream_info.sample_rate; 293 | pThis->m_pwfx_4->wBitsPerSample = metadata->data.stream_info.bits_per_sample; 294 | pThis->m_pwfx_4->nBlockAlign = (pThis->m_pwfx_4->nChannels * pThis->m_pwfx_4->wBitsPerSample) / 8; 295 | pThis->m_pwfx_4->nAvgBytesPerSec = pThis->m_pwfx_4->nSamplesPerSec * pThis->m_pwfx_4->nBlockAlign; 296 | pThis->m_pwfx_4->cbSize = 0; 297 | 298 | // Pre-allocate buffer for decoded data 299 | FLAC__uint64 totalSamples = metadata->data.stream_info.total_samples; 300 | unsigned channels = metadata->data.stream_info.channels; 301 | unsigned bitsPerSample = metadata->data.stream_info.bits_per_sample; 302 | 303 | // Calculate total buffer size in bytes 304 | size_t totalBufferSize = totalSamples * channels * (bitsPerSample / 8); 305 | 306 | // Pre-allocate the buffer 307 | pThis->m_pDecodedData.reserve(totalBufferSize); 308 | } 309 | else if (metadata->type == FLAC__METADATA_TYPE_VORBIS_COMMENT) 310 | { 311 | std::optional loop_start; 312 | std::optional loop_length; 313 | std::optional loop_end; 314 | for (unsigned int i = 0; i < metadata->data.vorbis_comment.num_comments; i++) 315 | { 316 | const char* tag = (const char*)metadata->data.vorbis_comment.comments[i].entry; 317 | unsigned int length = metadata->data.vorbis_comment.comments[i].length; 318 | spdlog::debug("FLAC comment: size {}, {}", length, tag); 319 | 320 | if (length > 10 && !_strnicmp(tag, "LOOPSTART=", 10)) 321 | loop_start = std::stoull(tag + 10, NULL, 10); 322 | else if (length > 11 && !_strnicmp(tag, "LOOPLENGTH=", 11)) 323 | loop_length = std::stoull(tag + 11, NULL, 10); 324 | else if (length > 8 && !_strnicmp(tag, "LOOPEND=", 8)) 325 | loop_end = std::stoull(tag + 8, NULL, 10); 326 | } 327 | 328 | if (loop_start.has_value() && loop_length.has_value()) 329 | loop_end = loop_start.value() + loop_length.value() - 1; 330 | 331 | if (loop_start.has_value() && loop_end.has_value()) 332 | { 333 | spdlog::info("FLAC loop: {} - {}", loop_start.value(), loop_end.value()); 334 | pThis->m_loopStart = loop_start.value(); 335 | pThis->m_loopEnd = loop_end.value(); 336 | pThis->m_loopEnabled = true; 337 | } 338 | } 339 | } 340 | 341 | void CFLACFile::ErrorCallback(const FLAC__StreamDecoder* decoder, FLAC__StreamDecoderErrorStatus status, void* client_data) 342 | { 343 | // Log the error 344 | OutputDebugString("FLAC decoding error: "); 345 | OutputDebugString(FLAC__StreamDecoderErrorStatusString[status]); 346 | OutputDebugString("\n"); 347 | } 348 | 349 | class AllowFLAC : public Hook 350 | { 351 | inline static SafetyHookMid hook = {}; 352 | static void destination(safetyhook::Context& ctx) 353 | { 354 | if (ctx.eax == 2) 355 | { 356 | CFLACFile* file = new CFLACFile(); 357 | ctx.eax = (uintptr_t)file; 358 | 359 | ctx.eip = 0x412008; // the code we hook heads toward end of function, move it back to the file loading part 360 | } 361 | } 362 | 363 | public: 364 | std::string_view description() override 365 | { 366 | return "AllowFLAC"; 367 | } 368 | 369 | bool validate() override 370 | { 371 | return Settings::AllowFLAC; 372 | } 373 | 374 | bool apply() override 375 | { 376 | hook = safetyhook::create_mid(Module::exe_ptr(0x120F4), destination); 377 | return !!hook; 378 | } 379 | 380 | static AllowFLAC instance; 381 | }; 382 | AllowFLAC AllowFLAC::instance; 383 | -------------------------------------------------------------------------------- /src/hooks_framerate.cpp: -------------------------------------------------------------------------------- 1 | #include "hook_mgr.hpp" 2 | #include "plugin.hpp" 3 | #include "game_addrs.hpp" 4 | #include "overlay/overlay.hpp" 5 | 6 | // from timeapi.h, which we can't include since our proxy timeBeginPeriod etc funcs will conflict... 7 | typedef struct timecaps_tag { 8 | UINT wPeriodMin; /* minimum period supported */ 9 | UINT wPeriodMax; /* maximum period supported */ 10 | } TIMECAPS; 11 | 12 | #include 13 | 14 | class Snooze 15 | { 16 | // Based on https://github.com/blat-blatnik/Snippets/blob/main/precise_sleep.c 17 | 18 | static inline HANDLE Timer; 19 | static inline int SchedulerPeriodMs; 20 | static inline INT64 QpcPerSecond; 21 | 22 | public: 23 | static void PreciseSleep(double seconds) 24 | { 25 | LARGE_INTEGER qpc; 26 | QueryPerformanceCounter(&qpc); 27 | INT64 targetQpc = (INT64)(qpc.QuadPart + seconds * QpcPerSecond); 28 | 29 | if (Timer) // Try using a high resolution timer first. 30 | { 31 | const double TOLERANCE = 0.001'02; 32 | INT64 maxTicks = (INT64)SchedulerPeriodMs * 9'500; 33 | for (;;) // Break sleep up into parts that are lower than scheduler period. 34 | { 35 | double remainingSeconds = (targetQpc - qpc.QuadPart) / (double)QpcPerSecond; 36 | INT64 sleepTicks = (INT64)((remainingSeconds - TOLERANCE) * 10'000'000); 37 | if (sleepTicks <= 0) 38 | break; 39 | 40 | LARGE_INTEGER due; 41 | due.QuadPart = -(sleepTicks > maxTicks ? maxTicks : sleepTicks); 42 | SetWaitableTimerEx(Timer, &due, 0, NULL, NULL, NULL, 0); 43 | WaitForSingleObject(Timer, INFINITE); 44 | QueryPerformanceCounter(&qpc); 45 | } 46 | } 47 | else // Fallback to Sleep. 48 | { 49 | const double TOLERANCE = 0.000'02; 50 | double sleepMs = (seconds - TOLERANCE) * 1000 - SchedulerPeriodMs; // Sleep for 1 scheduler period less than requested. 51 | int sleepSlices = (int)(sleepMs / SchedulerPeriodMs); 52 | if (sleepSlices > 0) 53 | Sleep((DWORD)sleepSlices * SchedulerPeriodMs); 54 | QueryPerformanceCounter(&qpc); 55 | } 56 | 57 | while (qpc.QuadPart < targetQpc) // Spin for any remaining time. 58 | { 59 | YieldProcessor(); 60 | QueryPerformanceCounter(&qpc); 61 | } 62 | } 63 | 64 | static void Init(void) 65 | { 66 | #ifndef PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION 67 | #define PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION 4 68 | #endif 69 | // Prevent timer resolution getting reset on Win11 70 | // https://stackoverflow.com/questions/77182958/windows-11-application-timing-becomes-uneven-when-backgrounded 71 | // (SPI call will silently fail on other OS) 72 | PROCESS_POWER_THROTTLING_STATE state = { 0 }; 73 | state.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION; 74 | state.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION; 75 | state.StateMask = 0; 76 | SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &state, sizeof(state)); 77 | 78 | typedef int(__stdcall* timeBeginPeriod_Fn) (int Period); 79 | typedef int(__stdcall* timeGetDevCaps_Fn) (TIMECAPS* ptc, UINT cbtc); 80 | 81 | auto winmm = LoadLibraryA("winmm.dll"); 82 | auto timeBeginPeriod = (timeBeginPeriod_Fn)GetProcAddress(winmm, "timeBeginPeriod"); 83 | auto timeGetDevCaps = (timeGetDevCaps_Fn)GetProcAddress(winmm, "timeGetDevCaps"); 84 | 85 | // Initialization 86 | Timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); 87 | TIMECAPS caps; 88 | timeGetDevCaps(&caps, sizeof caps); 89 | timeBeginPeriod(caps.wPeriodMin); 90 | SchedulerPeriodMs = (int)caps.wPeriodMin; 91 | LARGE_INTEGER qpf; 92 | QueryPerformanceFrequency(&qpf); 93 | QpcPerSecond = qpf.QuadPart; 94 | } 95 | }; 96 | 97 | class SumoUIFlashingTextFix : public Hook 98 | { 99 | static SumoUIFlashingTextFix instance; 100 | 101 | // Hacky fix for the flashing "Not Signed In" / "Signed In As" text when playing above 60FPS 102 | // Normally SumoFrontEndEvent_Ctrl calls into SumoFrontEnd_animate function, which then handles drawing the text 103 | // SumoFrontEndEvent_Ctrl is only ran at 60FPS however, and will skip frames when running above that 104 | // (If playing at 120FPS, 1/2 frames will skip calling EventControl, which will skip running SumoFrontEndEvent_Ctrl, and the text won't be drawn that frame) 105 | // 106 | // Unfortunately running SumoFrontEndEvent_Ctrl every frame makes the C2C UI speed up too 107 | // Instead this just removes the original SumoFrontEnd_animate caller, and handles calling that function ourselves every frame instead 108 | // 109 | // TODO: this fixes the Not Signed In text, but there are still other flashing Sumo menus that need a fix too (eg. showroom menus), maybe there's others too? 110 | public: 111 | static void draw() 112 | { 113 | if (!instance.active()) 114 | return; 115 | 116 | // Make sure sumo FE event is active... 117 | uint8_t* status = Module::exe_ptr(0x39FB48); 118 | if ((status[0x195] & 0x18) == 0 && (status[0x195] & 2) != 0) // 0x195 = EVENT_SUMOFE 119 | { 120 | uint8_t* frontend = (uint8_t*)Game::SumoFrontEnd_GetSingleton_4035F0(); 121 | // Check we're in the right state # 122 | if (frontend && *(int*)(frontend + 0x218) == 2) 123 | Game::SumoFrontEnd_animate_443110(frontend, 0); 124 | } 125 | } 126 | std::string_view description() override 127 | { 128 | return "SumoUIFlashingTextFix"; 129 | } 130 | 131 | bool validate() override 132 | { 133 | return (Settings::FramerateLimit == 0 || Settings::FramerateLimit > 60) && Settings::FramerateUnlockExperimental; 134 | } 135 | 136 | bool apply() override 137 | { 138 | 139 | constexpr int SumoFe_Animate_CallerAddr = 0x45C4E; 140 | Memory::VP::Nop(Module::exe_ptr(SumoFe_Animate_CallerAddr), 5); 141 | return true; 142 | } 143 | 144 | }; 145 | SumoUIFlashingTextFix SumoUIFlashingTextFix::instance; 146 | 147 | class ReplaceGameUpdateLoop : public Hook 148 | { 149 | inline static double FramelimiterFrequency = 0; 150 | inline static double FramelimiterPrevCounter = 0; 151 | 152 | inline static double FramelimiterDeviation = 0; 153 | 154 | inline static SafetyHookMid dest_hook = {}; 155 | static void destination(safetyhook::Context& ctx) 156 | { 157 | auto CurGameState = *Game::current_mode; 158 | 159 | // Skip framelimiter during load screens to help reduce load times 160 | bool skipFrameLimiter = Settings::FramerateLimit == 0; 161 | if (Settings::FramerateFastLoad > 0 && !skipFrameLimiter) 162 | { 163 | if (Settings::FramerateFastLoad != 3) 164 | { 165 | static bool isLoadScreenStarted = false; 166 | bool isLoadScreen = false; 167 | 168 | // Check if we're on load screen, if we are then disable framelimiter while game hasn't started (progress_code 65) 169 | if (CurGameState == STATE_START) 170 | { 171 | isLoadScreen = *Game::game_start_progress_code != 65; 172 | } 173 | 174 | skipFrameLimiter = isLoadScreen; 175 | 176 | // Toggle vsync if load screen state changed 177 | if (Settings::FramerateFastLoad > 1) 178 | { 179 | if (Game::D3DPresentParams->PresentationInterval != 0 && Game::D3DPresentParams->PresentationInterval != D3DPRESENT_INTERVAL_IMMEDIATE) 180 | { 181 | if (!isLoadScreenStarted && isLoadScreen) 182 | { 183 | auto NewParams = *Game::D3DPresentParams; 184 | NewParams.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; 185 | 186 | Game::Sumo_D3DResourcesRelease(); 187 | Game::D3DDevice()->Reset(&NewParams); 188 | Game::Sumo_D3DResourcesCreate(); 189 | 190 | isLoadScreenStarted = true; 191 | } 192 | 193 | if (isLoadScreenStarted && !isLoadScreen) 194 | { 195 | Game::Sumo_D3DResourcesRelease(); 196 | Game::D3DDevice()->Reset(Game::D3DPresentParams); 197 | Game::Sumo_D3DResourcesCreate(); 198 | 199 | isLoadScreenStarted = false; 200 | } 201 | } 202 | } 203 | } 204 | } 205 | 206 | if (!skipFrameLimiter) 207 | { 208 | // Framelimiter 209 | double timeElapsed = 0; 210 | double timeCurrent = 0; 211 | LARGE_INTEGER counter; 212 | 213 | double FramelimiterTargetFrametime = double(1000.f) / double(Settings::FramerateLimit); 214 | double FramelimiterMaxDeviation = FramelimiterTargetFrametime / (16.f * 1000.f); 215 | 216 | do 217 | { 218 | if (Settings::FramerateFastLoad == 3) 219 | Game::FileLoad_Ctrl(); 220 | 221 | QueryPerformanceCounter(&counter); 222 | timeCurrent = double(counter.QuadPart) / FramelimiterFrequency; 223 | timeElapsed = timeCurrent - FramelimiterPrevCounter; 224 | 225 | if (Settings::FramerateLimitMode == 0) // "efficient" mode 226 | { 227 | if (FramelimiterTargetFrametime + FramelimiterDeviation <= timeElapsed) 228 | { 229 | FramelimiterDeviation = 0; 230 | break; 231 | } 232 | else if ((FramelimiterTargetFrametime + FramelimiterDeviation) - timeElapsed > 2.0) 233 | Snooze::PreciseSleep(1.f / 1000.f); // Sleep for ~1ms 234 | else 235 | Sleep(0); // Yield thread's time-slice (does not actually sleep) 236 | } 237 | } while (FramelimiterTargetFrametime + FramelimiterDeviation > timeElapsed); 238 | 239 | QueryPerformanceCounter(&counter); 240 | timeCurrent = double(counter.QuadPart) / FramelimiterFrequency; 241 | timeElapsed = timeCurrent - FramelimiterPrevCounter; 242 | 243 | // Compensate for any deviation, in the next frame (based on dxvk util_fps_limiter) 244 | double deviation = timeElapsed - FramelimiterTargetFrametime; 245 | FramelimiterDeviation += deviation; 246 | // Limit the cumulative deviation 247 | FramelimiterDeviation = std::clamp(FramelimiterDeviation, -FramelimiterMaxDeviation, FramelimiterMaxDeviation); 248 | 249 | FramelimiterPrevCounter = timeCurrent; 250 | } 251 | else 252 | { 253 | // Framelimiter is disabled, make sure to call FileLoad_Ctrl if we're using fastload3 254 | if (Settings::FramerateFastLoad == 3) 255 | Game::FileLoad_Ctrl(); 256 | } 257 | 258 | Game::SetFrameStartCpuTime(); 259 | 260 | int numUpdates = Game::CalcNumUpdatesToRun(60); 261 | 262 | // Vanilla game would always reset numUpdates to 1 if it was 0 263 | // when running above 60FPS CalcNumUpdatesToRun would return 0 since game was running fast, for it to skip the current update, but game would still force it to update 264 | // if FrameUnlockExperimental is set then we'll allow 0 updates to run, allowing it to skip updating when game is running fast 265 | 266 | int minUpdates = Settings::FramerateUnlockExperimental ? 0 : 1; 267 | 268 | if (numUpdates < minUpdates) 269 | numUpdates = minUpdates; 270 | 271 | // need to call 43FA10 in order for "extend time" gfx to disappear 272 | Game::fn43FA10(numUpdates); 273 | 274 | // Increment power_on_timer based on numUpdates value, fixes above-60-fps animation issues such as water anims 275 | // This should be incremented at the end of the games main loop, but we don't have any hook near the end 276 | // Incrementing at the beginning before the main loop body should be equivalent 277 | *Game::power_on_timer = *Game::power_on_timer + numUpdates; 278 | 279 | AudioHooks_Update(numUpdates); 280 | 281 | if (numUpdates > 0) 282 | { 283 | // Reset vibration if we're not in main game state 284 | if (Settings::VibrationMode != 0 && CurGameState != GameState::STATE_GAME) 285 | SetVibration(Settings::VibrationControllerId, 0.0f, 0.0f); 286 | 287 | if (Settings::ControllerHotPlug) 288 | DInput_RegisterNewDevices(); 289 | } 290 | 291 | SumoUIFlashingTextFix::draw(); 292 | 293 | for (int curUpdateIdx = 0; curUpdateIdx < numUpdates; curUpdateIdx++) 294 | { 295 | // Fetch latest input state 296 | // (do this inside our update-loop so that any hooked game funcs have accurate state...) 297 | Input::Update(); 298 | 299 | if (!Overlay::IsActive || Overlay::IsBindingDialogActive) 300 | { 301 | void InputManager_Update(); 302 | InputManager_Update(); 303 | } 304 | 305 | if (!Overlay::IsActive) 306 | { 307 | Game::ReadIO(); 308 | } 309 | 310 | Game::SoundControl_mb(); 311 | Game::LinkControlReceive(); 312 | Game::ModeControl(); 313 | Game::EventControl(); 314 | Game::GhostCarExecServer(); 315 | Game::fn4666A0(); 316 | } 317 | } 318 | 319 | // Fixes animation rate of certain stage textures (beach waves / street lights...) 320 | // Vanilla game would add 1 to app_time every frame, new code will only add if a game tick is being ran on this frame 321 | // (as a bonus, this should also fix anim speed when running lower than 60FPS too) 322 | inline static SafetyHookInline SetTweeningTable = {}; 323 | static void SetTweeningTable_dest() 324 | { 325 | if (*Game::sprani_num_ticks > 0) 326 | { 327 | *Game::app_time += *Game::sprani_num_ticks; 328 | } 329 | } 330 | 331 | // EventDisplay adds 0.10471975 to sin_param every frame, if GetPauseFlag is false 332 | // This causes speed of flashing cars to change depending on framerate 333 | // We'll update it similar to SetTweeningTable so it only increments if a game tick is being ran 334 | // TODO: would probably be smoother to scale that 0.10471975 by deltatime instead 335 | inline static SafetyHookMid EventDisplay_midhook1 = {}; 336 | inline static SafetyHookMid EventDisplay_midhook2 = {}; 337 | inline static SafetyHookMid DispPlCar_midhook = {}; 338 | static void EventDisplay_dest(SafetyHookContext& ctx) 339 | { 340 | if (*Game::sprani_num_ticks == 0) 341 | ctx.eax = 1; // make func skip adding to sin_param 342 | } 343 | 344 | public: 345 | std::string_view description() override 346 | { 347 | return "ReplaceGameUpdateLoop"; 348 | } 349 | 350 | bool validate() override 351 | { 352 | return true; 353 | } 354 | 355 | bool apply() override 356 | { 357 | // framelimiter init 358 | { 359 | Snooze::Init(); 360 | 361 | LARGE_INTEGER frequency; 362 | LARGE_INTEGER counter; 363 | 364 | QueryPerformanceFrequency(&frequency); 365 | FramelimiterFrequency = double(frequency.QuadPart) / double(1000.f); 366 | QueryPerformanceCounter(&counter); 367 | FramelimiterPrevCounter = double(counter.QuadPart) / FramelimiterFrequency; 368 | } 369 | 370 | constexpr int HookAddr = 0x17C7B; 371 | constexpr int GameLoopFrameLimiterAddr = 0x17DD3; 372 | constexpr int GameLoopFileLoad_CtrlCaller = 0x17D8D; 373 | 374 | // disable broken framelimiter 375 | Memory::VP::Nop(Module::exe_ptr(GameLoopFrameLimiterAddr), 2); 376 | 377 | // replace game update loop with custom version 378 | Memory::VP::Nop(Module::exe_ptr(HookAddr), 0xA3); 379 | dest_hook = safetyhook::create_mid(Module::exe_ptr(HookAddr), destination); 380 | 381 | // disable power_on_timer increment so we can handle it 382 | Memory::VP::Nop(Module::exe_ptr(0x17D87), 6); 383 | 384 | // Disable FileLoad_Ctrl call, we'll handle it above ourselves 385 | if (Settings::FramerateFastLoad == 3) 386 | Memory::VP::Nop(Module::exe_ptr(GameLoopFileLoad_CtrlCaller), 5); 387 | 388 | if (Settings::FramerateUnlockExperimental) 389 | { 390 | constexpr int SetTweeningTable_Addr = 0xED60; 391 | SetTweeningTable = safetyhook::create_inline(Module::exe_ptr(SetTweeningTable_Addr), SetTweeningTable_dest); 392 | 393 | constexpr int EventDisplay_HookAddr1 = 0x3FC48; 394 | constexpr int EventDisplay_HookAddr2 = 0x3FE51; 395 | constexpr int DispPlCar_HookAddr = 0x6BE27; 396 | EventDisplay_midhook1 = safetyhook::create_mid(Module::exe_ptr(EventDisplay_HookAddr1), EventDisplay_dest); 397 | EventDisplay_midhook2 = safetyhook::create_mid(Module::exe_ptr(EventDisplay_HookAddr2), EventDisplay_dest); 398 | DispPlCar_midhook = safetyhook::create_mid(Module::exe_ptr(DispPlCar_HookAddr), EventDisplay_dest); 399 | } 400 | 401 | // Increase reflection update rate, default is 3 (30fps) 402 | // Set it to framerate limit div 10 (add 9 to make it round up to nearest 10) 403 | int numUpdates = (Settings::FramerateLimit + 9) / 10; 404 | if (numUpdates > 3) 405 | { 406 | constexpr int Envmap_RenderToCubeMap_PatchAddr = 0x1447E; 407 | Memory::VP::Nop(Module::exe_ptr(Envmap_RenderToCubeMap_PatchAddr), 2); 408 | 409 | constexpr int Envmap_RenderToCubeMap_PatchAddr2 = 0x14480 + 1; 410 | Memory::VP::Patch(Module::exe_ptr(Envmap_RenderToCubeMap_PatchAddr2), numUpdates); 411 | } 412 | 413 | return !!dest_hook; 414 | } 415 | 416 | static ReplaceGameUpdateLoop instance; 417 | }; 418 | ReplaceGameUpdateLoop ReplaceGameUpdateLoop::instance; 419 | 420 | class FullscreenRefreshRate : public Hook 421 | { 422 | 423 | public: 424 | std::string_view description() override 425 | { 426 | return "FullscreenRefreshRate"; 427 | } 428 | 429 | bool validate() override 430 | { 431 | return Settings::FramerateLimit != 60; 432 | } 433 | 434 | bool apply() override 435 | { 436 | constexpr int PatchAddr = 0xE9B9; 437 | Memory::VP::Patch(Module::exe_ptr(PatchAddr), Settings::FramerateLimit); 438 | 439 | return true; 440 | } 441 | 442 | static FullscreenRefreshRate instance; 443 | }; 444 | FullscreenRefreshRate FullscreenRefreshRate::instance; 445 | -------------------------------------------------------------------------------- /src/hooks_graphics.cpp: -------------------------------------------------------------------------------- 1 | #include "hook_mgr.hpp" 2 | #include "plugin.hpp" 3 | #include "game_addrs.hpp" 4 | #include 5 | #include 6 | 7 | class UseHiDefCharacters : public Hook 8 | { 9 | inline static const char ChrDrGh00_path[] = "Media\\CHR_DR_GH00.bin"; 10 | inline static const char ChrDrGh00_gamepath[] = "\\Media\\CHR_DR_GH00.bin"; 11 | inline static const char ChrDrGh00Usa_path[] = "Media\\CHR_DR_GH00_USA.bin"; 12 | inline static const char ChrDrGh00Usa_gamepath[] = "\\Media\\CHR_DR_GH00_USA.bin"; 13 | 14 | public: 15 | std::string_view description() override 16 | { 17 | return "UseHiDefCharacters"; 18 | } 19 | 20 | bool validate() override 21 | { 22 | return Settings::UseHiDefCharacters; 23 | } 24 | 25 | bool apply() override 26 | { 27 | // Switch Chr\CHR_DR_GH00*.bin usages to read from Media\CHR_DR_GH00*.bin instead, if they exist 28 | // (Chr\ versions are missing hair anims which Media\ versions fortunately include - Media\ versions are otherwise unused) 29 | { 30 | if (std::filesystem::exists(ChrDrGh00_path)) 31 | Game::chrset_info[ChrSet::CHR_DR_GH00].bin_ptr = ChrDrGh00_gamepath; 32 | if (std::filesystem::exists(ChrDrGh00Usa_path)) 33 | Game::chrset_info[ChrSet::CHR_DR_GH00_USA].bin_ptr = ChrDrGh00Usa_gamepath; 34 | } 35 | 36 | int* driver_chrsets = Module::exe_ptr(0x2549B0); 37 | int* heroine_chrsets = Module::exe_ptr(0x2549C8); 38 | 39 | // Switch Alberto CHR_DR_M00 -> CHR_DR_MH00 40 | driver_chrsets[0] = ChrSet::CHR_DR_MH00; 41 | Memory::VP::Patch(Module::exe_ptr(0x87F41 + 1), { uint8_t(ChrSet::CHR_DR_MH00) }); // O2SP 42 | 43 | // Switch Jennifer CHR_DR_L00 -> CHR_DR_LH00 44 | heroine_chrsets[3] = ChrSet::CHR_DR_LH00; 45 | Memory::VP::Patch(Module::exe_ptr(0x8803A + 1), { uint8_t(ChrSet::CHR_DR_LH00) }); // O2SP 46 | 47 | // Switch Clarissa CHR_DR_G00_* -> CHR_DR_GH00_* 48 | heroine_chrsets[4] = ChrSet::CHR_DR_GH00; // (game code handles switching heroine_chrsets to USA variant, so we don't need to check RestoreJPClarissa here) 49 | 50 | // Clarissa O2SP code (this is also patched by RestoreJPClarissa, so make sure both set it to same value...) 51 | Memory::VP::Patch(Module::exe_ptr(0x88044 + 1), { Settings::RestoreJPClarissa ? uint8_t(ChrSet::CHR_DR_GH00) : uint8_t(ChrSet::CHR_DR_GH00_USA) }); 52 | 53 | return true; 54 | } 55 | 56 | static UseHiDefCharacters instance; 57 | }; 58 | UseHiDefCharacters UseHiDefCharacters::instance; 59 | 60 | class RestoreCarBaseShadow : public Hook 61 | { 62 | static void __cdecl CalcPeraShadow(int a1, int a2, int a3, float a4) 63 | { 64 | // CalcPeraShadow code from C2C Xbox 65 | EVWORK_CAR* car = Game::pl_car(); 66 | 67 | Game::mxPushLoadMatrix(&car->matrix_B0); 68 | Game::mxTranslate(0.0f, 0.05f, 0.0f); 69 | 70 | // Xbox C2C would multiply a4 by 0.5, halving the opacity, which on PC made it almost invisible.. 71 | Game::DrawObjectAlpha_Internal(a1, a4 * Settings::CarBaseShadowOpacity, 0, -1); 72 | Game::mxPopMatrix(); 73 | } 74 | 75 | public: 76 | std::string_view description() override 77 | { 78 | return "RestoreCarBaseShadow"; 79 | } 80 | 81 | bool validate() override 82 | { 83 | return Settings::CarBaseShadowOpacity > 0; 84 | } 85 | 86 | bool apply() override 87 | { 88 | constexpr int DispCarModel_Common_HookAddr = 0x69EB4; 89 | constexpr int DispSelCarModel_HookAddr = 0x6AC76; // O2SP car selection 90 | constexpr int Sumo_DispSelCarModel_HookAddr = 0x6B766; // C2C car selection 91 | 92 | // These three funcs contain nullsub_1 calls which were CalcPeraShadow calls in O2SP / C2CXbox 93 | // We can't just hook nullsub_1 since a bunch of other nulled out code also calls it, instead we'll rewrite them to call our CalcPeraShadow func 94 | Memory::VP::InjectHook(Module::exe_ptr(DispCarModel_Common_HookAddr), CalcPeraShadow, Memory::HookType::Call); 95 | Memory::VP::InjectHook(Module::exe_ptr(DispSelCarModel_HookAddr), CalcPeraShadow, Memory::HookType::Call); 96 | Memory::VP::InjectHook(Module::exe_ptr(Sumo_DispSelCarModel_HookAddr), CalcPeraShadow, Memory::HookType::Call); 97 | 98 | return true; 99 | } 100 | 101 | static RestoreCarBaseShadow instance; 102 | }; 103 | RestoreCarBaseShadow RestoreCarBaseShadow::instance; 104 | 105 | class ReflectionResolution : public Hook 106 | { 107 | inline static std::array ReflectionResolution_Addrs = 108 | { 109 | // Envmap_Init 110 | 0x13B50 + 1, 111 | 0x13BA1 + 1, 112 | 0x13BA6 + 1, 113 | // D3D_CreateTemporaries 114 | 0x17A69 + 1, 115 | 0x17A88 + 1, 116 | 0x17A8D + 1, 117 | }; 118 | 119 | public: 120 | std::string_view description() override 121 | { 122 | return "ReflectionResolution"; 123 | } 124 | 125 | bool validate() override 126 | { 127 | return Settings::ReflectionResolution >= 2; 128 | } 129 | 130 | bool apply() override 131 | { 132 | for (const int& addr : ReflectionResolution_Addrs) 133 | { 134 | Memory::VP::Patch(Module::exe_ptr(addr), Settings::ReflectionResolution); 135 | } 136 | return true; 137 | } 138 | 139 | static ReflectionResolution instance; 140 | }; 141 | ReflectionResolution ReflectionResolution::instance; 142 | 143 | class DisableDPIScaling : public Hook 144 | { 145 | public: 146 | std::string_view description() override 147 | { 148 | return "DisableDPIScaling"; 149 | } 150 | 151 | bool validate() override 152 | { 153 | return Settings::DisableDPIScaling; 154 | } 155 | 156 | bool apply() override 157 | { 158 | SetProcessDPIAware(); 159 | return true; 160 | } 161 | 162 | static DisableDPIScaling instance; 163 | }; 164 | DisableDPIScaling DisableDPIScaling::instance; 165 | 166 | class ScreenEdgeCullFix : public Hook 167 | { 168 | const static int CalcBall3D2D_Addr = 0x49E70; 169 | 170 | // Hook CalcBall3D2D to rescale screen-ratio positions back to 4:3 positions that game code expects 171 | // (fixes objects like cones being culled out before they reach edge of the screen) 172 | inline static SafetyHookInline dest_orig = {}; 173 | static float __cdecl destination(float a1, Sphere* a2, Sphere* a3) 174 | { 175 | float ret = dest_orig.ccall(a1, a2, a3); 176 | 177 | constexpr float ratio_4_3 = 4.f / 3.f; 178 | 179 | float ratio_screen = Game::screen_resolution->x / Game::screen_resolution->y; 180 | 181 | a3->f0 = (a3->f0 / ratio_screen) * ratio_4_3; 182 | a3->f1 = (a3->f1 * ratio_screen) / ratio_4_3; 183 | return ret; 184 | } 185 | 186 | public: 187 | std::string_view description() override 188 | { 189 | return "ScreenEdgeCullFix"; 190 | } 191 | 192 | bool validate() override 193 | { 194 | return Settings::ScreenEdgeCullFix; 195 | } 196 | 197 | bool apply() override 198 | { 199 | dest_orig = safetyhook::create_inline(Module::exe_ptr(CalcBall3D2D_Addr), destination); 200 | return !!dest_orig; 201 | } 202 | 203 | static ScreenEdgeCullFix instance; 204 | }; 205 | ScreenEdgeCullFix ScreenEdgeCullFix::instance; 206 | 207 | class DisableStageCulling : public Hook 208 | { 209 | const static int CalcCulling_PatchAddr = 0x501F; 210 | 211 | public: 212 | std::string_view description() override 213 | { 214 | return "DisableStageCulling"; 215 | } 216 | 217 | bool validate() override 218 | { 219 | return Settings::DisableStageCulling; 220 | } 221 | 222 | bool apply() override 223 | { 224 | // Patch "if (CheckCulling(...))" -> no-op 225 | Memory::VP::Patch(Module::exe_ptr(CalcCulling_PatchAddr), { 0x90, 0x90 }); 226 | return true; 227 | } 228 | 229 | static DisableStageCulling instance; 230 | }; 231 | DisableStageCulling DisableStageCulling::instance; 232 | 233 | class DisableVehicleLODs : public Hook 234 | { 235 | const static int DispOthcar_PatchAddr = 0xAE4E9; 236 | public: 237 | std::string_view description() override 238 | { 239 | return "DisableVehicleLODs"; 240 | } 241 | 242 | bool validate() override 243 | { 244 | return Settings::DisableVehicleLODs; 245 | } 246 | 247 | bool apply() override 248 | { 249 | // Patch "eax = car.LodNumber" -> "eax = 0" 250 | Memory::VP::Patch(Module::exe_ptr(DispOthcar_PatchAddr), { 0x90, 0x31, 0xC0 }); 251 | 252 | return true; 253 | } 254 | 255 | static DisableVehicleLODs instance; 256 | }; 257 | DisableVehicleLODs DisableVehicleLODs::instance; 258 | 259 | class FixZBufferPrecision : public Hook 260 | { 261 | const static int CalcCameraMatrix_Addr = 0x84BD0; 262 | const static int Clr_SceneEffect_Addr = 0xBE70; 263 | 264 | inline static SafetyHookInline CalcCameraMatrix = {}; 265 | 266 | static inline bool allow_znear_override = true; 267 | static void CalcCameraMatrix_dest(EvWorkCamera* camera) 268 | { 269 | // improve z-buffer precision by increasing znear 270 | // game default is 0.1 which reduces precision of far objects massively, causing z-fighting and objects not drawing properly 271 | 272 | if (allow_znear_override) 273 | { 274 | // only set znear to 1 if... 275 | if ((camera->cam_mode_34A == 2 || camera->cam_mode_34A == 0) // ... in third-person or FPV 276 | && camera->cam_mode_timer_364 == 0 // ... not switching cameras 277 | && (*Game::current_mode == STATE_GAME || *Game::current_mode == STATE_GOAL)) // ... we're in main game state (not in STATE_START cutscene etc) 278 | { 279 | camera->perspective_znear_BC = 1.0f; 280 | } 281 | else 282 | { 283 | if (camera->cam_mode_timer_364 != 0 || *Game::current_mode != STATE_GAME) 284 | camera->perspective_znear_BC = 0.1f; // set znear to 0.1 during camera switch / cutscene 285 | else 286 | { 287 | float in_car_view_znear = 0.25f; // 0.25 seems fine for in-car view, doesn't improve as much as 1.0f but still better than 0.1f 288 | auto* pl_car = Game::pl_car(); 289 | if (pl_car) 290 | { 291 | if (pl_car->car_kind_11 == 7) // 360SP still shows gap with 0.25 292 | in_car_view_znear = 0.2f; 293 | } 294 | 295 | camera->perspective_znear_BC = in_car_view_znear; 296 | } 297 | } 298 | } 299 | CalcCameraMatrix.call(camera); 300 | } 301 | 302 | // hook Clr_SceneEffect so we can reset camera z-near before screen effects are draw 303 | inline static SafetyHookInline Clr_SceneEffect = {}; 304 | static void Clr_SceneEffect_dest(int a1) 305 | { 306 | FixZBufferPrecision::allow_znear_override = false; 307 | 308 | EvWorkCamera* camera = Module::exe_ptr(0x39FE10); 309 | 310 | float prev = camera->perspective_znear_BC; 311 | 312 | // apply vanilla znear 313 | camera->perspective_znear_BC = 0.05f; // game default = 0.1, but that causes lens flare to slightly clip, 0.05 allows it to fade properly 314 | CalcCameraMatrix_dest(camera); 315 | 316 | Clr_SceneEffect.call(a1); 317 | 318 | // restore orig znear 319 | camera->perspective_znear_BC = prev; 320 | CalcCameraMatrix_dest(camera); 321 | 322 | FixZBufferPrecision::allow_znear_override = true; 323 | } 324 | 325 | public: 326 | std::string_view description() override 327 | { 328 | return "FixZBufferPrecision"; 329 | } 330 | 331 | bool validate() override 332 | { 333 | return Settings::FixZBufferPrecision; 334 | } 335 | 336 | bool apply() override 337 | { 338 | CalcCameraMatrix = safetyhook::create_inline(Module::exe_ptr(CalcCameraMatrix_Addr), CalcCameraMatrix_dest); 339 | if (!CalcCameraMatrix) 340 | return false; 341 | 342 | Clr_SceneEffect = safetyhook::create_inline(Module::exe_ptr(Clr_SceneEffect_Addr), Clr_SceneEffect_dest); 343 | return !!Clr_SceneEffect; 344 | } 345 | 346 | static FixZBufferPrecision instance; 347 | }; 348 | FixZBufferPrecision FixZBufferPrecision::instance; 349 | 350 | class TransparencySupersampling : public Hook 351 | { 352 | const static int DeviceInitHookAddr = 0xEC2F; 353 | const static int DeviceResetHookAddr = 0x17A20; 354 | 355 | inline static SafetyHookMid dest_hook = {}; 356 | inline static SafetyHookMid deviceReset_hook = {}; 357 | static void destination(safetyhook::Context& ctx) 358 | { 359 | auto* device = Game::D3DDevice(); 360 | device->SetRenderState(D3DRS_MULTISAMPLEANTIALIAS, TRUE); 361 | device->SetRenderState(D3DRS_ANTIALIASEDLINEENABLE, TRUE); 362 | device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); 363 | device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); 364 | device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); 365 | device->SetRenderState(D3DRS_MULTISAMPLEMASK, 0xFFFFFFFF); 366 | 367 | // NVIDIA transparency supersampling 368 | device->SetRenderState(D3DRS_ADAPTIVETESS_Y, D3DFORMAT(MAKEFOURCC('A', 'T', 'O', 'C'))); 369 | device->SetRenderState(D3DRS_ADAPTIVETESS_Y, D3DFORMAT(MAKEFOURCC('S', 'S', 'A', 'A'))); 370 | } 371 | 372 | public: 373 | std::string_view description() override 374 | { 375 | return "TransparencySupersampling"; 376 | } 377 | 378 | bool validate() override 379 | { 380 | return Settings::TransparencySupersampling; 381 | } 382 | 383 | bool apply() override 384 | { 385 | dest_hook = safetyhook::create_mid(Module::exe_ptr(DeviceInitHookAddr), destination); 386 | deviceReset_hook = safetyhook::create_mid(Module::exe_ptr(DeviceResetHookAddr), destination); 387 | return !!dest_hook; 388 | } 389 | 390 | static TransparencySupersampling instance; 391 | }; 392 | TransparencySupersampling TransparencySupersampling::instance; 393 | 394 | class WindowedBorderless : public Hook 395 | { 396 | const static int WinMain_BorderlessWindow_WndStyleExAddr = 0x18175; 397 | const static int WinMain_BorderlessWindow_WndStyleAddr = 0x1817A; 398 | const static int Win32_Init_DisableWindowResize_Addr1 = 0xE9E3; 399 | const static int Win32_Init_DisableWindowResize_Addr2 = 0xEA30; 400 | const static int Win32_Init_SetWindowPos_Addr = 0xEAA7; 401 | 402 | inline static SafetyHookMid dest_hook = {}; 403 | static void destination(safetyhook::Context& ctx) 404 | { 405 | HWND window = HWND(ctx.ebp); 406 | SetWindowPos(window, 0, 407 | Settings::WindowPositionX, Settings::WindowPositionY, 408 | Game::screen_resolution->x, Game::screen_resolution->y, 409 | 0x40); 410 | } 411 | 412 | public: 413 | std::string_view description() override 414 | { 415 | return "WindowedBorderless"; 416 | } 417 | 418 | bool validate() override 419 | { 420 | return Settings::WindowedBorderless; 421 | } 422 | 423 | bool apply() override 424 | { 425 | auto* patch_addr = Module::exe_ptr(WinMain_BorderlessWindow_WndStyleExAddr); 426 | Memory::VP::Patch(patch_addr, uint32_t(0)); 427 | 428 | patch_addr = Module::exe_ptr(WinMain_BorderlessWindow_WndStyleAddr); 429 | Memory::VP::Patch(patch_addr, uint32_t(WS_POPUP)); 430 | 431 | patch_addr = Module::exe_ptr(Win32_Init_DisableWindowResize_Addr1); 432 | Memory::VP::Nop(patch_addr, 6); 433 | 434 | patch_addr = Module::exe_ptr(Win32_Init_DisableWindowResize_Addr2); 435 | Memory::VP::Nop(patch_addr, 6); 436 | 437 | // replace original SetWindowPos call 438 | Memory::VP::Nop(Module::exe_ptr(Win32_Init_SetWindowPos_Addr), 0x23); 439 | dest_hook = safetyhook::create_mid(Module::exe_ptr(Win32_Init_SetWindowPos_Addr), destination); 440 | 441 | return true; 442 | } 443 | 444 | static WindowedBorderless instance; 445 | }; 446 | WindowedBorderless WindowedBorderless::instance; 447 | 448 | class AnisotropicFiltering : public Hook 449 | { 450 | const static int ChangeTexAttribute_HookAddr1 = 0x9AD8; 451 | const static int ChangeTexAttribute_HookAddr2 = 0x8960; 452 | 453 | inline static SafetyHookMid dest_hook = {}; 454 | static void destination(safetyhook::Context& ctx) 455 | { 456 | int Sampler = ctx.ebp; 457 | 458 | Game::D3DDevice()->SetSamplerState(Sampler, D3DSAMP_MAXANISOTROPY, Settings::AnisotropicFiltering); 459 | } 460 | 461 | inline static SafetyHookMid dest_hook2 = {}; 462 | static void destination2(safetyhook::Context& ctx) 463 | { 464 | int Sampler = *(int*)(ctx.esp + 0xC); 465 | 466 | if (ctx.edi == D3DSAMP_MINFILTER || ctx.edi == D3DSAMP_MAGFILTER) 467 | { 468 | ctx.esi = D3DTEXF_ANISOTROPIC; 469 | 470 | Game::D3DDevice()->SetSamplerState(Sampler, D3DSAMP_MAXANISOTROPY, Settings::AnisotropicFiltering); 471 | } 472 | } 473 | 474 | public: 475 | std::string_view description() override 476 | { 477 | return "AnisotropicFiltering"; 478 | } 479 | 480 | bool validate() override 481 | { 482 | return Settings::AnisotropicFiltering > 0; 483 | } 484 | 485 | bool apply() override 486 | { 487 | dest_hook = safetyhook::create_mid(Module::exe_ptr(ChangeTexAttribute_HookAddr1), destination); 488 | dest_hook2 = safetyhook::create_mid(Module::exe_ptr(ChangeTexAttribute_HookAddr2), destination2); 489 | 490 | return true; 491 | } 492 | 493 | static AnisotropicFiltering instance; 494 | }; 495 | AnisotropicFiltering AnisotropicFiltering::instance; 496 | 497 | class VSyncOverride : public Hook 498 | { 499 | const static int D3DInit_HookAddr = 0xEB66; 500 | 501 | inline static SafetyHookMid dest_hook = {}; 502 | static void destination(safetyhook::Context& ctx) 503 | { 504 | Game::D3DPresentParams->PresentationInterval = Settings::VSync; 505 | if (!Settings::VSync) 506 | Game::D3DPresentParams->PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; 507 | 508 | // TODO: add MultiSampleType / MultiSampleQuality overrides here? 509 | // (doesn't seem any of them are improvement over vanilla "DX/ANTIALIASING = 2" though...) 510 | } 511 | 512 | public: 513 | std::string_view description() override 514 | { 515 | return "VSync"; 516 | } 517 | 518 | bool validate() override 519 | { 520 | return Settings::VSync != 1; 521 | } 522 | 523 | bool apply() override 524 | { 525 | dest_hook = safetyhook::create_mid(Module::exe_ptr(D3DInit_HookAddr), destination); 526 | 527 | return true; 528 | } 529 | 530 | static VSyncOverride instance; 531 | }; 532 | VSyncOverride VSyncOverride::instance; 533 | -------------------------------------------------------------------------------- /src/hooks_input.cpp: -------------------------------------------------------------------------------- 1 | #define WIN32_LEAN_AND_MEAN 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "hook_mgr.hpp" 10 | #include "plugin.hpp" 11 | #include "game_addrs.hpp" 12 | 13 | namespace Input 14 | { 15 | static int HudToggleVKey = 0; 16 | 17 | void PadUpdate(int controllerIndex) 18 | { 19 | PadStatePrev = PadStateCur; 20 | PadDigitalPrev = PadDigitalCur; 21 | 22 | if (XInputGetState(controllerIndex, &PadStateCur) != ERROR_SUCCESS) 23 | PadStateCur = { 0 }; 24 | 25 | PadDigitalCur = PadStateCur.Gamepad.wButtons; 26 | 27 | // Convert analog inputs to digital bitfield 28 | { 29 | if (PadStateCur.Gamepad.bLeftTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD) 30 | PadDigitalCur |= XINPUT_DIGITAL_LEFT_TRIGGER; 31 | if (PadStateCur.Gamepad.bRightTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD) 32 | PadDigitalCur |= XINPUT_DIGITAL_RIGHT_TRIGGER; 33 | 34 | // Check left stick 35 | if (PadStateCur.Gamepad.sThumbLY > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) 36 | PadDigitalCur |= XINPUT_DIGITAL_LS_UP; 37 | if (PadStateCur.Gamepad.sThumbLY < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) 38 | PadDigitalCur |= XINPUT_DIGITAL_LS_DOWN; 39 | if (PadStateCur.Gamepad.sThumbLX < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) 40 | PadDigitalCur |= XINPUT_DIGITAL_LS_LEFT; 41 | if (PadStateCur.Gamepad.sThumbLX > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) 42 | PadDigitalCur |= XINPUT_DIGITAL_LS_RIGHT; 43 | 44 | // Check right stick 45 | if (PadStateCur.Gamepad.sThumbRY > XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE) 46 | PadDigitalCur |= XINPUT_DIGITAL_RS_UP; 47 | if (PadStateCur.Gamepad.sThumbRY < -XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE) 48 | PadDigitalCur |= XINPUT_DIGITAL_RS_DOWN; 49 | if (PadStateCur.Gamepad.sThumbRX < -XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE) 50 | PadDigitalCur |= XINPUT_DIGITAL_RS_LEFT; 51 | if (PadStateCur.Gamepad.sThumbRX > XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE) 52 | PadDigitalCur |= XINPUT_DIGITAL_RS_RIGHT; 53 | } 54 | } 55 | 56 | void HudToggleUpdate() 57 | { 58 | static bool HudTogglePrevState = false; 59 | bool hudToggleKeyState = (GetAsyncKeyState(HudToggleVKey) & 0x8000); 60 | if (HudTogglePrevState != hudToggleKeyState) 61 | { 62 | HudTogglePrevState = hudToggleKeyState; 63 | if (!hudToggleKeyState) // if key is being released, toggle the hud flag 64 | *Game::navipub_disp_flg = (*Game::navipub_disp_flg == 0 ? 1 : 0); 65 | } 66 | } 67 | 68 | int StringToVK(std::string_view key) 69 | { 70 | // Convert key to uppercase 71 | std::string keyUpper(key); 72 | std::transform(keyUpper.begin(), keyUpper.end(), keyUpper.begin(), 73 | [](unsigned char c) { return std::toupper(c); }); 74 | 75 | static const std::unordered_map vkMap = { 76 | {"LBUTTON", VK_LBUTTON}, {"RBUTTON", VK_RBUTTON}, {"CANCEL", VK_CANCEL}, {"MBUTTON", VK_MBUTTON}, 77 | {"XBUTTON1", VK_XBUTTON1}, {"XBUTTON2", VK_XBUTTON2}, {"BACK", VK_BACK}, {"TAB", VK_TAB}, 78 | {"CLEAR", VK_CLEAR}, {"RETURN", VK_RETURN}, {"SHIFT", VK_SHIFT}, {"CONTROL", VK_CONTROL}, 79 | {"MENU", VK_MENU}, {"PAUSE", VK_PAUSE}, {"CAPITAL", VK_CAPITAL}, {"ESCAPE", VK_ESCAPE}, 80 | {"SPACE", VK_SPACE}, {"PRIOR", VK_PRIOR}, {"NEXT", VK_NEXT}, {"END", VK_END}, 81 | {"HOME", VK_HOME}, {"LEFT", VK_LEFT}, {"UP", VK_UP}, {"RIGHT", VK_RIGHT}, 82 | {"DOWN", VK_DOWN}, {"SELECT", VK_SELECT}, {"PRINT", VK_PRINT}, {"EXECUTE", VK_EXECUTE}, 83 | {"SNAPSHOT", VK_SNAPSHOT}, {"INSERT", VK_INSERT}, {"DELETE", VK_DELETE}, {"HELP", VK_HELP}, 84 | {"LWIN", VK_LWIN}, {"RWIN", VK_RWIN}, {"APPS", VK_APPS}, {"SLEEP", VK_SLEEP}, 85 | {"NUMPAD0", VK_NUMPAD0}, {"NUMPAD1", VK_NUMPAD1}, {"NUMPAD2", VK_NUMPAD2}, {"NUMPAD3", VK_NUMPAD3}, 86 | {"NUMPAD4", VK_NUMPAD4}, {"NUMPAD5", VK_NUMPAD5}, {"NUMPAD6", VK_NUMPAD6}, {"NUMPAD7", VK_NUMPAD7}, 87 | {"NUMPAD8", VK_NUMPAD8}, {"NUMPAD9", VK_NUMPAD9}, {"MULTIPLY", VK_MULTIPLY}, {"ADD", VK_ADD}, 88 | {"SEPARATOR", VK_SEPARATOR}, {"SUBTRACT", VK_SUBTRACT}, {"DECIMAL", VK_DECIMAL}, {"DIVIDE", VK_DIVIDE}, 89 | {"F1", VK_F1}, {"F2", VK_F2}, {"F3", VK_F3}, {"F4", VK_F4}, {"F5", VK_F5}, 90 | {"F6", VK_F6}, {"F7", VK_F7}, {"F8", VK_F8}, {"F9", VK_F9}, {"F10", VK_F10}, 91 | {"F11", VK_F11}, {"F12", VK_F12}, {"F13", VK_F13}, {"F14", VK_F14}, {"F15", VK_F15}, 92 | {"F16", VK_F16}, {"F17", VK_F17}, {"F18", VK_F18}, {"F19", VK_F19}, {"F20", VK_F20}, 93 | {"F21", VK_F21}, {"F22", VK_F22}, {"F23", VK_F23}, {"F24", VK_F24}, {"NUMLOCK", VK_NUMLOCK}, 94 | {"SCROLL", VK_SCROLL} 95 | // TODO: are there any others worth adding here? 96 | }; 97 | 98 | // Check if key is a single character 99 | if (keyUpper.size() == 1) 100 | { 101 | char ch = keyUpper[0]; 102 | if (std::isalnum(ch)) 103 | return std::toupper(ch); 104 | } 105 | 106 | // Search in the vkMap 107 | auto it = vkMap.find(keyUpper); 108 | if (it != vkMap.end()) 109 | return it->second; 110 | 111 | // If not found, return -1 or some invalid value 112 | return 0; 113 | } 114 | 115 | void Update() 116 | { 117 | static bool inited = false; 118 | if (!inited) 119 | { 120 | HudToggleVKey = StringToVK(Settings::HudToggleKey); 121 | inited = true; 122 | } 123 | 124 | // Update gamepad for main controller id 125 | PadUpdate(Settings::VibrationControllerId); 126 | 127 | if (HudToggleVKey) 128 | HudToggleUpdate(); 129 | } 130 | }; 131 | 132 | // Hooks into XINPUT1_4's DeviceIoControl via undocumented DriverHook(0xBAAD0001) call 133 | // Allows us to detect SET_GAMEPAD_STATE ioctl and send the GIP HID command for trigger impulses 134 | // Hopefully will work for most series controller connection types... 135 | 136 | // Defs from OpenXInput, GIP command code from X1nput 137 | constexpr DWORD IOCTL_XINPUT_BASE = 0x8000; 138 | static DWORD IOCTL_XINPUT_SET_GAMEPAD_STATE = CTL_CODE(IOCTL_XINPUT_BASE, 0x804, METHOD_BUFFERED, FILE_WRITE_ACCESS); // 0x8000A010 139 | struct InSetState_t 140 | { 141 | BYTE deviceIndex; 142 | BYTE ledState; 143 | BYTE leftMotorSpeed; 144 | BYTE rightMotorSpeed; 145 | BYTE flags; 146 | }; 147 | 148 | #define XUSB_SET_STATE_FLAG_VIBRATION ((BYTE)0x02) 149 | 150 | class ImpulseVibration : public Hook 151 | { 152 | #define MAX_STR 255 153 | inline static wchar_t wstr[MAX_STR]; 154 | 155 | static BOOL WINAPI DetourDeviceIoControl( 156 | HANDLE hDevice, 157 | DWORD dwIoControlCode, 158 | LPVOID lpInBuffer, 159 | DWORD nInBufferSize, 160 | LPVOID lpOutBuffer, 161 | DWORD nOutBufferSize, 162 | LPDWORD lpBytesReturned, 163 | LPOVERLAPPED lpOverlapped 164 | ) 165 | { 166 | auto ret = DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize, lpBytesReturned, lpOverlapped); 167 | if (dwIoControlCode != IOCTL_XINPUT_SET_GAMEPAD_STATE) 168 | return ret; 169 | 170 | // Don't send GIP command to x360, may cause issues with some bad third-party ones? 171 | if (HidD_GetProductString(hDevice, wstr, MAX_STR) && wcsstr(wstr, L"360")) 172 | return ret; 173 | 174 | if (!Settings::ImpulseVibrationMode) 175 | return ret; // how did we get here? 176 | 177 | InSetState_t* inData = (InSetState_t*)lpInBuffer; 178 | if ((inData->flags & XUSB_SET_STATE_FLAG_VIBRATION) != 0) 179 | { 180 | float leftTriggerInput = float(inData->leftMotorSpeed); 181 | float rightTriggerInput = float(inData->rightMotorSpeed); 182 | 183 | if (Settings::ImpulseVibrationMode == 2) // Swap L/R 184 | { 185 | leftTriggerInput = float(inData->rightMotorSpeed); 186 | rightTriggerInput = float(inData->leftMotorSpeed); 187 | } 188 | else if (Settings::ImpulseVibrationMode == 3) // Merge L/R by using whichever is highest 189 | { 190 | leftTriggerInput = rightTriggerInput = max(leftTriggerInput, rightTriggerInput); 191 | } 192 | 193 | uint8_t buf[9] = { 0 }; 194 | buf[0] = 0x03; // HID report ID (3 for bluetooth, any for USB) 195 | buf[1] = 0x0F; // Motor flag mask(?) 196 | buf[2] = uint8_t(leftTriggerInput * Settings::ImpulseVibrationLeftMultiplier); // Left trigger impulse 197 | buf[3] = uint8_t(rightTriggerInput * Settings::ImpulseVibrationRightMultiplier); // Right trigger impulse 198 | buf[4] = inData->leftMotorSpeed; // Left rumble 199 | buf[5] = inData->rightMotorSpeed; // Right rumble 200 | // "Pulse" 201 | buf[6] = 0xFF; // On time 202 | buf[7] = 0x00; // Off time 203 | buf[8] = 0xFF; // Number of repeats 204 | WriteFile(hDevice, buf, 9, lpBytesReturned, lpOverlapped); 205 | } 206 | 207 | return ret; 208 | } 209 | 210 | public: 211 | std::string_view description() override 212 | { 213 | return "ImpulseVibration"; 214 | } 215 | 216 | bool validate() override 217 | { 218 | return Settings::ImpulseVibrationMode != 0 && Settings::VibrationMode != 0 && !Settings::UseNewInput; 219 | } 220 | 221 | bool apply() override 222 | { 223 | auto xinput = LoadLibraryA("xinput1_4.dll"); 224 | if (!xinput) 225 | return false; 226 | 227 | typedef BOOL(__stdcall* DllMain_fn)(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved); 228 | auto dllmain = (DllMain_fn)GetProcAddress(xinput, "DllMain"); 229 | if (!dllmain) 230 | return false; 231 | 232 | dllmain(nullptr, 0xBAAD0001, DetourDeviceIoControl); 233 | 234 | return true; 235 | } 236 | 237 | static ImpulseVibration instance; 238 | }; 239 | ImpulseVibration ImpulseVibration::instance; 240 | 241 | class ControllerHotPlug : public Hook 242 | { 243 | const static int DInputInit_CallbackPtr_Addr = 0x3E10; 244 | 245 | public: 246 | inline static std::mutex mtx; 247 | inline static std::unique_ptr DeviceEnumerationThreadHandle; 248 | inline static std::vector KnownDevices; 249 | inline static std::queue NewDevices; 250 | 251 | static BOOL __stdcall DInput_EnumJoysticksCallback(const DIDEVICEINSTANCE* pdidInstance, VOID* pContext) 252 | { 253 | std::lock_guard lock(mtx); 254 | if (std::find(KnownDevices.begin(), KnownDevices.end(), pdidInstance->guidInstance) == KnownDevices.end()) 255 | { 256 | // GUID not found, add to the vector 257 | KnownDevices.push_back(pdidInstance->guidInstance); 258 | 259 | // Add the new device instance to the queue 260 | NewDevices.push(*pdidInstance); 261 | } 262 | 263 | return DIENUM_CONTINUE; 264 | } 265 | 266 | static void DeviceEnumerationThread() 267 | { 268 | #ifdef _DEBUG 269 | SetThreadDescription(GetCurrentThread(), L"DeviceEnumerationThread"); 270 | #endif 271 | 272 | while (true) 273 | { 274 | if (Game::DirectInput8()) 275 | Game::DirectInput8()->EnumDevices(DI8DEVCLASS_GAMECTRL, DInput_EnumJoysticksCallback, nullptr, DIEDFL_ATTACHEDONLY); 276 | 277 | std::this_thread::sleep_for(std::chrono::seconds(2)); // Poll every 2 seconds 278 | } 279 | } 280 | 281 | std::string_view description() override 282 | { 283 | return "ControllerHotPlug"; 284 | } 285 | 286 | bool validate() override 287 | { 288 | return Settings::ControllerHotPlug; 289 | } 290 | 291 | bool apply() override 292 | { 293 | // Patch games controller init code to go through our DInput_EnumJoysticksCallback func, so we can learn GUID of any already connected pads 294 | Memory::VP::Patch(Module::exe_ptr(DInputInit_CallbackPtr_Addr + 1), DInput_EnumJoysticksCallback); 295 | 296 | return true; 297 | } 298 | 299 | static ControllerHotPlug instance; 300 | }; 301 | ControllerHotPlug ControllerHotPlug::instance; 302 | 303 | void DInput_RegisterNewDevices() 304 | { 305 | if (!ControllerHotPlug::DeviceEnumerationThreadHandle) 306 | { 307 | ControllerHotPlug::DeviceEnumerationThreadHandle = std::make_unique(ControllerHotPlug::DeviceEnumerationThread); 308 | ControllerHotPlug::DeviceEnumerationThreadHandle->detach(); 309 | } 310 | 311 | std::lock_guard lock(ControllerHotPlug::mtx); 312 | while (!ControllerHotPlug::NewDevices.empty()) 313 | { 314 | DIDEVICEINSTANCE deviceInstance = ControllerHotPlug::NewDevices.front(); 315 | ControllerHotPlug::NewDevices.pop(); 316 | 317 | // Tell game about it 318 | Game::DInput_EnumJoysticksCallback(&deviceInstance, 0); 319 | 320 | // Sets some kind of active-device-number to let it work 321 | *Module::exe_ptr(0x3398D4) = 0; 322 | } 323 | } 324 | 325 | class SteeringDeadZone : public Hook 326 | { 327 | public: 328 | std::string_view description() override 329 | { 330 | return "SteeringDeadZone"; 331 | } 332 | 333 | bool validate() override 334 | { 335 | return Settings::SteeringDeadZone != 0.2f; 336 | } 337 | 338 | bool apply() override 339 | { 340 | constexpr int ReadVolume_UnkCheck_Addr = 0x538FC; 341 | constexpr int ReadVolume_LoadDeadZone_Addr = 0x538FE + 4; 342 | 343 | // Remove weird check which would override deadzone value to 0.0078125 344 | // (maybe that value is meant to be used for wheels, but game doesn't end up using it?) 345 | Memory::VP::Nop(Module::exe_ptr(ReadVolume_UnkCheck_Addr), 2); 346 | 347 | // Patch game code with Settings::SteeringDeadZone pointer 348 | Memory::VP::Patch(Module::exe_ptr(ReadVolume_LoadDeadZone_Addr), &Settings::SteeringDeadZone); 349 | 350 | return true; 351 | } 352 | 353 | static SteeringDeadZone instance; 354 | }; 355 | SteeringDeadZone SteeringDeadZone::instance; 356 | -------------------------------------------------------------------------------- /src/network.cpp: -------------------------------------------------------------------------------- 1 | #define WIN32_LEAN_AND_MEAN 2 | #include 3 | #include 4 | #pragma comment(lib, "winhttp.lib") 5 | #include 6 | #include "resource.h" 7 | 8 | namespace Util { 9 | 10 | std::string HttpGetRequest(const std::string& host, const std::wstring& path, int portNum = 80) 11 | { 12 | HINTERNET hSession = WinHttpOpen(L"OutRun2006Tweaks/" MODULE_VERSION_STR, 13 | WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, 14 | WINHTTP_NO_PROXY_NAME, 15 | WINHTTP_NO_PROXY_BYPASS, 0); 16 | if (!hSession) 17 | return ""; 18 | 19 | HINTERNET hConnect = WinHttpConnect(hSession, std::wstring(host.begin(), host.end()).c_str(), portNum, 0); 20 | if (!hConnect) 21 | { 22 | WinHttpCloseHandle(hSession); 23 | return ""; 24 | } 25 | 26 | HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", path.c_str(), 27 | NULL, WINHTTP_NO_REFERER, 28 | WINHTTP_DEFAULT_ACCEPT_TYPES, 29 | portNum == INTERNET_DEFAULT_HTTPS_PORT ? WINHTTP_FLAG_SECURE : 0); 30 | if (!hRequest) 31 | { 32 | WinHttpCloseHandle(hConnect); 33 | WinHttpCloseHandle(hSession); 34 | return ""; 35 | } 36 | 37 | BOOL bResults = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, 38 | WINHTTP_NO_REQUEST_DATA, 0, 0, 0); 39 | 40 | if (bResults) 41 | bResults = WinHttpReceiveResponse(hRequest, NULL); 42 | 43 | std::string response; 44 | if (bResults) 45 | { 46 | DWORD dwSize = 0; 47 | do 48 | { 49 | DWORD dwDownloaded = 0; 50 | if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) 51 | break; 52 | 53 | if (dwSize == 0) 54 | break; 55 | 56 | char* buffer = new char[dwSize + 1]; 57 | if (!WinHttpReadData(hRequest, buffer, dwSize, &dwDownloaded)) 58 | { 59 | delete[] buffer; 60 | break; 61 | } 62 | 63 | buffer[dwDownloaded] = '\0'; 64 | response.append(buffer, dwDownloaded); 65 | delete[] buffer; 66 | } while (dwSize > 0); 67 | } 68 | 69 | WinHttpCloseHandle(hRequest); 70 | WinHttpCloseHandle(hConnect); 71 | WinHttpCloseHandle(hSession); 72 | 73 | return response; 74 | } 75 | 76 | }; -------------------------------------------------------------------------------- /src/overlay/chatroom.cpp: -------------------------------------------------------------------------------- 1 | #define WIN32_LEAN_AND_MEAN 2 | #include 3 | #include 4 | #include "hook_mgr.hpp" 5 | #include "plugin.hpp" 6 | #include "game_addrs.hpp" 7 | #include 8 | #include "notifications.hpp" 9 | #include 10 | #include 11 | #include "overlay.hpp" 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | #include "resource.h" 20 | 21 | struct ChatMessage 22 | { 23 | std::string content; 24 | std::chrono::system_clock::time_point timestamp; 25 | }; 26 | 27 | std::string timePointToString(const std::chrono::system_clock::time_point& timePoint) 28 | { 29 | std::time_t timeT = std::chrono::system_clock::to_time_t(timePoint); 30 | std::tm tm = *std::localtime(&timeT); 31 | 32 | std::ostringstream oss; 33 | oss << std::put_time(&tm, "%H:%M:%S"); // hour, minute, second 34 | return oss.str(); 35 | } 36 | 37 | class ChatRoom : public OverlayWindow 38 | { 39 | private: 40 | ix::WebSocket webSocket; 41 | bool socketActive = false; 42 | bool socketJsonEnabled = false; 43 | 44 | bool isActive = false; 45 | char inputBuffer[256] = ""; 46 | std::deque messages; 47 | static constexpr size_t MAX_MESSAGES = 100; 48 | static constexpr float MESSAGE_DISPLAY_SECONDS = 5.0f; 49 | static constexpr float MESSAGE_VERYRECENT_SECONDS = 2.0f; 50 | 51 | std::mutex mtx; 52 | 53 | void connectWebSocket() 54 | { 55 | if (webSocket.getReadyState() == ix::ReadyState::Open) 56 | return; 57 | 58 | std::string url = "ws://" + Settings::DemonwareServerOverride + "/ws"; 59 | if (Settings::DemonwareServerOverride == "localhost") 60 | url = "ws://localhost:4444/ws"; 61 | webSocket.setUrl(url); 62 | 63 | webSocket.setOnMessageCallback([this](const ix::WebSocketMessagePtr& msg) 64 | { 65 | if (msg->type == ix::WebSocketMessageType::Open) 66 | { 67 | // Connection established 68 | // Let server know our version & switch to json mode 69 | socketJsonEnabled = false; // enabled once server acknowledges 70 | webSocket.send("//IDENT v" MODULE_VERSION_STR); 71 | } 72 | else if (msg->type == ix::WebSocketMessageType::Close) 73 | socketJsonEnabled = false; 74 | else if (msg->type == ix::WebSocketMessageType::Message) 75 | parseMessage(msg->str); 76 | }); 77 | 78 | webSocket.start(); 79 | } 80 | 81 | void parseCommandS2C(const std::string& command) 82 | { 83 | if (command.length() >= 7 && command.substr(0, 7) == "//IDENT") 84 | { 85 | // server ack'd our //IDENT, enable json mode 86 | spdlog::debug(__FUNCTION__ ": received IDENT acknowledgement, enabling json mode"); 87 | socketJsonEnabled = true; 88 | return; 89 | } 90 | 91 | // unknown S2C command, silently ignore 92 | spdlog::debug(__FUNCTION__ ": received unknown S2C command ({})", command); 93 | } 94 | 95 | void parseMessage(const std::string& content) 96 | { 97 | std::string msgContent; 98 | 99 | auto receivedTime = std::chrono::system_clock::now(); 100 | 101 | if (!socketJsonEnabled) 102 | { 103 | if (content.length() >= 2 && content.substr(0, 2) == "//") 104 | { 105 | parseCommandS2C(content); 106 | return; 107 | } 108 | 109 | msgContent = content; 110 | } 111 | else 112 | { 113 | Json::CharReaderBuilder builder; 114 | Json::Value root; 115 | std::string errs; 116 | 117 | std::istringstream stream(content); 118 | if (!Json::parseFromStream(builder, stream, &root, &errs)) 119 | { 120 | spdlog::error(__FUNCTION__ ": failed to parse json response ({})", content); 121 | return; 122 | } 123 | 124 | // root["Type"] - either Server or User 125 | // root["UserName"] - message originator 126 | // root["Room"] - lobby host name 127 | // root["Message"] - message text 128 | 129 | if (!root.isMember("Type") || !root.isMember("UserName") || !root.isMember("Room") || !root.isMember("Message")) 130 | { 131 | spdlog::error(__FUNCTION__ ": malformed json response ({})", content); 132 | return; 133 | } 134 | 135 | auto& room = root["Room"]; 136 | 137 | // TODO: compare room against current lobby host, accept if matches 138 | // right now we'll just silently ignore any messages with non-empty room, so future lobby messages won't show on older clients 139 | if (!room.empty() && !room.asString().empty()) 140 | return; 141 | 142 | auto messageType = root["Type"].asString(); 143 | auto userName = root["UserName"].asString(); 144 | auto message = root["Message"].asString(); 145 | 146 | if (messageType == "Server") 147 | { 148 | if (message.length() >= 2 && message.substr(0, 2) == "//") 149 | { 150 | parseCommandS2C(message); 151 | return; 152 | } 153 | 154 | msgContent = message; 155 | } 156 | else 157 | msgContent = std::format("[{}] {}: {}", timePointToString(receivedTime), userName, message); 158 | } 159 | 160 | { 161 | std::lock_guard lock(mtx); 162 | 163 | messages.push_front({ msgContent, receivedTime }); 164 | 165 | if (messages.size() > MAX_MESSAGES) 166 | messages.pop_back(); 167 | } 168 | } 169 | 170 | void sendMessage(const std::string& room, const std::string& message) 171 | { 172 | if (webSocket.getReadyState() != ix::ReadyState::Open) 173 | { 174 | spdlog::error(__FUNCTION__ ": failed to send message as websocket not ready"); 175 | return; 176 | } 177 | 178 | if (!socketJsonEnabled) 179 | { 180 | webSocket.send(message); 181 | return; 182 | } 183 | 184 | const char* onlineName = Game::SumoNet_OnlineUserName; 185 | 186 | Json::Value jsonData; 187 | jsonData["Type"] = "User"; // checked server-side 188 | jsonData["UserName"] = onlineName; // checked server-side 189 | jsonData["Room"] = room; 190 | jsonData["Message"] = message; 191 | 192 | Json::StreamWriterBuilder writer; 193 | writer["indentation"] = ""; // remove any indentation 194 | std::string jsonString = Json::writeString(writer, jsonData); 195 | 196 | webSocket.send(jsonString); 197 | } 198 | 199 | public: 200 | void init() override {} 201 | 202 | void render(bool overlayEnabled) override 203 | { 204 | auto socketState = webSocket.getReadyState(); 205 | 206 | // Connect to socket if chat is enabled 207 | if (socketState == ix::ReadyState::Closed && Overlay::ChatMode != Overlay::ChatMode_Disabled) 208 | connectWebSocket(); 209 | 210 | // Otherwise if socket connected and chat is disabled, close connection 211 | else if (socketState == ix::ReadyState::Open && Overlay::ChatMode == Overlay::ChatMode_Disabled) 212 | webSocket.stop(); 213 | 214 | // Toggle active mode with 'Y' key 215 | bool justOpened = false; 216 | if (!isActive && ImGui::IsKeyReleased(ImGuiKey_Y)) 217 | { 218 | justOpened = true; 219 | isActive = true; 220 | } 221 | 222 | if (isActive && ImGui::IsKeyReleased(ImGuiKey_Escape)) 223 | isActive = false; 224 | 225 | if (isActive && ImGui::IsKeyReleased(ImGuiKey_Enter)) 226 | isActive = false; 227 | 228 | if (isActive) 229 | Overlay::IsActive = true; 230 | 231 | auto currentTime = std::chrono::system_clock::now(); 232 | bool hasRecentMessages = false; 233 | bool hasVeryRecentMessages = false; 234 | 235 | { 236 | std::lock_guard lock(mtx); 237 | for (const auto& msg : messages) 238 | { 239 | auto duration = std::chrono::duration_cast( 240 | currentTime - msg.timestamp).count(); 241 | 242 | if (duration < MESSAGE_VERYRECENT_SECONDS) 243 | { 244 | hasVeryRecentMessages = true; 245 | hasRecentMessages = true; 246 | break; 247 | } 248 | if (duration < MESSAGE_DISPLAY_SECONDS) 249 | { 250 | hasRecentMessages = true; 251 | break; 252 | } 253 | } 254 | } 255 | 256 | if (!overlayEnabled) 257 | { 258 | // If mode is EnabledOnMenus, only show recent msgs when on menu 259 | if (Overlay::ChatMode == Overlay::ChatMode_EnabledOnMenus && hasRecentMessages) 260 | hasRecentMessages = !Game::is_in_game(); 261 | 262 | // If not active then only show window if there are recent messages 263 | if (!isActive && !hasRecentMessages) 264 | return; 265 | 266 | if (Overlay::ChatHideBackground) 267 | { 268 | ImGui::SetNextWindowBgAlpha(0.f); 269 | ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); 270 | } 271 | else 272 | { 273 | if (!isActive && !hasVeryRecentMessages) 274 | ImGui::SetNextWindowBgAlpha(0.3f); 275 | } 276 | } 277 | 278 | float screenWidth = float(Game::screen_resolution->x); 279 | float screenHeight = float(Game::screen_resolution->y); 280 | float contentWidth = screenHeight / (3.f / 4.f); 281 | float borderWidth = ((screenWidth - contentWidth) / 2) + 0.5f; 282 | 283 | float screenQuarterHeight = screenHeight / 4; 284 | 285 | ImGui::SetNextWindowPos(ImVec2(borderWidth + 20, (screenQuarterHeight * 3) - 20), ImGuiCond_FirstUseEver); 286 | ImGui::SetNextWindowSize(ImVec2(contentWidth - 40, screenQuarterHeight), ImGuiCond_FirstUseEver); 287 | 288 | if (ImGui::Begin("Chat Messages", nullptr, !overlayEnabled ? ImGuiWindowFlags_NoDecoration : 0)) 289 | { 290 | ImGui::SetWindowFontScale(Overlay::ChatFontSize); 291 | ImGui::BeginChild("ScrollingRegion", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false, ImGuiWindowFlags_HorizontalScrollbar); 292 | 293 | // Get total available height of scroll region 294 | float availableHeight = ImGui::GetContentRegionAvail().y; 295 | 296 | { 297 | std::lock_guard lock(mtx); 298 | 299 | // Calculate total height of messages 300 | float totalMessageHeight = 0; 301 | for (auto it = messages.rbegin(); it != messages.rend(); ++it) 302 | { 303 | const auto& msg = *it; 304 | float textHeight = ImGui::CalcTextSize(msg.content.c_str(), nullptr, true, ImGui::GetContentRegionAvail().x).y; 305 | totalMessageHeight += textHeight + ImGui::GetStyle().ItemSpacing.y; 306 | } 307 | 308 | // Add dummy spacing if content doesn't fill the height 309 | if (totalMessageHeight < availableHeight) 310 | ImGui::Dummy(ImVec2(0, availableHeight - totalMessageHeight)); 311 | 312 | // Draw messages 313 | for (auto it = messages.rbegin(); it != messages.rend(); ++it) 314 | { 315 | const auto& msg = *it; 316 | 317 | if (Overlay::ChatHideBackground) 318 | { 319 | ImVec2 textSize = ImGui::CalcTextSize(msg.content.c_str()); 320 | ImVec2 pos = ImGui::GetCursorScreenPos(); 321 | 322 | // Draw the background rectangle 323 | ImGui::GetWindowDrawList()->AddRectFilled( 324 | pos, 325 | ImVec2(pos.x + textSize.x, pos.y + textSize.y + ImGui::GetStyle().ItemSpacing.y), 326 | ImGui::ColorConvertFloat4ToU32(ImGui::GetStyle().Colors[ImGuiCol_FrameBg]) 327 | ); 328 | 329 | ImGui::TextWrapped("%s", msg.content.c_str()); 330 | } 331 | else 332 | ImGui::TextWrapped("%s", msg.content.c_str()); 333 | } 334 | } 335 | 336 | if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) 337 | ImGui::SetScrollHereY(1.0f); 338 | 339 | ImGui::EndChild(); 340 | 341 | if (isActive) 342 | { 343 | // Input box 344 | ImGui::Separator(); 345 | 346 | float availableWidth = ImGui::GetContentRegionAvail().x; 347 | ImGui::PushItemWidth(availableWidth); 348 | if (ImGui::InputText("##Message", inputBuffer, sizeof(inputBuffer), ImGuiInputTextFlags_EnterReturnsTrue)) 349 | { 350 | if (inputBuffer[0] != '\0') 351 | { 352 | sendMessage("", inputBuffer); 353 | inputBuffer[0] = '\0'; 354 | } 355 | } 356 | ImGui::PopItemWidth(); 357 | 358 | if (justOpened) 359 | ImGui::SetKeyboardFocusHere(-1); 360 | } 361 | } 362 | ImGui::End(); 363 | if (!overlayEnabled && Overlay::ChatHideBackground) 364 | ImGui::PopStyleVar(); // pop border removal 365 | } 366 | 367 | ~ChatRoom() 368 | { 369 | webSocket.stop(); 370 | } 371 | 372 | static ChatRoom instance; 373 | }; 374 | 375 | ChatRoom ChatRoom::instance; -------------------------------------------------------------------------------- /src/overlay/hooks_overlay.cpp: -------------------------------------------------------------------------------- 1 | #define WIN32_LEAN_AND_MEAN 2 | #include 3 | #include 4 | #include "hook_mgr.hpp" 5 | #include "plugin.hpp" 6 | #include "game_addrs.hpp" 7 | #include 8 | #include 9 | #include 10 | #include "overlay.hpp" 11 | 12 | bool overlayInited = false; 13 | bool overlayActive = false; 14 | 15 | struct CUSTOMVERTEX 16 | { 17 | FLOAT x, y, z, rhw; 18 | D3DCOLOR color; 19 | }; 20 | #define CUSTOMFVF (D3DFVF_XYZRHW | D3DFVF_DIFFUSE) 21 | 22 | IDirect3DVertexBuffer9* LetterboxVertex = nullptr; 23 | 24 | void CreateLetterboxVertex() 25 | { 26 | IDirect3DDevice9* d3ddev = Game::D3DDevice(); 27 | 28 | float screenWidth = float(Game::screen_resolution->x); 29 | float screenHeight = float(Game::screen_resolution->y); 30 | float contentWidth = screenHeight / (3.f / 4.f); 31 | float borderWidth = ((screenWidth - contentWidth) / 2) + 0.5f; 32 | 33 | // Add half-pixel offset so there's no white border... 34 | screenHeight = screenHeight + 0.5f; 35 | screenWidth = screenWidth + 0.5f; 36 | 37 | CUSTOMVERTEX vertices[] = 38 | { 39 | // Left border 40 | { -0.5f, -0.5f, 0.0f, 1.0f, D3DCOLOR_XRGB(0, 0, 0) }, 41 | { borderWidth, -0.5f, 0.0f, 1.0f, D3DCOLOR_XRGB(0, 0, 0) }, 42 | { -0.5f, screenHeight, 0.0f, 1.0f, D3DCOLOR_XRGB(0, 0, 0) }, 43 | { borderWidth, screenHeight, 0.0f, 1.0f, D3DCOLOR_XRGB(0, 0, 0) }, 44 | 45 | // Right border 46 | { (screenWidth - borderWidth), -0.5f, 0.0f, 1.0f, D3DCOLOR_XRGB(0, 0, 0) }, 47 | { screenWidth, -0.5f, 0.0f, 1.0f, D3DCOLOR_XRGB(0, 0, 0) }, 48 | { (screenWidth - borderWidth), screenHeight, 0.0f, 1.0f, D3DCOLOR_XRGB(0, 0, 0) }, 49 | { screenWidth, screenHeight, 0.0f, 1.0f, D3DCOLOR_XRGB(0, 0, 0) } 50 | }; 51 | 52 | d3ddev->CreateVertexBuffer(8 * sizeof(CUSTOMVERTEX), 53 | 0, 54 | CUSTOMFVF, 55 | D3DPOOL_MANAGED, 56 | &LetterboxVertex, 57 | NULL); 58 | 59 | void* pVoid; 60 | LetterboxVertex->Lock(0, 0, &pVoid, 0); 61 | memcpy(pVoid, vertices, sizeof(vertices)); 62 | LetterboxVertex->Unlock(); 63 | } 64 | 65 | class D3DHooks : public Hook 66 | { 67 | inline static SafetyHookMid midhook_d3dinit{}; 68 | static void D3DInit(SafetyHookContext& ctx) 69 | { 70 | if (ctx.eax == 0) 71 | return; // InitDirectX returned false 72 | 73 | CreateLetterboxVertex(); 74 | 75 | if (Settings::OverlayEnabled) 76 | { 77 | Overlay::init_imgui(); 78 | 79 | // Setup Platform/Renderer backends 80 | ImGui_ImplWin32_Init(Game::GameHwnd()); 81 | ImGui_ImplDX9_Init(Game::D3DDevice()); 82 | 83 | overlayInited = true; 84 | } 85 | } 86 | 87 | inline static SafetyHookMid midhook_d3dendscene{}; 88 | static void D3DEndScene(SafetyHookContext& ctx) 89 | { 90 | if (Settings::UILetterboxing > 0 && Settings::UIScalingMode > 0) 91 | { 92 | IDirect3DDevice9* d3ddev = Game::D3DDevice(); 93 | 94 | // Only show letterboxing if not in game, or UILetterboxing is 2 95 | if (!Game::is_in_game() || Settings::UILetterboxing == 2) 96 | { 97 | // Backup existing cullmode and set to none, otherwise we won't get drawn 98 | DWORD prevCullMode = D3DCULL_NONE; 99 | d3ddev->GetRenderState(D3DRS_CULLMODE, &prevCullMode); 100 | d3ddev->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); 101 | 102 | // Seems these SetRenderState/SetTexture calls are needed for DGVoodoo to render letterbox while imgui is active 103 | // DXVK/D3D9 seem to work fine without them 104 | // TODO: the game keeps its own copies of the render states so it can update them if needed, should we update the games copy here? 105 | { 106 | // Set render states 107 | d3ddev->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE); 108 | d3ddev->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE); 109 | d3ddev->SetRenderState(D3DRS_LIGHTING, FALSE); 110 | 111 | // Set texture stage states to avoid any texture influence 112 | d3ddev->SetTexture(0, NULL); 113 | d3ddev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_DISABLE); 114 | d3ddev->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_DISABLE); 115 | } 116 | 117 | // Set FVF and stream source 118 | d3ddev->SetFVF(CUSTOMFVF); 119 | d3ddev->SetStreamSource(0, LetterboxVertex, 0, sizeof(CUSTOMVERTEX)); 120 | 121 | // Draw left border 122 | d3ddev->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); 123 | 124 | // Draw right border 125 | d3ddev->DrawPrimitive(D3DPT_TRIANGLESTRIP, 4, 2); 126 | 127 | // Restore original cullmode 128 | d3ddev->SetRenderState(D3DRS_CULLMODE, prevCullMode); 129 | } 130 | } 131 | 132 | if (overlayInited) 133 | { 134 | ImGui_ImplDX9_NewFrame(); 135 | ImGui_ImplWin32_NewFrame(); 136 | overlayActive = Overlay::render(); 137 | ImGui::Render(); 138 | ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData()); 139 | } 140 | } 141 | 142 | inline static SafetyHookMid midhook_d3dTemporariesRelease{}; 143 | static void D3DTemporariesRelease(SafetyHookContext& ctx) 144 | { 145 | if (overlayInited) 146 | ImGui_ImplDX9_InvalidateDeviceObjects(); 147 | 148 | if (LetterboxVertex) 149 | { 150 | LetterboxVertex->Release(); 151 | LetterboxVertex = nullptr; 152 | } 153 | } 154 | 155 | inline static SafetyHookMid midhook_d3dTemporariesCreate{}; 156 | static void D3DTemporariesCreate(SafetyHookContext& ctx) 157 | { 158 | if (overlayInited) 159 | ImGui_ImplDX9_CreateDeviceObjects(); 160 | 161 | CreateLetterboxVertex(); 162 | } 163 | 164 | public: 165 | std::string_view description() override 166 | { 167 | return "D3DHooks"; 168 | } 169 | 170 | bool validate() override 171 | { 172 | return true; 173 | } 174 | 175 | bool apply() override 176 | { 177 | constexpr int InitDirectX_CallerResult_Addr = 0x1775E; 178 | constexpr int Direct3D_EndScene_CallerAddr = 0x17D4E; 179 | constexpr int D3D_ReleaseTemporaries_Addr = 0x17970; 180 | constexpr int D3D_CreateTemporaries_Addr = 0x17A20; 181 | 182 | midhook_d3dinit = safetyhook::create_mid(Module::exe_ptr(InitDirectX_CallerResult_Addr), D3DInit); 183 | midhook_d3dendscene = safetyhook::create_mid(Module::exe_ptr(Direct3D_EndScene_CallerAddr), D3DEndScene); 184 | 185 | midhook_d3dTemporariesRelease = safetyhook::create_mid(Module::exe_ptr(D3D_ReleaseTemporaries_Addr), D3DTemporariesRelease); 186 | midhook_d3dTemporariesCreate = safetyhook::create_mid(Module::exe_ptr(D3D_CreateTemporaries_Addr), D3DTemporariesCreate); 187 | 188 | Overlay::init(); 189 | 190 | return !!midhook_d3dinit && !!midhook_d3dendscene; 191 | } 192 | 193 | static D3DHooks instance; 194 | }; 195 | D3DHooks D3DHooks::instance; 196 | 197 | extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); 198 | 199 | class WndprocHook : public Hook 200 | { 201 | const static int WndProc_Addr = 0x17F90; 202 | 203 | inline static SafetyHookInline dest_orig = {}; 204 | static LRESULT __stdcall destination(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) 205 | { 206 | if (ImGui_ImplWin32_WndProcHandler(hwnd, msg, wParam, lParam)) 207 | return 1; 208 | 209 | if (Settings::WindowedHideMouseCursor && !overlayActive) 210 | { 211 | if (msg == WM_SETFOCUS || (msg == WM_ACTIVATE && lParam != WA_INACTIVE)) 212 | { 213 | ShowCursor(false); 214 | } 215 | } 216 | 217 | if (msg == WM_ERASEBKGND) // erase window to white during device reset 218 | { 219 | RECT rect; 220 | GetClientRect(hwnd, &rect); 221 | HBRUSH brush = CreateSolidBrush(RGB(0xFF, 0xFF, 0xFF)); 222 | FillRect((HDC)wParam, &rect, brush); 223 | DeleteObject(brush); 224 | return 1; 225 | } 226 | 227 | // Other message handling 228 | return dest_orig.stdcall(hwnd, msg, wParam, lParam); 229 | } 230 | 231 | public: 232 | std::string_view description() override 233 | { 234 | return "WndprocHook"; 235 | } 236 | 237 | bool validate() override 238 | { 239 | return true; 240 | } 241 | 242 | bool apply() override 243 | { 244 | dest_orig = safetyhook::create_inline(Module::exe_ptr(WndProc_Addr), destination); 245 | return !!dest_orig; 246 | } 247 | 248 | static WndprocHook instance; 249 | }; 250 | WndprocHook WndprocHook::instance; 251 | -------------------------------------------------------------------------------- /src/overlay/notifications.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "game_addrs.hpp" 7 | #include "overlay.hpp" 8 | 9 | inline size_t maxNotifications = 5; 10 | inline std::chrono::seconds displayDuration = std::chrono::seconds(10); 11 | 12 | inline ImVec2 notificationSize = { 600, 100 }; 13 | inline float notificationSpacing = 10.0f; 14 | inline float notificationTextScale = 1.5f; 15 | 16 | class Notifications 17 | { 18 | private: 19 | struct Notification 20 | { 21 | std::string message; 22 | std::chrono::time_point timestamp; 23 | int minDisplaySeconds; 24 | std::function onMouseClick; 25 | }; 26 | 27 | std::deque notifications; 28 | std::mutex notificationsMutex; 29 | 30 | public: 31 | void add(const std::string& message, int minDisplaySeconds = 0, std::function onMouseClick = nullptr) 32 | { 33 | std::lock_guard lock(notificationsMutex); 34 | notifications.push_back({ message, std::chrono::steady_clock::now(), minDisplaySeconds, onMouseClick }); 35 | 36 | if (notifications.size() > maxNotifications) 37 | notifications.pop_front(); 38 | } 39 | 40 | void render() 41 | { 42 | // Remove expired notifications 43 | auto now = std::chrono::steady_clock::now(); 44 | { 45 | std::lock_guard lock(notificationsMutex); 46 | 47 | while (!notifications.empty()) 48 | { 49 | auto& front = notifications.front(); 50 | 51 | auto duration = displayDuration; 52 | if (front.minDisplaySeconds > 0) 53 | duration = std::chrono::seconds(front.minDisplaySeconds); 54 | 55 | if (now - front.timestamp <= duration) // notif time hasn't elapsed yet? 56 | break; 57 | 58 | notifications.pop_front(); 59 | } 60 | } 61 | 62 | if (Game::is_in_game()) 63 | { 64 | if (Overlay::NotifyHideMode == Overlay::NotifyHideMode_AllRaces) 65 | return; 66 | 67 | if (Overlay::NotifyHideMode == Overlay::NotifyHideMode_OnlineRaces && 68 | *Game::SumoNet_CurNetDriver && (*Game::SumoNet_CurNetDriver)->is_in_lobby() && 69 | (*Game::game_mode == 3 || *Game::game_mode == 4)) 70 | return; 71 | } 72 | 73 | if (!Overlay::NotifyEnable) 74 | return; 75 | 76 | ImVec2 screenSize = ImGui::GetIO().DisplaySize; 77 | 78 | // Calculate starting position for the latest notification 79 | // (move it outside of letterbox if letterboxing enabled) 80 | float contentWidth = screenSize.y / (3.f / 4.f); 81 | float borderWidth = ((screenSize.x - contentWidth) / 2) + 0.5f; 82 | if (Settings::UILetterboxing != 1 || Game::is_in_game()) 83 | borderWidth = 0; 84 | 85 | float startX = screenSize.x - notificationSize.x - borderWidth - 10.f; // 10px padding from the right 86 | float curY = (screenSize.y / 4.0f) - (notifications.size() * (notificationSize.y + notificationSpacing) / 2.0f); 87 | 88 | std::lock_guard lock(notificationsMutex); 89 | 90 | for (size_t i = 0; i < notifications.size(); ++i) 91 | { 92 | auto windowSize = notificationSize; 93 | const auto& notification = notifications[i]; 94 | 95 | ImGui::SetNextWindowPos(ImVec2(startX, curY)); 96 | 97 | std::string windowName = "Notification " + std::to_string(i); 98 | ImGui::Begin(windowName.c_str(), nullptr, ImGuiWindowFlags_NoDecoration | 99 | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove | 100 | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing); 101 | 102 | // Check for click on the notification 103 | if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows) && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) 104 | if (notification.onMouseClick) 105 | notification.onMouseClick(); 106 | 107 | ImGui::SetWindowFontScale(notificationTextScale); 108 | 109 | // Split the message into lines 110 | std::vector lines; 111 | if (notification.message.find("---") == std::string::npos) 112 | lines.push_back(notification.message); // notificiation doesn't have splitter, treat it as all single line for centering 113 | else 114 | { 115 | std::istringstream messageStream(notification.message); 116 | std::string line; 117 | while (std::getline(messageStream, line)) { 118 | lines.push_back(line); 119 | } 120 | } 121 | 122 | // Calculate total height for all lines 123 | float totalTextHeight = 0.0f; 124 | for (const auto& singleLine : lines) { 125 | ImVec2 lineSize = ImGui::CalcTextSize(singleLine.c_str(), nullptr, true, windowSize.x - 20.0f); 126 | totalTextHeight += lineSize.y; 127 | } 128 | totalTextHeight += (lines.size() - 1) * ImGui::GetStyle().ItemSpacing.y; 129 | 130 | // Adjust window height if necessary 131 | if (totalTextHeight + 40.0f > windowSize.y) 132 | windowSize.y = totalTextHeight + 40.0f; 133 | 134 | ImGui::SetWindowSize(windowSize); 135 | 136 | curY += windowSize.y + notificationSpacing; 137 | 138 | // Center text block vertically 139 | float paddingY = 5.0f; 140 | float currentYOffset = (windowSize.y - totalTextHeight) / 2.0f; 141 | currentYOffset = max(currentYOffset, paddingY); 142 | 143 | for (const auto& singleLine : lines) { 144 | // Calculate individual line size 145 | ImVec2 lineSize = ImGui::CalcTextSize(singleLine.c_str(), nullptr, true, windowSize.x - 20.0f); 146 | 147 | // Center line horizontally 148 | if (singleLine != "---") 149 | { 150 | float paddingX = 10.0f; 151 | float offsetX = (windowSize.x - lineSize.x) / 2.0f; 152 | offsetX = max(offsetX, paddingX); 153 | 154 | ImGui::SetCursorPos(ImVec2(offsetX, currentYOffset)); 155 | ImGui::TextWrapped("%s", singleLine.c_str()); 156 | } 157 | else 158 | { 159 | ImGui::SetCursorPos(ImVec2(0, currentYOffset + (paddingY * 3))); 160 | ImGui::Separator(); 161 | } 162 | 163 | // Move down for the next line, including spacing 164 | currentYOffset += lineSize.y + ImGui::GetStyle().ItemSpacing.y; 165 | } 166 | 167 | ImGui::End(); 168 | } 169 | } 170 | 171 | static Notifications instance; 172 | }; 173 | -------------------------------------------------------------------------------- /src/overlay/overlay.cpp: -------------------------------------------------------------------------------- 1 | #define WIN32_LEAN_AND_MEAN 2 | #include 3 | #include 4 | #include "hook_mgr.hpp" 5 | #include "plugin.hpp" 6 | #include "game_addrs.hpp" 7 | #include 8 | #include "notifications.hpp" 9 | #include "resource.h" 10 | #include "overlay.hpp" 11 | #include 12 | 13 | Notifications Notifications::instance; 14 | 15 | bool f11_prev_state = false; // previously seen F11 state 16 | 17 | bool overlay_visible = false; // user wants overlay to show? 18 | 19 | class GlobalsWindow : public OverlayWindow 20 | { 21 | public: 22 | void init() override {} 23 | void render(bool overlayEnabled) override 24 | { 25 | if (!overlayEnabled) 26 | return; 27 | 28 | extern bool EnablePauseMenu; 29 | 30 | bool settingsChanged = false; 31 | 32 | if (ImGui::Begin("Globals", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) 33 | { 34 | uint8_t* frontEndData = *Module::exe_ptr(0x3B17E8); 35 | int frontEndStep = *(int*)frontEndData; 36 | int frontEndEvtStep = *(int*)(frontEndData + 0x218); 37 | char frontEndMenuLevel = *(char*)(frontEndData + 0x220); 38 | 39 | ImGui::Text("Info"); 40 | EVWORK_CAR* car = Game::pl_car(); 41 | ImGui::Text("game_mode: %d", *Game::game_mode); 42 | ImGui::Text("current_mode: %d", *Game::current_mode); 43 | ImGui::Text("Frontend step indexes: %d / %d / %d", frontEndStep, frontEndEvtStep, int(frontEndMenuLevel)); 44 | ImGui::Text("Lobby is active: %d", (*Game::SumoNet_CurNetDriver && (*Game::SumoNet_CurNetDriver)->is_in_lobby())); 45 | ImGui::Text("Lobby is host: %d", (*Game::SumoNet_CurNetDriver && (*Game::SumoNet_CurNetDriver)->is_hosting())); 46 | ImGui::Text("Is MP gamemode: %d", (*Game::game_mode == 3 || *Game::game_mode == 4)); 47 | ImGui::Text("Car kind: %d", int(car->car_kind_11)); 48 | ImGui::Text("Car position: %.3f %.3f %.3f", car->position_14.x, car->position_14.y, car->position_14.z); 49 | ImGui::Text("OnRoadPlace coli %d, stg %d, section %d", 50 | car->OnRoadPlace_5C.loadColiType_0, 51 | car->OnRoadPlace_5C.curStageIdx_C, 52 | car->OnRoadPlace_5C.roadSectionNum_8); 53 | 54 | GameStage cur_stage_num = *Game::stg_stage_num; 55 | ImGui::Text("Loaded Stage: %d (%s / %s)", cur_stage_num, Game::GetStageFriendlyName(cur_stage_num), Game::GetStageUniqueName(cur_stage_num)); 56 | 57 | if (Settings::DrawDistanceIncrease > 0) 58 | if (ImGui::Button("Open Draw Distance Debugger")) 59 | Game::DrawDistanceDebugEnabled = true; 60 | 61 | #ifdef _DEBUG 62 | if (ImGui::Button("Open Binding Dialog")) 63 | Overlay::IsBindingDialogActive = true; 64 | #endif 65 | 66 | ImGui::Separator(); 67 | ImGui::Text("Gameplay"); 68 | 69 | ImGui::Checkbox("Countdown timer enabled", Game::Sumo_CountdownTimerEnable); 70 | ImGui::Checkbox("Pause menu enabled", &EnablePauseMenu); 71 | ImGui::Checkbox("HUD enabled", (bool*)Game::navipub_disp_flg); 72 | 73 | ImGui::Separator(); 74 | ImGui::Text("Controls"); 75 | 76 | ImGui::SliderFloat("SteeringDeadZone", &Settings::SteeringDeadZone, 0.01, 1.0f); 77 | ImGui::SliderInt("VibrationStrength", &Settings::VibrationStrength, 0, 10); 78 | ImGui::SliderFloat("ImpulseVibrationLeftMultiplier", &Settings::ImpulseVibrationLeftMultiplier, 0.1, 1); 79 | ImGui::SliderFloat("ImpulseVibrationRightMultiplier", &Settings::ImpulseVibrationRightMultiplier, 0.1, 1); 80 | 81 | ImGui::Separator(); 82 | ImGui::Text("Graphics"); 83 | 84 | ImGui::SliderInt("FramerateLimit", &Settings::FramerateLimit, 30, 300); 85 | ImGui::SliderInt("DrawDistanceIncrease", &Settings::DrawDistanceIncrease, 0, 4096); 86 | ImGui::SliderInt("DrawDistanceBehind", &Settings::DrawDistanceBehind, 0, 4096); 87 | } 88 | 89 | ImGui::End(); 90 | 91 | if (settingsChanged) 92 | Overlay::settings_write(); 93 | } 94 | static GlobalsWindow instance; 95 | }; 96 | GlobalsWindow GlobalsWindow::instance; 97 | 98 | class UISettingsWindow : public OverlayWindow 99 | { 100 | bool ShowStyleEditor = false; 101 | 102 | public: 103 | void init() override {} 104 | void render(bool overlayEnabled) override 105 | { 106 | if (!overlayEnabled) 107 | return; 108 | 109 | if (ShowStyleEditor) 110 | { 111 | if (ImGui::Begin("UI Style Editor", &ShowStyleEditor)) 112 | ImGui::ShowStyleEditor(); 113 | ImGui::End(); 114 | } 115 | 116 | ImVec2 screenSize = ImGui::GetIO().DisplaySize; 117 | 118 | // Calculate starting position for the latest notification 119 | // (move it outside of letterbox if letterboxing enabled) 120 | float contentWidth = screenSize.y / (3.f / 4.f); 121 | float borderWidth = ((screenSize.x - contentWidth) / 2) + 0.5f; 122 | if (Settings::UILetterboxing != 1 || Game::is_in_game()) 123 | borderWidth = 0; 124 | 125 | float startX = screenSize.x - notificationSize.x - borderWidth - 10.f; // 10px padding from the right 126 | float curY = (screenSize.y / 4.0f); 127 | 128 | { 129 | bool settingsChanged = false; 130 | 131 | if (ImGui::Begin("UI Settings", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) 132 | { 133 | ImGui::SetWindowPos(ImVec2(startX, curY), ImGuiCond_FirstUseEver); 134 | 135 | if (ImGui::TreeNodeEx("Global", ImGuiTreeNodeFlags_DefaultOpen)) 136 | { 137 | if (ImGui::Button("Open UI Style Editor")) 138 | ShowStyleEditor = true; 139 | 140 | static bool fontScaleChanged = false; 141 | if (ImGui::SliderFloat("Font Scale", &Overlay::GlobalFontScale, 0.5f, 2.5f)) 142 | fontScaleChanged |= true; 143 | 144 | if (fontScaleChanged) 145 | if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) 146 | { 147 | settingsChanged |= true; 148 | fontScaleChanged = false; 149 | } 150 | 151 | ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.51f, 0.00f, 0.14f, 0.00f)); 152 | if (ImGui::Button("-")) 153 | { 154 | if (Overlay::GlobalFontScale > 1.0f) 155 | { 156 | Overlay::GlobalFontScale -= 0.05f; 157 | settingsChanged = true; 158 | } 159 | } 160 | 161 | ImGui::SameLine(); 162 | 163 | if (ImGui::Button("+")) 164 | { 165 | if (Overlay::GlobalFontScale < 5.0f) 166 | { 167 | Overlay::GlobalFontScale += 0.05f; 168 | settingsChanged = true; 169 | } 170 | } 171 | 172 | ImGui::PopStyleColor(); 173 | 174 | settingsChanged |= ImGui::SliderFloat("Overlay Opacity", &Overlay::GlobalOpacity, 0.1f, 1.0f); 175 | 176 | ImGui::TreePop(); 177 | } 178 | 179 | if (ImGui::TreeNodeEx("Notifications", ImGuiTreeNodeFlags_DefaultOpen)) 180 | { 181 | settingsChanged |= ImGui::Checkbox("Enable Notifications", &Overlay::NotifyEnable); 182 | settingsChanged |= ImGui::Checkbox("Enable Online Lobby Notifications", &Overlay::NotifyOnlineEnable); 183 | 184 | settingsChanged |= ImGui::SliderInt("Display Time", &Overlay::NotifyDisplayTime, 0, 60); 185 | settingsChanged |= ImGui::SliderInt("Online Update Time", &Overlay::NotifyOnlineUpdateTime, 10, 60); 186 | 187 | static const char* items[]{ "Never Hide", "Online Race", "Any Race" }; 188 | settingsChanged |= ImGui::Combo("Hide During", &Overlay::NotifyHideMode, items, IM_ARRAYSIZE(items)); 189 | 190 | ImGui::TreePop(); 191 | } 192 | 193 | if (ImGui::TreeNodeEx("Chat", ImGuiTreeNodeFlags_DefaultOpen)) 194 | { 195 | static const char* items[]{ "Disable", "Enable", "During Menus Only" }; 196 | settingsChanged |= ImGui::Combo("Chatroom", &Overlay::ChatMode, items, IM_ARRAYSIZE(items)); 197 | settingsChanged |= ImGui::SliderFloat("Chat Font Size", &Overlay::ChatFontSize, 0.5f, 2.5f); 198 | settingsChanged |= ImGui::Checkbox("Hide Chat Background", &Overlay::ChatHideBackground); 199 | 200 | ImGui::TreePop(); 201 | } 202 | } 203 | 204 | ImGui::End(); 205 | 206 | if (settingsChanged) 207 | { 208 | ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = Overlay::GlobalOpacity; 209 | ImGui::GetIO().FontGlobalScale = Overlay::GlobalFontScale; 210 | 211 | Overlay::settings_write(); 212 | } 213 | } 214 | } 215 | static UISettingsWindow instance; 216 | }; 217 | UISettingsWindow UISettingsWindow::instance; 218 | 219 | void Overlay::init() 220 | { 221 | Overlay::settings_read(); 222 | 223 | Notifications::instance.add("OutRun2006Tweaks v" MODULE_VERSION_STR " by emoose!\nPress F11 to open overlay.", 0, 224 | []() { 225 | std::string url = "https://github.com/emoose/OutRun2006Tweaks"; 226 | ShellExecuteA(nullptr, "open", url.c_str(), 0, 0, SW_SHOWNORMAL); 227 | }); 228 | 229 | if (Overlay::CourseReplacementEnabled) 230 | Notifications::instance.add("Note: Course Editor Override is enabled from previous session."); 231 | 232 | void ServerNotifications_Init(); 233 | ServerNotifications_Init(); 234 | 235 | void UpdateCheck_Init(); 236 | UpdateCheck_Init(); 237 | } 238 | 239 | void Overlay::init_imgui() 240 | { 241 | // Setup Dear ImGui context 242 | IMGUI_CHECKVERSION(); 243 | ImGui::CreateContext(); 244 | ImGuiIO& io = ImGui::GetIO(); (void)io; 245 | io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls 246 | io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls 247 | io.FontGlobalScale = Overlay::GlobalFontScale; 248 | 249 | // Setup Dear ImGui style 250 | ImGui::StyleColorsDark(); 251 | //ImGui::StyleColorsLight(); 252 | 253 | ImGuiStyle& style = ImGui::GetStyle(); 254 | style.PopupRounding = 20.0f; 255 | style.WindowRounding = 20.0f; 256 | style.ChildRounding = 20.0f; 257 | style.FrameRounding = 6.0f; // For buttons and other frames 258 | style.ScrollbarRounding = 6.0f; 259 | style.GrabRounding = 6.0f; // For sliders and scrollbars 260 | style.TabRounding = 6.0f; 261 | 262 | ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = Overlay::GlobalOpacity; 263 | ImGui::GetIO().FontGlobalScale = Overlay::GlobalFontScale; 264 | } 265 | 266 | void ForceShowCursor(bool show) 267 | { 268 | int counter = 0; 269 | 270 | // Adjust the counter until the cursor visibility matches the desired state 271 | do 272 | { 273 | counter = ShowCursor(show); 274 | } while ((show && counter < 0) || (!show && counter >= 0)); 275 | } 276 | 277 | bool Overlay::render() 278 | { 279 | IsActive = false; 280 | 281 | if (!s_hasInited) 282 | { 283 | for (const auto& wnd : s_windows) 284 | wnd->init(); 285 | s_hasInited = true; 286 | } 287 | 288 | if (ImGui::IsKeyReleased(ImGuiKey_F11)) 289 | { 290 | overlay_visible = !overlay_visible; 291 | ForceShowCursor(overlay_visible); 292 | } 293 | 294 | // Start the Dear ImGui frame 295 | ImGui::NewFrame(); 296 | 297 | // Notifications are rendered before any other window 298 | Notifications::instance.render(); 299 | 300 | if (Overlay::RequestBindingDialog) 301 | { 302 | ForceShowCursor(true); 303 | Overlay::IsBindingDialogActive = true; 304 | Overlay::RequestBindingDialog = false; 305 | } 306 | 307 | for (const auto& wnd : s_windows) 308 | wnd->render(overlay_visible); 309 | 310 | if (Overlay::RequestMouseHide) 311 | { 312 | if (!overlay_visible) 313 | ForceShowCursor(false); 314 | Overlay::RequestMouseHide = false; 315 | } 316 | 317 | ImGui::EndFrame(); 318 | 319 | if (overlay_visible) 320 | IsActive = true; 321 | 322 | return IsActive; 323 | } 324 | 325 | bool Overlay::settings_read() 326 | { 327 | spdlog::info("Overlay::settings_read - reading INI from {}", Module::OverlayIniPath.string()); 328 | 329 | inih::INIReader ini; 330 | try 331 | { 332 | ini = inih::INIReader(Module::OverlayIniPath); 333 | } 334 | catch (...) 335 | { 336 | spdlog::error("Overlay::settings_read - INI read failed! The file might not exist, or may have duplicate settings inside"); 337 | return false; 338 | } 339 | 340 | GlobalFontScale = ini.Get("Overlay", "FontScale", GlobalFontScale); 341 | GlobalOpacity = ini.Get("Overlay", "Opacity", GlobalOpacity); 342 | 343 | NotifyEnable = ini.Get("Notifications", "Enable", NotifyEnable); 344 | NotifyDisplayTime = ini.Get("Notifications", "DisplayTime", NotifyDisplayTime); 345 | NotifyOnlineEnable = ini.Get("Notifications", "OnlineEnable", NotifyOnlineEnable); 346 | NotifyOnlineUpdateTime = ini.Get("Notifications", "OnlineUpdateTime", NotifyOnlineUpdateTime); 347 | NotifyHideMode = ini.Get("Notifications", "HideMode", NotifyHideMode); 348 | NotifyUpdateCheck = ini.Get("Notifications", "CheckForUpdates", NotifyUpdateCheck); 349 | 350 | ChatMode = ini.Get("Chat", "ChatMode", ChatMode); 351 | ChatFontSize = ini.Get("Chat", "FontSize", ChatFontSize); 352 | ChatHideBackground = ini.Get("Chat", "HideBackground", ChatHideBackground); 353 | 354 | CourseReplacementEnabled = ini.Get("CourseReplacement", "Enabled", CourseReplacementEnabled); 355 | std::string CourseCode; 356 | CourseCode = ini.Get("CourseReplacement", "Code", CourseCode); 357 | strcpy_s(CourseReplacementCode, CourseCode.c_str()); 358 | 359 | return true; 360 | } 361 | 362 | bool Overlay::settings_write() 363 | { 364 | inih::INIReader ini; 365 | ini.Set("Overlay", "FontScale", GlobalFontScale); 366 | ini.Set("Overlay", "Opacity", GlobalOpacity); 367 | 368 | ini.Set("Notifications", "Enable", NotifyEnable); 369 | ini.Set("Notifications", "DisplayTime", NotifyDisplayTime); 370 | ini.Set("Notifications", "OnlineEnable", NotifyOnlineEnable); 371 | ini.Set("Notifications", "OnlineUpdateTime", NotifyOnlineUpdateTime); 372 | ini.Set("Notifications", "HideMode", NotifyHideMode); 373 | ini.Set("Notifications", "CheckForUpdates", NotifyUpdateCheck); 374 | 375 | ini.Set("Chat", "ChatMode", ChatMode); 376 | ini.Set("Chat", "FontSize", ChatFontSize); 377 | ini.Set("Chat", "HideBackground", ChatHideBackground); 378 | 379 | ini.Set("CourseReplacement", "Enabled", CourseReplacementEnabled); 380 | ini.Set("CourseReplacement", "Code", std::string(CourseReplacementCode)); 381 | 382 | inih::INIWriter writer; 383 | try 384 | { 385 | writer.write(Module::OverlayIniPath, ini); 386 | } 387 | catch (...) 388 | { 389 | spdlog::error("Overlay::settings_write - INI write failed!"); 390 | return false; 391 | } 392 | return true; 393 | } 394 | 395 | OverlayWindow::OverlayWindow() 396 | { 397 | Overlay::add_window(this); 398 | } 399 | -------------------------------------------------------------------------------- /src/overlay/overlay.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class OverlayWindow 4 | { 5 | public: 6 | OverlayWindow(); 7 | virtual ~OverlayWindow() = default; 8 | virtual void init() = 0; 9 | virtual void render(bool overlayEnabled) = 0; 10 | }; 11 | 12 | class Overlay 13 | { 14 | public: 15 | inline static float GlobalFontScale = 1.5f; 16 | inline static float GlobalOpacity = 0.8f; 17 | 18 | inline static bool NotifyEnable = true; 19 | inline static int NotifyDisplayTime = 7; 20 | inline static bool NotifyOnlineEnable = true; 21 | inline static int NotifyOnlineUpdateTime = 20; 22 | inline static int NotifyHideMode = 1; 23 | inline static bool NotifyUpdateCheck = true; 24 | 25 | enum NotifyHideModes 26 | { 27 | NotifyHideMode_Never = 0, 28 | NotifyHideMode_OnlineRaces = 1, 29 | NotifyHideMode_AllRaces = 2 30 | }; 31 | 32 | inline static bool CourseReplacementEnabled = false; 33 | inline static char CourseReplacementCode[256] = { 0 }; 34 | 35 | inline static int ChatMode = 0; 36 | inline static bool ChatHideBackground = true; 37 | inline static float ChatFontSize = 1.0f; 38 | 39 | enum ChatModes 40 | { 41 | ChatMode_Disabled = 0, 42 | ChatMode_Enabled = 1, 43 | ChatMode_EnabledOnMenus = 2, 44 | }; 45 | 46 | inline static bool IsActive = false; 47 | 48 | inline static bool RequestBindingDialog = false; 49 | inline static bool IsBindingDialogActive = false; 50 | inline static bool RequestMouseHide = false; 51 | 52 | private: 53 | inline static std::vector s_windows; 54 | inline static bool s_hasInited = false; 55 | 56 | public: 57 | static void init(); 58 | static void init_imgui(); 59 | 60 | static bool settings_read(); 61 | static bool settings_write(); 62 | 63 | static void add_window(OverlayWindow* window) 64 | { 65 | s_windows.emplace_back(window); 66 | } 67 | 68 | static bool render(); 69 | }; 70 | -------------------------------------------------------------------------------- /src/overlay/server_notifications.cpp: -------------------------------------------------------------------------------- 1 | #define WIN32_LEAN_AND_MEAN 2 | #include 3 | #include 4 | #include "hook_mgr.hpp" 5 | #include "plugin.hpp" 6 | #include "game_addrs.hpp" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "notifications.hpp" 12 | 13 | class ServerUpdater 14 | { 15 | public: 16 | ServerUpdater() {} 17 | 18 | void init() 19 | { 20 | running = true; 21 | updaterThread = std::thread(&ServerUpdater::monitorServers, this); 22 | } 23 | 24 | ~ServerUpdater() 25 | { 26 | running = false; 27 | if (updaterThread.joinable()) 28 | updaterThread.join(); 29 | } 30 | 31 | private: 32 | std::thread updaterThread; 33 | bool running = false; 34 | Json::Value previousServers; 35 | std::mutex dataMutex; 36 | 37 | int numServerUpdates = 0; 38 | 39 | void monitorServers() 40 | { 41 | while (running) 42 | { 43 | if (!Overlay::NotifyOnlineEnable || !Overlay::NotifyOnlineUpdateTime) 44 | return; 45 | 46 | try 47 | { 48 | std::string jsonContent = Util::HttpGetRequest(Settings::DemonwareServerOverride, L"/servers.json", Settings::DemonwareServerOverride == "localhost" ? 4444 : 80); 49 | if (!jsonContent.empty()) 50 | { 51 | Json::Value currentServerList = parseJson(jsonContent); 52 | 53 | if (currentServerList.isMember("Servers")) 54 | { 55 | auto& currentServers = currentServerList["Servers"]; 56 | handleNewServers(currentServers); 57 | 58 | // Save the current state as the previous state for the next check 59 | std::lock_guard lock(dataMutex); 60 | previousServers = currentServers; 61 | 62 | numServerUpdates++; 63 | } 64 | } 65 | } 66 | catch (const std::exception& e) 67 | { 68 | } 69 | 70 | if (Overlay::NotifyOnlineUpdateTime < 10) 71 | Overlay::NotifyOnlineUpdateTime = 10; // pls don't hammer us 72 | 73 | std::this_thread::sleep_for(std::chrono::seconds(Overlay::NotifyOnlineUpdateTime)); 74 | } 75 | } 76 | 77 | Json::Value parseJson(const std::string& jsonContent) 78 | { 79 | Json::Value root; 80 | Json::CharReaderBuilder reader; 81 | std::string errs; 82 | std::istringstream stream(jsonContent); 83 | if (!Json::parseFromStream(reader, stream, &root, &errs)) 84 | throw std::runtime_error("Failed to parse JSON: " + errs); 85 | 86 | return root; 87 | } 88 | 89 | void handleNewServers(const Json::Value& currentServers) 90 | { 91 | // Create a set of unique identifiers from the previous servers 92 | std::set previousIdentifiers; 93 | if (previousServers.isArray()) 94 | { 95 | for (const auto& previousServer : previousServers) 96 | { 97 | if (previousServer.isMember("HostName") && previousServer.isMember("Platform") && previousServer.isMember("Reachable")) 98 | { 99 | if (previousServer["Reachable"].asBool()) 100 | { 101 | std::string identifier = previousServer["HostName"].asString() + "_" + previousServer["Platform"].asString(); 102 | previousIdentifiers.insert(identifier); 103 | } 104 | } 105 | } 106 | } 107 | 108 | int numValid = 0; 109 | 110 | // Compare current servers to previous identifiers 111 | for (const auto& server : currentServers) 112 | { 113 | if (server.isMember("HostName") && server.isMember("Platform") && server.isMember("Reachable")) 114 | { 115 | bool reachable = server["Reachable"].asBool(); 116 | if (reachable) 117 | numValid++; 118 | 119 | if (numServerUpdates > 0) // have we fetched server info before? 120 | { 121 | auto hostName = server["HostName"].asString(); 122 | 123 | if (!hostName.empty()) 124 | { 125 | std::string identifier = hostName + "_" + server["Platform"].asString(); 126 | 127 | bool ourLobby = !strncmp(hostName.c_str(), Game::SumoNet_OnlineUserName, 16); 128 | if (!ourLobby) 129 | { 130 | if (reachable && previousIdentifiers.find(identifier) == previousIdentifiers.end()) 131 | Notifications::instance.add(hostName + " started hosting a lobby!"); 132 | } 133 | else 134 | { 135 | if (*Game::SumoNet_CurNetDriver && (*Game::SumoNet_CurNetDriver)->is_hosting_online() && !Game::is_in_game()) 136 | Notifications::instance.add(reachable ? 137 | "Your lobby is active & accessible!" : 138 | "Your lobby cannot be reached by the master server!\n\nYou may need to setup port-forwarding for UDP ports 41455/41456/41457.", reachable ? 0 : 20); 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | // First update since game launch and we have some servers, write a notify about it 146 | if (numServerUpdates == 0 && numValid > 0) 147 | { 148 | if (numValid == 1) 149 | Notifications::instance.add("There is 1 online lobby active!"); 150 | else 151 | Notifications::instance.add("There are " + std::to_string(numValid) + " online lobbies active!"); 152 | } 153 | } 154 | 155 | public: 156 | static ServerUpdater instance; 157 | }; 158 | ServerUpdater ServerUpdater::instance; 159 | 160 | void ServerNotifications_Init() 161 | { 162 | ServerUpdater::instance.init(); 163 | } 164 | -------------------------------------------------------------------------------- /src/overlay/update_check.cpp: -------------------------------------------------------------------------------- 1 | #define WIN32_LEAN_AND_MEAN 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "plugin.hpp" 8 | #include 9 | #include "notifications.hpp" 10 | #include "resource.h" 11 | 12 | uint64_t VersionToInteger(const std::string& version) 13 | { 14 | std::vector parts; 15 | size_t start = 0; 16 | size_t end = 0; 17 | if (!version.empty() && version[0] == 'v') 18 | start = 1; 19 | while ((end = version.find('.', start)) != std::string::npos) 20 | { 21 | parts.push_back(std::stoi(version.substr(start, end - start))); 22 | start = end + 1; 23 | } 24 | 25 | parts.push_back(std::stoi(version.substr(start))); 26 | uint64_t value = 0; 27 | for (size_t i = 0; i < min(4, parts.size()); ++i) 28 | value |= (static_cast(parts[i]) << ((3 - i) * 16)); 29 | return value; 30 | } 31 | 32 | bool IsVersionNewer(const std::string& latest, const std::string& current) 33 | { 34 | return VersionToInteger(latest) > VersionToInteger(current); 35 | } 36 | 37 | // Function to check the latest release version 38 | std::string UpdateCheck_IsNewerAvailable(const std::string& currentVersion, const std::string& repoOwner, const std::string& repoName) 39 | { 40 | std::wstring path = L"/repos/" + std::wstring(repoOwner.begin(), repoOwner.end()) + 41 | L"/" + std::wstring(repoName.begin(), repoName.end()) + L"/releases/latest"; 42 | 43 | std::string jsonResponse = Util::HttpGetRequest("api.github.com", path, 443); 44 | if (jsonResponse.empty()) 45 | { 46 | spdlog::error("UpdateCheck_IsNewerAvailable: Failed to fetch the latest release information"); 47 | return ""; 48 | } 49 | 50 | // Parse JSON response 51 | Json::CharReaderBuilder builder; 52 | Json::Value root; 53 | std::string errs; 54 | 55 | std::istringstream stream(jsonResponse); 56 | if (!Json::parseFromStream(builder, stream, &root, &errs)) 57 | { 58 | spdlog::error("UpdateCheck_IsNewerAvailable: JSON parsing error: " + errs); 59 | return ""; 60 | } 61 | 62 | std::string latestVersion = root["tag_name"].asString(); 63 | if (latestVersion.empty()) 64 | { 65 | spdlog::error("UpdateCheck_IsNewerAvailable: Invalid JSON: Missing 'tag_name'."); 66 | return ""; 67 | } 68 | 69 | // Compare versions 70 | if (IsVersionNewer(latestVersion, currentVersion)) 71 | { 72 | spdlog::info("UpdateCheck_IsNewerAvailable: newer version {} available, current version {}", latestVersion, currentVersion); 73 | return latestVersion; 74 | } 75 | else 76 | { 77 | spdlog::info("UpdateCheck_IsNewerAvailable: latest version {} matches current version {}", latestVersion, currentVersion); 78 | return ""; 79 | } 80 | } 81 | 82 | void UpdateCheck_Thread(const std::string& currentVersion, const std::string& repoOwner, const std::string& repoName) 83 | { 84 | std::string newerVersion = UpdateCheck_IsNewerAvailable(currentVersion, repoOwner, repoName); 85 | if (!newerVersion.empty()) 86 | Notifications::instance.add(std::format("A newer version of OutRun2006Tweaks is available ({})\n---\nPress F11 and click here to visit release page.", newerVersion), 20, 87 | [newerVersion]() { 88 | std::string url = "https://github.com/emoose/OutRun2006Tweaks/releases"; 89 | ShellExecuteA(nullptr, "open", url.c_str(), 0, 0, SW_SHOWNORMAL); 90 | }); 91 | } 92 | 93 | std::thread updateCheckThread; 94 | 95 | void UpdateCheck_Init() 96 | { 97 | if (Overlay::NotifyUpdateCheck) 98 | { 99 | updateCheckThread = std::thread(&UpdateCheck_Thread, MODULE_VERSION_STR, "emoose", "OutRun2006Tweaks"); 100 | updateCheckThread.detach(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/plugin.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "game.hpp" 6 | 7 | extern void DInput_RegisterNewDevices(); // hooks_input.cpp 8 | extern void SetVibration(int userId, float leftMotor, float rightMotor); // hooks_forcefeedback.cpp 9 | extern void AudioHooks_Update(int numUpdates); // hooks_audio.cpp 10 | extern void CDSwitcher_ReadIni(const std::filesystem::path& iniPath); 11 | 12 | namespace Module 13 | { 14 | // Info about our module 15 | inline HMODULE DllHandle{ 0 }; 16 | inline std::filesystem::path DllPath{}; 17 | 18 | // Info about the module we've been loaded into 19 | inline HMODULE ExeHandle{ 0 }; 20 | inline std::filesystem::path ExePath{}; 21 | 22 | inline std::filesystem::path LogPath{}; 23 | inline std::filesystem::path IniPath{}; 24 | inline std::filesystem::path UserIniPath{}; 25 | inline std::filesystem::path LodIniPath{}; 26 | inline std::filesystem::path OverlayIniPath{}; 27 | inline std::filesystem::path BindingsIniPath{}; 28 | 29 | template 30 | inline T* exe_ptr(uintptr_t offset) { if (ExeHandle) return (T*)(((uintptr_t)ExeHandle) + offset); else return nullptr; } 31 | inline uint8_t* exe_ptr(uintptr_t offset) { if (ExeHandle) return (uint8_t*)(((uintptr_t)ExeHandle) + offset); else return nullptr; } 32 | 33 | template 34 | inline T fn_ptr(uintptr_t offset) { if (ExeHandle) return (T)(((uintptr_t)ExeHandle) + offset); else return nullptr; } 35 | 36 | // Deduce the type by providing it as an argument, no need for ugly decltype stuff 37 | template 38 | inline T fn_ptr(uintptr_t offset, T& var) 39 | { 40 | if (ExeHandle) 41 | return reinterpret_cast(((uintptr_t)ExeHandle) + offset); 42 | else 43 | return nullptr; 44 | } 45 | 46 | void init(); 47 | } 48 | 49 | namespace Game 50 | { 51 | enum class GamepadType 52 | { 53 | None, 54 | PC, 55 | Xbox, 56 | PS, 57 | Switch 58 | }; 59 | 60 | inline static const char* PadTypes[] = 61 | { 62 | "None", 63 | "PC", 64 | "Xbox", 65 | "PlayStation", 66 | "Switch" 67 | }; 68 | 69 | inline std::chrono::system_clock::time_point StartupTime; 70 | inline float DeltaTime = (1.f / 60.f); 71 | 72 | inline bool DrawDistanceDebugEnabled = false; 73 | 74 | inline GamepadType CurrentPadType = GamepadType::PC; 75 | inline GamepadType ForcedPadType = GamepadType::None; 76 | }; 77 | 78 | namespace Settings 79 | { 80 | inline int FramerateLimit = 60; 81 | inline bool FramerateLimitMode = 0; 82 | inline int FramerateFastLoad = 3; 83 | inline bool FramerateUnlockExperimental = true; 84 | inline int VSync = 1; 85 | inline bool SingleCoreAffinity = true; 86 | 87 | inline bool WindowedBorderless = true; 88 | inline int WindowPositionX = 0; 89 | inline int WindowPositionY = 0; 90 | inline bool WindowedHideMouseCursor = true; 91 | inline bool DisableDPIScaling = true; 92 | inline bool AutoDetectResolution = true; 93 | 94 | inline bool AllowHorn = true; 95 | inline bool AllowWAV = true; 96 | inline bool AllowFLAC = true; 97 | 98 | inline bool CDSwitcherEnable = false; 99 | inline bool CDSwitcherDisplayTitle = true; 100 | inline int CDSwitcherTitleFont = 2; 101 | inline float CDSwitcherTitleFontSizeX = 0.3f; 102 | inline float CDSwitcherTitleFontSizeY = 0.5f; 103 | inline int CDSwitcherTitlePositionX = 375; 104 | inline int CDSwitcherTitlePositionY = 450; 105 | inline bool CDSwitcherShuffleTracks = false; 106 | inline std::string CDSwitcherTrackNext = "Back"; 107 | inline std::string CDSwitcherTrackPrevious = "RS+Back"; 108 | 109 | inline std::vector> CDTracks; 110 | 111 | inline int UIScalingMode = 1; 112 | inline int UILetterboxing = 1; 113 | inline int AnisotropicFiltering = 16; 114 | inline int ReflectionResolution = 2048; 115 | inline bool UseHiDefCharacters = true; 116 | inline bool TransparencySupersampling = true; 117 | inline bool ScreenEdgeCullFix = true; 118 | inline bool DisableVehicleLODs = true; 119 | inline bool DisableStageCulling = true; 120 | inline bool FixZBufferPrecision = true; 121 | inline float CarBaseShadowOpacity = 1.0f; 122 | inline int DrawDistanceIncrease = 0; 123 | inline int DrawDistanceBehind = 0; 124 | 125 | inline std::string TextureBaseFolder = "textures"; 126 | inline bool SceneTextureReplacement = true; 127 | inline bool SceneTextureExtract = false; 128 | inline bool UITextureReplacement = true; 129 | inline bool UITextureExtract = false; 130 | inline bool EnableTextureCache = true; 131 | inline bool UseNewTextureAllocator = true; 132 | 133 | inline bool UseNewInput = false; 134 | inline float SteeringDeadZone = 0.2f; 135 | inline bool ControllerHotPlug = false; 136 | inline bool DefaultManualTransmission = false; 137 | inline std::string HudToggleKey = ""; 138 | inline int VibrationMode = 0; 139 | inline int VibrationStrength = 10; 140 | inline int VibrationControllerId = 0; 141 | inline int ImpulseVibrationMode = 0; 142 | inline float ImpulseVibrationLeftMultiplier = 0.25f; 143 | inline float ImpulseVibrationRightMultiplier = 0.25f; 144 | 145 | inline int EnableHollyCourse2 = 1; 146 | inline bool SkipIntroLogos = false; 147 | inline bool DisableCountdownTimer = false; 148 | inline bool EnableLevelSelect = false; 149 | inline bool RestoreJPClarissa = false; 150 | inline bool ShowOutRunMilesOnMenu = true; 151 | inline bool AllowCharacterSelection = false; 152 | inline bool RandomHighwayAnimSets = false; 153 | inline std::string DemonwareServerOverride = "clarissa.port0.org"; 154 | inline bool ProtectLoginData = true; 155 | 156 | inline bool OverlayEnabled = true; 157 | 158 | inline bool FixPegasusClopping = true; 159 | inline bool FixRightSideBunkiAnimations = true; 160 | inline bool FixC2CRankings = true; 161 | inline bool PreventDESTSaveCorruption = true; 162 | inline bool FixLensFlarePath = true; 163 | inline bool FixIncorrectShading = true; 164 | inline bool FixParticleRendering = true; 165 | inline bool FixFullPedalChecks = true; 166 | inline bool HideOnlineSigninText = true; 167 | } 168 | 169 | namespace Util 170 | { 171 | std::string HttpGetRequest(const std::string& host, const std::wstring& path, int portNum = 80); // network.cpp 172 | 173 | inline uint32_t GetModuleTimestamp(HMODULE moduleHandle) 174 | { 175 | if (!moduleHandle) 176 | return 0; 177 | 178 | uint8_t* moduleData = (uint8_t*)moduleHandle; 179 | const IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)moduleData; 180 | const IMAGE_NT_HEADERS* ntHeaders = (IMAGE_NT_HEADERS*)(moduleData + dosHeader->e_lfanew); 181 | return ntHeaders->FileHeader.TimeDateStamp; 182 | } 183 | 184 | // Fetches path of module as std::filesystem::path, resizing buffer automatically if path length above MAX_PATH 185 | inline std::filesystem::path GetModuleFilePath(HMODULE moduleHandle) 186 | { 187 | std::vector buffer(MAX_PATH, L'\0'); 188 | 189 | DWORD result = GetModuleFileNameW(moduleHandle, buffer.data(), buffer.size()); 190 | while (GetLastError() == ERROR_INSUFFICIENT_BUFFER) 191 | { 192 | // Buffer was too small, resize and try again 193 | buffer.resize(buffer.size() * 2, L'\0'); 194 | result = GetModuleFileNameW(moduleHandle, buffer.data(), buffer.size()); 195 | } 196 | 197 | return std::wstring(buffer.data(), result); 198 | } 199 | 200 | inline uint32_t BitCount(uint32_t n) 201 | { 202 | n = n - ((n >> 1) & 0x55555555); // put count of each 2 bits into those 2 bits 203 | n = (n & 0x33333333) + ((n >> 2) & 0x33333333); // put count of each 4 bits into those 4 bits 204 | n = (n + (n >> 4)) & 0x0F0F0F0F; // put count of each 8 bits into those 8 bits 205 | n = n + (n >> 8); // put count of each 16 bits into their lowest 8 bits 206 | n = n + (n >> 16); // put count of each 32 bits into their lowest 8 bits 207 | return n & 0x0000003F; // return the count 208 | } 209 | 210 | // Function to trim spaces from the start of a string 211 | inline std::string ltrim(const std::string& s) 212 | { 213 | auto start = std::find_if_not(s.begin(), s.end(), [](unsigned char ch) 214 | { 215 | return std::isspace(ch); 216 | }); 217 | return std::string(start, s.end()); 218 | } 219 | 220 | // Function to trim spaces from the end of a string 221 | inline std::string rtrim(const std::string& s) 222 | { 223 | auto end = std::find_if_not(s.rbegin(), s.rend(), [](unsigned char ch) 224 | { 225 | return std::isspace(ch); 226 | }); 227 | return std::string(s.begin(), end.base()); 228 | } 229 | 230 | // Function to trim spaces from both ends of a string 231 | inline std::string trim(const std::string& s) 232 | { 233 | return ltrim(rtrim(s)); 234 | } 235 | } 236 | 237 | inline void WaitForDebugger() 238 | { 239 | #ifdef _DEBUG 240 | while (!IsDebuggerPresent()) 241 | { 242 | } 243 | #endif 244 | } 245 | -------------------------------------------------------------------------------- /src/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Resource.rc 4 | 5 | // Next default values for new objects 6 | // 7 | #ifdef APSTUDIO_INVOKED 8 | #ifndef APSTUDIO_READONLY_SYMBOLS 9 | #define _APS_NEXT_RESOURCE_VALUE 101 10 | #define _APS_NEXT_COMMAND_VALUE 40001 11 | #define _APS_NEXT_CONTROL_VALUE 1001 12 | #define _APS_NEXT_SYMED_VALUE 101 13 | #endif 14 | #endif 15 | 16 | #define MODULE_VERSION_MAJOR 0 17 | #define MODULE_VERSION_MINOR 6 18 | #define MODULE_VERSION_BUILD 1 19 | #define MODULE_VERSION_REVISION 0 20 | 21 | #define STR(value) #value 22 | #define STRINGIZE(value) STR(value) 23 | #define MODULE_VERSION_STR \ 24 | STRINGIZE(MODULE_VERSION_MAJOR) "." \ 25 | STRINGIZE(MODULE_VERSION_MINOR) "." \ 26 | STRINGIZE(MODULE_VERSION_BUILD) "." \ 27 | STRINGIZE(MODULE_VERSION_REVISION) 28 | --------------------------------------------------------------------------------