Compare commits

...

15 Commits

Author SHA1 Message Date
Evan Husted
1d88771d1b Play Report Analyzer v4
You can now access the *entire* play report data in any given value formatter.
The input types have been restructured and, notably, not every instance of Value has an ApplicationMetadata on it. It's now on the container type that also contains the matched values and the entire play report.
2025-02-08 00:22:34 -06:00
Evan Husted
4e8157688e UI: See what games do/don't have an image & dynamic RPC support in the Game Info popup 2025-02-07 18:34:11 -06:00
Evan Husted
5085af0050 UI: Changed the color of "Ingame" from yellow to orange to stand out better in light mode 2025-02-07 18:28:32 -06:00
Evan Husted
2c8edaf89e PlayReport: Add Sparse Multi Value formatters 2025-02-07 15:43:50 -06:00
Evan Husted
aa8ba8b503 Merge remote-tracking branch 'origin/master' 2025-02-06 22:56:33 -06:00
Evan Husted
a4211fec33 UI: Properly space the play time & last play date in the game info popup 2025-02-06 22:56:25 -06:00
Daenorth
54b233dd78 Updated the compat list. (#618) 2025-02-06 04:46:23 -06:00
Evan Husted
d1da937fce misc: chore: [ci skip] XMLdocs on new Play Report Analyzer members 2025-02-05 19:51:43 -06:00
Evan Husted
4a8f98126f [ci skip] remove test 2025-02-05 19:45:29 -06:00
Evan Husted
e55629a908 misc: chore: [ci skip] Play Report Analyzer: Added Multi Value formatters 2025-02-05 19:42:36 -06:00
Evan Husted
c638a7daf8 misc: chore: Move Play Report analyzer into a dedicated namespace and remove the PlayReport name prefix on types 2025-02-05 19:27:44 -06:00
Piplup
5e5e180fea PlayReportAnalyzer: Added Pokemon Scarlet and Violet (#630)
Every base game location excluding buildings are done, DLC locations
will be added at a later point
2025-02-05 18:32:27 -06:00
Hack茶ん
131fe71205 Update Korean translation (#624) 2025-02-05 02:40:37 -06:00
Evan Husted
6af388c623 misc: chore: [ci skip] oops forgot to localize the reset button & confirmation 2025-02-05 02:01:33 -06:00
Evan Husted
45cec4e7cf UI: In-app Configuration resetting 2025-02-05 01:42:27 -06:00
20 changed files with 1051 additions and 491 deletions

View File

@@ -332,6 +332,7 @@
0100E680149DC000,"Arcaea",,playable,2023-03-16 19:31:21
01003C2010C78000,"Archaica: The Path Of Light",crash,nothing,2020-10-16 13:22:26
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
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
@@ -426,6 +427,7 @@
0100E48013A34000,"Balan Wonderworld Demo",gpu;services;UE4;demo,ingame,2023-02-16 20:05:07
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
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
01008D30128E0000,"Bamerang",,playable,2022-10-26 00:29:39
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
0100DF0011A6A000,"Bladed Fury",,playable,2022-10-26 11:36:26
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
010039501405E000,"Blanc",gpu;slow,ingame,2023-02-22 14:00:13
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
01009740120FE000,"DISTRAINT 2",,playable,2020-09-03 16:08:12
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
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
@@ -1654,7 +1657,7 @@
0100A73006E74000,"Legendary Eleven",,playable,2021-06-08 12:09:03
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
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
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
@@ -1913,6 +1916,7 @@
010073E008E6E000,"Mugsters",,playable,2021-01-28 17:57:17
0100A8400471A000,"MUJO",,playable,2020-05-08 16:31:04
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
01008E200C5C2000,"Muse Dash",,playable,2020-06-06 14:41:29
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
0100746010E4C000,"NinNinDays",,playable,2022-11-20 15:17:29
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
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
@@ -2532,7 +2537,7 @@
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
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
01009840046BC000,"Semispheres",,playable,2021-01-06 23:08:31
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
010007700D4AC000,"The Forbidden Arts",,playable,2021-01-26 16:26:24
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
0100B13007A6A000,"The Gardens Between",,playable,2021-01-29 16:16:53
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
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
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
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
@@ -3159,6 +3167,7 @@
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
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
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
@@ -3208,6 +3217,7 @@
0100AB2010B4C000,"Unlock The King",,playable,2020-09-01 13:58:27
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
0100b6e012ebe000,"UNO",ldn-untested,ingame,2025-02-03 22:40:00
0100E5D00CC0C000,"Unravel Two",nvdec,playable,2024-05-23 15:45:05
010001300CC4A000,"Unruly Heroes",,playable,2021-01-07 18:09:31
0100B410138C0000,"Unspottable",,playable,2022-10-25 19:28:49
@@ -3372,6 +3382,7 @@
0100F47016F26000,"Yomawari 3",,playable,2022-05-10 08:26:51
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
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
0100F110029C8000,"Yooka-Laylee",,playable,2021-01-28 14:21:45
010022F00DA66000,"Yooka-Laylee and the Impossible Lair",,playable,2021-03-05 17:32:21
1 title_id game_name labels status last_updated
332 0100E680149DC000 Arcaea playable 2023-03-16 19:31:21
333 01003C2010C78000 Archaica: The Path Of Light crash nothing 2020-10-16 13:22:26
334 01004DA012976000 Area 86 playable 2020-12-16 16:45:52
335 01008d8006a6a000 Arena of Valor crash boots 2025-02-03 22:19:34
336 0100691013C46000 ARIA CHRONICLE playable 2022-11-16 13:50:55
337 0100D4A00B284000 ARK: Survival Evolved gpu;nvdec;online-broken;UE4;ldn-untested ingame 2024-04-16 00:53:56
338 0100C56012C96000 Arkanoid vs. Space Invaders services ingame 2021-01-21 12:50:30
427 0100E48013A34000 Balan Wonderworld Demo gpu;services;UE4;demo ingame 2023-02-16 20:05:07
428 0100CD801CE5E000 Balatro ingame 2024-04-21 02:01:53
429 010010A00DA48000 Baldur's Gate and Baldur's Gate II: Enhanced Editions 32-bit playable 2022-09-12 23:52:15
430 0100fd1014726000 Baldur's Gate: Dark Alliance ldn-untested ingame 2025-02-03 22:21:00
431 0100BC400FB64000 Balthazar's Dream playable 2022-09-13 00:13:22
432 01008D30128E0000 Bamerang playable 2022-10-26 00:29:39
433 010013C010C5C000 Banner of the Maid playable 2021-06-14 15:23:37
530 01005950022EC000 Blade Strangers nvdec playable 2022-07-17 19:02:43
531 0100DF0011A6A000 Bladed Fury playable 2022-10-26 11:36:26
532 0100CFA00CC74000 Blades of Time deadlock;online boots 2022-07-17 19:19:58
533 01003d700dd8a000 Blades boots 2025-02-03 22:22:00
534 01006CC01182C000 Blair Witch nvdec;UE4 playable 2022-10-01 14:06:16
535 010039501405E000 Blanc gpu;slow ingame 2023-02-22 14:00:13
536 0100698009C6E000 Blasphemous nvdec playable 2021-03-01 12:15:31
958 010012800EBAE000 Disney TSUM TSUM FESTIVAL crash menus 2020-07-14 14:05:28
959 01009740120FE000 DISTRAINT 2 playable 2020-09-03 16:08:12
960 010075B004DD2000 DISTRAINT: Deluxe Edition playable 2020-06-15 23:42:24
961 010027400CDC6000 Divinity: Original Sin 2 - Definitive Edition services;crash;online-broken;regression menus ingame 2023-08-13 17:20:03 2025-02-03 22:12:30
962 01001770115C8000 Dodo Peak nvdec;UE4 playable 2022-10-04 16:13:05
963 010077B0100DA000 Dogurai playable 2020-10-04 02:40:16
964 010048100D51A000 Dokapon Up! Mugen no Roulette gpu;Needs Update menus 2022-12-08 19:39:10
1657 0100A73006E74000 Legendary Eleven playable 2021-06-08 12:09:03
1658 0100A7700B46C000 Legendary Fishing online playable 2021-04-14 15:08:46
1659 0100739018020000 LEGO® 2K Drive gpu;ldn-works ingame 2024-04-09 02:05:12
1660 01003A30012C0000 010085500130a000 LEGO® CITY Undercover nvdec playable 2024-09-30 08:44:27
1661 010070D009FEC000 LEGO® DC Super-Villains playable 2021-05-27 18:10:37
1662 010052A00B5D2000 LEGO® Harry Potter™ Collection crash ingame 2024-01-31 10:28:07
1663 010073C01AF34000 LEGO® Horizon Adventures™ vulkan-backend-bug;opengl-backend-bug;UE4 ingame 2025-01-07 04:24:56
1916 010073E008E6E000 Mugsters playable 2021-01-28 17:57:17
1917 0100A8400471A000 MUJO playable 2020-05-08 16:31:04
1918 0100211005E94000 Mulaka playable 2021-01-28 18:07:20
1919 01008e2013fb4000 Multi Quiz ldn-untested ingame 2025-02-03 22:26:00
1920 010038B00B9AE000 Mummy Pinball playable 2022-08-05 16:08:11
1921 01008E200C5C2000 Muse Dash playable 2020-06-06 14:41:29
1922 010035901046C000 Mushroom Quest playable 2020-05-17 13:07:08
2032 010003C00B868000 Ninjin: Clash of Carrots online-broken playable 2024-07-10 05:12:26
2033 0100746010E4C000 NinNinDays playable 2022-11-20 15:17:29
2034 0100C9A00ECE6000 Nintendo 64™ – Nintendo Switch Online gpu;vulkan ingame 2024-04-23 20:21:07
2035 0100e0601c632000 Nintendo 64™ – Nintendo Switch Online: MATURE 17+ ingame 2025-02-03 22:27:00
2036 0100D870045B6000 Nintendo Entertainment System™ - Nintendo Switch Online online playable 2022-07-01 15:45:06
2037 0100C4B0034B2000 Nintendo Labo Toy-Con 01 Variety Kit gpu ingame 2022-08-07 12:56:07
2038 01001E9003502000 Nintendo Labo Toy-Con 03 Vehicle Kit services;crash menus 2022-08-03 17:20:11
2537 0100C3E00B700000 SEGA AGES Space Harrier playable 2021-01-11 12:57:40
2538 010054400D2E6000 SEGA AGES Virtua Racing online-broken playable 2023-01-29 17:08:39
2539 01001E700AC60000 SEGA AGES Wonder Boy: Monster Land online playable 2021-05-05 16:28:25
2540 0100B3C014BDA000 SEGA Genesis™ – Nintendo Switch Online crash;regression nothing ingame 2022-04-11 07:27:21 2025-02-03 22:13:30
2541 0100F7300B24E000 SEGA Mega Drive Classics online playable 2021-01-05 11:08:00
2542 01009840046BC000 Semispheres playable 2021-01-06 23:08:31
2543 0100D1800D902000 SENRAN KAGURA Peach Ball playable 2021-06-03 15:12:10
2969 0100C38004DCC000 The Flame In The Flood: Complete Edition gpu;nvdec;UE4 ingame 2022-08-22 16:23:49
2970 010007700D4AC000 The Forbidden Arts playable 2021-01-26 16:26:24
2971 010030700CBBC000 The friends of Ringo Ishikawa playable 2022-08-22 16:33:17
2972 0100b620139d8000 The Game of Life 2 ldn-untested ingame 2025-02-03 22:30:00
2973 01006350148DA000 The Gardener and the Wild Vines gpu ingame 2024-04-29 16:32:10
2974 0100B13007A6A000 The Gardens Between playable 2021-01-29 16:16:53
2975 010036E00FB20000 The Great Ace Attorney Chronicles playable 2023-06-22 21:26:29
2987 010015D003EE4000 The Jackbox Party Pack 2 online-working playable 2022-08-22 18:23:40
2988 0100CC80013D6000 The Jackbox Party Pack 3 slow;online-working playable 2022-08-22 18:41:06
2989 0100E1F003EE8000 The Jackbox Party Pack 4 online-working playable 2022-08-22 18:56:34
2990 01006fe0096ac000 The Jackbox Party Pack 5 ldn-untested boots 2025-02-03 22:32:00
2991 01005a400db52000 The Jackbox Party Pack 6 ldn-untested boots 2025-02-03 22:32:00
2992 010052C00B184000 The Journey Down: Chapter One nvdec playable 2021-02-24 13:32:41
2993 01006BC00B188000 The Journey Down: Chapter Three nvdec playable 2021-02-24 13:45:27
2994 01009AB00B186000 The Journey Down: Chapter Two nvdec playable 2021-02-24 13:32:13
3167 010055E00CA68000 Trine 4: The Nightmare Prince gpu nothing 2025-01-07 05:47:46
3168 0100D9000A930000 Trine Enchanted Edition ldn-untested;nvdec playable 2021-06-03 11:28:15
3169 01002D7010A54000 Trinity Trigger crash ingame 2023-03-03 03:09:09
3170 010020700a5e0000 TRIVIAL PURSUIT Live! ldn-untested ingame 2025-02-03 22:35:00
3171 0100868013FFC000 TRIVIAL PURSUIT Live! 2 boots 2022-12-19 00:04:33
3172 0100F78002040000 Troll and I™ gpu;nvdec ingame 2021-06-04 16:58:50
3173 0100145011008000 Trollhunters: Defenders of Arcadia gpu;nvdec ingame 2020-11-30 13:27:09
3217 0100AB2010B4C000 Unlock The King playable 2020-09-01 13:58:27
3218 0100A3E011CB0000 Unlock the King 2 playable 2021-06-15 20:43:55
3219 01005AA00372A000 UNO® for Nintendo Switch nvdec;ldn-untested playable 2022-07-28 14:49:47
3220 0100b6e012ebe000 UNO ldn-untested ingame 2025-02-03 22:40:00
3221 0100E5D00CC0C000 Unravel Two nvdec playable 2024-05-23 15:45:05
3222 010001300CC4A000 Unruly Heroes playable 2021-01-07 18:09:31
3223 0100B410138C0000 Unspottable playable 2022-10-25 19:28:49
3382 0100F47016F26000 Yomawari 3 playable 2022-05-10 08:26:51
3383 010012F00B6F2000 Yomawari: The Long Night Collection playable 2022-09-03 14:36:59
3384 0100CC600ABB2000 Yonder: The Cloud Catcher Chronicles (Retail Only) playable 2021-01-28 14:06:25
3385 0100534009ff2000 Yonder: The Cloud Catcher Chronicles playable 2025-02-03 22:19:13
3386 0100BE50042F6000 Yono and the Celestial Elephants playable 2021-01-28 18:23:58
3387 0100F110029C8000 Yooka-Laylee playable 2021-01-28 14:21:45
3388 010022F00DA66000 Yooka-Laylee and the Impossible Lair playable 2021-03-05 17:32:21

View File

@@ -1,5 +1,6 @@
using MsgPack;
using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Prepo.Types;
using Ryujinx.Memory;
using System;
using System.Threading;
@@ -8,7 +9,7 @@ namespace Ryujinx.Horizon
{
public static class HorizonStatic
{
internal static void HandlePlayReport(MessagePackObject report) =>
internal static void HandlePlayReport(PlayReport report) =>
new Thread(() => PlayReport?.Invoke(report))
{
Name = "HLE.PlayReportEvent",
@@ -16,7 +17,7 @@ namespace Ryujinx.Horizon
Priority = ThreadPriority.AboveNormal
}.Start();
public static event Action<MessagePackObject> PlayReport;
public static event Action<PlayReport> PlayReport;
[field: ThreadStatic]
public static HorizonOptions Options { get; private set; }

View File

@@ -1,4 +1,3 @@
using Gommon;
using MsgPack;
using MsgPack.Serialization;
using Ryujinx.Common.Logging;
@@ -12,19 +11,12 @@ using Ryujinx.Horizon.Sdk.Sf;
using Ryujinx.Horizon.Sdk.Sf.Hipc;
using System;
using System.Text;
using System.Threading;
using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId;
namespace Ryujinx.Horizon.Prepo.Ipc
{
partial class PrepoService : IPrepoService
{
enum PlayReportKind
{
Normal,
System,
}
private readonly ArpApi _arp;
private readonly PrepoServicePermissionLevel _permissionLevel;
private ulong _systemSessionId;
@@ -196,10 +188,17 @@ namespace Ryujinx.Horizon.Prepo.Ipc
{
return PrepoResult.InvalidBufferSize;
}
StringBuilder builder = new();
MessagePackObject deserializedReport = MessagePackSerializer.UnpackMessagePackObject(reportBuffer.ToArray());
PlayReport playReport = new()
{
Kind = playReportKind,
Room = gameRoom,
ReportData = deserializedReport
};
builder.AppendLine();
builder.AppendLine("PlayReport log:");
builder.AppendLine($" Kind: {playReportKind}");
@@ -209,10 +208,12 @@ namespace Ryujinx.Horizon.Prepo.Ipc
if (pid != 0)
{
builder.AppendLine($" Pid: {pid}");
playReport.Pid = pid;
}
else
{
builder.AppendLine($" ApplicationId: {applicationId}");
playReport.AppId = applicationId;
}
Result result = _arp.GetApplicationInstanceId(out ulong applicationInstanceId, pid);
@@ -223,17 +224,20 @@ namespace Ryujinx.Horizon.Prepo.Ipc
_arp.GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, applicationInstanceId).AbortOnFailure();
playReport.Version = applicationLaunchProperty.Version;
builder.AppendLine($" ApplicationVersion: {applicationLaunchProperty.Version}");
if (!userId.IsNull)
{
builder.AppendLine($" UserId: {userId}");
playReport.UserId = userId;
}
builder.AppendLine($" Room: {gameRoom}");
builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}");
HorizonStatic.HandlePlayReport(deserializedReport);
HorizonStatic.HandlePlayReport(playReport);
Logger.Info?.Print(LogClass.ServicePrepo, builder.ToString());

View File

@@ -0,0 +1,24 @@
using MsgPack;
using Ryujinx.Horizon.Sdk.Account;
using Ryujinx.Horizon.Sdk.Ncm;
namespace Ryujinx.Horizon.Prepo.Types
{
public struct PlayReport
{
public PlayReportKind Kind { get; init; }
public string Room { get; init; }
public MessagePackObject ReportData { get; init; }
public ApplicationId? AppId;
public ulong? Pid;
public uint Version;
public Uid? UserId;
}
public enum PlayReportKind
{
Normal,
System,
}
}

View File

@@ -584,7 +584,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "UI를 숨긴 상태에서 게임 시작",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -1576,50 +1576,50 @@
"ID": "GameListHeaderTimePlayed",
"Translations": {
"ar_SA": "",
"de_DE": "Spielzeit: {0}",
"el_GR": "Χρόνος: {0}",
"en_US": "Play Time: {0}",
"es_ES": "Tiempo jugado: {0}",
"fr_FR": "Temps de jeu: {0}",
"de_DE": "Spielzeit:",
"el_GR": "Χρόνος:",
"en_US": "Play Time:",
"es_ES": "Tiempo jugado:",
"fr_FR": "Temps de jeu:",
"he_IL": "",
"it_IT": "Tempo di gioco: {0}",
"ja_JP": "プレイ時間: {0}",
"ko_KR": "플레이 타임: {0}",
"no_NO": "Spilletid: {0}",
"pl_PL": "Czas w grze: {0}",
"pt_BR": "Tempo de jogo: {0}",
"ru_RU": "Время в игре: {0}",
"sv_SE": "Speltid: {0}",
"th_TH": "เล่นไปแล้ว: {0}",
"tr_TR": "Oynama Süresi: {0}",
"uk_UA": "Зіграно часу: {0}",
"zh_CN": "游玩时长: {0}",
"zh_TW": "遊玩時數: {0}"
"it_IT": "Tempo di gioco:",
"ja_JP": "プレイ時間:",
"ko_KR": "플레이 타임:",
"no_NO": "Spilletid:",
"pl_PL": "Czas w grze:",
"pt_BR": "Tempo de jogo:",
"ru_RU": "Время в игре:",
"sv_SE": "Speltid:",
"th_TH": "เล่นไปแล้ว:",
"tr_TR": "Oynama Süresi:",
"uk_UA": "Зіграно часу:",
"zh_CN": "游玩时长:",
"zh_TW": "遊玩時數:"
}
},
{
"ID": "GameListHeaderLastPlayed",
"Translations": {
"ar_SA": "",
"de_DE": "Zuletzt gespielt: {0}",
"el_GR": "Παίχτηκε: {0}",
"en_US": "Last Played: {0}",
"es_ES": "Jugado por última vez: {0}",
"fr_FR": "Dernière partie jouée: {0}",
"de_DE": "Zuletzt gespielt: ",
"el_GR": "Παίχτηκε: ",
"en_US": "Last Played:",
"es_ES": "Jugado por última vez:",
"fr_FR": "Dernière partie jouée:",
"he_IL": "",
"it_IT": "Ultima partita: {0}",
"ja_JP": "最終プレイ日時: {0}",
"ko_KR": "마지막 플레이: {0}",
"no_NO": "Sist Spilt: {0}",
"pl_PL": "Ostatnio grane: {0}",
"pt_BR": "Último jogo: {0}",
"ru_RU": "Последний запуск: {0}",
"sv_SE": "Senast spelad: {0}",
"th_TH": "เล่นล่าสุด: {0}",
"tr_TR": "Son Oynama Tarihi: {0}",
"uk_UA": "Востаннє зіграно: {0}",
"zh_CN": "最近游玩: {0}",
"zh_TW": "最近遊玩: {0}"
"it_IT": "Ultima partita:",
"ja_JP": "最終プレイ日時:",
"ko_KR": "마지막 플레이:",
"no_NO": "Sist Spilt:",
"pl_PL": "Ostatnio grane:",
"pt_BR": "Último jogo:",
"ru_RU": "Последний запуск:",
"sv_SE": "Senast spelad:",
"th_TH": "เล่นล่าสุด:",
"tr_TR": "Son Oynama Tarihi:",
"uk_UA": "Востаннє зіграно:",
"zh_CN": "最近游玩:",
"zh_TW": "最近遊玩:"
}
},
{
@@ -2259,7 +2259,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "PPTC 캐시 제거",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -2284,7 +2284,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "앱의 모든 PPTC 캐시 파일 삭제",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -2609,7 +2609,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "선택한 DLC 파일에서 RomFS 추출",
"no_NO": "Pakk ut RomFS filene fra valgt DLC fil",
"pl_PL": "",
"pt_BR": "",
@@ -2759,7 +2759,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "호환성 항목 표시",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -2784,7 +2784,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "일반적으로 도움말 메뉴를 통해 접근할 수 있는 호환성 목록에 선택한 게임을 표시합니다.",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -2809,7 +2809,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "게임 통계 표시",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -2834,7 +2834,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "그리드 보기 레이아웃에서 누락된 현재 선택된 게임에 대한 다양한 정보를 표시합니다.",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -3609,7 +3609,7 @@
"he_IL": "",
"it_IT": "Aggiornamenti e DLC che fanno riferimento a file mancanti verranno disabilitati automaticamente",
"ja_JP": "",
"ko_KR": "누락된 파일을 참조하는 DLC 및 업데이트가 자동으로 언로드",
"ko_KR": "누락된 파일을 참조하는 DLC 및 업데이트가 자동으로 불러오기 취소",
"no_NO": "DLC og oppdateringer som henviser til manglende filer, vil bli lastet ned automatisk",
"pl_PL": "",
"pt_BR": "DLCs e Atualizações que se referem a arquivos ausentes serão descarregadas automaticamente",
@@ -4384,7 +4384,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "스웨덴어",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -4409,7 +4409,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "노르웨이어",
"no_NO": "Norsk",
"pl_PL": "",
"pt_BR": "",
@@ -4484,7 +4484,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "매치 시스템 시간",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -6122,6 +6122,56 @@
"zh_TW": "關閉"
}
},
{
"ID": "SettingsButtonReset",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Reset Settings",
"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": "SettingsButtonResetConfirm",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "I want to reset my settings.",
"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": "SettingsButtonOk",
"Translations": {
@@ -8034,7 +8084,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "비활성화",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -8059,7 +8109,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "레인보우",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -8084,7 +8134,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "레인보우 속도",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -8109,7 +8159,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "색상",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -13359,7 +13409,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "다음에서 모든 PPTC 데이터를 제거하려고 합니다:\n\n{0}\n\n계속하시겠습니까?",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -19359,7 +19409,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "LED 설정",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -22284,7 +22334,7 @@
"he_IL": "ממשק רשת",
"it_IT": "Interfaccia di rete:",
"ja_JP": "ネットワークインタフェース:",
"ko_KR": "네트워크 인터페이스:",
"ko_KR": "네트워크 인터페이스 :",
"no_NO": "Nettverksgrensesnitt",
"pl_PL": "Interfejs sieci:",
"pt_BR": "Interface de rede:",
@@ -23259,7 +23309,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "최종 업데이트 : {0}",
"no_NO": "Sist oppdatert: {0}",
"pl_PL": "",
"pt_BR": "",
@@ -23634,7 +23684,7 @@
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"ko_KR": "추출할 DLC 선택",
"no_NO": "Velg en DLC og hente ut",
"pl_PL": "",
"pt_BR": "",
@@ -23646,6 +23696,56 @@
"zh_CN": "选择一个要解压的 DLC",
"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": ""
}
}
]
}

View File

@@ -4,15 +4,13 @@ using MsgPack;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Ava.Utilities.PlayReport;
using Ryujinx.Common;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.HLE;
using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Horizon;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Ryujinx.Horizon.Prepo.Types;
using System.Linq;
using System.Text;
@@ -41,6 +39,9 @@ namespace Ryujinx.Ava
private static RichPresence _discordPresencePlaying;
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()
{
_discordPresenceMain = new RichPresence
@@ -124,20 +125,22 @@ namespace Ryujinx.Ava
_currentApp = null;
}
private static void HandlePlayReport(MessagePackObject playReport)
private static void HandlePlayReport(PlayReport playReport)
{
if (_discordClient is null) return;
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
if (_discordPresencePlaying is null) return;
PlayReportAnalyzer.FormattedValue formattedValue =
PlayReport.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
FormattedValue formattedValue =
PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
if (!formattedValue.Handled) return;
_discordPresencePlaying.Details = formattedValue.Reset
? $"Playing {_currentApp.Title}"
: formattedValue.FormattedString;
_discordPresencePlaying.Details = TruncateToByteLength(
formattedValue.Reset
? $"Playing {_currentApp.Title}"
: formattedValue.FormattedString
);
if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
return; //don't trigger an update if the set presence Details are identical to current

View File

@@ -4,6 +4,7 @@
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView"
@@ -85,6 +86,49 @@
</StackPanel>
</Grid>
<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
HorizontalAlignment="Stretch"
IsVisible="{Binding AppData.HasLdnGames}"
@@ -92,22 +136,35 @@
TextAlignment="Start"
TextWrapping="Wrap" />
<Separator IsVisible="{Binding AppData.HasLdnGames}" Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
<StackPanel
HorizontalAlignment="Left"
VerticalAlignment="Top"
Orientation="Vertical"
Spacing="5">
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding FormattedLastPlayed}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding FormattedPlayTime}"
IsVisible="{Binding AppData.HasPlayedPreviously}"
TextAlignment="Start"
TextWrapping="Wrap" />
<StackPanel Orientation="Vertical" Spacing="5">
<Grid
ColumnDefinitions="Auto,*,Auto">
<TextBlock
Grid.Column="0"
Text="{ext:Locale GameListHeaderLastPlayed}"
VerticalAlignment="Top"
TextAlignment="Start"
TextWrapping="NoWrap" />
<TextBlock
Grid.Column="2"
Text="{Binding AppData.LastPlayedString}"
TextAlignment="End"
TextWrapping="Wrap" />
</Grid>
<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>

View File

@@ -18,7 +18,7 @@ namespace Ryujinx.Ava.UI.Helpers
LocaleKeys.CompatibilityListNothing or
LocaleKeys.CompatibilityListBoots or
LocaleKeys.CompatibilityListMenus => Brushes.Red,
LocaleKeys.CompatibilityListIngame => Brushes.Yellow,
LocaleKeys.CompatibilityListIngame => Brushes.DarkOrange,
_ => Brushes.ForestGreen
};

View File

@@ -12,10 +12,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version);
public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer);
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 FormattedLdnInfo =>

View File

@@ -2,7 +2,7 @@ using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using CommunityToolkit.Mvvm.Input;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL2;
@@ -28,8 +28,6 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
@@ -722,6 +720,25 @@ namespace Ryujinx.Ava.UI.ViewModels
CloseWindow?.Invoke();
}
[ObservableProperty] private bool _wantsToReset;
public AsyncRelayCommand ResetButton => Commands.Create(async () =>
{
if (!WantsToReset) return;
CloseWindow?.Invoke();
ConfigurationState.Instance.LoadDefault();
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
RyujinxApp.MainWindow.LoadApplications();
await ContentDialogHelper.CreateInfoDialog(
$"Your {RyujinxApp.FullAppName} configuration has been reset.",
"",
string.Empty,
LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
"Configuration Reset");
});
public void CancelButton()
{
RevertIfNotSaved();

View File

@@ -108,24 +108,36 @@
</Style>
</ui:NavigationView.Styles>
</ui:NavigationView>
<ReversibleStackPanel
Grid.Row="2"
Margin="10"
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right"
ReverseOrder="{x:Static helper:RunningPlatform.IsMacOS}">
<Button
Classes="accent"
Content="{ext:Locale SettingsButtonOk}"
Command="{Binding OkButton}" />
<Button
HotKey="Escape"
Content="{ext:Locale SettingsButtonCancel}"
Command="{Binding CancelButton}" />
<Button
Content="{ext:Locale SettingsButtonApply}"
Command="{Binding ApplyButton}" />
</ReversibleStackPanel>
<Grid Grid.Row="2"
ColumnDefinitions="Auto,*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Button
IsEnabled="{Binding WantsToReset}"
Margin="10"
Content="{ext:Locale SettingsButtonReset}"
Command="{Binding ResetButton}" />
<CheckBox IsChecked="{Binding WantsToReset}"/>
<TextBlock Text="{ext:Locale SettingsButtonResetConfirm}"/>
</StackPanel>
<ReversibleStackPanel
Grid.Column="2"
Margin="10"
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right"
ReverseOrder="{x:Static helper:RunningPlatform.IsMacOS}">
<Button
Classes="accent"
Content="{ext:Locale SettingsButtonOk}"
Command="{Binding OkButton}" />
<Button
HotKey="Escape"
Content="{ext:Locale SettingsButtonCancel}"
Command="{Binding CancelButton}" />
<Button
Content="{ext:Locale SettingsButtonApply}"
Command="{Binding ApplyButton}" />
</ReversibleStackPanel>
</Grid>
</Grid>
</window:StyleableAppWindow>

View File

@@ -63,6 +63,9 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public int GameCount { get; set; }
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 DateTime? LastPlayed { get; set; }

View File

@@ -1,85 +0,0 @@
using PlayReportFormattedValue = Ryujinx.Ava.Utilities.PlayReportAnalyzer.FormattedValue;
namespace Ryujinx.Ava.Utilities
{
public static class PlayReport
{
public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer()
.AddSpec(
"01007ef00011e000",
spec => spec
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
// reset to normal status when switching between normal & master mode in title screen
.AddValueFormatter("AoCVer", PlayReportFormattedValue.AlwaysResets)
)
.AddSpec(
"0100f2c0115b6000",
spec => spec.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
.AddSpec(
"0100000000010000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
)
.AddSpec(
"010075000ecbe000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
)
.AddSpec(
"010028600ebda000",
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
)
.AddSpec( // Global & China IDs
["0100152000022000", "010075100e8ec000"],
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
);
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
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";
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(PlayReportValue value)
=> value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(PlayReportValue value)
=> value.StringValue switch
{
// Single Player
"Single" => "Single Player",
// Multiplayer
"Multi-2players" => "Multiplayer 2 Players",
"Multi-3players" => "Multiplayer 3 Players",
"Multi-4players" => "Multiplayer 4 Players",
// Wireless/LAN Play
"Local-Single" => "Wireless/LAN Play",
"Local-2players" => "Wireless/LAN Play 2 Players",
// CC Classes
"50cc" => "50cc",
"100cc" => "100cc",
"150cc" => "150cc",
"Mirror" => "Mirror (150cc)",
"200cc" => "200cc",
// Modes
"GrandPrix" => "Grand Prix",
"TimeAttack" => "Time Trials",
"VS" => "VS Races",
"Battle" => "Battle Mode",
"RaceStart" => "Selecting a Course",
"Race" => "Racing",
_ => PlayReportFormattedValue.ForceReset
};
}
}

View File

@@ -0,0 +1,161 @@
using Gommon;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport
{
/// <summary>
/// The entrypoint for the Play Report analysis system.
/// </summary>
public class Analyzer
{
private readonly List<GameSpec> _specs = [];
public string[] TitleIds => Specs.SelectMany(x => x.TitleIds).ToArray();
public IReadOnlyList<GameSpec> Specs => new ReadOnlyCollection<GameSpec>(_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="Analyzer"/>, for chaining convenience.</returns>
public Analyzer AddSpec(string titleId, Func<GameSpec, GameSpec> transform)
{
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(transform(new GameSpec { 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="Analyzer"/>, for chaining convenience.</returns>
public Analyzer AddSpec(string titleId, Action<GameSpec> transform)
{
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(new GameSpec { 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="Analyzer"/>, for chaining convenience.</returns>
public Analyzer AddSpec(IEnumerable<string> titleIds,
Func<GameSpec, GameSpec> 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(GameSpec)}.");
_specs.Add(transform(new GameSpec { 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="Analyzer"/>, for chaining convenience.</returns>
public Analyzer AddSpec(IEnumerable<string> titleIds, Action<GameSpec> 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(GameSpec)}.");
_specs.Add(new GameSpec { TitleIds = [..tids] }.Apply(transform));
return this;
}
/// <summary>
/// Runs the configured <see cref="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,
Horizon.Prepo.Types.PlayReport playReport
)
{
if (!playReport.ReportData.IsDictionary)
return FormattedValue.Unhandled;
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
return FormattedValue.Unhandled;
foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
{
if (!playReport.ReportData.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
continue;
return formatSpec.Formatter(new SingleValue(valuePackObject)
{
Application = appMeta,
PlayReport = playReport
});
}
foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority))
{
List<MessagePackObject> packedObjects = [];
foreach (var reportKey in formatSpec.ReportKeys)
{
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
continue;
packedObjects.Add(valuePackObject);
}
if (packedObjects.Count != formatSpec.ReportKeys.Length)
return FormattedValue.Unhandled;
return formatSpec.Formatter(new MultiValue(packedObjects)
{
Application = appMeta,
PlayReport = playReport
});
}
foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority))
{
Dictionary<string, MessagePackObject> packedObjects = [];
foreach (var reportKey in formatSpec.ReportKeys)
{
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
continue;
packedObjects.Add(reportKey, valuePackObject);
}
return formatSpec.Formatter(
new SparseMultiValue(packedObjects)
{
Application = appMeta,
PlayReport = playReport
});
}
return FormattedValue.Unhandled;
}
}
}

View File

@@ -0,0 +1,40 @@
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(SingleValue 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(MultiValue 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(SparseMultiValue value);
}

View File

@@ -0,0 +1,87 @@
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport
{
public abstract class MatchedValue<T>
{
public MatchedValue(T matched)
{
Matched = matched;
}
/// <summary>
/// The currently running application's <see cref="ApplicationMetadata"/>.
/// </summary>
public ApplicationMetadata Application { get; init; }
/// <summary>
/// The entire play report.
/// </summary>
public Horizon.Prepo.Types.PlayReport PlayReport { get; init; }
/// <summary>
/// The matched value from the Play Report.
/// </summary>
public T Matched { 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 SingleValue : MatchedValue<Value>
{
public SingleValue(Value matched) : base(matched)
{
}
public static implicit operator SingleValue(MessagePackObject mpo) => new(mpo);
}
/// <summary>
/// The input data to a <see cref="MultiValueFormatter"/>,
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// and the matched <see cref="MessagePackObject"/>s from the Play Report.
/// </summary>
public class MultiValue : MatchedValue<Value[]>
{
public MultiValue(Value[] matched) : base(matched)
{
}
public MultiValue(IEnumerable<MessagePackObject> matched) : base(Value.ConvertPackedObjects(matched))
{
}
public static implicit operator MultiValue(List<MessagePackObject> matched)
=> new(matched.Select(x => new Value(x)).ToArray());
}
/// <summary>
/// The input data to a <see cref="SparseMultiValueFormatter"/>,
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// and the matched <see cref="MessagePackObject"/>s from the Play Report.
/// </summary>
public class SparseMultiValue : MatchedValue<Dictionary<string, Value>>
{
public SparseMultiValue(Dictionary<string, Value> matched) : base(matched)
{
}
public SparseMultiValue(Dictionary<string, MessagePackObject> matched) : base(Value.ConvertPackedObjectMap(matched))
{
}
public static implicit operator SparseMultiValue(Dictionary<string, MessagePackObject> matched)
=> new(matched
.ToDictionary(
x => x.Key,
x => new Value(x.Value)
)
);
}
}

View File

@@ -0,0 +1,127 @@
namespace Ryujinx.Ava.Utilities.PlayReport
{
public static class PlayReports
{
public static Analyzer Analyzer { get; } = new Analyzer()
.AddSpec(
"01007ef00011e000",
spec => spec
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
// reset to normal status when switching between normal & master mode in title screen
.AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets)
)
.AddSpec(
"0100f2c0115b6000",
spec => spec
.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
.AddSpec(
"0100000000010000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
)
.AddSpec(
"010075000ecbe000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
)
.AddSpec(
"010028600ebda000",
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
)
.AddSpec( // Global & China IDs
["0100152000022000", "010075100e8ec000"],
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
)
.AddSpec(
["0100a3d008c5c000", "01008f6008c5e000"],
spec => spec
.AddValueFormatter("area_no", PokemonSVArea)
.AddValueFormatter("team_circle", PokemonSVUnionCircle)
);
private static FormattedValue BreathOfTheWild_MasterMode(SingleValue value)
=> value.Matched.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset;
private static FormattedValue TearsOfTheKingdom_CurrentField(SingleValue value) =>
value.Matched.DoubleValue switch
{
> 800d => "Exploring the Sky Islands",
< -201d => "Exploring the Depths",
_ => "Roaming Hyrule"
};
private static FormattedValue SuperMarioOdyssey_AssistMode(SingleValue value)
=> value.Matched.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
private static FormattedValue SuperMarioOdysseyChina_AssistMode(SingleValue value)
=> value.Matched.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
private static FormattedValue SuperMario3DWorldOrBowsersFury(SingleValue value)
=> value.Matched.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
private static FormattedValue MarioKart8Deluxe_Mode(SingleValue value)
=> value.Matched.StringValue switch
{
// Single Player
"Single" => "Single Player",
// Multiplayer
"Multi-2players" => "Multiplayer 2 Players",
"Multi-3players" => "Multiplayer 3 Players",
"Multi-4players" => "Multiplayer 4 Players",
// Wireless/LAN Play
"Local-Single" => "Wireless/LAN Play",
"Local-2players" => "Wireless/LAN Play 2 Players",
// CC Classes
"50cc" => "50cc",
"100cc" => "100cc",
"150cc" => "150cc",
"Mirror" => "Mirror (150cc)",
"200cc" => "200cc",
// Modes
"GrandPrix" => "Grand Prix",
"TimeAttack" => "Time Trials",
"VS" => "VS Races",
"Battle" => "Battle Mode",
"RaceStart" => "Selecting a Course",
"Race" => "Racing",
_ => FormattedValue.ForceReset
};
private static FormattedValue PokemonSVUnionCircle(SingleValue value)
=> value.Matched.BoxedValue is 0 ? "Playing Alone" : "Playing in a group";
private static FormattedValue PokemonSVArea(SingleValue value)
=> value.Matched.StringValue switch
{
// Base Game Locations
"a_w01" => "South Area One",
"a_w02" => "Mesagoza",
"a_w03" => "The Pokemon League",
"a_w04" => "South Area Two",
"a_w05" => "South Area Four",
"a_w06" => "South Area Six",
"a_w07" => "South Area Five",
"a_w08" => "South Area Three",
"a_w09" => "West Area One",
"a_w10" => "Asado Desert",
"a_w11" => "West Area Two",
"a_w12" => "Medali",
"a_w13" => "Tagtree Thicket",
"a_w14" => "East Area Three",
"a_w15" => "Artazon",
"a_w16" => "East Area Two",
"a_w18" => "Casseroya Lake",
"a_w19" => "Glaseado Mountain",
"a_w20" => "North Area Three",
"a_w21" => "North Area One",
"a_w22" => "North Area Two",
"a_w23" => "The Great Crater of Paldea",
"a_w24" => "South Paldean Sea",
"a_w25" => "West Paldean Sea",
"a_w26" => "East Paldean Sea",
"a_w27" => "Nouth Paldean Sea",
//TODO DLC Locations
_ => FormattedValue.ForceReset
};
}
}

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

View File

@@ -0,0 +1,143 @@
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using System;
using System.Collections.Generic;
using System.Linq;
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 readonly struct Value
{
public Value(MessagePackObject packedValue)
{
PackedValue = packedValue;
}
/// <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();
}
public static implicit operator Value(MessagePackObject matched) => new(matched);
public static Value[] ConvertPackedObjects(IEnumerable<MessagePackObject> packObjects)
=> packObjects.Select(packObject => new Value(packObject)).ToArray();
public static Dictionary<string, Value> ConvertPackedObjectMap(Dictionary<string, MessagePackObject> packObjects)
=> packObjects.ToDictionary(
x => x.Key,
x => new Value(x.Value)
);
#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;
}
}

View File

@@ -1,282 +0,0 @@
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);
}