├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── .vscode ├── c_cpp_properties.json ├── launch.json └── tasks.json ├── LICENSE ├── RiDyL-Loader.code-workspace ├── build.ps1 ├── libmain ├── .gitignore ├── .vscode │ ├── .gitignore │ ├── settings.json │ └── tasks.json ├── Android.mk ├── Application.mk ├── build.ps1 ├── include │ ├── libmain.hpp │ └── libmain │ │ ├── _config.hpp │ │ └── utils.hpp ├── libmain.code-workspace └── src │ ├── interfaces.cpp │ ├── libmain.cpp │ ├── libmain_internal.hpp │ ├── log.hpp │ ├── main.cpp │ └── tinynew.cpp ├── libmodloader ├── .gitignore ├── .vscode │ ├── .gitignore │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── Android.mk ├── Application.mk ├── build.ps1 ├── include │ └── modloader │ │ ├── _config.hpp │ │ └── mem.hpp ├── libmodloader.code-workspace └── src │ ├── log.hpp │ ├── mem.cpp │ ├── modloader.cpp │ ├── modloader.hpp │ ├── protection.hpp │ └── tinynew.cpp ├── patch.ps1 ├── qpm.json ├── shared └── modloader.hpp └── tools ├── apkmod.ps1 ├── apktool.jar └── jadx-gui-1.0.0.exe /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | with: 11 | submodules: true 12 | lfs: true 13 | token: ${{ secrets.GITHUB_TOKEN }} 14 | - name: Install .NET Core 15 | uses: actions/setup-dotnet@v1.0.0 16 | with: 17 | dotnet-version: '2.2.103' # SDK Version to use. 18 | - name: Install Powershell 19 | run: sudo apt-get install -y powershell 20 | - name: Install Android NDK 21 | env: 22 | ndkname: android-ndk-r20 23 | run: | 24 | wget -q -O ndk.zip https://dl.google.com/android/repository/${ndkname}-linux-x86_64.zip 25 | unzip -q ndk.zip 26 | mv ${ndkname} ndk 27 | cd ndk 28 | pwd > ${GITHUB_WORKSPACE}/ndkpath.txt 29 | - name: Build 30 | run: | 31 | cd ${GITHUB_WORKSPACE} 32 | pwsh -Command ./build.ps1 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /locals 2 | /locals/** 3 | /tools/__* 4 | /tools/__*/** 5 | 6 | /apkpath.txt 7 | /ndkpath.txt 8 | # QPM Dependencies 9 | extern/ 10 | # QPM packages 11 | qpm.lock.json 12 | qpm.shared.json 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "RiDyL"] 2 | path = RiDyL 3 | url = https://github.com/nike4613/RiDyL.git 4 | [submodule "tools/Apkifier"] 5 | path = tools/Apkifier 6 | url = https://github.com/emulamer/Apkifier.git 7 | [submodule "beatsaber-hook"] 8 | path = beatsaber-hook 9 | url = https://github.com/sc2ad/beatsaber-hook 10 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "defines": [ 5 | "_DEBUG", 6 | "UNICODE", 7 | "_UNICODE", 8 | "VERSION=\"1.0.0\"", 9 | "ID=\"modloader\"", 10 | "__aarch64__" 11 | ], 12 | "includePath": [ 13 | "${workspaceFolder}/**", 14 | "c:/android-ndk-r22/**", 15 | "./shared", 16 | "./extern" 17 | ], 18 | "cStandard": "c11", 19 | "cppStandard": "c++20", 20 | "intelliSenseMode": "gcc-x64" 21 | } 22 | ], 23 | "version": 4 24 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "PowerShell Launch Current File", 9 | "type": "PowerShell", 10 | "request": "launch", 11 | "script": "${file}", 12 | "args": [], 13 | "cwd": "${file}" 14 | }, 15 | { 16 | "name": "PowerShell Launch Current File in Temporary Console", 17 | "type": "PowerShell", 18 | "request": "launch", 19 | "script": "${file}", 20 | "args": [], 21 | "cwd": "${file}", 22 | "createTemporaryIntegratedConsole": true 23 | }, 24 | { 25 | "name": "PowerShell Launch Current File w/Args Prompt", 26 | "type": "PowerShell", 27 | "request": "launch", 28 | "script": "${file}", 29 | "args": [ 30 | "${command:SpecifyScriptArgs}" 31 | ], 32 | "cwd": "${file}" 33 | }, 34 | { 35 | "name": "PowerShell Attach to Host Process", 36 | "type": "PowerShell", 37 | "request": "attach" 38 | }, 39 | { 40 | "name": "PowerShell Interactive Session", 41 | "type": "PowerShell", 42 | "request": "launch", 43 | "cwd": "" 44 | }, 45 | { 46 | "name": "PowerShell Attach Interactive Session Runspace", 47 | "type": "PowerShell", 48 | "request": "attach", 49 | "processId": "current" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build All", 8 | "type": "shell", 9 | "command": "powershell ./build.ps1", 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Anairkoen Schno 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. -------------------------------------------------------------------------------- /RiDyL-Loader.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "java.configuration.updateBuildConfiguration": "automatic" 9 | } 10 | } -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | $ridylPath = "$PSScriptRoot/RiDyL" 2 | $libmainPath = "$PSScriptRoot/libmain" 3 | $libmodloaderPath = "$PSScriptRoot/libmodloader" 4 | 5 | if (-not (Test-Path "$libmainPath/ndkpath.txt" -PathType Leaf)) { 6 | Copy-Item "$PSScriptRoot/ndkpath.txt" "$libmainPath/ndkpath.txt" 7 | } 8 | if (-not (Test-Path "$ridylPath/ndkpath.txt" -PathType Leaf)) { 9 | Copy-Item "$PSScriptRoot/ndkpath.txt" "$ridylPath/ndkpath.txt" 10 | } 11 | if (-not (Test-Path "$libmodloaderPath/ndkpath.txt" -PathType Leaf)) { 12 | Copy-Item "$PSScriptRoot/ndkpath.txt" "$libmodloaderPath/ndkpath.txt" 13 | } 14 | 15 | 16 | Set-Location $libmainPath 17 | ."$libmainPath/build.ps1" 18 | Set-Location $PSScriptRoot 19 | 20 | Set-Location $ridylPath 21 | ."$ridylPath/build.ps1" 22 | Set-Location $PSScriptRoot 23 | 24 | Set-Location $libmodloaderPath 25 | ."$libmodloaderPath/build.ps1" 26 | Set-Location $PSScriptRoot 27 | 28 | Rename-Item "$libmodloaderPath/libs/arm64-v8a/libmodloader.so" "$libmodloaderPath/libs/arm64-v8a/libmodloader64.so" -------------------------------------------------------------------------------- /libmain/.gitignore: -------------------------------------------------------------------------------- 1 | /ndkpath.txt 2 | /obj 3 | /obj/** 4 | /libs 5 | /libs/** -------------------------------------------------------------------------------- /libmain/.vscode/.gitignore: -------------------------------------------------------------------------------- 1 | c_cpp_properties.json -------------------------------------------------------------------------------- /libmain/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "C_Cpp.default.cppStandard": "c++20", 3 | "C_Cpp.default.cStandard": "c99", 4 | "C_Cpp.default.includePath": [ 5 | "${workspaceRoot}/include", "${workspaceRoot}/src" 6 | ], 7 | "C_Cpp.intelliSenseEngineFallback": "Enabled" 8 | } -------------------------------------------------------------------------------- /libmain/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build", 8 | "type": "shell", 9 | "command": "powershell ./build.ps1", 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /libmain/Android.mk: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2009 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # 16 | # 17 | LOCAL_PATH := $(call my-dir) 18 | 19 | TARGET_ARCH_ABI := arm64-v8a 20 | 21 | include $(CLEAR_VARS) 22 | 23 | rwildcard=$(wildcard $1$2) $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2)) 24 | 25 | LOCAL_LDLIBS := -llog -ldl 26 | LOCAL_MODULE := main 27 | LOCAL_CPPFLAGS := -std=c++2a -fno-rtti -Os -fvisibility=hidden -fno-exceptions -ffunction-sections -fdata-sections 28 | LOCAL_LDFLAGS := -flto=full -Wl,--gc-sections 29 | 30 | LOCAL_C_INCLUDES := ./include ./src 31 | LOCAL_SRC_FILES := $(call rwildcard,src/,*.cpp) 32 | 33 | include $(BUILD_SHARED_LIBRARY) -------------------------------------------------------------------------------- /libmain/Application.mk: -------------------------------------------------------------------------------- 1 | APP_ABI := arm64-v8a armeabi-v7a 2 | APP_PLATFORM := android-25 3 | APP_PIE := true 4 | APP_STL := c++_static -------------------------------------------------------------------------------- /libmain/build.ps1: -------------------------------------------------------------------------------- 1 | $NDKPath = Get-Content $PSScriptRoot/ndkpath.txt 2 | 3 | $buildScript = "$NDKPath/build/ndk-build" 4 | if (-not ($PSVersionTable.PSEdition -eq "Core")) { 5 | $buildScript += ".cmd" 6 | } 7 | 8 | & $buildScript NDK_PROJECT_PATH=$PSScriptRoot APP_BUILD_SCRIPT=$PSScriptRoot/Android.mk NDK_APPLICATION_MK=$PSScriptRoot/Application.mk -------------------------------------------------------------------------------- /libmain/include/libmain.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "libmain/utils.hpp" 6 | #include "libmain/_config.hpp" 7 | 8 | #define CHECK_MODLOADER_FUNCTION(funcname) \ 9 | static_assert(::std::is_same_v, ::jni::modloader::funcname##_t*>, \ 10 | "modloader_main either has the wrong signature, or does not exist!") 11 | 12 | #define CHECK_MODLOADER_MAIN CHECK_MODLOADER_FUNCTION(main) 13 | #define CHECK_MODLOADER_ACCEPT_UNITY_HANDLE CHECK_MODLOADER_FUNCTION(accept_unity_handle) 14 | #define CHECK_MODLOADER_PRELOAD CHECK_MODLOADER_FUNCTION(preload) 15 | 16 | namespace jni { 17 | 18 | namespace modloader { 19 | // preload_t is called in JNIOnLoad 20 | using preload_t = void() noexcept; 21 | // first, main is called. the interface it returns is given to LibUnity, but only after calling accept_unity_handle. 22 | // if you redirect GetJavaVM, it will be redirected to keep the patched chain alive. 23 | using main_t = JNINativeInterface(JavaVM* vm, JNIEnv* env, std::string_view loadSrc) noexcept; 24 | // this is called *before* calling LibUnity's JNI_OnLoad. 25 | using accept_unity_handle_t = void(void* unityModuleHandle) noexcept; 26 | } 27 | 28 | jboolean load(JNIEnv* env, jobject klass, jstring str) noexcept; 29 | jboolean unload(JNIEnv* env, jobject klass) noexcept; 30 | 31 | template 32 | using function = R(A...); 33 | 34 | // both of these use the first reserved slot to hold the original; you should use the second for other data 35 | namespace interface { 36 | JNIEnv* LIBMAIN_EXPORT get_patched_env(JNIEnv*) noexcept; 37 | 38 | template 39 | Interface LIBMAIN_EXPORT make_passthrough_interface(Interface const* const* i) noexcept; 40 | 41 | template struct interface_store_members; 42 | template<> 43 | struct interface_store_members { 44 | static constexpr void* JNINativeInterface::* original_member = &JNINativeInterface::reserved0; 45 | static constexpr void* JNINativeInterface::* user_member = &JNINativeInterface::reserved1; 46 | static constexpr void* JNINativeInterface::* extra_member = &JNINativeInterface::reserved2; 47 | }; 48 | template<> 49 | struct interface_store_members { 50 | static constexpr void* JNIInvokeInterface::* original_member = &JNIInvokeInterface::reserved0; 51 | static constexpr void* JNIInvokeInterface::* user_member = &JNIInvokeInterface::reserved1; 52 | static constexpr void* JNIInvokeInterface::* extra_member = &JNIInvokeInterface::reserved2; 53 | }; 54 | 55 | template 56 | using remove_ptr_ref_t = std::remove_cv_t>>; 57 | 58 | template 59 | T**& interface_original(T* i) noexcept 60 | { return reinterpret_cast(i->*(interface_store_members>::original_member)); } 61 | template 62 | T**const& interface_original(T const* i) noexcept 63 | { return reinterpret_cast(i->*(interface_store_members>::original_member)); } 64 | template 65 | T*const& interface_original(T const i) noexcept 66 | { return reinterpret_cast(i.functions->*(interface_store_members().functions)>>::original_member)); } 67 | 68 | template 69 | U*& interface_user(T* i) noexcept 70 | { return reinterpret_cast(i->*(interface_store_members>::user_member)); } 71 | template 72 | U*const& interface_user(T const* i) noexcept 73 | { return reinterpret_cast(i->*(interface_store_members>::user_member)); } 74 | template 75 | U*& interface_user(T i) noexcept 76 | { return reinterpret_cast(i.functions->*(interface_store_members>::user_member)); } 77 | 78 | template 79 | U*& interface_extra(T* i) noexcept 80 | { return reinterpret_cast(i->*(interface_store_members>::extra_member)); } 81 | template 82 | U*const& interface_extra(T const* i) noexcept 83 | { return reinterpret_cast(i->*(interface_store_members>::extra_member)); } 84 | template 85 | U*& interface_extra(T i) noexcept 86 | { return reinterpret_cast(i.functions->*(interface_store_members>::extra_member)); } 87 | 88 | template 89 | auto invoke_original(Iface** self, FType* Iface::* member, Args&& ...args) { 90 | auto original = interface_original(*self); 91 | return ((*original)->*member)(original, std::forward(args)...); 92 | } 93 | template 94 | auto invoke_original(IfaceWrap* self, FType* remove_ptr_ref_tfunctions)>::* member, Args&& ...args) { 95 | // using IfacePtr = remove_ptr_ref_tfunctions)>*; 96 | auto original = interface_original(self->functions); 97 | return ((*original)->*member)(reinterpret_cast(original), std::forward(args)...); 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /libmain/include/libmain/_config.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define LIBMAIN_EXPORT __attribute__((visibility("default"))) -------------------------------------------------------------------------------- /libmain/include/libmain/utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace util { 7 | 8 | template 9 | struct type_list { 10 | using identity = type_list<>; 11 | using reverse = type_list<>; 12 | using rest = type_list<>; 13 | template 14 | using append = type_list; 15 | 16 | templatetypename A> 17 | using apply = A<>; 18 | 19 | static constexpr auto size = 0; 20 | }; 21 | template 22 | struct type_list { 23 | using identity = type_list; 24 | 25 | using first = First; 26 | using rest = type_list; 27 | 28 | using reverse = typename rest::reverse::template append; 29 | 30 | using except_last = typename reverse::rest::reverse; 31 | using last = typename reverse::first; 32 | 33 | template 34 | using append = type_list; 35 | 36 | templatetypename A> 37 | using apply = A; 38 | 39 | static constexpr auto size = sizeof...(Rest) + 1; 40 | }; 41 | template 42 | struct type_list { 43 | using identity = type_list; 44 | 45 | using first = T; 46 | using rest = type_list<>; 47 | 48 | using reverse = identity; 49 | 50 | using except_last = rest; 51 | using last = first; 52 | 53 | template 54 | using append = type_list; 55 | 56 | templatetypename A> 57 | using apply = A; 58 | 59 | static constexpr auto size = 1; 60 | }; 61 | 62 | template 63 | using alias_t = T; 64 | 65 | } -------------------------------------------------------------------------------- /libmain/libmain.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "files.associations": { 9 | "__bit_reference": "cpp", 10 | "algorithm": "cpp", 11 | "cctype": "cpp", 12 | "cmath": "cpp", 13 | "cstddef": "cpp", 14 | "cstdio": "cpp", 15 | "cstdlib": "cpp", 16 | "cstring": "cpp", 17 | "new": "cpp", 18 | "typeinfo": "cpp", 19 | "__config": "cpp", 20 | "__debug": "cpp", 21 | "__functional_base": "cpp", 22 | "__hash_table": "cpp", 23 | "__node_handle": "cpp", 24 | "__nullptr": "cpp", 25 | "__split_buffer": "cpp", 26 | "__string": "cpp", 27 | "__tuple": "cpp", 28 | "array": "cpp", 29 | "atomic": "cpp", 30 | "bit": "cpp", 31 | "clocale": "cpp", 32 | "cstdint": "cpp", 33 | "cwchar": "cpp", 34 | "cwctype": "cpp", 35 | "exception": "cpp", 36 | "functional": "cpp", 37 | "initializer_list": "cpp", 38 | "iosfwd": "cpp", 39 | "iterator": "cpp", 40 | "limits": "cpp", 41 | "memory": "cpp", 42 | "optional": "cpp", 43 | "stdexcept": "cpp", 44 | "string_view": "cpp", 45 | "tuple": "cpp", 46 | "type_traits": "cpp", 47 | "unordered_map": "cpp", 48 | "utility": "cpp", 49 | "vector": "cpp", 50 | "version": "cpp", 51 | "map": "cpp", 52 | "__tree": "cpp" 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /libmain/src/interfaces.cpp: -------------------------------------------------------------------------------- 1 | #include "libmain.hpp" 2 | #include "log.hpp" 3 | 4 | using namespace jni::interface; 5 | 6 | // TODO: eliminate namespace name 7 | namespace detail { 8 | template struct func_ptr_helper; 9 | template 10 | struct func_ptr_helper { 11 | using ret = R; 12 | using args = util::type_list; 13 | static constexpr bool is_varargs = false; 14 | }; 15 | template 16 | struct func_ptr_helper { 17 | using ret = R; 18 | using args = util::type_list; 19 | static constexpr bool is_varargs = true; 20 | }; 21 | 22 | template 23 | struct make_passthrough_helper { 24 | template 25 | static auto varargs_form(T* o, Args... args, ...) { // this case cannot be a lambda because Clang cannot compile C varargs lambdas 26 | auto op = *reinterpret_cast(o); // Debug logging is omitted because this vararg is already probably broken 27 | return ((*interface_original(op))->*member)( // TODO: figure out how to actually wrap varargs properly (inline assembly?) 28 | reinterpret_cast(interface_original(op)), args...); 29 | } 30 | template 31 | static auto get_functor() noexcept { 32 | if constexpr (is_varargs) { 33 | return &varargs_form; 34 | } else { 35 | return [](T* o, Args... args) { 36 | if constexpr (debug_print) { 37 | logfp(ANDROID_LOG_DEBUG, "Invoking wrapped JNI function %s for %p", NameProvider{}.name, o); 38 | } 39 | auto op = *reinterpret_cast(o); 40 | return ((*interface_original(op))->*member)( 41 | reinterpret_cast(interface_original(op)), args...); 42 | }; 43 | } 44 | } 45 | }; 46 | } 47 | 48 | template 49 | auto make_passthrough() noexcept -> 50 | util::alias_t< 51 | std::enable_if_t< 52 | std::is_same_v< 53 | typename detail::func_ptr_helper::args::first, 54 | OwnType* 55 | >, 56 | FType 57 | >, 58 | typename detail::func_ptr_helper::ret, 59 | typename detail::func_ptr_helper::args 60 | > 61 | { 62 | return detail::func_ptr_helper::args::rest 63 | ::template apply 64 | ::template get_functor::is_varargs, debug_print, NameProvider>(); 65 | } 66 | 67 | 68 | namespace { 69 | template 70 | using variadic_ptr = R(*)(Args..., ...); 71 | template 72 | using func_ptr = R(*)(Args...); 73 | template 74 | using member_ptr = R(* Own::*)(Args...); 75 | 76 | template 77 | struct make_membr { 78 | template 79 | using ptr = member_ptr; 80 | }; 81 | 82 | templatetypename F, typename R> 83 | struct make_fptr { 84 | template 85 | using ptr = F; 86 | }; 87 | 88 | template 89 | using as_member = T O::*; 90 | } 91 | 92 | #define INTERFACE_DEBUG 93 | 94 | #ifdef INTERFACE_DEBUG 95 | # define PASSTHROUGH_DEBUG_ARGS(member) , true, NameProvider_##member 96 | #else 97 | # define PASSTHROUGH_DEBUG_ARGS(member) 98 | #endif 99 | 100 | #define PASSTHROUGH_(Interface, OwnType, var, member) var.member = make_passthrough, \ 101 | &Interface::member PASSTHROUGH_DEBUG_ARGS(member)>() 102 | 103 | #ifdef INTERFACE_DEBUG 104 | # define PASSTHROUGH(Interface, OwnType, var, member) struct NameProvider_##member { char const* name = #member; }; \ 105 | PASSTHROUGH_(Interface, OwnType, var, member) 106 | #else 107 | # define PASSTHROUGH(Interface, OwnType, var, member) PASSTHROUGH_(Interface, OwnType, var, member) 108 | #endif 109 | 110 | #define E(...) __VA_ARGS__ 111 | 112 | template<> 113 | JNIInvokeInterface LIBMAIN_EXPORT jni::interface::make_passthrough_interface(JNIInvokeInterface const*const* original) noexcept { 114 | JNIInvokeInterface i; 115 | interface_original(&i) = const_cast(original); 116 | 117 | #define M(member) PASSTHROUGH(JNIInvokeInterface, JavaVM, i, member) 118 | 119 | M(GetEnv); M(DestroyJavaVM); 120 | M(AttachCurrentThread); M(AttachCurrentThreadAsDaemon); 121 | M(DetachCurrentThread); 122 | 123 | #undef M 124 | 125 | return i; 126 | } 127 | 128 | 129 | template<> 130 | JNINativeInterface LIBMAIN_EXPORT jni::interface::make_passthrough_interface(JNINativeInterface const*const* original) noexcept { 131 | JNINativeInterface i; 132 | interface_original(&i) = const_cast(original); 133 | 134 | #define M(member) PASSTHROUGH(JNINativeInterface, JNIEnv, i, member) 135 | #define N(name) M(name); M(name##V); M(name##A); 136 | 137 | #define ObjTypesNO(O, pre, post) \ 138 | O(pre ## Boolean ## post); \ 139 | O(pre ## Byte ## post); \ 140 | O(pre ## Char ## post); \ 141 | O(pre ## Short ## post); \ 142 | O(pre ## Int ## post); \ 143 | O(pre ## Long ## post); \ 144 | O(pre ## Float ## post); \ 145 | O(pre ## Double ## post); 146 | #define ObjTypes(O, pre, post) \ 147 | O(pre ## Object ## post); \ 148 | ObjTypesNO(O, pre, post); 149 | #define ObjTypesV(O, pre, post) \ 150 | ObjTypes(O, pre, post); \ 151 | O(pre ## Void ## post); 152 | 153 | M(GetVersion); M(DefineClass); M(FindClass); 154 | M(FromReflectedMethod); M(FromReflectedField); 155 | M(ToReflectedMethod); M(ToReflectedField); 156 | M(GetSuperclass); M(IsAssignableFrom); 157 | M(Throw); M(ThrowNew); M(GetObjectRefType); 158 | M(ExceptionOccurred); M(ExceptionDescribe); 159 | M(ExceptionClear); M(FatalError); 160 | M(PushLocalFrame); M(PopLocalFrame); 161 | M(NewGlobalRef); M(DeleteGlobalRef); M(DeleteLocalRef); 162 | M(IsSameObject); M(NewLocalRef); M(EnsureLocalCapacity); 163 | M(AllocObject); N(NewObject); 164 | M(GetObjectClass); M(IsInstanceOf); M(GetMethodID); 165 | M(GetFieldID); M(GetStaticMethodID); M(GetStaticFieldID); 166 | M(NewString); M(GetStringLength); 167 | M(GetStringChars); M(ReleaseStringChars); 168 | M(NewStringUTF); M(GetStringUTFLength); M(GetStringUTFChars); 169 | M(ReleaseStringUTFChars); M(GetArrayLength); 170 | M(GetObjectArrayElement); M(SetObjectArrayElement); 171 | M(RegisterNatives); M(UnregisterNatives); 172 | M(MonitorEnter); M(MonitorExit); M(GetJavaVM); 173 | M(GetStringRegion); M(GetStringUTFRegion); 174 | M(GetPrimitiveArrayCritical); M(ReleasePrimitiveArrayCritical); 175 | M(GetStringCritical); M(ReleaseStringCritical); 176 | M(NewWeakGlobalRef); M(DeleteWeakGlobalRef); 177 | M(ExceptionCheck); M(NewDirectByteBuffer); 178 | M(GetDirectBufferAddress); M(GetDirectBufferCapacity); 179 | 180 | ObjTypesV(N, Call, Method); 181 | ObjTypesV(N, CallNonvirtual, Method); 182 | ObjTypesV(N, CallStatic, Method); 183 | ObjTypesNO(M, Get, ArrayElements); 184 | ObjTypesNO(M, Release, ArrayElements); 185 | ObjTypesNO(M, Get, ArrayRegion); 186 | ObjTypesNO(M, Set, ArrayRegion); 187 | ObjTypes(M, New, Array); 188 | ObjTypes(M, Get, Field); 189 | ObjTypes(M, Set, Field); 190 | ObjTypes(M, GetStatic, Field); 191 | ObjTypes(M, SetStatic, Field); 192 | 193 | #undef ObjTypesV 194 | #undef ObjTypes 195 | #undef N 196 | #undef M 197 | 198 | return i; 199 | } 200 | -------------------------------------------------------------------------------- /libmain/src/libmain.cpp: -------------------------------------------------------------------------------- 1 | #include "libmain.hpp" 2 | #include "libmain_internal.hpp" 3 | #include "log.hpp" 4 | 5 | /* 6 | This file defines equivalent functionality to stock libmain, with some extra logging. 7 | 8 | All custom functionality should be done elsewhere. 9 | */ 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | using namespace std::literals; 18 | using namespace jni; 19 | 20 | namespace { 21 | static std::map envPtrs = {}; 22 | 23 | static void* libModLoader = nullptr; 24 | static void* libUnityHandle = nullptr; 25 | 26 | // this is needed because libUnity stores the pointer, so this can never be invalidated 27 | static JNINativeInterface libUnityNInterface = {}; 28 | static JNIInvokeInterface libUnityIInterface = {}; 29 | static JavaVM libUnityVm = {&libUnityIInterface}; 30 | static bool usingCustomVm = false; 31 | } 32 | 33 | JNIEnv* jni::interface::get_patched_env(JNIEnv* env) noexcept { 34 | using namespace interface; 35 | if (reinterpret_cast(&interface_extra(env->functions)) == env) 36 | return env; // this is only the case when this is a constructed env 37 | 38 | logfp(ANDROID_LOG_DEBUG, "Looking up patched JNIEnv for 0x%p", env); 39 | 40 | auto eptr = envPtrs.find(env); 41 | if (eptr == envPtrs.end()) { 42 | // the JNIEnv ends up being stored in the extra reserved member. 43 | auto interf = new JNINativeInterface(libUnityNInterface); 44 | interface_original(interf) = reinterpret_cast(env); 45 | interface_extra(interf) = interf; 46 | auto envptr = reinterpret_cast(&interface_extra(interf)); 47 | 48 | logfp(ANDROID_LOG_DEBUG, "Created new patched JNIEnv at 0x%p", envptr); 49 | 50 | eptr = envPtrs.insert({env, envptr}).first; 51 | } else 52 | logfp(ANDROID_LOG_DEBUG, "Found patched JNIEnv at 0x%p", eptr->second); 53 | 54 | return eptr->second; 55 | } 56 | 57 | namespace { 58 | constexpr auto unityso = "libunity.so"sv; 59 | constexpr auto modloaderso = "libmodloader.so"sv; 60 | } 61 | 62 | void jni::modloader::preload() noexcept { 63 | log(ANDROID_LOG_VERBOSE, "Attempting to load libmodloader in jni::modloader::preload()"); 64 | 65 | libModLoader = dlopen(modloaderso.data(), RTLD_LAZY); 66 | if (libModLoader == nullptr) { 67 | logfp(ANDROID_LOG_WARN, "Could not load libmodloader.so: %s", dlerror()); 68 | return; 69 | } 70 | 71 | log(ANDROID_LOG_VERBOSE, "libmodloader loaded"); 72 | 73 | auto pre = reinterpret_cast(dlsym(libModLoader, "modloader_preload")); 74 | if (pre == nullptr) { 75 | logfp(ANDROID_LOG_WARN, "libmodloader does not have modloader_preload: %s", dlerror()); 76 | return; 77 | } 78 | 79 | log(ANDROID_LOG_VERBOSE, "Calling modloader_preload"); 80 | pre(); 81 | 82 | log(ANDROID_LOG_VERBOSE, "Preloading done"); 83 | } 84 | 85 | jboolean jni::load(JNIEnv* env, jobject klass, jstring str) noexcept { 86 | auto const len = env->GetStringUTFLength(str); 87 | 88 | constexpr auto sonameLen = std::max(unityso.length(), modloaderso.length()); 89 | 90 | char soname[len + 1 + sonameLen + 1]; // use stack local to prevent allocation 91 | soname[len] = 0; 92 | 93 | { 94 | auto chars = env->GetStringUTFChars(str, nullptr); 95 | std::copy(chars, chars + len, soname); // instead of using memcpy 96 | env->ReleaseStringUTFChars(str, chars); 97 | } 98 | 99 | if (libUnityHandle != nullptr) 100 | return true; 101 | 102 | logfp(ANDROID_LOG_VERBOSE, "Searching in %s", soname); 103 | 104 | JavaVM* vm = nullptr; 105 | 106 | if (env->GetJavaVM(&vm) < 0) { 107 | env->FatalError("Unable to retrieve Java VM"); // libmain wording 108 | return false; // doesn't actually reach here 109 | } 110 | 111 | auto unityVm = vm; 112 | 113 | log(ANDROID_LOG_VERBOSE, "Got JVM"); 114 | 115 | { // try load libmodloader 116 | soname[len] = '/'; 117 | // because std::copy returns the element after the last copied, which is what we want to be null 118 | auto endptr = std::copy(std::begin(modloaderso), std::end(modloaderso), soname + len + 1); 119 | *endptr = 0; 120 | 121 | if (libModLoader == nullptr) { 122 | logfp(ANDROID_LOG_VERBOSE, "libmodloader not preloaded; Looking for libmodloader at %s", soname); 123 | 124 | libModLoader = dlopen(soname, RTLD_LAZY); 125 | if (libModLoader == nullptr) { 126 | auto err = dlerror(); 127 | logfp(ANDROID_LOG_WARN, "Could not load libmodloader.so from %s: %s", soname, err); 128 | goto loadLibUnity; 129 | } 130 | } 131 | 132 | log(ANDROID_LOG_VERBOSE, "Loaded libmodloader"); 133 | 134 | auto main = reinterpret_cast(dlsym(libModLoader, "modloader_main")); 135 | if (main == nullptr) { 136 | logfp(ANDROID_LOG_WARN, "libmodloader does not have modloader_main: %s", dlerror()); 137 | goto loadLibUnity; 138 | } 139 | 140 | log(ANDROID_LOG_VERBOSE, "Using libmodloader's modloader_main"); 141 | libUnityNInterface = main(vm, env, soname); 142 | 143 | libUnityNInterface.GetJavaVM = [](JNIEnv* env, JavaVM** vmPtr) { 144 | *vmPtr = &libUnityVm; // always return same VM, as there should (i think) only be one 145 | return 0; 146 | }; 147 | 148 | usingCustomVm = true; 149 | unityVm = &libUnityVm; 150 | libUnityIInterface = interface::make_passthrough_interface(&vm->functions); 151 | libUnityIInterface.AttachCurrentThread = [](JavaVM* ptr, JNIEnv** envp, void* aarg) { 152 | using namespace interface; 153 | 154 | JNIEnv* env; 155 | auto ret = invoke_original(ptr, &JNIInvokeInterface::AttachCurrentThread, &env, aarg); 156 | if (ret) return ret; 157 | 158 | log(ANDROID_LOG_DEBUG, "Looking up patched env in AttachCurrentThread"); 159 | *envp = get_patched_env(env); 160 | return ret; 161 | }; 162 | libUnityIInterface.AttachCurrentThreadAsDaemon = [](JavaVM* ptr, JNIEnv** envp, void* aarg) { 163 | using namespace interface; 164 | 165 | JNIEnv* env; 166 | auto ret = invoke_original(ptr, &JNIInvokeInterface::AttachCurrentThreadAsDaemon, &env, aarg); 167 | if (ret) return ret; 168 | 169 | log(ANDROID_LOG_DEBUG, "Looking up patched env in AttachCurrentThread"); 170 | *envp = get_patched_env(env); 171 | return ret; 172 | }; 173 | libUnityIInterface.GetEnv = [](JavaVM* ptr, void** envp, jint ver) { 174 | using namespace interface; 175 | 176 | JNIEnv* env; 177 | auto ret = invoke_original(ptr, &JNIInvokeInterface::GetEnv, reinterpret_cast(&env), ver); 178 | if (ret) return ret; 179 | 180 | log(ANDROID_LOG_DEBUG, "Looking up patched env in AttachCurrentThread"); 181 | *envp = get_patched_env(env); 182 | return ret; 183 | }; 184 | } 185 | 186 | loadLibUnity: 187 | { // try load libunity 188 | soname[len] = '/'; 189 | // because std::copy returns the element after the last copied, which is what we want to be null 190 | auto endptr = std::copy(std::begin(unityso), std::end(unityso), soname + len + 1); 191 | *endptr = 0; 192 | 193 | libUnityHandle = dlopen(soname, RTLD_LAZY); 194 | if (libUnityHandle == nullptr) { 195 | libUnityHandle = dlopen(unityso.data(), RTLD_LAZY); 196 | if (libUnityHandle == nullptr) { 197 | auto err = dlerror(); 198 | logfp(ANDROID_LOG_WARN, "Could not load libunity.so from %s: %s", soname, err); 199 | std::array message = {0}; // this is what the original libmain allocates, so lets hope its enough 200 | // and doesn't use all of the rest of our stack space 201 | snprintf(message.data(), message.size(), "Unable to load library: %s [%s]", soname, err); 202 | env->FatalError(message.data()); 203 | return false; // doesn't actually reach here 204 | } 205 | } 206 | } 207 | 208 | if (libModLoader != nullptr) { 209 | auto acceptUHandle = reinterpret_cast(dlsym(libModLoader, "modloader_accept_unity_handle")); 210 | if (acceptUHandle == nullptr) { 211 | logfp(ANDROID_LOG_INFO, "libmodloader does not have modloader_accept_unity_handle: %s", dlerror()); 212 | } else { 213 | log(ANDROID_LOG_VERBOSE, "Calling libmodloader's modloader_accept_unity_handle"); 214 | 215 | acceptUHandle(libUnityHandle); 216 | } 217 | } 218 | 219 | { 220 | using JNI_OnLoad_t = jint(JavaVM*, void*); 221 | 222 | auto onload = reinterpret_cast(dlsym(libUnityHandle, "JNI_OnLoad")); 223 | if (onload != nullptr) { 224 | if (onload(unityVm, nullptr) > JNI_VERSION_1_6) { 225 | log(ANDROID_LOG_WARN, "libunity JNI_OnLoad requested unsupported VM version"); 226 | env->FatalError("Unsupported VM version"); // libmain wording 227 | return false; // doesn't actually reach here 228 | } 229 | } else { 230 | log(ANDROID_LOG_INFO, "libunity does not have a JNI_OnLoad"); 231 | } 232 | } 233 | 234 | logfp(ANDROID_LOG_INFO, "Successfully loaded and initialized %s", soname); 235 | 236 | return libUnityHandle != nullptr; 237 | } 238 | 239 | jboolean jni::unload(JNIEnv* env, jobject klass) noexcept { 240 | JavaVM* vm = nullptr; 241 | 242 | if (env->GetJavaVM(&vm) < 0) { 243 | env->FatalError("Unable to retrieve Java VM"); // libmain wording 244 | return false; // doesn't actually reach here 245 | } 246 | 247 | using JNI_OnUnload_t = jint(JavaVM*, void*); 248 | 249 | auto onunload = reinterpret_cast(dlsym(libUnityHandle, "JNI_OnUnload")); 250 | if (onunload != nullptr) { 251 | onunload(usingCustomVm ? &libUnityVm : vm, nullptr); 252 | } else { 253 | log(ANDROID_LOG_WARN, "libunity does not have a JNI_OnUnload"); 254 | } 255 | 256 | int code = dlclose(libUnityHandle); 257 | 258 | if (code != 0) { 259 | logfp(ANDROID_LOG_WARN, "Error occurred closing libunity: %s", dlerror()); 260 | } else { 261 | log(ANDROID_LOG_VERBOSE, "Successfully closed libunity"); 262 | } 263 | 264 | if (libModLoader != nullptr) { 265 | code = dlclose(libModLoader); 266 | if (code != 0) { 267 | logfp(ANDROID_LOG_WARN, "Error occurred closing libModLoader: %s", dlerror()); 268 | } else { 269 | log(ANDROID_LOG_VERBOSE, "Successfully closed libModLoader"); 270 | } 271 | } 272 | 273 | return true; 274 | } -------------------------------------------------------------------------------- /libmain/src/libmain_internal.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace jni::modloader { 4 | void preload() noexcept; 5 | } -------------------------------------------------------------------------------- /libmain/src/log.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define log(priority, message) __android_log_print(priority, "libmain - patched", message) 6 | #define logfp(priority, ...) __android_log_print(priority, "libmain - patched", __VA_ARGS__) -------------------------------------------------------------------------------- /libmain/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "log.hpp" 5 | #include "libmain.hpp" 6 | #include "libmain_internal.hpp" 7 | 8 | #include 9 | 10 | constexpr std::array NativeLoader_bindings = {{ 11 | { "load", "(Ljava/lang/String;)Z", (void*)&jni::load }, 12 | { "unload", "()Z", (void*)&jni::unload } 13 | }}; 14 | 15 | extern "C" jint LIBMAIN_EXPORT JNI_OnLoad(JavaVM* vm, void*) { 16 | JNIEnv* env = nullptr; 17 | 18 | log(ANDROID_LOG_INFO, "JNI_OnLoad called, linking JNI methods"); 19 | 20 | vm->AttachCurrentThread(&env, nullptr); 21 | auto klass = env->FindClass("com/unity3d/player/NativeLoader"); 22 | 23 | auto ret = env->RegisterNatives(klass, NativeLoader_bindings.data(), NativeLoader_bindings.size()); 24 | 25 | if (ret < 0) { 26 | logfp(ANDROID_LOG_WARN, "RegisterNatives failed with %d", ret); 27 | 28 | env->FatalError("com/unity3d/player/NativeLoader"); // this is such a useless fucking error message because the original libmain does this 29 | 30 | return -1; 31 | } 32 | 33 | log(ANDROID_LOG_VERBOSE, "Calling preload"); 34 | jni::modloader::preload(); 35 | 36 | log(ANDROID_LOG_INFO, "JNI_OnLoad done!"); 37 | 38 | return JNI_VERSION_1_6; 39 | } 40 | 41 | extern "C" void LIBMAIN_EXPORT JNI_OnUnload(JavaVM* vm, void*) { 42 | log(ANDROID_LOG_INFO, "JNI_OnUnload called"); 43 | } -------------------------------------------------------------------------------- /libmain/src/tinynew.cpp: -------------------------------------------------------------------------------- 1 | /* tinynew.cpp 2 | 3 | Overrides operators new and delete 4 | globally to reduce code size. 5 | 6 | Public domain, use however you wish. 7 | If you really need a license, consider it MIT: 8 | http://www.opensource.org/licenses/mit-license.php 9 | 10 | - Eric Agan 11 | Elegant Invention 12 | */ 13 | 14 | #include 15 | #include 16 | #include "log.hpp" 17 | 18 | void* operator new(std::size_t size) { 19 | return malloc(size); 20 | } 21 | 22 | void* operator new[](std::size_t size) { 23 | return malloc(size); 24 | } 25 | 26 | void operator delete(void* ptr) { 27 | free(ptr); 28 | } 29 | 30 | void operator delete[](void* ptr) { 31 | free(ptr); 32 | } 33 | 34 | /* Optionally you can override the 'nothrow' versions as well. 35 | This is useful if you want to catch failed allocs with your 36 | own debug code, or keep track of heap usage for example, 37 | rather than just eliminate exceptions. 38 | */ 39 | 40 | void* operator new(std::size_t size, const std::nothrow_t&) noexcept { 41 | return malloc(size); 42 | } 43 | 44 | void* operator new[](std::size_t size, const std::nothrow_t&) noexcept { 45 | return malloc(size); 46 | } 47 | 48 | void operator delete(void* ptr, const std::nothrow_t&) noexcept { 49 | free(ptr); 50 | } 51 | 52 | void operator delete[](void* ptr, const std::nothrow_t&) noexcept { 53 | free(ptr); 54 | } 55 | 56 | /* 57 | Additional stuff to help reduce binary size 58 | */ 59 | extern "C" void __cxa_pure_virtual() { 60 | log(ANDROID_LOG_ERROR, "Pure virtual function call"); 61 | while (1); 62 | } -------------------------------------------------------------------------------- /libmodloader/.gitignore: -------------------------------------------------------------------------------- 1 | /ndkpath.txt 2 | /obj 3 | /obj/** 4 | /libs 5 | /libs/** -------------------------------------------------------------------------------- /libmodloader/.vscode/.gitignore: -------------------------------------------------------------------------------- 1 | c_cpp_properties.json -------------------------------------------------------------------------------- /libmodloader/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "gdb", 9 | "request": "attach", 10 | "name": "Attach to gdbserver", 11 | "executable": "./libs/arm64-v8a/libmodloader.so", 12 | "target": ":2345", 13 | "remote": true, 14 | "cwd": "${workspaceRoot}", 15 | "valuesFormatting": "parseText" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /libmodloader/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "C_Cpp.default.cppStandard": "c++20", 3 | "C_Cpp.default.cStandard": "c99", 4 | "C_Cpp.default.includePath": [ 5 | "${workspaceRoot}/include", "${workspaceRoot}/src" 6 | ], 7 | "C_Cpp.intelliSenseEngineFallback": "Enabled" 8 | } -------------------------------------------------------------------------------- /libmodloader/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build", 8 | "type": "shell", 9 | "command": "powershell ./build.ps1", 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /libmodloader/Android.mk: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2009 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # 16 | # 17 | LOCAL_PATH := $(call my-dir) 18 | 19 | include $(CLEAR_VARS) 20 | 21 | LOCAL_MODULE := main 22 | LOCAL_SRC_FILES := ../libmain/libs/$(TARGET_ARCH_ABI)/libmain.so 23 | LOCAL_EXPORT_C_INCLUDES := ../libmain/include 24 | include $(PREBUILT_SHARED_LIBRARY) 25 | 26 | include $(CLEAR_VARS) 27 | 28 | rwildcard=$(wildcard $1$2) $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2)) 29 | 30 | LOCAL_LDLIBS := -llog 31 | LOCAL_MODULE := modloader 32 | LOCAL_CPPFLAGS := -fno-rtti -Wall -Wextra -Werror -Wno-unused-function -Wno-unused-parameter -Wno-sign-compare -O3 33 | LOCAL_CPP_FEATURES := exceptions 34 | LOCAL_SHARED_LIBRARIES := main 35 | LOCAL_C_INCLUDES := ./include ./src 36 | LOCAL_SRC_FILES := $(call rwildcard,src/,*.cpp) ../../beatsaber-hook/shared/inline-hook/inlineHook.c ../../beatsaber-hook/shared/inline-hook/relocate.c ../../beatsaber-hook/shared/inline-hook/And64InlineHook.cpp 37 | 38 | 39 | include $(BUILD_SHARED_LIBRARY) -------------------------------------------------------------------------------- /libmodloader/Application.mk: -------------------------------------------------------------------------------- 1 | APP_ABI := arm64-v8a armeabi-v7a 2 | APP_PLATFORM := android-25 3 | APP_PIE := true 4 | APP_STL := c++_static 5 | APP_CPPFLAGS := -std=gnu++2a -------------------------------------------------------------------------------- /libmodloader/build.ps1: -------------------------------------------------------------------------------- 1 | $NDKPath = Get-Content $PSScriptRoot/ndkpath.txt 2 | 3 | $buildScript = "$NDKPath/build/ndk-build" 4 | if (-not ($PSVersionTable.PSEdition -eq "Core")) { 5 | $buildScript += ".cmd" 6 | } 7 | 8 | & $buildScript NDK_PROJECT_PATH=$PSScriptRoot APP_BUILD_SCRIPT=$PSScriptRoot/Android.mk NDK_APPLICATION_MK=$PSScriptRoot/Application.mk -------------------------------------------------------------------------------- /libmodloader/include/modloader/_config.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define MODLOADER_EXPORT __attribute__((visibility("default"))) 4 | #define MODLOADER_HIDE __attribute__((visibility("hidden"))) -------------------------------------------------------------------------------- /libmodloader/include/modloader/mem.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "modloader/_config.hpp" 4 | #include 5 | #include 6 | 7 | namespace mem { 8 | enum class protection { 9 | none = 0, 10 | read = 0b001, write = 0b010, execute = 0b100, 11 | read_write = read | write, 12 | read_execute = read | execute, 13 | write_execute = write | execute, // usually disallowed by the system 14 | read_write_execute = read | write | execute 15 | }; 16 | 17 | int operator&(protection, protection) noexcept; 18 | 19 | // returns 0 or errno 20 | // WARNING: THIS WILL ALIGN THE POINTER TO THE NEXT PAGE BOUNDARY BEFORE AND CHANGE **ALL** OF IT 21 | int protect(void*, std::size_t, protection) noexcept; 22 | template 23 | int MODLOADER_HIDE protect(T* data, std::size_t count, protection prot) noexcept 24 | { return protect(reinterpret_cast(data), count*sizeof(T), prot); } 25 | template 26 | int MODLOADER_HIDE protect(T (&data)[N], protection prot) noexcept 27 | { return protect(data, N, prot); } 28 | template 29 | int MODLOADER_HIDE protect(std::span data, protection prot) noexcept 30 | { return protect(data.data(), data.size(), prot); } 31 | template 32 | int MODLOADER_HIDE protect(std::span data, protection prot) noexcept 33 | { return protect(data, N, prot); } 34 | 35 | struct aligned_t {}; 36 | constexpr aligned_t aligned = {}; 37 | } 38 | 39 | void* operator new(std::size_t, mem::aligned_t, std::size_t) noexcept; 40 | void* operator new[](std::size_t, mem::aligned_t, std::size_t) noexcept; 41 | -------------------------------------------------------------------------------- /libmodloader/libmodloader.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "files.associations": { 9 | "__bit_reference": "cpp", 10 | "algorithm": "cpp", 11 | "cctype": "cpp", 12 | "cmath": "cpp", 13 | "cstddef": "cpp", 14 | "cstdio": "cpp", 15 | "cstdlib": "cpp", 16 | "cstring": "cpp", 17 | "new": "cpp", 18 | "typeinfo": "cpp", 19 | "__config": "cpp", 20 | "__debug": "cpp", 21 | "__functional_base": "cpp", 22 | "__hash_table": "cpp", 23 | "__node_handle": "cpp", 24 | "__nullptr": "cpp", 25 | "__split_buffer": "cpp", 26 | "__string": "cpp", 27 | "__tuple": "cpp", 28 | "array": "cpp", 29 | "atomic": "cpp", 30 | "bit": "cpp", 31 | "clocale": "cpp", 32 | "cstdint": "cpp", 33 | "cwchar": "cpp", 34 | "cwctype": "cpp", 35 | "exception": "cpp", 36 | "functional": "cpp", 37 | "initializer_list": "cpp", 38 | "iosfwd": "cpp", 39 | "iterator": "cpp", 40 | "limits": "cpp", 41 | "memory": "cpp", 42 | "optional": "cpp", 43 | "stdexcept": "cpp", 44 | "string_view": "cpp", 45 | "tuple": "cpp", 46 | "type_traits": "cpp", 47 | "unordered_map": "cpp", 48 | "utility": "cpp", 49 | "vector": "cpp", 50 | "version": "cpp", 51 | "span": "cpp", 52 | "string": "cpp", 53 | "__tree": "cpp", 54 | "map": "cpp" 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /libmodloader/src/log.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define logp(priority, message) __android_log_print(priority, "libmodloader", message) 6 | #define logpf(priority, ...) __android_log_print(priority, "libmodloader", __VA_ARGS__) 7 | #define logpfm(priority, ...) __android_log_print(priority, info.tag.c_str(), __VA_ARGS__) -------------------------------------------------------------------------------- /libmodloader/src/mem.cpp: -------------------------------------------------------------------------------- 1 | #include "modloader/mem.hpp" 2 | #include 3 | #include 4 | #include 5 | 6 | namespace { 7 | auto pageSize = sysconf(_SC_PAGESIZE); 8 | } 9 | 10 | int mem::operator&(protection a, protection b) noexcept { 11 | return static_cast(a) & static_cast(b); 12 | } 13 | 14 | int mem::protect(void* data, std::size_t size, protection prot) noexcept { 15 | int mprot = PROT_NONE; 16 | if (prot & protection::read) 17 | mprot |= PROT_READ; 18 | if (prot & protection::write) 19 | mprot |= PROT_WRITE; 20 | if (prot & protection::execute) 21 | mprot |= PROT_EXEC; 22 | 23 | auto ptrs = reinterpret_cast(data); 24 | auto diff = ptrs % pageSize; 25 | ptrs -= diff; 26 | 27 | auto ret = mprotect(reinterpret_cast(ptrs), size + diff, mprot); 28 | if (ret != 0) return errno; 29 | else return 0; 30 | } 31 | 32 | void* operator new(std::size_t size, mem::aligned_t, std::size_t align) noexcept { 33 | return ::operator new(size, static_cast(align)); 34 | } 35 | void* operator new[](std::size_t size, mem::aligned_t, std::size_t align) noexcept { 36 | return ::operator new[](size, static_cast(align)); 37 | } -------------------------------------------------------------------------------- /libmodloader/src/modloader.cpp: -------------------------------------------------------------------------------- 1 | // Only define the Modloader class here, instead of getting it from the header 2 | #define MODLOADER_DEFINED 3 | #include 4 | #include 5 | #include 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 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include 30 | // #include "jit/jit.hpp" 31 | #include "log.hpp" 32 | 33 | #include 34 | #include 35 | #include "../../beatsaber-hook/shared/utils/utils.h" 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include "protection.hpp" 42 | 43 | #undef TAG 44 | #define TAG "libmodloader" 45 | 46 | #define MOD_PATH_FMT "/sdcard/Android/data/%s/files/mods/" 47 | #define LIBS_PATH_FMT "/sdcard/Android/data/%s/files/libs/" 48 | 49 | // There should only be ONE modloader PER GAME 50 | // Ideally, there is only ONE modloader per libmodloader.so 51 | class Modloader { 52 | public: 53 | static const std::string getLibIl2CppPath(); 54 | static const std::string getApplicationId(); 55 | static bool getAllConstructed(); 56 | static const ModloaderInfo getInfo(); 57 | static const std::unordered_map getMods(); 58 | static bool requireMod(const ModInfo&); 59 | static bool requireMod(std::string_view id, std::string_view version); 60 | static bool requireMod(std::string_view id); 61 | static const std::string getModloaderPath(); 62 | static const std::string getDestinationPath(); 63 | static JNIEnv* getJni(); 64 | // New members, specific to .cpp only 65 | static void init_mods() noexcept; 66 | static void load_mods() noexcept; 67 | static bool allConstructed; 68 | static std::string modloaderPath; 69 | static std::string modPath; 70 | static std::string libsPath; 71 | static std::string modTempPath; 72 | static std::string applicationId; 73 | static std::string libIl2CppPath; 74 | static void construct_mods() noexcept; 75 | static void setInfo(ModloaderInfo& info); 76 | private: 77 | static bool setDataDirs(); 78 | static ModloaderInfo info; 79 | static std::unordered_map mods; 80 | static std::unordered_set loadingMods; 81 | static void copy_to_temp(std::string path, const char* filename); 82 | static bool copy(std::string_view pathToCopy); 83 | static bool try_load_libs(); 84 | static bool try_setup_mods(); 85 | static bool try_load_recurse(std::vector>& failed, bool (*attempt_load)(std::string first, const char* second)); 86 | static bool lib_loader(std::string first, const char* second); 87 | static void* construct_mod(const char* filename); 88 | static bool create_mod(std::string modPath, const char* name); 89 | static void setup_mod(void *handle, ModInfo& modInfo); 90 | }; 91 | 92 | bool Modloader::allConstructed; 93 | std::string Modloader::modloaderPath; 94 | std::string Modloader::modPath; 95 | std::string Modloader::libsPath; 96 | std::string Modloader::modTempPath; 97 | std::string Modloader::applicationId; 98 | std::string Modloader::libIl2CppPath; 99 | ModloaderInfo Modloader::info; 100 | std::unordered_map Modloader::mods; 101 | std::unordered_set Modloader::loadingMods; 102 | static JNIEnv* modloaderEnv; 103 | 104 | // Generic utility functions 105 | #pragma region Generic Utilities 106 | 107 | static jobject getActivityFromUnityPlayerInternal(JNIEnv *env) { 108 | jclass clazz = env->FindClass("com/unity3d/player/UnityPlayer"); 109 | if (clazz == NULL) return nullptr; 110 | jfieldID actField = env->GetStaticFieldID(clazz, "currentActivity", "Landroid/app/Activity;"); 111 | if (actField == NULL) return nullptr; 112 | return env->GetStaticObjectField(clazz, actField); 113 | } 114 | 115 | static jobject getActivityFromUnityPlayer(JNIEnv *env) { 116 | jobject activity = getActivityFromUnityPlayerInternal(env); 117 | if (activity == NULL) { 118 | if (env->ExceptionCheck()) env->ExceptionDescribe(); 119 | logpf(ANDROID_LOG_ERROR, "libmain.getActivityFromUnityPlayer failed! See 'System.err' tag."); 120 | env->ExceptionClear(); 121 | } 122 | return activity; 123 | } 124 | 125 | static bool ensurePermsWithAppId(JNIEnv* env, jobject activity, std::string_view application_id) { 126 | #define ERR_CHECK(val, ...) auto val = __VA_ARGS__; \ 127 | if (val == nullptr) {logpf(ANDROID_LOG_ERROR, "Failed: " #__VA_ARGS__); return false;} 128 | 129 | ERR_CHECK(clazz, env->FindClass("com/unity3d/player/UnityPlayerActivity")); 130 | ERR_CHECK(intentClass, env->FindClass("android/content/Intent")); 131 | ERR_CHECK(intentCtorID, env->GetMethodID(intentClass, "", "(Ljava/lang/String;Landroid/net/Uri;)V")); 132 | ERR_CHECK(startActivity, env->GetMethodID(clazz, "startActivity", "(Landroid/content/Intent;)V")); 133 | ERR_CHECK(uriClass, env->FindClass("android/net/Uri")); 134 | ERR_CHECK(uriParse, env->GetStaticMethodID(uriClass, "parse", "(Ljava/lang/String;)Landroid/net/Uri;")); 135 | ERR_CHECK(envClass, env->FindClass("android/os/Environment")); 136 | ERR_CHECK(isExternalMethod, env->GetStaticMethodID(envClass, "isExternalStorageManager", "()Z")); 137 | 138 | auto result = env->CallStaticBooleanMethod(envClass, isExternalMethod); 139 | if (env->ExceptionCheck()) { 140 | logpf(ANDROID_LOG_ERROR, "Failed: to call Environment.getIsExternalStorageManager()"); 141 | return false; 142 | } 143 | logpf(ANDROID_LOG_INFO, "Fetch of isExternalStorageManager(): %d", result); 144 | if (!result) { 145 | logpf(ANDROID_LOG_DEBUG, "Attempt to show intent for MANAGE APP ALL FILES ACCESS PERMISSION:"); 146 | const jstring actionName = env->NewStringUTF("android.settings.MANAGE_APP_ALL_FILES_ACCESS_PERMISSION"); 147 | const jstring packageName = env->NewStringUTF(string_format("package:%s", application_id.data()).c_str()); 148 | // TODO: See if this gives us the correct location in the settings menu 149 | ERR_CHECK(uri, env->CallStaticObjectMethod(uriClass, uriParse, packageName)); 150 | ERR_CHECK(intent, env->NewObject(intentClass, intentCtorID, actionName, uri)); 151 | env->CallVoidMethod(activity, startActivity, intent); 152 | return !env->ExceptionCheck(); 153 | } 154 | return true; 155 | #undef ERR_CHECK 156 | } 157 | 158 | static bool ensurePermsInternal(JNIEnv* env, jobject activity) { 159 | #define ERR_CHECK(val, ...) auto val = __VA_ARGS__; \ 160 | if (val == nullptr) {logpf(ANDROID_LOG_ERROR, "Failed: " #__VA_ARGS__); return false;} 161 | 162 | ERR_CHECK(clazz, env->FindClass("com/unity3d/player/UnityPlayerActivity")); 163 | ERR_CHECK(checkSelfPermission, env->GetMethodID(clazz, "checkSelfPermission", "(Ljava/lang/String;)I")); 164 | ERR_CHECK(requestPermissions, env->GetMethodID(clazz, "requestPermissions", "([Ljava/lang/String;I)V")); 165 | ERR_CHECK(stringClass, env->FindClass("java/lang/String")); 166 | 167 | using namespace std::chrono_literals; 168 | constexpr static auto kRetryDelay = 6000ms; 169 | constexpr static auto kRetryCount = 3; 170 | 171 | { 172 | const jstring perm = env->NewStringUTF("android.permission.WRITE_EXTERNAL_STORAGE"); 173 | const jstring perm2 = env->NewStringUTF("android.permission.MANAGE_EXTERNAL_STORAGE"); 174 | for (int i = 0; i < kRetryCount; i++) { 175 | logpf(ANDROID_LOG_DEBUG, "Trial for permissions: %d/%d", i, kRetryCount); 176 | jint hasPerm = env->CallIntMethod(activity, checkSelfPermission, perm); 177 | logpf(ANDROID_LOG_DEBUG, "checkSelfPermission(WRITE_EXTERNAL_STORAGE, MANAGE_EXTERNAL_STORAGE) returned: %i", hasPerm); 178 | if (hasPerm != 0) { 179 | jobjectArray arr = env->NewObjectArray(2, stringClass, perm); 180 | env->SetObjectArrayElement(arr, 1, perm2); 181 | jint requestCode = 21326; // the number in the alphabet for each letter in BMBF (B=2, M=13, F=6) 182 | logpf(ANDROID_LOG_INFO, "Calling requestPermissions for WRITE_EXTERNAL_STORAGE, MANAGE_EXTERNAL_STORAGE!"); 183 | env->CallVoidMethod(activity, requestPermissions, arr, requestCode); 184 | if (env->ExceptionCheck()) return false; 185 | } else { 186 | logpf(ANDROID_LOG_DEBUG, "Permission is accepted!"); 187 | break; 188 | } 189 | std::this_thread::sleep_for(kRetryDelay); 190 | } 191 | } 192 | return !env->ExceptionCheck(); 193 | #undef ERR_CHECK 194 | } 195 | 196 | #define NULLOPT_UNLESS(expr, ...) ({ \ 197 | auto&& __tmp = (expr); \ 198 | if (!__tmp) {logpf(ANDROID_LOG_WARN, __VA_ARGS__); return std::nullopt;} \ 199 | __tmp; }) 200 | 201 | struct PathPair { 202 | jstring files_dir; 203 | jstring extern_dir; 204 | }; 205 | 206 | static std::optional getDestination(JNIEnv* env) { 207 | auto activityThreadClass = NULLOPT_UNLESS(env->FindClass("android/app/ActivityThread"), "Failed to find android.app.ActivityThread!"); 208 | auto currentActivityThreadMethod = NULLOPT_UNLESS(env->GetStaticMethodID(activityThreadClass, "currentActivityThread", "()Landroid/app/ActivityThread;"), "Failed to find ActivityThread.currentActivityThread"); 209 | auto contextClass = NULLOPT_UNLESS(env->FindClass("android/content/Context"), "Failed to find android.context.Context!"); 210 | auto getApplicationMethod = NULLOPT_UNLESS(env->GetMethodID(activityThreadClass, "getApplication", "()Landroid/app/Application;"), "Failed to find Application.getApplication"); 211 | auto filesDirMethod = NULLOPT_UNLESS(env->GetMethodID(contextClass, "getFilesDir", "()Ljava/io/File;"), "Failed to find Context.getFilesDir()!"); 212 | auto externalFilesDirMethod = NULLOPT_UNLESS(env->GetMethodID(contextClass, "getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;"), "Failed to find Context.getExternalFilesDir(String)!"); 213 | auto fileClass = NULLOPT_UNLESS(env->FindClass("java/io/File"), "Failed to find java.io.File!"); 214 | auto absDirMethod = NULLOPT_UNLESS(env->GetMethodID(fileClass, "getAbsolutePath", "()Ljava/lang/String;"), "Failed to find File.getAbsolutePath()!"); 215 | 216 | auto at = NULLOPT_UNLESS(env->CallStaticObjectMethod(activityThreadClass, currentActivityThreadMethod), "Returned result from currentActivityThread is null!"); 217 | auto context = NULLOPT_UNLESS(env->CallObjectMethod(at, getApplicationMethod), "Returned result from getApplication is null!"); 218 | auto file = NULLOPT_UNLESS(env->CallObjectMethod(context, filesDirMethod), "Returned result from getFilesDir is null!"); 219 | auto str = NULLOPT_UNLESS(env->CallObjectMethod(file, absDirMethod), "Returned result from getAbsolutePath is null!"); 220 | 221 | auto external_file = NULLOPT_UNLESS(env->CallObjectMethod(context, externalFilesDirMethod, nullptr), "Return result from getExternalFilesDir is null!"); 222 | auto external_str = NULLOPT_UNLESS(env->CallObjectMethod(external_file, absDirMethod), "Returned result from getAbsolutePath is null!"); 223 | 224 | return PathPair{reinterpret_cast(str), reinterpret_cast(external_str)}; 225 | } 226 | 227 | static bool ensurePerms(JNIEnv* env, jobject activity, std::string_view application_id) { 228 | auto result = ensurePermsInternal(env, activity); 229 | if (!result) { 230 | if (env->ExceptionCheck()) env->ExceptionDescribe(); 231 | logpf(ANDROID_LOG_ERROR, "libmodloader.ensurePermsInternal failed! See 'System.err' tag."); 232 | env->ExceptionClear(); 233 | } 234 | result &= ensurePermsWithAppId(env, activity, application_id); 235 | if (!result) { 236 | if (env->ExceptionCheck()) env->ExceptionDescribe(); 237 | logpf(ANDROID_LOG_ERROR, "libmodloader.ensurePermsWithAppId failed! See 'System.err' tag."); 238 | env->ExceptionClear(); 239 | } 240 | 241 | return false; 242 | } 243 | 244 | static const char* status_type(std::filesystem::file_type const type) { 245 | switch (type) { 246 | case std::filesystem::file_type::none: return "none"; 247 | case std::filesystem::file_type::not_found: return "not found"; 248 | case std::filesystem::file_type::regular: return "regular"; 249 | case std::filesystem::file_type::directory: return "directory"; 250 | case std::filesystem::file_type::symlink: return "symlink"; 251 | case std::filesystem::file_type::block: return "block"; 252 | case std::filesystem::file_type::character: return "character"; 253 | case std::filesystem::file_type::fifo: return "fifo"; 254 | case std::filesystem::file_type::socket: return "socket"; 255 | case std::filesystem::file_type::unknown: 256 | default: 257 | return "unknown"; 258 | } 259 | } 260 | 261 | static bool ensureExists(std::filesystem::path const& path) { 262 | std::error_code err{}; 263 | if (!std::filesystem::exists(path, err)) { 264 | if (err) { 265 | logpf(ANDROID_LOG_ERROR, "FAILED to check if directory exists: %s: errno: %d, message: %s", path.c_str(), err.value(), err.message().c_str()); 266 | return false; 267 | } 268 | logpf(ANDROID_LOG_INFO, "Directory: %s does not exist! Creating it...", path.c_str()); 269 | if (!std::filesystem::create_directories(path, err)) { 270 | // Assumes err is set if false is returned 271 | logpf(ANDROID_LOG_ERROR, "FAILED to create directories: %s: errno: %d, message: %s", path.c_str(), err.value(), err.message().c_str()); 272 | return false; 273 | } 274 | logpf(ANDROID_LOG_INFO, "Trying to chmod %s", path.c_str()); 275 | auto flags = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IWGRP | S_IROTH | S_IXOTH; 276 | if (chmod(path.c_str(), flags) < 0) { 277 | logpf(ANDROID_LOG_ERROR, "Failed to chmod %s with flags: %x! errno: %d, %s", path.c_str(), flags, errno, strerror(errno)); 278 | } 279 | } 280 | // Ensure directory has viable stats, log them 281 | logpf(ANDROID_LOG_INFO, "Checking file type of: %s...", path.c_str()); 282 | auto status = std::filesystem::status(path, err); 283 | if (err) { 284 | logpf(ANDROID_LOG_ERROR, "FAILED to check status of: %s: errno: %d, message: %s", path.c_str(), err.value(), err.message().c_str()); 285 | return false; 286 | } 287 | // Log status type 288 | logpf(ANDROID_LOG_INFO, "File type: %s perms: 0x%x", status_type(status.type()), status.permissions()); 289 | return status.type() == std::filesystem::file_type::directory; 290 | } 291 | 292 | static DIR* opendir_wrapper(std::string_view str) { 293 | #define STAT_CHECK(...) if ((__VA_ARGS__)) \ 294 | {logpf(ANDROID_LOG_ERROR, "Failed to open dir: %s: Condition: " #__VA_ARGS__ ": errno: %d, %s", str.data(), errno, strerror(errno)); return nullptr;} 295 | #define ERR_CHECK(...) if ((__VA_ARGS__)) \ 296 | {logpf(ANDROID_LOG_ERROR, "Failed to open dir: %s: " #__VA_ARGS__ ": errno: %d, %s", str.data(), errno, strerror(errno)); close(fd); return nullptr;} 297 | 298 | // First, check some info via stat 299 | struct stat64 buffer{}; 300 | STAT_CHECK(stat64(str.data(), &buffer) != 0); 301 | #ifdef __aarch64__ 302 | logpf(ANDROID_LOG_DEBUG, "file: %s, dev: %lu, ino: %lu, mode: %d, nlink: %d, uid: %d, gid: %d, rdid: %lu, sz: %ld, atime: %ld, mtime: %ld, ctime: %ld", 303 | str.data(), 304 | buffer.st_dev, 305 | buffer.st_ino, 306 | buffer.st_mode, 307 | buffer.st_nlink, 308 | buffer.st_uid, 309 | buffer.st_gid, 310 | buffer.st_rdev, 311 | buffer.st_size, 312 | buffer.st_atime, 313 | buffer.st_mtime, 314 | buffer.st_ctime 315 | ); 316 | #endif 317 | 318 | // We want to open this directory to iterate files under it 319 | auto fd = open(str.data(), O_RDONLY); 320 | ERR_CHECK(fd < 0) 321 | auto dir = fdopendir(fd); 322 | ERR_CHECK(dir == nullptr) 323 | 324 | #undef STAT_CHECK 325 | #undef ERR_CHECK 326 | 327 | return dir; 328 | } 329 | 330 | char *trimWhitespace(char *str) 331 | { 332 | char *end; 333 | while(isspace((unsigned char)*str)) str++; 334 | if(*str == 0) 335 | return str; 336 | 337 | end = str + strlen(str) - 1; 338 | while(end > str && isspace((unsigned char)*end)) end--; 339 | 340 | end[1] = '\0'; 341 | 342 | return str; 343 | } 344 | 345 | int mkpath(std::string stringPath, mode_t mode) { 346 | // Pass a copy of the string to mkpath 347 | char* file_path = stringPath.data(); 348 | for (char* p = strchr(file_path + 1, '/'); p; p = strchr(p + 1, '/')) { 349 | *p = '\0'; 350 | if (mkdir(file_path, mode) == -1) { 351 | if (errno != EEXIST) { 352 | *p = '/'; 353 | return -1; 354 | } 355 | } 356 | *p = '/'; 357 | } 358 | return 0; 359 | } 360 | #pragma endregion 361 | 362 | // Modloader functions 363 | #pragma region Modloader Functions 364 | 365 | const std::string Modloader::getModloaderPath() { 366 | return modloaderPath; 367 | } 368 | 369 | const std::string Modloader::getDestinationPath() { 370 | return modTempPath; 371 | } 372 | 373 | JNIEnv* Modloader::getJni() { 374 | return modloaderEnv; 375 | } 376 | 377 | // MUST BE CALLED BEFORE LOADING MODS 378 | bool Modloader::setDataDirs() 379 | { 380 | FILE *cmdline = fopen("/proc/self/cmdline", "r"); 381 | if (cmdline) { 382 | //not sure what the actual max is, but path_max should cover it 383 | char application_id[PATH_MAX] = {0}; 384 | fread(application_id, sizeof(application_id), 1, cmdline); 385 | fclose(cmdline); 386 | trimWhitespace(application_id); 387 | applicationId = application_id; 388 | // First, request permissions 389 | auto activity = getActivityFromUnityPlayer(modloaderEnv); 390 | if (activity == nullptr) logpfm(ANDROID_LOG_ERROR, "CANNOT HAVE NULL UNITY PLAYER ACTIVITY!"); 391 | else { 392 | ensurePerms(modloaderEnv, activity, applicationId); 393 | } 394 | auto res = getDestination(modloaderEnv); 395 | if (res) { 396 | auto str = modloaderEnv->GetStringUTFChars(res->files_dir, nullptr); 397 | modTempPath = str; 398 | modTempPath.push_back('/'); 399 | logpfm(ANDROID_LOG_INFO, "Destination path: %s", modTempPath.c_str()); 400 | // string is copied, so free the allocated version 401 | modloaderEnv->ReleaseStringUTFChars(res->files_dir, str); 402 | // Get and at least log external folder here 403 | auto extern_str = modloaderEnv->GetStringUTFChars(res->extern_dir, nullptr); 404 | logpfm(ANDROID_LOG_INFO, "External path: %s", extern_str); 405 | bool success = ensureExists(modTempPath); 406 | auto modsStdPath = std::filesystem::path(extern_str) / "mods"; 407 | success &= ensureExists(modsStdPath); 408 | auto libsStdPath = std::filesystem::path(extern_str) / "libs"; 409 | success &= ensureExists(libsStdPath); 410 | // TRAILING SLASHES ARE REQUIRED! 411 | modPath = modsStdPath.string() + "/"; 412 | libsPath = libsStdPath.string() + "/"; 413 | modloaderEnv->ReleaseStringUTFChars(res->extern_dir, extern_str); 414 | return success; 415 | } else { 416 | if (modloaderEnv->ExceptionCheck()) { 417 | modloaderEnv->ExceptionDescribe(); 418 | logpfm(ANDROID_LOG_ERROR, "getDestination failed! See 'System.err' tag."); 419 | modloaderEnv->ExceptionClear(); 420 | } 421 | modTempPath = string_format("/data/data/%s/", application_id); 422 | logpfm(ANDROID_LOG_WARN, "Could not obtain data path from JNI! Falling back to %s instead.", modTempPath.c_str()); 423 | modPath = string_format(MOD_PATH_FMT, application_id); 424 | libsPath = string_format(LIBS_PATH_FMT, application_id); 425 | logpfm(ANDROID_LOG_WARN, "Could not obtain external data path from JNI! Falling back to: %s, %s instead.", 426 | modPath.c_str(), libsPath.c_str()); 427 | } 428 | return ensureExists(modTempPath); 429 | } else { 430 | return false; 431 | } 432 | } 433 | 434 | void Modloader::copy_to_temp(std::string path, const char* filename) { 435 | auto full_path = path + filename; 436 | logpfm(ANDROID_LOG_INFO, "Copying file: %s", full_path.c_str()); 437 | struct stat64 buffer{}; 438 | if (stat64(full_path.c_str(), &buffer) != 0) { 439 | logpfm(ANDROID_LOG_DEBUG, "stat64 of file: %s failed: %d, %s", full_path.c_str(), errno, strerror(errno)); 440 | } else { 441 | #ifdef __aarch64__ 442 | logpfm(ANDROID_LOG_DEBUG, "file: %s, dev: %lu, ino: %lu, mode: %d, nlink: %d, uid: %d, gid: %d, rdid: %lu, sz: %ld, atime: %ld, mtime: %ld, ctime: %ld", 443 | full_path.c_str(), 444 | buffer.st_dev, 445 | buffer.st_ino, 446 | buffer.st_mode, 447 | buffer.st_nlink, 448 | buffer.st_uid, 449 | buffer.st_gid, 450 | buffer.st_rdev, 451 | buffer.st_size, 452 | buffer.st_atime, 453 | buffer.st_mtime, 454 | buffer.st_ctime 455 | ); 456 | #endif 457 | } 458 | int infile = open(full_path.c_str(), O_RDONLY); 459 | if (infile < 0) { 460 | logpfm(ANDROID_LOG_ERROR, "Failed to open input path: %s! errno: %d, %s", full_path.c_str(), errno, strerror(errno)); 461 | } 462 | off_t filesize = lseek(infile, 0, SEEK_END); 463 | if (filesize == -1) { 464 | logpfm(ANDROID_LOG_ERROR, "Failed to seek end fd: %d from path: %s! errno: %d, %s", infile, full_path.c_str(), errno, strerror(errno)); 465 | } 466 | lseek(infile, 0, SEEK_SET); 467 | if (filesize == -1) { 468 | logpfm(ANDROID_LOG_ERROR, "Failed to seek set fd: %d from path: %s! errno: %d, %s", infile, full_path.c_str(), errno, strerror(errno)); 469 | } 470 | 471 | logpfm(ANDROID_LOG_VERBOSE, "Temp path: %s", modTempPath.c_str()); 472 | std::string temp_path = modTempPath + filename; 473 | logpfm(ANDROID_LOG_VERBOSE, "Local full path: %s", temp_path.c_str()); 474 | 475 | int outfile = open(temp_path.c_str(), O_CREAT | O_WRONLY, 0777); 476 | if (outfile < 0) { 477 | logpfm(ANDROID_LOG_ERROR, "Failed to open output path: %s! errno: %d, %s", temp_path.c_str(), errno, strerror(errno)); 478 | } 479 | if (sendfile(outfile, infile, 0, filesize) < 0) { 480 | logpfm(ANDROID_LOG_ERROR, "Failed to send file from: %d to: %d with size: %ld! errno: %d, %s", infile, outfile, filesize, errno, strerror(errno)); 481 | } 482 | auto flags = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP; 483 | if (chmod(temp_path.c_str(), flags) < 0) { 484 | logpfm(ANDROID_LOG_ERROR, "Failed to chmod %s with flags: %x! errno: %d, %s", temp_path.c_str(), flags, errno, strerror(errno)); 485 | } 486 | close(infile); 487 | close(outfile); 488 | } 489 | 490 | // TODO Find a way to avoid calling constructor on mods that have offsetless hooks in constructor 491 | // Loads the mod at the given full_path 492 | // Returns the dlopened handle 493 | void* Modloader::construct_mod(const char* filename) { 494 | // Calls the constructor on the mod by loading it 495 | // Copying should have already taken place. 496 | std::string temp_path(modTempPath); 497 | temp_path.append(filename); 498 | auto *ret = dlopen(temp_path.c_str(), RTLD_NOW | RTLD_LOCAL); 499 | protect(); 500 | if (ret == NULL) { 501 | // Error logging (for if symbols cannot be resolved) 502 | auto s = dlerror(); 503 | logpfm(ANDROID_LOG_WARN, "dlerror when dlopening: %s: %s", temp_path.c_str(), s == NULL ? "null" : s); 504 | } 505 | return ret; 506 | } 507 | 508 | // Calls the setup(ModInfo&) function on the mod, if it exists 509 | // This will be immediately after mod construction 510 | void Modloader::setup_mod(void *handle, ModInfo& modInfo) { 511 | logpfm(ANDROID_LOG_VERBOSE, "Setting up mod handle: %p", handle); 512 | void (*setup_func)(ModInfo&); 513 | *(void**)(&setup_func) = dlsym(handle, "setup"); 514 | logpfm(ANDROID_LOG_VERBOSE, "Found setup function: %p", setup_func); 515 | if (setup_func) { 516 | // We don't need to pass in a Modloader pointer because we have one in static anyways! 517 | setup_func(modInfo); 518 | } 519 | } 520 | 521 | // Returns true if a mod was successfully created, false otherwise. 522 | bool Modloader::create_mod(std::string modPath, const char* name) { 523 | auto *modHandle = construct_mod(name); 524 | if (modHandle == NULL) { 525 | return false; 526 | } 527 | ModInfo modInfo; 528 | setup_mod(modHandle, modInfo); 529 | if (modInfo.id.empty()) { 530 | // Fallback to library name if it doesn't have an id 531 | modInfo.id = name; 532 | } 533 | if (modInfo.version.empty()) { 534 | // Fallback to 0.0.0 if it doesn't have a version 535 | modInfo.version = "0.0.0"; 536 | } 537 | // Don't overwrite existing mod IDs, warn when doing so 538 | if (!mods.try_emplace(modInfo.id, name, modPath, modInfo, modHandle).second) { 539 | logpfm(ANDROID_LOG_WARN, "Could not construct mod with name: %s, id: %s, version: %s because another mod of that same id exists!", name, modInfo.id.c_str(), modInfo.version.c_str()); 540 | } 541 | logpfm(ANDROID_LOG_INFO, "Created mod with name: %s, id: %s, version: %s, path: %s, handle: %p", name, modInfo.id.c_str(), modInfo.version.c_str(), modPath.c_str(), modHandle); 542 | return true; 543 | } 544 | 545 | // Copies all .so files from pathToCopy to the temp directory 546 | bool Modloader::copy(std::string_view pathToCopy) { 547 | logpfm(ANDROID_LOG_VERBOSE, "Copying all .so files from: %s to temp directory: %s", pathToCopy.data(), modTempPath.c_str()); 548 | struct dirent* dp; 549 | DIR *dir = opendir_wrapper(pathToCopy.data()); 550 | if (dir == nullptr) { 551 | logpfm(ANDROID_LOG_ERROR, "copy(%s): null dir! errno: %i, msg: %s", pathToCopy.data(), errno, strerror(errno)); 552 | // We can actually continue, but without copying over any of the libraries 553 | return false; 554 | } else { 555 | while ((dp = readdir(dir)) != NULL) { 556 | if (strlen(dp->d_name) > 3 && !strcmp(dp->d_name + strlen(dp->d_name) - 3, ".so")) { 557 | // We want to copy all .so files to our temp path, or we can dlopen them locally 558 | copy_to_temp(pathToCopy.data(), dp->d_name); 559 | } 560 | } 561 | closedir(dir); 562 | } 563 | return true; 564 | } 565 | 566 | /// @brief Attempts to recursively load failed .so files from the provided list. 567 | /// Modifies the vector and calls attempt_load on each potentially loadable mod/lib 568 | bool Modloader::try_load_recurse(std::vector>& failed, bool (*attempt_load)(std::string first, const char* second)) { 569 | if (failed.size() > 0) { 570 | auto oldSize = failed.size() + 1; 571 | // While the new failed size is less than the old size, continue to try to load 572 | // If we reach a point where we cannot load any mods (deadlock) we will have equivalent oldSize and failed.size() 573 | while (failed.size() < oldSize && failed.size() != 0) { 574 | std::vector> tempFailed; 575 | logpfm(ANDROID_LOG_WARN, "Failed List:"); 576 | for (const auto& item : failed) { 577 | auto str = item.first + item.second; 578 | logpfm(ANDROID_LOG_INFO, "Previously failed: %s Trying again...", str.c_str()); 579 | if (!attempt_load(item.first, item.second.c_str())) { 580 | // If we failed to open it again, we add it to the temporary list. 581 | logpfm(ANDROID_LOG_ERROR, "STILL Failed to dlopen: %s", str.c_str()); 582 | tempFailed.emplace_back(item.first, item.second); 583 | } else { 584 | logpfm(ANDROID_LOG_INFO, "Successfully loaded: %s", str.c_str()); 585 | } 586 | } 587 | oldSize = failed.size(); 588 | failed.clear(); 589 | failed = tempFailed; 590 | #ifdef __aarch64__ 591 | logpfm(ANDROID_LOG_VERBOSE, "After completing pass, had: %lu failed, now have: %lu", oldSize, failed.size()); 592 | #else 593 | logpfm(ANDROID_LOG_VERBOSE, "After completing pass, had: %u failed, now have: %u", oldSize, failed.size()); 594 | #endif 595 | } 596 | return failed.size() == 0; 597 | } 598 | return true; 599 | } 600 | 601 | bool Modloader::lib_loader(std::string name, const char* second) { 602 | auto str = name + second; 603 | auto* tmp = dlopen(str.c_str(), RTLD_LAZY | RTLD_LOCAL); 604 | if (tmp == NULL) { 605 | auto s = dlerror(); 606 | logpfm(ANDROID_LOG_ERROR, "Failed to dlopen: %s, dlerror: %s", str.c_str(), s == nullptr ? "NULL" : s); 607 | return false; 608 | } 609 | return true; 610 | } 611 | 612 | // Responsible for loading libraries 613 | // Returns true on success (all libs loaded) false otherwise 614 | bool Modloader::try_load_libs() { 615 | logpfm(ANDROID_LOG_INFO, "Loading all libs!"); 616 | // Failed to load libs 617 | std::vector> failed; 618 | 619 | // Try to open libs 620 | struct dirent* dp; 621 | DIR* dir = opendir_wrapper(libsPath.c_str()); 622 | if (dir == nullptr) { 623 | logpfm(ANDROID_LOG_ERROR, "Not opening libs %s: null dir! errno: %i, msg: %s!", libsPath.c_str(), errno, strerror(errno)); 624 | return false; 625 | } else { 626 | while ((dp = readdir(dir)) != NULL) { 627 | // Iterate over the directory AGAIN 628 | // This time this happens after all mods are copied so we can ensure proper linkage 629 | // dlopen each one 630 | if (strlen(dp->d_name) > 3 && !strcmp(dp->d_name + strlen(dp->d_name) - 3, ".so")) { 631 | auto str = modTempPath + dp->d_name; 632 | auto* tmp = dlopen(str.c_str(), RTLD_LAZY | RTLD_LOCAL); 633 | if (tmp == NULL) { 634 | auto s = dlerror(); 635 | logpfm(ANDROID_LOG_ERROR, "Failed to dlopen: %s, dlerror: %s", str.c_str(), s == nullptr ? "NULL" : s); 636 | failed.emplace_back(modTempPath, dp->d_name); 637 | } else { 638 | logpfm(ANDROID_LOG_INFO, "Successfully loaded lib: %s", str.c_str()); 639 | } 640 | // We shouldn't need to keep this handle anywhere, we can just throw it away without closing it. 641 | // This should hopefully force the library to stay open 642 | } 643 | } 644 | closedir(dir); 645 | } 646 | 647 | // List failed libs and try again 648 | // What we want to do here is while we haven't changed size of failed, we continue trying to load from failed. 649 | return try_load_recurse(failed, lib_loader); 650 | } 651 | 652 | // Responsible for setting up mods 653 | // Returns true on success (all mods loaded) false otherwise 654 | bool Modloader::try_setup_mods() { 655 | logpfm(ANDROID_LOG_INFO, "Constructing all mods!"); 656 | // Failed mods 657 | std::vector> failed; 658 | // Iterate over mods and attempt to construct them 659 | struct dirent* dp; 660 | DIR* dir = opendir_wrapper(modPath.c_str()); 661 | if (dir == nullptr) { 662 | logpfm(ANDROID_LOG_FATAL, "setup_mods(%s): %s: null dir! errno: %i, msg: %s", modloaderPath.data(), modPath.c_str(), errno, strerror(errno)); 663 | return false; 664 | } else { 665 | while ((dp = readdir(dir)) != NULL) { 666 | if (strlen(dp->d_name) > 3 && !strcmp(dp->d_name + strlen(dp->d_name) - 3, ".so")) { 667 | if (!create_mod(modPath, dp->d_name)) { 668 | // We create it with modPath, because create_mod checks the temp dir itself. 669 | // We want to make sure we create the mod with the correct path. 670 | failed.emplace_back(modPath, dp->d_name); 671 | } 672 | } 673 | } 674 | closedir(dir); 675 | } 676 | return try_load_recurse(failed, Modloader::create_mod); 677 | } 678 | 679 | void Modloader::construct_mods() noexcept { 680 | libIl2CppPath = modloaderPath + "/libil2cpp.so"; 681 | logpfm(ANDROID_LOG_DEBUG, "libil2cpp path: %s", libIl2CppPath.c_str()); 682 | // Protect at least once on startup 683 | protect(); 684 | // Open ourselves early to potentially fix some issues 685 | dlopen(NULL, RTLD_NOW|RTLD_GLOBAL); 686 | logpfm(ANDROID_LOG_DEBUG, "Constructing mods from modloader path: '%s'", modloaderPath.c_str()); 687 | bool modReady = true; 688 | if (!setDataDirs()) 689 | { 690 | logpfm(ANDROID_LOG_ERROR, "Unable to determine data directories."); 691 | modReady = false; 692 | } 693 | else if (mkpath(modPath, 0) != 0) 694 | { 695 | logpfm(ANDROID_LOG_ERROR, "Unable to access or create mod path at '%s'", modPath.c_str()); 696 | modReady = false; 697 | } 698 | else if (mkpath(libsPath, 0) != 0) 699 | { 700 | logpfm(ANDROID_LOG_ERROR, "Unable to access or create library path at: '%s'", libsPath.c_str()); 701 | modReady = false; 702 | } 703 | else if (mkpath(modTempPath, 0) != 0) 704 | { 705 | logpfm(ANDROID_LOG_ERROR, "Unable to access or create mod temporary path at '%s'", modTempPath.c_str()); 706 | modReady = false; 707 | } 708 | if (!modReady) { 709 | logpfm(ANDROID_LOG_ERROR, "QuestHook failed to initialize, mods will not load."); 710 | return; 711 | } 712 | 713 | // We need to clear out all .so files from our /data/data directory. 714 | // This happens because we want to make sure we don't take up ever increasing storage. 715 | struct dirent* dp; 716 | DIR* dir = opendir_wrapper(modTempPath.c_str()); 717 | if (dir == nullptr) { 718 | logpfm(ANDROID_LOG_ERROR, "Could not clear temp dir %s: null dir! errno: %i, msg: %s!", modTempPath.c_str(), errno, strerror(errno)); 719 | return; 720 | } else { 721 | while ((dp = readdir(dir)) != NULL) { 722 | if (strlen(dp->d_name) > 3 && !strcmp(dp->d_name + strlen(dp->d_name) - 3, ".so")) { 723 | auto str = modTempPath + dp->d_name; 724 | // Delete all .so files in our modTempPath 725 | if (unlink(str.c_str())) { 726 | logpfm(ANDROID_LOG_WARN, "Failed to delete: %s errno: %i, msg: %s", str.c_str(), errno, strerror(errno)); 727 | } else { 728 | logpfm(ANDROID_LOG_VERBOSE, "Deleted: %s", str.c_str()); 729 | } 730 | } 731 | } 732 | closedir(dir); 733 | } 734 | 735 | // Copy all mods and libs ahead of time, before any resolution occurs. 736 | bool success = copy(libsPath); 737 | if (!success) { 738 | logpfm(ANDROID_LOG_WARN, "One or more libs failed to copy! Continuing anyways..."); 739 | } 740 | success = copy(modPath); 741 | if (!success) { 742 | logpfm(ANDROID_LOG_WARN, "One or more mods failed to copy! Continuing anyways..."); 743 | return; 744 | } 745 | // Then, load all libs. 746 | // If we fail to load any, continue anyways. 747 | success = try_load_libs(); 748 | if (!success) { 749 | logpfm(ANDROID_LOG_WARN, "One or more libs failed to load! Continuing anyways..."); 750 | } 751 | success = try_setup_mods(); 752 | if (!success) { 753 | logpfm(ANDROID_LOG_WARN, "One or more mods failed to be setup! Continuing anyways..."); 754 | } 755 | 756 | Modloader::allConstructed = true; 757 | logpfm(ANDROID_LOG_INFO, "Done constructing mods!"); 758 | } 759 | 760 | static void* imagehandle; 761 | static void (*il2cppInit)(const char* domain_name); 762 | // Loads the mods after il2cpp has been initialized 763 | // Does not have to be offsetless since it is installed directly 764 | MAKE_HOOK(il2cppInitHook, NULL, void, const char* domain_name) 765 | { 766 | il2cppInitHook(domain_name); 767 | protect(); 768 | Modloader::load_mods(); 769 | } 770 | 771 | // Calls the init functions on all constructed mods 772 | void Modloader::init_mods() noexcept { 773 | if (!allConstructed) { 774 | logpfm(ANDROID_LOG_ERROR, "Tried to initalize mods, but they are not yet constructed!"); 775 | return; 776 | } 777 | logpfm(ANDROID_LOG_INFO, "Initializing all mods!"); 778 | 779 | for (auto& mod : mods) { 780 | mod.second.init_mod(); 781 | } 782 | 783 | logpfm(ANDROID_LOG_INFO, "Initialized all mods!"); 784 | logpfm(ANDROID_LOG_VERBOSE, "dlopening libil2cpp.so: %s", libIl2CppPath.c_str()); 785 | 786 | imagehandle = dlopen(libIl2CppPath.c_str(), RTLD_LOCAL | RTLD_LAZY); 787 | // On startup, we also want to protect everything, and ensure we have read/write 788 | protect(); 789 | if (imagehandle == NULL) { 790 | logpfm(ANDROID_LOG_FATAL, "Could not dlopen libil2cpp.so! Not calling load on mods!"); 791 | return; 792 | } 793 | *(void**)(&il2cppInit) = dlsym(imagehandle, "il2cpp_init"); 794 | logpfm(ANDROID_LOG_INFO, "Loaded: il2cpp_init (%p)", il2cppInit); 795 | if (il2cppInit) { 796 | INSTALL_HOOK_DIRECT(il2cppInitHook, il2cppInit); 797 | } else { 798 | logpfm(ANDROID_LOG_ERROR, "Failed to dlsym il2cpp_init!"); 799 | } 800 | } 801 | 802 | // Calls the load functions on all constructed mods 803 | void Modloader::load_mods() noexcept { 804 | if (!Modloader::allConstructed) { 805 | logpfm(ANDROID_LOG_ERROR, "Tried to load mods, but they are not yet constructed!"); 806 | return; 807 | } 808 | logpfm(ANDROID_LOG_INFO, "Loading all mods!"); 809 | 810 | for (auto& mod : mods) { 811 | // Add each mod to the loadingMods list immediately before so it is not double loaded 812 | if (!mod.second.get_loaded()) { 813 | loadingMods.insert(mod.second); 814 | mod.second.load_mod(); 815 | } else { 816 | logpfm(ANDROID_LOG_VERBOSE, "Mod: %s (id: %s) already loaded! Not loading again.", mod.first.c_str(), mod.second.info.id.c_str()); 817 | } 818 | } 819 | 820 | logpfm(ANDROID_LOG_INFO, "Loaded all mods!"); 821 | } 822 | 823 | // Returns the libil2cpp.so path 824 | const std::string Modloader::getLibIl2CppPath() { 825 | return libIl2CppPath; 826 | } 827 | 828 | // Returns the application ID 829 | const std::string Modloader::getApplicationId() { 830 | return applicationId; 831 | } 832 | 833 | // Returns whether all mods have been constructed or not 834 | bool Modloader::getAllConstructed() { 835 | return allConstructed; 836 | } 837 | 838 | const std::unordered_map Modloader::getMods() { 839 | std::unordered_map temp; 840 | for (auto& m : mods) { 841 | temp.try_emplace(m.first, m.second); 842 | } 843 | return temp; 844 | } 845 | 846 | const ModloaderInfo Modloader::getInfo() { 847 | return info; 848 | } 849 | 850 | void Modloader::setInfo(ModloaderInfo& info) { 851 | Modloader::info = info; 852 | } 853 | 854 | bool Modloader::requireMod(std::string_view id) { 855 | if (!allConstructed) { 856 | // Do nothing if not all mods are constructed 857 | return false; 858 | } 859 | logpfm(ANDROID_LOG_VERBOSE, "Requiring mod: %s", id.data()); 860 | auto m = mods.find(id.data()); 861 | if (m != mods.end()) { 862 | logpfm(ANDROID_LOG_VERBOSE, "Found matching mod!"); 863 | auto loading = loadingMods.find(m->second); 864 | if (loading != loadingMods.end()) { 865 | // If the mod is in our loadingMods, return early 866 | logpfm(ANDROID_LOG_VERBOSE, "Mod already in loadingMods (loading or is loaded!)"); 867 | return true; 868 | } 869 | if (!m->second.get_loaded()) { 870 | // If the mod isn't already loaded, load it. 871 | logpfm(ANDROID_LOG_VERBOSE, "Loading mod..."); 872 | loadingMods.insert(m->second); 873 | m->second.load_mod(); 874 | } 875 | return true; 876 | } 877 | return false; 878 | } 879 | bool Modloader::requireMod(const ModInfo& info) { 880 | return Modloader::requireMod(info.id, info.version); 881 | } 882 | bool Modloader::requireMod(std::string_view id, std::string_view version) { 883 | // Find the matching mod in our list of constructed mods 884 | // If it doesn't exist, exit immediately. 885 | // If we find that a mod that is being required requires a mod that requires us, we have deadlock 886 | // So, in such a case, we would like to simply return immediately if we detect that this is the case. 887 | // loadingMods is a vector of all mods that are being loaded at the moment. 888 | // If the mod that we are attempting to require is already in this list, we return immediately. 889 | // Otherwise, we invoke the mod.load function on that mod and let it run to completion. 890 | if (!allConstructed) { 891 | // Do nothing if not all mods are constructed 892 | return false; 893 | } 894 | logpfm(ANDROID_LOG_VERBOSE, "Requiring mod: %s", id.data()); 895 | auto m = mods.find(id.data()); 896 | if (m != mods.end()) { 897 | logpfm(ANDROID_LOG_VERBOSE, "Found matching mod!"); 898 | // Ensure version matches (a version match should be specific to the version for now) 899 | // Eventually, this should check if there exists a version >= the provided one. 900 | // TODO: ^ 901 | if (m->second.info.version != version) { 902 | logpfm(ANDROID_LOG_VERBOSE, "Version mismatch: desired: %s, actual: %s", version.data(), m->second.info.version.c_str()); 903 | return false; 904 | } 905 | auto loading = loadingMods.find(m->second); 906 | if (loading != loadingMods.end()) { 907 | // If the mod is in our loadingMods, return early 908 | logpfm(ANDROID_LOG_VERBOSE, "Mod already in loadingMods (loading or is loaded!)"); 909 | return true; 910 | } 911 | if (!m->second.get_loaded()) { 912 | // If the mod isn't already loaded, load it. 913 | logpfm(ANDROID_LOG_VERBOSE, "Loading mod..."); 914 | loadingMods.insert(m->second); 915 | m->second.load_mod(); 916 | } 917 | return true; 918 | } 919 | return false; 920 | } 921 | #pragma endregion 922 | 923 | // Mod functionality 924 | #pragma region Mod Functions 925 | bool Mod::get_loaded() const { 926 | return loaded; 927 | } 928 | 929 | // Calls the init() function on the mod, if it exists 930 | // This will be before il2cpp functionality is available 931 | // Called in preload 932 | void Mod::init_mod() { 933 | logpf(ANDROID_LOG_INFO, "Initializing mod: %s, id: %s, version: %s, handle: %p", pathName.c_str(), info.id.c_str(), info.version.c_str(), handle); 934 | if (!init_loaded) { 935 | *(void**)(&init_func) = dlsym(handle, "init"); 936 | init_loaded = true; 937 | } 938 | logpf(ANDROID_LOG_VERBOSE, "Found init function: %p", init_func); 939 | if (init_loaded && init_func) { 940 | Dl_info info; 941 | dladdr((void *)init_func, &info); 942 | logpf(ANDROID_LOG_VERBOSE, "dladdr of init function base: %p name: %s", info.dli_fbase, info.dli_sname); 943 | init_func(); 944 | } 945 | } 946 | 947 | // Calls the load() function on the mod, if it exists 948 | // This will be after il2cpp functionality is available 949 | // Called immediately after il2cpp_init 950 | void Mod::load_mod() { 951 | logpf(ANDROID_LOG_INFO, "Loading mod: %s, id: %s, version: %s", pathName.c_str(), info.id.c_str(), info.version.c_str()); 952 | if (!load_loaded) { 953 | *(void**)(&load_func) = dlsym(handle, "load"); 954 | load_loaded = true; 955 | } 956 | if (load_loaded && load_func) { 957 | load_func(); 958 | } 959 | loaded = true; 960 | } 961 | #pragma endregion 962 | 963 | static void init_all_mods() { 964 | Modloader::init_mods(); 965 | } 966 | 967 | extern "C" void modloader_preload() noexcept { 968 | logpf(ANDROID_LOG_VERBOSE, "modloader_preload called (should be really early)"); 969 | logpf(ANDROID_LOG_INFO, "Welcome!"); 970 | } 971 | 972 | extern "C" JNINativeInterface modloader_main(JavaVM* v, JNIEnv* env, std::string_view loadSrc) noexcept { 973 | logpf(ANDROID_LOG_VERBOSE, "modloader_main called with vm: 0x%p, env: 0x%p, loadSrc: %s", v, env, loadSrc.data()); 974 | 975 | auto iface = jni::interface::make_passthrough_interface(&env->functions); 976 | 977 | // Create libil2cpp path string. Should be in the same path as loadSrc (since libmodloader.so needs to be in the same path) 978 | char *dirPath = dirname(loadSrc.data()); 979 | if (dirPath == NULL) { 980 | logpf(ANDROID_LOG_FATAL, "loadSrc cannot be converted to a valid directory!"); 981 | return iface; 982 | } 983 | // TODO: Check if path exists before setting it and assuming it is valid 984 | ModloaderInfo info; 985 | info.name = "MainModloader"; 986 | info.tag = "main-modloader"; 987 | modloaderEnv = env; 988 | Modloader::setInfo(info); 989 | Modloader::modloaderPath = dirPath; 990 | Modloader::construct_mods(); 991 | 992 | return iface; 993 | } 994 | 995 | extern "C" void modloader_accept_unity_handle(void* uhandle) noexcept { 996 | logpf(ANDROID_LOG_VERBOSE, "modloader_accept_unity_handle called with uhandle: 0x%p", uhandle); 997 | 998 | init_all_mods(); 999 | } 1000 | 1001 | #pragma region C API 1002 | extern "C" const char* get_info_id(ModInfo* instance) { 1003 | return instance->id.c_str(); 1004 | } 1005 | extern "C" void set_info_id(ModInfo* instance, const char* name) { 1006 | instance->id = name; 1007 | } 1008 | extern "C" const char* get_info_version(ModInfo* instance) { 1009 | return instance->version.c_str(); 1010 | } 1011 | extern "C" void set_info_version(ModInfo* instance, const char* version) { 1012 | instance->version = version; 1013 | } 1014 | extern "C" const char* get_modloader_name(ModloaderInfo* instance) { 1015 | return instance->name.c_str(); 1016 | } 1017 | extern "C" const char* get_modloader_tag(ModloaderInfo* instance) { 1018 | return instance->tag.c_str(); 1019 | } 1020 | 1021 | CHECK_MODLOADER_PRELOAD; 1022 | CHECK_MODLOADER_MAIN; 1023 | CHECK_MODLOADER_ACCEPT_UNITY_HANDLE; -------------------------------------------------------------------------------- /libmodloader/src/modloader.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | struct ModInfo { 12 | std::string id; 13 | std::string version; 14 | }; 15 | 16 | struct ModloaderInfo { 17 | std::string name; 18 | std::string tag; 19 | }; 20 | 21 | // C API for nice rust linkage 22 | // Returned C strings are NOT copied, input C strings ARE copied. 23 | extern "C" const char* get_info_id(ModInfo* instance); 24 | extern "C" void set_info_id(ModInfo* instance, const char* name); 25 | extern "C" const char* get_info_version(ModInfo* instance); 26 | extern "C" void set_info_version(ModInfo* instance, const char* version); 27 | extern "C" const char* get_modloader_name(ModloaderInfo* instance); 28 | extern "C" const char* get_modloader_tag(ModloaderInfo* instance); 29 | 30 | class Mod; 31 | 32 | // Different copy of Modloader for the modloader itself vs. as a header 33 | #ifndef MODLOADER_DEFINED 34 | class Modloader { 35 | public: 36 | // Returns the libil2cpp.so path, should only be called AFTER mods have been constructed 37 | // Check Modloader::getAllConstructed() for validity 38 | static const std::string getLibIl2CppPath(); 39 | // Returns the application id, should only be called AFTER mods have been constructed 40 | // Check Modloader::getAllConstructed() for validity 41 | // Example return: com.beatgames.beatsaber 42 | static const std::string getApplicationId(); 43 | // Returns the path the modloader is located in, should only be called AFTER mods have been constructed 44 | // Check Modloader::getAllConstructed() for validity 45 | // Example return: /data/app/com.beatgames.beatsaber-HASH/lib/arm64 46 | static const std::string getModloaderPath(); 47 | // Returns the path the destination directory for dlopened mods/libs, should only be called AFTER mods have been constructed 48 | // Check Modloader::getAllConstructed() for validity 49 | // Example return: /data/data/com.beatgames.beatsaber 50 | static const std::string getDestinationPath(); 51 | // Returns the JNI for further use. 52 | static JNIEnv* getJni(); 53 | // Returns whether all mods on this modloader have been loaded or not 54 | static bool getAllConstructed(); 55 | // Modloader info 56 | static const ModloaderInfo getInfo(); 57 | // A map of id to mods managed by this modloader 58 | static const std::unordered_map getMods(); 59 | // Require another mod to be loaded, should only be called AFTER mods have been constructed 60 | // Will block until the required mod is loaded, if it exists. 61 | // If it does not exist, or this was called before mod loading was complete, returns immediately. 62 | static bool requireMod(const ModInfo&); 63 | // Same as requireMod(const ModInfo&) except uses an ID and a version string 64 | static bool requireMod(std::string_view id, std::string_view version); 65 | // Require another mod to be loaded, should only be called AFTER mods have been constructed 66 | // Will block until all versions of the specified id are loaded, if any exist. 67 | // If none exist, or if this was called before mod loading was complete (in the case of a recursive load) returns immediately. 68 | static bool requireMod(std::string_view id); 69 | }; 70 | #endif 71 | 72 | // Provides metadata for each mod 73 | class Mod { 74 | friend class Modloader; 75 | public: 76 | Mod(std::string_view name_, std::string_view path, ModInfo info_, void *handle_) : name(name_), pathName(path), info(info_), handle(handle_) {} 77 | const std::string name; 78 | const std::string pathName; 79 | const ModInfo info; 80 | bool get_loaded() const; 81 | bool operator==(const Mod& m) const { 82 | return info.id == m.info.id && info.version == m.info.version; 83 | } 84 | private: 85 | void init_mod(); 86 | void load_mod(); 87 | bool loaded = false; 88 | void *handle = nullptr; 89 | bool init_loaded = false; 90 | void (*init_func)(void) = NULL; 91 | bool load_loaded = false; 92 | void (*load_func)(void) = NULL; 93 | }; 94 | 95 | namespace std { 96 | template<> 97 | struct hash { 98 | size_t operator()(const Mod& m) const { 99 | return std::hash{}(m.info.id) ^ (std::hash{}(m.info.version) << 1); 100 | } 101 | }; 102 | } -------------------------------------------------------------------------------- /libmodloader/src/protection.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "log.hpp" 4 | #include 5 | #include 6 | 7 | 8 | void protect() { 9 | // If we look at /proc/self/maps we can see most of what we would probably care about. 10 | // For each of the things in /proc/self/maps, we probably want to look at the addresses of them and attempt to mprotect with 11 | // a READ as well as an execute. 12 | logpf(ANDROID_LOG_VERBOSE, "Protecting memory from /proc/self/maps!"); 13 | std::ifstream procMap("/proc/self/maps"); 14 | std::string line; 15 | while (std::getline(procMap, line)) { 16 | auto idx = line.find_first_of('-'); 17 | if (idx == std::string::npos) { 18 | logpf(ANDROID_LOG_ERROR, "Could not find '-' in line: %s", line.c_str()); 19 | continue; 20 | } 21 | auto startAddr = std::stoul(line.substr(0, idx), nullptr, 16); 22 | auto spaceIdx = line.find_first_of(' '); 23 | if (spaceIdx == std::string::npos) { 24 | logpf(ANDROID_LOG_ERROR, "Could not find ' ' in line: %s", line.c_str()); 25 | continue; 26 | } 27 | auto endAddr = std::stoul(line.substr(idx + 1, spaceIdx - idx - 1), nullptr, 16); 28 | // Permissions are 4 characters 29 | auto perms = line.substr(spaceIdx + 1, 4); 30 | if (perms.find('r') == std::string::npos && perms.find('x') != std::string::npos && perms.find('w') == std::string::npos) { 31 | logpf(ANDROID_LOG_DEBUG, "Line: %s", line.c_str()); 32 | // If we have execute, and we do not have read, and we do not have write, we need to protect. 33 | logpf(ANDROID_LOG_INFO, "Protecting memory: 0x%lx - 0x%lx with perms: %s to: +rx", startAddr, endAddr, perms.c_str()); 34 | if (mprotect(reinterpret_cast(startAddr), endAddr - startAddr, PROT_EXEC | PROT_READ) != 0) { 35 | logpf(ANDROID_LOG_ERROR, "Protection failed! errno: %s", strerror(errno)); 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /libmodloader/src/tinynew.cpp: -------------------------------------------------------------------------------- 1 | /* tinynew.cpp 2 | 3 | Overrides operators new and delete 4 | globally to reduce code size. 5 | 6 | Public domain, use however you wish. 7 | If you really need a license, consider it MIT: 8 | http://www.opensource.org/licenses/mit-license.php 9 | 10 | - Eric Agan 11 | Elegant Invention 12 | */ 13 | 14 | #include 15 | #include "log.hpp" 16 | #include "modloader/mem.hpp" 17 | 18 | [[nodiscard]] 19 | void* operator new(std::size_t size) { 20 | return malloc(size); 21 | } 22 | 23 | [[nodiscard]] 24 | void* operator new[](std::size_t size) { 25 | return malloc(size); 26 | } 27 | 28 | void operator delete(void* ptr) noexcept { 29 | free(ptr); 30 | } 31 | 32 | void operator delete[](void* ptr) noexcept { 33 | free(ptr); 34 | } 35 | 36 | /* Optionally you can override the 'nothrow' versions as well. 37 | This is useful if you want to catch failed allocs with your 38 | own debug code, or keep track of heap usage for example, 39 | rather than just eliminate exceptions. 40 | */ 41 | 42 | [[nodiscard]] 43 | void* operator new(std::size_t size, const std::nothrow_t&) noexcept { 44 | return malloc(size); 45 | } 46 | 47 | [[nodiscard]] 48 | void* operator new[](std::size_t size, const std::nothrow_t&) noexcept { 49 | return malloc(size); 50 | } 51 | 52 | void operator delete(void* ptr, const std::nothrow_t&) noexcept { 53 | free(ptr); 54 | } 55 | 56 | void operator delete[](void* ptr, const std::nothrow_t&) noexcept { 57 | free(ptr); 58 | } 59 | 60 | /* 61 | Aligned new/delete 62 | */ 63 | 64 | [[nodiscard]] 65 | void* operator new(std::size_t size, std::align_val_t align) { 66 | return memalign(static_cast(align), size); 67 | } 68 | 69 | [[nodiscard]] 70 | void* operator new[](std::size_t size, std::align_val_t align) { 71 | return memalign(static_cast(align), size); 72 | } 73 | 74 | void operator delete(void* ptr, std::align_val_t align) noexcept { 75 | free(ptr); 76 | } 77 | 78 | void operator delete[](void* ptr, std::align_val_t align) noexcept { 79 | free(ptr); 80 | } 81 | 82 | /* 83 | Additional stuff to help reduce binary size 84 | */ 85 | extern "C" void __cxa_pure_virtual() { 86 | logp(ANDROID_LOG_ERROR, "Pure virtual function call"); 87 | while (1); 88 | } -------------------------------------------------------------------------------- /patch.ps1: -------------------------------------------------------------------------------- 1 | ."$PSScriptRoot/build.ps1" 2 | 3 | $apkPath = Get-Content "$PSScriptRoot/apkpath.txt" 4 | $apkDir = [System.IO.Path]::GetDirectoryName($apkPath) 5 | 6 | $bindir = "$PSScriptRoot/libmodloader/libs/arm64-v8a/" 7 | 8 | $fsFiles = [System.Collections.ArrayList]@() 9 | $targetFiles = [System.Collections.ArrayList]@() 10 | 11 | foreach ($item in Get-ChildItem $bindir) { 12 | $ignore = $fsFiles.Add($item.FullName) 13 | $basename = $item.Name 14 | $ignore = $targetFiles.Add("lib/arm64-v8a/$basename") 15 | } 16 | 17 | ."$PSScriptRoot/tools/apkmod.ps1" -ApkPath $apkPath -ApkOutPath "$apkDir/patched.apk" ` 18 | -AddFile $fsFiles -ArcTarget $targetFiles -------------------------------------------------------------------------------- /qpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "sharedDir": "shared", 3 | "dependenciesDir": "extern", 4 | "info": { 5 | "name": "modloader", 6 | "id": "modloader", 7 | "version": "1.0.2", 8 | "url": "https://github.com/sc2ad/QuestLoader", 9 | "additionalData": { 10 | "branchName": "staticModloader", 11 | "soLink": "https://github.com/sc2ad/QuestLoader/releases/download/v1.0.0/libmodloader.so", 12 | "overrideSoName": "libmodloader.so" 13 | } 14 | }, 15 | "dependencies": [], 16 | "additionalData": {} 17 | } -------------------------------------------------------------------------------- /shared/modloader.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | struct ModInfo { 12 | std::string id; 13 | std::string version; 14 | }; 15 | 16 | struct ModloaderInfo { 17 | std::string name; 18 | std::string tag; 19 | }; 20 | 21 | // C API for nice rust linkage 22 | // Returned C strings are NOT copied, input C strings ARE copied. 23 | extern "C" const char* get_info_id(ModInfo* instance); 24 | extern "C" void set_info_id(ModInfo* instance, const char* name); 25 | extern "C" const char* get_info_version(ModInfo* instance); 26 | extern "C" void set_info_version(ModInfo* instance, const char* version); 27 | extern "C" const char* get_modloader_name(ModloaderInfo* instance); 28 | extern "C" const char* get_modloader_tag(ModloaderInfo* instance); 29 | 30 | class Mod; 31 | 32 | // Different copy of Modloader for the modloader itself vs. as a header 33 | #ifndef MODLOADER_DEFINED 34 | class Modloader { 35 | public: 36 | // Returns the libil2cpp.so path, should only be called AFTER mods have been constructed 37 | // Check Modloader::getAllConstructed() for validity 38 | static const std::string getLibIl2CppPath(); 39 | // Returns the application id, should only be called AFTER mods have been constructed 40 | // Check Modloader::getAllConstructed() for validity 41 | // Example return: com.beatgames.beatsaber 42 | static const std::string getApplicationId(); 43 | // Returns the path the modloader is located in, should only be called AFTER mods have been constructed 44 | // Check Modloader::getAllConstructed() for validity 45 | // Example return: /data/app/com.beatgames.beatsaber-HASH/lib/arm64 46 | static const std::string getModloaderPath(); 47 | // Returns the path the destination directory for dlopened mods/libs, should only be called AFTER mods have been constructed 48 | // Check Modloader::getAllConstructed() for validity 49 | // Example return: /data/data/com.beatgames.beatsaber 50 | static const std::string getDestinationPath(); 51 | // Returns the JNI for further use. 52 | static JNIEnv* getJni(); 53 | // Returns whether all mods on this modloader have been loaded or not 54 | static bool getAllConstructed(); 55 | // Modloader info 56 | static const ModloaderInfo getInfo(); 57 | // A map of id to mods managed by this modloader 58 | static const std::unordered_map getMods(); 59 | // Require another mod to be loaded, should only be called AFTER mods have been constructed 60 | // Will block until the required mod is loaded, if it exists. 61 | // If it does not exist, or this was called before mod loading was complete, returns immediately. 62 | static bool requireMod(const ModInfo&); 63 | // Same as requireMod(const ModInfo&) except uses an ID and a version string 64 | static bool requireMod(std::string_view id, std::string_view version); 65 | // Require another mod to be loaded, should only be called AFTER mods have been constructed 66 | // Will block until all versions of the specified id are loaded, if any exist. 67 | // If none exist, or if this was called before mod loading was complete (in the case of a recursive load) returns immediately. 68 | static bool requireMod(std::string_view id); 69 | }; 70 | #endif 71 | 72 | // Provides metadata for each mod 73 | class Mod { 74 | friend class Modloader; 75 | public: 76 | Mod(std::string_view name_, std::string_view path, ModInfo info_, void *handle_) : name(name_), pathName(path), info(info_), handle(handle_) {} 77 | const std::string name; 78 | const std::string pathName; 79 | const ModInfo info; 80 | bool get_loaded() const; 81 | bool operator==(const Mod& m) const { 82 | return info.id == m.info.id && info.version == m.info.version; 83 | } 84 | private: 85 | void init_mod(); 86 | void load_mod(); 87 | bool loaded = false; 88 | void *handle = nullptr; 89 | bool init_loaded = false; 90 | void (*init_func)(void) = NULL; 91 | bool load_loaded = false; 92 | void (*load_func)(void) = NULL; 93 | }; 94 | 95 | namespace std { 96 | template<> 97 | struct hash { 98 | size_t operator()(const Mod& m) const { 99 | return std::hash{}(m.info.id) ^ (std::hash{}(m.info.version) << 1); 100 | } 101 | }; 102 | } -------------------------------------------------------------------------------- /tools/apkmod.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory=$True)][string]$apkPath, 3 | [string]$apkOutPath = $apkPath, 4 | [Parameter(Mandatory=$True)][string[]]$addFile, 5 | [Parameter(Mandatory=$True)][string[]]$arcTarget, 6 | [switch]$noOverwrite = $false 7 | ) 8 | 9 | function Select-Zip { 10 | [CmdletBinding()] 11 | Param( 12 | $First, 13 | $Second, 14 | $ResultSelector = { ,$args } 15 | ) 16 | 17 | [System.Linq.Enumerable]::Zip($First, $Second, [Func[Object, Object, Object[]]]$ResultSelector) 18 | } 19 | 20 | $overwrite = (-not $noOverwrite) 21 | 22 | if ($apkPath -ne $apkOutPath) { 23 | Copy-Item $apkPath $apkOutPath 24 | $apkPath = $apkOutPath 25 | } 26 | 27 | $apkifierRoot = "$PSScriptRoot/Apkifier" 28 | $apkifierDir = "$apkifierRoot/bin/Release/net462" 29 | $apkifierPath = "$apkifierDir/Apkifier.dll" 30 | 31 | if (-not (Test-Path $apkifierPath -PathType Leaf)) { 32 | Write-Host "Compiling apkifier" 33 | 34 | $cwd = Get-Location 35 | Set-Location $apkifierRoot 36 | dotnet build -c Release 37 | Set-Location $cwd 38 | } 39 | 40 | if ($addFile.Count -ne $arcTarget.Count) { 41 | Write-Host "Length of AddFile must equal length of ArcTarget" 42 | Exit 43 | } 44 | 45 | Add-Type -Path $apkifierPath 46 | 47 | $apk = New-Object "Emulamer.Utils.Apkifier" -ArgumentList ($apkPath) 48 | 49 | foreach ($item in Select-Zip -First $addFile -Second $arcTarget) { 50 | Write-Host "Adding $($item[0]) as $($item[1])" 51 | $apk.Write($item[0], $item[1], $overwrite, $true) 52 | } 53 | 54 | Write-Host "Packing and signing" 55 | 56 | $apk.Dispose() -------------------------------------------------------------------------------- /tools/apktool.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sc2ad/QuestLoader/cf8408b9200bcc85d38b0a356e1cf831a14c9747/tools/apktool.jar -------------------------------------------------------------------------------- /tools/jadx-gui-1.0.0.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sc2ad/QuestLoader/cf8408b9200bcc85d38b0a356e1cf831a14c9747/tools/jadx-gui-1.0.0.exe --------------------------------------------------------------------------------