263 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			263 lines
		
	
	
		
			7.7 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 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<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, 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<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);
 | 
						|
				chatbox.soundOnNextSend = true;
 | 
						|
			}
 | 
						|
			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;
 | 
						|
	}
 | 
						|
} |