diff --git a/AppApi.cs b/AppApi.cs index 56049482..453f457c 100644 --- a/AppApi.cs +++ b/AppApi.cs @@ -194,18 +194,22 @@ namespace VRCX } } - public void StartGameFromPath(string path, string arguments) + public bool StartGameFromPath(string path, string arguments) { if (!path.EndsWith(".exe")) path = Path.Combine(path, "start_protected_game.exe"); + if (!File.Exists(path)) + return false; + Process.Start(new ProcessStartInfo { WorkingDirectory = Path.GetDirectoryName(path), FileName = path, UseShellExecute = false, Arguments = arguments - }).Close(); + })?.Close(); + return true; } public void OpenLink(string url) @@ -428,6 +432,11 @@ namespace VRCX return Program.Version; } + public bool VrcClosedGracefully() + { + return LogWatcher.Instance.VrcClosedGracefully; + } + public void ChangeTheme(int value) { WinformThemer.SetGlobalTheme(value); diff --git a/LogWatcher.cs b/LogWatcher.cs index f54acc07..5c28e92a 100644 --- a/LogWatcher.cs +++ b/LogWatcher.cs @@ -4,40 +4,28 @@ // This work is licensed under the terms of the MIT license. // For a copy, see . -using CefSharp; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using System.Threading; +using CefSharp; namespace VRCX { public class LogWatcher { - private class LogContext - { - public long Length; - public long Position; - public string RecentWorldName; - public bool ShaderKeywordsLimitReached = false; - public bool AudioDeviceChanged = false; - public string LastAudioDevice; - public string LastVideoError; - public string onJoinPhotonDisplayName; - public string locationDestination; - } - public static readonly LogWatcher Instance; - private readonly DirectoryInfo m_LogDirectoryInfo; private readonly Dictionary m_LogContextMap; // - private readonly ReaderWriterLockSlim m_LogListLock; + private readonly DirectoryInfo m_LogDirectoryInfo; private readonly List m_LogList; - private Thread m_Thread; - private bool m_ResetLog; + private readonly ReaderWriterLockSlim m_LogListLock; private bool m_FirstRun = true; - private static DateTime tillDate = DateTime.Now; + private bool m_ResetLog; + private Thread m_Thread; + private DateTime tillDate = DateTime.Now; + public bool VrcClosedGracefully; // NOTE // FileSystemWatcher() is unreliable @@ -102,7 +90,7 @@ namespace VRCX private void Update() { - if (m_ResetLog == true) + if (m_ResetLog) { m_FirstRun = true; m_ResetLog = false; @@ -121,7 +109,7 @@ namespace VRCX var deletedNameSet = new HashSet(m_LogContextMap.Keys); m_LogDirectoryInfo.Refresh(); - if (m_LogDirectoryInfo.Exists == true) + if (m_LogDirectoryInfo.Exists) { var fileInfos = m_LogDirectoryInfo.GetFiles("output_log_*.txt", SearchOption.TopDirectoryOnly); @@ -141,7 +129,7 @@ namespace VRCX continue; } - if (m_LogContextMap.TryGetValue(fileInfo.Name, out LogContext logContext) == true) + if (m_LogContextMap.TryGetValue(fileInfo.Name, out var logContext)) { deletedNameSet.Remove(fileInfo.Name); } @@ -168,7 +156,7 @@ namespace VRCX m_FirstRun = false; } - + private void ParseLog(FileInfo fileInfo, LogContext logContext) { try @@ -187,9 +175,17 @@ namespace VRCX break; } + if (line.Length == 0) + { + continue; + } + // 2020.10.31 23:36:28 Log - [VRCFlowManagerVRC] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd // 2021.02.03 10:18:58 Log - [DŽDŽDžDžDžDŽDŽDžDžDŽDžDžDžDžDŽDŽDŽDžDžDŽDŽDžDžDžDžDŽDžDžDžDžDŽDŽDŽDŽDŽDžDŽDžDŽDŽDŽDžDžDŽDžDžDž] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd + if (ParseLogUdonException(fileInfo, line)) + continue; + if (line.Length <= 36 || line[31] != '-') { @@ -198,12 +194,12 @@ namespace VRCX } if (DateTime.TryParseExact( - line.Substring(0, 19), - "yyyy.MM.dd HH:mm:ss", - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeLocal, - out DateTime lineDate - )) + line.Substring(0, 19), + "yyyy.MM.dd HH:mm:ss", + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal, + out var lineDate + )) { if (DateTime.Compare(lineDate, tillDate) <= 0) { @@ -214,33 +210,31 @@ namespace VRCX var offset = 34; if (line[offset] == '[') { - if (ParseLogOnPlayerJoinedOrLeft(fileInfo, logContext, line, offset) == true || - ParseLogLocation(fileInfo, logContext, line, offset) == true || - ParseLogLocationDestination(fileInfo, logContext, line, offset) == true || - ParseLogPortalSpawn(fileInfo, logContext, line, offset) == true || - ParseLogNotification(fileInfo, logContext, line, offset) == true || - ParseLogAPIRequest(fileInfo, logContext, line, offset) == true || - ParseLogJoinBlocked(fileInfo, logContext, line, offset) == true || - ParseLogAvatarPedestalChange(fileInfo, logContext, line, offset) == true || - ParseLogVideoError(fileInfo, logContext, line, offset) == true || - ParseLogVideoChange(fileInfo, logContext, line, offset) == true || - ParseLogUsharpVideoPlay(fileInfo, logContext, line, offset) == true || - ParseLogUsharpVideoSync(fileInfo, logContext, line, offset) == true || - ParseLogWorldVRCX(fileInfo, logContext, line, offset) == true || - ParseLogOnAudioConfigurationChanged(fileInfo, logContext, line, offset) == true || - ParseLogScreenshot(fileInfo, logContext, line, offset) == true) + if (ParseLogOnPlayerJoinedOrLeft(fileInfo, logContext, line, offset) || + ParseLogLocation(fileInfo, logContext, line, offset) || + ParseLogLocationDestination(fileInfo, logContext, line, offset) || + ParseLogPortalSpawn(fileInfo, logContext, line, offset) || + ParseLogNotification(fileInfo, logContext, line, offset) || + ParseLogAPIRequest(fileInfo, logContext, line, offset) || + ParseLogJoinBlocked(fileInfo, logContext, line, offset) || + ParseLogAvatarPedestalChange(fileInfo, logContext, line, offset) || + ParseLogVideoError(fileInfo, logContext, line, offset) || + ParseLogVideoChange(fileInfo, logContext, line, offset) || + ParseLogUsharpVideoPlay(fileInfo, logContext, line, offset) || + ParseLogUsharpVideoSync(fileInfo, logContext, line, offset) || + ParseLogWorldVRCX(fileInfo, logContext, line, offset) || + ParseLogOnAudioConfigurationChanged(fileInfo, logContext, line, offset) || + ParseLogScreenshot(fileInfo, logContext, line, offset)) { - continue; } } else { - if (ParseLogShaderKeywordsLimit(fileInfo, logContext, line, offset) == true || - ParseLogSDK2VideoPlay(fileInfo, logContext, line, offset) == true || - ParseApplicationQuit(fileInfo, logContext, line, offset) == true || - ParseOpenVRInit(fileInfo, logContext, line, offset) == true) + if (ParseLogShaderKeywordsLimit(fileInfo, logContext, line, offset) || + ParseLogSDK2VideoPlay(fileInfo, logContext, line, offset) || + ParseApplicationQuit(fileInfo, logContext, line, offset) || + ParseOpenVRInit(fileInfo, logContext, line, offset)) { - continue; } } } @@ -263,6 +257,7 @@ namespace VRCX if (MainForm.Instance != null && MainForm.Instance.Browser != null) MainForm.Instance.Browser.ExecuteScriptAsync("$app.addGameLogEvent", logLine); } + m_LogList.Add(item); } finally @@ -276,12 +271,12 @@ namespace VRCX // 2020.10.31 23:36:22 if (DateTime.TryParseExact( - line.Substring(0, 19), - "yyyy.MM.dd HH:mm:ss", - CultureInfo.InvariantCulture, - DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeLocal, - out DateTime dt - ) == false) + line.Substring(0, 19), + "yyyy.MM.dd HH:mm:ss", + CultureInfo.InvariantCulture, + DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeLocal, + out var dt + ) == false) { dt = DateTime.UtcNow; } @@ -334,10 +329,11 @@ namespace VRCX logContext.RecentWorldName }); - logContext.onJoinPhotonDisplayName = String.Empty; - logContext.LastAudioDevice = String.Empty; - logContext.LastVideoError = String.Empty; - logContext.locationDestination = String.Empty; + logContext.onJoinPhotonDisplayName = string.Empty; + logContext.LastAudioDevice = string.Empty; + logContext.LastVideoError = string.Empty; + logContext.locationDestination = string.Empty; + VrcClosedGracefully = false; return true; } @@ -355,7 +351,7 @@ namespace VRCX var lineOffset = line.LastIndexOf("] Took screenshot to: "); if (lineOffset < 0) return true; - + var screenshotPath = line.Substring(lineOffset + 22); AppendLog(new[] { fileInfo.Name, ConvertLogTimeToISO8601(line), "screenshot", screenshotPath }); return true; @@ -377,7 +373,7 @@ namespace VRCX logContext.locationDestination }); - logContext.locationDestination = String.Empty; + logContext.locationDestination = string.Empty; return true; } @@ -481,6 +477,7 @@ namespace VRCX }); return true; } + return false; } @@ -491,7 +488,7 @@ namespace VRCX if (line.Contains("Maximum number (384) of shader global keywords exceeded")) { - if (logContext.ShaderKeywordsLimitReached == true) + if (logContext.ShaderKeywordsLimitReached) return true; AppendLog(new[] @@ -766,7 +763,7 @@ namespace VRCX if (pos < 0) return false; - if (!String.IsNullOrEmpty(logContext.onJoinPhotonDisplayName)) + if (!string.IsNullOrEmpty(logContext.onJoinPhotonDisplayName)) { var endPos = line.LastIndexOf(" ("); var photonId = line.Substring(pos + 9, endPos - (pos + 9)); @@ -779,7 +776,7 @@ namespace VRCX logContext.onJoinPhotonDisplayName, photonId }); - logContext.onJoinPhotonDisplayName = String.Empty; + logContext.onJoinPhotonDisplayName = string.Empty; return true; } @@ -830,12 +827,13 @@ namespace VRCX lineOffset += 3; var endPos = line.Length - 1; var audioDevice = line.Substring(lineOffset, endPos - lineOffset); - if (String.IsNullOrEmpty(logContext.LastAudioDevice)) + if (string.IsNullOrEmpty(logContext.LastAudioDevice)) { logContext.AudioDeviceChanged = false; logContext.LastAudioDevice = audioDevice; return true; } + if (!logContext.AudioDeviceChanged || logContext.LastAudioDevice == audioDevice) return true; @@ -856,6 +854,27 @@ namespace VRCX return false; } + private bool ParseLogUdonException(FileInfo fileInfo, string line) + { + // 2022.11.29 04:27:33 Error - [UdonBehaviour] An exception occurred during Udon execution, this UdonBehaviour will be halted. + // VRC.Udon.VM.UdonVMException: An exception occurred in an UdonVM, execution will be halted. --->VRC.Udon.VM.UdonVMException: An exception occurred during EXTERN to 'VRCSDKBaseVRCPlayerApi.__get_displayName__SystemString'. --->System.NullReferenceException: Object reference not set to an instance of an object. + var lineOffset = line.IndexOf(" ---> VRC.Udon.VM.UdonVMException: "); + if (lineOffset < 0) + return false; + + var data = line.Substring(lineOffset); + + AppendLog(new[] + { + fileInfo.Name, + ConvertLogTimeToISO8601(line), + "udon-exception", + data + }); + + return true; + } + private bool ParseApplicationQuit(FileInfo fileInfo, LogContext logContext, string line, int offset) { // 2022.06.12 01:51:46 Log - VRCApplication: OnApplicationQuit at 1603.499 @@ -870,6 +889,8 @@ namespace VRCX "vrc-quit" }); + VrcClosedGracefully = true; + return true; } @@ -890,12 +911,12 @@ namespace VRCX return true; } - private void ParseDesktopMode(FileInfo fileInfo, string line) + private bool ParseDesktopMode(FileInfo fileInfo, string line) { // XR Device: None if (string.Compare(line, 0, " XR Device: None", 0, 19, StringComparison.Ordinal) != 0) - return; + return false; AppendLog(new[] { @@ -903,6 +924,8 @@ namespace VRCX ConvertLogTimeToISO8601(line), "desktop-mode" }); + + return true; } public string[][] Get() @@ -939,5 +962,18 @@ namespace VRCX return new string[][] { }; } + + private class LogContext + { + public bool AudioDeviceChanged; + public string LastAudioDevice; + public string LastVideoError; + public long Length; + public string locationDestination; + public string onJoinPhotonDisplayName; + public long Position; + public string RecentWorldName; + public bool ShaderKeywordsLimitReached; + } } } \ No newline at end of file diff --git a/html/src/app.js b/html/src/app.js index 43771a42..999b6802 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -5026,6 +5026,7 @@ speechSynthesis.getVoices(); API.currentUser.$online_for = ''; API.currentUser.$offline_for = Date.now(); this.autoVRChatCacheManagement(); + this.checkIfGameCrashed(); this.ipcTimeout = 0; } this.lastLocationReset(); @@ -9197,7 +9198,8 @@ speechSynthesis.getVoices(); if ( this.debugGameLog && gameLog.type !== 'photon-id' && - gameLog.type !== 'api-request' + gameLog.type !== 'api-request' && + gameLog.type !== 'udon-exception' ) { console.log('gameLog:', gameLog); } @@ -9525,6 +9527,15 @@ speechSynthesis.getVoices(); this.isGameNoVR = true; configRepository.setBool('isGameNoVR', this.isGameNoVR); break; + case 'udon-exception': + console.log('UdonException', gameLog.data); + // var entry = { + // created_at: gameLog.dt, + // type: 'Event', + // data: gameLog.data + // }; + // database.addGamelogEventToDatabase(entry); + break; } if (entry) { this.queueGameLogNoty(entry); @@ -13060,6 +13071,9 @@ speechSynthesis.getVoices(); $app.data.autoSweepVRChatCache = configRepository.getBool( 'VRCX_autoSweepVRChatCache' ); + $app.data.relaunchVRChatAfterCrash = configRepository.getBool( + 'VRCX_relaunchVRChatAfterCrash' + ); $app.data.vrcQuitFix = configRepository.getBool('VRCX_vrcQuitFix'); $app.data.vrBackgroundEnabled = configRepository.getBool( 'VRCX_vrBackgroundEnabled' @@ -13180,6 +13194,10 @@ speechSynthesis.getVoices(); 'VRCX_autoSweepVRChatCache', this.autoSweepVRChatCache ); + configRepository.setBool( + 'VRCX_relaunchVRChatAfterCrash', + this.relaunchVRChatAfterCrash + ); configRepository.setBool('VRCX_vrcQuitFix', this.vrcQuitFix); configRepository.setBool( 'VRCX_vrBackgroundEnabled', @@ -17738,9 +17756,29 @@ speechSynthesis.getVoices(); args.push('--no-vr'); } if (vrcLaunchPathOverride) { - AppApi.StartGameFromPath(vrcLaunchPathOverride, args.join(' ')); + AppApi.StartGameFromPath( + vrcLaunchPathOverride, + args.join(' ') + ).then((result) => { + if (!result) { + this.$message({ + message: + 'Failed to launch VRChat, invalid custom path set', + type: 'error' + }); + } else { + this.$message({ + message: 'VRChat launched', + type: 'success' + }); + } + }); } else { AppApi.StartGame(args.join(' ')); + this.$message({ + message: 'VRChat launched', + type: 'success' + }); } D.visible = false; }; @@ -20689,6 +20727,28 @@ speechSynthesis.getVoices(); } }; + $app.methods.checkIfGameCrashed = function () { + if (!this.relaunchVRChatAfterCrash) { + return; + } + var lastLocation = this.lastLocation.location; + AppApi.VrcClosedGracefully().then((result) => { + console.log(result, lastLocation); + if (result || !this.isRealInstance(lastLocation)) { + return; + } + var entry = { + created_at: new Date().toJSON(), + type: 'Event', + data: 'VRChat crashed, attempting to rejoin last instance' + }; + database.addGamelogEventToDatabase(entry); + this.queueGameLogNoty(entry); + this.addGameLog(entry); + this.launchGame(lastLocation); + }); + }; + $app.data.VRChatUsedCacheSize = ''; $app.data.VRChatTotalCacheSize = ''; $app.data.VRChatCacheSizeLoading = false; diff --git a/html/src/index.pug b/html/src/index.pug index b53b5f11..5e20f077 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -1406,15 +1406,14 @@ html 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') }} div.options-container - span.sub-header {{ $t('view.settings.advanced.advanced.pending_offline.header') }} - div.options-container-item - span.name {{ $t('view.settings.advanced.advanced.pending_offline.description') }} - el-button-group(style="display:block") - el-button(size="small" icon="el-icon-s-operation" @click="promptSetPendingOffline") {{ $t('view.settings.advanced.advanced.pending_offline.set_delay') }} span.sub-header {{ $t('view.settings.advanced.advanced.primary_password.header') }} div.options-container-item span.name(style="min-width:300px") {{ $t('view.settings.advanced.advanced.primary_password.description') }} el-switch(v-model="enablePrimaryPassword" @change="enablePrimaryPasswordChange" :disabled="!loginForm.savedCredentials[API.currentUser.username]") + span.sub-header {{ $t('view.settings.advanced.advanced.relaunch_vrchat.header') }} + div.options-container-item + span.name(style="min-width:300px") {{ $t('view.settings.advanced.advanced.relaunch_vrchat.description') }} + el-switch(v-model="relaunchVRChatAfterCrash" @change="saveOpenVROption") span.sub-header {{ $t('view.settings.advanced.advanced.vrchat_quit_fix.header') }} div.options-container-item span.name(style="min-width:300px") {{ $t('view.settings.advanced.advanced.vrchat_quit_fix.description') }} @@ -1497,6 +1496,11 @@ html el-button(size="small" icon="el-icon-time" @click="promptPhotonLobbyTimeoutThreshold" :disabled="!openVR") {{ $t('view.settings.advanced.photon.timeout_hud.timeout_threshold') }} div.options-container span.header {{ $t('view.settings.advanced.advanced.cache_debug.header') }} + span.sub-header {{ $t('view.settings.advanced.advanced.pending_offline.header') }} + div.options-container-item + span.name {{ $t('view.settings.advanced.advanced.pending_offline.description') }} + el-button-group(style="display:block") + el-button(size="small" icon="el-icon-s-operation" @click="promptSetPendingOffline") {{ $t('view.settings.advanced.advanced.pending_offline.set_delay') }} div.options-container-item span.name {{ $t('view.settings.advanced.advanced.cache_debug.disable_gamelog') }} el-switch(v-model="gameLogDisabled" @change="disableGameLogDialog") diff --git a/html/src/localization/strings/en.json b/html/src/localization/strings/en.json index 6cf7871f..4a0cc15a 100644 --- a/html/src/localization/strings/en.json +++ b/html/src/localization/strings/en.json @@ -359,6 +359,10 @@ "header": "Primary Password", "description": "Encrypt password (disables auto login)" }, + "relaunch_vrchat": { + "header": "Relaunch VRChat after crashing", + "description": "Rejoin last instance when VRChat crashes" + }, "vrchat_quit_fix": { "header": "VRChat Quit Fix", "description": "Kill VRChat after exiting game" diff --git a/html/src/service/gamelog.js b/html/src/service/gamelog.js index e373fd44..df3e7015 100644 --- a/html/src/service/gamelog.js +++ b/html/src/service/gamelog.js @@ -71,6 +71,10 @@ class GameLogService { case 'desktop-mode': break; + case 'udon-exception': + gameLog.data = args[0]; + break; + default: break; }