Beta Release
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user