├── DiscordRPC ├── RPC │ ├── Commands │ │ ├── ICommand.cs │ │ ├── SubscribeCommand.cs │ │ ├── PresenceCommand.cs │ │ ├── RespondCommand.cs │ │ └── CloseCommand.cs │ ├── Payload │ │ ├── ClosePayload.cs │ │ ├── IPayload.cs │ │ ├── PayloadArgument.cs │ │ ├── PayloadEvent.cs │ │ ├── ServerEvent.cs │ │ └── Command.cs │ └── RpcConnection.cs ├── Converters │ ├── EnumValueAttribute.cs │ └── EnumSnakeCaseConverter.cs ├── Exceptions │ ├── BadPresenceException.cs │ ├── InvalidConfigurationException.cs │ ├── InvalidPipeException.cs │ ├── UninitializedException.cs │ └── StringOutOfRangeException.cs ├── IO │ ├── Handshake.cs │ ├── Opcode.cs │ ├── INamedPipeClient.cs │ ├── PipeFrame.cs │ └── ManagedNamedPipeClient.cs ├── Registry │ ├── IUriSchemeCreator.cs │ ├── MacUriSchemeCreator.cs │ ├── UriScheme.cs │ ├── UnixUriSchemeCreator.cs │ └── WindowsUriSchemeCreator.cs ├── Message │ ├── SpectateMessage.cs │ ├── CloseMessage.cs │ ├── IMessage.cs │ ├── ConnectionFailedMessage.cs │ ├── JoinMessage.cs │ ├── JoinRequestMessage.cs │ ├── ConnectionEstablishedMessage.cs │ ├── ReadyMessage.cs │ ├── SubscribeMessage.cs │ ├── UnsubscribeMsesage.cs │ ├── PresenceMessage.cs │ ├── MessageType.cs │ └── ErrorMessage.cs ├── DiscordRPC.csproj ├── Configuration.cs ├── EventType.cs ├── Logging │ ├── LogLevel.cs │ ├── ILogger.cs │ ├── NullLogger.cs │ ├── FileLogger.cs │ └── ConsoleLogger.cs ├── Properties │ └── AssemblyInfo.cs ├── Helper │ ├── BackoffDelay.cs │ └── StringTools.cs ├── DiscordRPC.nuspec ├── Events.cs ├── Web │ └── WebRPC.cs └── User.cs ├── DarkflameRPC ├── World.cs ├── DarkflameRPC.csproj ├── DarkflameRPC.sln ├── Properties │ └── AssemblyInfo.cs └── Program.cs ├── README.md ├── LICENSE ├── DiscordRPC.sln └── .gitignore /DiscordRPC/RPC/Commands/ICommand.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.RPC.Payload; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace DiscordRPC.RPC.Commands 8 | { 9 | internal interface ICommand 10 | { 11 | IPayload PreparePayload(long nonce); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DiscordRPC/Converters/EnumValueAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace DiscordRPC.Converters 7 | { 8 | internal class EnumValueAttribute : Attribute 9 | { 10 | public string Value { get; set; } 11 | public EnumValueAttribute(string value) 12 | { 13 | this.Value = value; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DiscordRPC/Exceptions/BadPresenceException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace DiscordRPC.Exceptions 7 | { 8 | /// 9 | /// A BadPresenceException is thrown when invalid, incompatible or conflicting properties and is unable to be sent. 10 | /// 11 | public class BadPresenceException : Exception 12 | { 13 | internal BadPresenceException(string message) : base(message) { } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /DiscordRPC/Exceptions/InvalidConfigurationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace DiscordRPC.Exceptions 7 | { 8 | /// 9 | /// A InvalidConfigurationException is thrown when trying to perform a action that conflicts with the current configuration. 10 | /// 11 | public class InvalidConfigurationException : Exception 12 | { 13 | internal InvalidConfigurationException(string message) : base(message) { } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /DiscordRPC/Exceptions/InvalidPipeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace DiscordRPC.Exceptions 7 | { 8 | /// 9 | /// The exception that is thrown when a error occurs while communicating with a pipe or when a connection attempt fails. 10 | /// 11 | [System.Obsolete("Not actually used anywhere")] 12 | public class InvalidPipeException : Exception 13 | { 14 | internal InvalidPipeException(string message) : base(message) { } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DiscordRPC/IO/Handshake.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace DiscordRPC.IO 8 | { 9 | internal class Handshake 10 | { 11 | /// 12 | /// Version of the IPC API we are using 13 | /// 14 | [JsonProperty("v")] 15 | public int Version { get; set; } 16 | 17 | /// 18 | /// The ID of the app. 19 | /// 20 | [JsonProperty("client_id")] 21 | public string ClientID { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /DiscordRPC/Registry/IUriSchemeCreator.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Logging; 2 | 3 | namespace DiscordRPC.Registry 4 | { 5 | internal interface IUriSchemeCreator 6 | { 7 | /// 8 | /// Registers the URI scheme. If Steam ID is passed, the application will be launched through steam instead of directly. 9 | /// Additional arguments can be supplied if required. 10 | /// 11 | /// The register context. 12 | bool RegisterUriScheme(UriSchemeRegister register); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DiscordRPC/Message/SpectateMessage.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace DiscordRPC.Message 8 | { 9 | /// 10 | /// Called when the Discord Client wishes for this process to spectate a game. D -> C. 11 | /// 12 | public class SpectateMessage : JoinMessage 13 | { 14 | /// 15 | /// The type of message received from discord 16 | /// 17 | public override MessageType Type { get { return MessageType.Spectate; } } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /DiscordRPC/RPC/Payload/ClosePayload.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace DiscordRPC.RPC.Payload 8 | { 9 | internal class ClosePayload : IPayload 10 | { 11 | /// 12 | /// The close code the discord gave us 13 | /// 14 | [JsonProperty("code")] 15 | public int Code { get; set; } 16 | 17 | /// 18 | /// The close reason discord gave us 19 | /// 20 | [JsonProperty("message")] 21 | public string Reason { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /DiscordRPC/RPC/Commands/SubscribeCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using DiscordRPC.RPC.Payload; 6 | 7 | namespace DiscordRPC.RPC.Commands 8 | { 9 | internal class SubscribeCommand : ICommand 10 | { 11 | public ServerEvent Event { get; set; } 12 | public bool IsUnsubscribe { get; set; } 13 | 14 | public IPayload PreparePayload(long nonce) 15 | { 16 | return new EventPayload(nonce) 17 | { 18 | Command = IsUnsubscribe ? Command.Unsubscribe : Command.Subscribe, 19 | Event = Event 20 | }; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /DarkflameRPC/World.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace DiscordRPC.Example 8 | { 9 | class World 10 | { 11 | public string worldName; 12 | public string worldID; 13 | public string worldDescription; 14 | public string worldLargeIcon; 15 | 16 | public World(string name, string ID, string description, string largeIcon) 17 | { 18 | worldName = name; 19 | worldID = ID; 20 | worldDescription = description; 21 | worldLargeIcon = largeIcon; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DarkflameRPC/DarkflameRPC.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net45;netcoreapp3.1 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | -------------------------------------------------------------------------------- /DiscordRPC/Message/CloseMessage.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordRPC.Message 2 | { 3 | /// 4 | /// Called when the IPC has closed. 5 | /// 6 | public class CloseMessage : IMessage 7 | { 8 | /// 9 | /// The type of message 10 | /// 11 | public override MessageType Type { get { return MessageType.Close; } } 12 | 13 | /// 14 | /// The reason for the close 15 | /// 16 | public string Reason { get; internal set; } 17 | 18 | /// 19 | /// The closure code 20 | /// 21 | public int Code { get; internal set; } 22 | 23 | internal CloseMessage() { } 24 | internal CloseMessage(string reason) { this.Reason = reason; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /DiscordRPC/DiscordRPC.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net35;netstandard2.0 4 | DiscordRPC.nuspec 5 | false 6 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /DiscordRPC/Message/IMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DiscordRPC.Message 4 | { 5 | /// 6 | /// Messages received from discord. 7 | /// 8 | public abstract class IMessage 9 | { 10 | /// 11 | /// The type of message received from discord 12 | /// 13 | public abstract MessageType Type { get; } 14 | 15 | /// 16 | /// The time the message was created 17 | /// 18 | public DateTime TimeCreated { get { return _timecreated; } } 19 | private DateTime _timecreated; 20 | 21 | /// 22 | /// Creates a new instance of the message 23 | /// 24 | public IMessage() 25 | { 26 | _timecreated = DateTime.Now; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /DiscordRPC/Message/ConnectionFailedMessage.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace DiscordRPC.Message 8 | { 9 | /// 10 | /// Failed to establish any connection with discord. Discord is potentially not running? 11 | /// 12 | public class ConnectionFailedMessage : IMessage 13 | { 14 | /// 15 | /// The type of message received from discord 16 | /// 17 | public override MessageType Type { get { return MessageType.ConnectionFailed; } } 18 | 19 | /// 20 | /// The pipe we failed to connect too. 21 | /// 22 | public int FailedPipe { get; internal set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DiscordRPC/Message/JoinMessage.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace DiscordRPC.Message 8 | { 9 | /// 10 | /// Called when the Discord Client wishes for this process to join a game. D -> C. 11 | /// 12 | public class JoinMessage : IMessage 13 | { 14 | /// 15 | /// The type of message received from discord 16 | /// 17 | public override MessageType Type { get { return MessageType.Join; } } 18 | 19 | /// 20 | /// The to connect with. 21 | /// 22 | [JsonProperty("secret")] 23 | public string Secret { get; internal set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DiscordRPC/Message/JoinRequestMessage.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace DiscordRPC.Message 8 | { 9 | /// 10 | /// Called when some other person has requested access to this game. C -> D -> C. 11 | /// 12 | public class JoinRequestMessage : IMessage 13 | { 14 | /// 15 | /// The type of message received from discord 16 | /// 17 | public override MessageType Type { get { return MessageType.JoinRequest; } } 18 | 19 | /// 20 | /// The discord user that is requesting access. 21 | /// 22 | [JsonProperty("user")] 23 | public User User { get; internal set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DiscordRPC/IO/Opcode.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordRPC.IO 2 | { 3 | /// 4 | /// The operation code that the was sent under. This defines the type of frame and the data to expect. 5 | /// 6 | public enum Opcode : uint 7 | { 8 | /// 9 | /// Initial handshake frame 10 | /// 11 | Handshake = 0, 12 | 13 | /// 14 | /// Generic message frame 15 | /// 16 | Frame = 1, 17 | 18 | /// 19 | /// Discord has closed the connection 20 | /// 21 | Close = 2, 22 | 23 | /// 24 | /// Ping frame (not used?) 25 | /// 26 | Ping = 3, 27 | 28 | /// 29 | /// Pong frame (not used?) 30 | /// 31 | Pong = 4 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DiscordRPC/Message/ConnectionEstablishedMessage.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace DiscordRPC.Message 8 | { 9 | /// 10 | /// The connection to the discord client was succesfull. This is called before . 11 | /// 12 | public class ConnectionEstablishedMessage : IMessage 13 | { 14 | /// 15 | /// The type of message received from discord 16 | /// 17 | public override MessageType Type { get { return MessageType.ConnectionEstablished; } } 18 | 19 | /// 20 | /// The pipe we ended up connecting too 21 | /// 22 | public int ConnectedPipe { get; internal set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DiscordRPC/RPC/Commands/PresenceCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using DiscordRPC.RPC.Payload; 6 | using Newtonsoft.Json; 7 | 8 | namespace DiscordRPC.RPC.Commands 9 | { 10 | internal class PresenceCommand : ICommand 11 | { 12 | /// 13 | /// The process ID 14 | /// 15 | [JsonProperty("pid")] 16 | public int PID { get; set; } 17 | 18 | /// 19 | /// The rich presence to be set. Can be null. 20 | /// 21 | [JsonProperty("activity")] 22 | public RichPresence Presence { get; set; } 23 | 24 | public IPayload PreparePayload(long nonce) 25 | { 26 | return new ArgumentPayload(this, nonce) 27 | { 28 | Command = Command.SetActivity 29 | }; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DiscordRPC/Configuration.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace DiscordRPC 8 | { 9 | /// 10 | /// Configuration of the current RPC connection 11 | /// 12 | public class Configuration 13 | { 14 | /// 15 | /// The Discord API endpoint that should be used. 16 | /// 17 | [JsonProperty("api_endpoint")] 18 | public string ApiEndpoint { get; set; } 19 | 20 | /// 21 | /// The CDN endpoint 22 | /// 23 | [JsonProperty("cdn_host")] 24 | public string CdnHost { get; set; } 25 | 26 | /// 27 | /// The type of enviroment the connection on. Usually Production. 28 | /// 29 | [JsonProperty("enviroment")] 30 | public string Enviroment { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DiscordRPC/EventType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace DiscordRPC 7 | { 8 | /// 9 | /// The type of event receieved by the RPC. A flag type that can be combined. 10 | /// 11 | [System.Flags] 12 | public enum EventType 13 | { 14 | /// 15 | /// No event 16 | /// 17 | None = 0, 18 | 19 | /// 20 | /// Called when the Discord Client wishes to enter a game to spectate 21 | /// 22 | Spectate = 0x1, 23 | 24 | /// 25 | /// Called when the Discord Client wishes to enter a game to play. 26 | /// 27 | Join = 0x2, 28 | 29 | /// 30 | /// Called when another Discord Client has requested permission to join this game. 31 | /// 32 | JoinRequest = 0x4 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /DiscordRPC/RPC/Commands/RespondCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using DiscordRPC.RPC.Payload; 6 | using Newtonsoft.Json; 7 | 8 | namespace DiscordRPC.RPC.Commands 9 | { 10 | internal class RespondCommand : ICommand 11 | { 12 | /// 13 | /// The user ID that we are accepting / rejecting 14 | /// 15 | [JsonProperty("user_id")] 16 | public string UserID { get; set; } 17 | 18 | /// 19 | /// If true, the user will be allowed to connect. 20 | /// 21 | [JsonIgnore] 22 | public bool Accept { get; set; } 23 | 24 | public IPayload PreparePayload(long nonce) 25 | { 26 | return new ArgumentPayload(this, nonce) 27 | { 28 | Command = Accept ? Command.SendActivityJoinInvite : Command.CloseActivityJoinRequest 29 | }; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DiscordRPC/RPC/Commands/CloseCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using DiscordRPC.RPC.Payload; 6 | using Newtonsoft.Json; 7 | 8 | namespace DiscordRPC.RPC.Commands 9 | { 10 | internal class CloseCommand : ICommand 11 | { 12 | /// 13 | /// The process ID 14 | /// 15 | [JsonProperty("pid")] 16 | public int PID { get; set; } 17 | 18 | /// 19 | /// The rich presence to be set. Can be null. 20 | /// 21 | [JsonProperty("close_reason")] 22 | public string value = "Unity 5.5 doesn't handle thread aborts. Can you please close me discord?"; 23 | 24 | public IPayload PreparePayload(long nonce) 25 | { 26 | return new ArgumentPayload() 27 | { 28 | Command = Command.Dispatch, 29 | Nonce = null, 30 | Arguments = null 31 | }; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /DiscordRPC/Logging/LogLevel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace DiscordRPC.Logging 7 | { 8 | /// 9 | /// Level of logging to use. 10 | /// 11 | public enum LogLevel 12 | { 13 | /// 14 | /// Trace, Info, Warning and Errors are logged 15 | /// 16 | Trace = 1, 17 | 18 | /// 19 | /// Info, Warning and Errors are logged 20 | /// 21 | Info = 2, 22 | 23 | /// 24 | /// Warning and Errors are logged 25 | /// 26 | Warning = 3, 27 | 28 | /// 29 | /// Only Errors are logged 30 | /// 31 | Error = 4, 32 | 33 | /// 34 | /// Nothing is logged 35 | /// 36 | None = 256 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /DiscordRPC/Message/ReadyMessage.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using Newtonsoft.Json; 4 | 5 | namespace DiscordRPC.Message 6 | { 7 | /// 8 | /// Called when the ipc is ready to send arguments. 9 | /// 10 | public class ReadyMessage : IMessage 11 | { 12 | /// 13 | /// The type of message received from discord 14 | /// 15 | public override MessageType Type { get { return MessageType.Ready; } } 16 | 17 | /// 18 | /// The configuration of the connection 19 | /// 20 | [JsonProperty("config")] 21 | public Configuration Configuration { get; set; } 22 | 23 | /// 24 | /// User the connection belongs too 25 | /// 26 | [JsonProperty("user")] 27 | public User User { get; set; } 28 | 29 | /// 30 | /// The version of the RPC 31 | /// 32 | [JsonProperty("v")] 33 | public int Version { get; set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /DiscordRPC/Exceptions/UninitializedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace DiscordRPC.Exceptions 7 | { 8 | /// 9 | /// Thrown when an action is performed on a client that has not yet been initialized 10 | /// 11 | public class UninitializedException : Exception 12 | { 13 | /// 14 | /// Creates a new unintialized exception 15 | /// 16 | /// 17 | internal UninitializedException(string message) : base(message) { } 18 | 19 | /// 20 | /// Creates a new uninitialized exception with default message. 21 | /// 22 | internal UninitializedException() : this("Cannot perform action because the client has not been initialized yet or has been deinitialized.") { } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Darkflame Rich Presence Client 2 | 3 | This is built using the [Discord RPC C# project](https://github.com/Lachee/discord-rpc-csharp) and contains minimal code written by the Darkflame team in the DarkflameRPC folder. 4 | 5 | Build the DarkflameRPC.sln and run the generated DarkflameRPC.exe once you've opened your LEGO Universe game client. This doesn't start automatically with the LU client and must manually be run each time. 6 | 7 | This project features images from various contributors of [the LEGO Universe Wiki](https://legouniverse.fandom.com/wiki/LEGO_Universe_Wiki) and various promotional images of the game. This application solely allows you to display current world and time elapsed playing LEGO Universe as your Discord Rich Presence. 8 | 9 | # System Requirements 10 | 11 | This has only been tested and confirmed to work on Windows 10, using the windows path to the client log files. This will not work on other operating systems without changes. 12 | 13 | -------------------------------------------------------------------------------- /DiscordRPC/RPC/Payload/IPayload.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Converters; 2 | using Newtonsoft.Json; 3 | 4 | namespace DiscordRPC.RPC.Payload 5 | { 6 | /// 7 | /// Base Payload that is received by both client and server 8 | /// 9 | internal abstract class IPayload 10 | { 11 | /// 12 | /// The type of payload 13 | /// 14 | [JsonProperty("cmd"), JsonConverter(typeof(EnumSnakeCaseConverter))] 15 | public Command Command { get; set; } 16 | 17 | /// 18 | /// A incremental value to help identify payloads 19 | /// 20 | [JsonProperty("nonce")] 21 | public string Nonce { get; set; } 22 | 23 | protected IPayload() { } 24 | protected IPayload(long nonce) 25 | { 26 | Nonce = nonce.ToString(); 27 | } 28 | 29 | public override string ToString() 30 | { 31 | return "Payload || Command: " + Command.ToString() + ", Nonce: " + (Nonce != null ? Nonce.ToString() : "NULL"); 32 | } 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /DiscordRPC/Message/SubscribeMessage.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.RPC.Payload; 2 | 3 | namespace DiscordRPC.Message 4 | { 5 | /// 6 | /// Called as validation of a subscribe 7 | /// 8 | public class SubscribeMessage : IMessage 9 | { 10 | /// 11 | /// The type of message received from discord 12 | /// 13 | public override MessageType Type { get { return MessageType.Subscribe; } } 14 | 15 | /// 16 | /// The event that was subscribed too. 17 | /// 18 | public EventType Event { get; internal set; } 19 | 20 | internal SubscribeMessage(ServerEvent evt) 21 | { 22 | switch (evt) 23 | { 24 | default: 25 | case ServerEvent.ActivityJoin: 26 | Event = EventType.Join; 27 | break; 28 | 29 | case ServerEvent.ActivityJoinRequest: 30 | Event = EventType.JoinRequest; 31 | break; 32 | 33 | case ServerEvent.ActivitySpectate: 34 | Event = EventType.Spectate; 35 | break; 36 | 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /DiscordRPC/Message/UnsubscribeMsesage.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.RPC.Payload; 2 | 3 | namespace DiscordRPC.Message 4 | { 5 | /// 6 | /// Called as validation of a subscribe 7 | /// 8 | public class UnsubscribeMessage : IMessage 9 | { 10 | /// 11 | /// The type of message received from discord 12 | /// 13 | public override MessageType Type { get { return MessageType.Unsubscribe; } } 14 | 15 | /// 16 | /// The event that was subscribed too. 17 | /// 18 | public EventType Event { get; internal set; } 19 | 20 | internal UnsubscribeMessage(ServerEvent evt) 21 | { 22 | switch (evt) 23 | { 24 | default: 25 | case ServerEvent.ActivityJoin: 26 | Event = EventType.Join; 27 | break; 28 | 29 | case ServerEvent.ActivityJoinRequest: 30 | Event = EventType.JoinRequest; 31 | break; 32 | 33 | case ServerEvent.ActivitySpectate: 34 | Event = EventType.Spectate; 35 | break; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lachee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /DarkflameRPC/DarkflameRPC.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31702.278 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DarkflameRPC", "DarkflameRPC.csproj", "{A509ADA1-56D2-4A8F-8DAD-E06ACA2D073C}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A509ADA1-56D2-4A8F-8DAD-E06ACA2D073C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {A509ADA1-56D2-4A8F-8DAD-E06ACA2D073C}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {A509ADA1-56D2-4A8F-8DAD-E06ACA2D073C}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {A509ADA1-56D2-4A8F-8DAD-E06ACA2D073C}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {2D08C520-5F1C-404C-B44F-C005E1C1B1FF} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /DiscordRPC/Message/PresenceMessage.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace DiscordRPC.Message 4 | { 5 | /// 6 | /// Representation of the message received by discord when the presence has been updated. 7 | /// 8 | public class PresenceMessage : IMessage 9 | { 10 | /// 11 | /// The type of message received from discord 12 | /// 13 | public override MessageType Type { get { return MessageType.PresenceUpdate; } } 14 | 15 | internal PresenceMessage() : this(null) { } 16 | internal PresenceMessage(RichPresenceResponse rpr) 17 | { 18 | if (rpr == null) 19 | { 20 | Presence = null; 21 | Name = "No Rich Presence"; 22 | ApplicationID = ""; 23 | } 24 | else 25 | { 26 | Presence = (BaseRichPresence)rpr; 27 | Name = rpr.Name; 28 | ApplicationID = rpr.ClientID; 29 | } 30 | } 31 | 32 | /// 33 | /// The rich presence Discord has set 34 | /// 35 | public BaseRichPresence Presence { get; internal set; } 36 | 37 | /// 38 | /// The name of the application Discord has set it for 39 | /// 40 | public string Name { get; internal set; } 41 | 42 | /// 43 | /// The ID of the application discord has set it for 44 | /// 45 | public string ApplicationID { get; internal set; } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /DiscordRPC/RPC/Payload/PayloadArgument.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Converters; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace DiscordRPC.RPC.Payload 6 | { 7 | /// 8 | /// The payload that is sent by the client to discord for events such as setting the rich presence. 9 | /// 10 | /// SetPrecense 11 | /// 12 | /// 13 | internal class ArgumentPayload : IPayload 14 | { 15 | /// 16 | /// The data the server sent too us 17 | /// 18 | [JsonProperty("args", NullValueHandling = NullValueHandling.Ignore)] 19 | public JObject Arguments { get; set; } 20 | 21 | public ArgumentPayload() : base() { Arguments = null; } 22 | public ArgumentPayload(long nonce) : base(nonce) { Arguments = null; } 23 | public ArgumentPayload(object args, long nonce) : base(nonce) 24 | { 25 | SetObject(args); 26 | } 27 | 28 | /// 29 | /// Sets the obejct stored within the data. 30 | /// 31 | /// 32 | public void SetObject(object obj) 33 | { 34 | Arguments = JObject.FromObject(obj); 35 | } 36 | 37 | /// 38 | /// Gets the object stored within the Data 39 | /// 40 | /// 41 | /// 42 | public T GetObject() 43 | { 44 | return Arguments.ToObject(); 45 | } 46 | 47 | public override string ToString() 48 | { 49 | return "Argument " + base.ToString(); 50 | } 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /DiscordRPC/Logging/ILogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace DiscordRPC.Logging 7 | { 8 | /// 9 | /// Logging interface to log the internal states of the pipe. Logs are sent in a NON thread safe way. They can come from multiple threads and it is upto the ILogger to account for it. 10 | /// 11 | public interface ILogger 12 | { 13 | /// 14 | /// The level of logging to apply to this logger. 15 | /// 16 | LogLevel Level { get; set; } 17 | 18 | /// 19 | /// Debug trace messeages used for debugging internal elements. 20 | /// 21 | /// 22 | /// 23 | void Trace(string message, params object[] args); 24 | 25 | /// 26 | /// Informative log messages 27 | /// 28 | /// 29 | /// 30 | void Info(string message, params object[] args); 31 | 32 | /// 33 | /// Warning log messages 34 | /// 35 | /// 36 | /// 37 | void Warning(string message, params object[] args); 38 | 39 | /// 40 | /// Error log messsages 41 | /// 42 | /// 43 | /// 44 | void Error(string message, params object[] args); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /DarkflameRPC/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("DarkflameRPC")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("DarkflameRPC")] 13 | [assembly: AssemblyCopyright("Copyright © 2021")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("37fee3cf-72fe-4450-a71a-373d3b298f0f")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /DiscordRPC/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Discord RPC")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Discord RPC")] 13 | [assembly: AssemblyCopyright("Copyright © 2021")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("819d20d6-8d88-45c1-a4d2-aa21f10abd19")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.143.0")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /DiscordRPC.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31702.278 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordRPC", "DiscordRPC\DiscordRPC.csproj", "{819D20D6-8D88-45C1-A4D2-AA21F10ABD19}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DarkflameRPC", "DarkflameRPC\DarkflameRPC.csproj", "{85DE7626-A6A6-4F88-953C-22CADCF74C37}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {819D20D6-8D88-45C1-A4D2-AA21F10ABD19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {819D20D6-8D88-45C1-A4D2-AA21F10ABD19}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {819D20D6-8D88-45C1-A4D2-AA21F10ABD19}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {819D20D6-8D88-45C1-A4D2-AA21F10ABD19}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {85DE7626-A6A6-4F88-953C-22CADCF74C37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {85DE7626-A6A6-4F88-953C-22CADCF74C37}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {85DE7626-A6A6-4F88-953C-22CADCF74C37}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {85DE7626-A6A6-4F88-953C-22CADCF74C37}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {836162DF-AEF8-4551-8562-FEAD0F4550E3} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /DiscordRPC/Logging/NullLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace DiscordRPC.Logging 7 | { 8 | /// 9 | /// Ignores all log events 10 | /// 11 | public class NullLogger : ILogger 12 | { 13 | /// 14 | /// The level of logging to apply to this logger. 15 | /// 16 | public LogLevel Level { get; set; } 17 | 18 | /// 19 | /// Informative log messages 20 | /// 21 | /// 22 | /// 23 | public void Trace(string message, params object[] args) 24 | { 25 | //Null Logger, so no messages are acutally sent 26 | } 27 | 28 | /// 29 | /// Informative log messages 30 | /// 31 | /// 32 | /// 33 | public void Info(string message, params object[] args) 34 | { 35 | //Null Logger, so no messages are acutally sent 36 | } 37 | 38 | /// 39 | /// Warning log messages 40 | /// 41 | /// 42 | /// 43 | public void Warning(string message, params object[] args) 44 | { 45 | //Null Logger, so no messages are acutally sent 46 | } 47 | 48 | /// 49 | /// Error log messsages 50 | /// 51 | /// 52 | /// 53 | public void Error(string message, params object[] args) 54 | { 55 | //Null Logger, so no messages are acutally sent 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /DiscordRPC/Helper/BackoffDelay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace DiscordRPC.Helper 7 | { 8 | 9 | internal class BackoffDelay 10 | { 11 | /// 12 | /// The maximum time the backoff can reach 13 | /// 14 | public int Maximum { get; private set; } 15 | 16 | /// 17 | /// The minimum time the backoff can start at 18 | /// 19 | public int Minimum { get; private set; } 20 | 21 | /// 22 | /// The current time of the backoff 23 | /// 24 | public int Current { get { return _current; } } 25 | private int _current; 26 | 27 | /// 28 | /// The current number of failures 29 | /// 30 | public int Fails { get { return _fails; } } 31 | private int _fails; 32 | 33 | /// 34 | /// The random generator 35 | /// 36 | public Random Random { get; set; } 37 | 38 | private BackoffDelay() { } 39 | public BackoffDelay(int min, int max) : this(min, max, new Random()) { } 40 | public BackoffDelay(int min, int max, Random random) 41 | { 42 | this.Minimum = min; 43 | this.Maximum = max; 44 | 45 | this._current = min; 46 | this._fails = 0; 47 | this.Random = random; 48 | } 49 | 50 | /// 51 | /// Resets the backoff 52 | /// 53 | public void Reset() 54 | { 55 | _fails = 0; 56 | _current = Minimum; 57 | } 58 | 59 | public int NextDelay() 60 | { 61 | //Increment the failures 62 | _fails++; 63 | 64 | double diff = (Maximum - Minimum) / 100f; 65 | _current = (int)Math.Floor(diff * _fails) + Minimum; 66 | 67 | 68 | return Math.Min(Math.Max(_current, Minimum), Maximum); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /DiscordRPC/RPC/Payload/PayloadEvent.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Converters; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace DiscordRPC.RPC.Payload 6 | { 7 | /// 8 | /// Used for Discord IPC Events 9 | /// 10 | internal class EventPayload : IPayload 11 | { 12 | /// 13 | /// The data the server sent too us 14 | /// 15 | [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] 16 | public JObject Data { get; set; } 17 | 18 | /// 19 | /// The type of event the server sent 20 | /// 21 | [JsonProperty("evt"), JsonConverter(typeof(EnumSnakeCaseConverter))] 22 | public ServerEvent? Event { get; set; } 23 | 24 | /// 25 | /// Creates a payload with empty data 26 | /// 27 | public EventPayload() : base() { Data = null; } 28 | 29 | /// 30 | /// Creates a payload with empty data and a set nonce 31 | /// 32 | /// 33 | public EventPayload(long nonce) : base(nonce) { Data = null; } 34 | 35 | /// 36 | /// Gets the object stored within the Data 37 | /// 38 | /// 39 | /// 40 | public T GetObject() 41 | { 42 | if (Data == null) return default(T); 43 | return Data.ToObject(); 44 | } 45 | 46 | /// 47 | /// Converts the object into a human readable string 48 | /// 49 | /// 50 | public override string ToString() 51 | { 52 | return "Event " + base.ToString() + ", Event: " + (Event.HasValue ? Event.ToString() : "N/A"); 53 | } 54 | } 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /DiscordRPC/Message/MessageType.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace DiscordRPC.Message 3 | { 4 | /// 5 | /// Type of message. 6 | /// 7 | public enum MessageType 8 | { 9 | /// 10 | /// The Discord Client is ready to send and receive messages. 11 | /// 12 | Ready, 13 | 14 | /// 15 | /// The connection to the Discord Client is lost. The connection will remain close and unready to accept messages until the Ready event is called again. 16 | /// 17 | Close, 18 | 19 | /// 20 | /// A error has occured during the transmission of a message. For example, if a bad Rich Presence payload is sent, this event will be called explaining what went wrong. 21 | /// 22 | Error, 23 | 24 | /// 25 | /// The Discord Client has updated the presence. 26 | /// 27 | PresenceUpdate, 28 | 29 | /// 30 | /// The Discord Client has subscribed to an event. 31 | /// 32 | Subscribe, 33 | 34 | /// 35 | /// The Discord Client has unsubscribed from an event. 36 | /// 37 | Unsubscribe, 38 | 39 | /// 40 | /// The Discord Client wishes for this process to join a game. 41 | /// 42 | Join, 43 | 44 | /// 45 | /// The Discord Client wishes for this process to spectate a game. 46 | /// 47 | Spectate, 48 | 49 | /// 50 | /// Another discord user requests permission to join this game. 51 | /// 52 | JoinRequest, 53 | 54 | /// 55 | /// The connection to the discord client was succesfull. This is called before . 56 | /// 57 | ConnectionEstablished, 58 | 59 | /// 60 | /// Failed to establish any connection with discord. Discord is potentially not running? 61 | /// 62 | ConnectionFailed 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /DiscordRPC/IO/INamedPipeClient.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace DiscordRPC.IO 8 | { 9 | /// 10 | /// Pipe Client used to communicate with Discord. 11 | /// 12 | public interface INamedPipeClient : IDisposable 13 | { 14 | 15 | /// 16 | /// The logger for the Pipe client to use 17 | /// 18 | ILogger Logger { get; set; } 19 | 20 | /// 21 | /// Is the pipe client currently connected? 22 | /// 23 | bool IsConnected { get; } 24 | 25 | /// 26 | /// The pipe the client is currently connected too 27 | /// 28 | int ConnectedPipe { get; } 29 | 30 | /// 31 | /// Attempts to connect to the pipe. If 0-9 is passed to pipe, it should try to only connect to the specified pipe. If -1 is passed, the pipe will find the first available pipe. 32 | /// 33 | /// If -1 is passed, the pipe will find the first available pipe, otherwise it connects to the pipe that was supplied 34 | /// 35 | bool Connect(int pipe); 36 | 37 | /// 38 | /// Reads a frame if there is one available. Returns false if there is none. This should be non blocking (aka use a Peek first). 39 | /// 40 | /// The frame that has been read. Will be default(PipeFrame) if it fails to read 41 | /// Returns true if a frame has been read, otherwise false. 42 | bool ReadFrame(out PipeFrame frame); 43 | 44 | /// 45 | /// Writes the frame to the pipe. Returns false if any errors occur. 46 | /// 47 | /// The frame to be written 48 | bool WriteFrame(PipeFrame frame); 49 | 50 | /// 51 | /// Closes the connection 52 | /// 53 | void Close(); 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /DiscordRPC/Registry/MacUriSchemeCreator.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace DiscordRPC.Registry 10 | { 11 | internal class MacUriSchemeCreator : IUriSchemeCreator 12 | { 13 | private ILogger logger; 14 | public MacUriSchemeCreator(ILogger logger) 15 | { 16 | this.logger = logger; 17 | } 18 | 19 | public bool RegisterUriScheme(UriSchemeRegister register) 20 | { 21 | //var home = Environment.GetEnvironmentVariable("HOME"); 22 | //if (string.IsNullOrEmpty(home)) return; //TODO: Log Error 23 | 24 | string exe = register.ExecutablePath; 25 | if (string.IsNullOrEmpty(exe)) 26 | { 27 | logger.Error("Failed to register because the application could not be located."); 28 | return false; 29 | } 30 | 31 | logger.Trace("Registering Steam Command"); 32 | 33 | //Prepare the command 34 | string command = exe; 35 | if (register.UsingSteamApp) command = "steam://rungameid/" + register.SteamAppID; 36 | else logger.Warning("This library does not fully support MacOS URI Scheme Registration."); 37 | 38 | //get the folder ready 39 | string filepath = "~/Library/Application Support/discord/games"; 40 | var directory = Directory.CreateDirectory(filepath); 41 | if (!directory.Exists) 42 | { 43 | logger.Error("Failed to register because {0} does not exist", filepath); 44 | return false; 45 | } 46 | 47 | //Write the contents to file 48 | File.WriteAllText(filepath + "/" + register.ApplicationID + ".json", "{ \"command\": \"" + command + "\" }"); 49 | logger.Trace("Registered {0}, {1}", filepath + "/" + register.ApplicationID + ".json", command); 50 | return true; 51 | } 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /DiscordRPC/Exceptions/StringOutOfRangeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace DiscordRPC.Exceptions 7 | { 8 | /// 9 | /// A StringOutOfRangeException is thrown when the length of a string exceeds the allowed limit. 10 | /// 11 | public class StringOutOfRangeException : Exception 12 | { 13 | /// 14 | /// Maximum length the string is allowed to be. 15 | /// 16 | public int MaximumLength { get; private set; } 17 | 18 | /// 19 | /// Minimum length the string is allowed to be. 20 | /// 21 | public int MinimumLength { get; private set; } 22 | 23 | /// 24 | /// Creates a new string out of range exception with a range of min to max and a custom message 25 | /// 26 | /// The custom message 27 | /// Minimum length the string can be 28 | /// Maximum length the string can be 29 | internal StringOutOfRangeException(string message, int min, int max) : base(message) 30 | { 31 | MinimumLength = min; 32 | MaximumLength = max; 33 | } 34 | 35 | /// 36 | /// Creates a new sting out of range exception with a range of min to max 37 | /// 38 | /// 39 | /// 40 | internal StringOutOfRangeException(int minumum, int max) 41 | : this("Length of string is out of range. Expected a value between " + minumum + " and " + max, minumum, max) { } 42 | 43 | /// 44 | /// Creates a new sting out of range exception with a range of 0 to max 45 | /// 46 | /// 47 | internal StringOutOfRangeException(int max) 48 | : this("Length of string is out of range. Expected a value with a maximum length of " + max, 0, max) { } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /DiscordRPC/Message/ErrorMessage.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace DiscordRPC.Message 4 | { 5 | /// 6 | /// Created when a error occurs within the ipc and it is sent to the client. 7 | /// 8 | public class ErrorMessage : IMessage 9 | { 10 | /// 11 | /// The type of message received from discord 12 | /// 13 | public override MessageType Type { get { return MessageType.Error; } } 14 | 15 | /// 16 | /// The Discord error code. 17 | /// 18 | [JsonProperty("code")] 19 | public ErrorCode Code { get; internal set; } 20 | 21 | /// 22 | /// The message associated with the error code. 23 | /// 24 | [JsonProperty("message")] 25 | public string Message { get; internal set; } 26 | 27 | } 28 | 29 | /// 30 | /// The error message received by discord. See https://discordapp.com/developers/docs/topics/rpc#rpc-server-payloads-rpc-errors for documentation 31 | /// 32 | public enum ErrorCode 33 | { 34 | //Pipe Error Codes 35 | /// Pipe was Successful 36 | Success = 0, 37 | 38 | ///The pipe had an exception 39 | PipeException = 1, 40 | 41 | ///The pipe received corrupted data 42 | ReadCorrupt = 2, 43 | 44 | //Custom Error Code 45 | ///The functionality was not yet implemented 46 | NotImplemented = 10, 47 | 48 | //Discord RPC error codes 49 | ///Unkown Discord error 50 | UnkownError = 1000, 51 | 52 | ///Invalid Payload received 53 | InvalidPayload = 4000, 54 | 55 | ///Invalid command was sent 56 | InvalidCommand = 4002, 57 | 58 | /// Invalid event was sent 59 | InvalidEvent = 4004, 60 | 61 | /* 62 | InvalidGuild = 4003, 63 | InvalidChannel = 4005, 64 | InvalidPermissions = 4006, 65 | InvalidClientID = 4007, 66 | InvalidOrigin = 4008, 67 | InvalidToken = 4009, 68 | InvalidUser = 4010, 69 | OAuth2Error = 5000, 70 | SelectChannelTimeout = 5001, 71 | GetGuildTimeout = 5002, 72 | SelectVoiceForceRequired = 5003, 73 | CaptureShortcutAlreadyListening = 5004 74 | */ 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /DiscordRPC/DiscordRPC.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DiscordRichPresence 5 | 0.0.0 6 | Lachee 7 | Lachee 8 | Discord Rich Presence 9 | 10 | A .NET implementation of Discord's Rich Presence functionality. This library supports all features of the Rich Presence that the official C++ library supports, plus a few extra. 11 | 12 | "Players love to show off what they are playing with Discord’s status feature. With Rich Presence you can add beautiful art and detailed information to show off your game even more. This lets players know what their friends are doing, so they can decide to ask to join in and play together." 13 | - Discord 14 | 15 | This is an unoffical, non-discord supported library. 16 | 17 | Discord Rich Presence for your .NET project 18 | MIT 19 | https://github.com/Lachee/discord-rpc-csharp 20 | false 21 | Copyright (c) Lachee 2018 22 | 23 | Discord Discord Rich Presence RPC Discord-RPC 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /DiscordRPC/Converters/EnumSnakeCaseConverter.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Helper; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace DiscordRPC.Converters 8 | { 9 | /// 10 | /// Converts enums with the into Json friendly terms. 11 | /// 12 | internal class EnumSnakeCaseConverter : JsonConverter 13 | { 14 | public override bool CanConvert(Type objectType) 15 | { 16 | return objectType.IsEnum; 17 | } 18 | 19 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 20 | { 21 | if (reader.Value == null) return null; 22 | 23 | object val = null; 24 | if (TryParseEnum(objectType, (string)reader.Value, out val)) 25 | return val; 26 | 27 | return existingValue; 28 | } 29 | 30 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 31 | { 32 | var enumtype = value.GetType(); 33 | var name = Enum.GetName(enumtype, value); 34 | 35 | //Get each member and look for hte correct one 36 | var members = enumtype.GetMembers(BindingFlags.Public | BindingFlags.Static); 37 | foreach (var m in members) 38 | { 39 | if (m.Name.Equals(name)) 40 | { 41 | var attributes = m.GetCustomAttributes(typeof(EnumValueAttribute), true); 42 | if (attributes.Length > 0) 43 | { 44 | name = ((EnumValueAttribute)attributes[0]).Value; 45 | } 46 | } 47 | } 48 | 49 | writer.WriteValue(name); 50 | } 51 | 52 | 53 | public bool TryParseEnum(Type enumType, string str, out object obj) 54 | { 55 | //Make sure the string isn;t null 56 | if (str == null) 57 | { 58 | obj = null; 59 | return false; 60 | } 61 | 62 | //Get the real type 63 | Type type = enumType; 64 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) 65 | type = type.GetGenericArguments().First(); 66 | 67 | //Make sure its actually a enum 68 | if (!type.IsEnum) 69 | { 70 | obj = null; 71 | return false; 72 | } 73 | 74 | 75 | //Get each member and look for hte correct one 76 | var members = type.GetMembers(BindingFlags.Public | BindingFlags.Static); 77 | foreach (var m in members) 78 | { 79 | var attributes = m.GetCustomAttributes(typeof(EnumValueAttribute), true); 80 | foreach(var a in attributes) 81 | { 82 | var enumval = (EnumValueAttribute)a; 83 | if (str.Equals(enumval.Value)) 84 | { 85 | obj = Enum.Parse(type, m.Name, ignoreCase: true); 86 | 87 | return true; 88 | } 89 | } 90 | } 91 | 92 | //We failed 93 | obj = null; 94 | return false; 95 | } 96 | 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /DiscordRPC/Helper/StringTools.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace DiscordRPC.Helper 6 | { 7 | /// 8 | /// Collectin of helpful string extensions 9 | /// 10 | public static class StringTools 11 | { 12 | /// 13 | /// Will return null if the string is whitespace, otherwise it will return the string. 14 | /// 15 | /// The string to check 16 | /// Null if the string is empty, otherwise the string 17 | public static string GetNullOrString(this string str) 18 | { 19 | return str.Length == 0 || string.IsNullOrEmpty(str.Trim()) ? null : str; 20 | } 21 | 22 | /// 23 | /// Does the string fit within the given amount of bytes? Uses UTF8 encoding. 24 | /// 25 | /// The string to check 26 | /// The maximum number of bytes the string can take up 27 | /// True if the string fits within the number of bytes 28 | public static bool WithinLength(this string str, int bytes) 29 | { 30 | return str.WithinLength(bytes, Encoding.UTF8); 31 | } 32 | 33 | /// 34 | /// Does the string fit within the given amount of bytes? 35 | /// 36 | /// The string to check 37 | /// The maximum number of bytes the string can take up 38 | /// The encoding to count the bytes with 39 | /// True if the string fits within the number of bytes 40 | public static bool WithinLength(this string str, int bytes, Encoding encoding) 41 | { 42 | return encoding.GetByteCount(str) <= bytes; 43 | } 44 | 45 | 46 | /// 47 | /// Converts the string into UpperCamelCase (Pascal Case). 48 | /// 49 | /// The string to convert 50 | /// 51 | public static string ToCamelCase(this string str) 52 | { 53 | if (str == null) return null; 54 | 55 | return str.ToLower() 56 | .Split(new[] { "_", " " }, StringSplitOptions.RemoveEmptyEntries) 57 | .Select(s => char.ToUpper(s[0]) + s.Substring(1, s.Length - 1)) 58 | .Aggregate(string.Empty, (s1, s2) => s1 + s2); 59 | } 60 | 61 | /// 62 | /// Converts the string into UPPER_SNAKE_CASE 63 | /// 64 | /// The string to convert 65 | /// 66 | public static string ToSnakeCase(this string str) 67 | { 68 | if (str == null) return null; 69 | var concat = string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString()).ToArray()); 70 | return concat.ToUpper(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /DiscordRPC/Logging/FileLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace DiscordRPC.Logging 7 | { 8 | /// 9 | /// Logs the outputs to a file 10 | /// 11 | public class FileLogger : ILogger 12 | { 13 | /// 14 | /// The level of logging to apply to this logger. 15 | /// 16 | public LogLevel Level { get; set; } 17 | 18 | /// 19 | /// Should the output be coloured? 20 | /// 21 | public string File { get; set; } 22 | 23 | private object filelock; 24 | 25 | /// 26 | /// Creates a new instance of the file logger 27 | /// 28 | /// The path of the log file. 29 | public FileLogger(string path) 30 | : this(path, LogLevel.Info) { } 31 | 32 | /// 33 | /// Creates a new instance of the file logger 34 | /// 35 | /// The path of the log file. 36 | /// The level to assign to the logger. 37 | public FileLogger(string path, LogLevel level) 38 | { 39 | Level = level; 40 | File = path; 41 | filelock = new object(); 42 | } 43 | 44 | 45 | /// 46 | /// Informative log messages 47 | /// 48 | /// 49 | /// 50 | public void Trace(string message, params object[] args) 51 | { 52 | if (Level > LogLevel.Trace) return; 53 | lock (filelock) System.IO.File.AppendAllText(File, "\r\nTRCE: " + (args.Length > 0 ? string.Format(message, args) : message)); 54 | } 55 | 56 | /// 57 | /// Informative log messages 58 | /// 59 | /// 60 | /// 61 | public void Info(string message, params object[] args) 62 | { 63 | if (Level > LogLevel.Info) return; 64 | lock(filelock) System.IO.File.AppendAllText(File, "\r\nINFO: " + (args.Length > 0 ? string.Format(message, args) : message)); 65 | } 66 | 67 | /// 68 | /// Warning log messages 69 | /// 70 | /// 71 | /// 72 | public void Warning(string message, params object[] args) 73 | { 74 | if (Level > LogLevel.Warning) return; 75 | lock (filelock) 76 | System.IO.File.AppendAllText(File, "\r\nWARN: " + (args.Length > 0 ? string.Format(message, args) : message)); 77 | } 78 | 79 | /// 80 | /// Error log messsages 81 | /// 82 | /// 83 | /// 84 | public void Error(string message, params object[] args) 85 | { 86 | if (Level > LogLevel.Error) return; 87 | lock (filelock) 88 | System.IO.File.AppendAllText(File, "\r\nERR : " + (args.Length > 0 ? string.Format(message, args) : message)); 89 | } 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /DiscordRPC/Logging/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace DiscordRPC.Logging 7 | { 8 | /// 9 | /// Logs the outputs to the console using 10 | /// 11 | public class ConsoleLogger : ILogger 12 | { 13 | /// 14 | /// The level of logging to apply to this logger. 15 | /// 16 | public LogLevel Level { get; set; } 17 | 18 | /// 19 | /// Should the output be coloured? 20 | /// 21 | public bool Coloured { get; set; } 22 | 23 | /// 24 | /// A alias too 25 | /// 26 | public bool Colored { get { return Coloured; } set { Coloured = value; } } 27 | 28 | /// 29 | /// Creates a new instance of a Console Logger. 30 | /// 31 | public ConsoleLogger() 32 | { 33 | this.Level = LogLevel.Info; 34 | Coloured = false; 35 | } 36 | 37 | /// 38 | /// Creates a new instance of a Console Logger with a set log level 39 | /// 40 | /// 41 | /// 42 | public ConsoleLogger(LogLevel level, bool coloured = false) 43 | { 44 | Level = level; 45 | Coloured = coloured; 46 | } 47 | 48 | /// 49 | /// Informative log messages 50 | /// 51 | /// 52 | /// 53 | public void Trace(string message, params object[] args) 54 | { 55 | if (Level > LogLevel.Trace) return; 56 | 57 | if (Coloured) Console.ForegroundColor = ConsoleColor.Gray; 58 | Console.WriteLine("TRACE: " + message, args); 59 | } 60 | 61 | /// 62 | /// Informative log messages 63 | /// 64 | /// 65 | /// 66 | public void Info(string message, params object[] args) 67 | { 68 | if (Level > LogLevel.Info) return; 69 | 70 | if (Coloured) Console.ForegroundColor = ConsoleColor.White; 71 | Console.WriteLine("INFO: " + message, args); 72 | } 73 | 74 | /// 75 | /// Warning log messages 76 | /// 77 | /// 78 | /// 79 | public void Warning(string message, params object[] args) 80 | { 81 | if (Level > LogLevel.Warning) return; 82 | 83 | if (Coloured) Console.ForegroundColor = ConsoleColor.Yellow; 84 | Console.WriteLine("WARN: " + message, args); 85 | } 86 | 87 | /// 88 | /// Error log messsages 89 | /// 90 | /// 91 | /// 92 | public void Error(string message, params object[] args) 93 | { 94 | if (Level > LogLevel.Error) return; 95 | 96 | if (Coloured) Console.ForegroundColor = ConsoleColor.Red; 97 | Console.WriteLine("ERR : " + message, args); 98 | } 99 | 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /DiscordRPC/Registry/UriScheme.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Logging; 2 | using System; 3 | using System.Diagnostics; 4 | 5 | namespace DiscordRPC.Registry 6 | { 7 | internal class UriSchemeRegister 8 | { 9 | /// 10 | /// The ID of the Discord App to register 11 | /// 12 | public string ApplicationID { get; set; } 13 | 14 | /// 15 | /// Optional Steam App ID to register. If given a value, then the game will launch through steam instead of Discord. 16 | /// 17 | public string SteamAppID { get; set; } 18 | 19 | /// 20 | /// Is this register using steam? 21 | /// 22 | public bool UsingSteamApp { get { return !string.IsNullOrEmpty(SteamAppID) && SteamAppID != ""; } } 23 | 24 | /// 25 | /// The full executable path of the application. 26 | /// 27 | public string ExecutablePath { get; set; } 28 | 29 | private ILogger _logger; 30 | public UriSchemeRegister(ILogger logger, string applicationID, string steamAppID = null, string executable = null) 31 | { 32 | _logger = logger; 33 | ApplicationID = applicationID.Trim(); 34 | SteamAppID = steamAppID != null ? steamAppID.Trim() : null; 35 | ExecutablePath = executable ?? GetApplicationLocation(); 36 | } 37 | 38 | /// 39 | /// Registers the URI scheme, using the correct creator for the correct platform 40 | /// 41 | public bool RegisterUriScheme() 42 | { 43 | //Get the creator 44 | IUriSchemeCreator creator = null; 45 | switch(Environment.OSVersion.Platform) 46 | { 47 | case PlatformID.Win32Windows: 48 | case PlatformID.Win32S: 49 | case PlatformID.Win32NT: 50 | case PlatformID.WinCE: 51 | _logger.Trace("Creating Windows Scheme Creator"); 52 | creator = new WindowsUriSchemeCreator(_logger); 53 | break; 54 | 55 | case PlatformID.Unix: 56 | _logger.Trace("Creating Unix Scheme Creator"); 57 | creator = new UnixUriSchemeCreator(_logger); 58 | break; 59 | 60 | case PlatformID.MacOSX: 61 | _logger.Trace("Creating MacOSX Scheme Creator"); 62 | creator = new MacUriSchemeCreator(_logger); 63 | break; 64 | 65 | default: 66 | _logger.Error("Unkown Platform: " + Environment.OSVersion.Platform); 67 | throw new PlatformNotSupportedException("Platform does not support registration."); 68 | } 69 | 70 | //Regiser the app 71 | if (creator.RegisterUriScheme(this)) 72 | { 73 | _logger.Info("URI scheme registered."); 74 | return true; 75 | } 76 | 77 | return false; 78 | } 79 | 80 | /// 81 | /// Gets the FileName for the currently executing application 82 | /// 83 | /// 84 | public static string GetApplicationLocation() 85 | { 86 | return Process.GetCurrentProcess().MainModule.FileName; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /DiscordRPC/Registry/UnixUriSchemeCreator.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace DiscordRPC.Registry 10 | { 11 | internal class UnixUriSchemeCreator : IUriSchemeCreator 12 | { 13 | private ILogger logger; 14 | public UnixUriSchemeCreator(ILogger logger) 15 | { 16 | this.logger = logger; 17 | } 18 | 19 | public bool RegisterUriScheme(UriSchemeRegister register) 20 | { 21 | var home = Environment.GetEnvironmentVariable("HOME"); 22 | if (string.IsNullOrEmpty(home)) 23 | { 24 | logger.Error("Failed to register because the HOME variable was not set."); 25 | return false; 26 | } 27 | 28 | string exe = register.ExecutablePath; 29 | if (string.IsNullOrEmpty(exe)) 30 | { 31 | logger.Error("Failed to register because the application was not located."); 32 | return false; 33 | } 34 | 35 | //Prepare the command 36 | string command = null; 37 | if (register.UsingSteamApp) 38 | { 39 | //A steam command isntead 40 | command = "xdg-open steam://rungameid/" + register.SteamAppID; 41 | } 42 | else 43 | { 44 | //Just a regular discord command 45 | command = exe; 46 | } 47 | 48 | 49 | //Prepare the file 50 | string desktopFileFormat = 51 | @"[Desktop Entry] 52 | Name=Game {0} 53 | Exec={1} %u 54 | Type=Application 55 | NoDisplay=true 56 | Categories=Discord;Games; 57 | MimeType=x-scheme-handler/discord-{2}"; 58 | 59 | string file = string.Format(desktopFileFormat, register.ApplicationID, command, register.ApplicationID); 60 | 61 | //Prepare the path 62 | string filename = "/discord-" + register.ApplicationID + ".desktop"; 63 | string filepath = home + "/.local/share/applications"; 64 | var directory = Directory.CreateDirectory(filepath); 65 | if (!directory.Exists) 66 | { 67 | logger.Error("Failed to register because {0} does not exist", filepath); 68 | return false; 69 | } 70 | 71 | //Write the file 72 | File.WriteAllText(filepath + filename, file); 73 | 74 | //Register the Mime type 75 | if (!RegisterMime(register.ApplicationID)) 76 | { 77 | logger.Error("Failed to register because the Mime failed."); 78 | return false; 79 | } 80 | 81 | logger.Trace("Registered {0}, {1}, {2}", filepath + filename, file, command); 82 | return true; 83 | } 84 | 85 | private bool RegisterMime(string appid) 86 | { 87 | //Format the arguments 88 | string format = "default discord-{0}.desktop x-scheme-handler/discord-{0}"; 89 | string arguments = string.Format(format, appid); 90 | 91 | //Run the process and wait for response 92 | Process process = Process.Start("xdg-mime", arguments); 93 | process.WaitForExit(); 94 | 95 | //Return if succesful 96 | return process.ExitCode >= 0; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /DiscordRPC/Registry/WindowsUriSchemeCreator.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Logging; 2 | using System; 3 | 4 | namespace DiscordRPC.Registry 5 | { 6 | internal class WindowsUriSchemeCreator : IUriSchemeCreator 7 | { 8 | private ILogger logger; 9 | public WindowsUriSchemeCreator(ILogger logger) 10 | { 11 | this.logger = logger; 12 | } 13 | 14 | public bool RegisterUriScheme(UriSchemeRegister register) 15 | { 16 | if (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX) 17 | { 18 | throw new PlatformNotSupportedException("URI schemes can only be registered on Windows"); 19 | } 20 | 21 | //Prepare our location 22 | string location = register.ExecutablePath; 23 | if (location == null) 24 | { 25 | logger.Error("Failed to register application because the location was null."); 26 | return false; 27 | } 28 | 29 | //Prepare the Scheme, Friendly name, default icon and default command 30 | string scheme = "discord-" + register.ApplicationID; 31 | string friendlyName = "Run game " + register.ApplicationID + " protocol"; 32 | string defaultIcon = location; 33 | string command = location; 34 | 35 | //We have a steam ID, so attempt to replce the command with a steam command 36 | if (register.UsingSteamApp) 37 | { 38 | //Try to get the steam location. If found, set the command to a run steam instead. 39 | string steam = GetSteamLocation(); 40 | if (steam != null) 41 | command = string.Format("\"{0}\" steam://rungameid/{1}", steam, register.SteamAppID); 42 | 43 | } 44 | 45 | //Okay, now actually register it 46 | CreateUriScheme(scheme, friendlyName, defaultIcon, command); 47 | return true; 48 | } 49 | 50 | /// 51 | /// Creates the actual scheme 52 | /// 53 | /// 54 | /// 55 | /// 56 | /// 57 | private void CreateUriScheme(string scheme, string friendlyName, string defaultIcon, string command) 58 | { 59 | using (var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey("SOFTWARE\\Classes\\" + scheme)) 60 | { 61 | key.SetValue("", "URL:" + friendlyName); 62 | key.SetValue("URL Protocol", ""); 63 | 64 | using (var iconKey = key.CreateSubKey("DefaultIcon")) 65 | iconKey.SetValue("", defaultIcon); 66 | 67 | using (var commandKey = key.CreateSubKey("shell\\open\\command")) 68 | commandKey.SetValue("", command); 69 | } 70 | 71 | logger.Trace("Registered {0}, {1}, {2}", scheme, friendlyName, command); 72 | } 73 | 74 | /// 75 | /// Gets the current location of the steam client 76 | /// 77 | /// 78 | public string GetSteamLocation() 79 | { 80 | using (var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey("Software\\Valve\\Steam")) 81 | { 82 | if (key == null) return null; 83 | return key.GetValue("SteamExe") as string; 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /DiscordRPC/RPC/Payload/ServerEvent.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Converters; 2 | using System; 3 | using System.Runtime.Serialization; 4 | 5 | namespace DiscordRPC.RPC.Payload 6 | { 7 | /// 8 | /// See https://discordapp.com/developers/docs/topics/rpc#rpc-server-payloads-rpc-events for documentation 9 | /// 10 | internal enum ServerEvent 11 | { 12 | 13 | /// 14 | /// Sent when the server is ready to accept messages 15 | /// 16 | [EnumValue("READY")] 17 | Ready, 18 | 19 | /// 20 | /// Sent when something bad has happened 21 | /// 22 | [EnumValue("ERROR")] 23 | Error, 24 | 25 | /// 26 | /// Join Event 27 | /// 28 | [EnumValue("ACTIVITY_JOIN")] 29 | ActivityJoin, 30 | 31 | /// 32 | /// Spectate Event 33 | /// 34 | [EnumValue("ACTIVITY_SPECTATE")] 35 | ActivitySpectate, 36 | 37 | /// 38 | /// Request Event 39 | /// 40 | [EnumValue("ACTIVITY_JOIN_REQUEST")] 41 | ActivityJoinRequest, 42 | 43 | #if INCLUDE_FULL_RPC 44 | //Old things that are obsolete 45 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 46 | [EnumValue("GUILD_STATUS")] 47 | GuildStatus, 48 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 49 | [EnumValue("GUILD_CREATE")] 50 | GuildCreate, 51 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 52 | [EnumValue("CHANNEL_CREATE")] 53 | ChannelCreate, 54 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 55 | [EnumValue("VOICE_CHANNEL_SELECT")] 56 | VoiceChannelSelect, 57 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 58 | [EnumValue("VOICE_STATE_CREATED")] 59 | VoiceStateCreated, 60 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 61 | [EnumValue("VOICE_STATE_UPDATED")] 62 | VoiceStateUpdated, 63 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 64 | [EnumValue("VOICE_STATE_DELETE")] 65 | VoiceStateDelete, 66 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 67 | [EnumValue("VOICE_SETTINGS_UPDATE")] 68 | VoiceSettingsUpdate, 69 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 70 | [EnumValue("VOICE_CONNECTION_STATUS")] 71 | VoiceConnectionStatus, 72 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 73 | [EnumValue("SPEAKING_START")] 74 | SpeakingStart, 75 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 76 | [EnumValue("SPEAKING_STOP")] 77 | SpeakingStop, 78 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 79 | [EnumValue("MESSAGE_CREATE")] 80 | MessageCreate, 81 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 82 | [EnumValue("MESSAGE_UPDATE")] 83 | MessageUpdate, 84 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 85 | [EnumValue("MESSAGE_DELETE")] 86 | MessageDelete, 87 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 88 | [EnumValue("NOTIFICATION_CREATE")] 89 | NotificationCreate, 90 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 91 | [EnumValue("CAPTURE_SHORTCUT_CHANGE")] 92 | CaptureShortcutChange 93 | #endif 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /DiscordRPC/RPC/Payload/Command.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Converters; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace DiscordRPC.RPC.Payload 8 | { 9 | /// 10 | /// The possible commands that can be sent and received by the server. 11 | /// 12 | internal enum Command 13 | { 14 | /// 15 | /// event dispatch 16 | /// 17 | [EnumValue("DISPATCH")] 18 | Dispatch, 19 | 20 | /// 21 | /// Called to set the activity 22 | /// 23 | [EnumValue("SET_ACTIVITY")] 24 | SetActivity, 25 | 26 | /// 27 | /// used to subscribe to an RPC event 28 | /// 29 | [EnumValue("SUBSCRIBE")] 30 | Subscribe, 31 | 32 | /// 33 | /// used to unsubscribe from an RPC event 34 | /// 35 | [EnumValue("UNSUBSCRIBE")] 36 | Unsubscribe, 37 | 38 | /// 39 | /// Used to accept join requests. 40 | /// 41 | [EnumValue("SEND_ACTIVITY_JOIN_INVITE")] 42 | SendActivityJoinInvite, 43 | 44 | /// 45 | /// Used to reject join requests. 46 | /// 47 | [EnumValue("CLOSE_ACTIVITY_JOIN_REQUEST")] 48 | CloseActivityJoinRequest, 49 | 50 | /// 51 | /// used to authorize a new client with your app 52 | /// 53 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 54 | Authorize, 55 | 56 | /// 57 | /// used to authenticate an existing client with your app 58 | /// 59 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 60 | Authenticate, 61 | 62 | /// 63 | /// used to retrieve guild information from the client 64 | /// 65 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 66 | GetGuild, 67 | 68 | /// 69 | /// used to retrieve a list of guilds from the client 70 | /// 71 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 72 | GetGuilds, 73 | 74 | /// 75 | /// used to retrieve channel information from the client 76 | /// 77 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 78 | GetChannel, 79 | 80 | /// 81 | /// used to retrieve a list of channels for a guild from the client 82 | /// 83 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 84 | GetChannels, 85 | 86 | 87 | /// 88 | /// used to change voice settings of users in voice channels 89 | /// 90 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 91 | SetUserVoiceSettings, 92 | 93 | /// 94 | /// used to join or leave a voice channel, group dm, or dm 95 | /// 96 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 97 | SelectVoiceChannel, 98 | 99 | /// 100 | /// used to get the current voice channel the client is in 101 | /// 102 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 103 | GetSelectedVoiceChannel, 104 | 105 | /// 106 | /// used to join or leave a text channel, group dm, or dm 107 | /// 108 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 109 | SelectTextChannel, 110 | 111 | /// 112 | /// used to retrieve the client's voice settings 113 | /// 114 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 115 | GetVoiceSettings, 116 | 117 | /// 118 | /// used to set the client's voice settings 119 | /// 120 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 121 | SetVoiceSettings, 122 | 123 | /// 124 | /// used to capture a keyboard shortcut entered by the user RPC Events 125 | /// 126 | [Obsolete("This value is appart of the RPC API and is not supported by this library.", true)] 127 | CaptureShortcut 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /DiscordRPC/Events.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Message; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace DiscordRPC.Events 8 | { 9 | /// 10 | /// Called when the Discord Client is ready to send and receive messages. 11 | /// 12 | /// The Discord client handler that sent this event 13 | /// The arguments supplied with the event 14 | public delegate void OnReadyEvent(object sender, ReadyMessage args); 15 | 16 | /// 17 | /// Called when connection to the Discord Client is lost. The connection will remain close and unready to accept messages until the Ready event is called again. 18 | /// 19 | /// The Discord client handler that sent this event 20 | /// The arguments supplied with the event 21 | public delegate void OnCloseEvent(object sender, CloseMessage args); 22 | 23 | /// 24 | /// Called when a error has occured during the transmission of a message. For example, if a bad Rich Presence payload is sent, this event will be called explaining what went wrong. 25 | /// 26 | /// The Discord client handler that sent this event 27 | /// The arguments supplied with the event 28 | public delegate void OnErrorEvent(object sender, ErrorMessage args); 29 | 30 | /// 31 | /// Called when the Discord Client has updated the presence. 32 | /// 33 | /// The Discord client handler that sent this event 34 | /// The arguments supplied with the event 35 | public delegate void OnPresenceUpdateEvent(object sender, PresenceMessage args); 36 | 37 | /// 38 | /// Called when the Discord Client has subscribed to an event. 39 | /// 40 | /// The Discord client handler that sent this event 41 | /// The arguments supplied with the event 42 | public delegate void OnSubscribeEvent(object sender, SubscribeMessage args); 43 | 44 | /// 45 | /// Called when the Discord Client has unsubscribed from an event. 46 | /// 47 | /// The Discord client handler that sent this event 48 | /// The arguments supplied with the event 49 | public delegate void OnUnsubscribeEvent(object sender, UnsubscribeMessage args); 50 | 51 | /// 52 | /// Called when the Discord Client wishes for this process to join a game. 53 | /// 54 | /// The Discord client handler that sent this event 55 | /// The arguments supplied with the event 56 | public delegate void OnJoinEvent(object sender, JoinMessage args); 57 | 58 | /// 59 | /// Called when the Discord Client wishes for this process to spectate a game. 60 | /// 61 | /// The Discord client handler that sent this event 62 | /// The arguments supplied with the event 63 | public delegate void OnSpectateEvent(object sender, SpectateMessage args); 64 | 65 | /// 66 | /// Called when another discord user requests permission to join this game. 67 | /// 68 | /// The Discord client handler that sent this event 69 | /// The arguments supplied with the event 70 | public delegate void OnJoinRequestedEvent(object sender, JoinRequestMessage args); 71 | 72 | 73 | /// 74 | /// The connection to the discord client was succesfull. This is called before . 75 | /// 76 | /// The Discord client handler that sent this event 77 | /// The arguments supplied with the event 78 | public delegate void OnConnectionEstablishedEvent(object sender, ConnectionEstablishedMessage args); 79 | 80 | /// 81 | /// Failed to establish any connection with discord. Discord is potentially not running? 82 | /// 83 | /// The Discord client handler that sent this event 84 | /// The arguments supplied with the event 85 | public delegate void OnConnectionFailedEvent(object sender, ConnectionFailedMessage args); 86 | 87 | 88 | /// 89 | /// A RPC Message is received. 90 | /// 91 | /// The handler that sent this event 92 | /// The raw message from the RPC 93 | public delegate void OnRpcMessageEvent(object sender, IMessage msg); 94 | } 95 | -------------------------------------------------------------------------------- /DiscordRPC/IO/PipeFrame.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | 8 | namespace DiscordRPC.IO 9 | { 10 | /// 11 | /// A frame received and sent to the Discord client for RPC communications. 12 | /// 13 | public struct PipeFrame 14 | { 15 | /// 16 | /// The maxium size of a pipe frame (16kb). 17 | /// 18 | public static readonly int MAX_SIZE = 16 * 1024; 19 | 20 | /// 21 | /// The opcode of the frame 22 | /// 23 | public Opcode Opcode { get; set; } 24 | 25 | /// 26 | /// The length of the frame data 27 | /// 28 | public uint Length { get { return (uint) Data.Length; } } 29 | 30 | /// 31 | /// The data in the frame 32 | /// 33 | public byte[] Data { get; set; } 34 | 35 | /// 36 | /// The data represented as a string. 37 | /// 38 | public string Message 39 | { 40 | get { return GetMessage(); } 41 | set { SetMessage(value); } 42 | } 43 | 44 | /// 45 | /// Creates a new pipe frame instance 46 | /// 47 | /// The opcode of the frame 48 | /// The data of the frame that will be serialized as JSON 49 | public PipeFrame(Opcode opcode, object data) 50 | { 51 | //Set the opcode and a temp field for data 52 | Opcode = opcode; 53 | Data = null; 54 | 55 | //Set the data 56 | SetObject(data); 57 | } 58 | 59 | /// 60 | /// Gets the encoding used for the pipe frames 61 | /// 62 | public Encoding MessageEncoding { get { return Encoding.UTF8; } } 63 | 64 | /// 65 | /// Sets the data based of a string 66 | /// 67 | /// 68 | private void SetMessage(string str) { Data = MessageEncoding.GetBytes(str); } 69 | 70 | /// 71 | /// Gets a string based of the data 72 | /// 73 | /// 74 | private string GetMessage() { return MessageEncoding.GetString(Data); } 75 | 76 | /// 77 | /// Serializes the object into json string then encodes it into . 78 | /// 79 | /// 80 | public void SetObject(object obj) 81 | { 82 | string json = JsonConvert.SerializeObject(obj); 83 | SetMessage(json); 84 | } 85 | 86 | /// 87 | /// Sets the opcodes and serializes the object into a json string. 88 | /// 89 | /// 90 | /// 91 | public void SetObject(Opcode opcode, object obj) 92 | { 93 | Opcode = opcode; 94 | SetObject(obj); 95 | } 96 | 97 | /// 98 | /// Deserializes the data into the supplied type using JSON. 99 | /// 100 | /// The type to deserialize into 101 | /// 102 | public T GetObject() 103 | { 104 | string json = GetMessage(); 105 | return JsonConvert.DeserializeObject(json); 106 | } 107 | 108 | /// 109 | /// Attempts to read the contents of the frame from the stream 110 | /// 111 | /// 112 | /// 113 | public bool ReadStream(Stream stream) 114 | { 115 | //Try to read the opcode 116 | uint op; 117 | if (!TryReadUInt32(stream, out op)) 118 | return false; 119 | 120 | //Try to read the length 121 | uint len; 122 | if (!TryReadUInt32(stream, out len)) 123 | return false; 124 | 125 | uint readsRemaining = len; 126 | 127 | //Read the contents 128 | using (var mem = new MemoryStream()) 129 | { 130 | byte[] buffer = new byte[Min(2048, len)]; // read in chunks of 2KB 131 | int bytesRead; 132 | while ((bytesRead = stream.Read(buffer, 0, Min(buffer.Length, readsRemaining))) > 0) 133 | { 134 | readsRemaining -= len; 135 | mem.Write(buffer, 0, bytesRead); 136 | } 137 | 138 | byte[] result = mem.ToArray(); 139 | if (result.LongLength != len) 140 | return false; 141 | 142 | Opcode = (Opcode)op; 143 | Data = result; 144 | return true; 145 | } 146 | 147 | //fun 148 | //if (a != null) { do { yield return true; switch (a) { case 1: await new Task(); default: lock (obj) { foreach (b in c) { for (int d = 0; d < 1; d++) { a++; } } } while (a is typeof(int) || (new Class()) != null) } goto MY_LABEL; 149 | 150 | } 151 | 152 | /// 153 | /// Returns minimum value between a int and a unsigned int 154 | /// 155 | private int Min(int a, uint b) 156 | { 157 | if (b >= a) return a; 158 | return (int) b; 159 | } 160 | 161 | /// 162 | /// Attempts to read a UInt32 163 | /// 164 | /// 165 | /// 166 | /// 167 | private bool TryReadUInt32(Stream stream, out uint value) 168 | { 169 | //Read the bytes available to us 170 | byte[] bytes = new byte[4]; 171 | int cnt = stream.Read(bytes, 0, bytes.Length); 172 | 173 | //Make sure we actually have a valid value 174 | if (cnt != 4) 175 | { 176 | value = default(uint); 177 | return false; 178 | } 179 | 180 | value = BitConverter.ToUInt32(bytes, 0); 181 | return true; 182 | } 183 | 184 | /// 185 | /// Writes the frame into the target frame as one big byte block. 186 | /// 187 | /// 188 | public void WriteStream(Stream stream) 189 | { 190 | //Get all the bytes 191 | byte[] op = BitConverter.GetBytes((uint) Opcode); 192 | byte[] len = BitConverter.GetBytes(Length); 193 | 194 | //Copy it all into a buffer 195 | byte[] buff = new byte[op.Length + len.Length + Data.Length]; 196 | op.CopyTo(buff, 0); 197 | len.CopyTo(buff, op.Length); 198 | Data.CopyTo(buff, op.Length + len.Length); 199 | 200 | //Write it to the stream 201 | stream.Write(buff, 0, buff.Length); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | 290 | # Ignore any key files 291 | *.key 292 | 293 | * Ignore the resources except for .png 294 | [Rr]esources/* 295 | ![Rr]esources/*.png 296 | ![Rr]esources/*.md 297 | ![Rr]esources/*.txt 298 | 299 | # Cake - Uncomment if you are using it 300 | tools/ 301 | Thumbs.db -------------------------------------------------------------------------------- /DiscordRPC/Web/WebRPC.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Exceptions; 2 | using DiscordRPC.RPC; 3 | using DiscordRPC.RPC.Commands; 4 | using DiscordRPC.RPC.Payload; 5 | using Newtonsoft.Json; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Net; 9 | 10 | #if INCLUDE_WEB_RPC 11 | namespace DiscordRPC.Web 12 | { 13 | /// 14 | /// Handles HTTP Rich Presence Requests 15 | /// 16 | [System.Obsolete("Rich Presence over HTTP is no longer supported by Discord. See offical Rich Presence github for more information.")] 17 | public static class WebRPC 18 | { 19 | /// 20 | /// Sets the Rich Presence over the HTTP protocol. Does not support Join / Spectate and by default is blocking. 21 | /// 22 | /// The presence to send to discord 23 | /// The ID of the application 24 | /// The port the discord client is currently on. Specify this for testing. Will start scanning from supplied port. 25 | /// Returns the rich presence result from the server. This can be null if presence was set to be null, or if there was no valid response from the client. 26 | [System.Obsolete("Setting Rich Presence over HTTP is no longer supported by Discord. See offical Rich Presence github for more information.")] 27 | public static RichPresence SetRichPresence(RichPresence presence, string applicationID, int port = 6463) 28 | { 29 | try 30 | { 31 | RichPresence response; 32 | if (TrySetRichPresence(presence, out response, applicationID, port)) 33 | return response; 34 | 35 | return null; 36 | } 37 | catch (Exception) 38 | { 39 | throw; 40 | } 41 | } 42 | 43 | /// 44 | /// Attempts to set the Rich Presence over the HTTP protocol. Does not support Join / Specate and by default is blocking. 45 | /// 46 | /// The presence to send to discord 47 | /// The response object from the client 48 | /// The ID of the application 49 | /// The port the discord client is currently on. Specify this for testing. Will start scanning from supplied port. 50 | /// True if the response was valid from the server, otherwise false. 51 | [System.Obsolete("Setting Rich Presence over HTTP is no longer supported by Discord. See offical Rich Presence github for more information.")] 52 | public static bool TrySetRichPresence(RichPresence presence, out RichPresence response, string applicationID, int port = 6463) 53 | { 54 | //Validate the presence 55 | if (presence != null) 56 | { 57 | //Send valid presence 58 | //Validate the presence with our settings 59 | if (presence.HasSecrets()) 60 | throw new BadPresenceException("Cannot send a presence with secrets as HTTP endpoint does not suppport events."); 61 | 62 | if (presence.HasParty() && presence.Party.Max < presence.Party.Size) 63 | throw new BadPresenceException("Presence maximum party size cannot be smaller than the current size."); 64 | } 65 | 66 | //Iterate over the ports until the first succesfull one 67 | for (int p = port; p < 6472; p++) 68 | { 69 | //Prepare the url and json 70 | using (WebClient client = new WebClient()) 71 | { 72 | try 73 | { 74 | WebRequest request = PrepareRequest(presence, applicationID, p); 75 | client.Headers.Add("content-type", "application/json"); 76 | 77 | var result = client.UploadString(request.URL, request.Data); 78 | if (TryParseResponse(result, out response)) 79 | return true; 80 | } 81 | catch (Exception) 82 | { 83 | //Something went wrong, but we are just going to ignore it and try the next port. 84 | } 85 | } 86 | } 87 | 88 | //we failed, return null 89 | response = null; 90 | return false; 91 | } 92 | 93 | /// 94 | /// Attempts to parse the response of a Web Request to a rich presence 95 | /// 96 | /// The json data received by the client 97 | /// The parsed rich presence 98 | /// True if the parse was succesfull 99 | public static bool TryParseResponse(string json, out RichPresence response) 100 | { 101 | try 102 | { 103 | //Try to parse the JSON into a event 104 | EventPayload ev = JsonConvert.DeserializeObject(json); 105 | 106 | //We have a result, so parse the rich presence response and return it. 107 | if (ev != null) 108 | { 109 | //Parse the response into a rich presence response 110 | response = ev.GetObject(); 111 | return true; 112 | } 113 | 114 | }catch(Exception) { } 115 | 116 | //We failed. 117 | response = null; 118 | return false; 119 | } 120 | 121 | /// 122 | /// Prepares a struct containing data requried to make a succesful web client request to set the rich presence. 123 | /// 124 | /// The rich presence to set. 125 | /// The ID of the application the presence belongs too. 126 | /// The port the client is located on. The default port for the discord client is 6463, but it may move iteratively upto 6473 if the ports are unavailable. 127 | /// Returns a web request containing nessary data to make a POST request 128 | [System.Obsolete("WebRequests are no longer supported because of the removed HTTP functionality by Discord. See offical Rich Presence github for more information.")] 129 | public static WebRequest PrepareRequest(RichPresence presence, string applicationID, int port = 6463) 130 | { 131 | //Validate the presence 132 | if (presence != null) 133 | { 134 | //Send valid presence 135 | //Validate the presence with our settings 136 | if (presence.HasSecrets()) 137 | throw new BadPresenceException("Cannot send a presence with secrets as HTTP endpoint does not suppport events."); 138 | 139 | if (presence.HasParty() && presence.Party.Max < presence.Party.Size) 140 | throw new BadPresenceException("Presence maximum party size cannot be smaller than the current size."); 141 | } 142 | 143 | //Prepare some params 144 | int pid = System.Diagnostics.Process.GetCurrentProcess().Id; 145 | 146 | //Prepare the payload 147 | PresenceCommand command = new PresenceCommand() { PID = pid, Presence = presence }; 148 | var payload = command.PreparePayload(DateTime.UtcNow.ToFileTime()); 149 | 150 | string json = JsonConvert.SerializeObject(payload); 151 | 152 | string url = "http://127.0.0.1:" + port + "/rpc?v=" + RpcConnection.VERSION + "&client_id=" + applicationID; 153 | return new WebRequest(url, json); 154 | } 155 | } 156 | 157 | /// 158 | /// Details of a HTTP Post request that will set the rich presence. 159 | /// 160 | [System.Obsolete("Web Requests is no longer supported as Discord removed HTTP Rich Presence support. See offical Rich Presence github for more information.")] 161 | public struct WebRequest 162 | { 163 | private string _url; 164 | private string _json; 165 | private Dictionary _headers; 166 | 167 | /// 168 | /// The URL to send the POST request too 169 | /// 170 | public string URL { get { return _url; } } 171 | 172 | /// 173 | /// The JSON formatted body to send with the POST request 174 | /// 175 | public string Data { get { return _json; } } 176 | 177 | /// 178 | /// The headers to send with the body 179 | /// 180 | public Dictionary Headers { get { return _headers; } } 181 | 182 | internal WebRequest(string url, string json) 183 | { 184 | _url = url; 185 | _json = json; 186 | _headers = new Dictionary(); 187 | _headers.Add("content-type", "application/json"); 188 | } 189 | } 190 | } 191 | #endif -------------------------------------------------------------------------------- /DiscordRPC/User.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace DiscordRPC 8 | { 9 | /// 10 | /// Object representing a Discord user. This is used for join requests. 11 | /// 12 | public class User 13 | { 14 | /// 15 | /// Possible formats for avatars 16 | /// 17 | public enum AvatarFormat 18 | { 19 | /// 20 | /// Portable Network Graphics format (.png) 21 | /// Losses format that supports transparent avatars. Most recommended for stationary formats with wide support from many libraries. 22 | /// 23 | PNG, 24 | 25 | /// 26 | /// Joint Photographic Experts Group format (.jpeg) 27 | /// The format most cameras use. Lossy and does not support transparent avatars. 28 | /// 29 | JPEG, 30 | 31 | /// 32 | /// WebP format (.webp) 33 | /// Picture only version of WebM. Pronounced "weeb p". 34 | /// 35 | WebP, 36 | 37 | /// 38 | /// Graphics Interchange Format (.gif) 39 | /// Animated avatars that Discord Nitro users are able to use. If the user doesn't have an animated avatar, then it will just be a single frame gif. 40 | /// 41 | GIF //Gif, as in gift. 42 | } 43 | 44 | /// 45 | /// Possible square sizes of avatars. 46 | /// 47 | public enum AvatarSize 48 | { 49 | /// 16 x 16 pixels. 50 | x16 = 16, 51 | /// 32 x 32 pixels. 52 | x32 = 32, 53 | /// 64 x 64 pixels. 54 | x64 = 64, 55 | /// 128 x 128 pixels. 56 | x128 = 128, 57 | /// 256 x 256 pixels. 58 | x256 = 256, 59 | /// 512 x 512 pixels. 60 | x512 = 512, 61 | /// 1024 x 1024 pixels. 62 | x1024 = 1024, 63 | /// 2048 x 2048 pixels. 64 | x2048 = 2048 65 | } 66 | 67 | /// 68 | /// The snowflake ID of the user. 69 | /// 70 | [JsonProperty("id")] 71 | public ulong ID { get; private set; } 72 | 73 | /// 74 | /// The username of the player. 75 | /// 76 | [JsonProperty("username")] 77 | public string Username { get; private set; } 78 | 79 | /// 80 | /// The discriminator of the user. 81 | /// 82 | [JsonProperty("discriminator")] 83 | public int Discriminator { get; private set; } 84 | 85 | /// 86 | /// The avatar hash of the user. Too get a URL for the avatar, use the . This can be null if the user has no avatar. The will account for this and return the discord default. 87 | /// 88 | [JsonProperty("avatar")] 89 | public string Avatar { get; private set; } 90 | 91 | /// 92 | /// The flags on a users account, often represented as a badge. 93 | /// 94 | [JsonProperty("flags")] 95 | public Flag Flags { get; private set; } 96 | 97 | /// 98 | /// A flag on the user account 99 | /// 100 | [Flags] 101 | public enum Flag 102 | { 103 | /// No flag 104 | None = 0, 105 | 106 | /// Staff of Discord. 107 | Employee = 1 << 0, 108 | 109 | /// Partners of Discord. 110 | Partner = 1 << 1, 111 | 112 | /// Original HypeSquad which organise events. 113 | HypeSquad = 1 << 2, 114 | 115 | /// Bug Hunters that found and reported bugs in Discord. 116 | BugHunter = 1 << 3, 117 | 118 | //These 2 are mistery types 119 | //A = 1 << 4, 120 | //B = 1 << 5, 121 | 122 | /// The HypeSquad House of Bravery. 123 | HouseBravery = 1 << 6, 124 | 125 | /// The HypeSquad House of Brilliance. 126 | HouseBrilliance = 1 << 7, 127 | 128 | /// The HypeSquad House of Balance (the best one). 129 | HouseBalance = 1 << 8, 130 | 131 | /// Early Supporter of Discord and had Nitro before the store was released. 132 | EarlySupporter = 1 << 9, 133 | 134 | /// Apart of a team. 135 | /// Unclear if it is reserved for members that share a team with the current application. 136 | /// 137 | TeamUser = 1 << 10 138 | } 139 | 140 | /// 141 | /// The premium type of the user. 142 | /// 143 | [JsonProperty("premium_type")] 144 | public PremiumType Premium { get; private set; } 145 | 146 | /// 147 | /// Type of premium 148 | /// 149 | public enum PremiumType 150 | { 151 | /// No subscription to any forms of Nitro. 152 | None = 0, 153 | 154 | /// Nitro Classic subscription. Has chat perks and animated avatars. 155 | NitroClassic = 1, 156 | 157 | /// Nitro subscription. Has chat perks, animated avatars, server boosting, and access to free Nitro Games. 158 | Nitro = 2 159 | } 160 | 161 | /// 162 | /// The endpoint for the CDN. Normally cdn.discordapp.com. 163 | /// 164 | public string CdnEndpoint { get; private set; } 165 | 166 | /// 167 | /// Creates a new User instance. 168 | /// 169 | internal User() 170 | { 171 | CdnEndpoint = "cdn.discordapp.com"; 172 | } 173 | 174 | /// 175 | /// Updates the URL paths to the appropriate configuration 176 | /// 177 | /// The configuration received by the OnReady event. 178 | internal void SetConfiguration(Configuration configuration) 179 | { 180 | this.CdnEndpoint = configuration.CdnHost; 181 | } 182 | 183 | /// 184 | /// Gets a URL that can be used to download the user's avatar. If the user has not yet set their avatar, it will return the default one that discord is using. The default avatar only supports the format. 185 | /// 186 | /// The format of the target avatar 187 | /// The optional size of the avatar you wish for. Defaults to x128. 188 | /// 189 | public string GetAvatarURL(AvatarFormat format, AvatarSize size = AvatarSize.x128) 190 | { 191 | //Prepare the endpoint 192 | string endpoint = "/avatars/" + ID + "/" + Avatar; 193 | 194 | //The user has no avatar, so we better replace it with the default 195 | if (string.IsNullOrEmpty(Avatar)) 196 | { 197 | //Make sure we are only using PNG 198 | if (format != AvatarFormat.PNG) 199 | throw new BadImageFormatException("The user has no avatar and the requested format " + format.ToString() + " is not supported. (Only supports PNG)."); 200 | 201 | //Get the endpoint 202 | int descrim = Discriminator % 5; 203 | endpoint = "/embed/avatars/" + descrim; 204 | } 205 | 206 | //Finish of the endpoint 207 | return string.Format("https://{0}{1}{2}?size={3}", this.CdnEndpoint, endpoint, GetAvatarExtension(format), (int)size); 208 | } 209 | 210 | /// 211 | /// Returns the file extension of the specified format. 212 | /// 213 | /// The format to get the extention off 214 | /// Returns a period prefixed file extension. 215 | public string GetAvatarExtension(AvatarFormat format) 216 | { 217 | return "." + format.ToString().ToLowerInvariant(); 218 | } 219 | 220 | /// 221 | /// Formats the user into username#discriminator 222 | /// 223 | /// 224 | public override string ToString() 225 | { 226 | return Username + "#" + Discriminator.ToString("D4"); 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /DiscordRPC/IO/ManagedNamedPipeClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using DiscordRPC.Logging; 6 | using System.IO.Pipes; 7 | using System.Threading; 8 | using System.IO; 9 | 10 | namespace DiscordRPC.IO 11 | { 12 | /// 13 | /// A named pipe client using the .NET framework 14 | /// 15 | public sealed class ManagedNamedPipeClient : INamedPipeClient 16 | { 17 | /// 18 | /// Name format of the pipe 19 | /// 20 | const string PIPE_NAME = @"discord-ipc-{0}"; 21 | 22 | /// 23 | /// The logger for the Pipe client to use 24 | /// 25 | public ILogger Logger { get; set; } 26 | 27 | /// 28 | /// Checks if the client is connected 29 | /// 30 | public bool IsConnected 31 | { 32 | get 33 | { 34 | //This will trigger if the stream is disabled. This should prevent the lock check 35 | if (_isClosed) return false; 36 | lock (l_stream) 37 | { 38 | //We cannot be sure its still connected, so lets double check 39 | return _stream != null && _stream.IsConnected; 40 | } 41 | } 42 | } 43 | 44 | /// 45 | /// The pipe we are currently connected too. 46 | /// 47 | public int ConnectedPipe { get { return _connectedPipe; } } 48 | 49 | private int _connectedPipe; 50 | private NamedPipeClientStream _stream; 51 | 52 | private byte[] _buffer = new byte[PipeFrame.MAX_SIZE]; 53 | 54 | private Queue _framequeue = new Queue(); 55 | private object _framequeuelock = new object(); 56 | 57 | private volatile bool _isDisposed = false; 58 | private volatile bool _isClosed = true; 59 | 60 | private object l_stream = new object(); 61 | 62 | /// 63 | /// Creates a new instance of a Managed NamedPipe client. Doesn't connect to anything yet, just setups the values. 64 | /// 65 | public ManagedNamedPipeClient() 66 | { 67 | _buffer = new byte[PipeFrame.MAX_SIZE]; 68 | Logger = new NullLogger(); 69 | _stream = null; 70 | } 71 | 72 | /// 73 | /// Connects to the pipe 74 | /// 75 | /// 76 | /// 77 | public bool Connect(int pipe) 78 | { 79 | Logger.Trace("ManagedNamedPipeClient.Connection(" + pipe + ")"); 80 | 81 | if (_isDisposed) 82 | throw new ObjectDisposedException("NamedPipe"); 83 | 84 | if (pipe > 9) 85 | throw new ArgumentOutOfRangeException("pipe", "Argument cannot be greater than 9"); 86 | 87 | if (pipe < 0) 88 | { 89 | //Iterate until we connect to a pipe 90 | for (int i = 0; i < 10; i++) 91 | { 92 | if (AttemptConnection(i) || AttemptConnection(i, true)) 93 | { 94 | BeginReadStream(); 95 | return true; 96 | } 97 | } 98 | } 99 | else 100 | { 101 | //Attempt to connect to a specific pipe 102 | if (AttemptConnection(pipe) || AttemptConnection(pipe, true)) 103 | { 104 | BeginReadStream(); 105 | return true; 106 | } 107 | } 108 | 109 | //We failed to connect 110 | return false; 111 | } 112 | 113 | /// 114 | /// Attempts a new connection 115 | /// 116 | /// The pipe number to connect too. 117 | /// Should the connection to a sandbox be attempted? 118 | /// 119 | private bool AttemptConnection(int pipe, bool isSandbox = false) 120 | { 121 | if (_isDisposed) 122 | throw new ObjectDisposedException("_stream"); 123 | 124 | //If we are sandbox but we dont support sandbox, then skip 125 | string sandbox = isSandbox ? GetPipeSandbox() : ""; 126 | if (isSandbox && sandbox == null) 127 | { 128 | Logger.Trace("Skipping sandbox connection."); 129 | return false; 130 | } 131 | 132 | //Prepare the pipename 133 | Logger.Trace("Connection Attempt " + pipe + " (" + sandbox + ")"); 134 | string pipename = GetPipeName(pipe, sandbox); 135 | 136 | try 137 | { 138 | //Create the client 139 | lock (l_stream) 140 | { 141 | Logger.Info("Attempting to connect to " + pipename); 142 | _stream = new NamedPipeClientStream(".", pipename, PipeDirection.InOut, PipeOptions.Asynchronous); 143 | _stream.Connect(1000); 144 | 145 | //Spin for a bit while we wait for it to finish connecting 146 | Logger.Trace("Waiting for connection..."); 147 | do { Thread.Sleep(10); } while (!_stream.IsConnected); 148 | } 149 | 150 | //Store the value 151 | Logger.Info("Connected to " + pipename); 152 | _connectedPipe = pipe; 153 | _isClosed = false; 154 | } 155 | catch (Exception e) 156 | { 157 | //Something happened, try again 158 | //TODO: Log the failure condition 159 | Logger.Error("Failed connection to {0}. {1}", pipename, e.Message); 160 | Close(); 161 | } 162 | 163 | Logger.Trace("Done. Result: {0}", _isClosed); 164 | return !_isClosed; 165 | } 166 | 167 | /// 168 | /// Starts a read. Can be executed in another thread. 169 | /// 170 | private void BeginReadStream() 171 | { 172 | if (_isClosed) return; 173 | try 174 | { 175 | lock (l_stream) 176 | { 177 | //Make sure the stream is valid 178 | if (_stream == null || !_stream.IsConnected) return; 179 | 180 | Logger.Trace("Begining Read of {0} bytes", _buffer.Length); 181 | _stream.BeginRead(_buffer, 0, _buffer.Length, new AsyncCallback(EndReadStream), _stream.IsConnected); 182 | } 183 | } 184 | catch (ObjectDisposedException) 185 | { 186 | Logger.Warning("Attempted to start reading from a disposed pipe"); 187 | return; 188 | } 189 | catch (InvalidOperationException) 190 | { 191 | //The pipe has been closed 192 | Logger.Warning("Attempted to start reading from a closed pipe"); 193 | return; 194 | } 195 | catch (Exception e) 196 | { 197 | Logger.Error("An exception occured while starting to read a stream: {0}", e.Message); 198 | Logger.Error(e.StackTrace); 199 | } 200 | } 201 | 202 | /// 203 | /// Ends a read. Can be executed in another thread. 204 | /// 205 | /// 206 | private void EndReadStream(IAsyncResult callback) 207 | { 208 | Logger.Trace("Ending Read"); 209 | int bytes = 0; 210 | 211 | try 212 | { 213 | //Attempt to read the bytes, catching for IO exceptions or dispose exceptions 214 | lock (l_stream) 215 | { 216 | //Make sure the stream is still valid 217 | if (_stream == null || !_stream.IsConnected) return; 218 | 219 | //Read our btyes 220 | bytes = _stream.EndRead(callback); 221 | } 222 | } 223 | catch (IOException) 224 | { 225 | Logger.Warning("Attempted to end reading from a closed pipe"); 226 | return; 227 | } 228 | catch (NullReferenceException) 229 | { 230 | Logger.Warning("Attempted to read from a null pipe"); 231 | return; 232 | } 233 | catch (ObjectDisposedException) 234 | { 235 | Logger.Warning("Attemped to end reading from a disposed pipe"); 236 | return; 237 | } 238 | catch (Exception e) 239 | { 240 | Logger.Error("An exception occured while ending a read of a stream: {0}", e.Message); 241 | Logger.Error(e.StackTrace); 242 | return; 243 | } 244 | 245 | //How much did we read? 246 | Logger.Trace("Read {0} bytes", bytes); 247 | 248 | //Did we read anything? If we did we should enqueue it. 249 | if (bytes > 0) 250 | { 251 | //Load it into a memory stream and read the frame 252 | using (MemoryStream memory = new MemoryStream(_buffer, 0, bytes)) 253 | { 254 | try 255 | { 256 | PipeFrame frame = new PipeFrame(); 257 | if (frame.ReadStream(memory)) 258 | { 259 | Logger.Trace("Read a frame: {0}", frame.Opcode); 260 | 261 | //Enqueue the stream 262 | lock (_framequeuelock) 263 | _framequeue.Enqueue(frame); 264 | } 265 | else 266 | { 267 | //TODO: Enqueue a pipe close event here as we failed to read something. 268 | Logger.Error("Pipe failed to read from the data received by the stream."); 269 | Close(); 270 | } 271 | } 272 | catch (Exception e) 273 | { 274 | Logger.Error("A exception has occured while trying to parse the pipe data: " + e.Message); 275 | Close(); 276 | } 277 | } 278 | } 279 | else 280 | { 281 | //If we read 0 bytes, its probably a broken pipe. However, I have only confirmed this is the case for MacOSX. 282 | // I have added this check here just so the Windows builds are not effected and continue to work as expected. 283 | if (IsUnix()) 284 | { 285 | Logger.Error("Empty frame was read on " + Environment.OSVersion.ToString() + ", aborting."); 286 | Close(); 287 | } 288 | else 289 | { 290 | Logger.Warning("Empty frame was read. Please send report to Lachee."); 291 | } 292 | } 293 | 294 | //We are still connected, so continue to read 295 | if (!_isClosed && IsConnected) 296 | { 297 | Logger.Trace("Starting another read"); 298 | BeginReadStream(); 299 | } 300 | } 301 | 302 | /// 303 | /// Reads a frame, returning false if none are available 304 | /// 305 | /// 306 | /// 307 | public bool ReadFrame(out PipeFrame frame) 308 | { 309 | if (_isDisposed) 310 | throw new ObjectDisposedException("_stream"); 311 | 312 | //Check the queue, returning the pipe if we have anything available. Otherwise null. 313 | lock (_framequeuelock) 314 | { 315 | if (_framequeue.Count == 0) 316 | { 317 | //We found nothing, so just default and return null 318 | frame = default(PipeFrame); 319 | return false; 320 | } 321 | 322 | //Return the dequed frame 323 | frame = _framequeue.Dequeue(); 324 | return true; 325 | } 326 | } 327 | 328 | /// 329 | /// Writes a frame to the pipe 330 | /// 331 | /// 332 | /// 333 | public bool WriteFrame(PipeFrame frame) 334 | { 335 | if (_isDisposed) 336 | throw new ObjectDisposedException("_stream"); 337 | 338 | //Write the frame. We are assuming proper duplex connection here 339 | if (_isClosed || !IsConnected) 340 | { 341 | Logger.Error("Failed to write frame because the stream is closed"); 342 | return false; 343 | } 344 | 345 | try 346 | { 347 | //Write the pipe 348 | //This can only happen on the main thread so it should be fine. 349 | frame.WriteStream(_stream); 350 | return true; 351 | } 352 | catch (IOException io) 353 | { 354 | Logger.Error("Failed to write frame because of a IO Exception: {0}", io.Message); 355 | } 356 | catch (ObjectDisposedException) 357 | { 358 | Logger.Warning("Failed to write frame as the stream was already disposed"); 359 | } 360 | catch (InvalidOperationException) 361 | { 362 | Logger.Warning("Failed to write frame because of a invalid operation"); 363 | } 364 | 365 | //We must have failed the try catch 366 | return false; 367 | } 368 | 369 | /// 370 | /// Closes the pipe 371 | /// 372 | public void Close() 373 | { 374 | //If we are already closed, jsut exit 375 | if (_isClosed) 376 | { 377 | Logger.Warning("Tried to close a already closed pipe."); 378 | return; 379 | } 380 | 381 | //flush and dispose 382 | try 383 | { 384 | //Wait for the stream object to become available. 385 | lock (l_stream) 386 | { 387 | if (_stream != null) 388 | { 389 | try 390 | { 391 | //Stream isn't null, so flush it and then dispose of it.\ 392 | // We are doing a catch here because it may throw an error during this process and we dont care if it fails. 393 | _stream.Flush(); 394 | _stream.Dispose(); 395 | } 396 | catch (Exception) 397 | { 398 | //We caught an error, but we dont care anyways because we are disposing of the stream. 399 | } 400 | 401 | //Make the stream null and set our flag. 402 | _stream = null; 403 | _isClosed = true; 404 | } 405 | else 406 | { 407 | //The stream is already null? 408 | Logger.Warning("Stream was closed, but no stream was available to begin with!"); 409 | } 410 | } 411 | } 412 | catch (ObjectDisposedException) 413 | { 414 | //ITs already been disposed 415 | Logger.Warning("Tried to dispose already disposed stream"); 416 | } 417 | finally 418 | { 419 | //For good measures, we will mark the pipe as closed anyways 420 | _isClosed = true; 421 | _connectedPipe = -1; 422 | } 423 | } 424 | 425 | /// 426 | /// Disposes of the stream 427 | /// 428 | public void Dispose() 429 | { 430 | //Prevent double disposing 431 | if (_isDisposed) return; 432 | 433 | //Close the stream (disposing of it too) 434 | if (!_isClosed) Close(); 435 | 436 | //Dispose of the stream if it hasnt been destroyed already. 437 | lock (l_stream) 438 | { 439 | if (_stream != null) 440 | { 441 | _stream.Dispose(); 442 | _stream = null; 443 | } 444 | } 445 | 446 | //Set our dispose flag 447 | _isDisposed = true; 448 | } 449 | 450 | /// 451 | /// Returns a platform specific path that Discord is hosting the IPC on. 452 | /// 453 | /// The pipe number. 454 | /// The sandbox the pipe is in. Leave blank for no sandbox. 455 | /// 456 | public static string GetPipeName(int pipe, string sandbox = "") 457 | { 458 | if (!IsUnix()) return sandbox + string.Format(PIPE_NAME, pipe); 459 | return Path.Combine(GetTemporaryDirectory(), sandbox + string.Format(PIPE_NAME, pipe)); 460 | } 461 | 462 | /// 463 | /// Gets the name of the possible sandbox enviroment the pipe might be located within. If the platform doesn't support sandboxed Discord, then it will return null. 464 | /// 465 | /// 466 | public static string GetPipeSandbox() 467 | { 468 | switch (Environment.OSVersion.Platform) 469 | { 470 | default: 471 | return null; 472 | case PlatformID.Unix: 473 | return "snap.discord/"; 474 | } 475 | } 476 | 477 | /// 478 | /// Gets the temporary path for the current enviroment. Only applicable for UNIX based systems. 479 | /// 480 | /// 481 | private static string GetTemporaryDirectory() 482 | { 483 | string temp = null; 484 | temp = temp ?? Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); 485 | temp = temp ?? Environment.GetEnvironmentVariable("TMPDIR"); 486 | temp = temp ?? Environment.GetEnvironmentVariable("TMP"); 487 | temp = temp ?? Environment.GetEnvironmentVariable("TEMP"); 488 | temp = temp ?? "/tmp"; 489 | return temp; 490 | } 491 | 492 | /// 493 | /// Returns true if the current OS platform is Unix based (Unix or MacOSX). 494 | /// 495 | /// 496 | public static bool IsUnix() 497 | { 498 | switch (Environment.OSVersion.Platform) 499 | { 500 | default: 501 | return false; 502 | 503 | case PlatformID.Unix: 504 | case PlatformID.MacOSX: 505 | return true; 506 | } 507 | } 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /DarkflameRPC/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading; 8 | 9 | namespace DiscordRPC.Example 10 | { 11 | partial class Program 12 | { 13 | /// 14 | /// The level of logging to use. 15 | /// 16 | private static Logging.LogLevel logLevel = Logging.LogLevel.Error; 17 | 18 | /// 19 | /// The pipe to connect too. 20 | /// 21 | private static int discordPipe = -1; 22 | 23 | /// 24 | /// The current presence to send to discord. 25 | /// 26 | private static RichPresence presence = new RichPresence() 27 | { 28 | Details = "LEGO Universe", 29 | State = "Exploring the Universe!", 30 | Assets = new Assets() 31 | { 32 | LargeImageKey = "image_large", 33 | LargeImageText = "Put cool text here", 34 | SmallImageKey = "image_small" 35 | } 36 | }; 37 | 38 | /// 39 | /// The discord client 40 | /// 41 | private static DiscordRpcClient client; 42 | 43 | /// 44 | /// Is the main loop currently running? 45 | /// 46 | private static bool isRunning = true; 47 | 48 | /// 49 | /// The string builder for the command 50 | /// 51 | private static StringBuilder word = new StringBuilder(); 52 | 53 | /// 54 | /// Client log path. 55 | /// 56 | private static string clientLogLocation; 57 | 58 | /// 59 | /// Full list of worlds to check for when comparing LOAD ZONE messages. 60 | /// 61 | private static List worldsList; 62 | 63 | /// 64 | /// Current world ID that the player is at. 65 | /// 66 | private static string currentWorld; 67 | 68 | /// 69 | /// Stores timestamps of each world transfer so we don't accidentally loop through them. 70 | /// 71 | private static List worldTransferTimestamps; 72 | 73 | /// 74 | /// Tracks total time played in this session. 75 | /// 76 | private static Timestamps currentSessionTime; 77 | 78 | 79 | //Main Loop 80 | static void Main(string[] args) 81 | { 82 | clientLogLocation = (Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)+"\\LEGO Software\\LEGO Universe\\Log Files\\"); 83 | worldTransferTimestamps = new List(); 84 | //Reads the arguments for the pipe 85 | for (int i = 0; i < args.Length; i++) 86 | { 87 | switch (args[i]) 88 | { 89 | case "-pipe": 90 | discordPipe = int.Parse(args[++i]); 91 | break; 92 | 93 | default: break; 94 | } 95 | } 96 | // Populate our worlds list first 97 | PopulateWorlds(); 98 | // Setup the actual rich presence and wait for changes 99 | DarkflameClient(); 100 | 101 | // Close the program once this is done. 102 | Environment.Exit(0); 103 | } 104 | 105 | static void PopulateWorlds() 106 | { 107 | // Maps not listed will default to a temp description and name. 108 | worldsList = new List(); 109 | worldsList.Add(new World("Frostburgh", "98:2", "Spreading holiday cheer!", "frostburgh_large")); // Set to 98:2 since that's what the 4 characters will be when we grab 'em, at least using the DLU spliced ID for the world 110 | worldsList.Add(new World("Venture Explorer", "1000", "Starting my journey!", "ve_large")); 111 | worldsList.Add(new World("Return to Venture Explorer", "1001", "Reclaiming the ship!", "rve_large")); 112 | worldsList.Add(new World("Avant Gardens", "1100", "Investigating the Maelstrom!", "ag_large")); 113 | worldsList.Add(new World("Avant Gardens Survival", "1101", "Surviving the horde!", "ags_large")); 114 | worldsList.Add(new World("Spider Queen Battle", "1102", "Purifying Block Yard!", "spiderqueen_large")); 115 | worldsList.Add(new World("Block Yard", "1150", "Visiting a property!", "blockyard_large")); 116 | worldsList.Add(new World("Avant Grove", "1151", "Visiting a property!", "avantgrove_large")); 117 | worldsList.Add(new World("Nimbus Station", "1200", "Hanging out in the Plaza!", "nimbusplaza")); 118 | worldsList.Add(new World("Pet Cove", "1201", "Taming pets!", "petcove_large")); 119 | worldsList.Add(new World("Vertigo Loop Racetrack", "1203", "Racing across Nimbus Station!", "vertigoloop_large")); 120 | worldsList.Add(new World("Battle of Nimbus Station", "1204", "Fighting through time!", "bons_large")); 121 | worldsList.Add(new World("Nimbus Rock", "1250", "Visiting a property!", "nimbusrock_large")); 122 | worldsList.Add(new World("Nimbus Isle", "1251", "Visiting a property!", "nimbusisle_large")); 123 | worldsList.Add(new World("Gnarled Forest", "1300", "Clearing the Maelstrom from Brig Rock!", "gf_large")); 124 | worldsList.Add(new World("Gnarled Forest Shooting Gallery", "1302", "Going for a high score!", "gfsg_large")); 125 | worldsList.Add(new World("Keelhaul Canyon Racetrack", "1303", "Racing across Gnarled Forest!", "keelhaul_large")); 126 | worldsList.Add(new World("Chantey Shanty", "1350", "Visiting a property!", "chanteyshanty_large")); 127 | worldsList.Add(new World("Forbidden Valley", "1400", "Learning the ways of the Ninja!", "fv_large")); 128 | worldsList.Add(new World("Forbidden Valley Dragon Battle", "1402", "Defeating dragons!", "dragonbattle_large")); 129 | worldsList.Add(new World("Dragonmaw Chasm Racetrack", "1403", "Racing across Forbidden Valley!", "dragonmaw_large")); 130 | worldsList.Add(new World("Raven Bluff", "1450", "Visiting a property!", "ravenbluff_large")); 131 | worldsList.Add(new World("Starbase 3001", "1600", "Visiting the WBL worlds!", "starbase_large")); 132 | worldsList.Add(new World("DeepFreeze", "1601", "Luptario's world!", "deepfreeze_large")); 133 | worldsList.Add(new World("Robot City", "1602", "Deerbite's world!", "robotcity_large")); 134 | worldsList.Add(new World("MoonBase", "1603", "A-Team's world!", "moonbase_large")); 135 | worldsList.Add(new World("Portabello", "1604", "Brickazon's world!", "jaedoria_large")); 136 | worldsList.Add(new World("LEGO Club", "1700", "Saying hi to Max!", "legoclub_large")); 137 | worldsList.Add(new World("Crux Prime", "1800", "Pushing back the Maelstrom!", "crux_large")); 138 | worldsList.Add(new World("Nexus Tower", "1900", "Relaxing by the Nexus!", "nexustower_large")); 139 | worldsList.Add(new World("Ninjago Monastery", "2000", "Learning Spinjitzu!", "ninjago_large")); 140 | worldsList.Add(new World("Battle Against Frakjaw", "2001", "Defeating the Skulkin threat!", "frakjaw_large")); 141 | } 142 | 143 | static void DarkflameClient() 144 | { 145 | // == Create the client 146 | client = new DiscordRpcClient("734924666086359110", pipe: discordPipe) 147 | { 148 | Logger = new Logging.ConsoleLogger(logLevel, true) 149 | }; 150 | 151 | // == Subscribe to some events 152 | client.OnReady += (sender, msg) => 153 | { 154 | //Create some events so we know things are happening 155 | Console.WriteLine("Connected to discord with user {0}", msg.User.Username); 156 | }; 157 | 158 | client.OnPresenceUpdate += (sender, msg) => 159 | { 160 | //The presence has updated 161 | Console.WriteLine("Presence has been updated! "); 162 | }; 163 | 164 | // == Initialize 165 | client.Initialize(); 166 | 167 | // Start our session timer 168 | currentSessionTime = Timestamps.Now; 169 | 170 | // == Set the presence 171 | client.SetPresence(new RichPresence() 172 | { 173 | Details = "Login Screen", 174 | State = "Preparing to explore the Universe!", 175 | Timestamps = currentSessionTime, 176 | Assets = new Assets() 177 | { 178 | LargeImageKey = "login", 179 | LargeImageText = "Login Screen", 180 | SmallImageKey = "logo" 181 | }, 182 | Buttons = new Button[] 183 | { 184 | new Button() { Label = "Website", Url = "https://darkflameuniverse.org/" }, 185 | new Button() { Label = "Twitter", Url = "https://twitter.com/darkflameuniv" } 186 | } 187 | }); 188 | 189 | 190 | //Enter our main loop 191 | MainLoop(); 192 | 193 | // == At the very end we need to dispose of it 194 | client.Dispose(); 195 | } 196 | 197 | public static FileInfo GetNewestFile(DirectoryInfo directory) 198 | { 199 | return directory.GetFiles() 200 | .Union(directory.GetDirectories().Select(d => GetNewestFile(d))) 201 | .OrderByDescending(f => (f == null ? DateTime.MinValue : f.LastWriteTime)) 202 | .FirstOrDefault(); 203 | } 204 | 205 | static void UpdateWorldPresence(string worldID, DiscordRpcClient client) 206 | { 207 | if (currentWorld == worldID) 208 | return; 209 | currentWorld = worldID; 210 | 211 | string worldName = "Testmap"; 212 | string worldDescription = "Where am I??"; 213 | string worldLargeImage = "nimbusplaza"; 214 | string worldSmallImage = "happyflower_small"; // figured it'd be amusing to default the icon to the happy flower face 215 | 216 | foreach (World wrld in worldsList) 217 | { 218 | if (wrld.worldID == worldID) 219 | { 220 | worldName = wrld.worldName; 221 | worldDescription = wrld.worldDescription; 222 | worldLargeImage = wrld.worldLargeIcon; 223 | worldSmallImage = "logo"; 224 | break; 225 | } 226 | } 227 | 228 | // Now, check if it's a LUP world 229 | if (worldID == "1600" || worldID == "1601" || worldID == "1602" || worldID == "1603" || worldID == "1604") 230 | worldSmallImage = "lup_small"; 231 | 232 | client.SetPresence(new RichPresence() 233 | { 234 | Details = worldName, 235 | State = worldDescription, 236 | Timestamps = currentSessionTime, 237 | Assets = new Assets() 238 | { 239 | LargeImageKey = worldLargeImage, 240 | LargeImageText = worldName, 241 | SmallImageKey = worldSmallImage 242 | }, 243 | Buttons = new Button[] 244 | { 245 | new Button() { Label = "Website", Url = "https://darkflameuniverse.org/" }, 246 | new Button() { Label = "Twitter", Url = "https://twitter.com/darkflameuniv" } 247 | } 248 | }); 249 | } 250 | 251 | static void MainLoop() 252 | { 253 | /* 254 | * Enter a infinite loop, polling the Discord Client for events. 255 | * In game termonology, this will be equivalent to our main game loop. 256 | * If you were making a GUI application without a infinite loop, you could implement 257 | * this with timers. 258 | */ 259 | isRunning = true; 260 | while (client != null && isRunning) 261 | { 262 | //Check if the game is still open: 263 | isRunning = Process.GetProcessesByName("legouniverse").Length > 0; 264 | 265 | //We will invoke the client events. 266 | // In a game situation, you would do this in the Update. 267 | // Not required if AutoEvents is enabled. 268 | //if (client != null && !client.AutoEvents) 269 | // client.Invoke(); 270 | 271 | // Look for a log file 272 | if (Directory.GetFiles(clientLogLocation) != null) 273 | { 274 | // Sometimes a user can have more than one log; in that case, grab the latest file 275 | FileInfo logPath = GetNewestFile(new DirectoryInfo(clientLogLocation)); 276 | 277 | // Open the log and start reading 278 | FileStream logFileStream = new FileStream(clientLogLocation + logPath.ToString(), FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); 279 | StreamReader logFileReader = new StreamReader(logFileStream); 280 | string line; 281 | 282 | while ((line = logFileReader.ReadLine()) != null) 283 | { 284 | // If we've got a load zone message and it wasn't one we've seen before, update presence 285 | if (line != null && line.Contains("MSG_LOAD_ZONE") && !(worldTransferTimestamps.Contains(line.Substring(0,7)))) 286 | { 287 | // Update presence 288 | worldTransferTimestamps.Add(line.Substring(0, 7)); 289 | UpdateWorldPresence(line.Substring(57, 4), client); 290 | } 291 | } 292 | } 293 | // Genuinely not sure how this case could happen if you have LU installed and have launched it, but hey, who knows. 294 | else 295 | { 296 | Console.WriteLine("No log folder found!"); 297 | client.Dispose(); 298 | } 299 | 300 | //Try to read any keys if available 301 | if (Console.KeyAvailable) 302 | ProcessKey(); 303 | 304 | //This can be what ever value you want, as long as it is faster than 30 seconds. 305 | //Console.Write("+"); 306 | Thread.Sleep(250); 307 | 308 | } 309 | } 310 | 311 | static int cursorIndex = 0; 312 | static string previousCommand = ""; 313 | static void ProcessKey() 314 | { 315 | //Read they key 316 | var key = Console.ReadKey(true); 317 | switch (key.Key) 318 | { 319 | case ConsoleKey.Enter: 320 | //Write the new line 321 | Console.WriteLine(); 322 | cursorIndex = 0; 323 | 324 | //The enter key has been sent, so send the message 325 | previousCommand = word.ToString(); 326 | ExecuteCommand(previousCommand); 327 | 328 | word.Clear(); 329 | break; 330 | 331 | case ConsoleKey.Backspace: 332 | word.Remove(cursorIndex - 1, 1); 333 | Console.Write("\r \r"); 334 | Console.Write(word); 335 | cursorIndex--; 336 | break; 337 | 338 | case ConsoleKey.Delete: 339 | if (cursorIndex < word.Length) 340 | { 341 | word.Remove(cursorIndex, 1); 342 | Console.Write("\r \r"); 343 | Console.Write(word); 344 | } 345 | break; 346 | 347 | case ConsoleKey.LeftArrow: 348 | cursorIndex--; 349 | break; 350 | 351 | case ConsoleKey.RightArrow: 352 | cursorIndex++; 353 | break; 354 | 355 | case ConsoleKey.UpArrow: 356 | word.Clear().Append(previousCommand); 357 | Console.Write("\r \r"); 358 | Console.Write(word); 359 | break; 360 | 361 | default: 362 | if (!Char.IsControl(key.KeyChar)) 363 | { 364 | //Some other character key was sent 365 | Console.Write(key.KeyChar); 366 | word.Insert(cursorIndex, key.KeyChar); 367 | Console.Write("\r \r"); 368 | Console.Write(word); 369 | cursorIndex++; 370 | } 371 | break; 372 | } 373 | 374 | if (cursorIndex < 0) cursorIndex = 0; 375 | if (cursorIndex >= Console.BufferWidth) cursorIndex = Console.BufferWidth - 1; 376 | Console.SetCursorPosition(cursorIndex, Console.CursorTop); 377 | } 378 | 379 | static void ExecuteCommand(string word) 380 | { 381 | //Trim the extra spacing 382 | word = word.Trim(); 383 | 384 | //Prepare the command and its body 385 | string command = word; 386 | string body = ""; 387 | 388 | //Split the command and the values. 389 | int whitespaceIndex = word.IndexOf(' '); 390 | if (whitespaceIndex >= 0) 391 | { 392 | command = word.Substring(0, whitespaceIndex); 393 | if (whitespaceIndex < word.Length) 394 | body = word.Substring(whitespaceIndex + 1); 395 | } 396 | 397 | //Parse the command 398 | switch (command.ToLowerInvariant()) 399 | { 400 | case "close": 401 | client.Dispose(); 402 | break; 403 | 404 | #region State & Details 405 | case "state": 406 | //presence.State = body; 407 | presence.State = body; 408 | client.SetPresence(presence); 409 | break; 410 | 411 | case "details": 412 | presence.Details = body; 413 | client.SetPresence(presence); 414 | break; 415 | #endregion 416 | 417 | #region Asset Examples 418 | case "large_key": 419 | //If we do not have a asset object already, we must create it 420 | if (!presence.HasAssets()) 421 | presence.Assets = new Assets(); 422 | 423 | //Set the key then send it away 424 | presence.Assets.LargeImageKey = body; 425 | client.SetPresence(presence); 426 | break; 427 | 428 | case "large_text": 429 | //If we do not have a asset object already, we must create it 430 | if (!presence.HasAssets()) 431 | presence.Assets = new Assets(); 432 | 433 | //Set the key then send it away 434 | presence.Assets.LargeImageText = body; 435 | client.SetPresence(presence); 436 | break; 437 | 438 | case "small_key": 439 | //If we do not have a asset object already, we must create it 440 | if (!presence.HasAssets()) 441 | presence.Assets = new Assets(); 442 | 443 | //Set the key then send it away 444 | presence.Assets.SmallImageKey = body; 445 | client.SetPresence(presence); 446 | break; 447 | 448 | case "small_text": 449 | //If we do not have a asset object already, we must create it 450 | if (!presence.HasAssets()) 451 | presence.Assets = new Assets(); 452 | 453 | //Set the key then send it away 454 | presence.Assets.SmallImageText = body; 455 | client.SetPresence(presence); 456 | break; 457 | #endregion 458 | 459 | case "help": 460 | Console.WriteLine("Available Commands: state, details, large_key, large_text, small_key, small_text"); 461 | break; 462 | 463 | default: 464 | Console.WriteLine("Unkown Command '{0}'. Try 'help' for a list of commands", command); 465 | break; 466 | } 467 | 468 | } 469 | 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /DiscordRPC/RPC/RpcConnection.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC.Helper; 2 | using DiscordRPC.Message; 3 | using DiscordRPC.IO; 4 | using DiscordRPC.RPC.Commands; 5 | using DiscordRPC.RPC.Payload; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Threading; 9 | using Newtonsoft.Json; 10 | using DiscordRPC.Logging; 11 | using DiscordRPC.Events; 12 | 13 | namespace DiscordRPC.RPC 14 | { 15 | /// 16 | /// Communicates between the client and discord through RPC 17 | /// 18 | internal class RpcConnection : IDisposable 19 | { 20 | /// 21 | /// Version of the RPC Protocol 22 | /// 23 | public static readonly int VERSION = 1; 24 | 25 | /// 26 | /// The rate of poll to the discord pipe. 27 | /// 28 | public static readonly int POLL_RATE = 1000; 29 | 30 | /// 31 | /// Should we send a null presence on the fairwells? 32 | /// 33 | private static readonly bool CLEAR_ON_SHUTDOWN = true; 34 | 35 | /// 36 | /// Should we work in a lock step manner? This option is semi-obsolete and may not work as expected. 37 | /// 38 | private static readonly bool LOCK_STEP = false; 39 | 40 | /// 41 | /// The logger used by the RPC connection 42 | /// 43 | public ILogger Logger 44 | { 45 | get { return _logger; } 46 | set 47 | { 48 | _logger = value; 49 | if (namedPipe != null) 50 | namedPipe.Logger = value; 51 | } 52 | } 53 | private ILogger _logger; 54 | 55 | /// 56 | /// Called when a message is received from the RPC and is about to be enqueued. This is cross-thread and will execute on the RPC thread. 57 | /// 58 | public event OnRpcMessageEvent OnRpcMessage; 59 | 60 | #region States 61 | 62 | /// 63 | /// The current state of the RPC connection 64 | /// 65 | public RpcState State { get { var tmp = RpcState.Disconnected; lock (l_states) tmp = _state; return tmp; } } 66 | private RpcState _state; 67 | private readonly object l_states = new object(); 68 | 69 | /// 70 | /// The configuration received by the Ready 71 | /// 72 | public Configuration Configuration { get { Configuration tmp = null; lock (l_config) tmp = _configuration; return tmp; } } 73 | private Configuration _configuration = null; 74 | private readonly object l_config = new object(); 75 | 76 | private volatile bool aborting = false; 77 | private volatile bool shutdown = false; 78 | 79 | /// 80 | /// Indicates if the RPC connection is still running in the background 81 | /// 82 | public bool IsRunning { get { return thread != null; } } 83 | 84 | /// 85 | /// Forces the to call instead, safely saying goodbye to Discord. 86 | /// This option helps prevents ghosting in applications where the Process ID is a host and the game is executed within the host (ie: the Unity3D editor). This will tell Discord that we have no presence and we are closing the connection manually, instead of waiting for the process to terminate. 87 | /// 88 | public bool ShutdownOnly { get; set; } 89 | 90 | #endregion 91 | 92 | #region Privates 93 | 94 | private string applicationID; //ID of the Discord APP 95 | private int processID; //ID of the process to track 96 | 97 | private long nonce; //Current command index 98 | 99 | private Thread thread; //The current thread 100 | private INamedPipeClient namedPipe; 101 | 102 | private int targetPipe; //The pipe to taget. Leave as -1 for any available pipe. 103 | 104 | private readonly object l_rtqueue = new object(); //Lock for the send queue 105 | private readonly uint _maxRtQueueSize; 106 | private Queue _rtqueue; //The send queue 107 | 108 | private readonly object l_rxqueue = new object(); //Lock for the receive queue 109 | private readonly uint _maxRxQueueSize; //The max size of the RX queue 110 | private Queue _rxqueue; //The receive queue 111 | 112 | private AutoResetEvent queueUpdatedEvent = new AutoResetEvent(false); 113 | private BackoffDelay delay; //The backoff delay before reconnecting. 114 | #endregion 115 | 116 | /// 117 | /// Creates a new instance of the RPC. 118 | /// 119 | /// The ID of the Discord App 120 | /// The ID of the currently running process 121 | /// The target pipe to connect too 122 | /// The pipe client we shall use. 123 | /// The maximum size of the out queue 124 | /// The maximum size of the in queue 125 | public RpcConnection(string applicationID, int processID, int targetPipe, INamedPipeClient client, uint maxRxQueueSize = 128, uint maxRtQueueSize = 512) 126 | { 127 | this.applicationID = applicationID; 128 | this.processID = processID; 129 | this.targetPipe = targetPipe; 130 | this.namedPipe = client; 131 | this.ShutdownOnly = true; 132 | 133 | //Assign a default logger 134 | Logger = new ConsoleLogger(); 135 | 136 | delay = new BackoffDelay(500, 60 * 1000); 137 | _maxRtQueueSize = maxRtQueueSize; 138 | _rtqueue = new Queue((int)_maxRtQueueSize + 1); 139 | 140 | _maxRxQueueSize = maxRxQueueSize; 141 | _rxqueue = new Queue((int)_maxRxQueueSize + 1); 142 | 143 | nonce = 0; 144 | } 145 | 146 | 147 | private long GetNextNonce() 148 | { 149 | nonce += 1; 150 | return nonce; 151 | } 152 | 153 | #region Queues 154 | /// 155 | /// Enqueues a command 156 | /// 157 | /// The command to enqueue 158 | internal void EnqueueCommand(ICommand command) 159 | { 160 | Logger.Trace("Enqueue Command: " + command.GetType().FullName); 161 | 162 | //We cannot add anything else if we are aborting or shutting down. 163 | if (aborting || shutdown) return; 164 | 165 | //Enqueue the set presence argument 166 | lock (l_rtqueue) 167 | { 168 | //If we are too big drop the last element 169 | if (_rtqueue.Count == _maxRtQueueSize) 170 | { 171 | Logger.Error("Too many enqueued commands, dropping oldest one. Maybe you are pushing new presences to fast?"); 172 | _rtqueue.Dequeue(); 173 | } 174 | 175 | //Enqueue the message 176 | _rtqueue.Enqueue(command); 177 | } 178 | } 179 | 180 | /// 181 | /// Adds a message to the message queue. Does not copy the message, so besure to copy it yourself or dereference it. 182 | /// 183 | /// The message to add 184 | private void EnqueueMessage(IMessage message) 185 | { 186 | //Invoke the message 187 | try 188 | { 189 | if (OnRpcMessage != null) 190 | OnRpcMessage.Invoke(this, message); 191 | } 192 | catch (Exception e) 193 | { 194 | Logger.Error("Unhandled Exception while processing event: {0}", e.GetType().FullName); 195 | Logger.Error(e.Message); 196 | Logger.Error(e.StackTrace); 197 | } 198 | 199 | //Small queue sizes should just ignore messages 200 | if (_maxRxQueueSize <= 0) 201 | { 202 | Logger.Trace("Enqueued Message, but queue size is 0."); 203 | return; 204 | } 205 | 206 | //Large queue sizes should keep the queue in check 207 | Logger.Trace("Enqueue Message: " + message.Type); 208 | lock (l_rxqueue) 209 | { 210 | //If we are too big drop the last element 211 | if (_rxqueue.Count == _maxRxQueueSize) 212 | { 213 | Logger.Warning("Too many enqueued messages, dropping oldest one."); 214 | _rxqueue.Dequeue(); 215 | } 216 | 217 | //Enqueue the message 218 | _rxqueue.Enqueue(message); 219 | } 220 | } 221 | 222 | /// 223 | /// Dequeues a single message from the event stack. Returns null if none are available. 224 | /// 225 | /// 226 | internal IMessage DequeueMessage() 227 | { 228 | //Logger.Trace("Deque Message"); 229 | lock (l_rxqueue) 230 | { 231 | //We have nothing, so just return null. 232 | if (_rxqueue.Count == 0) return null; 233 | 234 | //Get the value and remove it from the list at the same time 235 | return _rxqueue.Dequeue(); 236 | } 237 | } 238 | 239 | /// 240 | /// Dequeues all messages from the event stack. 241 | /// 242 | /// 243 | internal IMessage[] DequeueMessages() 244 | { 245 | //Logger.Trace("Deque Multiple Messages"); 246 | lock (l_rxqueue) 247 | { 248 | //Copy the messages into an array 249 | IMessage[] messages = _rxqueue.ToArray(); 250 | 251 | //Clear the entire queue 252 | _rxqueue.Clear(); 253 | 254 | //return the array 255 | return messages; 256 | } 257 | } 258 | #endregion 259 | 260 | /// 261 | /// Main thread loop 262 | /// 263 | private void MainLoop() 264 | { 265 | //initialize the pipe 266 | Logger.Info("RPC Connection Started"); 267 | if (Logger.Level <= LogLevel.Trace) 268 | { 269 | Logger.Trace("============================"); 270 | Logger.Trace("Assembly: " + System.Reflection.Assembly.GetAssembly(typeof(RichPresence)).FullName); 271 | Logger.Trace("Pipe: " + namedPipe.GetType().FullName); 272 | Logger.Trace("Platform: " + Environment.OSVersion.ToString()); 273 | Logger.Trace("applicationID: " + applicationID); 274 | Logger.Trace("targetPipe: " + targetPipe); 275 | Logger.Trace("POLL_RATE: " + POLL_RATE); 276 | Logger.Trace("_maxRtQueueSize: " + _maxRtQueueSize); 277 | Logger.Trace("_maxRxQueueSize: " + _maxRxQueueSize); 278 | Logger.Trace("============================"); 279 | } 280 | 281 | //Forever trying to connect unless the abort signal is sent 282 | //Keep Alive Loop 283 | while (!aborting && !shutdown) 284 | { 285 | try 286 | { 287 | //Wrap everything up in a try get 288 | //Dispose of the pipe if we have any (could be broken) 289 | if (namedPipe == null) 290 | { 291 | Logger.Error("Something bad has happened with our pipe client!"); 292 | aborting = true; 293 | return; 294 | } 295 | 296 | //Connect to a new pipe 297 | Logger.Trace("Connecting to the pipe through the {0}", namedPipe.GetType().FullName); 298 | if (namedPipe.Connect(targetPipe)) 299 | { 300 | #region Connected 301 | //We connected to a pipe! Reset the delay 302 | Logger.Trace("Connected to the pipe. Attempting to establish handshake..."); 303 | EnqueueMessage(new ConnectionEstablishedMessage() { ConnectedPipe = namedPipe.ConnectedPipe }); 304 | 305 | //Attempt to establish a handshake 306 | EstablishHandshake(); 307 | Logger.Trace("Connection Established. Starting reading loop..."); 308 | 309 | //Continously iterate, waiting for the frame 310 | //We want to only stop reading if the inside tells us (mainloop), if we are aborting (abort) or the pipe disconnects 311 | // We dont want to exit on a shutdown, as we still have information 312 | PipeFrame frame; 313 | bool mainloop = true; 314 | while (mainloop && !aborting && !shutdown && namedPipe.IsConnected) 315 | { 316 | #region Read Loop 317 | 318 | //Iterate over every frame we have queued up, processing its contents 319 | if (namedPipe.ReadFrame(out frame)) 320 | { 321 | #region Read Payload 322 | Logger.Trace("Read Payload: {0}", frame.Opcode); 323 | 324 | //Do some basic processing on the frame 325 | switch (frame.Opcode) 326 | { 327 | //We have been told by discord to close, so we will consider it an abort 328 | case Opcode.Close: 329 | 330 | ClosePayload close = frame.GetObject(); 331 | Logger.Warning("We have been told to terminate by discord: ({0}) {1}", close.Code, close.Reason); 332 | EnqueueMessage(new CloseMessage() { Code = close.Code, Reason = close.Reason }); 333 | mainloop = false; 334 | break; 335 | 336 | //We have pinged, so we will flip it and respond back with pong 337 | case Opcode.Ping: 338 | Logger.Trace("PING"); 339 | frame.Opcode = Opcode.Pong; 340 | namedPipe.WriteFrame(frame); 341 | break; 342 | 343 | //We have ponged? I have no idea if Discord actually sends ping/pongs. 344 | case Opcode.Pong: 345 | Logger.Trace("PONG"); 346 | break; 347 | 348 | //A frame has been sent, we should deal with that 349 | case Opcode.Frame: 350 | if (shutdown) 351 | { 352 | //We are shutting down, so skip it 353 | Logger.Warning("Skipping frame because we are shutting down."); 354 | break; 355 | } 356 | 357 | if (frame.Data == null) 358 | { 359 | //We have invalid data, thats not good. 360 | Logger.Error("We received no data from the frame so we cannot get the event payload!"); 361 | break; 362 | } 363 | 364 | //We have a frame, so we are going to process the payload and add it to the stack 365 | EventPayload response = null; 366 | try { response = frame.GetObject(); } catch (Exception e) 367 | { 368 | Logger.Error("Failed to parse event! " + e.Message); 369 | Logger.Error("Data: " + frame.Message); 370 | } 371 | 372 | 373 | try { if (response != null) ProcessFrame(response); } catch(Exception e) 374 | { 375 | Logger.Error("Failed to process event! " + e.Message); 376 | Logger.Error("Data: " + frame.Message); 377 | } 378 | 379 | break; 380 | 381 | 382 | default: 383 | case Opcode.Handshake: 384 | //We have a invalid opcode, better terminate to be safe 385 | Logger.Error("Invalid opcode: {0}", frame.Opcode); 386 | mainloop = false; 387 | break; 388 | } 389 | 390 | #endregion 391 | } 392 | 393 | if (!aborting && namedPipe.IsConnected) 394 | { 395 | //Process the entire command queue we have left 396 | ProcessCommandQueue(); 397 | 398 | //Wait for some time, or until a command has been queued up 399 | queueUpdatedEvent.WaitOne(POLL_RATE); 400 | } 401 | 402 | #endregion 403 | } 404 | #endregion 405 | 406 | Logger.Trace("Left main read loop for some reason. Aborting: {0}, Shutting Down: {1}", aborting, shutdown); 407 | } 408 | else 409 | { 410 | Logger.Error("Failed to connect for some reason."); 411 | EnqueueMessage(new ConnectionFailedMessage() { FailedPipe = targetPipe }); 412 | } 413 | 414 | //If we are not aborting, we have to wait a bit before trying to connect again 415 | if (!aborting && !shutdown) 416 | { 417 | //We have disconnected for some reason, either a failed pipe or a bad reading, 418 | // so we are going to wait a bit before doing it again 419 | long sleep = delay.NextDelay(); 420 | 421 | Logger.Trace("Waiting {0}ms before attempting to connect again", sleep); 422 | Thread.Sleep(delay.NextDelay()); 423 | } 424 | } 425 | //catch(InvalidPipeException e) 426 | //{ 427 | // Logger.Error("Invalid Pipe Exception: {0}", e.Message); 428 | //} 429 | catch (Exception e) 430 | { 431 | Logger.Error("Unhandled Exception: {0}", e.GetType().FullName); 432 | Logger.Error(e.Message); 433 | Logger.Error(e.StackTrace); 434 | } 435 | finally 436 | { 437 | //Disconnect from the pipe because something bad has happened. An exception has been thrown or the main read loop has terminated. 438 | if (namedPipe.IsConnected) 439 | { 440 | //Terminate the pipe 441 | Logger.Trace("Closing the named pipe."); 442 | namedPipe.Close(); 443 | } 444 | 445 | //Update our state 446 | SetConnectionState(RpcState.Disconnected); 447 | } 448 | } 449 | 450 | //We have disconnected, so dispose of the thread and the pipe. 451 | Logger.Trace("Left Main Loop"); 452 | if (namedPipe != null) 453 | namedPipe.Dispose(); 454 | 455 | Logger.Info("Thread Terminated, no longer performing RPC connection."); 456 | } 457 | 458 | #region Reading 459 | 460 | /// Handles the response from the pipe and calls appropriate events and changes states. 461 | /// The response received by the server. 462 | private void ProcessFrame(EventPayload response) 463 | { 464 | Logger.Info("Handling Response. Cmd: {0}, Event: {1}", response.Command, response.Event); 465 | 466 | //Check if it is an error 467 | if (response.Event.HasValue && response.Event.Value == ServerEvent.Error) 468 | { 469 | //We have an error 470 | Logger.Error("Error received from the RPC"); 471 | 472 | //Create the event objetc and push it to the queue 473 | ErrorMessage err = response.GetObject(); 474 | Logger.Error("Server responded with an error message: ({0}) {1}", err.Code.ToString(), err.Message); 475 | 476 | //Enqueue the messsage and then end 477 | EnqueueMessage(err); 478 | return; 479 | } 480 | 481 | //Check if its a handshake 482 | if (State == RpcState.Connecting) 483 | { 484 | if (response.Command == Command.Dispatch && response.Event.HasValue && response.Event.Value == ServerEvent.Ready) 485 | { 486 | Logger.Info("Connection established with the RPC"); 487 | SetConnectionState(RpcState.Connected); 488 | delay.Reset(); 489 | 490 | //Prepare the object 491 | ReadyMessage ready = response.GetObject(); 492 | lock (l_config) 493 | { 494 | _configuration = ready.Configuration; 495 | ready.User.SetConfiguration(_configuration); 496 | } 497 | 498 | //Enqueue the message 499 | EnqueueMessage(ready); 500 | return; 501 | } 502 | } 503 | 504 | if (State == RpcState.Connected) 505 | { 506 | switch(response.Command) 507 | { 508 | //We were sent a dispatch, better process it 509 | case Command.Dispatch: 510 | ProcessDispatch(response); 511 | break; 512 | 513 | //We were sent a Activity Update, better enqueue it 514 | case Command.SetActivity: 515 | if (response.Data == null) 516 | { 517 | EnqueueMessage(new PresenceMessage()); 518 | } 519 | else 520 | { 521 | RichPresenceResponse rp = response.GetObject(); 522 | EnqueueMessage(new PresenceMessage(rp)); 523 | } 524 | break; 525 | 526 | case Command.Unsubscribe: 527 | case Command.Subscribe: 528 | 529 | //Prepare a serializer that can account for snake_case enums. 530 | JsonSerializer serializer = new JsonSerializer(); 531 | serializer.Converters.Add(new Converters.EnumSnakeCaseConverter()); 532 | 533 | //Go through the data, looking for the evt property, casting it to a server event 534 | var evt = response.GetObject().Event.Value; 535 | 536 | //Enqueue the appropriate message. 537 | if (response.Command == Command.Subscribe) 538 | EnqueueMessage(new SubscribeMessage(evt)); 539 | else 540 | EnqueueMessage(new UnsubscribeMessage(evt)); 541 | 542 | break; 543 | 544 | 545 | case Command.SendActivityJoinInvite: 546 | Logger.Trace("Got invite response ack."); 547 | break; 548 | 549 | case Command.CloseActivityJoinRequest: 550 | Logger.Trace("Got invite response reject ack."); 551 | break; 552 | 553 | //we have no idea what we were sent 554 | default: 555 | Logger.Error("Unkown frame was received! {0}", response.Command); 556 | return; 557 | } 558 | return; 559 | } 560 | 561 | Logger.Trace("Received a frame while we are disconnected. Ignoring. Cmd: {0}, Event: {1}", response.Command, response.Event); 562 | } 563 | 564 | private void ProcessDispatch(EventPayload response) 565 | { 566 | if (response.Command != Command.Dispatch) return; 567 | if (!response.Event.HasValue) return; 568 | 569 | switch(response.Event.Value) 570 | { 571 | //We are to join the server 572 | case ServerEvent.ActivitySpectate: 573 | var spectate = response.GetObject(); 574 | EnqueueMessage(spectate); 575 | break; 576 | 577 | case ServerEvent.ActivityJoin: 578 | var join = response.GetObject(); 579 | EnqueueMessage(join); 580 | break; 581 | 582 | case ServerEvent.ActivityJoinRequest: 583 | var request = response.GetObject(); 584 | EnqueueMessage(request); 585 | break; 586 | 587 | //Unkown dispatch event received. We should just ignore it. 588 | default: 589 | Logger.Warning("Ignoring {0}", response.Event.Value); 590 | break; 591 | } 592 | } 593 | 594 | #endregion 595 | 596 | #region Writting 597 | 598 | private void ProcessCommandQueue() 599 | { 600 | //Logger.Info("Checking command queue"); 601 | 602 | //We are not ready yet, dont even try 603 | if (State != RpcState.Connected) 604 | return; 605 | 606 | //We are aborting, so we will just log a warning so we know this is probably only going to send the CLOSE 607 | if (aborting) 608 | Logger.Warning("We have been told to write a queue but we have also been aborted."); 609 | 610 | //Prepare some variabels we will clone into with locks 611 | bool needsWriting = true; 612 | ICommand item = null; 613 | 614 | //Continue looping until we dont need anymore messages 615 | while (needsWriting && namedPipe.IsConnected) 616 | { 617 | lock (l_rtqueue) 618 | { 619 | //Pull the value and update our writing needs 620 | // If we have nothing to write, exit the loop 621 | needsWriting = _rtqueue.Count > 0; 622 | if (!needsWriting) break; 623 | 624 | //Peek at the item 625 | item = _rtqueue.Peek(); 626 | } 627 | 628 | //BReak out of the loop as soon as we send this item 629 | if (shutdown || (!aborting && LOCK_STEP)) 630 | needsWriting = false; 631 | 632 | //Prepare the payload 633 | IPayload payload = item.PreparePayload(GetNextNonce()); 634 | Logger.Trace("Attempting to send payload: " + payload.Command); 635 | 636 | //Prepare the frame 637 | PipeFrame frame = new PipeFrame(); 638 | if (item is CloseCommand) 639 | { 640 | //We have been sent a close frame. We better just send a handwave 641 | //Send it off to the server 642 | SendHandwave(); 643 | 644 | //Queue the item 645 | Logger.Trace("Handwave sent, ending queue processing."); 646 | lock (l_rtqueue) _rtqueue.Dequeue(); 647 | 648 | //Stop sending any more messages 649 | return; 650 | } 651 | else 652 | { 653 | if (aborting) 654 | { 655 | //We are aborting, so just dequeue the message and dont bother sending it 656 | Logger.Warning("- skipping frame because of abort."); 657 | lock (l_rtqueue) _rtqueue.Dequeue(); 658 | } 659 | else 660 | { 661 | //Prepare the frame 662 | frame.SetObject(Opcode.Frame, payload); 663 | 664 | //Write it and if it wrote perfectly fine, we will dequeue it 665 | Logger.Trace("Sending payload: " + payload.Command); 666 | if (namedPipe.WriteFrame(frame)) 667 | { 668 | //We sent it, so now dequeue it 669 | Logger.Trace("Sent Successfully."); 670 | lock (l_rtqueue) _rtqueue.Dequeue(); 671 | } 672 | else 673 | { 674 | //Something went wrong, so just giveup and wait for the next time around. 675 | Logger.Warning("Something went wrong during writing!"); 676 | return; 677 | } 678 | } 679 | } 680 | } 681 | } 682 | 683 | #endregion 684 | 685 | #region Connection 686 | 687 | /// 688 | /// Establishes the handshake with the server. 689 | /// 690 | /// 691 | private void EstablishHandshake() 692 | { 693 | Logger.Trace("Attempting to establish a handshake..."); 694 | 695 | //We are establishing a lock and not releasing it until we sent the handshake message. 696 | // We need to set the key, and it would not be nice if someone did things between us setting the key. 697 | 698 | //Check its state 699 | if (State != RpcState.Disconnected) 700 | { 701 | Logger.Error("State must be disconnected in order to start a handshake!"); 702 | return; 703 | } 704 | 705 | //Send it off to the server 706 | Logger.Trace("Sending Handshake..."); 707 | if (!namedPipe.WriteFrame(new PipeFrame(Opcode.Handshake, new Handshake() { Version = VERSION, ClientID = applicationID }))) 708 | { 709 | Logger.Error("Failed to write a handshake."); 710 | return; 711 | } 712 | 713 | //This has to be done outside the lock 714 | SetConnectionState(RpcState.Connecting); 715 | } 716 | 717 | /// 718 | /// Establishes a fairwell with the server by sending a handwave. 719 | /// 720 | private void SendHandwave() 721 | { 722 | Logger.Info("Attempting to wave goodbye..."); 723 | 724 | //Check its state 725 | if (State == RpcState.Disconnected) 726 | { 727 | Logger.Error("State must NOT be disconnected in order to send a handwave!"); 728 | return; 729 | } 730 | 731 | //Send the handwave 732 | if (!namedPipe.WriteFrame(new PipeFrame(Opcode.Close, new Handshake() { Version = VERSION, ClientID = applicationID }))) 733 | { 734 | Logger.Error("failed to write a handwave."); 735 | return; 736 | } 737 | } 738 | 739 | 740 | /// 741 | /// Attempts to connect to the pipe. Returns true on success 742 | /// 743 | /// 744 | public bool AttemptConnection() 745 | { 746 | Logger.Info("Attempting a new connection"); 747 | 748 | //The thread mustn't exist already 749 | if (thread != null) 750 | { 751 | Logger.Error("Cannot attempt a new connection as the previous connection thread is not null!"); 752 | return false; 753 | } 754 | 755 | //We have to be in the disconnected state 756 | if (State != RpcState.Disconnected) 757 | { 758 | Logger.Warning("Cannot attempt a new connection as the previous connection hasn't changed state yet."); 759 | return false; 760 | } 761 | 762 | if (aborting) 763 | { 764 | Logger.Error("Cannot attempt a new connection while aborting!"); 765 | return false; 766 | } 767 | 768 | //Start the thread up 769 | thread = new Thread(MainLoop); 770 | thread.Name = "Discord IPC Thread"; 771 | thread.IsBackground = true; 772 | thread.Start(); 773 | 774 | return true; 775 | } 776 | 777 | /// 778 | /// Sets the current state of the pipe, locking the l_states object for thread saftey. 779 | /// 780 | /// The state to set it too. 781 | private void SetConnectionState(RpcState state) 782 | { 783 | Logger.Trace("Setting the connection state to {0}", state.ToString().ToSnakeCase().ToUpperInvariant()); 784 | lock (l_states) 785 | { 786 | _state = state; 787 | } 788 | } 789 | 790 | /// 791 | /// Closes the connection and disposes of resources. This will not force termination, but instead allow Discord disconnect us after we say goodbye. 792 | /// This option helps prevents ghosting in applications where the Process ID is a host and the game is executed within the host (ie: the Unity3D editor). This will tell Discord that we have no presence and we are closing the connection manually, instead of waiting for the process to terminate. 793 | /// 794 | public void Shutdown() 795 | { 796 | //Enable the flag 797 | Logger.Trace("Initiated shutdown procedure"); 798 | shutdown = true; 799 | 800 | //Clear the commands and enqueue the close 801 | lock(l_rtqueue) 802 | { 803 | _rtqueue.Clear(); 804 | if (CLEAR_ON_SHUTDOWN) _rtqueue.Enqueue(new PresenceCommand() { PID = processID, Presence = null }); 805 | _rtqueue.Enqueue(new CloseCommand()); 806 | } 807 | 808 | //Trigger the event 809 | queueUpdatedEvent.Set(); 810 | } 811 | 812 | /// 813 | /// Closes the connection and disposes of resources. 814 | /// 815 | public void Close() 816 | { 817 | if (thread == null) 818 | { 819 | Logger.Error("Cannot close as it is not available!"); 820 | return; 821 | } 822 | 823 | if (aborting) 824 | { 825 | Logger.Error("Cannot abort as it has already been aborted"); 826 | return; 827 | } 828 | 829 | //Set the abort state 830 | if (ShutdownOnly) 831 | { 832 | Shutdown(); 833 | return; 834 | } 835 | 836 | //Terminate 837 | Logger.Trace("Updating Abort State..."); 838 | aborting = true; 839 | queueUpdatedEvent.Set(); 840 | } 841 | 842 | 843 | /// 844 | /// Closes the connection and disposes resources. Identical to but ignores the "ShutdownOnly" value. 845 | /// 846 | public void Dispose() 847 | { 848 | ShutdownOnly = false; 849 | Close(); 850 | } 851 | #endregion 852 | 853 | } 854 | 855 | /// 856 | /// State of the RPC connection 857 | /// 858 | internal enum RpcState 859 | { 860 | /// 861 | /// Disconnected from the discord client 862 | /// 863 | Disconnected, 864 | 865 | /// 866 | /// Connecting to the discord client. The handshake has been sent and we are awaiting the ready event 867 | /// 868 | Connecting, 869 | 870 | /// 871 | /// We are connect to the client and can send and receive messages. 872 | /// 873 | Connected 874 | } 875 | } --------------------------------------------------------------------------------