├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── cover.png ├── makefile ├── qpm.json └── src ├── conversions.hpp └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | /.obj/ 2 | /extern.cmake 3 | /extern/ 4 | /libmappingextensions.so 5 | /makefile.user 6 | /MappingExtensions.qmod 7 | /qpm.shared.json 8 | /qpm_defines.cmake 9 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(qpm_defines.cmake) 2 | 3 | cmake_minimum_required(VERSION 3.22) 4 | project(${COMPILE_ID}) 5 | 6 | set(CMAKE_CXX_STANDARD 20) 7 | set(CMAKE_CXX_STANDARD_REQUIRED 20) 8 | 9 | set(SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src) 10 | set(INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include) 11 | 12 | add_compile_options(-frtti -fPIE -fPIC -fexceptions) 13 | add_compile_definitions(VERSION=\"${MOD_VERSION}\") 14 | add_compile_definitions(ID=\"${MOD_ID}\") 15 | 16 | RECURSE_FILES(cpp_file_list ${SOURCE_DIR}/*.cpp) 17 | RECURSE_FILES(c_file_list ${SOURCE_DIR}/*.c) 18 | 19 | add_library( 20 | ${COMPILE_ID} 21 | SHARED 22 | ${cpp_file_list} 23 | ${c_file_list} 24 | ) 25 | 26 | target_include_directories(${COMPILE_ID} PRIVATE ${SOURCE_DIR}) 27 | target_include_directories(${COMPILE_ID} PRIVATE ${INCLUDE_DIR}) 28 | target_include_directories(${COMPILE_ID} PUBLIC ${SHARED_DIR}) 29 | target_include_directories(${COMPILE_ID} PRIVATE ${EXTERN_DIR}/includes/${CODEGEN_ID}/include) 30 | 31 | target_link_libraries(${COMPILE_ID} PRIVATE -llog -lz) 32 | include(extern.cmake) 33 | 34 | add_custom_command(TARGET ${COMPILE_ID} POST_BUILD 35 | COMMAND ${CMAKE_STRIP} -g -S -d --strip-all 36 | "lib${COMPILE_ID}.so" -o "stripped_lib${COMPILE_ID}.so" 37 | COMMENT "Strip debug symbols done on final binary.") 38 | 39 | add_custom_command(TARGET ${COMPILE_ID} POST_BUILD 40 | COMMAND ${CMAKE_COMMAND} -E make_directory debug 41 | COMMENT "Make directory for debug symbols" 42 | ) 43 | 44 | add_custom_command(TARGET ${COMPILE_ID} POST_BUILD 45 | COMMAND ${CMAKE_COMMAND} -E rename lib${COMPILE_ID}.so debug/lib${COMPILE_ID}.so 46 | COMMENT "Rename the lib to debug_ since it has debug symbols" 47 | ) 48 | 49 | add_custom_command(TARGET ${COMPILE_ID} POST_BUILD 50 | COMMAND ${CMAKE_COMMAND} -E rename stripped_lib${COMPILE_ID}.so lib${COMPILE_ID}.so 51 | COMMENT "Rename the stripped lib to regular" 52 | ) 53 | 54 | foreach(so_file ${so_list}) 55 | cmake_path(GET so_file FILENAME file) 56 | 57 | add_custom_command(TARGET ${COMPILE_ID} POST_BUILD 58 | COMMAND ${CMAKE_COMMAND} -E copy ${so_file} debug/${file} 59 | COMMENT "Copy so files for ndk stack" 60 | ) 61 | 62 | add_custom_command(TARGET ${COMPILE_ID} POST_BUILD 63 | COMMAND ${CMAKE_STRIP} -g -S -d --strip-all ${so_file} -o ${file} 64 | COMMENT "Strip debug symbols from the dependencies") 65 | endforeach() 66 | 67 | foreach(a_file ${a_list}) 68 | cmake_path(GET a_file FILENAME file) 69 | 70 | add_custom_command(TARGET ${COMPILE_ID} POST_BUILD 71 | COMMAND ${CMAKE_COMMAND} -E copy ${a_file} debug/${file} 72 | COMMENT "Copy a files for ndk stack") 73 | endforeach() 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kyle1413 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcelyte/MappingExtensions/af11adf68fb9b1362b9ebe009591bd12dc55ddc2/cover.png -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | #!/bin/make 2 | .SILENT: 3 | 4 | sinclude makefile.user 5 | 6 | CXX := $(NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/clang++ --target=aarch64-linux-android26 7 | CXXFLAGS := -O2 -std=c++20 -fPIC -ffunction-sections -fdata-sections -fvisibility=hidden -fdeclspec \ 8 | -Weverything -Wno-c++98-compat -Wno-pre-c++20-compat-pedantic -Werror -pedantic-errors \ 9 | -isystem extern/includes/libil2cpp/il2cpp/libil2cpp -isystem extern/includes/bs-cordl/include -isystem extern/includes/fmt/fmt/include \ 10 | -isystem extern/includes -DUNITY_2021 -DHAS_CODEGEN -DFMT_HEADER_ONLY 11 | LDFLAGS = -O2 -s -static-libstdc++ -shared -Wl,--no-undefined,--gc-sections,--fatal-warnings \ 12 | -Lextern/libs -l:$(notdir $(wildcard extern/libs/libbeatsaber-hook*.so)) -lsongcore -lpaperlog -lsl2 -llog 13 | ifdef NDK 14 | OBJDIR := .obj/$(shell $(CXX) -dumpmachine) 15 | else 16 | OBJDIR := .obj/unknown 17 | ndk: 18 | $(error Android NDK path not set) 19 | endif 20 | FILES := src/main.cpp .obj/And64InlineHook.cpp 21 | OBJS := $(FILES:%=$(OBJDIR)/%.o) 22 | 23 | qmod: MappingExtensions.qmod 24 | 25 | libmappingextensions.so: $(OBJS) | ndk 26 | @echo "[cxx $@]" 27 | $(CXX) $(LDFLAGS) $(OBJS) -o "$@" 28 | 29 | $(OBJDIR)/.obj/%.cpp.o: CXXFLAGS += -w -Iextern/includes/beatsaber-hook/shared/inline-hook 30 | $(OBJDIR)/%.cpp.o: %.cpp extern makefile | ndk 31 | @echo "[cxx $(notdir $@)]" 32 | @mkdir -p "$(@D)" 33 | $(CXX) $(CXXFLAGS) -c "$<" -o "$@" -MMD -MP 34 | 35 | .obj/And64InlineHook.cpp: extern/includes/beatsaber-hook/shared/inline-hook/And64InlineHook.cpp extern 36 | @echo "[sed $(notdir $@)]" 37 | @mkdir -p "$(@D)" 38 | sed 's/__attribute__((visibility("default")))//' $< > $@ 39 | 40 | .obj/mod.json: extern makefile 41 | @echo "[printf $(notdir $@)]" 42 | @mkdir -p "$(@D)" 43 | printf "{\n\ 44 | \"\$$schema\": \"https://raw.githubusercontent.com/Lauriethefish/QuestPatcher.QMod/main/QuestPatcher.QMod/Resources/qmod.schema.json\",\n\ 45 | \"_QPVersion\": \"1.2.0\",\n\ 46 | \"modloader\": \"Scotland2\",\n\ 47 | \"name\": \"Mapping Extensions\",\n\ 48 | \"id\": \"MappingExtensions\",\n\ 49 | \"author\": \"StackDoubleFlow, rxzz0, & rcelyte\",\n\ 50 | \"version\": \"0.24.1\",\n\ 51 | \"packageId\": \"com.beatgames.beatsaber\",\n\ 52 | \"packageVersion\": \"1.37.0_9064817954\",\n\ 53 | \"description\": \"This adds a host of new things you can do with your maps as a mapper, and allows you to play said maps as a player. An update of the port of the PC original mod by Kyle 1413. Previously maintained by zoller27osu.\",\n\ 54 | \"coverImage\": \"cover.png\",\n\ 55 | \"dependencies\": [\n\ 56 | {\n\ 57 | \"version\": \"^3.6.1\",\n\ 58 | \"id\": \"paper\",\n\ 59 | \"downloadIfMissing\": \"https://github.com/Fernthedev/paperlog/releases/download/v3.6.3/paperlog.qmod\"\n\ 60 | }, {\n\ 61 | \"version\": \"^1.1.12\",\n\ 62 | \"id\": \"songcore\",\n\ 63 | \"downloadIfMissing\": \"https://github.com/raineio/Quest-SongCore/releases/download/v1.1.13/SongCore.qmod\"\n\ 64 | }, {\n\ 65 | \"version\": \"^0.21.1\",\n\ 66 | \"id\": \"custom-json-data\",\n\ 67 | \"downloadIfMissing\": \"https://github.com/StackDoubleFlow/CustomJSONData/releases/download/v0.21.1/custom-json-data.qmod\",\n\ 68 | \"required\": false\n\ 69 | }, {\n\ 70 | \"version\": \"^1.5.1\",\n\ 71 | \"id\": \"NoodleExtensions\",\n\ 72 | \"downloadIfMissing\": \"https://github.com/StackDoubleFlow/NoodleExtensions/releases/download/v1.5.1/NoodleExtensions.qmod\",\n\ 73 | \"required\": false\n\ 74 | }\n\ 75 | ],\n\ 76 | \"lateModFiles\": [\"libmappingextensions.so\"],\n\ 77 | \"libraryFiles\": [\"$(notdir $(wildcard extern/libs/libbeatsaber-hook*.so))\"]\n\ 78 | }" > .obj/mod.json 79 | 80 | MappingExtensions.qmod: libmappingextensions.so .obj/mod.json 81 | @echo "[zip $@]" 82 | zip -j "$@" cover.png extern/libs/libbeatsaber-hook*.so libmappingextensions.so .obj/mod.json 83 | 84 | extern: qpm.json 85 | @echo "[qpm restore]" 86 | qpm-rust restore 87 | 88 | clean: 89 | @echo "[cleaning]" 90 | rm -rf .obj/ extern/ include/ MappingExtensions.qmod libmappingextensions.so 91 | qpm-rust clear || true 92 | 93 | .PHONY: clean ndk qmod 94 | 95 | sinclude $(FILES:%=$(OBJDIR)/%.d) 96 | -------------------------------------------------------------------------------- /qpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "sharedDir": "", 3 | "dependenciesDir": "extern", 4 | "info": { 5 | "name": "MappingExtensions", 6 | "id": "MappingExtensions", 7 | "version": "0.24.1", 8 | "url": "https://github.com/rcelyte/MappingExtensions", 9 | "additionalData": {} 10 | }, 11 | "dependencies": [ 12 | { 13 | "id": "beatsaber-hook", 14 | "versionRange": "^5.1.9", 15 | "additionalData": {} 16 | }, { 17 | "id": "bs-cordl", 18 | "versionRange": "^3700.0.0", 19 | "additionalData": {} 20 | }, { 21 | "id": "paper", 22 | "versionRange": "^3.6.1", 23 | "additionalData": {} 24 | }, { 25 | "id": "scotland2", 26 | "versionRange": "^0.1.4", 27 | "additionalData": { 28 | "includeQmod": false, 29 | "private": true 30 | } 31 | }, { 32 | "id": "sombrero", 33 | "versionRange": "^0.1.37", 34 | "additionalData": {} 35 | }, { 36 | "id": "songcore", 37 | "versionRange": "^1.1.12", 38 | "additionalData": {} 39 | } 40 | ], 41 | "additionalData": {} 42 | } 43 | -------------------------------------------------------------------------------- /src/conversions.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | static inline std::vector ConvertBpmChanges(float startBpm, System::Collections::Generic::List_1 *const events) { 5 | uint32_t first = 0, count = 0; 6 | for(int32_t i = events->get_Count() - 1; i >= 0; --i) { 7 | if(events->get_Item(i)->get_type() != BeatmapSaveDataCommon::BeatmapEventType::BpmChange) 8 | continue; 9 | first = static_cast(i); 10 | ++count; 11 | } 12 | const bool skipFirst = (count > 0 && std::bit_cast(events->get_Item(static_cast(first))->get_beat()) == 0); 13 | std::vector changes; 14 | changes.reserve(count + !skipFirst); 15 | GlobalNamespace::BpmTimeProcessor::BpmChangeData lastEntry = {0, 0, skipFirst ? events->get_Item(static_cast(first))->get_time() : startBpm}; // This looks wrong, but it matches what Beat Games did 16 | changes.push_back(lastEntry); 17 | for(uint32_t i = first + skipFirst; i < count; i++) { 18 | BeatmapSaveDataVersion2_6_0AndEarlier::EventData *const event = events->get_Item(static_cast(i)); 19 | if(event->get_type() != BeatmapSaveDataCommon::BeatmapEventType::BpmChange) 20 | continue; 21 | const float beat = event->get_time(); 22 | lastEntry = {GlobalNamespace::BpmTimeProcessor::CalculateTime(lastEntry, beat), beat, event->get_floatValue()}; 23 | changes.push_back(lastEntry); 24 | } 25 | return changes; 26 | } 27 | 28 | static inline std::vector ConvertBpmChanges(float startBpm, System::Collections::Generic::List_1 *const events) { 29 | const uint32_t count = static_cast(events->get_Count()); 30 | const bool skipFirst = (count > 0 && std::bit_cast(events->get_Item(0)->get_beat()) == 0); 31 | std::vector changes; 32 | changes.reserve(count + !skipFirst); 33 | GlobalNamespace::BpmTimeProcessor::BpmChangeData lastEntry = {0, 0, skipFirst ? events->get_Item(0)->get_bpm() : startBpm}; 34 | changes.push_back(lastEntry); 35 | for(uint32_t i = skipFirst; i < count; i++) { 36 | BeatmapSaveDataVersion3::BpmChangeEventData *const event = events->get_Item(static_cast(i)); 37 | const float beat = event->get_beat(); 38 | lastEntry = {GlobalNamespace::BpmTimeProcessor::CalculateTime(lastEntry, beat), beat, event->get_bpm()}; 39 | changes.push_back(lastEntry); 40 | } 41 | return changes; 42 | } 43 | 44 | struct BpmState { 45 | std::span changes; 46 | uint32_t current = 0; 47 | BpmState(std::span changeData) : changes(changeData) {} 48 | float GetTime(float beat) { 49 | while(current > 0 && changes[current].bpmChangeStartBpmTime >= beat) 50 | --current; 51 | while(current < changes.size() - 1 && changes[current + 1].bpmChangeStartBpmTime < beat) 52 | ++current; 53 | return changes[current].bpmChangeStartTime + (beat - changes[current].bpmChangeStartBpmTime) / changes[current].bpm * 60; 54 | } 55 | }; 56 | 57 | template struct RestoreBlob; 58 | 59 | template<> struct RestoreBlob { 60 | struct Ident { 61 | float time; 62 | int32_t lineIndex; 63 | GlobalNamespace::NoteData::GameplayType gameplayType; 64 | GlobalNamespace::ColorType colorType; 65 | GlobalNamespace::NoteCutDirection cutDirection; 66 | Ident(GlobalNamespace::NoteData *from) : 67 | time(from->get_time()), 68 | lineIndex(from->get_lineIndex()), 69 | gameplayType(from->get_gameplayType()), 70 | colorType(from->get_colorType()), 71 | cutDirection(from->get_cutDirection()) {} 72 | Ident(BeatmapSaveDataVersion2_6_0AndEarlier::NoteData *from, BpmState *bpmState) : 73 | time(bpmState->GetTime(from->get_time())), 74 | lineIndex(from->get_lineIndex()), 75 | gameplayType((from->get_type() == BeatmapSaveDataVersion2_6_0AndEarlier::NoteType::Bomb) ? 76 | GlobalNamespace::NoteData::GameplayType::Bomb : GlobalNamespace::NoteData::GameplayType::Normal), 77 | colorType((from->get_type() == BeatmapSaveDataVersion2_6_0AndEarlier::NoteType::Bomb) ? GlobalNamespace::ColorType::None : 78 | (from->get_type() == BeatmapSaveDataVersion2_6_0AndEarlier::NoteType::NoteA) ? GlobalNamespace::ColorType::ColorA : GlobalNamespace::ColorType::ColorB), 79 | cutDirection((from->get_type() == BeatmapSaveDataVersion2_6_0AndEarlier::NoteType::Bomb) ? GlobalNamespace::NoteCutDirection::None : 80 | GlobalNamespace::BeatmapTypeConverters::ConvertNoteCutDirection(from->get_cutDirection())) {} 81 | } ident; 82 | struct { 83 | BeatmapSaveDataCommon::NoteLineLayer lineLayer; 84 | BeatmapSaveDataCommon::NoteCutDirection cutDirection; 85 | } value; 86 | RestoreBlob(BeatmapSaveDataVersion2_6_0AndEarlier::NoteData *from, BpmState *bpmState) : ident(from, bpmState), 87 | value{from->get_lineLayer(), from->get_cutDirection()} {} 88 | }; 89 | 90 | template<> struct RestoreBlob { 91 | struct Ident { 92 | float time; 93 | int32_t lineIndex; 94 | float duration; 95 | int32_t width; 96 | Ident(GlobalNamespace::ObstacleData *from) : 97 | time(from->get_time()), 98 | lineIndex(from->get_lineIndex()), 99 | duration(from->get_duration()), 100 | width(from->get_width()) {} 101 | Ident(BeatmapSaveDataVersion2_6_0AndEarlier::ObstacleData *from, BpmState *bpmState) : 102 | time(bpmState->GetTime(from->get_time())), 103 | lineIndex(from->get_lineIndex()), 104 | duration(bpmState->GetTime(from->get_time() + from->get_duration()) - this->time), 105 | width(from->get_width()) {} 106 | } ident; 107 | BeatmapSaveDataVersion2_6_0AndEarlier::ObstacleType value; 108 | RestoreBlob(BeatmapSaveDataVersion2_6_0AndEarlier::ObstacleData *from, BpmState *bpmState) : ident(from, bpmState), value(from->get_type()) {} 109 | }; 110 | 111 | struct SliderLinematch { 112 | GlobalNamespace::ColorType colorType; 113 | float headTime; 114 | int32_t headLineIndex; 115 | GlobalNamespace::NoteCutDirection headCutDirection; 116 | float tailTime; 117 | int32_t tailLineIndex; 118 | SliderLinematch(GlobalNamespace::SliderData *from) : 119 | colorType(from->get_colorType()), 120 | headTime(from->get_time()), 121 | headLineIndex(from->get_headLineIndex()), 122 | headCutDirection(from->get_headCutDirection()), 123 | tailTime(from->get_tailTime()), 124 | tailLineIndex(from->get_tailLineIndex()) {} 125 | SliderLinematch(BeatmapSaveDataVersion2_6_0AndEarlier::SliderData *from, BpmState *bpmState) : 126 | colorType(GlobalNamespace::BeatmapTypeConverters::ConvertNoteColorType(from->get_colorType())), 127 | headTime(bpmState->GetTime(from->get_time())), 128 | headLineIndex(from->get_headLineIndex()), 129 | headCutDirection(GlobalNamespace::BeatmapTypeConverters::ConvertNoteCutDirection(from->get_headCutDirection())), 130 | tailTime(bpmState->GetTime(from->get_tailTime())), 131 | tailLineIndex(from->get_tailLineIndex()) {} 132 | SliderLinematch(BeatmapSaveDataVersion3::BaseSliderData *from, BpmState *bpmState) : 133 | colorType(GlobalNamespace::BeatmapTypeConverters::ConvertNoteColorType(from->get_colorType())), 134 | headTime(bpmState->GetTime(from->get_beat())), 135 | headLineIndex(from->get_headLine()), 136 | headCutDirection(GlobalNamespace::BeatmapTypeConverters::ConvertNoteCutDirection(from->get_headCutDirection())), 137 | tailTime(bpmState->GetTime(from->get_tailBeat())), 138 | tailLineIndex(from->get_tailLine()) {} 139 | }; 140 | 141 | struct SliderRestoreLayers { 142 | BeatmapSaveDataCommon::NoteLineLayer headLayer, tailLayer; 143 | }; 144 | 145 | template<> struct RestoreBlob { 146 | struct Ident { 147 | SliderLinematch base; 148 | GlobalNamespace::NoteCutDirection tailCutDirection; 149 | GlobalNamespace::SliderMidAnchorMode midAnchorMode; 150 | float headControlPointLengthMultiplier; 151 | float tailControlPointLengthMultiplier; 152 | Ident(GlobalNamespace::SliderData *from) : 153 | base(from), 154 | tailCutDirection(from->get_tailCutDirection()), 155 | midAnchorMode(from->get_midAnchorMode()), 156 | headControlPointLengthMultiplier(from->get_headControlPointLengthMultiplier()), 157 | tailControlPointLengthMultiplier(from->get_tailControlPointLengthMultiplier()) {} 158 | Ident(BeatmapSaveDataVersion2_6_0AndEarlier::SliderData *from, BpmState *bpmState) : 159 | base(from, bpmState), 160 | tailCutDirection(GlobalNamespace::BeatmapTypeConverters::ConvertNoteCutDirection(from->get_tailCutDirection())), 161 | midAnchorMode(GlobalNamespace::BeatmapTypeConverters::ConvertSliderMidAnchorMode(from->get_sliderMidAnchorMode())), 162 | headControlPointLengthMultiplier(from->get_headControlPointLengthMultiplier()), 163 | tailControlPointLengthMultiplier(from->get_tailControlPointLengthMultiplier()) {} 164 | } ident; 165 | SliderRestoreLayers value; 166 | RestoreBlob(BeatmapSaveDataVersion2_6_0AndEarlier::SliderData *from, BpmState *bpmState) : 167 | ident(from, bpmState), value{from->get_headLineLayer(), from->get_tailLineLayer()} {} 168 | }; 169 | 170 | template<> struct RestoreBlob { 171 | struct Ident { 172 | float time; 173 | int32_t lineIndex; 174 | GlobalNamespace::OffsetDirection offsetDirection; 175 | Ident(GlobalNamespace::WaypointData *from) : 176 | time(from->get_time()), 177 | lineIndex(from->get_lineIndex()), 178 | offsetDirection(from->get_offsetDirection()) {} 179 | Ident(BeatmapSaveDataVersion2_6_0AndEarlier::WaypointData *from, BpmState *bpmState) : 180 | time(bpmState->GetTime(from->get_time())), 181 | lineIndex(from->get_lineIndex()), 182 | offsetDirection(GlobalNamespace::BeatmapTypeConverters::ConvertOffsetDirection(from->get_offsetDirection())) {} 183 | } ident; 184 | BeatmapSaveDataCommon::NoteLineLayer value; 185 | RestoreBlob(BeatmapSaveDataVersion2_6_0AndEarlier::WaypointData *from, BpmState *bpmState) : ident(from, bpmState), value(from->get_lineLayer()) {} 186 | }; 187 | 188 | template<> struct RestoreBlob { 189 | struct Ident { 190 | float time; 191 | bool early; 192 | Ident(GlobalNamespace::SpawnRotationBeatmapEventData *from) : 193 | time(from->get_time()), 194 | early(from->get_executionOrder() < 0) {} 195 | Ident(BeatmapSaveDataVersion2_6_0AndEarlier::EventData *from, BpmState *bpmState) : 196 | time(bpmState->GetTime(from->get_time())), 197 | early(from->get_type() == BeatmapSaveDataCommon::BeatmapEventType::Event14) {} 198 | } ident; 199 | int32_t value; 200 | RestoreBlob(BeatmapSaveDataVersion2_6_0AndEarlier::EventData *from, BpmState *bpmState) : ident(from, bpmState), value(from->get_value()) {} 201 | }; 202 | 203 | template<> struct RestoreBlob { 204 | struct Ident { 205 | float time; 206 | int32_t lineIndex; 207 | GlobalNamespace::ColorType colorType; 208 | GlobalNamespace::NoteCutDirection cutDirection; 209 | float cutDirectionAngleOffset; 210 | Ident(GlobalNamespace::NoteData *from) : 211 | time(from->get_time()), 212 | lineIndex(from->get_lineIndex()), 213 | colorType(from->get_colorType()), 214 | cutDirection(from->get_cutDirection()), 215 | cutDirectionAngleOffset(from->get_cutDirectionAngleOffset()) {} 216 | Ident(BeatmapSaveDataVersion3::ColorNoteData *from, BpmState *bpmState) : 217 | time(bpmState->GetTime(from->get_beat())), 218 | lineIndex(from->get_line()), 219 | colorType(GlobalNamespace::BeatmapTypeConverters::ConvertNoteColorType(from->get_color())), 220 | cutDirection(GlobalNamespace::BeatmapTypeConverters::ConvertNoteCutDirection(from->get_cutDirection())), 221 | cutDirectionAngleOffset(static_cast(from->get_angleOffset())) {} 222 | } ident; 223 | struct { 224 | BeatmapSaveDataCommon::NoteLineLayer layer; 225 | BeatmapSaveDataCommon::NoteCutDirection cutDirection; 226 | } value; 227 | RestoreBlob(BeatmapSaveDataVersion3::ColorNoteData *from, BpmState *bpmState) : ident(from, bpmState), value{from->get_layer(), from->get_cutDirection()} {} 228 | }; 229 | 230 | template<> struct RestoreBlob { 231 | struct Ident { 232 | float time; 233 | int32_t lineIndex; 234 | Ident(GlobalNamespace::NoteData *from) : 235 | time(from->get_time()), 236 | lineIndex(from->get_lineIndex()) {} 237 | Ident(BeatmapSaveDataVersion3::BombNoteData *from, BpmState *bpmState) : 238 | time(bpmState->GetTime(from->get_beat())), 239 | lineIndex(from->get_line()) {} 240 | } ident; 241 | int32_t value; 242 | RestoreBlob(BeatmapSaveDataVersion3::BombNoteData *from, BpmState *bpmState) : ident(from, bpmState), value(from->get_layer()) {} 243 | }; 244 | 245 | template<> struct RestoreBlob { 246 | struct Ident { 247 | float time; 248 | int32_t lineIndex; 249 | float duration; 250 | int32_t width; 251 | int32_t height; 252 | Ident(GlobalNamespace::ObstacleData *from) : 253 | time(from->get_time()), 254 | lineIndex(from->get_lineIndex()), 255 | duration(from->get_duration()), 256 | width(from->get_width()), 257 | height(from->get_height()) {} 258 | Ident(BeatmapSaveDataVersion3::ObstacleData *from, BpmState *bpmState) : 259 | time(bpmState->GetTime(from->get_beat())), 260 | lineIndex(from->get_line()), 261 | duration(bpmState->GetTime(from->get_beat() + from->get_duration()) - this->time), 262 | width(from->get_width()), 263 | height(from->get_height()) {} 264 | } ident; 265 | int32_t value; 266 | RestoreBlob(BeatmapSaveDataVersion3::ObstacleData *from, BpmState *bpmState) : ident(from, bpmState), value(from->get_layer()) {} 267 | }; 268 | 269 | template<> struct RestoreBlob { 270 | struct Ident { 271 | SliderLinematch base; 272 | GlobalNamespace::NoteCutDirection tailCutDirection; 273 | GlobalNamespace::SliderMidAnchorMode midAnchorMode; 274 | float headControlPointLengthMultiplier; 275 | float tailControlPointLengthMultiplier; 276 | Ident(GlobalNamespace::SliderData *from) : 277 | base(from), 278 | tailCutDirection(from->get_tailCutDirection()), 279 | midAnchorMode(from->get_midAnchorMode()), 280 | headControlPointLengthMultiplier(from->get_headControlPointLengthMultiplier()), 281 | tailControlPointLengthMultiplier(from->get_tailControlPointLengthMultiplier()) {} 282 | Ident(BeatmapSaveDataVersion3::SliderData *from, BpmState *bpmState) : 283 | base(from, bpmState), 284 | tailCutDirection(GlobalNamespace::BeatmapTypeConverters::ConvertNoteCutDirection(from->get_tailCutDirection())), 285 | midAnchorMode(GlobalNamespace::BeatmapTypeConverters::ConvertSliderMidAnchorMode(from->get_sliderMidAnchorMode())), 286 | headControlPointLengthMultiplier(from->get_headControlPointLengthMultiplier()), 287 | tailControlPointLengthMultiplier(from->get_tailControlPointLengthMultiplier()) {} 288 | } ident; 289 | SliderRestoreLayers value; 290 | RestoreBlob(BeatmapSaveDataVersion3::SliderData *from, BpmState *bpmState) : 291 | ident(from, bpmState), value{from->get_headLayer(), from->get_tailLayer()} {} 292 | }; 293 | 294 | template<> struct RestoreBlob { 295 | struct Ident { 296 | SliderLinematch base; 297 | int32_t sliceCount; 298 | float squishAmount; 299 | Ident(GlobalNamespace::SliderData *from) : 300 | base(from), 301 | sliceCount(from->get_sliceCount()), 302 | squishAmount(from->get_squishAmount()) {} 303 | Ident(BeatmapSaveDataVersion3::BurstSliderData *from, BpmState *bpmState) : 304 | base(from, bpmState), 305 | sliceCount(from->get_sliceCount()), 306 | squishAmount(from->get_squishAmount()) {} 307 | } ident; 308 | SliderRestoreLayers value; 309 | RestoreBlob(BeatmapSaveDataVersion3::BurstSliderData *from, BpmState *bpmState) : 310 | ident(from, bpmState), value{from->get_headLayer(), from->get_tailLayer()} {} 311 | }; 312 | 313 | template<> struct RestoreBlob { 314 | struct Ident { 315 | float time; 316 | int32_t lineIndex; 317 | GlobalNamespace::OffsetDirection offsetDirection; 318 | Ident(GlobalNamespace::WaypointData *from) : 319 | time(from->get_time()), 320 | lineIndex(from->get_lineIndex()), 321 | offsetDirection(from->get_offsetDirection()) {} 322 | Ident(BeatmapSaveDataVersion3::WaypointData *from, BpmState *bpmState) : 323 | time(bpmState->GetTime(from->get_beat())), 324 | lineIndex(from->get_line()), 325 | offsetDirection(GlobalNamespace::BeatmapTypeConverters::ConvertOffsetDirection(from->get_offsetDirection())) {} 326 | } ident; 327 | BeatmapSaveDataCommon::NoteLineLayer value; 328 | RestoreBlob(BeatmapSaveDataVersion3::WaypointData *from, BpmState *bpmState) : ident(from, bpmState), value(from->get_layer()) {} 329 | }; 330 | 331 | template 332 | struct LayerCache { 333 | std::vector> cache; 334 | std::vector matched; 335 | size_t head = 0, failCount = 0; 336 | LayerCache(System::Collections::Generic::List_1 *const list, 337 | const std::vector *const changes) : matched(static_cast(list->get_Count())) { 338 | BpmState bpmState(std::span(&(*changes)[0], changes->size())); 339 | cache.reserve(matched.size()); 340 | for(uint32_t i = 0; i < static_cast(matched.size()); ++i) 341 | cache.emplace_back(list->get_Item(static_cast(i)), &bpmState); 342 | } 343 | LayerCache(System::Collections::Generic::List_1 *const list, 344 | const std::vector *const changes, bool (*const filter)(TObjectData*)) : matched() { 345 | BpmState bpmState(std::span(&(*changes)[0], changes->size())); 346 | for(uint32_t i = 0, count = static_cast(list->get_Count()); i < count; ++i) { 347 | if(TObjectData *const item = list->get_Item(static_cast(i)); filter(item)) { 348 | cache.emplace_back(item, &bpmState); 349 | matched.emplace_back(false); 350 | } 351 | } 352 | } 353 | std::optional::value)> restore(auto *const data) { 354 | const typename RestoreBlob::Ident match(data); 355 | for(size_t i = head; i < cache.size(); ++i) { 356 | if(matched[i] || memcmp(&match, &cache[i].ident, sizeof(match)) != 0) 357 | continue; 358 | matched[i] = true; 359 | if(i == head) { 360 | do { 361 | ++head; 362 | } while(head < cache.size() && matched[head]); 363 | } 364 | return cache[i].value; 365 | } 366 | ++failCount; 367 | return std::nullopt; 368 | } 369 | }; 370 | LayerCache(void) -> LayerCache; 371 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | // https://github.com/rxzz0/MappingExtensions/blob/main/src/main.cpp 2 | // Refactored and updated to 1.24.0+ by rcelyte 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include 28 | #include 29 | #include 30 | 31 | #include 32 | #include 33 | 34 | const Paper::ConstLoggerContext<18> logger = {"MappingExtensions"}; 35 | 36 | static const std::string_view requirementNames[] = { 37 | "Mapping Extensions", 38 | "Mapping Extensions-Precision Placement", 39 | "Mapping Extensions-Extra Note Angles", 40 | "Mapping Extensions-More Lanes", 41 | }; 42 | 43 | static bool ShouldActivate(GlobalNamespace::GameplayCoreSceneSetupData *const data) { 44 | auto *const beatmapLevel = il2cpp_utils::try_cast(data->beatmapLevel).value_or(nullptr); 45 | if(beatmapLevel == nullptr) { 46 | logger.warn("level missing SongCore metadata"); 47 | return false; 48 | } 49 | SongCore::CustomJSONData::CustomSaveDataInfo::BasicCustomDifficultyBeatmapDetails details = {}; 50 | const std::optional> info = beatmapLevel->get_CustomSaveDataInfo(); 51 | if(!info.has_value()) { 52 | logger.warn("get_CustomSaveDataInfo() failed"); 53 | return false; 54 | } 55 | if(!info->get().TryGetCharacteristicAndDifficulty( 56 | data->beatmapKey.beatmapCharacteristic->serializedName, data->beatmapKey.difficulty, *&details)) { 57 | logger.warn("TryGetCharacteristicAndDifficulty() failed"); 58 | return false; 59 | } 60 | for(const std::string &req : details.requirements) 61 | for(const std::string_view name : requirementNames) 62 | if(req == name) 63 | return true; 64 | return false; 65 | } 66 | 67 | static bool active = false; 68 | MAKE_HOOK_MATCH(GameplayCoreSceneSetupData_LoadTransformedBeatmapDataAsync, &GlobalNamespace::GameplayCoreSceneSetupData::LoadTransformedBeatmapDataAsync, System::Threading::Tasks::Task*, GlobalNamespace::GameplayCoreSceneSetupData *const self) { 69 | active = ShouldActivate(self); 70 | logger.info("ShouldActivate(): {}", active); 71 | return GameplayCoreSceneSetupData_LoadTransformedBeatmapDataAsync(self); 72 | } 73 | 74 | MAKE_HOOK_MATCH(GameplayCoreSceneSetupData_LoadTransformedBeatmapData, &GlobalNamespace::GameplayCoreSceneSetupData::LoadTransformedBeatmapData, void, GlobalNamespace::GameplayCoreSceneSetupData *const self) { 75 | active = false; 76 | logger.info("ShouldActivate(): sync"); 77 | GameplayCoreSceneSetupData_LoadTransformedBeatmapData(self); 78 | } 79 | 80 | /* PC version hooks */ 81 | 82 | static inline float SpawnRotationForEventValue(float orig, int32_t index) { 83 | if(index >= 1000 && index <= 1720) 84 | return static_cast(index - 1360); 85 | return orig; 86 | } 87 | 88 | static inline int32_t GetHeightForObstacleType(const int32_t orig, const int32_t obstacleType) { 89 | if(obstacleType < 1000 || obstacleType > 4005000) 90 | return orig; 91 | return ((obstacleType >= 4001 && obstacleType <= 4100000) ? (obstacleType - 4001) / 1000 : obstacleType - 1000) * 5 + 1000; 92 | } 93 | 94 | static inline GlobalNamespace::NoteLineLayer GetLayerForObstacleType(const GlobalNamespace::NoteLineLayer orig, const int32_t obstacleType) { 95 | if(obstacleType < 1000 || obstacleType > 4005000) 96 | return orig; 97 | const int32_t startHeight = (obstacleType >= 4001 && obstacleType <= 4100000) ? (obstacleType - 4001) % 1000 : 0; 98 | return static_cast(static_cast(startHeight) * (20.f / 3)) + 1334; 99 | } 100 | 101 | #include "conversions.hpp" 102 | MAKE_HOOK_MATCH(BeatmapDataLoaderVersion2_6_0AndEarlier_BeatmapDataLoader_GetBeatmapDataFromSaveData, &BeatmapDataLoaderVersion2_6_0AndEarlier::BeatmapDataLoader::GetBeatmapDataFromSaveData, GlobalNamespace::BeatmapData*, BeatmapSaveDataVersion2_6_0AndEarlier::BeatmapSaveData *const beatmapSaveData, BeatmapSaveDataVersion4::LightshowSaveData *const defaultLightshowSaveData, const GlobalNamespace::BeatmapDifficulty beatmapDifficulty, const float startBpm, const bool loadingForDesignatedEnvironment, GlobalNamespace::EnvironmentKeywords *const environmentKeywords, GlobalNamespace::IEnvironmentLightGroups *const environmentLightGroups, GlobalNamespace::PlayerSpecificSettings *const playerSpecificSettings) { 103 | if(!active) 104 | return BeatmapDataLoaderVersion2_6_0AndEarlier_BeatmapDataLoader_GetBeatmapDataFromSaveData(beatmapSaveData, defaultLightshowSaveData, 105 | beatmapDifficulty, startBpm, loadingForDesignatedEnvironment, environmentKeywords, environmentLightGroups, playerSpecificSettings); 106 | System::Collections::Generic::List_1 *const events = beatmapSaveData->get_events(); 107 | const std::vector bpmChanges = ConvertBpmChanges(startBpm, events); 108 | LayerCache noteCache(beatmapSaveData->get_notes(), &bpmChanges); 109 | LayerCache obstacleCache(beatmapSaveData->get_obstacles(), &bpmChanges); 110 | LayerCache sliderCache(beatmapSaveData->get_sliders(), &bpmChanges); 111 | LayerCache waypointCache(beatmapSaveData->get_waypoints(), &bpmChanges); 112 | LayerCache rotationCache(events, &bpmChanges, +[](BeatmapSaveDataVersion2_6_0AndEarlier::EventData *const event) { 113 | const BeatmapSaveDataCommon::BeatmapEventType type = event->get_type(); 114 | return type == BeatmapSaveDataCommon::BeatmapEventType::Event14 || type == BeatmapSaveDataCommon::BeatmapEventType::Event15; 115 | }); 116 | logger.info("Restoring {} notes, {} obstacles, {} sliders, {} waypoints, and {} rotation events", 117 | noteCache.cache.size(), obstacleCache.cache.size(), sliderCache.cache.size(), waypointCache.cache.size(), rotationCache.cache.size()); 118 | SafePtr result = BeatmapDataLoaderVersion2_6_0AndEarlier_BeatmapDataLoader_GetBeatmapDataFromSaveData( 119 | beatmapSaveData, defaultLightshowSaveData, beatmapDifficulty, startBpm, loadingForDesignatedEnvironment, 120 | environmentKeywords, environmentLightGroups, playerSpecificSettings); 121 | for(System::Collections::Generic::LinkedListNode_1 *iter = result->get_allBeatmapDataItems()->head, *const end = iter ? iter->prev : nullptr; iter; iter = iter->next) { 122 | GlobalNamespace::BeatmapDataItem *const item = iter->item; 123 | if(GlobalNamespace::NoteData *const data = il2cpp_utils::try_cast(item).value_or(nullptr)) { 124 | if(const auto value = noteCache.restore(data)) { 125 | data->noteLineLayer = value->lineLayer.value__; 126 | data->cutDirection = value->cutDirection.value__; 127 | } 128 | } else if(GlobalNamespace::ObstacleData *const obstacleData = il2cpp_utils::try_cast(item).value_or(nullptr)) { 129 | if(const std::optional type = obstacleCache.restore(obstacleData)) { 130 | obstacleData->lineLayer = GetLayerForObstacleType(obstacleData->lineLayer, type->value__); 131 | obstacleData->height = GetHeightForObstacleType(obstacleData->height, type->value__); 132 | } 133 | } else if(GlobalNamespace::SliderData *const sliderData = il2cpp_utils::try_cast(item).value_or(nullptr)) { 134 | if(const auto layers = sliderCache.restore(sliderData)) { 135 | sliderData->headLineLayer = layers->headLayer.value__; 136 | sliderData->headBeforeJumpLineLayer = layers->headLayer.value__; 137 | sliderData->tailLineLayer = layers->tailLayer.value__; 138 | sliderData->tailBeforeJumpLineLayer = layers->tailLayer.value__; 139 | } 140 | } else if(GlobalNamespace::WaypointData *const waypointData = il2cpp_utils::try_cast(item).value_or(nullptr)) { 141 | if(const std::optional lineLayer = waypointCache.restore(waypointData)) 142 | waypointData->lineLayer = lineLayer->value__; 143 | } else if(GlobalNamespace::SpawnRotationBeatmapEventData *const rotationData = il2cpp_utils::try_cast(item).value_or(nullptr)) { 144 | if(const std::optional rotation = rotationCache.restore(rotationData)) 145 | rotationData->_deltaRotation = SpawnRotationForEventValue(rotationData->_deltaRotation, *rotation); 146 | } 147 | if(iter == end) 148 | break; 149 | } 150 | if(noteCache.failCount || obstacleCache.failCount || sliderCache.failCount || waypointCache.failCount) 151 | logger.warn("Failed to restore {} notes, {} obstacles, {} sliders, {} waypoints, and {} rotation events", 152 | noteCache.failCount, obstacleCache.failCount, sliderCache.failCount, waypointCache.failCount, rotationCache.failCount); 153 | return result.ptr(); 154 | } 155 | 156 | MAKE_HOOK_MATCH(BeatmapDataLoaderVersion3_BeatmapDataLoader_GetBeatmapDataFromSaveData, &BeatmapDataLoaderVersion3::BeatmapDataLoader::GetBeatmapDataFromSaveData, GlobalNamespace::BeatmapData*, BeatmapSaveDataVersion3::BeatmapSaveData *const beatmapSaveData, BeatmapSaveDataVersion4::LightshowSaveData *const defaultLightshowSaveData, GlobalNamespace::BeatmapDifficulty beatmapDifficulty, const float startBpm, bool loadingForDesignatedEnvironment, GlobalNamespace::EnvironmentKeywords *const environmentKeywords, GlobalNamespace::IEnvironmentLightGroups *const environmentLightGroups, GlobalNamespace::PlayerSpecificSettings *const playerSpecificSettings, System::Diagnostics::Stopwatch *const stopwatch) { 157 | if(!active) 158 | return BeatmapDataLoaderVersion3_BeatmapDataLoader_GetBeatmapDataFromSaveData(beatmapSaveData, defaultLightshowSaveData, beatmapDifficulty, 159 | startBpm, loadingForDesignatedEnvironment, environmentKeywords, environmentLightGroups, playerSpecificSettings, stopwatch); 160 | const std::vector bpmChanges = ConvertBpmChanges(startBpm, beatmapSaveData->bpmEvents); 161 | LayerCache bombCache(beatmapSaveData->__cordl_internal_get_bombNotes(), &bpmChanges); 162 | LayerCache noteCache(beatmapSaveData->__cordl_internal_get_colorNotes(), &bpmChanges); 163 | LayerCache obstacleCache(beatmapSaveData->__cordl_internal_get_obstacles(), &bpmChanges); 164 | LayerCache burstCache(beatmapSaveData->__cordl_internal_get_burstSliders(), &bpmChanges); 165 | LayerCache sliderCache(beatmapSaveData->__cordl_internal_get_sliders(), &bpmChanges); 166 | LayerCache waypointCache(beatmapSaveData->__cordl_internal_get_waypoints(), &bpmChanges); 167 | logger.info("Restoring {} notes, {} bombs, {} obstacles, {} sliders, {} burst sliders, and {} waypoints", 168 | noteCache.cache.size(), bombCache.cache.size(), obstacleCache.cache.size(), sliderCache.cache.size(), burstCache.cache.size(), waypointCache.cache.size()); 169 | SafePtr result = BeatmapDataLoaderVersion3_BeatmapDataLoader_GetBeatmapDataFromSaveData( 170 | beatmapSaveData, defaultLightshowSaveData, beatmapDifficulty, startBpm, loadingForDesignatedEnvironment, 171 | environmentKeywords, environmentLightGroups, playerSpecificSettings, stopwatch); 172 | for(System::Collections::Generic::LinkedListNode_1 *iter = result->get_allBeatmapDataItems()->head, *const end = iter ? iter->prev : nullptr; iter; iter = iter->next) { 173 | GlobalNamespace::BeatmapDataItem *const item = iter->item; 174 | if(GlobalNamespace::NoteData *const data = il2cpp_utils::try_cast(item).value_or(nullptr); data) { 175 | if(data->gameplayType == GlobalNamespace::NoteData::GameplayType::Bomb) { 176 | if(const std::optional lineLayer = bombCache.restore(data)) 177 | data->noteLineLayer = *lineLayer; 178 | } else if(const auto value = noteCache.restore(data)) { 179 | data->noteLineLayer = value->layer.value__; 180 | data->cutDirection = value->cutDirection.value__; 181 | } 182 | } else if(GlobalNamespace::ObstacleData *const obstacleData = il2cpp_utils::try_cast(item).value_or(nullptr)) { 183 | if(const std::optional lineLayer = obstacleCache.restore(obstacleData)) 184 | obstacleData->lineLayer = *lineLayer; 185 | } else if(GlobalNamespace::SliderData *const sliderData = il2cpp_utils::try_cast(item).value_or(nullptr)) { 186 | if(const auto layers = (sliderData->sliderType == GlobalNamespace::SliderData::Type::Burst) ? burstCache.restore(sliderData) : sliderCache.restore(sliderData)) { 187 | sliderData->headLineLayer = layers->headLayer.value__; 188 | sliderData->headBeforeJumpLineLayer = layers->headLayer.value__; 189 | sliderData->tailLineLayer = layers->tailLayer.value__; 190 | sliderData->tailBeforeJumpLineLayer = layers->tailLayer.value__; 191 | } 192 | } else if(GlobalNamespace::WaypointData *const waypointData = il2cpp_utils::try_cast(item).value_or(nullptr)) { 193 | if(const std::optional lineLayer = waypointCache.restore(waypointData)) 194 | waypointData->lineLayer = lineLayer->value__; 195 | } 196 | if(iter == end) 197 | break; 198 | } 199 | if(noteCache.failCount || bombCache.failCount || obstacleCache.failCount || sliderCache.failCount || burstCache.failCount || waypointCache.failCount) 200 | logger.warn("Failed to restore {} notes, {} bombs, {} obstacles, {} sliders, {} burst sliders, and {} waypoints", 201 | noteCache.failCount, bombCache.failCount, obstacleCache.failCount, sliderCache.failCount, burstCache.failCount, waypointCache.failCount); 202 | return result.ptr(); 203 | } 204 | 205 | MAKE_HOOK_MATCH(BeatmapObjectsInTimeRowProcessor_HandleCurrentTimeSliceAllNotesAndSlidersDidFinishTimeSlice, &GlobalNamespace::BeatmapObjectsInTimeRowProcessor::HandleCurrentTimeSliceAllNotesAndSlidersDidFinishTimeSlice, void, GlobalNamespace::BeatmapObjectsInTimeRowProcessor *const self, GlobalNamespace::BeatmapObjectsInTimeRowProcessor::TimeSliceContainer_1* allObjectsTimeSlice, float nextTimeSliceTime) { 206 | BeatmapObjectsInTimeRowProcessor_HandleCurrentTimeSliceAllNotesAndSlidersDidFinishTimeSlice(self, allObjectsTimeSlice, nextTimeSliceTime); 207 | if(!active) 208 | return; 209 | System::Collections::Generic::IReadOnlyList_1 *const items = allObjectsTimeSlice->get_items(); 210 | const uint32_t itemCount = static_cast( 211 | reinterpret_cast*>(items)->get_Count()); 212 | 213 | std::unordered_map> notesInColumnsProcessingDictionaryOfLists; 214 | for(uint32_t i = 0; i < itemCount; ++i) { 215 | GlobalNamespace::NoteData *const note = il2cpp_utils::try_cast(items->get_Item(static_cast(i))).value_or(nullptr); 216 | if(!note) 217 | continue; 218 | std::vector *const list = ¬esInColumnsProcessingDictionaryOfLists.try_emplace(note->lineIndex).first->second; 219 | GlobalNamespace::NoteLineLayer lineLayer = note->noteLineLayer; 220 | std::vector::const_iterator pos = std::find_if(list->begin(), list->end(), [lineLayer](GlobalNamespace::NoteData *const e) { 221 | return e->noteLineLayer > lineLayer; 222 | }); 223 | list->insert(pos, note); 224 | } 225 | for(std::pair> &list : notesInColumnsProcessingDictionaryOfLists) 226 | for(uint32_t i = 0; i < static_cast(list.second.size()); ++i) 227 | list.second[i]->SetBeforeJumpNoteLineLayer(static_cast(i)); 228 | for(uint32_t i = 0; i < itemCount; ++i) { 229 | GlobalNamespace::SliderData *const slider = il2cpp_utils::try_cast(items->get_Item(static_cast(i))).value_or(nullptr); 230 | if(!slider) 231 | continue; 232 | for(uint32_t j = 0; j < itemCount; ++j) { 233 | GlobalNamespace::NoteData *const note = il2cpp_utils::try_cast(items->get_Item(static_cast(j))).value_or(nullptr); 234 | if(!note) 235 | continue; 236 | if(!GlobalNamespace::BeatmapObjectsInTimeRowProcessor::SliderHeadPositionOverlapsWithNote(slider, note)) 237 | continue; 238 | slider->SetHeadBeforeJumpLineLayer(note->beforeJumpNoteLineLayer); 239 | } 240 | } 241 | for(uint32_t i = 0; i < itemCount; ++i) { 242 | GlobalNamespace::SliderData *const slider = il2cpp_utils::try_cast(items->get_Item(static_cast(i))).value_or(nullptr); 243 | if(!slider) 244 | continue; 245 | for(uint32_t j = 0; j < itemCount; ++j) { 246 | GlobalNamespace::SliderData *const otherSlider = il2cpp_utils::try_cast(items->get_Item(static_cast(j))).value_or(nullptr); 247 | if(!otherSlider) 248 | continue; 249 | if(slider != otherSlider && GlobalNamespace::BeatmapObjectsInTimeRowProcessor::SliderHeadPositionOverlapsWithBurstTail(slider, otherSlider)) 250 | slider->SetHeadBeforeJumpLineLayer(otherSlider->tailBeforeJumpLineLayer); 251 | } 252 | for(uint32_t j = 0; j < itemCount; ++j) { 253 | GlobalNamespace::BeatmapObjectsInTimeRowProcessor::SliderTailData *const tailData = il2cpp_utils::try_cast(items->get_Item(static_cast(j))).value_or(nullptr); 254 | if(!tailData) 255 | continue; 256 | if(GlobalNamespace::BeatmapObjectsInTimeRowProcessor::SliderHeadPositionOverlapsWithBurstTail(slider, tailData->slider)) 257 | slider->SetHeadBeforeJumpLineLayer(tailData->slider->tailBeforeJumpLineLayer); 258 | } 259 | } 260 | for(uint32_t i = 0; i < itemCount; ++i) { 261 | GlobalNamespace::BeatmapObjectsInTimeRowProcessor::SliderTailData *const tailData = il2cpp_utils::try_cast(items->get_Item(static_cast(i))).value_or(nullptr); 262 | if(!tailData) 263 | continue; 264 | GlobalNamespace::SliderData *const slider = tailData->slider; 265 | for(uint32_t j = 0; j < itemCount; ++j) { 266 | GlobalNamespace::NoteData *const note = il2cpp_utils::try_cast(items->get_Item(static_cast(j))).value_or(nullptr); 267 | if(!note) 268 | continue; 269 | if(GlobalNamespace::BeatmapObjectsInTimeRowProcessor::SliderTailPositionOverlapsWithNote(slider, note)) 270 | slider->SetTailBeforeJumpLineLayer(note->beforeJumpNoteLineLayer); 271 | } 272 | } 273 | } 274 | 275 | MAKE_HOOK_MATCH(BeatmapObjectSpawnMovementData_GetNoteOffset, &GlobalNamespace::BeatmapObjectSpawnMovementData::GetNoteOffset, UnityEngine::Vector3, GlobalNamespace::BeatmapObjectSpawnMovementData *const self, int32_t noteLineIndex, const GlobalNamespace::NoteLineLayer noteLineLayer) { 276 | UnityEngine::Vector3 result = BeatmapObjectSpawnMovementData_GetNoteOffset(self, noteLineIndex, noteLineLayer); 277 | if(!active) 278 | return result; 279 | if(noteLineIndex <= -1000) 280 | noteLineIndex += 2000; 281 | else if(noteLineIndex < 1000) 282 | return result; 283 | return Sombrero::FastVector3(self->_rightVec) * (static_cast(-self->noteLinesCount + 1) * .5f + 284 | static_cast(noteLineIndex) * (GlobalNamespace::StaticBeatmapObjectSpawnMovementData::kNoteLinesDistance / 1000.f)) + 285 | Sombrero::FastVector3(0, GlobalNamespace::StaticBeatmapObjectSpawnMovementData::LineYPosForLineLayer(noteLineLayer), 0); 286 | } 287 | 288 | MAKE_HOOK_MATCH(StaticBeatmapObjectSpawnMovementData_Get2DNoteOffset, &GlobalNamespace::StaticBeatmapObjectSpawnMovementData::Get2DNoteOffset, UnityEngine::Vector2, int32_t noteLineIndex, int32_t noteLinesCount, GlobalNamespace::NoteLineLayer noteLineLayer) { 289 | UnityEngine::Vector2 result = StaticBeatmapObjectSpawnMovementData_Get2DNoteOffset(noteLineIndex, noteLinesCount, noteLineLayer); 290 | if(!active) 291 | return result; 292 | if(noteLineIndex <= -1000) 293 | noteLineIndex += 2000; 294 | else if(noteLineIndex < 1000) 295 | return result; 296 | return UnityEngine::Vector2( 297 | static_cast(-noteLinesCount + 1) * .5f + static_cast(noteLineIndex) * 298 | (GlobalNamespace::StaticBeatmapObjectSpawnMovementData::kNoteLinesDistance / 1000.f), 299 | GlobalNamespace::StaticBeatmapObjectSpawnMovementData::LineYPosForLineLayer(noteLineLayer)); 300 | } 301 | 302 | MAKE_HOOK_MATCH(BeatmapObjectSpawnMovementData_GetObstacleOffset, &GlobalNamespace::BeatmapObjectSpawnMovementData::GetObstacleOffset, UnityEngine::Vector3, GlobalNamespace::BeatmapObjectSpawnMovementData *const self, int32_t noteLineIndex, GlobalNamespace::NoteLineLayer noteLineLayer) { 303 | UnityEngine::Vector3 result = BeatmapObjectSpawnMovementData_GetObstacleOffset(self, noteLineIndex, noteLineLayer); 304 | if(!active) 305 | return result; 306 | if(noteLineIndex <= -1000) 307 | noteLineIndex += 2000; 308 | else if(noteLineIndex < 1000) 309 | return result; 310 | return Sombrero::FastVector3(self->_rightVec) * (static_cast(-self->noteLinesCount + 1) * .5f + 311 | static_cast(noteLineIndex) * (GlobalNamespace::StaticBeatmapObjectSpawnMovementData::kNoteLinesDistance / 1000.f)) + 312 | Sombrero::FastVector3(0, GlobalNamespace::StaticBeatmapObjectSpawnMovementData::LineYPosForLineLayer(noteLineLayer) + 313 | GlobalNamespace::StaticBeatmapObjectSpawnMovementData::kObstacleVerticalOffset, 0); 314 | } 315 | 316 | MAKE_HOOK_MATCH(BeatmapObjectSpawnMovementData_HighestJumpPosYForLineLayer, &GlobalNamespace::BeatmapObjectSpawnMovementData::HighestJumpPosYForLineLayer, float, GlobalNamespace::BeatmapObjectSpawnMovementData *const self, GlobalNamespace::NoteLineLayer lineLayer) { 317 | float result = BeatmapObjectSpawnMovementData_HighestJumpPosYForLineLayer(self, lineLayer); 318 | if(!active) 319 | return result; 320 | const float delta = (self->_topLinesHighestJumpPosY - self->_upperLinesHighestJumpPosY); 321 | if(lineLayer.value__ >= 1000 || lineLayer.value__ <= -1000) 322 | return self->_upperLinesHighestJumpPosY - delta - delta + self->_jumpOffsetYProvider->get_jumpOffsetY() + 323 | static_cast(lineLayer.value__) * (delta / 1000.f); 324 | if(static_cast(lineLayer.value__) > 2u) 325 | return self->_upperLinesHighestJumpPosY - delta + self->_jumpOffsetYProvider->get_jumpOffsetY() + (static_cast(lineLayer.value__) * delta); 326 | return result; 327 | } 328 | 329 | MAKE_HOOK_MATCH(NoteBasicCutInfoHelper_GetBasicCutInfo, &GlobalNamespace::NoteBasicCutInfoHelper::GetBasicCutInfo, void, UnityEngine::Transform *const noteTransform, GlobalNamespace::ColorType colorType, GlobalNamespace::NoteCutDirection cutDirection, GlobalNamespace::SaberType saberType, float saberBladeSpeed, UnityEngine::Vector3 cutDirVec, float cutAngleTolerance, ByRef directionOK, ByRef speedOK, ByRef saberTypeOK, ByRef cutDirDeviation, ByRef cutDirAngle) { 330 | if(active && cutDirection.value__ >= 2000 && cutDirection.value__ <= 2360) 331 | cutDirection = GlobalNamespace::NoteCutDirection::Any; 332 | NoteBasicCutInfoHelper_GetBasicCutInfo(noteTransform, colorType, cutDirection, saberType, saberBladeSpeed, cutDirVec, cutAngleTolerance, directionOK, speedOK, saberTypeOK, cutDirDeviation, cutDirAngle); 333 | } 334 | 335 | MAKE_HOOK_MATCH(NoteCutDirectionExtensions_Rotation, &GlobalNamespace::NoteCutDirectionExtensions::Rotation, UnityEngine::Quaternion, GlobalNamespace::NoteCutDirection cutDirection, float offset) { 336 | UnityEngine::Quaternion result = NoteCutDirectionExtensions_Rotation(cutDirection, offset); 337 | if(!active) 338 | return result; 339 | if(cutDirection.value__ >= 1000 && cutDirection.value__ <= 1360) { 340 | result = UnityEngine::Quaternion(); 341 | result.set_eulerAngles(UnityEngine::Vector3(0, 0, static_cast(1000 - cutDirection.value__))); 342 | } else if(cutDirection.value__ >= 2000 && cutDirection.value__ <= 2360) { 343 | result = UnityEngine::Quaternion(); 344 | result.set_eulerAngles(UnityEngine::Vector3(0, 0, static_cast(2000 - cutDirection.value__))); 345 | } 346 | return result; 347 | } 348 | 349 | MAKE_HOOK_MATCH(NoteCutDirectionExtensions_Direction, &GlobalNamespace::NoteCutDirectionExtensions::Direction, UnityEngine::Vector2, GlobalNamespace::NoteCutDirection cutDirection) { 350 | UnityEngine::Vector2 result = NoteCutDirectionExtensions_Direction(cutDirection); 351 | if(!active) 352 | return result; 353 | int32_t offset = 2000; 354 | if(cutDirection.value__ >= 1000 && cutDirection.value__ <= 1360) 355 | offset = 1000; 356 | else if(cutDirection.value__ < 2000 || cutDirection.value__ > 2360) 357 | return result; 358 | Sombrero::FastQuaternion quaternion = Sombrero::FastQuaternion(); 359 | quaternion.set_eulerAngles(UnityEngine::Vector3(0, 0, static_cast(offset - cutDirection.value__))); 360 | Sombrero::FastVector3 dir = quaternion * Sombrero::FastVector3::down(); 361 | return UnityEngine::Vector2(dir.x, dir.y); 362 | } 363 | 364 | MAKE_HOOK_MATCH(NoteCutDirectionExtensions_RotationAngle, &GlobalNamespace::NoteCutDirectionExtensions::RotationAngle, float, GlobalNamespace::NoteCutDirection cutDirection) { 365 | const float result = NoteCutDirectionExtensions_RotationAngle(cutDirection); 366 | if(!active) 367 | return result; 368 | if(cutDirection.value__ >= 1000 && cutDirection.value__ <= 1360) 369 | return static_cast(1000 - cutDirection.value__); 370 | if(cutDirection.value__ >= 2000 && cutDirection.value__ <= 2360) 371 | return static_cast(2000 - cutDirection.value__); 372 | return result; 373 | } 374 | 375 | MAKE_HOOK_MATCH(NoteCutDirectionExtensions_Mirrored, &GlobalNamespace::NoteCutDirectionExtensions::Mirrored, GlobalNamespace::NoteCutDirection, GlobalNamespace::NoteCutDirection cutDirection) { 376 | GlobalNamespace::NoteCutDirection result = NoteCutDirectionExtensions_Mirrored(cutDirection); 377 | if(!active) 378 | return result; 379 | if(cutDirection.value__ >= 1000 && cutDirection.value__ <= 1360) 380 | return 2360 - cutDirection.value__; 381 | if(cutDirection.value__ >= 2000 && cutDirection.value__ <= 2360) 382 | return 4360 - cutDirection.value__; 383 | return result; 384 | } 385 | 386 | static std::optional MirrorPrecisionLineIndex(const int32_t lineIndex) { 387 | if(lineIndex >= 1000 || lineIndex <= -1000) 388 | return ((lineIndex > -1000 && lineIndex < 4000) ? 5000 : 3000) - lineIndex; 389 | if(static_cast(lineIndex) > 3u) 390 | return 3 - lineIndex; 391 | return std::nullopt; 392 | } 393 | 394 | MAKE_HOOK_MATCH(NoteData_Mirror, &GlobalNamespace::NoteData::Mirror, void, GlobalNamespace::NoteData *const self, int32_t lineCount) { 395 | const int32_t lineIndex = self->lineIndex, flipLineIndex = self->flipLineIndex; 396 | NoteData_Mirror(self, lineCount); 397 | if(!active) 398 | return; 399 | if(const std::optional newLineIndex = MirrorPrecisionLineIndex(lineIndex)) 400 | self->set_lineIndex(*newLineIndex); 401 | if(const std::optional newFlipLineIndex = MirrorPrecisionLineIndex(flipLineIndex)) 402 | self->set_flipLineIndex(*newFlipLineIndex); 403 | } 404 | 405 | MAKE_HOOK_MATCH(ObstacleController_Init, &GlobalNamespace::ObstacleController::Init, void, GlobalNamespace::ObstacleController *const self, GlobalNamespace::ObstacleData *const obstacleData, float worldRotation, UnityEngine::Vector3 startPos, UnityEngine::Vector3 midPos, UnityEngine::Vector3 endPos, float move1Duration, float move2Duration, float singleLineWidth, float height) { 406 | if(!active) 407 | return ObstacleController_Init(self, obstacleData, worldRotation, startPos, midPos, endPos, move1Duration, move2Duration, singleLineWidth, height); 408 | if(obstacleData->height <= -1000) 409 | height = static_cast(obstacleData->height + 2000) / 1000.f * GlobalNamespace::StaticBeatmapObjectSpawnMovementData::kNoteLinesDistance; 410 | else if(obstacleData->height >= 1000) 411 | height = static_cast(obstacleData->height - 1000) / 1000.f * GlobalNamespace::StaticBeatmapObjectSpawnMovementData::kNoteLinesDistance; 412 | else if(obstacleData->height > 2) 413 | height = static_cast(obstacleData->height) * GlobalNamespace::StaticBeatmapObjectSpawnMovementData::kNoteLinesDistance; 414 | 415 | int32_t oldWidth = obstacleData->width; 416 | if(oldWidth <= -1000) 417 | obstacleData->width = oldWidth + 1000; 418 | else if(oldWidth >= 1000) 419 | obstacleData->width = oldWidth - 1000; 420 | else 421 | return ObstacleController_Init(self, obstacleData, worldRotation, startPos, midPos, endPos, move1Duration, move2Duration, singleLineWidth, height); 422 | float fix = singleLineWidth * (-999.f / 1000) * .5f; 423 | midPos.x += fix; 424 | endPos.x += fix; 425 | ObstacleController_Init(self, obstacleData, worldRotation, startPos, midPos, endPos, move1Duration, move2Duration, singleLineWidth / 1000, height); 426 | self->_startPos.x += fix; 427 | obstacleData->width = oldWidth; 428 | } 429 | 430 | static int32_t ToNormalizedPrecisionIndex(int32_t index) { 431 | if(index <= -1000) 432 | return index + 1000; 433 | if(index >= 1000) 434 | return index - 1000; 435 | return index * 1000; 436 | } 437 | 438 | MAKE_HOOK_MATCH(ObstacleData_Mirror, &GlobalNamespace::ObstacleData::Mirror, void, GlobalNamespace::ObstacleData *const self, int32_t lineCount) { 439 | int32_t lineIndex = self->lineIndex; 440 | ObstacleData_Mirror(self, lineCount); 441 | if(!active) 442 | return; 443 | if(lineIndex >= 1000 || lineIndex <= -1000 || self->width >= 1000 || self->width <= -1000) { 444 | const int32_t newIndex = 4000 - ToNormalizedPrecisionIndex(lineIndex) - ToNormalizedPrecisionIndex(self->width); 445 | self->lineIndex = (newIndex < 0) ? newIndex - 1000 : newIndex + 1000; 446 | } else if(static_cast(lineIndex) > 3u) { 447 | self->lineIndex = 4 - lineIndex - self->width; 448 | } 449 | } 450 | 451 | MAKE_HOOK_MATCH(SliderData_Mirror, &GlobalNamespace::SliderData::Mirror, void, GlobalNamespace::SliderData *const self, int32_t lineCount) { 452 | const int32_t headLineIndex = self->headLineIndex, tailLineIndex = self->tailLineIndex; 453 | SliderData_Mirror(self, lineCount); 454 | if(!active) 455 | return; 456 | if(const std::optional newHeadLineIndex = MirrorPrecisionLineIndex(headLineIndex)) 457 | self->headLineIndex = *newHeadLineIndex; 458 | if(const std::optional newTailLineIndex = MirrorPrecisionLineIndex(tailLineIndex)) 459 | self->tailLineIndex = *newTailLineIndex; 460 | } 461 | 462 | MAKE_HOOK_MATCH(SliderMeshController_CutDirectionToControlPointPosition, &GlobalNamespace::SliderMeshController::CutDirectionToControlPointPosition, UnityEngine::Vector3, GlobalNamespace::NoteCutDirection noteCutDirection) { 463 | UnityEngine::Vector3 result = SliderMeshController_CutDirectionToControlPointPosition(noteCutDirection); 464 | if(!active) 465 | return result; 466 | if(noteCutDirection.value__ >= 1000 && noteCutDirection.value__ <= 1360) { 467 | Sombrero::FastQuaternion quaternion = Sombrero::FastQuaternion(); 468 | quaternion.set_eulerAngles(UnityEngine::Vector3(0, 0, static_cast(1000 - noteCutDirection.value__))); 469 | return quaternion * Sombrero::FastVector3::down(); 470 | } 471 | if(noteCutDirection.value__ >= 2000 && noteCutDirection.value__ <= 2360) { 472 | Sombrero::FastQuaternion quaternion = Sombrero::FastQuaternion(); 473 | quaternion.set_eulerAngles(UnityEngine::Vector3(0, 0, static_cast(2000 - noteCutDirection.value__))); 474 | return quaternion * Sombrero::FastVector3::down(); 475 | } 476 | return result; 477 | } 478 | 479 | MAKE_HOOK_MATCH(StaticBeatmapObjectSpawnMovementData_LineYPosForLineLayer, &GlobalNamespace::StaticBeatmapObjectSpawnMovementData::LineYPosForLineLayer, float, GlobalNamespace::NoteLineLayer lineLayer) { 480 | float result = StaticBeatmapObjectSpawnMovementData_LineYPosForLineLayer(lineLayer); 481 | if(!active) 482 | return result; 483 | constexpr float delta = GlobalNamespace::StaticBeatmapObjectSpawnMovementData::kTopLinesYPos - GlobalNamespace::StaticBeatmapObjectSpawnMovementData::kUpperLinesYPos; 484 | if(lineLayer.value__ >= 1000 || lineLayer.value__ <= -1000) 485 | return GlobalNamespace::StaticBeatmapObjectSpawnMovementData::kUpperLinesYPos - delta * 2 + static_cast(lineLayer.value__) * (delta / 1000.f); 486 | if(static_cast(lineLayer.value__) > 2u) 487 | return GlobalNamespace::StaticBeatmapObjectSpawnMovementData::kUpperLinesYPos - delta + static_cast(lineLayer.value__) * delta; 488 | return result; 489 | } 490 | 491 | extern "C" void setup(CModInfo*); 492 | extern "C" [[gnu::visibility("default")]] void setup(CModInfo *const modInfo) { 493 | *modInfo = { 494 | .id = "MappingExtensions", 495 | .version = "0.24.1", 496 | .version_long = 15, 497 | }; 498 | logger.info("Leaving setup!"); 499 | } 500 | 501 | extern "C" void late_load(); 502 | extern "C" [[gnu::visibility("default")]] void late_load() { 503 | il2cpp_functions::Init(); 504 | 505 | INSTALL_HOOK(logger, GameplayCoreSceneSetupData_LoadTransformedBeatmapDataAsync) 506 | INSTALL_HOOK(logger, GameplayCoreSceneSetupData_LoadTransformedBeatmapData) 507 | if(const std::vector loaded = modloader::get_loaded(); std::find_if(loaded.begin(), loaded.end(), [](const modloader::ModData &data) { 508 | return data.info.id == "CustomJSONData"; 509 | }) == loaded.end()) { 510 | INSTALL_HOOK(logger, BeatmapDataLoaderVersion2_6_0AndEarlier_BeatmapDataLoader_GetBeatmapDataFromSaveData) 511 | INSTALL_HOOK(logger, BeatmapDataLoaderVersion3_BeatmapDataLoader_GetBeatmapDataFromSaveData) 512 | // INSTALL_HOOK(logger, BeatmapDataLoaderVersion4_BeatmapDataLoader_GetBeatmapDataFromSaveData) // TODO: implement 513 | } 514 | 515 | INSTALL_HOOK(logger, BeatmapObjectsInTimeRowProcessor_HandleCurrentTimeSliceAllNotesAndSlidersDidFinishTimeSlice) 516 | INSTALL_HOOK(logger, BeatmapObjectSpawnMovementData_GetNoteOffset) 517 | INSTALL_HOOK(logger, StaticBeatmapObjectSpawnMovementData_Get2DNoteOffset) 518 | INSTALL_HOOK(logger, BeatmapObjectSpawnMovementData_GetObstacleOffset) 519 | INSTALL_HOOK(logger, BeatmapObjectSpawnMovementData_HighestJumpPosYForLineLayer) 520 | // [HarmonyPatch(typeof(ColorNoteVisuals), nameof(ColorNoteVisuals.HandleNoteControllerDidInit))] 521 | INSTALL_HOOK(logger, NoteBasicCutInfoHelper_GetBasicCutInfo) 522 | INSTALL_HOOK(logger, NoteCutDirectionExtensions_Rotation) 523 | INSTALL_HOOK(logger, NoteCutDirectionExtensions_Direction) 524 | INSTALL_HOOK(logger, NoteCutDirectionExtensions_RotationAngle) 525 | INSTALL_HOOK(logger, NoteCutDirectionExtensions_Mirrored) 526 | INSTALL_HOOK(logger, NoteData_Mirror) 527 | INSTALL_HOOK(logger, ObstacleController_Init) 528 | INSTALL_HOOK(logger, ObstacleData_Mirror) 529 | INSTALL_HOOK(logger, SliderData_Mirror) 530 | INSTALL_HOOK(logger, SliderMeshController_CutDirectionToControlPointPosition) 531 | INSTALL_HOOK(logger, StaticBeatmapObjectSpawnMovementData_LineYPosForLineLayer) 532 | 533 | logger.info("Installed ME Hooks successfully!"); 534 | for(const std::string_view name : requirementNames) 535 | SongCore::API::Capabilities::RegisterCapability(name); 536 | } 537 | 538 | #include 539 | #include 540 | #include 541 | #include 542 | #include 543 | #include 544 | #include 545 | --------------------------------------------------------------------------------