mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-18 05:07:22 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67bf8f1568 |
No files matched your search
@@ -151,15 +151,6 @@
|
||||
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.SecondScreenWindow
|
||||
import com.droiddeck.launcher.ui.SecondScreenPresentation
|
||||
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: SecondScreenWindow? = null
|
||||
private var secondScreenPresentation: SecondScreenPresentation? = 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 = SecondScreenWindow(
|
||||
presentation = SecondScreenPresentation(
|
||||
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,7 +1425,6 @@ 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
|
||||
|
||||
+48
-9
@@ -1,17 +1,21 @@
|
||||
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
|
||||
@@ -33,9 +37,9 @@ import com.termux.terminal.TerminalSessionClient
|
||||
import com.termux.view.TerminalView
|
||||
import com.termux.view.TerminalViewClient
|
||||
|
||||
internal class SecondScreenControls(
|
||||
context: Context,
|
||||
class SecondScreenPresentation(
|
||||
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,
|
||||
@@ -43,8 +47,12 @@ internal class SecondScreenControls(
|
||||
private val onQam: () -> Unit,
|
||||
private val onClose: () -> Unit,
|
||||
private val pad: PadBridge? = null,
|
||||
) : LinearLayout(context) {
|
||||
private val root get() = this
|
||||
) : Presentation(sessionActivity, display) {
|
||||
private val root = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setBackgroundColor(Color.rgb(15, 18, 22))
|
||||
isFocusableInTouchMode = true
|
||||
}
|
||||
private var currentMode = SecondScreenMode.NONE
|
||||
private var keyboardHost: KeyboardHost? = null
|
||||
private var terminalSession: TerminalSession? = null
|
||||
@@ -52,22 +60,35 @@ internal class SecondScreenControls(
|
||||
private var terminalPid = -1
|
||||
private var ctrlActive = false
|
||||
private var altActive = false
|
||||
private var created = false
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
setBackgroundColor(Color.rgb(15, 18, 22))
|
||||
isFocusableInTouchMode = true
|
||||
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()
|
||||
}
|
||||
|
||||
fun showMode(mode: SecondScreenMode) {
|
||||
if (currentMode == mode) return
|
||||
if (currentMode == mode && isShowing) return
|
||||
currentMode = mode
|
||||
renderMode()
|
||||
if (created) renderMode()
|
||||
}
|
||||
|
||||
fun closeControls() {
|
||||
closeTerminal()
|
||||
keyboardHost?.hide()
|
||||
keyboardHost = null
|
||||
dismiss()
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
@@ -77,6 +98,14 @@ internal class SecondScreenControls(
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (hasFocus) {
|
||||
applyImmersiveMode()
|
||||
sessionActivity.refreshClipboard()
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderMode() {
|
||||
keyboardHost?.hide()
|
||||
keyboardHost = null
|
||||
@@ -262,6 +291,16 @@ internal class SecondScreenControls(
|
||||
|
||||
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 {
|
||||
@@ -1,139 +0,0 @@
|
||||
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>
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user