mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-07-03 17:11:59 -05:00
Implement player list
This commit is contained in:
parent
af2fda5649
commit
d9e2725e85
|
|
@ -5,11 +5,17 @@ package org.dolphinemu.dolphinemu.features.netplay
|
|||
|
||||
import androidx.annotation.Keep
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.Player
|
||||
|
||||
object Netplay {
|
||||
@Keep
|
||||
|
|
@ -27,6 +33,12 @@ object Netplay {
|
|||
private val _connectionErrors = Channel<String>(Channel.BUFFERED)
|
||||
val connectionErrors = _connectionErrors.receiveAsFlow()
|
||||
|
||||
private val _players = MutableSharedFlow<List<Player>>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val players = _players.asSharedFlow().distinctUntilChanged()
|
||||
|
||||
suspend fun join(): Boolean = withContext(Dispatchers.IO) {
|
||||
netPlayClientPointer = Join()
|
||||
val isConnected = netPlayClientPointer != 0L && isClientConnected()
|
||||
|
|
@ -48,6 +60,7 @@ object Netplay {
|
|||
releaseNetplayClient()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun releaseNetplayClient() {
|
||||
if (netPlayClientPointer != 0L) {
|
||||
ReleaseNetplayClient()
|
||||
|
|
@ -55,6 +68,7 @@ object Netplay {
|
|||
}
|
||||
_launchGame.flush()
|
||||
_connectionErrors.flush()
|
||||
_players.resetReplayCache()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
|
@ -79,6 +93,11 @@ object Netplay {
|
|||
_connectionErrors.trySend(message)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun onUpdate(players: Array<Player>) {
|
||||
_players.tryEmit(players.toList())
|
||||
}
|
||||
|
||||
// Settings
|
||||
|
||||
@JvmStatic
|
||||
|
|
|
|||
|
|
@ -3,11 +3,14 @@
|
|||
package org.dolphinemu.dolphinemu.features.netplay.model
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
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.SharingStarted
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.dolphinemu.dolphinemu.features.netplay.Netplay
|
||||
|
||||
|
|
@ -17,6 +20,9 @@ class NetplayViewModel : ViewModel() {
|
|||
private val _goBack = Channel<Unit>(CONFLATED)
|
||||
val goBack = _goBack.receiveAsFlow()
|
||||
|
||||
val players = Netplay.players
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
|
||||
init {
|
||||
if (!Netplay.isClientConnected()) {
|
||||
_goBack.trySend(Unit)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright 2003 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.dolphinemu.dolphinemu.features.netplay.model
|
||||
|
||||
data class Player(
|
||||
val pid: Int,
|
||||
val name: String,
|
||||
val revision: String,
|
||||
val ping: Int,
|
||||
val isHost: Boolean,
|
||||
val mapping: String,
|
||||
)
|
||||
|
|
@ -8,6 +8,7 @@ import android.os.Bundle
|
|||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
|
|
@ -15,7 +16,6 @@ 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.Netplay
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayViewModel
|
||||
import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
|
||||
import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
|
||||
|
|
@ -44,6 +44,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
|
|||
DolphinTheme {
|
||||
NetplayScreen(
|
||||
onBackClicked = { finish() },
|
||||
players = viewModel.players.collectAsState().value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,23 +2,46 @@
|
|||
|
||||
package org.dolphinemu.dolphinemu.features.netplay.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MediumTopAppBar
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.Player
|
||||
import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NetplayScreen(
|
||||
onBackClicked: () -> Unit,
|
||||
players: List<Player>,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
|
|
@ -34,7 +57,81 @@ fun NetplayScreen(
|
|||
},
|
||||
)
|
||||
},
|
||||
) { _ -> }
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.consumeWindowInsets(innerPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(innerPadding)
|
||||
.padding(horizontal = DolphinTheme.scaffoldPadding)
|
||||
) {
|
||||
PlayersTable(
|
||||
rows = buildList {
|
||||
add(listOf("Player", "Ping", "Mapping"))
|
||||
addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) })
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* The first row is treated as the column titles.
|
||||
*/
|
||||
@Composable
|
||||
private fun PlayersTable(
|
||||
rows: List<List<String>>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
rows.zipWithNext { a, b -> if (a.size != b.size) throw IllegalArgumentException("Rows must all contain the same number of elements.") }
|
||||
val maxWidths = remember { List(rows.first().size) { mutableIntStateOf(0) } }
|
||||
val density = LocalDensity.current
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = modifier
|
||||
) {
|
||||
rows.forEachIndexed { rowIndex, row ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
row.forEachIndexed { itemIndex, text ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
when {
|
||||
itemIndex == 0 -> Modifier.weight(1f)
|
||||
|
||||
maxWidths[itemIndex].intValue > 0 -> Modifier
|
||||
.width(with(density) { maxWidths[itemIndex].intValue.toDp() })
|
||||
|
||||
else -> Modifier
|
||||
}
|
||||
)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
val width = coordinates.size.width
|
||||
if (width > maxWidths[itemIndex].intValue) {
|
||||
maxWidths[itemIndex].intValue = width
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontWeight = if (rowIndex == 0) FontWeight.Medium else FontWeight.Normal,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rowIndex == 0) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
@ -42,5 +139,23 @@ fun NetplayScreen(
|
|||
private fun NetplayScreenPreview() {
|
||||
NetplayScreen(
|
||||
onBackClicked = {},
|
||||
players = listOf(
|
||||
Player(
|
||||
pid = 1,
|
||||
name = "Player 1",
|
||||
revision = "123",
|
||||
ping = 2,
|
||||
isHost = true,
|
||||
mapping = "m1"
|
||||
),
|
||||
Player(
|
||||
pid = 2,
|
||||
name = "Player 2",
|
||||
revision = "123",
|
||||
ping = 23,
|
||||
isHost = false,
|
||||
mapping = "m2"
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ static jfieldID s_net_play_client_pointer;
|
|||
static jfieldID s_netplay_boot_session_data_pointer;
|
||||
static jmethodID s_netplay_on_boot_game;
|
||||
static jmethodID s_netplay_on_connection_error;
|
||||
static jmethodID s_netplay_update;
|
||||
|
||||
static jclass s_netplay_player_class;
|
||||
static jmethodID s_netplay_player_constructor;
|
||||
|
||||
static jclass s_analytics_class;
|
||||
static jmethodID s_get_analytics_value;
|
||||
|
|
@ -264,6 +268,21 @@ jmethodID GetNetplayOnConnectionError()
|
|||
return s_netplay_on_connection_error;
|
||||
}
|
||||
|
||||
jmethodID GetNetplayUpdate()
|
||||
{
|
||||
return s_netplay_update;
|
||||
}
|
||||
|
||||
jclass GetNetplayPlayerClass()
|
||||
{
|
||||
return s_netplay_player_class;
|
||||
}
|
||||
|
||||
jmethodID GetNetplayPlayerConstructor()
|
||||
{
|
||||
return s_netplay_player_constructor;
|
||||
}
|
||||
|
||||
jclass GetPairClass()
|
||||
{
|
||||
return s_pair_class;
|
||||
|
|
@ -675,8 +694,15 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
|||
s_netplay_boot_session_data_pointer = env->GetStaticFieldID(netplay_class, "bootSessionDataPointer", "J");
|
||||
s_netplay_on_boot_game = env->GetStaticMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V");
|
||||
s_netplay_on_connection_error = env->GetStaticMethodID(netplay_class, "onConnectionError", "(Ljava/lang/String;)V");
|
||||
s_netplay_update = env->GetStaticMethodID(netplay_class, "onUpdate", "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V");
|
||||
env->DeleteLocalRef(netplay_class);
|
||||
|
||||
const jclass netplay_player_class =
|
||||
env->FindClass("org/dolphinemu/dolphinemu/features/netplay/model/Player");
|
||||
s_netplay_player_class = reinterpret_cast<jclass>(env->NewGlobalRef(netplay_player_class));
|
||||
s_netplay_player_constructor = env->GetMethodID(netplay_player_class, "<init>", "(ILjava/lang/String;Ljava/lang/String;IZLjava/lang/String;)V");
|
||||
env->DeleteLocalRef(netplay_player_class);
|
||||
|
||||
const jclass analytics_class = env->FindClass("org/dolphinemu/dolphinemu/utils/Analytics");
|
||||
s_analytics_class = reinterpret_cast<jclass>(env->NewGlobalRef(analytics_class));
|
||||
s_get_analytics_value = env->GetStaticMethodID(s_analytics_class, "getValue",
|
||||
|
|
@ -892,6 +918,7 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved)
|
|||
env->DeleteGlobalRef(s_game_file_cache_class);
|
||||
env->DeleteGlobalRef(s_game_file_cache_manager_class);
|
||||
env->DeleteGlobalRef(s_netplay_class);
|
||||
env->DeleteGlobalRef(s_netplay_player_class);
|
||||
env->DeleteGlobalRef(s_analytics_class);
|
||||
env->DeleteGlobalRef(s_pair_class);
|
||||
env->DeleteGlobalRef(s_hash_map_class);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ jfieldID GetNetPlayClientPointer();
|
|||
jfieldID GetNetplayBootSessionDataPointer();
|
||||
jmethodID GetNetplayOnBootGame();
|
||||
jmethodID GetNetplayOnConnectionError();
|
||||
jmethodID GetNetplayUpdate();
|
||||
|
||||
jclass GetNetplayPlayerClass();
|
||||
jmethodID GetNetplayPlayerConstructor();
|
||||
|
||||
jclass GetPairClass();
|
||||
jmethodID GetPairConstructor();
|
||||
|
|
|
|||
|
|
@ -24,7 +24,41 @@ void NetPlayUICallbacks::BootGame(const std::string& filename, std::unique_ptr<B
|
|||
|
||||
void NetPlayUICallbacks::StopGame() {}
|
||||
bool NetPlayUICallbacks::IsHosting() const { return false; }
|
||||
void NetPlayUICallbacks::Update() {}
|
||||
|
||||
void NetPlayUICallbacks::Update()
|
||||
{
|
||||
JNIEnv* env = IDCache::GetEnvForThread();
|
||||
auto* client = reinterpret_cast<NetPlay::NetPlayClient*>(
|
||||
env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer()));
|
||||
if (!client)
|
||||
return;
|
||||
|
||||
const std::vector<const NetPlay::Player*> players = client->GetPlayers();
|
||||
|
||||
jobjectArray player_array =
|
||||
env->NewObjectArray(static_cast<jsize>(players.size()), IDCache::GetNetplayPlayerClass(), nullptr);
|
||||
|
||||
for (jsize i = 0; i < static_cast<jsize>(players.size()); i++)
|
||||
{
|
||||
const NetPlay::Player* player = players[i];
|
||||
const std::string mapping = NetPlay::GetPlayerMappingString(
|
||||
player->pid, client->GetPadMapping(), client->GetGBAConfig(), client->GetWiimoteMapping());
|
||||
jobject player_obj = env->NewObject(
|
||||
IDCache::GetNetplayPlayerClass(), IDCache::GetNetplayPlayerConstructor(),
|
||||
static_cast<jint>(player->pid),
|
||||
ToJString(env, player->name),
|
||||
ToJString(env, player->revision),
|
||||
static_cast<jint>(player->ping),
|
||||
static_cast<jboolean>(player->IsHost()),
|
||||
ToJString(env, mapping));
|
||||
env->SetObjectArrayElement(player_array, i, player_obj);
|
||||
env->DeleteLocalRef(player_obj);
|
||||
}
|
||||
|
||||
env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayUpdate(), player_array);
|
||||
env->DeleteLocalRef(player_array);
|
||||
}
|
||||
|
||||
void NetPlayUICallbacks::AppendChat(const std::string&) {}
|
||||
|
||||
void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_identifier,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user