[frontend, web] refactor: web service frontend rewrite (#221)
All checks were successful
eden-build / android (push) Successful in 24m41s
eden-build / windows (msvc) (push) Successful in 26m9s
eden-build / source (push) Successful in 6m5s
eden-build / linux (push) Successful in 23m50s

- Automatic verification based on regex
- Token generation button
- Removed unneeded links
- public lobby creation [android]

Signed-off-by: crueter <swurl@swurl.xyz>
Co-authored-by: Aleksandr Popovich <alekpopo@pm.me>
Co-authored-by: Aleksandr Popovich <alekpopo@proton.me>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/221
Co-authored-by: crueter <swurl@swurl.xyz>
Co-committed-by: crueter <swurl@swurl.xyz>
This commit is contained in:
crueter 2025-07-01 01:44:12 +00:00 committed by crueter
parent 2fe728766e
commit 94c66f98bf
No known key found for this signature in database
GPG key ID: BA8734FD0EE46976
34 changed files with 1985 additions and 327 deletions

View file

@ -4,13 +4,12 @@ HEADER="$(cat "$PWD/.ci/license/header.txt")"
echo "Getting branch changes" echo "Getting branch changes"
# I created this cursed POSIX abomination only to discover a better solution BRANCH=`git rev-parse --abbrev-ref HEAD`
#BRANCH=`git rev-parse --abbrev-ref HEAD` COMMITS=`git log ${BRANCH} --not master --pretty=format:"%h"`
#COMMITS=`git log ${BRANCH} --not master --pretty=format:"%h"` RANGE="${COMMITS[${#COMMITS[@]}-1]}^..${COMMITS[0]}"
#RANGE="${COMMITS[${#COMMITS[@]}-1]}^..${COMMITS[0]}" FILES=`git diff-tree --no-commit-id --name-only ${RANGE} -r`
#FILES=`git diff-tree --no-commit-id --name-only ${RANGE} -r`
FILES=$(git diff --name-only master) #FILES=$(git diff --name-only master)
echo "Done" echo "Done"

View file

@ -1,7 +1,6 @@
// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.dialogs package org.yuzu.yuzu_emu.dialogs
import android.annotation.SuppressLint import android.annotation.SuppressLint
@ -19,6 +18,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogChatBinding import org.yuzu.yuzu_emu.databinding.DialogChatBinding
import org.yuzu.yuzu_emu.databinding.ItemChatMessageBinding import org.yuzu.yuzu_emu.databinding.ItemChatMessageBinding
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.network.NetPlayManager import org.yuzu.yuzu_emu.network.NetPlayManager
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -82,7 +82,7 @@ class ChatDialog(context: Context) : BottomSheetDialog(context) {
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun sendMessage(message: String) { private fun sendMessage(message: String) {
val username = NetPlayManager.getUsername(context) val username = StringSetting.WEB_USERNAME.getString()
NetPlayManager.netPlaySendMessage(message) NetPlayManager.netPlaySendMessage(message)
val chatMessage = ChatMessage( val chatMessage = ChatMessage(

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.dialogs package org.yuzu.yuzu_emu.dialogs
@ -28,6 +28,7 @@ import info.debatty.java.stringsimilarity.JaroWinkler
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogLobbyBrowserBinding import org.yuzu.yuzu_emu.databinding.DialogLobbyBrowserBinding
import org.yuzu.yuzu_emu.databinding.ItemLobbyRoomBinding import org.yuzu.yuzu_emu.databinding.ItemLobbyRoomBinding
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.network.NetPlayManager import org.yuzu.yuzu_emu.network.NetPlayManager
import java.util.Locale import java.util.Locale
@ -144,7 +145,7 @@ class LobbyBrowser(context: Context) : BottomSheetDialog(context) {
} }
private fun joinRoom(room: NetPlayManager.RoomInfo, password: String) { private fun joinRoom(room: NetPlayManager.RoomInfo, password: String) {
val username = NetPlayManager.getUsername(context) val username = StringSetting.WEB_USERNAME.getString()
Thread { Thread {
val result = NetPlayManager.netPlayJoinRoom(room.ip, room.port, username, password) val result = NetPlayManager.netPlayJoinRoom(room.ip, room.port, username, password)

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.dialogs package org.yuzu.yuzu_emu.dialogs
@ -24,6 +24,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.databinding.DialogMultiplayerConnectBinding import org.yuzu.yuzu_emu.databinding.DialogMultiplayerConnectBinding
@ -32,6 +33,7 @@ import org.yuzu.yuzu_emu.databinding.DialogMultiplayerRoomBinding
import org.yuzu.yuzu_emu.databinding.ItemBanListBinding import org.yuzu.yuzu_emu.databinding.ItemBanListBinding
import org.yuzu.yuzu_emu.databinding.ItemButtonNetplayBinding import org.yuzu.yuzu_emu.databinding.ItemButtonNetplayBinding
import org.yuzu.yuzu_emu.databinding.ItemTextNetplayBinding import org.yuzu.yuzu_emu.databinding.ItemTextNetplayBinding
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.network.NetPlayManager import org.yuzu.yuzu_emu.network.NetPlayManager
import org.yuzu.yuzu_emu.utils.CompatUtils import org.yuzu.yuzu_emu.utils.CompatUtils
import org.yuzu.yuzu_emu.utils.GameHelper import org.yuzu.yuzu_emu.utils.GameHelper
@ -180,9 +182,9 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
PopupMenu(view.context, view).apply { PopupMenu(view.context, view).apply {
menuInflater.inflate(R.menu.menu_netplay_member, menu) menuInflater.inflate(R.menu.menu_netplay_member, menu)
menu.findItem(R.id.action_kick).isEnabled = isModerator && menu.findItem(R.id.action_kick).isEnabled = isModerator &&
netPlayItems.name != NetPlayManager.getUsername(context) netPlayItems.name != StringSetting.WEB_USERNAME.getString()
menu.findItem(R.id.action_ban).isEnabled = isModerator && menu.findItem(R.id.action_ban).isEnabled = isModerator &&
netPlayItems.name != NetPlayManager.getUsername(context) netPlayItems.name != StringSetting.WEB_USERNAME.getString()
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
if (item.itemId == R.id.action_kick) { if (item.itemId == R.id.action_kick) {
NetPlayManager.netPlayKickUser(netPlayItems.name) NetPlayManager.netPlayKickUser(netPlayItems.name)
@ -297,13 +299,13 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
abstract class TextValidatorWatcher( abstract class TextValidatorWatcher(
private val btnConfirm: Button, private val btnConfirm: Button,
private val view: EditText, private val layout: TextInputLayout,
private val errorMessage: String private val errorMessage: String
) : TextWatcher { ) : TextWatcher {
companion object { companion object {
val validStates: HashMap<EditText, Boolean> = hashMapOf() val validStates: HashMap<TextInputLayout, Boolean> = hashMapOf()
} }
abstract fun validate(s: String): Boolean abstract fun validate(s: String): Boolean
override fun beforeTextChanged( override fun beforeTextChanged(
@ -325,20 +327,20 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
val input = s.toString() val input = s.toString()
val isValid = validate(input) val isValid = validate(input)
view.error = if (isValid) null else errorMessage layout.isErrorEnabled = !isValid
layout.error = if (isValid) null else errorMessage
validStates.put(view, isValid) validStates[layout] = isValid
btnConfirm.isEnabled = !validStates.containsValue(false) btnConfirm.isEnabled = !validStates.containsValue(false)
} }
} }
// TODO(alekpop, crueter): Properly handle getting banned (both during and in future connects) // TODO(alekpop, crueter): Properly handle getting banned (both during and in future connects)
private fun showNetPlayInputDialog(isCreateRoom: Boolean) { private fun showNetPlayInputDialog(isCreateRoom: Boolean) {
TextValidatorWatcher.validStates.clear()
val activity = CompatUtils.findActivity(context) val activity = CompatUtils.findActivity(context)
val dialog = BottomSheetDialog(activity) val dialog = BottomSheetDialog(activity)
val validStates: HashMap<EditText, Boolean> = hashMapOf()
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
dialog.behavior.skipCollapsed = dialog.behavior.skipCollapsed =
@ -347,6 +349,11 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
val binding = DialogMultiplayerRoomBinding.inflate(LayoutInflater.from(activity)) val binding = DialogMultiplayerRoomBinding.inflate(LayoutInflater.from(activity))
dialog.setContentView(binding.root) dialog.setContentView(binding.root)
val visibilityList: List<String> = listOf(
context.getString(R.string.multiplayer_public_visibility),
context.getString(R.string.multiplayer_unlisted_visibility),
)
binding.textTitle.text = activity.getString( binding.textTitle.text = activity.getString(
if (isCreateRoom) R.string.multiplayer_create_room if (isCreateRoom) R.string.multiplayer_create_room
else R.string.multiplayer_join_room else R.string.multiplayer_join_room
@ -355,7 +362,7 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
// setup listeners etc // setup listeners etc
val roomNameWatcher = object : TextValidatorWatcher( val roomNameWatcher = object : TextValidatorWatcher(
binding.btnConfirm, // TODO(alekpop, crueter): Figure out a better way to deal with this? binding.btnConfirm, // TODO(alekpop, crueter): Figure out a better way to deal with this?
binding.roomName, binding.layoutRoomName,
context.getString( context.getString(
R.string.multiplayer_room_name_error R.string.multiplayer_room_name_error
) )
@ -367,25 +374,32 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
val preferredWatcher = object : TextValidatorWatcher( val preferredWatcher = object : TextValidatorWatcher(
binding.btnConfirm, binding.btnConfirm,
binding.dropdownPreferredGameName, binding.preferredGameName,
context.getString(R.string.multiplayer_required) context.getString(R.string.multiplayer_required)
) { ) {
override fun validate(s: String): Boolean { override fun validate(s: String): Boolean {
return s.isNotEmpty() return s.isNotEmpty()
} }
}
override fun afterTextChanged(s: Editable?) { val visibilityWatcher = object : TextValidatorWatcher(
super.afterTextChanged(s) binding.btnConfirm,
binding.lobbyVisibility,
context.getString(R.string.multiplayer_token_required)
) {
override fun validate(s: String): Boolean {
if (s != context.getString(R.string.multiplayer_public_visibility)) {
return true;
}
// special case: remove dropdown arrow val token = StringSetting.WEB_TOKEN.getString()
val input = s.toString() return token.matches(Regex("[a-z]{48}"))
binding.preferredGameName.isEndIconVisible = validate(input)
} }
} }
val ipWatcher = object : TextValidatorWatcher( val ipWatcher = object : TextValidatorWatcher(
binding.btnConfirm, binding.btnConfirm,
binding.ipAddress, binding.layoutIpAddress,
context.getString(R.string.multiplayer_ip_error) context.getString(R.string.multiplayer_ip_error)
) { ) {
override fun validate(s: String): Boolean { override fun validate(s: String): Boolean {
@ -400,17 +414,17 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
val usernameWatcher = object : TextValidatorWatcher( val usernameWatcher = object : TextValidatorWatcher(
binding.btnConfirm, binding.btnConfirm,
binding.username, binding.layoutUsername,
context.getString(R.string.multiplayer_username_error) context.getString(R.string.multiplayer_username_error)
) { ) {
override fun validate(s: String): Boolean { override fun validate(s: String): Boolean {
return s.length >= 5 return s.length in 4..20
} }
} }
val portWatcher = object : TextValidatorWatcher( val portWatcher = object : TextValidatorWatcher(
binding.btnConfirm, binding.btnConfirm,
binding.ipPort, binding.layoutIpPort,
context.getString(R.string.multiplayer_port_error) context.getString(R.string.multiplayer_port_error)
) { ) {
override fun validate(s: String): Boolean { override fun validate(s: String): Boolean {
@ -421,6 +435,7 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
if (isCreateRoom) { if (isCreateRoom) {
binding.roomName.addTextChangedListener(roomNameWatcher) binding.roomName.addTextChangedListener(roomNameWatcher)
binding.dropdownPreferredGameName.addTextChangedListener(preferredWatcher) binding.dropdownPreferredGameName.addTextChangedListener(preferredWatcher)
binding.dropdownLobbyVisibility.addTextChangedListener(visibilityWatcher)
binding.dropdownPreferredGameName.apply { binding.dropdownPreferredGameName.apply {
setAdapter( setAdapter(
@ -431,19 +446,35 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
) )
) )
} }
binding.dropdownLobbyVisibility.setText(context.getString(R.string.multiplayer_unlisted_visibility))
binding.dropdownLobbyVisibility.apply {
setAdapter(
ArrayAdapter(
activity,
R.layout.dropdown_item,
visibilityList
)
)
}
} }
binding.ipAddress.addTextChangedListener(ipWatcher) binding.ipAddress.addTextChangedListener(ipWatcher)
binding.ipPort.addTextChangedListener(portWatcher) binding.ipPort.addTextChangedListener(portWatcher)
binding.username.addTextChangedListener(usernameWatcher) binding.username.addTextChangedListener(usernameWatcher)
binding.ipAddress.setText(NetPlayManager.getRoomAddress(activity))
binding.ipPort.setText(NetPlayManager.getRoomPort(activity)) binding.ipPort.setText(NetPlayManager.getRoomPort(activity))
binding.username.setText(NetPlayManager.getUsername(activity)) binding.username.setText(StringSetting.WEB_USERNAME.getString())
// manually trigger text listeners // manually trigger text listeners
if (isCreateRoom) { if (isCreateRoom) {
roomNameWatcher.afterTextChanged(binding.roomName.text) roomNameWatcher.afterTextChanged(binding.roomName.text)
preferredWatcher.afterTextChanged(binding.dropdownPreferredGameName.text) preferredWatcher.afterTextChanged(binding.dropdownPreferredGameName.text)
// It's not needed here, the watcher is called by the initial set method
// visibilityWatcher.afterTextChanged(binding.dropdownLobbyVisibility.text)
} }
ipWatcher.afterTextChanged(binding.ipAddress.text) ipWatcher.afterTextChanged(binding.ipAddress.text)
@ -451,8 +482,10 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
usernameWatcher.afterTextChanged(binding.username.text) usernameWatcher.afterTextChanged(binding.username.text)
binding.preferredGameName.visibility = if (isCreateRoom) View.VISIBLE else View.GONE binding.preferredGameName.visibility = if (isCreateRoom) View.VISIBLE else View.GONE
binding.lobbyVisibility.visibility = if (isCreateRoom) View.VISIBLE else View.GONE
binding.roomName.visibility = if (isCreateRoom) View.VISIBLE else View.GONE binding.roomName.visibility = if (isCreateRoom) View.VISIBLE else View.GONE
binding.maxPlayersContainer.visibility = if (isCreateRoom) View.VISIBLE else View.GONE binding.maxPlayersContainer.visibility = if (isCreateRoom) View.VISIBLE else View.GONE
binding.maxPlayersLabel.text = context.getString( binding.maxPlayersLabel.text = context.getString(
R.string.multiplayer_max_players_value, R.string.multiplayer_max_players_value,
binding.maxPlayers.value.toInt() binding.maxPlayers.value.toInt()
@ -464,7 +497,6 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
} }
// TODO(alekpop, crueter): Room descriptions // TODO(alekpop, crueter): Room descriptions
// TODO(alekpop, crueter): Public room creation
// TODO(alekpop, crueter): Preview preferred games // TODO(alekpop, crueter): Preview preferred games
binding.btnConfirm.setOnClickListener { binding.btnConfirm.setOnClickListener {
binding.btnConfirm.isEnabled = false binding.btnConfirm.isEnabled = false
@ -487,6 +519,9 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
val preferredIdx = gameNameList.indexOfFirst { it[0] == preferredGameName } val preferredIdx = gameNameList.indexOfFirst { it[0] == preferredGameName }
val preferredGameId = if (preferredIdx == -1) 0 else gameIdList[preferredIdx][0] val preferredGameId = if (preferredIdx == -1) 0 else gameIdList[preferredIdx][0]
val visibility = binding.dropdownLobbyVisibility.text.toString()
val isPublic = visibility == context.getString(R.string.multiplayer_public_visibility)
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
val result = if (isCreateRoom) { val result = if (isCreateRoom) {
NetPlayManager.netPlayCreateRoom( NetPlayManager.netPlayCreateRoom(
@ -497,23 +532,26 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
preferredGameId, preferredGameId,
password, password,
roomName, roomName,
maxPlayers maxPlayers,
isPublic
) )
} else { } else {
NetPlayManager.netPlayJoinRoom(ipAddress, port, username, password) NetPlayManager.netPlayJoinRoom(ipAddress, port, username, password)
} }
if (result == 0) { if (result == 0) {
// TODO(alekpop, crueter): These need to be moved as settings, editable in a tab StringSetting.WEB_USERNAME.setString(username)
NetPlayManager.setUsername(activity, username)
NetPlayManager.setRoomPort(activity, portStr) NetPlayManager.setRoomPort(activity, portStr)
NetPlayManager.setRoomAddress(activity, ipAddress)
if (!isCreateRoom) NetPlayManager.setRoomAddress(activity, ipAddress) if (!isCreateRoom) NetPlayManager.setRoomAddress(activity, ipAddress)
Toast.makeText( Toast.makeText(
YuzuApplication.appContext, YuzuApplication.appContext,
if (isCreateRoom) R.string.multiplayer_create_room_success if (isCreateRoom) R.string.multiplayer_create_room_success
else R.string.multiplayer_join_room_success, else R.string.multiplayer_join_room_success,
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
dialog.dismiss() dialog.dismiss()
} else { } else {
Toast.makeText( Toast.makeText(
@ -521,6 +559,7 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
R.string.multiplayer_could_not_connect, R.string.multiplayer_could_not_connect,
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
binding.btnConfirm.isEnabled = true binding.btnConfirm.isEnabled = true
binding.btnConfirm.text = activity.getString(R.string.ok) binding.btnConfirm.text = activity.getString(R.string.ok)
} }

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -9,9 +12,8 @@ enum class StringSetting(override val key: String) : AbstractStringSetting {
DRIVER_PATH("driver_path"), DRIVER_PATH("driver_path"),
DEVICE_NAME("device_name"), DEVICE_NAME("device_name"),
// TODO(crueter, alekpop): Netplay/settings needs to be properly worked into settings WEB_TOKEN("eden_token"),
WEB_TOKEN("yuzu_token"), WEB_USERNAME("eden_username"),
WEB_USERNAME("yuzu_username"),
; ;
override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal) override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal)

View file

@ -284,11 +284,32 @@ abstract class SettingsItem(
descriptionId = R.string.use_custom_rtc_description descriptionId = R.string.use_custom_rtc_description
) )
) )
put( put(
StringInputSetting( StringInputSetting(
StringSetting.WEB_TOKEN, StringSetting.WEB_TOKEN,
titleId = R.string.web_token, titleId = R.string.web_token,
descriptionId = R.string.web_token_description descriptionId = R.string.web_token_description,
onGenerate = {
val chars = "abcdefghijklmnopqrstuvwxyz"
(1..48).map { chars.random() }.joinToString("")
},
validator = { s ->
s?.matches(Regex("[a-z]{48}")) == true
},
errorId = R.string.multiplayer_token_error
)
)
put(
StringInputSetting(
StringSetting.WEB_USERNAME,
titleId = R.string.web_username,
descriptionId = R.string.web_username_description,
validator = { s ->
s?.length in 4..20
},
errorId = R.string.multiplayer_username_error
) )
) )
put(DateTimeSetting(LongSetting.CUSTOM_RTC, titleId = R.string.set_custom_rtc)) put(DateTimeSetting(LongSetting.CUSTOM_RTC, titleId = R.string.set_custom_rtc))

View file

@ -1,8 +1,12 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project // SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import android.text.Editable
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
@ -11,7 +15,10 @@ class StringInputSetting(
@StringRes titleId: Int = 0, @StringRes titleId: Int = 0,
titleString: String = "", titleString: String = "",
@StringRes descriptionId: Int = 0, @StringRes descriptionId: Int = 0,
descriptionString: String = "" descriptionString: String = "",
val onGenerate: (() -> String)? = null,
val validator: ((s: String?) -> Boolean)? = null,
@StringRes val errorId: Int = 0
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) { ) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_STRING_INPUT override val type = TYPE_STRING_INPUT

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -6,9 +9,13 @@ package org.yuzu.yuzu_emu.features.settings.ui
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -138,6 +145,46 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
stringInputBinding = DialogEditTextBinding.inflate(layoutInflater) stringInputBinding = DialogEditTextBinding.inflate(layoutInflater)
val item = settingsViewModel.clickedItem as StringInputSetting val item = settingsViewModel.clickedItem as StringInputSetting
stringInputBinding.editText.setText(item.getSelectedValue()) stringInputBinding.editText.setText(item.getSelectedValue())
val onGenerate = item.onGenerate
stringInputBinding.generate.isVisible = onGenerate != null
if (onGenerate != null) {
stringInputBinding.generate.setOnClickListener {
stringInputBinding.editText.setText(onGenerate())
}
}
val validator = item.validator
if (validator != null) {
val watcher = object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int
) {
}
override fun afterTextChanged(s: Editable?) {
stringInputBinding.editText.error =
if (validator(s.toString())) null else requireContext().getString(item.errorId)
}
}
stringInputBinding.editText.addTextChangedListener(watcher)
watcher.afterTextChanged(stringInputBinding.editText.text)
}
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title) .setTitle(item.title)
.setView(stringInputBinding.root) .setView(stringInputBinding.root)

View file

@ -212,30 +212,38 @@ class SettingsFragmentPresenter(
add(BooleanSetting.USE_CUSTOM_RTC.key) add(BooleanSetting.USE_CUSTOM_RTC.key)
add(LongSetting.CUSTOM_RTC.key) add(LongSetting.CUSTOM_RTC.key)
// TODO(alekpop): Add functionality add(HeaderSetting(R.string.network))
// add(HeaderSetting(R.string.network)) add(StringSetting.WEB_TOKEN.key)
// add(StringSetting.WEB_TOKEN.key) add(StringSetting.WEB_USERNAME.key)
} }
} }
private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) { private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
sl.apply { sl.apply {
// TODO(crueter): reorganize this, this is awful // TODO(crueter): reorganize this, this is awful
add(HeaderSetting(R.string.backend))
add(IntSetting.RENDERER_ACCURACY.key) add(IntSetting.RENDERER_ACCURACY.key)
add(IntSetting.RENDERER_RESOLUTION.key) add(IntSetting.RENDERER_RESOLUTION.key)
add(IntSetting.RENDERER_VSYNC.key)
add(IntSetting.RENDERER_SCALING_FILTER.key)
add(IntSetting.FSR_SHARPENING_SLIDER.key)
add(IntSetting.RENDERER_ANTI_ALIASING.key)
add(IntSetting.MAX_ANISOTROPY.key)
add(IntSetting.RENDERER_SCREEN_LAYOUT.key)
add(IntSetting.RENDERER_ASPECT_RATIO.key)
add(IntSetting.VERTICAL_ALIGNMENT.key)
add(BooleanSetting.PICTURE_IN_PICTURE.key)
add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key) add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key)
add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key) add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key)
add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key) add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key) add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key)
add(HeaderSetting(R.string.processing))
add(IntSetting.RENDERER_VSYNC.key)
add(IntSetting.RENDERER_ANTI_ALIASING.key)
add(IntSetting.MAX_ANISOTROPY.key)
add(IntSetting.RENDERER_SCALING_FILTER.key)
add(IntSetting.FSR_SHARPENING_SLIDER.key)
add(HeaderSetting(R.string.display))
add(IntSetting.RENDERER_SCREEN_LAYOUT.key)
add(IntSetting.RENDERER_ASPECT_RATIO.key)
add(IntSetting.VERTICAL_ALIGNMENT.key)
add(BooleanSetting.PICTURE_IN_PICTURE.key)
} }
} }

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.network package org.yuzu.yuzu_emu.network
@ -16,6 +16,7 @@ import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.dialogs.ChatMessage import org.yuzu.yuzu_emu.dialogs.ChatMessage
import java.net.Inet4Address import java.net.Inet4Address
import androidx.core.content.edit
object NetPlayManager { object NetPlayManager {
external fun netPlayCreateRoom( external fun netPlayCreateRoom(
@ -26,7 +27,8 @@ object NetPlayManager {
preferredGameId: Long, preferredGameId: Long,
password: String, password: String,
roomName: String, roomName: String,
maxPlayers: Int maxPlayers: Int,
isPublic: Boolean
): Int ): Int
external fun netPlayJoinRoom( external fun netPlayJoinRoom(
@ -125,17 +127,6 @@ object NetPlayManager {
adapterRefreshListener = listener adapterRefreshListener = listener
} }
fun getUsername(activity: Context): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
val name = "Eden${(Math.random() * 100).toInt()}"
return prefs.getString("NetPlayUsername", name) ?: name
}
fun setUsername(activity: Activity, name: String) {
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
prefs.edit().putString("NetPlayUsername", name).apply()
}
fun getRoomAddress(activity: Activity): String { fun getRoomAddress(activity: Activity): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(activity) val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
val address = getIpAddressByWifi(activity) val address = getIpAddressByWifi(activity)
@ -144,7 +135,7 @@ object NetPlayManager {
fun setRoomAddress(activity: Activity, address: String) { fun setRoomAddress(activity: Activity, address: String) {
val prefs = PreferenceManager.getDefaultSharedPreferences(activity) val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
prefs.edit().putString("NetPlayRoomAddress", address).apply() prefs.edit { putString("NetPlayRoomAddress", address) }
} }
fun getRoomPort(activity: Activity): String { fun getRoomPort(activity: Activity): String {
@ -154,7 +145,7 @@ object NetPlayManager {
fun setRoomPort(activity: Activity, port: String) { fun setRoomPort(activity: Activity, port: String) {
val prefs = PreferenceManager.getDefaultSharedPreferences(activity) val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
prefs.edit().putString("NetPlayRoomPort", port).apply() prefs.edit { putString("NetPlayRoomPort", port) }
} }
private val chatMessages = mutableListOf<ChatMessage>() private val chatMessages = mutableListOf<ChatMessage>()

View file

@ -975,12 +975,12 @@ Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayGetPublicRooms(
JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayCreateRoom( JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayCreateRoom(
JNIEnv* env, [[maybe_unused]] jobject obj, jstring ipaddress, jint port, JNIEnv* env, [[maybe_unused]] jobject obj, jstring ipaddress, jint port,
jstring username, jstring preferredGameName, jlong preferredGameId, jstring password, jstring username, jstring preferredGameName, jlong preferredGameId, jstring password,
jstring room_name, jint max_players) { jstring room_name, jint max_players, jboolean isPublic) {
return static_cast<jint>( return static_cast<jint>(
multiplayer->NetPlayCreateRoom(Common::Android::GetJString(env, ipaddress), port, multiplayer->NetPlayCreateRoom(Common::Android::GetJString(env, ipaddress), port,
Common::Android::GetJString(env, username), Common::Android::GetJString(env, preferredGameName), Common::Android::GetJString(env, username), Common::Android::GetJString(env, preferredGameName),
preferredGameId,Common::Android::GetJString(env, password), preferredGameId,Common::Android::GetJString(env, password),
Common::Android::GetJString(env, room_name), max_players)); Common::Android::GetJString(env, room_name), max_players, isPublic));
} }
JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayJoinRoom( JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayJoinRoom(

View file

@ -2,6 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -20,4 +21,13 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/generate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@string/generate"
app:layout_constraintEnd_toEndOf="@+id/edit_text_layout"
app:layout_constraintTop_toBottomOf="@+id/edit_text_layout" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -21,6 +21,7 @@
android:textColor="?attr/colorOnSurface" /> android:textColor="?attr/colorOnSurface" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_ip_address"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/multiplayer_ip_address" android:hint="@string/multiplayer_ip_address"
@ -34,6 +35,7 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_ip_port"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/multiplayer_ip_port" android:hint="@string/multiplayer_ip_port"
@ -47,6 +49,7 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_username"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/multiplayer_username" android:hint="@string/multiplayer_username"
@ -89,6 +92,7 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_room_name"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/multiplayer_room_name" android:hint="@string/multiplayer_room_name"
@ -125,6 +129,21 @@
android:text="@string/multiplayer_max_players_value" /> android:text="@string/multiplayer_max_players_value" />
</LinearLayout> </LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/lobby_visibility"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/multiplayer_lobby_type"
android:padding="8dp">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_lobby_visibility"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn_confirm" android:id="@+id/btn_confirm"
android:layout_width="wrap_content" android:layout_width="wrap_content"

File diff suppressed because it is too large Load diff

View file

@ -62,6 +62,7 @@
<item>@string/language_spanish</item> <item>@string/language_spanish</item>
<item>@string/language_taiwanese</item> <item>@string/language_taiwanese</item>
<item>@string/language_traditional_chinese</item> <item>@string/language_traditional_chinese</item>
<item>@string/language_serbian</item>
</string-array> </string-array>
<integer-array name="languageValues"> <integer-array name="languageValues">
@ -83,6 +84,7 @@
<item>5</item> <item>5</item>
<item>11</item> <item>11</item>
<item>16</item> <item>16</item>
<item>18</item>
</integer-array> </integer-array>
<string-array name="rendererApiNames"> <string-array name="rendererApiNames">

View file

@ -204,18 +204,22 @@
<string name="emulation_multiplayer">Multiplayer</string> <string name="emulation_multiplayer">Multiplayer</string>
<string name="multiplayer_game_name">Preferred Games</string> <string name="multiplayer_game_name">Preferred Games</string>
<string name="multiplayer_preferred_game_name">Preferred Game</string> <string name="multiplayer_preferred_game_name">Preferred Game</string>
<string name="multiplayer_lobby_type">Lobby Type</string>
<string name="multiplayer_no_game">No Games Found</string> <string name="multiplayer_no_game">No Games Found</string>
<string name="multiplayer_preferred_game_name_invalid">You must choose a Preferred Game to host a room.</string> <string name="multiplayer_preferred_game_name_invalid">You must choose a Preferred Game to host a room.</string>
<string name="multiplayer_room_name_error">Must be between 3 and 20 characters</string> <string name="multiplayer_room_name_error">Must be between 3 and 20 characters</string>
<string name="multiplayer_required">Required</string> <string name="multiplayer_required">Required</string>
<string name="multiplayer_token_required">Web Token required, go to Advanced Settings -> System -> Network</string>
<string name="multiplayer_ip_error">Invalid IP format</string> <string name="multiplayer_ip_error">Invalid IP format</string>
<string name="multiplayer_username_error">Must be at least 5 characters</string> <string name="multiplayer_username_error">Must be between 420 characters</string>
<string name="multiplayer_token_error">Must be 48 characters, and lowercase a-z only</string>
<string name="multiplayer_port_error">Must be between 1 and 65535</string> <string name="multiplayer_port_error">Must be between 1 and 65535</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="ok">Ok</string> <string name="ok">Ok</string>
<string name="refresh">Refresh</string> <string name="refresh">Refresh</string>
<string name="room_list">Room List</string> <string name="room_list">Room List</string>
<string name="multiplayer_public_visibility">Public</string>
<string name="multiplayer_unlisted_visibility">Unlisted</string>
<!-- Setup strings --> <!-- Setup strings -->
<string name="welcome">Welcome!</string> <string name="welcome">Welcome!</string>
@ -443,12 +447,20 @@
<string name="use_custom_rtc_description">Allows you to set a custom real-time clock separate from your current system time.</string> <string name="use_custom_rtc_description">Allows you to set a custom real-time clock separate from your current system time.</string>
<string name="set_custom_rtc">Set custom RTC</string> <string name="set_custom_rtc">Set custom RTC</string>
<string name="generate">Generate</string>
<!-- Network settings strings --> <!-- Network settings strings -->
<string name="web_token">Web Token</string> <string name="web_token">Web Token</string>
<string name="web_token_description">Web token used for creating public lobbies. It is a 48-character string containing only lowercase a-z.</string> <string name="web_token_description">Web token used for creating public lobbies. It is a 48-character string containing only lowercase a-z.</string>
<string name="web_username">Web Username</string>
<string name="web_username_description">Username to be shown in multiplayer lobbies. It must be 420 characters.</string>
<string name="network">Network</string> <string name="network">Network</string>
<!-- Graphics settings strings --> <!-- Graphics settings strings -->
<string name="backend">Backend</string>
<string name="display">Display</string>
<string name="processing">Post-Processing</string>
<string name="frame_skipping">WIP: Frameskip</string> <string name="frame_skipping">WIP: Frameskip</string>
<string name="frame_skipping_description">Toggle frame skipping to improve performance by reducing the number of rendered frames. This feature is still being worked on and will be enabled in future releases.</string> <string name="frame_skipping_description">Toggle frame skipping to improve performance by reducing the number of rendered frames. This feature is still being worked on and will be enabled in future releases.</string>
<string name="renderer_accuracy">Accuracy level</string> <string name="renderer_accuracy">Accuracy level</string>
@ -820,6 +832,7 @@
<string name="language_simplified_chinese" translatable="false">简体中文</string> <string name="language_simplified_chinese" translatable="false">简体中文</string>
<string name="language_traditional_chinese" translatable="false">正體中文</string> <string name="language_traditional_chinese" translatable="false">正體中文</string>
<string name="language_brazilian_portuguese" translatable="false">Português do Brasil</string> <string name="language_brazilian_portuguese" translatable="false">Português do Brasil</string>
<string name="language_serbian" translatable="false">српски</string>
<!-- Memory Sizes --> <!-- Memory Sizes -->
<string name="memory_byte">Byte</string> <string name="memory_byte">Byte</string>

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#include "common/android/id_cache.h" #include "common/android/id_cache.h"
@ -10,13 +10,17 @@
#include "network/network.h" #include "network/network.h"
#include "android/log.h" #include "android/log.h"
#include "common/settings.h"
#include "web_service/web_backend.h"
#include "web_service/verify_user_jwt.h"
#include "web_service/web_result.h"
#include <thread> #include <thread>
#include <chrono> #include <chrono>
namespace IDCache = Common::Android; namespace IDCache = Common::Android;
AndroidMultiplayer::AndroidMultiplayer(Core::System& system_, AndroidMultiplayer::AndroidMultiplayer(Core::System &system_,
std::shared_ptr<Core::AnnounceMultiplayerSession> session) std::shared_ptr<Core::AnnounceMultiplayerSession> session)
: system{system_}, announce_multiplayer_session(session) {} : system{system_}, announce_multiplayer_session(session) {}
@ -27,8 +31,8 @@ void AndroidMultiplayer::AddNetPlayMessage(jint type, jstring msg) {
IDCache::GetAddNetPlayMessage(), type, msg); IDCache::GetAddNetPlayMessage(), type, msg);
} }
void AndroidMultiplayer::AddNetPlayMessage(int type, const std::string& msg) { void AndroidMultiplayer::AddNetPlayMessage(int type, const std::string &msg) {
JNIEnv* env = IDCache::GetEnvForThread(); JNIEnv *env = IDCache::GetEnvForThread();
AddNetPlayMessage(type, Common::Android::ToJString(env, msg)); AddNetPlayMessage(type, Common::Android::ToJString(env, msg));
} }
@ -46,7 +50,7 @@ bool AndroidMultiplayer::NetworkInit() {
if (auto member = Network::GetRoomMember().lock()) { if (auto member = Network::GetRoomMember().lock()) {
// register the network structs to use in slots and signals // register the network structs to use in slots and signals
member->BindOnStateChanged([this](const Network::RoomMember::State& state) { member->BindOnStateChanged([this](const Network::RoomMember::State &state) {
if (state == Network::RoomMember::State::Joined || if (state == Network::RoomMember::State::Joined ||
state == Network::RoomMember::State::Moderator) { state == Network::RoomMember::State::Moderator) {
NetPlayStatus status; NetPlayStatus status;
@ -64,7 +68,7 @@ bool AndroidMultiplayer::NetworkInit() {
AddNetPlayMessage(static_cast<int>(status), msg); AddNetPlayMessage(static_cast<int>(status), msg);
} }
}); });
member->BindOnError([this](const Network::RoomMember::Error& error) { member->BindOnError([this](const Network::RoomMember::Error &error) {
NetPlayStatus status; NetPlayStatus status;
std::string msg; std::string msg;
switch (error) { switch (error) {
@ -107,29 +111,30 @@ bool AndroidMultiplayer::NetworkInit() {
} }
AddNetPlayMessage(static_cast<int>(status), msg); AddNetPlayMessage(static_cast<int>(status), msg);
}); });
member->BindOnStatusMessageReceived([this](const Network::StatusMessageEntry& status_message) { member->BindOnStatusMessageReceived(
NetPlayStatus status = NetPlayStatus::NO_ERROR; [this](const Network::StatusMessageEntry &status_message) {
std::string msg(status_message.nickname); NetPlayStatus status = NetPlayStatus::NO_ERROR;
switch (status_message.type) { std::string msg(status_message.nickname);
case Network::IdMemberJoin: switch (status_message.type) {
status = NetPlayStatus::MEMBER_JOIN; case Network::IdMemberJoin:
break; status = NetPlayStatus::MEMBER_JOIN;
case Network::IdMemberLeave: break;
status = NetPlayStatus::MEMBER_LEAVE; case Network::IdMemberLeave:
break; status = NetPlayStatus::MEMBER_LEAVE;
case Network::IdMemberKicked: break;
status = NetPlayStatus::MEMBER_KICKED; case Network::IdMemberKicked:
break; status = NetPlayStatus::MEMBER_KICKED;
case Network::IdMemberBanned: break;
status = NetPlayStatus::MEMBER_BANNED; case Network::IdMemberBanned:
break; status = NetPlayStatus::MEMBER_BANNED;
case Network::IdAddressUnbanned: break;
status = NetPlayStatus::ADDRESS_UNBANNED; case Network::IdAddressUnbanned:
break; status = NetPlayStatus::ADDRESS_UNBANNED;
} break;
AddNetPlayMessage(static_cast<int>(status), msg); }
}); AddNetPlayMessage(static_cast<int>(status), msg);
member->BindOnChatMessageReceived([this](const Network::ChatEntry& chat) { });
member->BindOnChatMessageReceived([this](const Network::ChatEntry &chat) {
NetPlayStatus status = NetPlayStatus::CHAT_MESSAGE; NetPlayStatus status = NetPlayStatus::CHAT_MESSAGE;
std::string msg(chat.nickname); std::string msg(chat.nickname);
msg += ": "; msg += ": ";
@ -140,10 +145,14 @@ bool AndroidMultiplayer::NetworkInit() {
return true; return true;
} }
NetPlayStatus AndroidMultiplayer::NetPlayCreateRoom(const std::string& ipaddress, int port,
const std::string& username, const std::string &preferredGameName, NetPlayStatus AndroidMultiplayer::NetPlayCreateRoom(const std::string &ipaddress, int port,
const u64 &preferredGameId, const std::string& password, const std::string &username,
const std::string& room_name, int max_players) { const std::string &preferredGameName,
const u64 &preferredGameId,
const std::string &password,
const std::string &room_name, int max_players,
bool isPublic) {
auto member = Network::GetRoomMember().lock(); auto member = Network::GetRoomMember().lock();
if (!member) { if (!member) {
return NetPlayStatus::NETWORK_ERROR; return NetPlayStatus::NETWORK_ERROR;
@ -168,17 +177,55 @@ NetPlayStatus AndroidMultiplayer::NetPlayCreateRoom(const std::string& ipaddress
.id = preferredGameId, .id = preferredGameId,
}; };
port = (port == 0) ? Network::DefaultRoomPort : static_cast<u16>(port); port = (port == 0) ? Network::DefaultRoomPort : static_cast<u16>(port);
if (!room->Create(room_name, "", ipaddress, static_cast<u16>(port), password, if (!room->Create(room_name, "", ipaddress, static_cast<u16>(port), password,
static_cast<u32>(std::min(max_players, 16)), username, game, std::make_unique<Network::VerifyUser::NullBackend>(), {})) { static_cast<u32>(std::min(max_players, 16)), username, game,
CreateVerifyBackend(isPublic), {})) {
return NetPlayStatus::CREATE_ROOM_ERROR; return NetPlayStatus::CREATE_ROOM_ERROR;
} }
// public announce session
if (isPublic) {
if (auto session = announce_multiplayer_session.lock()) {
WebService::WebResult result = session->Register();
if (result.result_code != WebService::WebResult::Code::Success) {
LOG_ERROR(WebService, "Failed to announce public room lobby");
room->Destroy();
return NetPlayStatus::CREATE_ROOM_ERROR;
}
session->Start();
} else {
LOG_ERROR(Network, "Failed to start announce session");
}
}
// Failsafe timer to avoid joining before creation // Failsafe timer to avoid joining before creation
std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::this_thread::sleep_for(std::chrono::milliseconds(100));
member->Join(username, ipaddress.c_str(), static_cast<u16>(port), 0, Network::NoPreferredIP, password, ""); std::string token;
// TODO(alekpop): properly handle the compile definition, it's not working right
//#ifdef ENABLE_WEB_SERVICE
// LOG_INFO(WebService, "Web Service enabled");
if (isPublic) {
WebService::Client client(Settings::values.web_api_url.GetValue(),
Settings::values.eden_username.GetValue(),
Settings::values.eden_token.GetValue());
token = client.GetExternalJWT(room->GetVerifyUID()).returned_data;
if (token.empty()) {
LOG_ERROR(WebService, "Could not get external JWT, verification may fail");
} else {
LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size());
}
}
//#else
// LOG_INFO(WebService, "Web Service disabled");
//#endif
member->Join(username, ipaddress.c_str(), static_cast<u16>(port), 0, Network::NoPreferredIP,
password, token);
// Failsafe timer to avoid joining before creation // Failsafe timer to avoid joining before creation
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
@ -194,8 +241,9 @@ NetPlayStatus AndroidMultiplayer::NetPlayCreateRoom(const std::string& ipaddress
return NetPlayStatus::CREATE_ROOM_ERROR; return NetPlayStatus::CREATE_ROOM_ERROR;
} }
NetPlayStatus AndroidMultiplayer::NetPlayJoinRoom(const std::string& ipaddress, int port, NetPlayStatus AndroidMultiplayer::NetPlayJoinRoom(const std::string &ipaddress, int port,
const std::string& username, const std::string& password) { const std::string &username,
const std::string &password) {
auto member = Network::GetRoomMember().lock(); auto member = Network::GetRoomMember().lock();
if (!member) { if (!member) {
return NetPlayStatus::NETWORK_ERROR; return NetPlayStatus::NETWORK_ERROR;
@ -209,7 +257,8 @@ NetPlayStatus AndroidMultiplayer::NetPlayJoinRoom(const std::string& ipaddress,
return NetPlayStatus::ALREADY_IN_ROOM; return NetPlayStatus::ALREADY_IN_ROOM;
} }
member->Join(username, ipaddress.c_str(), static_cast<u16>(port), 0, Network::NoPreferredIP, password, ""); member->Join(username, ipaddress.c_str(), static_cast<u16>(port), 0, Network::NoPreferredIP,
password, "");
// Wait a bit for the connection and join process to complete // Wait a bit for the connection and join process to complete
std::this_thread::sleep_for(std::chrono::milliseconds(500)); std::this_thread::sleep_for(std::chrono::milliseconds(500));
@ -226,7 +275,7 @@ NetPlayStatus AndroidMultiplayer::NetPlayJoinRoom(const std::string& ipaddress,
return NetPlayStatus::WRONG_PASSWORD; return NetPlayStatus::WRONG_PASSWORD;
} }
void AndroidMultiplayer::NetPlaySendMessage(const std::string& msg) { void AndroidMultiplayer::NetPlaySendMessage(const std::string &msg) {
if (auto room = Network::GetRoomMember().lock()) { if (auto room = Network::GetRoomMember().lock()) {
if (room->GetState() != Network::RoomMember::State::Joined && if (room->GetState() != Network::RoomMember::State::Joined &&
room->GetState() != Network::RoomMember::State::Moderator) { room->GetState() != Network::RoomMember::State::Moderator) {
@ -237,11 +286,11 @@ void AndroidMultiplayer::NetPlaySendMessage(const std::string& msg) {
} }
} }
void AndroidMultiplayer::NetPlayKickUser(const std::string& username) { void AndroidMultiplayer::NetPlayKickUser(const std::string &username) {
if (auto room = Network::GetRoomMember().lock()) { if (auto room = Network::GetRoomMember().lock()) {
auto members = room->GetMemberInformation(); auto members = room->GetMemberInformation();
auto it = std::find_if(members.begin(), members.end(), auto it = std::find_if(members.begin(), members.end(),
[&username](const Network::RoomMember::MemberInformation& member) { [&username](const Network::RoomMember::MemberInformation &member) {
return member.nickname == username; return member.nickname == username;
}); });
if (it != members.end()) { if (it != members.end()) {
@ -250,11 +299,11 @@ void AndroidMultiplayer::NetPlayKickUser(const std::string& username) {
} }
} }
void AndroidMultiplayer::NetPlayBanUser(const std::string& username) { void AndroidMultiplayer::NetPlayBanUser(const std::string &username) {
if (auto room = Network::GetRoomMember().lock()) { if (auto room = Network::GetRoomMember().lock()) {
auto members = room->GetMemberInformation(); auto members = room->GetMemberInformation();
auto it = std::find_if(members.begin(), members.end(), auto it = std::find_if(members.begin(), members.end(),
[&username](const Network::RoomMember::MemberInformation& member) { [&username](const Network::RoomMember::MemberInformation &member) {
return member.nickname == username; return member.nickname == username;
}); });
if (it != members.end()) { if (it != members.end()) {
@ -263,7 +312,7 @@ void AndroidMultiplayer::NetPlayBanUser(const std::string& username) {
} }
} }
void AndroidMultiplayer::NetPlayUnbanUser(const std::string& username) { void AndroidMultiplayer::NetPlayUnbanUser(const std::string &username) {
if (auto room = Network::GetRoomMember().lock()) { if (auto room = Network::GetRoomMember().lock()) {
room->SendModerationRequest(Network::RoomMessageTypes::IdModUnban, username); room->SendModerationRequest(Network::RoomMessageTypes::IdModUnban, username);
} }
@ -278,7 +327,7 @@ std::vector<std::string> AndroidMultiplayer::NetPlayRoomInfo() {
auto room_info = room->GetRoomInformation(); auto room_info = room->GetRoomInformation();
info_list.push_back(room_info.name + "|" + std::to_string(room_info.member_slots)); info_list.push_back(room_info.name + "|" + std::to_string(room_info.member_slots));
// all members // all members
for (const auto& member : members) { for (const auto &member: members) {
info_list.push_back(member.nickname); info_list.push_back(member.nickname);
} }
} }
@ -368,14 +417,29 @@ std::vector<std::string> AndroidMultiplayer::NetPlayGetBanList() {
auto [username_bans, ip_bans] = room->GetBanList(); auto [username_bans, ip_bans] = room->GetBanList();
// Add username bans // Add username bans
for (const auto& username : username_bans) { for (const auto &username: username_bans) {
ban_list.push_back(username); ban_list.push_back(username);
} }
// Add IP bans // Add IP bans
for (const auto& ip : ip_bans) { for (const auto &ip: ip_bans) {
ban_list.push_back(ip); ban_list.push_back(ip);
} }
} }
return ban_list; return ban_list;
} }
std::unique_ptr<Network::VerifyUser::Backend> AndroidMultiplayer::CreateVerifyBackend(bool use_validation) {
std::unique_ptr<Network::VerifyUser::Backend> verify_backend;
if (use_validation) {
//#ifdef ENABLE_WEB_SERVICE
verify_backend =
std::make_unique<WebService::VerifyUserJWT>(Settings::values.web_api_url.GetValue());
//#else
// verify_backend = std::make_unique<Network::VerifyUser::NullBackend>();
//#endif
} else {
verify_backend = std::make_unique<Network::VerifyUser::NullBackend>();
}
return verify_backend;
}

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#pragma once #pragma once
@ -66,7 +66,7 @@ public:
NetPlayStatus NetPlayCreateRoom(const std::string &ipaddress, int port, NetPlayStatus NetPlayCreateRoom(const std::string &ipaddress, int port,
const std::string &username, const std::string &preferredGameName, const std::string &username, const std::string &preferredGameName,
const u64 &preferredGameId, const std::string &password, const u64 &preferredGameId, const std::string &password,
const std::string &room_name, int max_players); const std::string &room_name, int max_players, bool isPublic);
NetPlayStatus NetPlayJoinRoom(const std::string &ipaddress, int port, NetPlayStatus NetPlayJoinRoom(const std::string &ipaddress, int port,
const std::string &username, const std::string &password); const std::string &username, const std::string &password);

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -118,7 +121,7 @@ void LogSettings() {
LOG_INFO(Config, "yuzu Configuration:"); LOG_INFO(Config, "yuzu Configuration:");
for (auto& [category, settings] : values.linkage.by_category) { for (auto& [category, settings] : values.linkage.by_category) {
for (const auto& setting : settings) { for (const auto& setting : settings) {
if (setting->Id() == values.yuzu_token.Id()) { if (setting->Id() == values.eden_token.Id()) {
// Hide the token secret, for security reasons. // Hide the token secret, for security reasons.
continue; continue;
} }

View file

@ -527,7 +527,7 @@ struct Values {
SwitchableSetting<Language, true> language_index{linkage, SwitchableSetting<Language, true> language_index{linkage,
Language::EnglishAmerican, Language::EnglishAmerican,
Language::Japanese, Language::Japanese,
Language::PortugueseBrazilian, Language::Serbian,
"language_index", "language_index",
Category::System}; Category::System};
SwitchableSetting<Region, true> region_index{linkage, Region::Usa, Region::Japan, SwitchableSetting<Region, true> region_index{linkage, Region::Usa, Region::Japan,
@ -700,9 +700,9 @@ struct Values {
// WebService // WebService
Setting<std::string> web_api_url{linkage, "api.ynet-fun.xyz", "web_api_url", Setting<std::string> web_api_url{linkage, "api.ynet-fun.xyz", "web_api_url",
Category::WebService}; Category::WebService};
Setting<std::string> yuzu_username{linkage, std::string(), "yuzu_username", Setting<std::string> eden_username{linkage, std::string(), "eden_username",
Category::WebService}; Category::WebService};
Setting<std::string> yuzu_token{linkage, std::string(), "yuzu_token", Category::WebService}; Setting<std::string> eden_token{linkage, std::string(), "eden_token", Category::WebService};
// Add-Ons // Add-Ons
std::map<u64, std::vector<std::string>> disabled_addons; std::map<u64, std::vector<std::string>> disabled_addons;

View file

@ -111,7 +111,7 @@ ENUM(AudioMode, Mono, Stereo, Surround);
ENUM(Language, Japanese, EnglishAmerican, French, German, Italian, Spanish, Chinese, Korean, Dutch, ENUM(Language, Japanese, EnglishAmerican, French, German, Italian, Spanish, Chinese, Korean, Dutch,
Portuguese, Russian, Taiwanese, EnglishBritish, FrenchCanadian, SpanishLatin, Portuguese, Russian, Taiwanese, EnglishBritish, FrenchCanadian, SpanishLatin,
ChineseSimplified, ChineseTraditional, PortugueseBrazilian); ChineseSimplified, ChineseTraditional, PortugueseBrazilian, Serbian);
ENUM(Region, Japan, Usa, Europe, Australia, China, Korea, Taiwan); ENUM(Region, Japan, Usa, Europe, Australia, China, Korea, Taiwan);

View file

@ -345,14 +345,14 @@ void LaunchRoom(int argc, char** argv, bool called_by_option)
LOG_INFO(Network, "Hosting a public room"); LOG_INFO(Network, "Hosting a public room");
Settings::values.web_api_url = web_api_url; Settings::values.web_api_url = web_api_url;
PadToken(token); PadToken(token);
Settings::values.yuzu_username = UsernameFromDisplayToken(token); Settings::values.eden_username = UsernameFromDisplayToken(token);
username = Settings::values.yuzu_username.GetValue(); username = Settings::values.eden_username.GetValue();
Settings::values.yuzu_token = TokenFromDisplayToken(token); Settings::values.eden_token = TokenFromDisplayToken(token);
} else { } else {
LOG_INFO(Network, "Hosting a public room"); LOG_INFO(Network, "Hosting a public room");
Settings::values.web_api_url = web_api_url; Settings::values.web_api_url = web_api_url;
Settings::values.yuzu_username = username; Settings::values.eden_username = username;
Settings::values.yuzu_token = token; Settings::values.eden_token = token;
} }
} }

View file

@ -1,9 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono> #include <chrono>
#include <future> #include <future>
#include <vector> #include <vector>
@ -13,9 +13,9 @@
#include "common/settings.h" #include "common/settings.h"
#include "network/network.h" #include "network/network.h"
#ifdef ENABLE_WEB_SERVICE //#ifdef ENABLE_WEB_SERVICE
#include "web_service/announce_room_json.h" #include "web_service/announce_room_json.h"
#endif //#endif
namespace Core { namespace Core {
@ -23,13 +23,13 @@ namespace Core {
static constexpr std::chrono::seconds announce_time_interval(15); static constexpr std::chrono::seconds announce_time_interval(15);
AnnounceMultiplayerSession::AnnounceMultiplayerSession() { AnnounceMultiplayerSession::AnnounceMultiplayerSession() {
#ifdef ENABLE_WEB_SERVICE //#ifdef ENABLE_WEB_SERVICE
backend = std::make_unique<WebService::RoomJson>(Settings::values.web_api_url.GetValue(), backend = std::make_unique<WebService::RoomJson>(Settings::values.web_api_url.GetValue(),
Settings::values.yuzu_username.GetValue(), Settings::values.eden_username.GetValue(),
Settings::values.yuzu_token.GetValue()); Settings::values.eden_token.GetValue());
#else //#else
backend = std::make_unique<AnnounceMultiplayerRoom::NullBackend>(); // backend = std::make_unique<AnnounceMultiplayerRoom::NullBackend>();
#endif //#endif
} }
WebService::WebResult AnnounceMultiplayerSession::Register() { WebService::WebResult AnnounceMultiplayerSession::Register() {
@ -156,11 +156,11 @@ bool AnnounceMultiplayerSession::IsRunning() const {
void AnnounceMultiplayerSession::UpdateCredentials() { void AnnounceMultiplayerSession::UpdateCredentials() {
ASSERT_MSG(!IsRunning(), "Credentials can only be updated when session is not running"); ASSERT_MSG(!IsRunning(), "Credentials can only be updated when session is not running");
#ifdef ENABLE_WEB_SERVICE //#ifdef ENABLE_WEB_SERVICE
backend = std::make_unique<WebService::RoomJson>(Settings::values.web_api_url.GetValue(), backend = std::make_unique<WebService::RoomJson>(Settings::values.web_api_url.GetValue(),
Settings::values.yuzu_username.GetValue(), Settings::values.eden_username.GetValue(),
Settings::values.yuzu_token.GetValue()); Settings::values.eden_token.GetValue());
#endif //#endif
} }
} // namespace Core } // namespace Core

View file

@ -1,5 +1,3 @@
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later

View file

@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later

View file

@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later

View file

@ -1,44 +1,52 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2017 Citra Emulator Project // SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include "yuzu/configuration/configure_web.h"
#include <QIcon> #include <QIcon>
#include <QMessageBox> #include <QMessageBox>
#if QT_VERSION_MAJOR >= 6
#include <QRegularExpressionValidator>
#else
#include <QRegExpValidator>
#endif
#include <QtConcurrent/QtConcurrentRun> #include <QtConcurrent/QtConcurrentRun>
#include "common/settings.h" #include "common/settings.h"
#include "ui_configure_web.h" #include "ui_configure_web.h"
#include "yuzu/configuration/configure_web.h"
#include "yuzu/uisettings.h" #include "yuzu/uisettings.h"
// static constexpr char token_delimiter{':'};
// static std::string GenerateDisplayToken(const std::string& username, const std::string& token) {
// if (username.empty() || token.empty()) {
// return {};
// }
// const std::string unencoded_display_token{username + token_delimiter + token};
// QByteArray b{unencoded_display_token.c_str()};
// QByteArray b64 = b.toBase64();
// return b64.toStdString();
// }
// static std::string UsernameFromDisplayToken(const std::string& display_token) {
// const std::string unencoded_display_token{
// QByteArray::fromBase64(display_token.c_str()).toStdString()};
// return unencoded_display_token.substr(0, unencoded_display_token.find(token_delimiter));
// }
// static std::string TokenFromDisplayToken(const std::string& display_token) {
// const std::string unencoded_display_token{
// QByteArray::fromBase64(display_token.c_str()).toStdString()};
// return unencoded_display_token.substr(unencoded_display_token.find(token_delimiter) + 1);
// }
ConfigureWeb::ConfigureWeb(QWidget* parent) ConfigureWeb::ConfigureWeb(QWidget* parent)
: QWidget(parent), ui(std::make_unique<Ui::ConfigureWeb>()) { : QWidget(parent)
, ui(std::make_unique<Ui::ConfigureWeb>())
, m_rng{QRandomGenerator::system()}
{
ui->setupUi(this); ui->setupUi(this);
connect(ui->button_verify_login, &QPushButton::clicked, this, &ConfigureWeb::VerifyLogin);
connect(&verify_watcher, &QFutureWatcher<bool>::finished, this, &ConfigureWeb::OnLoginVerified); QString user_regex = QStringLiteral(".{4,20}");
QString token_regex = QStringLiteral("[a-z]{48}");
#if QT_VERSION_MAJOR >= 6
QRegularExpressionValidator *username_validator = new QRegularExpressionValidator(this);
QRegularExpressionValidator *token_validator = new QRegularExpressionValidator(this);
username_validator->setRegularExpression(QRegularExpression(user_regex));
token_validator->setRegularExpression(QRegularExpression(token_regex));
#else
QRegExpValidator *username_validator = new QRegExpValidator(this);
QRegExpValidator *token_validator = new QRegExpValidator(this);
username_validator->setRegExp(QRegExp(user_regex));
token_validator->setRegExp(QRegExp(token_regex));
#endif
ui->edit_username->setValidator(username_validator);
ui->edit_token->setValidator(token_validator);
connect(ui->button_generate, &QPushButton::clicked, this, &ConfigureWeb::GenerateToken);
#ifndef USE_DISCORD_PRESENCE #ifndef USE_DISCORD_PRESENCE
ui->discord_group->setVisible(false); ui->discord_group->setVisible(false);
@ -60,87 +68,61 @@ void ConfigureWeb::changeEvent(QEvent* event) {
void ConfigureWeb::RetranslateUI() { void ConfigureWeb::RetranslateUI() {
ui->retranslateUi(this); ui->retranslateUi(this);
ui->web_signup_link->setText(
tr("<a href='https://profile.yuzu-emu.org/'><span style=\"text-decoration: underline; "
"color:#039be5;\">Sign up</span></a>"));
ui->web_token_info_link->setText(
tr("<a href='https://evilperson1337.notion.site/Hosting-a-Room-Inside-of-Eden-20457c2edaf680108abac6215a79acdb'><span style=\"text-decoration: "
"underline; color:#039be5;\">What is my token?</span></a>"));
} }
void ConfigureWeb::SetConfiguration() { void ConfigureWeb::SetConfiguration() {
ui->web_credentials_disclaimer->setWordWrap(true); connect(ui->edit_username, &QLineEdit::textChanged, this, &ConfigureWeb::VerifyLogin);
connect(ui->edit_token, &QLineEdit::textChanged, this, &ConfigureWeb::VerifyLogin);
ui->web_signup_link->setOpenExternalLinks(true); ui->edit_username->setText(QString::fromStdString(Settings::values.eden_username.GetValue()));
ui->web_token_info_link->setOpenExternalLinks(true); ui->edit_token->setText(QString::fromStdString(Settings::values.eden_token.GetValue()));
ui->edit_username->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); VerifyLogin();
ui->edit_token->setText(QString::fromStdString(Settings::values.yuzu_token.GetValue()));
// ui->edit_token->setText(QString::fromStdString(GenerateDisplayToken(
// Settings::values.yuzu_username.GetValue(), Settings::values.yuzu_token.GetValue())));
// Connect after setting the values, to avoid calling OnLoginChanged now
connect(ui->edit_token, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged);
user_verified = true;
ui->toggle_discordrpc->setChecked(UISettings::values.enable_discord_presence.GetValue()); ui->toggle_discordrpc->setChecked(UISettings::values.enable_discord_presence.GetValue());
} }
void ConfigureWeb::ApplyConfiguration() { void ConfigureWeb::GenerateToken() {
UISettings::values.enable_discord_presence = ui->toggle_discordrpc->isChecked(); constexpr size_t length = 48;
Settings::values.yuzu_username = ui->edit_username->text().toStdString(); QString set = QStringLiteral("abcdefghijklmnopqrstuvwxyz");
Settings::values.yuzu_token = ui->edit_token->text().toStdString(); QString result;
for (size_t i = 0; i < length; ++i) {
size_t idx = m_rng->bounded(set.length());
result.append(set.at(idx));
}
ui->edit_token->setText(result);
} }
void ConfigureWeb::OnLoginChanged() { void ConfigureWeb::ApplyConfiguration() {
if (ui->edit_token->text().isEmpty()) { UISettings::values.enable_discord_presence = ui->toggle_discordrpc->isChecked();
user_verified = true; Settings::values.eden_username = ui->edit_username->text().toStdString();
// Empty = no icon Settings::values.eden_token = ui->edit_token->text().toStdString();
ui->label_token_verified->setPixmap(QPixmap());
ui->label_token_verified->setToolTip(QString());
} else {
user_verified = false;
// Show an info icon if it's been changed, clearer than showing failure
const QPixmap pixmap = QIcon::fromTheme(QStringLiteral("info")).pixmap(16);
ui->label_token_verified->setPixmap(pixmap);
ui->label_token_verified->setToolTip(
tr("Unverified, please click Verify before saving configuration", "Tooltip"));
}
} }
void ConfigureWeb::VerifyLogin() { void ConfigureWeb::VerifyLogin() {
QMessageBox::warning(this, const QPixmap checked = QIcon::fromTheme(QStringLiteral("checked")).pixmap(16);
tr("Warning"), const QPixmap failed = QIcon::fromTheme(QStringLiteral("failed")).pixmap(16);
tr("Verification is currently nonfunctional, instead generate a random "
"48-character string with only lowercase a-z."));
// ui->button_verify_login->setDisabled(true);
// ui->button_verify_login->setText(tr("Verifying..."));
// ui->label_token_verified->setPixmap(QIcon::fromTheme(QStringLiteral("sync")).pixmap(16));
// ui->label_token_verified->setToolTip(tr("Verifying..."));
}
void ConfigureWeb::OnLoginVerified() { const bool username_good = ui->edit_username->hasAcceptableInput();
// ui->button_verify_login->setEnabled(true); const bool token_good = ui->edit_token->hasAcceptableInput();
// ui->button_verify_login->setText(tr("Verify"));
// if (verify_watcher.result()) {
// user_verified = true;
// ui->label_token_verified->setPixmap(QIcon::fromTheme(QStringLiteral("checked")).pixmap(16)); if (username_good) {
// ui->label_token_verified->setToolTip(tr("Verified", "Tooltip")); ui->label_username_verified->setPixmap(checked);
// ui->username->setText( ui->label_username_verified->setToolTip(tr("All Good", "Tooltip"));
// QString::fromStdString(UsernameFromDisplayToken(ui->edit_token->text().toStdString()))); } else {
// } else { ui->label_username_verified->setPixmap(failed);
// ui->label_token_verified->setPixmap(QIcon::fromTheme(QStringLiteral("failed")).pixmap(16)); ui->label_username_verified->setToolTip(tr("Must be between 4-20 characters", "Tooltip"));
// ui->label_token_verified->setToolTip(tr("Verification failed", "Tooltip")); }
// ui->username->setText(tr("Unspecified"));
// QMessageBox::critical(this, tr("Verification failed"), if (token_good) {
// tr("Verification failed. Check that you have entered your token " ui->label_token_verified->setPixmap(checked);
// "correctly, and that your internet connection is working.")); ui->label_token_verified->setToolTip(tr("All Good", "Tooltip"));
// } } else {
ui->label_token_verified->setPixmap(failed);
ui->label_token_verified->setToolTip(tr("Must be 48 characters, and lowercase a-z", "Tooltip"));
}
} }
void ConfigureWeb::SetWebServiceConfigEnabled(bool enabled) { void ConfigureWeb::SetWebServiceConfigEnabled(bool enabled) {

View file

@ -1,11 +1,15 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2017 Citra Emulator Project // SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include <memory> #include <QRandomGenerator>
#include <QFutureWatcher>
#include <QWidget> #include <QWidget>
#include <memory>
namespace Ui { namespace Ui {
class ConfigureWeb; class ConfigureWeb;
@ -25,14 +29,12 @@ private:
void changeEvent(QEvent* event) override; void changeEvent(QEvent* event) override;
void RetranslateUI(); void RetranslateUI();
void OnLoginChanged();
void VerifyLogin();
void OnLoginVerified();
void SetConfiguration(); void SetConfiguration();
bool user_verified = true;
QFutureWatcher<bool> verify_watcher;
std::unique_ptr<Ui::ConfigureWeb> ui; std::unique_ptr<Ui::ConfigureWeb> ui;
QRandomGenerator *m_rng;
private slots:
void GenerateToken();
void VerifyLogin();
}; };

View file

@ -25,42 +25,29 @@
<string>eden Web Service</string> <string>eden Web Service</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayoutYuzuWebService"> <layout class="QVBoxLayout" name="verticalLayoutYuzuWebService">
<item>
<widget class="QLabel" name="web_credentials_disclaimer">
<property name="text">
<string>By providing your username and token, you agree to allow eden to collect additional usage data, which may include user identifying information.</string>
</property>
</widget>
</item>
<item> <item>
<layout class="QGridLayout" name="gridLayoutYuzuUsername"> <layout class="QGridLayout" name="gridLayoutYuzuUsername">
<item row="2" column="3"> <item row="1" column="0">
<widget class="QPushButton" name="button_verify_login"> <widget class="QLabel" name="label_token">
<property name="text">
<string>Token: </string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_username">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="layoutDirection">
<enum>Qt::LayoutDirection::RightToLeft</enum>
</property>
<property name="text"> <property name="text">
<string>Verify</string> <string>Username: </string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="0" column="1" colspan="4">
<widget class="QLabel" name="web_signup_link">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Sign up</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="3">
<widget class="QLineEdit" name="edit_username"> <widget class="QLineEdit" name="edit_username">
<property name="maxLength"> <property name="maxLength">
<number>20</number> <number>20</number>
@ -70,25 +57,27 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="5">
<widget class="QLabel" name="label_token"> <widget class="QLabel" name="label_token_verified">
<property name="text"> <property name="sizePolicy">
<string>Token: </string> <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>24</width>
<height>0</height>
</size>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="4"> <item row="1" column="1" colspan="4">
<widget class="QLabel" name="label_token_verified"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_username">
<property name="text">
<string>Username: </string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="3">
<widget class="QLineEdit" name="edit_token"> <widget class="QLineEdit" name="edit_token">
<property name="inputMethodHints">
<set>Qt::InputMethodHint::ImhLowercaseOnly</set>
</property>
<property name="maxLength"> <property name="maxLength">
<number>80</number> <number>80</number>
</property> </property>
@ -97,14 +86,27 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="0" column="5">
<widget class="QLabel" name="web_token_info_link"> <widget class="QLabel" name="label_username_verified">
<property name="text"> <property name="text">
<string>What is my token?</string> <string/>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="2"> <item row="2" column="4">
<widget class="QPushButton" name="button_generate">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Generate</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="3">
<spacer name="horizontalSpacer"> <spacer name="horizontalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>

View file

@ -602,6 +602,7 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QWidget* parent)
PAIR(Language, ChineseSimplified, tr("Simplified Chinese")), PAIR(Language, ChineseSimplified, tr("Simplified Chinese")),
PAIR(Language, ChineseTraditional, tr("Traditional Chinese (正體中文)")), PAIR(Language, ChineseTraditional, tr("Traditional Chinese (正體中文)")),
PAIR(Language, PortugueseBrazilian, tr("Brazilian Portuguese (português do Brasil)")), PAIR(Language, PortugueseBrazilian, tr("Brazilian Portuguese (português do Brasil)")),
PAIR(Language, Serbian, tr("Serbian (српски)")),
}}); }});
translations->insert({Settings::EnumMetadata<Settings::Region>::Index(), translations->insert({Settings::EnumMetadata<Settings::Region>::Index(),
{ {

View file

@ -3591,8 +3591,8 @@ void GMainWindow::OnMenuReportCompatibility() {
return; return;
} }
if (!Settings::values.yuzu_token.GetValue().empty() && if (!Settings::values.eden_token.GetValue().empty() &&
!Settings::values.yuzu_username.GetValue().empty()) { !Settings::values.eden_username.GetValue().empty()) {
} else { } else {
QMessageBox::critical( QMessageBox::critical(
this, tr("Missing yuzu Account"), this, tr("Missing yuzu Account"),

View file

@ -1,9 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QComboBox> #include <QComboBox>
#include <QFuture> #include <QFuture>
#include <QIntValidator> #include <QIntValidator>
@ -38,9 +38,9 @@ DirectConnectWindow::DirectConnectWindow(Core::System& system_, QWidget* parent)
ui->nickname->setValidator(validation.GetNickname()); ui->nickname->setValidator(validation.GetNickname());
ui->nickname->setText( ui->nickname->setText(
QString::fromStdString(UISettings::values.multiplayer_nickname.GetValue())); QString::fromStdString(UISettings::values.multiplayer_nickname.GetValue()));
if (ui->nickname->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { if (ui->nickname->text().isEmpty() && !Settings::values.eden_username.GetValue().empty()) {
// Use yuzu Web Service user name as nickname by default // Use yuzu Web Service user name as nickname by default
ui->nickname->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); ui->nickname->setText(QString::fromStdString(Settings::values.eden_username.GetValue()));
} }
ui->ip->setValidator(validation.GetIP()); ui->ip->setValidator(validation.GetIP());
ui->ip->setText(QString::fromStdString(UISettings::values.multiplayer_ip.GetValue())); ui->ip->setText(QString::fromStdString(UISettings::values.multiplayer_ip.GetValue()));

View file

@ -1,9 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <future> #include <future>
#include <QColor> #include <QColor>
#include <QImage> #include <QImage>
@ -59,9 +59,9 @@ HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list,
// Restore the settings: // Restore the settings:
ui->username->setText( ui->username->setText(
QString::fromStdString(UISettings::values.multiplayer_room_nickname.GetValue())); QString::fromStdString(UISettings::values.multiplayer_room_nickname.GetValue()));
if (ui->username->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { if (ui->username->text().isEmpty() && !Settings::values.eden_username.GetValue().empty()) {
// Use eden Web Service user name as nickname by default // Use eden Web Service user name as nickname by default
ui->username->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); ui->username->setText(QString::fromStdString(Settings::values.eden_username.GetValue()));
} }
ui->room_name->setText( ui->room_name->setText(
QString::fromStdString(UISettings::values.multiplayer_room_name.GetValue())); QString::fromStdString(UISettings::values.multiplayer_room_name.GetValue()));
@ -167,7 +167,7 @@ void HostRoomWindow::Host() {
const bool created = const bool created =
room->Create(ui->room_name->text().toStdString(), room->Create(ui->room_name->text().toStdString(),
ui->room_description->toPlainText().toStdString(), "", port, password, ui->room_description->toPlainText().toStdString(), "", port, password,
ui->max_player->value(), Settings::values.yuzu_username.GetValue(), ui->max_player->value(), Settings::values.eden_username.GetValue(),
game, CreateVerifyBackend(is_public), ban_list); game, CreateVerifyBackend(is_public), ban_list);
if (!created) { if (!created) {
NetworkMessage::ErrorManager::ShowError( NetworkMessage::ErrorManager::ShowError(
@ -206,8 +206,8 @@ void HostRoomWindow::Host() {
#ifdef ENABLE_WEB_SERVICE #ifdef ENABLE_WEB_SERVICE
if (is_public) { if (is_public) {
WebService::Client client(Settings::values.web_api_url.GetValue(), WebService::Client client(Settings::values.web_api_url.GetValue(),
Settings::values.yuzu_username.GetValue(), Settings::values.eden_username.GetValue(),
Settings::values.yuzu_token.GetValue()); Settings::values.eden_token.GetValue());
if (auto room = Network::GetRoom().lock()) { if (auto room = Network::GetRoom().lock()) {
token = client.GetExternalJWT(room->GetVerifyUID()).returned_data; token = client.GetExternalJWT(room->GetVerifyUID()).returned_data;
} }

View file

@ -1,9 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QInputDialog> #include <QInputDialog>
#include <QList> #include <QList>
#include <QtConcurrent/QtConcurrentRun> #include <QtConcurrent/QtConcurrentRun>
@ -67,9 +67,9 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
// Try find the best nickname by default // Try find the best nickname by default
if (ui->nickname->text().isEmpty() || ui->nickname->text() == QStringLiteral("eden")) { if (ui->nickname->text().isEmpty() || ui->nickname->text() == QStringLiteral("eden")) {
if (!Settings::values.yuzu_username.GetValue().empty()) { if (!Settings::values.eden_username.GetValue().empty()) {
ui->nickname->setText( ui->nickname->setText(
QString::fromStdString(Settings::values.yuzu_username.GetValue())); QString::fromStdString(Settings::values.eden_username.GetValue()));
} else if (!GetProfileUsername().empty()) { } else if (!GetProfileUsername().empty()) {
ui->nickname->setText(QString::fromStdString(GetProfileUsername())); ui->nickname->setText(QString::fromStdString(GetProfileUsername()));
} else { } else {
@ -189,11 +189,11 @@ void Lobby::OnJoinRoom(const QModelIndex& source) {
QFuture<void> f = QtConcurrent::run([nickname, ip, port, password, verify_uid] { QFuture<void> f = QtConcurrent::run([nickname, ip, port, password, verify_uid] {
std::string token; std::string token;
#ifdef ENABLE_WEB_SERVICE #ifdef ENABLE_WEB_SERVICE
if (!Settings::values.yuzu_username.GetValue().empty() && if (!Settings::values.eden_username.GetValue().empty() &&
!Settings::values.yuzu_token.GetValue().empty()) { !Settings::values.eden_token.GetValue().empty()) {
WebService::Client client(Settings::values.web_api_url.GetValue(), WebService::Client client(Settings::values.web_api_url.GetValue(),
Settings::values.yuzu_username.GetValue(), Settings::values.eden_username.GetValue(),
Settings::values.yuzu_token.GetValue()); Settings::values.eden_token.GetValue());
token = client.GetExternalJWT(verify_uid).returned_data; token = client.GetExternalJWT(verify_uid).returned_data;
if (token.empty()) { if (token.empty()) {
LOG_ERROR(WebService, "Could not get external JWT, verification may fail"); LOG_ERROR(WebService, "Could not get external JWT, verification may fail");