Compare commits
6 Commits
dfbf390bbf
...
Canary-1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e8157688e | ||
|
|
5085af0050 | ||
|
|
2c8edaf89e | ||
|
|
aa8ba8b503 | ||
|
|
a4211fec33 | ||
|
|
54b233dd78 |
@@ -332,6 +332,7 @@
|
|||||||
0100E680149DC000,"Arcaea",,playable,2023-03-16 19:31:21
|
0100E680149DC000,"Arcaea",,playable,2023-03-16 19:31:21
|
||||||
01003C2010C78000,"Archaica: The Path Of Light",crash,nothing,2020-10-16 13:22:26
|
01003C2010C78000,"Archaica: The Path Of Light",crash,nothing,2020-10-16 13:22:26
|
||||||
01004DA012976000,"Area 86",,playable,2020-12-16 16:45:52
|
01004DA012976000,"Area 86",,playable,2020-12-16 16:45:52
|
||||||
|
01008d8006a6a000,"Arena of Valor",crash,boots,2025-02-03 22:19:34
|
||||||
0100691013C46000,"ARIA CHRONICLE",,playable,2022-11-16 13:50:55
|
0100691013C46000,"ARIA CHRONICLE",,playable,2022-11-16 13:50:55
|
||||||
0100D4A00B284000,"ARK: Survival Evolved",gpu;nvdec;online-broken;UE4;ldn-untested,ingame,2024-04-16 00:53:56
|
0100D4A00B284000,"ARK: Survival Evolved",gpu;nvdec;online-broken;UE4;ldn-untested,ingame,2024-04-16 00:53:56
|
||||||
0100C56012C96000,"Arkanoid vs. Space Invaders",services,ingame,2021-01-21 12:50:30
|
0100C56012C96000,"Arkanoid vs. Space Invaders",services,ingame,2021-01-21 12:50:30
|
||||||
@@ -426,6 +427,7 @@
|
|||||||
0100E48013A34000,"Balan Wonderworld Demo",gpu;services;UE4;demo,ingame,2023-02-16 20:05:07
|
0100E48013A34000,"Balan Wonderworld Demo",gpu;services;UE4;demo,ingame,2023-02-16 20:05:07
|
||||||
0100CD801CE5E000,"Balatro",,ingame,2024-04-21 02:01:53
|
0100CD801CE5E000,"Balatro",,ingame,2024-04-21 02:01:53
|
||||||
010010A00DA48000,"Baldur's Gate and Baldur's Gate II: Enhanced Editions",32-bit,playable,2022-09-12 23:52:15
|
010010A00DA48000,"Baldur's Gate and Baldur's Gate II: Enhanced Editions",32-bit,playable,2022-09-12 23:52:15
|
||||||
|
0100fd1014726000,"Baldur's Gate: Dark Alliance",ldn-untested,ingame,2025-02-03 22:21:00
|
||||||
0100BC400FB64000,"Balthazar's Dream",,playable,2022-09-13 00:13:22
|
0100BC400FB64000,"Balthazar's Dream",,playable,2022-09-13 00:13:22
|
||||||
01008D30128E0000,"Bamerang",,playable,2022-10-26 00:29:39
|
01008D30128E0000,"Bamerang",,playable,2022-10-26 00:29:39
|
||||||
010013C010C5C000,"Banner of the Maid",,playable,2021-06-14 15:23:37
|
010013C010C5C000,"Banner of the Maid",,playable,2021-06-14 15:23:37
|
||||||
@@ -528,6 +530,7 @@
|
|||||||
01005950022EC000,"Blade Strangers",nvdec,playable,2022-07-17 19:02:43
|
01005950022EC000,"Blade Strangers",nvdec,playable,2022-07-17 19:02:43
|
||||||
0100DF0011A6A000,"Bladed Fury",,playable,2022-10-26 11:36:26
|
0100DF0011A6A000,"Bladed Fury",,playable,2022-10-26 11:36:26
|
||||||
0100CFA00CC74000,"Blades of Time",deadlock;online,boots,2022-07-17 19:19:58
|
0100CFA00CC74000,"Blades of Time",deadlock;online,boots,2022-07-17 19:19:58
|
||||||
|
01003d700dd8a000,"Blades",,boots,2025-02-03 22:22:00
|
||||||
01006CC01182C000,"Blair Witch",nvdec;UE4,playable,2022-10-01 14:06:16
|
01006CC01182C000,"Blair Witch",nvdec;UE4,playable,2022-10-01 14:06:16
|
||||||
010039501405E000,"Blanc",gpu;slow,ingame,2023-02-22 14:00:13
|
010039501405E000,"Blanc",gpu;slow,ingame,2023-02-22 14:00:13
|
||||||
0100698009C6E000,"Blasphemous",nvdec,playable,2021-03-01 12:15:31
|
0100698009C6E000,"Blasphemous",nvdec,playable,2021-03-01 12:15:31
|
||||||
@@ -955,7 +958,7 @@
|
|||||||
010012800EBAE000,"Disney TSUM TSUM FESTIVAL",crash,menus,2020-07-14 14:05:28
|
010012800EBAE000,"Disney TSUM TSUM FESTIVAL",crash,menus,2020-07-14 14:05:28
|
||||||
01009740120FE000,"DISTRAINT 2",,playable,2020-09-03 16:08:12
|
01009740120FE000,"DISTRAINT 2",,playable,2020-09-03 16:08:12
|
||||||
010075B004DD2000,"DISTRAINT: Deluxe Edition",,playable,2020-06-15 23:42:24
|
010075B004DD2000,"DISTRAINT: Deluxe Edition",,playable,2020-06-15 23:42:24
|
||||||
010027400CDC6000,"Divinity: Original Sin 2 - Definitive Edition",services;crash;online-broken;regression,menus,2023-08-13 17:20:03
|
010027400CDC6000,"Divinity: Original Sin 2 - Definitive Edition",services;crash;online-broken;regression,ingame,2025-02-03 22:12:30
|
||||||
01001770115C8000,"Dodo Peak",nvdec;UE4,playable,2022-10-04 16:13:05
|
01001770115C8000,"Dodo Peak",nvdec;UE4,playable,2022-10-04 16:13:05
|
||||||
010077B0100DA000,"Dogurai",,playable,2020-10-04 02:40:16
|
010077B0100DA000,"Dogurai",,playable,2020-10-04 02:40:16
|
||||||
010048100D51A000,"Dokapon Up! Mugen no Roulette",gpu;Needs Update,menus,2022-12-08 19:39:10
|
010048100D51A000,"Dokapon Up! Mugen no Roulette",gpu;Needs Update,menus,2022-12-08 19:39:10
|
||||||
@@ -1654,7 +1657,7 @@
|
|||||||
0100A73006E74000,"Legendary Eleven",,playable,2021-06-08 12:09:03
|
0100A73006E74000,"Legendary Eleven",,playable,2021-06-08 12:09:03
|
||||||
0100A7700B46C000,"Legendary Fishing",online,playable,2021-04-14 15:08:46
|
0100A7700B46C000,"Legendary Fishing",online,playable,2021-04-14 15:08:46
|
||||||
0100739018020000,"LEGO® 2K Drive",gpu;ldn-works,ingame,2024-04-09 02:05:12
|
0100739018020000,"LEGO® 2K Drive",gpu;ldn-works,ingame,2024-04-09 02:05:12
|
||||||
01003A30012C0000,"LEGO® CITY Undercover",nvdec,playable,2024-09-30 08:44:27
|
010085500130a000,"LEGO® CITY Undercover",nvdec,playable,2024-09-30 08:44:27
|
||||||
010070D009FEC000,"LEGO® DC Super-Villains",,playable,2021-05-27 18:10:37
|
010070D009FEC000,"LEGO® DC Super-Villains",,playable,2021-05-27 18:10:37
|
||||||
010052A00B5D2000,"LEGO® Harry Potter™ Collection",crash,ingame,2024-01-31 10:28:07
|
010052A00B5D2000,"LEGO® Harry Potter™ Collection",crash,ingame,2024-01-31 10:28:07
|
||||||
010073C01AF34000,"LEGO® Horizon Adventures™",vulkan-backend-bug;opengl-backend-bug;UE4,ingame,2025-01-07 04:24:56
|
010073C01AF34000,"LEGO® Horizon Adventures™",vulkan-backend-bug;opengl-backend-bug;UE4,ingame,2025-01-07 04:24:56
|
||||||
@@ -1913,6 +1916,7 @@
|
|||||||
010073E008E6E000,"Mugsters",,playable,2021-01-28 17:57:17
|
010073E008E6E000,"Mugsters",,playable,2021-01-28 17:57:17
|
||||||
0100A8400471A000,"MUJO",,playable,2020-05-08 16:31:04
|
0100A8400471A000,"MUJO",,playable,2020-05-08 16:31:04
|
||||||
0100211005E94000,"Mulaka",,playable,2021-01-28 18:07:20
|
0100211005E94000,"Mulaka",,playable,2021-01-28 18:07:20
|
||||||
|
01008e2013fb4000,"Multi Quiz",ldn-untested,ingame,2025-02-03 22:26:00
|
||||||
010038B00B9AE000,"Mummy Pinball",,playable,2022-08-05 16:08:11
|
010038B00B9AE000,"Mummy Pinball",,playable,2022-08-05 16:08:11
|
||||||
01008E200C5C2000,"Muse Dash",,playable,2020-06-06 14:41:29
|
01008E200C5C2000,"Muse Dash",,playable,2020-06-06 14:41:29
|
||||||
010035901046C000,"Mushroom Quest",,playable,2020-05-17 13:07:08
|
010035901046C000,"Mushroom Quest",,playable,2020-05-17 13:07:08
|
||||||
@@ -2028,6 +2032,7 @@
|
|||||||
010003C00B868000,"Ninjin: Clash of Carrots",online-broken,playable,2024-07-10 05:12:26
|
010003C00B868000,"Ninjin: Clash of Carrots",online-broken,playable,2024-07-10 05:12:26
|
||||||
0100746010E4C000,"NinNinDays",,playable,2022-11-20 15:17:29
|
0100746010E4C000,"NinNinDays",,playable,2022-11-20 15:17:29
|
||||||
0100C9A00ECE6000,"Nintendo 64™ – Nintendo Switch Online",gpu;vulkan,ingame,2024-04-23 20:21:07
|
0100C9A00ECE6000,"Nintendo 64™ – Nintendo Switch Online",gpu;vulkan,ingame,2024-04-23 20:21:07
|
||||||
|
0100e0601c632000,"Nintendo 64™ – Nintendo Switch Online: MATURE 17+",,ingame,2025-02-03 22:27:00
|
||||||
0100D870045B6000,"Nintendo Entertainment System™ - Nintendo Switch Online",online,playable,2022-07-01 15:45:06
|
0100D870045B6000,"Nintendo Entertainment System™ - Nintendo Switch Online",online,playable,2022-07-01 15:45:06
|
||||||
0100C4B0034B2000,"Nintendo Labo Toy-Con 01 Variety Kit",gpu,ingame,2022-08-07 12:56:07
|
0100C4B0034B2000,"Nintendo Labo Toy-Con 01 Variety Kit",gpu,ingame,2022-08-07 12:56:07
|
||||||
01001E9003502000,"Nintendo Labo Toy-Con 03 Vehicle Kit",services;crash,menus,2022-08-03 17:20:11
|
01001E9003502000,"Nintendo Labo Toy-Con 03 Vehicle Kit",services;crash,menus,2022-08-03 17:20:11
|
||||||
@@ -2532,7 +2537,7 @@
|
|||||||
0100C3E00B700000,"SEGA AGES Space Harrier",,playable,2021-01-11 12:57:40
|
0100C3E00B700000,"SEGA AGES Space Harrier",,playable,2021-01-11 12:57:40
|
||||||
010054400D2E6000,"SEGA AGES Virtua Racing",online-broken,playable,2023-01-29 17:08:39
|
010054400D2E6000,"SEGA AGES Virtua Racing",online-broken,playable,2023-01-29 17:08:39
|
||||||
01001E700AC60000,"SEGA AGES Wonder Boy: Monster Land",online,playable,2021-05-05 16:28:25
|
01001E700AC60000,"SEGA AGES Wonder Boy: Monster Land",online,playable,2021-05-05 16:28:25
|
||||||
0100B3C014BDA000,"SEGA Genesis™ – Nintendo Switch Online",crash;regression,nothing,2022-04-11 07:27:21
|
0100B3C014BDA000,"SEGA Genesis™ – Nintendo Switch Online",crash;regression,ingame,2025-02-03 22:13:30
|
||||||
0100F7300B24E000,"SEGA Mega Drive Classics",online,playable,2021-01-05 11:08:00
|
0100F7300B24E000,"SEGA Mega Drive Classics",online,playable,2021-01-05 11:08:00
|
||||||
01009840046BC000,"Semispheres",,playable,2021-01-06 23:08:31
|
01009840046BC000,"Semispheres",,playable,2021-01-06 23:08:31
|
||||||
0100D1800D902000,"SENRAN KAGURA Peach Ball",,playable,2021-06-03 15:12:10
|
0100D1800D902000,"SENRAN KAGURA Peach Ball",,playable,2021-06-03 15:12:10
|
||||||
@@ -2964,6 +2969,7 @@
|
|||||||
0100C38004DCC000,"The Flame In The Flood: Complete Edition",gpu;nvdec;UE4,ingame,2022-08-22 16:23:49
|
0100C38004DCC000,"The Flame In The Flood: Complete Edition",gpu;nvdec;UE4,ingame,2022-08-22 16:23:49
|
||||||
010007700D4AC000,"The Forbidden Arts",,playable,2021-01-26 16:26:24
|
010007700D4AC000,"The Forbidden Arts",,playable,2021-01-26 16:26:24
|
||||||
010030700CBBC000,"The friends of Ringo Ishikawa",,playable,2022-08-22 16:33:17
|
010030700CBBC000,"The friends of Ringo Ishikawa",,playable,2022-08-22 16:33:17
|
||||||
|
0100b620139d8000,"The Game of Life 2",ldn-untested,ingame,2025-02-03 22:30:00
|
||||||
01006350148DA000,"The Gardener and the Wild Vines",gpu,ingame,2024-04-29 16:32:10
|
01006350148DA000,"The Gardener and the Wild Vines",gpu,ingame,2024-04-29 16:32:10
|
||||||
0100B13007A6A000,"The Gardens Between",,playable,2021-01-29 16:16:53
|
0100B13007A6A000,"The Gardens Between",,playable,2021-01-29 16:16:53
|
||||||
010036E00FB20000,"The Great Ace Attorney Chronicles",,playable,2023-06-22 21:26:29
|
010036E00FB20000,"The Great Ace Attorney Chronicles",,playable,2023-06-22 21:26:29
|
||||||
@@ -2981,6 +2987,8 @@
|
|||||||
010015D003EE4000,"The Jackbox Party Pack 2",online-working,playable,2022-08-22 18:23:40
|
010015D003EE4000,"The Jackbox Party Pack 2",online-working,playable,2022-08-22 18:23:40
|
||||||
0100CC80013D6000,"The Jackbox Party Pack 3",slow;online-working,playable,2022-08-22 18:41:06
|
0100CC80013D6000,"The Jackbox Party Pack 3",slow;online-working,playable,2022-08-22 18:41:06
|
||||||
0100E1F003EE8000,"The Jackbox Party Pack 4",online-working,playable,2022-08-22 18:56:34
|
0100E1F003EE8000,"The Jackbox Party Pack 4",online-working,playable,2022-08-22 18:56:34
|
||||||
|
01006fe0096ac000,"The Jackbox Party Pack 5",ldn-untested,boots,2025-02-03 22:32:00
|
||||||
|
01005a400db52000,"The Jackbox Party Pack 6",ldn-untested,boots,2025-02-03 22:32:00
|
||||||
010052C00B184000,"The Journey Down: Chapter One",nvdec,playable,2021-02-24 13:32:41
|
010052C00B184000,"The Journey Down: Chapter One",nvdec,playable,2021-02-24 13:32:41
|
||||||
01006BC00B188000,"The Journey Down: Chapter Three",nvdec,playable,2021-02-24 13:45:27
|
01006BC00B188000,"The Journey Down: Chapter Three",nvdec,playable,2021-02-24 13:45:27
|
||||||
01009AB00B186000,"The Journey Down: Chapter Two",nvdec,playable,2021-02-24 13:32:13
|
01009AB00B186000,"The Journey Down: Chapter Two",nvdec,playable,2021-02-24 13:32:13
|
||||||
@@ -3159,6 +3167,7 @@
|
|||||||
010055E00CA68000,"Trine 4: The Nightmare Prince",gpu,nothing,2025-01-07 05:47:46
|
010055E00CA68000,"Trine 4: The Nightmare Prince",gpu,nothing,2025-01-07 05:47:46
|
||||||
0100D9000A930000,"Trine Enchanted Edition",ldn-untested;nvdec,playable,2021-06-03 11:28:15
|
0100D9000A930000,"Trine Enchanted Edition",ldn-untested;nvdec,playable,2021-06-03 11:28:15
|
||||||
01002D7010A54000,"Trinity Trigger",crash,ingame,2023-03-03 03:09:09
|
01002D7010A54000,"Trinity Trigger",crash,ingame,2023-03-03 03:09:09
|
||||||
|
010020700a5e0000,"TRIVIAL PURSUIT Live!",ldn-untested,ingame,2025-02-03 22:35:00
|
||||||
0100868013FFC000,"TRIVIAL PURSUIT Live! 2",,boots,2022-12-19 00:04:33
|
0100868013FFC000,"TRIVIAL PURSUIT Live! 2",,boots,2022-12-19 00:04:33
|
||||||
0100F78002040000,"Troll and I™",gpu;nvdec,ingame,2021-06-04 16:58:50
|
0100F78002040000,"Troll and I™",gpu;nvdec,ingame,2021-06-04 16:58:50
|
||||||
0100145011008000,"Trollhunters: Defenders of Arcadia",gpu;nvdec,ingame,2020-11-30 13:27:09
|
0100145011008000,"Trollhunters: Defenders of Arcadia",gpu;nvdec,ingame,2020-11-30 13:27:09
|
||||||
@@ -3208,6 +3217,7 @@
|
|||||||
0100AB2010B4C000,"Unlock The King",,playable,2020-09-01 13:58:27
|
0100AB2010B4C000,"Unlock The King",,playable,2020-09-01 13:58:27
|
||||||
0100A3E011CB0000,"Unlock the King 2",,playable,2021-06-15 20:43:55
|
0100A3E011CB0000,"Unlock the King 2",,playable,2021-06-15 20:43:55
|
||||||
01005AA00372A000,"UNO® for Nintendo Switch",nvdec;ldn-untested,playable,2022-07-28 14:49:47
|
01005AA00372A000,"UNO® for Nintendo Switch",nvdec;ldn-untested,playable,2022-07-28 14:49:47
|
||||||
|
0100b6e012ebe000,"UNO",ldn-untested,ingame,2025-02-03 22:40:00
|
||||||
0100E5D00CC0C000,"Unravel Two",nvdec,playable,2024-05-23 15:45:05
|
0100E5D00CC0C000,"Unravel Two",nvdec,playable,2024-05-23 15:45:05
|
||||||
010001300CC4A000,"Unruly Heroes",,playable,2021-01-07 18:09:31
|
010001300CC4A000,"Unruly Heroes",,playable,2021-01-07 18:09:31
|
||||||
0100B410138C0000,"Unspottable",,playable,2022-10-25 19:28:49
|
0100B410138C0000,"Unspottable",,playable,2022-10-25 19:28:49
|
||||||
@@ -3372,6 +3382,7 @@
|
|||||||
0100F47016F26000,"Yomawari 3",,playable,2022-05-10 08:26:51
|
0100F47016F26000,"Yomawari 3",,playable,2022-05-10 08:26:51
|
||||||
010012F00B6F2000,"Yomawari: The Long Night Collection",,playable,2022-09-03 14:36:59
|
010012F00B6F2000,"Yomawari: The Long Night Collection",,playable,2022-09-03 14:36:59
|
||||||
0100CC600ABB2000,"Yonder: The Cloud Catcher Chronicles (Retail Only)",,playable,2021-01-28 14:06:25
|
0100CC600ABB2000,"Yonder: The Cloud Catcher Chronicles (Retail Only)",,playable,2021-01-28 14:06:25
|
||||||
|
0100534009ff2000,"Yonder: The Cloud Catcher Chronicles",,playable,2025-02-03 22:19:13
|
||||||
0100BE50042F6000,"Yono and the Celestial Elephants",,playable,2021-01-28 18:23:58
|
0100BE50042F6000,"Yono and the Celestial Elephants",,playable,2021-01-28 18:23:58
|
||||||
0100F110029C8000,"Yooka-Laylee",,playable,2021-01-28 14:21:45
|
0100F110029C8000,"Yooka-Laylee",,playable,2021-01-28 14:21:45
|
||||||
010022F00DA66000,"Yooka-Laylee and the Impossible Lair",,playable,2021-03-05 17:32:21
|
010022F00DA66000,"Yooka-Laylee and the Impossible Lair",,playable,2021-03-05 17:32:21
|
||||||
|
|||||||
|
@@ -1,4 +1,3 @@
|
|||||||
using Ryujinx.Common.Logging;
|
|
||||||
using Ryujinx.SDL2.Common;
|
using Ryujinx.SDL2.Common;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -37,7 +36,6 @@ namespace Ryujinx.Input.SDL2
|
|||||||
SDL2Driver.Instance.Initialize();
|
SDL2Driver.Instance.Initialize();
|
||||||
SDL2Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected;
|
SDL2Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected;
|
||||||
SDL2Driver.Instance.OnJoystickDisconnected += HandleJoyStickDisconnected;
|
SDL2Driver.Instance.OnJoystickDisconnected += HandleJoyStickDisconnected;
|
||||||
SDL2Driver.Instance.OnJoyBatteryUpdated += HandleJoyBatteryUpdated;
|
|
||||||
|
|
||||||
// Add already connected gamepads
|
// Add already connected gamepads
|
||||||
int numJoysticks = SDL_NumJoysticks();
|
int numJoysticks = SDL_NumJoysticks();
|
||||||
@@ -85,30 +83,19 @@ namespace Ryujinx.Input.SDL2
|
|||||||
|
|
||||||
private void HandleJoyStickDisconnected(int joystickInstanceId)
|
private void HandleJoyStickDisconnected(int joystickInstanceId)
|
||||||
{
|
{
|
||||||
bool joyConPairDisconnected = false;
|
|
||||||
if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id))
|
if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_gamepadsIds.Remove(id);
|
_gamepadsIds.Remove(id);
|
||||||
if (!SDL2JoyConPair.IsCombinable(_gamepadsIds))
|
|
||||||
{
|
|
||||||
_gamepadsIds.Remove(SDL2JoyConPair.Id);
|
|
||||||
joyConPairDisconnected = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OnGamepadDisconnected?.Invoke(id);
|
OnGamepadDisconnected?.Invoke(id);
|
||||||
if (joyConPairDisconnected)
|
|
||||||
{
|
|
||||||
OnGamepadDisconnected?.Invoke(SDL2JoyConPair.Id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleJoyStickConnected(int joystickDeviceId, int joystickInstanceId)
|
private void HandleJoyStickConnected(int joystickDeviceId, int joystickInstanceId)
|
||||||
{
|
{
|
||||||
bool joyConPairConnected = false;
|
|
||||||
if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE)
|
if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE)
|
||||||
{
|
{
|
||||||
if (_gamepadsInstanceIdsMapping.ContainsKey(joystickInstanceId))
|
if (_gamepadsInstanceIdsMapping.ContainsKey(joystickInstanceId))
|
||||||
@@ -133,29 +120,13 @@ namespace Ryujinx.Input.SDL2
|
|||||||
_gamepadsIds.Insert(joystickDeviceId, id);
|
_gamepadsIds.Insert(joystickDeviceId, id);
|
||||||
else
|
else
|
||||||
_gamepadsIds.Add(id);
|
_gamepadsIds.Add(id);
|
||||||
if (SDL2JoyConPair.IsCombinable(_gamepadsIds))
|
|
||||||
{
|
|
||||||
_gamepadsIds.Remove(SDL2JoyConPair.Id);
|
|
||||||
_gamepadsIds.Add(SDL2JoyConPair.Id);
|
|
||||||
joyConPairConnected = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OnGamepadConnected?.Invoke(id);
|
OnGamepadConnected?.Invoke(id);
|
||||||
if (joyConPairConnected)
|
|
||||||
{
|
|
||||||
OnGamepadConnected?.Invoke(SDL2JoyConPair.Id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleJoyBatteryUpdated(int joystickDeviceId, SDL_JoystickPowerLevel powerLevel)
|
|
||||||
{
|
|
||||||
Logger.Info?.Print(LogClass.Hid,
|
|
||||||
$"{SDL_GameControllerNameForIndex(joystickDeviceId)} power level: {powerLevel}");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
protected virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
@@ -186,14 +157,6 @@ namespace Ryujinx.Input.SDL2
|
|||||||
|
|
||||||
public IGamepad GetGamepad(string id)
|
public IGamepad GetGamepad(string id)
|
||||||
{
|
{
|
||||||
if (id == SDL2JoyConPair.Id)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return SDL2JoyConPair.GetGamepad(_gamepadsIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int joystickIndex = GetJoystickIndexByGamepadId(id);
|
int joystickIndex = GetJoystickIndexByGamepadId(id);
|
||||||
|
|
||||||
if (joystickIndex == -1)
|
if (joystickIndex == -1)
|
||||||
@@ -202,16 +165,12 @@ namespace Ryujinx.Input.SDL2
|
|||||||
}
|
}
|
||||||
|
|
||||||
nint gamepadHandle = SDL_GameControllerOpen(joystickIndex);
|
nint gamepadHandle = SDL_GameControllerOpen(joystickIndex);
|
||||||
|
|
||||||
if (gamepadHandle == nint.Zero)
|
if (gamepadHandle == nint.Zero)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SDL_GameControllerName(gamepadHandle).StartsWith(SDL2JoyCon.Prefix))
|
|
||||||
{
|
|
||||||
return new SDL2JoyCon(gamepadHandle, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SDL2Gamepad(gamepadHandle, id);
|
return new SDL2Gamepad(gamepadHandle, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,409 +0,0 @@
|
|||||||
using Ryujinx.Common.Configuration.Hid;
|
|
||||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
|
||||||
using Ryujinx.Common.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Numerics;
|
|
||||||
using System.Threading;
|
|
||||||
using static SDL2.SDL;
|
|
||||||
|
|
||||||
namespace Ryujinx.Input.SDL2
|
|
||||||
{
|
|
||||||
internal class SDL2JoyCon : IGamepad
|
|
||||||
{
|
|
||||||
private bool HasConfiguration => _configuration != null;
|
|
||||||
|
|
||||||
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, GamepadButtonInputId From)
|
|
||||||
{
|
|
||||||
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not GamepadButtonInputId.Unbound;
|
|
||||||
}
|
|
||||||
|
|
||||||
private StandardControllerInputConfig _configuration;
|
|
||||||
|
|
||||||
private readonly Dictionary<GamepadButtonInputId,SDL_GameControllerButton> _leftButtonsDriverMapping = new()
|
|
||||||
{
|
|
||||||
{ GamepadButtonInputId.LeftStick , SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSTICK },
|
|
||||||
{GamepadButtonInputId.DpadUp ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_Y},
|
|
||||||
{GamepadButtonInputId.DpadDown ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_A},
|
|
||||||
{GamepadButtonInputId.DpadLeft ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_B},
|
|
||||||
{GamepadButtonInputId.DpadRight ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_X},
|
|
||||||
{GamepadButtonInputId.Minus ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_START},
|
|
||||||
{GamepadButtonInputId.LeftShoulder,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE2},
|
|
||||||
{GamepadButtonInputId.LeftTrigger,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE4},
|
|
||||||
{GamepadButtonInputId.SingleRightTrigger0,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
|
|
||||||
{GamepadButtonInputId.SingleLeftTrigger0,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
|
|
||||||
};
|
|
||||||
private readonly Dictionary<GamepadButtonInputId,SDL_GameControllerButton> _rightButtonsDriverMapping = new()
|
|
||||||
{
|
|
||||||
{GamepadButtonInputId.RightStick,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSTICK},
|
|
||||||
{GamepadButtonInputId.A,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_B},
|
|
||||||
{GamepadButtonInputId.B,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_Y},
|
|
||||||
{GamepadButtonInputId.X,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_A},
|
|
||||||
{GamepadButtonInputId.Y,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_X},
|
|
||||||
{GamepadButtonInputId.Plus,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_START},
|
|
||||||
{GamepadButtonInputId.RightShoulder,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE1},
|
|
||||||
{GamepadButtonInputId.RightTrigger,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE3},
|
|
||||||
{GamepadButtonInputId.SingleRightTrigger1,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
|
|
||||||
{GamepadButtonInputId.SingleLeftTrigger1,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSHOULDER}
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly Dictionary<GamepadButtonInputId, SDL_GameControllerButton> _buttonsDriverMapping;
|
|
||||||
private readonly Lock _userMappingLock = new();
|
|
||||||
|
|
||||||
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
|
|
||||||
|
|
||||||
private readonly StickInputId[] _stickUserMapping = new StickInputId[(int)StickInputId.Count]
|
|
||||||
{
|
|
||||||
StickInputId.Unbound, StickInputId.Left, StickInputId.Right,
|
|
||||||
};
|
|
||||||
|
|
||||||
public GamepadFeaturesFlag Features { get; }
|
|
||||||
|
|
||||||
private nint _gamepadHandle;
|
|
||||||
|
|
||||||
private enum JoyConType
|
|
||||||
{
|
|
||||||
Left, Right
|
|
||||||
}
|
|
||||||
|
|
||||||
public const string Prefix = "Nintendo Switch Joy-Con";
|
|
||||||
public const string LeftName = "Nintendo Switch Joy-Con (L)";
|
|
||||||
public const string RightName = "Nintendo Switch Joy-Con (R)";
|
|
||||||
|
|
||||||
private readonly JoyConType _joyConType;
|
|
||||||
|
|
||||||
public SDL2JoyCon(nint gamepadHandle, string driverId)
|
|
||||||
{
|
|
||||||
_gamepadHandle = gamepadHandle;
|
|
||||||
_buttonsUserMapping = new List<ButtonMappingEntry>(10);
|
|
||||||
|
|
||||||
Name = SDL_GameControllerName(_gamepadHandle);
|
|
||||||
Id = driverId;
|
|
||||||
Features = GetFeaturesFlag();
|
|
||||||
|
|
||||||
// Enable motion tracking
|
|
||||||
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
|
|
||||||
{
|
|
||||||
if (SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL,
|
|
||||||
SDL_bool.SDL_TRUE) != 0)
|
|
||||||
{
|
|
||||||
Logger.Error?.Print(LogClass.Hid,
|
|
||||||
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_ACCEL}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO,
|
|
||||||
SDL_bool.SDL_TRUE) != 0)
|
|
||||||
{
|
|
||||||
Logger.Error?.Print(LogClass.Hid,
|
|
||||||
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_GYRO}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (Name)
|
|
||||||
{
|
|
||||||
case LeftName:
|
|
||||||
{
|
|
||||||
_buttonsDriverMapping = _leftButtonsDriverMapping;
|
|
||||||
_joyConType = JoyConType.Left;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RightName:
|
|
||||||
{
|
|
||||||
_buttonsDriverMapping = _rightButtonsDriverMapping;
|
|
||||||
_joyConType = JoyConType.Right;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private GamepadFeaturesFlag GetFeaturesFlag()
|
|
||||||
{
|
|
||||||
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
|
|
||||||
|
|
||||||
if (SDL_GameControllerHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL) == SDL_bool.SDL_TRUE &&
|
|
||||||
SDL_GameControllerHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO) == SDL_bool.SDL_TRUE)
|
|
||||||
{
|
|
||||||
result |= GamepadFeaturesFlag.Motion;
|
|
||||||
}
|
|
||||||
|
|
||||||
int error = SDL_GameControllerRumble(_gamepadHandle, 0, 0, 100);
|
|
||||||
|
|
||||||
if (error == 0)
|
|
||||||
{
|
|
||||||
result |= GamepadFeaturesFlag.Rumble;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Id { get; }
|
|
||||||
public string Name { get; }
|
|
||||||
public bool IsConnected => SDL_GameControllerGetAttached(_gamepadHandle) == SDL_bool.SDL_TRUE;
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing && _gamepadHandle != nint.Zero)
|
|
||||||
{
|
|
||||||
SDL_GameControllerClose(_gamepadHandle);
|
|
||||||
|
|
||||||
_gamepadHandle = nint.Zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void SetTriggerThreshold(float triggerThreshold)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
|
||||||
{
|
|
||||||
if (!Features.HasFlag(GamepadFeaturesFlag.Rumble))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
|
|
||||||
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
|
|
||||||
|
|
||||||
if (durationMs == uint.MaxValue)
|
|
||||||
{
|
|
||||||
if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, SDL_HAPTIC_INFINITY) !=
|
|
||||||
0)
|
|
||||||
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
|
|
||||||
}
|
|
||||||
else if (durationMs > SDL_HAPTIC_INFINITY)
|
|
||||||
{
|
|
||||||
Logger.Error?.Print(LogClass.Hid, $"Unsupported rumble duration {durationMs}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs) != 0)
|
|
||||||
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector3 GetMotionData(MotionInputId inputId)
|
|
||||||
{
|
|
||||||
SDL_SensorType sensorType = inputId switch
|
|
||||||
{
|
|
||||||
MotionInputId.Accelerometer => SDL_SensorType.SDL_SENSOR_ACCEL,
|
|
||||||
MotionInputId.Gyroscope => SDL_SensorType.SDL_SENSOR_GYRO,
|
|
||||||
_ => SDL_SensorType.SDL_SENSOR_INVALID
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!Features.HasFlag(GamepadFeaturesFlag.Motion) || sensorType is SDL_SensorType.SDL_SENSOR_INVALID)
|
|
||||||
return Vector3.Zero;
|
|
||||||
|
|
||||||
const int ElementCount = 3;
|
|
||||||
|
|
||||||
unsafe
|
|
||||||
{
|
|
||||||
float* values = stackalloc float[ElementCount];
|
|
||||||
|
|
||||||
int result = SDL_GameControllerGetSensorData(_gamepadHandle, sensorType, (nint)values, ElementCount);
|
|
||||||
|
|
||||||
if (result != 0)
|
|
||||||
return Vector3.Zero;
|
|
||||||
|
|
||||||
Vector3 value = _joyConType switch
|
|
||||||
{
|
|
||||||
JoyConType.Left => new Vector3(-values[2], values[1], values[0]),
|
|
||||||
JoyConType.Right => new Vector3(values[2], values[1], -values[0])
|
|
||||||
};
|
|
||||||
|
|
||||||
return inputId switch
|
|
||||||
{
|
|
||||||
MotionInputId.Gyroscope => RadToDegree(value),
|
|
||||||
MotionInputId.Accelerometer => GsToMs2(value),
|
|
||||||
_ => value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector3 RadToDegree(Vector3 rad) => rad * (180 / MathF.PI);
|
|
||||||
|
|
||||||
private static Vector3 GsToMs2(Vector3 gs) => gs / SDL_STANDARD_GRAVITY;
|
|
||||||
|
|
||||||
public void SetConfiguration(InputConfig configuration)
|
|
||||||
{
|
|
||||||
lock (_userMappingLock)
|
|
||||||
{
|
|
||||||
_configuration = (StandardControllerInputConfig)configuration;
|
|
||||||
|
|
||||||
_buttonsUserMapping.Clear();
|
|
||||||
|
|
||||||
// First update sticks
|
|
||||||
_stickUserMapping[(int)StickInputId.Left] = (StickInputId)_configuration.LeftJoyconStick.Joystick;
|
|
||||||
_stickUserMapping[(int)StickInputId.Right] = (StickInputId)_configuration.RightJoyconStick.Joystick;
|
|
||||||
|
|
||||||
|
|
||||||
switch (_joyConType)
|
|
||||||
{
|
|
||||||
case JoyConType.Left:
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (GamepadButtonInputId)_configuration.LeftJoyconStick.StickButton));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (GamepadButtonInputId)_configuration.LeftJoycon.DpadUp));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (GamepadButtonInputId)_configuration.LeftJoycon.DpadDown));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (GamepadButtonInputId)_configuration.LeftJoycon.DpadLeft));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (GamepadButtonInputId)_configuration.LeftJoycon.DpadRight));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonMinus));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonL));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonZl));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonSr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonSl));
|
|
||||||
break;
|
|
||||||
case JoyConType.Right:
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (GamepadButtonInputId)_configuration.RightJoyconStick.StickButton));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (GamepadButtonInputId)_configuration.RightJoycon.ButtonA));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (GamepadButtonInputId)_configuration.RightJoycon.ButtonB));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (GamepadButtonInputId)_configuration.RightJoycon.ButtonX));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (GamepadButtonInputId)_configuration.RightJoycon.ButtonY));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (GamepadButtonInputId)_configuration.RightJoycon.ButtonPlus));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (GamepadButtonInputId)_configuration.RightJoycon.ButtonR));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (GamepadButtonInputId)_configuration.RightJoycon.ButtonZr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (GamepadButtonInputId)_configuration.RightJoycon.ButtonSr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (GamepadButtonInputId)_configuration.RightJoycon.ButtonSl));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
SetTriggerThreshold(_configuration.TriggerThreshold);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public GamepadStateSnapshot GetStateSnapshot()
|
|
||||||
{
|
|
||||||
return IGamepad.GetStateSnapshot(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GamepadStateSnapshot GetMappedStateSnapshot()
|
|
||||||
{
|
|
||||||
GamepadStateSnapshot rawState = GetStateSnapshot();
|
|
||||||
GamepadStateSnapshot result = default;
|
|
||||||
|
|
||||||
lock (_userMappingLock)
|
|
||||||
{
|
|
||||||
if (_buttonsUserMapping.Count == 0)
|
|
||||||
return rawState;
|
|
||||||
|
|
||||||
|
|
||||||
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
|
|
||||||
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
|
|
||||||
{
|
|
||||||
if (!entry.IsValid)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Do not touch state of button already pressed
|
|
||||||
if (!result.IsPressed(entry.To))
|
|
||||||
{
|
|
||||||
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(float leftStickX, float leftStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Left]);
|
|
||||||
(float rightStickX, float rightStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Right]);
|
|
||||||
|
|
||||||
result.SetStick(StickInputId.Left, leftStickX, leftStickY);
|
|
||||||
result.SetStick(StickInputId.Right, rightStickX, rightStickY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static float ConvertRawStickValue(short value)
|
|
||||||
{
|
|
||||||
const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
|
|
||||||
|
|
||||||
return value * ConvertRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JoyconConfigControllerStick<GamepadInputId, Common.Configuration.Hid.Controller.StickInputId>
|
|
||||||
GetLogicalJoyStickConfig(StickInputId inputId)
|
|
||||||
{
|
|
||||||
switch (inputId)
|
|
||||||
{
|
|
||||||
case StickInputId.Left:
|
|
||||||
if (_configuration.RightJoyconStick.Joystick ==
|
|
||||||
Common.Configuration.Hid.Controller.StickInputId.Left)
|
|
||||||
return _configuration.RightJoyconStick;
|
|
||||||
else
|
|
||||||
return _configuration.LeftJoyconStick;
|
|
||||||
case StickInputId.Right:
|
|
||||||
if (_configuration.LeftJoyconStick.Joystick ==
|
|
||||||
Common.Configuration.Hid.Controller.StickInputId.Right)
|
|
||||||
return _configuration.LeftJoyconStick;
|
|
||||||
else
|
|
||||||
return _configuration.RightJoyconStick;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public (float, float) GetStick(StickInputId inputId)
|
|
||||||
{
|
|
||||||
if (inputId == StickInputId.Unbound)
|
|
||||||
return (0.0f, 0.0f);
|
|
||||||
|
|
||||||
if (inputId == StickInputId.Left && _joyConType == JoyConType.Right || inputId == StickInputId.Right && _joyConType == JoyConType.Left)
|
|
||||||
{
|
|
||||||
return (0.0f, 0.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
(short stickX, short stickY) = GetStickXY();
|
|
||||||
|
|
||||||
float resultX = ConvertRawStickValue(stickX);
|
|
||||||
float resultY = -ConvertRawStickValue(stickY);
|
|
||||||
|
|
||||||
if (HasConfiguration)
|
|
||||||
{
|
|
||||||
var joyconStickConfig = GetLogicalJoyStickConfig(inputId);
|
|
||||||
|
|
||||||
if (joyconStickConfig != null)
|
|
||||||
{
|
|
||||||
if (joyconStickConfig.InvertStickX)
|
|
||||||
resultX = -resultX;
|
|
||||||
|
|
||||||
if (joyconStickConfig.InvertStickY)
|
|
||||||
resultY = -resultY;
|
|
||||||
|
|
||||||
if (joyconStickConfig.Rotate90CW)
|
|
||||||
{
|
|
||||||
float temp = resultX;
|
|
||||||
resultX = resultY;
|
|
||||||
resultY = -temp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return inputId switch
|
|
||||||
{
|
|
||||||
StickInputId.Left when _joyConType == JoyConType.Left => (resultY, -resultX),
|
|
||||||
StickInputId.Right when _joyConType == JoyConType.Right => (-resultY, resultX),
|
|
||||||
_ => (0.0f, 0.0f)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private (short, short) GetStickXY()
|
|
||||||
{
|
|
||||||
return (
|
|
||||||
SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTX),
|
|
||||||
SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTY));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsPressed(GamepadButtonInputId inputId)
|
|
||||||
{
|
|
||||||
if (!_buttonsDriverMapping.TryGetValue(inputId, out var button))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SDL_GameControllerGetButton(_gamepadHandle, button) == 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
using Ryujinx.Common.Configuration.Hid;
|
|
||||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Numerics;
|
|
||||||
using static SDL2.SDL;
|
|
||||||
|
|
||||||
namespace Ryujinx.Input.SDL2
|
|
||||||
{
|
|
||||||
internal class SDL2JoyConPair(IGamepad left, IGamepad right) : IGamepad
|
|
||||||
{
|
|
||||||
private StandardControllerInputConfig _configuration;
|
|
||||||
|
|
||||||
private readonly StickInputId[] _stickUserMapping =
|
|
||||||
[
|
|
||||||
StickInputId.Unbound,
|
|
||||||
StickInputId.Left,
|
|
||||||
StickInputId.Right
|
|
||||||
];
|
|
||||||
|
|
||||||
public GamepadFeaturesFlag Features => (left?.Features ?? GamepadFeaturesFlag.None) |
|
|
||||||
(right?.Features ?? GamepadFeaturesFlag.None);
|
|
||||||
|
|
||||||
public const string Id = "JoyConPair";
|
|
||||||
string IGamepad.Id => Id;
|
|
||||||
|
|
||||||
public string Name => "* Nintendo Switch Joy-Con (L/R)";
|
|
||||||
public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true };
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
left?.Dispose();
|
|
||||||
right?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public GamepadStateSnapshot GetMappedStateSnapshot()
|
|
||||||
{
|
|
||||||
return GetStateSnapshot();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector3 GetMotionData(MotionInputId inputId)
|
|
||||||
{
|
|
||||||
return inputId switch
|
|
||||||
{
|
|
||||||
MotionInputId.Accelerometer or
|
|
||||||
MotionInputId.Gyroscope => left.GetMotionData(inputId),
|
|
||||||
MotionInputId.SecondAccelerometer => right.GetMotionData(MotionInputId.Accelerometer),
|
|
||||||
MotionInputId.SecondGyroscope => right.GetMotionData(MotionInputId.Gyroscope),
|
|
||||||
_ => Vector3.Zero
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public GamepadStateSnapshot GetStateSnapshot()
|
|
||||||
{
|
|
||||||
return IGamepad.GetStateSnapshot(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public (float, float) GetStick(StickInputId inputId)
|
|
||||||
{
|
|
||||||
return inputId switch
|
|
||||||
{
|
|
||||||
StickInputId.Left => left.GetStick(StickInputId.Left),
|
|
||||||
StickInputId.Right => right.GetStick(StickInputId.Right),
|
|
||||||
_ => (0, 0)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsPressed(GamepadButtonInputId inputId)
|
|
||||||
{
|
|
||||||
return left.IsPressed(inputId) || right.IsPressed(inputId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
|
||||||
{
|
|
||||||
if (lowFrequency != 0)
|
|
||||||
{
|
|
||||||
right.Rumble(lowFrequency, lowFrequency, durationMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (highFrequency != 0)
|
|
||||||
{
|
|
||||||
left.Rumble(highFrequency, highFrequency, durationMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lowFrequency == 0 && highFrequency == 0)
|
|
||||||
{
|
|
||||||
left.Rumble(0, 0, durationMs);
|
|
||||||
right.Rumble(0, 0, durationMs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetConfiguration(InputConfig configuration)
|
|
||||||
{
|
|
||||||
left.SetConfiguration(configuration);
|
|
||||||
right.SetConfiguration(configuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetTriggerThreshold(float triggerThreshold)
|
|
||||||
{
|
|
||||||
left.SetTriggerThreshold(triggerThreshold);
|
|
||||||
right.SetTriggerThreshold(triggerThreshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsCombinable(List<string> gamepadsIds)
|
|
||||||
{
|
|
||||||
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
|
|
||||||
return leftIndex >= 0 && rightIndex >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (int leftIndex, int rightIndex) DetectJoyConPair(List<string> gamepadsIds)
|
|
||||||
{
|
|
||||||
var gamepadNames = gamepadsIds.Where(gamepadId => gamepadId != Id)
|
|
||||||
.Select((_, index) => SDL_GameControllerNameForIndex(index)).ToList();
|
|
||||||
int leftIndex = gamepadNames.IndexOf(SDL2JoyCon.LeftName);
|
|
||||||
int rightIndex = gamepadNames.IndexOf(SDL2JoyCon.RightName);
|
|
||||||
|
|
||||||
return (leftIndex, rightIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IGamepad GetGamepad(List<string> gamepadsIds)
|
|
||||||
{
|
|
||||||
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
|
|
||||||
if (leftIndex == -1 || rightIndex == -1)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
nint leftGamepadHandle = SDL_GameControllerOpen(leftIndex);
|
|
||||||
nint rightGamepadHandle = SDL_GameControllerOpen(rightIndex);
|
|
||||||
|
|
||||||
if (leftGamepadHandle == nint.Zero || rightGamepadHandle == nint.Zero)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return new SDL2JoyConPair(new SDL2JoyCon(leftGamepadHandle, gamepadsIds[leftIndex]),
|
|
||||||
new SDL2JoyCon(rightGamepadHandle, gamepadsIds[rightIndex]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -269,7 +269,6 @@ namespace Ryujinx.Input.HLE
|
|||||||
if (motionConfig.MotionBackend != MotionInputBackendType.CemuHook)
|
if (motionConfig.MotionBackend != MotionInputBackendType.CemuHook)
|
||||||
{
|
{
|
||||||
_leftMotionInput = new MotionInput();
|
_leftMotionInput = new MotionInput();
|
||||||
_rightMotionInput = new MotionInput();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -302,20 +301,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
|
|
||||||
if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair)
|
if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair)
|
||||||
{
|
{
|
||||||
if (gamepad.Id== "JoyConPair")
|
_rightMotionInput = _leftMotionInput;
|
||||||
{
|
|
||||||
Vector3 rightAccelerometer = gamepad.GetMotionData(MotionInputId.SecondAccelerometer);
|
|
||||||
Vector3 rightGyroscope = gamepad.GetMotionData(MotionInputId.SecondGyroscope);
|
|
||||||
|
|
||||||
rightAccelerometer = new Vector3(rightAccelerometer.X, -rightAccelerometer.Z, rightAccelerometer.Y);
|
|
||||||
rightGyroscope = new Vector3(rightGyroscope.X, -rightGyroscope.Z, rightGyroscope.Y);
|
|
||||||
|
|
||||||
_rightMotionInput.Update(rightAccelerometer, rightGyroscope, (ulong)PerformanceCounter.ElapsedNanoseconds / 1000, controllerConfig.Motion.Sensitivity, (float)controllerConfig.Motion.GyroDeadzone);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_rightMotionInput = _leftMotionInput;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -350,7 +336,6 @@ namespace Ryujinx.Input.HLE
|
|||||||
// Reset states
|
// Reset states
|
||||||
State = default;
|
State = default;
|
||||||
_leftMotionInput = null;
|
_leftMotionInput = null;
|
||||||
_rightMotionInput = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,17 +21,5 @@ namespace Ryujinx.Input
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Values are in degrees</remarks>
|
/// <remarks>Values are in degrees</remarks>
|
||||||
Gyroscope,
|
Gyroscope,
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Second accelerometer.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>Values are in m/s^2</remarks>
|
|
||||||
SecondAccelerometer,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Second gyroscope.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>Values are in degrees</remarks>
|
|
||||||
SecondGyroscope
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,17 +25,14 @@ namespace Ryujinx.SDL2.Common
|
|||||||
|
|
||||||
public static Action<Action> MainThreadDispatcher { get; set; }
|
public static Action<Action> MainThreadDispatcher { get; set; }
|
||||||
|
|
||||||
private const uint SdlInitFlags = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK |
|
private const uint SdlInitFlags = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO;
|
||||||
SDL_INIT_AUDIO | SDL_INIT_VIDEO;
|
|
||||||
|
|
||||||
private bool _isRunning;
|
private bool _isRunning;
|
||||||
private uint _refereceCount;
|
private uint _refereceCount;
|
||||||
private Thread _worker;
|
private Thread _worker;
|
||||||
|
|
||||||
private const uint SDL_JOYBATTERYUPDATED = 1543;
|
|
||||||
public event Action<int, int> OnJoyStickConnected;
|
public event Action<int, int> OnJoyStickConnected;
|
||||||
public event Action<int> OnJoystickDisconnected;
|
public event Action<int> OnJoystickDisconnected;
|
||||||
public event Action<int, SDL_JoystickPowerLevel> OnJoyBatteryUpdated;
|
|
||||||
|
|
||||||
private ConcurrentDictionary<uint, Action<SDL_Event>> _registeredWindowHandlers;
|
private ConcurrentDictionary<uint, Action<SDL_Event>> _registeredWindowHandlers;
|
||||||
|
|
||||||
@@ -81,14 +78,12 @@ namespace Ryujinx.SDL2.Common
|
|||||||
// First ensure that we only enable joystick events (for connected/disconnected).
|
// First ensure that we only enable joystick events (for connected/disconnected).
|
||||||
if (SDL_GameControllerEventState(SDL_IGNORE) != SDL_IGNORE)
|
if (SDL_GameControllerEventState(SDL_IGNORE) != SDL_IGNORE)
|
||||||
{
|
{
|
||||||
Logger.Error?.PrintMsg(LogClass.Application,
|
Logger.Error?.PrintMsg(LogClass.Application, "Couldn't change the state of game controller events.");
|
||||||
"Couldn't change the state of game controller events.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SDL_JoystickEventState(SDL_ENABLE) < 0)
|
if (SDL_JoystickEventState(SDL_ENABLE) < 0)
|
||||||
{
|
{
|
||||||
Logger.Error?.PrintMsg(LogClass.Application,
|
Logger.Error?.PrintMsg(LogClass.Application, $"Failed to enable joystick event polling: {SDL_GetError()}");
|
||||||
$"Failed to enable joystick event polling: {SDL_GetError()}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable all joysticks information, we don't need them no need to flood the event queue for that.
|
// Disable all joysticks information, we don't need them no need to flood the event queue for that.
|
||||||
@@ -148,12 +143,7 @@ namespace Ryujinx.SDL2.Common
|
|||||||
|
|
||||||
OnJoystickDisconnected?.Invoke(evnt.cbutton.which);
|
OnJoystickDisconnected?.Invoke(evnt.cbutton.which);
|
||||||
}
|
}
|
||||||
else if ((uint)evnt.type == SDL_JOYBATTERYUPDATED)
|
else if (evnt.type is SDL_EventType.SDL_WINDOWEVENT or SDL_EventType.SDL_MOUSEBUTTONDOWN or SDL_EventType.SDL_MOUSEBUTTONUP)
|
||||||
{
|
|
||||||
OnJoyBatteryUpdated?.Invoke(evnt.cbutton.which, (SDL_JoystickPowerLevel)evnt.user.code);
|
|
||||||
}
|
|
||||||
else if (evnt.type is SDL_EventType.SDL_WINDOWEVENT or SDL_EventType.SDL_MOUSEBUTTONDOWN
|
|
||||||
or SDL_EventType.SDL_MOUSEBUTTONUP)
|
|
||||||
{
|
{
|
||||||
if (_registeredWindowHandlers.TryGetValue(evnt.window.windowID, out Action<SDL_Event> handler))
|
if (_registeredWindowHandlers.TryGetValue(evnt.window.windowID, out Action<SDL_Event> handler))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1576,50 +1576,50 @@
|
|||||||
"ID": "GameListHeaderTimePlayed",
|
"ID": "GameListHeaderTimePlayed",
|
||||||
"Translations": {
|
"Translations": {
|
||||||
"ar_SA": "",
|
"ar_SA": "",
|
||||||
"de_DE": "Spielzeit: {0}",
|
"de_DE": "Spielzeit:",
|
||||||
"el_GR": "Χρόνος: {0}",
|
"el_GR": "Χρόνος:",
|
||||||
"en_US": "Play Time: {0}",
|
"en_US": "Play Time:",
|
||||||
"es_ES": "Tiempo jugado: {0}",
|
"es_ES": "Tiempo jugado:",
|
||||||
"fr_FR": "Temps de jeu: {0}",
|
"fr_FR": "Temps de jeu:",
|
||||||
"he_IL": "",
|
"he_IL": "",
|
||||||
"it_IT": "Tempo di gioco: {0}",
|
"it_IT": "Tempo di gioco:",
|
||||||
"ja_JP": "プレイ時間: {0}",
|
"ja_JP": "プレイ時間:",
|
||||||
"ko_KR": "플레이 타임: {0}",
|
"ko_KR": "플레이 타임:",
|
||||||
"no_NO": "Spilletid: {0}",
|
"no_NO": "Spilletid:",
|
||||||
"pl_PL": "Czas w grze: {0}",
|
"pl_PL": "Czas w grze:",
|
||||||
"pt_BR": "Tempo de jogo: {0}",
|
"pt_BR": "Tempo de jogo:",
|
||||||
"ru_RU": "Время в игре: {0}",
|
"ru_RU": "Время в игре:",
|
||||||
"sv_SE": "Speltid: {0}",
|
"sv_SE": "Speltid:",
|
||||||
"th_TH": "เล่นไปแล้ว: {0}",
|
"th_TH": "เล่นไปแล้ว:",
|
||||||
"tr_TR": "Oynama Süresi: {0}",
|
"tr_TR": "Oynama Süresi:",
|
||||||
"uk_UA": "Зіграно часу: {0}",
|
"uk_UA": "Зіграно часу:",
|
||||||
"zh_CN": "游玩时长: {0}",
|
"zh_CN": "游玩时长:",
|
||||||
"zh_TW": "遊玩時數: {0}"
|
"zh_TW": "遊玩時數:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ID": "GameListHeaderLastPlayed",
|
"ID": "GameListHeaderLastPlayed",
|
||||||
"Translations": {
|
"Translations": {
|
||||||
"ar_SA": "",
|
"ar_SA": "",
|
||||||
"de_DE": "Zuletzt gespielt: {0}",
|
"de_DE": "Zuletzt gespielt: ",
|
||||||
"el_GR": "Παίχτηκε: {0}",
|
"el_GR": "Παίχτηκε: ",
|
||||||
"en_US": "Last Played: {0}",
|
"en_US": "Last Played:",
|
||||||
"es_ES": "Jugado por última vez: {0}",
|
"es_ES": "Jugado por última vez:",
|
||||||
"fr_FR": "Dernière partie jouée: {0}",
|
"fr_FR": "Dernière partie jouée:",
|
||||||
"he_IL": "",
|
"he_IL": "",
|
||||||
"it_IT": "Ultima partita: {0}",
|
"it_IT": "Ultima partita:",
|
||||||
"ja_JP": "最終プレイ日時: {0}",
|
"ja_JP": "最終プレイ日時:",
|
||||||
"ko_KR": "마지막 플레이: {0}",
|
"ko_KR": "마지막 플레이:",
|
||||||
"no_NO": "Sist Spilt: {0}",
|
"no_NO": "Sist Spilt:",
|
||||||
"pl_PL": "Ostatnio grane: {0}",
|
"pl_PL": "Ostatnio grane:",
|
||||||
"pt_BR": "Último jogo: {0}",
|
"pt_BR": "Último jogo:",
|
||||||
"ru_RU": "Последний запуск: {0}",
|
"ru_RU": "Последний запуск:",
|
||||||
"sv_SE": "Senast spelad: {0}",
|
"sv_SE": "Senast spelad:",
|
||||||
"th_TH": "เล่นล่าสุด: {0}",
|
"th_TH": "เล่นล่าสุด:",
|
||||||
"tr_TR": "Son Oynama Tarihi: {0}",
|
"tr_TR": "Son Oynama Tarihi:",
|
||||||
"uk_UA": "Востаннє зіграно: {0}",
|
"uk_UA": "Востаннє зіграно:",
|
||||||
"zh_CN": "最近游玩: {0}",
|
"zh_CN": "最近游玩:",
|
||||||
"zh_TW": "最近遊玩: {0}"
|
"zh_TW": "最近遊玩:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23696,6 +23696,56 @@
|
|||||||
"zh_CN": "选择一个要解压的 DLC",
|
"zh_CN": "选择一个要解压的 DLC",
|
||||||
"zh_TW": ""
|
"zh_TW": ""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ID": "GameInfoRpcImage",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Rich Presence Image",
|
||||||
|
"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": "GameInfoRpcDynamic",
|
||||||
|
"Translations": {
|
||||||
|
"ar_SA": "",
|
||||||
|
"de_DE": "",
|
||||||
|
"el_GR": "",
|
||||||
|
"en_US": "Dynamic Rich Presence",
|
||||||
|
"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": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ using Ryujinx.Common.Logging;
|
|||||||
using Ryujinx.HLE;
|
using Ryujinx.HLE;
|
||||||
using Ryujinx.HLE.Loaders.Processes;
|
using Ryujinx.HLE.Loaders.Processes;
|
||||||
using Ryujinx.Horizon;
|
using Ryujinx.Horizon;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Ryujinx.Ava
|
namespace Ryujinx.Ava
|
||||||
@@ -37,6 +38,9 @@ namespace Ryujinx.Ava
|
|||||||
private static RichPresence _discordPresencePlaying;
|
private static RichPresence _discordPresencePlaying;
|
||||||
private static ApplicationMetadata _currentApp;
|
private static ApplicationMetadata _currentApp;
|
||||||
|
|
||||||
|
public static bool HasAssetImage(string titleId) => TitleIDs.DiscordGameAssetKeys.ContainsIgnoreCase(titleId);
|
||||||
|
public static bool HasAnalyzer(string titleId) => PlayReports.Analyzer.TitleIds.ContainsIgnoreCase(titleId);
|
||||||
|
|
||||||
public static void Initialize()
|
public static void Initialize()
|
||||||
{
|
{
|
||||||
_discordPresenceMain = new RichPresence
|
_discordPresenceMain = new RichPresence
|
||||||
@@ -126,14 +130,16 @@ namespace Ryujinx.Ava
|
|||||||
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
|
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
|
||||||
if (_discordPresencePlaying is null) return;
|
if (_discordPresencePlaying is null) return;
|
||||||
|
|
||||||
Analyzer.FormattedValue formattedValue =
|
FormattedValue formattedValue =
|
||||||
PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
|
PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
|
||||||
|
|
||||||
if (!formattedValue.Handled) return;
|
if (!formattedValue.Handled) return;
|
||||||
|
|
||||||
_discordPresencePlaying.Details = formattedValue.Reset
|
_discordPresencePlaying.Details = TruncateToByteLength(
|
||||||
? $"Playing {_currentApp.Title}"
|
formattedValue.Reset
|
||||||
: formattedValue.FormattedString;
|
? $"Playing {_currentApp.Title}"
|
||||||
|
: formattedValue.FormattedString
|
||||||
|
);
|
||||||
|
|
||||||
if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
|
if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
|
||||||
return; //don't trigger an update if the set presence Details are identical to current
|
return; //don't trigger an update if the set presence Details are identical to current
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||||
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
|
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
|
||||||
xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels"
|
xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels"
|
||||||
|
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView"
|
x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView"
|
||||||
@@ -85,6 +86,49 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Separator Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
<Separator Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="5">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
<ui:SymbolIcon Foreground="ForestGreen" Symbol="Checkmark" IsVisible="{Binding AppData.HasRichPresenceAsset}"/>
|
||||||
|
<TextBlock
|
||||||
|
Foreground="ForestGreen"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
IsVisible="{Binding AppData.HasRichPresenceAsset}"
|
||||||
|
Text="{ext:Locale GameInfoRpcImage}"
|
||||||
|
TextAlignment="Start"
|
||||||
|
TextWrapping="Wrap" >
|
||||||
|
</TextBlock>
|
||||||
|
<ui:SymbolIcon Foreground="Red" Symbol="Cancel" IsVisible="{Binding !AppData.HasRichPresenceAsset}"/>
|
||||||
|
<TextBlock
|
||||||
|
Foreground="Red"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
IsVisible="{Binding !AppData.HasRichPresenceAsset}"
|
||||||
|
Text="{ext:Locale GameInfoRpcImage}"
|
||||||
|
TextAlignment="Start"
|
||||||
|
TextWrapping="Wrap" >
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
<ui:SymbolIcon Foreground="ForestGreen" Symbol="Checkmark" IsVisible="{Binding AppData.HasDynamicRichPresenceSupport}"/>
|
||||||
|
<TextBlock
|
||||||
|
Foreground="ForestGreen"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
IsVisible="{Binding AppData.HasDynamicRichPresenceSupport}"
|
||||||
|
Text="{ext:Locale GameInfoRpcDynamic}"
|
||||||
|
TextAlignment="Start"
|
||||||
|
TextWrapping="Wrap" >
|
||||||
|
</TextBlock>
|
||||||
|
<ui:SymbolIcon Foreground="Red" Symbol="Cancel" IsVisible="{Binding !AppData.HasDynamicRichPresenceSupport}"/>
|
||||||
|
<TextBlock
|
||||||
|
Foreground="Red"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
IsVisible="{Binding !AppData.HasDynamicRichPresenceSupport}"
|
||||||
|
Text="{ext:Locale GameInfoRpcDynamic}"
|
||||||
|
TextAlignment="Start"
|
||||||
|
TextWrapping="Wrap" >
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<Separator Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
IsVisible="{Binding AppData.HasLdnGames}"
|
IsVisible="{Binding AppData.HasLdnGames}"
|
||||||
@@ -92,22 +136,35 @@
|
|||||||
TextAlignment="Start"
|
TextAlignment="Start"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
<Separator IsVisible="{Binding AppData.HasLdnGames}" Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
<Separator IsVisible="{Binding AppData.HasLdnGames}" Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
|
||||||
<StackPanel
|
<StackPanel Orientation="Vertical" Spacing="5">
|
||||||
HorizontalAlignment="Left"
|
<Grid
|
||||||
VerticalAlignment="Top"
|
ColumnDefinitions="Auto,*,Auto">
|
||||||
Orientation="Vertical"
|
<TextBlock
|
||||||
Spacing="5">
|
Grid.Column="0"
|
||||||
<TextBlock
|
Text="{ext:Locale GameListHeaderLastPlayed}"
|
||||||
HorizontalAlignment="Stretch"
|
VerticalAlignment="Top"
|
||||||
Text="{Binding FormattedLastPlayed}"
|
TextAlignment="Start"
|
||||||
TextAlignment="Start"
|
TextWrapping="NoWrap" />
|
||||||
TextWrapping="Wrap" />
|
<TextBlock
|
||||||
<TextBlock
|
Grid.Column="2"
|
||||||
HorizontalAlignment="Stretch"
|
Text="{Binding AppData.LastPlayedString}"
|
||||||
Text="{Binding FormattedPlayTime}"
|
TextAlignment="End"
|
||||||
IsVisible="{Binding AppData.HasPlayedPreviously}"
|
TextWrapping="Wrap" />
|
||||||
TextAlignment="Start"
|
</Grid>
|
||||||
TextWrapping="Wrap" />
|
<Grid
|
||||||
|
ColumnDefinitions="Auto,*,Auto"
|
||||||
|
IsVisible="{Binding AppData.HasPlayedPreviously}">
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="0"
|
||||||
|
Text="{ext:Locale GameListHeaderTimePlayed}"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
TextAlignment="Start"
|
||||||
|
TextWrapping="NoWrap" />
|
||||||
|
<TextBlock Grid.Column="2"
|
||||||
|
Text="{Binding AppData.TimePlayedString}"
|
||||||
|
TextAlignment="End"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
LocaleKeys.CompatibilityListNothing or
|
LocaleKeys.CompatibilityListNothing or
|
||||||
LocaleKeys.CompatibilityListBoots or
|
LocaleKeys.CompatibilityListBoots or
|
||||||
LocaleKeys.CompatibilityListMenus => Brushes.Red,
|
LocaleKeys.CompatibilityListMenus => Brushes.Red,
|
||||||
LocaleKeys.CompatibilityListIngame => Brushes.Yellow,
|
LocaleKeys.CompatibilityListIngame => Brushes.DarkOrange,
|
||||||
_ => Brushes.ForestGreen
|
_ => Brushes.ForestGreen
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version);
|
public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version);
|
||||||
public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer);
|
public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer);
|
||||||
|
|
||||||
public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension);
|
public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension);
|
||||||
public string FormattedLastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed].Format(AppData.LastPlayedString);
|
|
||||||
public string FormattedPlayTime => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed].Format(AppData.TimePlayedString);
|
|
||||||
public string FormattedFileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize].Format(AppData.FileSizeString);
|
public string FormattedFileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize].Format(AppData.FileSizeString);
|
||||||
|
|
||||||
public string FormattedLdnInfo =>
|
public string FormattedLdnInfo =>
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
|||||||
public int GameCount { get; set; }
|
public int GameCount { get; set; }
|
||||||
|
|
||||||
public bool HasLdnGames => PlayerCount != 0 && GameCount != 0;
|
public bool HasLdnGames => PlayerCount != 0 && GameCount != 0;
|
||||||
|
|
||||||
|
public bool HasRichPresenceAsset => DiscordIntegrationModule.HasAssetImage(IdString);
|
||||||
|
public bool HasDynamicRichPresenceSupport => DiscordIntegrationModule.HasAnalyzer(IdString);
|
||||||
|
|
||||||
public TimeSpan TimePlayed { get; set; }
|
public TimeSpan TimePlayed { get; set; }
|
||||||
public DateTime? LastPlayed { get; set; }
|
public DateTime? LastPlayed { get; set; }
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using MsgPack;
|
|||||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
@@ -15,6 +16,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
|||||||
{
|
{
|
||||||
private readonly List<GameSpec> _specs = [];
|
private readonly List<GameSpec> _specs = [];
|
||||||
|
|
||||||
|
public string[] TitleIds => Specs.SelectMany(x => x.TitleIds).ToArray();
|
||||||
|
|
||||||
|
public IReadOnlyList<GameSpec> Specs => new ReadOnlyCollection<GameSpec>(_specs);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
|
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -78,7 +83,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs the configured <see cref="GameSpec.FormatterSpec"/> for the specified game title ID.
|
/// Runs the configured <see cref="GameSpec.FormatterSpec"/> for the specified game title ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -98,261 +103,48 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
|||||||
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
|
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
|
||||||
return FormattedValue.Unhandled;
|
return FormattedValue.Unhandled;
|
||||||
|
|
||||||
foreach (GameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
|
foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
|
||||||
{
|
{
|
||||||
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
|
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
return formatSpec.ValueFormatter(new Value
|
return formatSpec.Formatter(new Value { Application = appMeta, PackedValue = valuePackObject });
|
||||||
{
|
|
||||||
Application = appMeta, PackedValue = valuePackObject
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (GameSpec.MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority))
|
foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority))
|
||||||
{
|
{
|
||||||
List<MessagePackObject> packedObjects = [];
|
List<MessagePackObject> packedObjects = [];
|
||||||
foreach (var reportKey in formatSpec.ReportKeys)
|
foreach (var reportKey in formatSpec.ReportKeys)
|
||||||
{
|
{
|
||||||
if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
|
if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
packedObjects.Add(valuePackObject);
|
packedObjects.Add(valuePackObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packedObjects.Count != formatSpec.ReportKeys.Length)
|
if (packedObjects.Count != formatSpec.ReportKeys.Length)
|
||||||
return FormattedValue.Unhandled;
|
return FormattedValue.Unhandled;
|
||||||
|
|
||||||
return formatSpec.ValueFormatter(packedObjects
|
return formatSpec.Formatter(packedObjects
|
||||||
.Select(packObject => new Value { Application = appMeta, PackedValue = packObject })
|
.Select(packObject => new Value { Application = appMeta, PackedValue = packObject })
|
||||||
.ToArray());
|
.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority))
|
||||||
|
{
|
||||||
|
Dictionary<string, Value> packedObjects = [];
|
||||||
|
foreach (var reportKey in formatSpec.ReportKeys)
|
||||||
|
{
|
||||||
|
if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
packedObjects.Add(reportKey, new Value { Application = appMeta, PackedValue = valuePackObject });
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatSpec.Formatter(packedObjects);
|
||||||
|
}
|
||||||
|
|
||||||
return FormattedValue.Unhandled;
|
return FormattedValue.Unhandled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A potential formatted value returned by a <see cref="ValueFormatter"/>.
|
|
||||||
/// </summary>
|
|
||||||
public readonly struct FormattedValue
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Was any handler able to match anything in the Play Report?
|
|
||||||
/// </summary>
|
|
||||||
public bool Handled { get; private init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Did the handler request the caller of the <see cref="Analyzer"/> to reset the existing value?
|
|
||||||
/// </summary>
|
|
||||||
public bool Reset { get; private init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The formatted value, only present if <see cref="Handled"/> is true, and <see cref="Reset"/> is false.
|
|
||||||
/// </summary>
|
|
||||||
public string FormattedString { get; private init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The intended path of execution for having a string to return: simply return the string.
|
|
||||||
/// This implicit conversion will make the struct for you.<br/><br/>
|
|
||||||
///
|
|
||||||
/// If the input is null, <see cref="Unhandled"/> is returned.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="formattedValue">The formatted string value.</param>
|
|
||||||
/// <returns>The automatically constructed <see cref="FormattedValue"/> struct.</returns>
|
|
||||||
public static implicit operator FormattedValue(string formattedValue)
|
|
||||||
=> formattedValue is not null
|
|
||||||
? new FormattedValue { Handled = true, FormattedString = formattedValue }
|
|
||||||
: Unhandled;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return this to tell the caller there is no value to return.
|
|
||||||
/// </summary>
|
|
||||||
public static FormattedValue Unhandled => default;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return this to suggest the caller reset the value it's using the <see cref="Analyzer"/> for.
|
|
||||||
/// </summary>
|
|
||||||
public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="ValueFormatter"/>.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly ValueFormatter AlwaysResets = _ => ForceReset;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A delegate factory you can use to always return the specified
|
|
||||||
/// <paramref name="formattedValue"/> in a <see cref="ValueFormatter"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
|
|
||||||
public static ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A mapping of title IDs to value formatter specs.
|
|
||||||
///
|
|
||||||
/// <remarks>Generally speaking, use the <see cref="Analyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
|
|
||||||
/// </summary>
|
|
||||||
public class GameSpec
|
|
||||||
{
|
|
||||||
public required string[] TitleIds { get; init; }
|
|
||||||
public List<FormatterSpec> SimpleValueFormatters { get; } = [];
|
|
||||||
public List<MultiFormatterSpec> MultiValueFormatters { get; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a value formatter to the current <see cref="GameSpec"/>
|
|
||||||
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reportKey">The key name to match.</param>
|
|
||||||
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
|
|
||||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
|
||||||
public GameSpec AddValueFormatter(string reportKey, ValueFormatter valueFormatter)
|
|
||||||
{
|
|
||||||
SimpleValueFormatters.Add(new FormatterSpec
|
|
||||||
{
|
|
||||||
Priority = SimpleValueFormatters.Count, ReportKey = reportKey, ValueFormatter = valueFormatter
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a value formatter at a specific priority to the current <see cref="GameSpec"/>
|
|
||||||
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
|
||||||
/// <param name="reportKey">The key name to match.</param>
|
|
||||||
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
|
|
||||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
|
||||||
public GameSpec AddValueFormatter(int priority, string reportKey,
|
|
||||||
ValueFormatter valueFormatter)
|
|
||||||
{
|
|
||||||
SimpleValueFormatters.Add(new FormatterSpec
|
|
||||||
{
|
|
||||||
Priority = priority, ReportKey = reportKey, ValueFormatter = valueFormatter
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
|
|
||||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reportKeys">The key names to match.</param>
|
|
||||||
/// <param name="valueFormatter">The function which can format the values.</param>
|
|
||||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
|
||||||
public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter)
|
|
||||||
{
|
|
||||||
MultiValueFormatters.Add(new MultiFormatterSpec
|
|
||||||
{
|
|
||||||
Priority = SimpleValueFormatters.Count, ReportKeys = reportKeys, ValueFormatter = valueFormatter
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
|
|
||||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
|
||||||
/// <param name="reportKeys">The key names to match.</param>
|
|
||||||
/// <param name="valueFormatter">The function which can format the values.</param>
|
|
||||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
|
||||||
public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys,
|
|
||||||
MultiValueFormatter valueFormatter)
|
|
||||||
{
|
|
||||||
MultiValueFormatters.Add(new MultiFormatterSpec
|
|
||||||
{
|
|
||||||
Priority = priority, ReportKeys = reportKeys, ValueFormatter = valueFormatter
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
|
|
||||||
/// </summary>
|
|
||||||
public struct FormatterSpec
|
|
||||||
{
|
|
||||||
public required int Priority { get; init; }
|
|
||||||
public required string ReportKey { get; init; }
|
|
||||||
public ValueFormatter ValueFormatter { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values.
|
|
||||||
/// </summary>
|
|
||||||
public struct MultiFormatterSpec
|
|
||||||
{
|
|
||||||
public required int Priority { get; init; }
|
|
||||||
public required string[] ReportKeys { get; init; }
|
|
||||||
public MultiValueFormatter ValueFormatter { get; init; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The input data to a <see cref="ValueFormatter"/>,
|
|
||||||
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
|
|
||||||
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
|
|
||||||
/// </summary>
|
|
||||||
public class Value
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The currently running application's <see cref="ApplicationMetadata"/>.
|
|
||||||
/// </summary>
|
|
||||||
public ApplicationMetadata Application { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The matched value from the Play Report.
|
|
||||||
/// </summary>
|
|
||||||
public MessagePackObject PackedValue { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Access the <see cref="PackedValue"/> as its underlying .NET type.<br/>
|
|
||||||
///
|
|
||||||
/// Does not seem to work well with comparing numeric types,
|
|
||||||
/// so use XValue properties for that.
|
|
||||||
/// </summary>
|
|
||||||
public object BoxedValue => PackedValue.ToObject();
|
|
||||||
|
|
||||||
#region AsX accessors
|
|
||||||
|
|
||||||
public bool BooleanValue => PackedValue.AsBoolean();
|
|
||||||
public byte ByteValye => PackedValue.AsByte();
|
|
||||||
public sbyte SByteValye => PackedValue.AsSByte();
|
|
||||||
public short ShortValye => PackedValue.AsInt16();
|
|
||||||
public ushort UShortValye => PackedValue.AsUInt16();
|
|
||||||
public int IntValye => PackedValue.AsInt32();
|
|
||||||
public uint UIntValye => PackedValue.AsUInt32();
|
|
||||||
public long LongValye => PackedValue.AsInt64();
|
|
||||||
public ulong ULongValye => PackedValue.AsUInt64();
|
|
||||||
public float FloatValue => PackedValue.AsSingle();
|
|
||||||
public double DoubleValue => PackedValue.AsDouble();
|
|
||||||
public string StringValue => PackedValue.AsString();
|
|
||||||
public Span<byte> BinaryValue => PackedValue.AsBinary();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The delegate type that powers single value formatters.<br/>
|
|
||||||
/// Takes in the result value from the Play Report, and outputs:
|
|
||||||
/// <br/>
|
|
||||||
/// a formatted string,
|
|
||||||
/// <br/>
|
|
||||||
/// a signal that nothing was available to handle it,
|
|
||||||
/// <br/>
|
|
||||||
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
|
||||||
/// </summary>
|
|
||||||
public delegate Analyzer.FormattedValue ValueFormatter(Value value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The delegate type that powers multiple value formatters.<br/>
|
|
||||||
/// Takes in the result value from the Play Report, and outputs:
|
|
||||||
/// <br/>
|
|
||||||
/// a formatted string,
|
|
||||||
/// <br/>
|
|
||||||
/// a signal that nothing was available to handle it,
|
|
||||||
/// <br/>
|
|
||||||
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
|
||||||
/// </summary>
|
|
||||||
public delegate Analyzer.FormattedValue MultiValueFormatter(Value[] value);
|
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/Ryujinx/Utilities/PlayReport/Delegates.cs
Normal file
42
src/Ryujinx/Utilities/PlayReport/Delegates.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Utilities.PlayReport
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate type that powers single value formatters.<br/>
|
||||||
|
/// Takes in the result value from the Play Report, and outputs:
|
||||||
|
/// <br/>
|
||||||
|
/// a formatted string,
|
||||||
|
/// <br/>
|
||||||
|
/// a signal that nothing was available to handle it,
|
||||||
|
/// <br/>
|
||||||
|
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
||||||
|
/// </summary>
|
||||||
|
public delegate FormattedValue ValueFormatter(Value value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate type that powers multiple value formatters.<br/>
|
||||||
|
/// Takes in the result values from the Play Report, and outputs:
|
||||||
|
/// <br/>
|
||||||
|
/// a formatted string,
|
||||||
|
/// <br/>
|
||||||
|
/// a signal that nothing was available to handle it,
|
||||||
|
/// <br/>
|
||||||
|
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
||||||
|
/// </summary>
|
||||||
|
public delegate FormattedValue MultiValueFormatter(Value[] value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate type that powers multiple value formatters.
|
||||||
|
/// The dictionary passed to this delegate is sparsely populated;
|
||||||
|
/// that is, not every key specified in the Play Report needs to match for this to be used.<br/>
|
||||||
|
/// Takes in the result values from the Play Report, and outputs:
|
||||||
|
/// <br/>
|
||||||
|
/// a formatted string,
|
||||||
|
/// <br/>
|
||||||
|
/// a signal that nothing was available to handle it,
|
||||||
|
/// <br/>
|
||||||
|
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
||||||
|
/// </summary>
|
||||||
|
public delegate FormattedValue SparseMultiValueFormatter(Dictionary<string, Value> values);
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using static Ryujinx.Ava.Utilities.PlayReport.Analyzer;
|
namespace Ryujinx.Ava.Utilities.PlayReport
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Utilities.PlayReport
|
|
||||||
{
|
{
|
||||||
public static class PlayReports
|
public static class PlayReports
|
||||||
{
|
{
|
||||||
@@ -10,7 +8,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
|||||||
spec => spec
|
spec => spec
|
||||||
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
|
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
|
||||||
// reset to normal status when switching between normal & master mode in title screen
|
// reset to normal status when switching between normal & master mode in title screen
|
||||||
.AddValueFormatter("AoCVer", FormattedValue.AlwaysResets)
|
.AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets)
|
||||||
)
|
)
|
||||||
.AddSpec(
|
.AddSpec(
|
||||||
"0100f2c0115b6000",
|
"0100f2c0115b6000",
|
||||||
|
|||||||
140
src/Ryujinx/Utilities/PlayReport/Specs.cs
Normal file
140
src/Ryujinx/Utilities/PlayReport/Specs.cs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
using FluentAvalonia.Core;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Utilities.PlayReport
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A mapping of title IDs to value formatter specs.
|
||||||
|
///
|
||||||
|
/// <remarks>Generally speaking, use the <see cref="Analyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public class GameSpec
|
||||||
|
{
|
||||||
|
public required string[] TitleIds { get; init; }
|
||||||
|
public List<FormatterSpec> SimpleValueFormatters { get; } = [];
|
||||||
|
public List<MultiFormatterSpec> MultiValueFormatters { get; } = [];
|
||||||
|
public List<SparseMultiFormatterSpec> SparseMultiValueFormatters { get; } = [];
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a value formatter to the current <see cref="GameSpec"/>
|
||||||
|
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reportKey">The key name to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
|
||||||
|
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public GameSpec AddValueFormatter(string reportKey, ValueFormatter valueFormatter)
|
||||||
|
=> AddValueFormatter(SimpleValueFormatters.Count, reportKey, valueFormatter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||||
|
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||||
|
/// <param name="reportKey">The key name to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
|
||||||
|
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public GameSpec AddValueFormatter(int priority, string reportKey,
|
||||||
|
ValueFormatter valueFormatter)
|
||||||
|
{
|
||||||
|
SimpleValueFormatters.Add(new FormatterSpec
|
||||||
|
{
|
||||||
|
Priority = priority, ReportKey = reportKey, Formatter = valueFormatter
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
|
||||||
|
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reportKeys">The key names to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||||
|
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter)
|
||||||
|
=> AddMultiValueFormatter(MultiValueFormatters.Count, reportKeys, valueFormatter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||||
|
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||||
|
/// <param name="reportKeys">The key names to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||||
|
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys,
|
||||||
|
MultiValueFormatter valueFormatter)
|
||||||
|
{
|
||||||
|
MultiValueFormatters.Add(new MultiFormatterSpec
|
||||||
|
{
|
||||||
|
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
|
||||||
|
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// <br/><br/>
|
||||||
|
/// The 'Sparse' multi-value formatters do not require every key to be present.
|
||||||
|
/// If you need this requirement, use <see cref="AddMultiValueFormatter(string[], Ryujinx.Ava.Utilities.PlayReport.MultiValueFormatter)"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reportKeys">The key names to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||||
|
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public GameSpec AddSparseMultiValueFormatter(string[] reportKeys, SparseMultiValueFormatter valueFormatter)
|
||||||
|
=> AddSparseMultiValueFormatter(SparseMultiValueFormatters.Count, reportKeys, valueFormatter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||||
|
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||||
|
/// <br/><br/>
|
||||||
|
/// The 'Sparse' multi-value formatters do not require every key to be present.
|
||||||
|
/// If you need this requirement, use <see cref="AddMultiValueFormatter(int, string[], Ryujinx.Ava.Utilities.PlayReport.MultiValueFormatter)"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||||
|
/// <param name="reportKeys">The key names to match.</param>
|
||||||
|
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||||
|
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public GameSpec AddSparseMultiValueFormatter(int priority, string[] reportKeys,
|
||||||
|
SparseMultiValueFormatter valueFormatter)
|
||||||
|
{
|
||||||
|
SparseMultiValueFormatters.Add(new SparseMultiFormatterSpec
|
||||||
|
{
|
||||||
|
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
|
||||||
|
/// </summary>
|
||||||
|
public struct FormatterSpec
|
||||||
|
{
|
||||||
|
public required int Priority { get; init; }
|
||||||
|
public required string ReportKey { get; init; }
|
||||||
|
public ValueFormatter Formatter { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values.
|
||||||
|
/// </summary>
|
||||||
|
public struct MultiFormatterSpec
|
||||||
|
{
|
||||||
|
public required int Priority { get; init; }
|
||||||
|
public required string[] ReportKeys { get; init; }
|
||||||
|
public MultiValueFormatter Formatter { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their sparsely populated potential values.
|
||||||
|
/// </summary>
|
||||||
|
public struct SparseMultiFormatterSpec
|
||||||
|
{
|
||||||
|
public required int Priority { get; init; }
|
||||||
|
public required string[] ReportKeys { get; init; }
|
||||||
|
public SparseMultiValueFormatter Formatter { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
130
src/Ryujinx/Utilities/PlayReport/Value.cs
Normal file
130
src/Ryujinx/Utilities/PlayReport/Value.cs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
using MsgPack;
|
||||||
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Utilities.PlayReport
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The input data to a <see cref="ValueFormatter"/>,
|
||||||
|
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
|
||||||
|
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
|
||||||
|
/// </summary>
|
||||||
|
public class Value
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The currently running application's <see cref="ApplicationMetadata"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ApplicationMetadata Application { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The matched value from the Play Report.
|
||||||
|
/// </summary>
|
||||||
|
public MessagePackObject PackedValue { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access the <see cref="PackedValue"/> as its underlying .NET type.<br/>
|
||||||
|
///
|
||||||
|
/// Does not seem to work well with comparing numeric types,
|
||||||
|
/// so use XValue properties for that.
|
||||||
|
/// </summary>
|
||||||
|
public object BoxedValue => PackedValue.ToObject();
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
object boxed = BoxedValue;
|
||||||
|
return boxed == null
|
||||||
|
? "null"
|
||||||
|
: boxed.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region AsX accessors
|
||||||
|
|
||||||
|
public bool BooleanValue => PackedValue.AsBoolean();
|
||||||
|
public byte ByteValue => PackedValue.AsByte();
|
||||||
|
public sbyte SByteValue => PackedValue.AsSByte();
|
||||||
|
public short ShortValue => PackedValue.AsInt16();
|
||||||
|
public ushort UShortValue => PackedValue.AsUInt16();
|
||||||
|
public int IntValue => PackedValue.AsInt32();
|
||||||
|
public uint UIntValue => PackedValue.AsUInt32();
|
||||||
|
public long LongValue => PackedValue.AsInt64();
|
||||||
|
public ulong ULongValue => PackedValue.AsUInt64();
|
||||||
|
public float FloatValue => PackedValue.AsSingle();
|
||||||
|
public double DoubleValue => PackedValue.AsDouble();
|
||||||
|
public string StringValue => PackedValue.AsString();
|
||||||
|
public Span<byte> BinaryValue => PackedValue.AsBinary();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A potential formatted value returned by a <see cref="ValueFormatter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct FormattedValue
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Was any handler able to match anything in the Play Report?
|
||||||
|
/// </summary>
|
||||||
|
public bool Handled { get; private init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Did the handler request the caller of the <see cref="Analyzer"/> to reset the existing value?
|
||||||
|
/// </summary>
|
||||||
|
public bool Reset { get; private init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The formatted value, only present if <see cref="Handled"/> is true, and <see cref="Reset"/> is false.
|
||||||
|
/// </summary>
|
||||||
|
public string FormattedString { get; private init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The intended path of execution for having a string to return: simply return the string.
|
||||||
|
/// This implicit conversion will make the struct for you.<br/><br/>
|
||||||
|
///
|
||||||
|
/// If the input is null, <see cref="Unhandled"/> is returned.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="formattedValue">The formatted string value.</param>
|
||||||
|
/// <returns>The automatically constructed <see cref="FormattedValue"/> struct.</returns>
|
||||||
|
public static implicit operator FormattedValue(string formattedValue)
|
||||||
|
=> formattedValue is not null
|
||||||
|
? new FormattedValue { Handled = true, FormattedString = formattedValue }
|
||||||
|
: Unhandled;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
if (!Handled)
|
||||||
|
return "<Unhandled>";
|
||||||
|
|
||||||
|
if (Reset)
|
||||||
|
return "<Reset>";
|
||||||
|
|
||||||
|
return FormattedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return this to tell the caller there is no value to return.
|
||||||
|
/// </summary>
|
||||||
|
public static FormattedValue Unhandled => default;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return this to suggest the caller reset the value it's using the <see cref="Analyzer"/> for.
|
||||||
|
/// </summary>
|
||||||
|
public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="ValueFormatter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly ValueFormatter SingleAlwaysResets = _ => ForceReset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="MultiValueFormatter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly MultiValueFormatter MultiAlwaysResets = _ => ForceReset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A delegate factory you can use to always return the specified
|
||||||
|
/// <paramref name="formattedValue"/> in a <see cref="ValueFormatter"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
|
||||||
|
public static ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user