summaryrefslogtreecommitdiff
path: root/Source/Android/app/src/main/java/org
diff options
context:
space:
mode:
authorTom Pratt <tom.pratt@outlook.com>2026-05-12 11:20:01 -0700
committerTom Pratt <tompratt@squareup.com>2026-05-19 12:02:58 +0200
commitabd324e98d735d836d8131cef91b58f3278bc6fa (patch)
treed32b98f84035c45fed4a1ba65d67895ca8272511 /Source/Android/app/src/main/java/org
parent183d6d778ca8a87dd5ad1ae3513f7fcfd561369d (diff)
Make NetplaySession not a singleton
Create a new NetplaySession each time we try to join a netplay game. Hold onto it in NetplayManager so its available to the different activities that need to access it. Close the session when backing out of the netplay UI. Some guardrails in case things go out of sync: creating a session closes the old one if it is still around for some reason, finalizer in NetplaySession to release native resources if not closed explicitly for some reason. Profiling done to ensure all kotlin and native objects are successfully cleared / garbage collected.
Diffstat (limited to 'Source/Android/app/src/main/java/org')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt2
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt38
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt (renamed from Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt)140
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt52
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt47
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt11
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt6
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt15
8 files changed, 195 insertions, 116 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt
index 2e2c295c7d..b52b7b4fa4 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt
@@ -346,7 +346,7 @@ object NativeLibrary {
* Begins emulation for a netplay session, using the BootSessionData provided by the host.
*/
@JvmStatic
- external fun RunNetPlay(paths: Array<String>, riivolution: Boolean)
+ external fun RunNetPlay(paths: Array<String>, riivolution: Boolean, bootSessionDataPointer: Long)
@JvmStatic
external fun ChangeDisc(path: String)
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt
new file mode 100644
index 0000000000..eae44e65be
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplayManager.kt
@@ -0,0 +1,38 @@
+// Copyright 2003 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.features.netplay
+
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+object NetplayManager {
+
+ private val mutex = Mutex()
+
+ @Volatile
+ private var closeComplete: CompletableDeferred<Unit>? = null
+
+ @Volatile
+ var activeSession: NetplaySession? = null
+ private set
+
+ suspend fun createSession(): NetplaySession = mutex.withLock {
+ closeComplete?.await()
+
+ // Sessions should be closed by UI navigation, but just in case.
+ activeSession?.closeBlocking()
+
+ closeComplete = CompletableDeferred()
+
+ NetplaySession(
+ onClosed = {
+ activeSession = null
+ closeComplete?.complete(Unit)
+ }
+ ).also {
+ activeSession = it
+ }
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt
index 1cec94f19d..f47d9ab198 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt
@@ -6,7 +6,6 @@ package org.dolphinemu.dolphinemu.features.netplay
import androidx.annotation.Keep
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.BufferOverflow
@@ -29,14 +28,21 @@ import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
import org.dolphinemu.dolphinemu.features.netplay.model.Player
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
-object Netplay {
- @Keep
+class NetplaySession(
+ private val onClosed: (NetplaySession) -> Unit,
+) {
+
+ private var netPlayUICallbacksPointer: Long = nativeCreateUICallbacks()
+
private var netPlayClientPointer: Long = 0
- @Keep
private var bootSessionDataPointer: Long = 0
- private var sessionScope: CoroutineScope? = null
+ private val sessionScope = CoroutineScope(SupervisorJob())
+
+ @Volatile
+ var isClosed = false
+ private set
val isLaunching: Boolean
get() = bootSessionDataPointer != 0L
@@ -93,97 +99,96 @@ object Netplay {
val saveTransferProgress = _saveTransferProgress.asStateFlow()
suspend fun join(): Boolean = withContext(Dispatchers.IO) {
- val scope = createSessionScope()
+ if (isClosed) throw IllegalStateException("Cannot join a closed session")
- // Gather all messages that should appear in the chat window.
mergeMessages()
.runningFold(emptyList<NetplayMessage>()) { acc, msg -> listOf(msg) + acc }
.onEach { _messages.tryEmit(it) }
- .launchIn(scope)
+ .launchIn(sessionScope)
- netPlayClientPointer = Join()
- val isConnected = netPlayClientPointer != 0L && isClientConnected()
+ netPlayClientPointer = nativeJoin()
- if (!isActive) {
- releaseNetplayClient()
+ if (netPlayClientPointer == 0L || !isActive) {
+ closeBlocking()
return@withContext false
}
- if (isConnected) {
- return@withContext true
+ true
+ }
+
+ fun sendMessage(message: String) = nativeSendMessage(message)
+
+ fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer)
+
+ fun consumeBootSessionData(): Long {
+ return bootSessionDataPointer.also {
+ bootSessionDataPointer = 0
}
+ }
- releaseNetplayClient()
- false
+ suspend fun close() = withContext(Dispatchers.IO) {
+ closeBlocking()
}
- suspend fun quit() = withContext(Dispatchers.IO) {
- releaseNetplayClient()
+ @Synchronized
+ fun closeBlocking() {
+ if (isClosed) return
+ isClosed = true
+ sessionScope.cancel()
+ releaseNativeResources()
+ onClosed(this)
}
- @OptIn(ExperimentalCoroutinesApi::class)
- private fun releaseNetplayClient() {
- sessionScope?.cancel()
- sessionScope = null
+ protected fun finalize() {
+ releaseNativeResources()
+ }
- if (bootSessionDataPointer != 0L) {
- ReleaseBootSessionData()
+ private fun mergeMessages(): Flow<NetplayMessage> = merge(
+ chatMessages.map { NetplayMessage.Chat(it) },
+ game.map { NetplayMessage.GameChanged(it) },
+ hostInputAuthorityEnabled.map { NetplayMessage.HostInputAuthorityChanged(it) },
+ padBuffer.map { NetplayMessage.BufferChanged(it) },
+ )
+
+ private fun releaseNativeResources() {
+ val currentBootSessionDataPointer = bootSessionDataPointer
+ if (currentBootSessionDataPointer != 0L) {
bootSessionDataPointer = 0
+ nativeReleaseBootSessionData(currentBootSessionDataPointer)
}
- if (netPlayClientPointer != 0L) {
- ReleaseNetplayClient()
+ val currentNetPlayClientPointer = netPlayClientPointer
+ if (currentNetPlayClientPointer != 0L) {
netPlayClientPointer = 0
+ nativeReleaseClient(currentNetPlayClientPointer)
}
- _launchGame.flush()
- _stopGame.flush()
- _connectionErrors.flush()
- _players.resetReplayCache()
- _messages.resetReplayCache()
- _chatMessages.resetReplayCache()
- _game.resetReplayCache()
- _hostInputAuthorityEnabled.resetReplayCache()
- _padBuffer.resetReplayCache()
- _saveTransferProgress.value = null
- }
-
- private fun createSessionScope(): CoroutineScope {
- sessionScope?.cancel()
- return CoroutineScope(SupervisorJob() + Dispatchers.IO).also {
- sessionScope = it
+ val currentNetPlayUICallbacksPointer = netPlayUICallbacksPointer
+ if (currentNetPlayUICallbacksPointer != 0L) {
+ netPlayUICallbacksPointer = 0
+ nativeReleaseUICallbacks(currentNetPlayUICallbacksPointer)
}
}
- @JvmStatic
- private external fun Join(): Long
+ // JNI methods
- @JvmStatic
- external fun isClientConnected(): Boolean
+ private external fun nativeCreateUICallbacks(): Long
- @JvmStatic
- external fun sendMessage(message: String)
+ private external fun nativeJoin(): Long
- @JvmStatic
- external fun adjustPadBufferSize(buffer: Int)
+ private external fun nativeSendMessage(message: String)
- @JvmStatic
- private external fun ReleaseBootSessionData()
+ private external fun nativeAdjustPadBufferSize(buffer: Int)
- @JvmStatic
- private external fun ReleaseNetplayClient()
+ private external fun nativeReleaseUICallbacks(pointer: Long)
- private fun mergeMessages(): Flow<NetplayMessage> = merge(
- chatMessages.map { NetplayMessage.Chat(it) },
- game.map { NetplayMessage.GameChanged(it) },
- hostInputAuthorityEnabled.map { NetplayMessage.HostInputAuthorityChanged(it) },
- padBuffer.map { NetplayMessage.BufferChanged(it) },
- )
+ private external fun nativeReleaseClient(pointer: Long)
+
+ private external fun nativeReleaseBootSessionData(pointer: Long)
// NetPlayUI callbacks
@Keep
- @JvmStatic
fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) {
this.bootSessionDataPointer = bootSessionDataPointer
_stopGame.flush()
@@ -191,57 +196,47 @@ object Netplay {
}
@Keep
- @JvmStatic
fun onStopGame() {
_stopGame.trySend(Unit)
}
@Keep
- @JvmStatic
fun onConnectionLost() {
_connectionLost.trySend(Unit)
}
@Keep
- @JvmStatic
fun onConnectionError(message: String) {
_connectionErrors.trySend(message)
}
@Keep
- @JvmStatic
fun onUpdate(players: Array<Player>) {
_players.tryEmit(players.toList())
}
@Keep
- @JvmStatic
fun onChatMessageReceived(message: String) {
_chatMessages.tryEmit(message)
}
@Keep
- @JvmStatic
fun onHostInputAuthorityChanged(enabled: Boolean) {
_hostInputAuthorityEnabled.tryEmit(enabled)
}
@Keep
- @JvmStatic
fun onGameChanged(game: String) {
_game.tryEmit(game)
}
@Keep
- @JvmStatic
fun onPadBufferChanged(buffer: Int) {
- // Only for remote pad buffer settings. Ignore local max buffer changes.
if (_hostInputAuthorityEnabled.replayCache.firstOrNull() == true) return
_padBuffer.tryEmit(buffer)
}
@Keep
- @JvmStatic
fun onShowChunkedProgressDialog(title: String, dataSize: Long, playerIds: IntArray) {
val players = _players.replayCache.firstOrNull()
_saveTransferProgress.value = SaveTransferProgress(
@@ -258,7 +253,6 @@ object Netplay {
}
@Keep
- @JvmStatic
fun onSetChunkedProgress(playerId: Int, progress: Long) {
val current = _saveTransferProgress.value
_saveTransferProgress.value = current?.copy(
@@ -273,11 +267,9 @@ object Netplay {
}
@Keep
- @JvmStatic
fun onHideChunkedProgressDialog() {
_saveTransferProgress.value = null
}
-
}
private fun <T> Channel<T>.flush() {
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt
index 2a04e41c35..a9b34d3147 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt
@@ -3,22 +3,31 @@
package org.dolphinemu.dolphinemu.features.netplay.model
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
-import org.dolphinemu.dolphinemu.features.netplay.Netplay
+import org.dolphinemu.dolphinemu.features.netplay.NetplayManager
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
import org.dolphinemu.dolphinemu.services.GameFileCacheManager
-class NetplaySetupViewModel : ViewModel() {
+class NetplaySetupViewModel(
+ private val netplayManager: NetplayManager,
+) : ViewModel() {
+
private val _connectionRole = MutableStateFlow<ConnectionRole>(ConnectionRole.Connect)
val connectionRole = _connectionRole.asStateFlow()
@@ -45,7 +54,8 @@ class NetplaySetupViewModel : ViewModel() {
private val _connecting = MutableStateFlow(false)
val connecting = _connecting.asStateFlow()
- val errors = Netplay.connectionErrors
+ private val _errors = MutableSharedFlow<String>(extraBufferCapacity = 8)
+ val errors = _errors.asSharedFlow()
init {
GameFileCacheManager.startLoad()
@@ -89,16 +99,42 @@ class NetplaySetupViewModel : ViewModel() {
}
fun connect() {
+ if (_connecting.value) return
+
_connecting.value = true
viewModelScope.launch {
- GameFileCacheManager.isLoading().asFlow().first { it == false }
-
- if (Netplay.join()) {
- _showNetplayScreen.trySend(Unit)
+ var errorForwarding: Job? = null
+
+ try {
+ GameFileCacheManager.isLoading().asFlow().first { it == false }
+
+ val session = netplayManager.createSession()
+ errorForwarding = session.connectionErrors
+ .onEach { _errors.emit(it) }
+ .launchIn(this)
+
+ if (session.join()) {
+ _showNetplayScreen.trySend(Unit)
+ }
+ } finally {
+ errorForwarding?.cancel()
+ _connecting.value = false
}
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ // There should not be an active session at this point but in case one was created
+ // but launching the Netplay screen failed, close it.
+ netplayManager.activeSession?.closeBlocking()
+ }
- _connecting.value = false
+ class Factory(private val netplayManager: NetplayManager) : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ return NetplaySetupViewModel(netplayManager) as T
}
}
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
index 66ad26aaec..96f9d7328b 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt
@@ -3,51 +3,43 @@
package org.dolphinemu.dolphinemu.features.netplay.model
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-import org.dolphinemu.dolphinemu.features.netplay.Netplay
+import org.dolphinemu.dolphinemu.features.netplay.NetplaySession
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig
-class NetplayViewModel : ViewModel() {
- val launchGame = Netplay.launchGame
+class NetplayViewModel(
+ private val netplaySession: NetplaySession,
+) : ViewModel() {
- private val _goBack = Channel<Unit>(CONFLATED)
- val goBack = _goBack.receiveAsFlow()
+ val launchGame = netplaySession.launchGame
- val connectionLost = Netplay.connectionLost
+ val connectionLost = netplaySession.connectionLost
- val players = Netplay.players
+ val players = netplaySession.players
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
- val messages = Netplay.messages
+ val messages = netplaySession.messages
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
- val game = Netplay.game
+ val game = netplaySession.game
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "")
- val hostInputAuthority = Netplay.hostInputAuthorityEnabled
+ val hostInputAuthority = netplaySession.hostInputAuthorityEnabled
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
private val _maxBuffer = MutableStateFlow(IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.int)
val maxBuffer = _maxBuffer.asStateFlow()
- val saveTransferProgress = Netplay.saveTransferProgress
-
- init {
- if (!Netplay.isClientConnected()) {
- _goBack.trySend(Unit)
- }
- }
+ val saveTransferProgress = netplaySession.saveTransferProgress
fun sendMessage(message: String) {
val trimmedMessage = message.trim()
@@ -55,20 +47,29 @@ class NetplayViewModel : ViewModel() {
return
}
- Netplay.sendMessage(trimmedMessage)
+ netplaySession.sendMessage(trimmedMessage)
}
fun setMaxBuffer(buffer: Int) {
_maxBuffer.value = buffer
IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.setInt(NativeConfig.LAYER_BASE, buffer)
- Netplay.adjustPadBufferSize(buffer)
+ netplaySession.adjustPadBufferSize(buffer)
}
@OptIn(DelicateCoroutinesApi::class)
override fun onCleared() {
super.onCleared()
+ // Closing the netplay session is a bit slow for the main thread so launch in
+ // GlobalScope and allow the activity and view model to finish immediately.
GlobalScope.launch {
- Netplay.quit()
+ netplaySession.close()
+ }
+ }
+
+ class Factory(private val session: NetplaySession) : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ return NetplayViewModel(session) as T
}
}
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
index 8a8f860825..f2acf50107 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt
@@ -16,6 +16,7 @@ import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.dolphinemu.dolphinemu.activities.EmulationActivity
+import org.dolphinemu.dolphinemu.features.netplay.NetplayManager
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayViewModel
import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
@@ -29,11 +30,13 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
- val viewModel = ViewModelProvider(this)[NetplayViewModel::class.java]
+ val session = NetplayManager.activeSession
+ if (session == null) {
+ finish()
+ return
+ }
- viewModel.goBack
- .onEach { finish() }
- .launchIn(lifecycleScope)
+ val viewModel = ViewModelProvider(this, NetplayViewModel.Factory(session))[NetplayViewModel::class.java]
viewModel.launchGame
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt
index 8058b3ad96..145cf7d635 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplaySetupActivity.kt
@@ -15,6 +15,7 @@ import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import org.dolphinemu.dolphinemu.features.netplay.NetplayManager
import org.dolphinemu.dolphinemu.features.netplay.model.NetplaySetupViewModel
import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
@@ -28,7 +29,10 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
- val viewModel = ViewModelProvider(this)[NetplaySetupViewModel::class.java]
+ val viewModel = ViewModelProvider(
+ this,
+ NetplaySetupViewModel.Factory(NetplayManager)
+ )[NetplaySetupViewModel::class.java]
viewModel.showNetplayScreen
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt
index 49331d8a4c..c989cdc780 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt
@@ -16,7 +16,7 @@ import kotlinx.coroutines.launch
import org.dolphinemu.dolphinemu.NativeLibrary
import org.dolphinemu.dolphinemu.activities.EmulationActivity
import org.dolphinemu.dolphinemu.databinding.FragmentEmulationBinding
-import org.dolphinemu.dolphinemu.features.netplay.Netplay
+import org.dolphinemu.dolphinemu.features.netplay.NetplayManager
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.overlay.InputOverlay
@@ -211,7 +211,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
// Don't load temporary saves when launching Netplay, this path can trigger
// when a game starts due to orientation changes caused by a mismatch in menu
// vs emulation activity orientations.
- if (loadPreviousTemporaryState && !Netplay.isLaunching) {
+ val netplaySession = NetplayManager.activeSession
+ if (loadPreviousTemporaryState && netplaySession?.isLaunching != true) {
Log.debug("[EmulationFragment] Starting emulation thread from previous state.")
val paths = requireNotNull(gamePaths) {
"Cannot start emulation without any game paths"
@@ -221,16 +222,20 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
if (launchSystemMenu) {
Log.debug("[EmulationFragment] Starting emulation thread for the Wii Menu.")
NativeLibrary.RunSystemMenu()
- } else if (Netplay.isLaunching) {
+ } else if (netplaySession?.isLaunching == true) {
Log.debug("[EmulationFragment] Starting emulation thread for Netplay.")
val paths = requireNotNull(gamePaths) {
"Cannot start emulation without any game paths"
}
lifecycleScope.launch {
- Netplay.stopGame.first()
+ netplaySession.stopGame.first()
stopEmulation()
}
- NativeLibrary.RunNetPlay(paths, riivolution)
+ NativeLibrary.RunNetPlay(
+ paths,
+ riivolution,
+ netplaySession.consumeBootSessionData()
+ )
} else {
Log.debug("[EmulationFragment] Starting emulation thread.")
val paths = requireNotNull(gamePaths) {