mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-18 05:07:22 -04:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
352e4f8961 | ||
|
|
8ea019a76a | ||
|
|
4817500c49 | ||
|
|
3c8d1d0178 | ||
|
|
e6960700e5 |
No files matched your search
@@ -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
|
||||
|
||||
+9
-48
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user