Compare commits

..

No commits in common. "master" and "v0.5-canary-refresh" have entirely different histories.

143 changed files with 301 additions and 6227 deletions

12
.gitmodules vendored
View File

@ -9,22 +9,22 @@
url = https://github.com/mozilla/cubeb.git
[submodule "dynarmic"]
path = externals/dynarmic
url = https://github.com/yuzu-mirror/dynarmic.git
url = https://git.citron-emu.org/Citron/dynarmic.git
[submodule "libusb"]
path = externals/libusb/libusb
url = https://github.com/libusb/libusb.git
[submodule "discord-rpc"]
path = externals/discord-rpc
url = https://github.com/yuzu-mirror/discord-rpc.git
url = https://git.citron-emu.org/Citron/discord-rpc.git
[submodule "Vulkan-Headers"]
path = externals/Vulkan-Headers
url = https://github.com/KhronosGroup/Vulkan-Headers.git
[submodule "sirit"]
path = externals/sirit
url = https://github.com/yuzu-mirror/sirit.git
url = https://git.citron-emu.org/Citron/sirit.git
[submodule "mbedtls"]
path = externals/mbedtls
url = https://github.com/yuzu-mirror/mbedtls.git
url = https://git.citron-emu.org/Citron/mbedtls.git
[submodule "xbyak"]
path = externals/xbyak
url = https://github.com/herumi/xbyak.git
@ -57,13 +57,13 @@
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
[submodule "breakpad"]
path = externals/breakpad
url = https://github.com/yuzu-mirror/breakpad.git
url = https://git.citron-emu.org/Citron/breakpad.git
[submodule "simpleini"]
path = externals/simpleini
url = https://github.com/brofield/simpleini.git
[submodule "oaknut"]
path = externals/oaknut
url = https://github.com/yuzu-mirror/oaknut
url = https://git.citron-emu.org/Citron/oaknut.git
[submodule "Vulkan-Utility-Libraries"]
path = externals/Vulkan-Utility-Libraries
url = https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git

View File

@ -17,45 +17,6 @@ if (MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3 /WX-")
endif()
# PGO Configuration
option(CITRON_ENABLE_PGO_INSTRUMENT "Enable Profile-Guided Optimization instrumentation build" OFF)
option(CITRON_ENABLE_PGO_OPTIMIZE "Enable Profile-Guided Optimization optimization build" OFF)
if(MSVC)
if(CITRON_ENABLE_PGO_INSTRUMENT)
string(APPEND CMAKE_CXX_FLAGS_RELEASE " /GL /LTCG:PGINSTRUMENT")
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " /LTCG:PGINSTRUMENT")
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " /LTCG:PGINSTRUMENT")
elseif(CITRON_ENABLE_PGO_OPTIMIZE)
string(APPEND CMAKE_CXX_FLAGS_RELEASE " /GL /LTCG:PGOPTIMIZE")
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " /LTCG:PGOPTIMIZE")
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " /LTCG:PGOPTIMIZE")
endif()
else()
# GCC and Clang PGO flags
if(CITRON_ENABLE_PGO_INSTRUMENT)
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -fprofile-instr-generate")
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " -fprofile-instr-generate")
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " -fprofile-instr-generate")
else() # GCC
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -fprofile-generate")
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " -fprofile-generate")
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " -fprofile-generate")
endif()
elseif(CITRON_ENABLE_PGO_OPTIMIZE)
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -fprofile-instr-use=default.profdata")
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " -fprofile-instr-use=default.profdata")
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " -fprofile-instr-use=default.profdata")
else() # GCC
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -fprofile-use")
string(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE " -fprofile-use")
string(APPEND CMAKE_SHARED_LINKER_FLAGS_RELEASE " -fprofile-use")
endif()
endif()
endif()
# Check if SDL2::SDL2 target exists; if not, create an alias
if (TARGET SDL2::SDL2-static)
add_library(SDL2::SDL2 ALIAS SDL2::SDL2-static)
@ -137,25 +98,21 @@ endif()
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
if (ANDROID AND CITRON_DOWNLOAD_ANDROID_VVL)
set(vvl_version "1.4.309.0")
set(vvl_version "1.4.304.1")
set(vvl_zip_file "${CMAKE_BINARY_DIR}/externals/vvl-android.zip")
set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/app/src/main/jniLibs/arm64-v8a/")
set(vvl_final_lib "${vvl_lib_path}/libVkLayer_khronos_validation.so")
if (NOT EXISTS "${vvl_final_lib}")
if (NOT EXISTS "${vvl_zip_file}")
# Download and extract validation layer release to externals directory
if (NOT EXISTS "${vvl_zip_file}")
set(vvl_base_url "https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download")
file(DOWNLOAD "${vvl_base_url}/vulkan-sdk-${vvl_version}/android-binaries-${vvl_version}.zip"
"${vvl_zip_file}" SHOW_PROGRESS)
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${vvl_zip_file}"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals")
endif()
# Copy the arm64 binary to src/android/app/main/jniLibs
file(COPY "${CMAKE_BINARY_DIR}/externals/android-binaries-${vvl_version}/arm64-v8a/libVkLayer_khronos_validation.so"
DESTINATION "${vvl_lib_path}")
set(vvl_base_url "https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download")
file(DOWNLOAD "${vvl_base_url}/vulkan-sdk-${vvl_version}/android-binaries-${vvl_version}.zip"
"${vvl_zip_file}" SHOW_PROGRESS)
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${vvl_zip_file}"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals")
endif()
# Copy the arm64 binary to src/android/app/main/jniLibs
set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/app/src/main/jniLibs/arm64-v8a/")
file(COPY "${CMAKE_BINARY_DIR}/externals/android-binaries-${vvl_version}/arm64-v8a/libVkLayer_khronos_validation.so"
DESTINATION "${vvl_lib_path}")
endif()
if (ANDROID)
@ -169,23 +126,13 @@ if (CITRON_USE_BUNDLED_VCPKG)
if (CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a")
set(VCPKG_TARGET_TRIPLET "arm64-android")
# Detect host system (Windows or Linux)
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
set(VCPKG_HOST_TRIPLET "x64-windows")
else()
set(VCPKG_HOST_TRIPLET "x64-linux")
endif()
set(VCPKG_HOST_TRIPLET "x64-windows")
# this is to avoid CMake using the host pkg-config to find the host
# libraries when building for Android targets
set(PKG_CONFIG_EXECUTABLE "aarch64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64")
set(VCPKG_TARGET_TRIPLET "x64-android")
# Detect host system (Windows or Linux)
if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
set(VCPKG_HOST_TRIPLET "x64-windows")
else()
set(VCPKG_HOST_TRIPLET "x64-linux")
endif()
set(VCPKG_HOST_TRIPLET "x64-windows")
set(PKG_CONFIG_EXECUTABLE "x86_64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
else()
message(FATAL_ERROR "Unsupported Android architecture ${CMAKE_ANDROID_ARCH_ABI}")
@ -384,7 +331,7 @@ find_package(ZLIB REQUIRED)
find_package(zstd REQUIRED)
if (NOT CITRON_USE_EXTERNAL_VULKAN_HEADERS)
find_package(VulkanHeaders 1.4.313 REQUIRED)
find_package(VulkanHeaders 1.4.307 REQUIRED)
endif()
if (NOT CITRON_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES)
@ -447,7 +394,7 @@ if (ENABLE_SDL2)
if (CITRON_USE_BUNDLED_SDL2)
# Detect toolchain and platform
if ((MSVC_VERSION GREATER_EQUAL 1920) AND ARCHITECTURE_x86_64)
set(SDL2_VER "SDL2-2.28.2")
set(SDL2_VER "SDL2-2.32.0")
else()
message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable CITRON_USE_BUNDLED_SDL2 and provide your own.")
endif()
@ -594,7 +541,7 @@ endif()
# against all the src files. This should be used before making a pull request.
# =======================================================================
set(CLANG_FORMAT_POSTFIX "-15")
set(CLANG_FORMAT_POSTFIX "-18")
find_program(CLANG_FORMAT
NAMES clang-format${CLANG_FORMAT_POSTFIX}
clang-format
@ -605,7 +552,7 @@ if (NOT CLANG_FORMAT)
message(STATUS "Clang format not found! Downloading...")
set(CLANG_FORMAT "${PROJECT_BINARY_DIR}/externals/clang-format${CLANG_FORMAT_POSTFIX}.exe")
file(DOWNLOAD
https://github.com/yuzu-mirror/ext-windows-bin/raw/master/clang-format${CLANG_FORMAT_POSTFIX}.exe
https://git.citron-emu.org/Citron/ext-windows-bin/raw/master/clang-format${CLANG_FORMAT_POSTFIX}.exe
"${CLANG_FORMAT}" SHOW_PROGRESS
STATUS DOWNLOAD_SUCCESS)
if (NOT DOWNLOAD_SUCCESS EQUAL 0)

View File

@ -8,7 +8,7 @@
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
function(download_bundled_external remote_path lib_name prefix_var)
set(package_base_url "https://github.com/yuzu-mirror/")
set(package_base_url "https://git.citron-emu.org/Citron/")
set(package_repo "no_platform")
set(package_extension "no_platform")
if (WIN32)

View File

@ -1,5 +1,4 @@
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
# SPDX-FileCopyrightText: 2025 citron Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
# Allow systemd-logind to manage user access to hidraw with this file
@ -8,13 +7,13 @@
# Switch Pro Controller (USB/Bluetooth)
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="2009", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", KERNELS=="*057E:2009*", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", KERNELS=="*057e:2009*", MODE="0660", TAG+="uaccess"
# Joy-Con L (Bluetooth)
KERNEL=="hidraw*", KERNELS=="*057E:2006*", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", KERNELS=="*057e:2006*", MODE="0660", TAG+="uaccess"
# Joy-Con R (Bluetooth)
KERNEL=="hidraw*", KERNELS=="*057E:2007*", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", KERNELS=="*057e:2007*", MODE="0660", TAG+="uaccess"
# Joy-Con Charging Grip (USB)
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="200e", MODE="0660", TAG+="uaccess"

2
externals/SDL vendored

@ -1 +1 @@
Subproject commit 2359383fc187386204c3bb22de89655a494cd128
Subproject commit cc016b0046d563287f0aa9f09b958b5e70d43696

@ -1 +1 @@
Subproject commit e2e53a724677f6eba8ff0ce1ccb64ee321785cbd
Subproject commit 234c4b7370a8ea3239a214c9e871e4b17c89f4ab

@ -1 +1 @@
Subproject commit 4e246c56ec5afb5ad66b9b04374d39ac04675c8e
Subproject commit fe7a09b13899c5c77d956fa310286f7a7eb2c4ed

@ -1 +1 @@
Subproject commit 539c0a8d8e3733c9f25ea9a184c85c77504f1653
Subproject commit c788c52156f3ef7bc7ab769cb03c110a53ac8fcb

@ -1 +1 @@
Subproject commit 99e2af4e7837ca09b97d93a562dc12947179fc48
Subproject commit 9c1294eaddb88cb0e044c675ccae059a85fc9c6c

2
externals/vcpkg vendored

@ -1 +1 @@
Subproject commit 96d5fb3de135b86d7222c53f2352ca92827a156b
Subproject commit 37d46edf0f2024c3d04997a2d432d59278ca1dff

View File

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-FileCopyrightText: 2023 citron Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
import android.annotation.SuppressLint
@ -28,20 +28,20 @@ val autoVersion = (((System.currentTimeMillis() / 1000) - 1451606400) / 10).toIn
android {
namespace = "org.citron.citron_emu"
compileSdkVersion = "android-35"
ndkVersion = "29.0.13113456 rc1" // "26.1.10909125"
compileSdkVersion = "android-34"
ndkVersion = "26.1.10909125"
buildFeatures {
viewBinding = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "21"
jvmTarget = "17"
}
packaging {
@ -57,8 +57,7 @@ android {
// TODO If this is ever modified, change application_id in strings.xml
applicationId = "org.citron.citron_emu"
minSdk = 30
//noinspection EditedTargetSdkVersion
targetSdk = 35
targetSdk = 34
versionName = getGitVersion()
versionCode = if (System.getenv("AUTO_VERSIONED") == "true") {
@ -76,6 +75,15 @@ android {
buildConfigField("String", "BRANCH", "\"${getBranch()}\"")
}
android.applicationVariants.all {
val variant = this
variant.outputs.all {
if (this is com.android.build.gradle.internal.api.ApkVariantOutputImpl) {
outputFileName = "Citron-${variant.versionName}-${variant.name}.apk"
}
}
}
val keystoreFile = System.getenv("ANDROID_KEYSTORE_FILE")
signingConfigs {
if (keystoreFile != null) {
@ -108,11 +116,9 @@ android {
resValue("string", "app_name_suffixed", "Citron")
isDefault = true
isMinifyEnabled = true
isShrinkResources = true
isJniDebuggable = false
isDebuggable = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro"
)
}
@ -124,7 +130,7 @@ android {
signingConfig = signingConfigs.getByName("default")
isDebuggable = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro"
)
versionNameSuffix = "-relWithDebInfo"
@ -161,7 +167,7 @@ android {
externalNativeBuild {
cmake {
version = "4.0.1"
version = "3.22.1"
path = file("../../../CMakeLists.txt")
}
}
@ -179,11 +185,10 @@ android {
"-DCITRON_USE_BUNDLED_FFMPEG=ON",
"-DCITRON_ENABLE_LTO=ON",
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
)
abiFilters("arm64-v8a") // , "x86_64")
abiFilters("arm64-v8a", "x86_64")
}
}
}
@ -241,6 +246,7 @@ dependencies {
implementation("io.coil-kt:coil:2.2.2")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.window:window:1.2.0-beta03")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.4")
implementation("androidx.navigation:navigation-ui-ktx:2.7.4")

View File

@ -14,7 +14,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<application

View File

@ -21,7 +21,6 @@ import org.citron.citron_emu.utils.Log
import org.citron.citron_emu.model.InstallResult
import org.citron.citron_emu.model.Patch
import org.citron.citron_emu.model.GameVerificationResult
import org.citron.citron_emu.network.NetPlayManager
import java.net.NetworkInterface
/**
@ -244,27 +243,6 @@ object NativeLibrary {
return coreErrorAlertResult
}
@Keep
@JvmStatic
fun addNetPlayMessage(type: Int, message: String) {
val emulationActivity = sEmulationActivity.get()
if (emulationActivity != null) {
emulationActivity.addNetPlayMessages(type, message)
}
else {
NetPlayManager.addNetPlayMessage(type, message)
}
}
@Keep
@JvmStatic
fun clearChat() {
NetPlayManager.clearChat()
}
external fun netPlayInit()
@Keep
@JvmStatic
fun exitEmulationActivity(resultCode: Int) {

View File

@ -4,7 +4,6 @@
package org.citron.citron_emu.activities
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.PendingIntent
import android.app.PictureInPictureParams
import android.app.RemoteAction
@ -40,14 +39,12 @@ import org.citron.citron_emu.NativeLibrary
import org.citron.citron_emu.R
import org.citron.citron_emu.CitronApplication
import org.citron.citron_emu.databinding.ActivityEmulationBinding
import org.citron.citron_emu.dialogs.NetPlayDialog
import org.citron.citron_emu.features.input.NativeInput
import org.citron.citron_emu.features.settings.model.BooleanSetting
import org.citron.citron_emu.features.settings.model.IntSetting
import org.citron.citron_emu.features.settings.model.Settings
import org.citron.citron_emu.model.EmulationViewModel
import org.citron.citron_emu.model.Game
import org.citron.citron_emu.network.NetPlayManager
import org.citron.citron_emu.utils.InputHandler
import org.citron.citron_emu.utils.Log
import org.citron.citron_emu.utils.MemoryUtil
@ -83,19 +80,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
super.onCreate(savedInstanceState)
// Check if firmware is available
if (!NativeLibrary.isFirmwareAvailable()) {
AlertDialog.Builder(this)
.setTitle(R.string.firmware_missing_title)
.setMessage(R.string.firmware_missing_message)
.setPositiveButton(R.string.ok) { _, _ ->
finish()
}
.setCancelable(false)
.show()
return
}
// Add license verification at the start
LicenseVerifier.verifyLicense(this)
@ -425,16 +409,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
setPictureInPictureParams(pictureInPictureParamsBuilder.build())
}
fun displayMultiplayerDialog() {
val dialog = NetPlayDialog(this)
dialog.show()
}
fun addNetPlayMessages(type: Int, msg: String) {
NetPlayManager.addNetPlayMessage(type, msg)
}
private var pictureInPictureReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
if (intent.action == actionPlay) {

View File

@ -1,133 +0,0 @@
package org.citron.citron_emu.dialogs
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import org.citron.citron_emu.R
import org.citron.citron_emu.databinding.DialogChatBinding
import org.citron.citron_emu.databinding.ItemChatMessageBinding
import org.citron.citron_emu.network.NetPlayManager
import java.text.SimpleDateFormat
import java.util.*
class ChatMessage(
val nickname: String, // This is the common name youll see on private servers
val username: String, // Username is the community/forum username
val message: String,
val timestamp: String = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date())
) {
}
class ChatDialog(context: Context) : BottomSheetDialog(context) {
private lateinit var binding: DialogChatBinding
private lateinit var chatAdapter: ChatAdapter
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DialogChatBinding.inflate(LayoutInflater.from(context))
setContentView(binding.root)
NetPlayManager.setChatOpen(true)
setupRecyclerView()
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.skipCollapsed = context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
handler.post {
chatAdapter.notifyDataSetChanged()
binding.chatRecyclerView.post {
scrollToBottom()
}
}
NetPlayManager.setOnMessageReceivedListener { type, message ->
handler.post {
chatAdapter.notifyDataSetChanged()
scrollToBottom()
}
}
binding.sendButton.setOnClickListener {
val message = binding.chatInput.text.toString()
if (message.isNotBlank()) {
sendMessage(message)
binding.chatInput.text?.clear()
}
}
}
override fun dismiss() {
NetPlayManager.setChatOpen(false)
super.dismiss()
}
private fun sendMessage(message: String) {
val username = NetPlayManager.getUsername(context)
NetPlayManager.netPlaySendMessage(message)
val chatMessage = ChatMessage(
nickname = username,
username = "",
message = message,
timestamp = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date())
)
NetPlayManager.addChatMessage(chatMessage)
chatAdapter.notifyDataSetChanged()
scrollToBottom()
}
private fun setupRecyclerView() {
chatAdapter = ChatAdapter(NetPlayManager.getChatMessages())
binding.chatRecyclerView.layoutManager = LinearLayoutManager(context).apply {
stackFromEnd = true
}
binding.chatRecyclerView.adapter = chatAdapter
}
private fun scrollToBottom() {
binding.chatRecyclerView.scrollToPosition(chatAdapter.itemCount - 1)
}
}
class ChatAdapter(private val messages: List<ChatMessage>) :
RecyclerView.Adapter<ChatAdapter.ChatViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatViewHolder {
val binding = ItemChatMessageBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ChatViewHolder(binding)
}
override fun getItemCount(): Int = messages.size
override fun onBindViewHolder(holder: ChatViewHolder, position: Int) {
holder.bind(messages[position])
}
inner class ChatViewHolder(private val binding: ItemChatMessageBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(message: ChatMessage) {
binding.usernameText.text = message.nickname
binding.messageText.text = message.message
binding.userIcon.setImageResource(when (message.nickname) {
"System" -> R.drawable.ic_system
else -> R.drawable.ic_user
})
}
}
}

View File

@ -1,397 +0,0 @@
// Copyright 2024 Mandarine Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citron.citron_emu.dialogs
import android.content.Context
import org.citron.citron_emu.R
import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.citron.citron_emu.CitronApplication
import org.citron.citron_emu.databinding.DialogMultiplayerConnectBinding
import org.citron.citron_emu.databinding.DialogMultiplayerLobbyBinding
import org.citron.citron_emu.databinding.DialogMultiplayerRoomBinding
import org.citron.citron_emu.databinding.ItemBanListBinding
import org.citron.citron_emu.databinding.ItemButtonNetplayBinding
import org.citron.citron_emu.databinding.ItemTextNetplayBinding
import org.citron.citron_emu.utils.CompatUtils
import org.citron.citron_emu.network.NetPlayManager
class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
private lateinit var adapter: NetPlayAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.skipCollapsed = context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
when {
NetPlayManager.netPlayIsJoined() -> DialogMultiplayerLobbyBinding.inflate(layoutInflater)
.apply {
setContentView(root)
adapter = NetPlayAdapter()
listMultiplayer.layoutManager = LinearLayoutManager(context)
listMultiplayer.adapter = adapter
adapter.loadMultiplayerMenu()
btnLeave.setOnClickListener {
NetPlayManager.netPlayLeaveRoom()
dismiss()
}
btnChat.setOnClickListener {
ChatDialog(context).show()
}
refreshAdapterItems()
btnModeration.visibility = if (NetPlayManager.netPlayIsModerator()) View.VISIBLE else View.GONE
btnModeration.setOnClickListener {
showModerationDialog()
}
}
else -> {
DialogMultiplayerConnectBinding.inflate(layoutInflater).apply {
setContentView(root)
btnCreate.setOnClickListener {
showNetPlayInputDialog(true)
dismiss()
}
btnJoin.setOnClickListener {
showNetPlayInputDialog(false)
dismiss()
}
}
}
}
}
data class NetPlayItems(
val option: Int,
val name: String,
val type: Int,
val id: Int = 0
) {
companion object {
const val MULTIPLAYER_ROOM_TEXT = 1
const val MULTIPLAYER_ROOM_MEMBER = 2
const val MULTIPLAYER_SEPARATOR = 3
const val MULTIPLAYER_ROOM_COUNT = 4
const val TYPE_BUTTON = 0
const val TYPE_TEXT = 1
const val TYPE_SEPARATOR = 2
}
}
inner class NetPlayAdapter : RecyclerView.Adapter<NetPlayAdapter.NetPlayViewHolder>() {
val netPlayItems = mutableListOf<NetPlayItems>()
abstract inner class NetPlayViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
init {
itemView.setOnClickListener(this)
}
abstract fun bind(item: NetPlayItems)
}
inner class TextViewHolder(private val binding: ItemTextNetplayBinding) : NetPlayViewHolder(binding.root) {
private lateinit var netPlayItem: NetPlayItems
override fun onClick(clicked: View) {}
override fun bind(item: NetPlayItems) {
netPlayItem = item
binding.itemTextNetplayName.text = item.name
binding.itemIcon.apply {
val iconRes = when (item.option) {
NetPlayItems.MULTIPLAYER_ROOM_TEXT -> R.drawable.ic_system
NetPlayItems.MULTIPLAYER_ROOM_COUNT -> R.drawable.ic_joined
else -> 0
}
visibility = if (iconRes != 0) {
setImageResource(iconRes)
View.VISIBLE
} else View.GONE
}
}
}
inner class ButtonViewHolder(private val binding: ItemButtonNetplayBinding) : NetPlayViewHolder(binding.root) {
private lateinit var netPlayItems: NetPlayItems
private val isModerator = NetPlayManager.netPlayIsModerator()
init {
binding.itemButtonMore.apply {
visibility = View.VISIBLE
setOnClickListener { showPopupMenu(it) }
}
}
override fun onClick(clicked: View) {}
private fun showPopupMenu(view: View) {
PopupMenu(view.context, view).apply {
menuInflater.inflate(R.menu.menu_netplay_member, menu)
menu.findItem(R.id.action_kick).isEnabled = isModerator &&
netPlayItems.name != NetPlayManager.getUsername(context)
menu.findItem(R.id.action_ban).isEnabled = isModerator &&
netPlayItems.name != NetPlayManager.getUsername(context)
setOnMenuItemClickListener { item ->
if (item.itemId == R.id.action_kick) {
NetPlayManager.netPlayKickUser(netPlayItems.name)
true
} else if (item.itemId == R.id.action_ban) {
NetPlayManager.netPlayBanUser(netPlayItems.name)
true
} else false
}
show()
}
}
override fun bind(item: NetPlayItems) {
netPlayItems = item
binding.itemButtonNetplayName.text = netPlayItems.name
}
}
fun loadMultiplayerMenu() {
val infos = NetPlayManager.netPlayRoomInfo()
if (infos.isNotEmpty()) {
val roomInfo = infos[0].split("|")
netPlayItems.add(NetPlayItems(NetPlayItems.MULTIPLAYER_ROOM_TEXT, roomInfo[0], NetPlayItems.TYPE_TEXT))
netPlayItems.add(NetPlayItems(NetPlayItems.MULTIPLAYER_ROOM_COUNT, "${infos.size - 1}/${roomInfo[1]}", NetPlayItems.TYPE_TEXT))
netPlayItems.add(NetPlayItems(NetPlayItems.MULTIPLAYER_SEPARATOR, "", NetPlayItems.TYPE_SEPARATOR))
for (i in 1 until infos.size) {
netPlayItems.add(NetPlayItems(NetPlayItems.MULTIPLAYER_ROOM_MEMBER, infos[i], NetPlayItems.TYPE_BUTTON))
}
}
}
override fun getItemViewType(position: Int) = netPlayItems[position].type
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NetPlayViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
NetPlayItems.TYPE_TEXT -> TextViewHolder(ItemTextNetplayBinding.inflate(inflater, parent, false))
NetPlayItems.TYPE_BUTTON -> ButtonViewHolder(ItemButtonNetplayBinding.inflate(inflater, parent, false))
NetPlayItems.TYPE_SEPARATOR -> object : NetPlayViewHolder(inflater.inflate(R.layout.item_separator_netplay, parent, false)) {
override fun bind(item: NetPlayItems) {}
override fun onClick(clicked: View) {}
}
else -> throw IllegalStateException("Unsupported view type")
}
}
override fun onBindViewHolder(holder: NetPlayViewHolder, position: Int) {
holder.bind(netPlayItems[position])
}
override fun getItemCount() = netPlayItems.size
}
fun refreshAdapterItems() {
val handler = Handler(Looper.getMainLooper())
NetPlayManager.setOnAdapterRefreshListener() { type, msg ->
handler.post {
adapter.netPlayItems.clear()
adapter.loadMultiplayerMenu()
adapter.notifyDataSetChanged()
}
}
}
private fun showNetPlayInputDialog(isCreateRoom: Boolean) {
val activity = CompatUtils.findActivity(context)
val dialog = BottomSheetDialog(activity)
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
dialog.behavior.skipCollapsed = context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
val binding = DialogMultiplayerRoomBinding.inflate(LayoutInflater.from(activity))
dialog.setContentView(binding.root)
binding.textTitle.text = activity.getString(
if (isCreateRoom) R.string.multiplayer_create_room
else R.string.multiplayer_join_room
)
binding.ipAddress.setText(
if (isCreateRoom) NetPlayManager.getIpAddressByWifi(activity)
else NetPlayManager.getRoomAddress(activity)
)
binding.ipPort.setText(NetPlayManager.getRoomPort(activity))
binding.username.setText(NetPlayManager.getUsername(activity))
binding.roomName.visibility = if (isCreateRoom) View.VISIBLE else View.GONE
binding.maxPlayersContainer.visibility = if (isCreateRoom) View.VISIBLE else View.GONE
binding.maxPlayersLabel.text = context.getString(R.string.multiplayer_max_players_value, binding.maxPlayers.value.toInt())
binding.maxPlayers.addOnChangeListener { _, value, _ ->
binding.maxPlayersLabel.text = context.getString(R.string.multiplayer_max_players_value, value.toInt())
}
binding.btnConfirm.setOnClickListener {
binding.btnConfirm.isEnabled = false
binding.btnConfirm.text = activity.getString(R.string.disabled_button_text)
val ipAddress = binding.ipAddress.text.toString()
val username = binding.username.text.toString()
val portStr = binding.ipPort.text.toString()
val password = binding.password.text.toString()
val port = portStr.toIntOrNull() ?: run {
Toast.makeText(activity, R.string.multiplayer_port_invalid, Toast.LENGTH_LONG).show()
binding.btnConfirm.isEnabled = true
binding.btnConfirm.text = activity.getString(R.string.original_button_text)
return@setOnClickListener
}
val roomName = binding.roomName.text.toString()
val maxPlayers = binding.maxPlayers.value.toInt()
if (isCreateRoom && (roomName.length !in 3..20)) {
Toast.makeText(activity, R.string.multiplayer_room_name_invalid, Toast.LENGTH_LONG).show()
binding.btnConfirm.isEnabled = true
binding.btnConfirm.text = activity.getString(R.string.original_button_text)
return@setOnClickListener
}
if (ipAddress.length < 7 || username.length < 5) {
Toast.makeText(activity, R.string.multiplayer_input_invalid, Toast.LENGTH_LONG).show()
binding.btnConfirm.isEnabled = true
binding.btnConfirm.text = activity.getString(R.string.original_button_text)
} else {
Handler(Looper.getMainLooper()).post {
val result = if (isCreateRoom) {
NetPlayManager.netPlayCreateRoom(ipAddress, port, username, password, roomName, maxPlayers)
} else {
NetPlayManager.netPlayJoinRoom(ipAddress, port, username, password)
}
if (result == 0) {
NetPlayManager.setUsername(activity, username)
NetPlayManager.setRoomPort(activity, portStr)
if (!isCreateRoom) NetPlayManager.setRoomAddress(activity, ipAddress)
Toast.makeText(
CitronApplication.appContext,
if (isCreateRoom) R.string.multiplayer_create_room_success
else R.string.multiplayer_join_room_success,
Toast.LENGTH_LONG
).show()
dialog.dismiss()
} else {
Toast.makeText(activity, R.string.multiplayer_could_not_connect, Toast.LENGTH_LONG).show()
binding.btnConfirm.isEnabled = true
binding.btnConfirm.text = activity.getString(R.string.original_button_text)
}
}
}
}
dialog.show()
}
private fun showModerationDialog() {
val activity = CompatUtils.findActivity(context)
val dialog = MaterialAlertDialogBuilder(activity)
dialog.setTitle(R.string.multiplayer_moderation_title)
val banList = NetPlayManager.getBanList()
if (banList.isEmpty()) {
dialog.setMessage(R.string.multiplayer_no_bans)
dialog.setPositiveButton(R.string.ok, null)
dialog.show()
return
}
val view = LayoutInflater.from(context).inflate(R.layout.dialog_ban_list, null)
val recyclerView = view.findViewById<RecyclerView>(R.id.ban_list_recycler)
recyclerView.layoutManager = LinearLayoutManager(context)
lateinit var adapter: BanListAdapter
val onUnban: (String) -> Unit = { bannedItem ->
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.multiplayer_unban_title)
.setMessage(activity.getString(R.string.multiplayer_unban_message, bannedItem))
.setPositiveButton(R.string.multiplayer_unban) { _, _ ->
NetPlayManager.netPlayUnbanUser(bannedItem)
adapter.removeBan(bannedItem)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
adapter = BanListAdapter(banList, onUnban)
recyclerView.adapter = adapter
dialog.setView(view)
dialog.setPositiveButton(R.string.ok, null)
dialog.show()
}
private class BanListAdapter(
banList: List<String>,
private val onUnban: (String) -> Unit
) : RecyclerView.Adapter<BanListAdapter.ViewHolder>() {
private val usernameBans = banList.filter { !it.contains(".") }.toMutableList()
private val ipBans = banList.filter { it.contains(".") }.toMutableList()
class ViewHolder(val binding: ItemBanListBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemBanListBinding.inflate(
LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val isUsername = position < usernameBans.size
val item = if (isUsername) usernameBans[position] else ipBans[position - usernameBans.size]
holder.binding.apply {
banText.text = item
icon.setImageResource(if (isUsername) R.drawable.ic_user else R.drawable.ic_ip)
btnUnban.setOnClickListener { onUnban(item) }
}
}
override fun getItemCount() = usernameBans.size + ipBans.size
fun removeBan(bannedItem: String) {
val position = if (bannedItem.contains(".")) {
ipBans.indexOf(bannedItem).let { if (it >= 0) it + usernameBans.size else it }
} else {
usernameBans.indexOf(bannedItem)
}
if (position >= 0) {
if (bannedItem.contains(".")) {
ipBans.remove(bannedItem)
} else {
usernameBans.remove(bannedItem)
}
notifyItemRemoved(position)
}
}
}
}

View File

@ -18,7 +18,6 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
RENDERER_DEBUG("debug"),
RENDERER_ENHANCED_SHADER_BUILDING("use_enhanced_shader_building"),
PICTURE_IN_PICTURE("picture_in_picture"),
USE_CUSTOM_RTC("custom_rtc_enabled"),
BLACK_BACKGROUNDS("black_backgrounds"),

View File

@ -271,13 +271,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
true
}
R.id.menu_multiplayer -> {
emulationActivity?.displayMultiplayerDialog()
true
}
R.id.menu_controls -> {
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null,

View File

@ -119,16 +119,6 @@ class HomeSettingsFragment : Fragment() {
driverViewModel.selectedDriverTitle
)
)
add(
HomeSetting(
R.string.multiplayer,
R.string.multiplayer_description,
R.drawable.ic_multiplayer,
{
val action = mainActivity.displayMultiplayerDialog()
},
)
)
add(
HomeSetting(
R.string.applets,

View File

@ -180,62 +180,6 @@ class SetupFragment : Fragment() {
}
)
)
// Add title.keys installation page
add(
SetupPage(
R.drawable.ic_key,
R.string.install_title_keys,
R.string.install_title_keys_description,
R.drawable.ic_add,
true,
R.string.select_keys,
{
titleKeyCallback = it
getTitleKey.launch(arrayOf("*/*"))
},
true,
R.string.install_title_keys_warning,
R.string.install_title_keys_warning_description,
R.string.install_title_keys_warning_help,
{
val file = File(DirectoryInitialization.userDirectory + "/keys/title.keys")
if (file.exists()) {
StepState.COMPLETE
} else {
StepState.INCOMPLETE
}
}
)
)
// Add firmware installation page (mandatory)
add(
SetupPage(
R.drawable.ic_key,
R.string.install_firmware,
R.string.install_firmware_description,
R.drawable.ic_add,
true,
R.string.select_firmware,
{
firmwareCallback = it
getFirmware.launch(arrayOf("application/zip"))
},
true,
R.string.install_firmware_warning,
R.string.install_firmware_warning_description,
R.string.install_firmware_warning_help,
{
if (NativeLibrary.isFirmwareAvailable()) {
StepState.COMPLETE
} else {
StepState.INCOMPLETE
}
}
)
)
add(
SetupPage(
R.drawable.ic_controller,
@ -324,18 +268,6 @@ class SetupFragment : Fragment() {
return@setOnClickListener
}
// Special handling for firmware page - don't allow skipping
if (currentPage.titleId == R.string.install_firmware && !NativeLibrary.isFirmwareAvailable()) {
SetupWarningDialogFragment.newInstance(
currentPage.warningTitleId,
currentPage.warningDescriptionId,
currentPage.warningHelpLinkId,
index,
allowSkip = false
).show(childFragmentManager, SetupWarningDialogFragment.TAG)
return@setOnClickListener
}
if (!hasBeenWarned[index]) {
SetupWarningDialogFragment.newInstance(
currentPage.warningTitleId,
@ -414,30 +346,6 @@ class SetupFragment : Fragment() {
}
}
private lateinit var titleKeyCallback: SetupCallback
val getTitleKey =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result != null) {
mainActivity.processTitleKey(result)
titleKeyCallback.onStepCompleted()
}
}
private lateinit var firmwareCallback: SetupCallback
val getFirmware =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result != null) {
mainActivity.getFirmware.launch(arrayOf("application/zip"))
binding.root.postDelayed({
if (NativeLibrary.isFirmwareAvailable()) {
firmwareCallback.onStepCompleted()
}
}, 1000)
}
}
private lateinit var gamesDirCallback: SetupCallback
val getGamesDirectory =

View File

@ -17,7 +17,6 @@ class SetupWarningDialogFragment : DialogFragment() {
private var descriptionId: Int = 0
private var helpLinkId: Int = 0
private var page: Int = 0
private var allowSkip: Boolean = true
private lateinit var setupFragment: SetupFragment
@ -27,24 +26,17 @@ class SetupWarningDialogFragment : DialogFragment() {
descriptionId = requireArguments().getInt(DESCRIPTION)
helpLinkId = requireArguments().getInt(HELP_LINK)
page = requireArguments().getInt(PAGE)
allowSkip = requireArguments().getBoolean(ALLOW_SKIP, true)
setupFragment = requireParentFragment() as SetupFragment
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = MaterialAlertDialogBuilder(requireContext())
if (allowSkip) {
builder.setPositiveButton(R.string.warning_skip) { _: DialogInterface?, _: Int ->
.setPositiveButton(R.string.warning_skip) { _: DialogInterface?, _: Int ->
setupFragment.pageForward()
setupFragment.setPageWarned(page)
}
builder.setNegativeButton(R.string.warning_cancel, null)
} else {
// For mandatory steps, only show an OK button that dismisses the dialog
builder.setPositiveButton(R.string.ok, null)
}
.setNegativeButton(R.string.warning_cancel, null)
if (titleId != 0) {
builder.setTitle(titleId)
@ -56,7 +48,7 @@ class SetupWarningDialogFragment : DialogFragment() {
}
if (helpLinkId != 0) {
builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int ->
val helpLink = resources.getString(helpLinkId)
val helpLink = resources.getString(R.string.install_prod_keys_warning_help)
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink))
startActivity(intent)
}
@ -72,14 +64,12 @@ class SetupWarningDialogFragment : DialogFragment() {
private const val DESCRIPTION = "Description"
private const val HELP_LINK = "HelpLink"
private const val PAGE = "Page"
private const val ALLOW_SKIP = "AllowSkip"
fun newInstance(
titleId: Int,
descriptionId: Int,
helpLinkId: Int,
page: Int,
allowSkip: Boolean = true
page: Int
): SetupWarningDialogFragment {
val dialog = SetupWarningDialogFragment()
val bundle = Bundle()
@ -88,7 +78,6 @@ class SetupWarningDialogFragment : DialogFragment() {
putInt(DESCRIPTION, descriptionId)
putInt(HELP_LINK, helpLinkId)
putInt(PAGE, page)
putBoolean(ALLOW_SKIP, allowSkip)
}
dialog.arguments = bundle
return dialog

View File

@ -1,222 +0,0 @@
// Copyright 2024 Mandarine Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citron.citron_emu.network
import android.app.Activity
import android.content.Context
import android.net.wifi.WifiManager
import android.os.Handler
import android.os.Looper
import android.text.format.Formatter
import android.widget.Toast
import androidx.preference.PreferenceManager
import org.citron.citron_emu.CitronApplication
import org.citron.citron_emu.R
import org.citron.citron_emu.dialogs.ChatMessage
object NetPlayManager {
external fun netPlayCreateRoom(ipAddress: String, port: Int, username: String, password: String, roomName: String, maxPlayers: Int): Int
external fun netPlayJoinRoom(ipAddress: String, port: Int, username: String, password: String): Int
external fun netPlayRoomInfo(): Array<String>
external fun netPlayIsJoined(): Boolean
external fun netPlayIsHostedRoom(): Boolean
external fun netPlaySendMessage(msg: String)
external fun netPlayKickUser(username: String)
external fun netPlayLeaveRoom()
external fun netPlayIsModerator(): Boolean
external fun netPlayGetBanList(): Array<String>
external fun netPlayBanUser(username: String)
external fun netPlayUnbanUser(username: String)
private var messageListener: ((Int, String) -> Unit)? = null
private var adapterRefreshListener: ((Int, String) -> Unit)? = null
fun setOnMessageReceivedListener(listener: (Int, String) -> Unit) {
messageListener = listener
}
fun setOnAdapterRefreshListener(listener: (Int, String) -> Unit) {
adapterRefreshListener = listener
}
fun getUsername(activity: Context): String { val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
val name = "Citron${(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 {
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
val address = getIpAddressByWifi(activity)
return prefs.getString("NetPlayRoomAddress", address) ?: address
}
fun setRoomAddress(activity: Activity, address: String) {
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
prefs.edit().putString("NetPlayRoomAddress", address).apply()
}
fun getRoomPort(activity: Activity): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
return prefs.getString("NetPlayRoomPort", "24872") ?: "24872"
}
fun setRoomPort(activity: Activity, port: String) {
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
prefs.edit().putString("NetPlayRoomPort", port).apply()
}
private val chatMessages = mutableListOf<ChatMessage>()
private var isChatOpen = false
fun addChatMessage(message: ChatMessage) {
chatMessages.add(message)
}
fun getChatMessages(): List<ChatMessage> = chatMessages
fun clearChat() {
chatMessages.clear()
}
fun setChatOpen(isOpen: Boolean) {
isChatOpen = isOpen
}
fun addNetPlayMessage(type: Int, msg: String) {
val context = CitronApplication.appContext
val message = formatNetPlayStatus(context, type, msg)
when (type) {
NetPlayStatus.CHAT_MESSAGE -> {
val parts = msg.split(":", limit = 2)
if (parts.size == 2) {
val nickname = parts[0].trim()
val chatMessage = parts[1].trim()
addChatMessage(ChatMessage(
nickname = nickname,
username = "",
message = chatMessage
))
}
}
NetPlayStatus.MEMBER_JOIN,
NetPlayStatus.MEMBER_LEAVE,
NetPlayStatus.MEMBER_KICKED,
NetPlayStatus.MEMBER_BANNED -> {
addChatMessage(ChatMessage(
nickname = "System",
username = "",
message = message
))
}
}
Handler(Looper.getMainLooper()).post {
if (!isChatOpen) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
messageListener?.invoke(type, msg)
adapterRefreshListener?.invoke(type, msg)
}
private fun formatNetPlayStatus(context: Context, type: Int, msg: String): String {
return when (type) {
NetPlayStatus.NETWORK_ERROR -> context.getString(R.string.multiplayer_network_error)
NetPlayStatus.LOST_CONNECTION -> context.getString(R.string.multiplayer_lost_connection)
NetPlayStatus.NAME_COLLISION -> context.getString(R.string.multiplayer_name_collision)
NetPlayStatus.MAC_COLLISION -> context.getString(R.string.multiplayer_mac_collision)
NetPlayStatus.CONSOLE_ID_COLLISION -> context.getString(R.string.multiplayer_console_id_collision)
NetPlayStatus.WRONG_VERSION -> context.getString(R.string.multiplayer_wrong_version)
NetPlayStatus.WRONG_PASSWORD -> context.getString(R.string.multiplayer_wrong_password)
NetPlayStatus.COULD_NOT_CONNECT -> context.getString(R.string.multiplayer_could_not_connect)
NetPlayStatus.ROOM_IS_FULL -> context.getString(R.string.multiplayer_room_is_full)
NetPlayStatus.HOST_BANNED -> context.getString(R.string.multiplayer_host_banned)
NetPlayStatus.PERMISSION_DENIED -> context.getString(R.string.multiplayer_permission_denied)
NetPlayStatus.NO_SUCH_USER -> context.getString(R.string.multiplayer_no_such_user)
NetPlayStatus.ALREADY_IN_ROOM -> context.getString(R.string.multiplayer_already_in_room)
NetPlayStatus.CREATE_ROOM_ERROR -> context.getString(R.string.multiplayer_create_room_error)
NetPlayStatus.HOST_KICKED -> context.getString(R.string.multiplayer_host_kicked)
NetPlayStatus.UNKNOWN_ERROR -> context.getString(R.string.multiplayer_unknown_error)
NetPlayStatus.ROOM_UNINITIALIZED -> context.getString(R.string.multiplayer_room_uninitialized)
NetPlayStatus.ROOM_IDLE -> context.getString(R.string.multiplayer_room_idle)
NetPlayStatus.ROOM_JOINING -> context.getString(R.string.multiplayer_room_joining)
NetPlayStatus.ROOM_JOINED -> context.getString(R.string.multiplayer_room_joined)
NetPlayStatus.ROOM_MODERATOR -> context.getString(R.string.multiplayer_room_moderator)
NetPlayStatus.MEMBER_JOIN -> context.getString(R.string.multiplayer_member_join, msg)
NetPlayStatus.MEMBER_LEAVE -> context.getString(R.string.multiplayer_member_leave, msg)
NetPlayStatus.MEMBER_KICKED -> context.getString(R.string.multiplayer_member_kicked, msg)
NetPlayStatus.MEMBER_BANNED -> context.getString(R.string.multiplayer_member_banned, msg)
NetPlayStatus.ADDRESS_UNBANNED -> context.getString(R.string.multiplayer_address_unbanned)
NetPlayStatus.CHAT_MESSAGE -> msg
else -> ""
}
}
fun getIpAddressByWifi(activity: Activity): String {
var ipAddress = 0
val wifiManager = activity.getSystemService(WifiManager::class.java)
val wifiInfo = wifiManager.connectionInfo
if (wifiInfo != null) {
ipAddress = wifiInfo.ipAddress
}
if (ipAddress == 0) {
val dhcpInfo = wifiManager.dhcpInfo
if (dhcpInfo != null) {
ipAddress = dhcpInfo.ipAddress
}
}
return if (ipAddress == 0) {
"192.168.0.1"
} else {
Formatter.formatIpAddress(ipAddress)
}
}
fun getBanList(): List<String> {
return netPlayGetBanList().toList()
}
object NetPlayStatus {
const val NO_ERROR = 0
const val NETWORK_ERROR = 1
const val LOST_CONNECTION = 2
const val NAME_COLLISION = 3
const val MAC_COLLISION = 4
const val CONSOLE_ID_COLLISION = 5
const val WRONG_VERSION = 6
const val WRONG_PASSWORD = 7
const val COULD_NOT_CONNECT = 8
const val ROOM_IS_FULL = 9
const val HOST_BANNED = 10
const val PERMISSION_DENIED = 11
const val NO_SUCH_USER = 12
const val ALREADY_IN_ROOM = 13
const val CREATE_ROOM_ERROR = 14
const val HOST_KICKED = 15
const val UNKNOWN_ERROR = 16
const val ROOM_UNINITIALIZED = 17
const val ROOM_IDLE = 18
const val ROOM_JOINING = 19
const val ROOM_JOINED = 20
const val ROOM_MODERATOR = 21
const val MEMBER_JOIN = 22
const val MEMBER_LEAVE = 23
const val MEMBER_KICKED = 24
const val MEMBER_BANNED = 25
const val ADDRESS_UNBANNED = 26
const val CHAT_MESSAGE = 27
}
}

View File

@ -31,7 +31,6 @@ import org.citron.citron_emu.HomeNavigationDirections
import org.citron.citron_emu.NativeLibrary
import org.citron.citron_emu.R
import org.citron.citron_emu.databinding.ActivityMainBinding
import org.citron.citron_emu.dialogs.NetPlayDialog
import org.citron.citron_emu.features.settings.model.Settings
import org.citron.citron_emu.fragments.AddGameFolderDialogFragment
import org.citron.citron_emu.fragments.ProgressDialogFragment
@ -69,7 +68,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
ThemeHelper.setTheme(this)
NativeLibrary.netPlayInit()
super.onCreate(savedInstanceState)
@ -159,11 +157,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
setInsets()
}
fun displayMultiplayerDialog() {
val dialog = NetPlayDialog(this)
dialog.show()
}
private fun checkKeys() {
if (!NativeLibrary.areKeysPresent()) {
MessageDialogFragment.newInstance(
@ -377,57 +370,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return false
}
val getTitleKey =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result != null) {
processTitleKey(result)
}
}
fun processTitleKey(result: Uri): Boolean {
if (FileUtil.getExtension(result) != "keys") {
MessageDialogFragment.newInstance(
this,
titleId = R.string.reading_keys_failure,
descriptionId = R.string.install_title_keys_failure_extension_description
).show(supportFragmentManager, MessageDialogFragment.TAG)
return false
}
contentResolver.takePersistableUriPermission(
result,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
val dstPath = DirectoryInitialization.userDirectory + "/keys/"
if (FileUtil.copyUriToInternalStorage(
result,
dstPath,
"title.keys"
) != null
) {
if (NativeLibrary.reloadKeys()) {
Toast.makeText(
applicationContext,
R.string.install_keys_success,
Toast.LENGTH_SHORT
).show()
homeViewModel.setCheckKeys(true)
gamesViewModel.reloadGames(true)
return true
} else {
MessageDialogFragment.newInstance(
this,
titleId = R.string.invalid_keys_error,
descriptionId = R.string.install_keys_failure_description,
helpLinkId = R.string.dumping_keys_quickstart_link
).show(supportFragmentManager, MessageDialogFragment.TAG)
return false
}
}
return false
}
val getFirmware =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null) {

View File

@ -1,19 +0,0 @@
// Copyright 2024 Mandarine Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citron.citron_emu.utils
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
object CompatUtils {
fun findActivity(context: Context): Activity {
return when (context) {
is Activity -> context
is ContextWrapper -> findActivity(context.baseContext)
else -> throw IllegalArgumentException("Context is not an Activity")
}
}
}

View File

@ -13,8 +13,6 @@ import kotlin.system.exitProcess
object LicenseVerifier {
private const val EXPECTED_PACKAGE = "org.citron.citron_emu"
private const val ALTERNATE_PACKAGE = "com.miHoYo.Yuanshen"
private const val ALTERNATE_PACKAGE_2 = "com.antutu.ABenchMark"
private const val OFFICIAL_HASH = "308202e4308201cc020101300d06092a864886f70d010105050030373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b30090603550406130255533020170d3231303831383138303335305a180f32303531303831313138303335305a30373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b300906035504061302555330820122300d06092a864886f70d01010105000382010f003082010a0282010100803b4ba8d352ed0475a8442032eadb75ea0a865a0c310c59970bc5f011f162733941a17bac932e060a7f6b00e1d87e640d87951753ee396893769a6e4a60baddc2bf896cd46d5a08c8321879b955eeb6d9f43908029ec6e938433432c5a1ba19da26d8b3dba39f919695626fba5c412b4aba03d85f0246e79af54d6d57347aa6b5095fe916a34262e7060ef4d3f436e7ce03093757fb719b7e72267402289b0fd819673ee44b5aee23237be8e46be08df64b42de09be6090c49d6d0d7d301f0729e25c67eae2d862a87db0aa19db25ba291aae60c7740e0b745af0f1f236dadeb81fe29104a0731eb9091249a94bb56a90239b6496977ebaf1d98b6fa9f679cd0203010001300d06092a864886f70d01010505000382010100784d8e8d28b11bbdb09b5d9e7b8b4fac0d6defd2703d43da63ad4702af76f6ac700f5dcc2f480fbbf6fb664daa64132b36eb7a7880ade5be12919a14c8816b5c1da06870344902680e8ace430705d0a08158d44a3dc710fff6d60b6eb5eff4056bb7d462dafed5b8533c815988805c9f529ef1b70c7c10f1e225eded6db08f847ae805d8b37c174fa0b42cbab1053acb629711e60ce469de383173e714ae2ea76a975169785d1dbe330f803f7f12dd6616703dbaae4d4c327c5174bee83f83635e06f8634cf49d63ba5c3a4f865572740cf9e720e7df1d48fd7a4a2a651d7bb9f40d1cc6b6680b384827a6ea2a44cc1e5168218637fc5da0c3739caca8d21a1d"
fun verifyLicense(activity: Activity) {
@ -23,10 +21,7 @@ object LicenseVerifier {
val isEaBuild = currentPackage.endsWith(".ea")
// Check package name
if (!isDebugBuild && !isEaBuild &&
currentPackage != EXPECTED_PACKAGE &&
currentPackage != ALTERNATE_PACKAGE &&
currentPackage != ALTERNATE_PACKAGE_2) {
if (!isDebugBuild && !isEaBuild && currentPackage != EXPECTED_PACKAGE) {
showViolationDialog(activity)
return
}

View File

@ -20,7 +20,6 @@
#include <frontend_common/content_manager.h>
#include <jni.h>
#include "common/android/multiplayer/multiplayer.h"
#include "common/android/android_common.h"
#include "common/android/id_cache.h"
#include "common/detached_tasks.h"
@ -871,83 +870,4 @@ jboolean Java_org_citron_citron_1emu_NativeLibrary_areKeysPresent(JNIEnv* env, j
return ContentManager::AreKeysPresent();
}
JNIEXPORT jint JNICALL Java_org_citron_citron_1emu_network_NetPlayManager_netPlayCreateRoom(
JNIEnv* env, [[maybe_unused]] jobject obj, jstring ipaddress, jint port,
jstring username, jstring password, jstring room_name, jint max_players) {
return static_cast<jint>(
NetPlayCreateRoom(Common::Android::GetJString(env, ipaddress), port,
Common::Android::GetJString(env, username), Common::Android::GetJString(env, password),
Common::Android::GetJString(env, room_name), max_players));
}
JNIEXPORT jint JNICALL Java_org_citron_citron_1emu_network_NetPlayManager_netPlayJoinRoom(
JNIEnv* env, [[maybe_unused]] jobject obj, jstring ipaddress, jint port,
jstring username, jstring password) {
return static_cast<jint>(
NetPlayJoinRoom(Common::Android::GetJString(env, ipaddress), port,
Common::Android::GetJString(env, username), Common::Android::GetJString(env, password)));
}
JNIEXPORT jobjectArray JNICALL
Java_org_citron_citron_1emu_network_NetPlayManager_netPlayRoomInfo(
JNIEnv* env, [[maybe_unused]] jobject obj) {
return Common::Android::ToJStringArray(env, NetPlayRoomInfo());
}
JNIEXPORT jboolean JNICALL
Java_org_citron_citron_1emu_network_NetPlayManager_netPlayIsJoined(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
return NetPlayIsJoined();
}
JNIEXPORT jboolean JNICALL
Java_org_citron_citron_1emu_network_NetPlayManager_netPlayIsHostedRoom(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
return NetPlayIsHostedRoom();
}
JNIEXPORT void JNICALL
Java_org_citron_citron_1emu_network_NetPlayManager_netPlaySendMessage(
JNIEnv* env, [[maybe_unused]] jobject obj, jstring msg) {
NetPlaySendMessage(Common::Android::GetJString(env, msg));
}
JNIEXPORT void JNICALL Java_org_citron_citron_1emu_network_NetPlayManager_netPlayKickUser(
JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
NetPlayKickUser(Common::Android::GetJString(env, username));
}
JNIEXPORT void JNICALL Java_org_citron_citron_1emu_network_NetPlayManager_netPlayLeaveRoom(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
NetPlayLeaveRoom();
}
JNIEXPORT jboolean JNICALL
Java_org_citron_citron_1emu_network_NetPlayManager_netPlayIsModerator(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
return NetPlayIsModerator();
}
JNIEXPORT jobjectArray JNICALL
Java_org_citron_citron_1emu_network_NetPlayManager_netPlayGetBanList(
JNIEnv* env, [[maybe_unused]] jobject obj) {
return Common::Android::ToJStringArray(env, NetPlayGetBanList());
}
JNIEXPORT void JNICALL Java_org_citron_citron_1emu_network_NetPlayManager_netPlayBanUser(
JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
NetPlayBanUser(Common::Android::GetJString(env, username));
}
JNIEXPORT void JNICALL Java_org_citron_citron_1emu_network_NetPlayManager_netPlayUnbanUser(
JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
NetPlayUnbanUser(Common::Android::GetJString(env, username));
}
JNIEXPORT void JNICALL
Java_org_citron_citron_1emu_NativeLibrary_netPlayInit(
JNIEnv* env, [[maybe_unused]] jobject obj) {
NetworkInit(&EmulationSession::GetInstance().System().GetRoomNetwork());
}
} // extern "C"

View File

@ -1,31 +0,0 @@
#include "core/crypto/key_manager.h"
#include "core/hle/service/am/am.h"
#include "core/file_sys/registered_cache.h"
#include "core/file_sys/content_archive.h"
#include "core/system.h"
extern "C" {
JNIEXPORT jboolean JNICALL Java_org_citron_citron_1emu_NativeLibrary_isFirmwareAvailable(
JNIEnv* env, jobject obj) {
return Core::Crypto::KeyManager::Instance().IsFirmwareAvailable();
}
JNIEXPORT jboolean JNICALL Java_org_citron_citron_1emu_NativeLibrary_checkFirmwarePresence(
JNIEnv* env, jobject obj) {
constexpr u64 MiiEditId = 0x0100000000001009; // Mii Edit applet ID
constexpr u64 QLaunchId = 0x0100000000001000; // Home Menu applet ID
auto& system = Core::System::GetInstance();
auto bis_system = system.GetFileSystemController().GetSystemNANDContents();
if (!bis_system) {
return false;
}
auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program);
auto qlaunch_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program);
return (mii_applet_nca != nullptr && qlaunch_nca != nullptr);
}
} // extern "C"

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/>
</vector>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
</vector>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorPrimary"
android:pathData="M1,9l2,2c4.97,-4.97 13.03,-4.97 18,0l2,-2C16.93,2.93 7.08,2.93 1,9zM9,17l3,3 3,-3c-1.65,-1.66 -4.34,-1.66 -6,0zM5,13l2,2c2.76,-2.76 7.24,-2.76 10,0l2,-2C15.14,9.14 8.87,9.14 5,13z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
</vector>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M21,3L3,3c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h5v2h8v-2h5c1.1,0 1.99,-0.9 1.99,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,17L3,17L3,5h18v12z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/ban_list_recycler"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"/>

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:gravity="center"
app:strokeWidth="0dp"
app:cardCornerRadius="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="?colorSurface">
<View
android:layout_width="128dp"
android:layout_height="4dp"
android:layout_marginVertical="8dp"
android:backgroundTint="?colorSurfaceVariant" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -1,55 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<TextView
android:id="@+id/text_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/chat"
android:textAppearance="?attr/textAppearanceHeadline6"
android:gravity="center"
android:layout_marginBottom="16dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/chat_recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginBottom="16dp"
android:transcriptMode="alwaysScroll" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/type_message">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/chat_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:imeOptions="actionSend" />
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/send_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="bottom"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_send"
android:contentDescription="@string/send_message" />
</LinearLayout>
</LinearLayout>

View File

@ -1,72 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/text_title"
android:text="@string/multiplayer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadline6"
android:gravity="center"
android:layout_marginTop="4dp"
android:textColor="?attr/colorOnSurface" />
<ImageView
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:layout_marginBottom="24dp"
android:src="@drawable/ic_network"
app:tint="?attr/colorPrimary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_join"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/multiplayer_join_room"
app:icon="@drawable/ic_install"
app:cornerRadius="16dp" />
<Space
android:layout_width="16dp"
android:layout_height="match_parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_create"
style="@style/Widget.Material3.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/multiplayer_create_room"
app:icon="@drawable/ic_add"
app:cornerRadius="16dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -1,75 +0,0 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/text_title"
android:text="@string/multiplayer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadline6"
android:gravity="center"
android:layout_marginTop="4dp"
android:textColor="?attr/colorOnSurface" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_multiplayer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_chat"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="8dp"
android:enabled="true"
android:text="@string/multiplayer_chat"
app:icon="@drawable/ic_chat"
app:cornerRadius="16dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_moderation"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="8dp"
android:enabled="true"
android:text="@string/multiplayer_moderation"
app:cornerRadius="16dp"
app:icon="@drawable/ic_user" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_leave"
style="@style/Widget.Material3.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:text="@string/multiplayer_exit_room"
app:icon="@drawable/ic_exit"
app:cornerRadius="16dp" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@ -1,119 +0,0 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:clipToPadding="false"
android:clipChildren="false"
android:elevation="4dp">
<TextView
android:id="@+id/textTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadline6"
android:gravity="center"
android:paddingBottom="8dp"
android:textColor="?attr/colorOnSurface" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/multiplayer_ip_address"
android:padding="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/ip_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/multiplayer_ip_port"
android:padding="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/ip_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/multiplayer_username"
android:padding="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/multiplayer_password"
android:padding="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/multiplayer_room_name"
android:padding="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/room_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/max_players_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.slider.Slider
android:id="@+id/max_players"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:value="8"
android:valueFrom="2"
android:valueTo="16"
android:stepSize="1" />
<TextView
android:id="@+id/max_players_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/multiplayer_max_players_value" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok"
android:layout_gravity="center" />
</LinearLayout>
</ScrollView>

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_user"
android:layout_marginEnd="16dp"/>
<TextView
android:id="@+id/ban_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_unban"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/multiplayer_unban"/>
</LinearLayout>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="8dp">
<TextView
android:id="@+id/item_button_netplay_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?attr/textAppearanceBodyLarge" />
<ImageButton
android:id="@+id/item_button_more"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/multiplayer_more_options"
android:src="@drawable/ic_more_vert"
android:padding="12dp" />
</LinearLayout>

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<ImageView
android:id="@+id/user_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/username_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold" />
<TextView
android:id="@+id/message_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/timestamp_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:id="@+id/item_button_netplay_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:id="@+id/item_button_netplay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/multiplayer_kick_member"/>
<ImageButton
android:id="@+id/item_button_more"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_more_vert"/>
</LinearLayout>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"/>

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:id="@+id/item_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"/>
<TextView
android:id="@+id/item_text_netplay_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.divider.MaterialDivider
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp" />

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/item_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/item_text_netplay_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?attr/textAppearanceBodyLarge" />
</LinearLayout>

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/item_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/item_text_netplay_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?attr/textAppearanceBodyLarge" />
</LinearLayout>

View File

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="ExtraText">
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_pause_emulation"
@ -23,12 +21,6 @@
android:icon="@drawable/ic_controller"
android:title="@string/preferences_controls" />
<item
android:id="@+id/menu_multiplayer"
android:icon="@drawable/ic_multiplayer"
android:title="@string/multiplayer" />
<item
android:id="@+id/menu_overlay_controls"
android:icon="@drawable/ic_overlay"

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_kick"
android:title="@string/multiplayer_kick_member"
android:enabled="false" />
<item
android:id="@+id/action_ban"
android:title="@string/multiplayer_ban"
android:enabled="false" />
</menu>

View File

@ -23,9 +23,6 @@
<string name="keys">Keys</string>
<string name="keys_description">Select your &lt;b>prod.keys&lt;/b> file with the button below.</string>
<string name="select_keys">Select Keys</string>
<string name="firmware_missing_title">Missing Firmware</string>
<string name="firmware_missing_message">Firmware is required to launch games.\n\nPlease install firmware by placing your Switch firmware files in the appropriate location.</string>
<string name="ok">OK</string>
<string name="games">Games</string>
<string name="games_description">Select your &lt;b>Games&lt;/b> folder with the button below.</string>
<string name="done">Done</string>
@ -108,15 +105,11 @@
<string name="import_saves">Import</string>
<string name="export_saves">Export</string>
<string name="install_firmware">Install firmware</string>
<string name="install_firmware_description">Required for emulation of system features</string>
<string name="install_firmware_warning">Firmware installation is mandatory</string>
<string name="install_firmware_warning_description">Firmware is required for proper emulation. You must install firmware to continue.</string>
<string name="install_firmware_warning_help">https://citron-emu.org/help/quickstart/#dumping-system-firmware</string>
<string name="install_firmware_description">Firmware must be in a ZIP archive and is needed to boot some games</string>
<string name="firmware_installing">Installing firmware</string>
<string name="firmware_installed_success">Firmware successfully installed</string>
<string name="firmware_installed_failure">Failed to install firmware</string>
<string name="firmware_installed_failure_description">The selected file is not a valid firmware archive or is corrupt.</string>
<string name="select_firmware">Select Firmware</string>
<string name="firmware_installed_success">Firmware installed successfully</string>
<string name="firmware_installed_failure">Firmware installation failed</string>
<string name="firmware_installed_failure_description">Make sure the firmware nca files are at the root of the zip and try again.</string>
<string name="share_log">Share debug logs</string>
<string name="share_log_description">Share citron\'s log file to debug issues</string>
<string name="share_log_missing">No log file found</string>
@ -176,14 +169,6 @@
<string name="cabinet_restorer">Restorer</string>
<string name="cabinet_formatter">Formatter</string>
<!-- Title keys strings -->
<string name="install_title_keys">Install title.keys</string>
<string name="install_title_keys_description">Required for additional game compatibility</string>
<string name="install_title_keys_warning">Skip adding title keys?</string>
<string name="install_title_keys_warning_description">Title keys may be required for some games to function properly.</string>
<string name="install_title_keys_warning_help">https://citron-emu.org/</string>
<string name="install_title_keys_failure_extension_description">Verify your title keys file has a .keys extension and try again.</string>
<!-- About screen strings -->
<string name="gaia_is_not_real">Gaia isn\'t real</string>
<string name="copied_to_clipboard">Copied to clipboard</string>
@ -433,76 +418,6 @@
<string name="preferences_debug">Debug</string>
<string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string>
<!-- Multiplayer -->
<string name="multiplayer">Multiplayer</string>
<string name="multiplayer_description">Host your own game room or join an existing one to play with people</string>
<string name="multiplayer_room_title">Room: %1$s</string>
<string name="multiplayer_console_id">Console ID%1$s</string>
<string name="multiplayer_create_room">Create</string>
<string name="multiplayer_join_room">Join</string>
<string name="multiplayer_username">Username</string>
<string name="multiplayer_ip_address">IP Address</string>
<string name="multiplayer_ip_port">Port</string>
<string name="multiplayer_create_room_success">Room created successfully!</string>
<string name="multiplayer_join_room_success">Join the room successfully!</string>
<string name="multiplayer_create_room_failed">Failed to create room!</string>
<string name="multiplayer_join_room_failed">Failed to join room!</string>
<string name="multiplayer_input_invalid">Invalid address or name is too short!</string>
<string name="multiplayer_port_invalid">Invalid port!</string>
<string name="multiplayer_exit_room">Exit Room</string>
<string name="multiplayer_network_error">Network error</string>
<string name="multiplayer_lost_connection">Lost connection</string>
<string name="multiplayer_name_collision">Name collision</string>
<string name="multiplayer_mac_collision">Mac collision</string>
<string name="multiplayer_console_id_collision">Console ID collision</string>
<string name="multiplayer_wrong_version">Wrong version</string>
<string name="multiplayer_wrong_password">Wrong password</string>
<string name="multiplayer_could_not_connect">Could not connect</string>
<string name="multiplayer_room_is_full">Room is full</string>
<string name="multiplayer_host_banned">Host banned</string>
<string name="multiplayer_permission_denied">Permission denied</string>
<string name="multiplayer_no_such_user">No such user</string>
<string name="multiplayer_already_in_room">Already in room</string>
<string name="multiplayer_create_room_error">Create room error</string>
<string name="multiplayer_host_kicked">Host kicked</string>
<string name="multiplayer_unknown_error">unknown error</string>
<string name="multiplayer_room_uninitialized">Room uninitialized</string>
<string name="multiplayer_room_idle">Room idle</string>
<string name="multiplayer_room_joining">Room joining</string>
<string name="multiplayer_room_joined">Room joined</string>
<string name="multiplayer_room_moderator">Room moderator</string>
<string name="multiplayer_member_join">%1$s joined</string>
<string name="multiplayer_member_leave">%1$s left</string>
<string name="multiplayer_member_kicked">%1$s kicked</string>
<string name="multiplayer_member_banned">%1$s banned</string>
<string name="multiplayer_address_unbanned">address unbanned</string>
<string name="multiplayer_kick_member">Kick Out</string>
<string name="multiplayer_chat_input_hint">Send messages……</string>
<string name="multiplayer_password">Password</string>
<string name="original_button_text">Join</string>
<string name="disabled_button_text">Joining...</string>
<string name="multiplayer_room_name">Room Name</string>
<string name="multiplayer_room_name_invalid">Room name must be between 3 and 20 characters</string>
<string name="multiplayer_max_players">Max Players (16)</string>
<string name="multiplayer_max_players_value">Max Players: %d</string>
<string name="multiplayer_chat">Chat</string>
<string name="multiplayer_more_options">More Options</string>
<string name="multiplayer_ip_copied">IP Address copied to clipboard</string>
<string name="multiplayer_server_address">Server Address</string>
<string name="chat">Chat</string>
<string name="type_message">Type message……</string>
<string name="send">Send</string>
<string name="send_message">Send Message</string>
<string name="multiplayer_moderation">Moderation</string>
<string name="multiplayer_moderation_title">Ban List</string>
<string name="multiplayer_no_bans">No banned users</string>
<string name="multiplayer_unban_title">Unban User</string>
<string name="multiplayer_unban">Unban</string>
<string name="multiplayer_unban_message">Are you sure you want to unban %1$s?</string>
<string name="multiplayer_ban">Ban User</string>
<string name="cancel">Cancel</string>
<!-- Game properties -->
<string name="info">Info</string>
<string name="info_description">Program ID, developer, version</string>
@ -1259,4 +1174,5 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</string>
</resources>

View File

@ -4,8 +4,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.9.2" apply false
id("com.android.library") version "8.9.2" apply false
id("com.android.application") version "8.1.2" apply false
id("com.android.library") version "8.1.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
}

View File

@ -1,19 +1,20 @@
## For more details on how to configure your build environment visit
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
#Tue Mar 11 19:29:10 AEST 2025
android.defaults.buildfeatures.buildconfig=true
android.suppressUnsupportedCompileSdk=34
org.gradle.jvmargs=-Xms512m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
kotlin.parallel.tasks.in.project=true
org.gradle.jvmargs=-Xms512m -Dkotlin.daemon.jvm.options\="-Xmx2048M" -Xmx2048M -XX\:MaxMetaspaceSize\=512m -XX\:+HeapDumpOnOutOfMemoryError -Dfile.encoding\=UTF-8
android.defaults.buildfeatures.buildconfig=true
# Android Gradle plugin 8.0.2
android.suppressUnsupportedCompileSdk=34

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@ -1,13 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <memory>
#include <typeinfo>
#include <vector>
#include <QComboBox>
#include <QSpinBox>
#include <QSlider>
#include "common/common_types.h"
#include "common/settings.h"
#include "common/settings_enums.h"
@ -41,22 +38,7 @@ ConfigureCpu::ConfigureCpu(const Core::System& system_,
ConfigureCpu::~ConfigureCpu() = default;
void ConfigureCpu::SetConfiguration() {
// Set clock rate values from settings
const u32 clock_rate_mhz = Settings::values.cpu_clock_rate.GetValue() / 1'000'000;
ui->clock_rate_slider->setValue(static_cast<int>(clock_rate_mhz));
ui->clock_rate_spinbox->setValue(static_cast<int>(clock_rate_mhz));
// Connect slider and spinbox signals to keep them in sync
connect(ui->clock_rate_slider, &QSlider::valueChanged, this, [this](int value) {
ui->clock_rate_spinbox->setValue(value);
});
connect(ui->clock_rate_spinbox, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int value) {
ui->clock_rate_slider->setValue(value);
});
}
void ConfigureCpu::SetConfiguration() {}
void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
auto* accuracy_layout = ui->widget_accuracy->layout();
auto* backend_layout = ui->widget_backend->layout();
@ -117,9 +99,6 @@ void ConfigureCpu::ApplyConfiguration() {
for (const auto& apply_func : apply_funcs) {
apply_func(is_powered_on);
}
// Save the clock rate setting (convert from MHz to Hz)
Settings::values.cpu_clock_rate = static_cast<u32>(ui->clock_rate_spinbox->value()) * 1'000'000;
}
void ConfigureCpu::changeEvent(QEvent* event) {

View File

@ -126,67 +126,6 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="clock_rate_group">
<property name="title">
<string>CPU Clock Rate</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QLabel" name="label_clock_description">
<property name="text">
<string>CPU clock rate in MHz. Setting a higher clock rate will improve performance but may cause system instability. Default is 1020 MHz.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QSlider" name="clock_rate_slider">
<property name="minimum">
<number>500</number>
</property>
<property name="maximum">
<number>1785</number>
</property>
<property name="value">
<number>1020</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>100</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="clock_rate_spinbox">
<property name="suffix">
<string> MHz</string>
</property>
<property name="minimum">
<number>500</number>
</property>
<property name="maximum">
<number>1785</number>
</property>
<property name="value">
<number>1020</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">

View File

@ -423,12 +423,6 @@
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="use_auto_stub">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>This feature has been disabled.</string>
</property>
<property name="text">
<string>Enable Auto-Stub**</string>
</property>
@ -436,12 +430,6 @@
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="quest_flag">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>This feature has been disabled.</string>
</property>
<property name="text">
<string>Kiosk (Quest) Mode</string>
</property>

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "citron/configuration/shared_translation.h"
@ -147,11 +146,6 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
INSERT(
Settings, use_asynchronous_gpu_emulation, tr("Use asynchronous GPU emulation"),
tr("Uses an extra CPU thread for rendering.\nThis option should always remain enabled."));
INSERT(
Settings, respect_present_interval_zero, tr("Respect present interval 0 for unlocked FPS"),
tr("When enabled, present interval 0 will be used for games requesting unlocked FPS.\n"
"This matches console behavior more closely, but may cause higher battery usage and frame pacing issues.\n"
"When disabled (default), present interval 0 is capped at 120FPS to conserve battery."));
INSERT(Settings, nvdec_emulation, tr("NVDEC emulation:"),
tr("Specifies how videos should be decoded.\nIt can either use the CPU or the GPU for "
"decoding, or perform no decoding at all (black screen on videos).\n"

View File

@ -36,7 +36,7 @@ system_
DiscordEventHandlers handlers {};
// The number is the client ID for citron, it's used for images and the
// application name
Discord_Initialize("1361252452329848892", & handlers, 1, nullptr);
Discord_Initialize("1322413013248118888", & handlers, 1, nullptr);
}
DiscordImpl::~DiscordImpl() {

View File

@ -1539,6 +1539,7 @@ void GMainWindow::ConnectMenuEvents() {
connect_menu(ui->action_Stop, &GMainWindow::OnStopGame);
connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility);
connect_menu(ui->action_Open_Mods_Page, &GMainWindow::OnOpenModsPage);
connect_menu(ui->action_Open_Quickstart_Guide, &GMainWindow::OnOpenQuickstartGuide);
connect_menu(ui->action_Open_FAQ, &GMainWindow::OnOpenFAQ);
connect_menu(ui->action_Restart, &GMainWindow::OnRestartGame);
connect_menu(ui->action_Configure, &GMainWindow::OnConfigure);
@ -1843,7 +1844,9 @@ bool GMainWindow::LoadROM(const QString& filename, Service::AM::FrontendAppletPa
tr("Error while loading ROM! %1", "%1 signifies a numeric error code.")
.arg(QString::fromStdString(error_code));
const auto description =
tr("%1<br>This software is provided as-is without any warranty or support.<br>Please refer to community resources or documentation for assistance.",
tr("%1<br>Please follow <a href='https://citron-emu.org/help/quickstart/'>the "
"citron quickstart guide</a> to redump your files.<br>You can refer "
"to the citron wiki</a> or the citron Discord</a> for help.",
"%1 signifies an error string.")
.arg(QString::fromStdString(
GetResultStatusString(static_cast<Loader::ResultStatus>(error_id))));
@ -3575,6 +3578,10 @@ void GMainWindow::OnOpenModsPage() {
OpenURL(QUrl(QStringLiteral("https://git.citron-emu.org/Citron/Citron/wiki/Switch-Mods")));
}
void GMainWindow::OnOpenQuickstartGuide() {
OpenURL(QUrl(QStringLiteral("https://citron-emu.org/help/quickstart/")));
}
void GMainWindow::OnOpenFAQ() {
OpenURL(QUrl(QStringLiteral("https://citron-emu.org/wiki/faq/")));
}
@ -4780,24 +4787,19 @@ void GMainWindow::OnCheckFirmwareDecryption() {
}
bool GMainWindow::CheckFirmwarePresence() {
constexpr u64 MiiEditId = 0x0100000000001009; // Mii Edit applet ID
constexpr u64 QLaunchId = 0x0100000000001000; // Home Menu applet ID
constexpr u64 MiiEditId = static_cast<u64>(Service::AM::AppletProgramId::MiiEdit);
auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
if (!bis_system) {
return false;
}
// Check for essential system applets
auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program);
auto qlaunch_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program);
if (!mii_applet_nca || !qlaunch_nca) {
if (!mii_applet_nca) {
return false;
}
// Also check for essential keys
return Core::Crypto::KeyManager::Instance().IsFirmwareAvailable();
return true;
}
void GMainWindow::SetFirmwareVersion() {

View File

@ -336,6 +336,7 @@ private slots:
void OnPrepareForSleep(bool prepare_sleep);
void OnMenuReportCompatibility();
void OnOpenModsPage();
void OnOpenQuickstartGuide();
void OnOpenFAQ();
/// Called whenever a user selects a game in the game list widget.
void OnGameListLoadFile(QString game_path, u64 program_id);

View File

@ -360,6 +360,11 @@
<string>Open &amp;Mods Page</string>
</property>
</action>
<action name="action_Open_Quickstart_Guide">
<property name="text">
<string>Open &amp;Quickstart Guide</string>
</property>
</action>
<action name="action_Open_FAQ">
<property name="text">
<string>&amp;FAQ</string>

View File

@ -189,8 +189,6 @@ if(ANDROID)
android/android_common.h
android/id_cache.cpp
android/id_cache.h
android/multiplayer/multiplayer.cpp
android/multiplayer/multiplayer.h
android/applets/software_keyboard.cpp
android/applets/software_keyboard.h
)

View File

@ -34,15 +34,6 @@ jstring ToJString(JNIEnv* env, std::string_view str) {
static_cast<jint>(converted_string.size()));
}
jobjectArray ToJStringArray(JNIEnv* env, const std::vector<std::string>& strs) {
jobjectArray array =
env->NewObjectArray(static_cast<jsize>(strs.size()), env->FindClass("java/lang/String"), env->NewStringUTF(""));
for (std::size_t i = 0; i < strs.size(); ++i) {
env->SetObjectArrayElement(array, static_cast<jsize>(i), ToJString(env, strs[i]));
}
return array;
}
jstring ToJString(JNIEnv* env, std::u16string_view str) {
return ToJString(env, Common::UTF16ToUTF8(str));
}

View File

@ -1,11 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string>
#include <vector>
#include <jni.h>
#include "common/common_types.h"
@ -21,7 +19,7 @@ jobject ToJDouble(JNIEnv* env, double value);
s32 GetJInteger(JNIEnv* env, jobject jinteger);
jobject ToJInteger(JNIEnv* env, s32 value);
jobjectArray ToJStringArray(JNIEnv* env, const std::vector<std::string>& strs);
bool GetJBoolean(JNIEnv* env, jobject jboolean);
jobject ToJBoolean(JNIEnv* env, bool value);

View File

@ -8,9 +8,6 @@
#include "common/assert.h"
#include "common/fs/fs_android.h"
#include "video_core/rasterizer_interface.h"
#include "common/android/multiplayer/multiplayer.h"
#include <network/network.h>
static JavaVM* s_java_vm;
static jclass s_native_library_class;
@ -91,8 +88,6 @@ static jmethodID s_citron_input_device_get_supports_vibration;
static jmethodID s_citron_input_device_vibrate;
static jmethodID s_citron_input_device_get_axes;
static jmethodID s_citron_input_device_has_keys;
static jmethodID s_add_netplay_message;
static jmethodID s_clear_chat;
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
@ -393,15 +388,6 @@ jmethodID GetCitronDeviceHasKeys() {
return s_citron_input_device_has_keys;
}
jmethodID GetAddNetPlayMessage() {
return s_add_netplay_message;
}
jmethodID ClearChat() {
return s_clear_chat;
}
#ifdef __cplusplus
extern "C" {
#endif
@ -561,10 +547,6 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
s_citron_input_device_has_keys =
env->GetMethodID(citron_input_device_interface, "hasKeys", "([I)[Z");
env->DeleteLocalRef(citron_input_device_interface);
s_add_netplay_message = env->GetStaticMethodID(s_native_library_class, "addNetPlayMessage",
"(ILjava/lang/String;)V");
s_clear_chat = env->GetStaticMethodID(s_native_library_class, "clearChat", "()V");
// Initialize Android Storage
Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
@ -600,8 +582,6 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
// UnInitialize applets
SoftwareKeyboard::CleanupJNI(env);
NetworkShutdown();
}
#ifdef __cplusplus

View File

@ -5,7 +5,6 @@
#include <future>
#include <jni.h>
#include <network/network.h>
#include "video_core/rasterizer_interface.h"
@ -109,6 +108,5 @@ jmethodID GetCitronDeviceGetSupportsVibration();
jmethodID GetCitronDeviceVibrate();
jmethodID GetCitronDeviceGetAxes();
jmethodID GetCitronDeviceHasKeys();
jmethodID GetAddNetPlayMessage();
jmethodID ClearChat();
} // namespace Android
} // namespace Common::Android

View File

@ -1,350 +0,0 @@
// Copyright 2024 Mandarine Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "common/android/id_cache.h"
#include "multiplayer.h"
#include "common/android/android_common.h"
#include "core/core.h"
#include "network/network.h"
#include "android/log.h"
#include <thread>
#include <chrono>
namespace IDCache = Common::Android;
Network::RoomNetwork* room_network;
void AddNetPlayMessage(jint type, jstring msg) {
IDCache::GetEnvForThread()->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
IDCache::GetAddNetPlayMessage(), type, msg);
}
void AddNetPlayMessage(int type, const std::string& msg) {
JNIEnv* env = IDCache::GetEnvForThread();
AddNetPlayMessage(type, Common::Android::ToJString(env, msg));
}
void ClearChat() {
IDCache::GetEnvForThread()->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
IDCache::ClearChat());
}
bool NetworkInit(Network::RoomNetwork* room_network_) {
room_network = room_network_;
bool result = room_network->Init();
if (!result) {
return false;
}
if (auto member = room_network->GetRoomMember().lock()) {
// register the network structs to use in slots and signals
member->BindOnStateChanged([](const Network::RoomMember::State& state) {
if (state == Network::RoomMember::State::Joined ||
state == Network::RoomMember::State::Moderator) {
NetPlayStatus status;
std::string msg;
switch (state) {
case Network::RoomMember::State::Joined:
status = NetPlayStatus::ROOM_JOINED;
break;
case Network::RoomMember::State::Moderator:
status = NetPlayStatus::ROOM_MODERATOR;
break;
default:
return;
}
AddNetPlayMessage(static_cast<int>(status), msg);
}
});
member->BindOnError([](const Network::RoomMember::Error& error) {
NetPlayStatus status;
std::string msg;
switch (error) {
case Network::RoomMember::Error::LostConnection:
status = NetPlayStatus::LOST_CONNECTION;
break;
case Network::RoomMember::Error::HostKicked:
status = NetPlayStatus::HOST_KICKED;
break;
case Network::RoomMember::Error::UnknownError:
status = NetPlayStatus::UNKNOWN_ERROR;
break;
case Network::RoomMember::Error::NameCollision:
status = NetPlayStatus::NAME_COLLISION;
break;
case Network::RoomMember::Error::IpCollision:
status = NetPlayStatus::MAC_COLLISION;
break;
case Network::RoomMember::Error::WrongVersion:
status = NetPlayStatus::WRONG_VERSION;
break;
case Network::RoomMember::Error::WrongPassword:
status = NetPlayStatus::WRONG_PASSWORD;
break;
case Network::RoomMember::Error::CouldNotConnect:
status = NetPlayStatus::COULD_NOT_CONNECT;
break;
case Network::RoomMember::Error::RoomIsFull:
status = NetPlayStatus::ROOM_IS_FULL;
break;
case Network::RoomMember::Error::HostBanned:
status = NetPlayStatus::HOST_BANNED;
break;
case Network::RoomMember::Error::PermissionDenied:
status = NetPlayStatus::PERMISSION_DENIED;
break;
case Network::RoomMember::Error::NoSuchUser:
status = NetPlayStatus::NO_SUCH_USER;
break;
}
AddNetPlayMessage(static_cast<int>(status), msg);
});
member->BindOnStatusMessageReceived([](const Network::StatusMessageEntry& status_message) {
NetPlayStatus status = NetPlayStatus::NO_ERROR;
std::string msg(status_message.nickname);
switch (status_message.type) {
case Network::IdMemberJoin:
status = NetPlayStatus::MEMBER_JOIN;
break;
case Network::IdMemberLeave:
status = NetPlayStatus::MEMBER_LEAVE;
break;
case Network::IdMemberKicked:
status = NetPlayStatus::MEMBER_KICKED;
break;
case Network::IdMemberBanned:
status = NetPlayStatus::MEMBER_BANNED;
break;
case Network::IdAddressUnbanned:
status = NetPlayStatus::ADDRESS_UNBANNED;
break;
}
AddNetPlayMessage(static_cast<int>(status), msg);
});
member->BindOnChatMessageReceived([](const Network::ChatEntry& chat) {
NetPlayStatus status = NetPlayStatus::CHAT_MESSAGE;
std::string msg(chat.nickname);
msg += ": ";
msg += chat.message;
AddNetPlayMessage(static_cast<int>(status), msg);
});
}
return true;
}
NetPlayStatus NetPlayCreateRoom(const std::string& ipaddress, int port,
const std::string& username, const std::string& password,
const std::string& room_name, int max_players) {
__android_log_print(ANDROID_LOG_INFO, "NetPlay", "NetPlayCreateRoom called with ipaddress: %s, port: %d, username: %s, room_name: %s, max_players: %d", ipaddress.c_str(), port, username.c_str(), room_name.c_str(), max_players);
auto member = room_network->GetRoomMember().lock();
if (!member) {
return NetPlayStatus::NETWORK_ERROR;
}
if (member->GetState() == Network::RoomMember::State::Joining || member->IsConnected()) {
return NetPlayStatus::ALREADY_IN_ROOM;
}
auto room = room_network->GetRoom().lock();
if (!room) {
return NetPlayStatus::NETWORK_ERROR;
}
if (room_name.length() < 3 || room_name.length() > 20) {
return NetPlayStatus::CREATE_ROOM_ERROR;
}
// Placeholder game info
const AnnounceMultiplayerRoom::GameInfo game{
.name = "Default Game",
.id = 0, // Default program ID
};
port = (port == 0) ? Network::DefaultRoomPort : static_cast<u16>(port);
if (!room->Create(room_name, "", ipaddress, static_cast<u16>(port), password,
static_cast<u32>(std::min(max_players, 16)), username, game, nullptr, {})) {
return NetPlayStatus::CREATE_ROOM_ERROR;
}
// Failsafe timer to avoid joining before creation
std::this_thread::sleep_for(std::chrono::milliseconds(100));
member->Join(username, ipaddress.c_str(), static_cast<u16>(port), 0, Network::NoPreferredIP, password, "");
// Failsafe timer to avoid joining before creation
for (int i = 0; i < 5; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (member->GetState() == Network::RoomMember::State::Joined ||
member->GetState() == Network::RoomMember::State::Moderator) {
return NetPlayStatus::NO_ERROR;
}
}
// If join failed while room is created, clean up the room
room->Destroy();
return NetPlayStatus::CREATE_ROOM_ERROR;
}
NetPlayStatus NetPlayJoinRoom(const std::string& ipaddress, int port,
const std::string& username, const std::string& password) {
auto member = room_network->GetRoomMember().lock();
if (!member) {
return NetPlayStatus::NETWORK_ERROR;
}
port =
(port == 0) ? Network::DefaultRoomPort : static_cast<u16>(port);
if (member->GetState() == Network::RoomMember::State::Joining || member->IsConnected()) {
return NetPlayStatus::ALREADY_IN_ROOM;
}
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
std::this_thread::sleep_for(std::chrono::milliseconds(500));
if (member->GetState() == Network::RoomMember::State::Joined ||
member->GetState() == Network::RoomMember::State::Moderator) {
return NetPlayStatus::NO_ERROR;
}
if (!member->IsConnected()) {
return NetPlayStatus::COULD_NOT_CONNECT;
}
return NetPlayStatus::WRONG_PASSWORD;
}
void NetPlaySendMessage(const std::string& msg) {
if (auto room = room_network->GetRoomMember().lock()) {
if (room->GetState() != Network::RoomMember::State::Joined &&
room->GetState() != Network::RoomMember::State::Moderator) {
return;
}
room->SendChatMessage(msg);
}
}
void NetPlayKickUser(const std::string& username) {
if (auto room = room_network->GetRoomMember().lock()) {
auto members = room->GetMemberInformation();
auto it = std::find_if(members.begin(), members.end(),
[&username](const Network::RoomMember::MemberInformation& member) {
return member.nickname == username;
});
if (it != members.end()) {
room->SendModerationRequest(Network::RoomMessageTypes::IdModKick, username);
}
}
}
void NetPlayBanUser(const std::string& username) {
if (auto room = room_network->GetRoomMember().lock()) {
auto members = room->GetMemberInformation();
auto it = std::find_if(members.begin(), members.end(),
[&username](const Network::RoomMember::MemberInformation& member) {
return member.nickname == username;
});
if (it != members.end()) {
room->SendModerationRequest(Network::RoomMessageTypes::IdModBan, username);
}
}
}
void NetPlayUnbanUser(const std::string& username) {
if (auto room = room_network->GetRoomMember().lock()) {
room->SendModerationRequest(Network::RoomMessageTypes::IdModUnban, username);
}
}
std::vector<std::string> NetPlayRoomInfo() {
std::vector<std::string> info_list;
if (auto room = room_network->GetRoomMember().lock()) {
auto members = room->GetMemberInformation();
if (!members.empty()) {
// name and max players
auto room_info = room->GetRoomInformation();
info_list.push_back(room_info.name + "|" + std::to_string(room_info.member_slots));
// all members
for (const auto& member : members) {
info_list.push_back(member.nickname);
}
}
}
return info_list;
}
bool NetPlayIsJoined() {
auto member = room_network->GetRoomMember().lock();
if (!member) {
return false;
}
return (member->GetState() == Network::RoomMember::State::Joined ||
member->GetState() == Network::RoomMember::State::Moderator);
}
bool NetPlayIsHostedRoom() {
if (auto room = room_network->GetRoom().lock()) {
return room->GetState() == Network::Room::State::Open;
}
return false;
}
void NetPlayLeaveRoom() {
if (auto room = room_network->GetRoom().lock()) {
// if you are in a room, leave it
if (auto member = room_network->GetRoomMember().lock()) {
member->Leave();
}
ClearChat();
// if you are hosting a room, also stop hosting
if (room->GetState() == Network::Room::State::Open) {
room->Destroy();
}
}
}
void NetworkShutdown() {
room_network->Shutdown();
}
bool NetPlayIsModerator() {
auto member = room_network->GetRoomMember().lock();
if (!member) {
return false;
}
return member->GetState() == Network::RoomMember::State::Moderator;
}
std::vector<std::string> NetPlayGetBanList() {
std::vector<std::string> ban_list;
if (auto room = room_network->GetRoom().lock()) {
auto [username_bans, ip_bans] = room->GetBanList();
// Add username bans
for (const auto& username : username_bans) {
ban_list.push_back(username);
}
// Add IP bans
for (const auto& ip : ip_bans) {
ban_list.push_back(ip);
}
}
return ban_list;
}

View File

@ -1,65 +0,0 @@
// Copyright 2024 Mandarine Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <string>
#include <vector>
#include <common/common_types.h>
#include <network/network.h>
enum class NetPlayStatus : s32 {
NO_ERROR,
NETWORK_ERROR,
LOST_CONNECTION,
NAME_COLLISION,
MAC_COLLISION,
CONSOLE_ID_COLLISION,
WRONG_VERSION,
WRONG_PASSWORD,
COULD_NOT_CONNECT,
ROOM_IS_FULL,
HOST_BANNED,
PERMISSION_DENIED,
NO_SUCH_USER,
ALREADY_IN_ROOM,
CREATE_ROOM_ERROR,
HOST_KICKED,
UNKNOWN_ERROR,
ROOM_UNINITIALIZED,
ROOM_IDLE,
ROOM_JOINING,
ROOM_JOINED,
ROOM_MODERATOR,
MEMBER_JOIN,
MEMBER_LEAVE,
MEMBER_KICKED,
MEMBER_BANNED,
ADDRESS_UNBANNED,
CHAT_MESSAGE,
};
bool NetworkInit(Network::RoomNetwork* room_network);
NetPlayStatus NetPlayCreateRoom(const std::string& ipaddress, int port,
const std::string& username, const std::string& password,
const std::string& room_name, int max_players);
NetPlayStatus NetPlayJoinRoom(const std::string& ipaddress, int port,
const std::string& username, const std::string& password);
std::vector<std::string> NetPlayRoomInfo();
bool NetPlayIsJoined();
bool NetPlayIsHostedRoom();
bool NetPlayIsModerator();
void NetPlaySendMessage(const std::string& msg);
void NetPlayKickUser(const std::string& username);
void NetPlayBanUser(const std::string& username);
void NetPlayLeaveRoom();
std::string NetPlayGetConsoleId();
void NetworkShutdown();
std::vector<std::string> NetPlayGetBanList();
void NetPlayUnbanUser(const std::string& username);

View File

@ -35,6 +35,7 @@ struct RoomInformation {
u16 port; ///< The port of this room
GameInfo preferred_game; ///< Game to advertise that you want to play
std::string host_username; ///< Forum username of the host
bool enable_citron_mods; ///< Allow citron Moderators to moderate on this room
};
struct Room {

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -90,7 +89,6 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
SUB(Service, BGTC) \
SUB(Service, BTDRV) \
SUB(Service, BTM) \
SUB(Service, BSD) \
SUB(Service, Capture) \
SUB(Service, ERPT) \
SUB(Service, ETicket) \

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -58,7 +57,6 @@ enum class Class : u8 {
Service_BPC, ///< The BPC service
Service_BTDRV, ///< The Bluetooth driver service
Service_BTM, ///< The BTM service
Service_BSD, ///< The BSD sockets service
Service_Capture, ///< The capture service
Service_ERPT, ///< The error reporting service
Service_ETicket, ///< The ETicket service

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -199,7 +198,6 @@ struct Values {
MemoryLayout::Memory_12Gb,
"memory_layout_mode",
Category::Core};
SwitchableSetting<u32> cpu_clock_rate{linkage, 1'020'000'000, "cpu_clock_rate", Category::Cpu};
SwitchableSetting<bool> use_speed_limit{
linkage, true, "use_speed_limit", Category::Core, Specialization::Paired, false, true};
SwitchableSetting<u16, true> speed_limit{linkage,
@ -212,11 +210,6 @@ struct Values {
true,
true,
&use_speed_limit};
SwitchableSetting<bool> use_nce{linkage, true, "Use Native Code Execution", Category::Core};
// Memory
SwitchableSetting<bool> use_gpu_memory_manager{linkage, false, "Use GPU Memory Manager", Category::Core};
SwitchableSetting<bool> enable_memory_snapshots{linkage, false, "Enable Memory Snapshots", Category::Core};
// Cpu
SwitchableSetting<CpuBackend, true> cpu_backend{linkage,
@ -285,8 +278,6 @@ struct Values {
Category::Renderer};
SwitchableSetting<bool> use_asynchronous_gpu_emulation{
linkage, true, "use_asynchronous_gpu_emulation", Category::Renderer};
SwitchableSetting<bool> respect_present_interval_zero{
linkage, false, "respect_present_interval_zero", Category::Renderer};
SwitchableSetting<AstcDecodeMode, true> accelerate_astc{linkage,
#ifdef ANDROID
AstcDecodeMode::Cpu,
@ -401,11 +392,11 @@ struct Values {
Category::RendererAdvanced};
SwitchableSetting<bool> async_presentation{linkage,
#ifdef ANDROID
false, // Disabled due to crashes
true,
#else
false, // Disabled due to crashes
false,
#endif
"async_presentation", Category::RendererAdvanced}; // Hide from UI
"async_presentation", Category::RendererAdvanced};
SwitchableSetting<bool> renderer_force_max_clock{linkage, false, "force_max_clock",
Category::RendererAdvanced};
SwitchableSetting<bool> use_reactive_flushing{linkage,
@ -627,21 +618,11 @@ struct Values {
// Add-Ons
std::map<u64, std::vector<std::string>> disabled_addons;
// Renderer Advanced Settings
SwitchableSetting<bool> use_enhanced_shader_building{linkage, false, "Enhanced Shader Building",
Category::RendererAdvanced};
// Add a new setting for shader compilation priority
SwitchableSetting<int> shader_compilation_priority{linkage, 0, "Shader Compilation Priority",
Category::RendererAdvanced};
};
extern Values values;
void UpdateGPUAccuracy();
// boold isGPULevelNormal();
// TODO: ZEP
bool IsGPULevelExtreme();
bool IsGPULevelHigh();

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -9,7 +8,6 @@
#include <ratio>
#include "common/common_types.h"
#include "core/hardware_properties.h"
namespace Common {
@ -17,10 +15,7 @@ class WallClock {
public:
static constexpr u64 CNTFRQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
static constexpr u64 GPUTickFreq = 614'400'000; // GM20B GPU Tick Frequency = 614.4 MHz
// Changed from constexpr to function to get dynamic value from settings
static inline u64 CPUTickFreq() {
return Core::Hardware::BASE_CLOCK_RATE();
} // T210/4 A57 CPU Tick Frequency from settings
static constexpr u64 CPUTickFreq = 1'020'000'000; // T210/4 A57 CPU Tick Frequency = 1020.0 MHz
virtual ~WallClock() = default;
@ -81,28 +76,12 @@ protected:
using NsToCNTPCTRatio = std::ratio<CNTFRQ, std::nano::den>;
using NsToGPUTickRatio = std::ratio<GPUTickFreq, std::nano::den>;
// Cycle Timing - using functions for dynamic values
// Cycle Timing
// Update these to use functions instead of constexpr
struct CPUTickToNsRatio {
static inline std::intmax_t num = std::nano::den;
static inline std::intmax_t den = CPUTickFreq();
};
struct CPUTickToUsRatio {
static inline std::intmax_t num = std::micro::den;
static inline std::intmax_t den = CPUTickFreq();
};
struct CPUTickToCNTPCTRatio {
static inline std::intmax_t num = CNTFRQ;
static inline std::intmax_t den = CPUTickFreq();
};
struct CPUTickToGPUTickRatio {
static inline std::intmax_t num = GPUTickFreq;
static inline std::intmax_t den = CPUTickFreq();
};
using CPUTickToNsRatio = std::ratio<std::nano::den, CPUTickFreq>;
using CPUTickToUsRatio = std::ratio<std::micro::den, CPUTickFreq>;
using CPUTickToCNTPCTRatio = std::ratio<CNTFRQ, CPUTickFreq>;
using CPUTickToGPUTickRatio = std::ratio<GPUTickFreq, CPUTickFreq>;
};
std::unique_ptr<WallClock> CreateOptimalClock();

View File

@ -1,5 +1,4 @@
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-FileCopyrightText: 2025 citron Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
add_library(core STATIC
@ -777,12 +776,8 @@ add_library(core STATIC
hle/service/ngc/ngc.h
hle/service/nifm/nifm.cpp
hle/service/nifm/nifm.h
hle/service/nifm/nifm_utils.cpp
hle/service/nifm/nifm_utils.h
hle/service/nim/nim.cpp
hle/service/nim/nim.h
hle/service/nim/nim_utils.cpp
hle/service/nim/nim_utils.h
hle/service/npns/npns.cpp
hle/service/npns/npns.h
hle/service/ns/account_proxy_interface.cpp
@ -1050,14 +1045,6 @@ add_library(core STATIC
hle/service/sm/sm_controller.h
hle/service/sockets/bsd.cpp
hle/service/sockets/bsd.h
hle/service/sockets/bsd_nu.cpp
hle/service/sockets/bsd_nu.h
hle/service/sockets/bsdcfg.cpp
hle/service/sockets/bsdcfg.h
hle/service/sockets/dns_priv.cpp
hle/service/sockets/dns_priv.h
hle/service/sockets/ethc.cpp
hle/service/sockets/ethc.h
hle/service/sockets/nsd.cpp
hle/service/sockets/nsd.h
hle/service/sockets/sfdnsres.cpp
@ -1066,8 +1053,6 @@ add_library(core STATIC
hle/service/sockets/sockets.h
hle/service/sockets/sockets_translate.cpp
hle/service/sockets/sockets_translate.h
hle/service/sockets/socket_utils.cpp
hle/service/sockets/socket_utils.h
hle/service/spl/csrng.cpp
hle/service/spl/csrng.h
hle/service/spl/spl.cpp

View File

@ -1376,31 +1376,4 @@ bool KeyManager::AddTicket(const Ticket& ticket) {
SetKey(S128KeyType::Titlekey, key.value(), rights_id[1], rights_id[0]);
return true;
}
bool KeyManager::IsFirmwareAvailable() const {
// Check for essential keys that would only be present with firmware
if (!HasKey(S128KeyType::Master, 0)) {
return false;
}
// Check for at least one titlekek
bool has_titlekek = false;
for (size_t i = 0; i < CURRENT_CRYPTO_REVISION; ++i) {
if (HasKey(S128KeyType::Titlekek, i)) {
has_titlekek = true;
break;
}
}
if (!has_titlekek) {
return false;
}
// Check for header key
if (!HasKey(S256KeyType::Header)) {
return false;
}
return true;
}
} // namespace Core::Crypto

View File

@ -295,9 +295,6 @@ public:
void ReloadKeys();
bool AreKeysLoaded() const;
// Check if firmware is installed by verifying essential keys
bool IsFirmwareAvailable() const;
private:
KeyManager();

View File

@ -1,25 +0,0 @@
#include "core/system.h"
#include "core/file_sys/registered_cache.h"
#include "core/file_sys/content_archive.h"
#include "core/crypto/key_manager.h"
bool ContentManager::IsFirmwareAvailable() {
constexpr u64 MiiEditId = 0x0100000000001009; // Mii Edit applet ID
constexpr u64 QLaunchId = 0x0100000000001000; // Home Menu applet ID
auto& system = Core::System::GetInstance();
auto bis_system = system.GetFileSystemController().GetSystemNANDContents();
if (!bis_system) {
return false;
}
auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program);
auto qlaunch_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program);
if (!mii_applet_nca || !qlaunch_nca) {
return false;
}
// Also check for essential keys
return Core::Crypto::KeyManager::Instance().IsFirmwareAvailable();
}

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -9,13 +8,12 @@
#include "common/bit_util.h"
#include "common/common_types.h"
#include "common/settings.h"
namespace Core {
namespace Hardware {
inline u64 BASE_CLOCK_RATE() { return Settings::values.cpu_clock_rate.GetValue(); } // Default CPU Frequency set in settings, defaults to 1020 MHz
constexpr u64 BASE_CLOCK_RATE = 1'020'000'000; // Default CPU Frequency = 1020 MHz
constexpr u64 CNTFREQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
constexpr u32 NUM_CPU_CORES = 4; // Number of CPU Cores

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
@ -82,8 +81,8 @@ PerformanceConfiguration Controller::GetCurrentPerformanceConfiguration(Performa
void Controller::SetClockSpeed(u32 mhz) {
LOG_DEBUG(Service_APM, "called, mhz={:08X}", mhz);
// Update the clock rate setting with the provided MHz value (convert to Hz)
Settings::values.cpu_clock_rate = mhz * 1'000'000;
// TODO(DarkLordZach): Actually signal core_timing to change clock speed.
// TODO(Rodrigo): Remove [[maybe_unused]] when core_timing is used.
}
} // namespace Service::APM

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <queue>
@ -29,12 +28,12 @@ public:
{10102, nullptr, "UpdateFriendInfo"},
{10110, nullptr, "GetFriendProfileImage"},
{10120, &IFriendService::CheckFriendListAvailability, "CheckFriendListAvailability"},
{10121, &IFriendService::EnsureFriendListAvailable, "EnsureFriendListAvailable"},
{10121, nullptr, "EnsureFriendListAvailable"},
{10200, nullptr, "SendFriendRequestForApplication"},
{10211, nullptr, "AddFacedFriendRequestForApplication"},
{10400, &IFriendService::GetBlockedUserListIds, "GetBlockedUserListIds"},
{10420, &IFriendService::CheckBlockedUserListAvailability, "CheckBlockedUserListAvailability"},
{10421, &IFriendService::EnsureBlockedUserListAvailable, "EnsureBlockedUserListAvailable"},
{10421, nullptr, "EnsureBlockedUserListAvailable"},
{10500, nullptr, "GetProfileList"},
{10600, nullptr, "DeclareOpenOnlinePlaySession"},
{10601, &IFriendService::DeclareCloseOnlinePlaySession, "DeclareCloseOnlinePlaySession"},
@ -167,27 +166,11 @@ private:
LOG_WARNING(Service_Friend, "(STUBBED) called, uuid=0x{}", uuid.RawString());
// Signal the completion event to unblock any waiting threads
completion_event->Signal();
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push(true);
}
void EnsureFriendListAvailable(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const auto uuid{rp.PopRaw<Common::UUID>()};
LOG_WARNING(Service_Friend, "(STUBBED) EnsureFriendListAvailable called, uuid=0x{}", uuid.RawString());
// Signal the completion event to unblock any waiting threads
completion_event->Signal();
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void GetBlockedUserListIds(HLERequestContext& ctx) {
// This is safe to stub, as there should be no adverse consequences from reporting no
// blocked users.
@ -203,45 +186,11 @@ private:
LOG_WARNING(Service_Friend, "(STUBBED) called, uuid=0x{}", uuid.RawString());
// Signal the completion event to unblock any waiting threads
completion_event->Signal();
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push(true);
}
void EnsureBlockedUserListAvailable(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const auto uuid{rp.PopRaw<Common::UUID>()};
LOG_WARNING(Service_Friend, "(STUBBED) EnsureBlockedUserListAvailable called, uuid=0x{}", uuid.RawString());
// Signal the completion event to unblock any waiting threads
completion_event->Signal();
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void GetReceivedFriendInvitationCountCache(HLERequestContext& ctx) {
LOG_DEBUG(Service_Friend, "(STUBBED) called, check in out");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push(0); // Zero invitations
}
void Cancel(HLERequestContext& ctx) {
LOG_WARNING(Service_Friend, "Cancel called - returning immediately");
// Signal the completion event to unblock any waiting threads
completion_event->Signal();
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void DeclareCloseOnlinePlaySession(HLERequestContext& ctx) {
// Stub used by Splatoon 2
LOG_WARNING(Service_Friend, "(STUBBED) called");
@ -299,6 +248,14 @@ private:
rb.Push(ResultSuccess);
}
void GetReceivedFriendInvitationCountCache(HLERequestContext& ctx) {
LOG_DEBUG(Service_Friend, "(STUBBED) called, check in out");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push(0);
}
KernelHelpers::ServiceContext service_context;
Kernel::KEvent* completion_event;
@ -330,9 +287,6 @@ private:
void GetEvent(HLERequestContext& ctx) {
LOG_DEBUG(Service_Friend, "called");
// Signal the notification event to unblock any waiting threads
notification_event->Signal();
IPC::ResponseBuilder rb{ctx, 2, 1};
rb.Push(ResultSuccess);
rb.PushCopyObjects(notification_event->GetReadableEvent());
@ -409,11 +363,10 @@ private:
};
void Module::Interface::CreateFriendService(HLERequestContext& ctx) {
LOG_DEBUG(Service_Friend, "CreateFriendService called");
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
rb.Push(ResultSuccess);
rb.PushIpcInterface<IFriendService>(system);
LOG_DEBUG(Service_Friend, "called");
}
void Module::Interface::CreateNotificationService(HLERequestContext& ctx) {

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/core.h"
@ -7,7 +6,6 @@
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/nifm/nifm.h"
#include "core/hle/service/nifm/nifm_utils.h"
#include "core/hle/service/server_manager.h"
#include "network/network.h"

View File

@ -1,85 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <map>
#include <mutex>
#include <vector>
#include "common/logging/log.h"
#include "core/hle/service/nifm/nifm_utils.h"
namespace Service::NIFM::nn::nifm {
// Simple implementation to track network requests
namespace {
std::mutex g_request_mutex;
std::map<u32, NetworkRequest> g_requests;
u32 g_next_request_id = 1;
bool g_network_available = true; // Default to true for emulation
}
bool IsNetworkAvailable() {
// For emulation purposes, we'll just return the mocked availability
std::lock_guard lock(g_request_mutex);
return g_network_available;
}
u32 SubmitNetworkRequest() {
std::lock_guard lock(g_request_mutex);
if (!g_network_available) {
LOG_WARNING(Service_NIFM, "Network request submitted but network is not available");
}
u32 request_id = g_next_request_id++;
NetworkRequest request{
.request_id = request_id,
.is_pending = true,
.result = NetworkRequestResult::Success // Assume immediate success for emulation
};
g_requests[request_id] = request;
LOG_INFO(Service_NIFM, "Network request submitted with ID: {}", request_id);
return request_id;
}
NetworkRequestResult GetNetworkRequestResult(u32 request_id) {
std::lock_guard lock(g_request_mutex);
auto it = g_requests.find(request_id);
if (it == g_requests.end()) {
LOG_ERROR(Service_NIFM, "Tried to get result for invalid request ID: {}", request_id);
return NetworkRequestResult::Error;
}
// For emulation, we'll mark the request as no longer pending once the result is checked
it->second.is_pending = false;
return it->second.result;
}
bool CancelNetworkRequest(u32 request_id) {
std::lock_guard lock(g_request_mutex);
auto it = g_requests.find(request_id);
if (it == g_requests.end()) {
LOG_ERROR(Service_NIFM, "Tried to cancel invalid request ID: {}", request_id);
return false;
}
if (!it->second.is_pending) {
LOG_WARNING(Service_NIFM, "Tried to cancel a request that is not pending, ID: {}", request_id);
return false;
}
it->second.is_pending = false;
it->second.result = NetworkRequestResult::Canceled;
LOG_INFO(Service_NIFM, "Network request canceled with ID: {}", request_id);
return true;
}
} // namespace Service::NIFM::nn::nifm

View File

@ -1,44 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_types.h"
namespace Service::NIFM {
// Network request result codes
enum class NetworkRequestResult {
Success = 0,
Error = 1,
Canceled = 2,
Timeout = 3,
};
// Network request structure
struct NetworkRequest {
u32 request_id;
bool is_pending;
NetworkRequestResult result;
};
namespace nn::nifm {
// Checks if network connectivity is available
bool IsNetworkAvailable();
// Submits a network connection request
// Returns the request ID or 0 if the request failed
u32 SubmitNetworkRequest();
// Gets the status of a network request
// Returns the request result
NetworkRequestResult GetNetworkRequestResult(u32 request_id);
// Cancels a pending network request
// Returns true if the request was successfully canceled
bool CancelNetworkRequest(u32 request_id);
} // namespace nn::nifm
} // namespace Service::NIFM

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono>
@ -9,7 +8,6 @@
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/nim/nim.h"
#include "core/hle/service/nim/nim_utils.h"
#include "core/hle/service/server_manager.h"
#include "core/hle/service/service.h"

View File

@ -1,124 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <map>
#include <mutex>
#include "common/logging/log.h"
#include "core/hle/service/nim/nim_utils.h"
namespace Service::NIM::nn::nim {
// Simple implementation to track installation tasks
namespace {
std::mutex g_task_mutex;
std::map<u64, Task> g_tasks;
u64 g_next_task_id = 1;
bool g_service_available = true; // Default to true for emulation
}
bool IsServiceAvailable() {
std::lock_guard lock(g_task_mutex);
return g_service_available;
}
u64 CreateInstallTask(u64 application_id) {
std::lock_guard lock(g_task_mutex);
if (!g_service_available) {
LOG_WARNING(Service_NIM, "Installation task creation attempted but service is not available");
return 0;
}
u64 task_id = g_next_task_id++;
Task task{
.task_id = task_id,
.progress = {
.downloaded_bytes = 0,
.total_bytes = 1'000'000'000, // Fake 1GB download size
.status = TaskStatus::None
}
};
g_tasks[task_id] = task;
LOG_INFO(Service_NIM, "Installation task created for application 0x{:016X} with ID: {}",
application_id, task_id);
return task_id;
}
TaskProgress GetTaskProgress(u64 task_id) {
std::lock_guard lock(g_task_mutex);
auto it = g_tasks.find(task_id);
if (it == g_tasks.end()) {
LOG_ERROR(Service_NIM, "Tried to get progress for invalid task ID: {}", task_id);
return {0, 0, TaskStatus::Failed};
}
// If task is in download state, simulate progress
if (it->second.progress.status == TaskStatus::Downloading) {
// Simulate download progress (add 10% of total size)
auto& progress = it->second.progress;
const u64 increment = progress.total_bytes / 10;
progress.downloaded_bytes += increment;
if (progress.downloaded_bytes >= progress.total_bytes) {
progress.downloaded_bytes = progress.total_bytes;
progress.status = TaskStatus::Installing;
LOG_INFO(Service_NIM, "Task ID {} download complete, now installing", task_id);
}
} else if (it->second.progress.status == TaskStatus::Installing) {
// Simulate installation completion
it->second.progress.status = TaskStatus::Complete;
LOG_INFO(Service_NIM, "Task ID {} installation complete", task_id);
}
return it->second.progress;
}
bool StartInstallTask(u64 task_id) {
std::lock_guard lock(g_task_mutex);
auto it = g_tasks.find(task_id);
if (it == g_tasks.end()) {
LOG_ERROR(Service_NIM, "Tried to start invalid task ID: {}", task_id);
return false;
}
if (it->second.progress.status != TaskStatus::None &&
it->second.progress.status != TaskStatus::Pending) {
LOG_WARNING(Service_NIM, "Tried to start task ID {} which is already in progress", task_id);
return false;
}
it->second.progress.status = TaskStatus::Downloading;
LOG_INFO(Service_NIM, "Started installation task ID: {}", task_id);
return true;
}
bool CancelInstallTask(u64 task_id) {
std::lock_guard lock(g_task_mutex);
auto it = g_tasks.find(task_id);
if (it == g_tasks.end()) {
LOG_ERROR(Service_NIM, "Tried to cancel invalid task ID: {}", task_id);
return false;
}
if (it->second.progress.status == TaskStatus::Complete ||
it->second.progress.status == TaskStatus::Failed ||
it->second.progress.status == TaskStatus::Canceled) {
LOG_WARNING(Service_NIM, "Tried to cancel task ID {} which is already in a final state", task_id);
return false;
}
it->second.progress.status = TaskStatus::Canceled;
LOG_INFO(Service_NIM, "Canceled installation task ID: {}", task_id);
return true;
}
} // namespace Service::NIM::nn::nim

View File

@ -1,57 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_types.h"
namespace Service::NIM {
// Network installation task status
enum class TaskStatus {
None = 0,
Pending = 1,
Downloading = 2,
Installing = 3,
Complete = 4,
Failed = 5,
Canceled = 6,
};
// Network installation task progress
struct TaskProgress {
u64 downloaded_bytes;
u64 total_bytes;
TaskStatus status;
};
// Network installation task
struct Task {
u64 task_id;
TaskProgress progress;
};
namespace nn::nim {
// Checks if the NIM service is available
bool IsServiceAvailable();
// Creates a new installation task
// Returns the task ID or 0 if the task creation failed
u64 CreateInstallTask(u64 application_id);
// Gets the progress of an installation task
// Returns the task progress
TaskProgress GetTaskProgress(u64 task_id);
// Starts an installation task
// Returns true if the task was successfully started
bool StartInstallTask(u64 task_id);
// Cancels an installation task
// Returns true if the task was successfully canceled
bool CancelInstallTask(u64 task_id);
} // namespace nn::nim
} // namespace Service::NIM

View File

@ -1,11 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <boost/container/small_vector.hpp>
#include "common/microprofile.h"
#include "common/settings.h"
#include "core/hle/service/nvdrv/devices/nvdisp_disp0.h"
#include "core/hle/service/nvnflinger/buffer_item.h"
#include "core/hle/service/nvnflinger/buffer_item_consumer.h"
@ -23,20 +21,19 @@ s32 NormalizeSwapInterval(f32* out_speed_scale, s32 swap_interval) {
if (out_speed_scale) {
*out_speed_scale = 2.f * static_cast<f32>(1 - swap_interval);
}
// Only normalize swap_interval to 1 if we're not respecting present interval 0
if (swap_interval == 0 && Settings::values.respect_present_interval_zero.GetValue()) {
// Keep swap_interval as 0 to allow for unlocked FPS
} else {
swap_interval = 1;
}
swap_interval = 1;
}
if (swap_interval >= 5) {
// As an extension, treat high swap interval as precise speed control.
if (out_speed_scale) {
*out_speed_scale = static_cast<f32>(swap_interval) / 100.f;
}
swap_interval = 1;
}
return swap_interval;
}

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
@ -480,122 +479,6 @@ void BSD::EventFd(HLERequestContext& ctx) {
BuildErrnoResponse(ctx, Errno::SUCCESS);
}
void BSD::Sysctl(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
// Return an error if not implemented
rb.Push<s32>(-1);
rb.PushEnum(Errno::INVAL);
}
void BSD::Ioctl(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
// Return an error if not implemented
rb.Push<s32>(-1);
rb.PushEnum(Errno::INVAL);
}
void BSD::ShutdownAllSockets(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push<s32>(0);
rb.PushEnum(Errno::SUCCESS);
}
void BSD::GetResourceStatistics(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push<s32>(0);
rb.PushEnum(Errno::SUCCESS);
}
void BSD::RecvMMsg(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
// Return an error if not implemented
rb.Push<s32>(-1);
rb.PushEnum(Errno::INVAL);
}
void BSD::SendMMsg(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
// Return an error if not implemented
rb.Push<s32>(-1);
rb.PushEnum(Errno::INVAL);
}
void BSD::RegisterResourceStatisticsName(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSD::RegisterClientShared(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSD::GetSocketStatistics(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push<s32>(0);
rb.PushEnum(Errno::SUCCESS);
}
void BSD::NifIoctl(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
// Return an error if not implemented
rb.Push<s32>(-1);
rb.PushEnum(Errno::INVAL);
}
void BSD::SetThreadCoreMask(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSD::GetThreadCoreMask(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSD::SocketExempt(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
const u32 domain = rp.Pop<u32>();
const u32 type = rp.Pop<u32>();
const u32 protocol = rp.Pop<u32>();
LOG_WARNING(Service, "(STUBBED) called - domain={} type={} protocol={}", domain, type, protocol);
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push<s32>(-1); // Return -1 on exempted socket
rb.PushEnum(Errno::SUCCESS);
}
void BSD::Open(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
// Return an error if not implemented
rb.Push<s32>(-1);
rb.PushEnum(Errno::INVAL);
}
template <typename Work>
void BSD::ExecuteWork(HLERequestContext& ctx, Work work) {
work.Execute(this);
@ -625,9 +508,9 @@ std::pair<s32, Errno> BSD::SocketImpl(Domain domain, Type type, Protocol protoco
LOG_INFO(Service, "New socket fd={}", fd);
auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
auto room_member = room_network.GetRoomMember().lock();
if (room_member && room_member->IsConnected()) {
descriptor.socket = std::make_shared<Network::ProxySocket>(system.GetRoomNetwork());
descriptor.socket = std::make_shared<Network::ProxySocket>(room_network);
} else {
descriptor.socket = std::make_shared<Network::Socket>();
}
@ -1077,41 +960,27 @@ void BSD::BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noex
}
void BSD::OnProxyPacketReceived(const Network::ProxyPacket& packet) {
// Iterate through all file descriptors and pass the packet to each valid socket
for (auto& optional_descriptor : file_descriptors) {
if (!optional_descriptor.has_value()) {
continue;
}
FileDescriptor& descriptor = *optional_descriptor;
if (descriptor.socket) {
descriptor.socket->HandleProxyPacket(packet);
}
descriptor.socket.get()->HandleProxyPacket(packet);
}
}
s32 BSD::Connect(s32 socket, const SockAddrIn& addr) {
// Call ConnectImpl directly if possible, or return error
LOG_INFO(Service_BSD, "nn::socket::Connect called for socket {} with address {}:{}",
socket, addr.ip[0], addr.portno);
// For now, we're assuming the connection will succeed return 0
return 0;
}
BSD::BSD(Core::System& system_, const char* name)
: ServiceFramework{system_, name} {
: ServiceFramework{system_, name}, room_network{system_.GetRoomNetwork()} {
// clang-format off
static const FunctionInfo functions[] = {
{0, &BSD::RegisterClient, "RegisterClient"},
{1, &BSD::StartMonitoring, "StartMonitoring"},
{2, &BSD::Socket, "Socket"},
{3, &BSD::SocketExempt, "SocketExempt"},
{4, &BSD::Open, "Open"},
{3, nullptr, "SocketExempt"},
{4, nullptr, "Open"},
{5, &BSD::Select, "Select"},
{6, &BSD::Poll, "Poll"},
{7, &BSD::Sysctl, "Sysctl"},
{7, nullptr, "Sysctl"},
{8, &BSD::Recv, "Recv"},
{9, &BSD::RecvFrom, "RecvFrom"},
{10, &BSD::Send, "Send"},
@ -1123,32 +992,27 @@ BSD::BSD(Core::System& system_, const char* name)
{16, &BSD::GetSockName, "GetSockName"},
{17, &BSD::GetSockOpt, "GetSockOpt"},
{18, &BSD::Listen, "Listen"},
{19, &BSD::Ioctl, "Ioctl"},
{19, nullptr, "Ioctl"},
{20, &BSD::Fcntl, "Fcntl"},
{21, &BSD::SetSockOpt, "SetSockOpt"},
{22, &BSD::Shutdown, "Shutdown"},
{23, &BSD::ShutdownAllSockets, "ShutdownAllSockets"},
{23, nullptr, "ShutdownAllSockets"},
{24, &BSD::Write, "Write"},
{25, &BSD::Read, "Read"},
{26, &BSD::Close, "Close"},
{27, &BSD::DuplicateSocket, "DuplicateSocket"},
{28, &BSD::GetResourceStatistics, "GetResourceStatistics"},
{29, &BSD::RecvMMsg, "RecvMMsg"},
{30, &BSD::SendMMsg, "SendMMsg"},
{28, nullptr, "GetResourceStatistics"},
{29, nullptr, "RecvMMsg"},
{30, nullptr, "SendMMsg"},
{31, &BSD::EventFd, "EventFd"},
{32, &BSD::RegisterResourceStatisticsName, "RegisterResourceStatisticsName"},
{33, &BSD::RegisterClientShared, "RegisterClientShared"},
{34, &BSD::GetSocketStatistics, "GetSocketStatistics"},
{35, &BSD::NifIoctl, "NifIoctl"},
{200, &BSD::SetThreadCoreMask, "SetThreadCoreMask"},
{201, &BSD::GetThreadCoreMask, "GetThreadCoreMask"},
{32, nullptr, "RegisterResourceStatisticsName"},
{33, nullptr, "Initialize2"},
};
// clang-format on
RegisterHandlers(functions);
auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
if (room_member) {
if (auto room_member = room_network.GetRoomMember().lock()) {
proxy_packet_received = room_member->BindOnProxyPacketReceived(
[this](const Network::ProxyPacket& packet) { OnProxyPacketReceived(packet); });
} else {
@ -1157,8 +1021,7 @@ BSD::BSD(Core::System& system_, const char* name)
}
BSD::~BSD() {
auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
if (room_member) {
if (auto room_member = room_network.GetRoomMember().lock()) {
room_member->Unbind(proxy_packet_received);
}
}
@ -1168,4 +1031,31 @@ std::unique_lock<std::mutex> BSD::LockService() {
return {};
}
BSDCFG::BSDCFG(Core::System& system_) : ServiceFramework{system_, "bsdcfg"} {
// clang-format off
static const FunctionInfo functions[] = {
{0, nullptr, "SetIfUp"},
{1, nullptr, "SetIfUpWithEvent"},
{2, nullptr, "CancelIf"},
{3, nullptr, "SetIfDown"},
{4, nullptr, "GetIfState"},
{5, nullptr, "DhcpRenew"},
{6, nullptr, "AddStaticArpEntry"},
{7, nullptr, "RemoveArpEntry"},
{8, nullptr, "LookupArpEntry"},
{9, nullptr, "LookupArpEntry2"},
{10, nullptr, "ClearArpEntries"},
{11, nullptr, "ClearArpEntries2"},
{12, nullptr, "PrintArpEntries"},
{13, nullptr, "Unknown13"},
{14, nullptr, "Unknown14"},
{15, nullptr, "Unknown15"},
};
// clang-format on
RegisterHandlers(functions);
}
BSDCFG::~BSDCFG() = default;
} // namespace Service::Sockets

View File

@ -1,5 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@ -38,9 +37,6 @@ public:
Errno CloseImpl(s32 fd);
std::optional<std::shared_ptr<Network::SocketBase>> GetSocket(s32 fd);
// Static function that can be called from nn::socket::Connect
static s32 Connect(s32 socket, const SockAddrIn& addr);
private:
/// Maximum number of file descriptors
static constexpr size_t MAX_FD = 128;
@ -128,30 +124,11 @@ private:
Errno bsd_errno{};
};
struct LibraryConfigData {
u32 version;
u32 tcp_tx_buf_size;
u32 tcp_rx_buf_size;
u32 tcp_tx_buf_max_size;
u32 tcp_rx_buf_max_size;
u32 udp_tx_buf_size;
u32 udp_rx_buf_size;
u32 sb_efficiency;
};
// This is nn::socket::sf::IClient
void RegisterClient(HLERequestContext& ctx);
void StartMonitoring(HLERequestContext& ctx);
void Socket(HLERequestContext& ctx);
void SocketExempt(HLERequestContext& ctx);
void Open(HLERequestContext& ctx);
void Select(HLERequestContext& ctx);
void Poll(HLERequestContext& ctx);
void Sysctl(HLERequestContext& ctx);
void Recv(HLERequestContext& ctx);
void RecvFrom(HLERequestContext& ctx);
void Send(HLERequestContext& ctx);
void SendTo(HLERequestContext& ctx);
void Accept(HLERequestContext& ctx);
void Bind(HLERequestContext& ctx);
void Connect(HLERequestContext& ctx);
@ -159,25 +136,18 @@ private:
void GetSockName(HLERequestContext& ctx);
void GetSockOpt(HLERequestContext& ctx);
void Listen(HLERequestContext& ctx);
void Ioctl(HLERequestContext& ctx);
void Fcntl(HLERequestContext& ctx);
void SetSockOpt(HLERequestContext& ctx);
void Shutdown(HLERequestContext& ctx);
void ShutdownAllSockets(HLERequestContext& ctx);
void Recv(HLERequestContext& ctx);
void RecvFrom(HLERequestContext& ctx);
void Send(HLERequestContext& ctx);
void SendTo(HLERequestContext& ctx);
void Write(HLERequestContext& ctx);
void Read(HLERequestContext& ctx);
void Close(HLERequestContext& ctx);
void DuplicateSocket(HLERequestContext& ctx);
void GetResourceStatistics(HLERequestContext& ctx);
void RecvMMsg(HLERequestContext& ctx);
void SendMMsg(HLERequestContext& ctx);
void EventFd(HLERequestContext& ctx);
void RegisterResourceStatisticsName(HLERequestContext& ctx);
void RegisterClientShared(HLERequestContext& ctx);
void GetSocketStatistics(HLERequestContext& ctx);
void NifIoctl(HLERequestContext& ctx);
void SetThreadCoreMask(HLERequestContext& ctx);
void GetThreadCoreMask(HLERequestContext& ctx);
template <typename Work>
void ExecuteWork(HLERequestContext& ctx, Work work);
@ -201,23 +171,30 @@ private:
std::pair<s32, Errno> SendImpl(s32 fd, u32 flags, std::span<const u8> message);
std::pair<s32, Errno> SendToImpl(s32 fd, u32 flags, std::span<const u8> message,
std::span<const u8> addr);
s32 FindFreeFileDescriptorHandle() noexcept;
bool IsFileDescriptorValid(s32 fd) const noexcept;
void BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noexcept;
std::array<std::optional<FileDescriptor>, MAX_FD> file_descriptors;
Network::RoomNetwork& room_network;
/// Callback to parse and handle a received wifi packet.
void OnProxyPacketReceived(const Network::ProxyPacket& packet);
// Callback identifier for the OnProxyPacketReceived event.
Network::RoomMember::CallbackHandle<Network::ProxyPacket> proxy_packet_received;
/// Mapping of file descriptors to sockets
std::array<std::optional<FileDescriptor>, MAX_FD> file_descriptors{};
/// Mutex to protect file descriptor operations
std::mutex mutex;
protected:
virtual std::unique_lock<std::mutex> LockService() override;
};
class BSDCFG final : public ServiceFramework<BSDCFG> {
public:
explicit BSDCFG(Core::System& system_);
~BSDCFG() override;
};
} // namespace Service::Sockets

View File

@ -1,91 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/sockets/bsd_nu.h"
namespace Service::Sockets {
ISfUserService::ISfUserService(Core::System& system_)
: ServiceFramework{system_, "ISfUserService"},
service_context{system_, "ISfUserService"} {
// clang-format off
static const FunctionInfo functions[] = {
{0, &ISfUserService::Assign, "Assign"},
{128, &ISfUserService::GetUserInfo, "GetUserInfo"},
{129, &ISfUserService::GetStateChangedEvent, "GetStateChangedEvent"},
};
// clang-format on
RegisterHandlers(functions);
}
ISfUserService::~ISfUserService() = default;
void ISfUserService::Assign(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
rb.Push(ResultSuccess);
rb.PushIpcInterface<ISfAssignedNetworkInterfaceService>(system);
}
void ISfUserService::GetUserInfo(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void ISfUserService::GetStateChangedEvent(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
auto* event = service_context.CreateEvent("ISfUserService:StateChanged");
IPC::ResponseBuilder rb{ctx, 2, 1};
rb.Push(ResultSuccess);
rb.PushCopyObjects(event->GetReadableEvent());
}
ISfAssignedNetworkInterfaceService::ISfAssignedNetworkInterfaceService(Core::System& system_)
: ServiceFramework{system_, "ISfAssignedNetworkInterfaceService"} {
// clang-format off
static const FunctionInfo functions[] = {
{0, &ISfAssignedNetworkInterfaceService::AddSession, "AddSession"},
};
// clang-format on
RegisterHandlers(functions);
}
ISfAssignedNetworkInterfaceService::~ISfAssignedNetworkInterfaceService() = default;
void ISfAssignedNetworkInterfaceService::AddSession(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
BSD_NU::BSD_NU(Core::System& system_) : ServiceFramework{system_, "bsd:nu"} {
// clang-format off
static const FunctionInfo functions[] = {
{0, &BSD_NU::CreateUserService, "CreateUserService"},
};
// clang-format on
RegisterHandlers(functions);
}
BSD_NU::~BSD_NU() = default;
void BSD_NU::CreateUserService(HLERequestContext& ctx) {
LOG_DEBUG(Service, "called");
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
rb.Push(ResultSuccess);
rb.PushIpcInterface<ISfUserService>(system);
}
} // namespace Service::Sockets

View File

@ -1,46 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "core/hle/service/service.h"
#include "core/hle/service/kernel_helpers.h"
namespace Core {
class System;
}
namespace Service::Sockets {
class ISfUserService final : public ServiceFramework<ISfUserService> {
public:
explicit ISfUserService(Core::System& system_);
~ISfUserService() override;
private:
void Assign(HLERequestContext& ctx);
void GetUserInfo(HLERequestContext& ctx);
void GetStateChangedEvent(HLERequestContext& ctx);
KernelHelpers::ServiceContext service_context;
};
class ISfAssignedNetworkInterfaceService final : public ServiceFramework<ISfAssignedNetworkInterfaceService> {
public:
explicit ISfAssignedNetworkInterfaceService(Core::System& system_);
~ISfAssignedNetworkInterfaceService() override;
private:
void AddSession(HLERequestContext& ctx);
};
class BSD_NU final : public ServiceFramework<BSD_NU> {
public:
explicit BSD_NU(Core::System& system_);
~BSD_NU() override;
private:
void CreateUserService(HLERequestContext& ctx);
};
} // namespace Service::Sockets

View File

@ -1,140 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/sockets/bsdcfg.h"
namespace Service::Sockets {
BSDCFG::BSDCFG(Core::System& system_)
: ServiceFramework{system_, "bsdcfg"},
service_context{system_, "BSDCFG"} {
// clang-format off
static const FunctionInfo functions[] = {
{0, &BSDCFG::SetIfUp, "SetIfUp"},
{1, &BSDCFG::SetIfUpWithEvent, "SetIfUpWithEvent"},
{2, &BSDCFG::CancelIf, "CancelIf"},
{3, &BSDCFG::SetIfDown, "SetIfDown"},
{4, &BSDCFG::GetIfState, "GetIfState"},
{5, &BSDCFG::DhcpRenew, "DhcpRenew"},
{6, &BSDCFG::AddStaticArpEntry, "AddStaticArpEntry"},
{7, &BSDCFG::RemoveArpEntry, "RemoveArpEntry"},
{8, &BSDCFG::LookupArpEntry, "LookupArpEntry"},
{9, &BSDCFG::LookupArpEntry2, "LookupArpEntry2"},
{10, &BSDCFG::ClearArpEntries, "ClearArpEntries"},
{11, &BSDCFG::ClearArpEntries2, "ClearArpEntries2"},
{12, &BSDCFG::PrintArpEntries, "PrintArpEntries"},
{13, &BSDCFG::Cmd13, "Unknown13"},
{14, &BSDCFG::Cmd14, "Unknown14"},
{15, &BSDCFG::Cmd15, "Unknown15"},
};
// clang-format on
RegisterHandlers(functions);
}
BSDCFG::~BSDCFG() = default;
void BSDCFG::SetIfUp(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::SetIfUpWithEvent(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
auto* event = service_context.CreateEvent("BSDCFG:SetIfUpWithEvent");
IPC::ResponseBuilder rb{ctx, 2, 1};
rb.Push(ResultSuccess);
rb.PushCopyObjects(event->GetReadableEvent());
}
void BSDCFG::CancelIf(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::SetIfDown(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::GetIfState(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push<u32>(1); // Interface is up (stubbed)
}
void BSDCFG::DhcpRenew(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::AddStaticArpEntry(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::RemoveArpEntry(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::LookupArpEntry(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::LookupArpEntry2(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::ClearArpEntries(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::ClearArpEntries2(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::PrintArpEntries(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::Cmd13(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::Cmd14(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void BSDCFG::Cmd15(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
} // namespace Service::Sockets

View File

@ -1,41 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "core/hle/service/service.h"
#include "core/hle/service/kernel_helpers.h"
namespace Core {
class System;
}
namespace Service::Sockets {
class BSDCFG final : public ServiceFramework<BSDCFG> {
public:
explicit BSDCFG(Core::System& system_);
~BSDCFG() override;
private:
void SetIfUp(HLERequestContext& ctx);
void SetIfUpWithEvent(HLERequestContext& ctx);
void CancelIf(HLERequestContext& ctx);
void SetIfDown(HLERequestContext& ctx);
void GetIfState(HLERequestContext& ctx);
void DhcpRenew(HLERequestContext& ctx);
void AddStaticArpEntry(HLERequestContext& ctx);
void RemoveArpEntry(HLERequestContext& ctx);
void LookupArpEntry(HLERequestContext& ctx);
void LookupArpEntry2(HLERequestContext& ctx);
void ClearArpEntries(HLERequestContext& ctx);
void ClearArpEntries2(HLERequestContext& ctx);
void PrintArpEntries(HLERequestContext& ctx);
void Cmd13(HLERequestContext& ctx);
void Cmd14(HLERequestContext& ctx);
void Cmd15(HLERequestContext& ctx);
KernelHelpers::ServiceContext service_context;
};
} // namespace Service::Sockets

View File

@ -1,20 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/sockets/dns_priv.h"
namespace Service::Sockets {
DNS_PRIV::DNS_PRIV(Core::System& system_) : ServiceFramework{system_, "dns:priv"} {
// dns:priv doesn't have documented commands yet
static const FunctionInfo functions[] = {
{0, nullptr, "DummyFunction"},
};
RegisterHandlers(functions);
}
DNS_PRIV::~DNS_PRIV() = default;
} // namespace Service::Sockets

View File

@ -1,20 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "core/hle/service/service.h"
namespace Core {
class System;
}
namespace Service::Sockets {
class DNS_PRIV final : public ServiceFramework<DNS_PRIV> {
public:
explicit DNS_PRIV(Core::System& system_);
~DNS_PRIV() override;
};
} // namespace Service::Sockets

View File

@ -1,31 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/sockets/ethc.h"
namespace Service::Sockets {
ETHC_C::ETHC_C(Core::System& system_) : ServiceFramework{system_, "ethc:c"} {
// ethc:c doesn't have documented commands yet
static const FunctionInfo functions[] = {
{0, nullptr, "DummyFunction"},
};
RegisterHandlers(functions);
}
ETHC_C::~ETHC_C() = default;
ETHC_I::ETHC_I(Core::System& system_) : ServiceFramework{system_, "ethc:i"} {
// ethc:i doesn't have documented commands yet
static const FunctionInfo functions[] = {
{0, nullptr, "DummyFunction"},
};
RegisterHandlers(functions);
}
ETHC_I::~ETHC_I() = default;
} // namespace Service::Sockets

View File

@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "core/hle/service/service.h"
namespace Core {
class System;
}
namespace Service::Sockets {
class ETHC_C final : public ServiceFramework<ETHC_C> {
public:
explicit ETHC_C(Core::System& system_);
~ETHC_C() override;
};
class ETHC_I final : public ServiceFramework<ETHC_I> {
public:
explicit ETHC_I(Core::System& system_);
~ETHC_I() override;
};
} // namespace Service::Sockets

Some files were not shown because too many files have changed in this diff Show More