VRChatboxApp/Program.cs

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;
}
}