-
Notifications
You must be signed in to change notification settings - Fork 51
feat: Community Settings Sharing #564
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1515c2c
e4bee40
b8d768b
8590efb
176d38a
cf5f5f8
d223fcc
54b1362
11600a4
f43b792
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| package com.winlator.cmod.feature.community | ||
|
|
||
| import android.content.Context | ||
| import com.winlator.cmod.R | ||
| import com.winlator.cmod.feature.settings.DXVKConfigUtils | ||
| import com.winlator.cmod.feature.settings.GraphicsDriverConfigUtils | ||
| import com.winlator.cmod.runtime.content.AdrenotoolsManager | ||
| import com.winlator.cmod.runtime.content.ContentProfile | ||
| import com.winlator.cmod.runtime.content.ContentsManager | ||
| import com.winlator.cmod.runtime.system.GPUInformation | ||
| import com.winlator.cmod.runtime.wine.WineInfo | ||
| import org.json.JSONObject | ||
|
|
||
| /** | ||
| * Compares the components a downloaded config needs (Wine, DXVK, VKD3D, Box64, | ||
| * FEXCore) against what is actually installed via [ContentsManager], so the UI | ||
| * can show a "⚠ MISSING COMPONENT" warning before applying. | ||
| * | ||
| * Matching is deliberately lenient (treats anything it can't positively prove | ||
| * missing as present) to avoid false positives that would block a valid apply. | ||
| */ | ||
| object ComponentChecker { | ||
|
|
||
| data class Missing(val label: String) | ||
|
|
||
| private val SKIP = setOf("", "none", "system", "builtin", "auto", "wined3d") | ||
|
|
||
| fun findMissing(context: Context, contentsManager: ContentsManager, settings: JSONObject): List<Missing> { | ||
| contentsManager.syncContents() | ||
| val missing = mutableListOf<Missing>() | ||
|
|
||
| // Wine / Proton | ||
| val wineVer = settings.optString("wineVersion", "") | ||
| if (wineVer.lowercase() !in SKIP) { | ||
| val installed = installedNames(contentsManager, ContentProfile.ContentType.CONTENT_TYPE_WINE) + | ||
| installedNames(contentsManager, ContentProfile.ContentType.CONTENT_TYPE_PROTON) | ||
| // WineInfo resolves bundled/main versions; only flag if clearly a | ||
| // managed profile name that isn't installed. | ||
| val resolved = runCatching { | ||
| WineInfo.fromIdentifier(context, contentsManager, wineVer) | ||
| }.getOrNull() | ||
| if (resolved == null && !matches(wineVer, installed) && installed.isNotEmpty()) { | ||
| missing += Missing("Wine $wineVer") | ||
| } | ||
| } | ||
|
|
||
| // DXVK / VKD3D from dxwrapperConfig | ||
| val dxwrapper = settings.optString("dxwrapper", "") | ||
| if (dxwrapper.lowercase() !in SKIP) { | ||
| val cfg = DXVKConfigUtils.parseConfig(settings.optString("dxwrapperConfig", "")) | ||
| checkVersion(cfg.get("version"), "DXVK", | ||
| installedNames(contentsManager, ContentProfile.ContentType.CONTENT_TYPE_DXVK), missing) | ||
| checkVersion(cfg.get("vkd3dVersion"), "VKD3D", | ||
| installedNames(contentsManager, ContentProfile.ContentType.CONTENT_TYPE_VKD3D), missing) | ||
| } | ||
|
|
||
| // Box64 / WOWBox64 | ||
| checkVersion(settings.optString("box64Version", ""), "Box64", | ||
| installedNames(contentsManager, ContentProfile.ContentType.CONTENT_TYPE_BOX64) + | ||
| installedNames(contentsManager, ContentProfile.ContentType.CONTENT_TYPE_WOWBOX64), missing) | ||
|
|
||
| // FEXCore | ||
| checkVersion(settings.optString("fexcoreVersion", ""), "FEXCore", | ||
| installedNames(contentsManager, ContentProfile.ContentType.CONTENT_TYPE_FEXCORE), missing) | ||
|
|
||
| // Graphics driver version (built-in wrapper version or installed custom | ||
| // adrenotools driver). The `version=` token in graphicsDriverConfig is | ||
| // the selected driver; if it's not available here, the config can't be | ||
| // applied faithfully -> report it. | ||
| val gdVersion = runCatching { | ||
| (GraphicsDriverConfigUtils.parseGraphicsDriverConfig( | ||
| settings.optString("graphicsDriverConfig", ""))["version"] ?: "").trim() | ||
| }.getOrDefault("") | ||
| if (gdVersion.isNotEmpty() && gdVersion.lowercase() !in SKIP && | ||
| !graphicsDriverAvailable(context, gdVersion)) { | ||
| missing += Missing("Graphics driver \"$gdVersion\"") | ||
| } | ||
|
|
||
| return missing | ||
| } | ||
|
|
||
| private fun graphicsDriverAvailable(context: Context, version: String): Boolean { | ||
| // Built-in / system wrapper versions (filtered by GPU support). | ||
| runCatching { | ||
| val sys = context.resources.getStringArray(R.array.wrapper_graphics_driver_version_entries) | ||
| if (sys.any { it.equals(version, ignoreCase = true) }) { | ||
| return GPUInformation.isDriverSupported(version, context) | ||
| } | ||
| } | ||
| // Installed custom adrenotools drivers (the folder id == the version token). | ||
| return runCatching { | ||
| AdrenotoolsManager(context).enumarateInstalledDrivers() | ||
| ?.any { it.equals(version, ignoreCase = true) } ?: false | ||
| }.getOrDefault(false) | ||
| } | ||
|
|
||
| private fun checkVersion(version: String?, label: String, installed: List<String>, out: MutableList<Missing>) { | ||
| val v = version?.trim() ?: "" | ||
| if (v.lowercase() in SKIP) return | ||
| if (installed.isEmpty()) { | ||
| // Nothing of this type installed at all -> it's needed but absent. | ||
| out += Missing("$label $v") | ||
| } else if (!matches(v, installed)) { | ||
| out += Missing("$label $v") | ||
| } | ||
| } | ||
|
|
||
| private fun installedNames(cm: ContentsManager, type: ContentProfile.ContentType): List<String> { | ||
| val list = cm.getProfiles(type) ?: return emptyList() | ||
| return list.filter { it.isInstalled }.flatMap { | ||
| listOf(it.verName ?: "", ContentsManager.getEntryName(it)) | ||
| }.filter { it.isNotBlank() } | ||
| } | ||
|
|
||
| private fun matches(version: String, installed: List<String>): Boolean { | ||
| val v = version.lowercase() | ||
| return installed.any { name -> | ||
| val n = name.lowercase() | ||
| n == v || n.endsWith(v) || n.contains(v) || v.contains(n) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package com.winlator.cmod.feature.community | ||
|
|
||
| import com.winlator.cmod.runtime.container.Shortcut | ||
| import org.json.JSONObject | ||
|
|
||
| /** | ||
| * Applies a downloaded settings document to the CURRENT shortcut only, as | ||
| * shortcut-level overrides. It never mutates the Container, other shortcuts, the | ||
| * Exec line, icon, artwork, or container_id -- so containers can't be broken. | ||
| * EVERY known setting is applied verbatim (including the graphics-driver version | ||
| * and dxvk/vkd3d/gpuName tokens); availability is gated beforehand by | ||
| * ComponentChecker, which blocks apply + shows MISSING COMPONENT if anything the | ||
| * config needs isn't installed. | ||
| */ | ||
| object ConfigApplier { | ||
|
|
||
| /** The only keys an applied config may write. Mirrors the serializer. */ | ||
| private val ALLOWED = setOf( | ||
| "screenSize", "audioDriver", "midiSoundFont", "graphicsDriver", | ||
| "graphicsDriverConfig", "dxwrapper", "dxwrapperConfig", "swapRB", | ||
| "refreshRate", "fpsLimit", "sgsrEnabled", "sgsrUpscaleMode", "sgsrSharpness", | ||
| "wineVersion", "emulator", "emulator64", "lc_all", "desktopTheme", | ||
| "wincomponents", "envVars", "box64Version", "box64Preset", | ||
| "fexcoreVersion", "fexcorePreset", "startupSelection", "execArgs", | ||
| "fullscreenStretched", "cpuList", "cpuListWoW64", "inputType", | ||
| "exclusiveXInput", "numControllers", "disableXinput", "simTouchScreen", | ||
| "useColdClient", "unpackFiles", "useSteamInput", "steamOfflineMode", | ||
| "runtimePatcher", | ||
| ) | ||
|
|
||
| fun apply(shortcut: Shortcut, settings: JSONObject) { | ||
| val keys = settings.keys() | ||
| while (keys.hasNext()) { | ||
| val key = keys.next() | ||
| if (key !in ALLOWED) continue | ||
| shortcut.putExtra(key, settings.optString(key, "")) | ||
| } | ||
| // Force shortcut-level resolution so the applied overrides take effect. | ||
| shortcut.putExtra("use_container_defaults", "0") | ||
| shortcut.saveData() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package com.winlator.cmod.feature.community | ||
|
|
||
| import com.winlator.cmod.runtime.container.Container | ||
| import com.winlator.cmod.runtime.container.Shortcut | ||
| import org.json.JSONObject | ||
|
|
||
| /** | ||
| * Serializes the *effective* value of every portable shortcut setting (resolving | ||
| * shortcut override -> container default via [Shortcut.getSettingExtra]) into a | ||
| * settings map for sharing. Device-local driver tokens (version=/gpuName=) are | ||
| * blanked so a downloader keeps their own GPU driver. PII / structural / local | ||
| * fields (paths, uuid, container_id, controlsProfile, artwork) are excluded. | ||
| */ | ||
| object ConfigSerializer { | ||
|
|
||
| fun serialize(shortcut: Shortcut): JSONObject { | ||
| val c = shortcut.container | ||
| val s = JSONObject() | ||
|
|
||
| fun put(key: String, value: String?) { | ||
| if (!value.isNullOrBlank()) s.put(key, value) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This drops settings whose disabled state is represented by an absent or blank extra, such as Useful? React with 👍 / 👎. |
||
| } | ||
| fun eff(key: String, containerValue: String): String = | ||
| shortcut.getSettingExtra(key, containerValue) | ||
| fun extra(key: String, def: String): String = shortcut.getExtra(key, def) | ||
|
|
||
| // General / Display (explicit Java getter calls to avoid Kotlin | ||
| // property-name ambiguity for ALL-CAPS getters like getDXWrapper). | ||
| put("screenSize", eff("screenSize", c.getScreenSize())) | ||
| put("audioDriver", eff("audioDriver", c.getAudioDriver())) | ||
| put("midiSoundFont", eff("midiSoundFont", c.getMIDISoundFont())) | ||
| put("graphicsDriver", eff("graphicsDriver", c.getGraphicsDriver())) | ||
| put("graphicsDriverConfig", eff("graphicsDriverConfig", c.getGraphicsDriverConfig())) | ||
| put("dxwrapper", eff("dxwrapper", c.getDXWrapper())) | ||
| put("dxwrapperConfig", eff("dxwrapperConfig", c.getDXWrapperConfig())) | ||
| put("swapRB", extra("swapRB", "")) | ||
| put("refreshRate", extra("refreshRate", "")) | ||
| put("fpsLimit", extra("fpsLimit", "")) | ||
| put("sgsrEnabled", extra("sgsrEnabled", "")) | ||
| put("sgsrUpscaleMode", extra("sgsrUpscaleMode", "")) | ||
| put("sgsrSharpness", extra("sgsrSharpness", "")) | ||
|
|
||
| // Wine | ||
| put("wineVersion", eff("wineVersion", c.getWineVersion())) | ||
| put("emulator", eff("emulator", c.getEmulator())) | ||
| put("emulator64", eff("emulator64", c.getEmulator64())) | ||
| put("lc_all", eff("lc_all", c.getLC_ALL())) | ||
| put("desktopTheme", eff("desktopTheme", c.getDesktopTheme())) | ||
|
|
||
| // Components / Variables | ||
| put("wincomponents", eff("wincomponents", c.getWinComponents())) | ||
| put("envVars", eff("envVars", c.getEnvVars())) | ||
|
|
||
| // Advanced | ||
| put("box64Version", eff("box64Version", c.getBox64Version())) | ||
| put("box64Preset", eff("box64Preset", c.getBox64Preset())) | ||
| put("fexcoreVersion", eff("fexcoreVersion", c.getFEXCoreVersion())) | ||
| put("fexcorePreset", eff("fexcorePreset", c.getFEXCorePreset())) | ||
| put("startupSelection", eff("startupSelection", c.getStartupSelection().toString())) | ||
| put("execArgs", eff("execArgs", c.getExecArgs())) | ||
| put("fullscreenStretched", eff("fullscreenStretched", if (c.isFullscreenStretched()) "1" else "0")) | ||
| put("cpuList", eff("cpuList", c.getCPUList(true))) | ||
| put("cpuListWoW64", eff("cpuListWoW64", c.getCPUListWoW64(true))) | ||
|
|
||
| // Input | ||
| put("inputType", eff("inputType", c.getInputType().toString())) | ||
| put("exclusiveXInput", eff("exclusiveXInput", if (c.isExclusiveXInput()) "1" else "0")) | ||
| put("numControllers", extra("numControllers", "")) | ||
| put("disableXinput", extra("disableXinput", "")) | ||
| put("simTouchScreen", extra("simTouchScreen", "")) | ||
|
|
||
| // Steam (only for Steam games) | ||
| if (isSteam(shortcut)) { | ||
| put("useColdClient", eff("useColdClient", if (c.isUseColdClient()) "1" else "0")) | ||
| put("unpackFiles", eff("unpackFiles", if (c.isUnpackFiles()) "1" else "0")) | ||
| put("useSteamInput", extra("useSteamInput", "")) | ||
| put("steamOfflineMode", eff("steamOfflineMode", if (c.isSteamOfflineMode()) "1" else "0")) | ||
| put("runtimePatcher", eff("runtimePatcher", if (c.isRuntimePatcher()) "1" else "0")) | ||
| } | ||
| return s | ||
| } | ||
|
|
||
| fun gameKey(shortcut: Shortcut): String { | ||
| return when (storeOf(shortcut)) { | ||
| "STEAM" -> "steam:" + shortcut.getExtra("app_id", "").ifBlank { slug(shortcut.name) } | ||
| "GOG" -> "gog:" + shortcut.getExtra("gog_id", "").ifBlank { slug(shortcut.name) } | ||
| "EPIC" -> "epic:" + slug(shortcut.name) | ||
| else -> "name:" + slug(shortcut.name) | ||
| }.take(128) | ||
| } | ||
|
|
||
| fun storeOf(shortcut: Shortcut): String { | ||
| val raw = shortcut.getExtra("game_source", "").uppercase() | ||
| val allowed = setOf("STEAM", "EPIC", "GOG", "AMAZON", "UBISOFT", "EA", "BATTLENET") | ||
| return if (raw in allowed) raw else "CUSTOM" | ||
| } | ||
|
|
||
| private fun isSteam(shortcut: Shortcut): Boolean = | ||
| shortcut.getExtra("game_source", "").equals("steam", ignoreCase = true) | ||
|
|
||
| private fun slug(name: String): String { | ||
| val s = name.lowercase().map { if (it.isLetterOrDigit() || it == '-' || it == '.') it else '-' } | ||
| .joinToString("").trim('-') | ||
| return s.ifBlank { "game" }.take(100) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a config requests a Wine/Proton identifier that is not installed, this check treats it as present because
WineInfo.fromIdentifier()is not an existence check: it falls back to the bundledMAIN_WINE_VERSIONinstead of returning null for unrecognized or missing identifiers. As a result, the missing-component dialog is skipped and the config is applied with the wrong Wine runtime for games that require a custom Wine/Proton build; please check the installed profile names directly before considering it resolved.Useful? React with 👍 / 👎.