Compare commits
21 Commits
Canary-1.2
...
6fc4ae1098
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fc4ae1098 | ||
|
|
5286599019 | ||
|
|
24b35335f4 | ||
|
|
566f3d079a | ||
|
|
d7707d4176 | ||
|
|
7a9b62884a | ||
|
|
de9faf183a | ||
|
|
0bf7c5dfa2 | ||
|
|
11bc32d98e | ||
|
|
063430ea16 | ||
|
|
65f08caaa3 | ||
|
|
f225b18c05 | ||
|
|
d8549f687b | ||
|
|
5ab50680b4 | ||
|
|
1e75f3ea21 | ||
|
|
341ec02c03 | ||
|
|
4f30d8d73f | ||
|
|
a0edc5c2b0 | ||
|
|
158ea7b4d6 | ||
|
|
8bc3de8303 | ||
|
|
c812106611 |
@@ -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
|
||||||
|
|||||||
|
@@ -2,35 +2,36 @@ using MsgPack;
|
|||||||
using Ryujinx.Horizon.Common;
|
using Ryujinx.Horizon.Common;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Ryujinx.Horizon
|
namespace Ryujinx.Horizon
|
||||||
{
|
{
|
||||||
public static class HorizonStatic
|
public static class HorizonStatic
|
||||||
{
|
{
|
||||||
internal static void HandlePlayReport(MessagePackObject report) => PlayReportPrinted?.Invoke(report);
|
internal static void HandlePlayReport(MessagePackObject report) =>
|
||||||
|
new Thread(() => PlayReport?.Invoke(report))
|
||||||
|
{
|
||||||
|
Name = "HLE.PlayReportEvent",
|
||||||
|
IsBackground = true,
|
||||||
|
Priority = ThreadPriority.AboveNormal
|
||||||
|
}.Start();
|
||||||
|
|
||||||
public static event Action<MessagePackObject> PlayReportPrinted;
|
public static event Action<MessagePackObject> PlayReport;
|
||||||
|
|
||||||
[ThreadStatic]
|
|
||||||
private static HorizonOptions _options;
|
|
||||||
|
|
||||||
[ThreadStatic]
|
[field: ThreadStatic]
|
||||||
private static ISyscallApi _syscall;
|
public static HorizonOptions Options { get; private set; }
|
||||||
|
|
||||||
[ThreadStatic]
|
[field: ThreadStatic]
|
||||||
private static IVirtualMemoryManager _addressSpace;
|
public static ISyscallApi Syscall { get; private set; }
|
||||||
|
|
||||||
[ThreadStatic]
|
[field: ThreadStatic]
|
||||||
private static IThreadContext _threadContext;
|
public static IVirtualMemoryManager AddressSpace { get; private set; }
|
||||||
|
|
||||||
[ThreadStatic]
|
[field: ThreadStatic]
|
||||||
private static int _threadHandle;
|
public static IThreadContext ThreadContext { get; private set; }
|
||||||
|
|
||||||
public static HorizonOptions Options => _options;
|
[field: ThreadStatic]
|
||||||
public static ISyscallApi Syscall => _syscall;
|
public static int CurrentThreadHandle { get; private set; }
|
||||||
public static IVirtualMemoryManager AddressSpace => _addressSpace;
|
|
||||||
public static IThreadContext ThreadContext => _threadContext;
|
|
||||||
public static int CurrentThreadHandle => _threadHandle;
|
|
||||||
|
|
||||||
public static void Register(
|
public static void Register(
|
||||||
HorizonOptions options,
|
HorizonOptions options,
|
||||||
@@ -39,11 +40,11 @@ namespace Ryujinx.Horizon
|
|||||||
IThreadContext threadContext,
|
IThreadContext threadContext,
|
||||||
int threadHandle)
|
int threadHandle)
|
||||||
{
|
{
|
||||||
_options = options;
|
Options = options;
|
||||||
_syscall = syscallApi;
|
Syscall = syscallApi;
|
||||||
_addressSpace = addressSpace;
|
AddressSpace = addressSpace;
|
||||||
_threadContext = threadContext;
|
ThreadContext = threadContext;
|
||||||
_threadHandle = threadHandle;
|
CurrentThreadHandle = threadHandle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Gommon;
|
||||||
using MsgPack;
|
using MsgPack;
|
||||||
using MsgPack.Serialization;
|
using MsgPack.Serialization;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
@@ -11,6 +12,7 @@ using Ryujinx.Horizon.Sdk.Sf;
|
|||||||
using Ryujinx.Horizon.Sdk.Sf.Hipc;
|
using Ryujinx.Horizon.Sdk.Sf.Hipc;
|
||||||
using System;
|
using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId;
|
using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId;
|
||||||
|
|
||||||
namespace Ryujinx.Horizon.Prepo.Ipc
|
namespace Ryujinx.Horizon.Prepo.Ipc
|
||||||
|
|||||||
@@ -23069,7 +23069,7 @@
|
|||||||
"tr_TR": "",
|
"tr_TR": "",
|
||||||
"uk_UA": "",
|
"uk_UA": "",
|
||||||
"zh_CN": "可游玩",
|
"zh_CN": "可游玩",
|
||||||
"zh_TW": "可暢順遊玩 (Playable)"
|
"zh_TW": "可暢順遊玩"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23094,7 +23094,7 @@
|
|||||||
"tr_TR": "",
|
"tr_TR": "",
|
||||||
"uk_UA": "",
|
"uk_UA": "",
|
||||||
"zh_CN": "进入游戏",
|
"zh_CN": "进入游戏",
|
||||||
"zh_TW": "大致可遊玩 (Ingame)"
|
"zh_TW": "大致可遊玩"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23119,7 +23119,7 @@
|
|||||||
"tr_TR": "",
|
"tr_TR": "",
|
||||||
"uk_UA": "",
|
"uk_UA": "",
|
||||||
"zh_CN": "菜单",
|
"zh_CN": "菜单",
|
||||||
"zh_TW": "只開啟至遊戲開始功能表 (Menus)"
|
"zh_TW": "只開啟至遊戲開始功能表"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23144,7 +23144,7 @@
|
|||||||
"tr_TR": "",
|
"tr_TR": "",
|
||||||
"uk_UA": "",
|
"uk_UA": "",
|
||||||
"zh_CN": "启动",
|
"zh_CN": "启动",
|
||||||
"zh_TW": "只能啟動 (Boots)"
|
"zh_TW": "只能啟動"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23169,7 +23169,7 @@
|
|||||||
"tr_TR": "",
|
"tr_TR": "",
|
||||||
"uk_UA": "",
|
"uk_UA": "",
|
||||||
"zh_CN": "什么都没有",
|
"zh_CN": "什么都没有",
|
||||||
"zh_TW": "無法啟動 (Nothing)"
|
"zh_TW": "無法啟動"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
|
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
|
||||||
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
|
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
|
||||||
HorizonStatic.PlayReportPrinted += HandlePlayReport;
|
HorizonStatic.PlayReport += HandlePlayReport;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Update(object sender, ReactiveEventArgs<bool> evnt)
|
private static void Update(object sender, ReactiveEventArgs<bool> evnt)
|
||||||
@@ -117,11 +117,6 @@ namespace Ryujinx.Ava
|
|||||||
_currentApp = appMeta;
|
_currentApp = appMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdatePlayingState()
|
|
||||||
{
|
|
||||||
_discordClient?.SetPresence(_discordPresencePlaying);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SwitchToMainState()
|
private static void SwitchToMainState()
|
||||||
{
|
{
|
||||||
_discordClient?.SetPresence(_discordPresenceMain);
|
_discordClient?.SetPresence(_discordPresenceMain);
|
||||||
@@ -135,21 +130,20 @@ 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;
|
||||||
|
|
||||||
PlayReportFormattedValue value = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
|
PlayReportAnalyzer.FormattedValue formattedValue =
|
||||||
|
PlayReport.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
|
||||||
|
|
||||||
if (!value.Handled) return;
|
if (!formattedValue.Handled) return;
|
||||||
|
|
||||||
if (value.Reset)
|
_discordPresencePlaying.Details = formattedValue.Reset
|
||||||
{
|
? $"Playing {_currentApp.Title}"
|
||||||
_discordPresencePlaying.Details = $"Playing {_currentApp.Title}";
|
: formattedValue.FormattedString;
|
||||||
Logger.Info?.Print(LogClass.UI, "Reset Discord RPC based on a supported play report value formatter.");
|
|
||||||
}
|
if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
|
||||||
else
|
return; //don't trigger an update if the set presence Details are identical to current
|
||||||
{
|
|
||||||
_discordPresencePlaying.Details = value.FormattedString;
|
_discordClient.SetPresence(_discordPresencePlaying);
|
||||||
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
|
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
|
||||||
}
|
|
||||||
UpdatePlayingState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string TruncateToByteLength(string input)
|
private static string TruncateToByteLength(string input)
|
||||||
|
|||||||
@@ -86,6 +86,13 @@
|
|||||||
Text="{Binding Version}"
|
Text="{Binding Version}"
|
||||||
TextAlignment="Start"
|
TextAlignment="Start"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
|
<TextBlock
|
||||||
|
IsVisible="{Binding HasPlayabilityInfo}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Text="{Binding LocalizedStatus}"
|
||||||
|
Foreground="{Binding PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
|
||||||
|
TextAlignment="Start"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ using LibHac.Ns;
|
|||||||
using LibHac.Tools.Fs;
|
using LibHac.Tools.Fs;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Utilities.Compat;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||||
@@ -21,9 +23,30 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
|||||||
public bool Favorite { get; set; }
|
public bool Favorite { get; set; }
|
||||||
public byte[] Icon { get; set; }
|
public byte[] Icon { get; set; }
|
||||||
public string Name { get; set; } = "Unknown";
|
public string Name { get; set; } = "Unknown";
|
||||||
public ulong Id { get; set; }
|
|
||||||
|
private ulong _id;
|
||||||
|
|
||||||
|
public ulong Id
|
||||||
|
{
|
||||||
|
get => _id;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_id = value;
|
||||||
|
PlayabilityStatus = CompatibilityCsv.GetStatus(Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
public string Developer { get; set; } = "Unknown";
|
public string Developer { get; set; } = "Unknown";
|
||||||
public string Version { get; set; } = "0";
|
public string Version { get; set; } = "0";
|
||||||
|
|
||||||
|
public bool HasPlayabilityInfo => PlayabilityStatus != null;
|
||||||
|
|
||||||
|
public string LocalizedStatus =>
|
||||||
|
PlayabilityStatus.HasValue
|
||||||
|
? LocaleManager.Instance[PlayabilityStatus!.Value]
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
public LocaleKeys? PlayabilityStatus { get; set; }
|
||||||
|
|
||||||
public int PlayerCount { get; set; }
|
public int PlayerCount { get; set; }
|
||||||
public int GameCount { get; set; }
|
public int GameCount { get; set; }
|
||||||
public TimeSpan TimePlayed { get; set; }
|
public TimeSpan TimePlayed { get; set; }
|
||||||
|
|||||||
@@ -47,11 +47,6 @@ namespace Ryujinx.Ava.Utilities.Compat
|
|||||||
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatibility");
|
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatibility");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Unload()
|
|
||||||
{
|
|
||||||
_entries = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CompatibilityEntry[] _entries;
|
private static CompatibilityEntry[] _entries;
|
||||||
|
|
||||||
public static CompatibilityEntry[] Entries
|
public static CompatibilityEntry[] Entries
|
||||||
@@ -64,6 +59,11 @@ namespace Ryujinx.Ava.Utilities.Compat
|
|||||||
return _entries;
|
return _entries;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static LocaleKeys? GetStatus(string titleId)
|
||||||
|
=> Entries.FirstOrDefault(x => x.TitleId.HasValue && x.TitleId.Value.EqualsIgnoreCase(titleId))?.Status;
|
||||||
|
|
||||||
|
public static LocaleKeys? GetStatus(ulong titleId) => GetStatus(titleId.ToString("X16"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CompatibilityEntry
|
public class CompatibilityEntry
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ namespace Ryujinx.Ava.Utilities.Compat
|
|||||||
contentDialog.Styles.Add(closeButtonParent);
|
contentDialog.Styles.Add(closeButtonParent);
|
||||||
|
|
||||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||||
|
|
||||||
CompatibilityCsv.Unload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompatibilityList()
|
public CompatibilityList()
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
using Gommon;
|
using PlayReportFormattedValue = Ryujinx.Ava.Utilities.PlayReportAnalyzer.FormattedValue;
|
||||||
using MsgPack;
|
|
||||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
|
||||||
using Ryujinx.Common.Helper;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Utilities
|
namespace Ryujinx.Ava.Utilities
|
||||||
{
|
{
|
||||||
@@ -13,41 +7,55 @@ namespace Ryujinx.Ava.Utilities
|
|||||||
public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer()
|
public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer()
|
||||||
.AddSpec(
|
.AddSpec(
|
||||||
"01007ef00011e000",
|
"01007ef00011e000",
|
||||||
spec => spec.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
|
spec => spec
|
||||||
|
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
|
||||||
|
// reset to normal status when switching between normal & master mode in title screen
|
||||||
|
.AddValueFormatter("AoCVer", PlayReportFormattedValue.AlwaysResets)
|
||||||
)
|
)
|
||||||
.AddSpec( // Super Mario Odyssey
|
.AddSpec(
|
||||||
|
"0100f2c0115b6000",
|
||||||
|
spec => spec.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
|
||||||
|
.AddSpec(
|
||||||
"0100000000010000",
|
"0100000000010000",
|
||||||
spec =>
|
spec =>
|
||||||
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
|
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
|
||||||
)
|
)
|
||||||
.AddSpec( // Super Mario Odyssey (China)
|
.AddSpec(
|
||||||
"010075000ECBE000",
|
"010075000ecbe000",
|
||||||
spec =>
|
spec =>
|
||||||
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
|
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
|
||||||
)
|
)
|
||||||
.AddSpec( // Super Mario 3D World + Bowser's Fury
|
.AddSpec(
|
||||||
"010028600EBDA000",
|
"010028600ebda000",
|
||||||
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
|
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
|
||||||
)
|
)
|
||||||
.AddSpec( // Mario Kart 8 Deluxe, Mario Kart 8 Deluxe (China)
|
.AddSpec( // Global & China IDs
|
||||||
["0100152000022000", "010075100E8EC000"],
|
["0100152000022000", "010075100e8ec000"],
|
||||||
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
|
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
|
||||||
);
|
);
|
||||||
|
|
||||||
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref PlayReportValue value)
|
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(PlayReportValue value)
|
||||||
=> value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
|
=> value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
|
||||||
|
|
||||||
private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(ref PlayReportValue value)
|
private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(PlayReportValue value) =>
|
||||||
|
value.DoubleValue switch
|
||||||
|
{
|
||||||
|
> 800d => "Exploring the Sky Islands",
|
||||||
|
< -201d => "Exploring the Depths",
|
||||||
|
_ => "Roaming Hyrule"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(PlayReportValue value)
|
||||||
=> value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
|
=> value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
|
||||||
|
|
||||||
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value)
|
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(PlayReportValue value)
|
||||||
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
|
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
|
||||||
|
|
||||||
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(ref PlayReportValue value)
|
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(PlayReportValue value)
|
||||||
=> value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
|
=> value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
|
||||||
|
|
||||||
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(ref PlayReportValue value)
|
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(PlayReportValue value)
|
||||||
=> value.BoxedValue switch
|
=> value.StringValue switch
|
||||||
{
|
{
|
||||||
// Single Player
|
// Single Player
|
||||||
"Single" => "Single Player",
|
"Single" => "Single Player",
|
||||||
@@ -74,125 +82,4 @@ namespace Ryujinx.Ava.Utilities
|
|||||||
_ => PlayReportFormattedValue.ForceReset
|
_ => PlayReportFormattedValue.ForceReset
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Analyzer implementation
|
|
||||||
|
|
||||||
public class PlayReportAnalyzer
|
|
||||||
{
|
|
||||||
private readonly List<PlayReportGameSpec> _specs = [];
|
|
||||||
|
|
||||||
public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
|
||||||
{
|
|
||||||
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform)
|
|
||||||
{
|
|
||||||
_specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
|
||||||
{
|
|
||||||
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [..titleIds] }));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Action<PlayReportGameSpec> transform)
|
|
||||||
{
|
|
||||||
_specs.Add(new PlayReportGameSpec { TitleIds = [..titleIds] }.Apply(transform));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlayReportFormattedValue Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport)
|
|
||||||
{
|
|
||||||
if (!playReport.IsDictionary)
|
|
||||||
return PlayReportFormattedValue.Unhandled;
|
|
||||||
|
|
||||||
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
|
|
||||||
return PlayReportFormattedValue.Unhandled;
|
|
||||||
|
|
||||||
foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority))
|
|
||||||
{
|
|
||||||
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
PlayReportValue value = new()
|
|
||||||
{
|
|
||||||
Application = appMeta,
|
|
||||||
BoxedValue = valuePackObject.ToObject()
|
|
||||||
};
|
|
||||||
|
|
||||||
return formatSpec.ValueFormatter(ref value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PlayReportFormattedValue.Unhandled;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PlayReportGameSpec
|
|
||||||
{
|
|
||||||
public required string[] TitleIds { get; init; }
|
|
||||||
public List<PlayReportValueFormatterSpec> Analyses { get; } = [];
|
|
||||||
|
|
||||||
public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter)
|
|
||||||
{
|
|
||||||
Analyses.Add(new PlayReportValueFormatterSpec
|
|
||||||
{
|
|
||||||
Priority = Analyses.Count,
|
|
||||||
ReportKey = reportKey,
|
|
||||||
ValueFormatter = valueFormatter
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, PlayReportValueFormatter valueFormatter)
|
|
||||||
{
|
|
||||||
Analyses.Add(new PlayReportValueFormatterSpec
|
|
||||||
{
|
|
||||||
Priority = priority,
|
|
||||||
ReportKey = reportKey,
|
|
||||||
ValueFormatter = valueFormatter
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct PlayReportValue
|
|
||||||
{
|
|
||||||
public ApplicationMetadata Application { get; init; }
|
|
||||||
public object BoxedValue { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct PlayReportFormattedValue
|
|
||||||
{
|
|
||||||
public bool Handled { get; private init; }
|
|
||||||
|
|
||||||
public bool Reset { get; private init; }
|
|
||||||
|
|
||||||
public string FormattedString { get; private init; }
|
|
||||||
|
|
||||||
public static implicit operator PlayReportFormattedValue(string formattedValue)
|
|
||||||
=> new() { Handled = true, FormattedString = formattedValue };
|
|
||||||
|
|
||||||
public static PlayReportFormattedValue Unhandled => default;
|
|
||||||
public static PlayReportFormattedValue ForceReset => new() { Handled = true, Reset = true };
|
|
||||||
|
|
||||||
public static PlayReportValueFormatter AlwaysResets = AlwaysResetsImpl;
|
|
||||||
|
|
||||||
private static PlayReportFormattedValue AlwaysResetsImpl(ref PlayReportValue _) => ForceReset;
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct PlayReportValueFormatterSpec
|
|
||||||
{
|
|
||||||
public required int Priority { get; init; }
|
|
||||||
public required string ReportKey { get; init; }
|
|
||||||
public PlayReportValueFormatter ValueFormatter { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public delegate PlayReportFormattedValue PlayReportValueFormatter(ref PlayReportValue value);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|||||||
282
src/Ryujinx/Utilities/PlayReportAnalyzer.cs
Normal file
282
src/Ryujinx/Utilities/PlayReportAnalyzer.cs
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
using Gommon;
|
||||||
|
using MsgPack;
|
||||||
|
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The entrypoint for the Play Report analysis system.
|
||||||
|
/// </summary>
|
||||||
|
public class PlayReportAnalyzer
|
||||||
|
{
|
||||||
|
private readonly List<PlayReportGameSpec> _specs = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
|
||||||
|
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||||
|
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
|
||||||
|
public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
|
||||||
|
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
|
||||||
|
|
||||||
|
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
|
||||||
|
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||||
|
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
|
||||||
|
public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
|
||||||
|
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
|
||||||
|
|
||||||
|
_specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
|
||||||
|
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||||
|
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
|
||||||
|
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds,
|
||||||
|
Func<PlayReportGameSpec, PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
string[] tids = titleIds.ToArray();
|
||||||
|
Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
|
||||||
|
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
|
||||||
|
|
||||||
|
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [..tids] }));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
|
||||||
|
/// <param name="transform">The configuration function for the analysis spec.</param>
|
||||||
|
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
|
||||||
|
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Action<PlayReportGameSpec> transform)
|
||||||
|
{
|
||||||
|
string[] tids = titleIds.ToArray();
|
||||||
|
Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
|
||||||
|
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
|
||||||
|
|
||||||
|
_specs.Add(new PlayReportGameSpec { TitleIds = [..tids] }.Apply(transform));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the configured <see cref="PlayReportGameSpec.FormatterSpec"/> for the specified game title ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="runningGameId">The game currently running.</param>
|
||||||
|
/// <param name="appMeta">The Application metadata information, including localized game name and play time information.</param>
|
||||||
|
/// <param name="playReport">The Play Report received from HLE.</param>
|
||||||
|
/// <returns>A struct representing a possible formatted value.</returns>
|
||||||
|
public FormattedValue Format(
|
||||||
|
string runningGameId,
|
||||||
|
ApplicationMetadata appMeta,
|
||||||
|
MessagePackObject playReport
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!playReport.IsDictionary)
|
||||||
|
return FormattedValue.Unhandled;
|
||||||
|
|
||||||
|
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
|
||||||
|
return FormattedValue.Unhandled;
|
||||||
|
|
||||||
|
foreach (PlayReportGameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
|
||||||
|
{
|
||||||
|
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return formatSpec.ValueFormatter(new PlayReportValue
|
||||||
|
{
|
||||||
|
Application = appMeta, PackedValue = valuePackObject
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return FormattedValue.Unhandled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A potential formatted value returned by a <see cref="PlayReportValueFormatter"/>.
|
||||||
|
/// </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="PlayReportAnalyzer"/> 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="PlayReportAnalyzer"/> 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="PlayReportValueFormatter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly PlayReportValueFormatter AlwaysResets = _ => ForceReset;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A delegate factory you can use to always return the specified
|
||||||
|
/// <paramref name="formattedValue"/> in a <see cref="PlayReportValueFormatter"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
|
||||||
|
public static PlayReportValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A mapping of title IDs to value formatter specs.
|
||||||
|
///
|
||||||
|
/// <remarks>Generally speaking, use the <see cref="PlayReportAnalyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
|
||||||
|
/// </summary>
|
||||||
|
public class PlayReportGameSpec
|
||||||
|
{
|
||||||
|
public required string[] TitleIds { get; init; }
|
||||||
|
public List<FormatterSpec> SimpleValueFormatters { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a value formatter to the current <see cref="PlayReportGameSpec"/>
|
||||||
|
/// 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="PlayReportGameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter 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="PlayReportGameSpec"/>
|
||||||
|
/// 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="PlayReportGameSpec"/>, for chaining convenience.</returns>
|
||||||
|
public PlayReportGameSpec AddValueFormatter(int priority, string reportKey,
|
||||||
|
PlayReportValueFormatter valueFormatter)
|
||||||
|
{
|
||||||
|
SimpleValueFormatters.Add(new FormatterSpec
|
||||||
|
{
|
||||||
|
Priority = priority, ReportKey = reportKey, 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 PlayReportValueFormatter ValueFormatter { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The input data to a <see cref="PlayReportValueFormatter"/>,
|
||||||
|
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
|
||||||
|
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
|
||||||
|
/// </summary>
|
||||||
|
public class PlayReportValue
|
||||||
|
{
|
||||||
|
/// <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 <see cref="PackedValue"/> and the AsX (where X is a numerical type name i.e. Int32) methods 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 the entire analysis system (as it currently is).<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="PlayReportAnalyzer"/> for.
|
||||||
|
/// </summary>
|
||||||
|
public delegate PlayReportAnalyzer.FormattedValue PlayReportValueFormatter(PlayReportValue value);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user