diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..2e76f38
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 CrossCoreNightly
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..545625a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,36 @@
+# AssetSideLoader
+
+[README_EN.md](README_EN.md)
+
+## 介绍
+
+一个LSPosed模块,通过hook UnityEngine的内部方法实现从自定义路径加载文件,来替换External Storage或者APK中的资源。
+
+---
+
+## 说明
+
+- 需要先手动授予“获取应用列表”权限
+- 请先在LSPosed中激活模块,并且至少选择一个目标应用,之后启动APP
+- 启动APP后可以在`SELECT APPS`中选择目标应用,然后点击`DONE`返回
+ - 注意,即使在模块中选择了目标应用,也需要在LSPosed中为模块选择相应的目标应用
+ - `SELECT APPS`中只会显示`lib`目录下存在`libil2cpp.so`的应用
+- 选择目标应用后,可以在主界面点击对应的应用进入具体设置
+ - 如果回到主界面后没有显示对应的应用,请重启APP
+- 在具体设置中有三个路径需要手动填写
+ - `APK Patch`:APK文件中的资源相对于`/data/app/*/*/base.apk!/`的路径
+ - `Data Patch`:外部存储中的资源相对于`/storage/emulated/0/Android/com.example.www/files`的路径
+ - `Mod Patch`:自定义的mod文件夹路径,与Data Patch一样是外部存储中的相对路径
+ - APP期望在`Mod Patch`下有和`APK Patch`和`Data Patch`中要替换的文件相同的目录结构和文件名
+- 选择好路径后,点击`SAVE`保存设置,点击`DELETE`删除设置,点击SWITCH切换是否启用
+- 确定设定无误后,启动目标应用即可
+
+## 示例
+
+![example](doc/example.jpg)
+
+## 致谢
+
+- Perfare: [Zygisk-Il2CppDumper](https://github.com/Perfare/Zygisk-Il2CppDumper)
+- jmpewsL [Dobby](https://github.com/jmpews/Dobby)
+- LSPosed: [LSPosed](https://github.com/LSPosed/LSPosed)
\ No newline at end of file
diff --git a/README_EN.md b/README_EN.md
new file mode 100644
index 0000000..3e61e52
--- /dev/null
+++ b/README_EN.md
@@ -0,0 +1,33 @@
+# AssetSideLoader
+
+## Introduction
+
+A LSPosed module that replaces resources in External Storage or APK by hooking UnityEngine's internal methods to load files from a custom path.
+
+---
+## Description
+
+- You need to grant "Get App List" permission manually.
+- Please activate the module in LSPosed and select at least one target app, then launch the app.
+- After launching the app, you can select the target app in `SELECT APPS`, then click `DONE` to return.
+ - Note that even if you have selected a target app in the module, you still need to select the corresponding target app for the module in LSPosed.
+ - The `SELECT APPS` will only show applications that have `libil2cpp.so` in the `lib` directory.
+- After selecting the target application, you can click the corresponding application in the main interface to enter the specific settings.
+ - If the corresponding app is not displayed after returning to the main interface, please restart the app.
+- There are three paths you need to fill in manually
+ - `APK Patch`: the path of resources in APK file relative to `/data/app/*/*/base.apk!/`.
+ - `Data Patch`: path to resources in external storage relative to `/storage/emulated/0/Android/com.example.www/files`.
+ - `Mod Patch`: the path to the customized mod folder, which is a relative path in the external storage like the Data Patch.
+ - APP expects the same directory structure and file names under `Mod Patch` as the files to be replaced in `APK Patch` and `Data Patch`.
+- After selecting the path, click `SAVE` to save the settings, click `DELETE` to delete the settings, and click SWITCH to enable or disable the settings.
+- After making sure the settings are correct, start the target application.
+
+## Example
+
+![example](doc/example.jpg)
+
+## Credits
+
+- Perfare: [Zygisk-Il2CppDumper](https://github.com/Perfare/Zygisk-Il2CppDumper)
+- jmpewsL [Dobby](https://github.com/jmpews/Dobby)
+- LSPosed: [LSPosed](https://github.com/LSPosed/LSPosed)
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f7b1dac..9c1801b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,18 +1,19 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
+ id("kotlin-parcelize")
}
android {
- namespace = "com.axix.assetsideloader"
+ namespace = "top.axix.assetsideloader"
compileSdk = 34
defaultConfig {
- applicationId = "com.axix.assetsideloader"
- minSdk = 26
+ applicationId = "top.axix.assetsideloader"
+ minSdk = 24
targetSdk = 34
- versionCode = 106
- versionName = "1.0.6"
+ versionCode = 111
+ versionName = "1.1.1"
ndk {
}
@@ -34,10 +35,23 @@ android {
buildFeatures{
prefab = true
}
+
+ buildTypes {
+ debug {
+ isMinifyEnabled = false
+ }
+ }
+
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
+
+ lint {
+ abortOnError = true
+ checkReleaseBuilds = false
+ }
+
kotlinOptions {
jvmTarget = "1.8"
}
@@ -52,4 +66,6 @@ dependencies {
implementation("com.google.android.material:material:1.9.0")
testImplementation("junit:junit:4.13.2")
implementation("io.github.hexhacking:xdl:2.1.1")
+ implementation("androidx.recyclerview:recyclerview:1.2.1")
+ implementation("com.google.code.gson:gson:2.8.9")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d3d2835..8554786 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,6 +1,9 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ package="top.axix.assetsideloader">
+
+
+ android:theme="@style/Theme.AppCompat.DayNight">
+
+ android:value="SideLoad Unity Files" />
+ android:name="xposedsharedprefs"
+ android:value="true" />
+
+
-
+
+
-
\ No newline at end of file
diff --git a/app/src/main/assets/xposed_init b/app/src/main/assets/xposed_init
index 75976d7..17b53f3 100644
--- a/app/src/main/assets/xposed_init
+++ b/app/src/main/assets/xposed_init
@@ -1 +1 @@
-com.axix.assetsideloader.MainHook
\ No newline at end of file
+top.axix.assetsideloader.MainHook
\ No newline at end of file
diff --git a/app/src/main/cpp/main.cpp b/app/src/main/cpp/main.cpp
index 628b225..61fa0d6 100644
--- a/app/src/main/cpp/main.cpp
+++ b/app/src/main/cpp/main.cpp
@@ -4,10 +4,12 @@
#include
#include
#include
+#include
+#include
#include
-#include "xdl.h"
#include "log.h"
#include "utf8.h"
+#include "xdl.h"
#include "il2cpp-class.h"
//------------------------------------------------------------------------------------------------//
@@ -33,28 +35,30 @@ void init_il2cpp_api(void *handle) {
//------------------------------------------------------------------------------------------------//
-void* Handle = nullptr;
-
-std::string pakageName = "";
+typedef struct {
+ void* handle;
+ const char* pakageName;
+ const char* dataDir;
+ const char* apkDir;
+ const char* modDir;
+} AppInfo;
-std::string modDir = "/storage/emulated/0/Android/data/";
+AppInfo appInfo;
//------------------------------------------------------------------------------------------------//
void* ReplacePath(std::string path) {
- if (path.find("AssetBundles") != std::string::npos) {
- std::string modPath = modDir + path.substr(path.find("AssetBundles") + 12);
- if (modPath.find(".ys") != std::string::npos) {
- //去除.ys后缀
- modPath = modPath.substr(0, modPath.find(".ys"));
- }
- // 判断文件是否存在
- std::ifstream file(modPath);
- if (file) {
- LOG_I("Redirect from: %s", path.c_str());
- LOG_I("To: %s", modPath.c_str());
- return il2cpp_string_new(modPath.c_str());
- }
+ std::string new_path;
+ if (path.find(appInfo.apkDir) == 0) {
+ new_path = appInfo.modDir + path.substr(strlen(appInfo.apkDir));
+ } else if (path.find(appInfo.dataDir) == 0) {
+ new_path = appInfo.modDir + path.substr(strlen(appInfo.dataDir));
+ } else {
+ new_path = path;
+ }
+ if (std::__fs::filesystem::exists(new_path)) {
+ LOG_D("Replace path: %s -> %s", path.c_str(), new_path.c_str());
+ return il2cpp_string_new(new_path.c_str());
}
return il2cpp_string_new(path.c_str());
}
@@ -98,16 +102,12 @@ void* Hook_LoadFromFileAsync_Internal(void* path, uint32_t crc, uint64_t offset)
void hook_funcs(){
LOG_I("Init Il2cpp api...");
- init_il2cpp_api(Handle);
+ init_il2cpp_api(appInfo.handle);
LOG_I("hooking...");
- modDir += pakageName;
- modDir += "/files/mods";
- LOG_I("modDir: %s", modDir.c_str());
-
uint64_t LoadFromFile_Internal_addr = (uint64_t)il2cpp_resolve_icall("UnityEngine.AssetBundle::LoadFromFile_Internal(System.String,System.UInt32,System.UInt64)");
if (LoadFromFile_Internal_addr) {
- LOG_I("LoadFromFile_Internal_addr: %" PRIx64"", LoadFromFile_Internal_addr);
+ LOG_D("LoadFromFile_Internal_addr: %" PRIx64"", LoadFromFile_Internal_addr);
DobbyHook(
(void *)LoadFromFile_Internal_addr,
(void *)Hook_LoadFromFile_Internal,
@@ -117,7 +117,7 @@ void hook_funcs(){
uint64_t LoadFromFileAsync_Internal_addr = (uint64_t)il2cpp_resolve_icall("UnityEngine.AssetBundle::LoadFromFileAsync_Internal(System.String,System.UInt32,System.UInt64)");
if (LoadFromFileAsync_Internal_addr) {
- LOG_I("LoadFromFileAsync_Internal_addr: %" PRIx64"", LoadFromFileAsync_Internal_addr);
+ LOG_D("LoadFromFileAsync_Internal_addr: %" PRIx64"", LoadFromFileAsync_Internal_addr);
DobbyHook(
(void *)LoadFromFileAsync_Internal_addr,
(void *)Hook_LoadFromFileAsync_Internal,
@@ -135,11 +135,11 @@ il2cpp_init_func il2cpp_init_origin = nullptr;
int hook_il2cpp_init(const char *domain_name) {
int result = il2cpp_init_origin(domain_name);
DobbyDestroy((void*)hook_il2cpp_init);
- LOG_I("il2cpp_init finished with result: %d", result);
+ LOG_D("il2cpp_init finished with result: %d", result);
if (result == 1){
DobbyDestroy((void*)hook_il2cpp_init);
- Handle = xdl_open("libil2cpp.so", 0);
- LOG_I("libil2cpp.so handle: %p", Handle);
+ appInfo.handle = xdl_open("libil2cpp.so", 0);
+ LOG_D("libil2cpp.so handle: %p", appInfo.handle);
hook_funcs();
}
return result;
@@ -154,7 +154,7 @@ static dlsym_t orig_dlsym = nullptr;
void* my_dlsym(void* handle, const char* symbol){
void* addr = orig_dlsym(handle, symbol);
if (strcmp(symbol, "il2cpp_init") == 0) {
- LOG_I("symbol il2cpp_init found at: %p", addr);
+ LOG_D("symbol il2cpp_init found at: %p", addr);
DobbyDestroy((void*)my_dlsym);
DobbyHook(
(void*)addr,
@@ -169,8 +169,17 @@ void* my_dlsym(void* handle, const char* symbol){
extern "C"
JNIEXPORT void JNICALL
-Java_com_axix_assetsideloader_AssetSideLoader_InitHook(JNIEnv *env, jobject thiz, jstring str) {
- pakageName = env->GetStringUTFChars(str, NULL);
+Java_top_axix_assetsideloader_AssetSideLoader_InitHook(JNIEnv *env, jobject thiz, jstring pgn, jstring dataPath, jstring apkPath, jstring modPath) {
+ LOG_D("Native hook init...");
+ appInfo.pakageName = env->GetStringUTFChars(pgn, NULL);
+ LOG_D("Package name: %s", appInfo.pakageName);
+ appInfo.dataDir = env->GetStringUTFChars(dataPath, NULL);
+ LOG_D("Data dir: %s", appInfo.dataDir);
+ appInfo.apkDir = env->GetStringUTFChars(apkPath, NULL);
+ LOG_D("Apk dir: %s", appInfo.apkDir);
+ appInfo.modDir = env->GetStringUTFChars(modPath, NULL);
+ LOG_D("Mod dir: %s", appInfo.modDir);
LOG_D("Hooking dlsym...");
+
DobbyHook((void*) dlsym, (void*)my_dlsym, (void**)&orig_dlsym);
}
\ No newline at end of file
diff --git a/app/src/main/java/com/axix/assetsideloader/AssetSideLoader.kt b/app/src/main/java/com/axix/assetsideloader/AssetSideLoader.kt
deleted file mode 100644
index 8f28cbc..0000000
--- a/app/src/main/java/com/axix/assetsideloader/AssetSideLoader.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.axix.assetsideloader
-
-class AssetSideLoader {
- external fun InitHook(pakageName: String): Void
-
- companion object {
- init {
- System.loadLibrary("AssetSideLoader")
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/axix/assetsideloader/MainActivity.kt b/app/src/main/java/com/axix/assetsideloader/MainActivity.kt
deleted file mode 100644
index b142f1a..0000000
--- a/app/src/main/java/com/axix/assetsideloader/MainActivity.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.axix.assetsideloader
-
-import androidx.appcompat.app.AppCompatActivity
-import android.os.Bundle
-import com.axix.assetsideloader.R
-
-class MainActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/axix/assetsideloader/MainHook.kt b/app/src/main/java/com/axix/assetsideloader/MainHook.kt
deleted file mode 100644
index 8e5b3fd..0000000
--- a/app/src/main/java/com/axix/assetsideloader/MainHook.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.axix.assetsideloader
-
-import android.util.Log
-import de.robv.android.xposed.IXposedHookLoadPackage
-import de.robv.android.xposed.callbacks.XC_LoadPackage
-
-class MainHook : IXposedHookLoadPackage {
- override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
- if (lpparam.packageName == "com.bilibili.azurlane") {
- Log.i("AssetSideLoader", "Target application: ${lpparam.packageName} launched")
- AssetSideLoader().InitHook("com.bilibili.azurlane")
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/top/axix/assetsideloader/AppInfo.kt b/app/src/main/java/top/axix/assetsideloader/AppInfo.kt
new file mode 100644
index 0000000..f714b02
--- /dev/null
+++ b/app/src/main/java/top/axix/assetsideloader/AppInfo.kt
@@ -0,0 +1,44 @@
+package top.axix.assetsideloader
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import com.google.gson.Gson
+import java.io.Serializable
+
+data class AppInfo(
+ val appName: String,
+ val packageName: String,
+ val apkPath: String,
+ var dataPath: String,
+ var apkPatch: String,
+ var dataPatch: String,
+ var modPatch: String,
+ var isEnabled: Boolean,
+ var isSelected: Boolean
+) : Serializable {
+ constructor(context: Context, appInfo: ApplicationInfo) : this(
+ appName = appInfo.loadLabel(context.packageManager).toString(),
+ packageName = appInfo.packageName,
+ apkPath = appInfo.sourceDir + "!/",
+ dataPath = "/storage/emulated/0/Android/data/" + appInfo.packageName + "/files",
+ apkPatch = "",
+ dataPatch = "",
+ modPatch = "",
+ isEnabled = false,
+ isSelected = false
+ )
+
+ fun toJson(): String {
+ return Gson().toJson(this)
+ }
+
+ companion object {
+ fun fromJson(json: String): AppInfo {
+ return Gson().fromJson(json, AppInfo::class.java)
+ }
+ }
+}
+
+fun applicationInfoToAppInfo(context: Context, appInfos: List): List {
+ return appInfos.map { AppInfo(context, it) }
+}
\ No newline at end of file
diff --git a/app/src/main/java/top/axix/assetsideloader/AppInfoActivity.kt b/app/src/main/java/top/axix/assetsideloader/AppInfoActivity.kt
new file mode 100644
index 0000000..34e08e0
--- /dev/null
+++ b/app/src/main/java/top/axix/assetsideloader/AppInfoActivity.kt
@@ -0,0 +1,67 @@
+package top.axix.assetsideloader
+
+import android.os.Bundle
+import android.widget.Button
+import android.widget.EditText
+import android.widget.Switch
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import top.axix.assetsideloader.Global.appInfoData
+
+class AppInfoActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_app_info)
+
+ val position = intent.getIntExtra("position", -1)
+
+ if (position == -1){
+ finish()
+ }
+
+ val appName = findViewById(R.id.app_name)
+ val appPackageName = findViewById(R.id.app_pakage_name)
+ val apkPath = findViewById(R.id.apk_path)
+ val apkPatch = findViewById(R.id.apk_patch)
+ var dataPath = findViewById(R.id.data_path)
+ var dataPatch = findViewById(R.id.data_patch)
+ var modPatch = findViewById(R.id.mod_patch)
+ val enableSwitch = findViewById(R.id.enable_switch)
+ val saveButton = findViewById