255 lines
10 KiB
C#
255 lines
10 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using MSCLoader;
|
|
using UnityEngine;
|
|
|
|
namespace MSCO
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class DiscordPresence
|
|
{
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
public const string ApplicationId = "1553384871303450664";
|
|
|
|
const string ServersUrl = "https://msco.mbm.wtf/servers";
|
|
const int RetrySeconds = 15;
|
|
|
|
readonly object gate = new object();
|
|
readonly Queue<string> log = new Queue<string>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>Statusmeldungen des Hintergrund-Threads (im Hauptthread in die Konsole schreiben).</summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>Neue Anzeige setzen (wird nur gesendet, wenn sie sich geändert hat).</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Discord-IPC-Frame: int32 Opcode, int32 Länge, JSON (UTF-8).</summary>
|
|
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);
|
|
}
|
|
}
|