Compare commits

...
11 changed files with 346 additions and 57 deletions

No files matched your search

+9
View File
@@ -151,6 +151,15 @@
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".ui.SecondScreenActivity"
android:exported="false"
android:excludeFromRecents="true"
android:theme="@style/SessionTheme"
android:screenOrientation="sensorLandscape"
android:resizeableActivity="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density" />
<activity
android:name=".gpu.SteamVrRestartActivity"
android:exported="false"
@@ -107,7 +107,7 @@ import com.droiddeck.launcher.ui.HudText
import com.droiddeck.launcher.ui.LoadingOverlay
import com.droiddeck.launcher.ui.SessionDrawer
import com.droiddeck.launcher.ui.SessionPausedOverlay
import com.droiddeck.launcher.ui.SecondScreenPresentation
import com.droiddeck.launcher.ui.SecondScreenWindow
import com.droiddeck.launcher.ui.DroidDeckTheme
import com.droiddeck.launcher.wayland.CompositorHost
import com.droiddeck.launcher.wayland.WaylandCompositor
@@ -343,7 +343,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
private var selectedSecondScreenDisplay by mutableStateOf(SessionState.secondScreenDisplay)
private var started = false
private lateinit var displayManager: DisplayManager
private var secondScreenPresentation: SecondScreenPresentation? = null
private var secondScreenPresentation: SecondScreenWindow? = null
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = refreshSecondScreenDisplays()
override fun onDisplayRemoved(displayId: Int) = refreshSecondScreenDisplays()
@@ -2118,7 +2118,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
var presentation = secondScreenPresentation
if (presentation?.display?.displayId != target.displayId) {
presentation?.closeControls()
presentation = SecondScreenPresentation(
presentation = SecondScreenWindow(
this, target,
sendPointer = ::moveSecondScreenPointer,
sendButton = { button, pressed ->
@@ -32,6 +32,7 @@ object GameEnvironmentOptions {
Option("MESA_EXTENSION_MAX_YEAR", "", 0, Type.TEXT),
Option("MESA_GL_VERSION_OVERRIDE", "", 0, Type.TEXT),
Option("PULSE_LATENCY_MSEC", "", 0, Type.NUMBER),
Option("DROIDDECK_ANTICHEAT_OFFLINE", "1", R.string.game_env_anticheat_offline, Type.TOGGLE, listOf("0", "1")),
Option("WINE_LARGE_ADDRESS_AWARE", "0", 0, Type.TOGGLE, listOf("0", "1")),
Option("WINEDLLOVERRIDES", "", 0, Type.TEXT),
Option("GALLIUM_HUD", "simple", 0, Type.MULTIPLE, listOf("simple", "fps", "frametime")),
@@ -1425,6 +1425,7 @@ class SessionService : Service() {
}
override fun onTaskRemoved(rootIntent: Intent?) {
if (rootIntent?.component?.className == com.droiddeck.launcher.ui.SecondScreenActivity::class.java.name) return
if (pipTask || xrActivityVisible) {
Log.i(TAG, "panel task dismissed - retaining the session under its active surface policy")
activityVisible = false
@@ -1,21 +1,17 @@
package com.droiddeck.launcher.ui
import com.droiddeck.launcher.R
import android.app.Presentation
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.Display
import android.view.Gravity
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewConfiguration
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.FrameLayout
@@ -37,9 +33,9 @@ import com.termux.terminal.TerminalSessionClient
import com.termux.view.TerminalView
import com.termux.view.TerminalViewClient
class SecondScreenPresentation(
internal class SecondScreenControls(
context: Context,
private val sessionActivity: SessionActivity,
display: Display,
private val sendPointer: (x: Float, y: Float, width: Float, height: Float) -> Unit,
private val sendButton: (button: Int, pressed: Boolean) -> Unit,
private val sendWheel: (steps: Int) -> Unit,
@@ -47,12 +43,8 @@ class SecondScreenPresentation(
private val onQam: () -> Unit,
private val onClose: () -> Unit,
private val pad: PadBridge? = null,
) : Presentation(sessionActivity, display) {
private val root = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setBackgroundColor(Color.rgb(15, 18, 22))
isFocusableInTouchMode = true
}
) : LinearLayout(context) {
private val root get() = this
private var currentMode = SecondScreenMode.NONE
private var keyboardHost: KeyboardHost? = null
private var terminalSession: TerminalSession? = null
@@ -60,35 +52,22 @@ class SecondScreenPresentation(
private var terminalPid = -1
private var ctrlActive = false
private var altActive = false
private var created = false
init {
setCancelable(false)
setCanceledOnTouchOutside(false)
setOnDismissListener { closeTerminal() }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
created = true
window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
setContentView(root)
applyImmersiveMode()
renderMode()
orientation = VERTICAL
setBackgroundColor(Color.rgb(15, 18, 22))
isFocusableInTouchMode = true
}
fun showMode(mode: SecondScreenMode) {
if (currentMode == mode && isShowing) return
if (currentMode == mode) return
currentMode = mode
if (created) renderMode()
renderMode()
}
fun closeControls() {
closeTerminal()
keyboardHost?.hide()
keyboardHost = null
dismiss()
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@@ -98,14 +77,6 @@ class SecondScreenPresentation(
return super.dispatchKeyEvent(event)
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
applyImmersiveMode()
sessionActivity.refreshClipboard()
}
}
private fun renderMode() {
keyboardHost?.hide()
keyboardHost = null
@@ -291,16 +262,6 @@ class SecondScreenPresentation(
private fun weightParams() = LinearLayout.LayoutParams(-1, 0, 1f)
private fun applyImmersiveMode() {
@Suppress("DEPRECATION")
window?.decorView?.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
}
private inner class TrackpadView(context: Context) : View(context) {
private val gestures = TouchpadGestures(ViewConfiguration.get(context).scaledTouchSlop.toFloat(),
object : PointerGestures.Listener {
@@ -0,0 +1,139 @@
package com.droiddeck.launcher.ui
import android.app.Activity
import android.app.ActivityOptions
import android.content.Intent
import android.os.Bundle
import android.view.Display
import android.view.View
import android.view.WindowManager
import com.droiddeck.launcher.SessionActivity
import com.droiddeck.launcher.input.PadBridge
import com.droiddeck.launcher.input.SecondScreenMode
import com.droiddeck.launcher.session.SessionState
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicLong
class SecondScreenWindow(
private val sessionActivity: SessionActivity,
val display: Display,
private val sendPointer: (Float, Float, Float, Float) -> Unit,
private val sendButton: (Int, Boolean) -> Unit,
private val sendWheel: (Int) -> Unit,
private val onSteamMenu: () -> Unit,
private val onQam: () -> Unit,
private val onClose: () -> Unit,
private val pad: PadBridge? = null,
) {
private val token = tokens.incrementAndGet()
private var activity = WeakReference<SecondScreenActivity>(null)
private var controls: SecondScreenControls? = null
private var mode = SecondScreenMode.NONE
private var launching = false
private var closed = false
val window get() = activity.get()?.window
val isShowing get() = !closed && (launching || activity.get()?.isFinishing == false)
fun show() {
if (isShowing || closed) return
owners[token] = WeakReference(this)
launching = true
try {
val intent = Intent(sessionActivity, SecondScreenActivity::class.java)
.putExtra(OWNER, token)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
sessionActivity.startActivity(intent, ActivityOptions.makeBasic().setLaunchDisplayId(display.displayId).toBundle())
} catch (error: Exception) {
launching = false
owners.remove(token)
throw error
}
}
fun showMode(mode: SecondScreenMode) {
this.mode = mode
controls?.showMode(mode)
}
fun closeControls() {
closed = true
launching = false
owners.remove(token)
controls?.closeControls()
controls = null
activity.get()?.finishAndRemoveTask()
activity.clear()
}
internal fun attach(host: SecondScreenActivity): View {
controls?.closeControls()
activity = WeakReference(host)
launching = false
return SecondScreenControls(host, sessionActivity, sendPointer, sendButton, sendWheel,
onSteamMenu, onQam, onClose, pad).also {
controls = it
it.showMode(mode)
}
}
internal fun detach(host: SecondScreenActivity) {
if (activity.get() !== host) return
controls?.closeControls()
controls = null
activity.clear()
if (host.isFinishing && !closed) onClose()
}
internal fun refreshClipboard() = sessionActivity.refreshClipboard()
companion object {
private const val OWNER = "secondScreenOwner"
private val tokens = AtomicLong()
private val owners = mutableMapOf<Long, WeakReference<SecondScreenWindow>>()
internal fun find(intent: Intent): SecondScreenWindow? =
owners[intent.getLongExtra(OWNER, 0)]?.get()?.takeUnless { it.closed }
}
}
class SecondScreenActivity : Activity() {
private var owner: SecondScreenWindow? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val host = SecondScreenWindow.find(intent)
if (host == null || !SessionState.running) {
finishAndRemoveTask()
return
}
owner = host
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
setContentView(host.attach(this))
applyImmersiveMode()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
applyImmersiveMode()
owner?.refreshClipboard()
}
}
@Deprecated("Back is handled by the session controls")
override fun onBackPressed() = Unit
override fun onDestroy() {
owner?.detach(this)
owner = null
super.onDestroy()
}
private fun applyImmersiveMode() {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
}
}
+1
View File
@@ -86,6 +86,7 @@
<string name="game_env_no_fsync">Set to 1 to keep this game off droiddeck-fsync; it uses droiddeck-esync where a pack exists, otherwise droiddeck-ntsync.</string>
<string name="game_env_fsync_first">Set to 1 to run this game on droiddeck-fsync, or 0 to put droiddeck-esync first where a pack fits its Proton.</string>
<string name="game_env_xalia">Set to 0 to disable Proton’s gamepad-navigation helper for this game.</string>
<string name="game_env_anticheat_offline">Set to 1 to start an Easy Anti-Cheat game without its anti-cheat. The game must support offline play; online modes that require Easy Anti-Cheat remain unavailable.</string>
<string name="game_env_hud">DXVK overlay for D3D9–11. fps displays frame rate; unset hides it.</string>
<string name="game_env_dxvk_config">DXVK settings separated by semicolons. Recent builds use dxvk.maxFrameRate for frame limiting; prefer the session frame limiter for ordinary play.</string>
<string name="game_env_vkd3d_limit">D3D12 frame-rate limit. 0 disables the limit.</string>
@@ -0,0 +1,61 @@
package com.droiddeck.launcher.ui
import android.app.Application
import android.content.Intent
import android.hardware.display.DisplayManager
import android.view.Display
import com.droiddeck.launcher.SessionActivity
import com.droiddeck.launcher.session.SessionService
import com.droiddeck.launcher.session.SessionState
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
@RunWith(RobolectricTestRunner::class)
@Config(application = Application::class, manifest = Config.NONE, sdk = [28, 33])
@LooperMode(LooperMode.Mode.PAUSED)
class SecondScreenActivityTest {
@Before fun setUp() {
SessionState.running = true
SessionState.stopRequested = false
}
@After fun tearDown() {
SessionState.running = false
SessionState.stopRequested = false
}
@Test fun removingControlsTaskDoesNotStopTheRunningSession() {
val service = Robolectric.buildService(SessionService::class.java).get()
service.onTaskRemoved(Intent(service, SecondScreenActivity::class.java))
assertTrue(SessionState.running)
assertFalse(SessionState.stopRequested)
}
@Test fun activityRestoredWithoutItsSessionOwnerFinishes() {
val activity = Robolectric.buildActivity(SecondScreenActivity::class.java).create().get()
assertTrue(activity.isFinishing)
assertTrue(SessionState.running)
}
@Test fun closingBeforeTheSecondaryActivityStartsRejectsTheLateLaunch() {
val session = Robolectric.buildActivity(SessionActivity::class.java).get()
val display = session.getSystemService(DisplayManager::class.java).getDisplay(Display.DEFAULT_DISPLAY)
val controls = SecondScreenWindow(session, display, { _, _, _, _ -> }, { _, _ -> }, {}, {}, {}, {})
controls.show()
val intent = shadowOf(session).nextStartedActivity
controls.closeControls()
val activity = Robolectric.buildActivity(SecondScreenActivity::class.java, intent).create().get()
assertTrue(activity.isFinishing)
assertFalse(controls.isShowing)
assertTrue(SessionState.running)
}
}
@@ -119,6 +119,28 @@ def skip_red_launcher(command):
return command
def offline_without_anticheat(command, env):
"""Start the configured game executable when the player opts into launching without EAC."""
if env.get("DROIDDECK_ANTICHEAT_OFFLINE") != "1" or len(command) <= 2:
return command
folder = os.path.dirname(command[2])
try:
with open(os.path.join(folder, "EasyAntiCheat", "settings.json"), encoding="utf-8-sig") as source:
executable = json.load(source).get("executable")
except (OSError, ValueError, AttributeError):
return command
if not isinstance(executable, str) or not executable:
return command
relative = os.path.normpath(executable.replace("\\", "/"))
game = os.path.join(folder, relative)
if os.path.isabs(relative) or relative.startswith("..") or not os.path.isfile(game):
return command
if os.path.normcase(os.path.abspath(game)) == os.path.normcase(os.path.abspath(command[2])):
return command
print("droiddeck-game-env: started without Easy Anti-Cheat (offline only)", file=sys.stderr)
return command[:2] + [game] + command[3:]
def engine_fixes(exe, args):
"""Environment and extra arguments a game needs because of what it is built with."""
game = Game(exe, args)
@@ -307,6 +329,8 @@ def main():
env = apply_config(env, {"version": 1}, app_id, fixes)
except (OSError, ValueError, TypeError, AttributeError):
print("droiddeck-game-env: invalid configuration; using inherited environment", file=sys.stderr)
if app_id and len(command) > 1 and command[1] == "waitforexitandrun":
command = offline_without_anticheat(command, env)
if mic_enabled == "0":
env["BANNER_AUDIO_DIRECT_MIC"] = "0"
env = force_ssbs(command, env)
@@ -211,10 +211,25 @@ bl_idtech3() {
"""
BL_DIRECTAUDIO_SETUP = """
bl_is_steam_installscript_target() {
local target=${1:-}
local steam_root=${STEAM_COMPAT_CLIENT_INSTALL_PATH:-${XDG_DATA_HOME:-$HOME/.local/share}/Steam}
local helper="$steam_root/legacycompat/iscriptevaluator.exe"
[ -f "$helper" ] && [[ -n "$target" && "$target" -ef "$helper" ]]
}
bl_directaudio() {
case "${verb:-}" in
run|waitforexitandrun|runinprefix) ;;
*) return 0 ;;
esac
base=$1
[ -n "${BL_DIRECTAUDIO:-}" ] || return 0
[ -d "$BL_DIRECTAUDIO" ] || return 0
if bl_is_steam_installscript_target "${2:-}"; then
echo "droiddeck-proton: DirectAudio skipped for Steam's install-script evaluator" >&2
return 0
fi
version=$("$base/files/bin-arm64/wine" --version 2>/dev/null)
major=${version#wine-}
major=${major%%.*}
@@ -302,7 +317,7 @@ for name in %s; do
done
if [ -n "$depot" ]; then
bl_components "$depot"
bl_directaudio "$depot"
bl_directaudio "$depot" "$@"
bl_game_cpus
bl_idtech3 "$@"
bl_libpath
@@ -356,7 +371,7 @@ if [ -n "${PROOT_FP_KEY:-}" ] && [ -f "$audit" ]; then
esac
fi
bl_components "$here"
bl_directaudio "$here"
bl_directaudio "$here" "$@"
bl_game_cpus
bl_idtech3 "$@"
bl_libpath
+81 -4
View File
@@ -179,6 +179,27 @@ class GameEnvironmentTest(unittest.TestCase):
(other / "Other.exe").touch()
self.assertEqual(MODULE["engine_fixes"](str(other / "Other.exe"), []), ({}, [], []))
def test_easy_anti_cheat_games_start_offline_only_when_opted_in(self):
with tempfile.TemporaryDirectory() as tmp:
game = Path(tmp) / "ELDEN RING/Game"
(game / "EasyAntiCheat").mkdir(parents=True)
for name in ("start_protected_game.exe", "eldenring.exe"):
(game / name).touch()
(game / "EasyAntiCheat/settings.json").write_text('\ufeff{ "executable" : "eldenring.exe" }', encoding="utf-8")
command = ["proton", "waitforexitandrun", str(game / "start_protected_game.exe"), "-arg"]
offline = MODULE["offline_without_anticheat"]
self.assertEqual(offline(command, {}), command)
self.assertEqual(offline(command, {"DROIDDECK_ANTICHEAT_OFFLINE": "0"}), command)
self.assertEqual(offline(command, {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}),
["proton", "waitforexitandrun", str(game / "eldenring.exe"), "-arg"])
(game / "EasyAntiCheat/settings.json").write_text('{ "executable" : "../../escape.exe" }')
(Path(tmp) / "escape.exe").touch()
self.assertEqual(offline(command, {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}), command)
(game / "EasyAntiCheat/settings.json").write_text('{ "executable" : "missing.exe" }')
self.assertEqual(offline(command, {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}), command)
plain = ["proton", "waitforexitandrun", str(Path(tmp) / "Other.exe")]
self.assertEqual(offline(plain, {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}), plain)
def test_arkham_city_gets_full_x87_precision_over_the_preset(self):
with tempfile.TemporaryDirectory() as tmp:
city = Path(tmp) / "Batman Arkham City GOTY/Binaries/Win32"
@@ -434,6 +455,8 @@ class GameEnvironmentTest(unittest.TestCase):
self.assertEqual(changes, {"42": COMPAT["TOOL"]})
def test_both_proton_wrappers_call_environment_launcher(self):
self.assertIn('bl_directaudio "$depot" "$@"', COMPAT["LAUNCHER_SH"])
self.assertIn('bl_directaudio "$here" "$@"', COMPAT["EXTRA_WRAPPER_SH"] % "proton")
for script in (COMPAT["LAUNCHER_SH"], COMPAT["EXTRA_WRAPPER_SH"] % "proton"):
subprocess.run(["bash", "-n"], input=script, text=True, check=True)
self.assertIn('bl_run ${BL_TASKSET:-} /usr/local/bin/droiddeck-game-env', script)
@@ -849,12 +872,15 @@ class DirectAudioPrefixTest(unittest.TestCase):
(self.windows / folder).mkdir(parents=True)
self.reg = self.compat / "pfx/user.reg"
def run_setup(self):
script = COMPAT["BL_DIRECTAUDIO_SETUP"] + '\nbl_directaudio "$1"\necho "WINEDLLPATH=$WINEDLLPATH"\necho "WINE_AUDIO_DRIVER=${WINE_AUDIO_DRIVER:-}"\n'
env = dict(os.environ, BL_DIRECTAUDIO=str(self.sets), STEAM_COMPAT_DATA_PATH=str(self.compat))
def run_setup(self, target="game.exe", cwd=None, verb="waitforexitandrun"):
script = COMPAT["BL_DIRECTAUDIO_SETUP"] + '\nverb=$3\nbl_directaudio "$1" "$2"\necho "WINEDLLPATH=$WINEDLLPATH"\necho "WINE_AUDIO_DRIVER=${WINE_AUDIO_DRIVER:-}"\n'
steam = self.compat.parent / "Steam"
env = dict(os.environ, BL_DIRECTAUDIO=str(self.sets), STEAM_COMPAT_DATA_PATH=str(self.compat),
STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam))
env.pop("WINEDLLPATH", None)
env.pop("WINE_AUDIO_DRIVER", None)
return subprocess.run(["bash", "-c", script, "bash", str(self.tool)], env=env, capture_output=True, text=True, check=True)
return subprocess.run(["bash", "-c", script, "bash", str(self.tool), target, verb], cwd=cwd, env=env,
capture_output=True, text=True, check=True)
def audio_values(self):
found, current = [], None
@@ -869,6 +895,57 @@ class DirectAudioPrefixTest(unittest.TestCase):
path = self.windows / folder / "winedirectaudio.drv"
return os.readlink(path) if path.is_symlink() else None
def test_non_launch_verbs_preserve_audio_configuration(self):
for verb in ("getcompatpath", "getnativepath", "destroyprefix"):
with self.subTest(verb=verb):
self.reg.write_text('WINE REGISTRY Version 2\n\n[Software\\\\Wine\\\\Drivers] 1\n"Audio"="pulse"\n')
before = self.reg.read_bytes()
result = self.run_setup(verb=verb)
self.assertEqual(self.reg.read_bytes(), before)
self.assertIsNone(self.link("system32"))
self.assertIsNone(self.link("syswow64"))
self.assertIn("WINEDLLPATH=\n", result.stdout)
self.assertIn("WINE_AUDIO_DRIVER=\n", result.stdout)
def test_other_windows_launch_verbs_still_set_up_audio(self):
self.reg.write_text("WINE REGISTRY Version 2\n")
for verb in ("run", "runinprefix"):
with self.subTest(verb=verb):
result = self.run_setup(verb=verb)
self.assertIn("DirectAudio ready", result.stderr)
self.assertIn("WINE_AUDIO_DRIVER=directaudio,pulse\n", result.stdout)
self.assertEqual(self.link("system32"), str(self.audio / "lib/wine/aarch64-windows/winedirectaudio.drv"))
def test_steam_installscript_evaluator_does_not_change_prefix_audio(self):
legacycompat = self.compat.parent / "Steam/legacycompat"
legacycompat.mkdir(parents=True)
(legacycompat / "iscriptevaluator.exe").write_bytes(b"Steam install-script evaluator")
self.reg.write_text("WINE REGISTRY Version 2\n\n#arch=win64\n")
result = self.run_setup("iscriptevaluator.exe", cwd=legacycompat)
self.assertIn("DirectAudio skipped for Steam's install-script evaluator", result.stderr)
self.assertIn("WINEDLLPATH=\n", result.stdout)
self.assertIn("WINE_AUDIO_DRIVER=\n", result.stdout)
self.assertEqual(self.reg.read_text(), "WINE REGISTRY Version 2\n\n#arch=win64\n")
self.assertIsNone(self.link("system32"))
self.assertIsNone(self.link("syswow64"))
def test_same_basename_outside_steam_legacycompat_keeps_normal_setup(self):
legacycompat = self.compat.parent / "Steam/legacycompat"
legacycompat.mkdir(parents=True)
(legacycompat / "iscriptevaluator.exe").write_bytes(b"Steam install-script evaluator")
other = self.compat.parent / "other"
other.mkdir()
(other / "iscriptevaluator.exe").write_bytes(b"unrelated executable")
self.reg.write_text("WINE REGISTRY Version 2\n\n#arch=win64\n")
result = self.run_setup("iscriptevaluator.exe", cwd=other)
self.assertIn("DirectAudio selected", result.stderr)
self.assertEqual(self.link("system32"), str(self.audio / "lib/wine/aarch64-windows/winedirectaudio.drv"))
self.assertEqual(self.audio_values(), [("[Software\\\\Wine\\\\Drivers]", '"Audio"="directaudio,pulse"')])
def test_the_driver_is_placed_and_selected_the_way_wine_reads_it(self):
self.reg.write_text("WINE REGISTRY Version 2\n;; All keys relative to \\\\User\\\\S-1-5-21-0-0-0-1000\n\n#arch=win64\n")
result = self.run_setup()