get rid of dobby

looks like chaos has been waiting for us
This commit is contained in:
Chloe committed 2026-02-25 21:32:34 -07:00
1 parent 1a68cc5eb9
commit 5caa533a4a
5 files changed
+221 -198

No files matched your search

+1 -1
View File
@@ -29,7 +29,7 @@ android {
@Suppress("UnstableApiUsage") @Suppress("UnstableApiUsage")
externalNativeBuild { externalNativeBuild {
cmake { cmake {
arguments("-DUSE_TULIPHOOK:BOOL=OFF", "-DANDROID_STL=c++_shared") arguments("-DANDROID_STL=c++_shared")
} }
} }
+5 -36
View File
@@ -7,41 +7,10 @@ add_library(launcherfix SHARED launcher-fix.cpp)
target_compile_features(launcherfix PUBLIC cxx_std_20) target_compile_features(launcherfix PUBLIC cxx_std_20)
set_target_properties(launcherfix PROPERTIES CXX_EXTENSIONS OFF) set_target_properties(launcherfix PROPERTIES CXX_EXTENSIONS OFF)
include(FetchContent) set_target_properties(launcherfix PROPERTIES
INTERPROCEDURAL_OPTIMIZATION ON
option(USE_TULIPHOOK "enables choice of hooking library" OFF) C_VISIBILITY_PRESET hidden
if (USE_TULIPHOOK) CXX_VISIBILITY_PRESET hidden
FetchContent_Declare(TulipHook )
GIT_REPOSITORY https://github.com/geode-sdk/TulipHook.git
GIT_TAG v3.1.4
)
set(TULIP_LINK_SOURCE ON CACHE INTERNAL "")
FetchContent_MakeAvailable(TulipHook)
target_compile_definitions(launcherfix PRIVATE -DUSE_TULIPHOOK=1)
target_link_libraries(launcherfix PRIVATE TulipHook TulipHookInclude)
else()
FetchContent_Declare(dobby
# this is the last version that builds on android at all
GIT_REPOSITORY https://github.com/jmpews/Dobby
GIT_TAG 0932d69c320e786672361ab53825ba8f4245e9d3
)
FetchContent_GetProperties(dobby)
if(NOT dobby_POPULATED)
FetchContent_Populate(dobby)
# fixes Dobby build
# add_compile_definitions(typeof=__typeof__)
set(DOBBY_DEBUG OFF CACHE INTERNAL "" FORCE)
set(DOBBY_GENERATE_SHARED OFF CACHE INTERNAL "" FORCE)
add_subdirectory("${dobby_SOURCE_DIR}" ${dobby_BINARY_DIR} EXCLUDE_FROM_ALL)
target_include_directories(launcherfix PRIVATE ${dobby_SOURCE_DIR}/include)
target_link_libraries(launcherfix PRIVATE dobby)
endif()
endif()
target_link_libraries(launcherfix PRIVATE log) target_link_libraries(launcherfix PRIVATE log)
+209 -151
View File
@@ -6,16 +6,11 @@
#include <cstdint> #include <cstdint>
#include <android/log.h> #include <android/log.h>
#include <link.h> #include <link.h>
#include <span>
#include <unistd.h>
#include <sys/mman.h>
#ifndef DISABLE_LAUNCHER_FIX #define LOG_TAG "GeodeLauncher-Fix"
#ifdef USE_TULIPHOOK
#include <tulip/TulipHook.hpp>
#else
#include <dobby.h>
#endif
#endif
class DataPaths { class DataPaths {
public: public:
@@ -23,6 +18,8 @@ public:
std::string data_path{}; std::string data_path{};
std::string load_symbols_from{}; std::string load_symbols_from{};
bool patch_exceptions{};
static DataPaths& get_instance() { static DataPaths& get_instance() {
static auto paths_instance = DataPaths(); static auto paths_instance = DataPaths();
return paths_instance; return paths_instance;
@@ -32,6 +29,22 @@ private:
DataPaths() {} DataPaths() {}
}; };
struct ElfDynamicState {
std::uint32_t* hash_table{};
char* str_table{};
ElfW(Sym)* sym_table{};
void* plt_table{};
std::uint64_t plt_table_size_bytes{};
bool plt_table_rela{};
std::uint64_t soname_idx{};
bool is_empty() const {
return !hash_table || !str_table || !sym_table || !plt_table;
}
};
extern "C" extern "C"
JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_setDataPath( JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_setDataPath(
JNIEnv *env, JNIEnv *env,
@@ -74,9 +87,30 @@ JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_setOriginalDataPath(
env->ReleaseStringUTFChars(data_path, data_path_str); env->ReleaseStringUTFChars(data_path, data_path_str);
} }
extern "C"
JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_enableExceptionsRenaming(JNIEnv*, jobject) {
DataPaths::get_instance().patch_exceptions = true;
}
// copied from tuliphook source :)
bool set_mprotect(std::uintptr_t address, size_t size, uint32_t protection) {
auto const page_size = getpagesize();
auto const page_mask = page_size - 1;
auto const ptr = reinterpret_cast<uintptr_t>(address);
auto const aligned_ptr = ptr & (~page_mask);
auto const begin_size = ptr - aligned_ptr;
auto const page_count = (begin_size + size + page_mask) / page_size;
auto const aligned_size = page_count * page_size;
auto status = mprotect(reinterpret_cast<void*>(aligned_ptr), aligned_size, protection);
return status == 0;
}
// i copied this function from the android linker // i copied this function from the android linker
std::uint32_t elfhash(const char *_name) { std::uint32_t elfhash(const char *_name) {
const unsigned char *name = reinterpret_cast<const unsigned char*>(_name); const auto *name = reinterpret_cast<const unsigned char*>(_name);
std::uint32_t h = 0, g; std::uint32_t h = 0, g;
while (*name) { while (*name) {
@@ -108,82 +142,162 @@ bool patch_symbol(std::uint32_t* hash_table, char* str_table, ElfW(Sym)* sym_tab
if (ELF_ST_BIND(sym->st_info) == STB_GLOBAL && ELF_ST_TYPE(sym->st_info) == STT_FUNC) { if (ELF_ST_BIND(sym->st_info) == STB_GLOBAL && ELF_ST_TYPE(sym->st_info) == STT_FUNC) {
// we found it! now go rename the symbol // we found it! now go rename the symbol
#ifdef USE_TULIPHOOK __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "Disabling symbol at %p (%s)", sym_name, sym_name);
auto res = tulip::hook::writeMemory(sym_name, SYMBOL_PATCH_VALUE.data(), SYMBOL_PATCH_VALUE.size());
if (!res) { if (!set_mprotect(reinterpret_cast<std::uintptr_t>(sym_name), SYMBOL_PATCH_VALUE.size(), PROT_READ | PROT_WRITE)) {
__android_log_print(ANDROID_LOG_WARN, "GeodeLauncher-fix", "failed to patch symbol: %s", res.unwrapErr().c_str()); __android_log_print(ANDROID_LOG_WARN, LOG_TAG, "failed to patch symbol: could not set writable flag");
return false; return false;
}
memcpy(sym_name, SYMBOL_PATCH_VALUE.data(), SYMBOL_PATCH_VALUE.size());
if (!set_mprotect(reinterpret_cast<std::uintptr_t>(sym_name), SYMBOL_PATCH_VALUE.size(), PROT_READ)) {
__android_log_print(ANDROID_LOG_WARN, LOG_TAG, "failed to patch symbol: could not remove writable flag");
return false;
} }
#else
DobbyCodePatch(reinterpret_cast<void*>(sym_name), SYMBOL_PATCH_VALUE.data(), SYMBOL_PATCH_VALUE.size());
#endif
return true; return true;
} }
} }
} }
__android_log_print(ANDROID_LOG_WARN, "GeodeLauncher-fix", "could not find symbol %s to patch (hash: %u)", orig_name, hash); __android_log_print(ANDROID_LOG_WARN, LOG_TAG, "could not find symbol %s to patch (hash: %u)", orig_name, hash);
return false; return false;
} }
std::vector<std::string> get_symbols_listing() { // this is every function that i thought would be relevant
auto symbol_path = DataPaths::get_instance().load_symbols_from; constexpr std::array remove_symbols{
if (symbol_path.empty()) { "__gxx_personality_v0",
// this is every function that i thought would be relevant "__cxa_throw",
return { "__cxa_rethrow",
"__gxx_personality_v0", "__cxa_allocate_exception",
"__cxa_throw", "__cxa_end_catch",
"__cxa_rethrow", "__cxa_begin_catch",
"__cxa_allocate_exception", "__cxa_guard_abort",
"__cxa_end_catch", "__cxa_guard_acquire",
"__cxa_begin_catch", "__cxa_guard_release",
"__cxa_guard_abort", "__cxa_free_exception"
"__cxa_guard_acquire",
"__cxa_guard_release",
"__cxa_free_exception"
// android 10's libc doesn't have these symbols // android 10's libc doesn't have these symbols
// and the ndk is forbidden from exporting them!! so keep it for old mods... // and the ndk is forbidden from exporting them!! so keep it for old mods...
// "_Unwind_RaiseException", // "_Unwind_RaiseException",
// "_Unwind_Resume" // "_Unwind_Resume"
}; };
bool perform_symbol_patches(dl_phdr_info* info, const ElfDynamicState& state) {
// patch symbol names
auto so_name = state.str_table + state.soname_idx;
if (strcmp(so_name, "libcocos2dcpp.so") != 0) {
__android_log_print(ANDROID_LOG_WARN, LOG_TAG, "DT_SONAME gives us %s! that's not good", so_name);
} }
std::ifstream symbol_file{symbol_path}; auto has_error = false;
if (!symbol_file) { for (const auto& symbol : remove_symbols) {
__android_log_print(ANDROID_LOG_WARN, "GeodeLauncher-fix", "failed to read symbol file at %s", symbol_path.c_str()); if (!patch_symbol(state.hash_table, state.str_table, state.sym_table, symbol)) {
return {}; has_error = true;
}
std::vector<std::string> symbols_list{};
std::string current_line{};
while (std::getline(symbol_file, current_line)) {
if (!current_line.empty()) {
symbols_list.push_back(current_line);
} }
} }
return symbols_list; return !has_error;
}
#if defined(__LP64__)
#define ELF_R_SYM(i) ELF64_R_SYM(i)
#define ELF_R_TYPE(i) ELF64_R_TYPE(i)
#else
#define ELF_R_SYM(i) ELF32_R_SYM(i)
#define ELF_R_TYPE(i) ELF32_R_TYPE(i)
#endif
template <typename T, typename... Types>
concept is_one_of = (std::same_as<T, Types> || ...);
template <typename T> requires is_one_of<T, ElfW(Rela), ElfW(Rel)>
bool rewrite_symbol_reloc(std::uintptr_t addr, T& r, ElfW(Sym)* sym, void* func) {
auto type = ELF_R_TYPE(r.r_info);
// we're only interested in JUMP_SLOT symbols
if (type != R_AARCH64_JUMP_SLOT && type != R_ARM_JUMP_SLOT) {
__android_log_print(ANDROID_LOG_WARN, LOG_TAG, "asked to patch unknown symbol type: %llu", type);
return false;
}
auto write_addr = reinterpret_cast<std::uintptr_t*>(addr);
auto val = reinterpret_cast<std::uintptr_t>(func);
if constexpr (std::is_same_v<T, ElfW(Rela)>) {
// reloc = S + A
val += r.r_addend;
}
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "Patching relocation at vaddr %#lx (offset=%#llx): %#lx => %#lx", addr, r.r_offset, *write_addr, val);
if (!set_mprotect(addr, sizeof(std::uintptr_t), PROT_WRITE | PROT_READ)) {
__android_log_print(ANDROID_LOG_WARN, LOG_TAG, "Failed to patch relocation: failed to add writable flag");
return false;
}
// why use memcpy when you can do this :)
*write_addr = val;
if (!set_mprotect(addr, sizeof(std::uintptr_t), PROT_READ)) {
__android_log_print(ANDROID_LOG_WARN, LOG_TAG, "Failed to patch relocation: failed to remove writable flag");
return false;
}
return true;
}
FILE* fopen_hook(const char* pathname, const char* mode);
int rename_hook(const char* old_path, const char* new_path);
template <typename T> requires is_one_of<T, ElfW(Rela), ElfW(Rel)>
bool perform_reloc_patches(dl_phdr_info* info, const ElfDynamicState& state) {
auto table_size = state.plt_table_size_bytes / sizeof(T);
auto failed = false;
for (auto& plt : std::span{
reinterpret_cast<T*>(state.plt_table), table_size
}) {
auto sym = ELF_R_SYM(plt.r_info);
if (sym == 0) continue;
auto sym_data = state.sym_table + sym;
auto sym_name = state.str_table + sym_data->st_name;
auto reloc_addr = info->dlpi_addr + plt.r_offset;
if (strcmp(sym_name, "fopen") == 0) {
if (!rewrite_symbol_reloc(reloc_addr, plt, sym_data, reinterpret_cast<void*>(&fopen_hook))) {
failed = true;
}
} else if (strcmp(sym_name, "rename") == 0) {
if (!rewrite_symbol_reloc(reloc_addr, plt, sym_data, reinterpret_cast<void*>(&rename_hook))) {
failed = true;
}
}
}
return !failed;
} }
int on_dl_iterate(dl_phdr_info* info, size_t size, void* data) { int on_dl_iterate(dl_phdr_info* info, size_t size, void* data) {
// this is probably going to be gd // this is probably going to be gd
if (info->dlpi_name != nullptr && strstr(info->dlpi_name, "libcocos2dcpp.so") != nullptr) { if (info->dlpi_name != nullptr && strstr(info->dlpi_name, "libcocos2dcpp.so") != nullptr) {
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "Base address of libcocos2dcpp: %#llx", info->dlpi_addr);
// step 1: get the dynamic table // step 1: get the dynamic table
std::uintptr_t dyn_addr = 0u; std::uintptr_t dyn_addr = 0u;
for (auto i = 0u; i < info->dlpi_phnum; i++) { for (auto& phdr : std::span{ info->dlpi_phdr, info->dlpi_phnum}) {
auto phdr = info->dlpi_phdr + i; if (phdr.p_type == PT_DYNAMIC) {
dyn_addr = info->dlpi_addr + phdr.p_vaddr;
if (phdr->p_type == PT_DYNAMIC) {
dyn_addr = info->dlpi_addr + phdr->p_vaddr;
break; break;
} }
} }
if (dyn_addr == 0u) { if (dyn_addr == 0u) {
__android_log_print(ANDROID_LOG_WARN, "GeodeLauncher-fix", "failed to find libcocos dynamic section"); __android_log_print(ANDROID_LOG_WARN, LOG_TAG, "failed to find libcocos dynamic section");
return 0; return 0;
} }
@@ -191,26 +305,40 @@ int on_dl_iterate(dl_phdr_info* info, size_t size, void* data) {
auto dyn_entry = reinterpret_cast<ElfW(Dyn)*>(dyn_addr); auto dyn_entry = reinterpret_cast<ElfW(Dyn)*>(dyn_addr);
auto dyn_end_reached = false; auto dyn_end_reached = false;
std::uintptr_t sym_table_addr = 0u; ElfDynamicState state{};
std::uintptr_t str_table_addr = 0u;
std::uintptr_t hash_table_addr = 0u;
std::uintptr_t soname_idx = 0u;
while (!dyn_end_reached) { while (!dyn_end_reached) {
auto tag = dyn_entry->d_tag; auto tag = dyn_entry->d_tag;
switch (dyn_entry->d_tag) { switch (dyn_entry->d_tag) {
case DT_SYMTAB: case DT_SYMTAB:
sym_table_addr = info->dlpi_addr + dyn_entry->d_un.d_val; state.sym_table = reinterpret_cast<ElfW(Sym)*>(info->dlpi_addr + dyn_entry->d_un.d_val);
break; break;
case DT_STRTAB: case DT_STRTAB:
str_table_addr = info->dlpi_addr + dyn_entry->d_un.d_val; state.str_table = reinterpret_cast<char*>(info->dlpi_addr + dyn_entry->d_un.d_val);
break; break;
case DT_HASH: case DT_HASH:
hash_table_addr = info->dlpi_addr + dyn_entry->d_un.d_val; state.hash_table = reinterpret_cast<std::uint32_t*>(info->dlpi_addr + dyn_entry->d_un.d_val);
break; break;
case DT_SONAME: case DT_SONAME:
soname_idx = dyn_entry->d_un.d_val; state.soname_idx = dyn_entry->d_un.d_val;
break;
case DT_JMPREL:
state.plt_table = reinterpret_cast<void*>(info->dlpi_addr + dyn_entry->d_un.d_val);
break;
case DT_PLTREL: {
// for Android, arm64 = DT_RELA, arm32 = DT_REL
// could just gate this based on architecture but whatever
auto val = dyn_entry->d_un.d_val;
state.plt_table_rela = val == DT_RELA;
if (val != DT_RELA && val != DT_REL) {
__android_log_print(ANDROID_LOG_WARN, LOG_TAG, "unrecognized PLTREL type %llx", val);
}
break;
}
case DT_PLTRELSZ:
state.plt_table_size_bytes = dyn_entry->d_un.d_val;
break; break;
case DT_NULL: case DT_NULL:
dyn_end_reached = true; dyn_end_reached = true;
@@ -220,48 +348,38 @@ int on_dl_iterate(dl_phdr_info* info, size_t size, void* data) {
dyn_entry++; dyn_entry++;
} }
if (hash_table_addr == 0u || str_table_addr == 0u || sym_table_addr == 0u) { if (state.is_empty()) {
__android_log_print(ANDROID_LOG_ERROR, "GeodeLauncher-fix", "failed to parse dynamic section (at least one table is null)"); __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "failed to parse dynamic section (at least one table is null)");
return -1; return -1;
} }
// patch symbol names bool failed = state.plt_table_rela
auto hash_table = reinterpret_cast<std::uint32_t*>(hash_table_addr); ? !perform_reloc_patches<ElfW(Rela)>(info, state)
auto str_table = reinterpret_cast<char*>(str_table_addr); : !perform_reloc_patches<ElfW(Rel)>(info, state);
auto sym_table = reinterpret_cast<ElfW(Sym)*>(sym_table_addr);
auto so_name = str_table + soname_idx; if (DataPaths::get_instance().patch_exceptions) {
if (strcmp(so_name, "libcocos2dcpp.so") != 0) { if (!perform_symbol_patches(info, state)) {
__android_log_print(ANDROID_LOG_WARN, "GeodeLauncher-fix", "DT_SONAME gives us %s! that's not good", so_name); failed = true;
}
auto has_error = false;
auto symbols_listing = get_symbols_listing();
for (const auto& symbol : symbols_listing) {
if (!patch_symbol(hash_table, str_table, sym_table, symbol.c_str())) {
has_error = true;
} }
} }
if (has_error) { if (failed) {
__android_log_print( __android_log_print(
ANDROID_LOG_INFO, ANDROID_LOG_INFO,
"GeodeLauncher-fix", LOG_TAG,
"symbol patch diagnostics\nlibrary path: %s\naddrs: base=%p sym=%p str=%p hash=%p", "symbol patch diagnostics\nlibrary path: %s\naddrs: base=%p sym=%p str=%p hash=%p plt=%p",
info->dlpi_name, reinterpret_cast<void*>(info->dlpi_addr), sym_table, str_table, hash_table info->dlpi_name, reinterpret_cast<void*>(info->dlpi_addr), state.sym_table, state.str_table, state.hash_table, state.plt_table
); );
} }
return 1; return 1;
} }
return 0; return 0;
} }
// this should be called after gd is loaded but before geode // this should be called after gd is loaded but before geode
extern "C" extern "C" JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_performPatches(JNIEnv*, jobject) {
JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_performExceptionsRenaming(JNIEnv*, jobject) {
dl_iterate_phdr(on_dl_iterate, nullptr); dl_iterate_phdr(on_dl_iterate, nullptr);
} }
@@ -281,15 +399,13 @@ std::optional<std::string> redirect_path(const char* pathname) {
return std::nullopt; return std::nullopt;
} }
FILE* (*fopen_original)(const char *pathname, const char *mode);
FILE* fopen_hook(const char* pathname, const char* mode) { FILE* fopen_hook(const char* pathname, const char* mode) {
auto path_str = redirect_path(pathname); auto path_str = redirect_path(pathname);
auto redirect = path_str.has_value() ? path_str->c_str() : pathname; auto redirect = path_str.has_value() ? path_str->c_str() : pathname;
return fopen_original(redirect, mode); return fopen(redirect, mode);
} }
int (*rename_original)(const char* old_path, const char* new_path);
int rename_hook(const char* old_path, const char* new_path) { int rename_hook(const char* old_path, const char* new_path) {
auto old_path_str = redirect_path(old_path); auto old_path_str = redirect_path(old_path);
auto old_redirect = old_path_str ? old_path_str->c_str() : old_path; auto old_redirect = old_path_str ? old_path_str->c_str() : old_path;
@@ -297,63 +413,5 @@ int rename_hook(const char* old_path, const char* new_path) {
auto new_path_str = redirect_path(new_path); auto new_path_str = redirect_path(new_path);
auto new_redirect = new_path_str ? new_path_str->c_str() : new_path; auto new_redirect = new_path_str ? new_path_str->c_str() : new_path;
return rename_original(old_redirect, new_redirect); return rename(old_redirect, new_redirect);
}
bool hook_function(void* addr, auto* hook, auto** orig) {
#ifdef USE_TULIPHOOK
using namespace tulip::hook;
HandlerMetadata metadata = {
std::make_shared<PlatformConvention>(),
AbstractFunction::from(hook)
};
auto handler_result = createHandler(addr, metadata);
if (!handler_result) {
__android_log_print(ANDROID_LOG_WARN, "GeodeLauncher-Fix",
"failed to create handler: %s\n", handler_result.unwrapErr().c_str());
return false;
}
auto handler = *handler_result;
HookMetadata hook_metadata = {0};
createHook(handler, reinterpret_cast<void*>(hook), hook_metadata);
WrapperMetadata wrapper_metadata = {
std::make_unique<PlatformConvention>(),
AbstractFunction::from(hook)
};
auto wrapper = createWrapper(addr, std::move(wrapper_metadata));
if (!wrapper) {
__android_log_print(ANDROID_LOG_WARN, "GeodeLauncher-Fix",
"failed to create wrapper: %s\n", wrapper.unwrapErr().c_str());
return false;
}
*reinterpret_cast<void**>(orig) = *wrapper;
return true;
#else
DobbyHook(
addr,
reinterpret_cast<dobby_dummy_func_t>(hook),
reinterpret_cast<dobby_dummy_func_t*>(orig)
);
return true;
#endif
}
[[gnu::constructor]] [[gnu::used]] void setup_hooks() {
#ifndef DISABLE_LAUNCHER_FIX
auto fopen_addr = dlsym(RTLD_NEXT, "fopen");
hook_function(fopen_addr, &fopen_hook, &fopen_original);
auto rename_addr = dlsym(RTLD_NEXT, "rename");
hook_function(rename_addr, &rename_hook, &rename_original);
#endif
} }
@@ -172,19 +172,13 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME) tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
if (GamePackageUtils.getGameVersionCode(packageManager) >= 39L) { if (GamePackageUtils.getGameVersionCode(packageManager) >= 39L) {
/*
val customSymbols = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.CUSTOM_SYMBOL_LIST)
if (customSymbols) {
val symbolFile = File(LaunchUtils.getBaseDirectory(this), "exception_symbols.txt")
LauncherFix.enableCustomSymbolList(symbolFile.path)
}
*/
// this fix requires geode v3, which is 2.206+ // this fix requires geode v3, which is 2.206+
// there is a short period in which 2.206 users will still have geode v2, but whatever. ig // there is a short period in which 2.206 users will still have geode v2, but whatever. ig
LauncherFix.performExceptionsRenaming() LauncherFix.enableExceptionsRenaming()
} }
LauncherFix.performPatches()
loadInternalMods() loadInternalMods()
val baseView = createView() val baseView = createView()
@@ -13,7 +13,9 @@ object LauncherFix {
external fun setOriginalDataPath(dataPath: String) external fun setOriginalDataPath(dataPath: String)
external fun performExceptionsRenaming() external fun enableExceptionsRenaming()
external fun performPatches()
external fun enableCustomSymbolList(symbolsPath: String) external fun enableCustomSymbolList(symbolsPath: String)
} }