├── 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 | }
--------------------------------------------------------------------------------