@@ -3,7 +3,10 @@ package app.gamenative.gamefixes
33import android.content.Context
44import app.gamenative.data.GameSource
55import com.winlator.container.Container
6+ import com.winlator.contents.ContentsManager
67import com.winlator.core.WineRegistryEditor
8+ import com.winlator.core.WineInfo
9+ import com.winlator.core.envvars.EnvVars
710import java.io.File
811import timber.log.Timber
912
@@ -28,6 +31,113 @@ private const val EA_HEADLESS_INSTALLER_RELATIVE_PATH =
2831private const val EA_HEADLESS_MSI_RELATIVE_PATH =
2932 " __Installer/Origin/redist/internal/EAapp-wine-no-start.msi"
3033
34+ private fun ascii (value : String ) = value.toByteArray(Charsets .US_ASCII )
35+
36+ private fun utf16Le (value : String ) = value.toByteArray(Charsets .UTF_16LE )
37+
38+ private fun replaceEqualLengthBytes (file : File , from : ByteArray , to : ByteArray ): Int {
39+ require(from.size == to.size)
40+ if (! file.isFile) return 0
41+
42+ val contents = file.readBytes()
43+ var replacements = 0
44+ var offset = 0
45+ while (offset <= contents.size - from.size) {
46+ var matches = true
47+ for (index in from.indices) {
48+ if (contents[offset + index] != from[index]) {
49+ matches = false
50+ break
51+ }
52+ }
53+ if (! matches) {
54+ offset++
55+ continue
56+ }
57+ to.copyInto(contents, offset)
58+ replacements++
59+ offset + = from.size
60+ }
61+
62+ if (replacements > 0 ) {
63+ val backup = File (file.parentFile, " ${file.name} .ea-gamefix-backup" )
64+ if (! backup.exists()) file.copyTo(backup)
65+ file.writeBytes(contents)
66+ }
67+ return replacements
68+ }
69+
70+ private fun applyEaProtonCompatibilityPatches (context : Context , container : Container ) {
71+ // Content identifiers include the profile revision (for example
72+ // proton-11.0-1-arm64ec-1), while the extracted directory does not
73+ // necessarily include it. Resolve the profile exactly as the launcher does
74+ // instead of assuming that the identifier is also a directory name.
75+ val contentsManager = ContentsManager (context)
76+ val wineInfo = WineInfo .fromIdentifier(context, contentsManager, container.wineVersion)
77+ val protonRoot = wineInfo.path
78+ ?.takeIf { it.isNotBlank() }
79+ ?.let (::File )
80+ if (protonRoot == null || ! protonRoot.isDirectory) {
81+ Timber .tag(" GameFixes" ).w(
82+ " Cannot apply Sims 4 EA compatibility patches: no installed path for %s" ,
83+ container.wineVersion,
84+ )
85+ return
86+ }
87+ Timber .tag(" GameFixes" ).i(
88+ " Applying Sims 4 EA compatibility patches to %s" ,
89+ protonRoot.absolutePath,
90+ )
91+
92+ // Proton's EA hook forces the Qt host through Mesa software OpenGL. Zink
93+ // has no CPU device here, so Qt cannot create a pixel format at all.
94+ for (path in listOf (
95+ " lib/wine/x86_64-unix/ntdll.so" ,
96+ " lib/wine/aarch64-unix/ntdll.so" ,
97+ )) {
98+ replaceEqualLengthBytes(
99+ File (protonRoot, path),
100+ ascii(" LIBGL_ALWAYS_SOFTWARE" ),
101+ ascii(" XIBGL_ALWAYS_SOFTWARE" ),
102+ )
103+ }
104+
105+ // Proton also force-appends ANGLE Vulkan to EA's CEF subprocess. Keep the
106+ // Qt host on the normal graphics path and let the per-exe compatibility
107+ // configuration force the CEF helper onto its software path instead.
108+ for (path in listOf (
109+ " lib/wine/x86_64-windows/kernelbase.dll" ,
110+ " lib/wine/aarch64-windows/kernelbase.dll" ,
111+ " lib/wine/i386-windows/kernelbase.dll" ,
112+ )) {
113+ val file = File (protonRoot, path)
114+ replaceEqualLengthBytes(
115+ file,
116+ ascii(" EACefSubProcess.exe" ),
117+ ascii(" XACefSubProcess.exe" ),
118+ )
119+ replaceEqualLengthBytes(
120+ file,
121+ utf16Le(" EACefSubProcess.exe" ),
122+ utf16Le(" XACefSubProcess.exe" ),
123+ )
124+
125+ // This Proton hook forces desktop OpenGL for EA's Qt shell. Box64's
126+ // Zink path exits during initialization, while EA's bundled ANGLE
127+ // libraries render correctly.
128+ replaceEqualLengthBytes(
129+ file,
130+ ascii(" QT_OPENGL" ),
131+ ascii(" XT_OPENGL" ),
132+ )
133+ replaceEqualLengthBytes(
134+ file,
135+ utf16Le(" QT_OPENGL" ),
136+ utf16Le(" XT_OPENGL" ),
137+ )
138+ }
139+ }
140+
31141private fun configureEaInstallScript (installPath : String ) {
32142 val installScript = File (installPath, " installscript.vdf" )
33143 if (! installScript.isFile) return
@@ -192,42 +302,6 @@ internal fun applyEaCompatibilityRegistry(container: Container, gameExeWindowsPa
192302 }
193303}
194304
195-
196- /* *
197- * A staged self-update makes EA Desktop demand a client restart mid-session,
198- * which tears down any running game with it. Drop staged payloads, clear the
199- * pending flag, and keep the version directory unwritable so an update can't
200- * re-stage. (Remove the write protection deliberately when an EA update is
201- * actually wanted.)
202- */
203- private fun suppressEaSelfUpdate (container : Container ) {
204- val (version, _) = findInstalledEaDesktop(container) ? : return
205- val versionDir = File (container.rootDir, " $EA_DESKTOP_INSTALL_ROOT /$version " )
206-
207- versionDir.setWritable(true , false )
208- versionDir.listFiles()?.forEach { entry ->
209- val staged = entry.name != " EA Desktop" && entry.name != " VC" &&
210- (entry.isDirectory || entry.name.endsWith(" .zip" ) || entry.name.endsWith(" .zip.sig" ))
211- if (staged) {
212- Timber .tag(" GameFixes" ).i(" Removing staged EA update: %s" , entry.name)
213- entry.deleteRecursively()
214- }
215- }
216- versionDir.setWritable(false , false )
217-
218- val machineIni = File (container.rootDir, " .wine/drive_c/ProgramData/EA Desktop/machine.ini" )
219- if (machineIni.isFile) {
220- val lines = machineIni.readLines().map { line ->
221- when {
222- line.startsWith(" machine.updatepending=" ) -> " machine.updatepending=0"
223- line.startsWith(" machine.updateinfo=" ) -> " machine.updateinfo="
224- else -> line
225- }
226- }
227- machineIni.writeText(lines.joinToString(" \n " ))
228- }
229- }
230-
231305const val EA_LINK2EA_LAUNCH_SCRIPT_WINDOWS_PATH =
232306 " C:\\\\ ProgramData\\\\ GameNative\\\\ ea-link2ea-launch.cmd"
233307
@@ -342,7 +416,14 @@ val EaAppGameFix: GameFix = object : GameFix {
342416 ensureHeadlessInstallerFiles(installPath)
343417 configureEaInstallScript(installPath)
344418 writeLink2EaLaunchScript(container)
345- suppressEaSelfUpdate(container)
419+ container.putExtra(" dnsV4MappedShim" , " 1" )
420+ applyEaProtonCompatibilityPatches(context, container)
421+ val envVars = EnvVars (container.envVars)
422+ if (envVars.get(" QT_OPENGL" ) != " angle" ) {
423+ envVars.put(" QT_OPENGL" , " angle" )
424+ container.envVars = envVars.toString()
425+ container.saveData()
426+ }
346427 val userRegFile = File (container.rootDir, " .wine/user.reg" )
347428 if (! userRegFile.isFile) {
348429 userRegFile.parentFile?.mkdirs()
0 commit comments