mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2025-07-20 08:15:46 +00:00
Support loading custom game settings via intent
This commit introduces the ability to launch games with custom configurations supplied via an Android intent. This allows external applications to provide specific settings for a game at launch time. Key changes include: * **`CustomSettingsHandler.kt`**: A new class responsible for: * Processing incoming intents with custom settings. * Finding the target game in the user's library by its title ID. * Writing the custom settings to a per-game INI file (`config/custom/<title_id>.ini`). * Handling potential conflicts if a custom configuration already exists, prompting the user to overwrite or cancel. * Integrating with `DriverResolver` to check for and handle required GPU drivers specified in the custom settings. * Initializing the native per-game configuration. * **`DriverResolver.kt`**: A new utility class for managing GPU drivers specified in custom settings: * Extracts the driver path from the custom settings INI content. * Checks if the required driver exists locally. * If not found locally, searches for the driver in predefined GitHub repositories (Mr. Purple Turnip, GameHub Adreno 8xx, KIMCHI Turnip, Weab-Chan Freedreno). * Prompts the user to download and install the missing driver if found online. * Handles automatic download and installation of drivers using `DriverViewModel`. * Notifies the user if a required driver cannot be found or installed. * **`AndroidManifest.xml`**: * Added a new intent filter for the action `dev.eden.eden_emulator.LAUNCH_WITH_CUSTOM_CONFIG` to `EmulationActivity`. This allows the app to respond to custom settings intents. * **`EmulationFragment.kt`**: * Modified `onCreate` to detect and handle the new custom settings intent. * If a custom settings intent is received: * It uses `CustomSettingsHandler.applyCustomSettingsWithDriverCheck` to process the settings asynchronously. This allows for driver checks and user interaction (e.g., overwrite confirmation, driver installation). * Displays appropriate error messages via `Toast` if custom settings processing fails (e.g., game not found, driver issues). * The game is then launched with the applied custom settings. * If a regular file intent or navigation arguments are used, the existing logic for loading game configurations (including custom per-game configs) is retained. * Ensures that per-game configurations are correctly loaded or unloaded based on how the game is launched.
This commit is contained in:
parent
3df06da02c
commit
eb720ef074
4 changed files with 690 additions and 12 deletions
|
@ -85,6 +85,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/octet-stream" android:scheme="content"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="dev.eden.eden_emulator.LAUNCH_WITH_CUSTOM_CONFIG" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filter" />
|
||||
</activity>
|
||||
|
||||
|
@ -100,4 +104,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||
</provider>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
|
@ -44,6 +44,7 @@ import androidx.core.view.updateLayoutParams
|
|||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
|
@ -81,6 +82,10 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
|
|||
import org.yuzu.yuzu_emu.utils.ViewUtils
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
|
||||
import org.yuzu.yuzu_emu.utils.collect
|
||||
import org.yuzu.yuzu_emu.utils.CustomSettingsHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
|
@ -94,6 +99,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
private lateinit var gpuDriver: String
|
||||
|
||||
private var _binding: FragmentEmulationBinding? = null
|
||||
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val args by navArgs<EmulationFragmentArgs>()
|
||||
|
@ -108,6 +114,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
private lateinit var gpuModel: String
|
||||
private lateinit var fwVersion: String
|
||||
|
||||
private var intentGame: Game? = null
|
||||
private var isCustomSettingsIntent = false
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
if (context is EmulationActivity) {
|
||||
|
@ -125,9 +134,70 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
super.onCreate(savedInstanceState)
|
||||
updateOrientation()
|
||||
|
||||
val intentUri: Uri? = requireActivity().intent.data
|
||||
var intentGame: Game? = null
|
||||
if (intentUri != null) {
|
||||
val intent = requireActivity().intent
|
||||
val intentUri: Uri? = intent.data
|
||||
intentGame = null
|
||||
isCustomSettingsIntent = false
|
||||
|
||||
if (intent.action == CustomSettingsHandler.CUSTOM_CONFIG_ACTION) {
|
||||
val titleId = intent.getStringExtra(CustomSettingsHandler.EXTRA_TITLE_ID)
|
||||
val customSettings = intent.getStringExtra(CustomSettingsHandler.EXTRA_CUSTOM_SETTINGS)
|
||||
|
||||
if (titleId != null && customSettings != null) {
|
||||
Log.info("[EmulationFragment] Received custom settings intent for title: $titleId")
|
||||
|
||||
// Handle custom settings asynchronously to allow for driver checking/installation
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
intentGame = CustomSettingsHandler.applyCustomSettingsWithDriverCheck(
|
||||
titleId,
|
||||
customSettings,
|
||||
requireContext(),
|
||||
requireActivity() as? FragmentActivity,
|
||||
driverViewModel
|
||||
)
|
||||
|
||||
if (intentGame == null) {
|
||||
Log.error("[EmulationFragment] Custom settings processing failed for title ID: $titleId")
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
"Failed to apply custom settings. This could be due to:\n• Game not found in library\n• User cancelled configuration overwrite\n• Driver installation failed\n• Missing required drivers",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
requireActivity().finish()
|
||||
return@launch
|
||||
}
|
||||
|
||||
isCustomSettingsIntent = true
|
||||
|
||||
// Continue with game setup
|
||||
finishGameSetup()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.error("[EmulationFragment] Error processing custom settings: ${e.message}")
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
"Error processing custom settings: ${e.message}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
|
||||
// Return early to prevent synchronous continuation
|
||||
return
|
||||
} else {
|
||||
Log.error("[EmulationFragment] Custom settings intent missing required extras")
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
"Invalid custom settings data",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
requireActivity().finish()
|
||||
return
|
||||
}
|
||||
} else if (intentUri != null) {
|
||||
// Handle regular file intent
|
||||
intentGame = if (Game.extensions.contains(FileUtil.getExtension(intentUri))) {
|
||||
GameHelper.getGame(requireActivity().intent.data!!, false)
|
||||
} else {
|
||||
|
@ -135,6 +205,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
}
|
||||
}
|
||||
|
||||
if (!isCustomSettingsIntent) {
|
||||
finishGameSetup()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the game setup process (extracted for async custom settings handling)
|
||||
*/
|
||||
private fun finishGameSetup() {
|
||||
try {
|
||||
game = if (args.game != null) {
|
||||
args.game!!
|
||||
|
@ -151,8 +230,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
return
|
||||
}
|
||||
|
||||
// Always load custom settings when launching a game from an intent
|
||||
if (args.custom || intentGame != null) {
|
||||
// Handle configuration loading
|
||||
if (isCustomSettingsIntent) {
|
||||
// Custom settings already applied by CustomSettingsHandler
|
||||
Log.info("[EmulationFragment] Using custom settings from intent")
|
||||
} else if (args.custom || intentGame != null) {
|
||||
// Always load custom settings when launching a game from an intent
|
||||
SettingsFile.loadCustomConfig(game)
|
||||
NativeConfig.unloadPerGameConfig()
|
||||
} else {
|
||||
|
@ -162,13 +245,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
// Install the selected driver asynchronously as the game starts
|
||||
driverViewModel.onLaunchGame()
|
||||
|
||||
// So this fragment doesn't restart on configuration changes; i.e. rotation.
|
||||
retainInstance = true
|
||||
// Initialize emulation state (ViewModels handle state retention now)
|
||||
emulationState = EmulationState(game.path) {
|
||||
return@EmulationState driverViewModel.isInteractionAllowed.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the UI and start emulation in here.
|
||||
*/
|
||||
|
@ -634,7 +717,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
val batteryTemp = getBatteryTemperature()
|
||||
when (IntSetting.BAT_TEMPERATURE_UNIT.getInt(needsGlobal)) {
|
||||
0 -> sb.append(String.format("%.1f°C", batteryTemp))
|
||||
1 -> sb.append(String.format("%.1f°F", celsiusToFahrenheit(batteryTemp)))
|
||||
1 -> sb.append(
|
||||
String.format(
|
||||
"%.1f°F",
|
||||
celsiusToFahrenheit(batteryTemp)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -643,8 +731,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
|
||||
val battery: BatteryManager =
|
||||
requireContext().getSystemService(Context.BATTERY_SERVICE) as BatteryManager
|
||||
val batteryIntent = requireContext().registerReceiver(null,
|
||||
IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||
val batteryIntent = requireContext().registerReceiver(
|
||||
null,
|
||||
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
||||
)
|
||||
|
||||
val capacity = battery.getIntProperty(BATTERY_PROPERTY_CAPACITY)
|
||||
val nowUAmps = battery.getIntProperty(BATTERY_PROPERTY_CURRENT_NOW)
|
||||
|
@ -653,7 +743,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
|
||||
val status = batteryIntent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
||||
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
||||
status == BatteryManager.BATTERY_STATUS_FULL
|
||||
status == BatteryManager.BATTERY_STATUS_FULL
|
||||
|
||||
if (isCharging) {
|
||||
sb.append(" ${getString(R.string.charging)}")
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.utils
|
||||
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.yuzu.yuzu_emu.model.DriverViewModel
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import java.io.File
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
object CustomSettingsHandler {
|
||||
const val CUSTOM_CONFIG_ACTION = "dev.eden.eden_emulator.LAUNCH_WITH_CUSTOM_CONFIG"
|
||||
const val EXTRA_TITLE_ID = "title_id"
|
||||
const val EXTRA_CUSTOM_SETTINGS = "custom_settings"
|
||||
|
||||
/**
|
||||
* Apply custom settings from a string instead of loading from file
|
||||
* @param titleId The game title ID (16-digit hex string)
|
||||
* @param customSettings The complete INI file content as string
|
||||
* @param context Application context
|
||||
* @return Game object created from title ID, or null if not found
|
||||
*/
|
||||
fun applyCustomSettings(titleId: String, customSettings: String, context: Context): Game? {
|
||||
// For synchronous calls without driver checking
|
||||
Log.info("[CustomSettingsHandler] Applying custom settings for title ID: $titleId")
|
||||
// Find the game by title ID
|
||||
val game = findGameByTitleId(titleId, context)
|
||||
if (game == null) {
|
||||
Log.error("[CustomSettingsHandler] Game not found for title ID: $titleId")
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if config already exists - this should be handled by the caller
|
||||
val configFile = getConfigFile(titleId)
|
||||
if (configFile.exists()) {
|
||||
Log.warning("[CustomSettingsHandler] Config file already exists for title ID: $titleId")
|
||||
// The caller should have already asked the user about overwriting
|
||||
}
|
||||
|
||||
// Write the config file
|
||||
if (!writeConfigFile(titleId, customSettings)) {
|
||||
Log.error("[CustomSettingsHandler] Failed to write config file")
|
||||
return null
|
||||
}
|
||||
|
||||
// Initialize per-game config
|
||||
try {
|
||||
NativeConfig.initializePerGameConfig(game.programId, configFile.nameWithoutExtension)
|
||||
Log.info("[CustomSettingsHandler] Successfully applied custom settings")
|
||||
return game
|
||||
} catch (e: Exception) {
|
||||
Log.error("[CustomSettingsHandler] Failed to apply custom settings: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply custom settings with automatic driver checking and installation
|
||||
* @param titleId The game title ID (16-digit hex string)
|
||||
* @param customSettings The complete INI file content as string
|
||||
* @param context Application context
|
||||
* @param activity Fragment activity for driver installation dialogs (optional)
|
||||
* @param driverViewModel DriverViewModel for driver management (optional)
|
||||
* @return Game object created from title ID, or null if not found
|
||||
*/
|
||||
suspend fun applyCustomSettingsWithDriverCheck(
|
||||
titleId: String,
|
||||
customSettings: String,
|
||||
context: Context,
|
||||
activity: FragmentActivity?,
|
||||
driverViewModel: DriverViewModel?
|
||||
): Game? {
|
||||
Log.info("[CustomSettingsHandler] Applying custom settings for title ID: $titleId")
|
||||
// Find the game by title ID
|
||||
val game = findGameByTitleId(titleId, context)
|
||||
if (game == null) {
|
||||
Log.error("[CustomSettingsHandler] Game not found for title ID: $titleId")
|
||||
// This will be handled by the caller to show appropriate error message
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if config already exists
|
||||
val configFile = getConfigFile(titleId)
|
||||
if (configFile.exists() && activity != null) {
|
||||
Log.info("[CustomSettingsHandler] Config file already exists, asking user for confirmation")
|
||||
val shouldOverwrite = askUserToOverwriteConfig(activity, game.title)
|
||||
if (!shouldOverwrite) {
|
||||
Log.info("[CustomSettingsHandler] User chose not to overwrite existing config")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Check for driver requirements if activity and driverViewModel are provided
|
||||
if (activity != null && driverViewModel != null) {
|
||||
val driverPath = DriverResolver.extractDriverPath(customSettings)
|
||||
if (driverPath != null) {
|
||||
Log.info("[CustomSettingsHandler] Custom settings specify driver: $driverPath")
|
||||
val driverExists = DriverResolver.ensureDriverExists(driverPath, activity, driverViewModel)
|
||||
if (!driverExists) {
|
||||
Log.error("[CustomSettingsHandler] Required driver not available: $driverPath")
|
||||
// Don't write config if driver installation failed
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only write the config file after all checks pass
|
||||
if (!writeConfigFile(titleId, customSettings)) {
|
||||
Log.error("[CustomSettingsHandler] Failed to write config file")
|
||||
return null
|
||||
}
|
||||
|
||||
// Initialize per-game config
|
||||
try {
|
||||
NativeConfig.initializePerGameConfig(game.programId, configFile.nameWithoutExtension)
|
||||
Log.info("[CustomSettingsHandler] Successfully applied custom settings")
|
||||
return game
|
||||
} catch (e: Exception) {
|
||||
Log.error("[CustomSettingsHandler] Failed to apply custom settings: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a game by its title ID in the user's game library
|
||||
*/
|
||||
private fun findGameByTitleId(titleId: String, context: Context): Game? {
|
||||
Log.info("[CustomSettingsHandler] Searching for game with title ID: $titleId")
|
||||
// Convert hex title ID to decimal for comparison with programId
|
||||
val programIdDecimal = try {
|
||||
titleId.toLong(16).toString()
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.error("[CustomSettingsHandler] Invalid title ID format: $titleId")
|
||||
return null
|
||||
}
|
||||
|
||||
// Expected hex format with "0" prefix
|
||||
val expectedHex = "0${titleId.uppercase()}"
|
||||
// First check cached games for fast lookup
|
||||
GameHelper.cachedGameList.find { game ->
|
||||
game.programId == programIdDecimal ||
|
||||
game.programIdHex.equals(expectedHex, ignoreCase = true)
|
||||
}?.let { foundGame ->
|
||||
Log.info("[CustomSettingsHandler] Found game in cache: ${foundGame.title}")
|
||||
return foundGame
|
||||
}
|
||||
// If not in cache, perform full game library scan
|
||||
Log.info("[CustomSettingsHandler] Game not in cache, scanning full library...")
|
||||
val allGames = GameHelper.getGames()
|
||||
val foundGame = allGames.find { game ->
|
||||
game.programId == programIdDecimal ||
|
||||
game.programIdHex.equals(expectedHex, ignoreCase = true)
|
||||
}
|
||||
if (foundGame != null) {
|
||||
Log.info("[CustomSettingsHandler] Found game: ${foundGame.title} at ${foundGame.path}")
|
||||
} else {
|
||||
Log.warning("[CustomSettingsHandler] No game found for title ID: $titleId")
|
||||
}
|
||||
return foundGame
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the config file path for a title ID
|
||||
*/
|
||||
private fun getConfigFile(titleId: String): File {
|
||||
val configDir = File(DirectoryInitialization.userDirectory, "config/custom")
|
||||
return File(configDir, "$titleId.ini")
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the config file with the custom settings
|
||||
*/
|
||||
private fun writeConfigFile(titleId: String, customSettings: String): Boolean {
|
||||
return try {
|
||||
val configDir = File(DirectoryInitialization.userDirectory, "config/custom")
|
||||
if (!configDir.exists()) {
|
||||
configDir.mkdirs()
|
||||
}
|
||||
|
||||
val configFile = File(configDir, "$titleId.ini")
|
||||
configFile.writeText(customSettings)
|
||||
|
||||
Log.info("[CustomSettingsHandler] Wrote config file: ${configFile.absolutePath}")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.error("[CustomSettingsHandler] Failed to write config file: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user if they want to overwrite existing configuration
|
||||
*/
|
||||
private suspend fun askUserToOverwriteConfig(activity: FragmentActivity, gameTitle: String): Boolean {
|
||||
return suspendCoroutine { continuation ->
|
||||
activity.runOnUiThread {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle("Configuration Already Exists")
|
||||
.setMessage(
|
||||
"Custom settings already exist for '$gameTitle'.\n\n" +
|
||||
"Do you want to overwrite the existing configuration?\n\n" +
|
||||
"This action cannot be undone."
|
||||
)
|
||||
.setPositiveButton("Overwrite") { _, _ ->
|
||||
continuation.resume(true)
|
||||
}
|
||||
.setNegativeButton("Cancel") { _, _ ->
|
||||
continuation.resume(false)
|
||||
}
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,362 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.utils
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.yuzu.yuzu_emu.fragments.DriverFetcherFragment
|
||||
import org.yuzu.yuzu_emu.fragments.DriverFetcherFragment.SortMode
|
||||
import org.yuzu.yuzu_emu.model.DriverViewModel
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
object DriverResolver {
|
||||
private val client = OkHttpClient()
|
||||
|
||||
// Mirror of the repositories from DriverFetcherFragment
|
||||
private val repoList = listOf(
|
||||
DriverRepo("Mr. Purple Turnip", "MrPurple666/purple-turnip", 0),
|
||||
DriverRepo("GameHub Adreno 8xx", "crueter/GameHub-8Elite-Drivers", 1),
|
||||
DriverRepo("KIMCHI Turnip", "K11MCH1/AdrenoToolsDrivers", 2, true),
|
||||
DriverRepo("Weab-Chan Freedreno", "Weab-chan/freedreno_turnip-CI", 3),
|
||||
)
|
||||
|
||||
private data class DriverRepo(
|
||||
val name: String,
|
||||
val path: String,
|
||||
val sort: Int,
|
||||
val useTagName: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Extract driver path from custom settings INI content
|
||||
*/
|
||||
fun extractDriverPath(customSettings: String): String? {
|
||||
val lines = customSettings.lines()
|
||||
var inGpuDriverSection = false
|
||||
|
||||
for (line in lines) {
|
||||
val trimmed = line.trim()
|
||||
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
||||
inGpuDriverSection = trimmed == "[GpuDriver]"
|
||||
continue
|
||||
}
|
||||
|
||||
if (inGpuDriverSection && trimmed.startsWith("driver_path=")) {
|
||||
return trimmed.substringAfter("driver_path=")
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a driver exists and handle missing drivers
|
||||
*/
|
||||
suspend fun ensureDriverExists(
|
||||
driverPath: String,
|
||||
activity: FragmentActivity,
|
||||
driverViewModel: DriverViewModel
|
||||
): Boolean {
|
||||
Log.info("[DriverResolver] Checking driver path: $driverPath")
|
||||
|
||||
val driverFile = File(driverPath)
|
||||
if (driverFile.exists()) {
|
||||
Log.info("[DriverResolver] Driver exists at: $driverPath")
|
||||
return true
|
||||
}
|
||||
|
||||
Log.warning("[DriverResolver] Driver not found: $driverPath")
|
||||
|
||||
// Extract driver name from path
|
||||
val driverName = extractDriverNameFromPath(driverPath)
|
||||
if (driverName == null) {
|
||||
Log.error("[DriverResolver] Could not extract driver name from path")
|
||||
return false
|
||||
}
|
||||
|
||||
Log.info("[DriverResolver] Searching for downloadable driver: $driverName")
|
||||
|
||||
// Check if driver exists locally with different path
|
||||
val localDriver = findLocalDriver(driverName)
|
||||
if (localDriver != null) {
|
||||
Log.info("[DriverResolver] Found local driver: ${localDriver.first}")
|
||||
// The game can use this local driver, no need to download
|
||||
return true
|
||||
}
|
||||
|
||||
// Search for downloadable driver
|
||||
val downloadableDriver = findDownloadableDriver(driverName)
|
||||
if (downloadableDriver != null) {
|
||||
Log.info("[DriverResolver] Found downloadable driver: ${downloadableDriver.name}")
|
||||
|
||||
val shouldInstall = askUserToInstallDriver(activity, downloadableDriver.name)
|
||||
if (shouldInstall) {
|
||||
return downloadAndInstallDriver(activity, downloadableDriver, driverViewModel)
|
||||
}
|
||||
} else {
|
||||
Log.warning("[DriverResolver] No downloadable driver found for: $driverName")
|
||||
showDriverNotFoundDialog(activity, driverName)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract driver name from full path
|
||||
*/
|
||||
private fun extractDriverNameFromPath(driverPath: String): String? {
|
||||
val file = File(driverPath)
|
||||
val fileName = file.name
|
||||
|
||||
// Remove .zip extension and extract meaningful name
|
||||
if (fileName.endsWith(".zip")) {
|
||||
return fileName.substring(0, fileName.length - 4)
|
||||
}
|
||||
|
||||
return fileName
|
||||
}
|
||||
|
||||
/**
|
||||
* Find driver in local storage by name matching
|
||||
*/
|
||||
private fun findLocalDriver(driverName: String): Pair<String, GpuDriverMetadata>? {
|
||||
val availableDrivers = GpuDriverHelper.getDrivers()
|
||||
|
||||
// Try exact match first
|
||||
availableDrivers.find { (_, metadata) ->
|
||||
metadata.name?.contains(driverName, ignoreCase = true) == true
|
||||
}?.let { return it }
|
||||
|
||||
// Try partial match
|
||||
availableDrivers.find { (path, metadata) ->
|
||||
path.contains(driverName, ignoreCase = true) ||
|
||||
metadata.name?.contains(
|
||||
extractKeywords(driverName).first(),
|
||||
ignoreCase = true
|
||||
) == true
|
||||
}?.let { return it }
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract keywords from driver name for matching
|
||||
*/
|
||||
private fun extractKeywords(driverName: String): List<String> {
|
||||
val keywords = mutableListOf<String>()
|
||||
|
||||
// Common driver patterns
|
||||
when {
|
||||
driverName.contains("turnip", ignoreCase = true) -> keywords.add("turnip")
|
||||
driverName.contains("purple", ignoreCase = true) -> keywords.add("purple")
|
||||
driverName.contains("kimchi", ignoreCase = true) -> keywords.add("kimchi")
|
||||
driverName.contains("freedreno", ignoreCase = true) -> keywords.add("freedreno")
|
||||
driverName.contains("gamehub", ignoreCase = true) -> keywords.add("gamehub")
|
||||
}
|
||||
|
||||
// Version patterns
|
||||
Regex("v?\\d+\\.\\d+\\.\\d+").find(driverName)?.value?.let { keywords.add(it) }
|
||||
|
||||
if (keywords.isEmpty()) {
|
||||
keywords.add(driverName)
|
||||
}
|
||||
|
||||
return keywords
|
||||
}
|
||||
|
||||
/**
|
||||
* Find downloadable driver that matches the required driver
|
||||
*/
|
||||
private suspend fun findDownloadableDriver(driverName: String): DriverFetcherFragment.Artifact? {
|
||||
val keywords = extractKeywords(driverName)
|
||||
|
||||
for (repo in repoList) {
|
||||
// Check if this repo is relevant based on driver name
|
||||
val isRelevant = keywords.any { keyword ->
|
||||
repo.name.contains(keyword, ignoreCase = true) ||
|
||||
keyword.contains(repo.name.split(" ").first(), ignoreCase = true)
|
||||
}
|
||||
|
||||
if (!isRelevant) continue
|
||||
|
||||
try {
|
||||
val releases = fetchReleases(repo)
|
||||
val latestRelease = releases.firstOrNull { !it.prerelease }
|
||||
|
||||
latestRelease?.artifacts?.forEach { artifact ->
|
||||
if (matchesDriverName(artifact.name, driverName, keywords)) {
|
||||
return artifact
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.error("[DriverResolver] Failed to fetch releases for ${repo.name}: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if artifact name matches the required driver
|
||||
*/
|
||||
private fun matchesDriverName(
|
||||
artifactName: String,
|
||||
requiredName: String,
|
||||
keywords: List<String>
|
||||
): Boolean {
|
||||
// Exact match
|
||||
if (artifactName.equals(requiredName, ignoreCase = true)) return true
|
||||
|
||||
// Keyword matching
|
||||
return keywords.any { keyword ->
|
||||
artifactName.contains(keyword, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch releases from GitHub repo
|
||||
*/
|
||||
private suspend fun fetchReleases(repo: DriverRepo): List<DriverFetcherFragment.Release> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val request = Request.Builder()
|
||||
.url("https://api.github.com/repos/${repo.path}/releases")
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Failed to fetch releases: ${response.code}")
|
||||
}
|
||||
|
||||
val body = response.body?.string() ?: throw IOException("Empty response")
|
||||
DriverFetcherFragment.Release.fromJsonArray(body, repo.useTagName, SortMode.Default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask user if they want to install the missing driver
|
||||
*/
|
||||
private suspend fun askUserToInstallDriver(
|
||||
activity: FragmentActivity,
|
||||
driverName: String
|
||||
): Boolean {
|
||||
return suspendCoroutine { continuation ->
|
||||
activity.runOnUiThread {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle("Missing GPU Driver")
|
||||
.setMessage(
|
||||
"The custom settings require the GPU driver '$driverName' which is not installed.\n\n" +
|
||||
"Would you like to download and install it automatically?"
|
||||
)
|
||||
.setPositiveButton("Install") { _, _ ->
|
||||
continuation.resume(true)
|
||||
}
|
||||
.setNegativeButton("Cancel") { _, _ ->
|
||||
continuation.resume(false)
|
||||
}
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and install driver automatically
|
||||
*/
|
||||
private suspend fun downloadAndInstallDriver(
|
||||
activity: FragmentActivity,
|
||||
artifact: DriverFetcherFragment.Artifact,
|
||||
driverViewModel: DriverViewModel
|
||||
): Boolean {
|
||||
return try {
|
||||
Log.info("[DriverResolver] Downloading driver: ${artifact.name}")
|
||||
|
||||
val cacheDir =
|
||||
activity.externalCacheDir ?: throw IOException("Cache directory not available")
|
||||
cacheDir.mkdirs()
|
||||
|
||||
val file = File(cacheDir, artifact.name)
|
||||
|
||||
// Download the driver
|
||||
withContext(Dispatchers.IO) {
|
||||
val request = Request.Builder()
|
||||
.url(artifact.url)
|
||||
.header("Accept", "application/octet-stream")
|
||||
.build()
|
||||
|
||||
client.newBuilder()
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.build()
|
||||
.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Download failed: ${response.code}")
|
||||
}
|
||||
|
||||
response.body?.byteStream()?.use { input ->
|
||||
FileOutputStream(file).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
} ?: throw IOException("Empty response body")
|
||||
}
|
||||
}
|
||||
|
||||
if (file.length() == 0L) {
|
||||
throw IOException("Downloaded file is empty")
|
||||
}
|
||||
|
||||
// Install the driver on main thread
|
||||
withContext(Dispatchers.Main) {
|
||||
val driverData = GpuDriverHelper.getMetadataFromZip(file)
|
||||
val driverPath = "${GpuDriverHelper.driverStoragePath}${file.name}"
|
||||
|
||||
if (GpuDriverHelper.copyDriverToInternalStorage(file.toUri())) {
|
||||
driverViewModel.onDriverAdded(Pair(driverPath, driverData))
|
||||
Log.info("[DriverResolver] Successfully installed driver: ${driverData.name}")
|
||||
true
|
||||
} else {
|
||||
throw IOException("Failed to install driver")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.error("[DriverResolver] Failed to download/install driver: ${e.message}")
|
||||
withContext(Dispatchers.Main) {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle("Installation Failed")
|
||||
.setMessage("Failed to download and install the driver: ${e.message}")
|
||||
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
|
||||
.show()
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show dialog when driver cannot be found
|
||||
*/
|
||||
private fun showDriverNotFoundDialog(activity: FragmentActivity, driverName: String) {
|
||||
activity.runOnUiThread {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle("Driver Not Available")
|
||||
.setMessage(
|
||||
"The required GPU driver '$driverName' is not available for automatic download.\n\n" +
|
||||
"Please manually install the driver or launch the game with default settings."
|
||||
)
|
||||
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue