using System; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Microsoft.Win32; using System.Threading; using System.Windows.Forms; using System.Threading.Tasks; namespace VRCX { public partial class AppApiCef { public override string GetVRChatAppDataLocation() { return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat"; } public override string GetVRChatCacheLocation() { var defaultPath = Path.Join(GetVRChatAppDataLocation(), "Cache-WindowsPlayer"); try { var json = ReadConfigFile(); if (string.IsNullOrEmpty(json)) return defaultPath; var obj = JsonConvert.DeserializeObject(json, JsonSerializerSettings); if (obj["cache_directory"] == null) return defaultPath; var cacheDir = (string)obj["cache_directory"]; if (string.IsNullOrEmpty(cacheDir)) return defaultPath; var cachePath = Path.Join(cacheDir, "Cache-WindowsPlayer"); if (!Directory.Exists(cacheDir)) return defaultPath; return cachePath; } catch (Exception e) { logger.Error($"Error reading VRChat config file for cache location: {e}"); } return defaultPath; } public override string GetVRChatPhotosLocation() { var defaultPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "VRChat"); try { var json = ReadConfigFile(); if (string.IsNullOrEmpty(json)) return defaultPath; var obj = JsonConvert.DeserializeObject(json, JsonSerializerSettings); if (obj["picture_output_folder"] == null) return defaultPath; var photosDir = (string)obj["picture_output_folder"]; if (string.IsNullOrEmpty(photosDir) || !Directory.Exists(photosDir)) return defaultPath; return photosDir; } catch (Exception e) { logger.Error($"Error reading VRChat config file for photos location: {e}"); } return defaultPath; } public override string GetUGCPhotoLocation(string path = "") { if (string.IsNullOrEmpty(path)) { return GetVRChatPhotosLocation(); } try { if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } return path; } catch (Exception e) { logger.Error(e); return GetVRChatPhotosLocation(); } } private string GetSteamUserdataPathFromRegistry() { string steamUserdataPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"Steam\userdata"); try { using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\WOW6432Node\Valve\Steam")) { if (key != null) { object o = key.GetValue("InstallPath"); if (o != null) { steamUserdataPath = Path.Join(o.ToString(), @"userdata"); } } } } catch (Exception e) { logger.Error($"Failed to get Steam userdata path from registry: {e}"); } return steamUserdataPath; } public override string GetVRChatScreenshotsLocation() { // program files steam userdata screenshots var steamUserdataPath = GetSteamUserdataPathFromRegistry(); var screenshotPath = string.Empty; var latestWriteTime = DateTime.MinValue; if (!Directory.Exists(steamUserdataPath)) return screenshotPath; var steamUserDirs = Directory.GetDirectories(steamUserdataPath); foreach (var steamUserDir in steamUserDirs) { var screenshotDir = Path.Join(steamUserDir, @"760\remote\438100\screenshots"); if (!Directory.Exists(screenshotDir)) continue; var lastWriteTime = File.GetLastWriteTime(screenshotDir); if (lastWriteTime <= latestWriteTime) continue; latestWriteTime = lastWriteTime; screenshotPath = screenshotDir; } return screenshotPath; } public override bool OpenVrcxAppDataFolder() { var path = Program.AppDataDirectory; if (!Directory.Exists(path)) return false; OpenFolderAndSelectItem(path, true); return true; } public override bool OpenVrcAppDataFolder() { var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat"; if (!Directory.Exists(path)) return false; OpenFolderAndSelectItem(path, true); return true; } public override bool OpenVrcPhotosFolder() { var path = GetVRChatPhotosLocation(); if (!Directory.Exists(path)) return false; OpenFolderAndSelectItem(path, true); return true; } public override bool OpenUGCPhotosFolder(string ugcPath = "") { var path = GetUGCPhotoLocation(ugcPath); if (!Directory.Exists(path)) return false; OpenFolderAndSelectItem(path, true); return true; } public override bool OpenVrcScreenshotsFolder() { var path = GetVRChatScreenshotsLocation(); if (!Directory.Exists(path)) return false; OpenFolderAndSelectItem(path, true); return true; } public override bool OpenCrashVrcCrashDumps() { var path = Path.Join(Path.GetTempPath(), "VRChat", "VRChat", "Crashes"); if (!Directory.Exists(path)) return false; OpenFolderAndSelectItem(path, true); return true; } public override void OpenShortcutFolder() { var path = AutoAppLaunchManager.Instance.AppShortcutDirectory; if (!Directory.Exists(path)) return; OpenFolderAndSelectItem(path, true); } public override void OpenFolderAndSelectItem(string path, bool isFolder = false) { path = Path.GetFullPath(path); // 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) { OpenFolderAndSelectItemFallback(path); 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); OpenFolderAndSelectItemFallback(path); 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); } catch { OpenFolderAndSelectItemFallback(path); } finally { // Free the PIDLs we allocated earlier Marshal.FreeCoTaskMem(pidlFolder); Marshal.FreeCoTaskMem(pidlFile); } } private void OpenFolderAndSelectItemFallback(string path) { if (!File.Exists(path) && !Directory.Exists(path)) return; if (Directory.Exists(path)) { Process.Start("explorer.exe", path); } else { // open folder with file highlighted Process.Start("explorer.exe", $"/select,\"{path}\""); } } public override async Task OpenFolderSelectorDialog(string defaultPath = "") { var tcs = new TaskCompletionSource(); var staThread = new Thread(() => { try { using var openFolderDialog = new FolderBrowserDialog(); openFolderDialog.InitialDirectory = Directory.Exists(defaultPath) ? defaultPath : GetVRChatPhotosLocation(); var dialogResult = openFolderDialog.ShowDialog(MainForm.nativeWindow); if (dialogResult == DialogResult.OK) { tcs.SetResult(openFolderDialog.SelectedPath); } else { tcs.SetResult(defaultPath); } } catch (Exception ex) { tcs.SetException(ex); } }); staThread.SetApartmentState(ApartmentState.STA); staThread.Start(); return await tcs.Task; } public override async Task OpenFileSelectorDialog(string defaultPath = "", string defaultExt = "", string defaultFilter = "All files (*.*)|*.*") { var tcs = new TaskCompletionSource(); var staThread = new Thread(() => { try { using (var openFileDialog = new System.Windows.Forms.OpenFileDialog()) { if (Directory.Exists(defaultPath)) { openFileDialog.InitialDirectory = defaultPath; } openFileDialog.DefaultExt = defaultExt; openFileDialog.Filter = defaultFilter; var dialogResult = openFileDialog.ShowDialog(MainForm.nativeWindow); if (dialogResult == DialogResult.OK && !string.IsNullOrEmpty(openFileDialog.FileName)) { tcs.SetResult(openFileDialog.FileName); } else { tcs.SetResult(""); } } } catch (Exception ex) { tcs.SetException(ex); } }); staThread.SetApartmentState(ApartmentState.STA); staThread.Start(); return await tcs.Task; } } }