From 8431777386de631244f4dfc95e48adb6f67fd4fe Mon Sep 17 00:00:00 2001 From: nnaaa Date: Thu, 1 Apr 2021 16:18:44 -0400 Subject: [PATCH] Initial commit --- .gitignore | 37 ++ LICENSE | 21 + README.md | 10 + XSOverlay VRChat Parser.sln | 25 ++ XSOverlay VRChat Parser/Helpers/Annotation.cs | 18 + XSOverlay VRChat Parser/Helpers/EventType.cs | 11 + .../Helpers/LogEventType.cs | 9 + .../Helpers/TailSubscription.cs | 62 +++ .../Models/ConfigurationModel.cs | 191 ++++++++ XSOverlay VRChat Parser/Program.cs | 417 ++++++++++++++++++ .../PublishProfiles/FolderProfile.pubxml | 18 + .../Resources/Audio/player_joined.ogg | Bin 0 -> 6125 bytes .../Resources/Audio/player_left.ogg | Bin 0 -> 6132 bytes .../Resources/Icons/keywords_exceeded.png | Bin 0 -> 3431 bytes .../Resources/Icons/player_joined.png | Bin 0 -> 4886 bytes .../Resources/Icons/player_left.png | Bin 0 -> 4202 bytes .../Resources/Icons/portal_dropped.png | Bin 0 -> 6480 bytes .../Resources/Icons/world_changed.png | Bin 0 -> 5422 bytes .../XSOverlay VRChat Parser.csproj | 37 ++ 19 files changed, 856 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 XSOverlay VRChat Parser.sln create mode 100644 XSOverlay VRChat Parser/Helpers/Annotation.cs create mode 100644 XSOverlay VRChat Parser/Helpers/EventType.cs create mode 100644 XSOverlay VRChat Parser/Helpers/LogEventType.cs create mode 100644 XSOverlay VRChat Parser/Helpers/TailSubscription.cs create mode 100644 XSOverlay VRChat Parser/Models/ConfigurationModel.cs create mode 100644 XSOverlay VRChat Parser/Program.cs create mode 100644 XSOverlay VRChat Parser/Properties/PublishProfiles/FolderProfile.pubxml create mode 100644 XSOverlay VRChat Parser/Resources/Audio/player_joined.ogg create mode 100644 XSOverlay VRChat Parser/Resources/Audio/player_left.ogg create mode 100644 XSOverlay VRChat Parser/Resources/Icons/keywords_exceeded.png create mode 100644 XSOverlay VRChat Parser/Resources/Icons/player_joined.png create mode 100644 XSOverlay VRChat Parser/Resources/Icons/player_left.png create mode 100644 XSOverlay VRChat Parser/Resources/Icons/portal_dropped.png create mode 100644 XSOverlay VRChat Parser/Resources/Icons/world_changed.png create mode 100644 XSOverlay VRChat Parser/XSOverlay VRChat Parser.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a437a65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode + +# Rider +.idea + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn + +# Visual Studio 2015 +.vs/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..659925c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 nnaaa-vr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..05973fc --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +Simple VRChat log parser leveraging the XSNotifications library at https://github.com/nnaaa-vr/XSNotifications to display toast notifications in VR. Currently supported events include world changes, player joined, player left, portal dropped, and shader keywords exceeded. Logged events are also saved with timestamps to session logs, so previous sessions can be referenced if necessary (see who was where, what world you were in, et cetera). + +On first run, a config.json file will be generated at %AppData%\..\LocalLow\XSOverlay VRChat Parser\config.json. Please see this file for configuration options, and then restart the process after making any changes. + +The process runs in the background and does not have an active window. +The parsing is a bit messy at the moment (but functional). VRChat's log output is very inconsistent and has changed fairly frequently. The parsing function was written around two years ago and has just been patched as time went on. + +There are plans to expand the feature set of this application, including additional event types for those running extended logging in VRChat. Of course, feel free to make PRs yourselves! + +More detailed documentation will come soon. \ No newline at end of file diff --git a/XSOverlay VRChat Parser.sln b/XSOverlay VRChat Parser.sln new file mode 100644 index 0000000..cd6c1b6 --- /dev/null +++ b/XSOverlay VRChat Parser.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31105.61 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XSOverlay VRChat Parser", "XSOverlay VRChat Parser\XSOverlay VRChat Parser.csproj", "{02DC8D37-5DAC-448B-BAFE-14C8FEFBD782}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {02DC8D37-5DAC-448B-BAFE-14C8FEFBD782}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02DC8D37-5DAC-448B-BAFE-14C8FEFBD782}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02DC8D37-5DAC-448B-BAFE-14C8FEFBD782}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02DC8D37-5DAC-448B-BAFE-14C8FEFBD782}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {459F651C-259C-421A-9E69-346F3636C4E4} + EndGlobalSection +EndGlobal diff --git a/XSOverlay VRChat Parser/Helpers/Annotation.cs b/XSOverlay VRChat Parser/Helpers/Annotation.cs new file mode 100644 index 0000000..0e410ac --- /dev/null +++ b/XSOverlay VRChat Parser/Helpers/Annotation.cs @@ -0,0 +1,18 @@ +using System; + +namespace XSOverlay_VRChat_Parser.Helpers +{ + [AttributeUsage(AttributeTargets.Property)] + public class Annotation : Attribute + { + public string description; + public string groupDescription; + public bool startsGroup; + public Annotation(string _description, bool _startsGroup = false, string _groupDescription = "") + { + this.description = _description; + this.startsGroup = _startsGroup; + this.groupDescription = _groupDescription; + } + } +} diff --git a/XSOverlay VRChat Parser/Helpers/EventType.cs b/XSOverlay VRChat Parser/Helpers/EventType.cs new file mode 100644 index 0000000..8c3ca5b --- /dev/null +++ b/XSOverlay VRChat Parser/Helpers/EventType.cs @@ -0,0 +1,11 @@ +namespace XSOverlay_VRChat_Parser.Helpers +{ + public enum EventType + { + PlayerJoin, + PlayerLeft, + WorldChange, + KeywordsExceeded, + PortalDropped + } +} diff --git a/XSOverlay VRChat Parser/Helpers/LogEventType.cs b/XSOverlay VRChat Parser/Helpers/LogEventType.cs new file mode 100644 index 0000000..f5e6718 --- /dev/null +++ b/XSOverlay VRChat Parser/Helpers/LogEventType.cs @@ -0,0 +1,9 @@ +namespace XSOverlay_VRChat_Parser.Helpers +{ + public enum LogEventType + { + Error, + Event, + Info + } +} diff --git a/XSOverlay VRChat Parser/Helpers/TailSubscription.cs b/XSOverlay VRChat Parser/Helpers/TailSubscription.cs new file mode 100644 index 0000000..b58e826 --- /dev/null +++ b/XSOverlay VRChat Parser/Helpers/TailSubscription.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; + +namespace XSOverlay_VRChat_Parser.Helpers +{ + public class TailSubscription : IDisposable + { + private string _filePath { get; set; } + public delegate void OnUpdate(string content); + private OnUpdate updateFunc { get; set; } + private Timer timer { get; set; } + private long lastSize { get; set; } + + public TailSubscription(string filePath, OnUpdate func, long dueTimeMilliseconds, long frequencyMilliseconds) + { + _filePath = filePath; + updateFunc = func; + lastSize = new FileInfo(filePath).Length; + timer = new Timer(new TimerCallback(ExecOnUpdate), null, dueTimeMilliseconds, frequencyMilliseconds); + } + + private void ExecOnUpdate(object timerState) + { + if (!File.Exists(_filePath)) + { + Dispose(); + return; + } + + long size = new FileInfo(_filePath).Length; + + if (size > lastSize) + { + using (FileStream fs = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + using (StreamReader sr = new StreamReader(fs)) + { + sr.BaseStream.Seek(lastSize, SeekOrigin.Begin); + + StringBuilder outputContent = new StringBuilder(); + string line = string.Empty; + + while ((line = sr.ReadLine()) != null) + outputContent.Append(line + "\n"); + + lastSize = sr.BaseStream.Position; + + updateFunc(outputContent.ToString()); + } + } + } + } + + public void Dispose() + { + timer.Dispose(); + updateFunc = null; + } + } +} diff --git a/XSOverlay VRChat Parser/Models/ConfigurationModel.cs b/XSOverlay VRChat Parser/Models/ConfigurationModel.cs new file mode 100644 index 0000000..f24eb74 --- /dev/null +++ b/XSOverlay VRChat Parser/Models/ConfigurationModel.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Text.Json; +using XSNotifications.Enum; +using XSNotifications.Helpers; +using XSOverlay_VRChat_Parser.Helpers; + +namespace XSOverlay_VRChat_Parser.Models +{ + public class ConfigurationModel + { + [Annotation("Polling frequency for individual log file updates.", true, "GENERAL CONFIGURATION")] + public long ParseFrequencyMilliseconds { get; set; } + [Annotation("Polling frequency for new logs in OutputLogRoot")] + public long DirectoryPollFrequencyMilliseconds { get; set; } + [Annotation("Absolute path to output log root for VRChat. Environment variables will be expanded.")] + public string OutputLogRoot { get; set; } + [Annotation("Determines whether or not logs of parsed events will be written to the session log in the user folder. Valid values: true, false")] + public bool LogNotificationEvents { get; set; } + [Annotation("Volume for incoming notification sounds. Valid values: 0.0 -> 1.0.")] + public float NotificationVolume { get; set; } + [Annotation("Opacity for toast notifications. Valid values: 0.0 -> 1.0.")] + public float Opacity { get; set; } + + [Annotation("Determines whether or not player join notifications are delivered. Valid values: true, false", true, "PLAYER JOINED")] + public bool DisplayPlayerJoined { get; set; } + [Annotation("Period of time in seconds for the player join notification to remain on screen. Valid values: 0.0 -> float32 max")] + public float PlayerJoinedNotificationTimeoutSeconds { get; set; } + [Annotation("Relative path to icon for player joins. Other valid values include: \"\", \"default\", \"warning\", \"error\"")] + public string PlayerJoinedIconPath { get; set; } + [Annotation("Relative path to ogg-formatted audio for player joins. Other valid values include: \"\", \"default\", \"warning\", \"error\"")] + public string PlayerJoinedAudioPath { get; set; } + + [Annotation("Determines whether or not player left notifications are delivered. Valid values: true, false", true, "PLAYER LEFT")] + public bool DisplayPlayerLeft { get; set; } + [Annotation("Period of time in seconds for the player left notification to remain on screen. Valid values: 0.0 -> float32 max")] + public float PlayerLeftNotificationTimeoutSeconds { get; set; } + [Annotation("Relative path to icon for player left. Other valid values include: \"\", \"default\", \"warning\", \"error\"")] + public string PlayerLeftIconPath { get; set; } + [Annotation("Relative path to ogg-formatted audio for player left. Other valid values include: \"\", \"default\", \"warning\", \"error\"")] + public string PlayerLeftAudioPath { get; set; } + + [Annotation("Determines whether or not world change notifications are delivered. Valid values: true, false", true, "WORLD CHANGED")] + public bool DisplayWorldChanged { get; set; } + [Annotation("Period of time in seconds for player join/leave notifications to be silenced on world join. This is to avoid spam from enumerating everyone currently in the target world. Valid values: 0.0 -> float32 max")] + public long WorldJoinSilenceSeconds { get; set; } + [Annotation("Determines whether or not player join/leave notifications are silenced on world join. Warning, this gets spammy if on! Valid values: true, false")] + public bool DisplayJoinLeaveSilencedOverride { get; set; } + [Annotation("Period of time in seconds for the world changed notification to remain on screen. Value values: 0.0 -> float32 max")] + public float WorldChangedNotificationTimeoutSeconds { get; set; } + [Annotation("Relative path to icon for world changed. Other valid values include: \"\", \"default\", \"warning\", \"error\"")] + public string WorldChangedIconPath { get; set; } + [Annotation("Relative path to ogg-formatted audio for world changed. Other valid values include: \"\", \"default\", \"warning\", \"error\"")] + public string WorldChangedAudioPath { get; set; } + + [Annotation("Determines whether or not shader keywords exceeded notifications are delivered. Valid values: true, false", true, "SHADER KEYWORDS EXCEEDED")] + public bool DisplayMaximumKeywordsExceeded { get; set; } + [Annotation("Period of time in seconds for shader keywords exceeded notification to remain on screen. Valid values: 0.0 -> float32 max")] + public float MaximumKeywordsExceededTimeoutSeconds { get; set; } + [Annotation("Period of time in seconds after a shader keywords exceeded notification is sent to ignore shader keywords exceeded events. Valid values: 0.0 -> float32 max")] + public float MaximumKeywordsExceededCooldownSeconds { get; set; } + [Annotation("Relative path to icon for shader keywords exceeded. Other valid values include: \"\", \"default\", \"warning\", \"error\"")] + public string MaximumKeywordsExceededIconPath { get; set; } + [Annotation("Relative path to ogg-formatted audio for shader keywords exceeded. Other valid values include: \"\", \"default\", \"warning\", \"error\"")] + public string MaximumKeywordsExceededAudioPath { get; set; } + + [Annotation("Determines whether or not portal dropped notifications are delivered. Valid values: true, false", true, "PORTAL DROPPED")] + public bool DisplayPortalDropped { get; set; } + [Annotation("Period of time in seconds for portal dropped notification to remain on screen. Valid values: 0.0 -> float32 max")] + public float PortalDroppedTimeoutSeconds { get; set; } + [Annotation("Relative path to icon for portal dropped. Other valid values include: \"\", \"default\", \"warning\", \"error\"")] + public string PortalDroppedIconPath { get; set; } + [Annotation("Relative path to ogg-formatted audio for portal dropped. Other valid values include: \"\", \"default\", \"warning\", \"error\"")] + public string PortalDroppedAudioPath { get; set; } + + public ConfigurationModel() + { + ParseFrequencyMilliseconds = 300; + DirectoryPollFrequencyMilliseconds = 5000; + OutputLogRoot = @"%AppData%\..\LocalLow\VRChat\vrchat"; + LogNotificationEvents = true; + NotificationVolume = 0.2f; + Opacity = 0.75f; + + DisplayPlayerJoined = true; + PlayerJoinedNotificationTimeoutSeconds = 2.5f; + PlayerJoinedIconPath = @"\Resources\Icons\player_joined.png"; + PlayerJoinedAudioPath = @"\Resources\Audio\player_joined.ogg"; + + DisplayPlayerLeft = true; + PlayerLeftNotificationTimeoutSeconds = 2.5f; + PlayerLeftIconPath = @"\Resources\Icons\player_left.png"; + PlayerLeftAudioPath = @"\Resources\Audio\player_left.ogg"; + + DisplayWorldChanged = true; + WorldJoinSilenceSeconds = 20; + DisplayJoinLeaveSilencedOverride = false; + WorldChangedNotificationTimeoutSeconds = 3.0f; + WorldChangedIconPath = @"\Resources\Icons\world_changed.png"; + WorldChangedAudioPath = XSGlobals.GetBuiltInAudioSourceString(XSAudioDefault.Default); + + DisplayMaximumKeywordsExceeded = false; + MaximumKeywordsExceededTimeoutSeconds = 3.0f; + MaximumKeywordsExceededCooldownSeconds = 600.0f; + MaximumKeywordsExceededIconPath = @"\Resources\Icons\keywords_exceeded.png"; + MaximumKeywordsExceededAudioPath = XSGlobals.GetBuiltInAudioSourceString(XSAudioDefault.Warning); + + DisplayPortalDropped = true; + PortalDroppedTimeoutSeconds = 3.0f; + PortalDroppedIconPath = @"\Resources\Icons\portal_dropped.png"; + PortalDroppedAudioPath = XSGlobals.GetBuiltInAudioSourceString(XSAudioDefault.Default); + } + + public string GetLocalResourcePath(string path) => Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + path; + + public string AsJson(bool annotated = true) + { + string asJson = JsonSerializer.Serialize(this, new JsonSerializerOptions() { WriteIndented = true }); + + // JSON teeeeeeeeechnically doesn't support comments, but we can add them and there's a flag in our json reader for skipping them, so we're good. Theoretically. + if (annotated) + { + string[] lines = asJson.Split('\n'); + + StringBuilder sb = new StringBuilder(); + + PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + + Dictionary> propertyAnnotations = new Dictionary>(); + + foreach (PropertyInfo p in properties) + propertyAnnotations.Add(p.Name.ToLower(), new Tuple(p, + (Annotation)p.GetCustomAttributes(typeof(Annotation), true).GetValue(0))); + + StringBuilder commentBuilder = new StringBuilder(); + StringBuilder propertyBuilder = new StringBuilder(); + foreach (string line in lines) + { + if (line.Trim() == "}") + { + if (propertyBuilder.ToString() != string.Empty) + { + if (commentBuilder.Length > 0) + sb.Append('\n' + commentBuilder.ToString() + '\n'); + sb.Append(propertyBuilder.ToString() + "}"); + continue; + } + } + else if (!line.Contains(':')) + { + sb.Append(line + '\n'); + continue; + } + + string propertyNameLower = line.Substring(0, line.IndexOf(':')).Trim().Replace("\"", "").ToLower(); + string whitespace = line.Substring(0, line.IndexOf('\"')); + + if (propertyAnnotations.ContainsKey(propertyNameLower)) + { + Tuple pa = propertyAnnotations[propertyNameLower]; + + if (pa.Item2.startsGroup) + { + if (commentBuilder.Length > 0) + sb.Append('\n' + commentBuilder.ToString() + '\n'); + if (propertyBuilder.Length > 0) + sb.Append(propertyBuilder.ToString()); + + commentBuilder.Clear(); + propertyBuilder.Clear(); + + commentBuilder.Append($"{whitespace}// {pa.Item2.groupDescription}\n"); + } + + commentBuilder.Append($"{whitespace}// {pa.Item1.Name} : {pa.Item2.description}\n"); + propertyBuilder.Append(line + '\n'); + } + else + propertyBuilder.Append(line + '\n'); + } + + asJson = sb.ToString(); + } + + return asJson; + } + } +} diff --git a/XSOverlay VRChat Parser/Program.cs b/XSOverlay VRChat Parser/Program.cs new file mode 100644 index 0000000..7db39b3 --- /dev/null +++ b/XSOverlay VRChat Parser/Program.cs @@ -0,0 +1,417 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using XSNotifications; +using XSNotifications.Enum; +using XSNotifications.Helpers; +using XSOverlay_VRChat_Parser.Helpers; +using XSOverlay_VRChat_Parser.Models; + +namespace XSOverlay_VRChat_Parser +{ + class Program + { + static ConfigurationModel Configuration { get; set; } + + static HashSet IgnorableAudioPaths = new HashSet(); + static HashSet IgnorableIconPaths = new HashSet(); + + static string UserFolderPath { get; set; } + static string LogFileName { get; set; } + static string LastKnownLocationName { get; set; } // World name + static string LastKnownLocationID { get; set; } // World ID + + static readonly object logMutex = new object(); + + static Timer LogDetectionTimer { get; set; } + + static Dictionary Subscriptions { get; set; } + + static DateTime SilencedUntil = DateTime.Now, + LastMaximumKeywordsNotification = DateTime.Now; + + static XSNotifier Notifier { get; set; } + + static async Task Main(string[] args) + { + UserFolderPath = Environment.ExpandEnvironmentVariables(@"%AppData%\..\LocalLow\XSOverlay VRChat Parser"); + if (!Directory.Exists(UserFolderPath)) + Directory.CreateDirectory(UserFolderPath); + + if (!Directory.Exists($@"{UserFolderPath}\Logs")) + Directory.CreateDirectory($@"{UserFolderPath}\Logs"); + + DateTime now = DateTime.Now; + LogFileName = $"Session_{now.Year:0000}{now.Month:00}{now.Day:00}{now.Hour:00}{now.Minute:00}{now.Second:00}.log"; + Log(LogEventType.Info, $@"Log initialized at {UserFolderPath}\Logs\{LogFileName}"); + + try + { + if (!File.Exists($@"{UserFolderPath}\config.json")) + { + Configuration = new ConfigurationModel(); + File.WriteAllText($@"{UserFolderPath}\config.json", Configuration.AsJson()); + } + else + Configuration = JsonSerializer.Deserialize(File.ReadAllText($@"{UserFolderPath}\config.json"), new JsonSerializerOptions { ReadCommentHandling = JsonCommentHandling.Skip }); + + // Rewrite configuration to update it with any new fields not in existing configuration. Useful during update process and making sure the config always has updated annotations. + // Users shouldn't need to re-configure every time they update the software. + File.WriteAllText($@"{UserFolderPath}\config.json", Configuration.AsJson()); + } + catch (Exception ex) + { + Log(LogEventType.Error, "An exception occurred while attempting to read or write the configuration file."); + Log(ex); + return; + } + + IgnorableAudioPaths.Add(string.Empty); + IgnorableAudioPaths.Add(XSGlobals.GetBuiltInAudioSourceString(XSAudioDefault.Default)); + IgnorableAudioPaths.Add(XSGlobals.GetBuiltInAudioSourceString(XSAudioDefault.Warning)); + IgnorableAudioPaths.Add(XSGlobals.GetBuiltInAudioSourceString(XSAudioDefault.Error)); + + IgnorableIconPaths.Add(string.Empty); + IgnorableIconPaths.Add(XSGlobals.GetBuiltInIconTypeString(XSIconDefaults.Default)); + IgnorableIconPaths.Add(XSGlobals.GetBuiltInIconTypeString(XSIconDefaults.Warning)); + IgnorableIconPaths.Add(XSGlobals.GetBuiltInIconTypeString(XSIconDefaults.Error)); + + Subscriptions = new Dictionary(); + LogDetectionTimer = new Timer(new TimerCallback(LogDetectionTick), null, 0, Configuration.DirectoryPollFrequencyMilliseconds); + + Log(LogEventType.Info, $"Log detection timer initialized with poll frequency {Configuration.DirectoryPollFrequencyMilliseconds} and parse frequency {Configuration.ParseFrequencyMilliseconds}."); + + XSGlobals.DefaultSourceApp = "XSOverlay VRChat Parser"; + XSGlobals.DefaultOpacity = Configuration.Opacity; + XSGlobals.DefaultVolume = Configuration.NotificationVolume; + + try + { + Notifier = new XSNotifier(); + } + catch (Exception ex) + { + Log(LogEventType.Error, "An exception occurred while constructing XSNotifier."); + Log(ex); + Exit(); + } + + Log(LogEventType.Info, $"XSNotifier initialized."); + + try + { + Notifier.SendNotification(new XSNotification() + { + AudioPath = XSGlobals.GetBuiltInAudioSourceString(XSAudioDefault.Default), + Title = "Application Started", + Content = $"VRChat Log Parser has initialized.", + Height = 110.0f + }); + } + catch (Exception ex) + { + Log(LogEventType.Error, "An exception occurred while sending initialization notification."); + Log(ex); + Exit(); + } + + await Task.Delay(-1); // Shutdown should be managed by XSO, so just... hang around. Maybe implement periodic checks to see if XSO is running to avoid being orphaned. + } + + static void Exit() + { + Log(LogEventType.Info, "Disposing notifier and exiting application."); + + Notifier.Dispose(); + + foreach (var item in Subscriptions) + item.Value.Dispose(); + + Subscriptions.Clear(); + + Environment.Exit(-1); + } + + static void Log(LogEventType type, string message) + { + DateTime now = DateTime.Now; + string dateTimeStamp = $"[{now.Year:0000}/{now.Month:00}/{now.Day:00} {now.Hour:00}:{now.Minute:00}:{now.Second:00}]"; + + lock (logMutex) + { + switch (type) + { + case LogEventType.Error: + File.AppendAllText($@"{UserFolderPath}\Logs\{LogFileName}", $"{dateTimeStamp} [ERROR] {message}\r\n"); + break; + case LogEventType.Event: + if (Configuration.LogNotificationEvents) + File.AppendAllText($@"{UserFolderPath}\Logs\{LogFileName}", $"{dateTimeStamp} [EVENT] {message}\r\n"); + break; + case LogEventType.Info: + File.AppendAllText($@"{UserFolderPath}\Logs\{LogFileName}", $"{dateTimeStamp} [INFO] {message}\r\n"); + break; + } + } + } + + static void Log(Exception ex) + { + Log(LogEventType.Error, $"{ex.Message}\r\n{ex.InnerException}\r\n{ex.StackTrace}"); + } + + static void SendNotification(XSNotification notification) + { + try + { + Notifier.SendNotification(notification); + } + catch (Exception ex) + { + Log(LogEventType.Error, "An exception occurred while sending a routine event notification."); + Log(ex); + Exit(); + } + } + + static void LogDetectionTick(object timerState) + { + string[] allFiles = Directory.GetFiles(Environment.ExpandEnvironmentVariables(Configuration.OutputLogRoot)); + foreach (string fn in allFiles) + if (!Subscriptions.ContainsKey(fn) && fn.Contains("output_log")) + { + Subscriptions.Add(fn, new TailSubscription(fn, ParseTick, 0, Configuration.ParseFrequencyMilliseconds)); + Log(LogEventType.Info, $"A tail subscription was added to {fn}"); + } + } + + /// + /// This is messy, but they've changed format on me often enough that it's difficult to care! + /// + /// + static void ParseTick(string content) + { + List> ToSend = new List>(); + + if (!string.IsNullOrWhiteSpace(content)) + { + string[] lines = content.Split('\n'); + + foreach (string dirtyLine in lines) + { + string line = Regex.Replace(dirtyLine + .Replace("\r", "") + .Replace("\n", "") + .Replace("\t", "") + .Trim(), + @"\s+", " ", RegexOptions.Multiline); + + if (!string.IsNullOrWhiteSpace(line)) + { + int tocLoc = 0; + string[] tokens = line.Split(' '); + + // Get new LastKnownLocationName here + if (line.Contains("Joining or")) + { + for (int i = 0; i < tokens.Length; i++) + { + if (tokens[i] == "Room:") + { + tocLoc = i; + break; + } + } + + if (tokens.Length > tocLoc + 1) + { + string name = ""; + + for (int i = tocLoc + 1; i < tokens.Length; i++) + name += tokens[i] + " "; + + name = name.Trim(); + + LastKnownLocationName = name.Trim(); + } + } + // Get new LastKnownLocationID here + else if (line.Contains("Joining w")) + { + for (int i = 0; i < tokens.Length; i++) + { + if (tokens[i] == "Joining") + { + tocLoc = i; + break; + } + } + + if (tokens.Length > tocLoc + 1) + LastKnownLocationID = tokens[tocLoc + 1]; + } + // At this point, we have the location name/id and are transitioning. + else if (line.Contains("Successfully joined room")) + { + SilencedUntil = DateTime.Now.AddSeconds(Configuration.WorldJoinSilenceSeconds); + + ToSend.Add(new Tuple(EventType.WorldChange, new XSNotification() + { + Timeout = Configuration.WorldChangedNotificationTimeoutSeconds, + Icon = IgnorableIconPaths.Contains(Configuration.WorldChangedIconPath) ? Configuration.WorldChangedIconPath : Configuration.GetLocalResourcePath(Configuration.WorldChangedIconPath), + AudioPath = IgnorableAudioPaths.Contains(Configuration.WorldChangedAudioPath) ? Configuration.WorldChangedAudioPath : Configuration.GetLocalResourcePath(Configuration.WorldChangedAudioPath), + Title = LastKnownLocationName, + Content = $"{(Configuration.DisplayJoinLeaveSilencedOverride ? "" : $"Silencing notifications for {Configuration.WorldJoinSilenceSeconds} seconds.")}", + Height = 110 + })); + + Log(LogEventType.Event, $"[VRC] World changed to {LastKnownLocationName} -> {LastKnownLocationID}"); + } + // Get player joins here + else if (line.Contains("[Behaviour] OnPlayerJoined")) + { + for (int i = 0; i < tokens.Length; i++) + { + if (tokens[i] == "OnPlayerJoined") + { + tocLoc = i; + break; + } + } + + string message = ""; + string displayName = ""; + + if (tokens.Length > tocLoc + 1) + { + string name = ""; + + for (int i = tocLoc + 1; i < tokens.Length; i++) + name += tokens[i] + " "; + + displayName = name.Trim(); + + message += displayName; + } + else + { + message += "No username was provided."; + } + + ToSend.Add(new Tuple(EventType.PlayerJoin, new XSNotification() + { + Timeout = Configuration.PlayerJoinedNotificationTimeoutSeconds, + Icon = IgnorableIconPaths.Contains(Configuration.PlayerJoinedIconPath) ? Configuration.PlayerJoinedIconPath : Configuration.GetLocalResourcePath(Configuration.PlayerJoinedIconPath), + AudioPath = IgnorableAudioPaths.Contains(Configuration.PlayerJoinedAudioPath) ? Configuration.PlayerJoinedAudioPath : Configuration.GetLocalResourcePath(Configuration.PlayerJoinedAudioPath), + Title = message + })); + + Log(LogEventType.Event, $"[VRC] Join: {message}"); + } + // Get player leaves + else if (line.Contains("[Behaviour] OnPlayerLeft ")) + { + for (int i = 0; i < tokens.Length; i++) + { + if (tokens[i] == "OnPlayerLeft") + { + tocLoc = i; + break; + } + } + + string message = ""; + string displayName = ""; + + if (tokens.Length > tocLoc + 1) + { + string name = ""; + + for (int i = tocLoc + 1; i < tokens.Length; i++) + name += tokens[i] + " "; + + displayName = name.Trim(); + + message += displayName; + } + else + { + message += "No username was provided."; + } + + ToSend.Add(new Tuple(EventType.PlayerLeft, new XSNotification() + { + Timeout = Configuration.PlayerLeftNotificationTimeoutSeconds, + Icon = IgnorableIconPaths.Contains(Configuration.PlayerLeftIconPath) ? Configuration.PlayerLeftIconPath : Configuration.GetLocalResourcePath(Configuration.PlayerLeftIconPath), + AudioPath = IgnorableAudioPaths.Contains(Configuration.PlayerLeftAudioPath) ? Configuration.PlayerLeftAudioPath : Configuration.GetLocalResourcePath(Configuration.PlayerLeftAudioPath), + Title = message + })); + + Log(LogEventType.Event, $"[VRC] Leave: {message}"); + } + // Shader keyword limit exceeded + else if (line.Contains("Maximum number (256)")) + { + ToSend.Add(new Tuple(EventType.KeywordsExceeded, new XSNotification() + { + Timeout = Configuration.MaximumKeywordsExceededTimeoutSeconds, + Icon = IgnorableIconPaths.Contains(Configuration.MaximumKeywordsExceededIconPath) ? Configuration.MaximumKeywordsExceededIconPath : Configuration.GetLocalResourcePath(Configuration.MaximumKeywordsExceededIconPath), + AudioPath = IgnorableAudioPaths.Contains(Configuration.MaximumKeywordsExceededAudioPath) ? Configuration.MaximumKeywordsExceededAudioPath : Configuration.GetLocalResourcePath(Configuration.MaximumKeywordsExceededAudioPath), + Title = "Maximum shader keywords exceeded!" + })); + + Log(LogEventType.Event, $"[VRC] Maximum shader keywords exceeded!"); + } + // Portal dropped + else if (line.Contains("[Behaviour]") && line.Contains("Portals/PortalInternalDynamic")) + { + ToSend.Add(new Tuple(EventType.PortalDropped, new XSNotification() + { + Timeout = Configuration.PortalDroppedTimeoutSeconds, + Icon = IgnorableIconPaths.Contains(Configuration.PortalDroppedIconPath) ? Configuration.PortalDroppedIconPath : Configuration.GetLocalResourcePath(Configuration.PortalDroppedIconPath), + AudioPath = IgnorableAudioPaths.Contains(Configuration.PortalDroppedAudioPath) ? Configuration.PortalDroppedAudioPath : Configuration.GetLocalResourcePath(Configuration.PortalDroppedAudioPath), + Title = "A portal has been spawned." + })); + + Log(LogEventType.Event, $"[VRC] Portal dropped."); + } + } + } + } + + if (ToSend.Count > 0) + foreach (Tuple notification in ToSend) + { + if ( + (!CurrentlySilenced() && Configuration.DisplayPlayerJoined && notification.Item1 == EventType.PlayerJoin) + || (!CurrentlySilenced() && Configuration.DisplayPlayerLeft && notification.Item1 == EventType.PlayerLeft) + || (Configuration.DisplayWorldChanged && notification.Item1 == EventType.WorldChange) + || (Configuration.DisplayPortalDropped && notification.Item1 == EventType.PortalDropped) + ) + SendNotification(notification.Item2); + else if (Configuration.DisplayMaximumKeywordsExceeded && notification.Item1 == EventType.KeywordsExceeded + && DateTime.Now > LastMaximumKeywordsNotification.AddSeconds(Configuration.MaximumKeywordsExceededCooldownSeconds)) + { + LastMaximumKeywordsNotification = DateTime.Now; + SendNotification(notification.Item2); + } + } + } + + static bool CurrentlySilenced() + { + if (Configuration.DisplayJoinLeaveSilencedOverride) + return false; + + if (DateTime.Now > SilencedUntil) + return false; + + return true; + } + + } +} diff --git a/XSOverlay VRChat Parser/Properties/PublishProfiles/FolderProfile.pubxml b/XSOverlay VRChat Parser/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..1c8443c --- /dev/null +++ b/XSOverlay VRChat Parser/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,18 @@ + + + + + Release + Any CPU + C:\VS2019 Projects\XSOverlay VRChat Parser Deploy + FileSystem + net5.0 + win-x64 + true + False + False + False + + \ No newline at end of file diff --git a/XSOverlay VRChat Parser/Resources/Audio/player_joined.ogg b/XSOverlay VRChat Parser/Resources/Audio/player_joined.ogg new file mode 100644 index 0000000000000000000000000000000000000000..d7d19c543fb8e6cc07ff8ef8b724b4e139904524 GIT binary patch literal 6125 zcmai12|SeB`+tYA3`2vVQB#d&VuWif)kuX1gJhVorLqiJ%M4oGlp%X!k}dm?Wh9|! zlXWb~I+9AI5Z&rt$!-6i8QpvP-~a#f`@Ns@p3iyDbI$Xe=Xt)*^FGJH-`@!mfqpz% zvdaUKaZLhjEiCdN!#jW}n1WeU|5$PuCLCOcIS6+C{RnmnD7nTG+fi2E|JTzky3A+; zpt}YfI-u*o@W%xQc+-UJaW*)8J)*vzzMcV2(;*}@;1Gk(q=$wR1neZiyW?O;2>9L# z6I*6OFyBFg2`NN7Tu1ak8Wn<25VT*NAe-T5L*THBG>>Mntp%eFy@V`wLI=htnb`SP zhcon5gdjK+t%*o2nYIb+C;4MEQW=G$fL)u*aT1A+Pf3W@#q~a+W%Xr2q5TBBq>P&w z1Z-q#trCl-2s-b0l{gIO&R`ilmGV4>l-8xf(^C38T#TLmtI~{J#;-1wq#Dm@)(@CV z>(mdC?TDi^pPF0Zo+p_7q?x=Q9YUxF4xGhYT-g_8E-7FiPbkY`09MOt;h<<>0Lbbb z+U5$n_X_sjpt{2!+KviFg9esP&h}LBM0mPH(#Ikr#v-HKvSK~1N4Z^(^>`5LxfJV# ziTlz1jG9~$G@J2^lo)01>$D$_3QueKk`!}%ujvNRC4NDl4Z0}@ZS6<96bHuWP|B9S2 zbk{j-_c?XjI(7RY?R$S1IxH9tDp)!i+B+$F4k|_rE76@cdJY@WhpF_@Ncwmq-Q!x6 zTjP&_Sq_`Yv521{M-U>aq|_dBxd~>RrO}e^U`ud}p{s}o1+4-+3ilzP) zIR_HUvl7eW6KCR6H4`!e5^^e=ii3M<=9~X(`#W;180w&b$gyOo{}eebMC=X_O?BG# z3(Gog9Rvmqw_N$N10bj+4QVUPBaRfEQHs$hMThFV^*=`pppF{a4;uo*o`)c12vP%1 zW}y-pY389@T_uUr>S9GC@uGGNs~ly3B+00Tr2~D30$xVk^rxC(LkCa%oGhl-bjiRA(l<)CrqbNdWbKv6f&ti jzxrWt4 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 literal 0 HcmV?d00001 diff --git a/XSOverlay VRChat Parser/Resources/Audio/player_left.ogg b/XSOverlay VRChat Parser/Resources/Audio/player_left.ogg new file mode 100644 index 0000000000000000000000000000000000000000..2988768a322fdebc04ba7a7bde02adc33f0be266 GIT binary patch literal 6132 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 literal 0 HcmV?d00001 diff --git a/XSOverlay VRChat Parser/Resources/Icons/player_joined.png b/XSOverlay VRChat Parser/Resources/Icons/player_joined.png new file mode 100644 index 0000000000000000000000000000000000000000..6d13b30203eed71bfac6df02e4ba19dfe6b393c4 GIT binary patch literal 4886 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 literal 0 HcmV?d00001 diff --git a/XSOverlay VRChat Parser/Resources/Icons/world_changed.png b/XSOverlay VRChat Parser/Resources/Icons/world_changed.png new file mode 100644 index 0000000000000000000000000000000000000000..b3d2da828f7918a917d3f945b10bd8e30c6e467e GIT binary patch literal 5422 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 literal 0 HcmV?d00001 diff --git a/XSOverlay VRChat Parser/XSOverlay VRChat Parser.csproj b/XSOverlay VRChat Parser/XSOverlay VRChat Parser.csproj new file mode 100644 index 0000000..4f0e30c --- /dev/null +++ b/XSOverlay VRChat Parser/XSOverlay VRChat Parser.csproj @@ -0,0 +1,37 @@ + + + + WinExe + net5.0 + XSOverlay_VRChat_Parser + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + +