Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
nnaaa-vr committed Apr 1, 2021
0 parents commit 8431777
Show file tree
Hide file tree
Showing 19 changed files with 856 additions and 0 deletions.
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 25 additions & 0 deletions XSOverlay VRChat Parser.sln
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions XSOverlay VRChat Parser/Helpers/Annotation.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
11 changes: 11 additions & 0 deletions XSOverlay VRChat Parser/Helpers/EventType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace XSOverlay_VRChat_Parser.Helpers
{
public enum EventType
{
PlayerJoin,
PlayerLeft,
WorldChange,
KeywordsExceeded,
PortalDropped
}
}
9 changes: 9 additions & 0 deletions XSOverlay VRChat Parser/Helpers/LogEventType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace XSOverlay_VRChat_Parser.Helpers
{
public enum LogEventType
{
Error,
Event,
Info
}
}
62 changes: 62 additions & 0 deletions XSOverlay VRChat Parser/Helpers/TailSubscription.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
191 changes: 191 additions & 0 deletions XSOverlay VRChat Parser/Models/ConfigurationModel.cs
Original file line number Diff line number Diff line change
@@ -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<ConfigurationModel>(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<string, Tuple<PropertyInfo, Annotation>> propertyAnnotations = new Dictionary<string, Tuple<PropertyInfo, Annotation>>();

foreach (PropertyInfo p in properties)
propertyAnnotations.Add(p.Name.ToLower(), new Tuple<PropertyInfo, Annotation>(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<PropertyInfo, Annotation> 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;
}
}
}
Loading

0 comments on commit 8431777

Please sign in to comment.