From be5881f100305af7feeda7f2e45576636b86d703 Mon Sep 17 00:00:00 2001 From: awesomeangotti Date: Thu, 25 Jun 2026 20:59:56 +0000 Subject: [PATCH] Discord Rich Presence: New Super Mario Bros U Deluxe (#130) Add RPC support for NSMBUD play reports that generate on main menu and after finishing a course. Tracked things: Main menu Last played course Examples: Main menu ![image](/attachments/9f1506bd-fc8c-4eca-930d-64f15a7c650d) After finishing a course (By dying or by beating it) ![image](/attachments/a4af3f4f-230d-47ac-977a-20281a103cb6) In the future should I be doing batch PRs for RPC related things? Yes. Co-authored-by: Awesomeangotti <143439211+Awesomeangotti@users.noreply.github.com> Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/130 --- src/Ryujinx/Assets/RPCData/nsmbud.json | 216 ++++++++++++++++++ src/Ryujinx/Ryujinx.csproj | 2 + .../PlayReport/PlayReports.Formatters.cs | 84 +++++++ src/Ryujinx/Systems/PlayReport/PlayReports.cs | 6 + 4 files changed, 308 insertions(+) create mode 100644 src/Ryujinx/Assets/RPCData/nsmbud.json diff --git a/src/Ryujinx/Assets/RPCData/nsmbud.json b/src/Ryujinx/Assets/RPCData/nsmbud.json new file mode 100644 index 000000000..847dd3c81 --- /dev/null +++ b/src/Ryujinx/Assets/RPCData/nsmbud.json @@ -0,0 +1,216 @@ +{ + "mario": { + "1": { + "1": "Acorn Plains Way", + "2": "Tilted Tunnel", + "21": "Crushing-Cogs Tower", + "3": "Yoshi Hill", + "4": "Mushroom Heights", + "5": "Rise of the Piranha Plants", + "23": "Lemmy's Swingback Castle", + "13": "Blooper's Secret Lair" + }, + "2": { + "1": "Stone-Eye Zone", + "2": "Perilous Pokey Cave", + "3": "Fire Snake Cavern", + "21": "Stoneslide Tower", + "4": "Spike's Spouting Sands", + "5": "Dry Desert Mushrooms", + "6": "Blooming Lakitus", + "23": "Morton's Compactor Castle", + "14": "Piranha Plants on Ice" + }, + "3": { + "1": "Waterspout Beach", + "2": "Tropical Refresher", + "21": "Giant Skewer Tower", + "20": "Haunted Shipwreck", + "3": "Above the Cheep Cheep Seas", + "4": "Urchin Shoals", + "5": "Dragoneel's Undersea Grotto", + "23": "Larry's Torpedo Castle", + "15": "Skyward Stalk" + }, + "4": { + "1": "Spinning-Star Sky", + "2": "Cooligan Fields", + "21": "Freezing-Rain Tower", + "3": "Prickly Goombas!", + "4": "Scaling the Mountainside", + "5": "Icicle Caverns", + "20": "Swaying Ghost House", + "23": "Wendy's Shifting Castle", + "16": "Fliprus Lake" + }, + "5": { + "37": "The Mighty Cannonship", + "1": "Jungle of the Giants", + "2": "Bridge over Poisoned Waters", + "3": "Bramball Woods", + "21": "Snake Block Tower", + "20": "Which-Way Labyrinth", + "4": "Painted Swampland", + "5": "Deepsea Ruins", + "6": "Seesaw Bridge", + "7": "Wiggler Stampede", + "23": "Iggy's Volcanic Castle", + "17": "Flight of the Para-Beetles" + }, + "6": { + "1": "Fuzzy Clifftop", + "2": "Porcupuffer Falls", + "21": "Grinding-Stone Tower", + "3": "Wadlewing's Nest", + "4": "Light Blocks, Dark Tower", + "5": "Walking Piranha Plants!", + "6": "Thrilling Spine Coaster", + "22": "Screwtop Tower", + "7": "Shifting-Floor Cave", + "23": "Roy's Conveyor Castle" + }, + "7": { + "1": "Land of Flying Blocks", + "2": "Seesaw Shrooms", + "3": "Switchback Hill", + "21": "Slide Lift Tower", + "20": "Spinning Spirit House", + "4": "Bouncy Cloud Boomerangs", + "5": "A Quick Dip in the Sky", + "6": "Snaking above Mist Valley", + "23": "Ludwig's Clockwork Castle", + "37": "Boarding the Airship" + + }, + "8": { + "1": "Meteor Moat", + "2": "Magma-River Cruise", + "3": "Rising Tides of Lava", + "4": "firefall Cliffs", + "42": "Red-Hot Elevator Ride", + "43": "The Final Battle" + }, + "9": { + "1": "Spine-Tingling Spine Coaster", + "2": "Run for It", + "3": "Swim for Your Life!", + "4": "Hammerswing Caverns", + "5": "Spinning Platforms of Doom", + "6": "Fire Bar Cliffs", + "7": "Lakitu! Lakitu! Lakitu!", + "8": "Pendulum Castle", + "9": "Follow That Shell!" + }, + "11": { + "1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "6": "", + "7": "", + "8": "" + } + }, + "luigi": { + "1": { + "1": "Waddlewing Warning!", + "2": "Crooked Cavern", + "21": "Flame-Gear Tower", + "3": "Rolling Yoshi Hills", + "4": "Piranha Heights", + "5": "Piranha Gardens", + "23": "Lemmy's Lights-Out Castle", + "13": "Cheep Chomp Chase" + }, + "2": { + "1": "Spike's Tumbling Desert", + "2": "Underground Grrrols", + "3": "Piranhas in the Dark", + "21": "Wind-Up Tower", + "4": "The Walls Have Eyes", + "5": "Stone Spike Conveyors", + "6": "Spinning Sandstones", + "23": "Morton's Lava-Block Castle", + "14": "Slippery Rope Ladders" + }, + "3": { + "1": "Huckit Beach Resort", + "2": "Urchin Reef Romp", + "21": "Shish-Kebab Tower", + "20": "Haunted Cargo Hold", + "3": "Waterspout Sprint", + "4": "The Great Geysers", + "5": "Dragoneel Depths", + "23": "Larry's Trigger-Happy Castle", + "15": "Beanstalk Jungle" + }, + "4": { + "1": "Broozers and Barrels", + "2": "Cooligan Shrooms", + "21": "Icicle Tower", + "3": "Fire and Ice", + "4": "Weighty Waddlewings", + "5": "Ice-Slide Expressway", + "20": "Peek-a-Boo Ghost House", + "23": "Wendy's Thwomp Castle", + "16": "Fliprus Floes" + }, + "5": { + "1": "Giant Swing-Along", + "2": "Dancing Blocks, Poison Swamp", + "3": "Heart of Bramball Woods", + "21": "Stone-Snake Tower", + "20": "Boo's Favorite Haunt", + "4": "Painted Pipeworks", + "5": "Deepsea Stone-Eyes", + "6": "Sumo Bro Bridge", + "7": "Wiggler Floodlands", + "23": "Iggy's Swinging-Chains Castle", + "17": "Para-Beetle Parade" + }, + "6": { + "1": "Mount Fuzzy", + "2": "Porcupuffer Cavern", + "21": "Smashing-Stone Tower", + "3": "Spike's Seesaws", + "4": "Light-Up-Lift Tower", + "5": "Rising Piranhas", + "6": "Spine Coaster Stowaways", + "22": "Sumo Bro's Spinning Tower", + "7": "Switch-Lift Express", + "23": "Roy's Ironclad Castle" + }, + "7": { + "1": "Frozen Fuzzies", + "2": "Wiggler Rodeo", + "3": "Rainbow Skywalk", + "21": "Stonecrush Tower", + "20": "Vanishing Ghost House", + "4": "Above The Bouncy Clouds", + "5": "Flame Chomp Ferris Wheel", + "6": "Three-Headed Snake Block", + "23": "Ludwig's Block-Press Castle", + "37": "Bowser Jr. Showdown" + }, + "8": { + "1": "Magma Moat", + "2": "Magmaw River Cruise", + "3": "Hot Cogs", + "4": "Firefall Rising", + "42": "Current Event", + "43": "The Final Battle" + }, + "9": { + "1": "Spine Coaster Connections", + "2": "P Switch Peril", + "3": "Star Coin Deep Dive", + "4": "Hammerswing Hideout", + "5": "Under Construction", + "6": "Fire Bar Sprint", + "7": "Cloudy Capers", + "8": "Impossible Pendulums", + "9": "Flying Squirrel Ovation" + } + } +} diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index b2c9480d2..429d96f80 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -141,6 +141,7 @@ + @@ -175,6 +176,7 @@ + diff --git a/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs b/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs index 75cc97cd6..573296be5 100644 --- a/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs +++ b/src/Ryujinx/Systems/PlayReport/PlayReports.Formatters.cs @@ -1,10 +1,12 @@ using Gommon; using Humanizer; using MsgPack; +using Ryujinx.Common; using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Linq; +using System.Text.Json; namespace Ryujinx.Ava.Systems.PlayReport { @@ -1116,5 +1118,87 @@ namespace Ryujinx.Ava.Systems.PlayReport _ => "Wandering" }; } + + private static FormattedValue NsmbudRpc(SparseMultiValue values) + { + if (values.Matched.TryGetValue("WorldNo", out Value world) && values.Matched.TryGetValue("CourseNo", out Value course) | values.Matched.TryGetValue("GameModeType", out Value gamemode)) + { + string worldstr = world.ToString(); + string coursestr = course.ToString(); + int courseint = Int32.Parse(coursestr); + string gamemodestr = gamemode.ToString(); + + try + { + Dictionary>> output; + string data; + data = EmbeddedResources.ReadAllText("Ryujinx/Assets/RPCData/nsmbud.json"); + output = JsonSerializer.Deserialize>>>(data); + if (SpecialMapNames(courseint) == "Hazard") + { + return $"Last Played: Course {worldstr}-Hazard"; + } + string outputloc = output[MarioOrLuigiGamemode(gamemodestr)][worldstr][coursestr]; + return $"Last Played: Course {worldstr}-{SpecialMapNames(courseint)} | {outputloc}"; + } + catch + { + return FormattedValue.ForceReset; + } + } + + if (values.Matched.TryGetValue("RlId", out Value RlId) | values.Matched.TryGetValue("TotalPlayTime", out Value TotalPlayTime)) + { + return "At the main menu"; + } + + static string MarioOrLuigiGamemode(string? gamemode) => gamemode switch + { + "0" => "mario", + "1" => "luigi", + "4" => "mario", + "5" => "mario", + _ => gamemode + }; + + static string OtherGameMode(string? gamemode) => gamemode switch + { + "2" => "Boost Rush", + "3" => "Challenges", + "4" => "Coin Battle", + "5" => "Coin Battle Editor", + _ => "" + }; + + static string SpecialMapNames(int? course) => course switch + { + >= 1 and <= 9 => course.ToString(), + 13 => "Shortcut", + 14 => "Shortcut", + 15 => "Shortcut", + 16 => "Shortcut", + 17 => "Shortcut", + 20 => "Ghost", + 21 => "Tower", + 22 => "Tower", + 23 => "Castle", + 37 => "Airship", + 42 => "Castle", + 43 => "Castle", + _ => "Hazard" + }; + + // For future reference + // Tower course = 21, Castle course = 23,Haunted Mansion/ship = 20 + // Tower course 2 (rock candy) = 22 + // Peach castle 1 = 42, Peach final battle = 43 + // airship = 37, jungle beetles = 17 + // Glacier seals = 16, water leaf = 15 + // desert ice = 14, acorn squid = 13 + // all other course numbers are to be considered a hazard + + return ""; + + } } } diff --git a/src/Ryujinx/Systems/PlayReport/PlayReports.cs b/src/Ryujinx/Systems/PlayReport/PlayReports.cs index 16ff80933..df8779254 100644 --- a/src/Ryujinx/Systems/PlayReport/PlayReports.cs +++ b/src/Ryujinx/Systems/PlayReport/PlayReports.cs @@ -138,6 +138,12 @@ namespace Ryujinx.Ava.Systems.PlayReport .WithDescription("based on gold count, report info only in the mii selector, and gamestage (progression)") .AddSparseMultiValueFormatter(["gold", "secret", "stage"], MiitopiaRPC) ) + .AddSpec( + "0100ea80032ea000", // New Super Mario Bros U Deluxe + spec => spec + .WithDescription("based on world map return info.") + .AddSparseMultiValueFormatter(["WorldNo", "CourseNo", "RlId", "TotalPlayTime", "GameModeType"], NsmbudRpc) + ) ); private static string Playing(string game) => $"Playing {game}";