diff --git a/MinecraftClient/ChatBots/DiscordRpc.cs b/MinecraftClient/ChatBots/DiscordRpc.cs
index 6ab78ac9..f4be0532 100644
--- a/MinecraftClient/ChatBots/DiscordRpc.cs
+++ b/MinecraftClient/ChatBots/DiscordRpc.cs
@@ -1,5 +1,10 @@
using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Pipes;
+using System.Threading;
using DiscordRPC;
+using DiscordRPC.IO;
using DiscordRPC.Logging;
using MinecraftClient.Mapping;
using MinecraftClient.Scripting;
@@ -329,5 +334,394 @@ namespace MinecraftClient.ChatBots
return value.Length <= maxLength ? value : value[..(maxLength - 3)] + "...";
}
+
+ ///
+ /// Flatpak Discord exposes the RPC socket under app/com.discordapp.Discord,
+ /// but the currently published DiscordRichPresence package does not probe that path.
+ ///
+ private sealed class DiscordRpcPipeClient : INamedPipeClient
+ {
+ private const string DiscordPipePrefix = "discord-ipc-";
+ private const int MaximumPipeVariations = 10;
+
+ private static readonly string[] s_unixPackageDirectories =
+ [
+ // Official Discord clients
+ "app/com.discordapp.Discord",
+ "snap.discord",
+
+ // Community desktop clients / wrappers
+ "app/dev.vencord.Vesktop",
+ ".flatpak/dev.vencord.Vesktop/xdg-run",
+ "app/org.equicord.equibop",
+ "app/io.github.equicord.equibop",
+ "app/xyz.armcord.ArmCord",
+ "app/io.github.spacingbat3.webcord"
+ ];
+
+ private readonly byte[] _buffer = new byte[PipeFrame.MAX_SIZE];
+ private readonly Queue _frameQueue = new();
+ private readonly object _frameQueueLock = new();
+ private readonly object _streamLock = new();
+
+ private int _connectedPipe;
+ private NamedPipeClientStream? _stream;
+ private volatile bool _isClosed = true;
+ private volatile bool _isDisposed;
+
+ public ILogger Logger { get; set; } = new NullLogger();
+
+ public bool IsConnected
+ {
+ get
+ {
+ if (_isClosed)
+ return false;
+
+ lock (_streamLock)
+ return _stream is { IsConnected: true };
+ }
+ }
+
+ [Obsolete("The connected pipe is not neccessary information.")]
+ public int ConnectedPipe => _connectedPipe;
+
+ public bool Connect(int pipe)
+ {
+ Logger.Trace("DiscordRpcPipeClient.Connect({0})", pipe);
+
+ if (_isDisposed)
+ throw new ObjectDisposedException(nameof(DiscordRpcPipeClient));
+
+ if (pipe > 9)
+ throw new ArgumentOutOfRangeException(nameof(pipe), "Argument cannot be greater than 9");
+
+ int startPipe = pipe >= 0 ? pipe : 0;
+
+ foreach (string pipeName in GetPipeCandidates(startPipe))
+ {
+ if (AttemptConnection(pipeName))
+ {
+ BeginReadStream();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public bool ReadFrame(out PipeFrame frame)
+ {
+ if (_isDisposed)
+ throw new ObjectDisposedException(nameof(DiscordRpcPipeClient));
+
+ lock (_frameQueueLock)
+ {
+ if (_frameQueue.Count == 0)
+ {
+ frame = default;
+ return false;
+ }
+
+ frame = _frameQueue.Dequeue();
+ return true;
+ }
+ }
+
+ public bool WriteFrame(PipeFrame frame)
+ {
+ if (_isDisposed)
+ throw new ObjectDisposedException(nameof(DiscordRpcPipeClient));
+
+ if (_isClosed || !IsConnected)
+ {
+ Logger.Error("Failed to write frame because the stream is closed");
+ return false;
+ }
+
+ try
+ {
+ frame.WriteStream(_stream);
+ return true;
+ }
+ catch (IOException io)
+ {
+ Logger.Error("Failed to write frame because of a IO Exception: {0}", io.Message);
+ }
+ catch (ObjectDisposedException)
+ {
+ Logger.Warning("Failed to write frame as the stream was already disposed");
+ }
+ catch (InvalidOperationException)
+ {
+ Logger.Warning("Failed to write frame because of a invalid operation");
+ }
+
+ return false;
+ }
+
+ public void Close()
+ {
+ if (_isClosed)
+ {
+ Logger.Warning("Tried to close a already closed pipe.");
+ return;
+ }
+
+ try
+ {
+ lock (_streamLock)
+ {
+ if (_stream is not null)
+ {
+ try
+ {
+ _stream.Flush();
+ _stream.Dispose();
+ }
+ catch
+ {
+ }
+
+ _stream = null;
+ _isClosed = true;
+ }
+ else
+ {
+ Logger.Warning("Stream was closed, but no stream was available to begin with!");
+ }
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ Logger.Warning("Tried to dispose already disposed stream");
+ }
+ finally
+ {
+ _isClosed = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_isDisposed)
+ return;
+
+ if (!_isClosed)
+ Close();
+
+ lock (_streamLock)
+ {
+ _stream?.Dispose();
+ _stream = null;
+ }
+
+ _isDisposed = true;
+ }
+
+ private bool AttemptConnection(string pipeName)
+ {
+ if (_isDisposed)
+ throw new ObjectDisposedException(nameof(DiscordRpcPipeClient));
+
+ try
+ {
+ lock (_streamLock)
+ {
+ Logger.Info("Attempting to connect to {0}", pipeName);
+ _stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
+ _stream.Connect(0);
+
+ Logger.Trace("Waiting for connection...");
+ while (!_stream.IsConnected)
+ Thread.Sleep(10);
+ }
+
+ Logger.Info("Connected to {0}", pipeName);
+ _connectedPipe = int.Parse(pipeName[(pipeName.LastIndexOf('-') + 1)..], System.Globalization.CultureInfo.InvariantCulture);
+ _isClosed = false;
+ }
+ catch (Exception e)
+ {
+ Logger.Error("Failed connection to {0}. {1}", pipeName, e.Message);
+ Close();
+ }
+
+ Logger.Trace("Done. Result: {0}", _isClosed);
+ return !_isClosed;
+ }
+
+ private void BeginReadStream()
+ {
+ if (_isClosed)
+ return;
+
+ try
+ {
+ lock (_streamLock)
+ {
+ if (_stream is not { IsConnected: true })
+ return;
+
+ Logger.Trace("Beginning Read of {0} bytes", _buffer.Length);
+ _stream.BeginRead(_buffer, 0, _buffer.Length, EndReadStream, _stream.IsConnected);
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ Logger.Warning("Attempted to start reading from a disposed pipe");
+ }
+ catch (InvalidOperationException)
+ {
+ Logger.Warning("Attempted to start reading from a closed pipe");
+ }
+ catch (Exception e)
+ {
+ Logger.Error("An exception occurred while starting to read a stream: {0}", e.Message);
+ Logger.Error(e.StackTrace);
+ }
+ }
+
+ private void EndReadStream(IAsyncResult callback)
+ {
+ Logger.Trace("Ending Read");
+ int bytes;
+
+ try
+ {
+ lock (_streamLock)
+ {
+ if (_stream is not { IsConnected: true })
+ return;
+
+ bytes = _stream.EndRead(callback);
+ }
+ }
+ catch (IOException)
+ {
+ Logger.Warning("Attempted to end reading from a closed pipe");
+ return;
+ }
+ catch (NullReferenceException)
+ {
+ Logger.Warning("Attempted to read from a null pipe");
+ return;
+ }
+ catch (ObjectDisposedException)
+ {
+ Logger.Warning("Attempted to end reading from a disposed pipe");
+ return;
+ }
+ catch (Exception e)
+ {
+ Logger.Error("An exception occurred while ending a read of a stream: {0}", e.Message);
+ Logger.Error(e.StackTrace);
+ return;
+ }
+
+ Logger.Trace("Read {0} bytes", bytes);
+
+ if (bytes > 0)
+ {
+ using MemoryStream memory = new(_buffer, 0, bytes);
+ try
+ {
+ PipeFrame frame = new();
+ if (frame.ReadStream(memory))
+ {
+ Logger.Trace("Read a frame: {0}", frame.Opcode);
+ lock (_frameQueueLock)
+ _frameQueue.Enqueue(frame);
+ }
+ else
+ {
+ Logger.Error("Pipe failed to read from the data received by the stream.");
+ Close();
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.Error("An exception has occurred while trying to parse the pipe data: {0}", e.Message);
+ Close();
+ }
+ }
+ else
+ {
+ Logger.Error("Empty frame was read on {0}, aborting.", Environment.OSVersion);
+ Close();
+ }
+
+ if (!_isClosed && IsConnected)
+ {
+ Logger.Trace("Starting another read");
+ BeginReadStream();
+ }
+ }
+
+ private static IEnumerable GetPipeCandidates(int startPipe)
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ for (int i = startPipe; i < MaximumPipeVariations; i++)
+ yield return $"{DiscordPipePrefix}{i}";
+
+ yield break;
+ }
+
+ foreach (string runtimeDir in GetUnixRuntimeDirectories())
+ {
+ for (int index = startPipe; index < MaximumPipeVariations; index++)
+ {
+ string pipeFileName = $"{DiscordPipePrefix}{index}";
+
+ foreach (string packageDirectory in s_unixPackageDirectories)
+ {
+ string packagePipe = Path.Combine(runtimeDir, packageDirectory, pipeFileName);
+ if (File.Exists(packagePipe))
+ yield return packagePipe;
+ }
+
+ string defaultPipe = Path.Combine(runtimeDir, pipeFileName);
+ if (File.Exists(defaultPipe))
+ yield return defaultPipe;
+
+ foreach (string packageDirectory in s_unixPackageDirectories)
+ {
+ string packagePipe = Path.Combine(runtimeDir, packageDirectory, pipeFileName);
+ if (!File.Exists(packagePipe))
+ yield return packagePipe;
+ }
+
+ if (!File.Exists(defaultPipe))
+ yield return defaultPipe;
+ }
+ }
+ }
+
+ private static IEnumerable GetUnixRuntimeDirectories()
+ {
+ HashSet yielded = new(StringComparer.Ordinal);
+
+ string[] candidates =
+ [
+ Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR") ?? string.Empty,
+ Environment.GetEnvironmentVariable("TMPDIR") ?? string.Empty,
+ Environment.GetEnvironmentVariable("TMP") ?? string.Empty,
+ Environment.GetEnvironmentVariable("TEMP") ?? string.Empty,
+ Path.GetTempPath(),
+ "/tmp"
+ ];
+
+ foreach (string candidate in candidates)
+ {
+ if (string.IsNullOrWhiteSpace(candidate))
+ continue;
+
+ string normalized = candidate.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ if (yielded.Add(normalized))
+ yield return normalized;
+ }
+ }
+ }
}
}
diff --git a/MinecraftClient/ChatBots/DiscordRpcPipeClient.cs b/MinecraftClient/ChatBots/DiscordRpcPipeClient.cs
deleted file mode 100644
index 61f2e098..00000000
--- a/MinecraftClient/ChatBots/DiscordRpcPipeClient.cs
+++ /dev/null
@@ -1,399 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.IO.Pipes;
-using System.Threading;
-using DiscordRPC.IO;
-using DiscordRPC.Logging;
-
-namespace MinecraftClient.ChatBots
-{
- ///
- /// Flatpak Discord exposes the RPC socket under app/com.discordapp.Discord,
- /// but the currently published DiscordRichPresence package does not probe that path.
- ///
- internal sealed class DiscordRpcPipeClient : INamedPipeClient
- {
- private const string DiscordPipePrefix = "discord-ipc-";
- private const int MaximumPipeVariations = 10;
-
- private static readonly string[] s_unixPackageDirectories =
- [
- // Official Discord clients
- "app/com.discordapp.Discord",
- "snap.discord",
-
- // Community desktop clients / wrappers
- "app/dev.vencord.Vesktop",
- ".flatpak/dev.vencord.Vesktop/xdg-run",
- "app/org.equicord.equibop",
- "app/io.github.equicord.equibop",
- "app/xyz.armcord.ArmCord",
- "app/io.github.spacingbat3.webcord"
- ];
-
- private readonly byte[] _buffer = new byte[PipeFrame.MAX_SIZE];
- private readonly Queue _frameQueue = new();
- private readonly object _frameQueueLock = new();
- private readonly object _streamLock = new();
-
- private int _connectedPipe;
- private NamedPipeClientStream? _stream;
- private volatile bool _isClosed = true;
- private volatile bool _isDisposed;
-
- public ILogger Logger { get; set; } = new NullLogger();
-
- public bool IsConnected
- {
- get
- {
- if (_isClosed)
- return false;
-
- lock (_streamLock)
- return _stream is { IsConnected: true };
- }
- }
-
- [Obsolete("The connected pipe is not neccessary information.")]
- public int ConnectedPipe => _connectedPipe;
-
- public bool Connect(int pipe)
- {
- Logger.Trace("DiscordRpcPipeClient.Connect({0})", pipe);
-
- if (_isDisposed)
- throw new ObjectDisposedException(nameof(DiscordRpcPipeClient));
-
- if (pipe > 9)
- throw new ArgumentOutOfRangeException(nameof(pipe), "Argument cannot be greater than 9");
-
- int startPipe = pipe >= 0 ? pipe : 0;
-
- foreach (string pipeName in GetPipeCandidates(startPipe))
- {
- if (AttemptConnection(pipeName))
- {
- BeginReadStream();
- return true;
- }
- }
-
- return false;
- }
-
- public bool ReadFrame(out PipeFrame frame)
- {
- if (_isDisposed)
- throw new ObjectDisposedException(nameof(DiscordRpcPipeClient));
-
- lock (_frameQueueLock)
- {
- if (_frameQueue.Count == 0)
- {
- frame = default;
- return false;
- }
-
- frame = _frameQueue.Dequeue();
- return true;
- }
- }
-
- public bool WriteFrame(PipeFrame frame)
- {
- if (_isDisposed)
- throw new ObjectDisposedException(nameof(DiscordRpcPipeClient));
-
- if (_isClosed || !IsConnected)
- {
- Logger.Error("Failed to write frame because the stream is closed");
- return false;
- }
-
- try
- {
- frame.WriteStream(_stream);
- return true;
- }
- catch (IOException io)
- {
- Logger.Error("Failed to write frame because of a IO Exception: {0}", io.Message);
- }
- catch (ObjectDisposedException)
- {
- Logger.Warning("Failed to write frame as the stream was already disposed");
- }
- catch (InvalidOperationException)
- {
- Logger.Warning("Failed to write frame because of a invalid operation");
- }
-
- return false;
- }
-
- public void Close()
- {
- if (_isClosed)
- {
- Logger.Warning("Tried to close a already closed pipe.");
- return;
- }
-
- try
- {
- lock (_streamLock)
- {
- if (_stream is not null)
- {
- try
- {
- _stream.Flush();
- _stream.Dispose();
- }
- catch
- {
- }
-
- _stream = null;
- _isClosed = true;
- }
- else
- {
- Logger.Warning("Stream was closed, but no stream was available to begin with!");
- }
- }
- }
- catch (ObjectDisposedException)
- {
- Logger.Warning("Tried to dispose already disposed stream");
- }
- finally
- {
- _isClosed = true;
- }
- }
-
- public void Dispose()
- {
- if (_isDisposed)
- return;
-
- if (!_isClosed)
- Close();
-
- lock (_streamLock)
- {
- _stream?.Dispose();
- _stream = null;
- }
-
- _isDisposed = true;
- }
-
- private bool AttemptConnection(string pipeName)
- {
- if (_isDisposed)
- throw new ObjectDisposedException(nameof(DiscordRpcPipeClient));
-
- try
- {
- lock (_streamLock)
- {
- Logger.Info("Attempting to connect to {0}", pipeName);
- _stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
- _stream.Connect(0);
-
- Logger.Trace("Waiting for connection...");
- while (!_stream.IsConnected)
- Thread.Sleep(10);
- }
-
- Logger.Info("Connected to {0}", pipeName);
- _connectedPipe = int.Parse(pipeName[(pipeName.LastIndexOf('-') + 1)..], System.Globalization.CultureInfo.InvariantCulture);
- _isClosed = false;
- }
- catch (Exception e)
- {
- Logger.Error("Failed connection to {0}. {1}", pipeName, e.Message);
- Close();
- }
-
- Logger.Trace("Done. Result: {0}", _isClosed);
- return !_isClosed;
- }
-
- private void BeginReadStream()
- {
- if (_isClosed)
- return;
-
- try
- {
- lock (_streamLock)
- {
- if (_stream is not { IsConnected: true })
- return;
-
- Logger.Trace("Beginning Read of {0} bytes", _buffer.Length);
- _stream.BeginRead(_buffer, 0, _buffer.Length, EndReadStream, _stream.IsConnected);
- }
- }
- catch (ObjectDisposedException)
- {
- Logger.Warning("Attempted to start reading from a disposed pipe");
- }
- catch (InvalidOperationException)
- {
- Logger.Warning("Attempted to start reading from a closed pipe");
- }
- catch (Exception e)
- {
- Logger.Error("An exception occurred while starting to read a stream: {0}", e.Message);
- Logger.Error(e.StackTrace);
- }
- }
-
- private void EndReadStream(IAsyncResult callback)
- {
- Logger.Trace("Ending Read");
- int bytes;
-
- try
- {
- lock (_streamLock)
- {
- if (_stream is not { IsConnected: true })
- return;
-
- bytes = _stream.EndRead(callback);
- }
- }
- catch (IOException)
- {
- Logger.Warning("Attempted to end reading from a closed pipe");
- return;
- }
- catch (NullReferenceException)
- {
- Logger.Warning("Attempted to read from a null pipe");
- return;
- }
- catch (ObjectDisposedException)
- {
- Logger.Warning("Attempted to end reading from a disposed pipe");
- return;
- }
- catch (Exception e)
- {
- Logger.Error("An exception occurred while ending a read of a stream: {0}", e.Message);
- Logger.Error(e.StackTrace);
- return;
- }
-
- Logger.Trace("Read {0} bytes", bytes);
-
- if (bytes > 0)
- {
- using MemoryStream memory = new(_buffer, 0, bytes);
- try
- {
- PipeFrame frame = new();
- if (frame.ReadStream(memory))
- {
- Logger.Trace("Read a frame: {0}", frame.Opcode);
- lock (_frameQueueLock)
- _frameQueue.Enqueue(frame);
- }
- else
- {
- Logger.Error("Pipe failed to read from the data received by the stream.");
- Close();
- }
- }
- catch (Exception e)
- {
- Logger.Error("An exception has occurred while trying to parse the pipe data: {0}", e.Message);
- Close();
- }
- }
- else
- {
- Logger.Error("Empty frame was read on {0}, aborting.", Environment.OSVersion);
- Close();
- }
-
- if (!_isClosed && IsConnected)
- {
- Logger.Trace("Starting another read");
- BeginReadStream();
- }
- }
-
- private static IEnumerable GetPipeCandidates(int startPipe)
- {
- if (OperatingSystem.IsWindows())
- {
- for (int i = startPipe; i < MaximumPipeVariations; i++)
- yield return $"{DiscordPipePrefix}{i}";
-
- yield break;
- }
-
- foreach (string runtimeDir in GetUnixRuntimeDirectories())
- {
- for (int index = startPipe; index < MaximumPipeVariations; index++)
- {
- string pipeFileName = $"{DiscordPipePrefix}{index}";
-
- foreach (string packageDirectory in s_unixPackageDirectories)
- {
- string packagePipe = Path.Combine(runtimeDir, packageDirectory, pipeFileName);
- if (File.Exists(packagePipe))
- yield return packagePipe;
- }
-
- string defaultPipe = Path.Combine(runtimeDir, pipeFileName);
- if (File.Exists(defaultPipe))
- yield return defaultPipe;
-
- foreach (string packageDirectory in s_unixPackageDirectories)
- {
- string packagePipe = Path.Combine(runtimeDir, packageDirectory, pipeFileName);
- if (!File.Exists(packagePipe))
- yield return packagePipe;
- }
-
- if (!File.Exists(defaultPipe))
- yield return defaultPipe;
- }
- }
- }
-
- private static IEnumerable GetUnixRuntimeDirectories()
- {
- HashSet yielded = new(StringComparer.Ordinal);
-
- string[] candidates =
- [
- Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR") ?? string.Empty,
- Environment.GetEnvironmentVariable("TMPDIR") ?? string.Empty,
- Environment.GetEnvironmentVariable("TMP") ?? string.Empty,
- Environment.GetEnvironmentVariable("TEMP") ?? string.Empty,
- Path.GetTempPath(),
- "/tmp"
- ];
-
- foreach (string candidate in candidates)
- {
- if (string.IsNullOrWhiteSpace(candidate))
- continue;
-
- string normalized = candidate.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
- if (yielded.Add(normalized))
- yield return normalized;
- }
- }
- }
-}