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 { /// /// The main entry point for the application. /// [STAThread] static void Main() { VRChatboxApp.Start(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } /// /// Sends chatbox messages to VRChat OSC, avoids triggering anti-spam /// 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 bool soundOnNextSend = false; 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> 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(message)); osc.Send(new OscMessage("/chatbox/input", message, true, soundOnNextSend)); soundOnNextSend = false; 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 chats = new List(); 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); chatbox.soundOnNextSend = true; } RenderChatbox(); } } static class VRChatFPSMonitor { private const int AVERAGING_WINDOW = 2000; //ms static List frameTimestamps = new List(); static private Stopwatch watch = new Stopwatch(); static private Dictionary pid2name = new Dictionary(); 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 : EventArgs { public T Value { get; private set; } public EventArgs(T val) { Value = val; } }