diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index e7052a6f6..27cf7405b 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -28,7 +28,7 @@ jobs: configuration: [Release] platform: - { name: win-x64, zip_os_name: win_x64 } - #- { name: win-arm64, zip_os_name: win_arm64 } + - { name: win-arm64, zip_os_name: win_arm64 } - { name: linux-x64, zip_os_name: linux_x64 } - { name: linux-arm64, zip_os_name: linux_arm64 } #- { name: osx-x64, zip_os_name: osx_x64 } diff --git a/.forgejo/workflows/canary.yml b/.forgejo/workflows/canary.yml index 930e6b253..816ad3a99 100644 --- a/.forgejo/workflows/canary.yml +++ b/.forgejo/workflows/canary.yml @@ -32,9 +32,9 @@ jobs: matrix: platform: - { name: win-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_x64 } - #- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 } - - { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 } - - { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 } + - { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 } + - { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 } + - { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 } steps: - uses: actions/checkout@v6 diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 341430adb..905f22b21 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -26,9 +26,9 @@ jobs: matrix: platform: - { name: win-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_x64 } - #- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 } - - { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 } - - { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 } + - { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 } + - { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 } + - { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 } steps: - uses: actions/checkout@v6 diff --git a/Directory.Packages.props b/Directory.Packages.props index 7c4da891f..8c5ce0410 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,7 +45,7 @@ - + @@ -53,9 +53,10 @@ - - - + + + + @@ -64,4 +65,4 @@ - \ No newline at end of file + diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index db84fd3ae..e5de7b412 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -12275,6 +12275,56 @@ "zh_TW": "弱震動調節:" } }, + { + "ID": "ControllerSettingsRumbleUseHDRumble", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Enable HD Rumble", + "es_ES": "Activa vibración HD", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "HDRumbleTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Sends more data to the controller for better rumble.\n\nCurrently only supports first-party Nintendo Switch controllers.\n\nLeave ON if you're using JoyCons or a Pro Controller.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "DialogMessageSaveNotAvailableMessage", "Translations": { diff --git a/distribution/macos/construct_universal_dylib.py b/distribution/macos/construct_universal_dylib.py index 5d9321860..18b84399f 100644 --- a/distribution/macos/construct_universal_dylib.py +++ b/distribution/macos/construct_universal_dylib.py @@ -47,14 +47,12 @@ def get_new_name( input_component = str(input_dylib_path).replace(str(input_directory), "")[1:] return Path(os.path.join(output_directory, input_component)) - -def is_fat_file(dylib_path: Path) -> str: - res = subprocess.check_output([LIPO, "-info", str(dylib_path.absolute())]).decode( - "utf-8" - ) - - return not res.split("\n")[0].startswith("Non-fat file") - +def get_archs(dylib_path: Path) -> list[str]: + res = subprocess.check_output([LIPO, "-info", str(dylib_path)]).decode("utf-8") + if res.startswith("Non-fat file"): + return [res.split(":")[-1].strip()] + else: + return res.split("are:")[-1].strip().split() def construct_universal_dylib( arm64_input_dylib_path: Path, x86_64_input_dylib_path: Path, output_dylib_path: Path @@ -69,11 +67,12 @@ def construct_universal_dylib( os.path.basename(arm64_input_dylib_path.resolve()), output_dylib_path ) else: - if is_fat_file(arm64_input_dylib_path) or not x86_64_input_dylib_path.exists(): - with open(output_dylib_path, "wb") as dst: - with open(arm64_input_dylib_path, "rb") as src: - dst.write(src.read()) - else: + arm64_archs = get_archs(arm64_input_dylib_path) + x86_64_archs = get_archs(x86_64_input_dylib_path) if x86_64_input_dylib_path.exists() else [] + + if "arm64" in arm64_archs and "x86_64" in arm64_archs: + shutil.copy2(arm64_input_dylib_path, output_dylib_path) + elif x86_64_archs: subprocess.check_call( [ LIPO, diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs index ee8ab457d..f190996c1 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs @@ -16,5 +16,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller /// Enable Rumble /// public bool EnableRumble { get; set; } + + /// + /// Enable HD Rumble support + /// [] autos = ArrayPool>.Shared.Rent(count); + Span getOffsets = stackalloc int[Constants.MaxVertexBuffers]; + Span getSizes = stackalloc int[Constants.MaxVertexBuffers]; + Span offsets = stackalloc ulong[Constants.MaxVertexBuffers]; + Span sizes = stackalloc ulong[Constants.MaxVertexBuffers]; + Span strides = stackalloc ulong[Constants.MaxVertexBuffers]; + Span buffers = stackalloc VkBuffer[Constants.MaxVertexBuffers]; + + for (int i = 0; i < count; i++) + { + autos[i] = _bufferAutos[i]; + _bufferAutos[i] = null; + getOffsets[i] = _bufferOffsetsForGet[i]; + getSizes[i] = _bufferSizesForGet[i]; + offsets[i] = _offsets[i]; + sizes[i] = _sizes[i]; + strides[i] = _strides[i]; + } + + try + { + for (int i = 0; i < count; i++) + { + buffers[i] = autos[i].Get(cbs, getOffsets[i], getSizes[i]).Value; + autos[i] = null; + } + + for (int i = 0; i < count; i++) + { + _buffers[i] = buffers[i]; + _offsets[i] = offsets[i]; + _sizes[i] = sizes[i]; + _strides[i] = strides[i]; + } + + if (_gd.Capabilities.SupportsExtendedDynamicState) + { + _gd.ExtendedDynamicStateApi.CmdBindVertexBuffers2( + cbs.CommandBuffer, + baseBinding, + (uint)count, + _buffers.Pointer, + _offsets.Pointer, + _sizes.Pointer, + _strides.Pointer); + } + else + { + _gd.Api.CmdBindVertexBuffers(cbs.CommandBuffer, baseBinding, (uint)count, _buffers.Pointer, _offsets.Pointer); + } + } + finally + { + ArrayPool>.Shared.Return(autos, clearArray: true); + } } } diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index ccb541a34..a0b764158 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -435,8 +435,8 @@ namespace Ryujinx.Graphics.Vulkan _physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName), features2.Features.MultiViewport && !(IsMoltenVk && Vendor == Vendor.Amd), // Workaround for AMD on MoltenVK issue featuresRobustness2.NullDescriptor || IsMoltenVk, - supportsPushDescriptors && !IsMoltenVk, - propertiesPushDescriptor.MaxPushDescriptors, + supportsPushDescriptors, + IsMoltenVk ? 16 : propertiesPushDescriptor.MaxPushDescriptors, // In case an old version of MoltenVK is used, apply a limit to prevent vertex explosions. featuresPrimitiveTopologyListRestart.PrimitiveTopologyListRestart, featuresPrimitiveTopologyListRestart.PrimitiveTopologyPatchListRestart, supportsTransformFeedback, @@ -775,10 +775,11 @@ namespace Ryujinx.Graphics.Vulkan supportsShaderBallot: false, supportsShaderBarrierDivergence: Vendor != Vendor.Intel, supportsShaderFloat64: Capabilities.SupportsShaderFloat64, + supportsShaderNonUniformIndexing: featuresVk12.ShaderSampledImageArrayNonUniformIndexing && featuresVk12.ShaderStorageImageArrayNonUniformIndexing, - supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended && !IsMoltenVk, + supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended, supportsTextureShadowLod: false, supportsVertexStoreAndAtomics: features2.Features.VertexPipelineStoresAndAtomics, supportsViewportIndexVertexTessellation: featuresVk12.ShaderOutputViewportIndex, diff --git a/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj b/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj index 4791a3b27..b5335282e 100644 --- a/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj +++ b/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj @@ -14,7 +14,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + all + diff --git a/src/Ryujinx.Input.SDL3/NpadHdRumble.cs b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs index e367f6a9c..408b5213b 100644 --- a/src/Ryujinx.Input.SDL3/NpadHdRumble.cs +++ b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Hid; using SDL; using static SDL.SDL3; @@ -11,8 +12,9 @@ namespace Ryujinx.Input.SDL3 public unsafe class NpadHdRumble : IDisposable { private readonly SDL_hid_device* _hidHandle; - + private int _globalCount; + private ulong _lastWriteTicks; private NpadHdRumble(SDL_hid_device* hidHandle) { @@ -28,7 +30,7 @@ namespace Ryujinx.Input.SDL3 } ushort product = SDL_GetGamepadProduct(gamepadHandle); - if (product != 0x2006 && product != 0x2007 && product != 0x2009 && product != 0x200e) + if (!Enum.IsDefined(typeof(HDRumbleSupported), product)) { return null; } @@ -37,7 +39,7 @@ namespace Ryujinx.Input.SDL3 } // Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble - private void WriteHdRumble( + private bool WriteHdRumble( int encLeftLowFreq, int encLeftLowAmp, int encLeftHighFreq, int encLeftHighAmp, int encRightLowFreq, int encRightLowAmp, @@ -65,26 +67,35 @@ namespace Ryujinx.Input.SDL3 fixed (byte* ptr = buf) { - SDL_hid_write(_hidHandle, ptr, (nuint)buf.Length); + if (SendHDRumble(ptr, (nuint)buf.Length) >= 0) + { + return true; + } + + if (!String.IsNullOrEmpty(SDL_GetError())) + { + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); + } + return false; } } - private static int EncodeLowFreq(float lowFreq) { float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f); - return (int)Math.Round(32 * Math.Log2(lf * 0.1f)) - 0x40; + return (int) Math.Round(32 * Math.Log2(lf * 0.1f) - 0x40); } private static int EncodeHighFreq(float highFreq) { float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f); - return ((int)Math.Round(32 * Math.Log2(hf * 0.1f)) - 0x60) * 4; + return (int) Math.Round((32 * Math.Log2(hf * 0.1f) - 0x60) * 4); } private static int EncodeLowAmp(float rawAmp) { - int encodedAmp = 0; + double encodedAmp = 0; if (rawAmp is > 0 and < 0.012f) { @@ -92,15 +103,15 @@ namespace Ryujinx.Input.SDL3 } else if (rawAmp is >= 0.012f and < 0.112f) { - encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f)); + encodedAmp = 4 * Math.Log2(rawAmp * 110f); } else if (rawAmp is >= 0.112f and < 0.225f) { - encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f)); + encodedAmp = 16 * Math.Log2(rawAmp * 17f); } else if (rawAmp is >= 0.225f and <= 1f) { - encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f)); + encodedAmp = 32 * Math.Log2(rawAmp * 8.7f); } return (int)Math.Floor(encodedAmp / 2.0) + 64; @@ -108,7 +119,7 @@ namespace Ryujinx.Input.SDL3 private static int EncodeHighAmp(float rawAmp) { - int encodedAmp = 0; + double encodedAmp = 0; if (rawAmp is > 0 and < 0.012f) { @@ -116,23 +127,23 @@ namespace Ryujinx.Input.SDL3 } else if (rawAmp is >= 0.012f and < 0.112f) { - encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f)); + encodedAmp = 4 * Math.Log2(rawAmp * 110f); } else if (rawAmp is >= 0.112f and < 0.225f) { - encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f)); + encodedAmp = 16 * Math.Log2(rawAmp * 17f); } else if (rawAmp is >= 0.225f and <= 1f) { - encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f)); + encodedAmp = 32 * Math.Log2(rawAmp * 8.7f); } - return encodedAmp * 2; + return (int) Math.Round(encodedAmp * 2); } public bool HdRumble(VibrationValue left, VibrationValue right) { - WriteHdRumble(EncodeLowFreq(left.FrequencyLow), + return WriteHdRumble(EncodeLowFreq(left.FrequencyLow), EncodeLowAmp(left.AmplitudeLow), EncodeHighFreq(left.FrequencyHigh), EncodeHighAmp(left.AmplitudeHigh), @@ -140,7 +151,34 @@ namespace Ryujinx.Input.SDL3 EncodeLowAmp(right.AmplitudeLow), EncodeHighFreq(right.FrequencyHigh), EncodeHighAmp(right.AmplitudeHigh)); - return true; + } + + private int SendHDRumble(byte* data, nuint length) + { + int result = 0; + ulong currentTicks = SDL_GetTicks(); + + // Ditch rumble if we haven't hit the poll-rate yet. + // TODO: figure out a better way to do this + // While the polling check makes the rumble accurate, it also causes it to miss signals. + if ((currentTicks - _lastWriteTicks) < 8) // https://docs.handheldlegend.com/s/progcc-3/doc/lag-comparison-aAR1mV3JLX + { + return result; + } + + SDL_LockJoysticks(); + { + // Fun fact: Mario Kart 8 Deluxe sends rumble packets + // where the amplitude is zero, but the frequency isn't. + result = SDL_hid_write(_hidHandle, data, length); + if (result >= 0) + { + _lastWriteTicks = currentTicks; + } + } + SDL_UnlockJoysticks(); + + return result; } public void Dispose() @@ -148,4 +186,18 @@ namespace Ryujinx.Input.SDL3 SDL_hid_close(_hidHandle); } } + + public enum HDRumbleSupported : ushort + { + JoyConLeft = 0x2006, + JoyConRight = 0x2007, + JoyconPair = 0x2008, + ProController = 0x2009, + JoyconGrip = 0x200e, + Joycon2Right = 0x2066, + Joycon2Left = 0x2067, + Joycon2Pair = 0x2068, + Switch2ProController = 0x2069, + GamecubeController = 0x2073 + } } diff --git a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs index c23b64304..57f2940c8 100644 --- a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs +++ b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs @@ -197,10 +197,12 @@ namespace Ryujinx.Input.SDL3 return _hdRumble?.HdRumble(left, right) ?? false; } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { if ((Features & GamepadFeaturesFlag.Rumble) == 0) - return; + { + return false; + } ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); @@ -219,6 +221,15 @@ namespace Ryujinx.Input.SDL3 if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); } + + if (!String.IsNullOrEmpty(SDL_GetError())) + { + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); + return false; + } + + return true; } public Vector3 GetMotionData(MotionInputId inputId) diff --git a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs index 5d779518d..e9f11d713 100644 --- a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs +++ b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs @@ -162,7 +162,7 @@ namespace Ryujinx.Input.SDL3 public void SetTriggerThreshold(float triggerThreshold) { - + // No operations } public bool HDRumble(VibrationValue left, VibrationValue right) @@ -170,10 +170,12 @@ namespace Ryujinx.Input.SDL3 return _hdRumble?.HdRumble(left, right) ?? false; } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { if ((Features & GamepadFeaturesFlag.Rumble) == 0) - return; + { + return false; + } ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); @@ -192,6 +194,15 @@ namespace Ryujinx.Input.SDL3 if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); } + + if (!String.IsNullOrEmpty(SDL_GetError())) + { + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); + return false; + } + + return true; } public Vector3 GetMotionData(MotionInputId inputId) diff --git a/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs b/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs index 14352e5a4..6114674ad 100644 --- a/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs +++ b/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs @@ -1,4 +1,7 @@ +using Gommon; using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -61,7 +64,14 @@ namespace Ryujinx.Input.SDL3 return left.IsPressed(inputId) || right.IsPressed(inputId); } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool HDRumble(VibrationValue left, VibrationValue right) + { + // return _hdRumble?.HdRumble(left, right) ?? false; + // TODO: Track rumble and motion on both controllers + return false; + } + + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { if (lowFrequency != 0) { @@ -78,6 +88,15 @@ namespace Ryujinx.Input.SDL3 left.Rumble(0, 0, durationMs); right.Rumble(0, 0, durationMs); } + + if (!SDL_GetError().IsNullOrEmpty()) + { + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); + return false; + } + + return true; } public void SetConfiguration(InputConfig configuration) diff --git a/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs b/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs index f5da11a19..8b179f43f 100644 --- a/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs +++ b/src/Ryujinx.Input.SDL3/SDL3Keyboard.cs @@ -1,6 +1,7 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using System; using System.Collections.Generic; using System.Numerics; @@ -396,9 +397,14 @@ namespace Ryujinx.Input.SDL3 // No operations } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool HDRumble(VibrationValue left, VibrationValue right) { - // No operations + return false; + } + + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) + { + return false; } public Vector3 GetMotionData(MotionInputId inputId) diff --git a/src/Ryujinx.Input.SDL3/SDL3Mouse.cs b/src/Ryujinx.Input.SDL3/SDL3Mouse.cs index 9fdeb36ab..289a60d85 100644 --- a/src/Ryujinx.Input.SDL3/SDL3Mouse.cs +++ b/src/Ryujinx.Input.SDL3/SDL3Mouse.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using System; using System.Drawing; using System.Numerics; @@ -67,7 +68,12 @@ namespace Ryujinx.Input.SDL3 throw new NotImplementedException(); } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool HDRumble(VibrationValue left, VibrationValue right) + { + throw new NotImplementedException(); + } + + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { throw new NotImplementedException(); } diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs index a46ff8daf..85ca5ffcb 100644 --- a/src/Ryujinx.Input/HLE/NpadController.cs +++ b/src/Ryujinx.Input/HLE/NpadController.cs @@ -5,7 +5,6 @@ using Ryujinx.Common.Configuration.Hid.Controller.Motion; using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Hid; using System; -using System.Buffers; using System.Collections.Concurrent; using System.Numerics; using System.Runtime.CompilerServices; @@ -555,34 +554,37 @@ namespace Ryujinx.Input.HLE { if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue)) { - if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Rumble.EnableRumble) + if (_config is not StandardControllerInputConfig controllerConfig || + !controllerConfig.Rumble.EnableRumble) { - VibrationValue leftVibrationValue = dualVibrationValue.Item1; - VibrationValue rightVibrationValue = dualVibrationValue.Item2; - - float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble)); - float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble)); - - leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; - leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; - rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; - rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; - - if (_gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false) - { - _gamepad?.Rumble(low, high, uint.MaxValue); - } - - Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " + - $"L.low.amp={leftVibrationValue.AmplitudeLow}, " + - $"L.high.amp={leftVibrationValue.AmplitudeHigh}, " + - $"L.low.freq={leftVibrationValue.FrequencyLow}, " + - $"L.high.freq={leftVibrationValue.FrequencyHigh}, " + - $"R.low.amp={rightVibrationValue.AmplitudeLow}, " + - $"R.high.amp={rightVibrationValue.AmplitudeHigh} " + - $"R.low.freq={rightVibrationValue.FrequencyLow}, " + - $"R.high.freq={rightVibrationValue.FrequencyHigh}"); + return; } + + VibrationValue leftVibrationValue = dualVibrationValue.Item1; + VibrationValue rightVibrationValue = dualVibrationValue.Item2; + + leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; + leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; + rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; + rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; + + if (!controllerConfig.Rumble.UseHDRumble || _gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false) + { + float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15))); + float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85))); + _gamepad?.Rumble(low, high, 0xFFFFFFFF); + } + + Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " + + // Value=value/multiplier * multiplier (result) + $"L.low.amp={leftVibrationValue.AmplitudeLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeLow}), " + + $"L.high.amp={leftVibrationValue.AmplitudeHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeHigh}), " + + $"L.low.freq={leftVibrationValue.FrequencyLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyLow}), " + + $"L.high.freq={leftVibrationValue.FrequencyHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyHigh}), " + + $"R.low.amp={rightVibrationValue.AmplitudeLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeLow}), " + + $"R.high.amp={rightVibrationValue.AmplitudeHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeHigh}), " + + $"R.low.freq={rightVibrationValue.FrequencyLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyLow}), " + + $"R.high.freq={rightVibrationValue.FrequencyHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyHigh})"); } } } diff --git a/src/Ryujinx.Input/IGamepad.cs b/src/Ryujinx.Input/IGamepad.cs index d45ac0444..587fd53c0 100644 --- a/src/Ryujinx.Input/IGamepad.cs +++ b/src/Ryujinx.Input/IGamepad.cs @@ -80,10 +80,7 @@ namespace Ryujinx.Input /// /// The vibration data for the left side /// The vibration data for the right side - bool HDRumble(VibrationValue left, VibrationValue right) - { - return false; - } + bool HDRumble(VibrationValue left, VibrationValue right); /// /// Starts a rumble effect on the gamepad. @@ -91,10 +88,10 @@ namespace Ryujinx.Input /// The intensity of the low frequency from 0.0f to 1.0f /// The intensity of the high frequency from 0.0f to 1.0f /// The duration of the rumble effect in milliseconds. - void Rumble(float lowFrequency, float highFrequency, uint durationMs); + bool Rumble(float lowFrequency, float highFrequency, uint durationMs); /// - /// Get a snaphost of the state of the gamepad that is remapped with the informations from the set via . + /// Get a snaphost of the state of the gamepad that is remapped with the information from the set via . /// /// A remapped snaphost of the state of the gamepad. GamepadStateSnapshot GetMappedStateSnapshot(); diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs index af61b7b63..3574c3061 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs @@ -221,6 +221,7 @@ namespace Ryujinx.Headless StrongRumble = 1f, WeakRumble = 1f, EnableRumble = false, + UseHDRumble = true }, }; } diff --git a/src/Ryujinx/Input/AvaloniaKeyboard.cs b/src/Ryujinx/Input/AvaloniaKeyboard.cs index 031d8b033..704a15ba7 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboard.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboard.cs @@ -1,6 +1,7 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.Input; using System; using System.Collections.Generic; @@ -149,9 +150,20 @@ namespace Ryujinx.Ava.Input Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard"); } - public void SetTriggerThreshold(float triggerThreshold) { } + public void SetTriggerThreshold(float triggerThreshold) + { + // No operations + } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) { } + public bool HDRumble(VibrationValue left, VibrationValue right) + { + return false; + } + + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) + { + return false; + } public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero; diff --git a/src/Ryujinx/Input/AvaloniaMouse.cs b/src/Ryujinx/Input/AvaloniaMouse.cs index 52a341a01..8c449b9ee 100644 --- a/src/Ryujinx/Input/AvaloniaMouse.cs +++ b/src/Ryujinx/Input/AvaloniaMouse.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.Input; using System; using System.Drawing; @@ -64,8 +65,13 @@ namespace Ryujinx.Ava.Input { throw new NotImplementedException(); } + + public bool HDRumble(VibrationValue left, VibrationValue right) + { + throw new NotImplementedException(); + } - public void Rumble(float lowFrequency, float highFrequency, uint durationMs) + public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) { throw new NotImplementedException(); } diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 344962f09..35abab811 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -46,6 +46,7 @@ namespace Ryujinx.Ava private const uint MbIconwarning = 0x30; + [STAThread] public static int Main(string[] args) { Version = ReleaseInformation.Version; @@ -54,17 +55,39 @@ namespace Ryujinx.Ava { if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041)) { - _ = Win32NativeInterop.MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n", $"Ryujinx {Version}", MbIconwarning); + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run on an outdated version of Windows. Exiting..."); + _ = Win32NativeInterop.MessageBoxA(nint.Zero, + "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n", + $"Ryujinx {Version}", MbIconwarning); return 0; } - var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + string onedriveFiles = Environment.GetEnvironmentVariable("Onedrive"); + string onedriveConsumerFiles = Environment.GetEnvironmentVariable("OnedriveConsumer"); + string onedriveCommercialFiles = Environment.GetEnvironmentVariable("OnedriveCommercial"); + + // Apparently not everyone has OneDrive shoved onto their system. + if ((onedriveFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveFiles)) + || (onedriveConsumerFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveConsumerFiles)) + || (onedriveCommercialFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveCommercialFiles))) + { + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from a OneDrive folder. Exiting..."); + _ = Win32NativeInterop.MessageBoxA(nint.Zero, + "Ryujinx is not intended to be run from a OneDrive folder. Please move it out and relaunch.", + $"Ryujinx {Version}", MbIconwarning); + return 0; + } + + string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); if (Environment.CurrentDirectory.StartsWithIgnoreCase(programFiles) || Environment.CurrentDirectory.StartsWithIgnoreCase(programFilesX86)) { - _ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.", $"Ryujinx {Version}", MbIconwarning); + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the Program Files folder. Exiting..."); + _ = Win32NativeInterop.MessageBoxA(nint.Zero, + "Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.", + $"Ryujinx {Version}", MbIconwarning); return 0; } @@ -74,10 +97,70 @@ namespace Ryujinx.Ava // ...but this reads like it checks if the current is in/has the Windows admin role? lol if (new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)) { - _ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run as administrator.", $"Ryujinx {Version}", MbIconwarning); + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting..."); + _ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run as administrator.", + $"Ryujinx {Version}", MbIconwarning); return 0; } } + else // Unix + { + // sudo check + [DllImport("libc")] + static extern uint geteuid(); + bool root = geteuid().Equals(0); + + if (OperatingSystem.IsMacOS()) + { + if (root) + { + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting..."); + macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}", + "Ryujinx is not intended to be run as administrator.", "Ok"); + return 0; + } + + string downloadFiles = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"); + + if (Environment.CurrentDirectory.StartsWithIgnoreCase(downloadFiles)) + { + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the Downloads folder. Exiting..."); + macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}", + "Ryujinx is not intended to be run from the Downloads folder.", "Ok"); + return 0; + } + + string icloudFiles = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library/Mobile Documents/com~apple~CloudDocs"); + + if (Environment.CurrentDirectory.StartsWithIgnoreCase(icloudFiles)) + { + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the iCloud folder. Exiting..."); + macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}", + "Ryujinx is not intended to be run from the iCloud folder.", "Ok"); + return 0; + } + } + + if (OperatingSystem.IsLinux()) + { + if (root) + { + Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting..."); + LinuxSDLInterop.SimpleMessageBox($"Ryujinx {Version}", "Ryujinx is not intended to be run as administrator."); + return 0; + } + + string container = Environment.GetEnvironmentVariable("container"); + + if (container is not null && container.EqualsIgnoreCase("flatpak")) + { + Logger.Info?.PrintMsg(LogClass.Application, "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-="); + Logger.Warning?.PrintMsg(LogClass.Application, "This is very likely an unofficial build, Ryujinx does NOT have a flatpak!"); + Logger.Info?.PrintMsg(LogClass.Application, "Please visit https://ryujinx.app/ for our official builds."); + Logger.Info?.PrintMsg(LogClass.Application, "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-="); + } + } + } bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui"); bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps"); @@ -316,7 +399,7 @@ namespace Ryujinx.Ava "never" => HideCursorMode.Never, "onidle" => HideCursorMode.OnIdle, "always" => HideCursorMode.Always, - _ => ConfigurationState.Instance.HideCursor, + _ => ConfigurationState.Instance.HideCursor }; // Check if memoryManagerMode was overridden. diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 3bfddbff6..8a89f3a46 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -28,14 +28,14 @@ true partial - + - + true @@ -63,7 +63,7 @@ - + @@ -73,7 +73,7 @@ - + diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs index 8890055ac..1b86e4f39 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs @@ -334,6 +334,7 @@ namespace Ryujinx.Ava.Systems.Configuration EnableRumble = false, StrongRumble = 1f, WeakRumble = 1f, + UseHDRumble = true }; } } diff --git a/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs b/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs index 649c3cad6..d8df7b627 100644 --- a/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs +++ b/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs @@ -1070,6 +1070,23 @@ namespace Ryujinx.Ava.Systems.PlayReport _ => FormattedValue.ForceReset }; + private static FormattedValue TomodachiLifeLTD_Status(SingleValue value) + { + MessagePackObject messagePackObject = value.Matched.PackedValue; + MessagePackObjectDictionary messagePackObjectDictionary = messagePackObject.AsDictionary(); + + int miiCount = messagePackObjectDictionary["MiiNum"].AsInt32(); + int fountainLevel = messagePackObjectDictionary["FountainLevel"].AsInt32(); + + return $"Looking after {"Mii".ToQuantity(miiCount)}, with an island level of {fountainLevel}"; + } + + private static FormattedValue AnimalCrossingNewHorizons_AppCommon(SingleValue value) + { + MessagePackObject messagePackObject = value.Matched.PackedValue; + MessagePackObjectDictionary messagePackObjectDictionary = messagePackObject.AsDictionary(); + return $"Living on {messagePackObjectDictionary["LandName"].AsString()} Island"; + } } } diff --git a/src/Ryujinx/Systems/PlayReport/PlayReports.cs b/src/Ryujinx/Systems/PlayReport/PlayReports.cs index d483515bb..8880ed5d4 100644 --- a/src/Ryujinx/Systems/PlayReport/PlayReports.cs +++ b/src/Ryujinx/Systems/PlayReport/PlayReports.cs @@ -119,6 +119,19 @@ namespace Ryujinx.Ava.Systems.PlayReport "based on what game you first launch.\n\nNSO emulators do not print any Play Report information past the first game launch so it's all we got.") .AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame) ) + .AddSpec( + [ "010051f0207b2000", "0100ca502552a000" ], // Tomodachi Life: Living the Dream + Demo + spec => spec + .WithDescription( + "based on your total Mii count and island level.") + .AddValueFormatter("Common", TomodachiLifeLTD_Status) + ) + .AddSpec( + "01006f8002326000", // Animal Crossing New Horizons + spec => spec + .WithDescription("based on your island name.") + .AddValueFormatter("AppCmn", AnimalCrossingNewHorizons_AppCommon) + ) ); private static string Playing(string game) => $"Playing {game}"; diff --git a/src/Ryujinx/UI/Helpers/LinuxSDLInterop.cs b/src/Ryujinx/UI/Helpers/LinuxSDLInterop.cs new file mode 100644 index 000000000..95885ba5c --- /dev/null +++ b/src/Ryujinx/UI/Helpers/LinuxSDLInterop.cs @@ -0,0 +1,30 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class LinuxSDLInterop + { + // TODO: add a parameter for prompt style + // TODO: look into adding text for the button + // TODO: check success of prompt box + public static int SimpleMessageBox(string caption, string text) + { + const string sdl = "SDL2"; + + [DllImport(sdl)] + static extern int SDL_Init(uint flags); + + [DllImport(sdl, CallingConvention = CallingConvention.Cdecl)] + static extern int SDL_ShowSimpleMessageBox(uint flags, string title, string message, IntPtr window); + + [DllImport(sdl)] + static extern void SDL_Quit(); + + SDL_Init(0); + SDL_ShowSimpleMessageBox(32 /* 32 = warning style */, caption, text, IntPtr.Zero); + SDL_Quit(); + return 0; + } + } +} diff --git a/src/Ryujinx/UI/Helpers/macOSNativeInterop.cs b/src/Ryujinx/UI/Helpers/macOSNativeInterop.cs new file mode 100644 index 000000000..6dda1c823 --- /dev/null +++ b/src/Ryujinx/UI/Helpers/macOSNativeInterop.cs @@ -0,0 +1,62 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class macOSNativeInterop + { + // TODO: add a parameter for prompt style + // TODO: check success of prompt box + public static int SimpleMessageBox(string caption, string text, string button) + { + + // Grab what we need to make the message box. + const string ObjCRuntime = "/usr/lib/libobjc.A.dylib"; + const string FoundationFramework = "/System/Library/Frameworks/Foundation.framework/Foundation"; + const string AppKitFramework = "/System/Library/Frameworks/AppKit.framework/AppKit"; + + [DllImport(ObjCRuntime, EntryPoint = "sel_registerName")] + static extern IntPtr GetSelector(string name); + + [DllImport(ObjCRuntime, EntryPoint = "objc_getClass")] + static extern IntPtr GetClass(string name); + + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] + static extern IntPtr SendMessage(IntPtr target, IntPtr selector); + + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] + static extern IntPtr SendMessageWithParameter(IntPtr target, IntPtr selector, IntPtr param); + + [DllImport(ObjCRuntime)] + static extern IntPtr dlopen(string path, int mode); + + dlopen(AppKitFramework, 0x1); // have to invoke AppKit so that NSAlert doesn't return a null pointer + + IntPtr NSStringClass = GetClass("NSString"); + IntPtr Selector = GetSelector("stringWithUTF8String:"); + IntPtr SharedApp = SendMessage(GetClass("NSApplication"), GetSelector("sharedApplication")); + IntPtr NSAlert = SendMessage(GetClass("NSAlert"), GetSelector("alloc")); + IntPtr AlertInstance = SendMessage(NSAlert, GetSelector("init")); + + // Create caption, text, and button text. + IntPtr boxCaption = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi(caption)); + IntPtr boxText = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi(text)); + IntPtr boxButton = SendMessageWithParameter(NSStringClass, Selector, Marshal.StringToHGlobalAnsi(button)); + + // Set up the window. + SendMessageWithParameter(SharedApp, GetSelector("setActivationPolicy:"), IntPtr.Zero); // Give it a window. + SendMessageWithParameter(SharedApp, GetSelector("activateIgnoringOtherApps:"), (IntPtr) 1); // Force it to the front. + + // Set up the message box. + SendMessageWithParameter(AlertInstance, GetSelector("setAlertStyle:"), IntPtr.Zero); // Set style to warning. + SendMessageWithParameter(AlertInstance, GetSelector("setMessageText:"), boxCaption); + SendMessageWithParameter(AlertInstance, GetSelector("setInformativeText:"), boxText); + SendMessageWithParameter(AlertInstance, GetSelector("addButtonWithTitle:"), boxButton); + + // Send prompt to user, then clean up. + SendMessage(AlertInstance, GetSelector("runModal")); + SendMessage(AlertInstance, GetSelector("release")); + return 0; + } + } +} diff --git a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs index 0eeef45f5..2a076f525 100644 --- a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs +++ b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs @@ -20,6 +20,7 @@ namespace Ryujinx.Ava.UI.Models.Input public float WeakRumble { get; set; } public float StrongRumble { get; set; } + public bool UseHDRumble { get; set; } public string Id { get; set; } @@ -236,6 +237,7 @@ namespace Ryujinx.Ava.UI.Models.Input EnableRumble = controllerInput.Rumble.EnableRumble; WeakRumble = controllerInput.Rumble.WeakRumble; StrongRumble = controllerInput.Rumble.StrongRumble; + UseHDRumble = controllerInput.Rumble.UseHDRumble; } if (controllerInput.Led != null) @@ -307,6 +309,7 @@ namespace Ryujinx.Ava.UI.Models.Input EnableRumble = EnableRumble, WeakRumble = WeakRumble, StrongRumble = StrongRumble, + UseHDRumble = UseHDRumble, }, Led = new LedConfigController { diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index 51229af72..68559d6c1 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -789,6 +789,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input StrongRumble = 1f, WeakRumble = 1f, EnableRumble = false, + UseHDRumble = true }, }; } diff --git a/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs index e2323f567..74e0cd289 100644 --- a/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs @@ -9,5 +9,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input [ObservableProperty] public partial float WeakRumble { get; set; } + + [ObservableProperty] + public partial bool EnableHDRumble { get; set; } } } diff --git a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml index 98489aab0..49eb1a717 100644 --- a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml @@ -53,6 +53,15 @@ Margin="5,0" Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" /> + + + diff --git a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs index 347d011d5..655bdf591 100644 --- a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs @@ -22,6 +22,7 @@ namespace Ryujinx.Ava.UI.Views.Input { StrongRumble = config.StrongRumble, WeakRumble = config.WeakRumble, + EnableHDRumble = config.UseHDRumble }; InitializeComponent(); @@ -45,6 +46,7 @@ namespace Ryujinx.Ava.UI.Views.Input GamepadInputConfig config = viewModel.Config; config.StrongRumble = content.ViewModel.StrongRumble; config.WeakRumble = content.ViewModel.WeakRumble; + config.UseHDRumble = content.ViewModel.EnableHDRumble; }; await contentDialog.ShowAsync();