diff --git a/Source/Android/app/build.gradle.kts b/Source/Android/app/build.gradle.kts index 912906d2d8..27f76b22a2 100644 --- a/Source/Android/app/build.gradle.kts +++ b/Source/Android/app/build.gradle.kts @@ -141,6 +141,7 @@ dependencies { implementation(libs.androidx.profileinstaller) // Kotlin extensions for lifecycle components + implementation(libs.androidx.lifecycle.livedata.ktx) implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) @@ -151,6 +152,7 @@ dependencies { // For loading game covers from disk and GameTDB implementation(libs.coil) + implementation(libs.coil.compose) // For loading custom GPU drivers implementation(libs.kotlinx.serialization.json) @@ -164,6 +166,7 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material.icons) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.runtime.livedata) implementation(libs.androidx.compose.ui) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling.preview) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index 68bae9e732..e80a6d3c06 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress +import org.dolphinemu.dolphinemu.model.GameFile import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress @@ -149,6 +150,8 @@ class NetplaySession( fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer) + fun changeGame(gameFile: GameFile) = nativeChangeGame(gameFile) + fun startGame() = nativeStartGame() fun consumeBootSessionData(): Long { @@ -228,6 +231,8 @@ class NetplaySession( private external fun nativeReleaseBootSessionData(pointer: Long) + private external fun nativeChangeGame(gameFile: GameFile) + private external fun nativeStartGame() // NetPlayUI callbacks 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 e7518bdf43..50edaba597 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 @@ -4,17 +4,22 @@ 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.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.features.netplay.NetplaySession 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.model.GameFile +import org.dolphinemu.dolphinemu.services.GameFileCacheManager class NetplayViewModel( private val netplaySession: NetplaySession, @@ -41,10 +46,24 @@ class NetplayViewModel( private val _maxBuffer = MutableStateFlow(IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.int) val maxBuffer = _maxBuffer.asStateFlow() + val gameFiles = GameFileCacheManager.getGameFiles().asFlow() + .map { it.toList() } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + GameFileCacheManager.getGameFiles().value?.toList() ?: emptyList() + ) + val saveTransferProgress = netplaySession.saveTransferProgress val gameDigestProgress = netplaySession.gameDigestProgress + init { + if (netplaySession.isHosting) { + setInitialGame() + } + } + fun startGame() { netplaySession.startGame() } @@ -64,6 +83,21 @@ class NetplayViewModel( netplaySession.adjustPadBufferSize(buffer) } + fun changeGame(gameFile: GameFile) { + StringSetting.NETPLAY_GAME.setString(NativeConfig.LAYER_BASE, gameFile.getGameId()) + netplaySession.changeGame(gameFile) + } + + private fun setInitialGame() { + val game = gameFiles.value + .find { it.getGameId() == StringSetting.NETPLAY_GAME.string } + ?: gameFiles.value.firstOrNull() + + if (game != null) { + changeGame(game) + } + } + @OptIn(DelicateCoroutinesApi::class) override fun onCleared() { super.onCleared() 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 ce1f7dec19..55888dde6d 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 @@ -9,6 +9,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.livedata.observeAsState import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.flowWithLifecycle @@ -53,6 +54,8 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { game = viewModel.game.collectAsState().value, isHosting = viewModel.isHosting, onStartGame = viewModel::startGame, + onGameSelected = viewModel::changeGame, + gameFiles = viewModel.gameFiles.collectAsState().value, players = viewModel.players.collectAsState().value, hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value, maxBuffer = viewModel.maxBuffer.collectAsState().value, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index f1413636e4..a5c20bdd22 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -3,12 +3,14 @@ package org.dolphinemu.dolphinemu.features.netplay.ui import android.content.res.Configuration +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -19,6 +21,9 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions @@ -30,6 +35,7 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Remove import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.HorizontalDivider @@ -42,9 +48,10 @@ import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -55,6 +62,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext @@ -66,8 +75,11 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import org.dolphinemu.dolphinemu.R @@ -75,10 +87,12 @@ import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress +import org.dolphinemu.dolphinemu.model.GameFile import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme +import org.dolphinemu.dolphinemu.utils.CoilUtils import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @@ -91,6 +105,8 @@ fun NetplayScreen( onSendMessage: (String) -> Unit, game: String, onStartGame: () -> Unit, + onGameSelected: (GameFile) -> Unit, + gameFiles: List, hostInputAuthorityEnabled: Boolean, maxBuffer: Int, onMaxBufferChanged: (Int) -> Unit, @@ -125,12 +141,21 @@ fun NetplayScreen( .consumeWindowInsets(innerPadding) .padding(innerPadding) + var showChat by rememberSaveable { mutableStateOf(false) } + var showGamePicker by rememberSaveable { mutableStateOf(false) } + if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { LandscapeContent( isHosting = isHosting, messages = messages, onSendMessage = onSendMessage, + showChat = showChat, + onShowChatChanged = { showChat = it }, game = game, + gameFiles = gameFiles, + onGameSelected = onGameSelected, + showGamePicker = showGamePicker, + onShowGamePickerChanged = { showGamePicker = it }, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, maxBuffer = maxBuffer, @@ -142,7 +167,13 @@ fun NetplayScreen( isHosting = isHosting, messages = messages, onSendMessage = onSendMessage, + showChat = showChat, + onShowChatChanged = { showChat = it }, game = game, + gameFiles = gameFiles, + onGameSelected = onGameSelected, + showGamePicker = showGamePicker, + onShowGamePickerChanged = { showGamePicker = it }, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, maxBuffer = maxBuffer, @@ -201,7 +232,13 @@ private fun PortraitContent( isHosting: Boolean, messages: List, onSendMessage: (String) -> Unit, + showChat: Boolean, + onShowChatChanged: (Boolean) -> Unit, game: String, + gameFiles: List, + onGameSelected: (GameFile) -> Unit, + showGamePicker: Boolean, + onShowGamePickerChanged: (Boolean) -> Unit, players: List, hostInputAuthorityEnabled: Boolean, maxBuffer: Int, @@ -215,6 +252,8 @@ private fun PortraitContent( Chat( messages = messages, onSendMessage = onSendMessage, + showBottomSheet = showChat, + onShowBottomSheetChanged = onShowChatChanged, modifier = Modifier .fillMaxWidth() .height(200.dp) @@ -225,10 +264,15 @@ private fun PortraitContent( PLayersAndSettings( game = game, + gameFiles = gameFiles, + onGameSelected = onGameSelected, + showGamePicker = showGamePicker, + onShowGamePickerChanged = onShowGamePickerChanged, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, maxBuffer = maxBuffer, onMaxBufferChanged = onMaxBufferChanged, + isHosting = isHosting, modifier = Modifier .padding(horizontal = DolphinTheme.scaffoldPadding), ) @@ -244,7 +288,13 @@ private fun LandscapeContent( isHosting: Boolean, messages: List, onSendMessage: (String) -> Unit, + showChat: Boolean, + onShowChatChanged: (Boolean) -> Unit, game: String, + gameFiles: List, + onGameSelected: (GameFile) -> Unit, + showGamePicker: Boolean, + onShowGamePickerChanged: (Boolean) -> Unit, players: List, hostInputAuthorityEnabled: Boolean, maxBuffer: Int, @@ -257,6 +307,8 @@ private fun LandscapeContent( Chat( messages = messages, onSendMessage = onSendMessage, + showBottomSheet = showChat, + onShowBottomSheetChanged = onShowChatChanged, modifier = Modifier .weight(1f) .fillMaxHeight() @@ -270,10 +322,15 @@ private fun LandscapeContent( ) { PLayersAndSettings( game = game, + gameFiles = gameFiles, + onGameSelected = onGameSelected, + showGamePicker = showGamePicker, + onShowGamePickerChanged = onShowGamePickerChanged, players = players, hostInputAuthorityEnabled = hostInputAuthorityEnabled, maxBuffer = maxBuffer, onMaxBufferChanged = onMaxBufferChanged, + isHosting = isHosting, modifier = Modifier .padding(horizontal = DolphinTheme.scaffoldPadding) ) @@ -288,22 +345,27 @@ private fun LandscapeContent( @Composable private fun PLayersAndSettings( game: String, + gameFiles: List, + onGameSelected: (GameFile) -> Unit, + showGamePicker: Boolean, + onShowGamePickerChanged: (Boolean) -> Unit, players: List, hostInputAuthorityEnabled: Boolean, maxBuffer: Int, onMaxBufferChanged: (Int) -> Unit, + isHosting: Boolean, modifier: Modifier = Modifier, ) { Column( modifier = modifier ) { - OutlinedTextField( - value = game, - onValueChange = {}, - label = { Text(stringResource(R.string.netplay_game_label)) }, - readOnly = true, - modifier = Modifier - .fillMaxWidth() + GamePicker( + game = game, + gameFiles = gameFiles, + onGameSelected = onGameSelected, + showGamePicker = showGamePicker, + onShowGamePickerChanged = onShowGamePickerChanged, + isHosting = isHosting, ) MenuSpacer() @@ -345,6 +407,8 @@ private fun PLayersAndSettings( private fun Chat( messages: List, onSendMessage: (String) -> Unit, + showBottomSheet: Boolean, + onShowBottomSheetChanged: (Boolean) -> Unit, modifier: Modifier, ) { val context = LocalContext.current @@ -361,12 +425,18 @@ private fun Chat( draftMessage = "" } - var showBottomSheet by remember { mutableStateOf(false) } - val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val density = LocalDensity.current + val bottomSheetState = remember { + SheetState( + skipPartiallyExpanded = true, + density = density, + initialValue = if (showBottomSheet) SheetValue.Expanded else SheetValue.Hidden, + ) + } if (showBottomSheet) { ModalBottomSheet( - onDismissRequest = { showBottomSheet = false }, + onDismissRequest = { onShowBottomSheetChanged(false) }, sheetState = bottomSheetState, modifier = Modifier .statusBarsPadding() @@ -407,7 +477,7 @@ private fun Chat( } OutlinedBox( - onClick = { showBottomSheet = true }, + onClick = { onShowBottomSheetChanged(true) }, label = { Text(stringResource(R.string.netplay_chat_label)) }, modifier = modifier ) { @@ -422,6 +492,123 @@ private fun Chat( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun GamePicker( + game: String, + gameFiles: List, + onGameSelected: (GameFile) -> Unit, + showGamePicker: Boolean, + onShowGamePickerChanged: (Boolean) -> Unit, + isHosting: Boolean, +) { + val density = LocalDensity.current + val bottomSheetState = remember { + SheetState( + skipPartiallyExpanded = true, + density = density, + initialValue = if (showGamePicker) SheetValue.Expanded else SheetValue.Hidden, + ) + } + + if (showGamePicker) { + ModalBottomSheet( + onDismissRequest = { onShowGamePickerChanged(false) }, + sheetState = bottomSheetState, + modifier = Modifier.statusBarsPadding() + ) { + GameList( + gameFiles = gameFiles, + onGameSelected = { gameFile -> + onGameSelected(gameFile) + onShowGamePickerChanged(false) + }, + contentPadding = PaddingValues( + start = DolphinTheme.scaffoldPadding, + end = DolphinTheme.scaffoldPadding, + bottom = 16.dp + ), + ) + } + } + + Box( + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = game, + onValueChange = {}, + label = { Text(stringResource(R.string.netplay_game_label)) }, + readOnly = true, + modifier = Modifier.fillMaxWidth() + ) + if (isHosting) { + Box( + modifier = Modifier + .matchParentSize() + .padding(top = 8.dp) + .clip(MaterialTheme.shapes.extraSmall) + .clickable { onShowGamePickerChanged(true) } + ) + } + } +} + +@Composable +private fun GameList( + gameFiles: List, + onGameSelected: (GameFile) -> Unit, + contentPadding: PaddingValues = PaddingValues(), +) { + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 120.dp), + contentPadding = contentPadding, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items(gameFiles, key = { it.getPath() }) { gameFile -> + GameGridItem( + gameFile = gameFile, + onClick = { onGameSelected(gameFile) }, + ) + } + } +} + +@Composable +private fun GameGridItem( + gameFile: GameFile, + onClick: () -> Unit, +) { + Card( + onClick = onClick, + ) { + Column { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(gameFile) + .error(R.drawable.no_banner) + .build(), + contentDescription = gameFile.getTitle(), + contentScale = ContentScale.Crop, + imageLoader = CoilUtils.imageLoader, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(0.7f) + ) + Text( + text = gameFile.getTitle(), + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + minLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(8.dp) + ) + } + } +} + /** * A table arranged into columns sized to wrap the largest item. Except the * first column which takes up the remaining space left by the other columns. @@ -787,6 +974,8 @@ private fun PreviewNetplayScreen() { game = "Game name", isHosting = true, onStartGame = {}, + onGameSelected = {}, + gameFiles = emptyList(), hostInputAuthorityEnabled = true, maxBuffer = 10, onMaxBufferChanged = {}, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt index d3070a51ca..610f45b2a3 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt @@ -115,7 +115,8 @@ enum class StringSetting( "" ), NETPLAY_ADDRESS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Address", "127.0.0.1"), - NETPLAY_NICKNAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Nickname", "Player"); + NETPLAY_NICKNAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Nickname", "Player"), + NETPLAY_GAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Game", ""); override val isOverridden: Boolean get() = NativeConfig.isOverridden(file, section, key) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt index 846fcd564e..8c3fb2e3f3 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt @@ -66,7 +66,7 @@ class GameCoverKeyer : Keyer { } object CoilUtils { - private val imageLoader = ImageLoader.Builder(DolphinApplication.getAppContext()) + val imageLoader = ImageLoader.Builder(DolphinApplication.getAppContext()) .components { add(GameCoverKeyer()) add(GameCoverFetcher.Factory()) diff --git a/Source/Android/gradle/libs.versions.toml b/Source/Android/gradle/libs.versions.toml index e29e0c2e10..f4811334ac 100644 --- a/Source/Android/gradle/libs.versions.toml +++ b/Source/Android/gradle/libs.versions.toml @@ -35,6 +35,7 @@ androidx-cardview = { group = "androidx.cardview", name = "cardview", version.re androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } @@ -45,6 +46,7 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" } androidx-leanback = { group = "androidx.leanback", name = "leanback", version.ref = "leanback" } +androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preferenceKtx" } @@ -55,6 +57,7 @@ androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "s androidx-tvprovider = { group = "androidx.tvprovider", name = "tvprovider", version.ref = "tvprovider" } androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" } +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } desugar_jdk_libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar_jdk_libs" } filepicker = { group = "com.nononsenseapps", name = "filepicker", version.ref = "filepicker" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 1dd511954f..527ca40f40 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -148,6 +148,21 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeHost(JNIEnv return reinterpret_cast(server); } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeChangeGame(JNIEnv* env, + jobject obj, + jobject jgame_file) +{ + auto* server = GetServerPointer(env, obj); + if (!server) + return; + + const auto& game_file = *reinterpret_cast*>( + env->GetLongField(jgame_file, IDCache::GetGameFilePointer())); + + server->ChangeGame(game_file->GetSyncIdentifier(), game_file->GetLongName()); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(JNIEnv* env, jobject obj)