mirror of https://git.citron-emu.org/Citron/Citron
Compare commits
91 Commits
v0.5-canar
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
57cf5a0daf | |
|
|
020492f1fa | |
|
|
21ca0b3119 | |
|
|
58401f5b39 | |
|
|
1ad69f3545 | |
|
|
48eed78d1a | |
|
|
5f962dd1c6 | |
|
|
25abfe36a3 | |
|
|
2f57a35d2d | |
|
|
66bdd6ed27 | |
|
|
ff9c61e7c7 | |
|
|
e72d695115 | |
|
|
0cdd546152 | |
|
|
3205c9b691 | |
|
|
f1e169e060 | |
|
|
278486d059 | |
|
|
bbd3253169 | |
|
|
a1f3414bde | |
|
|
175a427c27 | |
|
|
18def48dfe | |
|
|
a4088f3a1e | |
|
|
b66b3ca639 | |
|
|
3a1c178711 | |
|
|
964bbf489a | |
|
|
19febba866 | |
|
|
0dac3c1dbd | |
|
|
5d952717ff | |
|
|
b25c7653e6 | |
|
|
edfb500ee7 | |
|
|
ebfc9d8347 | |
|
|
1fd5fefcb1 | |
|
|
55dc3f8ec1 | |
|
|
7edbccbdc9 | |
|
|
e06526cbbc | |
|
|
0448d8146f | |
|
|
98c515871e | |
|
|
278ac75a37 | |
|
|
ec402a0510 | |
|
|
8cb6e6d5d4 | |
|
|
51800e249b | |
|
|
21594b73aa | |
|
|
d869045b77 | |
|
|
f2931c7566 | |
|
|
12c63997d2 | |
|
|
1023125be5 | |
|
|
dad8859679 | |
|
|
834cc89548 | |
|
|
38b259d099 | |
|
|
e7e9453667 | |
|
|
ae75413cc3 | |
|
|
6a31da5905 | |
|
|
a5125d008a | |
|
|
b24dd921aa | |
|
|
9a65205dba | |
|
|
af4f08be33 | |
|
|
0d0963d32f | |
|
|
4491127f52 | |
|
|
c304afe2b3 | |
|
|
b8240b4214 | |
|
|
91487f6d96 | |
|
|
031c635095 | |
|
|
90a8165f77 | |
|
|
6565055865 | |
|
|
ee3d858935 | |
|
|
31694994f2 | |
|
|
4197fa84a0 | |
|
|
e4342324fe | |
|
|
78b0080b96 | |
|
|
644ed69285 | |
|
|
3554f55fc9 | |
|
|
dc9532b4d1 | |
|
|
1308e2b935 | |
|
|
5caecd8151 | |
|
|
dc9fbcc893 | |
|
|
b1d5d4e5be | |
|
|
f0d8daf755 | |
|
|
cbb9a35166 | |
|
|
cfe437aacf | |
|
|
9b293c3a98 | |
|
|
84e5fbc089 | |
|
|
a442078ee4 | |
|
|
cc610ad9b6 | |
|
|
5a65f9a094 | |
|
|
5ca1f0e365 | |
|
|
a36baad0f0 | |
|
|
ed115d3f72 | |
|
|
d9619b7eed | |
|
|
dbe5bf1d18 | |
|
|
7903415fa4 | |
|
|
3bb4d97e9e | |
|
|
a41f7b7a56 |
|
|
@ -9,22 +9,22 @@
|
||||||
url = https://github.com/mozilla/cubeb.git
|
url = https://github.com/mozilla/cubeb.git
|
||||||
[submodule "dynarmic"]
|
[submodule "dynarmic"]
|
||||||
path = externals/dynarmic
|
path = externals/dynarmic
|
||||||
url = https://git.citron-emu.org/Citron/dynarmic.git
|
url = https://github.com/yuzu-mirror/dynarmic.git
|
||||||
[submodule "libusb"]
|
[submodule "libusb"]
|
||||||
path = externals/libusb/libusb
|
path = externals/libusb/libusb
|
||||||
url = https://github.com/libusb/libusb.git
|
url = https://github.com/libusb/libusb.git
|
||||||
[submodule "discord-rpc"]
|
[submodule "discord-rpc"]
|
||||||
path = externals/discord-rpc
|
path = externals/discord-rpc
|
||||||
url = https://git.citron-emu.org/Citron/discord-rpc.git
|
url = https://github.com/yuzu-mirror/discord-rpc.git
|
||||||
[submodule "Vulkan-Headers"]
|
[submodule "Vulkan-Headers"]
|
||||||
path = externals/Vulkan-Headers
|
path = externals/Vulkan-Headers
|
||||||
url = https://github.com/KhronosGroup/Vulkan-Headers.git
|
url = https://github.com/KhronosGroup/Vulkan-Headers.git
|
||||||
[submodule "sirit"]
|
[submodule "sirit"]
|
||||||
path = externals/sirit
|
path = externals/sirit
|
||||||
url = https://git.citron-emu.org/Citron/sirit.git
|
url = https://github.com/yuzu-mirror/sirit.git
|
||||||
[submodule "mbedtls"]
|
[submodule "mbedtls"]
|
||||||
path = externals/mbedtls
|
path = externals/mbedtls
|
||||||
url = https://git.citron-emu.org/Citron/mbedtls.git
|
url = https://github.com/yuzu-mirror/mbedtls.git
|
||||||
[submodule "xbyak"]
|
[submodule "xbyak"]
|
||||||
path = externals/xbyak
|
path = externals/xbyak
|
||||||
url = https://github.com/herumi/xbyak.git
|
url = https://github.com/herumi/xbyak.git
|
||||||
|
|
@ -57,13 +57,13 @@
|
||||||
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
|
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
|
||||||
[submodule "breakpad"]
|
[submodule "breakpad"]
|
||||||
path = externals/breakpad
|
path = externals/breakpad
|
||||||
url = https://git.citron-emu.org/Citron/breakpad.git
|
url = https://github.com/yuzu-mirror/breakpad.git
|
||||||
[submodule "simpleini"]
|
[submodule "simpleini"]
|
||||||
path = externals/simpleini
|
path = externals/simpleini
|
||||||
url = https://github.com/brofield/simpleini.git
|
url = https://github.com/brofield/simpleini.git
|
||||||
[submodule "oaknut"]
|
[submodule "oaknut"]
|
||||||
path = externals/oaknut
|
path = externals/oaknut
|
||||||
url = https://git.citron-emu.org/Citron/oaknut.git
|
url = https://github.com/yuzu-mirror/oaknut
|
||||||
[submodule "Vulkan-Utility-Libraries"]
|
[submodule "Vulkan-Utility-Libraries"]
|
||||||
path = externals/Vulkan-Utility-Libraries
|
path = externals/Vulkan-Utility-Libraries
|
||||||
url = https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git
|
url = https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,45 @@ if (MSVC)
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3 /WX-")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3 /WX-")
|
||||||
endif()
|
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
|
# Check if SDL2::SDL2 target exists; if not, create an alias
|
||||||
if (TARGET SDL2::SDL2-static)
|
if (TARGET SDL2::SDL2-static)
|
||||||
add_library(SDL2::SDL2 ALIAS SDL2::SDL2-static)
|
add_library(SDL2::SDL2 ALIAS SDL2::SDL2-static)
|
||||||
|
|
@ -98,10 +137,14 @@ endif()
|
||||||
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
|
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
|
||||||
|
|
||||||
if (ANDROID AND CITRON_DOWNLOAD_ANDROID_VVL)
|
if (ANDROID AND CITRON_DOWNLOAD_ANDROID_VVL)
|
||||||
set(vvl_version "1.4.304.1")
|
set(vvl_version "1.4.309.0")
|
||||||
set(vvl_zip_file "${CMAKE_BINARY_DIR}/externals/vvl-android.zip")
|
set(vvl_zip_file "${CMAKE_BINARY_DIR}/externals/vvl-android.zip")
|
||||||
if (NOT EXISTS "${vvl_zip_file}")
|
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}")
|
||||||
# Download and extract validation layer release to externals directory
|
# 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")
|
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"
|
file(DOWNLOAD "${vvl_base_url}/vulkan-sdk-${vvl_version}/android-binaries-${vvl_version}.zip"
|
||||||
"${vvl_zip_file}" SHOW_PROGRESS)
|
"${vvl_zip_file}" SHOW_PROGRESS)
|
||||||
|
|
@ -110,10 +153,10 @@ if (ANDROID AND CITRON_DOWNLOAD_ANDROID_VVL)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Copy the arm64 binary to src/android/app/main/jniLibs
|
# 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"
|
file(COPY "${CMAKE_BINARY_DIR}/externals/android-binaries-${vvl_version}/arm64-v8a/libVkLayer_khronos_validation.so"
|
||||||
DESTINATION "${vvl_lib_path}")
|
DESTINATION "${vvl_lib_path}")
|
||||||
endif()
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
if (ANDROID)
|
if (ANDROID)
|
||||||
set(CMAKE_SKIP_INSTALL_RULES ON)
|
set(CMAKE_SKIP_INSTALL_RULES ON)
|
||||||
|
|
@ -126,13 +169,23 @@ if (CITRON_USE_BUNDLED_VCPKG)
|
||||||
|
|
||||||
if (CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a")
|
if (CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a")
|
||||||
set(VCPKG_TARGET_TRIPLET "arm64-android")
|
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")
|
set(VCPKG_HOST_TRIPLET "x64-windows")
|
||||||
|
else()
|
||||||
|
set(VCPKG_HOST_TRIPLET "x64-linux")
|
||||||
|
endif()
|
||||||
# this is to avoid CMake using the host pkg-config to find the host
|
# this is to avoid CMake using the host pkg-config to find the host
|
||||||
# libraries when building for Android targets
|
# libraries when building for Android targets
|
||||||
set(PKG_CONFIG_EXECUTABLE "aarch64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
|
set(PKG_CONFIG_EXECUTABLE "aarch64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
|
||||||
elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64")
|
elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64")
|
||||||
set(VCPKG_TARGET_TRIPLET "x64-android")
|
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")
|
set(VCPKG_HOST_TRIPLET "x64-windows")
|
||||||
|
else()
|
||||||
|
set(VCPKG_HOST_TRIPLET "x64-linux")
|
||||||
|
endif()
|
||||||
set(PKG_CONFIG_EXECUTABLE "x86_64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
|
set(PKG_CONFIG_EXECUTABLE "x86_64-none-linux-android-pkg-config" CACHE FILEPATH "" FORCE)
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR "Unsupported Android architecture ${CMAKE_ANDROID_ARCH_ABI}")
|
message(FATAL_ERROR "Unsupported Android architecture ${CMAKE_ANDROID_ARCH_ABI}")
|
||||||
|
|
@ -331,7 +384,7 @@ find_package(ZLIB REQUIRED)
|
||||||
find_package(zstd REQUIRED)
|
find_package(zstd REQUIRED)
|
||||||
|
|
||||||
if (NOT CITRON_USE_EXTERNAL_VULKAN_HEADERS)
|
if (NOT CITRON_USE_EXTERNAL_VULKAN_HEADERS)
|
||||||
find_package(VulkanHeaders 1.4.307 REQUIRED)
|
find_package(VulkanHeaders 1.4.313 REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (NOT CITRON_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES)
|
if (NOT CITRON_USE_EXTERNAL_VULKAN_UTILITY_LIBRARIES)
|
||||||
|
|
@ -394,7 +447,7 @@ if (ENABLE_SDL2)
|
||||||
if (CITRON_USE_BUNDLED_SDL2)
|
if (CITRON_USE_BUNDLED_SDL2)
|
||||||
# Detect toolchain and platform
|
# Detect toolchain and platform
|
||||||
if ((MSVC_VERSION GREATER_EQUAL 1920) AND ARCHITECTURE_x86_64)
|
if ((MSVC_VERSION GREATER_EQUAL 1920) AND ARCHITECTURE_x86_64)
|
||||||
set(SDL2_VER "SDL2-2.32.0")
|
set(SDL2_VER "SDL2-2.28.2")
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable CITRON_USE_BUNDLED_SDL2 and provide your own.")
|
message(FATAL_ERROR "No bundled SDL2 binaries for your toolchain. Disable CITRON_USE_BUNDLED_SDL2 and provide your own.")
|
||||||
endif()
|
endif()
|
||||||
|
|
@ -541,7 +594,7 @@ endif()
|
||||||
# against all the src files. This should be used before making a pull request.
|
# against all the src files. This should be used before making a pull request.
|
||||||
# =======================================================================
|
# =======================================================================
|
||||||
|
|
||||||
set(CLANG_FORMAT_POSTFIX "-18")
|
set(CLANG_FORMAT_POSTFIX "-15")
|
||||||
find_program(CLANG_FORMAT
|
find_program(CLANG_FORMAT
|
||||||
NAMES clang-format${CLANG_FORMAT_POSTFIX}
|
NAMES clang-format${CLANG_FORMAT_POSTFIX}
|
||||||
clang-format
|
clang-format
|
||||||
|
|
@ -552,7 +605,7 @@ if (NOT CLANG_FORMAT)
|
||||||
message(STATUS "Clang format not found! Downloading...")
|
message(STATUS "Clang format not found! Downloading...")
|
||||||
set(CLANG_FORMAT "${PROJECT_BINARY_DIR}/externals/clang-format${CLANG_FORMAT_POSTFIX}.exe")
|
set(CLANG_FORMAT "${PROJECT_BINARY_DIR}/externals/clang-format${CLANG_FORMAT_POSTFIX}.exe")
|
||||||
file(DOWNLOAD
|
file(DOWNLOAD
|
||||||
https://git.citron-emu.org/Citron/ext-windows-bin/raw/master/clang-format${CLANG_FORMAT_POSTFIX}.exe
|
https://github.com/yuzu-mirror/ext-windows-bin/raw/master/clang-format${CLANG_FORMAT_POSTFIX}.exe
|
||||||
"${CLANG_FORMAT}" SHOW_PROGRESS
|
"${CLANG_FORMAT}" SHOW_PROGRESS
|
||||||
STATUS DOWNLOAD_SUCCESS)
|
STATUS DOWNLOAD_SUCCESS)
|
||||||
if (NOT DOWNLOAD_SUCCESS EQUAL 0)
|
if (NOT DOWNLOAD_SUCCESS EQUAL 0)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
|
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||||
function(download_bundled_external remote_path lib_name prefix_var)
|
function(download_bundled_external remote_path lib_name prefix_var)
|
||||||
|
|
||||||
set(package_base_url "https://git.citron-emu.org/Citron/")
|
set(package_base_url "https://github.com/yuzu-mirror/")
|
||||||
set(package_repo "no_platform")
|
set(package_repo "no_platform")
|
||||||
set(package_extension "no_platform")
|
set(package_extension "no_platform")
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
# SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
# Allow systemd-logind to manage user access to hidraw with this file
|
# Allow systemd-logind to manage user access to hidraw with this file
|
||||||
|
|
@ -7,13 +8,13 @@
|
||||||
|
|
||||||
# Switch Pro Controller (USB/Bluetooth)
|
# Switch Pro Controller (USB/Bluetooth)
|
||||||
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="2009", MODE="0660", TAG+="uaccess"
|
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)
|
# 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)
|
# 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)
|
# Joy-Con Charging Grip (USB)
|
||||||
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="200e", MODE="0660", TAG+="uaccess"
|
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="200e", MODE="0660", TAG+="uaccess"
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit cc016b0046d563287f0aa9f09b958b5e70d43696
|
Subproject commit 2359383fc187386204c3bb22de89655a494cd128
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 234c4b7370a8ea3239a214c9e871e4b17c89f4ab
|
Subproject commit e2e53a724677f6eba8ff0ce1ccb64ee321785cbd
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit fe7a09b13899c5c77d956fa310286f7a7eb2c4ed
|
Subproject commit 4e246c56ec5afb5ad66b9b04374d39ac04675c8e
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit c788c52156f3ef7bc7ab769cb03c110a53ac8fcb
|
Subproject commit 539c0a8d8e3733c9f25ea9a184c85c77504f1653
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 9c1294eaddb88cb0e044c675ccae059a85fc9c6c
|
Subproject commit 99e2af4e7837ca09b97d93a562dc12947179fc48
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 37d46edf0f2024c3d04997a2d432d59278ca1dff
|
Subproject commit 96d5fb3de135b86d7222c53f2352ca92827a156b
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: 2023 citron Emulator Project
|
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
|
@ -28,20 +28,20 @@ val autoVersion = (((System.currentTimeMillis() / 1000) - 1451606400) / 10).toIn
|
||||||
android {
|
android {
|
||||||
namespace = "org.citron.citron_emu"
|
namespace = "org.citron.citron_emu"
|
||||||
|
|
||||||
compileSdkVersion = "android-34"
|
compileSdkVersion = "android-35"
|
||||||
ndkVersion = "26.1.10909125"
|
ndkVersion = "29.0.13113456 rc1" // "26.1.10909125"
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "17"
|
jvmTarget = "21"
|
||||||
}
|
}
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
|
|
@ -57,7 +57,8 @@ android {
|
||||||
// TODO If this is ever modified, change application_id in strings.xml
|
// TODO If this is ever modified, change application_id in strings.xml
|
||||||
applicationId = "org.citron.citron_emu"
|
applicationId = "org.citron.citron_emu"
|
||||||
minSdk = 30
|
minSdk = 30
|
||||||
targetSdk = 34
|
//noinspection EditedTargetSdkVersion
|
||||||
|
targetSdk = 35
|
||||||
versionName = getGitVersion()
|
versionName = getGitVersion()
|
||||||
|
|
||||||
versionCode = if (System.getenv("AUTO_VERSIONED") == "true") {
|
versionCode = if (System.getenv("AUTO_VERSIONED") == "true") {
|
||||||
|
|
@ -75,15 +76,6 @@ android {
|
||||||
buildConfigField("String", "BRANCH", "\"${getBranch()}\"")
|
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")
|
val keystoreFile = System.getenv("ANDROID_KEYSTORE_FILE")
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
if (keystoreFile != null) {
|
if (keystoreFile != null) {
|
||||||
|
|
@ -116,9 +108,11 @@ android {
|
||||||
resValue("string", "app_name_suffixed", "Citron")
|
resValue("string", "app_name_suffixed", "Citron")
|
||||||
isDefault = true
|
isDefault = true
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
isJniDebuggable = false
|
||||||
isDebuggable = false
|
isDebuggable = false
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +124,7 @@ android {
|
||||||
signingConfig = signingConfigs.getByName("default")
|
signingConfig = signingConfigs.getByName("default")
|
||||||
isDebuggable = true
|
isDebuggable = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
versionNameSuffix = "-relWithDebInfo"
|
versionNameSuffix = "-relWithDebInfo"
|
||||||
|
|
@ -167,7 +161,7 @@ android {
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
version = "3.22.1"
|
version = "4.0.1"
|
||||||
path = file("../../../CMakeLists.txt")
|
path = file("../../../CMakeLists.txt")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -185,10 +179,11 @@ android {
|
||||||
"-DCITRON_USE_BUNDLED_FFMPEG=ON",
|
"-DCITRON_USE_BUNDLED_FFMPEG=ON",
|
||||||
"-DCITRON_ENABLE_LTO=ON",
|
"-DCITRON_ENABLE_LTO=ON",
|
||||||
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
|
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
|
||||||
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
|
||||||
|
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
|
||||||
)
|
)
|
||||||
|
|
||||||
abiFilters("arm64-v8a", "x86_64")
|
abiFilters("arm64-v8a") // , "x86_64")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +241,6 @@ dependencies {
|
||||||
implementation("io.coil-kt:coil:2.2.2")
|
implementation("io.coil-kt:coil:2.2.2")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
implementation("androidx.window:window:1.2.0-beta03")
|
implementation("androidx.window:window:1.2.0-beta03")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:2.7.4")
|
implementation("androidx.navigation:navigation-fragment-ktx:2.7.4")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:2.7.4")
|
implementation("androidx.navigation:navigation-ui-ktx:2.7.4")
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import org.citron.citron_emu.utils.Log
|
||||||
import org.citron.citron_emu.model.InstallResult
|
import org.citron.citron_emu.model.InstallResult
|
||||||
import org.citron.citron_emu.model.Patch
|
import org.citron.citron_emu.model.Patch
|
||||||
import org.citron.citron_emu.model.GameVerificationResult
|
import org.citron.citron_emu.model.GameVerificationResult
|
||||||
|
import org.citron.citron_emu.network.NetPlayManager
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -243,6 +244,27 @@ object NativeLibrary {
|
||||||
return coreErrorAlertResult
|
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
|
@Keep
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun exitEmulationActivity(resultCode: Int) {
|
fun exitEmulationActivity(resultCode: Int) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
package org.citron.citron_emu.activities
|
package org.citron.citron_emu.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
import android.app.RemoteAction
|
import android.app.RemoteAction
|
||||||
|
|
@ -39,12 +40,14 @@ import org.citron.citron_emu.NativeLibrary
|
||||||
import org.citron.citron_emu.R
|
import org.citron.citron_emu.R
|
||||||
import org.citron.citron_emu.CitronApplication
|
import org.citron.citron_emu.CitronApplication
|
||||||
import org.citron.citron_emu.databinding.ActivityEmulationBinding
|
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.input.NativeInput
|
||||||
import org.citron.citron_emu.features.settings.model.BooleanSetting
|
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.IntSetting
|
||||||
import org.citron.citron_emu.features.settings.model.Settings
|
import org.citron.citron_emu.features.settings.model.Settings
|
||||||
import org.citron.citron_emu.model.EmulationViewModel
|
import org.citron.citron_emu.model.EmulationViewModel
|
||||||
import org.citron.citron_emu.model.Game
|
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.InputHandler
|
||||||
import org.citron.citron_emu.utils.Log
|
import org.citron.citron_emu.utils.Log
|
||||||
import org.citron.citron_emu.utils.MemoryUtil
|
import org.citron.citron_emu.utils.MemoryUtil
|
||||||
|
|
@ -80,6 +83,19 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
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
|
// Add license verification at the start
|
||||||
LicenseVerifier.verifyLicense(this)
|
LicenseVerifier.verifyLicense(this)
|
||||||
|
|
||||||
|
|
@ -409,6 +425,16 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||||
setPictureInPictureParams(pictureInPictureParamsBuilder.build())
|
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() {
|
private var pictureInPictureReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent) {
|
override fun onReceive(context: Context?, intent: Intent) {
|
||||||
if (intent.action == actionPlay) {
|
if (intent.action == actionPlay) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,397 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
||||||
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
|
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
|
||||||
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
|
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
|
||||||
RENDERER_DEBUG("debug"),
|
RENDERER_DEBUG("debug"),
|
||||||
|
RENDERER_ENHANCED_SHADER_BUILDING("use_enhanced_shader_building"),
|
||||||
PICTURE_IN_PICTURE("picture_in_picture"),
|
PICTURE_IN_PICTURE("picture_in_picture"),
|
||||||
USE_CUSTOM_RTC("custom_rtc_enabled"),
|
USE_CUSTOM_RTC("custom_rtc_enabled"),
|
||||||
BLACK_BACKGROUNDS("black_backgrounds"),
|
BLACK_BACKGROUNDS("black_backgrounds"),
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
R.id.menu_multiplayer -> {
|
||||||
|
emulationActivity?.displayMultiplayerDialog()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
R.id.menu_controls -> {
|
R.id.menu_controls -> {
|
||||||
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
||||||
null,
|
null,
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,16 @@ class HomeSettingsFragment : Fragment() {
|
||||||
driverViewModel.selectedDriverTitle
|
driverViewModel.selectedDriverTitle
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
add(
|
||||||
|
HomeSetting(
|
||||||
|
R.string.multiplayer,
|
||||||
|
R.string.multiplayer_description,
|
||||||
|
R.drawable.ic_multiplayer,
|
||||||
|
{
|
||||||
|
val action = mainActivity.displayMultiplayerDialog()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
add(
|
add(
|
||||||
HomeSetting(
|
HomeSetting(
|
||||||
R.string.applets,
|
R.string.applets,
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,62 @@ 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(
|
add(
|
||||||
SetupPage(
|
SetupPage(
|
||||||
R.drawable.ic_controller,
|
R.drawable.ic_controller,
|
||||||
|
|
@ -268,6 +324,18 @@ class SetupFragment : Fragment() {
|
||||||
return@setOnClickListener
|
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]) {
|
if (!hasBeenWarned[index]) {
|
||||||
SetupWarningDialogFragment.newInstance(
|
SetupWarningDialogFragment.newInstance(
|
||||||
currentPage.warningTitleId,
|
currentPage.warningTitleId,
|
||||||
|
|
@ -346,6 +414,30 @@ 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
|
private lateinit var gamesDirCallback: SetupCallback
|
||||||
|
|
||||||
val getGamesDirectory =
|
val getGamesDirectory =
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ class SetupWarningDialogFragment : DialogFragment() {
|
||||||
private var descriptionId: Int = 0
|
private var descriptionId: Int = 0
|
||||||
private var helpLinkId: Int = 0
|
private var helpLinkId: Int = 0
|
||||||
private var page: Int = 0
|
private var page: Int = 0
|
||||||
|
private var allowSkip: Boolean = true
|
||||||
|
|
||||||
private lateinit var setupFragment: SetupFragment
|
private lateinit var setupFragment: SetupFragment
|
||||||
|
|
||||||
|
|
@ -26,17 +27,24 @@ class SetupWarningDialogFragment : DialogFragment() {
|
||||||
descriptionId = requireArguments().getInt(DESCRIPTION)
|
descriptionId = requireArguments().getInt(DESCRIPTION)
|
||||||
helpLinkId = requireArguments().getInt(HELP_LINK)
|
helpLinkId = requireArguments().getInt(HELP_LINK)
|
||||||
page = requireArguments().getInt(PAGE)
|
page = requireArguments().getInt(PAGE)
|
||||||
|
allowSkip = requireArguments().getBoolean(ALLOW_SKIP, true)
|
||||||
|
|
||||||
setupFragment = requireParentFragment() as SetupFragment
|
setupFragment = requireParentFragment() as SetupFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||||
.setPositiveButton(R.string.warning_skip) { _: DialogInterface?, _: Int ->
|
|
||||||
|
if (allowSkip) {
|
||||||
|
builder.setPositiveButton(R.string.warning_skip) { _: DialogInterface?, _: Int ->
|
||||||
setupFragment.pageForward()
|
setupFragment.pageForward()
|
||||||
setupFragment.setPageWarned(page)
|
setupFragment.setPageWarned(page)
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.warning_cancel, null)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
if (titleId != 0) {
|
if (titleId != 0) {
|
||||||
builder.setTitle(titleId)
|
builder.setTitle(titleId)
|
||||||
|
|
@ -48,7 +56,7 @@ class SetupWarningDialogFragment : DialogFragment() {
|
||||||
}
|
}
|
||||||
if (helpLinkId != 0) {
|
if (helpLinkId != 0) {
|
||||||
builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int ->
|
builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int ->
|
||||||
val helpLink = resources.getString(R.string.install_prod_keys_warning_help)
|
val helpLink = resources.getString(helpLinkId)
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink))
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink))
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
@ -64,12 +72,14 @@ class SetupWarningDialogFragment : DialogFragment() {
|
||||||
private const val DESCRIPTION = "Description"
|
private const val DESCRIPTION = "Description"
|
||||||
private const val HELP_LINK = "HelpLink"
|
private const val HELP_LINK = "HelpLink"
|
||||||
private const val PAGE = "Page"
|
private const val PAGE = "Page"
|
||||||
|
private const val ALLOW_SKIP = "AllowSkip"
|
||||||
|
|
||||||
fun newInstance(
|
fun newInstance(
|
||||||
titleId: Int,
|
titleId: Int,
|
||||||
descriptionId: Int,
|
descriptionId: Int,
|
||||||
helpLinkId: Int,
|
helpLinkId: Int,
|
||||||
page: Int
|
page: Int,
|
||||||
|
allowSkip: Boolean = true
|
||||||
): SetupWarningDialogFragment {
|
): SetupWarningDialogFragment {
|
||||||
val dialog = SetupWarningDialogFragment()
|
val dialog = SetupWarningDialogFragment()
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
|
|
@ -78,6 +88,7 @@ class SetupWarningDialogFragment : DialogFragment() {
|
||||||
putInt(DESCRIPTION, descriptionId)
|
putInt(DESCRIPTION, descriptionId)
|
||||||
putInt(HELP_LINK, helpLinkId)
|
putInt(HELP_LINK, helpLinkId)
|
||||||
putInt(PAGE, page)
|
putInt(PAGE, page)
|
||||||
|
putBoolean(ALLOW_SKIP, allowSkip)
|
||||||
}
|
}
|
||||||
dialog.arguments = bundle
|
dialog.arguments = bundle
|
||||||
return dialog
|
return dialog
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ import org.citron.citron_emu.HomeNavigationDirections
|
||||||
import org.citron.citron_emu.NativeLibrary
|
import org.citron.citron_emu.NativeLibrary
|
||||||
import org.citron.citron_emu.R
|
import org.citron.citron_emu.R
|
||||||
import org.citron.citron_emu.databinding.ActivityMainBinding
|
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.features.settings.model.Settings
|
||||||
import org.citron.citron_emu.fragments.AddGameFolderDialogFragment
|
import org.citron.citron_emu.fragments.AddGameFolderDialogFragment
|
||||||
import org.citron.citron_emu.fragments.ProgressDialogFragment
|
import org.citron.citron_emu.fragments.ProgressDialogFragment
|
||||||
|
|
@ -68,6 +69,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
|
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
|
||||||
|
|
||||||
ThemeHelper.setTheme(this)
|
ThemeHelper.setTheme(this)
|
||||||
|
NativeLibrary.netPlayInit()
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
|
@ -157,6 +159,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
setInsets()
|
setInsets()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun displayMultiplayerDialog() {
|
||||||
|
val dialog = NetPlayDialog(this)
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun checkKeys() {
|
private fun checkKeys() {
|
||||||
if (!NativeLibrary.areKeysPresent()) {
|
if (!NativeLibrary.areKeysPresent()) {
|
||||||
MessageDialogFragment.newInstance(
|
MessageDialogFragment.newInstance(
|
||||||
|
|
@ -370,6 +377,57 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
return false
|
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 =
|
val getFirmware =
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,8 @@ import kotlin.system.exitProcess
|
||||||
|
|
||||||
object LicenseVerifier {
|
object LicenseVerifier {
|
||||||
private const val EXPECTED_PACKAGE = "org.citron.citron_emu"
|
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"
|
private const val OFFICIAL_HASH = "308202e4308201cc020101300d06092a864886f70d010105050030373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b30090603550406130255533020170d3231303831383138303335305a180f32303531303831313138303335305a30373116301406035504030c0d416e64726f69642044656275673110300e060355040a0c07416e64726f6964310b300906035504061302555330820122300d06092a864886f70d01010105000382010f003082010a0282010100803b4ba8d352ed0475a8442032eadb75ea0a865a0c310c59970bc5f011f162733941a17bac932e060a7f6b00e1d87e640d87951753ee396893769a6e4a60baddc2bf896cd46d5a08c8321879b955eeb6d9f43908029ec6e938433432c5a1ba19da26d8b3dba39f919695626fba5c412b4aba03d85f0246e79af54d6d57347aa6b5095fe916a34262e7060ef4d3f436e7ce03093757fb719b7e72267402289b0fd819673ee44b5aee23237be8e46be08df64b42de09be6090c49d6d0d7d301f0729e25c67eae2d862a87db0aa19db25ba291aae60c7740e0b745af0f1f236dadeb81fe29104a0731eb9091249a94bb56a90239b6496977ebaf1d98b6fa9f679cd0203010001300d06092a864886f70d01010505000382010100784d8e8d28b11bbdb09b5d9e7b8b4fac0d6defd2703d43da63ad4702af76f6ac700f5dcc2f480fbbf6fb664daa64132b36eb7a7880ade5be12919a14c8816b5c1da06870344902680e8ace430705d0a08158d44a3dc710fff6d60b6eb5eff4056bb7d462dafed5b8533c815988805c9f529ef1b70c7c10f1e225eded6db08f847ae805d8b37c174fa0b42cbab1053acb629711e60ce469de383173e714ae2ea76a975169785d1dbe330f803f7f12dd6616703dbaae4d4c327c5174bee83f83635e06f8634cf49d63ba5c3a4f865572740cf9e720e7df1d48fd7a4a2a651d7bb9f40d1cc6b6680b384827a6ea2a44cc1e5168218637fc5da0c3739caca8d21a1d"
|
||||||
|
|
||||||
fun verifyLicense(activity: Activity) {
|
fun verifyLicense(activity: Activity) {
|
||||||
|
|
@ -21,7 +23,10 @@ object LicenseVerifier {
|
||||||
val isEaBuild = currentPackage.endsWith(".ea")
|
val isEaBuild = currentPackage.endsWith(".ea")
|
||||||
|
|
||||||
// Check package name
|
// Check package name
|
||||||
if (!isDebugBuild && !isEaBuild && currentPackage != EXPECTED_PACKAGE) {
|
if (!isDebugBuild && !isEaBuild &&
|
||||||
|
currentPackage != EXPECTED_PACKAGE &&
|
||||||
|
currentPackage != ALTERNATE_PACKAGE &&
|
||||||
|
currentPackage != ALTERNATE_PACKAGE_2) {
|
||||||
showViolationDialog(activity)
|
showViolationDialog(activity)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
#include <frontend_common/content_manager.h>
|
#include <frontend_common/content_manager.h>
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include "common/android/multiplayer/multiplayer.h"
|
||||||
#include "common/android/android_common.h"
|
#include "common/android/android_common.h"
|
||||||
#include "common/android/id_cache.h"
|
#include "common/android/id_cache.h"
|
||||||
#include "common/detached_tasks.h"
|
#include "common/detached_tasks.h"
|
||||||
|
|
@ -870,4 +871,83 @@ jboolean Java_org_citron_citron_1emu_NativeLibrary_areKeysPresent(JNIEnv* env, j
|
||||||
return ContentManager::AreKeysPresent();
|
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"
|
} // extern "C"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
#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"
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?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"/>
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?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"/>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?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" />
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
<menu xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
tools:ignore="ExtraText">
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_pause_emulation"
|
android:id="@+id/menu_pause_emulation"
|
||||||
|
|
@ -21,6 +23,12 @@
|
||||||
android:icon="@drawable/ic_controller"
|
android:icon="@drawable/ic_controller"
|
||||||
android:title="@string/preferences_controls" />
|
android:title="@string/preferences_controls" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_multiplayer"
|
||||||
|
android:icon="@drawable/ic_multiplayer"
|
||||||
|
android:title="@string/multiplayer" />
|
||||||
|
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_overlay_controls"
|
android:id="@+id/menu_overlay_controls"
|
||||||
android:icon="@drawable/ic_overlay"
|
android:icon="@drawable/ic_overlay"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -23,6 +23,9 @@
|
||||||
<string name="keys">Keys</string>
|
<string name="keys">Keys</string>
|
||||||
<string name="keys_description">Select your <b>prod.keys</b> file with the button below.</string>
|
<string name="keys_description">Select your <b>prod.keys</b> file with the button below.</string>
|
||||||
<string name="select_keys">Select Keys</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">Games</string>
|
||||||
<string name="games_description">Select your <b>Games</b> folder with the button below.</string>
|
<string name="games_description">Select your <b>Games</b> folder with the button below.</string>
|
||||||
<string name="done">Done</string>
|
<string name="done">Done</string>
|
||||||
|
|
@ -105,11 +108,15 @@
|
||||||
<string name="import_saves">Import</string>
|
<string name="import_saves">Import</string>
|
||||||
<string name="export_saves">Export</string>
|
<string name="export_saves">Export</string>
|
||||||
<string name="install_firmware">Install firmware</string>
|
<string name="install_firmware">Install firmware</string>
|
||||||
<string name="install_firmware_description">Firmware must be in a ZIP archive and is needed to boot some games</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="firmware_installing">Installing firmware</string>
|
<string name="firmware_installing">Installing firmware</string>
|
||||||
<string name="firmware_installed_success">Firmware installed successfully</string>
|
<string name="firmware_installed_success">Firmware successfully installed</string>
|
||||||
<string name="firmware_installed_failure">Firmware installation failed</string>
|
<string name="firmware_installed_failure">Failed to install firmware</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="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="share_log">Share debug logs</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_description">Share citron\'s log file to debug issues</string>
|
||||||
<string name="share_log_missing">No log file found</string>
|
<string name="share_log_missing">No log file found</string>
|
||||||
|
|
@ -169,6 +176,14 @@
|
||||||
<string name="cabinet_restorer">Restorer</string>
|
<string name="cabinet_restorer">Restorer</string>
|
||||||
<string name="cabinet_formatter">Formatter</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 -->
|
<!-- About screen strings -->
|
||||||
<string name="gaia_is_not_real">Gaia isn\'t real</string>
|
<string name="gaia_is_not_real">Gaia isn\'t real</string>
|
||||||
<string name="copied_to_clipboard">Copied to clipboard</string>
|
<string name="copied_to_clipboard">Copied to clipboard</string>
|
||||||
|
|
@ -418,6 +433,76 @@
|
||||||
<string name="preferences_debug">Debug</string>
|
<string name="preferences_debug">Debug</string>
|
||||||
<string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</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 -->
|
<!-- Game properties -->
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="info_description">Program ID, developer, version</string>
|
<string name="info_description">Program ID, developer, version</string>
|
||||||
|
|
@ -1174,5 +1259,4 @@ 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
|
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.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "8.1.2" apply false
|
id("com.android.application") version "8.9.2" apply false
|
||||||
id("com.android.library") version "8.1.2" apply false
|
id("com.android.library") version "8.9.2" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
|
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,19 @@
|
||||||
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
## For more details on how to configure your build environment visit
|
||||||
# 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
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
#
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
org.gradle.jvmargs=-Xms512m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
# 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
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
kotlin.parallel.tasks.in.project=true
|
kotlin.parallel.tasks.in.project=true
|
||||||
android.defaults.buildfeatures.buildconfig=true
|
org.gradle.jvmargs=-Xms512m -Dkotlin.daemon.jvm.options\="-Xmx2048M" -Xmx2048M -XX\:MaxMetaspaceSize\=512m -XX\:+HeapDumpOnOutOfMemoryError -Dfile.encoding\=UTF-8
|
||||||
|
|
||||||
# Android Gradle plugin 8.0.2
|
|
||||||
android.suppressUnsupportedCompileSdk=34
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <typeinfo>
|
#include <typeinfo>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QSlider>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "common/settings_enums.h"
|
#include "common/settings_enums.h"
|
||||||
|
|
@ -38,7 +41,22 @@ ConfigureCpu::ConfigureCpu(const Core::System& system_,
|
||||||
|
|
||||||
ConfigureCpu::~ConfigureCpu() = default;
|
ConfigureCpu::~ConfigureCpu() = default;
|
||||||
|
|
||||||
void ConfigureCpu::SetConfiguration() {}
|
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::Setup(const ConfigurationShared::Builder& builder) {
|
void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
||||||
auto* accuracy_layout = ui->widget_accuracy->layout();
|
auto* accuracy_layout = ui->widget_accuracy->layout();
|
||||||
auto* backend_layout = ui->widget_backend->layout();
|
auto* backend_layout = ui->widget_backend->layout();
|
||||||
|
|
@ -99,6 +117,9 @@ void ConfigureCpu::ApplyConfiguration() {
|
||||||
for (const auto& apply_func : apply_funcs) {
|
for (const auto& apply_func : apply_funcs) {
|
||||||
apply_func(is_powered_on);
|
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) {
|
void ConfigureCpu::changeEvent(QEvent* event) {
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,67 @@
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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>
|
<item>
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
|
|
|
||||||
|
|
@ -423,6 +423,12 @@
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="6" column="0">
|
||||||
<widget class="QCheckBox" name="use_auto_stub">
|
<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">
|
<property name="text">
|
||||||
<string>Enable Auto-Stub**</string>
|
<string>Enable Auto-Stub**</string>
|
||||||
</property>
|
</property>
|
||||||
|
|
@ -430,6 +436,12 @@
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QCheckBox" name="quest_flag">
|
<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">
|
<property name="text">
|
||||||
<string>Kiosk (Quest) Mode</string>
|
<string>Kiosk (Quest) Mode</string>
|
||||||
</property>
|
</property>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include "citron/configuration/shared_translation.h"
|
#include "citron/configuration/shared_translation.h"
|
||||||
|
|
@ -146,6 +147,11 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
|
||||||
INSERT(
|
INSERT(
|
||||||
Settings, use_asynchronous_gpu_emulation, tr("Use asynchronous GPU emulation"),
|
Settings, use_asynchronous_gpu_emulation, tr("Use asynchronous GPU emulation"),
|
||||||
tr("Uses an extra CPU thread for rendering.\nThis option should always remain enabled."));
|
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:"),
|
INSERT(Settings, nvdec_emulation, tr("NVDEC emulation:"),
|
||||||
tr("Specifies how videos should be decoded.\nIt can either use the CPU or the GPU for "
|
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"
|
"decoding, or perform no decoding at all (black screen on videos).\n"
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ system_
|
||||||
DiscordEventHandlers handlers {};
|
DiscordEventHandlers handlers {};
|
||||||
// The number is the client ID for citron, it's used for images and the
|
// The number is the client ID for citron, it's used for images and the
|
||||||
// application name
|
// application name
|
||||||
Discord_Initialize("1322413013248118888", & handlers, 1, nullptr);
|
Discord_Initialize("1361252452329848892", & handlers, 1, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
DiscordImpl::~DiscordImpl() {
|
DiscordImpl::~DiscordImpl() {
|
||||||
|
|
|
||||||
|
|
@ -1539,7 +1539,6 @@ void GMainWindow::ConnectMenuEvents() {
|
||||||
connect_menu(ui->action_Stop, &GMainWindow::OnStopGame);
|
connect_menu(ui->action_Stop, &GMainWindow::OnStopGame);
|
||||||
connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility);
|
connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility);
|
||||||
connect_menu(ui->action_Open_Mods_Page, &GMainWindow::OnOpenModsPage);
|
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_Open_FAQ, &GMainWindow::OnOpenFAQ);
|
||||||
connect_menu(ui->action_Restart, &GMainWindow::OnRestartGame);
|
connect_menu(ui->action_Restart, &GMainWindow::OnRestartGame);
|
||||||
connect_menu(ui->action_Configure, &GMainWindow::OnConfigure);
|
connect_menu(ui->action_Configure, &GMainWindow::OnConfigure);
|
||||||
|
|
@ -1844,9 +1843,7 @@ bool GMainWindow::LoadROM(const QString& filename, Service::AM::FrontendAppletPa
|
||||||
tr("Error while loading ROM! %1", "%1 signifies a numeric error code.")
|
tr("Error while loading ROM! %1", "%1 signifies a numeric error code.")
|
||||||
.arg(QString::fromStdString(error_code));
|
.arg(QString::fromStdString(error_code));
|
||||||
const auto description =
|
const auto description =
|
||||||
tr("%1<br>Please follow <a href='https://citron-emu.org/help/quickstart/'>the "
|
tr("%1<br>This software is provided as-is without any warranty or support.<br>Please refer to community resources or documentation for assistance.",
|
||||||
"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.")
|
"%1 signifies an error string.")
|
||||||
.arg(QString::fromStdString(
|
.arg(QString::fromStdString(
|
||||||
GetResultStatusString(static_cast<Loader::ResultStatus>(error_id))));
|
GetResultStatusString(static_cast<Loader::ResultStatus>(error_id))));
|
||||||
|
|
@ -3578,10 +3575,6 @@ void GMainWindow::OnOpenModsPage() {
|
||||||
OpenURL(QUrl(QStringLiteral("https://git.citron-emu.org/Citron/Citron/wiki/Switch-Mods")));
|
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() {
|
void GMainWindow::OnOpenFAQ() {
|
||||||
OpenURL(QUrl(QStringLiteral("https://citron-emu.org/wiki/faq/")));
|
OpenURL(QUrl(QStringLiteral("https://citron-emu.org/wiki/faq/")));
|
||||||
}
|
}
|
||||||
|
|
@ -4787,19 +4780,24 @@ void GMainWindow::OnCheckFirmwareDecryption() {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GMainWindow::CheckFirmwarePresence() {
|
bool GMainWindow::CheckFirmwarePresence() {
|
||||||
constexpr u64 MiiEditId = static_cast<u64>(Service::AM::AppletProgramId::MiiEdit);
|
constexpr u64 MiiEditId = 0x0100000000001009; // Mii Edit applet ID
|
||||||
|
constexpr u64 QLaunchId = 0x0100000000001000; // Home Menu applet ID
|
||||||
|
|
||||||
auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
|
auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
|
||||||
if (!bis_system) {
|
if (!bis_system) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for essential system applets
|
||||||
auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program);
|
auto mii_applet_nca = bis_system->GetEntry(MiiEditId, FileSys::ContentRecordType::Program);
|
||||||
if (!mii_applet_nca) {
|
auto qlaunch_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program);
|
||||||
|
|
||||||
|
if (!mii_applet_nca || !qlaunch_nca) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// Also check for essential keys
|
||||||
|
return Core::Crypto::KeyManager::Instance().IsFirmwareAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::SetFirmwareVersion() {
|
void GMainWindow::SetFirmwareVersion() {
|
||||||
|
|
|
||||||
|
|
@ -336,7 +336,6 @@ private slots:
|
||||||
void OnPrepareForSleep(bool prepare_sleep);
|
void OnPrepareForSleep(bool prepare_sleep);
|
||||||
void OnMenuReportCompatibility();
|
void OnMenuReportCompatibility();
|
||||||
void OnOpenModsPage();
|
void OnOpenModsPage();
|
||||||
void OnOpenQuickstartGuide();
|
|
||||||
void OnOpenFAQ();
|
void OnOpenFAQ();
|
||||||
/// Called whenever a user selects a game in the game list widget.
|
/// Called whenever a user selects a game in the game list widget.
|
||||||
void OnGameListLoadFile(QString game_path, u64 program_id);
|
void OnGameListLoadFile(QString game_path, u64 program_id);
|
||||||
|
|
|
||||||
|
|
@ -360,11 +360,6 @@
|
||||||
<string>Open &Mods Page</string>
|
<string>Open &Mods Page</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_Open_Quickstart_Guide">
|
|
||||||
<property name="text">
|
|
||||||
<string>Open &Quickstart Guide</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="action_Open_FAQ">
|
<action name="action_Open_FAQ">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&FAQ</string>
|
<string>&FAQ</string>
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,8 @@ if(ANDROID)
|
||||||
android/android_common.h
|
android/android_common.h
|
||||||
android/id_cache.cpp
|
android/id_cache.cpp
|
||||||
android/id_cache.h
|
android/id_cache.h
|
||||||
|
android/multiplayer/multiplayer.cpp
|
||||||
|
android/multiplayer/multiplayer.h
|
||||||
android/applets/software_keyboard.cpp
|
android/applets/software_keyboard.cpp
|
||||||
android/applets/software_keyboard.h
|
android/applets/software_keyboard.h
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,15 @@ jstring ToJString(JNIEnv* env, std::string_view str) {
|
||||||
static_cast<jint>(converted_string.size()));
|
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) {
|
jstring ToJString(JNIEnv* env, std::u16string_view str) {
|
||||||
return ToJString(env, Common::UTF16ToUTF8(str));
|
return ToJString(env, Common::UTF16ToUTF8(str));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
|
@ -19,7 +21,7 @@ jobject ToJDouble(JNIEnv* env, double value);
|
||||||
|
|
||||||
s32 GetJInteger(JNIEnv* env, jobject jinteger);
|
s32 GetJInteger(JNIEnv* env, jobject jinteger);
|
||||||
jobject ToJInteger(JNIEnv* env, s32 value);
|
jobject ToJInteger(JNIEnv* env, s32 value);
|
||||||
|
jobjectArray ToJStringArray(JNIEnv* env, const std::vector<std::string>& strs);
|
||||||
bool GetJBoolean(JNIEnv* env, jobject jboolean);
|
bool GetJBoolean(JNIEnv* env, jobject jboolean);
|
||||||
jobject ToJBoolean(JNIEnv* env, bool value);
|
jobject ToJBoolean(JNIEnv* env, bool value);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/fs/fs_android.h"
|
#include "common/fs/fs_android.h"
|
||||||
#include "video_core/rasterizer_interface.h"
|
#include "video_core/rasterizer_interface.h"
|
||||||
|
#include "common/android/multiplayer/multiplayer.h"
|
||||||
|
#include <network/network.h>
|
||||||
|
|
||||||
|
|
||||||
static JavaVM* s_java_vm;
|
static JavaVM* s_java_vm;
|
||||||
static jclass s_native_library_class;
|
static jclass s_native_library_class;
|
||||||
|
|
@ -88,6 +91,8 @@ static jmethodID s_citron_input_device_get_supports_vibration;
|
||||||
static jmethodID s_citron_input_device_vibrate;
|
static jmethodID s_citron_input_device_vibrate;
|
||||||
static jmethodID s_citron_input_device_get_axes;
|
static jmethodID s_citron_input_device_get_axes;
|
||||||
static jmethodID s_citron_input_device_has_keys;
|
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;
|
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
|
||||||
|
|
||||||
|
|
@ -388,6 +393,15 @@ jmethodID GetCitronDeviceHasKeys() {
|
||||||
return s_citron_input_device_has_keys;
|
return s_citron_input_device_has_keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jmethodID GetAddNetPlayMessage() {
|
||||||
|
return s_add_netplay_message;
|
||||||
|
}
|
||||||
|
|
||||||
|
jmethodID ClearChat() {
|
||||||
|
return s_clear_chat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -547,6 +561,10 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
s_citron_input_device_has_keys =
|
s_citron_input_device_has_keys =
|
||||||
env->GetMethodID(citron_input_device_interface, "hasKeys", "([I)[Z");
|
env->GetMethodID(citron_input_device_interface, "hasKeys", "([I)[Z");
|
||||||
env->DeleteLocalRef(citron_input_device_interface);
|
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
|
// Initialize Android Storage
|
||||||
Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
|
Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
|
||||||
|
|
@ -582,6 +600,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
|
||||||
|
|
||||||
// UnInitialize applets
|
// UnInitialize applets
|
||||||
SoftwareKeyboard::CleanupJNI(env);
|
SoftwareKeyboard::CleanupJNI(env);
|
||||||
|
|
||||||
|
NetworkShutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
#include <future>
|
#include <future>
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
#include <network/network.h>
|
||||||
|
|
||||||
#include "video_core/rasterizer_interface.h"
|
#include "video_core/rasterizer_interface.h"
|
||||||
|
|
||||||
|
|
@ -108,5 +109,6 @@ jmethodID GetCitronDeviceGetSupportsVibration();
|
||||||
jmethodID GetCitronDeviceVibrate();
|
jmethodID GetCitronDeviceVibrate();
|
||||||
jmethodID GetCitronDeviceGetAxes();
|
jmethodID GetCitronDeviceGetAxes();
|
||||||
jmethodID GetCitronDeviceHasKeys();
|
jmethodID GetCitronDeviceHasKeys();
|
||||||
|
jmethodID GetAddNetPlayMessage();
|
||||||
} // namespace Common::Android
|
jmethodID ClearChat();
|
||||||
|
} // namespace Android
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,350 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
// 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);
|
||||||
|
|
@ -35,7 +35,6 @@ struct RoomInformation {
|
||||||
u16 port; ///< The port of this room
|
u16 port; ///< The port of this room
|
||||||
GameInfo preferred_game; ///< Game to advertise that you want to play
|
GameInfo preferred_game; ///< Game to advertise that you want to play
|
||||||
std::string host_username; ///< Forum username of the host
|
std::string host_username; ///< Forum username of the host
|
||||||
bool enable_citron_mods; ///< Allow citron Moderators to moderate on this room
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Room {
|
struct Room {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -89,6 +90,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
|
||||||
SUB(Service, BGTC) \
|
SUB(Service, BGTC) \
|
||||||
SUB(Service, BTDRV) \
|
SUB(Service, BTDRV) \
|
||||||
SUB(Service, BTM) \
|
SUB(Service, BTM) \
|
||||||
|
SUB(Service, BSD) \
|
||||||
SUB(Service, Capture) \
|
SUB(Service, Capture) \
|
||||||
SUB(Service, ERPT) \
|
SUB(Service, ERPT) \
|
||||||
SUB(Service, ETicket) \
|
SUB(Service, ETicket) \
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -57,6 +58,7 @@ enum class Class : u8 {
|
||||||
Service_BPC, ///< The BPC service
|
Service_BPC, ///< The BPC service
|
||||||
Service_BTDRV, ///< The Bluetooth driver service
|
Service_BTDRV, ///< The Bluetooth driver service
|
||||||
Service_BTM, ///< The BTM service
|
Service_BTM, ///< The BTM service
|
||||||
|
Service_BSD, ///< The BSD sockets service
|
||||||
Service_Capture, ///< The capture service
|
Service_Capture, ///< The capture service
|
||||||
Service_ERPT, ///< The error reporting service
|
Service_ERPT, ///< The error reporting service
|
||||||
Service_ETicket, ///< The ETicket service
|
Service_ETicket, ///< The ETicket service
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -198,6 +199,7 @@ struct Values {
|
||||||
MemoryLayout::Memory_12Gb,
|
MemoryLayout::Memory_12Gb,
|
||||||
"memory_layout_mode",
|
"memory_layout_mode",
|
||||||
Category::Core};
|
Category::Core};
|
||||||
|
SwitchableSetting<u32> cpu_clock_rate{linkage, 1'020'000'000, "cpu_clock_rate", Category::Cpu};
|
||||||
SwitchableSetting<bool> use_speed_limit{
|
SwitchableSetting<bool> use_speed_limit{
|
||||||
linkage, true, "use_speed_limit", Category::Core, Specialization::Paired, false, true};
|
linkage, true, "use_speed_limit", Category::Core, Specialization::Paired, false, true};
|
||||||
SwitchableSetting<u16, true> speed_limit{linkage,
|
SwitchableSetting<u16, true> speed_limit{linkage,
|
||||||
|
|
@ -210,6 +212,11 @@ struct Values {
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
&use_speed_limit};
|
&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
|
// Cpu
|
||||||
SwitchableSetting<CpuBackend, true> cpu_backend{linkage,
|
SwitchableSetting<CpuBackend, true> cpu_backend{linkage,
|
||||||
|
|
@ -278,6 +285,8 @@ struct Values {
|
||||||
Category::Renderer};
|
Category::Renderer};
|
||||||
SwitchableSetting<bool> use_asynchronous_gpu_emulation{
|
SwitchableSetting<bool> use_asynchronous_gpu_emulation{
|
||||||
linkage, true, "use_asynchronous_gpu_emulation", Category::Renderer};
|
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,
|
SwitchableSetting<AstcDecodeMode, true> accelerate_astc{linkage,
|
||||||
#ifdef ANDROID
|
#ifdef ANDROID
|
||||||
AstcDecodeMode::Cpu,
|
AstcDecodeMode::Cpu,
|
||||||
|
|
@ -392,11 +401,11 @@ struct Values {
|
||||||
Category::RendererAdvanced};
|
Category::RendererAdvanced};
|
||||||
SwitchableSetting<bool> async_presentation{linkage,
|
SwitchableSetting<bool> async_presentation{linkage,
|
||||||
#ifdef ANDROID
|
#ifdef ANDROID
|
||||||
true,
|
false, // Disabled due to crashes
|
||||||
#else
|
#else
|
||||||
false,
|
false, // Disabled due to crashes
|
||||||
#endif
|
#endif
|
||||||
"async_presentation", Category::RendererAdvanced};
|
"async_presentation", Category::RendererAdvanced}; // Hide from UI
|
||||||
SwitchableSetting<bool> renderer_force_max_clock{linkage, false, "force_max_clock",
|
SwitchableSetting<bool> renderer_force_max_clock{linkage, false, "force_max_clock",
|
||||||
Category::RendererAdvanced};
|
Category::RendererAdvanced};
|
||||||
SwitchableSetting<bool> use_reactive_flushing{linkage,
|
SwitchableSetting<bool> use_reactive_flushing{linkage,
|
||||||
|
|
@ -618,11 +627,21 @@ struct Values {
|
||||||
|
|
||||||
// Add-Ons
|
// Add-Ons
|
||||||
std::map<u64, std::vector<std::string>> disabled_addons;
|
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;
|
extern Values values;
|
||||||
|
|
||||||
void UpdateGPUAccuracy();
|
void UpdateGPUAccuracy();
|
||||||
|
// boold isGPULevelNormal();
|
||||||
|
// TODO: ZEP
|
||||||
bool IsGPULevelExtreme();
|
bool IsGPULevelExtreme();
|
||||||
bool IsGPULevelHigh();
|
bool IsGPULevelHigh();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -8,6 +9,7 @@
|
||||||
#include <ratio>
|
#include <ratio>
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
#include "core/hardware_properties.h"
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
|
|
||||||
|
|
@ -15,7 +17,10 @@ class WallClock {
|
||||||
public:
|
public:
|
||||||
static constexpr u64 CNTFRQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
|
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
|
static constexpr u64 GPUTickFreq = 614'400'000; // GM20B GPU Tick Frequency = 614.4 MHz
|
||||||
static constexpr u64 CPUTickFreq = 1'020'000'000; // T210/4 A57 CPU Tick Frequency = 1020.0 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
|
||||||
|
|
||||||
virtual ~WallClock() = default;
|
virtual ~WallClock() = default;
|
||||||
|
|
||||||
|
|
@ -76,12 +81,28 @@ protected:
|
||||||
using NsToCNTPCTRatio = std::ratio<CNTFRQ, std::nano::den>;
|
using NsToCNTPCTRatio = std::ratio<CNTFRQ, std::nano::den>;
|
||||||
using NsToGPUTickRatio = std::ratio<GPUTickFreq, std::nano::den>;
|
using NsToGPUTickRatio = std::ratio<GPUTickFreq, std::nano::den>;
|
||||||
|
|
||||||
// Cycle Timing
|
// Cycle Timing - using functions for dynamic values
|
||||||
|
|
||||||
using CPUTickToNsRatio = std::ratio<std::nano::den, CPUTickFreq>;
|
// Update these to use functions instead of constexpr
|
||||||
using CPUTickToUsRatio = std::ratio<std::micro::den, CPUTickFreq>;
|
struct CPUTickToNsRatio {
|
||||||
using CPUTickToCNTPCTRatio = std::ratio<CNTFRQ, CPUTickFreq>;
|
static inline std::intmax_t num = std::nano::den;
|
||||||
using CPUTickToGPUTickRatio = std::ratio<GPUTickFreq, CPUTickFreq>;
|
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();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<WallClock> CreateOptimalClock();
|
std::unique_ptr<WallClock> CreateOptimalClock();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
||||||
|
# SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
add_library(core STATIC
|
add_library(core STATIC
|
||||||
|
|
@ -776,8 +777,12 @@ add_library(core STATIC
|
||||||
hle/service/ngc/ngc.h
|
hle/service/ngc/ngc.h
|
||||||
hle/service/nifm/nifm.cpp
|
hle/service/nifm/nifm.cpp
|
||||||
hle/service/nifm/nifm.h
|
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.cpp
|
||||||
hle/service/nim/nim.h
|
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.cpp
|
||||||
hle/service/npns/npns.h
|
hle/service/npns/npns.h
|
||||||
hle/service/ns/account_proxy_interface.cpp
|
hle/service/ns/account_proxy_interface.cpp
|
||||||
|
|
@ -1045,6 +1050,14 @@ add_library(core STATIC
|
||||||
hle/service/sm/sm_controller.h
|
hle/service/sm/sm_controller.h
|
||||||
hle/service/sockets/bsd.cpp
|
hle/service/sockets/bsd.cpp
|
||||||
hle/service/sockets/bsd.h
|
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.cpp
|
||||||
hle/service/sockets/nsd.h
|
hle/service/sockets/nsd.h
|
||||||
hle/service/sockets/sfdnsres.cpp
|
hle/service/sockets/sfdnsres.cpp
|
||||||
|
|
@ -1053,6 +1066,8 @@ add_library(core STATIC
|
||||||
hle/service/sockets/sockets.h
|
hle/service/sockets/sockets.h
|
||||||
hle/service/sockets/sockets_translate.cpp
|
hle/service/sockets/sockets_translate.cpp
|
||||||
hle/service/sockets/sockets_translate.h
|
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.cpp
|
||||||
hle/service/spl/csrng.h
|
hle/service/spl/csrng.h
|
||||||
hle/service/spl/spl.cpp
|
hle/service/spl/spl.cpp
|
||||||
|
|
|
||||||
|
|
@ -1376,4 +1376,31 @@ bool KeyManager::AddTicket(const Ticket& ticket) {
|
||||||
SetKey(S128KeyType::Titlekey, key.value(), rights_id[1], rights_id[0]);
|
SetKey(S128KeyType::Titlekey, key.value(), rights_id[1], rights_id[0]);
|
||||||
return true;
|
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
|
} // namespace Core::Crypto
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,9 @@ public:
|
||||||
void ReloadKeys();
|
void ReloadKeys();
|
||||||
bool AreKeysLoaded() const;
|
bool AreKeysLoaded() const;
|
||||||
|
|
||||||
|
// Check if firmware is installed by verifying essential keys
|
||||||
|
bool IsFirmwareAvailable() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
KeyManager();
|
KeyManager();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
#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();
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -8,12 +9,13 @@
|
||||||
|
|
||||||
#include "common/bit_util.h"
|
#include "common/bit_util.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
#include "common/settings.h"
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
|
|
||||||
namespace Hardware {
|
namespace Hardware {
|
||||||
|
|
||||||
constexpr u64 BASE_CLOCK_RATE = 1'020'000'000; // Default CPU Frequency = 1020 MHz
|
inline u64 BASE_CLOCK_RATE() { return Settings::values.cpu_clock_rate.GetValue(); } // Default CPU Frequency set in settings, defaults to 1020 MHz
|
||||||
constexpr u64 CNTFREQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
|
constexpr u64 CNTFREQ = 19'200'000; // CNTPCT_EL0 Frequency = 19.2 MHz
|
||||||
constexpr u32 NUM_CPU_CORES = 4; // Number of CPU Cores
|
constexpr u32 NUM_CPU_CORES = 4; // Number of CPU Cores
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -81,8 +82,8 @@ PerformanceConfiguration Controller::GetCurrentPerformanceConfiguration(Performa
|
||||||
|
|
||||||
void Controller::SetClockSpeed(u32 mhz) {
|
void Controller::SetClockSpeed(u32 mhz) {
|
||||||
LOG_DEBUG(Service_APM, "called, mhz={:08X}", mhz);
|
LOG_DEBUG(Service_APM, "called, mhz={:08X}", mhz);
|
||||||
// TODO(DarkLordZach): Actually signal core_timing to change clock speed.
|
// Update the clock rate setting with the provided MHz value (convert to Hz)
|
||||||
// TODO(Rodrigo): Remove [[maybe_unused]] when core_timing is used.
|
Settings::values.cpu_clock_rate = mhz * 1'000'000;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Service::APM
|
} // namespace Service::APM
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
|
@ -28,12 +29,12 @@ public:
|
||||||
{10102, nullptr, "UpdateFriendInfo"},
|
{10102, nullptr, "UpdateFriendInfo"},
|
||||||
{10110, nullptr, "GetFriendProfileImage"},
|
{10110, nullptr, "GetFriendProfileImage"},
|
||||||
{10120, &IFriendService::CheckFriendListAvailability, "CheckFriendListAvailability"},
|
{10120, &IFriendService::CheckFriendListAvailability, "CheckFriendListAvailability"},
|
||||||
{10121, nullptr, "EnsureFriendListAvailable"},
|
{10121, &IFriendService::EnsureFriendListAvailable, "EnsureFriendListAvailable"},
|
||||||
{10200, nullptr, "SendFriendRequestForApplication"},
|
{10200, nullptr, "SendFriendRequestForApplication"},
|
||||||
{10211, nullptr, "AddFacedFriendRequestForApplication"},
|
{10211, nullptr, "AddFacedFriendRequestForApplication"},
|
||||||
{10400, &IFriendService::GetBlockedUserListIds, "GetBlockedUserListIds"},
|
{10400, &IFriendService::GetBlockedUserListIds, "GetBlockedUserListIds"},
|
||||||
{10420, &IFriendService::CheckBlockedUserListAvailability, "CheckBlockedUserListAvailability"},
|
{10420, &IFriendService::CheckBlockedUserListAvailability, "CheckBlockedUserListAvailability"},
|
||||||
{10421, nullptr, "EnsureBlockedUserListAvailable"},
|
{10421, &IFriendService::EnsureBlockedUserListAvailable, "EnsureBlockedUserListAvailable"},
|
||||||
{10500, nullptr, "GetProfileList"},
|
{10500, nullptr, "GetProfileList"},
|
||||||
{10600, nullptr, "DeclareOpenOnlinePlaySession"},
|
{10600, nullptr, "DeclareOpenOnlinePlaySession"},
|
||||||
{10601, &IFriendService::DeclareCloseOnlinePlaySession, "DeclareCloseOnlinePlaySession"},
|
{10601, &IFriendService::DeclareCloseOnlinePlaySession, "DeclareCloseOnlinePlaySession"},
|
||||||
|
|
@ -166,11 +167,27 @@ private:
|
||||||
|
|
||||||
LOG_WARNING(Service_Friend, "(STUBBED) called, uuid=0x{}", uuid.RawString());
|
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};
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.Push(true);
|
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) {
|
void GetBlockedUserListIds(HLERequestContext& ctx) {
|
||||||
// This is safe to stub, as there should be no adverse consequences from reporting no
|
// This is safe to stub, as there should be no adverse consequences from reporting no
|
||||||
// blocked users.
|
// blocked users.
|
||||||
|
|
@ -186,11 +203,45 @@ private:
|
||||||
|
|
||||||
LOG_WARNING(Service_Friend, "(STUBBED) called, uuid=0x{}", uuid.RawString());
|
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};
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.Push(true);
|
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) {
|
void DeclareCloseOnlinePlaySession(HLERequestContext& ctx) {
|
||||||
// Stub used by Splatoon 2
|
// Stub used by Splatoon 2
|
||||||
LOG_WARNING(Service_Friend, "(STUBBED) called");
|
LOG_WARNING(Service_Friend, "(STUBBED) called");
|
||||||
|
|
@ -248,14 +299,6 @@ private:
|
||||||
rb.Push(ResultSuccess);
|
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;
|
KernelHelpers::ServiceContext service_context;
|
||||||
|
|
||||||
Kernel::KEvent* completion_event;
|
Kernel::KEvent* completion_event;
|
||||||
|
|
@ -287,6 +330,9 @@ private:
|
||||||
void GetEvent(HLERequestContext& ctx) {
|
void GetEvent(HLERequestContext& ctx) {
|
||||||
LOG_DEBUG(Service_Friend, "called");
|
LOG_DEBUG(Service_Friend, "called");
|
||||||
|
|
||||||
|
// Signal the notification event to unblock any waiting threads
|
||||||
|
notification_event->Signal();
|
||||||
|
|
||||||
IPC::ResponseBuilder rb{ctx, 2, 1};
|
IPC::ResponseBuilder rb{ctx, 2, 1};
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.PushCopyObjects(notification_event->GetReadableEvent());
|
rb.PushCopyObjects(notification_event->GetReadableEvent());
|
||||||
|
|
@ -363,10 +409,11 @@ private:
|
||||||
};
|
};
|
||||||
|
|
||||||
void Module::Interface::CreateFriendService(HLERequestContext& ctx) {
|
void Module::Interface::CreateFriendService(HLERequestContext& ctx) {
|
||||||
|
LOG_DEBUG(Service_Friend, "CreateFriendService called");
|
||||||
|
|
||||||
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
|
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
|
||||||
rb.Push(ResultSuccess);
|
rb.Push(ResultSuccess);
|
||||||
rb.PushIpcInterface<IFriendService>(system);
|
rb.PushIpcInterface<IFriendService>(system);
|
||||||
LOG_DEBUG(Service_Friend, "called");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Module::Interface::CreateNotificationService(HLERequestContext& ctx) {
|
void Module::Interface::CreateNotificationService(HLERequestContext& ctx) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
|
|
@ -6,6 +7,7 @@
|
||||||
#include "core/hle/service/ipc_helpers.h"
|
#include "core/hle/service/ipc_helpers.h"
|
||||||
#include "core/hle/service/kernel_helpers.h"
|
#include "core/hle/service/kernel_helpers.h"
|
||||||
#include "core/hle/service/nifm/nifm.h"
|
#include "core/hle/service/nifm/nifm.h"
|
||||||
|
#include "core/hle/service/nifm/nifm_utils.h"
|
||||||
#include "core/hle/service/server_manager.h"
|
#include "core/hle/service/server_manager.h"
|
||||||
#include "network/network.h"
|
#include "network/network.h"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
// 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
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
// 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
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
|
@ -8,6 +9,7 @@
|
||||||
#include "core/hle/service/ipc_helpers.h"
|
#include "core/hle/service/ipc_helpers.h"
|
||||||
#include "core/hle/service/kernel_helpers.h"
|
#include "core/hle/service/kernel_helpers.h"
|
||||||
#include "core/hle/service/nim/nim.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/server_manager.h"
|
||||||
#include "core/hle/service/service.h"
|
#include "core/hle/service/service.h"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
// 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
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
// 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
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include <boost/container/small_vector.hpp>
|
#include <boost/container/small_vector.hpp>
|
||||||
|
|
||||||
#include "common/microprofile.h"
|
#include "common/microprofile.h"
|
||||||
|
#include "common/settings.h"
|
||||||
#include "core/hle/service/nvdrv/devices/nvdisp_disp0.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.h"
|
||||||
#include "core/hle/service/nvnflinger/buffer_item_consumer.h"
|
#include "core/hle/service/nvnflinger/buffer_item_consumer.h"
|
||||||
|
|
@ -21,19 +23,20 @@ s32 NormalizeSwapInterval(f32* out_speed_scale, s32 swap_interval) {
|
||||||
if (out_speed_scale) {
|
if (out_speed_scale) {
|
||||||
*out_speed_scale = 2.f * static_cast<f32>(1 - swap_interval);
|
*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) {
|
if (swap_interval >= 5) {
|
||||||
// As an extension, treat high swap interval as precise speed control.
|
// As an extension, treat high swap interval as precise speed control.
|
||||||
if (out_speed_scale) {
|
if (out_speed_scale) {
|
||||||
*out_speed_scale = static_cast<f32>(swap_interval) / 100.f;
|
*out_speed_scale = static_cast<f32>(swap_interval) / 100.f;
|
||||||
}
|
}
|
||||||
|
|
||||||
swap_interval = 1;
|
swap_interval = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return swap_interval;
|
return swap_interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
|
@ -479,6 +480,122 @@ void BSD::EventFd(HLERequestContext& ctx) {
|
||||||
BuildErrnoResponse(ctx, Errno::SUCCESS);
|
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>
|
template <typename Work>
|
||||||
void BSD::ExecuteWork(HLERequestContext& ctx, Work work) {
|
void BSD::ExecuteWork(HLERequestContext& ctx, Work work) {
|
||||||
work.Execute(this);
|
work.Execute(this);
|
||||||
|
|
@ -508,9 +625,9 @@ std::pair<s32, Errno> BSD::SocketImpl(Domain domain, Type type, Protocol protoco
|
||||||
|
|
||||||
LOG_INFO(Service, "New socket fd={}", fd);
|
LOG_INFO(Service, "New socket fd={}", fd);
|
||||||
|
|
||||||
auto room_member = room_network.GetRoomMember().lock();
|
auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
|
||||||
if (room_member && room_member->IsConnected()) {
|
if (room_member && room_member->IsConnected()) {
|
||||||
descriptor.socket = std::make_shared<Network::ProxySocket>(room_network);
|
descriptor.socket = std::make_shared<Network::ProxySocket>(system.GetRoomNetwork());
|
||||||
} else {
|
} else {
|
||||||
descriptor.socket = std::make_shared<Network::Socket>();
|
descriptor.socket = std::make_shared<Network::Socket>();
|
||||||
}
|
}
|
||||||
|
|
@ -960,27 +1077,41 @@ void BSD::BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noex
|
||||||
}
|
}
|
||||||
|
|
||||||
void BSD::OnProxyPacketReceived(const Network::ProxyPacket& packet) {
|
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) {
|
for (auto& optional_descriptor : file_descriptors) {
|
||||||
if (!optional_descriptor.has_value()) {
|
if (!optional_descriptor.has_value()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
FileDescriptor& descriptor = *optional_descriptor;
|
FileDescriptor& descriptor = *optional_descriptor;
|
||||||
descriptor.socket.get()->HandleProxyPacket(packet);
|
if (descriptor.socket) {
|
||||||
|
descriptor.socket->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)
|
BSD::BSD(Core::System& system_, const char* name)
|
||||||
: ServiceFramework{system_, name}, room_network{system_.GetRoomNetwork()} {
|
: ServiceFramework{system_, name} {
|
||||||
// clang-format off
|
// clang-format off
|
||||||
static const FunctionInfo functions[] = {
|
static const FunctionInfo functions[] = {
|
||||||
{0, &BSD::RegisterClient, "RegisterClient"},
|
{0, &BSD::RegisterClient, "RegisterClient"},
|
||||||
{1, &BSD::StartMonitoring, "StartMonitoring"},
|
{1, &BSD::StartMonitoring, "StartMonitoring"},
|
||||||
{2, &BSD::Socket, "Socket"},
|
{2, &BSD::Socket, "Socket"},
|
||||||
{3, nullptr, "SocketExempt"},
|
{3, &BSD::SocketExempt, "SocketExempt"},
|
||||||
{4, nullptr, "Open"},
|
{4, &BSD::Open, "Open"},
|
||||||
{5, &BSD::Select, "Select"},
|
{5, &BSD::Select, "Select"},
|
||||||
{6, &BSD::Poll, "Poll"},
|
{6, &BSD::Poll, "Poll"},
|
||||||
{7, nullptr, "Sysctl"},
|
{7, &BSD::Sysctl, "Sysctl"},
|
||||||
{8, &BSD::Recv, "Recv"},
|
{8, &BSD::Recv, "Recv"},
|
||||||
{9, &BSD::RecvFrom, "RecvFrom"},
|
{9, &BSD::RecvFrom, "RecvFrom"},
|
||||||
{10, &BSD::Send, "Send"},
|
{10, &BSD::Send, "Send"},
|
||||||
|
|
@ -992,27 +1123,32 @@ BSD::BSD(Core::System& system_, const char* name)
|
||||||
{16, &BSD::GetSockName, "GetSockName"},
|
{16, &BSD::GetSockName, "GetSockName"},
|
||||||
{17, &BSD::GetSockOpt, "GetSockOpt"},
|
{17, &BSD::GetSockOpt, "GetSockOpt"},
|
||||||
{18, &BSD::Listen, "Listen"},
|
{18, &BSD::Listen, "Listen"},
|
||||||
{19, nullptr, "Ioctl"},
|
{19, &BSD::Ioctl, "Ioctl"},
|
||||||
{20, &BSD::Fcntl, "Fcntl"},
|
{20, &BSD::Fcntl, "Fcntl"},
|
||||||
{21, &BSD::SetSockOpt, "SetSockOpt"},
|
{21, &BSD::SetSockOpt, "SetSockOpt"},
|
||||||
{22, &BSD::Shutdown, "Shutdown"},
|
{22, &BSD::Shutdown, "Shutdown"},
|
||||||
{23, nullptr, "ShutdownAllSockets"},
|
{23, &BSD::ShutdownAllSockets, "ShutdownAllSockets"},
|
||||||
{24, &BSD::Write, "Write"},
|
{24, &BSD::Write, "Write"},
|
||||||
{25, &BSD::Read, "Read"},
|
{25, &BSD::Read, "Read"},
|
||||||
{26, &BSD::Close, "Close"},
|
{26, &BSD::Close, "Close"},
|
||||||
{27, &BSD::DuplicateSocket, "DuplicateSocket"},
|
{27, &BSD::DuplicateSocket, "DuplicateSocket"},
|
||||||
{28, nullptr, "GetResourceStatistics"},
|
{28, &BSD::GetResourceStatistics, "GetResourceStatistics"},
|
||||||
{29, nullptr, "RecvMMsg"},
|
{29, &BSD::RecvMMsg, "RecvMMsg"},
|
||||||
{30, nullptr, "SendMMsg"},
|
{30, &BSD::SendMMsg, "SendMMsg"},
|
||||||
{31, &BSD::EventFd, "EventFd"},
|
{31, &BSD::EventFd, "EventFd"},
|
||||||
{32, nullptr, "RegisterResourceStatisticsName"},
|
{32, &BSD::RegisterResourceStatisticsName, "RegisterResourceStatisticsName"},
|
||||||
{33, nullptr, "Initialize2"},
|
{33, &BSD::RegisterClientShared, "RegisterClientShared"},
|
||||||
|
{34, &BSD::GetSocketStatistics, "GetSocketStatistics"},
|
||||||
|
{35, &BSD::NifIoctl, "NifIoctl"},
|
||||||
|
{200, &BSD::SetThreadCoreMask, "SetThreadCoreMask"},
|
||||||
|
{201, &BSD::GetThreadCoreMask, "GetThreadCoreMask"},
|
||||||
};
|
};
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
RegisterHandlers(functions);
|
RegisterHandlers(functions);
|
||||||
|
|
||||||
if (auto room_member = room_network.GetRoomMember().lock()) {
|
auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
|
||||||
|
if (room_member) {
|
||||||
proxy_packet_received = room_member->BindOnProxyPacketReceived(
|
proxy_packet_received = room_member->BindOnProxyPacketReceived(
|
||||||
[this](const Network::ProxyPacket& packet) { OnProxyPacketReceived(packet); });
|
[this](const Network::ProxyPacket& packet) { OnProxyPacketReceived(packet); });
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1021,7 +1157,8 @@ BSD::BSD(Core::System& system_, const char* name)
|
||||||
}
|
}
|
||||||
|
|
||||||
BSD::~BSD() {
|
BSD::~BSD() {
|
||||||
if (auto room_member = room_network.GetRoomMember().lock()) {
|
auto room_member = system.GetRoomNetwork().GetRoomMember().lock();
|
||||||
|
if (room_member) {
|
||||||
room_member->Unbind(proxy_packet_received);
|
room_member->Unbind(proxy_packet_received);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1031,31 +1168,4 @@ std::unique_lock<std::mutex> BSD::LockService() {
|
||||||
return {};
|
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
|
} // namespace Service::Sockets
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
@ -37,6 +38,9 @@ public:
|
||||||
Errno CloseImpl(s32 fd);
|
Errno CloseImpl(s32 fd);
|
||||||
std::optional<std::shared_ptr<Network::SocketBase>> GetSocket(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:
|
private:
|
||||||
/// Maximum number of file descriptors
|
/// Maximum number of file descriptors
|
||||||
static constexpr size_t MAX_FD = 128;
|
static constexpr size_t MAX_FD = 128;
|
||||||
|
|
@ -124,11 +128,30 @@ private:
|
||||||
Errno bsd_errno{};
|
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 RegisterClient(HLERequestContext& ctx);
|
||||||
void StartMonitoring(HLERequestContext& ctx);
|
void StartMonitoring(HLERequestContext& ctx);
|
||||||
void Socket(HLERequestContext& ctx);
|
void Socket(HLERequestContext& ctx);
|
||||||
|
void SocketExempt(HLERequestContext& ctx);
|
||||||
|
void Open(HLERequestContext& ctx);
|
||||||
void Select(HLERequestContext& ctx);
|
void Select(HLERequestContext& ctx);
|
||||||
void Poll(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 Accept(HLERequestContext& ctx);
|
||||||
void Bind(HLERequestContext& ctx);
|
void Bind(HLERequestContext& ctx);
|
||||||
void Connect(HLERequestContext& ctx);
|
void Connect(HLERequestContext& ctx);
|
||||||
|
|
@ -136,18 +159,25 @@ private:
|
||||||
void GetSockName(HLERequestContext& ctx);
|
void GetSockName(HLERequestContext& ctx);
|
||||||
void GetSockOpt(HLERequestContext& ctx);
|
void GetSockOpt(HLERequestContext& ctx);
|
||||||
void Listen(HLERequestContext& ctx);
|
void Listen(HLERequestContext& ctx);
|
||||||
|
void Ioctl(HLERequestContext& ctx);
|
||||||
void Fcntl(HLERequestContext& ctx);
|
void Fcntl(HLERequestContext& ctx);
|
||||||
void SetSockOpt(HLERequestContext& ctx);
|
void SetSockOpt(HLERequestContext& ctx);
|
||||||
void Shutdown(HLERequestContext& ctx);
|
void Shutdown(HLERequestContext& ctx);
|
||||||
void Recv(HLERequestContext& ctx);
|
void ShutdownAllSockets(HLERequestContext& ctx);
|
||||||
void RecvFrom(HLERequestContext& ctx);
|
|
||||||
void Send(HLERequestContext& ctx);
|
|
||||||
void SendTo(HLERequestContext& ctx);
|
|
||||||
void Write(HLERequestContext& ctx);
|
void Write(HLERequestContext& ctx);
|
||||||
void Read(HLERequestContext& ctx);
|
void Read(HLERequestContext& ctx);
|
||||||
void Close(HLERequestContext& ctx);
|
void Close(HLERequestContext& ctx);
|
||||||
void DuplicateSocket(HLERequestContext& ctx);
|
void DuplicateSocket(HLERequestContext& ctx);
|
||||||
|
void GetResourceStatistics(HLERequestContext& ctx);
|
||||||
|
void RecvMMsg(HLERequestContext& ctx);
|
||||||
|
void SendMMsg(HLERequestContext& ctx);
|
||||||
void EventFd(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>
|
template <typename Work>
|
||||||
void ExecuteWork(HLERequestContext& ctx, Work work);
|
void ExecuteWork(HLERequestContext& ctx, Work work);
|
||||||
|
|
@ -171,30 +201,23 @@ private:
|
||||||
std::pair<s32, Errno> SendImpl(s32 fd, u32 flags, std::span<const u8> message);
|
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::pair<s32, Errno> SendToImpl(s32 fd, u32 flags, std::span<const u8> message,
|
||||||
std::span<const u8> addr);
|
std::span<const u8> addr);
|
||||||
|
|
||||||
s32 FindFreeFileDescriptorHandle() noexcept;
|
s32 FindFreeFileDescriptorHandle() noexcept;
|
||||||
bool IsFileDescriptorValid(s32 fd) const noexcept;
|
bool IsFileDescriptorValid(s32 fd) const noexcept;
|
||||||
|
|
||||||
void BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) 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);
|
void OnProxyPacketReceived(const Network::ProxyPacket& packet);
|
||||||
|
|
||||||
// Callback identifier for the OnProxyPacketReceived event.
|
|
||||||
Network::RoomMember::CallbackHandle<Network::ProxyPacket> proxy_packet_received;
|
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:
|
protected:
|
||||||
virtual std::unique_lock<std::mutex> LockService() override;
|
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
|
} // namespace Service::Sockets
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
// 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
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
// 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
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
// 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
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
// 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
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// 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
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// 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
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// 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
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
// 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
Loading…
Reference in New Issue