├── .gitignore ├── .vscode ├── c_cpp_properties.json ├── settings.json └── tasks.json ├── CMakeLists.txt ├── LICENSE ├── README.md ├── TODO.md ├── build.ps1 ├── copy.ps1 ├── createqmod.ps1 ├── extern.cmake ├── include ├── EventTypes.hpp ├── Recording │ ├── NoteEventRecorder.hpp │ ├── ObstacleEventRecorder.hpp │ ├── PlayerRecorder.hpp │ └── ReplayRecorder.hpp ├── ReplayManager.hpp ├── Replaying │ ├── NoteEventReplayer.hpp │ ├── ObstacleEventReplayer.hpp │ ├── PlayerReplayer.hpp │ └── Replayer.hpp ├── Sprites.hpp ├── UI │ ├── ReplayViewController.hpp │ └── UIManager.hpp ├── Utils │ ├── FileUtils.hpp │ ├── FindComponentsUtils.hpp │ ├── MathUtils.hpp │ ├── ModifiersUtils.hpp │ ├── ReplayUtils.hpp │ ├── SaberUtils.hpp │ ├── SongUtils.hpp │ ├── TimeUtils.hpp │ ├── TypeUtils.hpp │ ├── UIUtils.hpp │ └── UnityUtils.hpp ├── hooks.hpp └── static-defines.hpp ├── ndk-stack.ps1 ├── qpm.json ├── qpm_defines.cmake └── src ├── Hooks ├── AudioTimeSyncController.cpp ├── HapticFeedbackController.cpp ├── Notes │ ├── BombNoteController.cpp │ ├── CutScoreBuffer.cpp │ ├── FlyingScoreEffect.cpp │ ├── GameNoteController.cpp │ ├── GoodCutScoringElement.cpp │ ├── NoteController.cpp │ ├── NoteCutter.cpp │ ├── RelativeScoreAndImmediateRankCounter.cpp │ └── SaberSwingRatingCounter.cpp ├── Obstacles │ └── GameEnergyCounter.cpp ├── PauseController.cpp ├── Player │ ├── PlayerTransforms.cpp │ └── SaberMovementData.cpp ├── ResultsViewController.cpp ├── SaberManager.cpp ├── ScoreController.cpp ├── SinglePlayerLevelSelectionFlowCoordinator.cpp ├── SoloFreePlayFlowController.cpp ├── StandardLevelDetailView.cpp ├── StandardLevelGameplayManager.cpp └── StandardLevelScenesTransitionSetupDataSO.cpp ├── Recording ├── NoteEventRecorder.cpp ├── ObstacleEventRecorder.cpp ├── PlayerRecorder.cpp └── ReplayRecorder.cpp ├── ReplayManager.cpp ├── Replaying ├── NoteEventReplayer.cpp ├── ObstacleEventReplayer.cpp ├── PlayerReplayer.cpp └── Replayer.cpp ├── UI ├── ReplayViewController.cpp └── UIManager.cpp ├── Utils ├── FindComponentsUtils.cpp └── SongUtils.cpp └── main.cpp /.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 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # VSCode config stuff 35 | !.vscode/c_cpp_properties.json 36 | !.vscode/tasks.json 37 | 38 | # NDK stuff 39 | out/ 40 | [Ll]ib/ 41 | [Ll]ibs/ 42 | [Oo]bj/ 43 | [Oo]bjs/ 44 | ndkpath.txt 45 | *.zip 46 | *.txt 47 | 48 | extern 49 | 50 | build/ 51 | 52 | qpm.shared.json 53 | 54 | Android.mk.backup 55 | 56 | *.qmod -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "defines": [ 5 | "__GNUC__", 6 | "__aarch64__" 7 | ], 8 | "includePath": [ 9 | "${workspaceFolder}/**", 10 | "${workspaceFolder}/include/**", 11 | "${workspaceFolder}/shared/**", 12 | "${workspaceFolder}/extern/**", 13 | "${workspaceFolder}/extern/codegen/include/**", 14 | "${workspaceFolder}/extern/ffmpeg/**", 15 | "c:/Users/henwi/Desktop/ModdingBeatSaber/android-ndk-r21d/**" 16 | ], 17 | "name": "Quest", 18 | "cStandard": "c11", 19 | "cppStandard": "c++20", 20 | "intelliSenseMode": "clang-x64" 21 | } 22 | ], 23 | "version": 4 24 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "iosfwd": "cpp", 4 | "__config": "cpp", 5 | "__nullptr": "cpp", 6 | "thread": "cpp", 7 | "any": "cpp", 8 | "deque": "cpp", 9 | "list": "cpp", 10 | "map": "cpp", 11 | "optional": "cpp", 12 | "queue": "cpp", 13 | "set": "cpp", 14 | "stack": "cpp", 15 | "unordered_map": "cpp", 16 | "unordered_set": "cpp", 17 | "variant": "cpp", 18 | "vector": "cpp", 19 | "__bit_reference": "cpp", 20 | "__debug": "cpp", 21 | "__errc": "cpp", 22 | "__functional_base": "cpp", 23 | "__hash_table": "cpp", 24 | "__locale": "cpp", 25 | "__mutex_base": "cpp", 26 | "__node_handle": "cpp", 27 | "__split_buffer": "cpp", 28 | "__string": "cpp", 29 | "__threading_support": "cpp", 30 | "__tree": "cpp", 31 | "__tuple": "cpp", 32 | "algorithm": "cpp", 33 | "array": "cpp", 34 | "atomic": "cpp", 35 | "bit": "cpp", 36 | "bitset": "cpp", 37 | "cctype": "cpp", 38 | "cfenv": "cpp", 39 | "charconv": "cpp", 40 | "chrono": "cpp", 41 | "cinttypes": "cpp", 42 | "clocale": "cpp", 43 | "cmath": "cpp", 44 | "codecvt": "cpp", 45 | "compare": "cpp", 46 | "complex": "cpp", 47 | "condition_variable": "cpp", 48 | "csetjmp": "cpp", 49 | "csignal": "cpp", 50 | "cstdarg": "cpp", 51 | "cstddef": "cpp", 52 | "cstdint": "cpp", 53 | "cstdio": "cpp", 54 | "cstdlib": "cpp", 55 | "cstring": "cpp", 56 | "ctime": "cpp", 57 | "cwchar": "cpp", 58 | "cwctype": "cpp", 59 | "exception": "cpp", 60 | "coroutine": "cpp", 61 | "propagate_const": "cpp", 62 | "forward_list": "cpp", 63 | "fstream": "cpp", 64 | "functional": "cpp", 65 | "future": "cpp", 66 | "initializer_list": "cpp", 67 | "iomanip": "cpp", 68 | "ios": "cpp", 69 | "iostream": "cpp", 70 | "istream": "cpp", 71 | "iterator": "cpp", 72 | "limits": "cpp", 73 | "locale": "cpp", 74 | "memory": "cpp", 75 | "mutex": "cpp", 76 | "new": "cpp", 77 | "numeric": "cpp", 78 | "ostream": "cpp", 79 | "random": "cpp", 80 | "ratio": "cpp", 81 | "regex": "cpp", 82 | "scoped_allocator": "cpp", 83 | "span": "cpp", 84 | "sstream": "cpp", 85 | "stdexcept": "cpp", 86 | "streambuf": "cpp", 87 | "string": "cpp", 88 | "string_view": "cpp", 89 | "strstream": "cpp", 90 | "system_error": "cpp", 91 | "tuple": "cpp", 92 | "type_traits": "cpp", 93 | "typeindex": "cpp", 94 | "typeinfo": "cpp", 95 | "utility": "cpp", 96 | "valarray": "cpp", 97 | "xstring": "cpp", 98 | "xlocale": "cpp", 99 | "xlocbuf": "cpp", 100 | "concepts": "cpp", 101 | "filesystem": "cpp", 102 | "shared_mutex": "cpp", 103 | "xfacet": "cpp", 104 | "xhash": "cpp", 105 | "xiosbase": "cpp", 106 | "xlocinfo": "cpp", 107 | "xlocmes": "cpp", 108 | "xlocmon": "cpp", 109 | "xlocnum": "cpp", 110 | "xloctime": "cpp", 111 | "xmemory": "cpp", 112 | "xstddef": "cpp", 113 | "xtr1common": "cpp", 114 | "xtree": "cpp", 115 | "xutility": "cpp", 116 | "hash_map": "cpp", 117 | "hash_set": "cpp", 118 | "*.tcc": "cpp", 119 | "memory_resource": "cpp", 120 | "ranges": "cpp", 121 | "stop_token": "cpp", 122 | "resumable": "cpp", 123 | "xthread": "cpp", 124 | "xmemory0": "cpp" 125 | }, 126 | "C_Cpp.errorSquiggles": "Disabled", 127 | "cmake.configureOnOpen": true 128 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "NDK Build", 6 | "detail": "Builds the library using ndk-build.cmd", 7 | "type": "shell", 8 | "command": "ndk-build", 9 | "windows": { 10 | "command": "ndk-build.cmd" 11 | }, 12 | "args": ["NDK_PROJECT_PATH=.", "APP_BUILD_SCRIPT=./Android.mk", "NDK_APPLICATION_MK=./Application.mk"], 13 | "group": "build", 14 | "options": { 15 | "env": {} 16 | } 17 | }, 18 | { 19 | "label": "Powershell Build", 20 | "detail": "Builds the library using Powershell (recommended)", 21 | "type": "shell", 22 | "command": "./build.ps1", 23 | "windows": { 24 | "command": "./build.ps1" 25 | }, 26 | "group": { 27 | "kind": "build", 28 | "isDefault": true 29 | }, 30 | "options": { 31 | "env": {} 32 | } 33 | }, 34 | { 35 | "label": "Powershell Build and Copy", 36 | "detail": "Builds and copies the library to the Quest using adb and force-quits Beat Saber", 37 | "type": "shell", 38 | "command": "./copy.ps1", 39 | "windows": { 40 | "command": "./copy.ps1" 41 | }, 42 | "group": "build", 43 | "options": { 44 | "env": {} 45 | } 46 | }, 47 | { 48 | "label": "BMBF Build", 49 | "detail": "Builds a .zip to be uploaded into BMBF", 50 | "type": "shell", 51 | "command": "./buildBMBF.ps1", 52 | "windows": { 53 | "command": "./buildBMBF.ps1" 54 | }, 55 | "args": [], 56 | "group": "build", 57 | "options": { 58 | "env": {} 59 | } 60 | }, 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | # include some defines automatically made by qpm 3 | include(qpm_defines.cmake) 4 | 5 | cmake_minimum_required(VERSION 3.21) 6 | project(${COMPILE_ID}) 7 | 8 | # c++ standard 9 | set(CMAKE_CXX_STANDARD 20) 10 | set(CMAKE_CXX_STANDARD_REQUIRED 20) 11 | 12 | # define that stores the actual source directory 13 | set(SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src) 14 | set(INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include) 15 | 16 | # compile options used 17 | add_compile_options(-frtti -fexceptions) 18 | add_compile_options(-O3) 19 | # compile definitions used 20 | add_compile_definitions(VERSION=\"${MOD_VERSION}\") 21 | add_compile_definitions(ID=\"${MOD_ID}\") 22 | add_compile_definitions(MOD_ID=\"${MOD_ID}\") 23 | add_compile_definitions(COMPILE_ID=\"replay\") 24 | 25 | # recursively get all src files 26 | RECURSE_FILES(cpp_file_list ${SOURCE_DIR}/*.cpp) 27 | RECURSE_FILES(c_file_list ${SOURCE_DIR}/*.c) 28 | 29 | # add all src files to compile 30 | add_library( 31 | ${COMPILE_ID} 32 | SHARED 33 | ${cpp_file_list} 34 | ${c_file_list} 35 | ) 36 | 37 | target_include_directories(${COMPILE_ID} PRIVATE .) 38 | 39 | # add src dir as include dir 40 | target_include_directories(${COMPILE_ID} PRIVATE ${SOURCE_DIR}) 41 | # add include dir as include dir 42 | target_include_directories(${COMPILE_ID} PRIVATE ${INCLUDE_DIR}) 43 | # add shared dir as include dir 44 | target_include_directories(${COMPILE_ID} PUBLIC ${SHARED_DIR}) 45 | # codegen includes 46 | target_include_directories(${COMPILE_ID} PRIVATE ${EXTERN_DIR}/includes/${CODEGEN_ID}/include) 47 | # rapidjson includes 48 | target_include_directories(${COMPILE_ID} PRIVATE ${EXTERN_DIR}/includes/beatsaber-hook/shared/rapidjson/include) 49 | 50 | target_link_libraries(${COMPILE_ID} PRIVATE -llog) 51 | # add extern stuff like libs and other includes 52 | include(extern.cmake) 53 | 54 | add_custom_command(TARGET ${COMPILE_ID} POST_BUILD 55 | COMMAND ${CMAKE_STRIP} -d --strip-all 56 | "lib${COMPILE_ID}.so" -o "stripped_lib${COMPILE_ID}.so" 57 | COMMENT "Strip debug symbols done on final binary.") 58 | 59 | add_custom_command(TARGET ${COMPILE_ID} POST_BUILD 60 | COMMAND ${CMAKE_COMMAND} -E make_directory debug 61 | COMMENT "Rename the lib to debug_ since it has debug symbols" 62 | ) 63 | 64 | add_custom_command(TARGET ${COMPILE_ID} POST_BUILD 65 | COMMAND ${CMAKE_COMMAND} -E rename lib${COMPILE_ID}.so debug/lib${COMPILE_ID}.so 66 | COMMENT "Rename the lib to debug_ since it has debug symbols" 67 | ) 68 | 69 | add_custom_command(TARGET ${COMPILE_ID} POST_BUILD 70 | COMMAND ${CMAKE_COMMAND} -E rename stripped_lib${COMPILE_ID}.so lib${COMPILE_ID}.so 71 | COMMENT "Rename the stripped lib to regular" 72 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Replay 2 | 3 | ## Credits 4 | 5 | * [zoller27osu](https://github.com/zoller27osu), [Sc2ad](https://github.com/Sc2ad) and [jakibaki](https://github.com/jakibaki) - [beatsaber-hook](https://github.com/sc2ad/beatsaber-hook) 6 | * [raftario](https://github.com/raftario) - [vscode-bsqm](https://github.com/raftario/vscode-bsqm) and [this template](https://github.com/raftario/bmbf-mod-template) 7 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | - Fix score not longer replaying as the exact same (might be because of obstacles) 3 | - Fix obstacle interaction not working 4 | - Make percent in ui be greener closer to 100% 5 | - After watching a replay and then playing a map modifiers are still what they were in the replay 6 | - Replay menu in level succeed and failed screens, can choose to overwrite current replay or watch the last play (saved in temp file) 7 | - Get note controller from https://discord.com/channels/441805394323439646/864240224400572467/939952451560288336 8 | - Set obstacle player interaction count manually to make more accurate 9 | - Time manipulation 10 | - UI for the above sad 11 | - Speed manipulation 12 | - more ui 13 | - Add config file with utils 14 | - Check replay edge cases and ensure correct total score 15 | - Add camera manipulation 16 | - check jump y offset 17 | - Add Avatar stuff 18 | - Add hollywood 19 | - Make fancy hollywood ui 20 | - Mux audio in game for mp4 21 | - Make hollywood render faster? 22 | - Upload mp4 from quest to youtube???? 23 | 24 | # UI Page Ideas 25 | ## Center View Controller 26 | - openable graph of percentage and energy throughout play 27 | 28 | - Camera drop down menu? 29 | 30 | ## Right View Controller (Camera Manipulation) 31 | - drop down menu, each camera type has different ui 32 | 33 | ## Left View Controller (Hollywood) 34 | - fps 35 | - resolution 36 | - fov 37 | - bitrate 38 | 39 | Based on above settings: 40 | - approximate render time 41 | - approximate file size 42 | 43 | - Slider to choose when to start and end replay 44 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | Param( 2 | [Parameter(Mandatory=$false)] 3 | [Switch]$clean 4 | ) 5 | 6 | # if user specified clean, remove all build files 7 | if ($clean.IsPresent) 8 | { 9 | if (Test-Path -Path "build") 10 | { 11 | remove-item build -R 12 | } 13 | } 14 | 15 | $NDKPath = Get-Content $PSScriptRoot/ndkpath.txt 16 | 17 | if (($clean.IsPresent) -or (-not (Test-Path -Path "build"))) 18 | { 19 | $out = new-item -Path build -ItemType Directory 20 | } 21 | 22 | cd build 23 | & cmake -G "Ninja" -DCMAKE_BUILD_TYPE="RelWithDebInfo" ../ 24 | & cmake --build . -j 8 25 | cd .. -------------------------------------------------------------------------------- /copy.ps1: -------------------------------------------------------------------------------- 1 | & $PSScriptRoot/build.ps1 2 | if ($?) { 3 | adb push build/libreplay.so /sdcard/Android/data/com.beatgames.beatsaber/files/mods/libreplay.so 4 | if ($?) { 5 | adb shell am force-stop com.beatgames.beatsaber 6 | adb shell am start com.beatgames.beatsaber/com.unity3d.player.UnityPlayerActivity 7 | if ($args[0] -eq "--log") { 8 | $timestamp = Get-Date -Format "MM-dd HH:mm:ss.fff" 9 | adb logcat -c 10 | adb logcat -T "$timestamp" main-modloader:W QuestHook[Chroma`|v0.1.0]:* QuestHook[UtilsLogger`|v1.0.12]:* AndroidRuntime:E *:S 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /createqmod.ps1: -------------------------------------------------------------------------------- 1 | Param( 2 | [String]$qmodname="replay", 3 | [Parameter(Mandatory=$false)] 4 | [Switch]$clean 5 | ) 6 | 7 | if ($qmodName -eq "") 8 | { 9 | echo "Give a proper qmod name and try again" 10 | exit 11 | } 12 | $mod = "./mod.json" 13 | $modJson = Get-Content $mod -Raw | ConvertFrom-Json 14 | 15 | $filelist = @($mod) 16 | 17 | $cover = "./" + $modJson.coverImage 18 | if ((-not ($cover -eq "./")) -and (Test-Path $cover)) 19 | { 20 | $filelist += ,$cover 21 | } 22 | 23 | foreach ($mod in $modJson.modFiles) 24 | { 25 | $path = "./build/" + $mod 26 | if (-not (Test-Path $path)) 27 | { 28 | $path = "./extern/libs/" + $mod 29 | } 30 | $filelist += $path 31 | } 32 | 33 | foreach ($lib in $modJson.libraryFiles) 34 | { 35 | $path = "./extern/libs/" + $lib 36 | if (-not (Test-Path $path)) 37 | { 38 | $path = "./build/" + $lib 39 | } 40 | $filelist += $path 41 | } 42 | 43 | $zip = $qmodName + ".zip" 44 | $qmod = $qmodName + ".qmod" 45 | 46 | if ((-not ($clean.IsPresent)) -and (Test-Path $qmod)) 47 | { 48 | echo "Making Clean Qmod" 49 | Move-Item $qmod $zip -Force 50 | } 51 | 52 | Compress-Archive -Path $filelist -DestinationPath $zip -Update 53 | Move-Item $zip $qmod -Force -------------------------------------------------------------------------------- /extern.cmake: -------------------------------------------------------------------------------- 1 | # YOU SHOULD NOT MANUALLY EDIT THIS FILE, QPM WILL VOID ALL CHANGES 2 | # always added 3 | target_include_directories(${COMPILE_ID} PRIVATE ${EXTERN_DIR}/includes) 4 | target_include_directories(${COMPILE_ID} SYSTEM PRIVATE ${EXTERN_DIR}/includes/libil2cpp/il2cpp/libil2cpp) 5 | 6 | # includes and compile options added by other libraries 7 | RECURSE_FILES(src_inline_hook_beatsaber_hook_local_extra_c ${EXTERN_DIR}/includes/beatsaber-hook/src/inline-hook/*.c) 8 | RECURSE_FILES(src_inline_hook_beatsaber_hook_local_extra_cpp ${EXTERN_DIR}/includes/beatsaber-hook/src/inline-hook/*.cpp) 9 | target_sources(${COMPILE_ID} PRIVATE ${src_inline_hook_beatsaber_hook_local_extra_c}) 10 | target_sources(${COMPILE_ID} PRIVATE ${src_inline_hook_beatsaber_hook_local_extra_cpp}) 11 | target_include_directories(${COMPILE_ID} SYSTEM PRIVATE ${EXTERN_DIR}/includes/questui/shared/cppcodec) 12 | 13 | # libs dir -> stores .so or .a files (or symlinked!) 14 | target_link_directories(${COMPILE_ID} PRIVATE ${EXTERN_DIR}/libs) 15 | RECURSE_FILES(so_list ${EXTERN_DIR}/libs/*.so) 16 | RECURSE_FILES(a_list ${EXTERN_DIR}/libs/*.a) 17 | 18 | # every .so or .a that needs to be linked, put here! 19 | # I don't believe you need to specify if a lib is static or not, poggers! 20 | target_link_libraries(${COMPILE_ID} PRIVATE 21 | ${so_list} 22 | ${a_list} 23 | ) 24 | -------------------------------------------------------------------------------- /include/EventTypes.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "GlobalNamespace/NoteData.hpp" 5 | #include "GlobalNamespace/NoteLineLayer.hpp" 6 | #include "GlobalNamespace/ColorType.hpp" 7 | #include "GlobalNamespace/NoteCutDirection.hpp" 8 | #include "GlobalNamespace/NoteCutInfo.hpp" 9 | #include "GlobalNamespace/NoteController.hpp" 10 | #include "GlobalNamespace/SaberSwingRatingCounter.hpp" 11 | #include "GlobalNamespace/CutScoreBuffer.hpp" 12 | #include "UnityEngine/Transform.hpp" 13 | #include "UnityEngine/Quaternion.hpp" 14 | #include 15 | 16 | // avoid using namespace in headers, but do as you wish 17 | using namespace GlobalNamespace; 18 | 19 | namespace Replay { 20 | namespace PlayerEventTypes { 21 | struct EulerTransform { 22 | UnityEngine::Vector3 position; 23 | UnityEngine::Vector3 rotation; 24 | 25 | constexpr EulerTransform() = default; 26 | 27 | constexpr EulerTransform(const UnityEngine::Vector3 &position, const UnityEngine::Vector3 &rotation) : position( 28 | position), rotation(rotation) {} 29 | }; 30 | 31 | struct EulerTransformEvent { 32 | float time; 33 | EulerTransform transform; 34 | 35 | constexpr EulerTransformEvent() = default; 36 | 37 | constexpr EulerTransformEvent(const float time, const EulerTransform& transform) : time(time), transform(transform) {} 38 | 39 | void Write(std::ofstream& writer) const { 40 | writer.write(reinterpret_cast(&time), sizeof(float)); 41 | writer.write(reinterpret_cast(&transform), sizeof(EulerTransform)); 42 | } 43 | 44 | constexpr EulerTransformEvent(std::ifstream& reader) { 45 | reader.read(reinterpret_cast(&time), sizeof(float)); 46 | reader.read(reinterpret_cast(&transform), sizeof(EulerTransform)); 47 | } 48 | }; 49 | 50 | const inline static byte headEventID = 0b00000000; 51 | 52 | const inline static byte leftSaberEventID = 0b00000001; 53 | 54 | const inline static byte rightSaberEventID = 0b00000010; 55 | 56 | struct PlayerTransforms { 57 | EulerTransform head; 58 | EulerTransform leftSaber; 59 | EulerTransform rightSaber; 60 | 61 | constexpr PlayerTransforms() = default; 62 | 63 | constexpr PlayerTransforms(const EulerTransform &head, const EulerTransform &leftSaber, 64 | const EulerTransform &rightSaber) : head(head), leftSaber(leftSaber), 65 | rightSaber(rightSaber) {} 66 | 67 | constexpr PlayerTransforms(UnityEngine::Transform* headTransform, UnityEngine::Transform* leftTransform, UnityEngine::Transform* rightTransform) { 68 | head = {headTransform->get_position(), headTransform->get_eulerAngles()}; 69 | leftSaber = {leftTransform->get_position(), leftTransform->get_eulerAngles()}; 70 | rightSaber = {rightTransform->get_position(), rightTransform->get_eulerAngles()}; 71 | } 72 | 73 | constexpr PlayerTransforms(UnityEngine::Vector3 headPos, UnityEngine::Quaternion headRot, UnityEngine::Vector3 leftPos, UnityEngine::Quaternion leftRot, UnityEngine::Vector3 rightPos, UnityEngine::Quaternion rightRot) { 74 | head = {headPos, headRot.get_eulerAngles()}; 75 | leftSaber = {leftPos, leftRot.get_eulerAngles()}; 76 | rightSaber = {rightPos, rightRot.get_eulerAngles()}; 77 | } 78 | }; 79 | 80 | struct PlayerEvent { 81 | float time; 82 | PlayerTransforms playerTransforms; 83 | UnityEngine::Vector3 leftSaberTopPos; 84 | UnityEngine::Vector3 rightSaberTopPos; 85 | 86 | constexpr PlayerEvent() = default; 87 | 88 | constexpr PlayerEvent(const float time, const PlayerTransforms& playerTransforms, const UnityEngine::Vector3& leftSaberTopPos, const UnityEngine::Vector3& rightSaberTopPos) : 89 | time(time), playerTransforms(playerTransforms), leftSaberTopPos(leftSaberTopPos), rightSaberTopPos(rightSaberTopPos) {} 90 | }; 91 | } 92 | 93 | namespace NoteEventTypes { 94 | struct SimpleNoteCutInfo { 95 | bool speedOK; 96 | bool directionOK; 97 | bool saberTypeOK; 98 | bool wasCutTooSoon; 99 | float saberSpeed; 100 | UnityEngine::Vector3 saberDir; 101 | int saberType; 102 | float timeDeviation; 103 | float cutDirDeviation; 104 | UnityEngine::Vector3 cutPoint; 105 | UnityEngine::Vector3 cutNormal; 106 | float cutDistanceToCenter; 107 | float cutAngle; 108 | UnityEngine::Vector3 worldRotation; 109 | UnityEngine::Vector3 noteRotation; 110 | UnityEngine::Vector3 notePosition; 111 | 112 | constexpr SimpleNoteCutInfo() = default; 113 | 114 | SimpleNoteCutInfo(NoteCutInfo& noteCutInfo) { 115 | speedOK = noteCutInfo.speedOK; 116 | directionOK = noteCutInfo.directionOK; 117 | saberTypeOK = noteCutInfo.saberTypeOK; 118 | wasCutTooSoon = noteCutInfo.wasCutTooSoon; 119 | saberSpeed = noteCutInfo.saberSpeed; 120 | saberDir = noteCutInfo.saberDir; 121 | saberType = (int) noteCutInfo.saberType; 122 | timeDeviation = noteCutInfo.timeDeviation; 123 | cutDirDeviation = noteCutInfo.cutDirDeviation; 124 | cutPoint = noteCutInfo.cutPoint; 125 | cutNormal = noteCutInfo.cutNormal; 126 | cutDistanceToCenter = noteCutInfo.cutDistanceToCenter; 127 | cutAngle = noteCutInfo.cutAngle; 128 | worldRotation = noteCutInfo.worldRotation.get_eulerAngles(); 129 | noteRotation = noteCutInfo.noteRotation.get_eulerAngles(); 130 | notePosition = noteCutInfo.notePosition; 131 | } 132 | 133 | bool AllIsOkay() { 134 | return speedOK && directionOK && saberTypeOK && !wasCutTooSoon; 135 | } 136 | }; 137 | 138 | struct SwingRating { 139 | int beforeCutRating; 140 | int afterCutRating; 141 | 142 | constexpr SwingRating() = default; 143 | 144 | constexpr SwingRating(CutScoreBuffer* cutScoreBuffer) { 145 | beforeCutRating = cutScoreBuffer->get_beforeCutScore(); 146 | afterCutRating = cutScoreBuffer->get_afterCutScore(); 147 | } 148 | 149 | constexpr SwingRating(int beforeCutRating, int afterCutRating) : beforeCutRating(beforeCutRating), afterCutRating(afterCutRating) {} 150 | }; 151 | 152 | struct NoteCutEvent { 153 | int noteHash; 154 | float time; 155 | SimpleNoteCutInfo noteCutInfo; 156 | SwingRating swingRating; 157 | 158 | constexpr NoteCutEvent() = default; 159 | 160 | NoteCutEvent(int noteHash, float time, CutScoreBuffer* cutScoreBuffer) : noteHash(noteHash), time(time) { 161 | // I did not know a better way to make compiler happy, feel free to fix 162 | SimpleNoteCutInfo newNoteCutInfo(cutScoreBuffer->noteCutInfo); 163 | noteCutInfo = newNoteCutInfo; 164 | 165 | SwingRating newSwingRating(cutScoreBuffer); 166 | swingRating = newSwingRating; 167 | } 168 | 169 | NoteCutEvent(int noteHash, float time, NoteCutInfo badNoteCutInfo) : noteHash(noteHash), time(time) { 170 | // I did not know a better way to make compiler happy, feel free to fix 171 | SimpleNoteCutInfo newNoteCutInfo(badNoteCutInfo); 172 | noteCutInfo = newNoteCutInfo; 173 | 174 | SwingRating newSwingRating(0.0f, 0.0f); 175 | swingRating = newSwingRating; 176 | } 177 | 178 | constexpr NoteCutEvent(std::ifstream& reader) { 179 | reader.read(reinterpret_cast(¬eHash), sizeof(int)); 180 | reader.read(reinterpret_cast(&time), sizeof(float)); 181 | reader.read(reinterpret_cast(¬eCutInfo), sizeof(SimpleNoteCutInfo)); 182 | reader.read(reinterpret_cast(&swingRating), sizeof(SwingRating)); 183 | } 184 | 185 | void Write(std::ofstream& writer) const { 186 | writer.write(reinterpret_cast(¬eHash), sizeof(int)); 187 | writer.write(reinterpret_cast(&time), sizeof(float)); 188 | writer.write(reinterpret_cast(¬eCutInfo), sizeof(SimpleNoteCutInfo)); 189 | writer.write(reinterpret_cast(&swingRating), sizeof(SwingRating)); 190 | } 191 | }; 192 | 193 | const inline static byte cutEventID = 0b00000011; 194 | 195 | struct NoteMissEvent { 196 | int noteHash; 197 | float time; 198 | 199 | // allows emplace to work 200 | NoteMissEvent(int noteHash, float time) : noteHash(noteHash), time(time) {} 201 | 202 | NoteMissEvent() = default; 203 | 204 | void Write(std::ofstream& writer) const { 205 | writer.write(reinterpret_cast(¬eHash), sizeof(int)); 206 | writer.write(reinterpret_cast(&time), sizeof(float)); 207 | } 208 | 209 | constexpr NoteMissEvent(std::ifstream& reader) { 210 | reader.read(reinterpret_cast(¬eHash), sizeof(int)); 211 | reader.read(reinterpret_cast(&time), sizeof(float)); 212 | } 213 | }; 214 | 215 | const inline static byte missEventID = 0b00000100; 216 | } 217 | 218 | namespace ObstacleEventTypes { 219 | struct ObstacleEvent { 220 | float time; 221 | float energy; 222 | 223 | constexpr ObstacleEvent() = default; 224 | 225 | constexpr ObstacleEvent(float time, float energy) : time(time), energy(energy) {} 226 | 227 | constexpr ObstacleEvent(std::ifstream& reader) { 228 | reader.read(reinterpret_cast(&time), sizeof(float)); 229 | reader.read(reinterpret_cast(&energy), sizeof(float)); 230 | } 231 | 232 | void Write(std::ofstream& writer) const { 233 | writer.write(reinterpret_cast(&time), sizeof(float)); 234 | writer.write(reinterpret_cast(&energy), sizeof(float)); 235 | } 236 | }; 237 | 238 | const inline static byte eventID = 0b00000101; 239 | } 240 | } 241 | 242 | template<> 243 | struct std::hash 244 | { 245 | std::size_t operator()(NoteData* s) const noexcept 246 | { 247 | std::size_t h1 = std::hash{}(s->time); 248 | std::size_t h2 = std::hash{}(s->lineIndex); 249 | std::size_t h3 = std::hash{}(s->noteLineLayer); 250 | std::size_t h4 = std::hash{}(s->beforeJumpNoteLineLayer); 251 | std::size_t h5 = std::hash{}(s->gameplayType); 252 | std::size_t h6 = std::hash{}(s->scoringType); 253 | std::size_t h7 = std::hash{}(s->colorType); 254 | std::size_t h8 = std::hash{}(s->cutDirection); 255 | std::size_t h9 = std::hash{}(s->timeToNextColorNote); 256 | std::size_t h10 = std::hash{}(s->timeToPrevColorNote); 257 | std::size_t h11 = std::hash{}(s->flipLineIndex); 258 | std::size_t h12 = std::hash{}(s->flipYSide); 259 | std::size_t h13 = std::hash{}(s->cutDirectionAngleOffset); 260 | std::size_t h14 = std::hash{}(s->cutSfxVolumeMultiplier); 261 | 262 | return h1 ^ h2 ^ h3 ^ h4 ^ h5 ^ h6 ^ h7 ^ h8 ^ h9 ^ h10 ^ h11 ^ h12 ^ h13 ^ h14; 263 | } 264 | }; -------------------------------------------------------------------------------- /include/Recording/NoteEventRecorder.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "GlobalNamespace/CutScoreBuffer.hpp" 5 | #include "GlobalNamespace/NoteCutInfo.hpp" 6 | #include "GlobalNamespace/NoteData.hpp" 7 | #include "fstream" 8 | #include "EventTypes.hpp" 9 | #include "Utils/SongUtils.hpp" 10 | #include "Utils/ReplayUtils.hpp" 11 | #include "Utils/FileUtils.hpp" 12 | #include 13 | 14 | using namespace Replay::NoteEventTypes; 15 | using namespace GlobalNamespace; 16 | 17 | namespace Replay { 18 | class NoteEventRecorder { 19 | private: 20 | std::vector cutEvents; 21 | std::vector missEvents; 22 | 23 | float frameTime = 0; 24 | int eventsInFrame = 0; 25 | 26 | float GetEventSaveTime(float songTime); 27 | public: 28 | std::vector> cutTimes; 29 | 30 | void AddCutEvent(CutScoreBuffer* cutScoreBuffer, float time); 31 | void AddCutEvent(NoteCutInfo noteCutInfo, float time); 32 | 33 | void AddMissEvent(NoteController* noteController); 34 | 35 | void WriteEvents(std::ofstream& output); 36 | 37 | float GetAverageCutScore(); 38 | }; 39 | } -------------------------------------------------------------------------------- /include/Recording/ObstacleEventRecorder.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "GlobalNamespace/GameEnergyCounter.hpp" 5 | #include "GlobalNamespace/PlayerHeadAndObstacleInteraction.hpp" 6 | #include "GlobalNamespace/ObstacleController.hpp" 7 | #include "System/Collections/Generic/HashSet_1.hpp" 8 | #include "UnityEngine/Time.hpp" 9 | #include "fstream" 10 | #include "EventTypes.hpp" 11 | #include "Utils/SongUtils.hpp" 12 | #include "Utils/FileUtils.hpp" 13 | 14 | using namespace Replay; 15 | 16 | namespace Replay { 17 | class ObstacleEventRecorder { 18 | private: 19 | std::vector events; 20 | 21 | bool lastInteracting = false; 22 | public: 23 | void AddEvent(GlobalNamespace::GameEnergyCounter* gameEnergyCounter); 24 | 25 | void WriteEvents(std::ofstream& output); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /include/Recording/PlayerRecorder.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "UnityEngine/Transform.hpp" 5 | #include "UnityEngine/Time.hpp" 6 | #include "GlobalNamespace/SaberType.hpp" 7 | #include "GlobalNamespace/BladeMovementDataElement.hpp" 8 | #include "fstream" 9 | #include "EventTypes.hpp" 10 | #include "Utils/SongUtils.hpp" 11 | #include "Utils/FileUtils.hpp" 12 | #include "Utils/SaberUtils.hpp" 13 | #include "Utils/ReplayUtils.hpp" 14 | 15 | using namespace Replay; 16 | 17 | namespace Replay { 18 | class PlayerRecorder { 19 | private: 20 | GlobalNamespace::BladeMovementDataElement leftSaberLastSavedMovement; 21 | 22 | GlobalNamespace::BladeMovementDataElement rightSaberLastSavedMovement; 23 | 24 | std::vector playerEvents; 25 | 26 | std::vector headEvents; 27 | std::vector leftSaberEvents; 28 | std::vector rightSaberEvents; 29 | 30 | void GetImportantEvents();// change name later 31 | 32 | void AddSaberEvent(GlobalNamespace::SaberType saberType); 33 | public: 34 | void AddEvent(PlayerEventTypes::PlayerTransforms const& playerTransforms); 35 | 36 | void AddSaberMovement(GlobalNamespace::BladeMovementDataElement bladeMovement, GlobalNamespace::SaberType saberType); 37 | 38 | void WriteEvents(std::ofstream& output); 39 | }; 40 | } -------------------------------------------------------------------------------- /include/Recording/ReplayRecorder.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "fstream" 5 | #include "vector" 6 | #include "Recording/PlayerRecorder.hpp" 7 | #include "Recording/NoteEventRecorder.hpp" 8 | #include "Recording/ObstacleEventRecorder.hpp" 9 | #include "GlobalNamespace/LevelCompletionResults.hpp" 10 | #include "Utils/ReplayUtils.hpp" 11 | #include 12 | 13 | #include "rapidjson/document.h" 14 | #include "rapidjson/stringbuffer.h" 15 | #include 16 | #include 17 | 18 | namespace Replay { 19 | class ReplayRecorder { 20 | private: 21 | void CreateClearedSpecificMetadata(GlobalNamespace::LevelCompletionResults* results, rapidjson::Document::AllocatorType& allocator); 22 | 23 | void CreateFailedSpecificMetadata(GlobalNamespace::LevelCompletionResults* results, rapidjson::Document::AllocatorType& allocator); 24 | 25 | void CreateMetadata(GlobalNamespace::LevelCompletionResults* results); 26 | 27 | bool ShouldMoveFile(GlobalNamespace::LevelCompletionResults* results, std::string_view filepath); 28 | 29 | void WriteReplayFile(std::string path); 30 | public: 31 | void Init(); 32 | 33 | void StopRecording(GlobalNamespace::LevelCompletionResults* results); 34 | 35 | PlayerRecorder playerRecorder; 36 | NoteEventRecorder noteEventRecorder; 37 | ObstacleEventRecorder obstacleEventRecorder; 38 | 39 | rapidjson::Document metadata; 40 | }; 41 | } -------------------------------------------------------------------------------- /include/ReplayManager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | #include "Recording/ReplayRecorder.hpp" 4 | #include "Replaying/Replayer.hpp" 5 | #include "Utils/SongUtils.hpp" 6 | 7 | namespace Replay { 8 | enum ReplayState { 9 | RECORDING, 10 | REPLAYING, 11 | NONE 12 | }; 13 | 14 | class ReplayManager { 15 | public: 16 | static inline Replay::ReplayState replayState; 17 | 18 | static Replay::ReplayRecorder recorder; 19 | 20 | static Replay::Replayer replayer; 21 | }; 22 | } -------------------------------------------------------------------------------- /include/Replaying/NoteEventReplayer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "GlobalNamespace/NoteController.hpp" 5 | #include "GlobalNamespace/NoteCutInfo.hpp" 6 | #include "GlobalNamespace/NoteData.hpp" 7 | #include "GlobalNamespace/SharedCoroutineStarter.hpp" 8 | #include "fstream" 9 | #include "EventTypes.hpp" 10 | #include "Utils/SongUtils.hpp" 11 | #include "Utils/ReplayUtils.hpp" 12 | #include 13 | #include "System/Collections/IEnumerator.hpp" 14 | #include "custom-types/shared/coroutine.hpp" 15 | #include "Utils/SaberUtils.hpp" 16 | 17 | #include "UnityEngine/Resources.hpp" 18 | #include "GlobalNamespace/GameNoteController.hpp" 19 | #include "GlobalNamespace/Saber.hpp" 20 | #include "GlobalNamespace/SaberManager.hpp" 21 | #include "GlobalNamespace/SaberType.hpp" 22 | #include "GlobalNamespace/SaberTypeObject.hpp" 23 | #include "GlobalNamespace/NoteData.hpp" 24 | #include "GlobalNamespace/ColorType.hpp" 25 | 26 | // Using namespace in headers is icky 27 | using namespace Replay::NoteEventTypes; 28 | 29 | namespace Replay { 30 | struct ActiveNoteCutEvent { 31 | NoteController* note; 32 | NoteCutEvent event; 33 | 34 | constexpr ActiveNoteCutEvent() = default; 35 | 36 | constexpr ActiveNoteCutEvent(NoteController *note, const NoteCutEvent &event) : note(note), event(event) {} 37 | }; 38 | 39 | struct SwingRatingData { 40 | int noteHash; 41 | SwingRating swingRating; 42 | 43 | constexpr SwingRatingData() = default; 44 | 45 | constexpr SwingRatingData(int noteHash, SwingRating swingRating) : noteHash(noteHash), swingRating(swingRating) {} 46 | }; 47 | 48 | struct ActiveNoteMissEvent { 49 | NoteController* note; 50 | NoteMissEvent event; 51 | 52 | constexpr ActiveNoteMissEvent() = default; 53 | 54 | constexpr ActiveNoteMissEvent(NoteController *note, const NoteMissEvent &event) : note(note), event(event) {} 55 | }; 56 | 57 | struct EventToRun { 58 | float time; 59 | bool isCutEvent; 60 | int eventIndex; 61 | 62 | constexpr EventToRun() = default; 63 | 64 | constexpr EventToRun(float time, bool isCutEvent, int eventIndex) : time(time), isCutEvent(isCutEvent), eventIndex(eventIndex) {} 65 | 66 | bool operator < (const EventToRun& str) const { 67 | return (time < str.time); 68 | } 69 | 70 | bool operator > (const EventToRun& str) const { 71 | return (eventIndex > str.eventIndex); 72 | } 73 | }; 74 | 75 | class NoteEventReplayer { 76 | private: 77 | custom_types::Helpers::Coroutine Update(); 78 | GlobalNamespace::SaberManager* saberManager; 79 | 80 | public: 81 | void Init(); 82 | 83 | std::vector cutEvents; 84 | std::vector activeCutEvents; 85 | 86 | std::vector swingRatings; 87 | 88 | std::vector missEvents; 89 | std::vector activeMissEvents; 90 | 91 | void AddActiveEvents(GlobalNamespace::NoteController* noteController); 92 | 93 | void ReadCutEvents(std::ifstream& input, int eventsLength); 94 | 95 | void ReadMissEvents(std::ifstream& input, int eventsLength); 96 | }; 97 | } -------------------------------------------------------------------------------- /include/Replaying/ObstacleEventReplayer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "GlobalNamespace/GameEnergyCounter.hpp" 5 | #include "GlobalNamespace/PlayerHeadAndObstacleInteraction.hpp" 6 | #include "GlobalNamespace/ObstacleController.hpp" 7 | #include "System/Collections/Generic/List_1.hpp" 8 | #include "fstream" 9 | #include "EventTypes.hpp" 10 | #include "Utils/ReplayUtils.hpp" 11 | #include "Utils/SongUtils.hpp" 12 | 13 | using namespace Replay::ObstacleEventTypes; 14 | using namespace System::Collections::Generic; 15 | 16 | namespace Replay { 17 | class ObstacleEventReplayer { 18 | private: 19 | int lastPlayerObstacleInteractionCount = 0; 20 | public: 21 | std::vector events; 22 | 23 | int lastIndex = 0; 24 | 25 | int nextEventIndex = 0; 26 | 27 | void ReadEvents(std::ifstream& input, int eventsLength); 28 | 29 | bool ShouldSetEnergy(); 30 | }; 31 | } -------------------------------------------------------------------------------- /include/Replaying/PlayerReplayer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "UnityEngine/Transform.hpp" 5 | #include "UnityEngine/Quaternion.hpp" 6 | #include "UnityEngine/Vector3.hpp" 7 | #include "GlobalNamespace/PlayerTransforms.hpp" 8 | #include "fstream" 9 | #include "EventTypes.hpp" 10 | #include "Utils/ReplayUtils.hpp" 11 | 12 | using namespace Replay::PlayerEventTypes; 13 | 14 | namespace Replay { 15 | class PlayerReplayer { 16 | private: 17 | std::vector headEvents; 18 | std::vector leftSaberEvents; 19 | std::vector rightSaberEvents; 20 | 21 | int headIndex = 0; 22 | int leftSaberIndex = 0; 23 | int rightSaberIndex = 0; 24 | public: 25 | void ReadHeadEvents(std::ifstream& input, int eventsLength); 26 | void ReadLeftSaberEvents(std::ifstream& input, int eventsLength); 27 | void ReadRightSaberEvents(std::ifstream& input, int eventsLength); 28 | 29 | void SetPlayerTransforms(GlobalNamespace::PlayerTransforms* playerTransforms); 30 | }; 31 | } -------------------------------------------------------------------------------- /include/Replaying/Replayer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "fstream" 5 | #include "vector" 6 | #include "Replaying/PlayerReplayer.hpp" 7 | #include "Replaying/NoteEventReplayer.hpp" 8 | #include "Replaying/ObstacleEventReplayer.hpp" 9 | #include "Utils/ReplayUtils.hpp" 10 | 11 | #include "System/Collections/IEnumerator.hpp" 12 | #include "custom-types/shared/coroutine.hpp" 13 | 14 | using namespace Replay; 15 | 16 | namespace Replay { 17 | class Replayer { 18 | private: 19 | custom_types::Helpers::Coroutine WaitForSongStartToInit(); 20 | 21 | public: 22 | void Init(std::string_view path); 23 | 24 | Replay::PlayerReplayer playerReplayer; 25 | Replay::NoteEventReplayer noteEventReplayer; 26 | Replay::ObstacleEventReplayer obstacleEventReplayer; 27 | 28 | void ReadReplayFile(std::string_view path); 29 | }; 30 | } -------------------------------------------------------------------------------- /include/Sprites.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace Replay::Sprites { 5 | static std::string ReplayIcon = R"(iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAABcmSURBVHhe7Z0JlBzFeYCZ3dnZ2UN7iJXEKYQOJCyEecICIxtsCA8wIMLlGAiWzZEHhvg5MQEbOwGCQ4KIjHHe8yUrQYkNAduYcBgkhDnM6cBqhSRLMpZWx2pXs/fuzM6xu1opX838q7DbXT0910737Hzv9eue6arqqv//66+q7urqI4oUKVKkyGTFI/uC5dChQ55YLOYPBAJHzZw5c2pPT8+RIyMjpepcSUmJZ9q0aZX8PtTV1RWpr6/v5XxHbW1tz/79+8Nz5swZjidSwBSUAbS0tBxfXV29xO/3n4py55WWls70eDwnsE1j80uwpBw8eFDtIgcOHOhmv4Ntk9fr3RAKhTZWVlZu8/l8BWMYrjaA7u7uBVVVVZ9C0Z9lW8pfs1F04mSOwDiUYWxl/9bg4ODaoaGhV6dPnz4op12HqwwAd16C0Bej7Mv4eT21chb7vJYBQxggX+vwOGsHBgZ+UVNTE5RTrsAVBhAMBmeXl5ffhOK/xHas/O04MIQBds/jhX7Ofj37ofiJIqmDe1e1/Tw6aP+DYAfZXAX5DtA8PEg5TpAiFbFDNBotRXDLaWc/EFm6GgwhRlnW0DycKkV0FKZNAAo4g91ytqkU4G161aty7c4Yhnnr6ur+guvcQ3s6X/7OGpTpCNJu5TCsaifHwzQrPRjcdJqVEvoTdSiqoqysrIZ2Xf2XiJglsIWDXPeXeLW7Ganskr/zjsEAKPyXEc5qDg9LgIyvY3y8jN5u1oc/CMaDEi5GGStR/AL5O21QYif53UAZNrJ9QM37I8O3PR0dHb1LliyJj++soPye4eHhclz3PAx/DkO+BRjFQgxiPtsigtgeTppBeQe5xkrytKK+vj4kfzuDSCTSQOaCynWNhxr0VxIsa6CcE1HYC3KJtEDZ7WyPorRr+/r65m3durVEks86XKMyHA6fH4vFHlJGhqyGJBspQ/y9Ks8c5iy/KUOmrkxkzwgG8IIEy5je3l4vhf86AlRDqJQh3l7ysxLPsRQjKpdkJxyM4SjKcQdye4c8jUj2UoK4L1AWNZzNP+Tn6kS2jGD1v5NgGUEbOJtCvyXJ2oY4Iwj7ebzU5WzZbaCzAEZ9Et7sHrK6M5Fj+2A8fRTveoa7+R2Wt7e3Xyx5MkAGN0qwtEH5V1LYbknSFoTvRfErEM48ScbRkGUvsrqC7f1ECeyDAT3Nrk6Smni4+CfiOTEhEwMgegWFW51IyR4oXvXW/5nDIyUZV0G+PXgqdR/jtXiBbEK5PyROfoaM9JRPlXwYoPbulWApQRt9FIV6Q5KxQxQBPMz1GiQJV0P77qE8V7Gl0jQMUGG+IElMHP39/TMkA2Z0sqXURqmbHyjfdsER0jq2rN8DcAL0ESoxhnuRR0SKawnhDmIE/9DV1TVx/QLG+kfL9Q2Q+eENGzbYHrKQ+YuIZqu9p6ydpP9FPJDjOnfZhpHDAoz8bSl6UpDjj9hNjFx27NhRSYcrceVxkJFDLS0tlRLUEoJfwhaNR0wCyn+Oax4jUScFFFsNg+9ib0tGGMzPqBxlEj13bNy4sZyMaceznJ8jQbUQzJbyUTyXGv4mh865ETLBRKPR01HuloRErMFDPpVzI9iyZYuXC5neCVTQBFgaAEEuZbOj/FYKfo5Em9TQN6hGFuqJZ1Lwwo+zy21zQK38MHE5I3QST5ZgBujwnUuQpB0cCtvENlOiFQHEUkqlWMH+YFxIFiC7H+e0Y8g1tI9hGdd+QoKNAeWfwumeRCg9WPB6CpC/Gx0OBvF4kM+NGELS5wuEu1+iZZ9YLPaOXMdAe3v7+RLsMIzXj+XUrkQIPWT61+wyepI2GaAyfR4jiCWkpuUgTfX1EiW70DHRDlH6+vpulGBxOjs7fWT293JaC0by2M6dO70SrUgSENmFbMma0zCyP12iZA+U9bpcwEBPT8/NEiwO7vy7ckoLlvpEa2trUfkpEgqFrk7WHCD/nVTY7N4qpxP4mKRvgObhKxJMhfsCGbTstJDBteyKbj9NkN+XbMj4N+yyN5QmsUfiKZvzNRUGLzGHfGmHiwoy1kSnsSaeaBrQ3MzlOveGw+Gn2a9iv1hOTSroO31HRKqFMLdI8MxB2A9JugZwSyozJVzwd4l/zME42tlSng3LMHMKTcaNGM86khl/S3IQD3SVBJ00UO5SvO0TCRFoCaG37DxDQQHfkEQNoPiVKOFW+WkKij9AmHMluaTs3r27Ek9xDYV8nriWN5E430r+8jYDKF9QIdTj9I0iBlMIo/pumTcFDEPuTyRpBDf8NkoIy09TyOgdkpSWYDDo5ToXocxHidKRiGkPRh5/JslMKqggpyB7y5EBsr9BgqcPruQ2SS9lsMIXUarWCnHxCyjI/YRrkygpEwgE1JT1SQmys9QNcu2gYk2V4OlB5+sGSS8lsM4+LNBwi5eh4wlk/Btk7n8JltbEyVFIo2nfvn2T9uFRd3d3KTL4rYjDFM5/T4KnBxb055JWSqD8wzeJOjo6GvAkqjO3HsM4IEEygnQ2q2nfcolJCx3xE5GF1QhsCB2mP6UM5Z0jCdmGDL2McspR+tUYwnMoPtmtTLtE8R7PRKPRK7H+4s0kgWb2DpGPKejgNxI0dYivnReoA4W/hBGk3a6PY4gCvIQxfZlO5zTJVpGPgBcoQ+abRF4G0IV6cPdpCZ4aCP1jks5EQp4Pvo/i76TPMKlmB6ULFeQCJbeE+IzgOX8pQVOjsbFxjqSRc7DiVhT/j2wL+ZnflyJchpIX8ns2LkgTkOkBRl0fk+BaDELftm3bcQsWLGiRn1mHvPWQ8efY/7ysrOx19ZaunMoKsVisgjTPYVvMcYPf7+/nWps5frm2ttZZL2NmiJqfUVFR8Z78NIAX+JHP57tNftqDJsBHRGVEWQOFD5Lms3TmLscqc3Inj3SruIa6x2A6MYUaEeL8I729vQXxvsEolPcVKaIBzqlXzmxN5D0MgvQgrJRe3zKDNIbZVOfwBiy1VpLPCfQb5nGdP8ilLSHcfgzhUonqelDw+VI0Uzh/rQS1D52xtHv0XPBPdFC+hSeZLcnlFIzrKK65Wy5vF7VYw5P0po+SZFwLZSlh0z4noJypv9VNe9ks8W3BRdqoVQ/iXs+kNk7onTqure0IJYO4HZT1Og5d3QFF9l9LlMiUGOdTW1hraGhIvaVqCcLrwp3+MBgMfq61tTX3Ly2YgKdaItnJBOUNfkGZZ0iyroNKdyy60M4ewgDukqD2wIWfglB6Jf5h1EUQ+vOcV525vM/0IY8PS9YyhrJ1Uq6b8v6OfppgwNqVVtDZeglmH6xmNhHVK92/53g9Pfi/7e7udtRNGgxAu9AE55rI/51slpNXxkNZ1/X19TljxY4UQD/XSBEMYNyxgYGBKRK0cKDG7pUyGti/f79a0Gn0hslNCEHdD7AFYXupUTfTp3HNy6oouJp8a+cL0OG9RoIWDnTg9kn5DDQ2No55ckjYYzAEW69gjUL41xDsSZKE4yG/r0rWDVD+H0uwMUyaZ+t+v7+tpKRELd1yPfLYL39bQvjPVFRUNOFp/iYQCDjeG+C1tOs4UZaz5bBwQJmbE/ZtpKurSztfHkE1sP2KYEnfxRuFvsRrtLNJ763nE/oviyW7BmgehvECFRK0MKBcTYnimWJ595HzHmq2WhVN24yMR7WxCPnv9u7d60hvwAhGvanVJ9k1gNGfJUELA8qUtgGMQq1uwJP8jPC2vQHh33aqN8AAtItS4cXGvNnleqiNO6RsBrZv336cBLMFglvGph1VjEd5AzzIt/J1E0wHbv6HkkUD5PenEuwwru4EUhO1N6PC4XBK7R2dJPWIehGK/Qk/DyX+1ePxeCp8Pt8DM2bMeJMh1sfl77xDpfijHBogzwavNWln2JpRWlraz3YrrlK9nbtH/raE8GdUVVWpm2X30vHM+91Ravl2OTRjdmdn55g7na42AIQfk0MDKCUqhylTVla2PhKJnIqH+QE/k64wTs0q93q999XX1zeiANNFNCYKjFdrAORz+r59+wrnzSpqacadwGTQcz6bZmG7pJkUwqoVTi+U6BMOTV8ZRqB9METeMntxxElQnpwbgKKvr28KNVut12frxRaMoJ9RQtIV1XIFBqCdz0ET4Oh7GSmBNad1Iyhd6GF/FuVuk0tYghKekWgTDv0R7ewoDPmTEiyOq/sAFLReDg3s2bMn667O7/e/Rjt6OkagFrG2nMzKqOIS3HFePhhF30jb/yFPY+ZEFkcBKYIBRBDwtzG+T2EIW+RvA4QrpTNp+zX5bIKnUp+vM4WO6piRStEA0qS8vPy9YDB4Bp7gV/KXAQzF8S+5uNoAUEKnHBpYvHhxhxzmjLq6OjWdTHvDiRFElRxOKFZNAJ3AiBzGKXqANKEzpeYjvsf4/xL5ywD9gLx8Hs7KKGm2xtzlLBpAioRCoUqE+ADt+1u08/FZR2ZgHOhhJPW5eFmAvGmfT5DnpLe5XQO1UPs+wKZNm7I+rw/FLyXprYkrWMMw8EmJNuEgF+1DMjqIYx4Jaz1AU1OTp7Gx0dEegvJo1wygl561p3Qos5LtEWrPG/zULpg9CoYSjEQi35afE8revXs99AG0Q+BoNGrdB0CoakWq7y1cuLBv0aJF6p2+1+nMaF1docO4WS1KtQWhqjUSk1YIlB+mll1WU1OzQ/6aUNBXOTrU3h/hnL5zrJ4U0W49Q6AxUKhOLPpECeYYMFTtPfrdu3dn9NoXNaWK9P+NpGyva4ScmlD+aZJEXti2bdtcyY4BdBsOBAI+CWqEtkONa02hYCslmGMgWzl5FoDHuwhh2f7YFYqPEec+Ooh5f9KGxzpbsmUAg/5Qgh1mjEvz+XzaDzngAgtvVuk4UGItQlpFJ/oFhnC2Xm7FUBqpHGciu/umTJkyKH/nDfJhtZDWTtkfZowB9Pf3B+TQjFm4D0e9NoXwtePdPXv2pDQjCJd/KUau2nr1keyk5aRCqdfk7ibeWZWVlR/I33kHb6RdU5k8G+5LjDEAv9+vvhZyQH6Owev1Tq+oqDhafjoCFFAthwa6u7ttvQoVDAan0nH6T8r+LLXe1jxCVetxtUvwFA9S67P+Sf0M0c78xcNZewCEEDKzklEo8JlyWBBgQBdWV1dvwbiX89NOrVfTwr+J8s9C8Zvkb8egPuuHB9M+7yfvBk9lGNbgQjbIoYHy8vKlcugIGJebeisFxqqtmdT6GdTi/0JYL5KGLa+G8t9CNotoY1fU1tY6rdbHoSlaQplM5yUqz04T3ig/9dCm/T2BTcGF/EGCOQKylNIogP881AL1elggESQ5KD1InK/SP3L8q2GUS7tQBN7ufQlmTU9Pz6cljgGEcbCjo8Mxn3wjS7YNAONVL4Bov4ZiBuFfwVu45lvG5PdFyboBzj0swaxpbm5WXw/VLgk/MDCQ2rJjOYTsaA0gEonEh3Ecqs+xXYvt7k+cSQ5hw8jgdje9Hk5ejyTf2smgDFXtL4yFtTwp8Qxw7tcSLO9Qq7UGQD4bUfxfIxTtyhlmEGc9nby8TehMl8HBwaukCAaQwXBbW5v9OZJYy3KJa4DEBkOhkCPW8KWW/kCylTGUK0j/51a1TJ4k7yqQxVNSFAOce1mC2QM336AULfENYG1flaB5BUPU3vZMBQT0Ekbvulo/CnJQ+tKuDkL5Um+2iaT9fiBu8l0JlnfIp/rAVFogtH6M+Ub6C4bhsJugKVRfYTeFMo7QP0h9bgSJqvXzdIwgOEd8xg0DUEu/tEi+bEO8tdScvEzbziboqYSyaNd15Pw7EjQ16AipqU/aJWM59+8SNO+gyHkYgfar5+PoxN1/kb0r2/rxoKcrEsUyh35N+msCYFlqmTgdUQTvmHYTN1dHfv8FQ1C3sw3wf5jasJo8F9T3CCiXduxPJe0MBAJpf7xT3TJV8wOsPkrwHxLUMfT19dVQwy+j8HfRtj+o9jRXyxgGaWfJuBXKdy7l0+qHvtoKCZoepKFunb6RSM4I1x5CuCmtxFEkO6jhKgrWLgeDbgZx/5nftSWRiyVNUzCQxyRokQkE5SdbJn6NBM0M0lLfD9AOCWEEI/mMBC8yASBzLzppTIjfCOdGBjL5dNx4aFOXSdqmYG1NXFA/2bBIVqEze7uI3hT08bgEzQ50BktI9E1J3xRcUmpLkhdJCyra0dRwq+G5msWc/eVtMYDTSFj7MSEurL7J49rbqW4AMatOueV6x1TE1RI8+5C45cMXjOTd9vb2YlOQI5Cv5XedOd/H0DB38zZpe+qp6e1yPVMwkszGnkVMoeafjHitvhms7vrdLsFzBxe5Ua6n4wCdxs9J8CJZIBQKTaHiWX4VjYr3bltbW+6/r6zeDcDVaJ89K1QnJRwOz5UoRTJAPalEuU+IaHUg7vDEvb+JO5rBRS2nWGEEfyLjWV+pa7KBDO8RkWpBH1+X4BMH/QF1v91yhW0y/yYWnNIbOkX+H+R3sw0Zqy+85WfuIk3BQ4lsWKK+65f3NXTdBv2oK9G9dpKnAvnv6e/vz99DrubmZvUde+3jyI+gjKBw1qjNMchKLVatneIlhKn9+X9TCyudymZnMsazwWCw2BwkgSbz88gqmfKpdyPqdTZnMDg4eBJG0CWZ00JnRT1UKrjn8tkC+dyixBQXlgXI+26J4hzI1xKs0vJGhYIwWynoAolWBBBLKXL5DpUo6WdrCLJKrQMkUZ1FKBQ6jzxGE1nVQyF6MIKLJNqkpre3txblqy+YJYWRl7of4Oy3lcjgJWx2jAAbGL6ntbV10j47oPzqEzW2JrMyKnhq165djvo+kRZ6pxdi1QOSd0sI9zaWPamaBIxe3d27jeIn6+zFwVAed43yR8Gyl7B1SBksIVw4Go3eogQj0QuWcDg8C+XbfpkF2axi55qXVMeAUhdQAO2qlePB0t8lzhKJXlDg5Xx4u7uQR9KOsjCC27+zpaXF3e8vUPBpFPoVKZQdRqghjxbK/P3+/v4SdVePclk+zRuH+u7QdZKE+6FAapizAkNIFM8GhO3FI9yHAF37kSPG65djzFaLWBig3LuIc7okUVhQsCsooHY+mxmEVwszfZ9DV7zH19bW5qPGL1fNWaIE9qGS/Lf6QJUkVZgMDAwcj1LVs4GUIM4Q29P0ES6ls+i4W8rk6zhq/AMYue2VSD5CiKZS3QUs+E5wHASlJpUsR6FJbx+bQdxOttXUtAs6OztzPwtGA0o/QSmOvLysDFSylxJ4ilcpR/Zn8boB2vejqTFrkIPtBZnHg+DbUMLTkUjkK6SnFkrOWa+5ubm5nutcQJ7vQenvcK0D8UykAfHbMaDrOcxrL98RQww8wtKysrJ/9Xg8ma5DqGS7D6NoQsDbSHc7Qt7R1dW10+v1BubPn5/0axmbN28uKS0tramtrZ1RX18/k+OT2U4hvaWkcXIJSNB0iWFAP6Xm319ZWdkl/+UNx4wxVU2gJl+HsP+JLatf+yBtdWMqopocDK0DZQ5zrSF1Dn2qvoWvvLzcj2Ia+H0shugnD/G42YI8qCZiDenfT/qt8neR8eDGvdTca1CSWrfY9WBcIQzrJ+Fw2HHfW3A0wWDQi9AuwVWuRYhpt7X5gjyr8bxaV9gRK6rpcMVtRrz1TFy3WrPoL3GfC/nLkflWhornehmjXYPyn6Efof28vVNwhQGMggF46NR9nPb6Wo4voj1dhEHktQwoPcamvNTjDOd+i9J75JQrcJUBjKenp+f4ioqKZRyeR6ftk/TSVQcucTJHqA4kO7Vy+Jt4ptf4/S5KDyfOug9XG8B4MIjpKOMUDtUXvk9jUx7iOLY6jMN2WfEuqmaHiKe+G7SL4x249T205+p7wZvnzp2r/TSr2ygoAzBj+/btpX6/v3zatGnVKPMYmo0qFNpQVVUVn2ChvqXL70M+n2+IcX9/a2trrKGhYV8gEBiYNWuW9nsERYoUKVKkiKs54oj/A4k/lPokS5B4AAAAAElFTkSuQmCC)"; 6 | } -------------------------------------------------------------------------------- /include/UI/ReplayViewController.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "GlobalNamespace/LevelBar.hpp" 5 | #include "UnityEngine/GameObject.hpp" 6 | #include "UnityEngine/Transform.hpp" 7 | #include "UnityEngine/RectTransform.hpp" 8 | #include "TMPro/TextMeshProUGUI.hpp" 9 | 10 | #include "custom-types/shared/macros.hpp" 11 | #include "HMUI/ViewController.hpp" 12 | 13 | #include "System/Collections/IEnumerator.hpp" 14 | #include "custom-types/shared/coroutine.hpp" 15 | #include "UnityEngine/WaitForSeconds.hpp" 16 | 17 | DECLARE_CLASS_CODEGEN(Replay::UI, ReplayViewController, HMUI::ViewController, 18 | 19 | DECLARE_OVERRIDE_METHOD(void, DidActivate, il2cpp_utils::FindMethodUnsafe("HMUI", "ViewController", "DidActivate", 3), bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling); 20 | 21 | public: 22 | void Init(std::string_view filePath, bool overwriteFile); 23 | 24 | std::string path; 25 | bool overwrite; 26 | 27 | void CreateLevelBar(UnityEngine::Transform* parent); 28 | void CreateText(UnityEngine::RectTransform* parent); 29 | void CreateButtons(UnityEngine::RectTransform* parent); 30 | 31 | void SetupLevelBar(); 32 | void SetText(); 33 | void SetButton(bool overwrite); 34 | 35 | UnityEngine::GameObject* levelBar; 36 | 37 | TMPro::TextMeshProUGUI* dateText; 38 | TMPro::TextMeshProUGUI* scoreOrFailedText; 39 | TMPro::TextMeshProUGUI* modifiersText; 40 | 41 | TMPro::TextMeshProUGUI* averageCutScoreText; 42 | TMPro::TextMeshProUGUI* missedNotesText; 43 | TMPro::TextMeshProUGUI* maxComboText; 44 | 45 | UnityEngine::UI::Button* deleteButton; 46 | ) -------------------------------------------------------------------------------- /include/UI/UIManager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "GlobalNamespace/SinglePlayerLevelSelectionFlowCoordinator.hpp" 5 | #include "GlobalNamespace/StandardLevelDetailView.hpp" 6 | #include "UnityEngine/RectTransform.hpp" 7 | 8 | #include "UI/ReplayViewController.hpp" 9 | #include "ReplayManager.hpp" 10 | 11 | namespace Replay::UI { 12 | class UIManager { 13 | public: 14 | static inline GlobalNamespace::SinglePlayerLevelSelectionFlowCoordinator* singlePlayerFlowCoordinator; 15 | 16 | static inline Replay::UI::ReplayViewController* replayViewController; 17 | 18 | static inline UnityEngine::Transform* buttonParent; 19 | 20 | static void SetReplayButtonOnClick(UnityEngine::Transform* buttonTransform, std::string path, bool overwrite = false); 21 | static UnityEngine::Transform* CreateReplayButton(UnityEngine::Transform* parent, UnityEngine::UI::Button* templateButton, UnityEngine::UI::Button* actionButton, std::string path, bool overwrite = false); 22 | 23 | static void CreateReplayCanvas(GlobalNamespace::StandardLevelDetailView* standardLevelDetailView, bool replayFileExists); 24 | 25 | static void SetReplayButtonCanvasActive(bool active) { 26 | buttonParent->Find(newcsstr("ReplayButtonCanvas"))->get_gameObject()->SetActive(active);//Name of canvas is defined else where, use that eventually 27 | float xpos = active ? 4.2 : -1.8; 28 | ((UnityEngine::RectTransform*) buttonParent)->set_anchoredPosition({xpos, -55}); 29 | } 30 | }; 31 | } -------------------------------------------------------------------------------- /include/Utils/FileUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include 5 | #include "rapidjson/document.h" 6 | #include "rapidjson/stringbuffer.h" 7 | #include "rapidjson/error/error.h" 8 | #include 9 | #include 10 | 11 | namespace Replay { 12 | class FileUtils { 13 | public: 14 | template 15 | static void WriteEvents(std::vector events, byte eventID, std::ofstream& output) { 16 | unsigned int eventCount = (int)events.size(); 17 | 18 | if(eventCount == 0) return; 19 | 20 | //Write events header 21 | output.write(reinterpret_cast(&eventID), sizeof(byte)); 22 | output.write(reinterpret_cast(&eventCount), sizeof(unsigned int)); 23 | 24 | //Write data 25 | for(T event : events) { 26 | event.Write(output); 27 | } 28 | } 29 | 30 | static rapidjson::Document GetMetadataFromReplayFile(std::string_view path) { 31 | log("Reading Replay file metadata at %s", path.data()); 32 | std::ifstream input = std::ifstream(path, std::ios::binary); 33 | 34 | rapidjson::Document metadata; 35 | 36 | if(input.is_open()) { 37 | int magicBytes; 38 | input.read(reinterpret_cast(&magicBytes), sizeof(int)); 39 | if(magicBytes != replayMagicBytes) { 40 | log("INCORRECT MAGIC BYTES"); 41 | return metadata; 42 | } 43 | 44 | byte version; 45 | input.read(reinterpret_cast(&version), sizeof(byte)); 46 | 47 | int metadataLength; 48 | input.read(reinterpret_cast(&metadataLength), sizeof(int)); 49 | 50 | log("Metadata length is %zu", (size_t) metadataLength); 51 | 52 | std::string metadataString; 53 | metadataString.resize(metadataLength); 54 | input.read(metadataString.data(), (size_t) metadataLength); 55 | 56 | log("%s", metadataString.c_str()); 57 | 58 | rapidjson::ParseResult ok = metadata.Parse(metadataString.c_str()); 59 | if (!ok) { 60 | log("JSON parse error"); 61 | } 62 | } else { 63 | log("COULD NOT FIND REPLAY FILE"); 64 | } 65 | 66 | return metadata; 67 | } 68 | }; 69 | } -------------------------------------------------------------------------------- /include/Utils/FindComponentsUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "GlobalNamespace/SimpleDialogPromptViewController.hpp" 5 | #include "GlobalNamespace/LevelSelectionNavigationController.hpp" 6 | #include "HMUI/ScreenSystem.hpp" 7 | 8 | namespace Replay::FindComponentsUtils { 9 | 10 | #define CacheFindComponentDeclare(namespace, name) namespace::name* Get##name(); 11 | 12 | CacheFindComponentDeclare(GlobalNamespace, SimpleDialogPromptViewController) 13 | CacheFindComponentDeclare(GlobalNamespace, LevelSelectionNavigationController) 14 | CacheFindComponentDeclare(HMUI, ScreenSystem) 15 | 16 | void ClearCache(); 17 | 18 | } -------------------------------------------------------------------------------- /include/Utils/MathUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "UnityEngine/Quaternion.hpp" 4 | #include "UnityEngine/Vector3.hpp" 5 | 6 | namespace Replay { 7 | class MathUtils { 8 | public: 9 | // TODO: Use sombrero! 10 | static UnityEngine::Quaternion LerpEulerAngles(UnityEngine::Vector3 angleA, UnityEngine::Vector3 angleB, float amount) { 11 | UnityEngine::Quaternion quaternionA = UnityEngine::Quaternion::Euler(angleA); 12 | UnityEngine::Quaternion quaternionB = UnityEngine::Quaternion::Euler(angleB); 13 | 14 | return UnityEngine::Quaternion::Lerp(quaternionA, quaternionB, amount); 15 | } 16 | 17 | // TODO: Sombrero! 18 | static constexpr UnityEngine::Vector3 Lerp(UnityEngine::Vector3 const& value1, UnityEngine::Vector3 const& value2, float amount) { 19 | return {value1.x + (value2.x - value1.x) * amount, value1.y + (value2.y - value1.y) * amount, value1.z + (value2.z - value1.z) * amount}; 20 | } 21 | }; 22 | } -------------------------------------------------------------------------------- /include/Utils/ModifiersUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "GlobalNamespace/GameplayModifiers.hpp" 5 | 6 | namespace Replay { 7 | class ModifiersUtils { 8 | public: 9 | static std::vector ModifiersToStrings(GlobalNamespace::GameplayModifiers* gameplayModifiers) { 10 | std::vector strings; 11 | 12 | if(gameplayModifiers->energyType == GlobalNamespace::GameplayModifiers::EnergyType::Battery) strings.push_back("BatteryEnergy"); 13 | if(gameplayModifiers->noFailOn0Energy) strings.push_back("NoFail"); 14 | if(gameplayModifiers->instaFail) strings.push_back("InstaFail"); 15 | if(gameplayModifiers->enabledObstacleType == GlobalNamespace::GameplayModifiers::EnabledObstacleType::NoObstacles) strings.push_back("NoWalls"); 16 | if(gameplayModifiers->noBombs) strings.push_back("NoBombs"); 17 | if(gameplayModifiers->strictAngles) strings.push_back("StrictAngles"); 18 | if(gameplayModifiers->disappearingArrows) strings.push_back("DisappearingArrows"); 19 | if(gameplayModifiers->songSpeed == GlobalNamespace::GameplayModifiers::SongSpeed::Slower) strings.push_back("SlowerSong"); 20 | if(gameplayModifiers->songSpeed == GlobalNamespace::GameplayModifiers::SongSpeed::Faster) strings.push_back("FasterSong"); 21 | if(gameplayModifiers->songSpeed == GlobalNamespace::GameplayModifiers::SongSpeed::SuperFast) strings.push_back("SuperFastSong"); 22 | if(gameplayModifiers->noArrows) strings.push_back("NoArrows"); 23 | if(gameplayModifiers->ghostNotes) strings.push_back("GhostNotes"); 24 | if(gameplayModifiers->proMode) strings.push_back("ProMode"); 25 | if(gameplayModifiers->zenMode) strings.push_back("ZenMode"); 26 | if(gameplayModifiers->smallCubes) strings.push_back("SmallCubes"); 27 | 28 | return strings; 29 | } 30 | 31 | static GlobalNamespace::GameplayModifiers* CreateModifiersFromStrings(std::vector strings) { 32 | GlobalNamespace::GameplayModifiers::EnergyType energyType = GlobalNamespace::GameplayModifiers::EnergyType::Bar; 33 | if(std::count(strings.begin(), strings.end(), "BatteryEnergy")) { 34 | energyType = GlobalNamespace::GameplayModifiers::EnergyType::Battery; 35 | } 36 | 37 | GlobalNamespace::GameplayModifiers::EnabledObstacleType enabledObstacleType = GlobalNamespace::GameplayModifiers::EnabledObstacleType::All; 38 | if(std::count(strings.begin(), strings.end(), "NoWalls")) { 39 | enabledObstacleType = GlobalNamespace::GameplayModifiers::EnabledObstacleType::NoObstacles; 40 | } 41 | 42 | GlobalNamespace::GameplayModifiers::SongSpeed songSpeed = GlobalNamespace::GameplayModifiers::SongSpeed::Normal; 43 | if(std::count(strings.begin(), strings.end(), "SlowerSong")) { 44 | songSpeed = GlobalNamespace::GameplayModifiers::SongSpeed::Slower; 45 | } else if(std::count(strings.begin(), strings.end(), "FasterSong")) { 46 | songSpeed = GlobalNamespace::GameplayModifiers::SongSpeed::Faster; 47 | } else if(std::count(strings.begin(), strings.end(), "SuperFastSong")) { 48 | songSpeed = GlobalNamespace::GameplayModifiers::SongSpeed::SuperFast; 49 | } 50 | 51 | GlobalNamespace::GameplayModifiers* modifiers = GlobalNamespace::GameplayModifiers::New_ctor( 52 | energyType, 53 | std::count(strings.begin(), strings.end(), "NoFail"), 54 | std::count(strings.begin(), strings.end(), "InstaFail"), 55 | false, 56 | enabledObstacleType, 57 | std::count(strings.begin(), strings.end(), "NoBombs"), 58 | false, 59 | std::count(strings.begin(), strings.end(), "StrictAngles"), 60 | std::count(strings.begin(), strings.end(), "DisappearingArrows"), 61 | songSpeed, 62 | std::count(strings.begin(), strings.end(), "NoArrows"), 63 | std::count(strings.begin(), strings.end(), "GhostNotes"), 64 | std::count(strings.begin(), strings.end(), "ProMode"), 65 | std::count(strings.begin(), strings.end(), "ZenMode"), 66 | std::count(strings.begin(), strings.end(), "SmallCubes") 67 | ); 68 | 69 | return modifiers; 70 | } 71 | 72 | static std::string GetInitialsFromModifierName(std::string modifierName) { 73 | if(modifierName == "BatteryEnergy") { 74 | return "BE"; 75 | } else if(modifierName == "InstaFail") { 76 | return "IF"; 77 | } else if(modifierName == "NoFail") { 78 | return "NF"; 79 | } else if(modifierName == "NoWalls") { 80 | return "NW"; 81 | } else if(modifierName == "NoBombs") { 82 | return "NB"; 83 | } else if(modifierName == "StrictAngles") { 84 | return "SA"; 85 | } else if(modifierName == "DisappearingArrows") { 86 | return "DA"; 87 | } else if(modifierName == "SlowerSong") { 88 | return "SS"; 89 | } else if(modifierName == "FasterSong") { 90 | return "FS"; 91 | } else if(modifierName == "SuperFastSong") { 92 | return "SFS"; 93 | } else if(modifierName == "NoArrows") { 94 | return "NA"; 95 | } else if(modifierName == "GhostNotes") { 96 | return "GN"; 97 | } else if(modifierName == "ProMode") { 98 | return "PM"; 99 | } else if(modifierName == "ZenMode") { 100 | return "ZM"; 101 | } else if(modifierName == "SmallCubes") { 102 | return "SC"; 103 | } 104 | 105 | return "No matching modifier"; 106 | } 107 | }; 108 | } -------------------------------------------------------------------------------- /include/Utils/ReplayUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "Utils/SongUtils.hpp" 5 | #include "Utils/SaberUtils.hpp" 6 | 7 | #include "GlobalNamespace/NoteController.hpp" 8 | #include "GlobalNamespace/NoteData.hpp" 9 | #include "GlobalNamespace/NoteCutInfo.hpp" 10 | #include "GlobalNamespace/SaberSwingRatingCounter.hpp" 11 | #include "GlobalNamespace/GameplayModifiers.hpp" 12 | #include "EventTypes.hpp" 13 | #include 14 | 15 | namespace Replay { 16 | class ReplayUtils { 17 | public: 18 | static std::string GetFileName(std::string_view hash) { 19 | return hash + replayFileExtension; 20 | } 21 | 22 | static std::string GetModDirectory() { 23 | return "/sdcard/ModData/com.beatgames.beatsaber/Mods/Replay/"; 24 | } 25 | 26 | static std::string GetReplaysDirectory() { 27 | return GetModDirectory() + "Replays/"; 28 | } 29 | 30 | static std::string GetReplayFilePath(std::string_view hash) { 31 | return GetReplaysDirectory() + GetFileName(hash); 32 | } 33 | 34 | static std::string GetTempReplayFilePath() { 35 | return GetModDirectory() + "temp" + replayFileExtension; 36 | } 37 | 38 | template 39 | static constexpr int GetCurrentIndex(std::vector const& events, int lastIndex) { 40 | float songTime = Replay::SongUtils::GetSongTime(); 41 | int eventsLength = events.size(); 42 | 43 | int iterations = 0; 44 | if(songTime < events[lastIndex].time) { 45 | while(events[lastIndex + iterations - 1].time > songTime && lastIndex + iterations - 1 >= 0) { 46 | iterations--; 47 | } 48 | } else { 49 | while(events[lastIndex + iterations + 1].time < songTime && lastIndex + iterations + 1 < eventsLength) { 50 | iterations++; 51 | } 52 | } 53 | 54 | return lastIndex + iterations; 55 | } 56 | 57 | template 58 | static constexpr float LerpAmountBetweenEvents(T const& eventA, T const& eventB) { 59 | float timeA = eventA.time; 60 | float timeB = eventB.time; 61 | 62 | float songTime = Replay::SongUtils::GetSongTime(); 63 | 64 | return (songTime - timeA) / (timeB - timeA); 65 | } 66 | 67 | static int GetNoteHash(GlobalNamespace::NoteData* noteData) { 68 | std::hash noteDataHash; 69 | 70 | return (int) noteDataHash(noteData); 71 | } 72 | 73 | static GlobalNamespace::NoteCutInfo CreateNoteCutInfoFromSimple(Replay::NoteEventTypes::SimpleNoteCutInfo simpleNoteCutInfo, GlobalNamespace::NoteData* noteData) { 74 | return GlobalNamespace::NoteCutInfo( 75 | noteData, 76 | simpleNoteCutInfo.speedOK, 77 | simpleNoteCutInfo.directionOK, 78 | simpleNoteCutInfo.saberTypeOK, 79 | simpleNoteCutInfo.wasCutTooSoon, 80 | simpleNoteCutInfo.saberSpeed, 81 | simpleNoteCutInfo.saberDir, 82 | simpleNoteCutInfo.saberType, 83 | simpleNoteCutInfo.timeDeviation, 84 | simpleNoteCutInfo.cutDirDeviation, 85 | simpleNoteCutInfo.cutPoint, 86 | simpleNoteCutInfo.cutNormal, 87 | simpleNoteCutInfo.cutAngle, 88 | simpleNoteCutInfo.cutDistanceToCenter, 89 | UnityEngine::Quaternion::Euler(simpleNoteCutInfo.worldRotation), 90 | UnityEngine::Quaternion::Inverse(UnityEngine::Quaternion::Euler(simpleNoteCutInfo.worldRotation)), 91 | UnityEngine::Quaternion::Euler(simpleNoteCutInfo.noteRotation), 92 | simpleNoteCutInfo.notePosition, 93 | reinterpret_cast(Replay::SaberUtils::GetSaberForType(simpleNoteCutInfo.saberType)->get_movementData()) 94 | ); 95 | } 96 | }; 97 | } -------------------------------------------------------------------------------- /include/Utils/SaberUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "GlobalNamespace/NoteController.hpp" 5 | #include "GlobalNamespace/GameNoteController.hpp" 6 | #include "GlobalNamespace/NoteData.hpp" 7 | #include "GlobalNamespace/Saber.hpp" 8 | #include "GlobalNamespace/SaberManager.hpp" 9 | #include "GlobalNamespace/SaberTypeObject.hpp" 10 | #include "GlobalNamespace/SaberType.hpp" 11 | #include "GlobalNamespace/SaberSwingRatingCounter.hpp" 12 | #include "GlobalNamespace/ISaberSwingRatingCounter.hpp" 13 | 14 | namespace Replay { 15 | 16 | struct SaberUtils { 17 | inline static GlobalNamespace::SaberManager* saberManager; 18 | 19 | static GlobalNamespace::Saber* GetSaberForType(GlobalNamespace::SaberType saberType) { 20 | return saberType == saberManager->leftSaber->saberType->saberType ? saberManager->leftSaber : saberManager->rightSaber; 21 | } 22 | }; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /include/Utils/SongUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "GlobalNamespace/AudioTimeSyncController.hpp" 5 | #include "GlobalNamespace/StandardLevelDetailView.hpp" 6 | #include "GlobalNamespace/BeatmapLevelSO.hpp" 7 | #include "GlobalNamespace/IDifficultyBeatmap.hpp" 8 | #include "GlobalNamespace/IDifficultyBeatmapSet.hpp" 9 | #include "GlobalNamespace/BeatmapDifficulty.hpp" 10 | #include "GlobalNamespace/BeatmapCharacteristicSO.hpp" 11 | #include "GlobalNamespace/ScoreController.hpp" 12 | #include "GlobalNamespace/PlayerSpecificSettings.hpp" 13 | 14 | using namespace il2cpp_utils; 15 | 16 | namespace Replay { 17 | class SongUtils { 18 | private: 19 | static inline std::string mapID; 20 | public: 21 | static inline GlobalNamespace::AudioTimeSyncController* audioTimeSyncController; 22 | 23 | static inline bool inSong; 24 | 25 | static float GetSongTime() { 26 | static auto const *timeSyncControllerClass = classof(GlobalNamespace::AudioTimeSyncController *); 27 | auto *timeSourceObject = reinterpret_cast(audioTimeSyncController); 28 | if (timeSourceObject->klass == timeSyncControllerClass) { 29 | auto *timeSyncController = reinterpret_cast(audioTimeSyncController); 30 | return timeSyncController->songTime; 31 | } else { 32 | return audioTimeSyncController->get_songTime(); 33 | } 34 | } 35 | 36 | static std::string GetMapID(); 37 | 38 | static void SetMapID(GlobalNamespace::StandardLevelDetailView* standardLevelDetailView); 39 | 40 | static inline GlobalNamespace::ScoreController* scoreController; 41 | 42 | static inline GlobalNamespace::PlayerSpecificSettings* playerSpecificSettings; 43 | 44 | static inline bool didFail; 45 | static inline float failTime; 46 | 47 | static inline GlobalNamespace::IBeatmapLevel* beatmapLevel; 48 | static inline GlobalNamespace::BeatmapDifficulty beatmapDifficulty; 49 | static inline GlobalNamespace::BeatmapCharacteristicSO* beatmapCharacteristic; 50 | static inline GlobalNamespace::IReadonlyBeatmapData* beatmapData; 51 | }; 52 | } -------------------------------------------------------------------------------- /include/Utils/TimeUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include 5 | #include 6 | 7 | namespace Replay { 8 | class TimeUtils { 9 | public: 10 | static std::string GetStringForTimeSince(std::time_t start, std::time_t end) { 11 | auto startTimePoint = std::chrono::system_clock::from_time_t(start); 12 | auto endTimePoint = std::chrono::system_clock::from_time_t(end); 13 | 14 | std::chrono::duration duration = endTimePoint - startTimePoint; 15 | 16 | int seconds = std::chrono::duration_cast(duration).count(); 17 | int minutes = seconds / 60; 18 | int hours = minutes / 60; 19 | int days = hours / 24; 20 | int weeks = days / 7; 21 | int months = weeks / 4; 22 | int years = weeks / 52; 23 | 24 | std::string unit; 25 | int value; 26 | 27 | if(years != 0) { 28 | unit = "year"; 29 | value = years; 30 | } else if(months != 0) { 31 | unit = "month"; 32 | value = months; 33 | } else if(weeks != 0) { 34 | unit = "week"; 35 | value = weeks; 36 | } else if(days != 0) { 37 | unit = "day"; 38 | value = days; 39 | } else if(hours != 0) { 40 | unit = "hour"; 41 | value = hours; 42 | } else if(minutes != 0) { 43 | unit = "minute"; 44 | value = minutes; 45 | } else { 46 | unit = "second"; 47 | value = seconds; 48 | } 49 | 50 | if(value != 1) { 51 | unit = unit + "s"; 52 | } 53 | 54 | return std::to_string(value) + " " + unit + " ago"; 55 | } 56 | 57 | static std::string SecondsToString(float value) { 58 | int minutes = (int)(value / 60.0f); 59 | int seconds = (int)(value - (float(minutes) * 60.0f)); 60 | 61 | std::string minutesString = std::to_string(minutes); 62 | std::string secondsString = std::to_string(seconds); 63 | if(seconds < 10) { 64 | secondsString = "0" + std::to_string(seconds); 65 | } 66 | 67 | return minutesString+":"+secondsString; 68 | } 69 | }; 70 | } -------------------------------------------------------------------------------- /include/Utils/TypeUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | namespace Replay::TypeUtils { 5 | std::string FloatToString(float value, int precision = 2) { 6 | float power = (float) pow(100, precision); 7 | value = std::round(value * power) / power; 8 | 9 | std::stringstream stream; 10 | stream << std::fixed << std::setprecision(precision) << value; 11 | 12 | return stream.str(); 13 | } 14 | } -------------------------------------------------------------------------------- /include/Utils/UIUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | namespace Replay::UIUtils { 5 | std::string GetLayeredText(std::string label, std::string value) { 6 | return "" + label + "\n" + value + ""; 7 | } 8 | } -------------------------------------------------------------------------------- /include/Utils/UnityUtils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "static-defines.hpp" 3 | 4 | #include "UnityEngine/Transform.hpp" 5 | #include "UnityEngine/GameObject.hpp" 6 | 7 | namespace Replay::UnityUtils { 8 | void SetAllActive(UnityEngine::Transform* transform, bool active) { 9 | transform->get_gameObject()->set_active(active); 10 | 11 | int childCount = transform->get_childCount(); 12 | for(int i = 0; i < childCount; i++) { 13 | SetAllActive(transform->GetChild(i), active); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /include/hooks.hpp: -------------------------------------------------------------------------------- 1 | // Implementation by https://github.com/StackDoubleFlow 2 | // yoinked from 3 | #pragma once 4 | #include "beatsaber-hook/shared/utils/logging.hpp" 5 | #include 6 | 7 | namespace Replay { 8 | class Hooks { 9 | private: 10 | static inline std::vector installFuncs; 11 | public: 12 | static void AddInstallFunc(void(*installFunc)(Logger& logger)) 13 | { 14 | installFuncs.push_back(installFunc); 15 | } 16 | 17 | static void InstallHooks(Logger& logger) 18 | { 19 | for (auto installFunc : installFuncs) 20 | { 21 | installFunc(logger); 22 | } 23 | } 24 | }; 25 | } 26 | 27 | #define ReplayInstallHooks(func) \ 28 | struct __ReplayRegister##func { \ 29 | __ReplayRegister##func() { \ 30 | Replay::Hooks::AddInstallFunc(func); \ 31 | } \ 32 | }; \ 33 | static __ReplayRegister##func __ReplayRegisterInstance##func; -------------------------------------------------------------------------------- /include/static-defines.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "modloader/shared/modloader.hpp" 4 | #include "beatsaber-hook/shared/utils/logging.hpp" 5 | #include "beatsaber-hook/shared/utils/hooking.hpp" 6 | #include "custom-types/shared/register.hpp" 7 | #include "questui/shared/QuestUI.hpp" 8 | #include "hooks.hpp" 9 | 10 | using byte = unsigned char; 11 | 12 | #define eventIdByteSize 1 13 | #define eventCountByteSize 4 14 | 15 | #define replayMagicBytes 0x443d3d38 16 | #define fileVersion 0b00000000 17 | #define replayFileExtension ".questplay" 18 | 19 | // My excuse for it being defined here is for universal colors across the mod, definitely belongs in this file yep 20 | #define RED std::string("#cc1818") 21 | #define GREEN std::string("#2adb44") 22 | #define TEAL std::string("#1dbcd1") 23 | 24 | #define USE_CODEGEN_FIELDS 25 | 26 | static ModInfo modInfo; 27 | 28 | static Logger& replayLogger() 29 | { 30 | modInfo.id = ID; 31 | modInfo.version = VERSION; 32 | static Logger* logger = new Logger(modInfo, LoggerOptions(false, true)); 33 | return *logger; 34 | } 35 | 36 | #define log(...) replayLogger().info(__VA_ARGS__) -------------------------------------------------------------------------------- /ndk-stack.ps1: -------------------------------------------------------------------------------- 1 | $NDKPath = Get-Content ./ndkpath.txt 2 | 3 | $stackScript = "$NDKPath/ndk-stack" 4 | if (-not ($PSVersionTable.PSEdition -eq "Core")) { 5 | $stackScript += ".cmd" 6 | } 7 | 8 | Get-Content ./log.log | & $stackScript -sym ./build/debug/ > log_unstripped.log -------------------------------------------------------------------------------- /qpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "sharedDir": "shared", 3 | "dependenciesDir": "extern", 4 | "info": { 5 | "name": "Replay", 6 | "id": "Replay", 7 | "version": "1.0.0", 8 | "url": null, 9 | "additionalData": { 10 | "overrideSoName": "libreplay.so" 11 | } 12 | }, 13 | "dependencies": [ 14 | { 15 | "id": "beatsaber-hook", 16 | "versionRange": "*", 17 | "additionalData": { 18 | "extraFiles": [ 19 | "src/inline-hook" 20 | ] 21 | } 22 | }, 23 | { 24 | "id": "questui", 25 | "versionRange": "*", 26 | "additionalData": {} 27 | }, 28 | { 29 | "id": "codegen", 30 | "versionRange": "*", 31 | "additionalData": {} 32 | }, 33 | { 34 | "id": "custom-types", 35 | "versionRange": "*", 36 | "additionalData": {} 37 | } 38 | ], 39 | "additionalData": {} 40 | } -------------------------------------------------------------------------------- /qpm_defines.cmake: -------------------------------------------------------------------------------- 1 | # YOU SHOULD NOT MANUALLY EDIT THIS FILE, QPM WILL VOID ALL CHANGES 2 | # Version defines, pretty useful 3 | set(MOD_VERSION "1.0.0") 4 | # take the mod name and just remove spaces, that will be MOD_ID, if you don't like it change it after the include of this file 5 | set(MOD_ID "Replay") 6 | 7 | # derived from override .so name or just id_version 8 | set(COMPILE_ID "replay") 9 | # derived from whichever codegen package is installed, will default to just codegen 10 | set(CODEGEN_ID "codegen") 11 | 12 | # given from qpm, automatically updated from qpm.json 13 | set(EXTERN_DIR_NAME "extern") 14 | set(SHARED_DIR_NAME "shared") 15 | 16 | # if no target given, use Debug 17 | if (NOT DEFINED CMAKE_BUILD_TYPE) 18 | set(CMAKE_BUILD_TYPE "Debug") 19 | endif() 20 | 21 | # defines used in ninja / cmake ndk builds 22 | if (NOT DEFINED CMAKE_ANDROID_NDK) 23 | if (EXISTS "${CMAKE_CURRENT_LIST_DIR}/ndkpath.txt") 24 | file (STRINGS "ndkpath.txt" CMAKE_ANDROID_NDK) 25 | else() 26 | if(EXISTS $ENV{ANDROID_NDK_HOME}) 27 | set(CMAKE_ANDROID_NDK $ENV{ANDROID_NDK_HOME}) 28 | elseif(EXISTS $ENV{ANDROID_NDK_LATEST_HOME}) 29 | set(CMAKE_ANDROID_NDK $ENV{ANDROID_NDK_LATEST_HOME}) 30 | endif() 31 | endif() 32 | endif() 33 | if (NOT DEFINED CMAKE_ANDROID_NDK) 34 | message(Big time error buddy, no NDK) 35 | endif() 36 | message(Using NDK ${CMAKE_ANDROID_NDK}) 37 | string(REPLACE "\\" "/" CMAKE_ANDROID_NDK ${CMAKE_ANDROID_NDK}) 38 | 39 | set(ANDROID_PLATFORM 24) 40 | set(ANDROID_ABI arm64-v8a) 41 | set(ANDROID_STL c++_static) 42 | 43 | set(CMAKE_TOOLCHAIN_FILE ${CMAKE_ANDROID_NDK}/build/cmake/android.toolchain.cmake) 44 | # define used for external data, mostly just the qpm dependencies 45 | set(EXTERN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${EXTERN_DIR_NAME}) 46 | set(SHARED_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${SHARED_DIR_NAME}) 47 | # get files by filter recursively 48 | MACRO(RECURSE_FILES return_list filter) 49 | FILE(GLOB_RECURSE new_list ${filter}) 50 | SET(file_list "") 51 | FOREACH(file_path ${new_list}) 52 | SET(file_list ${file_list} ${file_path}) 53 | ENDFOREACH() 54 | LIST(REMOVE_DUPLICATES file_list) 55 | SET(${return_list} ${file_list}) 56 | ENDMACRO() -------------------------------------------------------------------------------- /src/Hooks/AudioTimeSyncController.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/AudioTimeSyncController.hpp" 4 | #include "UnityEngine/AudioSource.hpp" 5 | #include "Utils/SongUtils.hpp" 6 | #include "ReplayManager.hpp" 7 | 8 | using namespace GlobalNamespace; 9 | using namespace Replay; 10 | 11 | MAKE_HOOK_FIND_INSTANCE(AudioTimeSyncController_ctor, classof(AudioTimeSyncController*), ".ctor", void, AudioTimeSyncController* self) { 12 | AudioTimeSyncController_ctor(self); 13 | 14 | SongUtils::audioTimeSyncController = self; 15 | SongUtils::inSong = true; 16 | } 17 | 18 | MAKE_HOOK_MATCH(SongUpdate, &AudioTimeSyncController::Update, void, AudioTimeSyncController* self) { 19 | 20 | // log("SongUpdate"); 21 | 22 | if(ReplayManager::replayState == ReplayState::REPLAYING) { 23 | // UnityEngine::AudioSource* audio = self->audioSource; 24 | 25 | // float roundedReplaySpeed = float(int((0.25f)*100))/100; 26 | 27 | // self->timeScale = roundedReplaySpeed; 28 | // audio->set_pitch(roundedReplaySpeed); 29 | } 30 | 31 | SongUpdate(self); 32 | } 33 | 34 | void AudioTimeSyncControllerHook(Logger& logger) { 35 | INSTALL_HOOK(logger, AudioTimeSyncController_ctor); 36 | INSTALL_HOOK(logger, SongUpdate); 37 | } 38 | 39 | ReplayInstallHooks(AudioTimeSyncControllerHook); -------------------------------------------------------------------------------- /src/Hooks/HapticFeedbackController.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/HapticFeedbackController.hpp" 4 | #include "Utils/SongUtils.hpp" 5 | #include "ReplayManager.hpp" 6 | 7 | using namespace GlobalNamespace; 8 | using namespace Replay; 9 | 10 | MAKE_HOOK_MATCH(HapticFeedbackController_PlayHapticFeedback, &HapticFeedbackController::PlayHapticFeedback, void, HapticFeedbackController* self, UnityEngine::XR::XRNode node, Libraries::HM::HMLib::VR::HapticPresetSO* hapticPreset) { 11 | if(SongUtils::inSong && ReplayManager::replayState == ReplayState::REPLAYING) return; 12 | 13 | HapticFeedbackController_PlayHapticFeedback(self, node, hapticPreset); 14 | } 15 | 16 | void HapticFeedbackControllerHook(Logger& logger) { 17 | INSTALL_HOOK(logger, HapticFeedbackController_PlayHapticFeedback); 18 | } 19 | 20 | ReplayInstallHooks(HapticFeedbackControllerHook); -------------------------------------------------------------------------------- /src/Hooks/Notes/BombNoteController.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/BombNoteController.hpp" 4 | #include "ReplayManager.hpp" 5 | 6 | using namespace GlobalNamespace; 7 | using namespace Replay; 8 | 9 | MAKE_HOOK_MATCH(BombNoteController_NoteDidPassMissedMarker, &BombNoteController::NoteDidPassMissedMarker, void, BombNoteController* self) { 10 | if(ReplayManager::replayState == ReplayState::REPLAYING) { 11 | return; 12 | } 13 | 14 | BombNoteController_NoteDidPassMissedMarker(self); 15 | } 16 | 17 | void BombNoteControllerHook(Logger& logger) { 18 | INSTALL_HOOK(logger, BombNoteController_NoteDidPassMissedMarker); 19 | } 20 | 21 | ReplayInstallHooks(BombNoteControllerHook); -------------------------------------------------------------------------------- /src/Hooks/Notes/CutScoreBuffer.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/CutScoreBuffer.hpp" 4 | #include "GlobalNamespace/NoteCutInfo.hpp" 5 | #include "GlobalNamespace/NoteData.hpp" 6 | #include "GlobalNamespace/ISaberSwingRatingCounter.hpp" 7 | #include "ReplayManager.hpp" 8 | #include "Recording/NoteEventRecorder.hpp" 9 | 10 | using namespace GlobalNamespace; 11 | using namespace Replay; 12 | 13 | MAKE_HOOK_MATCH(CutScoreBuffer_HandleSaberSwingRatingCounterDidFinish, &CutScoreBuffer::HandleSaberSwingRatingCounterDidFinish, void, CutScoreBuffer* self, ISaberSwingRatingCounter* swingRatingCounter) { 14 | CutScoreBuffer_HandleSaberSwingRatingCounterDidFinish(self, swingRatingCounter); 15 | 16 | if(ReplayManager::replayState == ReplayState::RECORDING) { 17 | auto &cutTimes = ReplayManager::recorder.noteEventRecorder.cutTimes; 18 | 19 | for (auto eventIt = cutTimes.begin(); eventIt != cutTimes.end(); eventIt++) { 20 | auto const &cutTime = *eventIt; 21 | 22 | if(cutTime.second == self) { 23 | ReplayManager::recorder.noteEventRecorder.AddCutEvent(self, cutTime.first); 24 | 25 | cutTimes.erase(eventIt); 26 | 27 | return; 28 | } 29 | } 30 | } 31 | } 32 | 33 | MAKE_HOOK_MATCH(CutScoreBuffer_Init, &CutScoreBuffer::Init, bool, CutScoreBuffer* self, ByRef noteCutInfo) { 34 | bool returnValue = CutScoreBuffer_Init(self, noteCutInfo); 35 | 36 | if(ReplayManager::replayState == ReplayState::RECORDING) { 37 | if(returnValue) { 38 | ReplayManager::recorder.noteEventRecorder.cutTimes.push_back(std::make_pair(SongUtils::GetSongTime(), self)); 39 | } else { 40 | ReplayManager::recorder.noteEventRecorder.AddCutEvent(self, SongUtils::GetSongTime()); 41 | } 42 | } 43 | 44 | return returnValue; 45 | } 46 | 47 | void CutScoreBufferHook(Logger& logger) { 48 | INSTALL_HOOK(logger, CutScoreBuffer_HandleSaberSwingRatingCounterDidFinish); 49 | INSTALL_HOOK(logger, CutScoreBuffer_Init); 50 | } 51 | 52 | ReplayInstallHooks(CutScoreBufferHook); -------------------------------------------------------------------------------- /src/Hooks/Notes/FlyingScoreEffect.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/FlyingScoreEffect.hpp" 4 | #include "GlobalNamespace/NoteCutInfo.hpp" 5 | #include "GlobalNamespace/SaberSwingRatingCounter.hpp" 6 | #include "GlobalNamespace/ISaberSwingRatingCounter.hpp" 7 | #include "ReplayManager.hpp" 8 | 9 | using namespace GlobalNamespace; 10 | using namespace Replay; 11 | 12 | // MAKE_HOOK_MATCH(FlyingScoreEffect_InitAndPresent, &FlyingScoreEffect::InitAndPresent, void, FlyingScoreEffect* self, ByRef noteCutInfo, int multiplier, float duration, UnityEngine::Vector3 targetPos, UnityEngine::Quaternion rotation, UnityEngine::Color color) { 13 | // FlyingScoreEffect_InitAndPresent(self, noteCutInfo, multiplier, duration, targetPos, rotation, color); 14 | 15 | // if(ReplayManager::replayState == ReplayState::REPLAYING) { 16 | // self->HandleSaberSwingRatingCounterDidChange(reinterpret_cast(noteCutInfo->swingRatingCounter), 0); 17 | // } 18 | // } 19 | 20 | void FlyingScoreEffectHook(Logger& logger) { 21 | // INSTALL_HOOK(logger, FlyingScoreEffect_InitAndPresent); 22 | } 23 | 24 | ReplayInstallHooks(FlyingScoreEffectHook); -------------------------------------------------------------------------------- /src/Hooks/Notes/GameNoteController.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/GameNoteController.hpp" 4 | #include "ReplayManager.hpp" 5 | 6 | using namespace GlobalNamespace; 7 | using namespace Replay; 8 | 9 | MAKE_HOOK_MATCH(GameNoteController_NoteDidPassMissedMarker, &GameNoteController::NoteDidPassMissedMarker, void, GameNoteController* self) { 10 | if(ReplayManager::replayState == ReplayState::REPLAYING) { 11 | return; 12 | } 13 | 14 | GameNoteController_NoteDidPassMissedMarker(self); 15 | } 16 | 17 | void GameNoteControllerHook(Logger& logger) { 18 | INSTALL_HOOK(logger, GameNoteController_NoteDidPassMissedMarker); 19 | } 20 | 21 | ReplayInstallHooks(GameNoteControllerHook); -------------------------------------------------------------------------------- /src/Hooks/Notes/GoodCutScoringElement.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/CutScoreBuffer.hpp" 4 | #include "GlobalNamespace/NoteCutInfo.hpp" 5 | #include "GlobalNamespace/NoteData.hpp" 6 | #include "GlobalNamespace/GoodCutScoringElement.hpp" 7 | #include "ReplayManager.hpp" 8 | #include "Recording/NoteEventRecorder.hpp" 9 | 10 | using namespace GlobalNamespace; 11 | using namespace Replay; 12 | 13 | MAKE_HOOK_MATCH(GoodCutScoringElement_Init, &GoodCutScoringElement::Init, void, GoodCutScoringElement* self, NoteCutInfo noteCutInfo) { 14 | GoodCutScoringElement_Init(self, noteCutInfo); 15 | 16 | if(ReplayManager::replayState == ReplayState::REPLAYING) { 17 | self->cutScoreBuffer->saberSwingRatingCounter->Finish(); 18 | 19 | float beforeCutScore = 0.0f; 20 | float afterCutScore = 0.0f; 21 | 22 | auto noteHash = Replay::ReplayUtils::GetNoteHash(noteCutInfo.noteData); 23 | 24 | for (auto eventIt = ReplayManager::replayer.noteEventReplayer.swingRatings.begin(); eventIt != ReplayManager::replayer.noteEventReplayer.swingRatings.end(); eventIt++) { 25 | auto const &swingRating = *eventIt; 26 | 27 | if(noteHash == swingRating.noteHash) { 28 | beforeCutScore = swingRating.swingRating.beforeCutRating; 29 | afterCutScore = swingRating.swingRating.afterCutRating; 30 | 31 | ReplayManager::replayer.noteEventReplayer.swingRatings.erase(eventIt); 32 | 33 | break; 34 | } 35 | } 36 | 37 | self->cutScoreBuffer->afterCutScore = beforeCutScore; 38 | self->cutScoreBuffer->beforeCutScore = afterCutScore; 39 | } 40 | } 41 | 42 | void GoodCutScoringElementHook(Logger& logger) { 43 | INSTALL_HOOK(logger, GoodCutScoringElement_Init); 44 | } 45 | 46 | ReplayInstallHooks(GoodCutScoringElementHook); -------------------------------------------------------------------------------- /src/Hooks/Notes/NoteController.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/NoteController.hpp" 4 | #include "GlobalNamespace/NoteCutInfo.hpp" 5 | #include "ReplayManager.hpp" 6 | #include "Replaying/NoteEventReplayer.hpp" 7 | 8 | using namespace GlobalNamespace; 9 | using namespace Replay; 10 | 11 | MAKE_HOOK_MATCH(NoteController_Init, &NoteController::Init, void, GlobalNamespace::NoteController* self, NoteData* noteData, float worldRotation, UnityEngine::Vector3 moveStartPos, UnityEngine::Vector3 moveEndPos, UnityEngine::Vector3 jumpEndPos, float moveDuration, float jumpDuration, float jumpGravity, float endRotation, float uniformScale, bool rotateTowardsPlayer, bool useRandomRotation) { 12 | NoteController_Init(self, noteData, worldRotation, moveStartPos, moveEndPos, jumpEndPos, moveDuration, jumpDuration, jumpGravity, endRotation, uniformScale, rotateTowardsPlayer, useRandomRotation); 13 | 14 | if(ReplayManager::replayState == ReplayState::REPLAYING) { 15 | ReplayManager::replayer.noteEventReplayer.AddActiveEvents(self); 16 | } 17 | } 18 | 19 | MAKE_HOOK_MATCH(NoteController_SendNoteWasCutEvent, &NoteController::SendNoteWasCutEvent, void, NoteController* self, ByRef noteCutInfo) { 20 | NoteController_SendNoteWasCutEvent(self, noteCutInfo); 21 | 22 | if(ReplayManager::replayState == ReplayState::RECORDING && !noteCutInfo->get_allIsOK()) { 23 | ReplayManager::recorder.noteEventRecorder.AddCutEvent(noteCutInfo.heldRef, SongUtils::GetSongTime()); 24 | } 25 | } 26 | 27 | MAKE_HOOK_MATCH(NoteController_SendNoteWasMissedEvent, &NoteController::SendNoteWasMissedEvent, void, NoteController* self) { 28 | NoteController_SendNoteWasMissedEvent(self); 29 | 30 | if(ReplayManager::replayState == ReplayState::RECORDING) { 31 | ReplayManager::recorder.noteEventRecorder.AddMissEvent(self); 32 | } 33 | } 34 | 35 | void NoteControllerHook(Logger& logger) { 36 | INSTALL_HOOK(logger, NoteController_Init); 37 | INSTALL_HOOK(logger, NoteController_SendNoteWasCutEvent); 38 | INSTALL_HOOK(logger, NoteController_SendNoteWasMissedEvent); 39 | } 40 | 41 | ReplayInstallHooks(NoteControllerHook); -------------------------------------------------------------------------------- /src/Hooks/Notes/NoteCutter.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/NoteCutter.hpp" 4 | #include "GlobalNamespace/Saber.hpp" 5 | #include "ReplayManager.hpp" 6 | 7 | using namespace GlobalNamespace; 8 | using namespace Replay; 9 | 10 | MAKE_HOOK_MATCH(NoteCutter_Cut, &NoteCutter::Cut, void, NoteCutter* self, Saber* saber) { 11 | if(ReplayManager::replayState == ReplayState::REPLAYING) { 12 | return; 13 | } 14 | 15 | NoteCutter_Cut(self, saber); 16 | } 17 | 18 | void NoteCutterHook(Logger& logger) { 19 | INSTALL_HOOK(logger, NoteCutter_Cut); 20 | } 21 | 22 | ReplayInstallHooks(NoteCutterHook); -------------------------------------------------------------------------------- /src/Hooks/Notes/RelativeScoreAndImmediateRankCounter.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/RelativeScoreAndImmediateRankCounter.hpp" 4 | #include "GlobalNamespace/ScoreModel.hpp" 5 | #include "Utils/SongUtils.hpp" 6 | #include "ReplayManager.hpp" 7 | 8 | using namespace GlobalNamespace; 9 | using namespace Replay; 10 | 11 | // MAKE_HOOK_MATCH(RelativeScoreAndImmediateRankCounter_HandleScoreControllerImmediateMaxPossibleScoreDidChange, &RelativeScoreAndImmediateRankCounter::HandleScoreControllerImmediateMaxPossibleScoreDidChange, void, RelativeScoreAndImmediateRankCounter* self, int immediateMaxPossibleScore, int immediateMaxPossibleModifiedScore) { 12 | // if(ReplayManager::replayState == ReplayState::REPLAYING) { 13 | // immediateMaxPossibleScore = ScoreModel::MaxRawScoreForNumberOfNotes(SongUtils::scoreController->cutOrMissedNotes); 14 | // immediateMaxPossibleModifiedScore = ScoreModel::GetModifiedScoreForGameplayModifiersScoreMultiplier(immediateMaxPossibleScore, SongUtils::scoreController->gameplayModifiersScoreMultiplier); 15 | // } 16 | 17 | // RelativeScoreAndImmediateRankCounter_HandleScoreControllerImmediateMaxPossibleScoreDidChange(self, immediateMaxPossibleScore, immediateMaxPossibleModifiedScore); 18 | // } 19 | 20 | // void RelativeScoreAndImmediateRankCounterHook(Logger& logger) { 21 | // INSTALL_HOOK(logger, RelativeScoreAndImmediateRankCounter_HandleScoreControllerImmediateMaxPossibleScoreDidChange); 22 | // } 23 | 24 | // ReplayInstallHooks(RelativeScoreAndImmediateRankCounterHook); -------------------------------------------------------------------------------- /src/Hooks/Notes/SaberSwingRatingCounter.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/SaberSwingRatingCounter.hpp" 4 | #include "GlobalNamespace/BladeMovementDataElement.hpp" 5 | #include "Recording/NoteEventRecorder.hpp" 6 | #include "Recording/ReplayRecorder.hpp" 7 | #include "ReplayManager.hpp" 8 | 9 | using namespace GlobalNamespace; 10 | using namespace Replay; 11 | 12 | MAKE_HOOK_MATCH(SaberSwingRatingCounter_ProcessNewData, &SaberSwingRatingCounter::ProcessNewData, void, SaberSwingRatingCounter* self, BladeMovementDataElement newData, BladeMovementDataElement prevData, bool prevDataAreValid) { 13 | if(ReplayManager::replayState == ReplayState::REPLAYING) { 14 | self->Finish(); 15 | return; 16 | } 17 | 18 | SaberSwingRatingCounter_ProcessNewData(self, newData, prevData, prevDataAreValid); 19 | } 20 | 21 | void SaberSwingRatingCounterHook(Logger& logger) { 22 | INSTALL_HOOK(logger, SaberSwingRatingCounter_ProcessNewData); 23 | } 24 | 25 | ReplayInstallHooks(SaberSwingRatingCounterHook); -------------------------------------------------------------------------------- /src/Hooks/Obstacles/GameEnergyCounter.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/GameEnergyCounter.hpp" 4 | #include "ReplayManager.hpp" 5 | #include "Recording/ObstacleEventRecorder.hpp" 6 | #include "Replaying/ObstacleEventReplayer.hpp" 7 | 8 | using namespace GlobalNamespace; 9 | using namespace Replay; 10 | 11 | MAKE_HOOK_MATCH(GameEnergyCounter_LateUpdate, &GameEnergyCounter::LateUpdate, void, GameEnergyCounter* self) { 12 | GameEnergyCounter_LateUpdate(self); 13 | 14 | if(ReplayManager::replayState == ReplayState::RECORDING) { 15 | ReplayManager::recorder.obstacleEventRecorder.AddEvent(self); 16 | } else if(ReplayManager::replayState == ReplayState::REPLAYING) { 17 | if(ReplayManager::replayer.obstacleEventReplayer.ShouldSetEnergy()) { 18 | auto& obstacleReplayer = ReplayManager::replayer.obstacleEventReplayer; 19 | float energyChange = obstacleReplayer.events[obstacleReplayer.nextEventIndex - 1].energy - self->get_energy(); 20 | 21 | self->ProcessEnergyChange(energyChange); 22 | } 23 | } 24 | } 25 | 26 | void GameEnergyCounterHook(Logger& logger) { 27 | INSTALL_HOOK(logger, GameEnergyCounter_LateUpdate); 28 | } 29 | 30 | ReplayInstallHooks(GameEnergyCounterHook); -------------------------------------------------------------------------------- /src/Hooks/PauseController.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/PauseController.hpp" 4 | #include "Utils/SongUtils.hpp" 5 | #include "ReplayManager.hpp" 6 | 7 | using namespace GlobalNamespace; 8 | using namespace Replay; 9 | 10 | MAKE_HOOK_MATCH(PauseController_HandlePauseMenuManagerDidPressMenuButton, &PauseController::HandlePauseMenuManagerDidPressMenuButton, void, PauseController* self) { 11 | PauseController_HandlePauseMenuManagerDidPressMenuButton(self); 12 | 13 | SongUtils::inSong = false; 14 | } 15 | 16 | void PauseControllerHook(Logger& logger) { 17 | INSTALL_HOOK(logger, PauseController_HandlePauseMenuManagerDidPressMenuButton); 18 | } 19 | 20 | ReplayInstallHooks(PauseControllerHook); -------------------------------------------------------------------------------- /src/Hooks/Player/PlayerTransforms.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/PlayerTransforms.hpp" 4 | #include "Recording/PlayerRecorder.hpp" 5 | #include "EventTypes.hpp" 6 | #include "ReplayManager.hpp" 7 | #include "fstream" 8 | 9 | using namespace GlobalNamespace; 10 | using namespace Replay; 11 | 12 | MAKE_HOOK_MATCH(PlayerTransforms_Update, &GlobalNamespace::PlayerTransforms::Update, void, GlobalNamespace::PlayerTransforms* self) { 13 | PlayerTransforms_Update(self); 14 | 15 | if(ReplayManager::replayState == ReplayState::RECORDING) { 16 | ReplayManager::recorder.playerRecorder.AddEvent(PlayerEventTypes::PlayerTransforms(self->headPseudoLocalPos, self->headPseudoLocalRot, self->leftHandPseudoLocalPos, self->leftHandPseudoLocalRot, self->rightHandPseudoLocalPos, self->rightHandPseudoLocalRot)); 17 | } else if(ReplayManager::replayState == ReplayState::REPLAYING) { 18 | ReplayManager::replayer.playerReplayer.SetPlayerTransforms(self); 19 | } 20 | } 21 | 22 | void PlayerTransformsHook(Logger& logger) { 23 | INSTALL_HOOK(logger, PlayerTransforms_Update); 24 | } 25 | 26 | ReplayInstallHooks(PlayerTransformsHook); -------------------------------------------------------------------------------- /src/Hooks/Player/SaberMovementData.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/SaberMovementData.hpp" 4 | #include "GlobalNamespace/SaberType.hpp" 5 | #include "Recording/PlayerRecorder.hpp" 6 | #include "EventTypes.hpp" 7 | #include "ReplayManager.hpp" 8 | #include "Utils/SaberUtils.hpp" 9 | #include "fstream" 10 | 11 | using namespace GlobalNamespace; 12 | using namespace Replay; 13 | 14 | MAKE_HOOK_MATCH(SaberMovementData_AddNewData, &SaberMovementData::AddNewData, void, SaberMovementData* self, UnityEngine::Vector3 topPos, UnityEngine::Vector3 bottomPos, float time) { 15 | SaberMovementData_AddNewData(self, topPos, bottomPos, time); 16 | 17 | if(ReplayManager::replayState == ReplayState::RECORDING) { 18 | SaberType saberType = self == SaberUtils::saberManager->leftSaber->get_movementData() ? SaberType::SaberA : SaberType::SaberB; 19 | 20 | ReplayManager::recorder.playerRecorder.AddSaberMovement(self->get_lastAddedData(), saberType); 21 | } 22 | } 23 | 24 | void SaberMovementDataHook(Logger& logger) { 25 | INSTALL_HOOK(logger, SaberMovementData_AddNewData); 26 | } 27 | 28 | ReplayInstallHooks(SaberMovementDataHook); -------------------------------------------------------------------------------- /src/Hooks/ResultsViewController.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/ResultsViewController.hpp" 4 | #include "GlobalNamespace/IReadonlyBeatmapData.hpp" 5 | #include "ReplayManager.hpp" 6 | #include "UI/UIManager.hpp" 7 | 8 | using namespace GlobalNamespace; 9 | using namespace Replay; 10 | 11 | MAKE_HOOK_MATCH(ResultsViewController_Init, &ResultsViewController::Init, void, ResultsViewController* self, LevelCompletionResults* levelCompletionResults, IReadonlyBeatmapData* transformedBeatmapData, IDifficultyBeatmap* difficultyBeatmap, bool practice, bool newHighScore) { 12 | ResultsViewController_Init(self, levelCompletionResults, transformedBeatmapData, difficultyBeatmap, practice, newHighScore); 13 | 14 | SongUtils::inSong = false; 15 | 16 | if(ReplayManager::replayState == ReplayState::RECORDING && !practice) { 17 | ReplayManager::recorder.StopRecording(levelCompletionResults); 18 | } 19 | } 20 | 21 | MAKE_HOOK_MATCH(ResultsViewController_DidActivate, &ResultsViewController::DidActivate, void, ResultsViewController* self, bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) { 22 | ResultsViewController_DidActivate(self, firstActivation, addedToHierarchy, screenSystemEnabling); 23 | 24 | UnityEngine::Transform* parent = self->restartButton->get_transform()->GetParent(); 25 | UnityEngine::Transform* buttonTransform = parent->Find(newcsstr("ReplayButton"));//Replay button name is defined in two other places, make better 26 | 27 | if(buttonTransform) { 28 | std::string path = fileexists(ReplayUtils::GetTempReplayFilePath()) ? ReplayUtils::GetTempReplayFilePath() : ReplayUtils::GetReplayFilePath(SongUtils::GetMapID()); 29 | Replay::UI::UIManager::SetReplayButtonOnClick(buttonTransform, path, true); 30 | } else { 31 | ((UnityEngine::RectTransform*) Replay::UI::UIManager::CreateReplayButton(parent, self->restartButton, self->restartButton, ReplayUtils::GetTempReplayFilePath(), true))->set_anchoredPosition({48, 0}); 32 | } 33 | } 34 | 35 | void ResultsViewControllerHook(Logger& logger) { 36 | INSTALL_HOOK(logger, ResultsViewController_Init); 37 | INSTALL_HOOK(logger, ResultsViewController_DidActivate); 38 | } 39 | 40 | ReplayInstallHooks(ResultsViewControllerHook); -------------------------------------------------------------------------------- /src/Hooks/SaberManager.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "Utils/SaberUtils.hpp" 4 | #include "GlobalNamespace/SaberManager.hpp" 5 | #include "UnityEngine/Resources.hpp" 6 | 7 | using namespace GlobalNamespace; 8 | using namespace Replay; 9 | 10 | MAKE_HOOK_MATCH(SaberManager_Start, &SaberManager::Start, void, SaberManager* self) { 11 | SaberManager_Start(self); 12 | 13 | SaberUtils::saberManager = self; 14 | } 15 | 16 | void SaberManagerHook(Logger& logger) { 17 | INSTALL_HOOK(logger, SaberManager_Start); 18 | } 19 | 20 | ReplayInstallHooks(SaberManagerHook); -------------------------------------------------------------------------------- /src/Hooks/ScoreController.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/ScoreController.hpp" 4 | #include "Utils/SongUtils.hpp" 5 | 6 | using namespace GlobalNamespace; 7 | using namespace Replay; 8 | 9 | MAKE_HOOK_MATCH(ScoreController_Start, &ScoreController::Start, void, ScoreController* self) { 10 | ScoreController_Start(self); 11 | 12 | SongUtils::scoreController = self; 13 | } 14 | 15 | void ScoreControllerHook(Logger& logger) { 16 | INSTALL_HOOK(logger, ScoreController_Start); 17 | } 18 | 19 | ReplayInstallHooks(ScoreControllerHook); -------------------------------------------------------------------------------- /src/Hooks/SinglePlayerLevelSelectionFlowCoordinator.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/SinglePlayerLevelSelectionFlowCoordinator.hpp" 4 | #include "Polyglot/Localization.hpp" 5 | #include "UI/UIManager.hpp" 6 | #include "HMUI/ViewController.hpp" 7 | #include "HMUI/ViewController_AnimationType.hpp" 8 | #include "HMUI/ViewController_AnimationDirection.hpp" 9 | 10 | using namespace GlobalNamespace; 11 | using namespace Replay::UI; 12 | using namespace HMUI; 13 | 14 | MAKE_HOOK_MATCH(SinglePlayerLevelSelectionFlowCoordinator_LevelSelectionFlowCoordinatorTopViewControllerWillChange, &SinglePlayerLevelSelectionFlowCoordinator::LevelSelectionFlowCoordinatorTopViewControllerWillChange, void, SinglePlayerLevelSelectionFlowCoordinator* self, ViewController* oldViewController, ViewController* newViewController, ViewController::AnimationType animationType) { 15 | if(newViewController == UIManager::replayViewController) { 16 | self->SetLeftScreenViewController(nullptr, animationType); 17 | self->SetRightScreenViewController(nullptr, animationType); 18 | self->SetBottomScreenViewController(nullptr, animationType); 19 | self->SetTitle(Polyglot::Localization::Get(il2cpp_utils::newcsstr("REPLAY")), animationType); 20 | self->set_showBackButton(true); 21 | return; 22 | } 23 | 24 | SinglePlayerLevelSelectionFlowCoordinator_LevelSelectionFlowCoordinatorTopViewControllerWillChange(self, oldViewController, newViewController, animationType); 25 | } 26 | 27 | MAKE_HOOK_MATCH(SinglePlayerLevelSelectionFlowCoordinator_BackButtonWasPressed, &SinglePlayerLevelSelectionFlowCoordinator::BackButtonWasPressed, void, SinglePlayerLevelSelectionFlowCoordinator* self, ViewController* topViewController) { 28 | if(topViewController == UIManager::replayViewController) { 29 | self->DismissViewController(UIManager::replayViewController, ViewController::AnimationDirection::Horizontal, nullptr, false); 30 | return; 31 | } 32 | 33 | SinglePlayerLevelSelectionFlowCoordinator_BackButtonWasPressed(self, topViewController); 34 | } 35 | 36 | void SinglePlayerLevelSelectionFlowCoordinatorHook(Logger& logger) { 37 | INSTALL_HOOK(logger, SinglePlayerLevelSelectionFlowCoordinator_LevelSelectionFlowCoordinatorTopViewControllerWillChange); 38 | INSTALL_HOOK(logger, SinglePlayerLevelSelectionFlowCoordinator_BackButtonWasPressed); 39 | } 40 | 41 | ReplayInstallHooks(SinglePlayerLevelSelectionFlowCoordinatorHook); -------------------------------------------------------------------------------- /src/Hooks/SoloFreePlayFlowController.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/SoloFreePlayFlowCoordinator.hpp" 4 | #include "GlobalNamespace/SinglePlayerLevelSelectionFlowCoordinator.hpp" 5 | #include "UI/UIManager.hpp" 6 | 7 | using namespace GlobalNamespace; 8 | using namespace Replay::UI; 9 | 10 | MAKE_HOOK_MATCH(SoloFreePlayFlowCoordinator_SinglePlayerLevelSelectionFlowCoordinatorDidActivate, &SoloFreePlayFlowCoordinator::SinglePlayerLevelSelectionFlowCoordinatorDidActivate, void, SoloFreePlayFlowCoordinator* self, bool firstActivation, bool addedToHierarchy) { 11 | SoloFreePlayFlowCoordinator_SinglePlayerLevelSelectionFlowCoordinatorDidActivate(self, firstActivation, addedToHierarchy); 12 | 13 | UIManager::singlePlayerFlowCoordinator = reinterpret_cast(self); 14 | } 15 | 16 | void SoloFreePlayFlowCoordinatorHook(Logger& logger) { 17 | INSTALL_HOOK(logger, SoloFreePlayFlowCoordinator_SinglePlayerLevelSelectionFlowCoordinatorDidActivate); 18 | } 19 | 20 | ReplayInstallHooks(SoloFreePlayFlowCoordinatorHook); -------------------------------------------------------------------------------- /src/Hooks/StandardLevelDetailView.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/StandardLevelDetailView.hpp" 4 | #include "GlobalNamespace/BeatmapData.hpp" 5 | 6 | #include "Utils/SongUtils.hpp" 7 | #include "Utils/ReplayUtils.hpp" 8 | #include "Utils/FileUtils.hpp" 9 | #include "UI/UIManager.hpp" 10 | 11 | using namespace GlobalNamespace; 12 | using namespace Replay; 13 | 14 | MAKE_HOOK_MATCH(StandardLevelDetailView_RefreshContent, &StandardLevelDetailView::RefreshContent, void, StandardLevelDetailView* self) { 15 | StandardLevelDetailView_RefreshContent(self); 16 | 17 | SongUtils::SetMapID(self); 18 | 19 | SongUtils::beatmapLevel = self->selectedDifficultyBeatmap->get_level(); 20 | SongUtils::beatmapDifficulty = self->selectedDifficultyBeatmap->get_difficulty(); 21 | SongUtils::beatmapCharacteristic = self->selectedDifficultyBeatmap->get_parentDifficultyBeatmapSet()->get_beatmapCharacteristic(); 22 | 23 | bool replayFileExists = fileexists(ReplayUtils::GetReplayFilePath(SongUtils::GetMapID())); 24 | 25 | Replay::UI::UIManager::CreateReplayCanvas(self, replayFileExists); 26 | } 27 | 28 | void StandardLevelDetailViewHook(Logger& logger) { 29 | INSTALL_HOOK(logger, StandardLevelDetailView_RefreshContent); 30 | } 31 | 32 | ReplayInstallHooks(StandardLevelDetailViewHook); -------------------------------------------------------------------------------- /src/Hooks/StandardLevelGameplayManager.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/StandardLevelGameplayManager.hpp" 4 | #include "Utils/SongUtils.hpp" 5 | #include "ReplayManager.hpp" 6 | 7 | using namespace GlobalNamespace; 8 | using namespace Replay; 9 | 10 | MAKE_HOOK_MATCH(StandardLevelGameplayManager_HandleGameEnergyDidReach0, &StandardLevelGameplayManager::HandleGameEnergyDidReach0, void, StandardLevelGameplayManager* self) { 11 | if(ReplayManager::replayState == ReplayState::RECORDING) { 12 | SongUtils::didFail = true; 13 | SongUtils::failTime = SongUtils::GetSongTime(); 14 | } 15 | 16 | StandardLevelGameplayManager_HandleGameEnergyDidReach0(self); 17 | } 18 | 19 | void StandardLevelGameplayManagerHook(Logger& logger) { 20 | INSTALL_HOOK(logger, StandardLevelGameplayManager_HandleGameEnergyDidReach0); 21 | } 22 | 23 | ReplayInstallHooks(StandardLevelGameplayManagerHook); -------------------------------------------------------------------------------- /src/Hooks/StandardLevelScenesTransitionSetupDataSO.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | #include "GlobalNamespace/StandardLevelScenesTransitionSetupDataSO.hpp" 4 | #include "Utils/SongUtils.hpp" 5 | #include "Utils/ModifiersUtils.hpp" 6 | #include "Utils/FileUtils.hpp" 7 | #include "UI/UIManager.hpp" 8 | #include "ReplayManager.hpp" 9 | 10 | #include "rapidjson/document.h" 11 | #include "rapidjson/stringbuffer.h" 12 | #include 13 | #include 14 | 15 | using namespace GlobalNamespace; 16 | using namespace Replay; 17 | 18 | MAKE_HOOK_MATCH(StandardLevelScenesTransitionSetupDataSO_Init, &StandardLevelScenesTransitionSetupDataSO::Init, void, StandardLevelScenesTransitionSetupDataSO* self, ::StringW gameMode, GlobalNamespace::IDifficultyBeatmap* difficultyBeatmap, GlobalNamespace::IPreviewBeatmapLevel* previewBeatmapLevel, GlobalNamespace::OverrideEnvironmentSettings* overrideEnvironmentSettings, GlobalNamespace::ColorScheme* overrideColorScheme, GlobalNamespace::GameplayModifiers* gameplayModifiers, GlobalNamespace::PlayerSpecificSettings* playerSpecificSettings, GlobalNamespace::PracticeSettings* practiceSettings, ::StringW backButtonText, bool useTestNoteCutSoundEffects, bool startPaused) { 19 | if(ReplayManager::replayState == ReplayState::REPLAYING) { 20 | rapidjson::Document metadata = FileUtils::GetMetadataFromReplayFile(Replay::UI::UIManager::replayViewController->path); 21 | 22 | std::vector modifierStrings; 23 | for(const auto& value : metadata["Modifiers"].GetArray()) { 24 | modifierStrings.push_back(value.GetString()); 25 | } 26 | 27 | gameplayModifiers = ModifiersUtils::CreateModifiersFromStrings(modifierStrings); 28 | 29 | PlayerSpecificSettings* settings = PlayerSpecificSettings::New_ctor( 30 | metadata["PlayerSettings"]["LeftHanded"].GetBool(), 31 | metadata["PlayerSettings"]["Height"].GetFloat(), 32 | metadata["PlayerSettings"]["AutoHeight"].GetBool(), 33 | playerSpecificSettings->sfxVolume, 34 | playerSpecificSettings->reduceDebris, 35 | playerSpecificSettings->noTextsAndHuds, 36 | playerSpecificSettings->noFailEffects, 37 | playerSpecificSettings->advancedHud, 38 | playerSpecificSettings->autoRestart, 39 | playerSpecificSettings->saberTrailIntensity, 40 | playerSpecificSettings->noteJumpDurationTypeSettings, 41 | playerSpecificSettings->noteJumpFixedDuration, 42 | playerSpecificSettings->noteJumpStartBeatOffset, 43 | playerSpecificSettings->hideNoteSpawnEffect, 44 | playerSpecificSettings->adaptiveSfx, 45 | playerSpecificSettings->environmentEffectsFilterDefaultPreset, 46 | playerSpecificSettings->environmentEffectsFilterExpertPlusPreset 47 | ); 48 | 49 | playerSpecificSettings = settings; 50 | } 51 | 52 | StandardLevelScenesTransitionSetupDataSO_Init(self, gameMode, difficultyBeatmap, previewBeatmapLevel, overrideEnvironmentSettings, overrideColorScheme, gameplayModifiers, playerSpecificSettings, practiceSettings, backButtonText, useTestNoteCutSoundEffects, startPaused); 53 | 54 | SongUtils::playerSpecificSettings = playerSpecificSettings; 55 | } 56 | 57 | void StandardLevelScenesTransitionSetupDataSOHook(Logger& logger) { 58 | INSTALL_HOOK(logger, StandardLevelScenesTransitionSetupDataSO_Init); 59 | } 60 | 61 | ReplayInstallHooks(StandardLevelScenesTransitionSetupDataSOHook); -------------------------------------------------------------------------------- /src/Recording/NoteEventRecorder.cpp: -------------------------------------------------------------------------------- 1 | #include "Recording/NoteEventRecorder.hpp" 2 | 3 | #include 4 | 5 | float Replay::NoteEventRecorder::GetEventSaveTime(float songTime) { 6 | //This seems like a really bad solution for note cut race conditions, fix later? 7 | if(songTime == frameTime) { 8 | eventsInFrame++; 9 | 10 | float newTime = nextafterf(songTime, songTime + 1); 11 | 12 | // For if there are 3 or more events in a single frame (extremely unlikely except paulllssss) 13 | for(int i = 0; i < eventsInFrame - 2; i++) { 14 | newTime = nextafterf(newTime, songTime + 1); 15 | } 16 | 17 | return newTime; 18 | } 19 | 20 | frameTime = songTime; 21 | eventsInFrame = 1; 22 | 23 | return songTime; 24 | } 25 | 26 | void Replay::NoteEventRecorder::AddCutEvent(CutScoreBuffer* cutScoreBuffer, float time) { 27 | cutEvents.emplace_back(Replay::ReplayUtils::GetNoteHash(cutScoreBuffer->noteCutInfo.noteData), GetEventSaveTime(time), cutScoreBuffer); 28 | } 29 | 30 | void Replay::NoteEventRecorder::AddCutEvent(NoteCutInfo noteCutInfo, float time) { 31 | cutEvents.emplace_back(Replay::ReplayUtils::GetNoteHash(noteCutInfo.noteData), GetEventSaveTime(time), noteCutInfo); 32 | } 33 | 34 | void Replay::NoteEventRecorder::AddMissEvent(NoteController* noteController) { 35 | missEvents.emplace_back(Replay::ReplayUtils::GetNoteHash(noteController->get_noteData()), GetEventSaveTime(Replay::SongUtils::GetSongTime())); 36 | } 37 | 38 | void Replay::NoteEventRecorder::WriteEvents(std::ofstream& output) { 39 | Replay::FileUtils::WriteEvents(cutEvents, Replay::NoteEventTypes::cutEventID, output); 40 | 41 | Replay::FileUtils::WriteEvents(missEvents, Replay::NoteEventTypes::missEventID, output); 42 | } 43 | 44 | float Replay::NoteEventRecorder::GetAverageCutScore() { 45 | float total = 0; 46 | int goodCuts = 0; 47 | for(NoteEventTypes::NoteCutEvent event : cutEvents) { 48 | if(!event.noteCutInfo.AllIsOkay()) continue; 49 | 50 | // May cause issues with chains 51 | int beforeRating = std::round(event.swingRating.beforeCutRating); 52 | int afterRating = std::round(event.swingRating.afterCutRating); 53 | int accuracy = std::round((1.0f - std::clamp(event.noteCutInfo.cutDistanceToCenter / 0.3f, 0.0f, 1.0f)) * 15); 54 | 55 | int cutScore = beforeRating + afterRating + accuracy; 56 | 57 | total += cutScore; 58 | goodCuts++; 59 | } 60 | if(goodCuts == 0) return 0; 61 | return total / (float) goodCuts; 62 | } -------------------------------------------------------------------------------- /src/Recording/ObstacleEventRecorder.cpp: -------------------------------------------------------------------------------- 1 | #include "Recording/ObstacleEventRecorder.hpp" 2 | 3 | void Replay::ObstacleEventRecorder::AddEvent(GlobalNamespace::GameEnergyCounter* gameEnergyCounter) { 4 | bool playerObstacleInteracting = (bool) gameEnergyCounter->playerHeadAndObstacleInteraction->intersectingObstacles->count; 5 | if(playerObstacleInteracting == lastInteracting) return; 6 | 7 | float songTime = SongUtils::GetSongTime(); 8 | float energy = gameEnergyCounter->get_energy(); 9 | 10 | events.emplace_back(songTime, energy); 11 | 12 | lastInteracting = playerObstacleInteracting; 13 | } 14 | 15 | void Replay::ObstacleEventRecorder::WriteEvents(std::ofstream& output) { 16 | Replay::FileUtils::WriteEvents(events, Replay::ObstacleEventTypes::eventID, output); 17 | } 18 | -------------------------------------------------------------------------------- /src/Recording/PlayerRecorder.cpp: -------------------------------------------------------------------------------- 1 | #include "Recording/PlayerRecorder.hpp" 2 | 3 | #include "Utils/MathUtils.hpp" 4 | 5 | void Replay::PlayerRecorder::AddEvent(PlayerEventTypes::PlayerTransforms const& playerTransforms) { 6 | float songTime = SongUtils::GetSongTime(); 7 | if(!playerEvents.empty() && songTime == playerEvents[playerEvents.size() - 1].time) return; 8 | 9 | playerEvents.emplace_back(songTime, playerTransforms, SaberUtils::saberManager->leftSaber->saberBladeTopPos, SaberUtils::saberManager->rightSaber->saberBladeTopPos); 10 | } 11 | 12 | void Replay::PlayerRecorder::AddSaberEvent(GlobalNamespace::SaberType saberType) { 13 | int lastAddedEventIndex = playerEvents.size() - 1; 14 | if(saberType == SaberType::SaberA) { 15 | leftSaberEvents.emplace_back(playerEvents[lastAddedEventIndex].time, playerEvents[lastAddedEventIndex].playerTransforms.leftSaber); 16 | } else { 17 | rightSaberEvents.emplace_back(playerEvents[lastAddedEventIndex].time, playerEvents[lastAddedEventIndex].playerTransforms.rightSaber); 18 | } 19 | } 20 | 21 | void Replay::PlayerRecorder::AddSaberMovement(GlobalNamespace::BladeMovementDataElement bladeMovement, GlobalNamespace::SaberType saberType) { 22 | //Not in the mood rn to not hard code this, forgive me 23 | float angleNeeded = 30;//These values can be optimized more 24 | float minDistance = 0.05f; 25 | float maxDistance = 1; 26 | float maxTime = 1.0f / 10.0f; 27 | 28 | float songTime = SongUtils::GetSongTime(); 29 | 30 | if(saberType == SaberType::SaberA) { 31 | if(leftSaberLastSavedMovement.segmentNormal == UnityEngine::Vector3::get_zero()) { 32 | leftSaberLastSavedMovement = bladeMovement; 33 | return; 34 | } 35 | 36 | float lastEventTime = 0; 37 | if(!leftSaberEvents.empty()) lastEventTime = leftSaberEvents[leftSaberEvents.size() - 1].time; 38 | 39 | float timeSinceLastEvent = songTime - lastEventTime; 40 | float distance = UnityEngine::Vector3::Distance(bladeMovement.topPos, leftSaberLastSavedMovement.topPos); 41 | float angle = UnityEngine::Vector3::Angle(bladeMovement.segmentNormal, leftSaberLastSavedMovement.segmentNormal); 42 | 43 | bool addEvent = (angle > angleNeeded && distance > minDistance) || distance > maxDistance || timeSinceLastEvent > maxTime; 44 | 45 | if(addEvent) { 46 | leftSaberLastSavedMovement = bladeMovement; 47 | AddSaberEvent(saberType); 48 | return; 49 | } 50 | } else { 51 | if(rightSaberLastSavedMovement.segmentNormal == UnityEngine::Vector3::get_zero()) { 52 | rightSaberLastSavedMovement = bladeMovement; 53 | return; 54 | } 55 | 56 | float lastEventTime = 0; 57 | if(!rightSaberEvents.empty()) lastEventTime = rightSaberEvents[rightSaberEvents.size() - 1].time; 58 | 59 | float timeSinceLastEvent = songTime - lastEventTime; 60 | float distance = UnityEngine::Vector3::Distance(bladeMovement.topPos, rightSaberLastSavedMovement.topPos); 61 | float angle = UnityEngine::Vector3::Angle(bladeMovement.segmentNormal, rightSaberLastSavedMovement.segmentNormal); 62 | 63 | bool addEvent = (angle > angleNeeded && distance > minDistance) || distance > maxDistance || timeSinceLastEvent > maxTime; 64 | 65 | if(addEvent) { 66 | rightSaberLastSavedMovement = bladeMovement; 67 | AddSaberEvent(saberType); 68 | return; 69 | } 70 | } 71 | } 72 | 73 | void Replay::PlayerRecorder::GetImportantEvents() { 74 | headEvents.emplace_back(playerEvents[0].time, playerEvents[0].playerTransforms.head); 75 | leftSaberEvents.emplace_back(playerEvents[0].time, playerEvents[0].playerTransforms.leftSaber); 76 | rightSaberEvents.emplace_back(playerEvents[0].time, playerEvents[0].playerTransforms.rightSaber); 77 | 78 | float timePerEvent = 1.0f / 10.0f;// Can probably be lower, check after adding smooth camera (also maybe dynamic head events?) 79 | float lastSavedEventTime = 0; 80 | 81 | for(auto& event : playerEvents) { 82 | if(event.time - lastSavedEventTime > timePerEvent) { 83 | headEvents.emplace_back(event.time, event.playerTransforms.head); 84 | lastSavedEventTime = event.time; 85 | } 86 | } 87 | 88 | int lastIndex = playerEvents.size() - 1; 89 | headEvents.emplace_back(playerEvents[lastIndex].time, playerEvents[lastIndex].playerTransforms.head); 90 | leftSaberEvents.emplace_back(playerEvents[lastIndex].time, playerEvents[lastIndex].playerTransforms.leftSaber); 91 | rightSaberEvents.emplace_back(playerEvents[lastIndex].time, playerEvents[lastIndex].playerTransforms.rightSaber); 92 | } 93 | 94 | void Replay::PlayerRecorder::WriteEvents(std::ofstream& output) { 95 | GetImportantEvents(); 96 | 97 | Replay::FileUtils::WriteEvents(headEvents, Replay::PlayerEventTypes::headEventID, output); 98 | 99 | Replay::FileUtils::WriteEvents(leftSaberEvents, Replay::PlayerEventTypes::leftSaberEventID, output); 100 | 101 | Replay::FileUtils::WriteEvents(rightSaberEvents, Replay::PlayerEventTypes::rightSaberEventID, output); 102 | log("Average saber events per second per hand: %f", (float)((rightSaberEvents.size() + leftSaberEvents.size()) / 2) / playerEvents[playerEvents.size() - 1].time); 103 | } -------------------------------------------------------------------------------- /src/Recording/ReplayRecorder.cpp: -------------------------------------------------------------------------------- 1 | #include "Recording/ReplayRecorder.hpp" 2 | 3 | #include "Utils/ModifiersUtils.hpp" 4 | 5 | #include 6 | #include 7 | 8 | using namespace rapidjson; 9 | 10 | void Replay::ReplayRecorder::Init() { 11 | log("Setting up Replay Recorder"); 12 | playerRecorder = Replay::PlayerRecorder(); 13 | noteEventRecorder = Replay::NoteEventRecorder(); 14 | obstacleEventRecorder = Replay::ObstacleEventRecorder(); 15 | } 16 | 17 | void Replay::ReplayRecorder::CreateClearedSpecificMetadata(GlobalNamespace::LevelCompletionResults* results, rapidjson::Document::AllocatorType& allocator) { 18 | Value clearedInfo(kObjectType); 19 | 20 | clearedInfo.AddMember("RawScore", results->multipliedScore, allocator); 21 | clearedInfo.AddMember("ModifiedScore", results->modifiedScore, allocator); 22 | 23 | metadata.AddMember("ClearedInfo", clearedInfo, allocator); 24 | } 25 | 26 | void Replay::ReplayRecorder::CreateFailedSpecificMetadata(GlobalNamespace::LevelCompletionResults* results, rapidjson::Document::AllocatorType& allocator) { 27 | Value failedInfo(kObjectType); 28 | 29 | failedInfo.AddMember("FailedTime", SongUtils::failTime, allocator); 30 | 31 | metadata.AddMember("FailedInfo", failedInfo, allocator); 32 | } 33 | 34 | void Replay::ReplayRecorder::CreateMetadata(GlobalNamespace::LevelCompletionResults* results) { 35 | metadata.SetObject(); 36 | 37 | rapidjson::Document::AllocatorType& allocator = metadata.GetAllocator(); 38 | 39 | GlobalNamespace::PlayerSpecificSettings* settings = SongUtils::playerSpecificSettings; 40 | 41 | Value playerSettings(kObjectType); 42 | playerSettings.AddMember("LeftHanded", settings->leftHanded, allocator); 43 | playerSettings.AddMember("AutoHeight", settings->automaticPlayerHeight, allocator); 44 | playerSettings.AddMember("Height", settings->playerHeight, allocator); 45 | metadata.AddMember("PlayerSettings", playerSettings, allocator); 46 | 47 | std::vector modifierStrings = ModifiersUtils::ModifiersToStrings(results->gameplayModifiers); 48 | if(!modifierStrings.empty()) { 49 | Value modifiers(kArrayType); 50 | for(std::string modifierName : modifierStrings) { 51 | if(modifierName == "NoFail" && !SongUtils::didFail) continue; 52 | modifiers.PushBack(rapidjson::Value{}.SetString(modifierName.c_str(), modifierName.length(), allocator), allocator); 53 | } 54 | metadata.AddMember("Modifiers", modifiers, allocator); 55 | } 56 | 57 | int64_t timeSet = static_cast(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())); 58 | 59 | Value info(kObjectType); 60 | info.AddMember("TimeSet", timeSet, allocator); 61 | info.AddMember("AverageCutScore", noteEventRecorder.GetAverageCutScore(), allocator); 62 | info.AddMember("GoodCuts", results->goodCutsCount, allocator); 63 | info.AddMember("MissedNotes", results->badCutsCount + results->missedCount, allocator); 64 | info.AddMember("MaxCombo", results->maxCombo, allocator); 65 | metadata.AddMember("Info", info, allocator); 66 | 67 | if(!SongUtils::didFail) { 68 | CreateClearedSpecificMetadata(results, allocator); 69 | } else { 70 | CreateFailedSpecificMetadata(results, allocator); 71 | } 72 | } 73 | 74 | void Replay::ReplayRecorder::StopRecording(GlobalNamespace::LevelCompletionResults* results) { 75 | CreateMetadata(results); 76 | WriteReplayFile(ReplayUtils::GetTempReplayFilePath()); 77 | 78 | std::string filepath = ReplayUtils::GetReplayFilePath(SongUtils::GetMapID()); 79 | if(ShouldMoveFile(results, filepath)) { 80 | log("Moving replay file to permanent location"); 81 | std::filesystem::rename(ReplayUtils::GetTempReplayFilePath(), ReplayUtils::GetReplayFilePath(SongUtils::GetMapID())); 82 | } else { 83 | log("Not permanently saving replay file, current file has higher priority"); 84 | } 85 | } 86 | 87 | bool Replay::ReplayRecorder::ShouldMoveFile(GlobalNamespace::LevelCompletionResults* results, std::string_view filepath) { 88 | if(!fileexists(filepath)) return true; 89 | 90 | Document metadata = FileUtils::GetMetadataFromReplayFile(filepath); 91 | if(metadata.HasMember("ClearedInfo")) { 92 | if(SongUtils::didFail) return false; 93 | 94 | if(results->modifiedScore >= metadata["ClearedInfo"]["ModifiedScore"].GetInt()) return true; 95 | 96 | return false; 97 | } else if(metadata.HasMember("FailedInfo")) { 98 | if(!SongUtils::didFail) return true; 99 | 100 | if(SongUtils::failTime > metadata["FailedInfo"]["FailedTime"].GetFloat()) return true; 101 | 102 | return false; 103 | } 104 | 105 | return true; 106 | } 107 | 108 | void Replay::ReplayRecorder::WriteReplayFile(std::string path) { 109 | log("Writing Replay file at %s", path.c_str()); 110 | std::filesystem::create_directories(Replay::ReplayUtils::GetReplaysDirectory()); 111 | 112 | std::ofstream output = std::ofstream(path, std::ios::binary); 113 | 114 | int magicBytes = replayMagicBytes; 115 | output.write(reinterpret_cast(&magicBytes), sizeof(int)); 116 | 117 | byte version = fileVersion; 118 | output.write(reinterpret_cast(&version), sizeof(byte)); 119 | 120 | StringBuffer buffer; 121 | Writer writer(buffer); 122 | writer.SetMaxDecimalPlaces(2); 123 | metadata.Accept(writer); 124 | const char* metadataString = buffer.GetString(); 125 | 126 | unsigned int metadataLength = (int) strlen(metadataString); 127 | output.write(reinterpret_cast(&metadataLength), sizeof(unsigned int)); 128 | 129 | output.write(metadataString, strlen(metadataString)); 130 | 131 | playerRecorder.WriteEvents(output); 132 | noteEventRecorder.WriteEvents(output); 133 | obstacleEventRecorder.WriteEvents(output); 134 | 135 | output.flush(); 136 | } -------------------------------------------------------------------------------- /src/ReplayManager.cpp: -------------------------------------------------------------------------------- 1 | #include "ReplayManager.hpp" 2 | 3 | Replay::ReplayRecorder Replay::ReplayManager::recorder; 4 | 5 | Replay::Replayer Replay::ReplayManager::replayer; -------------------------------------------------------------------------------- /src/Replaying/NoteEventReplayer.cpp: -------------------------------------------------------------------------------- 1 | #include "Replaying/NoteEventReplayer.hpp" 2 | 3 | void Replay::NoteEventReplayer::Init() { 4 | GlobalNamespace::SharedCoroutineStarter::get_instance()->StartCoroutine(custom_types::Helpers::CoroutineHelper::New(Update())); 5 | } 6 | 7 | void Replay::NoteEventReplayer::AddActiveEvents(GlobalNamespace::NoteController* noteController) { 8 | auto noteHash = Replay::ReplayUtils::GetNoteHash(noteController->noteData); 9 | 10 | for (auto eventIt = cutEvents.begin(); eventIt != cutEvents.end(); eventIt++) { 11 | auto const ¬eCutEvent = *eventIt; 12 | 13 | if(noteHash == noteCutEvent.noteHash) { 14 | activeCutEvents.emplace_back(noteController, noteCutEvent); 15 | 16 | cutEvents.erase(eventIt); 17 | 18 | return; 19 | } 20 | } 21 | 22 | for (auto eventIt = missEvents.begin(); eventIt != missEvents.end(); eventIt++) { 23 | auto const ¬eMissEvent = *eventIt; 24 | 25 | if(noteHash == noteMissEvent.noteHash) { 26 | activeMissEvents.emplace_back(noteController, noteMissEvent); 27 | 28 | missEvents.erase(eventIt); 29 | 30 | return; 31 | } 32 | } 33 | log("Could not find event for note!"); 34 | } 35 | 36 | void Replay::NoteEventReplayer::ReadCutEvents(std::ifstream& input, int eventsLength) { 37 | for(int i = 0; i < eventsLength; i++) { 38 | cutEvents.emplace_back(input); 39 | } 40 | } 41 | 42 | void Replay::NoteEventReplayer::ReadMissEvents(std::ifstream& input, int eventsLength) { 43 | for(int i = 0; i < eventsLength; i++) { 44 | missEvents.emplace_back(input); 45 | } 46 | } 47 | 48 | static const Il2CppType * NoteCutInfoT(ByRef info) { 49 | return il2cpp_utils::il2cpp_type_check::il2cpp_no_arg_type>::get(); 50 | } 51 | 52 | //GlobalNamespace::NoteController:: 53 | void SendNoteWasCutEvent(GlobalNamespace::NoteController* self, ByRef noteCutInfo) { 54 | static auto ___internal__logger = ::Logger::get().WithContext("GlobalNamespace::NoteController::SendNoteWasCutEvent"); 55 | static auto* ___internal__method = THROW_UNLESS((::il2cpp_utils::FindMethod(self, "SendNoteWasCutEvent", std::vector{}, ::std::vector{::NoteCutInfoT(noteCutInfo)}))); 56 | ::il2cpp_utils::RunMethodRethrow(self, ___internal__method, noteCutInfo); 57 | } 58 | 59 | #pragma clang diagnostic push 60 | #pragma clang diagnostic ignored "-Winvalid-noreturn" 61 | #pragma ide diagnostic ignored "EndlessLoop" 62 | custom_types::Helpers::Coroutine Replay::NoteEventReplayer::Update() { 63 | while(SongUtils::inSong) { 64 | float songTime = Replay::SongUtils::GetSongTime(); 65 | 66 | // Feel free to make this not terrible, just trying to run the events in order of time 67 | std::vector eventsToRun; 68 | 69 | for(int i = 0; i < activeCutEvents.size(); i++) { 70 | float eventTime = activeCutEvents[i].event.time; 71 | if(songTime > eventTime) { 72 | eventsToRun.emplace_back(eventTime, true, i); 73 | } 74 | } 75 | 76 | for(int i = 0; i < activeMissEvents.size(); i++) { 77 | float eventTime = activeMissEvents[i].event.time; 78 | if(songTime > eventTime) { 79 | eventsToRun.emplace_back(eventTime, false, i); 80 | } 81 | } 82 | 83 | if(eventsToRun.empty()) co_yield nullptr; 84 | 85 | // I am truly sorry to whoever reads this code next, I am just doing the first thing that comes to mind 86 | std::sort(eventsToRun.begin(), eventsToRun.end()); 87 | 88 | for(auto& eventToRun : eventsToRun) { 89 | if(eventToRun.isCutEvent) { 90 | auto& activeCutEvent = activeCutEvents[eventToRun.eventIndex]; 91 | 92 | GlobalNamespace::NoteCutInfo noteCutInfo = ReplayUtils::CreateNoteCutInfoFromSimple(activeCutEvent.event.noteCutInfo, activeCutEvent.note->get_noteData()); 93 | 94 | if(noteCutInfo.get_allIsOK()) swingRatings.emplace_back(activeCutEvent.event.noteHash, activeCutEvent.event.swingRating); 95 | 96 | SendNoteWasCutEvent(activeCutEvent.note, byref(noteCutInfo)); 97 | } else { 98 | activeMissEvents[eventToRun.eventIndex].note->SendNoteWasMissedEvent(); 99 | } 100 | } 101 | 102 | // Two sorts every frame... I am gonna scrape out my eyes 103 | std::sort(eventsToRun.begin(), eventsToRun.end(), std::greater()); 104 | 105 | for(auto& eventToRun : eventsToRun) { 106 | if(eventToRun.isCutEvent) { 107 | activeCutEvents.erase(activeCutEvents.begin() + eventToRun.eventIndex); 108 | } else { 109 | activeMissEvents.erase(activeMissEvents.begin() + eventToRun.eventIndex); 110 | } 111 | } 112 | 113 | co_yield nullptr; 114 | } 115 | co_return; 116 | } 117 | #pragma clang diagnostic pop 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/Replaying/ObstacleEventReplayer.cpp: -------------------------------------------------------------------------------- 1 | #include "Replaying/ObstacleEventReplayer.hpp" 2 | 3 | void Replay::ObstacleEventReplayer::ReadEvents(std::ifstream& input, int eventsLength) { 4 | for(int i = 0; i < eventsLength; i++) { 5 | events.emplace_back(input); 6 | } 7 | } 8 | 9 | bool Replay::ObstacleEventReplayer::ShouldSetEnergy() { 10 | if(nextEventIndex >= events.size()) return false; 11 | 12 | auto& event = events[nextEventIndex]; 13 | 14 | if(SongUtils::GetSongTime() > event.time) { 15 | nextEventIndex++; 16 | 17 | return true; 18 | } 19 | 20 | return false; 21 | } -------------------------------------------------------------------------------- /src/Replaying/PlayerReplayer.cpp: -------------------------------------------------------------------------------- 1 | #include "Replaying/PlayerReplayer.hpp" 2 | 3 | #include "Utils/MathUtils.hpp" 4 | 5 | void Replay::PlayerReplayer::ReadHeadEvents(std::ifstream& input, int eventsLength) { 6 | for(int i = 0; i < eventsLength; i++) { 7 | headEvents.emplace_back(input); 8 | } 9 | } 10 | 11 | void Replay::PlayerReplayer::ReadLeftSaberEvents(std::ifstream& input, int eventsLength) { 12 | for(int i = 0; i < eventsLength; i++) { 13 | leftSaberEvents.emplace_back(input); 14 | } 15 | } 16 | 17 | void Replay::PlayerReplayer::ReadRightSaberEvents(std::ifstream& input, int eventsLength) { 18 | for(int i = 0; i < eventsLength; i++) { 19 | rightSaberEvents.emplace_back(input); 20 | } 21 | } 22 | 23 | void Replay::PlayerReplayer::SetPlayerTransforms(GlobalNamespace::PlayerTransforms* playerTransforms) { 24 | // Grossly hard coded, reminder to fix in future 25 | 26 | UnityEngine::Transform* origin = playerTransforms->useOriginParentTransformForPseudoLocalCalculations ? playerTransforms->originParentTransform : playerTransforms->headTransform->GetParent(); 27 | 28 | headIndex = Replay::ReplayUtils::GetCurrentIndex(headEvents, headIndex); 29 | EulerTransformEvent event = headEvents[headIndex]; 30 | 31 | UnityEngine::Transform* head = playerTransforms->headTransform; 32 | head->set_rotation(origin->get_rotation() * UnityEngine::Quaternion::Euler(event.transform.rotation)); 33 | head->set_position(origin->TransformPoint(event.transform.position)); 34 | 35 | leftSaberIndex = Replay::ReplayUtils::GetCurrentIndex(leftSaberEvents, leftSaberIndex); 36 | event = leftSaberEvents[leftSaberIndex]; 37 | EulerTransformEvent nextEvent = leftSaberEvents[std::min(leftSaberIndex + 1, (int)leftSaberEvents.size() - 1)]; 38 | 39 | float lerpAmount = std::max(0.0f, std::min(1.0f, Replay::ReplayUtils::LerpAmountBetweenEvents(event, nextEvent))); 40 | 41 | UnityEngine::Transform* leftSaber = playerTransforms->leftHandTransform; 42 | leftSaber->set_rotation(origin->get_rotation() * Replay::MathUtils::LerpEulerAngles(event.transform.rotation, nextEvent.transform.rotation, lerpAmount)); 43 | leftSaber->set_position(origin->TransformPoint(Replay::MathUtils::Lerp(event.transform.position, nextEvent.transform.position, lerpAmount))); 44 | 45 | rightSaberIndex = Replay::ReplayUtils::GetCurrentIndex(rightSaberEvents, rightSaberIndex); 46 | event = rightSaberEvents[rightSaberIndex]; 47 | nextEvent = rightSaberEvents[std::min(rightSaberIndex + 1, (int)rightSaberEvents.size() - 1)]; 48 | 49 | lerpAmount = std::max(0.0f, std::min(1.0f, Replay::ReplayUtils::LerpAmountBetweenEvents(event, nextEvent))); 50 | 51 | UnityEngine::Transform* rightSaber = playerTransforms->rightHandTransform; 52 | rightSaber->set_rotation(origin->get_rotation() * Replay::MathUtils::LerpEulerAngles(event.transform.rotation, nextEvent.transform.rotation, lerpAmount)); 53 | rightSaber->set_position(origin->TransformPoint(Replay::MathUtils::Lerp(event.transform.position, nextEvent.transform.position, lerpAmount))); 54 | } -------------------------------------------------------------------------------- /src/Replaying/Replayer.cpp: -------------------------------------------------------------------------------- 1 | #include "Replaying/Replayer.hpp" 2 | 3 | #include "rapidjson/document.h" 4 | #include 5 | 6 | using namespace rapidjson; 7 | 8 | custom_types::Helpers::Coroutine Replay::Replayer::WaitForSongStartToInit() { 9 | while(!SongUtils::inSong) { 10 | co_yield nullptr; 11 | } 12 | 13 | noteEventReplayer.Init(); 14 | co_return; 15 | } 16 | 17 | void Replay::Replayer::Init(std::string_view path) { 18 | log("Setting up Replayer"); 19 | playerReplayer = Replay::PlayerReplayer(); 20 | noteEventReplayer = Replay::NoteEventReplayer(); 21 | obstacleEventReplayer = Replay::ObstacleEventReplayer(); 22 | 23 | ReadReplayFile(path); 24 | 25 | GlobalNamespace::SharedCoroutineStarter::get_instance()->StartCoroutine(custom_types::Helpers::CoroutineHelper::New(Replay::Replayer::WaitForSongStartToInit())); 26 | } 27 | 28 | void Replay::Replayer::ReadReplayFile(std::string_view path) { 29 | log("Reading Replay file at %s", path.data()); 30 | std::ifstream input = std::ifstream(path, std::ios::binary); 31 | 32 | if(input.is_open()) { 33 | int magicBytes; 34 | input.read(reinterpret_cast(&magicBytes), sizeof(int)); 35 | if(magicBytes != replayMagicBytes) { 36 | log("INCORRECT MAGIC BYTES"); 37 | return; 38 | } 39 | 40 | byte version; 41 | input.read(reinterpret_cast(&version), sizeof(byte)); 42 | log("File version is %i", version); 43 | 44 | int metadataLength; 45 | input.read(reinterpret_cast(&metadataLength), sizeof(int)); 46 | 47 | char* metadataString = new char[metadataLength]; 48 | input.read(metadataString, (size_t) metadataLength); 49 | 50 | Document metadata; 51 | metadata.Parse(metadataString); 52 | 53 | free(metadataString); 54 | 55 | byte eventID; 56 | while(input.read(reinterpret_cast(&eventID), sizeof(byte))) { 57 | unsigned int eventsLength; 58 | input.read(reinterpret_cast(&eventsLength), sizeof(unsigned int)); 59 | 60 | log("Event %i has %i events", eventID, eventsLength); // Add log for size in bytes of events 61 | 62 | int startByte = input.tellg(); 63 | 64 | switch(eventID) { 65 | case PlayerEventTypes::headEventID: 66 | playerReplayer.ReadHeadEvents(input, eventsLength); 67 | break; 68 | case PlayerEventTypes::leftSaberEventID: 69 | playerReplayer.ReadLeftSaberEvents(input, eventsLength); 70 | break; 71 | case PlayerEventTypes::rightSaberEventID: 72 | playerReplayer.ReadRightSaberEvents(input, eventsLength); 73 | break; 74 | case NoteEventTypes::cutEventID: 75 | noteEventReplayer.ReadCutEvents(input, eventsLength); 76 | break; 77 | case NoteEventTypes::missEventID: 78 | noteEventReplayer.ReadMissEvents(input, eventsLength); 79 | break; 80 | case ObstacleEventTypes::eventID: 81 | obstacleEventReplayer.ReadEvents(input, eventsLength); 82 | break; 83 | } 84 | 85 | int endByte = input.tellg(); 86 | int bytes = endByte - startByte; 87 | float kiloBytes = (float) bytes / 1024.0f; 88 | float megaBytes = (float) kiloBytes / 1024.0f; 89 | log("Length of %i bytes (%f KB or %f MB)", bytes, kiloBytes, megaBytes); 90 | } 91 | } else { 92 | log("COULD NOT FIND REPLAY FILE"); 93 | } 94 | } -------------------------------------------------------------------------------- /src/UI/ReplayViewController.cpp: -------------------------------------------------------------------------------- 1 | #include "UI/ReplayViewController.hpp" 2 | 3 | #include "GlobalNamespace/ScoreModel.hpp" 4 | #include "GlobalNamespace/SimpleDialogPromptViewController.hpp" 5 | #include "UnityEngine/RectOffset.hpp" 6 | #include "UnityEngine/Object.hpp" 7 | #include "HMUI/TitleViewController.hpp" 8 | #include "HMUI/ViewController.hpp" 9 | #include "HMUI/ViewController_AnimationDirection.hpp" 10 | #include "VRUIControls/VRGraphicRaycaster.hpp" 11 | #include "System/Action_1.hpp" 12 | #include "UnityEngine/Events/UnityAction.hpp" 13 | 14 | #include "questui/shared/BeatSaberUI.hpp" 15 | 16 | #include "UI/UIManager.hpp" 17 | #include "ReplayManager.hpp" 18 | #include "Utils/ReplayUtils.hpp" 19 | #include "Utils/ModifiersUtils.hpp" 20 | #include "Utils/TypeUtils.hpp" 21 | #include "Utils/UnityUtils.hpp" 22 | #include "Utils/FindComponentsUtils.hpp" 23 | #include "Utils/TimeUtils.hpp" 24 | #include "Utils/UIUtils.hpp" 25 | 26 | using namespace Replay; 27 | using namespace Replay::UI; 28 | using namespace QuestUI; 29 | using namespace QuestUI::BeatSaberUI; 30 | using namespace VRUIControls; 31 | using namespace UnityEngine::Events; 32 | 33 | DEFINE_TYPE(Replay::UI, ReplayViewController); 34 | 35 | void Replay::UI::ReplayViewController::Init(std::string_view filePath, bool overwriteFile) { 36 | path = filePath; 37 | overwrite = overwriteFile; 38 | } 39 | 40 | void Replay::UI::ReplayViewController::DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling) { 41 | if(firstActivation) { 42 | get_gameObject()->GetComponent()->physicsRaycaster = BeatSaberUI::GetPhysicsRaycasterWithCache(); 43 | // WHy this no worky? it worky on delete view controller, why not worky here 44 | 45 | CreateLevelBar(get_transform()); 46 | 47 | CreateText(get_rectTransform()); 48 | 49 | CreateButtons(get_rectTransform()); 50 | } 51 | 52 | if(addedToHierarchy) { 53 | SetupLevelBar(); 54 | 55 | SetText(); 56 | 57 | SetButton(overwrite); 58 | } 59 | } 60 | 61 | void Replay::UI::ReplayViewController::CreateLevelBar(UnityEngine::Transform* parent) { 62 | levelBar = ArrayUtil::First(UnityEngine::Resources::FindObjectsOfTypeAll(), [] (GlobalNamespace::LevelBar* x) { return to_utf8(csstrtostr(x->get_transform()->GetParent()->get_name())) == "PracticeViewController"; })->get_gameObject(); 63 | 64 | levelBar->set_name(newcsstr("ReplayLevelBarSimple")); 65 | levelBar->get_transform()->SetParent(parent, false); 66 | levelBar->GetComponent()->set_anchoredPosition(UnityEngine::Vector2(0, -3.0f)); 67 | } 68 | 69 | #define CreateCenteredText(text, parent, fontSize, lineSpacing) text = QuestUI::BeatSaberUI::CreateText(parent, ""); \ 70 | text->set_fontSize(fontSize); \ 71 | text->set_alignment(TMPro::TextAlignmentOptions::Center); \ 72 | text->set_lineSpacing(lineSpacing); 73 | 74 | void Replay::UI::ReplayViewController::CreateText(UnityEngine::RectTransform* parent) { 75 | UnityEngine::UI::HorizontalLayoutGroup* horizontalLayout = CreateHorizontalLayoutGroup(parent); 76 | horizontalLayout->set_spacing(0); 77 | horizontalLayout->set_childControlWidth(false); 78 | horizontalLayout->set_childForceExpandWidth(false); 79 | horizontalLayout->GetComponent()->set_anchoredPosition(UnityEngine::Vector2(34.0f, 1)); 80 | 81 | float childrenWidth = 43.5f; 82 | float childrenSpacing = 2.5f; 83 | 84 | UnityEngine::UI::VerticalLayoutGroup* layout1 = CreateVerticalLayoutGroup(horizontalLayout->get_rectTransform()); 85 | layout1->set_spacing(childrenSpacing); 86 | layout1->GetComponent()->set_preferredWidth(childrenWidth); 87 | 88 | UnityEngine::UI::VerticalLayoutGroup* layout2 = CreateVerticalLayoutGroup(horizontalLayout->get_rectTransform()); 89 | layout2->set_spacing(childrenSpacing); 90 | layout2->GetComponent()->set_preferredWidth(childrenWidth); 91 | 92 | float fontSize = 4.5f; 93 | float lineSpacing = -35.0f; 94 | 95 | CreateCenteredText(dateText, layout1->get_transform(), fontSize, lineSpacing); 96 | CreateCenteredText(scoreOrFailedText, layout1->get_transform(), fontSize, lineSpacing); 97 | CreateCenteredText(modifiersText, layout1->get_transform(), fontSize, lineSpacing); 98 | 99 | CreateCenteredText(averageCutScoreText, layout2->get_transform(), fontSize, lineSpacing); 100 | CreateCenteredText(missedNotesText, layout2->get_transform(), fontSize, lineSpacing); 101 | CreateCenteredText(maxComboText, layout2->get_transform(), fontSize, lineSpacing); 102 | } 103 | 104 | GlobalNamespace::SimpleDialogPromptViewController* deleteDialogPromptViewController = nullptr; 105 | 106 | GlobalNamespace::SimpleDialogPromptViewController* getDeleteDialogPromptViewController() { 107 | if(!deleteDialogPromptViewController) { 108 | deleteDialogPromptViewController = UnityEngine::Object::Instantiate(FindComponentsUtils::GetSimpleDialogPromptViewController()); 109 | deleteDialogPromptViewController->GetComponent()->physicsRaycaster = BeatSaberUI::GetPhysicsRaycasterWithCache(); 110 | static auto dialogViewControllerName = il2cpp_utils::newcsstr("DeleteDialogPromptViewController"); 111 | deleteDialogPromptViewController->set_name(dialogViewControllerName); 112 | deleteDialogPromptViewController->get_gameObject()->SetActive(false); 113 | } 114 | return deleteDialogPromptViewController; 115 | } 116 | 117 | void Replay::UI::ReplayViewController::CreateButtons(UnityEngine::RectTransform* parent) { 118 | UnityEngine::UI::HorizontalLayoutGroup* buttonLayout = CreateHorizontalLayoutGroup(parent); 119 | buttonLayout->set_spacing(3); 120 | buttonLayout->set_childControlWidth(false); 121 | buttonLayout->set_childForceExpandWidth(false); 122 | buttonLayout->set_childControlHeight(false); 123 | buttonLayout->set_childForceExpandHeight(false); 124 | buttonLayout->set_childAlignment(UnityEngine::TextAnchor::MiddleCenter); 125 | buttonLayout->GetComponent()->set_anchoredPosition(UnityEngine::Vector2(-3, -25)); 126 | 127 | UnityEngine::Vector2 size(40, 10); 128 | 129 | deleteButton = CreateUIButton( 130 | buttonLayout->get_transform(), 131 | "" 132 | ); 133 | deleteButton->get_gameObject()->GetComponent()->set_sizeDelta(size); 134 | 135 | CreateUIButton( 136 | buttonLayout->get_transform(), 137 | "Watch Replay", 138 | "OkButton", 139 | [this]() { 140 | log("Replay button pressed"); 141 | ReplayManager::replayState = ReplayState::REPLAYING; 142 | ReplayManager::replayer = Replayer(); 143 | ReplayManager::replayer.Init(path); 144 | 145 | UIManager::singlePlayerFlowCoordinator->StartLevelOrShow360Prompt(nullptr, false); 146 | } 147 | )->GetComponent()->set_sizeDelta(size); 148 | } 149 | 150 | void Replay::UI::ReplayViewController::SetupLevelBar() { 151 | GlobalNamespace::LevelBar* levelBarComponent = levelBar->GetComponent(); 152 | levelBarComponent->showDifficultyAndCharacteristic = true; 153 | levelBarComponent->Setup(reinterpret_cast(SongUtils::beatmapLevel), SongUtils::beatmapCharacteristic, SongUtils::beatmapDifficulty); 154 | } 155 | 156 | void Replay::UI::ReplayViewController::SetText() { 157 | rapidjson::Document metadata = FileUtils::GetMetadataFromReplayFile(path); 158 | 159 | auto replayTime = static_cast(metadata["Info"]["TimeSet"].GetInt64()); 160 | auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); 161 | 162 | dateText->set_text(newcsstr(UIUtils::GetLayeredText("Date Set", TimeUtils::GetStringForTimeSince(replayTime, now)))); 163 | 164 | if(metadata.HasMember("ClearedInfo")) { 165 | int maxScore = GlobalNamespace::ScoreModel::ComputeMaxMultipliedScoreForBeatmap(SongUtils::beatmapData); 166 | int modifiedScore = metadata["ClearedInfo"]["ModifiedScore"].GetInt(); 167 | float percentage = ((float) modifiedScore / (float) maxScore) * 100; 168 | 169 | scoreOrFailedText->set_text(newcsstr(UIUtils::GetLayeredText("Score", std::to_string(modifiedScore) + " (" + TypeUtils::FloatToString(percentage) + "%)"))); 170 | } else { 171 | float failedSongTime = metadata["FailedInfo"]["FailedTime"].GetFloat(); 172 | float songLength = reinterpret_cast(SongUtils::beatmapLevel)->get_songDuration(); 173 | 174 | scoreOrFailedText->set_text(newcsstr(UIUtils::GetLayeredText("Failed", "" + TimeUtils::SecondsToString(failedSongTime) + " / " + TimeUtils::SecondsToString(songLength)))); 175 | } 176 | 177 | std::string modifiersString = ""; 178 | 179 | for(const auto& value : metadata["Modifiers"].GetArray()) { 180 | modifiersString = modifiersString + ModifiersUtils::GetInitialsFromModifierName(value.GetString()) + " "; 181 | } 182 | 183 | if(modifiersString.empty()) { 184 | modifiersString = "None"; 185 | } else { 186 | modifiersString.pop_back(); // Remove last space 187 | } 188 | 189 | modifiersText->set_text(newcsstr(UIUtils::GetLayeredText("Modifiers", modifiersString))); 190 | 191 | float averageCutScore = metadata["Info"]["AverageCutScore"].GetFloat(); 192 | float cutPercentage = (averageCutScore / 115.0f) * 100; 193 | 194 | averageCutScoreText->set_text(newcsstr(UIUtils::GetLayeredText("Average Cut Score", TypeUtils::FloatToString(averageCutScore) + " (" + TypeUtils::FloatToString(cutPercentage) + "%)"))); 195 | 196 | missedNotesText->set_text(newcsstr(UIUtils::GetLayeredText("Missed Notes", "" + std::to_string(metadata["Info"]["MissedNotes"].GetInt()) + ""))); 197 | 198 | maxComboText->set_text(newcsstr(UIUtils::GetLayeredText("Max Combo", std::to_string(metadata["Info"]["MaxCombo"].GetInt())))); 199 | } 200 | 201 | void Replay::UI::ReplayViewController::SetButton(bool overwriteFile) { 202 | std::string actionName = overwriteFile ? "Overwrite" : "Delete"; 203 | 204 | static auto contentName = il2cpp_utils::newcsstr("Content"); 205 | static auto textName = il2cpp_utils::newcsstr("Text"); 206 | auto contentTransform = deleteButton->get_transform()->Find(contentName); 207 | contentTransform->Find(textName)->GetComponent()->set_text(newcsstr(actionName)); 208 | 209 | std::function deleteFunction = (std::function) [actionName, overwriteFile] () { 210 | auto titleName = il2cpp_utils::newcsstr(actionName + " Replay"); 211 | auto deleteName = il2cpp_utils::newcsstr(actionName); 212 | auto cancelName = il2cpp_utils::newcsstr("Cancel"); 213 | auto text = u"Are you sure?"; 214 | 215 | getDeleteDialogPromptViewController()->Init(titleName, il2cpp_utils::newcsstr(text), deleteName, cancelName, il2cpp_utils::MakeDelegate*>(classof(System::Action_1*), 216 | (std::function) [overwriteFile] (int selectedButton) { 217 | UIManager::singlePlayerFlowCoordinator->DismissViewController(getDeleteDialogPromptViewController(), ViewController::AnimationDirection::Horizontal, nullptr, selectedButton == 0); 218 | 219 | if(selectedButton == 0) { 220 | if(overwriteFile) { 221 | std::filesystem::rename(UIManager::replayViewController->path.c_str(), ReplayUtils::GetReplayFilePath(SongUtils::GetMapID())); 222 | } else { 223 | std::remove(UIManager::replayViewController->path.c_str()); 224 | } 225 | 226 | UIManager::singlePlayerFlowCoordinator->BackButtonWasPressed(UIManager::replayViewController); 227 | 228 | UIManager::SetReplayButtonCanvasActive(overwriteFile); 229 | } 230 | } 231 | )); 232 | UIManager::singlePlayerFlowCoordinator->PresentViewController(getDeleteDialogPromptViewController(), nullptr, ViewController::AnimationDirection::Horizontal, false); 233 | }; 234 | 235 | auto onClick = UnityEngine::UI::Button::ButtonClickedEvent::New_ctor(); 236 | onClick->AddListener(il2cpp_utils::MakeDelegate(classof(UnityAction*), deleteFunction)); 237 | 238 | deleteButton->set_onClick(onClick); 239 | } -------------------------------------------------------------------------------- /src/UI/UIManager.cpp: -------------------------------------------------------------------------------- 1 | #include "UI/UIManager.hpp" 2 | 3 | #include "UnityEngine/Transform.hpp" 4 | #include "UnityEngine/GameObject.hpp" 5 | #include "UnityEngine/UI/LayoutElement.hpp" 6 | #include "UnityEngine/Material.hpp" 7 | #include "UnityEngine/Resources.hpp" 8 | #include "UnityEngine/Events/UnityAction.hpp" 9 | 10 | #include "TMPro/TextMeshProUGUI.hpp" 11 | #include "TMPro/TextAlignmentOptions.hpp" 12 | 13 | #include "HMUI/ViewController.hpp" 14 | #include "HMUI/ViewController_AnimationDirection.hpp" 15 | 16 | #include "questui/shared/BeatSaberUI.hpp" 17 | #include "questui/shared/ArrayUtil.hpp" 18 | 19 | #include "Utils/FileUtils.hpp" 20 | #include "Utils/TimeUtils.hpp" 21 | #include "Sprites.hpp" 22 | 23 | #include "rapidjson/document.h" 24 | #include "rapidjson/stringbuffer.h" 25 | #include 26 | #include 27 | 28 | using namespace Replay::UI; 29 | using namespace GlobalNamespace; 30 | using namespace UnityEngine; 31 | using namespace UnityEngine::UI; 32 | using namespace UnityEngine::Events; 33 | using namespace il2cpp_utils; 34 | using namespace QuestUI; 35 | using namespace HMUI; 36 | 37 | std::function getReplayFunction(std::string path, bool overwrite) { 38 | std::function replayFunction = (std::function) [path, overwrite] () { 39 | if(UIManager::replayViewController == nullptr) UIManager::replayViewController = BeatSaberUI::CreateViewController(); 40 | UIManager::replayViewController->Init(path, overwrite); 41 | UIManager::singlePlayerFlowCoordinator->PresentViewController(UIManager::replayViewController, nullptr, ViewController::AnimationDirection::Horizontal, false); 42 | }; 43 | return replayFunction; 44 | } 45 | 46 | Button::ButtonClickedEvent* createReplayOnClick(std::string path, bool overwrite) { 47 | auto onClick = Button::ButtonClickedEvent::New_ctor(); 48 | onClick->AddListener(il2cpp_utils::MakeDelegate(classof(UnityAction*), getReplayFunction(path, overwrite))); 49 | return onClick; 50 | } 51 | 52 | std::function getPlayButtonFunction() { 53 | static std::function playButtonFunction = (std::function) [] () { 54 | log("Play button pressed"); 55 | ReplayManager::replayState = ReplayState::RECORDING; 56 | 57 | SongUtils::didFail = false; 58 | 59 | ReplayManager::recorder = ReplayRecorder(); 60 | ReplayManager::recorder.Init(); 61 | }; 62 | return playButtonFunction; 63 | } 64 | 65 | void UIManager::SetReplayButtonOnClick(UnityEngine::Transform* buttonTransform, std::string path, bool overwrite) { 66 | buttonTransform->GetComponent()->set_onClick(createReplayOnClick(path, overwrite)); 67 | } 68 | 69 | UnityEngine::Transform* UIManager::CreateReplayButton(UnityEngine::Transform* parent, UnityEngine::UI::Button* templateButton, UnityEngine::UI::Button* actionButton, std::string path, bool overwrite) { 70 | static auto replayButtonName = il2cpp_utils::newcsstr("ReplayButton"); 71 | 72 | UnityEngine::Transform* buttonTransform = Object::Instantiate(templateButton->get_gameObject(), parent)->get_transform(); 73 | buttonTransform->set_name(replayButtonName); 74 | 75 | static auto contentName = il2cpp_utils::newcsstr("Content"); 76 | static auto textName = il2cpp_utils::newcsstr("Text"); 77 | auto contentTransform = buttonTransform->Find(contentName); 78 | Object::Destroy(contentTransform->Find(textName)->get_gameObject()); 79 | Object::Destroy(contentTransform->GetComponent()); 80 | 81 | static auto iconName = il2cpp_utils::newcsstr("Icon"); 82 | auto iconGameObject = GameObject::New_ctor(iconName); 83 | auto imageView = iconGameObject->AddComponent(); 84 | auto iconTransform = imageView->get_rectTransform(); 85 | iconTransform->SetParent(contentTransform, false); 86 | imageView->set_material(ArrayUtil::First(Resources::FindObjectsOfTypeAll(), [] (Material* x) { return to_utf8(csstrtostr(x->get_name())) == "UINoGlow"; })); 87 | imageView->set_sprite(BeatSaberUI::Base64ToSprite(Replay::Sprites::ReplayIcon)); 88 | imageView->set_preserveAspect(true); 89 | 90 | float scale = 1.3f; 91 | iconTransform->set_localScale(UnityEngine::Vector3(scale, scale, scale)); 92 | 93 | ((RectTransform*) buttonTransform)->set_sizeDelta({10, 10}); 94 | ((RectTransform*) buttonTransform)->set_anchoredPosition({5, -5}); 95 | 96 | SetReplayButtonOnClick(buttonTransform, path, overwrite); 97 | buttonTransform->GetComponent()->set_interactable(true); 98 | 99 | actionButton->get_onClick()->AddListener(il2cpp_utils::MakeDelegate(classof(UnityAction*), getPlayButtonFunction())); 100 | 101 | return buttonTransform; 102 | } 103 | 104 | void UIManager::CreateReplayCanvas(StandardLevelDetailView* standardLevelDetailView, bool replayFileExists) { 105 | static auto canvasName = il2cpp_utils::newcsstr("ReplayButtonCanvas"); 106 | static auto replayButtonName = il2cpp_utils::newcsstr("ReplayButton"); 107 | static auto failedTextName = il2cpp_utils::newcsstr("FailedText"); 108 | 109 | auto playButton = standardLevelDetailView->actionButton; 110 | auto templateButton = standardLevelDetailView->practiceButton; 111 | 112 | buttonParent = templateButton->get_transform()->GetParent(); 113 | auto canvasTransform = (RectTransform*) buttonParent->Find(canvasName); 114 | 115 | Transform* replayButtonTransform = nullptr; 116 | Transform* failedTextTransform = nullptr; 117 | 118 | TMPro::TextMeshProUGUI* failedTimeText = nullptr; 119 | 120 | if(canvasTransform) { 121 | replayButtonTransform = canvasTransform->Find(replayButtonName); 122 | SetReplayButtonOnClick(replayButtonTransform, ReplayUtils::GetReplayFilePath(SongUtils::GetMapID())); 123 | failedTextTransform = canvasTransform->Find(failedTextName); 124 | failedTimeText = failedTextTransform->get_gameObject()->GetComponent(); 125 | } else { 126 | canvasTransform = (RectTransform*) BeatSaberUI::CreateCanvas()->get_transform(); 127 | canvasTransform->set_name(canvasName); 128 | canvasTransform->SetParent(buttonParent, false); 129 | canvasTransform->set_localScale({1, 1, 1}); 130 | canvasTransform->set_sizeDelta({10, 10}); 131 | canvasTransform->set_anchoredPosition({0, -5}); 132 | auto canvasLayout = canvasTransform->get_gameObject()->AddComponent(); 133 | canvasLayout->set_preferredWidth(10); 134 | canvasTransform->SetAsLastSibling(); 135 | 136 | replayButtonTransform = CreateReplayButton(canvasTransform, templateButton, playButton, ReplayUtils::GetReplayFilePath(SongUtils::GetMapID())); 137 | 138 | failedTimeText = QuestUI::BeatSaberUI::CreateText(canvasTransform, "", true, {-0.5f, -7}); 139 | failedTimeText->set_alignment(TMPro::TextAlignmentOptions::Center); 140 | failedTimeText->set_fontSize(5); 141 | failedTimeText->set_lineSpacing(-45); 142 | failedTimeText->get_gameObject()->set_name(failedTextName); 143 | 144 | replayButtonTransform->SetAsLastSibling(); 145 | } 146 | 147 | SetReplayButtonCanvasActive(replayFileExists); 148 | 149 | rapidjson::Document metadata = FileUtils::GetMetadataFromReplayFile(ReplayUtils::GetReplayFilePath(SongUtils::GetMapID())); 150 | 151 | if(metadata.HasMember("FailedInfo") && replayFileExists) { 152 | float failedSongTime = metadata["FailedInfo"]["FailedTime"].GetFloat(); 153 | 154 | failedTimeText->get_gameObject()->SetActive(true); 155 | failedTimeText->set_text(newcsstr(TimeUtils::SecondsToString(failedSongTime))); 156 | } else { 157 | failedTimeText->get_gameObject()->SetActive(false); 158 | } 159 | } -------------------------------------------------------------------------------- /src/Utils/FindComponentsUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "Utils/FindComponentsUtils.hpp" 2 | 3 | #include "questui/shared/ArrayUtil.hpp" 4 | 5 | #include "UnityEngine/Resources.hpp" 6 | 7 | using namespace GlobalNamespace; 8 | using namespace HMUI; 9 | using namespace UnityEngine; 10 | 11 | namespace Replay::FindComponentsUtils { 12 | 13 | #define CacheNotFoundWarningLog(type) log("Can't find '" #type "'! (This shouldn't happen and can cause unexpected behaviour)"); 14 | 15 | #define CacheFindComponentDefineFirst(name) \ 16 | name* _##name = nullptr; \ 17 | name* Get##name() { \ 18 | if(!_##name) \ 19 | _##name = QuestUI::ArrayUtil::First(Resources::FindObjectsOfTypeAll()); \ 20 | if(!_##name) \ 21 | CacheNotFoundWarningLog(_##name) \ 22 | return _##name; \ 23 | } 24 | #define CacheFindComponentDefineLast(name) \ 25 | name* _##name = nullptr; \ 26 | name* Get##name() { \ 27 | if(!_##name) \ 28 | _##name = QuestUI::ArrayUtil::Last(Resources::FindObjectsOfTypeAll()); \ 29 | if(!_##name) \ 30 | CacheNotFoundWarningLog(_##name) \ 31 | return _##name; \ 32 | } 33 | #define CacheClearComponent(name) _##name = nullptr; 34 | 35 | CacheFindComponentDefineLast(SimpleDialogPromptViewController) 36 | CacheFindComponentDefineFirst(LevelSelectionNavigationController) 37 | CacheFindComponentDefineLast(ScreenSystem) 38 | 39 | void ClearCache() { 40 | CacheClearComponent(SimpleDialogPromptViewController) 41 | CacheClearComponent(LevelSelectionNavigationController) 42 | CacheClearComponent(ScreenSystem) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/Utils/SongUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "Utils/SongUtils.hpp" 2 | 3 | #include "custom-types/shared/coroutine.hpp" 4 | #include "questui/shared/ArrayUtil.hpp" 5 | #include "UnityEngine/Resources.hpp" 6 | #include "GlobalNamespace/BeatmapLevelsModel.hpp" 7 | #include "GlobalNamespace/SharedCoroutineStarter.hpp" 8 | #include "GlobalNamespace/IReadonlyBeatmapData.hpp" 9 | #include "System/Threading/Tasks/Task_1.hpp" 10 | 11 | std::string Replay::SongUtils::GetMapID() { 12 | return mapID; 13 | } 14 | 15 | custom_types::Helpers::Coroutine GetBeatmapData(GlobalNamespace::IDifficultyBeatmap* difficultyBeatmap, std::string mapID) { 16 | auto* model = QuestUI::ArrayUtil::First(UnityEngine::Resources::FindObjectsOfTypeAll()); 17 | 18 | auto* preview = model->GetLevelPreviewForLevelId(mapID); 19 | if(preview == nullptr) co_return; 20 | 21 | auto* envInfo = preview->get_environmentInfo(); 22 | 23 | auto* result = difficultyBeatmap->GetBeatmapDataAsync(envInfo); 24 | 25 | while(!result->get_IsCompleted()) co_yield nullptr; 26 | 27 | Replay::SongUtils::beatmapData = result->get_ResultOnSuccess(); 28 | 29 | co_return; 30 | } 31 | 32 | void Replay::SongUtils::SetMapID(GlobalNamespace::StandardLevelDetailView* standardLevelDetailView) { 33 | auto* Level = reinterpret_cast(standardLevelDetailView->level); 34 | if(Level == nullptr) { 35 | log("Beatmap Level is null"); 36 | return; 37 | } 38 | std::string LevelID = to_utf8(csstrtostr(Level->get_levelID())); 39 | 40 | std::string rawID = LevelID; 41 | 42 | if(LevelID.find("custom_level_") != std::string::npos) { 43 | LevelID.erase(LevelID.begin(), LevelID.begin()+13); 44 | transform(LevelID.begin(), LevelID.end(), LevelID.begin(), ::tolower); 45 | } 46 | 47 | auto* SelectedBeatmapDifficulty = standardLevelDetailView->selectedDifficultyBeatmap; 48 | int Difficulty = SelectedBeatmapDifficulty->get_difficulty(); 49 | 50 | auto* parentDifficultyBeatmapSet = SelectedBeatmapDifficulty->get_parentDifficultyBeatmapSet(); 51 | auto* beatmapCharacteristic = parentDifficultyBeatmapSet->get_beatmapCharacteristic(); 52 | std::string modeName = to_utf8(csstrtostr(beatmapCharacteristic->compoundIdPartName)); 53 | 54 | mapID = LevelID + "_" + std::to_string(Difficulty); 55 | if(!modeName.empty()) mapID = mapID + "_" + modeName; 56 | 57 | log("MapID is %s", mapID.c_str()); 58 | 59 | GlobalNamespace::SharedCoroutineStarter::get_instance()->StartCoroutine(custom_types::Helpers::CoroutineHelper::New(GetBeatmapData(SelectedBeatmapDifficulty, rawID))); 60 | } -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "static-defines.hpp" 2 | 3 | extern "C" void setup(ModInfo& info) { 4 | info.id = ID; 5 | info.version = VERSION; 6 | 7 | modInfo = info; 8 | } 9 | 10 | extern "C" void load() { 11 | il2cpp_functions::Init(); 12 | 13 | custom_types::Register::AutoRegister(); 14 | 15 | log("Installing Replay hooks..."); 16 | Replay::Hooks::InstallHooks(replayLogger()); 17 | log("Installed Replay hooks!"); 18 | } --------------------------------------------------------------------------------