From bb31ce573669d07aa5e5673c8d08b8a32fb2afc9 Mon Sep 17 00:00:00 2001 From: Natsumi <11171153+Natsumi-sama@users.noreply.github.com> Date: Wed, 10 May 2023 16:57:25 +1200 Subject: [PATCH] feat: Add an automatic app launcher (#541) * refactor: Change OpenImageFolder to use a winapi call It will open the given file in any existing explorer instances instead of opening a new shell every time, handle longer paths, and work with third-party filesystem viewers with winapi hooks(well, this one). * feat: Add an automatic app launcher The launcer will automatically launch apps in the 'startup' folder under the VRCX appdata folder when VRChat has started, and close them when VRChat dies(or is closed). * refactor: Add new class for monitoring VRC processes This replaces the old AppApi functionality that would poll all processes twice to grab the status of both VRChat and vrserver.exe every... *checks app.js* 500ms. It also raises events for when a monitored process is started/closed, mainly for the new AppLauncher feature, which is now using this class instead of monitoring vrchat itself. * refactor: Add tooltip for launch folder button * docs: Add some notes on potential issues with Process.HasExited * Change CheckGameRunning from polling to events --------- Co-authored-by: Teacup --- AppApi.cs | 83 +++++++++-- AutoAppLaunchManager.cs | 139 +++++++++++++++++ ProcessMonitor.cs | 206 ++++++++++++++++++++++++++ Program.cs | 4 + ScreenshotHelper.cs | 54 ++++++- VRCX.csproj | 2 + WinApi.cs | 4 + html/src/app.js | 104 +++++++------ html/src/index.pug | 5 +- html/src/localization/strings/en.json | 2 + 10 files changed, 537 insertions(+), 66 deletions(-) create mode 100644 AutoAppLaunchManager.cs create mode 100644 ProcessMonitor.cs 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)",