Add Discord Rich Presence
Add Discord Rich Presence to Ryujinx. Only tested to work on windows.
This commit is contained in:
parent
6b23a2c125
commit
05862d202f
3 changed files with 277 additions and 2 deletions
|
@ -96,6 +96,10 @@ namespace Ryujinx.HLE.HOS
|
||||||
|
|
||||||
public string CurrentTitle { get; private set; }
|
public string CurrentTitle { get; private set; }
|
||||||
|
|
||||||
|
public string TitleName { get; private set; }
|
||||||
|
|
||||||
|
public string TitleID { get; private set; }
|
||||||
|
|
||||||
public IntegrityCheckLevel FsIntegrityCheckLevel { get; set; }
|
public IntegrityCheckLevel FsIntegrityCheckLevel { get; set; }
|
||||||
|
|
||||||
internal long HidBaseAddress { get; private set; }
|
internal long HidBaseAddress { get; private set; }
|
||||||
|
@ -228,6 +232,7 @@ namespace Ryujinx.HLE.HOS
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentTitle = metaData.Aci0.TitleId.ToString("x16");
|
CurrentTitle = metaData.Aci0.TitleId.ToString("x16");
|
||||||
|
TitleID = metaData.Aci0.TitleId.ToString("x16");
|
||||||
|
|
||||||
LoadNso("rtld");
|
LoadNso("rtld");
|
||||||
LoadNso("main");
|
LoadNso("main");
|
||||||
|
@ -426,6 +431,7 @@ namespace Ryujinx.HLE.HOS
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentTitle = metaData.Aci0.TitleId.ToString("x16");
|
CurrentTitle = metaData.Aci0.TitleId.ToString("x16");
|
||||||
|
TitleID = metaData.Aci0.TitleId.ToString("x16");
|
||||||
|
|
||||||
LoadNso("rtld");
|
LoadNso("rtld");
|
||||||
LoadNso("main");
|
LoadNso("main");
|
||||||
|
@ -517,10 +523,13 @@ namespace Ryujinx.HLE.HOS
|
||||||
Nacp controlData = new Nacp(controlFile.AsStream());
|
Nacp controlData = new Nacp(controlFile.AsStream());
|
||||||
|
|
||||||
CurrentTitle = controlData.Descriptions[(int)State.DesiredTitleLanguage].Title;
|
CurrentTitle = controlData.Descriptions[(int)State.DesiredTitleLanguage].Title;
|
||||||
|
TitleName = controlData.Descriptions[(int)State.DesiredTitleLanguage].Title;
|
||||||
|
TitleID = metaData.Aci0.TitleId.ToString("x16");
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(CurrentTitle))
|
if (string.IsNullOrWhiteSpace(CurrentTitle))
|
||||||
{
|
{
|
||||||
CurrentTitle = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title;
|
CurrentTitle = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title;
|
||||||
|
TitleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title;
|
||||||
}
|
}
|
||||||
|
|
||||||
return controlData;
|
return controlData;
|
||||||
|
@ -533,6 +542,7 @@ namespace Ryujinx.HLE.HOS
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
CurrentTitle = metaData.Aci0.TitleId.ToString("x16");
|
CurrentTitle = metaData.Aci0.TitleId.ToString("x16");
|
||||||
|
TitleID = metaData.Aci0.TitleId.ToString("x16");
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadNso("rtld");
|
LoadNso("rtld");
|
||||||
|
|
220
Ryujinx/DiscordRpc.cs
Normal file
220
Ryujinx/DiscordRpc.cs
Normal file
|
@ -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<IntPtr> _buffers = new List<IntPtr>(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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the <see cref="RichPresenceStruct"/> reprensentation of this instance
|
||||||
|
/// </summary>
|
||||||
|
/// <returns><see cref="RichPresenceStruct"/> reprensentation of this instance</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a pointer to a representation of the given string with a size of maxbytes
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">String to convert</param>
|
||||||
|
/// <param name="maxbytes">Max number of bytes to use</param>
|
||||||
|
/// <returns>Pointer to the UTF-8 representation of <see cref="input"/></returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert string to UTF-8 and add null termination
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="toconv">string to convert</param>
|
||||||
|
/// <returns>UTF-8 representation of <see cref="toconv"/> with added null termination</returns>
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clamp the string to the given byte length preserving null termination
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="toclamp">string to clamp</param>
|
||||||
|
/// <param name="maxbytes">max bytes the resulting string should have (including null termination)</param>
|
||||||
|
/// <returns>null terminated string with a byte length less or equal to <see cref="maxbytes"/></returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Free the allocated memory for conversion to <see cref="RichPresenceStruct"/>
|
||||||
|
/// </summary>
|
||||||
|
internal void FreeMem()
|
||||||
|
{
|
||||||
|
for (var i = _buffers.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(_buffers[i]);
|
||||||
|
_buffers.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,11 +5,16 @@ using Ryujinx.Graphics.Gal.OpenGL;
|
||||||
using Ryujinx.HLE;
|
using Ryujinx.HLE;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
namespace Ryujinx
|
namespace Ryujinx
|
||||||
{
|
{
|
||||||
class Program
|
class Program
|
||||||
{
|
{
|
||||||
|
private static DiscordRpc.RichPresence Presence;
|
||||||
|
|
||||||
|
private static DiscordRpc.EventHandlers Handlers;
|
||||||
|
|
||||||
public static string ApplicationDirectory => AppDomain.CurrentDomain.BaseDirectory;
|
public static string ApplicationDirectory => AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
|
@ -28,6 +33,42 @@ namespace Ryujinx
|
||||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||||
AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
|
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 (args.Length == 1)
|
||||||
{
|
{
|
||||||
if (Directory.Exists(args[0]))
|
if (Directory.Exists(args[0]))
|
||||||
|
@ -42,14 +83,14 @@ namespace Ryujinx
|
||||||
if (romFsFiles.Length > 0)
|
if (romFsFiles.Length > 0)
|
||||||
{
|
{
|
||||||
Logger.PrintInfo(LogClass.Application, "Loading as cart with RomFS.");
|
Logger.PrintInfo(LogClass.Application, "Loading as cart with RomFS.");
|
||||||
|
|
||||||
device.LoadCart(args[0], romFsFiles[0]);
|
device.LoadCart(args[0], romFsFiles[0]);
|
||||||
|
SetPresence("cart-with-romfs");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.PrintInfo(LogClass.Application, "Loading as cart WITHOUT RomFS.");
|
Logger.PrintInfo(LogClass.Application, "Loading as cart WITHOUT RomFS.");
|
||||||
|
|
||||||
device.LoadCart(args[0]);
|
device.LoadCart(args[0]);
|
||||||
|
SetPresence("cart-without-romfs");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (File.Exists(args[0]))
|
else if (File.Exists(args[0]))
|
||||||
|
@ -59,19 +100,23 @@ namespace Ryujinx
|
||||||
case ".xci":
|
case ".xci":
|
||||||
Logger.PrintInfo(LogClass.Application, "Loading as XCI.");
|
Logger.PrintInfo(LogClass.Application, "Loading as XCI.");
|
||||||
device.LoadXci(args[0]);
|
device.LoadXci(args[0]);
|
||||||
|
SetPresence("xci");
|
||||||
break;
|
break;
|
||||||
case ".nca":
|
case ".nca":
|
||||||
Logger.PrintInfo(LogClass.Application, "Loading as NCA.");
|
Logger.PrintInfo(LogClass.Application, "Loading as NCA.");
|
||||||
device.LoadNca(args[0]);
|
device.LoadNca(args[0]);
|
||||||
|
SetPresence("nca");
|
||||||
break;
|
break;
|
||||||
case ".nsp":
|
case ".nsp":
|
||||||
case ".pfs0":
|
case ".pfs0":
|
||||||
Logger.PrintInfo(LogClass.Application, "Loading as NSP.");
|
Logger.PrintInfo(LogClass.Application, "Loading as NSP.");
|
||||||
device.LoadNsp(args[0]);
|
device.LoadNsp(args[0]);
|
||||||
|
SetPresence("nsp");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Logger.PrintInfo(LogClass.Application, "Loading as homebrew.");
|
Logger.PrintInfo(LogClass.Application, "Loading as homebrew.");
|
||||||
device.LoadProgram(args[0]);
|
device.LoadProgram(args[0]);
|
||||||
|
SetPresence("nro-nso");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue