Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
980 changes: 980 additions & 0 deletions app/src/main/java/app/gamenative/api/CommunityConfigService.kt

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ enum class AppOptionMenuType(@StringRes val title: Int) {
SubmitFeedback(R.string.option_submit_feedback),
ResetDrm(R.string.option_reset_drm),
UseKnownConfig(R.string.option_use_known_config),
BrowseCommunityConfigs(R.string.option_browse_community_configs),
ImportConfig(R.string.import_config),
ExportConfig(R.string.export_config),
ImportSaves(R.string.option_import_saves),
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -431,15 +431,16 @@ class CustomGameAppScreen : BaseAppScreen() {
}

/**
* For Custom games, only show Export/Import config in the Container section.
* We intentionally omit the generic "Use known config" here.
* Custom games omit the generic "Use known config" action but retain the
* community browser and config transfer actions.
*/
@Composable
override fun getConfigMenuOptions(
context: Context,
libraryItem: LibraryItem,
): List<AppMenuOption> {
return listOfNotNull(
getBrowseCommunityConfigsOption(context, libraryItem),
getExportConfigOption(context, libraryItem),
getImportConfigOption(context, libraryItem),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import androidx.compose.material.icons.filled.Image
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.RestartAlt
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.SdStorage
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Share
Expand Down Expand Up @@ -330,6 +331,7 @@ private fun getIconForOption(type: AppOptionMenuType): ImageVector {
AppOptionMenuType.SubmitFeedback -> Icons.Default.Feedback
AppOptionMenuType.ResetDrm -> Icons.Default.Key
AppOptionMenuType.UseKnownConfig -> Icons.Default.Build
AppOptionMenuType.BrowseCommunityConfigs -> Icons.Default.Search
AppOptionMenuType.Uninstall -> Icons.Default.Delete
AppOptionMenuType.VerifyFiles -> Icons.Default.VerifiedUser
AppOptionMenuType.Update -> Icons.Default.Update
Expand Down Expand Up @@ -383,6 +385,7 @@ private fun groupOptions(options: List<AppMenuOption>): Map<OptionCategory, List
AppOptionMenuType.ResetToDefaults,
AppOptionMenuType.ResetDrm,
AppOptionMenuType.UseKnownConfig,
AppOptionMenuType.BrowseCommunityConfigs,
AppOptionMenuType.ImportConfig,
AppOptionMenuType.ExportConfig,
AppOptionMenuType.ImportSaves,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,15 @@ object ContainerConfigTransfer {
val matchType = "exact_gpu_match"

// 1) Parse config into a validated map of fields to apply
val bestConfigMap = BestConfigService.parseConfigToContainerData(
val parsedResult = BestConfigService.parseConfigResult(
context = context,
configJson = configJson,
matchType = matchType,
applyKnownConfig = true,
) ?: emptyMap()
)
val bestConfigMap = parsedResult.config

val missingComponents = BestConfigService.consumeLastMissingComponents()
val missingComponents = parsedResult.missingComponents
if (bestConfigMap.isEmpty()) {
if (missingComponents.isNotEmpty()) {
BaseAppScreen.showMissingComponentsDialog(appId, missingComponents) {
Expand All @@ -109,8 +110,8 @@ object ContainerConfigTransfer {
try {
val forced = BestConfigService.parseConfigToContainerData(
context, configJson, matchType, true, forceApply = true,
) ?: emptyMap()
if (forced.isEmpty()) {
)
if (forced.isNullOrEmpty()) {
SnackbarManager.show(context.getString(R.string.best_config_known_config_invalid))
return@launch
}
Expand Down
147 changes: 105 additions & 42 deletions app/src/main/java/app/gamenative/utils/BestConfigService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,23 @@ import app.gamenative.PrefManager
import app.gamenative.R
import com.winlator.box86_64.Box86_64PresetManager
import com.winlator.container.Container
import com.winlator.container.ContainerData
import com.winlator.core.DefaultVersion
import com.winlator.contents.ContentProfile
import com.winlator.core.DefaultVersion
import com.winlator.core.GPUInformation
import com.winlator.fexcore.FEXCorePresetManager
import com.winlator.core.KeyValueSet
import com.winlator.fexcore.FEXCorePresetManager
import java.util.Locale
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonObject
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import timber.log.Timber
import java.util.Locale
import java.util.concurrent.ConcurrentHashMap

/**
* Service for fetching best configurations for games from GameNative API.
Expand All @@ -37,14 +35,6 @@ object BestConfigService {
// In-memory cache keyed by "${gameName}_${gpuName}"
private val cache = ConcurrentHashMap<String, BestConfigResponse>()

// unavailable components from last config validation
private var lastMissingComponents: List<String> = emptyList()

fun consumeLastMissingComponents(): List<String> {
val result = lastMissingComponents
lastMissingComponents = emptyList()
return result
}
/**
* Data class for API response.
*/
Expand All @@ -61,7 +51,7 @@ object BestConfigService {
*/
data class CompatibilityMessage(
val text: String,
val color: Color
val color: Color,
)

data class ManifestInstallRequest(
Expand All @@ -70,6 +60,11 @@ object BestConfigService {
val isDriver: Boolean = false,
)

data class ParsedConfigResult(
val config: Map<String, Any?>,
val missingComponents: List<String> = emptyList(),
)

/**
* Fetches best configuration for a game.
* Returns cached response if available, otherwise makes API call.
Expand Down Expand Up @@ -179,7 +174,8 @@ object BestConfigService {

/**
* Filters config JSON based on match type.
* For fallback_match, excludes containerVariant, graphicsDriver, dxwrapper, and dxwrapperConfig.
* For fallback_match, excludes GPU-specific driver and wrapper settings. The container variant
* remains because it determines which runtime and dependency variants the config requires.
*/
fun filterConfigByMatchType(config: JsonObject, matchType: String, storeMatch: Boolean = true): JsonObject {
val filtered = config.toMutableMap()
Expand All @@ -194,7 +190,7 @@ object BestConfigService {
}

if (matchType == "fallback_match") {
// Exclude containerVariant, graphicsDriver, dxwrapper, dxwrapperConfig
// Exclude GPU-specific driver and wrapper settings.
filtered.remove("graphicsDriver")
filtered.remove("graphicsDriverVersion")
filtered.remove("graphicsDriverConfig")
Expand Down Expand Up @@ -242,6 +238,16 @@ object BestConfigService {
filteredJson.put("graphicsDriverVersion", "Turnip Adreno Driver T26 (@Mr_Purple_666)")
}

if (GPUInformation.isAdreno8Elite(context) &&
!GPUInformation.isAdreno8EliteGen5(context) &&
!matched.matches(Regex(".*adreno.*\\b83[0-9]\\b.*"))
) {
val kvs = KeyValueSet(filteredJson.optString("graphicsDriverConfig", ""))
kvs.put("version", ContainerUtils.WRAPPER_ADRENO_8ELITE)
filteredJson.put("graphicsDriverConfig", kvs.toString())
filteredJson.put("graphicsDriverVersion", ContainerUtils.WRAPPER_ADRENO_8ELITE)
}

if (GPUInformation.isAdrenoA12(context) && !matched.matches(Regex(".*adreno.*\\ba12\\b.*"))) {
val kvs = KeyValueSet(filteredJson.optString("graphicsDriverConfig", ""))
kvs.put("version", ContainerUtils.WRAPPER_ADRENO_A12)
Expand All @@ -262,6 +268,24 @@ object BestConfigService {
return filteredJson
}

private fun prepareConfigForApplication(
context: Context,
configJson: JsonObject,
matchType: String,
storeMatch: Boolean = true,
matchedGpu: String = "",
preserveConfigValues: Boolean = false,
): JSONObject {
val effectiveMatchType = if (preserveConfigValues) "exact_gpu_match" else matchType
val filteredConfig = filterConfigByMatchType(configJson, effectiveMatchType, storeMatch)
val filteredJson = JSONObject(filteredConfig.toString())
return if (preserveConfigValues) {
filteredJson
} else {
applyGpuFamilyOverrides(context, filteredJson, matchedGpu)
}
}

/**
* Validates component versions in the filtered JSON.
* Returns list of human-readable descriptions of missing/unavailable components.
Expand Down Expand Up @@ -488,10 +512,16 @@ object BestConfigService {
configJson: JsonObject,
matchType: String,
matchedGpu: String = "",
preserveConfigValues: Boolean = false,
): List<ManifestInstallRequest> {
val updatedConfigJson = Json.parseToJsonElement(configJson.toString()).jsonObject
val filteredConfig = filterConfigByMatchType(updatedConfigJson, matchType)
val filteredJson = applyGpuFamilyOverrides(context, JSONObject(filteredConfig.toString()), matchedGpu)
val filteredJson = prepareConfigForApplication(
context = context,
configJson = updatedConfigJson,
matchType = matchType,
matchedGpu = matchedGpu,
preserveConfigValues = preserveConfigValues,
)
val installed = ManifestComponentHelper.loadInstalledContentLists(context)
val manifest = ManifestRepository.loadManifest(context)
val installedContent = installed.installed
Expand Down Expand Up @@ -742,6 +772,7 @@ object BestConfigService {
* First parses values (using PrefManager defaults for validation), then validates component versions.
* Returns map with only fields present in config (no defaults), or empty map if validation fails.
* When forceApply is true, missing components are replaced with defaults instead of rejecting.
* When preserveConfigValues is true, match filtering and device-specific substitutions are skipped.
*/
suspend fun parseConfigToContainerData(
context: Context,
Expand All @@ -751,7 +782,28 @@ object BestConfigService {
storeMatch: Boolean = true,
forceApply: Boolean = false,
matchedGpu: String = "",
): Map<String, Any?>? {
preserveConfigValues: Boolean = false,
): Map<String, Any?>? = parseConfigResult(
context = context,
configJson = configJson,
matchType = matchType,
applyKnownConfig = applyKnownConfig,
storeMatch = storeMatch,
forceApply = forceApply,
matchedGpu = matchedGpu,
preserveConfigValues = preserveConfigValues,
).config

suspend fun parseConfigResult(
context: Context,
configJson: JsonObject,
matchType: String,
applyKnownConfig: Boolean,
storeMatch: Boolean = true,
forceApply: Boolean = false,
matchedGpu: String = "",
preserveConfigValues: Boolean = false,
): ParsedConfigResult {
try {
val originalJson = JSONObject(configJson.toString())

Expand All @@ -763,13 +815,13 @@ object BestConfigService {
if (originalJson.has("useLegacyDRM") && !originalJson.isNull("useLegacyDRM")) {
resultMap["useLegacyDRM"] = originalJson.optBoolean("useLegacyDRM", PrefManager.useLegacyDRM)
}
return resultMap
return ParsedConfigResult(resultMap)
}

else {
if (!originalJson.has("containerVariant") || originalJson.isNull("containerVariant")) {
Timber.tag("BestConfigService").w("containerVariant is missing or null in original config, returning empty map")
return mapOf()
return ParsedConfigResult(emptyMap())
}

val containerVariant = originalJson.optString("containerVariant", "")
Expand All @@ -778,7 +830,7 @@ object BestConfigService {
// server best-config responses nor JSON imports can switch a container to glibc.
if (BuildConfig.MODERN_ANDROID && containerVariant.equals(Container.GLIBC, ignoreCase = true)) {
Timber.tag("BestConfigService").w("Rejecting glibc containerVariant on modern flavor")
return mapOf()
return ParsedConfigResult(emptyMap())
}

if (!originalJson.has("wineVersion") || originalJson.isNull("wineVersion")) {
Expand All @@ -787,16 +839,16 @@ object BestConfigService {
}
else {
Timber.tag("BestConfigService").w("wineVersion is missing or null in original config, returning empty map")
return mapOf()
return ParsedConfigResult(emptyMap())
}
}
if (!originalJson.has("dxwrapper") || originalJson.isNull("dxwrapper")) {
Timber.tag("BestConfigService").w("dxwrapper is missing or null in original config, returning empty map")
return mapOf()
return ParsedConfigResult(emptyMap())
}
if (!originalJson.has("dxwrapperConfig") || originalJson.isNull("dxwrapperConfig")) {
Timber.tag("BestConfigService").w("dxwrapperConfig is missing or null in original config, returning empty map")
return mapOf()
return ParsedConfigResult(emptyMap())
}

// Also check they're not empty strings
Expand All @@ -806,35 +858,41 @@ object BestConfigService {

if (containerVariant.isEmpty()) {
Timber.tag("BestConfigService").w("containerVariant is empty in original config, returning empty map")
return mapOf()
return ParsedConfigResult(emptyMap())
}
if (wineVersion.isEmpty()) {
Timber.tag("BestConfigService").w("wineVersion is empty in original config, returning empty map")
return mapOf()
return ParsedConfigResult(emptyMap())
}
if (dxwrapper.isEmpty()) {
Timber.tag("BestConfigService").w("dxwrapper is empty in original config, returning empty map")
return mapOf()
return ParsedConfigResult(emptyMap())
}
if (dxwrapperConfig.isEmpty()) {
Timber.tag("BestConfigService").w("dxwrapperConfig is empty in original config, returning empty map")
return mapOf()
return ParsedConfigResult(emptyMap())
}

// Step 1: Filter config based on match type, then apply GPU-family overrides
// Step 1: Prepare the config using either device-adapted or value-preserving behavior
val updatedConfigJson = Json.parseToJsonElement(originalJson.toString()).jsonObject
val filteredConfig = filterConfigByMatchType(updatedConfigJson, matchType, storeMatch)
val filteredJson = applyGpuFamilyOverrides(context, JSONObject(filteredConfig.toString()), matchedGpu)
val filteredJson = prepareConfigForApplication(
context = context,
configJson = updatedConfigJson,
matchType = matchType,
storeMatch = storeMatch,
matchedGpu = matchedGpu,
preserveConfigValues = preserveConfigValues,
)

// Step 2: check for unavailable component versions
lastMissingComponents = validateComponentVersions(context, filteredJson)
if (lastMissingComponents.isNotEmpty()) {
val missingComponents = validateComponentVersions(context, filteredJson)
if (missingComponents.isNotEmpty()) {
if (!forceApply) {
Timber.tag("BestConfigService").w("Config rejected: missing components: ${lastMissingComponents.joinToString(", ")}")
return mapOf()
Timber.tag("BestConfigService").w("Config rejected: missing components: ${missingComponents.joinToString(", ")}")
return ParsedConfigResult(emptyMap(), missingComponents)
}
Timber.tag("BestConfigService").w("Force-applying config, replacing missing components with defaults: ${lastMissingComponents.joinToString(", ")}")
replaceWithDefaults(filteredJson, lastMissingComponents)
Timber.tag("BestConfigService").w("Force-applying config, replacing missing components with defaults: ${missingComponents.joinToString(", ")}")
replaceWithDefaults(filteredJson, missingComponents)
}

// Step 3: Build map with only fields present in filteredJson (not defaults)
Expand All @@ -861,7 +919,12 @@ object BestConfigService {
resultMap["execArgs"] = filteredJson.optString("execArgs", "")
}
if (filteredJson.has("startupSelection") && !filteredJson.isNull("startupSelection")) {
resultMap["startupSelection"] = filteredJson.optInt("startupSelection", PrefManager.startupSelection).toByte()
val startupSelection = filteredJson.optInt("startupSelection", PrefManager.startupSelection)
resultMap["startupSelection"] = if (preserveConfigValues) {
startupSelection
} else {
startupSelection.toByte()
}
}
Comment thread
Nightwalker743 marked this conversation as resolved.
if (filteredJson.has("box64Version") && !filteredJson.isNull("box64Version")) {
resultMap["box64Version"] = filteredJson.optString("box64Version", "")
Expand Down Expand Up @@ -922,11 +985,11 @@ object BestConfigService {
resultMap["videoMemorySize"] = filteredJson.optString("videoMemorySize", PrefManager.videoMemorySize)
}

return resultMap
return ParsedConfigResult(resultMap, missingComponents)
}
} catch (e: Exception) {
Timber.tag("BestConfigService").e(e, "Failed to parse config to ContainerData: ${e.message}")
return mapOf()
return ParsedConfigResult(emptyMap())
}
}
}
Expand Down
Loading
Loading