diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index b5ce555a16..91f5162acb 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -96,6 +96,10 @@ namespace Ryujinx.HLE.HOS public string CurrentTitle { get; private set; } + public string TitleName { get; private set; } + + public string TitleID { get; private set; } + public IntegrityCheckLevel FsIntegrityCheckLevel { get; set; } internal long HidBaseAddress { get; private set; } @@ -228,6 +232,7 @@ namespace Ryujinx.HLE.HOS } CurrentTitle = metaData.Aci0.TitleId.ToString("x16"); + TitleID = metaData.Aci0.TitleId.ToString("x16"); LoadNso("rtld"); LoadNso("main"); @@ -426,6 +431,7 @@ namespace Ryujinx.HLE.HOS } CurrentTitle = metaData.Aci0.TitleId.ToString("x16"); + TitleID = metaData.Aci0.TitleId.ToString("x16"); LoadNso("rtld"); LoadNso("main"); @@ -517,10 +523,13 @@ namespace Ryujinx.HLE.HOS Nacp controlData = new Nacp(controlFile.AsStream()); CurrentTitle = controlData.Descriptions[(int)State.DesiredTitleLanguage].Title; + TitleName = controlData.Descriptions[(int)State.DesiredTitleLanguage].Title; + TitleID = metaData.Aci0.TitleId.ToString("x16"); if (string.IsNullOrWhiteSpace(CurrentTitle)) { CurrentTitle = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title; + TitleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title; } return controlData; @@ -533,6 +542,7 @@ namespace Ryujinx.HLE.HOS else { CurrentTitle = metaData.Aci0.TitleId.ToString("x16"); + TitleID = metaData.Aci0.TitleId.ToString("x16"); } LoadNso("rtld"); diff --git a/Ryujinx/DiscordRpc.cs b/Ryujinx/DiscordRpc.cs new file mode 100644 index 0000000000..a0aa9a84a8 --- /dev/null +++ b/Ryujinx/DiscordRpc.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +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 64 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 64 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, 64); + _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/Program.cs b/Ryujinx/Program.cs index 42a6a74159..8e1889e932 100644 --- a/Ryujinx/Program.cs +++ b/Ryujinx/Program.cs @@ -5,11 +5,16 @@ using Ryujinx.Graphics.Gal.OpenGL; using Ryujinx.HLE; using System; using System.IO; +using System.Collections; namespace Ryujinx { class Program { + private static DiscordRpc.RichPresence Presence; + + private static DiscordRpc.EventHandlers Handlers; + public static string ApplicationDirectory => AppDomain.CurrentDomain.BaseDirectory; static void Main(string[] args) @@ -28,6 +33,42 @@ namespace Ryujinx AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit; + void SetPresence(string FileType) + { + ArrayList RPsupported = new ArrayList + { + "01006a800016e000", + "01009aa000faa000", + "0100a5c00d162000" + }; //temporary array until i make an external one to be read in + + if (File.Exists("./discord-rpc.dll")) + { + if (RPsupported.Contains(device.System.TitleID)) + { + Presence.largeImageKey = device.System.TitleID; + } + else + { + Presence.largeImageKey = "ryujinx"; + } + Presence.details = $"Playing {device.System.TitleName} ({device.System.TitleID})"; + Presence.state = "[state]"; + Presence.largeImageText = device.System.TitleName; + Presence.startTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); + Presence.smallImageKey = FileType; + Presence.smallImageText = FileType.ToUpper().Replace("-", " "); + DiscordRpc.UpdatePresence(Presence); + } + } + + if (File.Exists("./discord-rpc.dll")) + { + Handlers = new DiscordRpc.EventHandlers(); + Presence = new DiscordRpc.RichPresence(); + DiscordRpc.Initialize("568815339807309834", ref Handlers, true, null); + } + if (args.Length == 1) { if (Directory.Exists(args[0])) @@ -42,14 +83,14 @@ namespace Ryujinx if (romFsFiles.Length > 0) { Logger.PrintInfo(LogClass.Application, "Loading as cart with RomFS."); - device.LoadCart(args[0], romFsFiles[0]); + SetPresence("cart-with-romfs"); } else { Logger.PrintInfo(LogClass.Application, "Loading as cart WITHOUT RomFS."); - device.LoadCart(args[0]); + SetPresence("cart-without-romfs"); } } else if (File.Exists(args[0])) @@ -59,19 +100,23 @@ namespace Ryujinx case ".xci": Logger.PrintInfo(LogClass.Application, "Loading as XCI."); device.LoadXci(args[0]); + SetPresence("xci"); break; case ".nca": Logger.PrintInfo(LogClass.Application, "Loading as NCA."); device.LoadNca(args[0]); + SetPresence("nca"); break; case ".nsp": case ".pfs0": Logger.PrintInfo(LogClass.Application, "Loading as NSP."); device.LoadNsp(args[0]); + SetPresence("nsp"); break; default: Logger.PrintInfo(LogClass.Application, "Loading as homebrew."); device.LoadProgram(args[0]); + SetPresence("nro-nso"); break; } }