diff --git a/Ryujinx.HLE/OsHle/Horizon.cs b/Ryujinx.HLE/OsHle/Horizon.cs index a5bf0616c6..ef60f07deb 100644 --- a/Ryujinx.HLE/OsHle/Horizon.cs +++ b/Ryujinx.HLE/OsHle/Horizon.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.Loaders.Executables; +using Ryujinx.HLE.Loaders.Npdm; using Ryujinx.HLE.Logging; using Ryujinx.HLE.OsHle.Handles; using System; @@ -76,6 +77,20 @@ namespace Ryujinx.HLE.OsHle } } + void LoadNpdm(string FileName) + { + string File = Directory.GetFiles(ExeFsDir, FileName)[0]; + + Ns.Log.PrintInfo(LogClass.Loader, $"Loading NPDM..."); + + using (FileStream Input = new FileStream(File, FileMode.Open)) + { + SystemState.TitleMetadata = new Npdm(Input); + } + } + + LoadNpdm("*.npdm"); + LoadNso("rtld"); MainProcess.SetEmptyArgs(); diff --git a/Ryujinx.HLE/OsHle/SystemStateMgr.cs b/Ryujinx.HLE/OsHle/SystemStateMgr.cs index e78082c45a..14c0137358 100644 --- a/Ryujinx.HLE/OsHle/SystemStateMgr.cs +++ b/Ryujinx.HLE/OsHle/SystemStateMgr.cs @@ -1,3 +1,4 @@ +using Ryujinx.HLE.Loaders.Npdm; using System; namespace Ryujinx.HLE.OsHle @@ -36,6 +37,23 @@ namespace Ryujinx.HLE.OsHle internal string ActiveAudioOutput { get; private set; } + internal Npdm TitleMetadata { get; set; } + + public string GetNpdmTitleName() + { + return TitleMetadata.TitleName; + } + + public string GetNpdmTitleId() + { + return TitleMetadata.ACI0.TitleId; + } + + public bool GetNpdmIs64Bit() + { + return TitleMetadata.Is64Bits; + } + public SystemStateMgr() { SetLanguage(SystemLanguage.AmericanEnglish); diff --git a/Ryujinx/Discord/DiscordRpc.cs b/Ryujinx/Discord/DiscordRpc.cs new file mode 100644 index 0000000000..c17712a7df --- /dev/null +++ b/Ryujinx/Discord/DiscordRpc.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +// https://raw.githubusercontent.com/discordapp/discord-rpc/master/examples/button-clicker/Assets/DiscordRpc.cs +public class DiscordRpc +{ + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void ReadyCallback(ref DiscordUser connectedUser); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void DisconnectedCallback(int errorCode, string message); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void ErrorCallback(int errorCode, string message); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void JoinCallback(string secret); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SpectateCallback(string secret); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void RequestCallback(ref DiscordUser request); + + public struct EventHandlers + { + public ReadyCallback readyCallback; + public DisconnectedCallback disconnectedCallback; + public ErrorCallback errorCallback; + public JoinCallback joinCallback; + public SpectateCallback spectateCallback; + public RequestCallback requestCallback; + } + + [Serializable, StructLayout(LayoutKind.Sequential)] + public struct RichPresenceStruct + { + public IntPtr state; /* max 128 bytes */ + public IntPtr details; /* max 128 bytes */ + public long startTimestamp; + public long endTimestamp; + public IntPtr largeImageKey; /* max 32 bytes */ + public IntPtr largeImageText; /* max 128 bytes */ + public IntPtr smallImageKey; /* max 32 bytes */ + public IntPtr smallImageText; /* max 128 bytes */ + public IntPtr partyId; /* max 128 bytes */ + public int partySize; + public int partyMax; + public IntPtr matchSecret; /* max 128 bytes */ + public IntPtr joinSecret; /* max 128 bytes */ + public IntPtr spectateSecret; /* max 128 bytes */ + public bool instance; + } + + [Serializable] + public struct DiscordUser + { + public string userId; + public string username; + public string discriminator; + public string avatar; + } + + public enum Reply + { + No = 0, + Yes = 1, + Ignore = 2 + } + + [DllImport("discord-rpc", EntryPoint = "Discord_Initialize", CallingConvention = CallingConvention.Cdecl)] + public static extern void Initialize(string applicationId, ref EventHandlers handlers, bool autoRegister, string optionalSteamId); + + [DllImport("discord-rpc", EntryPoint = "Discord_Shutdown", CallingConvention = CallingConvention.Cdecl)] + public static extern void Shutdown(); + + [DllImport("discord-rpc", EntryPoint = "Discord_RunCallbacks", CallingConvention = CallingConvention.Cdecl)] + public static extern void RunCallbacks(); + + [DllImport("discord-rpc", EntryPoint = "Discord_UpdatePresence", CallingConvention = CallingConvention.Cdecl)] + private static extern void UpdatePresenceNative(ref RichPresenceStruct presence); + + [DllImport("discord-rpc", EntryPoint = "Discord_ClearPresence", CallingConvention = CallingConvention.Cdecl)] + public static extern void ClearPresence(); + + [DllImport("discord-rpc", EntryPoint = "Discord_Respond", CallingConvention = CallingConvention.Cdecl)] + public static extern void Respond(string userId, Reply reply); + + [DllImport("discord-rpc", EntryPoint = "Discord_UpdateHandlers", CallingConvention = CallingConvention.Cdecl)] + public static extern void UpdateHandlers(ref EventHandlers handlers); + + public static void UpdatePresence(RichPresence presence) + { + var presencestruct = presence.GetStruct(); + UpdatePresenceNative(ref presencestruct); + presence.FreeMem(); + } + + public class RichPresence + { + private RichPresenceStruct _presence; + private readonly List _buffers = new List(10); + + public string state; /* max 128 bytes */ + public string details; /* max 128 bytes */ + public long startTimestamp; + public long endTimestamp; + public string largeImageKey; /* max 32 bytes */ + public string largeImageText; /* max 128 bytes */ + public string smallImageKey; /* max 32 bytes */ + public string smallImageText; /* max 128 bytes */ + public string partyId; /* max 128 bytes */ + public int partySize; + public int partyMax; + public string matchSecret; /* max 128 bytes */ + public string joinSecret; /* max 128 bytes */ + public string spectateSecret; /* max 128 bytes */ + public bool instance; + + /// + /// Get the reprensentation of this instance + /// + /// reprensentation of this instance + internal RichPresenceStruct GetStruct() + { + if (_buffers.Count > 0) + { + FreeMem(); + } + + _presence.state = StrToPtr(state, 128); + _presence.details = StrToPtr(details, 128); + _presence.startTimestamp = startTimestamp; + _presence.endTimestamp = endTimestamp; + _presence.largeImageKey = StrToPtr(largeImageKey, 32); + _presence.largeImageText = StrToPtr(largeImageText, 128); + _presence.smallImageKey = StrToPtr(smallImageKey, 32); + _presence.smallImageText = StrToPtr(smallImageText, 128); + _presence.partyId = StrToPtr(partyId, 128); + _presence.partySize = partySize; + _presence.partyMax = partyMax; + _presence.matchSecret = StrToPtr(matchSecret, 128); + _presence.joinSecret = StrToPtr(joinSecret, 128); + _presence.spectateSecret = StrToPtr(spectateSecret, 128); + _presence.instance = instance; + + return _presence; + } + + /// + /// Returns a pointer to a representation of the given string with a size of maxbytes + /// + /// String to convert + /// Max number of bytes to use + /// Pointer to the UTF-8 representation of + private IntPtr StrToPtr(string input, int maxbytes) + { + if (string.IsNullOrEmpty(input)) return IntPtr.Zero; + var convstr = StrClampBytes(input, maxbytes); + var convbytecnt = Encoding.UTF8.GetByteCount(convstr); + var buffer = Marshal.AllocHGlobal(convbytecnt); + _buffers.Add(buffer); + Marshal.Copy(Encoding.UTF8.GetBytes(convstr), 0, buffer, convbytecnt); + return buffer; + } + + /// + /// Convert string to UTF-8 and add null termination + /// + /// string to convert + /// UTF-8 representation of with added null termination + private static string StrToUtf8NullTerm(string toconv) + { + var str = toconv.Trim(); + var bytes = Encoding.Default.GetBytes(str); + if (bytes.Length > 0 && bytes[bytes.Length - 1] != 0) + { + str += "\0\0"; + } + return Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(str)); + } + + /// + /// Clamp the string to the given byte length preserving null termination + /// + /// string to clamp + /// max bytes the resulting string should have (including null termination) + /// null terminated string with a byte length less or equal to + private static string StrClampBytes(string toclamp, int maxbytes) + { + var str = StrToUtf8NullTerm(toclamp); + var strbytes = Encoding.UTF8.GetBytes(str); + + if (strbytes.Length <= maxbytes) + { + return str; + } + + var newstrbytes = new byte[] { }; + Array.Copy(strbytes, 0, newstrbytes, 0, maxbytes - 1); + newstrbytes[newstrbytes.Length - 1] = 0; + newstrbytes[newstrbytes.Length - 2] = 0; + + return Encoding.UTF8.GetString(newstrbytes); + } + + /// + /// Free the allocated memory for conversion to + /// + internal void FreeMem() + { + for (var i = _buffers.Count - 1; i >= 0; i--) + { + Marshal.FreeHGlobal(_buffers[i]); + _buffers.RemoveAt(i); + } + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Program.cs b/Ryujinx/Ui/Program.cs index 5cacc6228b..dd5b53beff 100644 --- a/Ryujinx/Ui/Program.cs +++ b/Ryujinx/Ui/Program.cs @@ -5,15 +5,28 @@ using Ryujinx.Graphics.Gal.OpenGL; using Ryujinx.HLE; using System; using System.IO; +using System.Runtime.InteropServices; namespace Ryujinx { class Program { + private static DiscordRpc.RichPresence Presence; + + private static DiscordRpc.EventHandlers Handlers; + static void Main(string[] args) { Console.Title = "Ryujinx Console"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Handlers = new DiscordRpc.EventHandlers(); + Presence = new DiscordRpc.RichPresence(); + + DiscordRpc.Initialize("467315377412767744", ref Handlers, true, null); + } + IGalRenderer Renderer = new OGLRenderer(); IAalOutput AudioOut = new OpenALAudioOut(); @@ -47,6 +60,32 @@ namespace Ryujinx Ns.LoadCart(args[0]); } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (Ns.Os.SystemState.GetNpdmTitleName() != "Application") + { + Presence.details = $"{Ns.Os.SystemState.GetNpdmTitleName()} ({Ns.Os.SystemState.GetNpdmTitleId()})"; + } + else + { + Presence.details = Ns.Os.SystemState.GetNpdmTitleId(); + } + + if (Ns.Os.SystemState.GetNpdmIs64Bit()) + { + Presence.state = "Playing a 64-bit game!"; + } + else + { + Presence.state = "Playing a 32-bit game!"; + } + + Presence.largeImageKey = "icon"; + Presence.largeImageText = "Ryujinx"; + + DiscordRpc.UpdatePresence(Presence); + } } else if (File.Exists(args[0])) {