diff --git a/DiamondRuntime b/DiamondRuntime index d59d524..856d57a 160000 --- a/DiamondRuntime +++ b/DiamondRuntime @@ -1 +1 @@ -Subproject commit d59d52410d7374412504cdc94a778679acc91d99 +Subproject commit 856d57abb29afdfed03a84f53cd48b4fde602063 diff --git a/public/Skins/counterr.png b/public/Skins/counterr.png new file mode 100644 index 0000000..edd7d54 Binary files /dev/null and b/public/Skins/counterr.png differ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d307a47..cd9ca6f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1051,6 +1051,7 @@ dependencies = [ "futures-util", "http 1.4.0", "image 0.24.9", + "jni", "libc", "lzxd", "once_cell", @@ -1074,6 +1075,7 @@ dependencies = [ "tokio-util", "url", "uuid 1.23.0", + "wry", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b3dd5fe..5e466a1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -38,6 +38,10 @@ lzxd = "0.2.6" uuid = { version = "1", features = ["v4"] } tauri-plugin-deep-link = "2" +[target.'cfg(target_os = "android")'.dependencies] +wry = { version = "0.54", default-features = false } +jni = "0.21" + [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2" tauri-plugin-drpc = "*" diff --git a/src-tauri/gen/android/app/build.gradle.kts b/src-tauri/gen/android/app/build.gradle.kts index 55eb21b..f44a4ea 100644 --- a/src-tauri/gen/android/app/build.gradle.kts +++ b/src-tauri/gen/android/app/build.gradle.kts @@ -35,7 +35,7 @@ android { manifestPlaceholders["usesCleartextTraffic"] = "false" applicationId = "com.emerald.legacy" minSdk = 26 - targetSdk = 36 + targetSdk = 28 versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") } @@ -67,6 +67,12 @@ android { buildFeatures { buildConfig = true } + lint { + disable += "ExpiredTargetSdkVersion" + } + packaging { + resources.excludes += "lib/*/jni/**" + } } rust { @@ -74,6 +80,7 @@ rust { } dependencies { + implementation(project(":diamondruntime")) implementation("androidx.webkit:webkit:1.14.0") implementation("androidx.appcompat:appcompat:1.7.1") implementation("androidx.activity:activity-ktx:1.10.1") diff --git a/src-tauri/gen/android/app/proguard-rules.pro b/src-tauri/gen/android/app/proguard-rules.pro index 481bb43..efc3900 100644 --- a/src-tauri/gen/android/app/proguard-rules.pro +++ b/src-tauri/gen/android/app/proguard-rules.pro @@ -1,21 +1,19 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html +-keep class dev.lcehub.emerald.** { *; } +-keepclassmembers class dev.lcehub.emerald.** { *; } +-dontwarn dev.lcehub.emerald.** +-keep class com.github.luben.zstd.** { *; } +-keepclassmembers class com.github.luben.zstd.** { *; } +-dontwarn com.github.luben.zstd.** +-keep class com.emerald.legacy.TauriActivity { + public ; + public ; +} +-keep class com.emerald.legacy.MainActivity { + public ; +} -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +-dontwarn com.android.org.conscrypt.SSLParametersImpl +-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl +-dontwarn org.bouncycastle.jsse.BCSSLParameters +-dontwarn org.bouncycastle.jsse.BCSSLSocket +-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider diff --git a/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/src-tauri/gen/android/app/src/main/AndroidManifest.xml index 7a1ca94..90b8a8c 100644 --- a/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + @@ -9,7 +10,9 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/Theme.emerald_legacy_launcher" - android:usesCleartextTraffic="${usesCleartextTraffic}"> + android:extractNativeLibs="true" + android:usesCleartextTraffic="${usesCleartextTraffic}" + tools:replace="android:label,android:theme"> + android:grantUriPermissions="true" + tools:replace="android:authorities"> diff --git a/src-tauri/gen/android/app/src/main/java/com/emerald/legacy/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/com/emerald/legacy/MainActivity.kt index d0b415e..3ca8f28 100644 --- a/src-tauri/gen/android/app/src/main/java/com/emerald/legacy/MainActivity.kt +++ b/src-tauri/gen/android/app/src/main/java/com/emerald/legacy/MainActivity.kt @@ -21,5 +21,8 @@ class MainActivity : TauriActivity() { super.onWebViewCreate(webView) webView.settings.useWideViewPort = true webView.settings.loadWithOverviewMode = true + webView.settings.setSupportZoom(false) + webView.settings.builtInZoomControls = false + webView.settings.displayZoomControls = false } } diff --git a/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml index 782d63b..f2c4376 100644 --- a/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml +++ b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml @@ -2,4 +2,5 @@ + diff --git a/src-tauri/gen/android/settings.gradle b/src-tauri/gen/android/settings.gradle index 3939116..9006af6 100644 --- a/src-tauri/gen/android/settings.gradle +++ b/src-tauri/gen/android/settings.gradle @@ -1,3 +1,5 @@ include ':app' +include ':diamondruntime' +project(':diamondruntime').projectDir = new File(settingsDir, '../../../DiamondRuntime/app/app') apply from: 'tauri.settings.gradle' diff --git a/src-tauri/src/android_runtime.rs b/src-tauri/src/android_runtime.rs new file mode 100644 index 0000000..cd64b6c --- /dev/null +++ b/src-tauri/src/android_runtime.rs @@ -0,0 +1,83 @@ +pub enum BridgeAction { + Play, + OpenContainer, + OpenSettings, +} + +pub fn launch_bridge(instance_path: String, action: BridgeAction) -> Result<(), String> { + #[cfg(target_os = "android")] + { + use jni::objects::{JObject, JValue}; + use jni::sys::jint; + use jni::JNIEnv; + use wry::prelude::{dispatch, find_class}; + fn start_bridge( + env: &mut JNIEnv, + activity: &JObject, + action: &str, + instance_path: &str, + ) -> jni::errors::Result<()> { + let bridge_class = + find_class(env, activity, "dev.lcehub.emerald.LauncherBridgeActivity".to_string())?; + let intent_class = env.find_class("android/content/Intent")?; + let intent = env.new_object( + intent_class, + "(Landroid/content/Context;Ljava/lang/Class;)V", + &[(&activity).into(), (&bridge_class).into()], + )?; + + let extra_action = env.new_string("launcher_action")?; + let action_str = env.new_string(action)?; + env.call_method( + &intent, + "putExtra", + "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;", + &[(&extra_action).into(), (&action_str).into()], + )?; + + let extra_path = env.new_string("instance_path")?; + let path_str = env.new_string(instance_path)?; + env.call_method( + &intent, + "putExtra", + "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;", + &[(&extra_path).into(), (&path_str).into()], + )?; + + env.call_method( + &intent, + "addFlags", + "(I)Landroid/content/Intent;", + &[JValue::Int(0x10000000 as jint)], + )?; + + env.call_method( + activity, + "startActivity", + "(Landroid/content/Intent;)V", + &[(&intent).into()], + )?; + + Ok(()) + } + + let action_str = match action { + BridgeAction::Play => "play", + BridgeAction::OpenContainer => "open", + BridgeAction::OpenSettings => "settings", + } + .to_string(); + dispatch(move |env, activity, _webview| { + if let Err(e) = start_bridge(env, activity, &action_str, &instance_path) { + eprintln!("[android_bridge] failed to start activity: {e}"); + } + }); + + Ok(()) + } + #[cfg(not(target_os = "android"))] + { + let _ = (instance_path, action); + Err("Only supported on Android".into()) + } +} diff --git a/src-tauri/src/commands/game.rs b/src-tauri/src/commands/game.rs index 6660c4e..45361fe 100644 --- a/src-tauri/src/commands/game.rs +++ b/src-tauri/src/commands/game.rs @@ -32,8 +32,31 @@ pub async fn launch_game( ) -> Result<(), String> { #[cfg(target_os = "android")] { - let _ = (app, state, instance_id, servers, extra_args); - return Err("Launching the game is not implemented on Android. -neo".into()); + let _ = (state, extra_args); + let mut servers = servers; + let working_dir = util::get_instance_working_dir(&app, &instance_id); + if !working_dir.join("Minecraft.Client.exe").exists() { + return Err("Game executable not found in instance folder.".into()); + } + + let config_val = config::load_config_raw(app.clone()); + let lce_online = McServer { name: "LCEOnline Game".into(), ip: "127.0.0.1".into(), port: 61000 }; + if !servers.iter().any(|s| s.ip == lce_online.ip && s.port == lce_online.port) { + servers.push(lce_online); + } + if let Some(ref saved) = config_val.saved_servers { + for s in saved { + if !servers.iter().any(|existing| existing.ip == s.ip && existing.port == s.port) { + servers.push(s.clone()); + } + } + } + ensure_server_list(&working_dir, servers); + + crate::android_runtime::launch_bridge( + working_dir.to_string_lossy().to_string(), + crate::android_runtime::BridgeAction::Play, + ) } #[cfg(not(target_os = "android"))] launch_game_desktop(app, state, instance_id, servers, extra_args).await @@ -312,8 +335,37 @@ pub fn check_game_installed(app: AppHandle, instance_id: String) -> bool { #[allow(non_snake_case)] pub fn open_instance_folder(app: AppHandle, instance_id: String) { let folder = util::get_instance_working_dir(&app, &instance_id); - if folder.exists() { - let _ = app.opener().open_path(folder.to_str().unwrap(), None::<&str>); + #[cfg(target_os = "android")] + { + let _ = std::fs::create_dir_all(&folder); + let _ = crate::android_runtime::launch_bridge( + folder.to_string_lossy().to_string(), + crate::android_runtime::BridgeAction::OpenContainer, + ); + } + #[cfg(not(target_os = "android"))] + { + if folder.exists() { + let _ = app.opener().open_path(folder.to_str().unwrap(), None::<&str>); + } + } +} + +#[tauri::command] +#[allow(non_snake_case)] +pub fn open_container_settings(app: AppHandle, instance_id: String) -> Result<(), String> { + #[cfg(target_os = "android")] + { + let folder = util::get_instance_working_dir(&app, &instance_id); + crate::android_runtime::launch_bridge( + folder.to_string_lossy().to_string(), + crate::android_runtime::BridgeAction::OpenSettings, + ) + } + #[cfg(not(target_os = "android"))] + { + let _ = (app, instance_id); + Err("Only supported on Android".into()) } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 357f5cf..e742f25 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,8 @@ mod playtime; mod platform; mod networking; mod workshop_server; +#[cfg(target_os = "android")] +mod android_runtime; mod commands; use std::collections::HashMap; use std::sync::Arc; @@ -81,6 +83,7 @@ pub fn run() { config_cmds::load_config, download::download_and_install, game::open_instance_folder, + game::open_container_settings, download::cancel_download, runners::get_available_runners, config_cmds::get_external_palettes, diff --git a/src/components/views/SettingsView.tsx b/src/components/views/SettingsView.tsx index 5230b11..842f84b 100644 --- a/src/components/views/SettingsView.tsx +++ b/src/components/views/SettingsView.tsx @@ -43,6 +43,7 @@ const SettingsView = memo(function SettingsView() { setLaunchEnvVars, skipIntro, setSkipIntro, + profile, } = useConfig(); const { currentTrack, skipTrack, tracks, playPressSound, playBackSound } = useAudio(); @@ -323,6 +324,26 @@ const SettingsView = memo(function SettingsView() { setFocusIndex(0); }, }); + if (isAndroid && profile) { + items.push({ + id: "container_settings", + label: "Container Settings", + type: "button", + onClick: () => { + playPressSound(); + TauriService.openContainerSettings(profile).catch(console.error); + }, + }); + items.push({ + id: "open_container", + label: "Open Container", + type: "button", + onClick: () => { + playPressSound(); + TauriService.openInstanceFolder(profile).catch(console.error); + }, + }); + } for (const action of pluginSettingsActions) { items.push({ id: action.id, @@ -582,6 +603,7 @@ const SettingsView = memo(function SettingsView() { launchPrefix, launchEnvVars, skipIntro, + profile, ]); useEffect(() => { diff --git a/src/components/views/SkinsView.tsx b/src/components/views/SkinsView.tsx index 5b12c8b..0dfb3d0 100644 --- a/src/components/views/SkinsView.tsx +++ b/src/components/views/SkinsView.tsx @@ -66,6 +66,12 @@ const DEFAULT_SKINS: SavedSkin[] = [ }, { id: "amy", name: "Amy", url: "/Skins/amy.png", isSlim: true }, //neo: she's the best btw { id: "huckle", name: "Huckle", url: "/Skins/huckle.png", isSlim: true }, + { + id: "counterract", + name: "CounterrAct", + url: "/Skins/counterr.png", + isSlim: true, + }, ]; const HeadPreview = memo(function HeadPreview({ src }: { src: string }) { diff --git a/src/hooks/useGameManager.ts b/src/hooks/useGameManager.ts index 7185811..e40ad6c 100644 --- a/src/hooks/useGameManager.ts +++ b/src/hooks/useGameManager.ts @@ -9,6 +9,7 @@ import { } from "react"; import { TauriService, type CustomEdition } from "../services/TauriService"; import { getCurrentWindow } from "@tauri-apps/api/window"; +import { usePlatform } from "./usePlatform"; import type { Edition } from "../types/edition"; async function imageUrlToBase64(url: string): Promise { @@ -149,6 +150,7 @@ export function useGameManager({ setCustomizations, extraLaunchArgs, }: GameManagerProps) { + const { isAndroid } = usePlatform(); const [installs, setInstalls] = useState([]); const [isGameRunning, setIsGameRunning] = useState(false); const [downloadProgress, setDownloadProgress] = useState< @@ -401,7 +403,7 @@ export function useGameManager({ gameLogRef.current = true; setError(null); setGameLog(log); - getCurrentWindow().unminimize(); + if (!isAndroid) getCurrentWindow().unminimize(); }); return () => { unlistenDownload.then((u) => u()); @@ -410,7 +412,7 @@ export function useGameManager({ unlistenRetry.then((u) => u()); unlistenGameLog.then((u) => u()); }; - }, [customEditions, checkInstalls]); + }, [customEditions, checkInstalls, isAndroid]); const downloadRunner = useCallback( async (name: string, url: string) => { @@ -508,7 +510,7 @@ export function useGameManager({ setError(null); setIsGameRunning(true); try { - getCurrentWindow().minimize(); + if (!isAndroid) getCurrentWindow().minimize(); const currentEdition = editions.find((e) => e.instanceId === profile); await TauriService.launchGame( profile, @@ -537,7 +539,7 @@ export function useGameManager({ } finally { setIsGameRunning(false); } - }, [isGameRunning, profile, extraLaunchArgs]); + }, [isGameRunning, profile, extraLaunchArgs, isAndroid]); const stopGame = useCallback(async () => { try { diff --git a/src/services/TauriService.ts b/src/services/TauriService.ts index c50640d..c13ad33 100644 --- a/src/services/TauriService.ts +++ b/src/services/TauriService.ts @@ -127,6 +127,10 @@ export class TauriService { return invoke("open_instance_folder", { instanceId }); } + static async openContainerSettings(instanceId: string): Promise { + return invoke("open_container_settings", { instanceId }); + } + static async deleteInstance(instanceId: string): Promise { return invoke("delete_instance", { instanceId }); }