using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; using System.Threading; using MSCLoader; using UnityEngine; namespace MSCO { /// /// Discord Rich Presence über Discords lokale IPC-Schnittstelle (Named Pipe "discord-ipc-N"), /// ohne zusätzliche DLL. Liest/schreibt direkt mit ReadFile/WriteFile (Unitys altes Mono kommt /// mit Pipes in FileStream nicht zurecht). Läuft in einem Hintergrund-Thread; ist Discord nicht /// offen, wird es alle 15 s erneut versucht. Statusmeldungen landen über TakeLog() in der Konsole. /// public class DiscordPresence { /// /// Application ID aus dem Discord Developer Portal (https://discord.com/developers/applications). /// Unter "Rich Presence → Art Assets" ein Bild mit dem Namen "logo" hochladen (branding/logo-512.png). /// public const string ApplicationId = "1553384871303450664"; const string ServersUrl = "https://msco.mbm.wtf/servers"; const int RetrySeconds = 15; readonly object gate = new object(); readonly Queue log = new Queue(); string pendingActivity; // JSON der Aktivität, die noch gesendet werden muss string lastActivity; string lastStatus; Thread worker; volatile bool running; int nonce; int pid; public static bool Configured { get { return ApplicationId.Trim('0').Length > 0; } } public void Start() { if (running) return; if (!Configured) { ModConsole.Print("[MSCO] Discord Rich Presence: no application id set (DiscordPresence.ApplicationId)"); return; } if (Application.platform != RuntimePlatform.WindowsPlayer && Application.platform != RuntimePlatform.WindowsEditor) return; try { pid = System.Diagnostics.Process.GetCurrentProcess().Id; } catch { pid = 0; } running = true; worker = new Thread(Run) { IsBackground = true, Name = "MSCO Discord" }; worker.Start(); } public void Stop() { running = false; } /// Statusmeldungen des Hintergrund-Threads (im Hauptthread in die Konsole schreiben). public string TakeLog() { lock (gate) return log.Count > 0 ? log.Dequeue() : null; } void Status(string text) { lock (gate) { if (text == lastStatus) return; // gleiche Meldung nicht alle 15 s wiederholen lastStatus = text; log.Enqueue(text); } } /// Neue Anzeige setzen (wird nur gesendet, wenn sie sich geändert hat). public void SetActivity(string details, string state, int partySize, int partyMax, string partyId, long startUnix) { var sb = new StringBuilder(); sb.Append("{\"details\":").Append(Json(details)); if (!string.IsNullOrEmpty(state)) sb.Append(",\"state\":").Append(Json(state)); if (startUnix > 0) sb.Append(",\"timestamps\":{\"start\":").Append(startUnix).Append('}'); sb.Append(",\"assets\":{\"large_image\":\"logo\",\"large_text\":\"My Summer Car Online\"}"); if (partySize > 0 && partyMax >= partySize) sb.Append(",\"party\":{\"id\":").Append(Json(partyId ?? "msco")).Append(",\"size\":[").Append(partySize).Append(',').Append(partyMax).Append("]}"); sb.Append(",\"buttons\":[{\"label\":\"MSCO Servers\",\"url\":").Append(Json(ServersUrl)).Append("}]}"); string activity = sb.ToString(); lock (gate) { if (activity == lastActivity) return; lastActivity = activity; pendingActivity = activity; } } // ---------------------------------------------------------------- Hintergrund-Thread void Run() { while (running) { IntPtr pipe = InvalidHandle; try { pipe = Connect(); if (pipe == InvalidHandle) { Status("Discord not running (will retry)"); Sleep(RetrySeconds); continue; } Write(pipe, 0, "{\"v\":1,\"client_id\":" + Json(ApplicationId) + "}"); string ready = Read(pipe); if (ready.IndexOf("READY", StringComparison.Ordinal) < 0) throw new Exception("Unexpected handshake reply: " + Short(ready)); Status("Discord connected"); lock (gate) pendingActivity = lastActivity; // nach (Wieder-)Verbindung erneut senden while (running) { string activity; lock (gate) { activity = pendingActivity; pendingActivity = null; } if (activity != null) { Write(pipe, 1, "{\"cmd\":\"SET_ACTIVITY\",\"args\":{\"pid\":" + pid + ",\"activity\":" + activity + "},\"nonce\":\"" + (++nonce) + "\"}"); string reply = Read(pipe); if (reply.IndexOf("\"evt\":\"ERROR\"", StringComparison.Ordinal) >= 0) Status("Discord rejected the activity: " + Short(reply)); } Thread.Sleep(1000); } } catch (Exception e) { Status("Discord connection lost: " + e.Message); } finally { if (pipe != InvalidHandle) CloseHandle(pipe); } if (running) Sleep(RetrySeconds); } } void Sleep(int seconds) { for (int i = 0; i < seconds * 10 && running; i++) Thread.Sleep(100); } static IntPtr Connect() { for (int i = 0; i < 10; i++) { IntPtr handle = CreateFile(@"\\.\pipe\discord-ipc-" + i, GenericRead | GenericWrite, 0, IntPtr.Zero, OpenExisting, 0, IntPtr.Zero); if (handle != InvalidHandle) return handle; } return InvalidHandle; } /// Discord-IPC-Frame: int32 Opcode, int32 Länge, JSON (UTF-8). static void Write(IntPtr pipe, int opcode, string json) { byte[] payload = Encoding.UTF8.GetBytes(json); var frame = new byte[8 + payload.Length]; BitConverter.GetBytes(opcode).CopyTo(frame, 0); BitConverter.GetBytes(payload.Length).CopyTo(frame, 4); Buffer.BlockCopy(payload, 0, frame, 8, payload.Length); int written; if (!WriteFile(pipe, frame, frame.Length, out written, IntPtr.Zero) || written != frame.Length) throw new Exception("WriteFile failed (" + Marshal.GetLastWin32Error() + ")"); } static string Read(IntPtr pipe) { byte[] header = ReadExactly(pipe, 8); int opcode = BitConverter.ToInt32(header, 0); int length = BitConverter.ToInt32(header, 4); if (length < 0 || length > 64 * 1024) throw new Exception("Bad Discord frame"); string json = Encoding.UTF8.GetString(ReadExactly(pipe, length)); if (opcode == 2) throw new Exception("Discord closed the connection: " + Short(json)); return json; } static byte[] ReadExactly(IntPtr pipe, int count) { var data = new byte[count]; int offset = 0; var chunk = new byte[count]; while (offset < count) { int read; if (!ReadFile(pipe, chunk, count - offset, out read, IntPtr.Zero) || read <= 0) throw new Exception("ReadFile failed (" + Marshal.GetLastWin32Error() + ")"); Buffer.BlockCopy(chunk, 0, data, offset, read); offset += read; } return data; } static string Short(string s) { return s == null ? "" : s.Length > 160 ? s.Substring(0, 160) + "..." : s; } static string Json(string value) { var sb = new StringBuilder("\""); foreach (char c in value ?? "") { switch (c) { case '"': sb.Append("\\\""); break; case '\\': sb.Append("\\\\"); break; case '\n': sb.Append("\\n"); break; case '\r': break; case '\t': sb.Append(' '); break; default: if (c < 0x20) break; sb.Append(c); break; } } return sb.Append('"').ToString(); } static readonly IntPtr InvalidHandle = new IntPtr(-1); const uint GenericRead = 0x80000000; const uint GenericWrite = 0x40000000; const uint OpenExisting = 3; [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] static extern IntPtr CreateFile(string name, uint access, uint share, IntPtr security, uint creation, uint flags, IntPtr template); [DllImport("kernel32.dll", SetLastError = true)] static extern bool ReadFile(IntPtr handle, byte[] buffer, int toRead, out int read, IntPtr overlapped); [DllImport("kernel32.dll", SetLastError = true)] static extern bool WriteFile(IntPtr handle, byte[] buffer, int toWrite, out int written, IntPtr overlapped); [DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr handle); } }