diff --git a/AppApi.cs b/AppApi.cs
index 2a6d16e9..5b3961b4 100644
--- a/AppApi.cs
+++ b/AppApi.cs
@@ -13,6 +13,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
+using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
@@ -37,6 +38,17 @@ namespace VRCX
static AppApi()
{
Instance = new AppApi();
+
+ ProcessMonitor.Instance.ProcessStarted += Instance.OnProcessStateChanged;
+ ProcessMonitor.Instance.ProcessExited += Instance.OnProcessStateChanged;
+ }
+
+ private void OnProcessStateChanged(MonitoredProcess monitoredProcess)
+ {
+ if (!monitoredProcess.HasName("VRChat") && !monitoredProcess.HasName("vrserver"))
+ return;
+
+ CheckGameRunning();
}
public string MD5File(string Blob)
@@ -117,26 +129,23 @@ namespace VRCX
Cef.GetGlobalCookieManager().DeleteCookies();
}
- public bool[] CheckGameRunning()
+ public void CheckGameRunning()
{
var isGameRunning = false;
var isSteamVRRunning = false;
- if (Process.GetProcessesByName("vrchat").Length > 0)
+ if (ProcessMonitor.Instance.IsProcessRunning("VRChat", true))
{
isGameRunning = true;
}
- if (Process.GetProcessesByName("vrserver").Length > 0)
+ if (ProcessMonitor.Instance.IsProcessRunning("vrserver", true))
{
isSteamVRRunning = true;
}
- return new[]
- {
- isGameRunning,
- isSteamVRRunning
- };
+ if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading)
+ MainForm.Instance.Browser.ExecuteScriptAsync("$app.updateIsGameRunning", isGameRunning, isSteamVRRunning);
}
public int QuitGame()
@@ -721,6 +730,9 @@ namespace VRCX
public void GetScreenshotMetadata(string path)
{
+ if (string.IsNullOrEmpty(path))
+ return;
+
var fileName = Path.GetFileNameWithoutExtension(path);
var metadata = new JObject();
if (File.Exists(path) && path.EndsWith(".png"))
@@ -748,7 +760,7 @@ namespace VRCX
}
catch (Exception ex)
{
- metadata.Add("error", $"This file contains invalid LFS/SSM metadata unable to be parsed by VRCX. \n({ex.Message})\n Text: {metadataString}");
+ metadata.Add("error", $"This file contains invalid LFS/SSM metadata unable to be parsed by VRCX. \n({ex.Message})\nText: {metadataString}");
}
}
else
@@ -759,7 +771,7 @@ namespace VRCX
}
catch (JsonReaderException ex)
{
- metadata.Add("error", $"This file contains invalid metadata unable to be parsed by VRCX. \n({ex.Message})\n Text: {metadataString}");
+ metadata.Add("error", $"This file contains invalid metadata unable to be parsed by VRCX. \n({ex.Message})\nText: {metadataString}");
}
}
}
@@ -828,13 +840,56 @@ namespace VRCX
}
}
- public void OpenImageFolder(string path)
+ public void OpenShortcutFolder()
{
- if (!File.Exists(path))
+ var path = AutoAppLaunchManager.Instance.AppShortcutDirectory;
+ if (!Directory.Exists(path))
return;
- // open folder with file highlighted
- Process.Start("explorer.exe", $"/select,\"{path}\"");
+ OpenFolderAndSelectItem(path, true);
+ }
+
+ public void OpenFolderAndSelectItem(string path, bool isFolder = false)
+ {
+ // I don't think it's quite meant for it, but SHOpenFolderAndSelectItems can open folders by passing the folder path as the item to select, as a child to itself, somehow. So we'll check to see if 'path' is a folder as well.
+ if (!File.Exists(path) && !Directory.Exists(path))
+ return;
+
+ var folderPath = isFolder ? path : Path.GetDirectoryName(path);
+ IntPtr pidlFolder;
+ IntPtr pidlFile;
+ uint psfgaoOut;
+
+ // Convert our managed strings to PIDLs. PIDLs are essentially pointers to the actual file system objects, separate from the "display name", which is the human-readable path to the file/folder. We're parsing the display name into a PIDL here.
+ // The windows shell uses PIDLs to identify objects in winapi calls, so we'll need to use them to open the folder and select the file. Cool stuff!
+ var result = WinApi.SHParseDisplayName(folderPath, IntPtr.Zero, out pidlFolder, 0, out psfgaoOut);
+ if (result != 0)
+ {
+ return;
+ }
+
+ result = WinApi.SHParseDisplayName(path, IntPtr.Zero, out pidlFile, 0, out psfgaoOut);
+ if (result != 0)
+ {
+ // Free the PIDL we allocated earlier if we failed to parse the display name of the file.
+ Marshal.FreeCoTaskMem(pidlFolder);
+ return;
+ }
+
+ IntPtr[] files = { pidlFile };
+
+ try
+ {
+ // Open the containing folder and select our file. SHOpenFolderAndSelectItems will respect existing explorer instances, open a new one if none exist, will properly handle paths > 120 chars, and work with third-party filesystem viewers that hook into winapi calls.
+ // It can select multiple items, but we only need to select one.
+ WinApi.SHOpenFolderAndSelectItems(pidlFolder, (uint)files.Length, files, 0);
+ }
+ finally
+ {
+ // Free the PIDLs we allocated earlier
+ Marshal.FreeCoTaskMem(pidlFolder);
+ Marshal.FreeCoTaskMem(pidlFile);
+ }
}
public void FlashWindow()
diff --git a/AutoAppLaunchManager.cs b/AutoAppLaunchManager.cs
new file mode 100644
index 00000000..303b5662
--- /dev/null
+++ b/AutoAppLaunchManager.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+
+namespace VRCX
+{
+ ///
+ /// The class responsible for launching user-defined applications when VRChat opens/closes.
+ ///
+ public class AutoAppLaunchManager
+ {
+ public static AutoAppLaunchManager Instance { get; private set; }
+ public bool Enabled = true;
+ public readonly string AppShortcutDirectory;
+
+ private DateTime startTime = DateTime.Now;
+ private List startedProcesses = new List();
+ private static readonly byte[] shortcutSignatureBytes = { 0x4C, 0x00, 0x00, 0x00 }; // signature for ShellLinkHeader\
+
+ static AutoAppLaunchManager()
+ {
+ Instance = new AutoAppLaunchManager();
+ }
+
+ public AutoAppLaunchManager()
+ {
+ AppShortcutDirectory = Path.Combine(Program.AppDataDirectory, "startup");
+
+ if (!Directory.Exists(AppShortcutDirectory))
+ {
+ Directory.CreateDirectory(AppShortcutDirectory);
+ }
+
+ ProcessMonitor.Instance.ProcessStarted += OnProcessStarted;
+ ProcessMonitor.Instance.ProcessExited += OnProcessExited;
+ }
+
+ private void OnProcessExited(MonitoredProcess monitoredProcess)
+ {
+ if (startedProcesses.Count == 0 || !monitoredProcess.HasName("VRChat"))
+ return;
+
+ foreach (var process in startedProcesses)
+ {
+ if (!process.HasExited)
+ process.Kill();
+ }
+
+ startedProcesses.Clear();
+ }
+
+ private void OnProcessStarted(MonitoredProcess monitoredProcess)
+ {
+ if (!monitoredProcess.HasName("VRChat") || monitoredProcess.Process.StartTime < startTime)
+ return;
+
+ if (startedProcesses.Count > 0)
+ {
+ foreach (var process in startedProcesses)
+ {
+ if (!process.HasExited)
+ process.Kill();
+ }
+
+ startedProcesses.Clear();
+ }
+
+ var shortcutFiles = FindShortcutFiles(AppShortcutDirectory);
+
+ if (shortcutFiles.Length > 0)
+ {
+ foreach (var file in shortcutFiles)
+ {
+ var process = Process.Start(file);
+ startedProcesses.Add(process);
+ }
+ }
+ }
+
+ internal void Init()
+ {
+ // What are you lookin at?
+ }
+
+ internal void Exit()
+ {
+ Enabled = false;
+
+ foreach (var process in startedProcesses)
+ {
+ if (!process.HasExited)
+ process.Kill();
+ }
+ }
+
+ ///
+ /// Finds windows shortcut files in a given folder.
+ ///
+ /// The folder path.
+ /// An array of shortcut paths. If none, then empty.
+ private static string[] FindShortcutFiles(string folderPath)
+ {
+ DirectoryInfo directoryInfo = new DirectoryInfo(folderPath);
+ FileInfo[] files = directoryInfo.GetFiles();
+ List ret = new List();
+
+ foreach (FileInfo file in files)
+ {
+ if (IsShortcutFile(file.FullName))
+ {
+ ret.Add(file.FullName);
+ }
+ }
+
+ return ret.ToArray();
+ }
+
+ ///
+ /// Determines whether the specified file path is a shortcut by checking the file header.
+ ///
+ /// The file path.
+ /// true if the given file path is a shortcut, otherwise false
+ private static bool IsShortcutFile(string filePath)
+ {
+ byte[] headerBytes = new byte[4];
+ using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ {
+ if (fileStream.Length >= 4)
+ {
+ fileStream.Read(headerBytes, 0, 4);
+ }
+ }
+
+ return headerBytes.SequenceEqual(shortcutSignatureBytes);
+ }
+ }
+}
diff --git a/ProcessMonitor.cs b/ProcessMonitor.cs
new file mode 100644
index 00000000..a2dc847e
--- /dev/null
+++ b/ProcessMonitor.cs
@@ -0,0 +1,206 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Timers;
+
+namespace VRCX
+{
+ // I don't think this applies to our use case, but I'm leaving it here for reference.
+ // https://stackoverflow.com/questions/2519673/process-hasexited-returns-true-even-though-process-is-running
+ // "When a process is started, it is assigned a PID. If the User is then prompted with the User Account Control dialog and selects 'Yes', the process is re-started and assigned a new PID."
+ // There's no docs for this, but Process.HasExited also seems to be checked every time the property is accessed, so it's not cached. Which means Process.Refresh() is not needed for our use case.
+
+ ///
+ /// A class that monitors given processes and raises events when they are started or exited.
+ /// Intended to be used to monitor VRChat and VRChat-related processes.
+ ///
+ internal class ProcessMonitor
+ {
+ private readonly Dictionary monitoredProcesses;
+ private readonly Timer monitorProcessTimer;
+
+ static ProcessMonitor()
+ {
+ Instance = new ProcessMonitor();
+ }
+
+ public ProcessMonitor()
+ {
+ monitoredProcesses = new Dictionary();
+
+ monitorProcessTimer = new Timer();
+ monitorProcessTimer.Interval = 1000;
+ monitorProcessTimer.Elapsed += MonitorProcessTimer_Elapsed;
+ }
+
+ public static ProcessMonitor Instance { get; private set; }
+
+ ///
+ /// Raised when a monitored process is started.
+ ///
+ public event Action ProcessStarted;
+
+ ///
+ /// Raised when a monitored process is exited.
+ ///
+ public event Action ProcessExited;
+
+ public void Init()
+ {
+ AddProcess("vrchat");
+ AddProcess("vrserver");
+ monitorProcessTimer.Start();
+ }
+
+ public void Exit()
+ {
+ monitorProcessTimer.Stop();
+ monitoredProcesses.Values.ToList().ForEach(x => x.ProcessExited());
+ }
+
+ private void MonitorProcessTimer_Elapsed(object sender, ElapsedEventArgs e)
+ {
+ var processesNeedingUpdate = new List();
+
+ // Check if any of the monitored processes have been opened or closed.
+ foreach (var keyValuePair in monitoredProcesses)
+ {
+ var monitoredProcess = keyValuePair.Value;
+ var process = monitoredProcess.Process;
+ var name = monitoredProcess.ProcessName;
+
+ if (monitoredProcess.IsRunning)
+ {
+ if (monitoredProcess.Process == null || monitoredProcess.Process.HasExited)
+ {
+ monitoredProcess.ProcessExited();
+ ProcessExited?.Invoke(monitoredProcess);
+ }
+
+ monitoredProcess.Process.Refresh();
+ }
+ else
+ {
+ processesNeedingUpdate.Add(monitoredProcess);
+ }
+ }
+
+ // We do it this way so we're not constantly polling for processes if we don't actually need to (aka, all processes are already accounted for).
+ if (processesNeedingUpdate.Count == 0)
+ return;
+
+ var processes = Process.GetProcesses();
+ foreach (var monitoredProcess in processesNeedingUpdate)
+ {
+ var process = processes.FirstOrDefault(p => string.Equals(p.ProcessName, monitoredProcess.ProcessName, StringComparison.OrdinalIgnoreCase));
+ if (process != null)
+ {
+ monitoredProcess.ProcessStarted(process);
+ ProcessStarted?.Invoke(monitoredProcess);
+ }
+ }
+ }
+
+ ///
+ /// Checks if a process if currently being monitored and if it is running.
+ ///
+ /// The name of the process to check for.
+ /// If true, will manually check if the given process is running should the the monitored process not be initialized yet.
+ /// Whether the given process is monitored and currently running.
+ public bool IsProcessRunning(string processName, bool ensureCheck = false)
+ {
+ processName = processName.ToLower();
+ if (monitoredProcesses.ContainsKey(processName))
+ {
+ var process = monitoredProcesses[processName];
+
+ if (ensureCheck && process.Process == null)
+ return Process.GetProcessesByName(processName).FirstOrDefault() != null;
+
+ return process.IsRunning;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Adds a process to be monitored.
+ ///
+ ///
+ public void AddProcess(Process process)
+ {
+ if (monitoredProcesses.ContainsKey(process.ProcessName.ToLower()))
+ return;
+
+ monitoredProcesses.Add(process.ProcessName.ToLower(), new MonitoredProcess(process));
+ }
+
+ ///
+ /// Adds a process to be monitored.
+ ///
+ ///
+ public void AddProcess(string processName)
+ {
+ if (monitoredProcesses.ContainsKey(processName.ToLower()))
+ {
+ return;
+ }
+
+ monitoredProcesses.Add(processName, new MonitoredProcess(processName));
+ }
+
+ ///
+ /// Removes a process from being monitored.
+ ///
+ ///
+ public void RemoveProcess(string processName)
+ {
+ if (monitoredProcesses.ContainsKey(processName.ToLower()))
+ {
+ monitoredProcesses.Remove(processName);
+ }
+ }
+ }
+
+ internal class MonitoredProcess
+ {
+ public MonitoredProcess(Process process)
+ {
+ Process = process;
+ ProcessName = process.ProcessName.ToLower();
+
+ if (!process.HasExited)
+ IsRunning = true;
+ }
+
+ public MonitoredProcess(string processName)
+ {
+ ProcessName = processName;
+ IsRunning = false;
+ }
+
+ public Process Process { get; private set; }
+ public string ProcessName { get; private set; }
+ public bool IsRunning { get; private set; }
+
+ public bool HasName(string processName)
+ {
+ return ProcessName.Equals(processName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public void ProcessExited()
+ {
+ IsRunning = false;
+ Process?.Dispose();
+ Process = null;
+ }
+
+ public void ProcessStarted(Process process)
+ {
+ Process = process;
+ ProcessName = process.ProcessName.ToLower();
+ IsRunning = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
index f7532b28..ba6f2a33 100644
--- a/Program.cs
+++ b/Program.cs
@@ -78,12 +78,14 @@ namespace VRCX
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
+ ProcessMonitor.Instance.Init();
SQLite.Instance.Init();
VRCXStorage.Load();
CpuMonitor.Instance.Init();
Discord.Instance.Init();
WebApi.Instance.Init();
LogWatcher.Instance.Init();
+ AutoAppLaunchManager.Instance.Init();
CefService.Instance.Init();
IPCServer.Instance.Init();
@@ -93,6 +95,7 @@ namespace VRCX
VRCXVR.Instance.Exit();
CefService.Instance.Exit();
+ AutoAppLaunchManager.Instance.Exit();
LogWatcher.Instance.Exit();
WebApi.Instance.Exit();
@@ -100,6 +103,7 @@ namespace VRCX
CpuMonitor.Instance.Exit();
VRCXStorage.Save();
SQLite.Instance.Exit();
+ ProcessMonitor.Instance.Exit();
}
}
}
\ No newline at end of file
diff --git a/ScreenshotHelper.cs b/ScreenshotHelper.cs
index 379a17aa..b2b26d8e 100644
--- a/ScreenshotHelper.cs
+++ b/ScreenshotHelper.cs
@@ -187,7 +187,11 @@ namespace VRCX
var metadata = new JObject();
// lfs|2|author:usr_032383a7-748c-4fb2-94e4-bcb928e5de6b,Natsumi-sama|world:wrld_b016712b-5ce6-4bcb-9144-c8ed089b520f,35372,pet park test|pos:-60.49379,-0.002925932,5.805772|players:usr_9d73bff9-4543-4b6f-a004-9e257869ff50,-0.85,-0.17,-0.58,Olivia.;usr_3097f91e-a816-4c7a-a625-38fbfdee9f96,12.30,13.72,0.08,Zettai Ryouiki;usr_032383a7-748c-4fb2-94e4-bcb928e5de6b,0.68,0.32,-0.28,Natsumi-sama;usr_7525f45f-517e-442b-9abc-fbcfedb29f84,0.51,0.64,0.70,Weyoun
// lfs|2|author:usr_8c0a2f22-26d4-4dc9-8396-2ab40e3d07fc,knah|world:wrld_fb4edc80-6c48-43f2-9bd1-2fa9f1345621,35341,Luminescent Ledge|pos:8.231676,0.257298,-0.1983307|rq:2|players:usr_65b9eeeb-7c91-4ad2-8ce4-addb1c161cd6,0.74,0.59,1.57,Jakkuba;usr_6a50647f-d971-4281-90c3-3fe8caf2ba80,8.07,9.76,0.16,SopwithPup;usr_8c0a2f22-26d4-4dc9-8396-2ab40e3d07fc,0.26,1.03,-0.28,knah;usr_7f593ad1-3e9e-4449-a623-5c1c0a8d8a78,0.15,0.60,1.46,NekOwneD
+ // lfs|cvr|1|author:047b30bd-089d-887c-8734-b0032df5d176,Hordini|world:2e73b387-c6d4-45e9-b998-0fd6aa122c1d,i+efec20004ef1cd8b-404003-93833f-1aee112a,Bono's Basement (Anime) (#816724)|pos:2.196716,0.01250899,-3.817466|players:5301af21-eb8d-7b36-3ef4-b623fa51c2c6,3.778407,0.01250887,-3.815876,DDAkebono;f9e5c36c-41b0-7031-1185-35b4034010c0,4.828233,0.01250893,-3.920135,Natsumi
var lfs = metadataString.Split('|');
+ if (lfs[1] == "cvr")
+ lfs = lfs.Skip(1).ToArray();
+
var version = int.Parse(lfs[1]);
var application = lfs[0];
metadata.Add("application", application);
@@ -219,6 +223,16 @@ namespace VRCX
{
case "author":
var author = split[1].Split(',');
+ if (application == "cvr")
+ {
+ metadata.Add("author", new JObject
+ {
+ { "id", "" },
+ { "displayName", $"{author[1]} ({author[0]})" }
+ });
+ break;
+ }
+
metadata.Add("author", new JObject
{
{ "id", author[0] },
@@ -226,7 +240,17 @@ namespace VRCX
});
break;
case "world":
- if (version == 1)
+ if (application == "cvr")
+ {
+ var world = split[1].Split(',');
+ metadata.Add("world", new JObject
+ {
+ { "id", "" },
+ { "name", $"{world[2]} ({world[0]})" },
+ { "instanceId", "" }
+ });
+ }
+ else if (version == 1)
{
metadata.Add("world", new JObject
{
@@ -265,14 +289,28 @@ namespace VRCX
foreach (var player in players)
{
var playerSplit = player.Split(',');
- playersArray.Add(new JObject
+ if (application == "cvr")
{
- { "id", playerSplit[0] },
- { "x", playerSplit[1] },
- { "y", playerSplit[2] },
- { "z", playerSplit[3] },
- { "displayName", playerSplit[4] }
- });
+ playersArray.Add(new JObject
+ {
+ { "id", "" },
+ { "x", playerSplit[1] },
+ { "y", playerSplit[2] },
+ { "z", playerSplit[3] },
+ { "displayName", $"{playerSplit[4]} ({playerSplit[0]})" }
+ });
+ }
+ else
+ {
+ playersArray.Add(new JObject
+ {
+ { "id", playerSplit[0] },
+ { "x", playerSplit[1] },
+ { "y", playerSplit[2] },
+ { "z", playerSplit[3] },
+ { "displayName", playerSplit[4] }
+ });
+ }
}
metadata.Add("players", playersArray);
diff --git a/VRCX.csproj b/VRCX.csproj
index 982cd898..99bd1e81 100644
--- a/VRCX.csproj
+++ b/VRCX.csproj
@@ -84,12 +84,14 @@
+
+
diff --git a/WinApi.cs b/WinApi.cs
index e1550dd8..641948a1 100644
--- a/WinApi.cs
+++ b/WinApi.cs
@@ -19,5 +19,9 @@ namespace VRCX
[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
+ public static extern int SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string pszName, IntPtr pbc, out IntPtr ppidl, uint sfgaoIn, out uint psfgaoOut);
+ [DllImport("shell32.dll", CharSet = CharSet.Auto)]
+ public static extern IntPtr SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, IntPtr[] apidl, uint dwFlags);
}
}
diff --git a/html/src/app.js b/html/src/app.js
index 81ca96f3..fa679f39 100644
--- a/html/src/app.js
+++ b/html/src/app.js
@@ -4885,6 +4885,7 @@ speechSynthesis.getVoices();
this.checkForVRCXUpdate();
}
});
+ AppApi.CheckGameRunning();
API.$on('SHOW_WORLD_DIALOG', (tag) => this.showWorldDialog(tag));
API.$on('SHOW_WORLD_DIALOG_SHORTNAME', (tag) =>
this.verifyShortName('', tag)
@@ -5045,45 +5046,12 @@ speechSynthesis.getVoices();
this.nextClearVRCXCacheCheck = this.clearVRCXCacheFrequency;
this.clearVRCXCache();
}
- AppApi.CheckGameRunning().then(
- ([isGameRunning, isSteamVRRunning]) => {
- this.updateOpenVR(isGameRunning, isSteamVRRunning);
- if (isGameRunning !== this.isGameRunning) {
- this.isGameRunning = isGameRunning;
- if (isGameRunning) {
- API.currentUser.$online_for = Date.now();
- API.currentUser.$offline_for = '';
- } else {
- configRepository.setBool(
- 'isGameNoVR',
- this.isGameNoVR
- );
- API.currentUser.$online_for = '';
- API.currentUser.$offline_for = Date.now();
- this.autoVRChatCacheManagement();
- this.checkIfGameCrashed();
- this.ipcTimeout = 0;
- }
- this.lastLocationReset();
- this.clearNowPlaying();
- this.updateVRLastLocation();
- workerTimers.setTimeout(
- () => this.checkVRChatDebugLogging(),
- 60000
- );
- this.nextDiscordUpdate = 0;
- }
- if (isSteamVRRunning !== this.isSteamVRRunning) {
- this.isSteamVRRunning = isSteamVRRunning;
- }
- if (--this.nextDiscordUpdate <= 0) {
- this.nextDiscordUpdate = 7;
- if (this.discordActive) {
- this.updateDiscord();
- }
- }
+ if (--this.nextDiscordUpdate <= 0) {
+ this.nextDiscordUpdate = 7;
+ if (this.discordActive) {
+ this.updateDiscord();
}
- );
+ }
}
} catch (err) {
API.isRefreshFriendsLoading = false;
@@ -5092,6 +5060,43 @@ speechSynthesis.getVoices();
workerTimers.setTimeout(() => this.updateLoop(), 500);
};
+ $app.methods.updateIsGameRunning = function (
+ isGameRunning,
+ isSteamVRRunning
+ ) {
+ console.log(
+ `updateIsGameRunning isGameRunning:${isGameRunning} isSteamVRRunning:${isSteamVRRunning}`
+ );
+ if (isGameRunning !== this.isGameRunning) {
+ this.isGameRunning = isGameRunning;
+ if (isGameRunning) {
+ API.currentUser.$online_for = Date.now();
+ API.currentUser.$offline_for = '';
+ } else {
+ configRepository.setBool('isGameNoVR', this.isGameNoVR);
+ API.currentUser.$online_for = '';
+ API.currentUser.$offline_for = Date.now();
+ this.autoVRChatCacheManagement();
+ this.checkIfGameCrashed();
+ this.ipcTimeout = 0;
+ }
+ this.lastLocationReset();
+ this.clearNowPlaying();
+ this.updateVRLastLocation();
+ workerTimers.setTimeout(
+ () => this.checkVRChatDebugLogging(),
+ 60000
+ );
+ this.nextDiscordUpdate = 0;
+ console.log('isGameRunning changed', isGameRunning);
+ }
+ if (isSteamVRRunning !== this.isSteamVRRunning) {
+ this.isSteamVRRunning = isSteamVRRunning;
+ console.log('isSteamVRRunning changed', isSteamVRRunning);
+ }
+ this.updateOpenVR();
+ };
+
$app.data.debug = false;
$app.data.debugWebRequests = false;
$app.data.debugWebSocket = false;
@@ -9631,10 +9636,12 @@ speechSynthesis.getVoices();
case 'openvr-init':
this.isGameNoVR = false;
configRepository.setBool('isGameNoVR', this.isGameNoVR);
+ this.updateOpenVR();
break;
case 'desktop-mode':
this.isGameNoVR = true;
configRepository.setBool('isGameNoVR', this.isGameNoVR);
+ this.updateOpenVR();
break;
case 'udon-exception':
console.log('UdonException', gameLog.data);
@@ -13384,7 +13391,7 @@ speechSynthesis.getVoices();
this.updateVRConfigVars();
this.updateVRLastLocation();
AppApi.ExecuteVrOverlayFunction('notyClear', '');
- this.updateOpenVR(this.isGameRunning, this.isSteamVRRunning);
+ this.updateOpenVR();
};
$app.methods.saveSortFavoritesOption = function () {
this.getLocalWorldFavorites();
@@ -13523,6 +13530,7 @@ speechSynthesis.getVoices();
if (!this.timeoutHudOverlay) {
AppApi.ExecuteVrOverlayFunction('updateHudTimeout', '[]');
}
+ this.updateOpenVR();
};
$app.data.logResourceLoad = configRepository.getBool(
'VRCX_logResourceLoad'
@@ -13973,12 +13981,12 @@ speechSynthesis.getVoices();
});
};
- $app.methods.updateOpenVR = function (isGameRunning, isSteamVRRunning) {
+ $app.methods.updateOpenVR = function () {
if (
this.openVR &&
!this.isGameNoVR &&
- isSteamVRRunning &&
- (isGameRunning || this.openVRAlways)
+ this.isSteamVRRunning &&
+ (this.isGameRunning || this.openVRAlways)
) {
var hmdOverlay = false;
if (
@@ -15006,6 +15014,9 @@ speechSynthesis.getVoices();
});
$app.methods.showUserDialog = function (userId) {
+ if (!userId) {
+ return;
+ }
this.$nextTick(() => adjustDialogZ(this.$refs.userDialog.$el));
var D = this.userDialog;
D.id = userId;
@@ -20586,6 +20597,12 @@ speechSynthesis.getVoices();
this.VRChatConfigFile.screenshot_res_width = res.width;
};
+ // Auto Launch Shortcuts
+
+ $app.methods.openShortcutFolder = function () {
+ AppApi.OpenShortcutFolder();
+ };
+
// Screenshot Helper
$app.methods.saveScreenshotHelper = function () {
@@ -20747,7 +20764,7 @@ speechSynthesis.getVoices();
};
$app.methods.openImageFolder = function (path) {
- AppApi.OpenImageFolder(path).then(() => {
+ AppApi.OpenFolderAndSelectItem(path).then(() => {
this.$message({
message: 'Opened image folder',
type: 'success'
@@ -20804,6 +20821,7 @@ speechSynthesis.getVoices();
this.progressPieFilter
);
this.updateVRLastLocation();
+ this.updateOpenVR();
};
$app.methods.showYouTubeApiDialog = function () {
diff --git a/html/src/index.pug b/html/src/index.pug
index 7eabc6d5..782d7764 100644
--- a/html/src/index.pug
+++ b/html/src/index.pug
@@ -1418,6 +1418,9 @@ html
el-button(size="small" icon="el-icon-s-operation" @click="showVRChatConfig()") VRChat config.json
el-button(size="small" icon="el-icon-s-operation" @click="showLaunchOptions()") {{ $t('view.settings.advanced.advanced.launch_options') }}
el-button(size="small" icon="el-icon-picture" @click="showScreenshotMetadataDialog()") {{ $t('view.settings.advanced.advanced.screenshot_metadata') }}
+ el-button(size="small" icon="el-icon-folder" @click="openShortcutFolder()") {{ $t('view.settings.advanced.advanced.auto_launch') }}
+ el-tooltip(placement="top" style="margin-left:5px" :content="$t('view.settings.advanced.advanced.auto_launch_tooltip')")
+ i.el-icon-warning
div.options-container
span.sub-header {{ $t('view.settings.advanced.advanced.primary_password.header') }}
div.options-container-item
@@ -3880,7 +3883,7 @@ html
img(v-lazy="screenshotMetadataDialog.metadata.nextFilePath" style="height:700px")
br
template(v-if="screenshotMetadataDialog.metadata.error")
- pre(v-text="screenshotMetadataDialog.metadata.error")
+ pre(v-text="screenshotMetadataDialog.metadata.error" style="white-space:pre-wrap;font-size:12px")
br
span(v-for="user in screenshotMetadataDialog.metadata.players" style="margin-top:5px")
span.x-link(v-text="user.displayName" @click="lookupUser(user)")
diff --git a/html/src/localization/strings/en.json b/html/src/localization/strings/en.json
index e831b04d..80352fc0 100644
--- a/html/src/localization/strings/en.json
+++ b/html/src/localization/strings/en.json
@@ -354,6 +354,8 @@
"header": "Advanced",
"launch_options": "Launch Options",
"screenshot_metadata": "Screenshot Metadata",
+ "auto_launch": "Auto-Launch Folder",
+ "auto_launch_tooltip": "To auto-launch apps with VRChat, place shortcuts in this folder.",
"pending_offline": {
"header": "Pending Offline",
"description": "Delay before marking user as offline (fixes false positives)",