mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
Electron support for Linux (#1074)
* init * SQLite changes * Move html folder, edit build scripts * AppApi interface * Build flags * AppApi inheritance * Finishing touches * Merge upstream changes * Test CI * Fix class inits * Rename AppApi * Merge upstream changes * Fix SQLiteLegacy on Linux, Add Linux interop, build tools * Linux specific localisation strings * Make it run * Bring back most of Linux functionality * Clean up * Fix TTS voices * Fix UI var * Changes * Electron minimise to tray * Remove separate toggle for WlxOverlay * Fixes * Touchups * Move csproj * Window zoom, Desktop Notifications, VR check on Linux * Fix desktop notifications, VR check spam * Fix building on Linux * Clean up * Fix WebApi headers * Rewrite VRCX updater * Clean up * Linux updater * Add Linux to build action * init * SQLite changes * Move html folder, edit build scripts * AppApi interface * Build flags * AppApi inheritance * Finishing touches * Merge upstream changes * Test CI * Fix class inits * Rename AppApi * Merge upstream changes * Fix SQLiteLegacy on Linux, Add Linux interop, build tools * Linux specific localisation strings * Make it run * Bring back most of Linux functionality * Clean up * Fix TTS voices * Changes * Electron minimise to tray * Remove separate toggle for WlxOverlay * Fixes * Touchups * Move csproj * Window zoom, Desktop Notifications, VR check on Linux * Fix desktop notifications, VR check spam * Fix building on Linux * Clean up * Fix WebApi headers * Rewrite VRCX updater * Clean up * Linux updater * Add Linux to build action * Test updater * Rebase and handle merge conflicts * Fix Linux updater * Fix Linux app restart * Fix friend order * Handle AppImageInstaller, show an install message on Linux * Updates to the AppImage installer * Fix Linux updater, fix set version, check for .NET, copy wine prefix * Handle random errors * Rotate tall prints * try fix Linux restart bug * Final --------- Co-authored-by: rs189 <35667100+rs189@users.noreply.github.com>
This commit is contained in:
247
Dotnet/AppApi/Cef/AppApiCef.cs
Normal file
247
Dotnet/AppApi/Cef/AppApiCef.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
// Copyright(c) 2019-2022 pypy, Natsumi and individual contributors.
|
||||
// All rights reserved.
|
||||
//
|
||||
// This work is licensed under the terms of the MIT license.
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using librsync.net;
|
||||
using Microsoft.Toolkit.Uwp.Notifications;
|
||||
using Microsoft.Win32;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApiCef : AppApi
|
||||
{
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Shows the developer tools for the main browser window.
|
||||
/// </summary>
|
||||
public override void ShowDevTools()
|
||||
{
|
||||
MainForm.Instance.Browser.ShowDevTools();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all cookies from the global cef cookie manager.
|
||||
/// </summary>
|
||||
public override void DeleteAllCookies()
|
||||
{
|
||||
Cef.GetGlobalCookieManager().DeleteCookies();
|
||||
}
|
||||
|
||||
public override void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
|
||||
{
|
||||
Program.VRCXVRInstance.SetActive(active, hmdOverlay, wristOverlay, menuButton, overlayHand);
|
||||
}
|
||||
|
||||
public override void RefreshVR()
|
||||
{
|
||||
Program.VRCXVRInstance.Restart();
|
||||
}
|
||||
|
||||
public override void RestartVR()
|
||||
{
|
||||
Program.VRCXVRInstance.Restart();
|
||||
}
|
||||
|
||||
public override void SetZoom(double zoomLevel)
|
||||
{
|
||||
MainForm.Instance.Browser.SetZoomLevel(zoomLevel);
|
||||
}
|
||||
|
||||
public override async Task<double> GetZoom()
|
||||
{
|
||||
return await MainForm.Instance.Browser.GetZoomLevelAsync();
|
||||
}
|
||||
|
||||
public override void DesktopNotification(string BoldText, string Text = "", string Image = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
ToastContentBuilder builder = new ToastContentBuilder();
|
||||
|
||||
if (Uri.TryCreate(Image, UriKind.Absolute, out Uri uri))
|
||||
builder.AddAppLogoOverride(uri);
|
||||
|
||||
if (!string.IsNullOrEmpty(BoldText))
|
||||
builder.AddText(BoldText);
|
||||
|
||||
if (!string.IsNullOrEmpty(Text))
|
||||
builder.AddText(Text);
|
||||
|
||||
builder.Show();
|
||||
}
|
||||
catch (System.AccessViolationException ex)
|
||||
{
|
||||
logger.Warn(ex, "Unable to send desktop notification");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Unknown error when sending desktop notification");
|
||||
}
|
||||
}
|
||||
|
||||
public override void RestartApplication(bool isUpgrade)
|
||||
{
|
||||
var args = new List<string>();
|
||||
|
||||
if (isUpgrade)
|
||||
args.Add(StartupArgs.VrcxLaunchArguments.IsUpgradePrefix);
|
||||
|
||||
if (StartupArgs.LaunchArguments.IsDebug)
|
||||
args.Add(StartupArgs.VrcxLaunchArguments.IsDebugPrefix);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(StartupArgs.LaunchArguments.ConfigDirectory))
|
||||
args.Add($"{StartupArgs.VrcxLaunchArguments.ConfigDirectoryPrefix}={StartupArgs.LaunchArguments.ConfigDirectory}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(StartupArgs.LaunchArguments.ProxyUrl))
|
||||
args.Add($"{StartupArgs.VrcxLaunchArguments.ProxyUrlPrefix}={StartupArgs.LaunchArguments.ProxyUrl}");
|
||||
|
||||
var vrcxProcess = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Path.Combine(Program.BaseDirectory, "VRCX.exe"),
|
||||
Arguments = string.Join(' ', args),
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = Program.BaseDirectory
|
||||
}
|
||||
};
|
||||
vrcxProcess.Start();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
public override bool CheckForUpdateExe()
|
||||
{
|
||||
return File.Exists(Path.Combine(Program.AppDataDirectory, "update.exe"));
|
||||
}
|
||||
|
||||
public override void ExecuteAppFunction(string function, string json)
|
||||
{
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync($"$app.{function}", json);
|
||||
}
|
||||
|
||||
public override void ExecuteVrFeedFunction(string function, string json)
|
||||
{
|
||||
Program.VRCXVRInstance.ExecuteVrFeedFunction(function, json);
|
||||
}
|
||||
|
||||
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||
{
|
||||
Program.VRCXVRInstance.ExecuteVrOverlayFunction(function, json);
|
||||
}
|
||||
|
||||
public override string GetLaunchCommand()
|
||||
{
|
||||
var command = StartupArgs.LaunchArguments.LaunchCommand;
|
||||
StartupArgs.LaunchArguments.LaunchCommand = string.Empty;
|
||||
return command;
|
||||
}
|
||||
|
||||
public override void FocusWindow()
|
||||
{
|
||||
MainForm.Instance.Invoke(new Action(() => { MainForm.Instance.Focus_Window(); }));
|
||||
}
|
||||
|
||||
public override void ChangeTheme(int value)
|
||||
{
|
||||
WinformThemer.SetGlobalTheme(value);
|
||||
}
|
||||
|
||||
public override void DoFunny()
|
||||
{
|
||||
WinformThemer.DoFunny();
|
||||
}
|
||||
|
||||
public override string GetClipboard()
|
||||
{
|
||||
var clipboard = string.Empty;
|
||||
var thread = new Thread(() => clipboard = Clipboard.GetText());
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
thread.Join();
|
||||
return clipboard;
|
||||
}
|
||||
|
||||
public override void SetStartup(bool enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
|
||||
if (key == null)
|
||||
{
|
||||
logger.Warn("Failed to open startup registry key");
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
var path = Application.ExecutablePath;
|
||||
key.SetValue("VRCX", $"\"{path}\" --startup");
|
||||
}
|
||||
else
|
||||
{
|
||||
key.DeleteValue("VRCX", false);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Warn(e, "Failed to set startup");
|
||||
}
|
||||
}
|
||||
|
||||
public override void CopyImageToClipboard(string path)
|
||||
{
|
||||
if (!File.Exists(path) ||
|
||||
(!path.EndsWith(".png") &&
|
||||
!path.EndsWith(".jpg") &&
|
||||
!path.EndsWith(".jpeg") &&
|
||||
!path.EndsWith(".gif") &&
|
||||
!path.EndsWith(".bmp") &&
|
||||
!path.EndsWith(".webp")))
|
||||
return;
|
||||
|
||||
MainForm.Instance.BeginInvoke(new MethodInvoker(() =>
|
||||
{
|
||||
var image = Image.FromFile(path);
|
||||
// Clipboard.SetImage(image);
|
||||
var data = new DataObject();
|
||||
data.SetData(DataFormats.Bitmap, image);
|
||||
data.SetFileDropList(new StringCollection { path });
|
||||
Clipboard.SetDataObject(data, true);
|
||||
}));
|
||||
}
|
||||
|
||||
public override void FlashWindow()
|
||||
{
|
||||
MainForm.Instance.BeginInvoke(new MethodInvoker(() => { WinformThemer.Flash(MainForm.Instance); }));
|
||||
}
|
||||
|
||||
public override void SetUserAgent()
|
||||
{
|
||||
using var client = MainForm.Instance.Browser.GetDevToolsClient();
|
||||
_ = client.Network.SetUserAgentOverrideAsync(Program.Version);
|
||||
}
|
||||
|
||||
public override bool IsRunningUnderWine()
|
||||
{
|
||||
return Wine.GetIfWine();
|
||||
}
|
||||
}
|
||||
}
|
||||
89
Dotnet/AppApi/Cef/AppApiVr.cs
Normal file
89
Dotnet/AppApi/Cef/AppApiVr.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using CefSharp;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class AppApiVr
|
||||
{
|
||||
public static readonly AppApiVr Instance;
|
||||
|
||||
static AppApiVr()
|
||||
{
|
||||
Instance = new AppApiVr();
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
// Create Instance before Cef tries to bind it
|
||||
}
|
||||
|
||||
public void VrInit()
|
||||
{
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.vrInit", "");
|
||||
}
|
||||
|
||||
public void ToggleSystemMonitor(bool enabled)
|
||||
{
|
||||
SystemMonitor.Instance.Start(enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current CPU usage as a percentage.
|
||||
/// </summary>
|
||||
/// <returns>The current CPU usage as a percentage.</returns>
|
||||
public float CpuUsage()
|
||||
{
|
||||
return SystemMonitor.Instance.CpuUsage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an array of arrays containing information about the connected VR devices.
|
||||
/// Each sub-array contains the type of device and its current state
|
||||
/// </summary>
|
||||
/// <returns>An array of arrays containing information about the connected VR devices.</returns>
|
||||
public string[][] GetVRDevices()
|
||||
{
|
||||
return Program.VRCXVRInstance.GetDevices();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of milliseconds that the system has been running.
|
||||
/// </summary>
|
||||
/// <returns>The number of milliseconds that the system has been running.</returns>
|
||||
public double GetUptime()
|
||||
{
|
||||
return SystemMonitor.Instance.UpTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current language of the operating system.
|
||||
/// </summary>
|
||||
/// <returns>The current language of the operating system.</returns>
|
||||
public string CurrentCulture()
|
||||
{
|
||||
return CultureInfo.CurrentCulture.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file path of the custom user js file, if it exists.
|
||||
/// </summary>
|
||||
/// <returns>The file path of the custom user js file, or an empty string if it doesn't exist.</returns>
|
||||
public string CustomVrScriptPath()
|
||||
{
|
||||
var output = string.Empty;
|
||||
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX\\customvr.js");
|
||||
if (File.Exists(filePath))
|
||||
output = filePath;
|
||||
return output;
|
||||
}
|
||||
|
||||
public bool IsRunningUnderWine()
|
||||
{
|
||||
return Wine.GetIfWine();
|
||||
}
|
||||
}
|
||||
}
|
||||
342
Dotnet/AppApi/Cef/Folders.cs
Normal file
342
Dotnet/AppApi/Cef/Folders.cs
Normal file
@@ -0,0 +1,342 @@
|
||||
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 json = ReadConfigFile();
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
var obj = JsonConvert.DeserializeObject<JObject>(json);
|
||||
if (obj["cache_directory"] != null)
|
||||
{
|
||||
var cacheDir = (string)obj["cache_directory"];
|
||||
if (!string.IsNullOrEmpty(cacheDir) && Directory.Exists(cacheDir))
|
||||
{
|
||||
return cacheDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Path.Combine(GetVRChatAppDataLocation(), "Cache-WindowsPlayer");
|
||||
}
|
||||
|
||||
public override string GetVRChatPhotosLocation()
|
||||
{
|
||||
var json = ReadConfigFile();
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
var obj = JsonConvert.DeserializeObject<JObject>(json);
|
||||
if (obj["picture_output_folder"] != null)
|
||||
{
|
||||
var photosDir = (string)obj["picture_output_folder"];
|
||||
if (!string.IsNullOrEmpty(photosDir) && Directory.Exists(photosDir))
|
||||
{
|
||||
return photosDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "VRChat");
|
||||
}
|
||||
|
||||
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.Combine(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.Combine(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.Combine(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.Combine(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<string> OpenFolderSelectorDialog(string defaultPath = "")
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
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<string> OpenFileSelectorDialog(string defaultPath = "", string defaultExt = "", string defaultFilter = "All files (*.*)|*.*")
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
140
Dotnet/AppApi/Cef/GameHandler.cs
Normal file
140
Dotnet/AppApi/Cef/GameHandler.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using CefSharp;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApiCef
|
||||
{
|
||||
public override void OnProcessStateChanged(MonitoredProcess monitoredProcess)
|
||||
{
|
||||
if (!monitoredProcess.HasName("VRChat") && !monitoredProcess.HasName("vrserver"))
|
||||
return;
|
||||
|
||||
CheckGameRunning();
|
||||
}
|
||||
|
||||
public override void CheckGameRunning()
|
||||
{
|
||||
var isGameRunning = false;
|
||||
var isSteamVRRunning = false;
|
||||
var isHmdAfk = false;
|
||||
|
||||
if (ProcessMonitor.Instance.IsProcessRunning("VRChat"))
|
||||
isGameRunning = true;
|
||||
|
||||
if (Wine.GetIfWine())
|
||||
{
|
||||
var wineTmpPath = Path.Combine(Program.AppDataDirectory, "wine.tmp");
|
||||
if (File.Exists(wineTmpPath))
|
||||
{
|
||||
var wineTmp = File.ReadAllText(wineTmpPath);
|
||||
if (wineTmp.Contains("isGameRunning=true"))
|
||||
isGameRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ProcessMonitor.Instance.IsProcessRunning("vrserver"))
|
||||
isSteamVRRunning = true;
|
||||
|
||||
if (Program.VRCXVRInstance != null)
|
||||
isHmdAfk = Program.VRCXVRInstance.IsHmdAfk;
|
||||
|
||||
// TODO: fix this throwing an exception for being called before the browser is ready. somehow it gets past the checks
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.updateIsGameRunning", isGameRunning, isSteamVRRunning, isHmdAfk);
|
||||
}
|
||||
|
||||
public override bool IsGameRunning()
|
||||
{
|
||||
// unused
|
||||
return ProcessMonitor.Instance.IsProcessRunning("VRChat");
|
||||
}
|
||||
|
||||
public override bool IsSteamVRRunning()
|
||||
{
|
||||
// unused
|
||||
return ProcessMonitor.Instance.IsProcessRunning("vrserver");
|
||||
}
|
||||
|
||||
public override int QuitGame()
|
||||
{
|
||||
var processes = Process.GetProcessesByName("vrchat");
|
||||
if (processes.Length == 1)
|
||||
processes[0].Kill();
|
||||
|
||||
return processes.Length;
|
||||
}
|
||||
|
||||
public override bool StartGame(string arguments)
|
||||
{
|
||||
// try stream first
|
||||
try
|
||||
{
|
||||
using var key = Registry.ClassesRoot.OpenSubKey(@"steam\shell\open\command");
|
||||
// "C:\Program Files (x86)\Steam\steam.exe" -- "%1"
|
||||
var match = Regex.Match(key.GetValue(string.Empty) as string, "^\"(.+?)\\\\steam.exe\"");
|
||||
if (match.Success)
|
||||
{
|
||||
var path = match.Groups[1].Value;
|
||||
// var _arguments = Uri.EscapeDataString(arguments);
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
WorkingDirectory = path,
|
||||
FileName = $"{path}\\steam.exe",
|
||||
UseShellExecute = false,
|
||||
Arguments = $"-applaunch 438100 {arguments}"
|
||||
})
|
||||
?.Close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
logger.Warn("Failed to start VRChat from Steam");
|
||||
}
|
||||
|
||||
// fallback
|
||||
try
|
||||
{
|
||||
using var key = Registry.ClassesRoot.OpenSubKey(@"VRChat\shell\open\command");
|
||||
// "C:\Program Files (x86)\Steam\steamapps\common\VRChat\launch.exe" "%1" %*
|
||||
var match = Regex.Match(key.GetValue(string.Empty) as string, "(?!\")(.+?\\\\VRChat.*)(!?\\\\launch.exe\")");
|
||||
if (match.Success)
|
||||
{
|
||||
var path = match.Groups[1].Value;
|
||||
return StartGameFromPath(path, arguments);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
logger.Warn("Failed to start VRChat from registry");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool StartGameFromPath(string path, string arguments)
|
||||
{
|
||||
if (!path.EndsWith(".exe"))
|
||||
path = Path.Combine(path, "launch.exe");
|
||||
|
||||
if (!path.EndsWith("launch.exe") || !File.Exists(path))
|
||||
return false;
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
WorkingDirectory = Path.GetDirectoryName(path),
|
||||
FileName = path,
|
||||
UseShellExecute = false,
|
||||
Arguments = arguments
|
||||
})?.Close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
279
Dotnet/AppApi/Cef/RegistryPlayerPrefs.cs
Normal file
279
Dotnet/AppApi/Cef/RegistryPlayerPrefs.cs
Normal file
@@ -0,0 +1,279 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApiCef
|
||||
{
|
||||
[DllImport("advapi32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
|
||||
private static extern uint RegSetValueEx(
|
||||
UIntPtr hKey,
|
||||
[MarshalAs(UnmanagedType.LPStr)] string lpValueName,
|
||||
int Reserved,
|
||||
RegistryValueKind dwType,
|
||||
byte[] lpData,
|
||||
int cbData);
|
||||
|
||||
[DllImport("advapi32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
|
||||
private static extern int RegOpenKeyEx(
|
||||
UIntPtr hKey,
|
||||
string subKey,
|
||||
int ulOptions,
|
||||
int samDesired,
|
||||
out UIntPtr hkResult);
|
||||
|
||||
[DllImport("advapi32.dll")]
|
||||
private static extern int RegCloseKey(UIntPtr hKey);
|
||||
|
||||
private string AddHashToKeyName(string key)
|
||||
{
|
||||
// https://discussions.unity.com/t/playerprefs-changing-the-name-of-keys/30332/4
|
||||
// VRC_GROUP_ORDER_usr_032383a7-748c-4fb2-94e4-bcb928e5de6b_h2810492971
|
||||
uint hash = 5381;
|
||||
foreach (var c in key)
|
||||
hash = (hash * 33) ^ c;
|
||||
return key + "_h" + hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the value of the specified key from the VRChat group in the windows registry.
|
||||
/// </summary>
|
||||
/// <param name="key">The name of the key to retrieve.</param>
|
||||
/// <returns>The value of the specified key, or null if the key does not exist.</returns>
|
||||
public override object GetVRChatRegistryKey(string key)
|
||||
{
|
||||
var keyName = AddHashToKeyName(key);
|
||||
using var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat");
|
||||
var data = regKey?.GetValue(keyName);
|
||||
if (data == null)
|
||||
return null;
|
||||
|
||||
var type = regKey.GetValueKind(keyName);
|
||||
switch (type)
|
||||
{
|
||||
case RegistryValueKind.Binary:
|
||||
return Encoding.ASCII.GetString((byte[])data);
|
||||
|
||||
case RegistryValueKind.DWord:
|
||||
if (data.GetType() != typeof(long))
|
||||
return data;
|
||||
|
||||
long.TryParse(data.ToString(), out var longValue);
|
||||
var bytes = BitConverter.GetBytes(longValue);
|
||||
var doubleValue = BitConverter.ToDouble(bytes, 0);
|
||||
return doubleValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override string GetVRChatRegistryKeyString(string key)
|
||||
{
|
||||
// for electron
|
||||
return GetVRChatRegistryKey(key)?.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of the specified key in the VRChat group in the windows registry.
|
||||
/// </summary>
|
||||
/// <param name="key">The name of the key to set.</param>
|
||||
/// <param name="value">The value to set for the specified key.</param>
|
||||
/// <param name="typeInt">The RegistryValueKind type.</param>
|
||||
/// <returns>True if the key was successfully set, false otherwise.</returns>
|
||||
public override bool SetVRChatRegistryKey(string key, object value, int typeInt)
|
||||
{
|
||||
var type = (RegistryValueKind)typeInt;
|
||||
var keyName = AddHashToKeyName(key);
|
||||
using var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat", true);
|
||||
if (regKey == null)
|
||||
return false;
|
||||
|
||||
object setValue = null;
|
||||
switch (type)
|
||||
{
|
||||
case RegistryValueKind.Binary:
|
||||
setValue = Encoding.ASCII.GetBytes(value.ToString());
|
||||
break;
|
||||
|
||||
case RegistryValueKind.DWord:
|
||||
setValue = value;
|
||||
break;
|
||||
}
|
||||
|
||||
if (setValue == null)
|
||||
return false;
|
||||
|
||||
regKey.SetValue(keyName, setValue, type);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of the specified key in the VRChat group in the windows registry.
|
||||
/// </summary>
|
||||
/// <param name="key">The name of the key to set.</param>
|
||||
/// <param name="value">The value to set for the specified key.</param>
|
||||
public override void SetVRChatRegistryKey(string key, byte[] value)
|
||||
{
|
||||
var keyName = AddHashToKeyName(key);
|
||||
var hKey = (UIntPtr)0x80000001; // HKEY_LOCAL_MACHINE
|
||||
const int keyWrite = 0x20006;
|
||||
const string keyFolder = @"SOFTWARE\VRChat\VRChat";
|
||||
var openKeyResult = RegOpenKeyEx(hKey, keyFolder, 0, keyWrite, out var folderPointer);
|
||||
if (openKeyResult != 0)
|
||||
throw new Exception("Error opening registry key. Error code: " + openKeyResult);
|
||||
|
||||
var setKeyResult = RegSetValueEx(folderPointer, keyName, 0, RegistryValueKind.DWord, value, value.Length);
|
||||
if (setKeyResult != 0)
|
||||
throw new Exception("Error setting registry value. Error code: " + setKeyResult);
|
||||
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
|
||||
public override Dictionary<string, Dictionary<string, object>> GetVRChatRegistry()
|
||||
{
|
||||
var output = new Dictionary<string, Dictionary<string, object>>();
|
||||
using var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat");
|
||||
if (regKey == null)
|
||||
throw new Exception("Nothing to backup.");
|
||||
|
||||
var keys = regKey.GetValueNames();
|
||||
|
||||
Span<long> spanLong = stackalloc long[1];
|
||||
Span<double> doubleSpan = MemoryMarshal.Cast<long, double>(spanLong);
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
var data = regKey.GetValue(key);
|
||||
var index = key.LastIndexOf("_h", StringComparison.Ordinal);
|
||||
if (index <= 0)
|
||||
continue;
|
||||
|
||||
var keyName = key.Substring(0, index);
|
||||
if (data == null)
|
||||
continue;
|
||||
|
||||
var type = regKey.GetValueKind(key);
|
||||
switch (type)
|
||||
{
|
||||
case RegistryValueKind.Binary:
|
||||
var binDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "data", Encoding.ASCII.GetString((byte[])data) },
|
||||
{ "type", type }
|
||||
};
|
||||
output.Add(keyName, binDict);
|
||||
break;
|
||||
|
||||
case RegistryValueKind.DWord:
|
||||
if (data.GetType() != typeof(long))
|
||||
{
|
||||
var dwordDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "data", data },
|
||||
{ "type", type }
|
||||
};
|
||||
output.Add(keyName, dwordDict);
|
||||
break;
|
||||
}
|
||||
|
||||
spanLong[0] = (long)data;
|
||||
var doubleValue = doubleSpan[0];
|
||||
var floatDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "data", doubleValue },
|
||||
{ "type", 100 } // it's special
|
||||
};
|
||||
output.Add(keyName, floatDict);
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.WriteLine($"Unknown registry value kind: {type}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public override void SetVRChatRegistry(string json)
|
||||
{
|
||||
CreateVRChatRegistryFolder();
|
||||
Span<double> spanDouble = stackalloc double[1];
|
||||
var dict = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, object>>>(json);
|
||||
foreach (var item in dict)
|
||||
{
|
||||
var data = (JsonElement)item.Value["data"];
|
||||
if (!int.TryParse(item.Value["type"].ToString(), out var type))
|
||||
throw new Exception("Unknown type: " + item.Value["type"]);
|
||||
|
||||
if (data.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
if (type == 100)
|
||||
{
|
||||
// fun handling of double to long to byte array
|
||||
spanDouble[0] = data.Deserialize<double>();
|
||||
var valueLong = MemoryMarshal.Cast<double, long>(spanDouble)[0];
|
||||
const int dataLength = sizeof(long);
|
||||
var dataBytes = new byte[dataLength];
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(valueLong), 0, dataBytes, 0, dataLength);
|
||||
SetVRChatRegistryKey(item.Key, dataBytes);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (int.TryParse(data.ToString(), out var intValue))
|
||||
{
|
||||
SetVRChatRegistryKey(item.Key, intValue, type);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Exception("Unknown number type: " + item.Key);
|
||||
}
|
||||
|
||||
SetVRChatRegistryKey(item.Key, data, type);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool HasVRChatRegistryFolder()
|
||||
{
|
||||
using var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat");
|
||||
return regKey != null;
|
||||
}
|
||||
|
||||
private void CreateVRChatRegistryFolder()
|
||||
{
|
||||
if (HasVRChatRegistryFolder())
|
||||
return;
|
||||
|
||||
using var key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\VRChat\VRChat");
|
||||
if (key == null)
|
||||
throw new Exception("Error creating registry key.");
|
||||
}
|
||||
|
||||
public override void DeleteVRChatRegistryFolder()
|
||||
{
|
||||
using var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat");
|
||||
if (regKey == null)
|
||||
return;
|
||||
|
||||
Registry.CurrentUser.DeleteSubKeyTree(@"SOFTWARE\VRChat\VRChat");
|
||||
}
|
||||
|
||||
public override string ReadVrcRegJsonFile(string filepath)
|
||||
{
|
||||
if (!File.Exists(filepath))
|
||||
return string.Empty;
|
||||
|
||||
var json = File.ReadAllText(filepath);
|
||||
return json;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
Dotnet/AppApi/Cef/Screenshot.cs
Normal file
40
Dotnet/AppApi/Cef/Screenshot.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApiCef
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds metadata to a PNG screenshot file and optionally renames the file to include the specified world ID.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the PNG screenshot file.</param>
|
||||
/// <param name="metadataString">The metadata to add to the screenshot file.</param>
|
||||
/// <param name="worldId">The ID of the world to associate with the screenshot.</param>
|
||||
/// <param name="changeFilename">Whether to rename the screenshot file to include the world ID.</param>
|
||||
public override string AddScreenshotMetadata(string path, string metadataString, string worldId, bool changeFilename = false)
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||
if (!File.Exists(path) || !path.EndsWith(".png") || !fileName.StartsWith("VRChat_"))
|
||||
return string.Empty;
|
||||
|
||||
if (changeFilename)
|
||||
{
|
||||
var newFileName = $"{fileName}_{worldId}";
|
||||
var newPath = Path.Combine(Path.GetDirectoryName(path), newFileName + Path.GetExtension(path));
|
||||
File.Move(path, newPath);
|
||||
path = newPath;
|
||||
}
|
||||
|
||||
ScreenshotHelper.WritePNGDescription(path, metadataString);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user