# Amethyst Android - Minecraft Java Edition Launcher ## Introduction Amethyst Android is a sophisticated launcher that enables Minecraft: Java Edition to run natively on Android devices. Built upon the foundation of Boardwalk and PojavLauncher, it provides a complete Java runtime environment on Android using custom OpenJDK ports (versions 8, 17, and 21) with full OpenGL rendering via GL4ES, ANGLE, or Zink renderers. The launcher supports all Minecraft versions from rd-132211 (early development releases) through modern 1.21+ snapshots, delivering desktop-quality gaming on mobile devices with custom touch controls, gamepad support, and gyroscope input. The architecture leverages JNI (Java Native Interface) to bridge Android's Dalvik/ART runtime with OpenJDK, providing seamless AWT graphics integration, OpenAL audio, and LWJGL3 compatibility. Core functionality includes Microsoft and local account authentication, comprehensive mod loader support (Forge, Fabric, NeoForge, OptiFine, Quilt), modpack installation via CurseForge and Modrinth APIs, multi-runtime management across ARM32/ARM64/x86/x86_64 architectures, and an advanced custom control system with visual editor. The launcher operates as a multi-process Android application with separate processes for the main UI, game execution, and mod installers. ## Core APIs and Key Functions ### JVM Launch Interface ```java package com.oracle.dalvik; public final class VMLauncher { public static native int launchJVM(String[] args); } ``` Native JNI method that launches OpenJDK from Android. Called from MainActivity with JVM arguments, classpath, and main class. Returns JVM exit code. The implementation is in `jre_launcher.c` which uses `JLI_Launch_func` from OpenJDK's libjli.so. ```java // Example usage from game launch flow String[] jvmArgs = new String[] { "-Xms" + ramAllocation + "M", "-Xmx" + ramAllocation + "M", "-Djava.library.path=" + nativeLibDir, "-Dorg.lwjgl.librarypath=" + lwjglLibPath, "-cp", classPath, mainClass, "--username", account.username, "--version", versionName, "--gameDir", gameDirectory, "--assetsDir", assetsDir, "--assetIndex", assetIndex, "--accessToken", account.accessToken, "--userType", account.isMicrosoft ? "msa" : "legacy" }; int exitCode = VMLauncher.launchJVM(jvmArgs); if (exitCode != 0) { Log.e("GameLauncher", "JVM exited with code: " + exitCode); } ``` ### Download Utilities with Progress Monitoring ```java package net.kdt.pojavlaunch.utils; public class DownloadUtils { public static void downloadFileMonitored( String urlInput, File outputFile, byte[] buffer, Tools.DownloaderFeedback monitor ) throws IOException; public static T downloadStringCached( String url, String cacheName, ParseCallback parseCallback ) throws IOException, ParseException; } ``` Download files with real-time progress tracking and automatic caching. The monitored download supports pause/resume and reports progress to UI via callback. Cached downloads store parsed results for 24 hours. ```java // Download Minecraft client JAR with progress bar File clientJar = new File(versionDir, version + ".jar"); byte[] buffer = new byte[65536]; // 64KB buffer DownloadUtils.downloadFileMonitored( versionInfo.downloads.get("client").url, clientJar, buffer, new Tools.DownloaderFeedback() { @Override public void updateProgress(int current, int total) { int percentage = (int)((current / (float)total) * 100); ProgressKeeper.submitProgress( "download_client", percentage, R.string.downloading_client ); } } ); // Download and cache version manifest with automatic parsing JMinecraftVersionList versionList = DownloadUtils.downloadStringCached( "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json", "version_manifest.json", jsonString -> Tools.GLOBAL_GSON.fromJson(jsonString, JMinecraftVersionList.class) ); ``` ### Cross-Thread Event System ```java package net.kdt.pojavlaunch.extra; public final class ExtraCore { public static void setValue(String key, Object value); public static Object getValue(String key); public static void addExtraListener(String key, ExtraListener listener); public static void removeExtraListenerFromValue(String key, ExtraListener listener); } public interface ExtraListener { boolean onValueSet(String key, T value); } ``` Thread-safe singleton for publishing events and values across activities/fragments without context leaks. Uses weak references to prevent memory leaks. Listeners return true to auto-unregister after receiving event. ```java // Fragment publishing progress update ExtraCore.setValue(ExtraConstants.REFRESH_VERSION_LIST, true); ExtraCore.setValue(ExtraConstants.MINECRAFT_VERSION, selectedVersion); // Activity listening for version selection @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ExtraCore.addExtraListener( ExtraConstants.MINECRAFT_VERSION, (key, value) -> { String version = (String) value; updateVersionDisplay(version); return false; // Keep listening } ); } // One-time listener that auto-removes ExtraCore.addExtraListener( ExtraConstants.INSTALL_MODPACK, (key, modLoader) -> { ModLoader loader = (ModLoader) modLoader; launchGame(loader.getVersionId()); return true; // Remove after first event } ); ``` ### Minecraft Account Management ```java package net.kdt.pojavlaunch.value; public class MinecraftAccount { public String accessToken; public String profileId; public String username; public boolean isMicrosoft; public String msaRefreshToken; public long expiresAt; public String save() throws IOException; public static MinecraftAccount load(String name); public void updateSkinFace(); public Bitmap getSkinFace(); public boolean isLocal(); } ``` Manages Minecraft authentication for both Microsoft and local accounts. Handles token storage, skin caching, and account persistence as JSON files. ```java // Create Microsoft account after OAuth flow MinecraftAccount account = new MinecraftAccount(); account.username = "Player123"; account.profileId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"; account.accessToken = oauthResponse.getAccessToken(); account.isMicrosoft = true; account.msaRefreshToken = oauthResponse.getRefreshToken(); account.expiresAt = System.currentTimeMillis() + (3600 * 1000); // 1 hour account.save(); // Saves to DIR_ACCOUNT_NEW/Player123.json // Load existing account MinecraftAccount loadedAccount = MinecraftAccount.load("Player123"); if (loadedAccount != null) { if (loadedAccount.isMicrosoft && System.currentTimeMillis() > loadedAccount.expiresAt) { // Refresh token with Microsoft API MicrosoftBackgroundLogin.refreshToken(loadedAccount); } loadedAccount.updateSkinFace(); // Downloads from mc-heads.net Bitmap skinFace = loadedAccount.getSkinFace(); profileImageView.setImageBitmap(skinFace); } // Create local/offline account MinecraftAccount localAccount = new MinecraftAccount(); localAccount.username = "OfflinePlayer"; localAccount.accessToken = "0"; // Special value for local accounts localAccount.save(); assert localAccount.isLocal() == true; ``` ### CurseForge Modpack API ```java package net.kdt.pojavlaunch.modloaders.modpacks.api; public class CurseforgeApi implements ModpackApi { public CurseforgeApi(String apiKey); public SearchResult searchMod(SearchFilters filters, SearchResult previousPage); public ModDetail getModDetails(ModItem item); public ModLoader installMod(ModDetail detail, int selectedVersion) throws IOException; } ``` Integrates with CurseForge API v1 for searching and installing mods/modpacks. Supports pagination, version filtering, and automatic dependency resolution. ```java // Initialize API with key CurseforgeApi api = new CurseforgeApi("$2a$10$your_api_key_here"); // Search for modpacks SearchFilters filters = new SearchFilters(); filters.name = "All the Mods"; filters.mcVersion = "1.20.1"; filters.isModpack = true; SearchResult results = api.searchMod(filters, null); Log.i("CurseForge", "Found " + results.totalResultCount + " modpacks"); for (ModItem mod : results.results) { Log.i("CurseForge", mod.name + ": " + mod.description); } // Get detailed version info for selected modpack ModDetail details = api.getModDetails(results.results[0]); for (int i = 0; i < details.versionNames.length; i++) { Log.i("CurseForge", "Version: " + details.versionNames[i] + " | MC: " + details.mcVersionNames[i] + " | URL: " + details.versionUrls[i] ); } // Install modpack version with progress tracking try { ModLoader loader = api.installMod(details, 0); // Install first version Log.i("CurseForge", "Installed modpack: " + loader.getVersionId()); // Launch the installed modpack MinecraftProfile profile = new MinecraftProfile(); profile.versionId = loader.getVersionId(); profile.save(); } catch (IOException e) { Log.e("CurseForge", "Installation failed", e); } ``` ### Custom Control System ```java package net.kdt.pojavlaunch.customcontrols; public class ControlData { public static final int SPECIALBTN_KEYBOARD = -1; public static final int SPECIALBTN_MOUSEPRI = -3; // Left click public static final int SPECIALBTN_MOUSESEC = -4; // Right click public String name; public int[] keycodes; // Up to 4 simultaneous keys public String dynamicX, dynamicY; // Expressions for positioning public float width, height; // In dp units public float opacity; // 0.0 to 1.0 public boolean isToggle; public boolean displayInGame, displayInMenu; } public class ControlLayout { public ArrayList mControlDataList; public void save(String path) throws IOException; public static ControlLayout loadFromFile(File file) throws IOException; } ``` Defines on-screen touch controls with dynamic positioning, multi-key support, and context-aware visibility. Controls use expression-based positioning for responsive layouts. ```java // Create custom jump button ControlData jumpButton = new ControlData("Jump"); jumpButton.keycodes = new int[]{GLFW_KEY_SPACE}; jumpButton.dynamicX = "screen_width * 0.85"; // 85% from left jumpButton.dynamicY = "screen_height * 0.75"; // 75% from top jumpButton.width = 80; // 80dp jumpButton.height = 80; jumpButton.opacity = 0.7f; jumpButton.displayInGame = true; jumpButton.displayInMenu = false; jumpButton.bgColor = 0x80FFFFFF; // Semi-transparent white jumpButton.strokeColor = 0xFF000000; jumpButton.strokeWidth = 2; jumpButton.cornerRadius = 50; // Circular // Create WASD movement cluster ControlData forwardBtn = new ControlData("Forward"); forwardBtn.keycodes = new int[]{GLFW_KEY_W}; forwardBtn.dynamicX = "screen_width * 0.15"; forwardBtn.dynamicY = "screen_height * 0.65"; forwardBtn.width = 60; forwardBtn.height = 60; // Create toggle-able sprint button ControlData sprintBtn = new ControlData("Sprint"); sprintBtn.keycodes = new int[]{GLFW_KEY_LEFT_CONTROL}; sprintBtn.isToggle = true; // Tap once to enable, tap again to disable sprintBtn.displayInGame = true; // Combo button (Ctrl+1 for hotbar slot) ControlData hotbarSlot = new ControlData("Hotbar 1"); hotbarSlot.keycodes = new int[]{GLFW_KEY_LEFT_CONTROL, GLFW_KEY_1}; // Build complete layout ControlLayout layout = new ControlLayout(); layout.mControlDataList = new ArrayList<>(); layout.mControlDataList.add(jumpButton); layout.mControlDataList.add(forwardBtn); layout.mControlDataList.add(sprintBtn); layout.mControlDataList.add(hotbarSlot); // Save to file File layoutFile = new File(Tools.CTRLMAP_PATH, "my_custom_layout.json"); layout.save(layoutFile.getAbsolutePath()); // Load and apply layout ControlLayout loaded = ControlLayout.loadFromFile(layoutFile); LauncherPreferences.PREF_DEFAULTCTRL_PATH = layoutFile.getAbsolutePath(); ``` ### Launcher Preferences System ```java package net.kdt.pojavlaunch.prefs; public class LauncherPreferences { public static SharedPreferences DEFAULT_PREF; public static float PREF_BUTTONSIZE; public static float PREF_MOUSESCALE; public static int PREF_RAM_ALLOCATION; public static String PREF_DEFAULT_RUNTIME; public static boolean PREF_ENABLE_GYRO; public static float PREF_GYRO_SENSITIVITY; public static void loadPreferences(Context ctx); } ``` Centralized preference management with automatic defaults based on device capabilities. All preferences are cached in static fields for fast access during gameplay. ```java // Initialize preferences (typically in Application.onCreate()) LauncherPreferences.DEFAULT_PREF = PreferenceManager.getDefaultSharedPreferences(context); LauncherPreferences.loadPreferences(context); // Configure RAM allocation SharedPreferences.Editor editor = LauncherPreferences.DEFAULT_PREF.edit(); editor.putInt("allocation", 2048); // 2GB RAM editor.putString("defaultCtrl", Tools.CTRLMAP_PATH + "/custom_layout.json"); editor.putInt("buttonscale", 120); // 120% button size editor.putInt("mousescale", 150); // 150% mouse sensitivity editor.putBoolean("ignoreNotch", true); editor.apply(); // Reload preferences after changes LauncherPreferences.loadPreferences(context); // Access preferences during gameplay int ramMB = LauncherPreferences.PREF_RAM_ALLOCATION; String jvmArgs = "-Xms" + ramMB + "M -Xmx" + ramMB + "M"; // Configure gyroscope controls if (LauncherPreferences.PREF_ENABLE_GYRO) { gyroControl.setSensitivity(LauncherPreferences.PREF_GYRO_SENSITIVITY); gyroControl.setInvertX(LauncherPreferences.PREF_GYRO_INVERT_X); gyroControl.setInvertY(LauncherPreferences.PREF_GYRO_INVERT_Y); gyroControl.setSampleRate(LauncherPreferences.PREF_GYRO_SAMPLE_RATE); } // Platform-specific renderer selection String renderer = DEFAULT_PREF.getString("renderer", "auto"); switch (renderer) { case "opengles2": setenv("POJAV_RENDERER", "opengles2"); break; case "opengles3": setenv("POJAV_RENDERER", "opengles3"); break; case "vulkan_zink": setenv("POJAV_RENDERER", "vulkan_zink"); break; } ``` ### Minecraft Version Manifest Parsing ```java package net.kdt.pojavlaunch; public class JMinecraftVersionList { public Map latest; // "release" -> "1.20.4", "snapshot" -> "24w10a" public Version[] versions; public static class Version { public String id; // "1.20.4" public String type; // "release", "snapshot", "old_beta" public String url; // Version JSON URL public String releaseTime; public Map downloads; public DependentLibrary[] libraries; public String mainClass; public Arguments arguments; // 1.13+ format public String minecraftArguments; // Legacy format public JavaVersionInfo javaVersion; } } ``` GSON-annotated data classes for parsing Mojang's version manifest and individual version metadata. Supports both modern (1.13+) and legacy argument formats. ```java // Fetch and parse version manifest String manifestJson = DownloadUtils.downloadString( "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json" ); JMinecraftVersionList manifest = Tools.GLOBAL_GSON.fromJson( manifestJson, JMinecraftVersionList.class ); Log.i("VersionList", "Latest release: " + manifest.latest.get("release")); Log.i("VersionList", "Latest snapshot: " + manifest.latest.get("snapshot")); // Find specific version JMinecraftVersionList.Version targetVersion = null; for (JMinecraftVersionList.Version v : manifest.versions) { if (v.id.equals("1.20.1")) { targetVersion = v; break; } } // Download version JSON String versionJson = DownloadUtils.downloadString(targetVersion.url); JMinecraftVersionList.Version versionInfo = Tools.GLOBAL_GSON.fromJson( versionJson, JMinecraftVersionList.Version.class ); // Check Java requirement if (versionInfo.javaVersion != null) { int requiredJava = versionInfo.javaVersion.majorVersion; Log.i("VersionInfo", "Requires Java " + requiredJava); String runtimePath = MultiRTUtils.getRuntimeForVersion(requiredJava); setenv("JAVA_HOME", runtimePath, 1); } // Download client JAR MinecraftClientInfo clientInfo = versionInfo.downloads.get("client"); File clientJar = new File(versionDir, versionInfo.id + ".jar"); DownloadUtils.downloadFile(clientInfo.url, clientJar); // Verify SHA1 hash if (!Tools.compareSHA1(clientJar, clientInfo.sha1)) { throw new IOException("Client JAR verification failed"); } ``` ### Multi-Runtime Management ```java package net.kdt.pojavlaunch.multirt; public class MultiRTUtils { public static Runtime read(File path); public static List getRuntimes(); public static String getExactJreName(int javaVersion); } public class Runtime { public String name; // Display name public String javaVersion; // "8", "17", "21" public String versionString; // Full version string } ``` Manages multiple OpenJDK installations for different Minecraft versions. Automatically selects appropriate runtime based on version requirements. ```java // List all installed runtimes List runtimes = MultiRTUtils.getRuntimes(); for (Runtime runtime : runtimes) { Log.i("Runtime", runtime.name + " - Java " + runtime.javaVersion); } // Get runtime for Minecraft version int requiredJavaVersion = versionInfo.javaVersion.majorVersion; String runtimeName = MultiRTUtils.getExactJreName(requiredJavaVersion); File runtimePath = new File(Tools.RUNTIME_HOME, runtimeName); if (!runtimePath.exists()) { throw new RuntimeException( "Java " + requiredJavaVersion + " runtime not found. " + "Please install " + runtimeName + " from launcher settings." ); } // Set environment for JVM launch String javaHome = runtimePath.getAbsolutePath(); String javaExec = javaHome + "/bin/java"; setenv("JAVA_HOME", javaHome, 1); setenv("PATH", javaHome + "/bin:" + System.getenv("PATH"), 1); // Launch with specific runtime String[] launchArgs = new String[] { "-Djava.home=" + javaHome, "-Djava.library.path=" + javaHome + "/lib", // ... other JVM args }; VMLauncher.launchJVM(launchArgs); ``` ### Architecture Detection ```java package net.kdt.pojavlaunch; public class Architecture { public static final int ARCH_ARM64 = 0x1; public static final int ARCH_ARM = 0x2; public static final int ARCH_X86 = 0x4; public static final int ARCH_X86_64 = 0x8; public static int getDeviceArchitecture(); public static boolean is32BitsDevice(); public static String getArchAsString(int arch); } ``` Detects device CPU architecture for loading correct native libraries and runtimes. Returns bitflags allowing multi-arch detection. ```java // Detect device architecture int arch = Architecture.getDeviceArchitecture(); String archName = Architecture.getArchAsString(arch); Log.i("Device", "Architecture: " + archName); // Load architecture-specific natives String nativeLibDir; if ((arch & Architecture.ARCH_ARM64) != 0) { nativeLibDir = nativeBaseDir + "/arm64-v8a"; } else if ((arch & Architecture.ARCH_ARM) != 0) { nativeLibDir = nativeBaseDir + "/armeabi-v7a"; } else if ((arch & Architecture.ARCH_X86_64) != 0) { nativeLibDir = nativeBaseDir + "/x86_64"; } else if ((arch & Architecture.ARCH_X86) != 0) { nativeLibDir = nativeBaseDir + "/x86"; } // Check memory constraints for 32-bit devices if (Architecture.is32BitsDevice()) { int maxRam = 768; // MB limit for 32-bit if (LauncherPreferences.PREF_RAM_ALLOCATION > maxRam) { Log.w("Memory", "Reducing RAM from " + LauncherPreferences.PREF_RAM_ALLOCATION + "MB to " + maxRam + "MB"); LauncherPreferences.PREF_RAM_ALLOCATION = maxRam; } } // Select compatible renderer if (arch == Architecture.ARCH_ARM || arch == Architecture.ARCH_X86) { // 32-bit: prefer OpenGL ES 2.0 setenv("POJAV_RENDERER", "opengles2", 1); } else { // 64-bit: can use Vulkan/Zink setenv("POJAV_RENDERER", "vulkan_zink", 1); } ``` ### Gamepad Input Mapping ```java package net.kdt.pojavlaunch.customcontrols.gamepad; public class GamepadMap { public Map buttonMap; public Map joystickMap; public void save(File file) throws IOException; public static GamepadMap load(File file) throws IOException; } public class GamepadButton { public int keycode; // GLFW keycode to emit public boolean isToggle; } ``` Maps physical gamepad buttons to Minecraft keyboard/mouse inputs. Supports Xbox, PlayStation, and generic controllers via Android's input system. ```java // Create gamepad mapping GamepadMap map = new GamepadMap(); map.buttonMap = new HashMap<>(); // Map A button to Jump (Space) GamepadButton jumpBtn = new GamepadButton(); jumpBtn.keycode = GLFW_KEY_SPACE; jumpBtn.isToggle = false; map.buttonMap.put(KeyEvent.KEYCODE_BUTTON_A, jumpBtn); // Map B button to Sneak (Shift) with toggle GamepadButton sneakBtn = new GamepadButton(); sneakBtn.keycode = GLFW_KEY_LEFT_SHIFT; sneakBtn.isToggle = true; // Hold mode map.buttonMap.put(KeyEvent.KEYCODE_BUTTON_B, sneakBtn); // Map left stick to WASD movement GamepadJoystick leftStick = new GamepadJoystick(); leftStick.upKeycode = GLFW_KEY_W; leftStick.downKeycode = GLFW_KEY_S; leftStick.leftKeycode = GLFW_KEY_A; leftStick.rightKeycode = GLFW_KEY_D; leftStick.deadzone = 0.2f; // 20% deadzone map.joystickMap = new HashMap<>(); map.joystickMap.put(MotionEvent.AXIS_X, leftStick); // Map right stick to mouse camera GamepadJoystick rightStick = new GamepadJoystick(); rightStick.isMouseControl = true; rightStick.sensitivity = 1.5f; map.joystickMap.put(MotionEvent.AXIS_Z, rightStick); // Save mapping File mapFile = new File(Tools.CTRLMAP_PATH, "gamepad_default.json"); map.save(mapFile); // Load and apply at runtime GamepadMap loadedMap = GamepadMap.load(mapFile); Gamepad.setMap(loadedMap); ``` ### Progress Tracking System ```java package net.kdt.pojavlaunch.progresskeeper; public class ProgressKeeper { public static void submitProgress(String key, int progress, int messageRes); public static void submitProgress(String key, int progress, String message); public static void removeProgress(String key); } ``` Global progress notification system for downloads and long-running tasks. Automatically updates Android notifications and UI progress bars. ```java // Track download progress String progressKey = "download_libraries"; ProgressKeeper.submitProgress(progressKey, 0, R.string.downloading_libraries); for (int i = 0; i < libraries.length; i++) { DependentLibrary lib = libraries[i]; File libFile = new File(libDir, lib.name); int percentage = (int)((i / (float)libraries.length) * 100); ProgressKeeper.submitProgress( progressKey, percentage, "Downloading " + lib.name ); DownloadUtils.downloadFile(lib.downloads.artifact.url, libFile); } ProgressKeeper.submitProgress(progressKey, 100, R.string.download_complete); Thread.sleep(1000); // Show completion briefly ProgressKeeper.removeProgress(progressKey); // Track mod installation ProgressKeeper.submitProgress("install_forge", 0, "Installing Forge..."); try { ForgeUtils.installForge(versionDir, forgeVersion); ProgressKeeper.submitProgress("install_forge", 100, "Forge installed"); } catch (IOException e) { ProgressKeeper.removeProgress("install_forge"); throw e; } ``` ## Integration and Use Cases Amethyst Android serves as a complete desktop-to-mobile gaming bridge, enabling full Minecraft Java Edition gameplay on Android devices with performance comparable to low-end desktop systems. Primary use cases include playing vanilla Minecraft with touch controls or gamepad, installing and managing extensive modpacks from CurseForge/Modrinth with automatic dependency resolution, developing custom control layouts for specific gameplay styles or accessibility needs, and testing Minecraft mods/plugins in mobile environments. The launcher's multi-process architecture isolates game crashes from the launcher UI, ensuring stability during development and testing workflows. Integration patterns center around extending the launcher's capabilities through custom control schemes, mod loader plugins, and renderer backends. Developers can create custom renderers by implementing the EGL bridge interface in native code, add new mod repositories by implementing the ModpackApi interface, design specialized control layouts using the ControlData JSON format with dynamic expression-based positioning, and integrate with external authentication systems by extending the MinecraftAccount class. The architecture supports runtime Java code injection via Java agents (used for Forge installer and DNS injection), making it suitable for advanced modding scenarios and network redirection. Performance optimization focuses on native library preloading, memory-mapped asset access, and JIT compilation through OpenJDK's Hotspot VM with mobile-specific tuning for ARM architectures.