Compare commits

...

91 Commits

Author SHA1 Message Date
Zephyron 57cf5a0daf build: bump VulkanHeaders minimum version
- Update required VulkanHeaders from 1.4.307 to 1.4.313
- Ensures compatibility with newer Vulkan development packages

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-27 13:59:49 +10:00
Zephyron 020492f1fa chore: update vcpkg baseline
- Update vcpkg builtin-baseline from c82f74 to bc99451
- Provides newer Boost libraries with io_context support
- Ensures consistent Boost ASIO compatibility across platforms

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-27 13:58:58 +10:00
Zephyron 21ca0b3119 fix: update deprecated boost::asio::io_service to io_context
Updates UDP client and related test files to use boost::asio::io_context
instead of the deprecated io_service. This change is required for compatibility
with newer versions of Boost ASIO, which has renamed the class.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-27 13:57:27 +10:00
Zephyron 58401f5b39 fix: remove invalid WSAEBUSY Windows socket error code
- Fixes Windows compilation error by removing the WSAEBUSY case in TranslateNativeError.
- This error code does not exist in the Windows Sockets API as documented in the Microsoft documentation, but was incorrectly included in the Windows-specific error handling code.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-27 13:38:35 +10:00
Zephyron 1ad69f3545 Update submodules: SDL, vcpkg, and Vulkan-Headers
- Update SDL to fix pipewire-related compile error
  - Removes need to hardcode -DSDL_PIPEWIRE=OFF in toolchain
- Update vcpkg to latest version
- Update Vulkan-Headers to latest version

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-27 13:27:21 +10:00
Zephyron 48eed78d1a socket: Implement missing errno values and improve network error handling
Add support for missing errno values needed by TOTK:
- Add BUSY (16) for "Device or resource busy" errors
- Add NOTSOCK (88) for "Socket operation on non-socket" errors

Improvements:
- Update TranslateNativeError on both Windows and Unix to handle new error codes
- Change socket error logging for NOTSOCK from WARNING to DEBUG level
- Fix formatting in Unix errno translation code
- Update shader storage buffer tracking range to accommodate TOTK buffers
- Add hex format to storage buffer logging for easier comparison with bias range
- Change storage buffer tracking log level from WARNING to DEBUG

These changes help prevent error messages in games
that use network features not fully implemented in the emulator yet.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-25 14:38:28 +10:00
Zephyron 5f962dd1c6 android: Update Vulkan Validation Layer to 1.4.309.0
Updates the Android Vulkan Validation Layer (VVL) from version 1.4.304.1
to 1.4.309.0. This ensures compatibility with the latest Vulkan specification
and provides improved validation capabilities for the Android build.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-22 17:12:54 +10:00
Zephyron 25abfe36a3 android: Update build configuration and package identifiers
Updates the Android build configuration with several important changes:

- Change application ID from com.antutu.ABenchMark to org.citron.citron_emu
- Upgrade CMake version from 3.31.6 to 4.0.1
- Update Android Gradle plugin from 8.9.0 to 8.9.2
- Add CMAKE_POLICY_VERSION_MINIMUM=3.5 to CMake arguments
- Keep Kotlin version at 1.9.20

These changes align the Android package identifier with the Citron project
and update build tool versions to ensure compatibility with modern Android
development requirements.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-22 16:58:55 +10:00
Zephyron 2f57a35d2d video_core/vulkan: Fix callback variable shadowing in async shader compilation
Resolves a variable shadowing issue in AsyncCompileShader where the callback
lambda parameter was shadowing the outer callback variable. This was causing
compilation warnings/errors in Android Studio. The fix:

- Renames the outer callback variable to 'outer_callback'
- Renames the inner lambda callback parameters to 'inner_callback'
- Maintains consistent naming across all error handling paths

This change improves code clarity and eliminates compiler warnings while
maintaining the same functionality for async shader compilation.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-22 16:57:57 +10:00
Zephyron 66bdd6ed27 video_core: Add fallback handling for failed storage buffer lookups
Implements a more robust error handling approach when storage buffer lookups
fail in the buffer cache. Instead of returning a null binding, the code now:

- Provides a fallback buffer with safe default values
- Implements warning rate limiting to prevent log spam
- Tracks warning counts per cbuf_index
- Logs detailed debug information periodically

This change helps prevent potential crashes when storage buffer lookups fail
while still maintaining visibility into the issue through strategic logging.

The fallback mechanism uses a safe static address and a reasonable buffer
size (16KB) to handle cases where the normal GPU to CPU address translation
fails.

Also updates copyright headers to include citron Emulator Project.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-22 16:56:59 +10:00
Zephyron ff9c61e7c7 video_core: Improve texture cache memory management to prevent leaks
Implement several improvements to the texture cache memory management system
to address memory leaks that occur in memory-intensive games like TOTK
(Title ID 0100F2C0115B6000). These changes prevent the gradual memory
increase that eventually leads to crashes or undefined behavior.

Key improvements:
- Enhance garbage collection with more aggressive cleanup thresholds
- Add emergency resource cleanup for persistent high memory usage
- Improve DeleteImage to ensure proper resource deallocation
- Make DelayedDestructionRing thread-safe with proper mutex protection
- Track consecutive high-memory frames to detect potential leaks
- Add emergency cleanup mechanism for extreme memory pressure situations
- Use proper type casting in std::max to fix compilation errors

This should significantly improve stability during extended gameplay
sessions with memory-intensive titles.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-20 17:39:14 +10:00
Zephyron e72d695115 feat(services): Implement nn::socket, nn::nifm, and nn::nim networking services
Add Nintendo Switch network service implementations to support modders
working with network functionality in their game modifications:

- Add nn::socket utilities including InetAton and Connect functions
- Implement sockaddr/in_addr structures matching official Nintendo APIs
- Add nn::nifm networking interface services with IsNetworkAvailable and SubmitNetworkRequest
- Implement nn::nim network installation management services
- Fix BSD socket implementation to properly handle proxy packets
- Add Service_BSD log category for better debugging

These changes provide crucial networking API support for modders like
MaxLastBreath and projects like NX Optimizer (https://www.nxoptimizer.com/)
that need to hook into Nintendo's network services for code injection mods.
This implementation follows the official documentation at SwitchBrew and
enables proper network connectivity in modded games.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-20 15:35:25 +10:00
Zephyron 0cdd546152 externals: Update Vulkan dependencies to latest versions
Update Vulkan-Headers, Vulkan-Utility-Libraries, VulkanMemoryAllocator, and vcpkg submodules to their latest versions to ensure compatibility with newer Vulkan features and improve rendering performance.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-19 13:26:01 +10:00
Zephyron 3205c9b691 fix(vulkan): address compiler warnings for Linux
- Fix variable shadowing in ShaderManager constructor by renaming parameter
- Remove unused variables in vk_texture_manager.cpp to avoid warnings
- Fix int conversion warning in syscall return value

These changes fix build errors when using certain optimized compile flags for Linux.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-18 14:26:04 +10:00
Zephyron f1e169e060 fix: correct implementation of present interval 0 for unlocked FPS
Fixes issues in commit bbd3253169 that could cause
crashes and deadlocks. The feature now works as intended, allowing games using
present interval 0 to run with truly unlocked FPS.

This ensures proper functionality of dynamic framerate mods like UltraCam
by MaxLastBreath (https://www.nxoptimizer.com/) without stability problems.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-18 10:11:16 +10:00
Zephyron 278486d059 feat: add CPU clock rate slider to settings
Implement a slider in the CPU settings tab to adjust the BASE_CLOCK_RATE
up to 1,785 MHz (Switch's official maximum clock rate). Default remains
at 1,020 MHz.

This change:
- Adds UI slider and spinbox to configure_cpu.ui with range 500-1785 MHz
- Makes BASE_CLOCK_RATE dynamic by reading from settings
- Modifies WallClock to handle dynamic clock rate changes
- Updates APM controller to properly set the clock rate
- Changes clock rate settings category from Core to CPU

The user can now easily adjust the CPU clock rate to improve performance
or manage thermals and power consumption.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-16 22:02:10 +10:00
Zephyron bbd3253169 feat: add option to respect present interval 0 as unlocked FPS
When enabled, this feature allows games using present interval 0 to run with
truly unlocked FPS, matching actual hardware behavior more accurately.

Previously, Citron would cap present interval 0 at 120FPS to conserve battery,
but this prevented proper functionality of dynamic framerate mods like UltraCam
by MaxLastBreath (https://www.nxoptimizer.com/).

The setting is disabled by default to maintain the current behavior for most users.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-16 19:28:15 +10:00
Zephyron a1f3414bde service/sockets: Implement network services for new firmware versions
This commit implements various network services required for newer firmware
versions. Key changes include:

- Add bsd:nu service for firmware 15.0.0+ with proper event handling
- Add bsdcfg implementation with complete interface declarations
- Add dns:priv and ethc (c/i) services
- Register all new services in the service manager
- Extend BSD implementation with additional socket operations
- Remove room_network instance variable in favor of system.GetRoomNetwork()
- Fix kernel event creation by using ServiceContext in all appropriate places
- Update build system to include new source files

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-15 17:19:53 +10:00
Zephyron 175a427c27 refactor(vulkan): remove depth buffer workarounds and excessive logging
- Remove special handling for reversed depth scenarios that were added for Civilization 7
- Remove excessive logging in Vulkan renderer
- Update Discord client ID
- Update Vulkan-related external dependencies

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-14 22:02:29 +10:00
Zephyron 18def48dfe feat(video_core): Fix Linux compilation issues in Hybrid Memory Manager
- Added missing <thread> header for std::thread usage
- Added <fcntl.h> for O_CLOEXEC and O_NONBLOCK definitions
- Fixed struct initialization order in uffdio_copy to match declaration order

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-12 17:50:39 +10:00
Zephyron a4088f3a1e Add Windows support to Hybrid Memory Manager
This commit adds Windows-specific implementation of the fault-managed memory
system, providing similar functionality to the existing Linux/Android implementation.

Key changes:
- Added Windows-specific memory management using VirtualAlloc/VirtualFree
- Implemented Windows vectored exception handler for page fault handling
- Added proper memory protection and page fault handling on Windows
- Updated memory snapshot functionality to work on Windows
- Added proper cleanup of Windows-specific resources
- Fixed type conversion issues in memory management code
- Added proper error handling for Windows memory operations
- Fixed VRAM Memory Layout Mode to allow up to 12Gb

The implementation uses Windows-specific APIs:
- VirtualAlloc/VirtualFree for memory management
- AddVectoredExceptionHandler for page fault handling
- VirtualProtect for memory protection management

This change maintains feature parity with the Linux/Android implementation
while using Windows-native APIs for better performance and reliability.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-12 16:15:51 +10:00
Zephyron b66b3ca639 nvn(fix): Optimize shader performance by enhancing NVN bias settings
Improve GPU storage buffer detection and memory access patterns:
- Expand NVN bias address range (0x100-0x800 vs 0x110-0x610)
- Increase alignment from 16 to 32 bytes for optimal memory access
- Raise default alignment from 8 to 16 bytes for non-biased addresses
- Refactor bias handling code for better readability
- Add detailed performance-related comments

These changes help identify more storage buffers within shaders and
ensure memory accesses are better aligned, which improves overall
shader compilation and execution performance.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-12 15:14:14 +10:00
Zephyron 3a1c178711 Revert "nvn: Optimize shader performance by enhancing NVN bias settings"
This reverts commit 19febba866.
2025-04-12 15:12:19 +10:00
Zephyron 964bbf489a feat(video_core): Implement HybridMemory for advanced Vulkan memory management
Adds a new cross-platform memory management system with enhanced capabilities:
- Fault-managed memory allocation for Linux/Android platforms
- Memory snapshot and differential snapshot support
- Predictive memory reuse tracking for optimized access patterns
- Vulkan compute buffer integration
- User-configurable settings for enabling features

The system integrates with the existing Vulkan renderer to provide more
efficient memory handling, especially for compute-intensive workloads.

Co-authored-by: boss.sfc <boss.sfc@citron-emu.org>
Co-committed-by: boss.sfc <boss.sfc@citron-emu.org>
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-10 20:22:00 +10:00
Zephyron 19febba866 nvn: Optimize shader performance by enhancing NVN bias settings
Improve GPU storage buffer detection and memory access patterns:
- Expand NVN bias address range (0x100-0x800 vs 0x110-0x610)
- Increase alignment from 16 to 32 bytes for optimal memory access
- Raise default alignment from 8 to 16 bytes for non-biased addresses
- Refactor bias handling code for better readability
- Add detailed performance-related comments

These changes help identify more storage buffers within shaders and
ensure memory accesses are better aligned, which improves overall
shader compilation and execution performance.

Update Vulkan dependencies to their latest versions.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-05 00:46:51 +10:00
Zephyron 0dac3c1dbd renderer/friend: Improve reversed depth handling and Friend service
This commit makes two significant improvements:

1. Vulkan renderer:
   - Detect and properly handle reversed depth buffers (clear_depth < 0.5)
   - Force depth write enable when needed with reversed depth
   - Use GREATER_OR_EQUAL comparison for reversed depth scenarios
   - Fix transparency issues in games like Civilization 7 by adjusting blend factors
   - Add detailed logging for depth buffer operations

2. Friend service:
   - Implement previously stubbed functions including EnsureFriendListAvailable
     and EnsureBlockedUserListAvailable
   - Add proper event signaling to prevent games from hanging
   - Implement Cancel function for improved compatibility
   - Update copyright notice for the Citron project

These changes improve compatibility with modern games using reversed depth
buffers and prevent hangs in titles that rely on Friend service functionality.

Co-authored-by: m33ts4k0z <m33ts4k0z@citron-emu.org>
Co-committed-by: m33ts4k0z <m33ts4k0z@citron-emu.org>
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-04-03 16:17:55 +10:00
Zephyron 5d952717ff video_core: Enhance Vulkan shader compilation with async threading system
Implement a robust asynchronous shader compilation system inspired by commit
1fd5fefcb1. This enhancement provides:

- True multi-threaded shader compilation with atomic status tracking
- Persistent disk caching for faster shader loading
- Command queue system for background processing
- Integration with Citron's scheduler for better resource management
- Parallel shader loading to reduce startup times
- Improved error handling and recovery mechanisms

These changes significantly reduce shader compilation stuttering and improve
overall performance when using asynchronous shaders. The implementation
maintains compatibility with Citron's existing architecture while adding
more robust threading capabilities.

Co-authored-by: boss.sfc <boss.sfc@citron-emu.org>
Co-committed-by: boss.sfc <boss.sfc@citron-emu.org>
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-31 21:01:01 +10:00
Zephyron b25c7653e6 feat(vulkan): implement enhanced texture and shader management
This commit adds improved Vulkan functionality to the Citron emulator:

- Add thread-safe texture management with automatic error recovery
- Implement shader caching with validation support
- Add robust error handling for Vulkan operations
- Implement platform-specific initialization for Windows, Linux, and Android

These enhancements improve stability when handling texture loading errors
and provide better recovery mechanisms for Vulkan failures.

Co-authored-by: boss.sfc <boss.sfc@citron-emu.org>
Co-committed-by: boss.sfc <boss.sfc@citron-emu.org>
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-28 18:25:36 +10:00
Zephyron edfb500ee7 build: fix linux compilation
- Removes unnecessary \ from Copyright Line Causing Linux Builds To Fail

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-28 14:54:54 +10:00
Zephyron ebfc9d8347 memory: Implement enhanced memory management system
Add a flexible memory region management system that provides:
- Memory region type classification (System, Graphics, IO, Binary)
- Memory region permission management (executable, writable)
- Binary base address randomization for ASLR
- Dynamic memory mapping capabilities

Credit: boss.smc@citron-emu.org
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-27 23:25:24 +10:00
Zephyron 1fd5fefcb1 WIP: Enhance shader compilation performance and control
This commit adds new settings and optimizations for shader compilation:

- Add new settings:
  - use_enhanced_shader_building: Enable enhanced shader compilation
  - shader_compilation_priority: Control shader compilation priority

- Improve shader compilation performance:
  - Optimize worker thread allocation based on CPU cores
  - Add smarter async shader compilation heuristics
  - Prioritize vertex and fragment shader compilation
  - Add performance tracking and logging

- Add performance monitoring:
  - Track shader compilation times
  - Log slow shader compilations
  - Monitor async shader compilation statistics

This is a work in progress commit. Further optimizations and refinements
will be needed based on testing and feedback.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-27 20:56:23 +10:00
Zephyron 55dc3f8ec1 Update external dependency URLs and versions
- Change SDL2 bundled version from 2.32.0 to 2.28.2
- Downgrade clang-format version from 18 to 15
- Replace citron-emu.org URLs with GitHub mirror URLs:
  - Update clang-format download URL to use yuzu-mirror repository
  - Update package base URL for external dependencies

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-25 20:30:33 +10:00
Zephyron 7edbccbdc9 Revert "Update submodule URLs from yuzu-mirror to Citron repositories"
This reverts commit d1b7aebe8c.
2025-03-25 17:45:50 +10:00
Zephyron e06526cbbc Update Vulkan-related dependencies and vcpkg
- Update Vulkan-Headers from cacef303 to 78c35974
- Update VulkanMemoryAllocator from c788c521 to 29b35ea4
- Update vcpkg from e40d24cb to a7d06b3a

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-21 17:23:02 +10:00
Zephyron 0448d8146f Update controller udev rules with consistent vendor ID formatting
- Add copyright notice for citron Emulator Project
- Standardize Nintendo vendor ID format to uppercase (057E) in Bluetooth controller rules
- Maintain same permissions and access settings for all controllers

REF: 6ead429195

Reviewed-on: http://vub63vv26q6v27xzv2dtcd25xumubshogm67yrpaz2rculqxs7jlfqad.onion/torzu-emu/torzu/pulls/106
Co-authored-by: deftdawg <deftdawg@noreply.localhost>
Co-committed-by: deftdawg <deftdawg@noreply.localhost>
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-21 17:13:06 +10:00
Zephyron 98c515871e ui: Disable the Kiosk (Quest) Mode configuration option
This commit disables the "Kiosk (Quest) Mode" checkbox in the debug configuration
UI by setting it to non-interactive and adding a tooltip indicating that the
feature has been disabled.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-20 18:49:00 +10:00
Zephyron 278ac75a37 ui: Disable the Auto-Stub configuration option
This commit disables the "Enable Auto-Stub" checkbox in the debug configuration
UI by setting it to non-interactive and adding a tooltip indicating that the
feature has been disabled.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-17 14:02:15 +10:00
Zephyron ec402a0510 feat(build): Add host system detection for Android cross-compilation
- Modify CMakeLists.txt to detect whether the host system is Windows or Linux
- Set VCPKG_HOST_TRIPLET dynamically to either "x64-windows" or "x64-linux" based on CMAKE_HOST_SYSTEM_NAME
- Previously, the host triplet was hardcoded to "x64-windows", which prevented proper building on Linux hosts

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-17 12:26:35 +10:00
Zephyron 8cb6e6d5d4 Revert "Android: Implement TLB optimization to prevent deadlocks and improve performance"
This reverts commit 21594b73aa.
2025-03-17 12:20:38 +10:00
Zephyron 51800e249b service/ssl: Register ssl:s service to fix game hangs
Added registration for the 'ssl:s' service using the same implementation as
the regular 'ssl' service. This fixes issues with certain titles that hang indefinitely while
waiting for this service to become available.

The issue appears in logs as:
"Server is not registered! service=ssl:s"
"Waiting for service ssl:s to become available"

This is a simple fix that reuses the existing SSL implementation instead of
creating a separate one, as both services share the same functionality.

This commit enhances the Multiplayer Function

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-16 19:33:00 +10:00
Zephyron 21594b73aa Android: Implement TLB optimization to prevent deadlocks and improve performance
This commit addresses critical TLB (Translation Lookaside Buffer) issues on Android by implementing several optimizations:

- Add new BufferCacheAccelerator to manage memory range overlap detection
- Implement TLB-aware memory barriers to prevent unnecessary invalidations
- Add a TLB caching system to avoid redundant flushing operations
- Create a counter to limit outstanding memory operations and prevent TLB thrashing
- Implement TLB prefetching for better memory access patterns
- Add targeted memory barriers for more precise synchronization

These changes significantly reduce the likelihood of the "0.0 FPS deadlock" issue on Android devices by maintaining a more stable TLB state and preventing cache thrashing.

TODO: Merge & Adapt Camille LaVey's TLB Method To Further Improve
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-14 18:56:16 +10:00
Zephyron d869045b77 build: Move PGO configuration to earlier in CMakeLists.txt
The Profile-Guided Optimization configuration was previously placed at the
end of the CMakeLists.txt file, after all targets were already defined.
This was causing the PGO flags to have no effect on the build process.

This commit moves the PGO configuration to immediately after the initial
compiler flags setup and before any targets are defined, ensuring that
all targets will properly receive the PGO instrumentation or optimization
flags when those options are enabled.

This allows both CITRON_ENABLE_PGO_INSTRUMENT and CITRON_ENABLE_PGO_OPTIMIZE
to function correctly for all build targets.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-13 17:03:21 +10:00
Zephyron f2931c7566 vulkan: Implement AMD driver workaround for logic operations
- Added conditional check for AMD graphics drivers
- Automatically disable logic operations when float vertex attributes
  are present to work around driver quirks
- Maintain original logic op state to preserve emulator behavior
- Prepare dynamic state management infrastructure for future
  OpenGL implementation changes

OpenGL implementation will follow in subsequent commits.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-13 16:30:23 +10:00
Zephyron 12c63997d2 core(base_clock_rate): Revert Back To 1,020 MHz
- This Commit Accidentally Slipped By In A Previous Commit (dad8859679)
- This Has Caused Many Errors & Networking Issues Particularly On Android
- Now Uses Safe, Default 1.02 GHz

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-13 16:10:04 +10:00
Zephyron 1023125be5 android: Add mandatory firmware and title.keys setup steps
- Downgrade Kotlin and Android plugin versions to ensure build compatibility.
- Add setup steps for title.keys installation and mandatory firmware setup.
- Persist auto-generated keys to improve emulator compatibility.
- Update key manager to write derived keys to filesystem.
- Implement UI handling for mandatory firmware installation checks.
2025-03-13 16:04:11 +10:00
Zephyron dad8859679 android: Update build system and optimize ARM NCE implementation
- Update Kotlin from 1.9.20 to 2.1.20-RC2
- Upgrade Java version from 17 to 21
- Update Android Gradle plugin from 8.1.2 to 8.9.0
- Update Gradle wrapper from 8.10.2 to 8.11.1
- Update NDK version to 29.0.13113456 rc1
- Update all Android dependencies to latest versions
- Simplify ARM NCE implementation by removing custom TLB handling
- Improve alignment and access fault handling
- Update hardware BASE_CLOCK_RATE from 1.02GHz to 1.785GHz
- Add citron Emulator Project copyright notices

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-03-11 20:48:51 +10:00
Zephyron 834cc89548 externals: Update Vulkan and vcpkg submodules
Update the following external dependencies:
- Vulkan-Headers: 0f0cfd8 → cacef303
- Vulkan-Utility-Libraries: 50563f4 → bc3a4d9f
- vcpkg: cd1099f4 → e40d24cb

This commit updates the external dependencies to their latest
compatible versions to ensure we're using up-to-date libraries.
2025-03-11 15:47:53 +10:00
vampiric_x 38b259d099 Fix a few strings 2025-03-10 23:16:30 +01:00
vampiric_x e7e9453667 android: Initial multiplayer support
And give room owners mod access on both Android and QT
2025-03-10 22:37:56 +01:00
Zephyron ae75413cc3 Remove quickstart guide references to address legal concerns
- Removed "Open Quickstart Guide" menu item and associated functions
- Replaced ROM loading error popup containing quickstart guide links
  with a neutral disclaimer stating the software is provided as-is
  without warranty or support
- This change helps minimize legal liability by avoiding
  specific instructional content while directing users to community
  resources for assistance
2025-03-09 22:23:54 +10:00
Zephyron 6a31da5905 cmake: Add Profile-Guided Optimization (PGO) support
Adds support for Profile-Guided Optimization builds on both Windows (MSVC)
and Linux (GCC/Clang) platforms. This allows for performance optimizations
based on real usage patterns.

For MSVC:
- Adds /GL and /LTCG:PGINSTRUMENT flags for instrumentation
- Adds /GL and /LTCG:PGOPTIMIZE flags for optimization

For GCC:
- Adds -fprofile-generate flags for instrumentation
- Adds -fprofile-use flags for optimization

For Clang:
- Adds -fprofile-instr-generate flags for instrumentation
- Adds -fprofile-instr-use flags for optimization

Controlled by two new CMake options:
- CITRON_ENABLE_PGO_INSTRUMENT: Enable instrumentation build
- CITRON_ENABLE_PGO_OPTIMIZE: Enable optimization build

Updated submodules:
- Vulkan-Headers to 0f0cfd8
- Vulkan-Utility-Libraries to 50563f4
- vcpkg to cd1099f
2025-03-07 20:23:23 +10:00
Zephyron a5125d008a revert 78b0080b96
revert TLB: Parallel Page Table Walk Logic Implementation
2025-03-06 06:44:38 +00:00
Zephyron b24dd921aa revert 31694994f2
revert arm_nce: Hash TLB to MLP L2 Update
2025-03-06 06:44:06 +00:00
Zephyron 9a65205dba revert ee3d858935
revert Follow Up Of the previous commit with the update of TLB update
2025-03-06 06:43:37 +00:00
Zephyron af4f08be33 revert 6565055865
revert Fix: Core_Memory logging and ARM_NCE Mutex logging
2025-03-06 06:42:48 +00:00
Zephyron 0d0963d32f revert 90a8165f77
revert Added: Core_Memory to filter logging class
2025-03-06 06:42:30 +00:00
Zephyron 4491127f52 revert 031c635095
revert arm: corrected declarations
2025-03-06 06:41:01 +00:00
Zephyron c304afe2b3 revert 78b0080b96
revert TLB: Parallel Page Table Walk Logic Implementation
2025-03-06 06:40:24 +00:00
Zephyron b8240b4214 revert 644ed69285
revert arm:
2025-03-06 06:40:17 +00:00
Zephyron 91487f6d96 revert 3554f55fc9
revert TLB Update
2025-03-06 06:39:49 +00:00
CamilleLaVey 031c635095 arm: corrected declarations 2025-03-05 11:12:44 -04:00
CamilleLaVey 90a8165f77 Added: Core_Memory to filter logging class 2025-03-05 01:31:22 -04:00
CamilleLaVey 6565055865 Fix: Core_Memory logging and ARM_NCE Mutex logging 2025-03-05 00:25:31 -04:00
CamilleLaVey ee3d858935 Follow Up Of the previous commit with the update of TLB update 2025-03-04 22:50:01 -04:00
CamilleLaVey 31694994f2 arm_nce: Hash TLB to MLP L2 Update 2025-03-04 22:28:03 -04:00
CamilleLaVey 4197fa84a0 revert e4342324fe
revert Changes of the previous commits
2025-03-04 02:33:14 +00:00
CamilleLaVey e4342324fe Changes of the previous commits 2025-03-03 21:49:07 -04:00
CamilleLaVey 78b0080b96 TLB: Parallel Page Table Walk Logic Implementation 2025-03-03 21:44:56 -04:00
CamilleLaVey 644ed69285 arm: 2025-03-03 21:23:13 -04:00
CamilleLaVey 3554f55fc9 TLB Update 2025-03-03 20:55:08 -04:00
Zephyron dc9532b4d1 Revert "CMake: Enable C++ latest and coroutines for MSVC builds"
This reverts commit 1308e2b935.
2025-03-03 17:13:29 +10:00
Zephyron 1308e2b935 CMake: Enable C++ latest and coroutines for MSVC builds
Add /std:c++latest and /await compiler flags for MSVC builds to enable
the latest C++ features and coroutine support.
2025-03-03 16:35:57 +10:00
Zephyron 5caecd8151 Android: Remove redundant firmware check
Remove duplicate firmware presence check in EmulationActivity. The
isFirmwareAvailable() check already handles this functionality, making
checkFirmwarePresence() redundant.
2025-03-03 16:35:18 +10:00
Zephyron dc9fbcc893 Android: Downgrade build dependencies and SDK versions
- Revert Android Gradle plugin from 8.8.1 to 8.1.2
- Downgrade Kotlin version from 2.1.20-RC to 1.9.20
- Lower Java/JVM target from 21 to 17
- Reduce CMake version from 3.31.5 to 3.22.1
- Update various Android dependencies to stable versions:
  - Navigation Safe Args plugin to 2.6.0
  - ktlint to 0.47.1
  - Play Publisher plugin to 3.8.6
- Remove custom APK naming scheme
- Fix CMake boolean arguments (OFF -> 0)
- Remove citron copyright header
2025-03-03 16:34:35 +10:00
Zephyron b1d5d4e5be Update external dependencies
- Update Vulkan-Utility-Libraries submodule to 5f41f2a
- Update vcpkg submodule to efb1e74
2025-03-03 16:33:01 +10:00
CamilleLaVey f0d8daf755 revert cbb9a35166
revert Re-Enabled Vulkan Functions disabled on Adreno to improve compatibility and performance and check further issues within the current changes.
2025-03-03 04:00:13 +00:00
CamilleLaVey cbb9a35166 Re-Enabled Vulkan Functions disabled on Adreno to improve compatibility and performance and check further issues within the current changes. 2025-03-02 23:17:43 -04:00
Zephyron cfe437aacf arm: Improve TLB implementation and fault handling in NCE
This commit enhances the Translation Lookaside Buffer (TLB) implementation
in the ARM Native Code Execution (NCE) component to increase stability,
particularly on Android devices. The changes prioritize robustness and
error recovery over performance optimizations.

Key improvements:
- Replace set-associative TLB with a simpler linear search implementation
- Implement a basic LRU replacement policy for TLB entries
- Add validation checks for memory addresses before TLB insertion
- Ensure proper page alignment for guest and host addresses
- Enhance alignment fault handling with instruction skipping as fallback
- Add comprehensive debug logging for memory access errors
- Improve error recovery in guest memory access scenarios

These changes should significantly reduce crashes during emulation on
Android devices by gracefully handling memory access edge cases that
previously resulted in hard crashes.

Co-Authored-By: Camille LaVey <camillelavey@citron-emu.org>
Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-02-28 17:11:07 +10:00
Zephyron 9b293c3a98 shader_recompiler: Implement vertex count lookup for Geometry stage
Add proper handling of input topologies in the Geometry stage for all three
shader backends (GLASM, GLSL, SPIRV). This implementation uses a lookup table
approach to determine vertex counts based on input topology type (Points,
Lines, LinesAdjacency, Triangles, TrianglesAdjacency) and shifts the vertex
count by 16 bits as required by the invocation info format.

Additional changes:
- Fixed TessellationControl and TessellationEval stages to properly break
  after emitting code
- Added proper header include for runtime_info.h in GLASM backend
- Improved code documentation with clear commenting patterns

This change ensures accurate geometry shader behavior across all backends,
improving compatibility with games that rely on proper vertex count reporting.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
2025-02-28 17:08:27 +10:00
Zephyron 84e5fbc089 feat: Make firmware mandatory for title launching
This commit implements a requirement for firmware to be installed before titles
can be launched, similar to how keys are required.

Changes include:
- Add IsFirmwareAvailable method to KeyManager to check for essential keys
- Add CheckFirmwarePresence method to verify actual firmware files (NCAs)
- Add firmware checks in game loading process for both desktop and Android
- Add error messages when firmware is missing
- Add strings for firmware-related error messages

The implementation checks for both essential keys and the presence of system
applets like Mii Edit and Home Menu to ensure proper firmware is installed.
Games will not launch if firmware is missing, and users will be shown an
appropriate error message.
2025-02-28 16:15:10 +10:00
Zephyron a442078ee4 feat: Remove autogenerated key functionality
This commit removes the functionality that automatically generates and writes
keys to *_autogenerated files. The key derivation logic is preserved, but
derived keys are now only stored in memory and not written to disk.

Changes include:
- Remove loading from *_autogenerated key files
- Make WriteKeyToFile a no-op function
- Remove all file writing operations in SetKey methods
- Remove file writing for keyblobs and other derived keys
- Update copyright notices

This change improves security by not storing derived keys on disk and
simplifies the key management system.
2025-02-28 15:27:12 +10:00
Zephyron cc610ad9b6 renderer: Disable async presentation due to crashes
- Disable async presentation by default on both Android and desktop platforms due to stability issues.
2025-02-27 13:19:08 +10:00
Zephyron 5a65f9a094 Update external dependencies
- Vulkan-Utility-Libraries: 6be00ca → 2d8f273
- FFmpeg: d161604 → 99e2af4
- vcpkg: 9a7a33f → 23b33f5
2025-02-27 13:08:19 +10:00
Zephyron 5ca1f0e365 core/arm/nce: Implement TLB caching system
Adds a software TLB cache to improve memory access performance in the NCE
(Native Code Execution) system. Key changes include:

- Implement set-associative TLB with 64 sets and 8 ways
- Add TLB lookup before memory access in HandleGuestAccessFault
- Implement LRU replacement policy with access frequency consideration
- Add thread context caching to reduce overhead
- Add proper synchronization with mutex locks
- Add helper functions for TLB management (lookup, insert, invalidate)

This change should improve performance by reducing redundant memory
translations and providing faster access to frequently used pages.
2025-02-25 18:37:14 +10:00
Zephyron a36baad0f0 externals: Update submodule versions
- vcpkg: 37d46ed → 9a7a33f
- Vulkan-Headers: 234c4b7 → 952f776
- Vulkan-Utility-Libraries: fe7a09b → 6be00ca
- ffmpeg: 9c1294e → d161604
2025-02-25 18:34:32 +10:00
Zephyron ed115d3f72 Revert "settings: Enable auto-stub by default"
This reverts commit 7903415fa4.
2025-02-24 19:05:05 +10:00
Zephyron d9619b7eed vulkan: Improve memory allocation robustness
Enhances the Vulkan memory allocator with better OOM handling and memory
alignment:

* Add memory recovery by cleaning up empty allocations before failing
* Implement proper fallback to non-device-local memory
* Simplify memory alignment handling for different vendors
* Add better error logging for allocation failures
* Add IsEmpty() helper to track unused allocations
* Fix alignment requirements for Adreno (4KB) vs other vendors

These changes improve the robustness of memory allocation, particularly
in low-memory situations, and streamline vendor-specific alignment
requirements.
2025-02-22 19:08:42 +10:00
Zephyron dbe5bf1d18 android: Update build dependencies and configurations
Updates various build dependencies and configurations for the Android build:

* Upgrade Android Gradle Plugin to 8.8.1
* Update Kotlin to 2.1.20-RC
* Upgrade NDK to 28.0.13004108
* Update target/compile SDK to Android 35
* Upgrade Java/Kotlin target to JVM 21
* Enable additional release optimizations (shrinkResources, proguard-android-optimize)
* Update CMake to 3.31.5
* Update various AndroidX and support library dependencies
* Change CMake boolean flags from 0/1 to OFF/ON
* Update ktlint to 0.51.1 and related plugins

This commit modernizes the Android build system and enables additional
optimizations for release builds.
2025-02-22 19:07:53 +10:00
Zephyron 7903415fa4 settings: Enable auto-stub by default
Changes the default value of use_auto_stub from false to true in the settings.
This setting controls whether unimplemented functions should be automatically
stubbed during execution.
2025-02-22 19:07:16 +10:00
Zephyron 3bb4d97e9e cmake: Optimize Android VVL download logic
Improve the Vulkan Validation Layer (VVL) download logic for Android by checking
for the final library file instead of just the zip archive. This prevents
unnecessary re-downloads and extractions when the library is already in place.

The check now looks for libVkLayer_khronos_validation.so in the final
destination path before attempting to download and extract the archive.
2025-02-22 19:06:05 +10:00
Zephyron a41f7b7a56 feat: Add AnTuTu to license verification for Android app 2025-02-22 10:05:27 +10:00
143 changed files with 6229 additions and 303 deletions

12
.gitmodules vendored
View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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"

2
externals/SDL vendored

@ -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

2
externals/vcpkg vendored

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

View File

@ -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")

View File

@ -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

View File

@ -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) {

View File

@ -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) {

View File

@ -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
})
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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"),

View File

@ -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,

View File

@ -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,

View File

@ -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 =

View File

@ -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

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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")
}
}
}

View File

@ -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
} }

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"/>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"/>

View File

@ -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>

View File

@ -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" />

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -23,6 +23,9 @@
<string name="keys">Keys</string> <string name="keys">Keys</string>
<string name="keys_description">Select your &lt;b>prod.keys&lt;/b> file with the button below.</string> <string name="keys_description">Select your &lt;b>prod.keys&lt;/b> file with the button below.</string>
<string name="select_keys">Select Keys</string> <string name="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 &lt;b>Games&lt;/b> folder with the button below.</string> <string name="games_description">Select your &lt;b>Games&lt;/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>

View File

@ -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
} }

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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">

View File

@ -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>

View File

@ -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"

View File

@ -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() {

View File

@ -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() {

View File

@ -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);

View File

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

View File

@ -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
) )

View File

@ -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));
} }

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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);

View File

@ -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 {

View File

@ -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) \

View File

@ -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

View File

@ -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();

View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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();
}

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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;
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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