258 lines
7.6 KiB
C#
258 lines
7.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using System.Timers;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.IO;
|
|
using Microsoft.Diagnostics.Tracing.Session;
|
|
using Rug.Osc;
|
|
|
|
namespace VRChatboxApp {
|
|
static class Program {
|
|
/// <summary>
|
|
/// The main entry point for the application.
|
|
/// </summary>
|
|
[STAThread]
|
|
static void Main() {
|
|
|
|
VRChatboxApp.Start();
|
|
|
|
Application.EnableVisualStyles();
|
|
Application.SetCompatibleTextRenderingDefault(false);
|
|
Application.Run(new Form1());
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Sends chatbox messages to VRChat OSC, avoids triggering anti-spam
|
|
/// </summary>
|
|
class VRChatboxSender {
|
|
|
|
// think vrchat locks chat once 5th message sent within 5 seconds, so allow only 4
|
|
private const int QUOTA_SIZE = 4;
|
|
private const int QUOTA_TIME = 5000;
|
|
|
|
private OscSender osc;
|
|
private int quota = QUOTA_SIZE;
|
|
private System.Timers.Timer quotaResetTimer;
|
|
private string pendingMessage;
|
|
//private string lastMessage;
|
|
//private DateTime lastMessageTime;
|
|
|
|
public VRChatboxSender(string ip_address, int dstport, int srcport) {
|
|
osc = new OscSender(System.Net.IPAddress.Parse(ip_address), srcport, dstport);
|
|
osc.Connect();
|
|
}
|
|
|
|
public event EventHandler<EventArgs<string>> OnSend;
|
|
|
|
public void send(string message) {
|
|
/* this is only good if viewer hasnt turned down chatbox display duration
|
|
if (message == lastMessage && (DateTime.Now - lastMessageTime).Seconds < 30) return;
|
|
else {
|
|
lastMessage = message;
|
|
lastMessageTime = DateTime.Now;
|
|
}*/
|
|
if (quota == 0) {
|
|
//Console.WriteLine("pending: " + text);
|
|
pendingMessage = message;
|
|
} else {
|
|
Console.WriteLine("send: " + message);
|
|
OnSend(this, new EventArgs<string>(message));
|
|
osc.Send(new OscMessage("/chatbox/input", message, true));
|
|
quota--;
|
|
if (quotaResetTimer == null) {
|
|
quotaResetTimer = new System.Timers.Timer(QUOTA_TIME);
|
|
quotaResetTimer.Elapsed += (sender, e) => {
|
|
//Console.WriteLine("timer end");
|
|
quota = QUOTA_SIZE;
|
|
if (pendingMessage != null) {
|
|
send(pendingMessage);
|
|
pendingMessage = null;
|
|
}
|
|
quotaResetTimer.Close();
|
|
quotaResetTimer = null;
|
|
};
|
|
quotaResetTimer.AutoReset = false;
|
|
quotaResetTimer.Start();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
static class VRChatboxApp {
|
|
public static VRChatboxSender chatbox = new VRChatboxSender("127.0.0.1", 9000, 8999);
|
|
|
|
private static System.Timers.Timer timer;
|
|
private static List<string> chats = new List<string>();
|
|
|
|
private static string GenerateStats() {
|
|
var time = DateTime.Now.ToShortTimeString();
|
|
var fps = VRChatFPSMonitor.GetFPS();
|
|
var stats = time + ", " + fps + " FPS";
|
|
var app = ActiveAppGetter.GetActiveAppName();
|
|
var self = Process.GetCurrentProcess().ProcessName;
|
|
if (app != "VRChat.exe" && app != self) stats += ", using " + app;
|
|
return stats;
|
|
}
|
|
|
|
private static void RenderChatbox() {
|
|
string content;
|
|
var stats = GenerateStats();
|
|
if (chats.Count == 0) {
|
|
chatbox.send(stats);
|
|
return;
|
|
}
|
|
var separator = " | ";
|
|
var chatString = String.Join(separator, chats);
|
|
while (chatString.Length > 144) {
|
|
chats.RemoveAt(0);
|
|
chatString = String.Join(separator, chats);
|
|
}
|
|
|
|
var excessLength = stats.Length + separator.Length + chatString.Length - 144;
|
|
if (excessLength > 0) stats = stats.Substring(0, Math.Max(stats.Length - separator.Length - excessLength, 0));
|
|
if (stats.Length > 0) stats += separator;
|
|
content = stats + chatString;
|
|
|
|
chatbox.send(content);
|
|
}
|
|
|
|
public static void Start() {
|
|
VRChatFPSMonitor.Start();
|
|
timer = new System.Timers.Timer(2000);
|
|
timer.Elapsed += (sender, e) => RenderChatbox();
|
|
timer.Start();
|
|
}
|
|
|
|
public static void SendChatMessage(string message) {
|
|
if (message.Length > 144) message = message.Substring(0, 144);
|
|
if (String.IsNullOrEmpty(message)) chats.Clear();
|
|
else chats.Add(message);
|
|
RenderChatbox();
|
|
}
|
|
|
|
}
|
|
|
|
|
|
static class VRChatFPSMonitor {
|
|
|
|
private const int AVERAGING_WINDOW = 2000; //ms
|
|
static List<long> frameTimestamps = new List<long>();
|
|
static private Stopwatch watch = new Stopwatch();
|
|
|
|
static private Dictionary<int, string> pid2name = new Dictionary<int, string>();
|
|
|
|
private const int EventID_DxgiPresentStart = 42;
|
|
static private readonly Guid DXGI_provider = Guid.Parse("{CA11C036-0102-4A2D-A6AD-F03CFED5D3C9}");
|
|
static private TraceEventSession traceEventSession = new TraceEventSession("vrcfpsmon");
|
|
|
|
static private void RecordFrameTimestamp(long timestamp) {
|
|
frameTimestamps.Add(timestamp);
|
|
var min = watch.ElapsedMilliseconds - AVERAGING_WINDOW;
|
|
while (true) {
|
|
if (frameTimestamps.ElementAt(0) < min) frameTimestamps.RemoveAt(0);
|
|
else break;
|
|
}
|
|
}
|
|
|
|
static public void Start() {
|
|
watch.Start();
|
|
traceEventSession.EnableProvider("Microsoft-Windows-DXGI");
|
|
traceEventSession.Source.AllEvents += data => {
|
|
if ((int)data.ID == EventID_DxgiPresentStart && data.ProviderGuid == DXGI_provider) {
|
|
var pid = data.ProcessID;
|
|
var timestamp = watch.ElapsedMilliseconds;
|
|
if (!pid2name.ContainsKey(pid)) {
|
|
var process = Process.GetProcessById(pid);
|
|
pid2name.Add(pid, process.ProcessName);
|
|
}
|
|
if (pid2name[pid] == "VRChat") {
|
|
RecordFrameTimestamp(watch.ElapsedMilliseconds);
|
|
}
|
|
}
|
|
};
|
|
var traceEventThread = new Thread(() => {
|
|
traceEventSession.Source.Process();
|
|
});
|
|
traceEventThread.IsBackground = true;
|
|
traceEventThread.Start();
|
|
}
|
|
|
|
static public int GetFPS() {
|
|
return frameTimestamps.Count() / (AVERAGING_WINDOW / 1000);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
static class ActiveAppGetter {
|
|
[Flags]
|
|
private enum ProcessAccessFlags : uint {
|
|
PROCESS_ALL_ACCESS = 0x001F0FFF,
|
|
PROCESS_CREATE_PROCESS = 0x00000080,
|
|
PROCESS_CREATE_THREAD = 0x00000002,
|
|
PROCESS_DUP_HANDLE = 0x00000040,
|
|
PROCESS_QUERY_INFORMATION = 0x00000400,
|
|
PROCESS_QUERY_LIMITED_INFORMATION = 0x00001000,
|
|
PROCESS_SET_INFORMATION = 0x00000200,
|
|
PROCESS_SET_QUOTA = 0x00000100,
|
|
PROCESS_SUSPEND_RESUME = 0x00000800,
|
|
PROCESS_TERMINATE = 0x00000001,
|
|
PROCESS_VM_OPERATION = 0x00000008,
|
|
PROCESS_VM_READ = 0x00000010,
|
|
PROCESS_VM_WRITE = 0x00000020
|
|
}
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern IntPtr GetForegroundWindow();
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern IntPtr GetWindowThreadProcessId(IntPtr hWnd, out IntPtr lpdwProcessId);
|
|
|
|
[DllImport("kernel32.dll")]
|
|
private static extern bool QueryFullProcessImageName([In] IntPtr hProcess, [In] uint dwFlags, [Out] StringBuilder lpExeName, ref uint lpdwSize);
|
|
|
|
[DllImport("kernel32.dll")]
|
|
private static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, bool bInheritHandle, int dwProcessId);
|
|
|
|
public static string GetActiveAppName() {
|
|
try {
|
|
IntPtr foregroundWindowId = GetForegroundWindow();
|
|
GetWindowThreadProcessId(foregroundWindowId, out IntPtr activeAppProcessId);
|
|
IntPtr hprocess = OpenProcess(ProcessAccessFlags.PROCESS_QUERY_LIMITED_INFORMATION, false, (int)activeAppProcessId);
|
|
uint lpdwSize = 1024;
|
|
StringBuilder lpExeName = new StringBuilder((int)lpdwSize);
|
|
QueryFullProcessImageName(hprocess, 0, lpExeName, ref lpdwSize);
|
|
var exePath = lpExeName.ToString();
|
|
FileVersionInfo appInfo = FileVersionInfo.GetVersionInfo(exePath);
|
|
return String.IsNullOrEmpty(appInfo.FileDescription) ? Path.GetFileName(exePath) : appInfo.FileDescription;
|
|
} catch (Exception e) {
|
|
return e.Message;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
public class EventArgs<T> : EventArgs {
|
|
public T Value { get; private set; }
|
|
|
|
public EventArgs(T val) {
|
|
Value = val;
|
|
}
|
|
} |